Files
szjs/node_modules/file-entry-cache/cache.js
2025-03-07 22:27:18 +08:00

319 lines
7.7 KiB
JavaScript

/* eslint-disable unicorn/no-this-assignment, func-names, no-multi-assign */
const path = require('node:path');
const crypto = require('node:crypto');
module.exports = {
createFromFile(filePath, useChecksum, currentWorkingDir) {
const fname = path.basename(filePath);
const dir = path.dirname(filePath);
return this.create(fname, dir, useChecksum, currentWorkingDir);
},
create(cacheId, _path, useChecksum, currentWorkingDir) {
const fs = require('node:fs');
const flatCache = require('flat-cache');
const cache = flatCache.load(cacheId, _path);
let normalizedEntries = {};
const removeNotFoundFiles = function removeNotFoundFiles() {
const cachedEntries = cache.keys();
// Remove not found entries
for (const fPath of cachedEntries) {
try {
let filePath = fPath;
if (currentWorkingDir) {
filePath = path.join(currentWorkingDir, fPath);
}
fs.statSync(filePath);
} catch (error) {
if (error.code === 'ENOENT') {
cache.removeKey(fPath);
}
}
}
};
removeNotFoundFiles();
return {
/**
* The flat cache storage used to persist the metadata of the `files
* @type {Object}
*/
cache,
/**
* To enable relative paths as the key with current working directory
* @type {string}
*/
currentWorkingDir: currentWorkingDir ?? undefined,
/**
* 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(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(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(files) {
const me = this;
files ||= [];
const res = {
changedFiles: [],
notFoundFiles: [],
notChangedFiles: [],
};
for (const entry of me.normalizeEntries(files)) {
if (entry.changed) {
res.changedFiles.push(entry.key);
continue;
}
if (entry.notFound) {
res.notFoundFiles.push(entry.key);
continue;
}
res.notChangedFiles.push(entry.key);
}
return res;
},
getFileDescriptor(file) {
let fstat;
try {
fstat = fs.statSync(file);
} catch (error) {
this.removeEntry(file);
return {key: file, notFound: true, err: error};
}
if (useChecksum) {
return this._getFileDescriptorUsingChecksum(file);
}
return this._getFileDescriptorUsingMtimeAndSize(file, fstat);
},
_getFileKey(file) {
if (this.currentWorkingDir) {
return file.split(this.currentWorkingDir).pop();
}
return file;
},
_getFileDescriptorUsingMtimeAndSize(file, fstat) {
let meta = cache.getKey(this._getFileKey(file));
const cacheExists = Boolean(meta);
const cSize = fstat.size;
const cTime = fstat.mtime.getTime();
let isDifferentDate;
let isDifferentSize;
if (meta) {
isDifferentDate = cTime !== meta.mtime;
isDifferentSize = cSize !== meta.size;
} else {
meta = {size: cSize, mtime: cTime};
}
const nEntry = (normalizedEntries[this._getFileKey(file)] = {
key: this._getFileKey(file),
changed: !cacheExists || isDifferentDate || isDifferentSize,
meta,
});
return nEntry;
},
_getFileDescriptorUsingChecksum(file) {
let meta = cache.getKey(this._getFileKey(file));
const cacheExists = Boolean(meta);
let contentBuffer;
try {
contentBuffer = fs.readFileSync(file);
} catch {
contentBuffer = '';
}
let isDifferent = true;
const hash = this.getHash(contentBuffer);
if (meta) {
isDifferent = hash !== meta.hash;
} else {
meta = {hash};
}
const nEntry = (normalizedEntries[this._getFileKey(file)] = {
key: this._getFileKey(file),
changed: !cacheExists || isDifferent,
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(files) {
const me = this;
files ||= [];
return me
.normalizeEntries(files)
.filter(entry => entry.changed)
.map(entry => entry.key);
},
/**
* Return the list of files
* @method normalizeEntries
* @param files
* @returns {*}
*/
normalizeEntries(files) {
files ||= [];
const me = this;
const nEntries = files.map(file => 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(entryName) {
delete normalizedEntries[this._getFileKey(entryName)];
cache.removeKey(this._getFileKey(entryName));
},
/**
* Delete the cache file from the disk
* @method deleteCacheFile
*/
deleteCacheFile() {
cache.removeCacheFile();
},
/**
* Remove the cache from the file and clear the memory cache
*/
destroy() {
normalizedEntries = {};
cache.destroy();
},
_getMetaForFileUsingCheckSum(cacheEntry) {
let filePath = cacheEntry.key;
if (this.currentWorkingDir) {
filePath = path.join(this.currentWorkingDir, filePath);
}
const contentBuffer = fs.readFileSync(filePath);
const hash = this.getHash(contentBuffer);
const meta = Object.assign(cacheEntry.meta, {hash});
delete meta.size;
delete meta.mtime;
return meta;
},
_getMetaForFileUsingMtimeAndSize(cacheEntry) {
let filePath = cacheEntry.key;
if (currentWorkingDir) {
filePath = path.join(currentWorkingDir, filePath);
}
const stat = fs.statSync(filePath);
const 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(noPrune) {
removeNotFoundFiles();
noPrune = noPrune === undefined ? true : noPrune;
const entries = normalizedEntries;
const keys = Object.keys(entries);
if (keys.length === 0) {
return;
}
const me = this;
for (const entryName of keys) {
const cacheEntry = entries[entryName];
try {
const meta = useChecksum
? me._getMetaForFileUsingCheckSum(cacheEntry)
: me._getMetaForFileUsingMtimeAndSize(cacheEntry);
cache.setKey(this._getFileKey(entryName), meta);
} catch (error) {
// If the file does not exists we don't save it
// other errors are just thrown
if (error.code !== 'ENOENT') {
throw error;
}
}
}
cache.save(noPrune);
},
};
},
};