| 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); |
| }, |
| }; |
| }, |
| }; |