"use strict"; const path = require('path'), Promise = require('bluebird'), fs = Promise.promisifyAll(require('fs-extra')), multer = require('multer'), request = require('request'), url = require('url'), farmhash = require('farmhash'), _ = require('lodash'); var regFolderName = new RegExp("^[a-z0-9][a-z0-9\-]*[a-z0-9]$"); const maxDownloadFileSize = 3145728; // 3 MB /** * Uploads */ module.exports = { _uploadsPath: './repo/uploads', _uploadsThumbsPath: './data/thumbs', /** * Initialize Local Data Storage model * * @return {Object} Uploads model instance */ init() { this._uploadsPath = path.resolve(ROOTPATH, appconfig.paths.repo, 'uploads'); this._uploadsThumbsPath = path.resolve(ROOTPATH, appconfig.paths.data, 'thumbs'); return this; }, /** * Gets the thumbnails folder path. * * @return {String} The thumbs path. */ getThumbsPath() { return this._uploadsThumbsPath; }, /** * Gets the uploads folders. * * @return {Array} The uploads folders. */ getUploadsFolders() { return db.UplFolder.find({}, 'name').sort('name').exec().then((results) => { return (results) ? _.map(results, 'name') : [{ name: '' }]; }); }, /** * Creates an uploads folder. * * @param {String} folderName The folder name * @return {Promise} Promise of the operation */ createUploadsFolder(folderName) { let self = this; folderName = _.kebabCase(_.trim(folderName)); if(_.isEmpty(folderName) || !regFolderName.test(folderName)) { return Promise.resolve(self.getUploadsFolders()); } return fs.ensureDirAsync(path.join(self._uploadsPath, folderName)).then(() => { return db.UplFolder.findOneAndUpdate({ _id: 'f:' + folderName }, { name: folderName }, { upsert: true }); }).then(() => { return self.getUploadsFolders(); }); }, /** * Check if folder is valid and exists * * @param {String} folderName The folder name * @return {Boolean} True if valid */ validateUploadsFolder(folderName) { return db.UplFolder.findOne({ name: folderName }).then((f) => { return (f) ? path.resolve(this._uploadsPath, folderName) : false; }); }, /** * Adds one or more uploads files. * * @param {Array} arrFiles The uploads files * @return {Void} Void */ addUploadsFiles(arrFiles) { if(_.isArray(arrFiles) || _.isPlainObject(arrFiles)) { //this._uploadsDb.Files.insert(arrFiles); } return; }, /** * Gets the uploads files. * * @param {String} cat Category type * @param {String} fld Folder * @return {Array} The files matching the query */ getUploadsFiles(cat, fld) { return db.UplFile.find({ category: cat, folder: 'f:' + fld }).sort('filename').exec(); }, /** * Deletes an uploads file. * * @param {string} uid The file unique ID * @return {Promise} Promise of the operation */ deleteUploadsFile(uid) { let self = this; return db.UplFile.findOneAndRemove({ _id: uid }).then((f) => { if(f) { return self.deleteUploadsFileTry(f, 0); } return true; }); }, deleteUploadsFileTry(f, attempt) { let self = this; let fFolder = (f.folder && f.folder !== 'f:') ? f.folder.slice(2) : './'; return Promise.join( fs.removeAsync(path.join(self._uploadsThumbsPath, f._id + '.png')), fs.removeAsync(path.resolve(self._uploadsPath, fFolder, f.filename)) ).catch((err) => { if(err.code === 'EBUSY' && attempt < 5) { return Promise.delay(100).then(() => { return self.deleteUploadsFileTry(f, attempt + 1); }); } else { winston.warn('Unable to delete uploads file ' + f.filename + '. File is locked by another process and multiple attempts failed.'); return true; } }); }, /** * Downloads a file from url. * * @param {String} fFolder The folder * @param {String} fUrl The full URL * @return {Promise} Promise of the operation */ downloadFromUrl(fFolder, fUrl) { let self = this; let fUrlObj = url.parse(fUrl); let fUrlFilename = _.last(_.split(fUrlObj.pathname, '/')); let destFolder = _.chain(fFolder).trim().toLower().value(); return upl.validateUploadsFolder(destFolder).then((destFolderPath) => { if(!destFolderPath) { return Promise.reject(new Error('Invalid Folder')); } return lcdata.validateUploadsFilename(fUrlFilename, destFolder).then((destFilename) => { let destFilePath = path.resolve(destFolderPath, destFilename); return new Promise((resolve, reject) => { let rq = request({ url: fUrl, method: 'GET', followRedirect: true, maxRedirects: 5, timeout: 10000 }); let destFileStream = fs.createWriteStream(destFilePath); let curFileSize = 0; rq.on('data', (data) => { curFileSize += data.length; if(curFileSize > maxDownloadFileSize) { rq.abort(); destFileStream.destroy(); fs.remove(destFilePath); reject(new Error('Remote file is too large!')); } }).on('error', (err) => { destFileStream.destroy(); fs.remove(destFilePath); reject(err); }); destFileStream.on('finish', () => { resolve(true); }); rq.pipe(destFileStream); }); }); }); }, /** * Move/Rename a file * * @param {String} uid The file ID * @param {String} fld The destination folder * @param {String} nFilename The new filename (optional) * @return {Promise} Promise of the operation */ moveUploadsFile(uid, fld, nFilename) { let self = this; return db.UplFolder.findById('f:' + fld).then((folder) => { if(folder) { return db.UplFile.findById(uid).then((originFile) => { //-> Check if rename is valid let nameCheck = null; if(nFilename) { let originFileObj = path.parse(originFile.filename); nameCheck = lcdata.validateUploadsFilename(nFilename + originFileObj.ext, folder.name); } else { nameCheck = Promise.resolve(originFile.filename); } return nameCheck.then((destFilename) => { let originFolder = (originFile.folder && originFile.folder !== 'f:') ? originFile.folder.slice(2) : './'; let sourceFilePath = path.resolve(self._uploadsPath, originFolder, originFile.filename); let destFilePath = path.resolve(self._uploadsPath, folder.name, destFilename); let preMoveOps = []; //-> Check for invalid operations if(sourceFilePath === destFilePath) { return Promise.reject(new Error('Invalid Operation!')); } //-> Delete DB entry preMoveOps.push(db.UplFile.findByIdAndRemove(uid)); //-> Move thumbnail ahead to avoid re-generation if(originFile.category === 'image') { let fUid = farmhash.fingerprint32(folder.name + '/' + destFilename); let sourceThumbPath = path.resolve(self._uploadsThumbsPath, originFile._id + '.png'); let destThumbPath = path.resolve(self._uploadsThumbsPath, fUid + '.png'); preMoveOps.push(fs.moveAsync(sourceThumbPath, destThumbPath)); } else { preMoveOps.push(Promise.resolve(true)); } //-> Proceed to move actual file return Promise.all(preMoveOps).then(() => { return fs.moveAsync(sourceFilePath, destFilePath, { clobber: false }); }); }) }); } else { return Promise.reject(new Error('Invalid Destination Folder')); } }); } };