UNPKG

tileblaster

Version:

a quick and versatile map tile caching proxy

163 lines (124 loc) 3.96 kB
// store files to disk, use xattr to keep metadata const fs = require("node:fs"); const dur = require("dur"); const path = require("node:path"); const xattr = require("fs-xattr"); const rfc822date = require("./rfc822date"); const store = module.exports = function store({ root, ext }){ if (!(this instanceof store)) return new store(...arguments); this.root = path.resolve(process.cwd(), root); this.ext = ext || ".tmp"; this.cache = {}; // FIXME use lru cache return this; }; // find best file on disk store.prototype.find = function(file, extensions, fn){ const self = this; // find all files in question Promise.allSettled([ "", ...extensions.map(function(e){ return e[0] === "." ? e : "."+e; })].map(function(ext){ return new Promise(function(resolve, reject){ const filepath = path.join(self.root, file+ext); fs.stat(filepath, function(err, stats){ if (err) reject(); // get extended attributes xattr.get(filepath, "user.tileblaster").then(function(attr){ try { attr = JSON.parse(attr); } catch (err) { return reject(err); } // check if expired if (attr.expires && (attr.expires === true || attr.expires < Date.now())) return reject(); resolve({ file: filepath, stats: stats, attr: attr, }); }).catch(function(err){ reject(err); }); }) }); })).then(function(found){ // extract found = found.filter(function(f){ return f.status === "fulfilled"; }).map(function(f){ return f.value; }) switch (found.length) { case 0: return fn(null, null); break; case 1: return fn(null, found[0]); break; default: // find the smallest size return fn(null, Array.from(found).sort(function(a,b){ return a.stats.size - b.stats.size; }).shift()); break; } }); return this; }; // store file and attributes to disk store.prototype.put = function(tile, fn){ const self = this; const destfile = path.join(self.root, tile.path); const tmpfile = destfile+"."+Date.now()+self.ext; // check if exists and still valid? self.check(destfile, function(err, isValid){ if (!err && isValid) return fn(null); // end if file is still valid // ensure dest dir fs.mkdir(path.dirname(tmpfile), { recursive: true }, function(err){ if (err) return fn(err); // write tmp file fs.writeFile(tmpfile, tile.buffer, function(err){ if (err) return fn(err); // set etag and rfc822-date fs.stat(tmpfile, function(err, stats){ if (err) return fn(err); tile.headers.etag = '"'+Math.floor(stats.mtimeMs/1000).toString(16)+'-'+(stats.size).toString(16)+'"'; tile.headers["last-modified"] = rfc822date(stats.mtime); tile.headers["expires"] = rfc822date(new Date(tile.expires)); // store anything but buffer in xattr const attr = Object.entries(tile).reduce(function(attr, [ k, v ]){ if (k !== "buffer") attr[k] = v; return attr; },{}); // set attributes xattr.set(tmpfile, "user.tileblaster", JSON.stringify(attr)).then(function(){ // switch tmp → file fs.rename(tmpfile, destfile, function(err){ if (err) return fn(err), fs.unlink(tmpfile, function(){}); // attempt unlinking, no feedback fn(null); }); }).catch(function(err){ return fn(err); }); }); }); }); }); return this; }; store.prototype.check = function(file, fn) { // err, isValid const self = this; fs.stat(file, function(err, stat){ if (err && err.code === "ENOENT") return fn(null, false); // does not exist if (err) return fn(err); xattr.get(file, "user.tileblaster").then(function(attr){ // try parse xattr try { attr = JSON.parse(attr); } catch (err) { return fn(err); } // file is valid when no expires property or expires < now return fn(null, (!attr.expires || attr.expires < Date.now())); }).catch(function(err){ return fn(err); }); }); return this; };