@e280/mammoth 0.1.0-2 → 0.1.0-20
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 +112 -5
- package/package.json +7 -8
- package/s/core/{iceberg-memory.ts → buckets/memory-bucket.ts} +3 -7
- package/s/{browser/iceberg-opfs.ts → core/buckets/opfs-bucket.ts} +5 -11
- package/s/core/consts.ts +8 -0
- package/s/core/index.ts +6 -2
- package/s/core/mammoth.ts +54 -46
- package/s/core/manifest/fns/commit.ts +27 -0
- package/s/core/manifest/fns/get-expired-writes.ts +11 -0
- package/s/core/manifest/fns/get-stats.ts +10 -0
- package/s/core/manifest/fns/move-write-to-trash.ts +11 -0
- package/s/core/manifest/fns/need-info.ts +10 -0
- package/s/core/manifest/fns/schedule-deletion.ts +19 -0
- package/s/core/manifest/manifest.ts +27 -0
- package/s/core/manifest/types.ts +5 -0
- package/s/core/parts/analyze.ts +26 -0
- package/s/core/parts/streamify.ts +18 -0
- package/s/core/types.ts +33 -9
- package/s/core/utils/cleanup.ts +16 -0
- package/s/core/utils/is-expired.ts +9 -0
- package/s/{browser → core}/utils/is-not-found.ts +1 -1
- package/s/core/utils/not-found.ts +7 -0
- package/s/core/utils/save.ts +28 -0
- package/s/node/disk-bucket.ts +55 -0
- package/s/{browser → node}/index.ts +1 -1
- package/s/node/utils/is-not-found.ts +9 -0
- package/s/test.ts +41 -16
- package/x/core/{iceberg-memory.d.ts → buckets/memory-bucket.d.ts} +2 -3
- package/x/core/{iceberg-memory.js → buckets/memory-bucket.js} +3 -6
- package/x/core/buckets/memory-bucket.js.map +1 -0
- package/x/{browser/iceberg-opfs.d.ts → core/buckets/opfs-bucket.d.ts} +2 -3
- package/x/{browser/iceberg-opfs.js → core/buckets/opfs-bucket.js} +5 -10
- package/x/core/buckets/opfs-bucket.js.map +1 -0
- package/x/core/consts.d.ts +4 -0
- package/x/core/consts.js +6 -0
- package/x/core/consts.js.map +1 -0
- package/x/core/index.d.ts +4 -2
- package/x/core/index.js +4 -2
- package/x/core/index.js.map +1 -1
- package/x/core/mammoth.d.ts +7 -5
- package/x/core/mammoth.js +51 -40
- package/x/core/mammoth.js.map +1 -1
- package/x/core/manifest/fns/commit.d.ts +3 -0
- package/x/core/manifest/fns/commit.js +22 -0
- package/x/core/manifest/fns/commit.js.map +1 -0
- package/x/core/manifest/fns/get-expired-writes.d.ts +2 -0
- package/x/core/manifest/fns/get-expired-writes.js +8 -0
- package/x/core/manifest/fns/get-expired-writes.js.map +1 -0
- package/x/core/manifest/fns/get-stats.d.ts +2 -0
- package/x/core/manifest/fns/get-stats.js +5 -0
- package/x/core/manifest/fns/get-stats.js.map +1 -0
- package/x/core/manifest/fns/move-write-to-trash.d.ts +3 -0
- package/x/core/manifest/fns/move-write-to-trash.js +7 -0
- package/x/core/manifest/fns/move-write-to-trash.js.map +1 -0
- package/x/core/manifest/fns/need-info.d.ts +3 -0
- package/x/core/manifest/fns/need-info.js +6 -0
- package/x/core/manifest/fns/need-info.js.map +1 -0
- package/x/core/manifest/fns/schedule-deletion.d.ts +3 -0
- package/x/core/manifest/fns/schedule-deletion.js +13 -0
- package/x/core/manifest/fns/schedule-deletion.js.map +1 -0
- package/x/core/manifest/manifest.d.ts +15 -0
- package/x/core/manifest/manifest.js +19 -0
- package/x/core/manifest/manifest.js.map +1 -0
- package/x/core/manifest/types.d.ts +3 -0
- package/x/core/manifest/types.js +2 -0
- package/x/core/manifest/types.js.map +1 -0
- package/x/core/parts/analyze.d.ts +2 -0
- package/x/core/parts/analyze.js +17 -0
- package/x/core/parts/analyze.js.map +1 -0
- package/x/core/parts/streamify.d.ts +1 -0
- package/x/core/parts/streamify.js +15 -0
- package/x/core/parts/streamify.js.map +1 -0
- package/x/core/types.d.ts +24 -9
- package/x/core/utils/cleanup.d.ts +3 -0
- package/x/core/utils/cleanup.js +11 -0
- package/x/core/utils/cleanup.js.map +1 -0
- package/x/core/utils/is-expired.d.ts +1 -0
- package/x/core/utils/is-expired.js +7 -0
- package/x/core/utils/is-expired.js.map +1 -0
- package/x/core/utils/is-not-found.d.ts +1 -0
- package/x/{browser → core}/utils/is-not-found.js +1 -1
- package/x/core/utils/is-not-found.js.map +1 -0
- package/x/core/utils/not-found.d.ts +2 -0
- package/x/core/utils/not-found.js +4 -0
- package/x/core/utils/not-found.js.map +1 -0
- package/x/core/utils/save.d.ts +2 -0
- package/x/core/utils/save.js +16 -0
- package/x/core/utils/save.js.map +1 -0
- package/x/node/disk-bucket.d.ts +9 -0
- package/x/node/disk-bucket.js +43 -0
- package/x/node/disk-bucket.js.map +1 -0
- package/x/node/index.d.ts +2 -0
- package/x/node/index.js +3 -0
- package/x/node/index.js.map +1 -0
- package/x/node/utils/is-not-found.d.ts +1 -0
- package/x/node/utils/is-not-found.js +6 -0
- package/x/node/utils/is-not-found.js.map +1 -0
- package/x/test.js +39 -16
- package/x/test.js.map +1 -1
- package/s/browser/example.ts +0 -26
- package/s/core/hash-stream.ts +0 -19
- package/x/browser/example.d.ts +0 -1
- package/x/browser/example.js +0 -20
- package/x/browser/example.js.map +0 -1
- package/x/browser/iceberg-opfs.js.map +0 -1
- package/x/browser/index.d.ts +0 -2
- package/x/browser/index.js +0 -3
- package/x/browser/index.js.map +0 -1
- package/x/browser/utils/is-not-found.d.ts +0 -1
- package/x/browser/utils/is-not-found.js.map +0 -1
- package/x/core/hash-stream.d.ts +0 -1
- package/x/core/hash-stream.js +0 -14
- package/x/core/hash-stream.js.map +0 -1
- package/x/core/iceberg-memory.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,13 +1,120 @@
|
|
|
1
1
|
|
|
2
|
-
|
|
2
|
+

|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
# 🦣 mammoth
|
|
5
|
+
> *big files. small api.*
|
|
6
|
+
|
|
7
|
+
**mammoth is a file storage typescript library.** we use it in our web apps to store files like user uploads. it's content-addressed. files are streamed into a bucket, identified by their blake3 hash, and deduplicated. mammoth works the same whether it's backed by a bucket in memory, on disk, in the cloud, or in the browser's opfs. mammoth uses [kv](https://github.com/e280/kv) for bookkeeping.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @e280/mammoth @e280/kv
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import {Mammoth} from "@e280/mammoth"
|
|
15
|
+
|
|
16
|
+
const mammoth = new Mammoth()
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 🦣 mammoth stores files.
|
|
20
|
+
- **write a file,** and you get back its blake3 `hash` and `size` in bytes.
|
|
21
|
+
```ts
|
|
22
|
+
const hash = await mammoth.write(blob.stream())
|
|
23
|
+
```
|
|
24
|
+
- **read a file,** identified by its hash.
|
|
25
|
+
```ts
|
|
26
|
+
const blob = await mammoth.read(hash)
|
|
27
|
+
```
|
|
28
|
+
- **delete a file.**
|
|
29
|
+
```ts
|
|
30
|
+
await mammoth.delete(hash)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 🦣 mammoth is bucket-agnostic.
|
|
34
|
+
- **memory bucket,** for testing.
|
|
35
|
+
```ts
|
|
36
|
+
import {Mammoth, MemoryBucket} from "@e280/mammoth"
|
|
37
|
+
import {Kv} from "@e280/kv"
|
|
38
|
+
|
|
39
|
+
const mammoth = new Mammoth(
|
|
40
|
+
|
|
41
|
+
// bucket for blob storage
|
|
42
|
+
new MemoryBucket(),
|
|
43
|
+
|
|
44
|
+
// kv for metadata bookkeeping
|
|
45
|
+
new Kv(),
|
|
46
|
+
)
|
|
47
|
+
```
|
|
48
|
+
defaults shown, this is equivalent:
|
|
49
|
+
```ts
|
|
50
|
+
const mammoth = new Mammoth()
|
|
51
|
+
```
|
|
52
|
+
- **disk bucket,** for nodejs servers, example using [leveldb](https://github.com/google/leveldb). *(note the import paths)*
|
|
53
|
+
```ts
|
|
54
|
+
import {Mammoth, DiskBucket} from "@e280/mammoth/node"
|
|
55
|
+
import {Kv, LevelMagazine} from "@e280/kv"
|
|
56
|
+
import {Level} from "level"
|
|
57
|
+
|
|
58
|
+
const mammoth = new Mammoth(
|
|
59
|
+
new DiskBucket("./data/mammoth/bucket"),
|
|
60
|
+
new Kv(new LevelMagazine(new Level("./data/mammoth/kv"))),
|
|
61
|
+
)
|
|
62
|
+
```
|
|
63
|
+
- **[opfs](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system) bucket and [indexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) kv,** for clientside in-browser storage.
|
|
64
|
+
```ts
|
|
65
|
+
import {Mammoth, OpfsBucket} from "@e280/mammoth"
|
|
66
|
+
import {Kv, idbOpen, IdbMagazine} from "@e280/kv"
|
|
67
|
+
|
|
68
|
+
const mammoth = new Mammoth(
|
|
69
|
+
new OpfsBucket(await navigator.storage.getDirectory()),
|
|
70
|
+
new Kv(new IdbMagazine(await idbOpen("mammoth"))),
|
|
71
|
+
)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## 🦣 more mammoth methods.
|
|
75
|
+
- **check if a file exists,** get back a boolean.
|
|
76
|
+
```ts
|
|
77
|
+
const exists = await mammoth.has(hash)
|
|
78
|
+
```
|
|
79
|
+
- **get file info,** including `size` in bytes, `added` timestamp, and bucket `id`.
|
|
80
|
+
```ts
|
|
81
|
+
const {size, added, id} = await mammoth.info(hash)
|
|
82
|
+
```
|
|
83
|
+
- **get stats,** for the whole datalake.
|
|
84
|
+
```ts
|
|
85
|
+
await mammoth.stats()
|
|
86
|
+
// {count: 123, size: 123456789}
|
|
87
|
+
```
|
|
88
|
+
- **loop over every file,** with their hashes and infos.
|
|
89
|
+
```ts
|
|
90
|
+
for await (const [hash, info] of mammoth.entries())
|
|
91
|
+
console.log(hash, info)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## 🦣 mammoth-brained tips.
|
|
95
|
+
- **`analyze` is for hashing files.**
|
|
96
|
+
```ts
|
|
97
|
+
import {analyze} from "@e280/mammoth"
|
|
98
|
+
|
|
99
|
+
const {hash, size} = await analyze(blob.stream())
|
|
100
|
+
```
|
|
101
|
+
- **`analyze` can help you avoid unnecessary uploads.**
|
|
102
|
+
```ts
|
|
103
|
+
const {hash} = await analyze(blob.stream())
|
|
104
|
+
|
|
105
|
+
if (!await mammoth.has(hash))
|
|
106
|
+
await mammoth.write(blob.stream())
|
|
107
|
+
```
|
|
108
|
+
- **`streamify` makes a stream for a Uint8Array.**
|
|
109
|
+
```ts
|
|
110
|
+
import {streamify} from "@e280/mammoth"
|
|
111
|
+
|
|
112
|
+
const readable = await streamify(new Uint8Array([0xDE, 0xAD, 0xBE, 0xEF]))
|
|
113
|
+
```
|
|
5
114
|
|
|
6
115
|
|
|
7
116
|
|
|
8
117
|
<br/><br/>
|
|
9
118
|
|
|
10
|
-
|
|
11
|
-
reward us with github stars
|
|
12
|
-
build with us at https://e280.org/ but only if you're cool
|
|
119
|
+
*https://e280.org/*
|
|
13
120
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@e280/mammoth",
|
|
3
|
-
"version": "0.1.0-
|
|
3
|
+
"version": "0.1.0-20",
|
|
4
4
|
"description": "big file storage",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Chase Moskal <chasemoskal@gmail.com>",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"sideEffects": false,
|
|
10
10
|
"exports": {
|
|
11
11
|
".": "./x/core/index.js",
|
|
12
|
-
"./
|
|
12
|
+
"./node": "./x/node/index.js"
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"x",
|
|
@@ -21,10 +21,12 @@
|
|
|
21
21
|
"test": "node x/test.js",
|
|
22
22
|
"count": "find s -path '*/_archive' -prune -o -name '*.ts' -exec wc -l {} +"
|
|
23
23
|
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"@e280/kv": "^0.2.0-5"
|
|
26
|
+
},
|
|
24
27
|
"dependencies": {
|
|
25
|
-
"@
|
|
26
|
-
"@e280/stz": "^0.3.
|
|
27
|
-
"@noble/hashes": "^2.2.0"
|
|
28
|
+
"@awasm/noble": "^0.1.4",
|
|
29
|
+
"@e280/stz": "^0.3.6"
|
|
28
30
|
},
|
|
29
31
|
"devDependencies": {
|
|
30
32
|
"@e280/octo": "^0.1.3",
|
|
@@ -32,9 +34,6 @@
|
|
|
32
34
|
"@types/node": "^26.1.1",
|
|
33
35
|
"typescript": "^7.0.2"
|
|
34
36
|
},
|
|
35
|
-
"allowScripts": {
|
|
36
|
-
"classic-level@3.0.0": true
|
|
37
|
-
},
|
|
38
37
|
"keywords": [],
|
|
39
38
|
"homepage": "https://github.com/e280/mammoth#readme",
|
|
40
39
|
"repository": {
|
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
|
|
2
2
|
import {bytes, got, nap} from "@e280/stz"
|
|
3
|
-
import {
|
|
4
|
-
import {relaxer} from "
|
|
3
|
+
import {Bucket, Id} from "../types.js"
|
|
4
|
+
import {relaxer} from "../utils/relaxer.js"
|
|
5
5
|
|
|
6
|
-
export class
|
|
6
|
+
export class MemoryBucket implements Bucket {
|
|
7
7
|
#map = new Map<string, Uint8Array>()
|
|
8
8
|
|
|
9
9
|
async has(id: Id) {
|
|
10
10
|
return this.#map.has(id)
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
async size(id: Id) {
|
|
14
|
-
return got(this.#map.get(id)).byteLength
|
|
15
|
-
}
|
|
16
|
-
|
|
17
13
|
async delete(id: Id) {
|
|
18
14
|
this.#map.delete(id)
|
|
19
15
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import {Bucket, Id} from "../types.js"
|
|
3
|
+
import {isNotFoundErr} from "../utils/is-not-found.js"
|
|
4
4
|
|
|
5
|
-
export class
|
|
5
|
+
export class OpfsBucket implements Bucket {
|
|
6
6
|
#directory
|
|
7
7
|
|
|
8
8
|
constructor(directory: FileSystemDirectoryHandle) {
|
|
@@ -15,23 +15,17 @@ export class IcebergOpfs implements Iceberg {
|
|
|
15
15
|
return true
|
|
16
16
|
}
|
|
17
17
|
catch (error) {
|
|
18
|
-
if (
|
|
18
|
+
if (isNotFoundErr(error)) return false
|
|
19
19
|
throw error
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
async size(id: Id) {
|
|
24
|
-
const handle = await this.#directory.getFileHandle(id)
|
|
25
|
-
const file = await handle.getFile()
|
|
26
|
-
return file.size
|
|
27
|
-
}
|
|
28
|
-
|
|
29
23
|
async delete(id: Id) {
|
|
30
24
|
try {
|
|
31
25
|
await this.#directory.removeEntry(id)
|
|
32
26
|
}
|
|
33
27
|
catch (error) {
|
|
34
|
-
if (!
|
|
28
|
+
if (!isNotFoundErr(error))
|
|
35
29
|
throw error
|
|
36
30
|
}
|
|
37
31
|
}
|
package/s/core/consts.ts
ADDED
package/s/core/index.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
|
|
2
|
-
export * from "./
|
|
3
|
-
export * from "./
|
|
2
|
+
export * from "./buckets/memory-bucket.js"
|
|
3
|
+
export * from "./buckets/opfs-bucket.js"
|
|
4
|
+
|
|
5
|
+
export * from "./parts/analyze.js"
|
|
6
|
+
export * from "./parts/streamify.js"
|
|
7
|
+
|
|
4
8
|
export * from "./mammoth.js"
|
|
5
9
|
export * from "./types.js"
|
|
6
10
|
|
package/s/core/mammoth.ts
CHANGED
|
@@ -1,72 +1,80 @@
|
|
|
1
1
|
|
|
2
2
|
import {Kv} from "@e280/kv"
|
|
3
|
-
import {
|
|
4
|
-
import {blake3} from "@noble/hashes/blake3.js"
|
|
3
|
+
import {lane, queue} from "@e280/stz"
|
|
5
4
|
|
|
5
|
+
import {consts} from "./consts.js"
|
|
6
|
+
import {save} from "./utils/save.js"
|
|
7
|
+
import {Hash, Bucket} from "./types.js"
|
|
8
|
+
import {cleanup} from "./utils/cleanup.js"
|
|
6
9
|
import {randomId} from "./utils/random-id.js"
|
|
7
|
-
import {
|
|
10
|
+
import {Manifest} from "./manifest/manifest.js"
|
|
11
|
+
import {commit} from "./manifest/fns/commit.js"
|
|
12
|
+
import {needInfo} from "./manifest/fns/need-info.js"
|
|
13
|
+
import {getStats} from "./manifest/fns/get-stats.js"
|
|
14
|
+
import {MemoryBucket} from "./buckets/memory-bucket.js"
|
|
15
|
+
import {scheduleDeletion} from "./manifest/fns/schedule-deletion.js"
|
|
16
|
+
import {moveWriteToTrash} from "./manifest/fns/move-write-to-trash.js"
|
|
8
17
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
#
|
|
18
|
+
/** file storage datalake, content-addressed with blake3 hashes. */
|
|
19
|
+
export class Mammoth {
|
|
20
|
+
#bucket
|
|
21
|
+
#manifest
|
|
22
|
+
#wholesome = lane(consts.max_jobs)
|
|
12
23
|
|
|
13
|
-
constructor(
|
|
14
|
-
this.#
|
|
15
|
-
this.#
|
|
24
|
+
constructor(bucket: Bucket = new MemoryBucket(), kv = new Kv()) {
|
|
25
|
+
this.#bucket = bucket
|
|
26
|
+
this.#manifest = new Manifest(kv)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async* entries() {
|
|
30
|
+
yield* this.#manifest.catalog.entries()
|
|
16
31
|
}
|
|
17
32
|
|
|
18
33
|
async has(hash: Hash) {
|
|
19
|
-
return this.#
|
|
34
|
+
return this.#manifest.catalog.has(hash)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async info(hash: Hash) {
|
|
38
|
+
return needInfo(this.#manifest, hash)
|
|
20
39
|
}
|
|
21
40
|
|
|
22
|
-
async
|
|
23
|
-
|
|
24
|
-
return this.#iceberg.size(id)
|
|
41
|
+
async stats() {
|
|
42
|
+
return getStats(this.#manifest)
|
|
25
43
|
}
|
|
26
44
|
|
|
27
45
|
async read(hash: Hash) {
|
|
28
|
-
const id = await this
|
|
29
|
-
return this.#
|
|
46
|
+
const {id} = await this.info(hash)
|
|
47
|
+
return this.#bucket.read(id)
|
|
30
48
|
}
|
|
31
49
|
|
|
32
50
|
async delete(hash: Hash) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
51
|
+
await this.#wholesome(async() => {
|
|
52
|
+
const info = await this.#manifest.catalog.get(hash)
|
|
53
|
+
if (info)
|
|
54
|
+
await scheduleDeletion(this.#manifest, hash, info)
|
|
55
|
+
})
|
|
56
|
+
await this.#cleanup()
|
|
38
57
|
}
|
|
39
58
|
|
|
40
59
|
async write(readable: ReadableStream<Uint8Array>): Promise<Hash> {
|
|
41
60
|
const id = randomId()
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
61
|
+
await this.#manifest.writes.set(id, {created: Date.now()})
|
|
62
|
+
try {
|
|
63
|
+
const analysis = await save(this.#bucket, id, readable)
|
|
64
|
+
const {hash, size} = analysis
|
|
65
|
+
const info = {id, size, added: Date.now()}
|
|
66
|
+
await this.#wholesome(() => commit(this.#manifest, hash, info))
|
|
67
|
+
return analysis.hash
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
await moveWriteToTrash(this.#manifest, id)
|
|
71
|
+
throw error
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
void this.#cleanup().catch(e => console.error("cleanup failed", e))
|
|
50
75
|
}
|
|
51
|
-
|
|
52
|
-
await writer.close()
|
|
53
|
-
await done
|
|
54
|
-
const hash = hex.fromBytes(hasher.digest())
|
|
55
|
-
await this.delete(hash)
|
|
56
|
-
await this.#index.set(hash, id)
|
|
57
|
-
return hash
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async #getFileId(hash: Hash): Promise<Id | undefined> {
|
|
61
|
-
return this.#index.get<string>(hash)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async #needFileId(hash: Hash): Promise<Id> {
|
|
65
|
-
return got(await this.#getFileId(hash), `file not found by hash "${hash}"`)
|
|
66
76
|
}
|
|
67
77
|
|
|
68
|
-
|
|
69
|
-
yield* this.#index.keys()
|
|
70
|
-
}
|
|
78
|
+
#cleanup = queue(() => cleanup(this.#bucket, this.#manifest), consts.max_jobs)
|
|
71
79
|
}
|
|
72
80
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
|
|
2
|
+
import {getStats} from "./get-stats.js"
|
|
3
|
+
import {Manifest} from "../manifest.js"
|
|
4
|
+
import {Hash, Info} from "../../types.js"
|
|
5
|
+
|
|
6
|
+
export async function commit(manifest: Manifest, hash: Hash, info: Info) {
|
|
7
|
+
const {kv, stats, catalog, writes, trash} = manifest
|
|
8
|
+
const isNewFile = !await catalog.has(hash)
|
|
9
|
+
|
|
10
|
+
if (isNewFile) {
|
|
11
|
+
const s = await getStats(manifest)
|
|
12
|
+
s.count += 1
|
|
13
|
+
s.size += info.size
|
|
14
|
+
await kv.commit([
|
|
15
|
+
writes.op.delete(info.id),
|
|
16
|
+
catalog.op.set(hash, info),
|
|
17
|
+
stats.op.set(s),
|
|
18
|
+
])
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
await kv.commit([
|
|
22
|
+
writes.op.delete(info.id),
|
|
23
|
+
trash.op.set(info.id, true),
|
|
24
|
+
])
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
|
|
2
|
+
import {Manifest} from "../manifest.js"
|
|
3
|
+
import {isExpired} from "../../utils/is-expired.js"
|
|
4
|
+
|
|
5
|
+
export async function* getExpiredWrites({writes}: Manifest) {
|
|
6
|
+
for await (const [id, w] of writes.entries()) {
|
|
7
|
+
if (isExpired(w.created))
|
|
8
|
+
yield id
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
|
|
2
|
+
import {got} from "@e280/stz"
|
|
3
|
+
import {Hash} from "../../types.js"
|
|
4
|
+
import {Manifest} from "../manifest.js"
|
|
5
|
+
import {notFoundMessage} from "../../utils/not-found.js"
|
|
6
|
+
|
|
7
|
+
export async function needInfo({catalog}: Manifest, hash: Hash) {
|
|
8
|
+
return got(await catalog.get(hash), notFoundMessage(hash))
|
|
9
|
+
}
|
|
10
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
|
|
2
|
+
import {getStats} from "./get-stats.js"
|
|
3
|
+
import {Manifest} from "../manifest.js"
|
|
4
|
+
import {Hash, Info} from "../../types.js"
|
|
5
|
+
|
|
6
|
+
export async function scheduleDeletion(manifest: Manifest, hash: Hash, info: Info) {
|
|
7
|
+
const {kv, stats, catalog, trash} = manifest
|
|
8
|
+
|
|
9
|
+
const s = await getStats(manifest)
|
|
10
|
+
s.count -= 1
|
|
11
|
+
s.size -= info.size
|
|
12
|
+
|
|
13
|
+
await kv.commit([
|
|
14
|
+
catalog.op.delete(hash),
|
|
15
|
+
trash.op.set(info.id, true),
|
|
16
|
+
stats.op.set(s),
|
|
17
|
+
])
|
|
18
|
+
}
|
|
19
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
|
|
2
|
+
import {Kv} from "@e280/kv"
|
|
3
|
+
import {Write} from "./types.js"
|
|
4
|
+
import {Info, Stats} from "../types.js"
|
|
5
|
+
|
|
6
|
+
export class Manifest {
|
|
7
|
+
|
|
8
|
+
/** statistics about the whole datalake */
|
|
9
|
+
stats
|
|
10
|
+
|
|
11
|
+
/** associates file hashes with bucket ids */
|
|
12
|
+
catalog
|
|
13
|
+
|
|
14
|
+
/** temporary record of writes in-progress */
|
|
15
|
+
writes
|
|
16
|
+
|
|
17
|
+
/** bucket ids that are pending deletion */
|
|
18
|
+
trash
|
|
19
|
+
|
|
20
|
+
constructor(public kv: Kv) {
|
|
21
|
+
this.stats = kv.store<Stats>("stats")
|
|
22
|
+
this.catalog = kv.scope<Info>("catalog")
|
|
23
|
+
this.writes = kv.scope<Write>("writes")
|
|
24
|
+
this.trash = kv.scope<true>("trash")
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
|
|
2
|
+
import {hex, nap} from "@e280/stz"
|
|
3
|
+
import {blake3} from "@awasm/noble"
|
|
4
|
+
import {Analysis} from "../types.js"
|
|
5
|
+
import {relaxer} from "../utils/relaxer.js"
|
|
6
|
+
|
|
7
|
+
export async function analyze(
|
|
8
|
+
readable: ReadableStream<Uint8Array>
|
|
9
|
+
): Promise<Analysis> {
|
|
10
|
+
|
|
11
|
+
let size = 0
|
|
12
|
+
const hasher = blake3.create()
|
|
13
|
+
const relax = relaxer()
|
|
14
|
+
|
|
15
|
+
for await (const chunk of readable) {
|
|
16
|
+
hasher.update(chunk)
|
|
17
|
+
size += chunk.byteLength
|
|
18
|
+
|
|
19
|
+
if (relax())
|
|
20
|
+
await nap()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const hash = hex.fromBytes(hasher.digest())
|
|
24
|
+
return {hash, size}
|
|
25
|
+
}
|
|
26
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
|
|
2
|
+
export function streamify(bytes: Uint8Array, chunkSize = 64 * 1024) {
|
|
3
|
+
let offset = 0
|
|
4
|
+
|
|
5
|
+
return new ReadableStream<Uint8Array>({
|
|
6
|
+
pull(controller) {
|
|
7
|
+
if (offset >= bytes.length) {
|
|
8
|
+
controller.close()
|
|
9
|
+
return
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const end = Math.min(offset + chunkSize, bytes.length)
|
|
13
|
+
controller.enqueue(bytes.subarray(offset, end))
|
|
14
|
+
offset = end
|
|
15
|
+
},
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
package/s/core/types.ts
CHANGED
|
@@ -2,20 +2,44 @@
|
|
|
2
2
|
export type Hash = string
|
|
3
3
|
export type Id = string
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/** file blob store. */
|
|
6
|
+
export type Bucket = {
|
|
6
7
|
has(id: Id): Promise<boolean>
|
|
7
|
-
size(id: Id): Promise<number>
|
|
8
8
|
delete(id: Id): Promise<void>
|
|
9
9
|
read(id: Id): Promise<Blob>
|
|
10
10
|
write(id: Id, readable: ReadableStream<Uint8Array>): Promise<void>
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
/** report about a file */
|
|
14
|
+
export type Analysis = {
|
|
15
|
+
|
|
16
|
+
/** blake3 hash of the file's contents */
|
|
17
|
+
hash: Hash
|
|
18
|
+
|
|
19
|
+
/** filesize in bytes */
|
|
20
|
+
size: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** metadata for a single file. */
|
|
24
|
+
export type Info = {
|
|
25
|
+
|
|
26
|
+
/** bucket id for this file's data. */
|
|
27
|
+
id: Id
|
|
28
|
+
|
|
29
|
+
/** filesize in bytes. */
|
|
30
|
+
size: number
|
|
31
|
+
|
|
32
|
+
/** when this file was added to the datalake. */
|
|
33
|
+
added: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** statistics for the whole datalake. */
|
|
37
|
+
export type Stats = {
|
|
38
|
+
|
|
39
|
+
/** total number of bytes in the whole datalake. */
|
|
40
|
+
size: number
|
|
41
|
+
|
|
42
|
+
/** total number of files in the datalake. */
|
|
43
|
+
count: number
|
|
20
44
|
}
|
|
21
45
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
|
|
2
|
+
import {Bucket} from "../types.js"
|
|
3
|
+
import {Manifest} from "../manifest/manifest.js"
|
|
4
|
+
import {getExpiredWrites} from "../manifest/fns/get-expired-writes.js"
|
|
5
|
+
import {moveWriteToTrash} from "../manifest/fns/move-write-to-trash.js"
|
|
6
|
+
|
|
7
|
+
export async function cleanup(bucket: Bucket, manifest: Manifest) {
|
|
8
|
+
for await (const id of getExpiredWrites(manifest))
|
|
9
|
+
await moveWriteToTrash(manifest, id)
|
|
10
|
+
|
|
11
|
+
for await (const id of manifest.trash.keys()) {
|
|
12
|
+
await bucket.delete(id)
|
|
13
|
+
await manifest.trash.delete(id)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|