tileserver-gl-light 5.5.0-pre.2 → 5.5.0-pre.3

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.
@@ -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
- // Determine the final profile: Config takes precedence over URL
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
- // Initialize with defaults/environment
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
- const queryString = url.split('?')[1];
88
+ // Parse URL parameters
89
+ const [cleanUrl, queryString] = url.split('?');
67
90
  if (queryString) {
68
91
  const params = new URLSearchParams(queryString);
69
- // Update profile if provided in url parameters
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
- // Update requestPayer if provided in url parameters
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
- // Clean URL for format detection (remove trailing slashes)
79
- const baseUrl = url.split('?')[0];
80
- const cleanUrl = baseUrl.replace(/\/+$/, '');
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
- // Format 1: s3://endpoint/bucket/key (S3-compatible storage)
83
- // Example: s3://storage.example.com/mybucket/path/to/tiles.pmtile
84
- const endpointMatch = cleanUrl.match(/^s3:\/\/([^/]+)\/([^/]+)\/(.+)$/);
85
- if (endpointMatch) {
86
- return {
87
- endpoint: `https://${endpointMatch[1]}`,
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
- // Format 2: s3://bucket/key (AWS S3 default)
97
- // Example: s3://my-bucket/path/to/tiles.pmtiles
98
- const awsMatch = cleanUrl.match(/^s3:\/\/([^/]+)\/(.+)$/);
99
- if (awsMatch) {
100
- return {
101
- endpoint: null, // Use default AWS endpoint
102
- bucket: awsMatch[1],
103
- key: awsMatch[2],
104
- region,
105
- profile,
106
- requestPayer,
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(`Invalid S3 URL format: ${url}`);
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
- if (!isGzipped) {
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';
@@ -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('*', hostParts[0]));
169
+ newParts.unshift(domain.replace(/\*/g, hostParts[0]));
170
170
  newDomains.push(newParts.join('.'));
171
171
  }
172
172
  } else {