Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

389 lines
10KB

  1. package main
  2. import (
  3. "biukop.com/sfm/loan"
  4. "context"
  5. "errors"
  6. log "github.com/sirupsen/logrus"
  7. "io/ioutil"
  8. "net/http"
  9. "os"
  10. "os/exec"
  11. "path/filepath"
  12. "strconv"
  13. "strings"
  14. "sync"
  15. "time"
  16. )
  17. type uploadsOnDisk struct {
  18. Upload loan.Uploads
  19. }
  20. func (m *uploadsOnDisk) convertUploadsToPDF() (e error) {
  21. return convertUploadsToPDF(m.Upload)
  22. }
  23. func convertUploadsToPDF(ul loan.Uploads) (e error) {
  24. m := uploadsOnDisk{}
  25. m.Upload = ul
  26. if strings.Contains(strings.ToLower(ul.Format), "excel") ||
  27. strings.Contains(strings.ToLower(ul.Format), "spreadsheet") {
  28. e = m.convertExcelToPDF()
  29. return // excel is converted
  30. }
  31. if strings.Contains(strings.ToLower(ul.Format), "/pdf") {
  32. return // no need to convert
  33. }
  34. e = errors.New("don't know how to convert file to PDF")
  35. log.Error("don't know how to convert file to PDF", ul)
  36. return
  37. }
  38. func (m *uploadsOnDisk) convertUploadsToJpg() (e error) {
  39. return convertUploadsToJpg(m.Upload)
  40. }
  41. func convertUploadsToJpg(ul loan.Uploads) (e error) {
  42. m := uploadsOnDisk{}
  43. m.Upload = ul
  44. if strings.Contains(strings.ToLower(ul.Format), "excel") ||
  45. strings.Contains(strings.ToLower(ul.Format), "spreadsheet") {
  46. e = m.convertExcelToJpg()
  47. return // excel is converted
  48. }
  49. if strings.Contains(strings.ToLower(ul.Format), "/pdf") {
  50. e = m.convertPDFToJpg()
  51. return // excel is converted
  52. }
  53. e = errors.New("don't know how to convert file to image")
  54. log.Error("don't know how to convert file to image", ul)
  55. return
  56. }
  57. func (m *uploadsOnDisk) convertUploadsToThumb() (e error) {
  58. return convertUploadsToThumb(m.Upload)
  59. }
  60. func convertUploadsToThumb(ul loan.Uploads) (e error) {
  61. m := uploadsOnDisk{}
  62. m.Upload = ul
  63. if !fileExists(m.jpgPath()) {
  64. e = m.convertUploadsToJpg()
  65. if e != nil {
  66. return
  67. }
  68. }
  69. e = ConvertImageToThumbnail(m.jpgPath(), m.thumbPath(), 256)
  70. if e != nil {
  71. log.Error("cannot create thumbnail for uploads", m.Upload, e)
  72. return
  73. }
  74. return
  75. }
  76. func (m *uploadsOnDisk) filePath() string {
  77. return config.UploadsDir.FileDir + strconv.Itoa(int(m.Upload.Id)) + ".uploads"
  78. }
  79. func (m *uploadsOnDisk) jpgPath() string {
  80. return config.UploadsDir.JpgDir + strconv.Itoa(int(m.Upload.Id)) + ".jpg"
  81. }
  82. func (m *uploadsOnDisk) thumbPath() string {
  83. return config.UploadsDir.ThumbDir + strconv.Itoa(int(m.Upload.Id)) + ".webp"
  84. }
  85. func (m *uploadsOnDisk) pdfPath() string {
  86. return config.UploadsDir.PdfDir + strconv.Itoa(int(m.Upload.Id)) + ".pdf"
  87. }
  88. func (m *uploadsOnDisk) jsonPath() string {
  89. return config.UploadsDir.JsonDir + strconv.Itoa(int(m.Upload.Id)) + ".json"
  90. }
  91. func (m *uploadsOnDisk) convertExcelToPDF() (e error) {
  92. if fileExists(m.pdfPath()) {
  93. log.Info("Skip conversion excel to PDF , already exists", m)
  94. return
  95. }
  96. return m.convertExcelTo("pdf")
  97. }
  98. func (m *uploadsOnDisk) convertExcelToJpg() (e error) {
  99. if fileExists(m.jpgPath()) {
  100. log.Info("Skip conversion excel to Jpg , already exists", m)
  101. return
  102. }
  103. return m.convertExcelTo("jpg")
  104. }
  105. var libreOfficeMutex sync.Mutex // make sure we only have one libreoffice running at a time
  106. func (m *uploadsOnDisk) convertExcelTo(format string) (e error) {
  107. if format != "pdf" && format != "jpg" {
  108. e = errors.New("convert excel to unsupported format " + format)
  109. return
  110. }
  111. dst := m.jpgPath()
  112. if format == "pdf" {
  113. dst = m.pdfPath()
  114. }
  115. dir, e := ioutil.TempDir(config.TempDir, "tmp-convert-xls-to-"+format+"-")
  116. if e != nil {
  117. log.Error("cannot create tmp dir for converting image", m.Upload, e)
  118. return
  119. }
  120. defer os.RemoveAll(dir)
  121. libreOfficeMutex.Lock() //ensure only one libreoffice is running
  122. // Create a new context and add a timeout to it
  123. ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  124. defer cancel() // The cancel should be deferred so resources are cleaned up
  125. // Create the command with our context
  126. cmd := exec.CommandContext(ctx, "libreoffice", "--convert-to", format, "--outdir", dir, m.filePath())
  127. // This time we can simply use Output() to get the result.
  128. out, e := cmd.Output()
  129. libreOfficeMutex.Unlock()
  130. // We want to check the context error to see if the timeout was executed.
  131. // The error returned by cmd.Output() will be OS specific based on what
  132. // happens when a process is killed.
  133. if ctx.Err() == context.DeadlineExceeded {
  134. log.Error(cmd.String(), " timed out")
  135. switch format {
  136. case "pdf":
  137. _, e = copyFile(config.UploadsDir.PdfDefault, m.pdfPath())
  138. break
  139. case "jpg":
  140. _, e = copyFile(config.UploadsDir.JpgDefault, m.jpgPath())
  141. break
  142. }
  143. return
  144. }
  145. // If there's no context error, we know the command completed (or errored).
  146. //fmt.Println("Output:", string(out))
  147. //if err != nil {
  148. // fmt.Println("Non-zero exit code:", err)
  149. //}
  150. // ---------- for cases not using ctx
  151. // for some unknown reason, libreoffice may just hung for ever
  152. //cmd := exec.Command("libreoffice", "--convert-to", format, "--outdir", dir, m.filePath())
  153. //strCmd := cmd.String()
  154. //log.Debug("command is ", strCmd)
  155. //out, e := cmd.Output()
  156. // ------------ end of without ctx
  157. if e != nil {
  158. log.Error("cannot converting Excel to "+format+":", m.Upload, e)
  159. switch format {
  160. case "pdf":
  161. _, e = copyFile(config.UploadsDir.PdfDefault, m.pdfPath())
  162. break
  163. case "jpg":
  164. _, e = copyFile(config.UploadsDir.JpgDefault, m.jpgPath())
  165. break
  166. }
  167. return
  168. } else { // success
  169. log.Info("convert to "+format, m.Upload, " output: ", string(out))
  170. }
  171. _, name := filepath.Split(m.filePath())
  172. src := dir + string(os.PathSeparator) + fileNameWithoutExtTrimSuffix(name) + "." + format
  173. e = os.Rename(src, dst) // there should be only one jpg
  174. return
  175. }
  176. // first page to thumbnail
  177. // all page to single jpg
  178. var convertPDFMutex sync.Mutex // make sure we only have one convert running at a time
  179. func (m *uploadsOnDisk) convertPDFToJpg() (e error) {
  180. if fileExists(m.jpgPath()) {
  181. // no need to reconvert it again
  182. log.Info("PDF to JPG skipped it already exists ", m)
  183. return
  184. }
  185. dir, e := ioutil.TempDir(config.TempDir, "tmp-convert-pdf-to-jpg-")
  186. if e != nil {
  187. log.Error("cannot create tmp dir for converting image", m.Upload, e)
  188. return
  189. }
  190. defer os.RemoveAll(dir)
  191. convertPDFMutex.Lock()
  192. // convert -density 3000 abc.pdf path/tmp/result.png
  193. // could be path/tmp/result-0, result-1, result-2, ... png
  194. target := dir + string(os.PathSeparator) + "result.jpg" //.jpg suffix is important
  195. cmd := exec.Command("convert", "-density", "300", m.filePath(), target)
  196. strCmd := cmd.String()
  197. log.Debug("command is ", strCmd)
  198. out, e := cmd.Output()
  199. convertPDFMutex.Unlock()
  200. if e != nil {
  201. log.Error("cannot create png file for PDF", m.Upload, e)
  202. _, e = copyFile(config.UploadsDir.JpgDefault, m.jpgPath())
  203. return
  204. } else {
  205. log.Info("convert ", m.Upload, " output: ", string(out))
  206. }
  207. // montage -mode concatenate -tile 1x 30*png 30.jpg
  208. if fileExists(target) { // single file,
  209. _ = ConvertImageToThumbnail(target, m.thumbPath(), 256)
  210. e = os.Rename(target, m.jpgPath()) // there should be only one jpg
  211. } else { // multi-page, we have -0 -1 -2 -3 -4 files
  212. firstPage := dir + string(os.PathSeparator) + "result-0.jpg"
  213. _ = ConvertImageToThumbnail(firstPage, m.thumbPath(), 256)
  214. batch := dir + string(os.PathSeparator) + "result*jpg" // result* is important
  215. target = dir + string(os.PathSeparator) + "final.jpg" // .jpg suffix is important
  216. cmd = exec.Command("montage", "-mode", "concatenate", "-tile", "1x", batch, target)
  217. strCmd = cmd.String()
  218. log.Debug("command is ", strCmd)
  219. out, e = cmd.Output()
  220. if e != nil {
  221. return
  222. } else {
  223. log.Info("montage ", m, " output: ", string(out))
  224. }
  225. e = os.Rename(target, m.jpgPath()) // give combined file to target
  226. }
  227. return
  228. }
  229. func (m *uploadsOnDisk) GetFileType() (ret string, e error) {
  230. strMime, e := GetFileContentType(m.filePath())
  231. if e != nil {
  232. return
  233. }
  234. if strings.ToLower(strMime) == "application/pdf" {
  235. return "pdf", nil
  236. }
  237. if strings.ToLower(strMime) == "application/vnd.ms-excel" || strings.ToLower(m.Upload.Format) ==
  238. "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" {
  239. return "excel", nil
  240. }
  241. if strings.ToLower(strMime) == "application/zip" &&
  242. strings.ToLower(m.Upload.Format) ==
  243. "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" {
  244. return "opensheet", nil
  245. }
  246. // check suffix which is not reliable
  247. ext := filepath.Ext(m.Upload.FileName)
  248. ext = strings.ToLower(ext)
  249. if ext[0] == '.' {
  250. ext = ext[1:] // remove the first char 'dot' .
  251. }
  252. switch ext {
  253. case "xls", "xlsx":
  254. return "excel", nil
  255. case "pdf":
  256. return "pdf", nil
  257. default:
  258. log.Warn("unhandled uploads type", ext)
  259. }
  260. return "", nil
  261. }
  262. func (m *uploadsOnDisk) DeleteAll() (e error) {
  263. eJpg := os.Remove(m.jpgPath())
  264. eFile := os.Remove(m.filePath())
  265. ePdf := os.Remove(m.pdfPath())
  266. eThumb := os.Remove(m.thumbPath())
  267. eMeta := m.Upload.Delete()
  268. strId := strconv.Itoa(int(m.Upload.Id))
  269. errMsg := ""
  270. if eJpg != nil {
  271. errMsg += " jpg "
  272. }
  273. if eFile != nil {
  274. errMsg += " original "
  275. }
  276. if ePdf != nil {
  277. errMsg += " pdf "
  278. }
  279. if eThumb != nil {
  280. errMsg += " thumb "
  281. }
  282. if eMeta != nil {
  283. errMsg += " Meta in DB "
  284. e = errors.New(errMsg + " cannot be deleted " + strId)
  285. }
  286. if errMsg != "" {
  287. log.Error(errMsg, "files on disk cannot be deleted need disk cleaning ", m.Upload)
  288. }
  289. return
  290. }
  291. // GetFileContentType
  292. // tested, not accurate with xls, xlsx, it becomes zip and octstream sometime.
  293. func GetFileContentType(filename string) (contentType string, e error) {
  294. contentType = ""
  295. input, e := os.OpenFile(filename, os.O_RDONLY, 0755)
  296. // Only the first 512 bytes are used to sniff the content type.
  297. buffer := make([]byte, 512)
  298. _, e = input.Read(buffer)
  299. if e != nil {
  300. return
  301. }
  302. // Use the net/http package's handy Detect ContentType function. Always returns a valid
  303. // content-type by returning "application/octet-stream" if no others seemed to match.
  304. contentType = http.DetectContentType(buffer)
  305. return
  306. }
  307. func ConvertImageToThumbnail(srcPath string, dstPath string, size int) (e error) {
  308. if fileExists(dstPath) {
  309. log.Info("skip converting thumbnail it exists", dstPath)
  310. return
  311. }
  312. if size <= 0 { //thumb nail width
  313. size = 256
  314. }
  315. // convert -thumbnail 200 abc.png thumb.abc.png
  316. cmd := exec.Command("convert", "-thumbnail", strconv.Itoa(size), srcPath, dstPath)
  317. strCmd := cmd.String()
  318. log.Debug("create thumbnail: ", strCmd)
  319. out, e := cmd.Output()
  320. if e != nil {
  321. log.Error("Failed to convert thumbnail", e)
  322. _, e = copyFile(config.UploadsDir.ThumbDefault, dstPath)
  323. return
  324. } else { // success
  325. log.Info("success output: \n: ", string(out))
  326. }
  327. return
  328. }