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.

288 lines
6.3 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. farmhash = require('farmhash'),
  8. moment = require('moment'),
  9. chokidar = require('chokidar'),
  10. sharp = require('sharp'),
  11. _ = require('lodash');
  12. /**
  13. * Uploads
  14. *
  15. * @param {Object} appconfig The application configuration
  16. */
  17. module.exports = {
  18. _uploadsPath: './repo/uploads',
  19. _uploadsThumbsPath: './data/thumbs',
  20. _watcher: null,
  21. /**
  22. * Initialize Uploads model
  23. *
  24. * @param {Object} appconfig The application config
  25. * @return {Object} Uploads model instance
  26. */
  27. init(appconfig) {
  28. let self = this;
  29. self._uploadsPath = path.resolve(ROOTPATH, appconfig.paths.repo, 'uploads');
  30. self._uploadsThumbsPath = path.resolve(ROOTPATH, appconfig.paths.data, 'thumbs');
  31. // Disable Sharp cache, as it cause file locks issues when deleting uploads.
  32. sharp.cache(false);
  33. return self;
  34. },
  35. /**
  36. * Watch the uploads folder for changes
  37. *
  38. * @return {Void} Void
  39. */
  40. watch() {
  41. let self = this;
  42. self._watcher = chokidar.watch(self._uploadsPath, {
  43. persistent: true,
  44. ignoreInitial: true,
  45. cwd: self._uploadsPath,
  46. depth: 1,
  47. awaitWriteFinish: true
  48. });
  49. //-> Add new upload file
  50. self._watcher.on('add', (p) => {
  51. let pInfo = self.parseUploadsRelPath(p);
  52. return self.processFile(pInfo.folder, pInfo.filename).then((mData) => {
  53. return db.UplFile.findByIdAndUpdate(mData._id, mData, { upsert: true });
  54. }).then(() => {
  55. return git.commitUploads('Uploaded ' + p);
  56. });
  57. });
  58. //-> Remove upload file
  59. self._watcher.on('unlink', (p) => {
  60. let pInfo = self.parseUploadsRelPath(p);
  61. return git.commitUploads('Deleted/Renamed ' + p);
  62. });
  63. },
  64. /**
  65. * Initial Uploads scan
  66. *
  67. * @return {Promise<Void>} Promise of the scan operation
  68. */
  69. initialScan() {
  70. let self = this;
  71. return fs.readdirAsync(self._uploadsPath).then((ls) => {
  72. // Get all folders
  73. return Promise.map(ls, (f) => {
  74. return fs.statAsync(path.join(self._uploadsPath, f)).then((s) => { return { filename: f, stat: s }; });
  75. }).filter((s) => { return s.stat.isDirectory(); }).then((arrDirs) => {
  76. let folderNames = _.map(arrDirs, 'filename');
  77. folderNames.unshift('');
  78. // Add folders to DB
  79. return db.UplFolder.remove({}).then(() => {
  80. return db.UplFolder.insertMany(_.map(folderNames, (f) => {
  81. return {
  82. _id: 'f:' + f,
  83. name: f
  84. };
  85. }));
  86. }).then(() => {
  87. // Travel each directory and scan files
  88. let allFiles = [];
  89. return Promise.map(folderNames, (fldName) => {
  90. let fldPath = path.join(self._uploadsPath, fldName);
  91. return fs.readdirAsync(fldPath).then((fList) => {
  92. return Promise.map(fList, (f) => {
  93. return upl.processFile(fldName, f).then((mData) => {
  94. if(mData) {
  95. allFiles.push(mData);
  96. }
  97. return true;
  98. });
  99. }, {concurrency: 3});
  100. });
  101. }, {concurrency: 1}).finally(() => {
  102. // Add files to DB
  103. return db.UplFile.remove({}).then(() => {
  104. if(_.isArray(allFiles) && allFiles.length > 0) {
  105. return db.UplFile.insertMany(allFiles);
  106. } else {
  107. return true;
  108. }
  109. });
  110. });
  111. });
  112. });
  113. }).then(() => {
  114. // Watch for new changes
  115. return upl.watch();
  116. });
  117. },
  118. /**
  119. * Parse relative Uploads path
  120. *
  121. * @param {String} f Relative Uploads path
  122. * @return {Object} Parsed path (folder and filename)
  123. */
  124. parseUploadsRelPath(f) {
  125. let fObj = path.parse(f);
  126. return {
  127. folder: fObj.dir,
  128. filename: fObj.base
  129. };
  130. },
  131. /**
  132. * Get metadata from file and generate thumbnails if necessary
  133. *
  134. * @param {String} fldName The folder name
  135. * @param {String} f The filename
  136. * @return {Promise<Object>} Promise of the file metadata
  137. */
  138. processFile(fldName, f) {
  139. let self = this;
  140. let fldPath = path.join(self._uploadsPath, fldName);
  141. let fPath = path.join(fldPath, f);
  142. let fPathObj = path.parse(fPath);
  143. let fUid = farmhash.fingerprint32(fldName + '/' + f);
  144. return fs.statAsync(fPath).then((s) => {
  145. if(!s.isFile()) { return false; }
  146. // Get MIME info
  147. let mimeInfo = fileType(readChunk.sync(fPath, 0, 262));
  148. // Images
  149. if(s.size < 3145728) { // ignore files larger than 3MB
  150. if(_.includes(['image/png', 'image/jpeg', 'image/gif', 'image/webp'], mimeInfo.mime)) {
  151. return self.getImageMetadata(fPath).then((mImgData) => {
  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: _.pick(mImgData, ['format', 'width', 'height', 'density', 'hasAlpha', 'orientation']),
  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) => {
  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: 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 sharp(sourcePath)
  198. .withoutEnlargement()
  199. .resize(150,150)
  200. .background('white')
  201. .embed()
  202. .flatten()
  203. .toFormat('png')
  204. .toFile(destPath);
  205. },
  206. /**
  207. * Gets the image metadata.
  208. *
  209. * @param {String} sourcePath The source path
  210. * @return {Object} The image metadata.
  211. */
  212. getImageMetadata(sourcePath) {
  213. return sharp(sourcePath).metadata();
  214. }
  215. };