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ů.

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