"use strict"; var path = require('path'), Promise = require('bluebird'), fs = Promise.promisifyAll(require('fs-extra')), readChunk = require('read-chunk'), fileType = require('file-type'), mime = require('mime-types'), farmhash = require('farmhash'), moment = require('moment'), chokidar = require('chokidar'), sharp = require('sharp'), _ = require('lodash'); /** * Uploads * * @param {Object} appconfig The application configuration */ module.exports = { _uploadsPath: './repo/uploads', _uploadsThumbsPath: './data/thumbs', _watcher: null, /** * Initialize Uploads model * * @param {Object} appconfig The application config * @return {Object} Uploads model instance */ init(appconfig) { let self = this; self._uploadsPath = path.resolve(ROOTPATH, appconfig.paths.repo, 'uploads'); self._uploadsThumbsPath = path.resolve(ROOTPATH, appconfig.paths.data, 'thumbs'); // Disable Sharp cache, as it cause file locks issues when deleting uploads. sharp.cache(false); return self; }, /** * Watch the uploads folder for changes * * @return {Void} Void */ watch() { let self = this; self._watcher = chokidar.watch(self._uploadsPath, { persistent: true, ignoreInitial: true, cwd: self._uploadsPath, depth: 1, awaitWriteFinish: true }); //-> Add new upload file self._watcher.on('add', (p) => { let pInfo = self.parseUploadsRelPath(p); return self.processFile(pInfo.folder, pInfo.filename).then((mData) => { return db.UplFile.findByIdAndUpdate(mData._id, mData, { upsert: true }); }).then(() => { return git.commitUploads('Uploaded ' + p); }); }); //-> Remove upload file self._watcher.on('unlink', (p) => { let pInfo = self.parseUploadsRelPath(p); return git.commitUploads('Deleted/Renamed ' + p); }); }, /** * Initial Uploads scan * * @return {Promise} Promise of the scan operation */ initialScan() { let self = this; return fs.readdirAsync(self._uploadsPath).then((ls) => { // Get all folders return Promise.map(ls, (f) => { return fs.statAsync(path.join(self._uploadsPath, f)).then((s) => { return { filename: f, stat: s }; }); }).filter((s) => { return s.stat.isDirectory(); }).then((arrDirs) => { let folderNames = _.map(arrDirs, 'filename'); folderNames.unshift(''); // Add folders to DB return db.UplFolder.remove({}).then(() => { return db.UplFolder.insertMany(_.map(folderNames, (f) => { return { _id: 'f:' + f, name: f }; })); }).then(() => { // Travel each directory and scan files let allFiles = []; return Promise.map(folderNames, (fldName) => { let fldPath = path.join(self._uploadsPath, fldName); return fs.readdirAsync(fldPath).then((fList) => { return Promise.map(fList, (f) => { return upl.processFile(fldName, f).then((mData) => { if(mData) { allFiles.push(mData); } return true; }); }, {concurrency: 3}); }); }, {concurrency: 1}).finally(() => { // Add files to DB return db.UplFile.remove({}).then(() => { if(_.isArray(allFiles) && allFiles.length > 0) { return db.UplFile.insertMany(allFiles); } else { return true; } }); }); }); }); }).then(() => { // Watch for new changes return upl.watch(); }); }, /** * Parse relative Uploads path * * @param {String} f Relative Uploads path * @return {Object} Parsed path (folder and filename) */ parseUploadsRelPath(f) { let fObj = path.parse(f); return { folder: fObj.dir, filename: fObj.base }; }, /** * Get metadata from file and generate thumbnails if necessary * * @param {String} fldName The folder name * @param {String} f The filename * @return {Promise} Promise of the file metadata */ processFile(fldName, f) { let self = this; let fldPath = path.join(self._uploadsPath, fldName); let fPath = path.join(fldPath, f); let fPathObj = path.parse(fPath); let fUid = farmhash.fingerprint32(fldName + '/' + f); return fs.statAsync(fPath).then((s) => { if(!s.isFile()) { return false; } // Get MIME info let mimeInfo = fileType(readChunk.sync(fPath, 0, 262)); if(_.isNil(mimeInfo)) { mimeInfo = { mime: mime.lookup(fPathObj.ext) || 'application/octet-stream' }; } // Images if(s.size < 3145728) { // ignore files larger than 3MB if(_.includes(['image/png', 'image/jpeg', 'image/gif', 'image/webp'], mimeInfo.mime)) { return self.getImageMetadata(fPath).then((mImgData) => { let cacheThumbnailPath = path.parse(path.join(self._uploadsThumbsPath, fUid + '.png')); let cacheThumbnailPathStr = path.format(cacheThumbnailPath); let mData = { _id: fUid, category: 'image', mime: mimeInfo.mime, extra: _.pick(mImgData, ['format', 'width', 'height', 'density', 'hasAlpha', 'orientation']), folder: 'f:' + fldName, filename: f, basename: fPathObj.name, filesize: s.size }; // Generate thumbnail return fs.statAsync(cacheThumbnailPathStr).then((st) => { return st.isFile(); }).catch((err) => { return false; }).then((thumbExists) => { return (thumbExists) ? mData : fs.ensureDirAsync(cacheThumbnailPath.dir).then(() => { return self.generateThumbnail(fPath, cacheThumbnailPathStr); }).return(mData); }); }); } } // Other Files return { _id: fUid, category: 'binary', mime: mimeInfo.mime, folder: 'f:' + fldName, filename: f, basename: fPathObj.name, filesize: s.size }; }); }, /** * Generate thumbnail of image * * @param {String} sourcePath The source path * @param {String} destPath The destination path * @return {Promise} Promise returning the resized image info */ generateThumbnail(sourcePath, destPath) { return sharp(sourcePath) .withoutEnlargement() .resize(150,150) .background('white') .embed() .flatten() .toFormat('png') .toFile(destPath); }, /** * Gets the image metadata. * * @param {String} sourcePath The source path * @return {Object} The image metadata. */ getImageMetadata(sourcePath) { return sharp(sourcePath).metadata(); } };