tileblaster 0.4.9 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/UNLICENSE.md +9 -0
- package/bin/tileblaster.js +75 -25
- package/builtins/cache.js +160 -0
- package/builtins/check.js +115 -0
- package/builtins/compress.js +91 -0
- package/builtins/cors.js +33 -0
- package/builtins/deliver.js +34 -0
- package/builtins/dump.js +19 -0
- package/builtins/edit.js +30 -0
- package/builtins/mbtiles.js +52 -0
- package/builtins/modernize.js +57 -0
- package/builtins/noop.js +5 -0
- package/builtins/optimize.js +119 -0
- package/builtins/parse.js +52 -0
- package/builtins/pmtiles.js +80 -0
- package/builtins/sharp.js +46 -0
- package/builtins/tileserver.js +106 -0
- package/builtins/versatiles.js +68 -0
- package/config.dist.js +153 -0
- package/docs/config.md +343 -0
- package/docs/examples.md +257 -0
- package/docs/tileblaster.png +0 -0
- package/docs/tileblaster.svg +261 -0
- package/docs/todo.md +140 -0
- package/lib/debug.js +68 -0
- package/lib/decompress.js +44 -0
- package/lib/load.js +26 -0
- package/lib/mime.js +51 -0
- package/lib/purge.js +109 -0
- package/lib/retrieve.js +15 -0
- package/lib/rfc822date.js +9 -0
- package/lib/router.js +95 -0
- package/lib/store.js +161 -0
- package/lib/strtpl.js +15 -0
- package/lib/tasks.js +50 -0
- package/package.json +25 -13
- package/plugins/example.js +22 -0
- package/readme.md +66 -33
- package/tileblaster.js +445 -0
- package/LICENSE +0 -21
- package/config.js.dist +0 -81
- package/lib/tileblaster.js +0 -789
package/lib/purge.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// purge cache cache
|
|
2
|
+
|
|
3
|
+
const worker = require("node:worker_threads");
|
|
4
|
+
|
|
5
|
+
if (worker.isMainThread) {
|
|
6
|
+
|
|
7
|
+
const debug = require("./debug");
|
|
8
|
+
|
|
9
|
+
const purge = module.exports = function purge(){
|
|
10
|
+
|
|
11
|
+
// create cleanup worker
|
|
12
|
+
const debug = require("./debug");
|
|
13
|
+
debug.info("Creating Purge Worker");
|
|
14
|
+
new worker.Worker(__filename, {
|
|
15
|
+
argv: process.argv.slice(2),
|
|
16
|
+
env: process.env
|
|
17
|
+
}).on("message", function(message){
|
|
18
|
+
if (message.hasOwnProperty("purged")) debug.info("Purged %d files", message.purged);
|
|
19
|
+
}).unref();
|
|
20
|
+
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// run cleanup if called directly
|
|
24
|
+
if (require.main === module) purge();
|
|
25
|
+
|
|
26
|
+
} else { // cleanup worker
|
|
27
|
+
|
|
28
|
+
const dur = require("dur");
|
|
29
|
+
const config = require("./config");
|
|
30
|
+
const klaw = require("klaw");
|
|
31
|
+
const path = require("node:path");
|
|
32
|
+
const fs = require("node:fs");
|
|
33
|
+
|
|
34
|
+
// determine paths
|
|
35
|
+
if (!config.paths) config.paths = {};
|
|
36
|
+
if (!config.paths.work) config.paths.work = path.resolve(os.homedir(), "tileblaster");
|
|
37
|
+
if (!config.paths.data) config.paths.data = path.resolve(config.paths.work, "data");
|
|
38
|
+
|
|
39
|
+
const caches = Object.entries(config.maps).reduce(function(caches,[map,tasks]){
|
|
40
|
+
|
|
41
|
+
// find cache config fore map
|
|
42
|
+
let cache = tasks.find(function(task){
|
|
43
|
+
return task.builtin === "cache" && task.hasOwnProperty("expires");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// no cache here
|
|
47
|
+
if (!cache) return caches;
|
|
48
|
+
|
|
49
|
+
// parse expires, lifted from the builtin
|
|
50
|
+
let expires;
|
|
51
|
+
switch (typeof cache.expires) {
|
|
52
|
+
case "boolean":
|
|
53
|
+
expires = cache.expires;
|
|
54
|
+
break;
|
|
55
|
+
case "number":
|
|
56
|
+
expires = ((Number.isFinite(cache.expires)) ? Math.max(0,Math.round(cache.expires*1000)) : (cache.expires > 0)) || true;
|
|
57
|
+
break;
|
|
58
|
+
case "string":
|
|
59
|
+
expires = dur(cache.expires, true);
|
|
60
|
+
break;
|
|
61
|
+
default:
|
|
62
|
+
expires = true; // expires immediately
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!expires) return caches;
|
|
67
|
+
if (expires === true) expires = 0;
|
|
68
|
+
|
|
69
|
+
caches.push({
|
|
70
|
+
dir: path.join(config.paths.data, map),
|
|
71
|
+
expires: Date.now()+expires,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return caches;
|
|
75
|
+
|
|
76
|
+
},[]);
|
|
77
|
+
|
|
78
|
+
if (caches.length === 0) {
|
|
79
|
+
worker.parentPort.postMessage({ purged: 0 });
|
|
80
|
+
} else {
|
|
81
|
+
|
|
82
|
+
// create tasks
|
|
83
|
+
let purged = 0;
|
|
84
|
+
Promise.allSettled(caches.map(function(cache){
|
|
85
|
+
return new Promise(function(resolve,reject){
|
|
86
|
+
|
|
87
|
+
let deletable = [];
|
|
88
|
+
|
|
89
|
+
klaw(cache.dir).on("data", function(file){
|
|
90
|
+
if (file.stats.mtimeMs < cache.expires) deletable.push(file.path);
|
|
91
|
+
}).on("end", function(){
|
|
92
|
+
Promise.allSettled(deletable.map(function(file){
|
|
93
|
+
return new Promise(function(resolve,reject){
|
|
94
|
+
fs.unlink(file, function(err){
|
|
95
|
+
if (err) return reject(err);
|
|
96
|
+
purged++;
|
|
97
|
+
resolve();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
})).then(resolve).catch(reject);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
})).then(function(){
|
|
104
|
+
worker.parentPort.postMessage({ purged });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
};
|
package/lib/retrieve.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const phin = require("phin");
|
|
2
|
+
|
|
3
|
+
// prepare agents with keepalive enabled
|
|
4
|
+
const agents = {
|
|
5
|
+
http: new require("node:http").Agent({ keepAlive: true }),
|
|
6
|
+
https: new require("node:https").Agent({ keepAlive: true }),
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// agentify phin
|
|
10
|
+
const retrieve = module.exports = async function retrieve(opts, fn) {
|
|
11
|
+
if (typeof opts === "string") opts = { url: opts }; // ensure opts is object
|
|
12
|
+
if (!opts.hasOwnProperty("core")) opts.core = {}; // ensure core is present
|
|
13
|
+
if (!opts.core.hasOwnProperty("agent")) opts.core.agent = agents[opts.url.slice(0,opts.url.indexOf(":"))]; // set agent by protocol
|
|
14
|
+
return phin(opts, fn);
|
|
15
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// convert a date object into a rfc 822 date
|
|
2
|
+
|
|
3
|
+
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
4
|
+
const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
5
|
+
|
|
6
|
+
module.exports = function(date){
|
|
7
|
+
let tzo = date.getTimezoneOffset();
|
|
8
|
+
return days[date.getDay()] + ", " + date.getDate().toString().padStart(2,"0") + " " + months[date.getMonth()] + " " + date.getFullYear() + " " + date.getHours().toString().padStart(2,"0") + ":" + date.getMinutes().toString().padStart(2,"0") + ":" + date.getSeconds().toString().padStart(2,"0") + " " + (tzo > 0 ? "-" : "+") + Math.abs(Math.floor(tzo/60)).toString().padStart(2,"0") + Math.abs(tzo%60).toString().padStart(2,"0");
|
|
9
|
+
};
|
package/lib/router.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
const normalize = require("node:path").normalize;
|
|
2
|
+
const tasks = require("./tasks");
|
|
3
|
+
|
|
4
|
+
// unfancy router, good enough for tileblaster
|
|
5
|
+
const router = module.exports = function router({ mountpoint }) {
|
|
6
|
+
if (!(this instanceof router)) return new router(...arguments);
|
|
7
|
+
const self = this;
|
|
8
|
+
|
|
9
|
+
self.routes = {};
|
|
10
|
+
self.uses = [];
|
|
11
|
+
|
|
12
|
+
// mountpoint, without trailing slash
|
|
13
|
+
self.mountpoint = self.fixpath(mountpoint || "");
|
|
14
|
+
|
|
15
|
+
// default default route, very bare bones
|
|
16
|
+
self.routes[""] = function(req, res){
|
|
17
|
+
res.statusCode = 404, res.end();
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return self;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// route handler, pass to server
|
|
24
|
+
router.prototype.serve = function(req, res) {
|
|
25
|
+
const self = this
|
|
26
|
+
|
|
27
|
+
// prepare
|
|
28
|
+
let path = req.url
|
|
29
|
+
|
|
30
|
+
// if url get path only
|
|
31
|
+
if (path.charCodeAt(0) !== 47) path = path.replace(/^https?:\/\/.*?\//,"/");
|
|
32
|
+
|
|
33
|
+
// trim query string and fragment
|
|
34
|
+
if (path.includes("?")) path = path.slice(0, path.indexOf("?"));
|
|
35
|
+
if (path.includes("#")) path = path.slice(0, path.indexOf("#"));
|
|
36
|
+
|
|
37
|
+
// trim mountpoint
|
|
38
|
+
if (self.mountpoint && path.slice(0,self.mountpoint.length) === self.mountpoint) path = path.slice(self.mountpoint.length);
|
|
39
|
+
|
|
40
|
+
// fix path
|
|
41
|
+
path = self.fixpath(path);
|
|
42
|
+
|
|
43
|
+
// FIXME some header prep? other things?
|
|
44
|
+
|
|
45
|
+
// find route
|
|
46
|
+
let route = path;
|
|
47
|
+
// while (!self.routes.hasOwnProperty(route)) route = route.slice(0, route.lastIndexOf("/")); // in case we need it later
|
|
48
|
+
if (!self.routes.hasOwnProperty(route)) route = ""; // good for now
|
|
49
|
+
|
|
50
|
+
// apply to req
|
|
51
|
+
req.path = path;
|
|
52
|
+
req.route = route;
|
|
53
|
+
|
|
54
|
+
// apply uses
|
|
55
|
+
tasks(self.uses).run(req, res, function(err, req, res){
|
|
56
|
+
|
|
57
|
+
// very unglamourous server error
|
|
58
|
+
if (err) return res.statusCode = 500, res.end();
|
|
59
|
+
|
|
60
|
+
// call route
|
|
61
|
+
self.routes[req.route](req, res);
|
|
62
|
+
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// add route
|
|
68
|
+
router.prototype.route = function(path, route) { // function(req, res){}
|
|
69
|
+
return this.routes[path] = route, this;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// add middleware
|
|
73
|
+
router.prototype.use = function(use) { // function(req, res, next){}
|
|
74
|
+
return this.uses.push(use), this;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// set default route
|
|
78
|
+
router.prototype.default = function(route) {
|
|
79
|
+
return this.routes[""] = route, this;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// fix path
|
|
83
|
+
router.prototype.fixpath = function(path) {
|
|
84
|
+
|
|
85
|
+
// ensure leadin slash
|
|
86
|
+
if (path.charCodeAt(0) !== 47) path = "/"+path;
|
|
87
|
+
|
|
88
|
+
// remove trailing slashes
|
|
89
|
+
while (path.length > 1 && path.charCodeAt(path.length-1) === 47) path = path.slice(0, -1);
|
|
90
|
+
|
|
91
|
+
// normalize path
|
|
92
|
+
path = normalize(path);
|
|
93
|
+
|
|
94
|
+
return path;
|
|
95
|
+
};
|
package/lib/store.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// store files to disk, use xattr to keep metadata
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const dur = require("dur");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
const xattr = require("fs-xattr");
|
|
7
|
+
const rfc822date = require("./rfc822date");
|
|
8
|
+
|
|
9
|
+
const store = module.exports = function store({ root }){
|
|
10
|
+
if (!(this instanceof store)) return new store(...arguments);
|
|
11
|
+
|
|
12
|
+
this.root = path.resolve(process.cwd(), root);
|
|
13
|
+
this.cache = {}; // FIXME use lru cache
|
|
14
|
+
|
|
15
|
+
return this;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// find best file on disk
|
|
19
|
+
store.prototype.find = function(file, extensions, fn){
|
|
20
|
+
const self = this;
|
|
21
|
+
|
|
22
|
+
// find all files in question
|
|
23
|
+
Promise.allSettled([ "", ...extensions.map(function(e){
|
|
24
|
+
return e[0] === "." ? e : "."+e;
|
|
25
|
+
})].map(function(ext){
|
|
26
|
+
return new Promise(function(resolve, reject){
|
|
27
|
+
const filepath = path.join(self.root, file+ext);
|
|
28
|
+
fs.stat(filepath, function(err, stats){
|
|
29
|
+
if (err) reject();
|
|
30
|
+
// get extended attributes
|
|
31
|
+
xattr.get(filepath, "user.tileblaster").then(function(attr){
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
attr = JSON.parse(attr);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
return reject(err);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// check if expired
|
|
40
|
+
if (attr.expires && (attr.expires === true || attr.expires < Date.now())) return reject();
|
|
41
|
+
|
|
42
|
+
resolve({
|
|
43
|
+
file: filepath,
|
|
44
|
+
stats: stats,
|
|
45
|
+
attr: attr,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
}).catch(function(err){
|
|
49
|
+
reject(err);
|
|
50
|
+
});
|
|
51
|
+
})
|
|
52
|
+
});
|
|
53
|
+
})).then(function(found){
|
|
54
|
+
|
|
55
|
+
// extract
|
|
56
|
+
found = found.filter(function(f){
|
|
57
|
+
return f.status === "fulfilled";
|
|
58
|
+
}).map(function(f){
|
|
59
|
+
return f.value;
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
switch (found.length) {
|
|
63
|
+
case 0: return fn(null, null); break;
|
|
64
|
+
case 1: return fn(null, found[0]); break;
|
|
65
|
+
default:
|
|
66
|
+
// find the smallest size
|
|
67
|
+
return fn(null, Array.from(found).sort(function(a,b){
|
|
68
|
+
return a.stats.size - b.stats.size;
|
|
69
|
+
}).shift());
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return this;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// store file and attributes to disk
|
|
78
|
+
store.prototype.put = function(tile, fn){
|
|
79
|
+
const self = this;
|
|
80
|
+
|
|
81
|
+
const destfile = path.join(self.root, tile.path);
|
|
82
|
+
const tmpfile = destfile+".tmp";
|
|
83
|
+
|
|
84
|
+
// check if exists and still valid?
|
|
85
|
+
self.check(destfile, function(err, isValid){
|
|
86
|
+
if (!err && isValid) return fn(null); // end if file is still valid
|
|
87
|
+
|
|
88
|
+
// ensure dest dir
|
|
89
|
+
fs.mkdir(path.dirname(tmpfile), { recursive: true }, function(err){
|
|
90
|
+
if (err) return fn(err);
|
|
91
|
+
|
|
92
|
+
// write tmp file
|
|
93
|
+
fs.writeFile(tmpfile, tile.buffer, function(err){
|
|
94
|
+
if (err) return fn(err);
|
|
95
|
+
|
|
96
|
+
// set etag and rfc822-date
|
|
97
|
+
fs.stat(tmpfile, function(err, stats){
|
|
98
|
+
if (err) return fn(err);
|
|
99
|
+
|
|
100
|
+
tile.headers.etag = '"'+Math.floor(stats.mtimeMs/1000).toString(16)+'-'+(stats.size).toString(16)+'"';
|
|
101
|
+
tile.headers["last-modified"] = rfc822date(stats.mtime);
|
|
102
|
+
tile.headers["expires"] = rfc822date(new Date(tile.expires));
|
|
103
|
+
|
|
104
|
+
// store anything but buffer in xattr
|
|
105
|
+
const attr = Object.entries(tile).reduce(function(attr, [ k, v ]){
|
|
106
|
+
if (k !== "buffer") attr[k] = v;
|
|
107
|
+
return attr;
|
|
108
|
+
},{});
|
|
109
|
+
|
|
110
|
+
// set attributes
|
|
111
|
+
xattr.set(tmpfile, "user.tileblaster", JSON.stringify(attr)).then(function(){
|
|
112
|
+
|
|
113
|
+
// switch tmp → file
|
|
114
|
+
fs.rename(tmpfile, destfile, function(err){
|
|
115
|
+
if (err) return fn(err), fs.unlink(tmpfile, function(){}); // attempt unlinking, no feedback
|
|
116
|
+
fn(null);
|
|
117
|
+
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
}).catch(function(err){
|
|
121
|
+
return fn(err);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return this;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
store.prototype.check = function(file, fn) { // err, isValid
|
|
136
|
+
const self = this;
|
|
137
|
+
|
|
138
|
+
fs.stat(file, function(err, stat){
|
|
139
|
+
if (err && err.code === "ENOENT") return fn(null, false); // does not exist
|
|
140
|
+
if (err) return fn(err);
|
|
141
|
+
|
|
142
|
+
xattr.get(file, "user.tileblaster").then(function(attr){
|
|
143
|
+
|
|
144
|
+
// try parse xattr
|
|
145
|
+
try {
|
|
146
|
+
attr = JSON.parse(attr);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
return fn(err);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// file is valid when no expires property or expires < now
|
|
152
|
+
return fn(null, (!attr.expires || attr.expires < Date.now()));
|
|
153
|
+
|
|
154
|
+
}).catch(function(err){
|
|
155
|
+
return fn(err);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return this;
|
|
161
|
+
};
|
package/lib/strtpl.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// minimalistic string tempate, no regex
|
|
2
|
+
const types = { string: true, number: true, boolean: true };
|
|
3
|
+
const strtpl = module.exports = function strtpl(str, params){
|
|
4
|
+
let out = "", j = 0;
|
|
5
|
+
for (let i = 0; i < str.length; i++) {
|
|
6
|
+
j = i+1;
|
|
7
|
+
if (str[i] === "{" && str[i+2] === "}" && str[j] >= "a" && str[j] <= "z") {
|
|
8
|
+
if (types[typeof params[str[j]]]) out += params[str[j]];
|
|
9
|
+
i+= 2;
|
|
10
|
+
} else {
|
|
11
|
+
out += str[i];
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return out;
|
|
15
|
+
};
|
package/lib/tasks.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// recursive task queue
|
|
2
|
+
const tasks = module.exports = function tasks(jobs){
|
|
3
|
+
if (!(this instanceof tasks)) return new tasks(...arguments);
|
|
4
|
+
const self = this;
|
|
5
|
+
self.stack = [];
|
|
6
|
+
if (jobs) self.push(jobs);
|
|
7
|
+
return this;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// add jobs to stack
|
|
11
|
+
tasks.prototype.push = function(jobs){
|
|
12
|
+
const self = this;
|
|
13
|
+
if (!Array.isArray(jobs)) jobs = [ jobs ];
|
|
14
|
+
jobs.forEach(function(fn){
|
|
15
|
+
if (typeof fn === "function") self.stack.push(fn);
|
|
16
|
+
});
|
|
17
|
+
return this;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// recursively run stack until empty
|
|
21
|
+
tasks.prototype.run = function(){
|
|
22
|
+
const self = this;
|
|
23
|
+
let args = Array.from(arguments);
|
|
24
|
+
let fn = args.pop(); // callback should always be last argument
|
|
25
|
+
if (typeof fn !== "function") throw new Error("tasks.run needs a function argument");
|
|
26
|
+
if (typeof args === "function") fn = args, args = {};
|
|
27
|
+
if (self.stack.length === 0) return fn(null, ...args);
|
|
28
|
+
try {
|
|
29
|
+
self.stack.shift()(...args, function(err){
|
|
30
|
+
if (err) return fn(err, ...args);
|
|
31
|
+
return self.run(...args, fn);
|
|
32
|
+
}, function(label){ // skip
|
|
33
|
+
if (label) {
|
|
34
|
+
// skip stack until found
|
|
35
|
+
let nextlabel = self.stack.findIndex((f)=>(f.label === label));
|
|
36
|
+
if (nextlabel >= 0) {
|
|
37
|
+
self.stack = self.stack.slice(nextlabel);
|
|
38
|
+
return self.run(...args, fn);
|
|
39
|
+
} else {
|
|
40
|
+
return fn(new Error("Tasks: Unable to find '"+label+"'"), ...args);
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
return fn(null, ...args); // end execution
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
} catch (err) {
|
|
47
|
+
return fn(err, ...args);
|
|
48
|
+
}
|
|
49
|
+
return this;
|
|
50
|
+
};
|
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tileblaster",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
5
|
-
"main": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "a quick and versatile map tile caching proxy",
|
|
5
|
+
"main": "tileblaster.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"tileblaster": "bin/tileblaster.js"
|
|
8
8
|
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "DEBUG=tileblaster node bin/tileblaster-dev.js"
|
|
11
|
+
},
|
|
9
12
|
"repository": {
|
|
10
13
|
"type": "git",
|
|
11
14
|
"url": "git+https://github.com/yetzt/tileblaster.git"
|
|
@@ -17,25 +20,34 @@
|
|
|
17
20
|
},
|
|
18
21
|
"homepage": "https://github.com/yetzt/tileblaster#readme",
|
|
19
22
|
"dependencies": {
|
|
20
|
-
"
|
|
21
|
-
"
|
|
23
|
+
"colrz": "^0.0.5",
|
|
24
|
+
"dur": "^1.0.1",
|
|
25
|
+
"fs-xattr": "^0.3.1",
|
|
26
|
+
"klaw": "^4.1.0",
|
|
22
27
|
"minimist": "^1.2.8",
|
|
23
28
|
"node-watch": "^0.7.3",
|
|
24
29
|
"phin": "^3.7.0",
|
|
25
30
|
"quu": "^0.4.3"
|
|
26
31
|
},
|
|
27
32
|
"optionalDependencies": {
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
33
|
+
"fzstd": "^0.1.0",
|
|
34
|
+
"js-mozjpeg": "^0.1.2",
|
|
35
|
+
"mbg": "^0.0.2",
|
|
36
|
+
"node-liblzma": "^1.1.9",
|
|
37
|
+
"optipng-js": "^0.1.2",
|
|
38
|
+
"pmtiles": "^2.7.1",
|
|
39
|
+
"sharp": "^0.32.0",
|
|
40
|
+
"versatiles": "^0.3.1",
|
|
41
|
+
"vtt": "^0.0.3"
|
|
34
42
|
},
|
|
35
43
|
"engines": {
|
|
36
|
-
"node": ">=
|
|
44
|
+
"node": ">=14"
|
|
37
45
|
},
|
|
38
46
|
"os": [
|
|
39
|
-
"
|
|
47
|
+
"darwin",
|
|
48
|
+
"linux"
|
|
49
|
+
],
|
|
50
|
+
"keywords": [
|
|
51
|
+
"tileblaster", "tileserver", "map", "tiles", "tile", "mbtiles", "versatiles", "pmtiles", "server", "proxy", "cache", "vectortiles", "rastertiles", "mvt", "pbf"
|
|
40
52
|
]
|
|
41
53
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// bare bones example plugin
|
|
2
|
+
module.exports = function({ req, res, opts, data }, next, skip){
|
|
3
|
+
|
|
4
|
+
// next() calls the next task
|
|
5
|
+
// skip(name) skips all the next tasks until a plugin or builtin calles `name`, or until the end
|
|
6
|
+
|
|
7
|
+
// req and res are your http connection.
|
|
8
|
+
// if you consume res (e.g. send a resonse and call `res.edn()`),
|
|
9
|
+
// then set `res.used = true;` and use skip() to end processing
|
|
10
|
+
|
|
11
|
+
// opts contains everything from the config
|
|
12
|
+
|
|
13
|
+
// data contains all the data that's passed along
|
|
14
|
+
// data.tile for the "main" tile
|
|
15
|
+
// data.tiles[] for alternative versions
|
|
16
|
+
|
|
17
|
+
// let's so something:
|
|
18
|
+
this.lib.debug.info("I'm a plugin!");
|
|
19
|
+
|
|
20
|
+
// continue
|
|
21
|
+
next();
|
|
22
|
+
};
|
package/readme.md
CHANGED
|
@@ -1,58 +1,91 @@
|
|
|
1
1
|
# tileblaster
|
|
2
2
|
|
|
3
|
-
tileblaster
|
|
3
|
+

|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
tileblaster is a versatile caching proxy server for map tiles. it can handle many different tile sources and file formats
|
|
6
|
+
and can optimise tiles on the fly and speed up delivery by acting as a cache.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
## Awesome things you can do with tileblaster
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
* Serve tiles from any ZXY/TMS tileserver, [VersaTiles container](https://versatiles.org/), pmtiles container or mbtiles database.
|
|
11
|
+
* Edit vector tiles on the fly with [Vector Tile Transformer](https://www.npmjs.com/package/vtt)
|
|
12
|
+
* Optimize raster tiles with `mozjpeg` / `optipng`, convert them to `webp` / `aviv` format on the fly or edit them with [sharp](https://www.npmjs.com/package/sharp).
|
|
13
|
+
* Precompress tiles with `gzip` and `brotli`
|
|
14
|
+
* Cache remote files locally
|
|
10
15
|
|
|
11
|
-
|
|
16
|
+
and much more
|
|
12
17
|
|
|
13
|
-
|
|
18
|
+
## What tileblaster isn't
|
|
14
19
|
|
|
15
|
-
|
|
20
|
+
tileblaster is not a tileserver, it does not read raw OpenStreetMap data or create map tiles from scratch; you need to
|
|
21
|
+
have a source for map tiles. You can of course use tools like [tilemaker](https://tilemaker.org/) to create your own
|
|
22
|
+
tilesets, use freely available ready-made tiles from [Versatiles](https://versatiles.org/) or use another tileserver
|
|
23
|
+
if you're allowed to do so.
|
|
16
24
|
|
|
17
|
-
##
|
|
25
|
+
## Install
|
|
18
26
|
|
|
19
|
-
|
|
27
|
+
`npm i -g tileblaster`
|
|
20
28
|
|
|
21
|
-
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
`tileblaster [options] [-c] config.js`
|
|
32
|
+
|
|
33
|
+
### Options
|
|
34
|
+
|
|
35
|
+
* `-c` `--config <config.js>` - load config file
|
|
36
|
+
* `-p` `--port <[host:]port>` - listen on this port (overrides config)
|
|
37
|
+
* `-s` `--socket <socket[,mode,gid]>` - on this socket (overrides config)
|
|
38
|
+
* `-t` `--threads <num>` - number of threads (overrides config)
|
|
39
|
+
* `-h` `--help` - print help screen
|
|
40
|
+
* `-v` `--verbose` - enable debug output
|
|
41
|
+
* `-q` `--quiet` - disable debug output
|
|
42
|
+
|
|
43
|
+
## Configuration
|
|
44
|
+
|
|
45
|
+
See [Configuration](docs/config.md) and [Examples](docs/examples.md)
|
|
46
|
+
|
|
47
|
+
### Plugins
|
|
48
|
+
|
|
49
|
+
tileblaster supports plugins. They work just like builtins, but you can specify them in `config.plugins`
|
|
50
|
+
|
|
51
|
+
[Example Plugin](plugins/example.js)
|
|
52
|
+
|
|
53
|
+
### Nginx
|
|
54
|
+
|
|
55
|
+
tileblaster is easy to use with nginx acting as a reverse proxy. Here is a simple example:
|
|
22
56
|
|
|
23
57
|
```
|
|
24
|
-
upstream
|
|
25
|
-
server
|
|
58
|
+
upstream tileblaster {
|
|
59
|
+
server 127.0.0.1:28897;
|
|
60
|
+
# server unix:/path/to/tileblaster.socket; # ← if you use sockets
|
|
26
61
|
}
|
|
27
62
|
|
|
28
63
|
server {
|
|
29
|
-
listen 80;
|
|
30
|
-
server_name tileblaster;
|
|
31
|
-
|
|
32
|
-
gzip_static on;
|
|
33
|
-
# brotli_static on; # if ngx_brotli is available
|
|
34
|
-
|
|
35
|
-
if (-f $document_root/$uri.err) {
|
|
36
|
-
return 204;
|
|
37
|
-
}
|
|
38
64
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
65
|
+
# ...
|
|
66
|
+
|
|
67
|
+
location /tileblaster { # ← set config.server.mount to the same path
|
|
68
|
+
proxy_set_header Host $host;
|
|
69
|
+
proxy_set_header Origin $http_origin;
|
|
70
|
+
proxy_set_header Accept-Encoding $http_accept_encoding;
|
|
71
|
+
proxy_set_header Accept-Language $http_accept_language;
|
|
72
|
+
proxy_set_header Accept $http_accept;
|
|
73
|
+
proxy_set_header If-Modified-Since $http_if_modified_since
|
|
74
|
+
proxy_set_header If-None-Match $http_if_none_match
|
|
75
|
+
proxy_http_version 1.1;
|
|
76
|
+
proxy_pass http://tileblaster;
|
|
42
77
|
}
|
|
43
78
|
|
|
44
|
-
location @tileblaster {
|
|
45
|
-
proxy_pass http://upstream_tileblaster;
|
|
46
|
-
}
|
|
47
79
|
}
|
|
80
|
+
|
|
48
81
|
```
|
|
49
82
|
|
|
50
|
-
##
|
|
83
|
+
## Optional Dependencies
|
|
84
|
+
|
|
85
|
+
tileblaster has a few optional dependencies, that are mostly used for image manilulation and optimisation (Sharp, MozJPEG, OptiPNG) or more complex tile sources (Versatiles, PMTiles, MBTiles).
|
|
51
86
|
|
|
52
|
-
|
|
87
|
+
If you don't need them, install tileblaster with `npm i -g tileblaster --no-optional`
|
|
53
88
|
|
|
54
|
-
|
|
55
|
-
* `<z>`, `<x>` and `<z>` are the tile coorinates
|
|
56
|
-
* `<d>` is the optional pixel density marker, for example `@2x`
|
|
57
|
-
* `<ext>` is the extension, for example `png`, `geojson` or `pbf`
|
|
89
|
+
## License
|
|
58
90
|
|
|
91
|
+
[Unlicense](./UNLICENSE.md)
|