@walkthru-earth/objex 0.1.0 → 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.
Files changed (73) hide show
  1. package/README.md +1 -1
  2. package/dist/components/browser/FileBrowser.svelte +37 -1
  3. package/dist/components/browser/FileRow.svelte +8 -3
  4. package/dist/components/browser/FileTreeSidebar.svelte +1 -3
  5. package/dist/components/layout/AboutSheet.svelte +126 -0
  6. package/dist/components/layout/AboutSheet.svelte.d.ts +6 -0
  7. package/dist/components/layout/ConnectionDialog.svelte +186 -138
  8. package/dist/components/layout/ConnectionDialog.svelte.d.ts +1 -0
  9. package/dist/components/layout/Sidebar.svelte +19 -3
  10. package/dist/components/layout/TabBar.svelte +4 -7
  11. package/dist/components/viewers/CodeViewer.svelte +17 -9
  12. package/dist/components/viewers/ImageViewer.svelte +6 -16
  13. package/dist/components/viewers/MarkdownViewer.svelte +8 -16
  14. package/dist/components/viewers/MediaViewer.svelte +6 -17
  15. package/dist/components/viewers/ModelViewer.svelte +4 -2
  16. package/dist/components/viewers/NotebookViewer.svelte +90 -40
  17. package/dist/components/viewers/PdfViewer.svelte +5 -3
  18. package/dist/components/viewers/RawViewer.svelte +4 -2
  19. package/dist/components/viewers/TableGrid.svelte +3 -2
  20. package/dist/components/viewers/ZarrMapViewer.svelte +334 -40
  21. package/dist/components/viewers/ZarrMapViewer.svelte.d.ts +3 -8
  22. package/dist/components/viewers/ZarrViewer.svelte +459 -178
  23. package/dist/components/viewers/map/AttributeTable.svelte +1 -6
  24. package/dist/components/viewers/pmtiles/PmtilesArchiveView.svelte +2 -6
  25. package/dist/components/viewers/pmtiles/PmtilesTileInspector.svelte +96 -22
  26. package/dist/constants.d.ts +28 -0
  27. package/dist/constants.js +34 -0
  28. package/dist/file-icons/index.js +6 -0
  29. package/dist/i18n/ar.js +34 -0
  30. package/dist/i18n/en.js +34 -0
  31. package/dist/index.d.ts +4 -1
  32. package/dist/index.js +7 -1
  33. package/dist/query/wasm.js +5 -4
  34. package/dist/storage/browser-cloud.d.ts +7 -0
  35. package/dist/storage/browser-cloud.js +74 -7
  36. package/dist/storage/providers.d.ts +53 -0
  37. package/dist/storage/providers.js +318 -0
  38. package/dist/stores/connections.svelte.js +5 -5
  39. package/dist/stores/query-history.svelte.js +4 -5
  40. package/dist/stores/settings.svelte.js +4 -4
  41. package/dist/types.d.ts +2 -2
  42. package/dist/utils/clipboard.d.ts +13 -0
  43. package/dist/utils/clipboard.js +38 -0
  44. package/dist/utils/error.d.ts +8 -0
  45. package/dist/utils/error.js +12 -0
  46. package/dist/utils/format.d.ts +10 -0
  47. package/dist/utils/format.js +22 -0
  48. package/dist/utils/host-detection.js +78 -18
  49. package/dist/utils/notebook.d.ts +59 -0
  50. package/dist/utils/notebook.js +211 -0
  51. package/dist/utils/parquet-metadata.js +1 -1
  52. package/dist/utils/pmtiles-tile.js +2 -1
  53. package/dist/utils/pmtiles.js +2 -1
  54. package/dist/utils/storage-url.d.ts +1 -1
  55. package/dist/utils/storage-url.js +82 -24
  56. package/dist/utils/url-state.js +2 -7
  57. package/dist/utils/url.d.ts +15 -1
  58. package/dist/utils/url.js +45 -19
  59. package/dist/utils/zarr.d.ts +60 -20
  60. package/dist/utils/zarr.js +450 -103
  61. package/package.json +64 -52
  62. package/dist/assets/favicon.svg +0 -17
  63. package/dist/components/CLAUDE.md +0 -44
  64. package/dist/components/viewers/CLAUDE.md +0 -60
  65. package/dist/file-icons/CLAUDE.md +0 -21
  66. package/dist/i18n/CLAUDE.md +0 -19
  67. package/dist/query/CLAUDE.md +0 -22
  68. package/dist/storage/CLAUDE.md +0 -23
  69. package/dist/stores/CLAUDE.md +0 -29
  70. package/dist/types/notebookjs.d.ts +0 -14
  71. package/dist/utils/CLAUDE.md +0 -54
  72. package/dist/utils/analytics.d.ts +0 -10
  73. package/dist/utils/analytics.js +0 -38
@@ -7,6 +7,7 @@
7
7
  *
8
8
  * Also extracts `rootPrefix` when the app is hosted inside a subfolder.
9
9
  */
10
+ import { buildProviderBaseUrl } from '../storage/providers.js';
10
11
  import { parseStorageUrl } from './storage-url.js';
11
12
  /**
12
13
  * Extract root prefix from pathname.
@@ -28,20 +29,8 @@ function extractRootPrefix(pathname) {
28
29
  /**
29
30
  * Build a normalized API endpoint URL for a detected provider.
30
31
  */
31
- function buildBucketUrl(provider, endpoint, bucket) {
32
- if (endpoint) {
33
- return `${endpoint.replace(/\/$/, '')}/${bucket}`;
34
- }
35
- switch (provider) {
36
- case 'gcs':
37
- return `https://storage.googleapis.com/${bucket}`;
38
- case 'azure':
39
- return `${endpoint}/${bucket}`;
40
- case 'storj':
41
- return `https://gateway.storjshare.io/${bucket}`;
42
- default:
43
- return `https://s3.us-east-1.amazonaws.com/${bucket}`;
44
- }
32
+ function buildBucketUrl(provider, endpoint, bucket, region) {
33
+ return buildProviderBaseUrl((provider === 'unknown' ? 's3' : provider), endpoint, bucket, region || '');
45
34
  }
46
35
  /**
47
36
  * Detect hosting bucket from current URL.
@@ -178,7 +167,7 @@ export function detectHostBucket() {
178
167
  const doSpaces = host.match(/^(.+)\.([a-z0-9-]+)\.digitaloceanspaces\.com$/);
179
168
  if (doSpaces) {
180
169
  return {
181
- provider: 's3',
170
+ provider: 'digitalocean',
182
171
  bucket: doSpaces[1],
183
172
  region: doSpaces[2],
184
173
  endpoint: `https://${doSpaces[2]}.digitaloceanspaces.com`,
@@ -190,7 +179,7 @@ export function detectHostBucket() {
190
179
  const doCdn = host.match(/^(.+)\.([a-z0-9-]+)\.cdn\.digitaloceanspaces\.com$/);
191
180
  if (doCdn) {
192
181
  return {
193
- provider: 's3',
182
+ provider: 'digitalocean',
194
183
  bucket: doCdn[1],
195
184
  region: doCdn[2],
196
185
  endpoint: `https://${doCdn[2]}.digitaloceanspaces.com`,
@@ -204,7 +193,7 @@ export function detectHostBucket() {
204
193
  const parts = pathname.replace(/^\//, '').split('/').filter(Boolean);
205
194
  if (parts.length > 0) {
206
195
  return {
207
- provider: 's3',
196
+ provider: 'wasabi',
208
197
  bucket: parts[0],
209
198
  region: wasabi[1],
210
199
  endpoint: `https://${host}`,
@@ -217,7 +206,7 @@ export function detectHostBucket() {
217
206
  const b2s3 = host.match(/^(.+)\.s3\.([a-z0-9-]+)\.backblazeb2\.com$/);
218
207
  if (b2s3) {
219
208
  return {
220
- provider: 's3',
209
+ provider: 'b2',
221
210
  bucket: b2s3[1],
222
211
  region: b2s3[2],
223
212
  endpoint: `https://s3.${b2s3[2]}.backblazeb2.com`,
@@ -267,6 +256,77 @@ export function detectHostBucket() {
267
256
  };
268
257
  }
269
258
  }
259
+ // Contabo: <region>.contabostorage.com/<bucket>
260
+ const contabo = host.match(/^([a-z0-9]+)\.contabostorage\.com$/);
261
+ if (contabo) {
262
+ const parts = pathname.replace(/^\//, '').split('/').filter(Boolean);
263
+ if (parts.length > 0) {
264
+ return {
265
+ provider: 'contabo',
266
+ bucket: parts[0],
267
+ region: contabo[1],
268
+ endpoint: `${url.protocol}//${url.host}`,
269
+ rootPrefix: parts.length > 1 ? extractRootPrefix(`/${parts.slice(1).join('/')}`) : '',
270
+ bucketUrl: `${url.protocol}//${url.host}/${parts[0]}`
271
+ };
272
+ }
273
+ }
274
+ // Hetzner: <region>.your-objectstorage.com/<bucket>
275
+ const hetzner = host.match(/^([a-z0-9]+)\.your-objectstorage\.com$/);
276
+ if (hetzner) {
277
+ const parts = pathname.replace(/^\//, '').split('/').filter(Boolean);
278
+ if (parts.length > 0) {
279
+ return {
280
+ provider: 'hetzner',
281
+ bucket: parts[0],
282
+ region: hetzner[1],
283
+ endpoint: `${url.protocol}//${url.host}`,
284
+ rootPrefix: parts.length > 1 ? extractRootPrefix(`/${parts.slice(1).join('/')}`) : '',
285
+ bucketUrl: `${url.protocol}//${url.host}/${parts[0]}`
286
+ };
287
+ }
288
+ }
289
+ // Linode / Akamai: <bucket>.<region>.linodeobjects.com or <region>.linodeobjects.com/<bucket>
290
+ const linodeVhost = host.match(/^(.+)\.([a-z0-9-]+)\.linodeobjects\.com$/);
291
+ if (linodeVhost) {
292
+ return {
293
+ provider: 'linode',
294
+ bucket: linodeVhost[1],
295
+ region: linodeVhost[2],
296
+ endpoint: `https://${linodeVhost[2]}.linodeobjects.com`,
297
+ rootPrefix: extractRootPrefix(pathname),
298
+ bucketUrl: `https://${linodeVhost[2]}.linodeobjects.com/${linodeVhost[1]}`
299
+ };
300
+ }
301
+ const linodePath = host.match(/^([a-z0-9-]+)\.linodeobjects\.com$/);
302
+ if (linodePath) {
303
+ const parts = pathname.replace(/^\//, '').split('/').filter(Boolean);
304
+ if (parts.length > 0) {
305
+ return {
306
+ provider: 'linode',
307
+ bucket: parts[0],
308
+ region: linodePath[1],
309
+ endpoint: `https://${url.host}`,
310
+ rootPrefix: parts.length > 1 ? extractRootPrefix(`/${parts.slice(1).join('/')}`) : '',
311
+ bucketUrl: `https://${url.host}/${parts[0]}`
312
+ };
313
+ }
314
+ }
315
+ // OVHcloud: s3.<region>.io.cloud.ovh.net/<bucket>
316
+ const ovh = host.match(/^s3\.([a-z0-9-]+)\.io\.cloud\.ovh\.(?:net|us)$/);
317
+ if (ovh) {
318
+ const parts = pathname.replace(/^\//, '').split('/').filter(Boolean);
319
+ if (parts.length > 0) {
320
+ return {
321
+ provider: 'ovhcloud',
322
+ bucket: parts[0],
323
+ region: ovh[1],
324
+ endpoint: `https://${url.host}`,
325
+ rootPrefix: parts.length > 1 ? extractRootPrefix(`/${parts.slice(1).join('/')}`) : '',
326
+ bucketUrl: `https://${url.host}/${parts[0]}`
327
+ };
328
+ }
329
+ }
270
330
  // MinIO / localhost / private IPs
271
331
  const isLocal = host === 'localhost' ||
272
332
  host === '127.0.0.1' ||
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Lightweight browser-native Jupyter notebook renderer.
3
+ * Replaces `notebookjs` which depends on jsdom/Buffer (Node.js only).
4
+ * Handles nbformat 2, 3, 4, and 5.
5
+ */
6
+ export interface NotebookConfig {
7
+ markdown: (md: string) => string;
8
+ ansi: (text: string) => string;
9
+ highlighter: (code: string, lang: string) => string;
10
+ }
11
+ interface RawNotebook {
12
+ nbformat: number;
13
+ nbformat_minor?: number;
14
+ metadata?: Record<string, any>;
15
+ cells?: RawCell[];
16
+ worksheets?: {
17
+ cells: RawCell[];
18
+ }[];
19
+ }
20
+ interface RawCell {
21
+ cell_type: string;
22
+ source?: string | string[];
23
+ input?: string | string[];
24
+ outputs?: RawOutput[];
25
+ prompt_number?: number;
26
+ execution_count?: number | null;
27
+ level?: number;
28
+ language?: string;
29
+ }
30
+ interface RawOutput {
31
+ output_type: string;
32
+ data?: Record<string, string | string[]>;
33
+ text?: string | string[];
34
+ stream?: string;
35
+ name?: string;
36
+ png?: string;
37
+ jpeg?: string;
38
+ svg?: string;
39
+ html?: string;
40
+ latex?: string;
41
+ traceback?: string[];
42
+ ename?: string;
43
+ evalue?: string;
44
+ [key: string]: any;
45
+ }
46
+ export interface NotebookMeta {
47
+ kernelName: string;
48
+ language: string;
49
+ cellCount: number;
50
+ }
51
+ /**
52
+ * Parse and render a Jupyter notebook JSON to a DOM element.
53
+ * Returns the rendered element and metadata.
54
+ */
55
+ export declare function renderNotebook(raw: RawNotebook, config: NotebookConfig): {
56
+ element: HTMLElement;
57
+ meta: NotebookMeta;
58
+ };
59
+ export {};
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Lightweight browser-native Jupyter notebook renderer.
3
+ * Replaces `notebookjs` which depends on jsdom/Buffer (Node.js only).
4
+ * Handles nbformat 2, 3, 4, and 5.
5
+ */
6
+ const PREFIX = 'nb-';
7
+ // ── Helpers ──────────────────────────────────────────────────────────────────
8
+ function el(tag, classNames = []) {
9
+ const e = document.createElement(tag);
10
+ if (classNames.length)
11
+ e.className = classNames.map((c) => PREFIX + c).join(' ');
12
+ return e;
13
+ }
14
+ function escapeHTML(raw) {
15
+ return raw.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
16
+ }
17
+ function joinText(text) {
18
+ if (Array.isArray(text))
19
+ return text.join('');
20
+ return text ?? '';
21
+ }
22
+ // ── Display renderers ────────────────────────────────────────────────────────
23
+ const IMAGE_FORMATS = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];
24
+ function renderImage(format, data) {
25
+ const img = el('img', ['image-output']);
26
+ img.src = `data:${format};base64,${joinText(data).replace(/\n/g, '')}`;
27
+ return img;
28
+ }
29
+ function renderDisplayData(output, config) {
30
+ // Resolve data from v4 `data` field or v3 flat fields
31
+ const getData = (mime) => output.data?.[mime] ?? output[mime];
32
+ // Priority order (richest first)
33
+ for (const fmt of IMAGE_FORMATS) {
34
+ const d = getData(fmt);
35
+ if (d)
36
+ return renderImage(fmt, d);
37
+ }
38
+ const svg = getData('image/svg+xml') ?? getData('text/svg+xml');
39
+ if (svg) {
40
+ const wrapper = el('div', ['svg-output']);
41
+ wrapper.innerHTML = joinText(svg);
42
+ return wrapper;
43
+ }
44
+ const html = getData('text/html');
45
+ if (html) {
46
+ const wrapper = el('div', ['html-output']);
47
+ wrapper.innerHTML = joinText(html);
48
+ return wrapper;
49
+ }
50
+ const md = getData('text/markdown');
51
+ if (md) {
52
+ const wrapper = el('div', ['html-output']);
53
+ wrapper.innerHTML = config.markdown(joinText(md));
54
+ return wrapper;
55
+ }
56
+ const latex = getData('text/latex');
57
+ if (latex) {
58
+ const wrapper = el('div', ['latex-output']);
59
+ wrapper.textContent = joinText(latex);
60
+ return wrapper;
61
+ }
62
+ const plain = getData('text/plain');
63
+ if (plain) {
64
+ const pre = el('pre', ['text-output']);
65
+ pre.innerHTML = config.ansi(escapeHTML(joinText(plain)));
66
+ return pre;
67
+ }
68
+ return el('div', ['empty-output']);
69
+ }
70
+ function renderStream(output, config) {
71
+ const streamName = output.stream ?? output.name ?? 'stdout';
72
+ const pre = el('pre', [streamName]);
73
+ pre.innerHTML = config.ansi(escapeHTML(joinText(output.text ?? '')));
74
+ return pre;
75
+ }
76
+ function renderError(output, config) {
77
+ const pre = el('pre', ['pyerr']);
78
+ const raw = (output.traceback ?? []).join('\n');
79
+ pre.innerHTML = config.ansi(escapeHTML(raw));
80
+ return pre;
81
+ }
82
+ // ── Output coalescing (merge consecutive same-name streams) ──────────────────
83
+ function coalesceStreams(outputs) {
84
+ if (!outputs.length)
85
+ return outputs;
86
+ const result = [{ ...outputs[0] }];
87
+ for (let i = 1; i < outputs.length; i++) {
88
+ const o = outputs[i];
89
+ const last = result[result.length - 1];
90
+ if (o.output_type === 'stream' &&
91
+ last.output_type === 'stream' &&
92
+ (o.stream ?? o.name) === (last.stream ?? last.name)) {
93
+ // Merge text
94
+ const lastText = Array.isArray(last.text) ? last.text : [last.text ?? ''];
95
+ const oText = Array.isArray(o.text) ? o.text : [o.text ?? ''];
96
+ last.text = lastText.concat(oText);
97
+ }
98
+ else {
99
+ result.push({ ...o });
100
+ }
101
+ }
102
+ return result;
103
+ }
104
+ // ── Cell renderers ───────────────────────────────────────────────────────────
105
+ function renderCodeCell(cell, lang, config) {
106
+ const cellEl = el('div', ['cell', 'code-cell']);
107
+ // Input
108
+ const source = joinText(cell.source ?? cell.input ?? '');
109
+ if (source) {
110
+ const holder = el('div', ['input']);
111
+ const num = cell.prompt_number ?? cell.execution_count;
112
+ if (typeof num === 'number' && num > -1) {
113
+ holder.setAttribute('data-prompt-number', String(num));
114
+ }
115
+ const pre = el('pre');
116
+ const code = document.createElement('code');
117
+ code.setAttribute('data-language', lang);
118
+ code.className = `lang-${lang}`;
119
+ code.innerHTML = config.highlighter(escapeHTML(source), lang);
120
+ pre.appendChild(code);
121
+ holder.appendChild(pre);
122
+ cellEl.appendChild(holder);
123
+ }
124
+ // Outputs
125
+ const rawOutputs = coalesceStreams(cell.outputs ?? []);
126
+ for (const output of rawOutputs) {
127
+ const outer = el('div', ['output']);
128
+ let inner;
129
+ switch (output.output_type) {
130
+ case 'display_data':
131
+ case 'execute_result':
132
+ case 'pyout':
133
+ inner = renderDisplayData(output, config);
134
+ break;
135
+ case 'stream':
136
+ inner = renderStream(output, config);
137
+ break;
138
+ case 'error':
139
+ case 'pyerr':
140
+ inner = renderError(output, config);
141
+ break;
142
+ default:
143
+ inner = el('div', ['empty-output']);
144
+ }
145
+ outer.appendChild(inner);
146
+ cellEl.appendChild(outer);
147
+ }
148
+ return cellEl;
149
+ }
150
+ function renderMarkdownCell(cell, config) {
151
+ const cellEl = el('div', ['cell', 'markdown-cell']);
152
+ const source = joinText(cell.source ?? cell.input ?? '');
153
+ cellEl.innerHTML = config.markdown(source);
154
+ return cellEl;
155
+ }
156
+ function renderHeadingCell(cell) {
157
+ const level = Math.min(Math.max(cell.level ?? 1, 1), 6);
158
+ const heading = el(`h${level}`, ['cell', 'heading-cell']);
159
+ heading.innerHTML = escapeHTML(joinText(cell.source ?? cell.input ?? ''));
160
+ return heading;
161
+ }
162
+ function renderRawCell(cell) {
163
+ const cellEl = el('div', ['cell', 'raw-cell']);
164
+ cellEl.innerHTML = escapeHTML(joinText(cell.source ?? cell.input ?? ''));
165
+ return cellEl;
166
+ }
167
+ /**
168
+ * Parse and render a Jupyter notebook JSON to a DOM element.
169
+ * Returns the rendered element and metadata.
170
+ */
171
+ export function renderNotebook(raw, config) {
172
+ const meta = raw.metadata ?? {};
173
+ const lang = meta.kernelspec?.language ?? meta.language_info?.name ?? meta.language ?? 'python';
174
+ const kernelName = meta.kernelspec?.display_name ?? meta.language_info?.name ?? '';
175
+ // Normalize cells: v4+ has top-level `cells`, v2/v3 uses `worksheets`
176
+ let allCells = [];
177
+ if (Array.isArray(raw.cells)) {
178
+ allCells = raw.cells;
179
+ }
180
+ else if (Array.isArray(raw.worksheets)) {
181
+ for (const ws of raw.worksheets) {
182
+ if (Array.isArray(ws.cells))
183
+ allCells.push(...ws.cells);
184
+ }
185
+ }
186
+ const notebook = el('div', ['notebook']);
187
+ for (const cell of allCells) {
188
+ let rendered;
189
+ switch (cell.cell_type) {
190
+ case 'code':
191
+ rendered = renderCodeCell(cell, cell.language ?? lang, config);
192
+ break;
193
+ case 'markdown':
194
+ rendered = renderMarkdownCell(cell, config);
195
+ break;
196
+ case 'heading':
197
+ rendered = renderHeadingCell(cell);
198
+ break;
199
+ case 'raw':
200
+ rendered = renderRawCell(cell);
201
+ break;
202
+ default:
203
+ rendered = renderRawCell(cell);
204
+ }
205
+ notebook.appendChild(rendered);
206
+ }
207
+ return {
208
+ element: notebook,
209
+ meta: { kernelName, language: lang, cellCount: allCells.length }
210
+ };
211
+ }
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * This provides instant metadata display before DuckDB-WASM finishes loading.
8
8
  */
9
- const WGS84_CODES = new Set([4326, 4979]);
9
+ import { WGS84_CODES } from '../constants.js';
10
10
  /** Map hyparquet schema element types to DuckDB-like type strings. */
11
11
  function mapParquetType(col) {
12
12
  const lt = col.logical_type;
@@ -5,6 +5,7 @@
5
5
  * vector tiles into a structure suitable for SVG rendering and
6
6
  * feature inspection.
7
7
  */
8
+ import { LAYER_HUE_MULTIPLIER } from '../constants.js';
8
9
  const GEOM_TYPES = ['Unknown', 'Point', 'LineString', 'Polygon'];
9
10
  /**
10
11
  * Fetch and decode an MVT tile from a PMTiles archive.
@@ -60,5 +61,5 @@ export function tileMimeType(format) {
60
61
  }
61
62
  /** Compute the hue for layer index i (same palette as buildPmtilesLayers). */
62
63
  export function layerHue(i) {
63
- return (i * 137) % 360;
64
+ return (i * LAYER_HUE_MULTIPLIER) % 360;
64
65
  }
@@ -1,4 +1,5 @@
1
1
  import { PMTiles, Protocol } from 'pmtiles';
2
+ import { LAYER_HUE_MULTIPLIER } from '../constants.js';
2
3
  let protocol = null;
3
4
  export function getPmtilesProtocol() {
4
5
  if (!protocol) {
@@ -96,7 +97,7 @@ export function buildPmtilesLayers(sourceId, metadata) {
96
97
  const layers = [];
97
98
  for (let i = 0; i < metadata.layers.length; i++) {
98
99
  const layerId = metadata.layers[i];
99
- const hue = (i * 137) % 360;
100
+ const hue = (i * LAYER_HUE_MULTIPLIER) % 360;
100
101
  layers.push({
101
102
  id: `${layerId}-fill`,
102
103
  type: 'fill',
@@ -35,7 +35,7 @@
35
35
  *
36
36
  * Also handles plain bucket names (no protocol).
37
37
  */
38
- export type StorageProvider = 's3' | 'gcs' | 'r2' | 'minio' | 'azure' | 'storj' | 'unknown';
38
+ export type StorageProvider = string;
39
39
  export interface ParsedStorageUrl {
40
40
  bucket: string;
41
41
  region: string;
@@ -35,25 +35,25 @@
35
35
  *
36
36
  * Also handles plain bucket names (no protocol).
37
37
  */
38
- /** All recognized URI scheme prefixes (lowercase) */
39
- const SCHEME_MAP = {
40
- 's3://': { provider: 's3', strip: 5 },
41
- 's3a://': { provider: 's3', strip: 6 },
42
- 's3n://': { provider: 's3', strip: 6 },
43
- 'aws://': { provider: 's3', strip: 6 },
44
- 'r2://': { provider: 'r2', strip: 5 },
45
- 'gs://': { provider: 'gcs', strip: 5 },
46
- 'gcs://': { provider: 'gcs', strip: 6 },
47
- 'azure://': { provider: 'azure', strip: 8 },
48
- 'az://': { provider: 'azure', strip: 5 },
49
- 'abfs://': { provider: 'azure', strip: 7 },
50
- 'abfss://': { provider: 'azure', strip: 8 },
51
- 'wasbs://': { provider: 'azure', strip: 8 },
52
- 'adl://': { provider: 'azure', strip: 6 },
53
- 'storj://': { provider: 'storj', strip: 8 },
54
- 'sj://': { provider: 'storj', strip: 5 },
55
- 'swift://': { provider: 'unknown', strip: 8 }
56
- };
38
+ import { PROVIDERS } from '../storage/providers.js';
39
+ /**
40
+ * Build SCHEME_MAP from the provider registry's `schemes` arrays.
41
+ * Each scheme like "s3" generates an entry `"s3://": { provider: "s3", strip: 5 }`.
42
+ */
43
+ function buildSchemeMap() {
44
+ const map = {};
45
+ for (const [id, def] of Object.entries(PROVIDERS)) {
46
+ for (const scheme of def.schemes) {
47
+ const key = `${scheme}://`;
48
+ map[key] = { provider: id, strip: key.length };
49
+ }
50
+ }
51
+ // Non-registry schemes (no corresponding provider)
52
+ map['swift://'] = { provider: 'unknown', strip: 8 };
53
+ return map;
54
+ }
55
+ /** All recognized URI scheme prefixes (lowercase), derived from provider registry */
56
+ const SCHEME_MAP = buildSchemeMap();
57
57
  function defaultResult(defaults) {
58
58
  return {
59
59
  bucket: '',
@@ -174,7 +174,7 @@ export function parseStorageUrl(input, defaults = {}) {
174
174
  bucket: doVhost[1],
175
175
  region: doVhost[2],
176
176
  endpoint: `${url.protocol}//${doVhost[2]}.digitaloceanspaces.com`,
177
- provider: 's3',
177
+ provider: 'digitalocean',
178
178
  prefix: pathParts.join('/')
179
179
  };
180
180
  }
@@ -185,7 +185,7 @@ export function parseStorageUrl(input, defaults = {}) {
185
185
  bucket: pathParts[0],
186
186
  region: doPath[1],
187
187
  endpoint: `${url.protocol}//${url.host}`,
188
- provider: 's3',
188
+ provider: 'digitalocean',
189
189
  prefix: pathParts.slice(1).join('/')
190
190
  };
191
191
  }
@@ -197,7 +197,7 @@ export function parseStorageUrl(input, defaults = {}) {
197
197
  bucket: pathParts[0],
198
198
  region: wasabiMatch[1],
199
199
  endpoint: `${url.protocol}//${url.host}`,
200
- provider: 's3',
200
+ provider: 'wasabi',
201
201
  prefix: pathParts.slice(1).join('/')
202
202
  };
203
203
  }
@@ -209,7 +209,7 @@ export function parseStorageUrl(input, defaults = {}) {
209
209
  bucket: b2S3[1],
210
210
  region: b2S3[2],
211
211
  endpoint: `${url.protocol}//s3.${b2S3[2]}.backblazeb2.com`,
212
- provider: 's3',
212
+ provider: 'b2',
213
213
  prefix: pathParts.join('/')
214
214
  };
215
215
  }
@@ -220,7 +220,7 @@ export function parseStorageUrl(input, defaults = {}) {
220
220
  bucket: pathParts[1],
221
221
  region: defaults.region || 'us-west-000',
222
222
  endpoint: `${url.protocol}//${url.host}`,
223
- provider: 's3',
223
+ provider: 'b2',
224
224
  prefix: pathParts.slice(2).join('/')
225
225
  };
226
226
  }
@@ -259,6 +259,64 @@ export function parseStorageUrl(input, defaults = {}) {
259
259
  prefix: pathParts.slice(1).join('/')
260
260
  };
261
261
  }
262
+ // --- Contabo ---
263
+ // <region>.contabostorage.com/<bucket>
264
+ const contaboMatch = host.match(/^([a-z0-9]+)\.contabostorage\.com$/);
265
+ if (contaboMatch && pathParts.length > 0) {
266
+ return {
267
+ bucket: pathParts[0],
268
+ region: contaboMatch[1],
269
+ endpoint: `${url.protocol}//${url.host}`,
270
+ provider: 'contabo',
271
+ prefix: pathParts.slice(1).join('/')
272
+ };
273
+ }
274
+ // --- Hetzner ---
275
+ // <region>.your-objectstorage.com/<bucket>
276
+ const hetznerMatch = host.match(/^([a-z0-9]+)\.your-objectstorage\.com$/);
277
+ if (hetznerMatch && pathParts.length > 0) {
278
+ return {
279
+ bucket: pathParts[0],
280
+ region: hetznerMatch[1],
281
+ endpoint: `${url.protocol}//${url.host}`,
282
+ provider: 'hetzner',
283
+ prefix: pathParts.slice(1).join('/')
284
+ };
285
+ }
286
+ // --- Linode / Akamai ---
287
+ // <bucket>.<region>.linodeobjects.com or <region>.linodeobjects.com/<bucket>
288
+ const linodeVhost = host.match(/^(.+)\.([a-z0-9-]+)\.linodeobjects\.com$/);
289
+ if (linodeVhost) {
290
+ return {
291
+ bucket: linodeVhost[1],
292
+ region: linodeVhost[2],
293
+ endpoint: `${url.protocol}//${linodeVhost[2]}.linodeobjects.com`,
294
+ provider: 'linode',
295
+ prefix: pathParts.join('/')
296
+ };
297
+ }
298
+ const linodePath = host.match(/^([a-z0-9-]+)\.linodeobjects\.com$/);
299
+ if (linodePath && pathParts.length > 0) {
300
+ return {
301
+ bucket: pathParts[0],
302
+ region: linodePath[1],
303
+ endpoint: `${url.protocol}//${url.host}`,
304
+ provider: 'linode',
305
+ prefix: pathParts.slice(1).join('/')
306
+ };
307
+ }
308
+ // --- OVHcloud ---
309
+ // s3.<region>.io.cloud.ovh.net/<bucket>
310
+ const ovhMatch = host.match(/^s3\.([a-z0-9-]+)\.io\.cloud\.ovh\.(?:net|us)$/);
311
+ if (ovhMatch && pathParts.length > 0) {
312
+ return {
313
+ bucket: pathParts[0],
314
+ region: ovhMatch[1],
315
+ endpoint: `${url.protocol}//${url.host}`,
316
+ provider: 'ovhcloud',
317
+ prefix: pathParts.slice(1).join('/')
318
+ };
319
+ }
262
320
  // --- MinIO ---
263
321
  // Common patterns: minio.<domain>, localhost with port
264
322
  const isMinioLike = host.includes('minio') ||
@@ -10,18 +10,13 @@
10
10
  * Uses SvelteKit's replaceState to avoid conflicts with the router.
11
11
  */
12
12
  import { replaceState } from '$app/navigation';
13
+ import { buildProviderBaseUrl } from '../storage/providers.js';
13
14
  import { parseStorageUrl } from './storage-url.js';
14
15
  /**
15
16
  * Build the base HTTPS URL for a connection (endpoint + bucket).
16
17
  */
17
18
  function buildBaseUrl(conn) {
18
- if (conn.endpoint) {
19
- return `${conn.endpoint.replace(/\/$/, '')}/${conn.bucket}`;
20
- }
21
- if (conn.provider === 'gcs') {
22
- return `https://storage.googleapis.com/${conn.bucket}`;
23
- }
24
- return `https://s3.${conn.region || 'us-east-1'}.amazonaws.com/${conn.bucket}`;
19
+ return buildProviderBaseUrl(conn.provider, conn.endpoint, conn.bucket, conn.region);
25
20
  }
26
21
  /**
27
22
  * Build a full storage URL from a Connection + optional object prefix.
@@ -4,7 +4,11 @@ import type { Tab } from '../types.js';
4
4
  * Works for any viewer that needs an HTTP-accessible URL (COG, PMTiles, Zarr, etc.)
5
5
  */
6
6
  export declare function buildHttpsUrl(tab: Tab): string;
7
- /** Map provider to its native URI scheme prefix. */
7
+ /**
8
+ * Map provider to its native URI scheme prefix.
9
+ * Derived from the registry's `schemes` array (first entry is the primary scheme).
10
+ * Falls back to 's3' for providers without a scheme (S3-compatible).
11
+ */
8
12
  export declare function getNativeScheme(provider: string): string;
9
13
  /**
10
14
  * Build a provider-native protocol URL (s3://bucket/path, sj://bucket/path, etc.).
@@ -25,3 +29,13 @@ export declare function buildDuckDbUrl(tab: Tab): string;
25
29
  * False for authenticated S3 (needs signed URLs or blob download via adapter).
26
30
  */
27
31
  export declare function canStreamDirectly(tab: Tab): boolean;
32
+ /**
33
+ * Convert a cloud storage protocol URL (s3://, gs://) to an HTTPS URL
34
+ * for browser access. Returns the original URL if already HTTP(S) or unknown.
35
+ *
36
+ * Supported:
37
+ * - `s3://bucket/key` → `https://s3.{region}.amazonaws.com/{bucket}/{key}`
38
+ * (region auto-detected from bucket name when possible, e.g. "us-west-2.opendata.source.coop")
39
+ * - `gs://bucket/key` → `https://storage.googleapis.com/{bucket}/{key}`
40
+ */
41
+ export declare function resolveCloudUrl(url: string): string;