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;
|