tileserver-gl-light 5.5.0-pre.2 → 5.5.0-pre.4
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/CHANGELOG.md +3 -1
- package/docs/config.rst +41 -1
- package/docs/usage.rst +5 -0
- package/package.json +5 -5
- package/public/resources/elevation-control.js +1 -1
- package/public/resources/maplibre-gl.js +4 -4
- package/public/resources/maplibre-gl.js.map +1 -1
- package/src/pmtiles_adapter.js +76 -41
- package/src/serve_data.js +2 -4
- package/src/serve_rendered.js +3 -0
- package/src/server.js +8 -0
- package/src/utils.js +1 -1
package/src/pmtiles_adapter.js
CHANGED
|
@@ -17,6 +17,7 @@ class S3Source {
|
|
|
17
17
|
* @param {string} [s3Profile] - Optional AWS credential profile name from config.
|
|
18
18
|
* @param {boolean} [configRequestPayer] - Optional flag from config for requester pays buckets.
|
|
19
19
|
* @param {string} [configRegion] - Optional AWS region from config.
|
|
20
|
+
* @param {string} [s3UrlFormat] - Optional S3 URL format from config: 'aws' or 'custom'.
|
|
20
21
|
* @param {boolean} [verbose] - Whether to show verbose logging.
|
|
21
22
|
*/
|
|
22
23
|
constructor(
|
|
@@ -24,24 +25,37 @@ class S3Source {
|
|
|
24
25
|
s3Profile,
|
|
25
26
|
configRequestPayer,
|
|
26
27
|
configRegion,
|
|
28
|
+
s3UrlFormat,
|
|
27
29
|
verbose = false,
|
|
28
30
|
) {
|
|
29
|
-
const parsed = this.parseS3Url(s3Url);
|
|
31
|
+
const parsed = this.parseS3Url(s3Url, s3UrlFormat);
|
|
30
32
|
this.bucket = parsed.bucket;
|
|
31
33
|
this.key = parsed.key;
|
|
32
34
|
this.endpoint = parsed.endpoint;
|
|
33
35
|
this.url = s3Url;
|
|
34
36
|
this.verbose = verbose;
|
|
35
37
|
|
|
36
|
-
//
|
|
38
|
+
// Apply configuration precedence: Config > URL > Default
|
|
39
|
+
// Using || for strings (empty string = not set)
|
|
40
|
+
// Using ?? for booleans (false is valid value)
|
|
37
41
|
const profile = s3Profile || parsed.profile;
|
|
38
|
-
|
|
39
|
-
// Determine requestPayer: Config takes precedence over URL
|
|
40
42
|
this.requestPayer = configRequestPayer ?? parsed.requestPayer;
|
|
41
|
-
|
|
42
|
-
// Determine region: Config takes precedence over URL
|
|
43
43
|
this.region = configRegion || parsed.region;
|
|
44
44
|
|
|
45
|
+
// Log precedence decisions for debugging
|
|
46
|
+
if (verbose >= 3) {
|
|
47
|
+
console.log(`S3 config precedence for ${s3Url}:`);
|
|
48
|
+
console.log(
|
|
49
|
+
` Profile: ${s3Profile ? 'config' : parsed.profile ? 'url' : 'default'} = ${profile || 'none'}`,
|
|
50
|
+
);
|
|
51
|
+
console.log(
|
|
52
|
+
` Region: ${configRegion ? 'config' : parsed.region !== (process.env.AWS_REGION || 'us-east-1') ? 'url' : 'env/default'} = ${this.region}`,
|
|
53
|
+
);
|
|
54
|
+
console.log(
|
|
55
|
+
` RequestPayer: ${configRequestPayer !== undefined ? 'config' : parsed.requestPayer ? 'url' : 'default'} = ${this.requestPayer}`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
45
59
|
// Create S3 client
|
|
46
60
|
this.s3Client = this.createS3Client(
|
|
47
61
|
parsed.endpoint,
|
|
@@ -54,60 +68,78 @@ class S3Source {
|
|
|
54
68
|
/**
|
|
55
69
|
* Parses various S3 URL formats into bucket, key, endpoint, region, and profile.
|
|
56
70
|
* @param {string} url - The S3 URL to parse.
|
|
71
|
+
* @param {string} [s3UrlFormat] - Optional format override: 'aws' or 'custom'.
|
|
57
72
|
* @returns {object} - An object containing bucket, key, endpoint, region, and profile.
|
|
58
73
|
* @throws {Error} - Throws an error if the URL format is invalid.
|
|
59
74
|
*/
|
|
60
|
-
parseS3Url(url) {
|
|
61
|
-
//
|
|
75
|
+
parseS3Url(url, s3UrlFormat) {
|
|
76
|
+
// Validate s3UrlFormat if provided
|
|
77
|
+
if (s3UrlFormat && s3UrlFormat !== 'aws' && s3UrlFormat !== 'custom') {
|
|
78
|
+
console.warn(
|
|
79
|
+
`Invalid s3UrlFormat: "${s3UrlFormat}". Must be "aws" or "custom". Using auto-detection.`,
|
|
80
|
+
);
|
|
81
|
+
s3UrlFormat = undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
62
84
|
let region = process.env.AWS_REGION || 'us-east-1';
|
|
63
85
|
let profile = null;
|
|
64
86
|
let requestPayer = false;
|
|
65
87
|
|
|
66
|
-
|
|
88
|
+
// Parse URL parameters
|
|
89
|
+
const [cleanUrl, queryString] = url.split('?');
|
|
67
90
|
if (queryString) {
|
|
68
91
|
const params = new URLSearchParams(queryString);
|
|
69
|
-
//
|
|
92
|
+
// URL parameters override defaults
|
|
70
93
|
profile = params.get('profile') ?? profile;
|
|
71
|
-
// Update region if provided in url parameters
|
|
72
94
|
region = params.get('region') ?? region;
|
|
73
|
-
|
|
95
|
+
s3UrlFormat = s3UrlFormat ?? params.get('s3UrlFormat'); // Config overrides URL
|
|
96
|
+
|
|
74
97
|
const payerVal = params.get('requestPayer');
|
|
75
98
|
requestPayer = payerVal === 'true' || payerVal === '1';
|
|
76
99
|
}
|
|
77
100
|
|
|
78
|
-
//
|
|
79
|
-
const
|
|
80
|
-
|
|
101
|
+
// Helper to build result object
|
|
102
|
+
const buildResult = (endpoint, bucket, key) => ({
|
|
103
|
+
endpoint: endpoint ? `https://${endpoint}` : null,
|
|
104
|
+
bucket,
|
|
105
|
+
key,
|
|
106
|
+
region,
|
|
107
|
+
profile,
|
|
108
|
+
requestPayer,
|
|
109
|
+
});
|
|
81
110
|
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
bucket: endpointMatch[2],
|
|
89
|
-
key: endpointMatch[3],
|
|
90
|
-
region,
|
|
91
|
-
profile,
|
|
92
|
-
requestPayer,
|
|
93
|
-
};
|
|
94
|
-
}
|
|
111
|
+
// Define patterns based on format
|
|
112
|
+
const patterns = {
|
|
113
|
+
customWithDot: /^s3:\/\/([^/]*\.[^/]+)\/([^/]+)\/(.+)$/, // Auto-detect: requires dot
|
|
114
|
+
customForced: /^s3:\/\/([^/]+)\/([^/]+)\/(.+)$/, // Explicit: no dot required
|
|
115
|
+
aws: /^s3:\/\/([^/]+)\/(.+)$/,
|
|
116
|
+
};
|
|
95
117
|
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
118
|
+
// Match based on s3UrlFormat or auto-detect
|
|
119
|
+
let match;
|
|
120
|
+
|
|
121
|
+
if (s3UrlFormat === 'custom') {
|
|
122
|
+
match = cleanUrl.match(patterns.customForced);
|
|
123
|
+
if (match) return buildResult(match[1], match[2], match[3]);
|
|
124
|
+
} else if (s3UrlFormat === 'aws') {
|
|
125
|
+
match = cleanUrl.match(patterns.aws);
|
|
126
|
+
if (match) return buildResult(null, match[1], match[2]);
|
|
127
|
+
} else {
|
|
128
|
+
// Auto-detection: try custom (with dot) first, then AWS
|
|
129
|
+
match = cleanUrl.match(patterns.customWithDot);
|
|
130
|
+
if (match) return buildResult(match[1], match[2], match[3]);
|
|
131
|
+
|
|
132
|
+
match = cleanUrl.match(patterns.aws);
|
|
133
|
+
if (match) return buildResult(null, match[1], match[2]);
|
|
108
134
|
}
|
|
109
135
|
|
|
110
|
-
throw new Error(
|
|
136
|
+
throw new Error(
|
|
137
|
+
`Invalid S3 URL format: ${url}\n` +
|
|
138
|
+
`Expected formats:\n` +
|
|
139
|
+
` AWS S3: s3://bucket-name/path/to/file.pmtiles\n` +
|
|
140
|
+
` Custom endpoint: s3://endpoint.com/bucket/path/to/file.pmtiles\n` +
|
|
141
|
+
`Use s3UrlFormat parameter to override auto-detection if needed.`,
|
|
142
|
+
);
|
|
111
143
|
}
|
|
112
144
|
|
|
113
145
|
/**
|
|
@@ -282,6 +314,7 @@ async function readFileBytes(fd, buffer, offset) {
|
|
|
282
314
|
* @param {string} [s3Profile] - Optional AWS credential profile name.
|
|
283
315
|
* @param {boolean} [requestPayer] - Optional flag for requester pays buckets.
|
|
284
316
|
* @param {string} [s3Region] - Optional AWS region.
|
|
317
|
+
* @param {string} [s3UrlFormat] - Optional S3 URL format: 'aws' or 'custom'.
|
|
285
318
|
* @param {boolean} [verbose] - Whether to show verbose logging.
|
|
286
319
|
* @returns {PMTiles} - A PMTiles instance.
|
|
287
320
|
*/
|
|
@@ -290,6 +323,7 @@ export function openPMtiles(
|
|
|
290
323
|
s3Profile,
|
|
291
324
|
requestPayer,
|
|
292
325
|
s3Region,
|
|
326
|
+
s3UrlFormat,
|
|
293
327
|
verbose = 0,
|
|
294
328
|
) {
|
|
295
329
|
let pmtiles = undefined;
|
|
@@ -303,6 +337,7 @@ export function openPMtiles(
|
|
|
303
337
|
s3Profile,
|
|
304
338
|
requestPayer,
|
|
305
339
|
s3Region,
|
|
340
|
+
s3UrlFormat,
|
|
306
341
|
verbose,
|
|
307
342
|
);
|
|
308
343
|
pmtiles = new PMTiles(source);
|
package/src/serve_data.js
CHANGED
|
@@ -121,7 +121,6 @@ export const serve_data = {
|
|
|
121
121
|
|
|
122
122
|
if (isGzipped) {
|
|
123
123
|
data = await gunzipP(data);
|
|
124
|
-
isGzipped = false;
|
|
125
124
|
}
|
|
126
125
|
|
|
127
126
|
if (tileJSONFormat === 'pbf') {
|
|
@@ -164,9 +163,7 @@ export const serve_data = {
|
|
|
164
163
|
headers['Content-Encoding'] = 'gzip';
|
|
165
164
|
res.set(headers);
|
|
166
165
|
|
|
167
|
-
|
|
168
|
-
data = await gzipP(data);
|
|
169
|
-
}
|
|
166
|
+
data = await gzipP(data);
|
|
170
167
|
|
|
171
168
|
return res.status(200).send(data);
|
|
172
169
|
});
|
|
@@ -392,6 +389,7 @@ export const serve_data = {
|
|
|
392
389
|
params.s3Profile,
|
|
393
390
|
params.requestPayer,
|
|
394
391
|
params.s3Region,
|
|
392
|
+
params.s3UrlFormat,
|
|
395
393
|
verbose,
|
|
396
394
|
);
|
|
397
395
|
sourceType = 'pmtiles';
|
package/src/serve_rendered.js
CHANGED
|
@@ -1594,6 +1594,7 @@ export const serve_rendered = {
|
|
|
1594
1594
|
let s3Profile;
|
|
1595
1595
|
let requestPayer;
|
|
1596
1596
|
let s3Region;
|
|
1597
|
+
let s3UrlFormat;
|
|
1597
1598
|
const dataInfo = dataResolver(dataId);
|
|
1598
1599
|
if (dataInfo.inputFile) {
|
|
1599
1600
|
inputFile = dataInfo.inputFile;
|
|
@@ -1602,6 +1603,7 @@ export const serve_rendered = {
|
|
|
1602
1603
|
s3Profile = dataInfo.s3Profile;
|
|
1603
1604
|
requestPayer = dataInfo.requestPayer;
|
|
1604
1605
|
s3Region = dataInfo.s3Region;
|
|
1606
|
+
s3UrlFormat = dataInfo.s3UrlFormat;
|
|
1605
1607
|
} else {
|
|
1606
1608
|
console.error(`ERROR: data "${inputFile}" not found!`);
|
|
1607
1609
|
process.exit(1);
|
|
@@ -1622,6 +1624,7 @@ export const serve_rendered = {
|
|
|
1622
1624
|
s3Profile,
|
|
1623
1625
|
requestPayer,
|
|
1624
1626
|
s3Region,
|
|
1627
|
+
s3UrlFormat,
|
|
1625
1628
|
verbose,
|
|
1626
1629
|
);
|
|
1627
1630
|
// eslint-disable-next-line security/detect-object-injection -- name is from style sources object keys
|
package/src/server.js
CHANGED
|
@@ -290,6 +290,7 @@ async function start(opts) {
|
|
|
290
290
|
let resolvedS3Profile;
|
|
291
291
|
let resolvedRequestPayer;
|
|
292
292
|
let resolvedS3Region;
|
|
293
|
+
let resolvedS3UrlFormat;
|
|
293
294
|
|
|
294
295
|
for (const id of Object.keys(data)) {
|
|
295
296
|
// eslint-disable-next-line security/detect-object-injection -- id is from Object.keys of data config
|
|
@@ -327,6 +328,11 @@ async function start(opts) {
|
|
|
327
328
|
resolvedS3Profile = sourceData.s3Profile;
|
|
328
329
|
}
|
|
329
330
|
|
|
331
|
+
// Get s3UrlFormat if present
|
|
332
|
+
if (Object.hasOwn(sourceData, 's3UrlFormat')) {
|
|
333
|
+
resolvedS3UrlFormat = sourceData.s3UrlFormat;
|
|
334
|
+
}
|
|
335
|
+
|
|
330
336
|
// Get requestPayer if present
|
|
331
337
|
if (Object.hasOwn(sourceData, 'requestPayer')) {
|
|
332
338
|
resolvedRequestPayer = !!sourceData.requestPayer;
|
|
@@ -354,6 +360,7 @@ async function start(opts) {
|
|
|
354
360
|
s3Profile: undefined,
|
|
355
361
|
requestPayer: false,
|
|
356
362
|
s3Region: undefined,
|
|
363
|
+
s3UrlFormat: undefined,
|
|
357
364
|
};
|
|
358
365
|
}
|
|
359
366
|
|
|
@@ -385,6 +392,7 @@ async function start(opts) {
|
|
|
385
392
|
s3Profile: resolvedS3Profile,
|
|
386
393
|
requestPayer: resolvedRequestPayer,
|
|
387
394
|
s3Region: resolvedS3Region,
|
|
395
|
+
s3UrlFormat: resolvedS3UrlFormat,
|
|
388
396
|
};
|
|
389
397
|
},
|
|
390
398
|
),
|
package/src/utils.js
CHANGED
|
@@ -166,7 +166,7 @@ export function getTileUrls(
|
|
|
166
166
|
if (domain.indexOf('*') !== -1) {
|
|
167
167
|
if (relativeSubdomainsUsable) {
|
|
168
168
|
const newParts = hostParts.slice(1);
|
|
169
|
-
newParts.unshift(domain.replace(
|
|
169
|
+
newParts.unshift(domain.replace(/\*/g, hostParts[0]));
|
|
170
170
|
newDomains.push(newParts.join('.'));
|
|
171
171
|
}
|
|
172
172
|
} else {
|