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.

294 lines
6.4 KiB

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