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.

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