You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

255 lines
6.9 KiB

  1. 'use strict'
  2. const path = require('path')
  3. const Promise = require('bluebird')
  4. const fs = Promise.promisifyAll(require('fs-extra'))
  5. const readChunk = require('read-chunk')
  6. const fileType = require('file-type')
  7. const mime = require('mime-types')
  8. const farmhash = require('farmhash')
  9. const chokidar = require('chokidar')
  10. const sharp = require('sharp')
  11. const _ = require('lodash')
  12. /**
  13. * Uploads - Agent
  14. */
  15. module.exports = {
  16. _uploadsPath: './repo/uploads',
  17. _uploadsThumbsPath: './data/thumbs',
  18. _watcher: null,
  19. /**
  20. * Initialize Uploads model
  21. *
  22. * @return {Object} Uploads model instance
  23. */
  24. init () {
  25. let self = this
  26. self._uploadsPath = path.resolve(ROOTPATH, appconfig.paths.repo, 'uploads')
  27. self._uploadsThumbsPath = path.resolve(ROOTPATH, appconfig.paths.data, 'thumbs')
  28. // Disable Sharp cache, as it cause file locks issues when deleting uploads.
  29. sharp.cache(false)
  30. return self
  31. },
  32. /**
  33. * Watch the uploads folder for changes
  34. *
  35. * @return {Void} Void
  36. */
  37. watch () {
  38. let self = this
  39. self._watcher = chokidar.watch(self._uploadsPath, {
  40. persistent: true,
  41. ignoreInitial: true,
  42. cwd: self._uploadsPath,
  43. depth: 1,
  44. awaitWriteFinish: true
  45. })
  46. // -> Add new upload file
  47. self._watcher.on('add', (p) => {
  48. let pInfo = self.parseUploadsRelPath(p)
  49. return self.processFile(pInfo.folder, pInfo.filename).then((mData) => {
  50. return db.UplFile.findByIdAndUpdate(mData._id, mData, { upsert: true })
  51. }).then(() => {
  52. return git.commitUploads('Uploaded ' + p)
  53. })
  54. })
  55. // -> Remove upload file
  56. self._watcher.on('unlink', (p) => {
  57. return git.commitUploads('Deleted/Renamed ' + p)
  58. })
  59. },
  60. /**
  61. * Initial Uploads scan
  62. *
  63. * @return {Promise<Void>} Promise of the scan operation
  64. */
  65. initialScan () {
  66. let self = this
  67. return fs.readdirAsync(self._uploadsPath).then((ls) => {
  68. // Get all folders
  69. return Promise.map(ls, (f) => {
  70. return fs.statAsync(path.join(self._uploadsPath, f)).then((s) => { return { filename: f, stat: s } })
  71. }).filter((s) => { return s.stat.isDirectory() }).then((arrDirs) => {
  72. let folderNames = _.map(arrDirs, 'filename')
  73. folderNames.unshift('')
  74. // Add folders to DB
  75. return db.UplFolder.remove({}).then(() => {
  76. return db.UplFolder.insertMany(_.map(folderNames, (f) => {
  77. return {
  78. _id: 'f:' + f,
  79. name: f
  80. }
  81. }))
  82. }).then(() => {
  83. // Travel each directory and scan files
  84. let allFiles = []
  85. return Promise.map(folderNames, (fldName) => {
  86. let fldPath = path.join(self._uploadsPath, fldName)
  87. return fs.readdirAsync(fldPath).then((fList) => {
  88. return Promise.map(fList, (f) => {
  89. return upl.processFile(fldName, f).then((mData) => {
  90. if (mData) {
  91. allFiles.push(mData)
  92. }
  93. return true
  94. })
  95. }, {concurrency: 3})
  96. })
  97. }, {concurrency: 1}).finally(() => {
  98. // Add files to DB
  99. return db.UplFile.remove({}).then(() => {
  100. if (_.isArray(allFiles) && allFiles.length > 0) {
  101. return db.UplFile.insertMany(allFiles)
  102. } else {
  103. return true
  104. }
  105. })
  106. })
  107. })
  108. })
  109. }).then(() => {
  110. // Watch for new changes
  111. return upl.watch()
  112. })
  113. },
  114. /**
  115. * Parse relative Uploads path
  116. *
  117. * @param {String} f Relative Uploads path
  118. * @return {Object} Parsed path (folder and filename)
  119. */
  120. parseUploadsRelPath (f) {
  121. let fObj = path.parse(f)
  122. return {
  123. folder: fObj.dir,
  124. filename: fObj.base
  125. }
  126. },
  127. /**
  128. * Get metadata from file and generate thumbnails if necessary
  129. *
  130. * @param {String} fldName The folder name
  131. * @param {String} f The filename
  132. * @return {Promise<Object>} Promise of the file metadata
  133. */
  134. processFile (fldName, f) {
  135. let self = this
  136. let fldPath = path.join(self._uploadsPath, fldName)
  137. let fPath = path.join(fldPath, f)
  138. let fPathObj = path.parse(fPath)
  139. let fUid = farmhash.fingerprint32(fldName + '/' + f)
  140. return fs.statAsync(fPath).then((s) => {
  141. if (!s.isFile()) { return false }
  142. // Get MIME info
  143. let mimeInfo = fileType(readChunk.sync(fPath, 0, 262))
  144. if (_.isNil(mimeInfo)) {
  145. mimeInfo = {
  146. mime: mime.lookup(fPathObj.ext) || 'application/octet-stream'
  147. }
  148. }
  149. // Images
  150. if (s.size < 3145728) { // ignore files larger than 3MB
  151. if (_.includes(['image/png', 'image/jpeg', 'image/gif', 'image/webp'], mimeInfo.mime)) {
  152. return self.getImageMetadata(fPath).then((mImgData) => {
  153. let cacheThumbnailPath = path.parse(path.join(self._uploadsThumbsPath, fUid + '.png'))
  154. let cacheThumbnailPathStr = path.format(cacheThumbnailPath)
  155. let mData = {
  156. _id: fUid,
  157. category: 'image',
  158. mime: mimeInfo.mime,
  159. extra: _.pick(mImgData, ['format', 'width', 'height', 'density', 'hasAlpha', 'orientation']),
  160. folder: 'f:' + fldName,
  161. filename: f,
  162. basename: fPathObj.name,
  163. filesize: s.size
  164. }
  165. // Generate thumbnail
  166. return fs.statAsync(cacheThumbnailPathStr).then((st) => {
  167. return st.isFile()
  168. }).catch((err) => { // eslint-disable-line handle-callback-err
  169. return false
  170. }).then((thumbExists) => {
  171. return (thumbExists) ? mData : fs.ensureDirAsync(cacheThumbnailPath.dir).then(() => {
  172. return self.generateThumbnail(fPath, cacheThumbnailPathStr)
  173. }).return(mData)
  174. })
  175. })
  176. }
  177. }
  178. // Other Files
  179. return {
  180. _id: fUid,
  181. category: 'binary',
  182. mime: mimeInfo.mime,
  183. folder: 'f:' + fldName,
  184. filename: f,
  185. basename: fPathObj.name,
  186. filesize: s.size
  187. }
  188. })
  189. },
  190. /**
  191. * Generate thumbnail of image
  192. *
  193. * @param {String} sourcePath The source path
  194. * @param {String} destPath The destination path
  195. * @return {Promise<Object>} Promise returning the resized image info
  196. */
  197. generateThumbnail (sourcePath, destPath) {
  198. return sharp(sourcePath)
  199. .withoutEnlargement()
  200. .resize(150, 150)
  201. .background('white')
  202. .embed()
  203. .flatten()
  204. .toFormat('png')
  205. .toFile(destPath)
  206. },
  207. /**
  208. * Gets the image metadata.
  209. *
  210. * @param {String} sourcePath The source path
  211. * @return {Object} The image metadata.
  212. */
  213. getImageMetadata (sourcePath) {
  214. return sharp(sourcePath).metadata()
  215. }
  216. }