uploads-agent.js 6.3 KB

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