var path = require('path'); var crypto = require('crypto'); module.exports = { createFromFile: function (filePath, useChecksum) { var fname = path.basename(filePath); var dir = path.dirname(filePath); return this.create(fname, dir, useChecksum); }, create: function (cacheId, _path, useChecksum) { var fs = require('fs'); var flatCache = require('flat-cache'); var cache = flatCache.load(cacheId, _path); var normalizedEntries = {}; var removeNotFoundFiles = function removeNotFoundFiles() { const cachedEntries = cache.keys(); // remove not found entries cachedEntries.forEach(function remover(fPath) { try { fs.statSync(fPath); } catch (err) { if (err.code === 'ENOENT') { cache.removeKey(fPath); } } }); }; removeNotFoundFiles(); return { /** * the flat cache storage used to persist the metadata of the `files * @type {Object} */ cache: cache, /** * Given a buffer, calculate md5 hash of its content. * @method getHash * @param {Buffer} buffer buffer to calculate hash on * @return {String} content hash digest */ getHash: function (buffer) { return crypto.createHash('md5').update(buffer).digest('hex'); }, /** * Return whether or not a file has changed since last time reconcile was called. * @method hasFileChanged * @param {String} file the filepath to check * @return {Boolean} wheter or not the file has changed */ hasFileChanged: function (file) { return this.getFileDescriptor(file).changed; }, /** * given an array of file paths it return and object with three arrays: * - changedFiles: Files that changed since previous run * - notChangedFiles: Files that haven't change * - notFoundFiles: Files that were not found, probably deleted * * @param {Array} files the files to analyze and compare to the previous seen files * @return {[type]} [description] */ analyzeFiles: function (files) { var me = this; files = files || []; var res = { changedFiles: [], notFoundFiles: [], notChangedFiles: [], }; me.normalizeEntries(files).forEach(function (entry) { if (entry.changed) { res.changedFiles.push(entry.key); return; } if (entry.notFound) { res.notFoundFiles.push(entry.key); return; } res.notChangedFiles.push(entry.key); }); return res; }, getFileDescriptor: function (file) { var fstat; try { fstat = fs.statSync(file); } catch (ex) { this.removeEntry(file); return { key: file, notFound: true, err: ex }; } if (useChecksum) { return this._getFileDescriptorUsingChecksum(file); } return this._getFileDescriptorUsingMtimeAndSize(file, fstat); }, _getFileDescriptorUsingMtimeAndSize: function (file, fstat) { var meta = cache.getKey(file); var cacheExists = !!meta; var cSize = fstat.size; var cTime = fstat.mtime.getTime(); var isDifferentDate; var isDifferentSize; if (!meta) { meta = { size: cSize, mtime: cTime }; } else { isDifferentDate = cTime !== meta.mtime; isDifferentSize = cSize !== meta.size; } var nEntry = (normalizedEntries[file] = { key: file, changed: !cacheExists || isDifferentDate || isDifferentSize, meta: meta, }); return nEntry; }, _getFileDescriptorUsingChecksum: function (file) { var meta = cache.getKey(file); var cacheExists = !!meta; var contentBuffer; try { contentBuffer = fs.readFileSync(file); } catch (ex) { contentBuffer = ''; } var isDifferent = true; var hash = this.getHash(contentBuffer); if (!meta) { meta = { hash: hash }; } else { isDifferent = hash !== meta.hash; } var nEntry = (normalizedEntries[file] = { key: file, changed: !cacheExists || isDifferent, meta: meta, }); return nEntry; }, /** * Return the list o the files that changed compared * against the ones stored in the cache * * @method getUpdated * @param files {Array} the array of files to compare against the ones in the cache * @returns {Array} */ getUpdatedFiles: function (files) { var me = this; files = files || []; return me .normalizeEntries(files) .filter(function (entry) { return entry.changed; }) .map(function (entry) { return entry.key; }); }, /** * return the list of files * @method normalizeEntries * @param files * @returns {*} */ normalizeEntries: function (files) { files = files || []; var me = this; var nEntries = files.map(function (file) { return me.getFileDescriptor(file); }); //normalizeEntries = nEntries; return nEntries; }, /** * Remove an entry from the file-entry-cache. Useful to force the file to still be considered * modified the next time the process is run * * @method removeEntry * @param entryName */ removeEntry: function (entryName) { delete normalizedEntries[entryName]; cache.removeKey(entryName); }, /** * Delete the cache file from the disk * @method deleteCacheFile */ deleteCacheFile: function () { cache.removeCacheFile(); }, /** * remove the cache from the file and clear the memory cache */ destroy: function () { normalizedEntries = {}; cache.destroy(); }, _getMetaForFileUsingCheckSum: function (cacheEntry) { var contentBuffer = fs.readFileSync(cacheEntry.key); var hash = this.getHash(contentBuffer); var meta = Object.assign(cacheEntry.meta, { hash: hash }); delete meta.size; delete meta.mtime; return meta; }, _getMetaForFileUsingMtimeAndSize: function (cacheEntry) { var stat = fs.statSync(cacheEntry.key); var meta = Object.assign(cacheEntry.meta, { size: stat.size, mtime: stat.mtime.getTime(), }); delete meta.hash; return meta; }, /** * Sync the files and persist them to the cache * @method reconcile */ reconcile: function (noPrune) { removeNotFoundFiles(); noPrune = typeof noPrune === 'undefined' ? true : noPrune; var entries = normalizedEntries; var keys = Object.keys(entries); if (keys.length === 0) { return; } var me = this; keys.forEach(function (entryName) { var cacheEntry = entries[entryName]; try { var meta = useChecksum ? me._getMetaForFileUsingCheckSum(cacheEntry) : me._getMetaForFileUsingMtimeAndSize(cacheEntry); cache.setKey(entryName, meta); } catch (err) { // if the file does not exists we don't save it // other errors are just thrown if (err.code !== 'ENOENT') { throw err; } } }); cache.save(noPrune); }, }; }, };