uploads.js 7.4 KB

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