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.

308 lines
7.4 KiB

  1. "use strict";
  2. const path = require('path'),
  3. Promise = require('bluebird'),
  4. fs = Promise.promisifyAll(require('fs-extra')),
  5. multer = require('multer'),
  6. request = require('request'),
  7. url = require('url'),
  8. farmhash = require('farmhash'),
  9. _ = require('lodash');
  10. var regFolderName = new RegExp("^[a-z0-9][a-z0-9\-]*[a-z0-9]$");
  11. const maxDownloadFileSize = 3145728; // 3 MB
  12. /**
  13. * Uploads
  14. */
  15. module.exports = {
  16. _uploadsPath: './repo/uploads',
  17. _uploadsThumbsPath: './data/thumbs',
  18. /**
  19. * Initialize Local Data Storage model
  20. *
  21. * @param {Object} appconfig The application config
  22. * @return {Object} Uploads model instance
  23. */
  24. init(appconfig) {
  25. this._uploadsPath = path.resolve(ROOTPATH, appconfig.paths.repo, 'uploads');
  26. this._uploadsThumbsPath = path.resolve(ROOTPATH, appconfig.paths.data, 'thumbs');
  27. return this;
  28. },
  29. /**
  30. * Gets the thumbnails folder path.
  31. *
  32. * @return {String} The thumbs path.
  33. */
  34. getThumbsPath() {
  35. return this._uploadsThumbsPath;
  36. },
  37. /**
  38. * Gets the uploads folders.
  39. *
  40. * @return {Array<String>} The uploads folders.
  41. */
  42. getUploadsFolders() {
  43. return db.UplFolder.find({}, 'name').sort('name').exec().then((results) => {
  44. return (results) ? _.map(results, 'name') : [{ name: '' }];
  45. });
  46. },
  47. /**
  48. * Creates an uploads folder.
  49. *
  50. * @param {String} folderName The folder name
  51. * @return {Promise} Promise of the operation
  52. */
  53. createUploadsFolder(folderName) {
  54. let self = this;
  55. folderName = _.kebabCase(_.trim(folderName));
  56. if(_.isEmpty(folderName) || !regFolderName.test(folderName)) {
  57. return Promise.resolve(self.getUploadsFolders());
  58. }
  59. return fs.ensureDirAsync(path.join(self._uploadsPath, folderName)).then(() => {
  60. return db.UplFolder.findOneAndUpdate({
  61. _id: 'f:' + folderName
  62. }, {
  63. name: folderName
  64. }, {
  65. upsert: true
  66. });
  67. }).then(() => {
  68. return self.getUploadsFolders();
  69. });
  70. },
  71. /**
  72. * Check if folder is valid and exists
  73. *
  74. * @param {String} folderName The folder name
  75. * @return {Boolean} True if valid
  76. */
  77. validateUploadsFolder(folderName) {
  78. return db.UplFolder.findOne({ name: folderName }).then((f) => {
  79. return (f) ? path.resolve(this._uploadsPath, folderName) : false;
  80. });
  81. },
  82. /**
  83. * Adds one or more uploads files.
  84. *
  85. * @param {Array<Object>} arrFiles The uploads files
  86. * @return {Void} Void
  87. */
  88. addUploadsFiles(arrFiles) {
  89. if(_.isArray(arrFiles) || _.isPlainObject(arrFiles)) {
  90. //this._uploadsDb.Files.insert(arrFiles);
  91. }
  92. return;
  93. },
  94. /**
  95. * Gets the uploads files.
  96. *
  97. * @param {String} cat Category type
  98. * @param {String} fld Folder
  99. * @return {Array<Object>} The files matching the query
  100. */
  101. getUploadsFiles(cat, fld) {
  102. return db.UplFile.find({
  103. category: cat,
  104. folder: 'f:' + fld
  105. }).sort('filename').exec();
  106. },
  107. /**
  108. * Deletes an uploads file.
  109. *
  110. * @param {string} uid The file unique ID
  111. * @return {Promise} Promise of the operation
  112. */
  113. deleteUploadsFile(uid) {
  114. let self = this;
  115. return db.UplFile.findOneAndRemove({ _id: uid }).then((f) => {
  116. if(f) {
  117. return self.deleteUploadsFileTry(f, 0);
  118. }
  119. return true;
  120. });
  121. },
  122. deleteUploadsFileTry(f, attempt) {
  123. let self = this;
  124. let fFolder = (f.folder && f.folder !== 'f:') ? f.folder.slice(2) : './';
  125. return Promise.join(
  126. fs.removeAsync(path.join(self._uploadsThumbsPath, f._id + '.png')),
  127. fs.removeAsync(path.resolve(self._uploadsPath, fFolder, f.filename))
  128. ).catch((err) => {
  129. if(err.code === 'EBUSY' && attempt < 5) {
  130. return Promise.delay(100).then(() => {
  131. return self.deleteUploadsFileTry(f, attempt + 1);
  132. });
  133. } else {
  134. winston.warn('Unable to delete uploads file ' + f.filename + '. File is locked by another process and multiple attempts failed.');
  135. return true;
  136. }
  137. });
  138. },
  139. /**
  140. * Downloads a file from url.
  141. *
  142. * @param {String} fFolder The folder
  143. * @param {String} fUrl The full URL
  144. * @return {Promise} Promise of the operation
  145. */
  146. downloadFromUrl(fFolder, fUrl) {
  147. let self = this;
  148. let fUrlObj = url.parse(fUrl);
  149. let fUrlFilename = _.last(_.split(fUrlObj.pathname, '/'));
  150. let destFolder = _.chain(fFolder).trim().toLower().value();
  151. return upl.validateUploadsFolder(destFolder).then((destFolderPath) => {
  152. if(!destFolderPath) {
  153. return Promise.reject(new Error('Invalid Folder'));
  154. }
  155. return lcdata.validateUploadsFilename(fUrlFilename, destFolder).then((destFilename) => {
  156. let destFilePath = path.resolve(destFolderPath, destFilename);
  157. return new Promise((resolve, reject) => {
  158. let rq = request({
  159. url: fUrl,
  160. method: 'GET',
  161. followRedirect: true,
  162. maxRedirects: 5,
  163. timeout: 10000
  164. });
  165. let destFileStream = fs.createWriteStream(destFilePath);
  166. let curFileSize = 0;
  167. rq.on('data', (data) => {
  168. curFileSize += data.length;
  169. if(curFileSize > maxDownloadFileSize) {
  170. rq.abort();
  171. destFileStream.destroy();
  172. fs.remove(destFilePath);
  173. reject(new Error('Remote file is too large!'));
  174. }
  175. }).on('error', (err) => {
  176. destFileStream.destroy();
  177. fs.remove(destFilePath);
  178. reject(err);
  179. });
  180. destFileStream.on('finish', () => {
  181. resolve(true);
  182. })
  183. rq.pipe(destFileStream);
  184. });
  185. });
  186. });
  187. },
  188. /**
  189. * Move/Rename a file
  190. *
  191. * @param {String} uid The file ID
  192. * @param {String} fld The destination folder
  193. * @param {String} nFilename The new filename (optional)
  194. * @return {Promise} Promise of the operation
  195. */
  196. moveUploadsFile(uid, fld, nFilename) {
  197. let self = this;
  198. return db.UplFolder.findById('f:' + fld).then((folder) => {
  199. if(folder) {
  200. return db.UplFile.findById(uid).then((originFile) => {
  201. //-> Check if rename is valid
  202. let nameCheck = null;
  203. if(nFilename) {
  204. let originFileObj = path.parse(originFile.filename);
  205. nameCheck = lcdata.validateUploadsFilename(nFilename + originFileObj.ext, folder.name);
  206. } else {
  207. nameCheck = Promise.resolve(originFile.filename);
  208. }
  209. return nameCheck.then((destFilename) => {
  210. let originFolder = (originFile.folder && originFile.folder !== 'f:') ? originFile.folder.slice(2) : './';
  211. let sourceFilePath = path.resolve(self._uploadsPath, originFolder, originFile.filename);
  212. let destFilePath = path.resolve(self._uploadsPath, folder.name, destFilename);
  213. let preMoveOps = [];
  214. //-> Check for invalid operations
  215. if(sourceFilePath === destFilePath) {
  216. return Promise.reject(new Error('Invalid Operation!'));
  217. }
  218. //-> Delete DB entry
  219. preMoveOps.push(db.UplFile.findByIdAndRemove(uid));
  220. //-> Move thumbnail ahead to avoid re-generation
  221. if(originFile.category === 'image') {
  222. let fUid = farmhash.fingerprint32(folder.name + '/' + destFilename);
  223. let sourceThumbPath = path.resolve(self._uploadsThumbsPath, originFile._id + '.png');
  224. let destThumbPath = path.resolve(self._uploadsThumbsPath, fUid + '.png');
  225. preMoveOps.push(fs.moveAsync(sourceThumbPath, destThumbPath));
  226. } else {
  227. preMoveOps.push(Promise.resolve(true));
  228. }
  229. //-> Proceed to move actual file
  230. return Promise.all(preMoveOps).then(() => {
  231. return fs.moveAsync(sourceFilePath, destFilePath, {
  232. clobber: false
  233. });
  234. });
  235. })
  236. });
  237. } else {
  238. return Promise.reject(new Error('Invalid Destination Folder'));
  239. }
  240. });
  241. }
  242. };