@earthmover/icechunk 2.0.0-alpha.11 → 2.0.0-alpha.12
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/README.md +38 -8
- package/fetch-storage.d.ts +20 -0
- package/fetch-storage.js +130 -0
- package/package.json +18 -6
package/README.md
CHANGED
|
@@ -46,14 +46,30 @@ In-memory storage works on all platforms. The following backends are available o
|
|
|
46
46
|
- **HTTP** — `Storage.newHttp(baseUrl, config?)`
|
|
47
47
|
- **Local Filesystem** — `Storage.newLocalFilesystem(path)`
|
|
48
48
|
|
|
49
|
+
### Fetch Storage (read-only, works everywhere)
|
|
50
|
+
|
|
51
|
+
For read-only access to a publicly hosted Icechunk repository, use the built-in fetch-based storage. This works on both native and WASM builds, making it the easiest way to open a repository in the browser:
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { Repository } from '@earthmover/icechunk'
|
|
55
|
+
import { createFetchStorage } from '@earthmover/icechunk/fetch-storage'
|
|
56
|
+
|
|
57
|
+
const storage = createFetchStorage('https://my-bucket.s3.us-west-2.amazonaws.com/my-repo.icechunk')
|
|
58
|
+
const repo = await Repository.open(storage)
|
|
59
|
+
const session = await repo.readonlySession({ branch: 'main' })
|
|
60
|
+
const keys = await session.store.list()
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The repository must be on S3-compatible storage with public read access and XML listing enabled. `createFetchStorage` uses the browser `fetch` API under the hood, so it works in any environment where `fetch` is available.
|
|
64
|
+
|
|
49
65
|
### Custom Storage Backends
|
|
50
66
|
|
|
51
67
|
For WASM builds (or any environment where the built-in backends aren't suitable), you can provide your own storage implementation in JavaScript using `Storage.newCustom()`:
|
|
52
68
|
|
|
53
69
|
```typescript
|
|
54
70
|
const storage = Storage.newCustom({
|
|
55
|
-
canWrite: async () => true,
|
|
56
|
-
getObjectRange: async ({ path, rangeStart, rangeEnd }) => {
|
|
71
|
+
canWrite: async (_err, ) => true,
|
|
72
|
+
getObjectRange: async (_err, { path, rangeStart, rangeEnd }) => {
|
|
57
73
|
const headers: Record<string, string> = {}
|
|
58
74
|
if (rangeStart != null && rangeEnd != null) {
|
|
59
75
|
headers['Range'] = `bytes=${rangeStart}-${rangeEnd - 1}`
|
|
@@ -61,15 +77,17 @@ const storage = Storage.newCustom({
|
|
|
61
77
|
const resp = await fetch(`https://my-bucket.example.com/${path}`, { headers })
|
|
62
78
|
return { data: new Uint8Array(await resp.arrayBuffer()), version: { etag: resp.headers.get('etag') ?? undefined } }
|
|
63
79
|
},
|
|
64
|
-
putObject: async ({ path, data, contentType }) => { /* ... */ },
|
|
65
|
-
copyObject: async ({ from, to }) => { /* ... */ },
|
|
66
|
-
listObjects: async (prefix) => { /* return [{ id, createdAt, sizeBytes }] */ },
|
|
67
|
-
deleteBatch: async ({ prefix, batch }) => { /* return { deletedObjects, deletedBytes } */ },
|
|
68
|
-
getObjectLastModified: async (path) => { /* return Date */ },
|
|
69
|
-
getObjectConditional: async ({ path, previousVersion }) => { /* ... */ },
|
|
80
|
+
putObject: async (_err, { path, data, contentType }) => { /* ... */ },
|
|
81
|
+
copyObject: async (_err, { from, to }) => { /* ... */ },
|
|
82
|
+
listObjects: async (_err, prefix) => { /* return [{ id, createdAt, sizeBytes }] */ },
|
|
83
|
+
deleteBatch: async (_err, { prefix, batch }) => { /* return { deletedObjects, deletedBytes } */ },
|
|
84
|
+
getObjectLastModified: async (_err, path) => { /* return Date */ },
|
|
85
|
+
getObjectConditional: async (_err, { path, previousVersion }) => { /* ... */ },
|
|
70
86
|
})
|
|
71
87
|
```
|
|
72
88
|
|
|
89
|
+
> **Note:** Callbacks use the Node.js error-first convention — the first argument is always `null` (reserved for errors) and the actual arguments follow. Use `_err` to skip it.
|
|
90
|
+
|
|
73
91
|
This is the primary way to use cloud storage in the browser, where native Rust networking is unavailable. Each callback method maps to an operation on the underlying `Storage` trait. See the exported `Storage*` TypeScript interfaces for the full type signatures.
|
|
74
92
|
|
|
75
93
|
### Virtual Chunks
|
|
@@ -84,6 +102,18 @@ Install the package with the `--cpu=wasm32` flag to get the WASM binary:
|
|
|
84
102
|
npm install @earthmover/icechunk --cpu=wasm32
|
|
85
103
|
```
|
|
86
104
|
|
|
105
|
+
To open a public repository in the browser, use fetch storage:
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
import { Repository } from '@earthmover/icechunk'
|
|
109
|
+
import { createFetchStorage } from '@earthmover/icechunk/fetch-storage'
|
|
110
|
+
|
|
111
|
+
const storage = createFetchStorage('https://my-bucket.s3.us-west-2.amazonaws.com/my-repo.icechunk')
|
|
112
|
+
const repo = await Repository.open(storage)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
For more control, use `Storage.newCustom()` to implement your own storage backend with any JS networking library.
|
|
116
|
+
|
|
87
117
|
The WASM build uses `SharedArrayBuffer` for threading, which requires your server to send these headers:
|
|
88
118
|
|
|
89
119
|
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Storage } from '@earthmover/icechunk'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Create a read-only storage backend that fetches objects over HTTP.
|
|
5
|
+
*
|
|
6
|
+
* Works with any publicly accessible icechunk repository hosted on S3-compatible
|
|
7
|
+
* storage. Requires the bucket to support anonymous reads and S3 XML listing.
|
|
8
|
+
*
|
|
9
|
+
* @param baseUrl - Base URL of the icechunk repository (no trailing slash)
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { Repository } from '@earthmover/icechunk'
|
|
14
|
+
* import { createFetchStorage } from '@earthmover/icechunk/fetch-storage'
|
|
15
|
+
*
|
|
16
|
+
* const storage = createFetchStorage('https://my-bucket.s3.us-west-2.amazonaws.com/path/to/repo.icechunk')
|
|
17
|
+
* const repo = await Repository.open(storage)
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare function createFetchStorage(baseUrl: string): Storage
|
package/fetch-storage.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only fetch-based storage backend for Icechunk.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import { Repository } from '@earthmover/icechunk'
|
|
6
|
+
* import { createFetchStorage } from '@earthmover/icechunk/fetch-storage'
|
|
7
|
+
*
|
|
8
|
+
* const storage = createFetchStorage('https://my-bucket.s3.amazonaws.com/my-repo.icechunk')
|
|
9
|
+
* const repo = await Repository.open(storage)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { Storage } = require('@earthmover/icechunk')
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} baseUrl - Base URL of the icechunk repository (no trailing slash)
|
|
16
|
+
* @returns {Storage}
|
|
17
|
+
*/
|
|
18
|
+
function createFetchStorage(baseUrl) {
|
|
19
|
+
// Normalize: strip trailing slash
|
|
20
|
+
const base = baseUrl.replace(/\/+$/, '')
|
|
21
|
+
|
|
22
|
+
function throwForStatus(resp, url) {
|
|
23
|
+
if (resp.ok) return
|
|
24
|
+
if (resp.status === 404) {
|
|
25
|
+
throw new Error(`ObjectNotFound: ${url}`)
|
|
26
|
+
}
|
|
27
|
+
throw new Error(`HTTP ${resp.status}: ${url}`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return Storage.newCustom({
|
|
31
|
+
canWrite: async (_err) => false,
|
|
32
|
+
|
|
33
|
+
getObjectRange: async (_err, { path, rangeStart, rangeEnd }) => {
|
|
34
|
+
const url = `${base}/${path}`
|
|
35
|
+
const headers = {}
|
|
36
|
+
|
|
37
|
+
if (rangeStart != null && rangeEnd != null) {
|
|
38
|
+
headers['Range'] = `bytes=${rangeStart}-${rangeEnd - 1}`
|
|
39
|
+
} else if (rangeStart != null) {
|
|
40
|
+
headers['Range'] = `bytes=${rangeStart}-`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const resp = await fetch(url, { headers })
|
|
44
|
+
throwForStatus(resp, url)
|
|
45
|
+
|
|
46
|
+
const data = new Uint8Array(await resp.arrayBuffer())
|
|
47
|
+
const etag = resp.headers.get('etag') ?? undefined
|
|
48
|
+
return { data, version: { etag } }
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
putObject: async () => {
|
|
52
|
+
throw new Error('Read-only storage: putObject not supported')
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
copyObject: async () => {
|
|
56
|
+
throw new Error('Read-only storage: copyObject not supported')
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
listObjects: async (_err, prefix) => {
|
|
60
|
+
// Try S3-style XML listing
|
|
61
|
+
// Derive bucket URL and key prefix from the base URL
|
|
62
|
+
// e.g. https://bucket.s3.region.amazonaws.com/prefix -> bucket URL + key prefix
|
|
63
|
+
const url = new URL(base)
|
|
64
|
+
const keyPrefix = url.pathname.replace(/^\//, '') + '/' + prefix
|
|
65
|
+
const listUrl = `${url.origin}/?list-type=2&prefix=${encodeURIComponent(keyPrefix)}`
|
|
66
|
+
|
|
67
|
+
const resp = await fetch(listUrl)
|
|
68
|
+
throwForStatus(resp, listUrl)
|
|
69
|
+
|
|
70
|
+
const xml = await resp.text()
|
|
71
|
+
const results = []
|
|
72
|
+
const contentRegex = /<Contents>([\s\S]*?)<\/Contents>/g
|
|
73
|
+
let match
|
|
74
|
+
while ((match = contentRegex.exec(xml)) !== null) {
|
|
75
|
+
const block = match[1]
|
|
76
|
+
const key = block.match(/<Key>(.*?)<\/Key>/)?.[1]
|
|
77
|
+
const lastModified = block.match(/<LastModified>(.*?)<\/LastModified>/)?.[1]
|
|
78
|
+
const size = block.match(/<Size>(.*?)<\/Size>/)?.[1]
|
|
79
|
+
if (key && lastModified && size) {
|
|
80
|
+
const id = key.startsWith(keyPrefix) ? key.slice(keyPrefix.length) : key
|
|
81
|
+
results.push({
|
|
82
|
+
id,
|
|
83
|
+
createdAt: new Date(lastModified),
|
|
84
|
+
sizeBytes: Number(size),
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return results
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
deleteBatch: async () => {
|
|
93
|
+
throw new Error('Read-only storage: deleteBatch not supported')
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
getObjectLastModified: async (_err, path) => {
|
|
97
|
+
const url = `${base}/${path}`
|
|
98
|
+
const resp = await fetch(url, { method: 'HEAD' })
|
|
99
|
+
throwForStatus(resp, url)
|
|
100
|
+
const lastModified = resp.headers.get('last-modified')
|
|
101
|
+
if (!lastModified) {
|
|
102
|
+
throw new Error(`No Last-Modified header: ${url}`)
|
|
103
|
+
}
|
|
104
|
+
return new Date(lastModified)
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
getObjectConditional: async (_err, { path, previousVersion }) => {
|
|
108
|
+
const url = `${base}/${path}`
|
|
109
|
+
const headers = {}
|
|
110
|
+
|
|
111
|
+
if (previousVersion?.etag) {
|
|
112
|
+
headers['If-None-Match'] = previousVersion.etag
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const resp = await fetch(url, { headers })
|
|
116
|
+
|
|
117
|
+
if (resp.status === 304) {
|
|
118
|
+
return { kind: 'on_latest_version' }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
throwForStatus(resp, url)
|
|
122
|
+
|
|
123
|
+
const data = new Uint8Array(await resp.arrayBuffer())
|
|
124
|
+
const etag = resp.headers.get('etag') ?? undefined
|
|
125
|
+
return { kind: 'modified', data, newVersion: { etag } }
|
|
126
|
+
},
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = { createFetchStorage }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@earthmover/icechunk",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.12",
|
|
4
4
|
"description": "JavaScript/TypeScript bindings for Icechunk",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"repository": {
|
|
@@ -19,10 +19,22 @@
|
|
|
19
19
|
"science",
|
|
20
20
|
"geospatial"
|
|
21
21
|
],
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"browser": "./browser.js",
|
|
25
|
+
"default": "./index.js"
|
|
26
|
+
},
|
|
27
|
+
"./fetch-storage": {
|
|
28
|
+
"types": "./fetch-storage.d.ts",
|
|
29
|
+
"default": "./fetch-storage.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
22
32
|
"files": [
|
|
23
33
|
"index.d.ts",
|
|
24
34
|
"index.js",
|
|
25
|
-
"browser.js"
|
|
35
|
+
"browser.js",
|
|
36
|
+
"fetch-storage.js",
|
|
37
|
+
"fetch-storage.d.ts"
|
|
26
38
|
],
|
|
27
39
|
"napi": {
|
|
28
40
|
"binaryName": "icechunk",
|
|
@@ -108,9 +120,9 @@
|
|
|
108
120
|
},
|
|
109
121
|
"packageManager": "yarn@4.12.0",
|
|
110
122
|
"optionalDependencies": {
|
|
111
|
-
"@earthmover/icechunk-win32-x64-msvc": "2.0.0-alpha.
|
|
112
|
-
"@earthmover/icechunk-linux-x64-gnu": "2.0.0-alpha.
|
|
113
|
-
"@earthmover/icechunk-darwin-arm64": "2.0.0-alpha.
|
|
114
|
-
"@earthmover/icechunk-wasm32-wasi": "2.0.0-alpha.
|
|
123
|
+
"@earthmover/icechunk-win32-x64-msvc": "2.0.0-alpha.12",
|
|
124
|
+
"@earthmover/icechunk-linux-x64-gnu": "2.0.0-alpha.12",
|
|
125
|
+
"@earthmover/icechunk-darwin-arm64": "2.0.0-alpha.12",
|
|
126
|
+
"@earthmover/icechunk-wasm32-wasi": "2.0.0-alpha.12"
|
|
115
127
|
}
|
|
116
128
|
}
|