238 lines
5.4 KiB
JavaScript
238 lines
5.4 KiB
JavaScript
const fs = require("node:fs");
|
||
const path = require("node:path");
|
||
const debugUtil = require("debug");
|
||
const { parse } = require("flatted");
|
||
|
||
const debug = debugUtil("Eleventy:Fetch");
|
||
const debugAssets = debugUtil("Eleventy:Assets");
|
||
|
||
const DirectoryManager = require("./DirectoryManager.js");
|
||
const ExistsCache = require("./ExistsCache.js");
|
||
|
||
let existsCache = new ExistsCache();
|
||
|
||
class FileCache {
|
||
#source;
|
||
#directoryManager;
|
||
#metadata;
|
||
#defaultType;
|
||
#contents;
|
||
#dryRun = false;
|
||
#cacheDirectory = ".cache";
|
||
#savePending = false;
|
||
#counts = {
|
||
read: 0,
|
||
write: 0,
|
||
};
|
||
|
||
constructor(cacheFilename, options = {}) {
|
||
this.cacheFilename = cacheFilename;
|
||
if(options.dir) {
|
||
this.#cacheDirectory = options.dir;
|
||
}
|
||
if(options.source) {
|
||
this.#source = options.source;
|
||
}
|
||
}
|
||
|
||
setDefaultType(type) {
|
||
if(type) {
|
||
this.#defaultType = type;
|
||
}
|
||
}
|
||
|
||
setDryRun(val) {
|
||
this.#dryRun = Boolean(val);
|
||
}
|
||
|
||
setDirectoryManager(manager) {
|
||
this.#directoryManager = manager;
|
||
}
|
||
|
||
ensureDir() {
|
||
if (this.#dryRun || existsCache.exists(this.#cacheDirectory)) {
|
||
return;
|
||
}
|
||
|
||
if(!this.#directoryManager) {
|
||
// standalone fallback (for tests)
|
||
this.#directoryManager = new DirectoryManager();
|
||
}
|
||
|
||
this.#directoryManager.create(this.#cacheDirectory);
|
||
}
|
||
|
||
set(type, contents, extraMetadata = {}) {
|
||
this.#savePending = true;
|
||
|
||
this.#metadata = {
|
||
cachedAt: Date.now(),
|
||
type,
|
||
// source: this.#source,
|
||
metadata: extraMetadata,
|
||
};
|
||
|
||
this.#contents = contents;
|
||
}
|
||
|
||
get fsPath() {
|
||
return path.join(this.#cacheDirectory, this.cacheFilename);
|
||
}
|
||
|
||
getContentsPath(type) {
|
||
if(!type) {
|
||
throw new Error("Missing cache type for " + this.fsPath);
|
||
}
|
||
|
||
// normalize to storage type
|
||
if(type === "xml") {
|
||
type = "text";
|
||
} else if(type === "parsed-xml") {
|
||
type = "json";
|
||
}
|
||
|
||
return `${this.fsPath}.${type}`;
|
||
}
|
||
|
||
// only when side loaded (buffer content)
|
||
get contentsPath() {
|
||
return this.getContentsPath(this.#metadata?.type);
|
||
}
|
||
|
||
get() {
|
||
if(this.#metadata) {
|
||
return this.#metadata;
|
||
}
|
||
|
||
if(!existsCache.exists(this.fsPath)) {
|
||
return;
|
||
}
|
||
|
||
debug(`Fetching from cache ${this.fsPath}`);
|
||
if(this.#source) {
|
||
debugAssets("[11ty/eleventy-fetch] Reading via %o", this.#source);
|
||
} else {
|
||
debugAssets("[11ty/eleventy-fetch] Reading %o", this.fsPath);
|
||
}
|
||
|
||
this.#counts.read++;
|
||
let data = fs.readFileSync(this.fsPath, "utf8");
|
||
|
||
let json;
|
||
// Backwards compatibility with previous caches usingn flat-cache and `flatted`
|
||
if(data.startsWith(`[["1"],`)) {
|
||
let flattedParsed = parse(data);
|
||
if(flattedParsed?.[0]?.value) {
|
||
json = flattedParsed?.[0]?.value
|
||
}
|
||
} else {
|
||
json = JSON.parse(data);
|
||
}
|
||
|
||
this.#metadata = json;
|
||
|
||
return json;
|
||
}
|
||
|
||
_backwardsCompatGetContents(rawData, type) {
|
||
if (type === "json") {
|
||
return rawData.contents;
|
||
} else if (type === "text") {
|
||
return rawData.contents.toString();
|
||
}
|
||
|
||
// buffer
|
||
return Buffer.from(rawData.contents);
|
||
}
|
||
|
||
hasContents(type) {
|
||
if(this.#contents) {
|
||
return true;
|
||
}
|
||
if(this.get()?.contents) { // backwards compat with very old caches
|
||
return true;
|
||
}
|
||
return existsCache.exists(this.getContentsPath(type));
|
||
}
|
||
|
||
getType() {
|
||
return this.#metadata?.type || this.#defaultType;
|
||
}
|
||
|
||
getContents() {
|
||
if(this.#contents) {
|
||
return this.#contents;
|
||
}
|
||
|
||
let metadata = this.get();
|
||
// backwards compat with old caches
|
||
if(metadata?.contents) {
|
||
// already parsed, part of the top level file
|
||
let normalizedContent = this._backwardsCompatGetContents(this.get(), this.getType());
|
||
this.#contents = normalizedContent;
|
||
return normalizedContent;
|
||
}
|
||
|
||
if(!existsCache.exists(this.contentsPath)) {
|
||
return;
|
||
}
|
||
|
||
debug(`Fetching from cache ${this.contentsPath}`);
|
||
if(this.#source) {
|
||
debugAssets("[11ty/eleventy-fetch] Reading (side loaded) via %o", this.#source);
|
||
} else {
|
||
debugAssets("[11ty/eleventy-fetch] Reading (side loaded) %o", this.contentsPath);
|
||
}
|
||
|
||
// It is intentional to store contents in a separate file from the metadata: we don’t want to
|
||
// have to read the entire contents via JSON.parse (or otherwise) to check the cache validity.
|
||
this.#counts.read++;
|
||
let type = metadata?.type || this.getType();
|
||
let data = fs.readFileSync(this.contentsPath);
|
||
if (type === "json" || type === "parsed-xml") {
|
||
data = JSON.parse(data);
|
||
}
|
||
this.#contents = data;
|
||
return data;
|
||
}
|
||
|
||
save() {
|
||
if(this.#dryRun || !this.#savePending || this.#metadata && Object.keys(this.#metadata) === 0) {
|
||
return;
|
||
}
|
||
|
||
this.ensureDir(); // doesn’t add to counts (yet?)
|
||
|
||
// contents before metadata
|
||
debugAssets("[11ty/eleventy-fetch] Writing %o (side loaded) from %o", this.contentsPath, this.#source);
|
||
|
||
this.#counts.write++;
|
||
// the contents must exist before the cache metadata are saved below
|
||
let contents = this.#contents;
|
||
let type = this.getType();
|
||
if (type === "json" || type === "parsed-xml") {
|
||
contents = JSON.stringify(contents);
|
||
}
|
||
fs.writeFileSync(this.contentsPath, contents);
|
||
debug(`Writing ${this.contentsPath}`);
|
||
|
||
this.#counts.write++;
|
||
debugAssets("[11ty/eleventy-fetch] Writing %o from %o", this.fsPath, this.#source);
|
||
fs.writeFileSync(this.fsPath, JSON.stringify(this.#metadata), "utf8");
|
||
debug(`Writing ${this.fsPath}`);
|
||
}
|
||
|
||
// for testing
|
||
getAllPossibleFilePaths() {
|
||
let types = ["text", "buffer", "json"];
|
||
let paths = new Set();
|
||
paths.add(this.fsPath);
|
||
for(let type of types) {
|
||
paths.add(this.getContentsPath(type));
|
||
}
|
||
return Array.from(paths);
|
||
}
|
||
}
|
||
|
||
module.exports = FileCache;
|