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.

252 lines
6.8 KiB

  1. 'use strict'
  2. /* global wiki */
  3. const path = require('path')
  4. const Promise = require('bluebird')
  5. const fs = Promise.promisifyAll(require('fs-extra'))
  6. const readChunk = require('read-chunk')
  7. const fileType = require('file-type')
  8. const mime = require('mime-types')
  9. const crypto = require('crypto')
  10. const chokidar = require('chokidar')
  11. const jimp = require('jimp')
  12. const imageSize = Promise.promisify(require('image-size'))
  13. const _ = require('lodash')
  14. /**
  15. * Uploads - Agent
  16. */
  17. module.exports = {
  18. _uploadsPath: './repo/uploads',
  19. _uploadsThumbsPath: './data/thumbs',
  20. _watcher: null,
  21. /**
  22. * Initialize Uploads model
  23. *
  24. * @return {Object} Uploads model instance
  25. */
  26. init () {
  27. let self = this
  28. self._uploadsPath = path.resolve(wiki.ROOTPATH, wiki.config.paths.repo, 'uploads')
  29. self._uploadsThumbsPath = path.resolve(wiki.ROOTPATH, wiki.config.paths.data, 'thumbs')
  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 wiki.db.UplFile.findByIdAndUpdate(mData._id, mData, { upsert: true })
  51. }).then(() => {
  52. return wiki.git.commitUploads(wiki.lang.t('git:uploaded', { path: p }))
  53. })
  54. })
  55. // -> Remove upload file
  56. self._watcher.on('unlink', (p) => {
  57. return wiki.git.commitUploads(wiki.lang.t('git:deleted', { path: 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 wiki.db.UplFolder.remove({}).then(() => {
  76. return wiki.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 wiki.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 wiki.db.UplFile.remove({}).then(() => {
  100. if (_.isArray(allFiles) && allFiles.length > 0) {
  101. return wiki.db.UplFile.insertMany(allFiles)
  102. } else {
  103. return true
  104. }
  105. })
  106. })
  107. })
  108. })
  109. }).then(() => {
  110. // Watch for new changes
  111. return wiki.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 = crypto.createHash('md5').update(fldName + '/' + f).digest('hex')
  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/bmp'], mimeInfo.mime)) {
  152. return self.getImageSize(fPath).then((mImgSize) => {
  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: mImgSize,
  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 jimp.read(sourcePath).then(img => {
  199. return img
  200. .contain(150, 150)
  201. .write(destPath)
  202. })
  203. },
  204. /**
  205. * Gets the image dimensions.
  206. *
  207. * @param {String} sourcePath The source path
  208. * @return {Object} The image dimensions.
  209. */
  210. getImageSize (sourcePath) {
  211. return imageSize(sourcePath)
  212. }
  213. }