UNPKG

tileblaster

Version:

a quick and versatile map tile caching proxy

161 lines (128 loc) 4.29 kB
const fs = require("node:fs"); const dur = require("dur"); const path = require("node:path"); const xattr = require("fs-xattr"); const cache = {}; // file cache module.exports = function({ opts, data, res }, next, skip){ const debug = this.lib.debug; const config = this.config; // cache opts if (!cache.hasOwnProperty(data.map)) { // store instance cache.store = this.lib.store({ root: config.paths.data, ext: "."+config.id+".tmp" }); // merge opts of all uses of the cache builtin in a map config opts = config.maps[data.map].filter(function(use){ return use.builtin === "cache"; }).reduce(function(opts, useopts){ return Object.entries(useopts).reduce(function(opts, [k,v]){ return opts[k]=v,opts; }, opts); },{}); cache[data.map] = {}; // skipto cache[data.map].skipto = opts.skipto || false; // FIXME check valid label? // true: expires instantly, false: expires never, number: seconds, string: parse if (opts.hasOwnProperty("expires")) { switch (typeof opts.expires) { case "boolean": cache[data.map].expires = opts.expires; break; case "number": cache[data.map].expires = ((Number.isFinite(opts.expires)) ? Math.max(0,Math.round(opts.expires*1000)) : (opts.expires > 0)) || true; break; case "string": cache[data.map].expires = dur(opts.expires, true); break; default: cache[data.map].expires = true; // expires immediately break; } } else { cache[data.map].expires = false; } }; opts = cache[data.map]; const store = cache.store; // if tile length is empty, retrieve. otherwise save if (data.tiles.length === 0) { // find extensions let extensions = Object.entries(data.req.supports).reduce(function(extensions, [ext, supports]){ if (supports) extensions.push(ext); return extensions; },[]); // get from storage return store.find(data.req.path, extensions, function(err, tile){ if (err) return debug.error("Cache: Could not find %s: %s", data.req.path, err), next(); if (!tile) return next(); // check etag if (data.req.etag && tile.attr.headers && data.req.etag === tile.attr.headers.etag) { res.statusCode = 304; res.end(); res.used = true; return skip(); // skip rest of jobs }; // check last-modified if (data.req.last && tile.stats && data.req.last < tile.stats.mtimeMs) { res.statusCode = 304; res.end(); res.used = true; return skip(); // skip rest of jobs }; // if skipto is set, build tile data from cache and skip if (opts.skipto) { data.tile = tile.attr; fs.readFile(tile.file, function(err, buf){ if (err) return debug.error("Cache: Could not read %s: %s", tile.file, err), next(); data.tile.buffer = buf; data.tiles.push(data.tile); skip(opts.skipto); }); } else { // send tile to client // if tile buffer is empty, return 204 if (tile.stats.size === 0) { res.statusCode = 204; res.used = true; res.end(); return skip(); // skip rest of jobs } // set status and content-type res.statusCode = tile.attr.status; // set headers Object.entries({ ...tile.attr.headers, // tile-specific "content-type": tile.attr.mimetype, "content-length": tile.stats.size, }).forEach(function([ k, v ]){ res.setHeader(k, v); }); // send as stream fs.createReadStream(tile.file).pipe(res); res.used = true; skip(); } }); } else { // set expires on all tiles if (opts.expires !== false) { let expires = Date.now()+opts.expires; data.tile.expires = expires; data.tiles.forEach(function(tile){ tile.expires = expires; }); } // no need to wait for files to get stored next(); // skip storing if tiles expire immediately if (opts.expires === true) return debug.warn("Cache: Skipping due to immediate expiration in map %s", data.map.magenta); // store all tiles if not stored Promise.allSettled(data.tiles.map(function(tile){ return new Promise(function(reject, resolve){ store.put(tile, function(err){ if (err) return debug.warn("Cache: Error storing %s: %s", tile.path.magenta, err), reject(err); return debug.info("Cache: Stored %s", tile.path.magenta), resolve(); }); }); })); }; };