@e280/mammoth 0.1.0-5 → 0.1.0-7
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 +10 -6
- package/package.json +2 -2
- package/s/browser/example.ts +2 -2
- package/s/browser/opfs-bucket.ts +0 -6
- package/s/core/{hash-stream.ts → analyze.ts} +6 -2
- package/s/core/consts.ts +8 -0
- package/s/core/index.ts +2 -2
- package/s/core/mammoth.ts +50 -68
- package/s/core/memory-bucket.ts +0 -4
- package/s/core/types.ts +25 -2
- package/s/core/utils/is-expired.ts +2 -1
- package/s/core/utils/manifest.ts +113 -0
- package/s/core/utils/{save-and-hash.ts → save.ts} +7 -2
- package/s/node/disk-bucket.ts +0 -5
- package/s/test.ts +14 -13
- package/x/browser/example.js +2 -2
- package/x/browser/example.js.map +1 -1
- package/x/browser/opfs-bucket.d.ts +0 -1
- package/x/browser/opfs-bucket.js +0 -5
- package/x/browser/opfs-bucket.js.map +1 -1
- package/x/core/analyze.d.ts +2 -0
- package/x/core/{hash-stream.js → analyze.js} +6 -3
- package/x/core/analyze.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 +2 -2
- package/x/core/index.js +2 -2
- package/x/core/index.js.map +1 -1
- package/x/core/mammoth.d.ts +5 -5
- package/x/core/mammoth.js +46 -61
- package/x/core/mammoth.js.map +1 -1
- package/x/core/memory-bucket.d.ts +0 -1
- package/x/core/memory-bucket.js +0 -3
- package/x/core/memory-bucket.js.map +1 -1
- package/x/core/types.d.ts +18 -2
- package/x/core/utils/is-expired.js +2 -1
- package/x/core/utils/is-expired.js.map +1 -1
- package/x/core/utils/manifest.d.ts +19 -0
- package/x/core/utils/manifest.js +91 -0
- package/x/core/utils/manifest.js.map +1 -0
- package/x/core/utils/save.d.ts +2 -0
- package/x/core/utils/{save-and-hash.js → save.js} +2 -2
- package/x/core/utils/save.js.map +1 -0
- package/x/node/disk-bucket.d.ts +0 -1
- package/x/node/disk-bucket.js +0 -4
- package/x/node/disk-bucket.js.map +1 -1
- package/x/test.js +14 -13
- package/x/test.js.map +1 -1
- package/x/core/hash-stream.d.ts +0 -1
- package/x/core/hash-stream.js.map +0 -1
- package/x/core/utils/save-and-hash.d.ts +0 -5
- package/x/core/utils/save-and-hash.js.map +0 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# 🦣 mammoth
|
|
5
5
|
> *big files. small api.*
|
|
6
6
|
|
|
7
|
-
**mammoth is a
|
|
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.
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
npm install @e280/mammoth
|
|
@@ -17,9 +17,9 @@ const mammoth = new Mammoth()
|
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
### 🦣 mammoth stores files.
|
|
20
|
-
- **write a file,** and you get back its blake3 hash.
|
|
20
|
+
- **write a file,** and you get back its blake3 `hash` and `size` in bytes.
|
|
21
21
|
```ts
|
|
22
|
-
const hash = await mammoth.write(blob.stream())
|
|
22
|
+
const {hash, size} = await mammoth.write(blob.stream())
|
|
23
23
|
```
|
|
24
24
|
- **read a file,** identified by its hash.
|
|
25
25
|
```ts
|
|
@@ -37,11 +37,15 @@ const mammoth = new Mammoth()
|
|
|
37
37
|
import {Kv} from "@e280/kv"
|
|
38
38
|
|
|
39
39
|
const mammoth = new Mammoth(
|
|
40
|
+
|
|
41
|
+
// bucket for blob storage
|
|
40
42
|
new MemoryBucket(),
|
|
43
|
+
|
|
44
|
+
// kv for metadata bookkeeping
|
|
41
45
|
new Kv(),
|
|
42
46
|
)
|
|
43
47
|
```
|
|
44
|
-
defaults shown,
|
|
48
|
+
defaults shown, this is equivalent:
|
|
45
49
|
```ts
|
|
46
50
|
const mammoth = new Mammoth()
|
|
47
51
|
```
|
|
@@ -74,9 +78,9 @@ const mammoth = new Mammoth()
|
|
|
74
78
|
```ts
|
|
75
79
|
const exists = await mammoth.has(hash)
|
|
76
80
|
```
|
|
77
|
-
- **get file
|
|
81
|
+
- **get file info,** including `size` in bytes, `added` timestamp, and bucket `id`.
|
|
78
82
|
```ts
|
|
79
|
-
const size = await mammoth.
|
|
83
|
+
const {size, added, id} = await mammoth.info(hash)
|
|
80
84
|
```
|
|
81
85
|
- **get stats,** for the whole datalake.
|
|
82
86
|
```ts
|
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-7",
|
|
4
4
|
"description": "big file storage",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Chase Moskal <chasemoskal@gmail.com>",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@e280/kv": "^0.1.5",
|
|
27
|
-
"@e280/stz": "^0.3.
|
|
27
|
+
"@e280/stz": "^0.3.5",
|
|
28
28
|
"@noble/hashes": "^2.2.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
package/s/browser/example.ts
CHANGED
|
@@ -19,7 +19,7 @@ export async function example() {
|
|
|
19
19
|
const myFile = new Blob([new Uint8Array([0xDE, 0xAD, 0xBE, 0xEF])])
|
|
20
20
|
|
|
21
21
|
// write a file as an example
|
|
22
|
-
const hash = await mammoth.write(myFile.stream())
|
|
23
|
-
console.log(`mammoth stored file
|
|
22
|
+
const {hash} = await mammoth.write(myFile.stream())
|
|
23
|
+
console.log(`mammoth stored file "${hash}"`)
|
|
24
24
|
}
|
|
25
25
|
|
package/s/browser/opfs-bucket.ts
CHANGED
|
@@ -20,12 +20,6 @@ export class OpfsBucket implements Bucket {
|
|
|
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)
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
|
|
2
2
|
import {hex, nap} from "@e280/stz"
|
|
3
3
|
import {blake3} from "@noble/hashes/blake3.js"
|
|
4
|
+
import {Analysis} from "./types.js"
|
|
4
5
|
import {relaxer} from "./utils/relaxer.js"
|
|
5
6
|
|
|
6
|
-
export async function
|
|
7
|
+
export async function analyze(readable: ReadableStream<Uint8Array>): Promise<Analysis> {
|
|
8
|
+
let size = 0
|
|
7
9
|
const hasher = blake3.create()
|
|
8
10
|
const relax = relaxer()
|
|
9
11
|
|
|
10
12
|
for await (const chunk of readable) {
|
|
11
13
|
hasher.update(chunk)
|
|
14
|
+
size += chunk.byteLength
|
|
12
15
|
|
|
13
16
|
if (relax())
|
|
14
17
|
await nap()
|
|
15
18
|
}
|
|
16
19
|
|
|
17
|
-
|
|
20
|
+
const hash = hex.fromBytes(hasher.digest())
|
|
21
|
+
return {hash, size}
|
|
18
22
|
}
|
|
19
23
|
|
package/s/core/consts.ts
ADDED
package/s/core/index.ts
CHANGED
package/s/core/mammoth.ts
CHANGED
|
@@ -1,100 +1,82 @@
|
|
|
1
1
|
|
|
2
2
|
import {Kv} from "@e280/kv"
|
|
3
|
-
import {
|
|
3
|
+
import {lane, queue} from "@e280/stz"
|
|
4
4
|
|
|
5
|
+
import {consts} from "./consts.js"
|
|
6
|
+
import {save} from "./utils/save.js"
|
|
7
|
+
import {Manifest} from "./utils/manifest.js"
|
|
5
8
|
import {randomId} from "./utils/random-id.js"
|
|
6
|
-
import {notFound} from "./utils/not-found.js"
|
|
7
9
|
import {MemoryBucket} from "./memory-bucket.js"
|
|
8
|
-
import {
|
|
9
|
-
import {saveAndHash} from "./utils/save-and-hash.js"
|
|
10
|
-
import {Hash, Bucket, Id, Stats, Wip} from "./types.js"
|
|
10
|
+
import {Hash, Bucket, Analysis} from "./types.js"
|
|
11
11
|
|
|
12
12
|
/** file storage datalake, content-addressed with blake3 hashes. */
|
|
13
13
|
export class Mammoth {
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#bucket // file blobstore
|
|
14
|
+
#bucket
|
|
15
|
+
#manifest
|
|
16
|
+
#wholesome = lane(consts.max_jobs)
|
|
18
17
|
|
|
19
18
|
constructor(bucket: Bucket = new MemoryBucket(), kv = new Kv()) {
|
|
20
|
-
this.#ids = kv.scope<string>("ids")
|
|
21
|
-
this.#wip = kv.scope<Wip>("wip")
|
|
22
|
-
this.#stats = kv.store<Stats>("stats")
|
|
23
19
|
this.#bucket = bucket
|
|
20
|
+
this.#manifest = new Manifest(kv)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async* hashes() {
|
|
24
|
+
yield* this.#manifest.hashes()
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
async has(hash: Hash) {
|
|
27
|
-
return this.#
|
|
28
|
+
return this.#manifest.hasHash(hash)
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
async
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
async info(hash: Hash) {
|
|
32
|
+
return this.#manifest.needInfo(hash)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async stats() {
|
|
36
|
+
return this.#manifest.getStats()
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
async read(hash: Hash) {
|
|
36
|
-
const id =
|
|
40
|
+
const {id} = await this.#manifest.needInfo(hash)
|
|
37
41
|
return this.#bucket.read(id)
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
async delete(hash: Hash) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
stats.count--
|
|
48
|
-
stats.size -= size
|
|
49
|
-
})
|
|
50
|
-
}
|
|
45
|
+
await this.#wholesome(async() => {
|
|
46
|
+
const info = await this.#manifest.getInfo(hash)
|
|
47
|
+
if (info)
|
|
48
|
+
await this.#manifest.scheduleDeletion(hash, info)
|
|
49
|
+
})
|
|
50
|
+
await this.#cleanup()
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
async write(readable: ReadableStream<Uint8Array>): Promise<
|
|
53
|
+
async write(readable: ReadableStream<Uint8Array>): Promise<Analysis> {
|
|
54
54
|
const id = randomId()
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
return structuredClone((await this.#stats.get()) ?? {count: 0, size: 0})
|
|
55
|
+
await this.#manifest.addWip(id)
|
|
56
|
+
try {
|
|
57
|
+
const analysis = await save(this.#bucket, id, readable)
|
|
58
|
+
const {hash, size} = analysis
|
|
59
|
+
const info = {id, size, added: Date.now()}
|
|
60
|
+
await this.#wholesome(() => this.#manifest.commit(hash, info))
|
|
61
|
+
return analysis
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
await this.#manifest.moveWipToTrash(id)
|
|
65
|
+
throw error
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
void this.#cleanup().catch(e => console.error("cleanup failed", e))
|
|
69
|
+
}
|
|
71
70
|
}
|
|
72
71
|
|
|
73
|
-
#
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
await this.#stats.set(stats)
|
|
77
|
-
})
|
|
72
|
+
#cleanup = queue(async() => {
|
|
73
|
+
for await (const id of this.#manifest.getExpiredWipIds())
|
|
74
|
+
await this.#manifest.moveWipToTrash(id)
|
|
78
75
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
await this.#
|
|
82
|
-
}
|
|
83
|
-
else {
|
|
84
|
-
await this.#ids.set(hash, id)
|
|
85
|
-
await this.#updateStats(stats => {
|
|
86
|
-
stats.count++
|
|
87
|
-
stats.size += size
|
|
88
|
-
})
|
|
76
|
+
for await (const id of this.#manifest.listTrashIds()) {
|
|
77
|
+
await this.#bucket.delete(id)
|
|
78
|
+
await this.#manifest.dropTrashRecord(id)
|
|
89
79
|
}
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
#selfClean = queue(async() => {
|
|
93
|
-
const expiredIds = (await collect(this.#wip.entries({limit: 64})))
|
|
94
|
-
.filter(([,wip]) => isExpired(wip))
|
|
95
|
-
.map(([id]) => id)
|
|
96
|
-
await Promise.all(expiredIds.map(id => this.#bucket.delete(id)))
|
|
97
|
-
await this.#wip.del(...expiredIds)
|
|
98
|
-
})
|
|
80
|
+
}, consts.max_jobs)
|
|
99
81
|
}
|
|
100
82
|
|
package/s/core/memory-bucket.ts
CHANGED
package/s/core/types.ts
CHANGED
|
@@ -5,18 +5,41 @@ export type Id = string
|
|
|
5
5
|
/** file blob store. */
|
|
6
6
|
export type Bucket = {
|
|
7
7
|
has(id: Id): Promise<boolean>
|
|
8
|
-
size(id: Id): Promise<number>
|
|
9
8
|
delete(id: Id): Promise<void>
|
|
10
9
|
read(id: Id): Promise<Blob>
|
|
11
10
|
write(id: Id, readable: ReadableStream<Uint8Array>): Promise<void>
|
|
12
11
|
}
|
|
13
12
|
|
|
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. */
|
|
14
37
|
export type Stats = {
|
|
15
38
|
|
|
16
39
|
/** total number of bytes in the whole datalake. */
|
|
17
40
|
size: number
|
|
18
41
|
|
|
19
|
-
/** total number of
|
|
42
|
+
/** total number of files in the datalake. */
|
|
20
43
|
count: number
|
|
21
44
|
}
|
|
22
45
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
|
|
2
2
|
import {time} from "@e280/stz"
|
|
3
3
|
import {Wip} from "../types.js"
|
|
4
|
+
import {consts} from "../consts.js"
|
|
4
5
|
|
|
5
6
|
export function isExpired({created}: Wip) {
|
|
6
7
|
const since = Date.now() - created
|
|
7
|
-
return since > time.days(
|
|
8
|
+
return since > time.days(consts.expiry_time)
|
|
8
9
|
}
|
|
9
10
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
|
|
2
|
+
import {Kv} from "@e280/kv"
|
|
3
|
+
import {got} from "@e280/stz"
|
|
4
|
+
import {notFound} from "./not-found.js"
|
|
5
|
+
import {isExpired} from "./is-expired.js"
|
|
6
|
+
import {Hash, Id, Info, Stats, Wip} from "../types.js"
|
|
7
|
+
|
|
8
|
+
export class Manifest {
|
|
9
|
+
#kv
|
|
10
|
+
|
|
11
|
+
/** statistics about the whole datalake */
|
|
12
|
+
#stats
|
|
13
|
+
|
|
14
|
+
/** associates file hashes with bucket ids */
|
|
15
|
+
#info
|
|
16
|
+
|
|
17
|
+
/** temporary record of writes in-progress */
|
|
18
|
+
#wip
|
|
19
|
+
|
|
20
|
+
/** statistics about the whole datalake */
|
|
21
|
+
#trash
|
|
22
|
+
|
|
23
|
+
constructor(kv = new Kv()) {
|
|
24
|
+
this.#kv = kv
|
|
25
|
+
this.#stats = kv.store<Stats>("stats")
|
|
26
|
+
this.#info = kv.scope<Info>("info")
|
|
27
|
+
this.#wip = kv.scope<Wip>("wip")
|
|
28
|
+
this.#trash = kv.scope<true>("trash")
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async getStats() {
|
|
32
|
+
return structuredClone((await this.#stats.get()) ?? {count: 0, size: 0})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async* hashes() {
|
|
36
|
+
yield* this.#info.keys()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async hasHash(hash: Hash) {
|
|
40
|
+
return this.#info.has(hash)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async getInfo(hash: Hash) {
|
|
44
|
+
return await this.#info.get(hash)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async needInfo(hash: Hash) {
|
|
48
|
+
return got(await this.getInfo(hash), notFound(hash))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async addWip(id: Id) {
|
|
52
|
+
await this.#wip.set(id, {created: Date.now()})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async deleteWip(...ids: Id[]) {
|
|
56
|
+
await this.#wip.del(...ids)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async* getExpiredWipIds() {
|
|
60
|
+
for await (const [id, wip] of this.#wip.entries()) {
|
|
61
|
+
if (isExpired(wip))
|
|
62
|
+
yield id
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async moveWipToTrash(id: Id) {
|
|
67
|
+
await this.#kv.transaction(() => [
|
|
68
|
+
this.#wip.write.del(id),
|
|
69
|
+
this.#trash.write.set(id, true),
|
|
70
|
+
])
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async dropTrashRecord(id: Id) {
|
|
74
|
+
await this.#trash.del(id)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async* listTrashIds() {
|
|
78
|
+
yield* this.#trash.keys()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async scheduleDeletion(hash: Hash, info: Info) {
|
|
82
|
+
const stats = await this.getStats()
|
|
83
|
+
stats.count -= 1
|
|
84
|
+
stats.size -= info.size
|
|
85
|
+
await this.#kv.transaction(() => [
|
|
86
|
+
this.#info.write.del(hash),
|
|
87
|
+
this.#trash.write.set(info.id, true),
|
|
88
|
+
this.#stats.write.set(stats),
|
|
89
|
+
])
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async commit(hash: Hash, info: Info) {
|
|
93
|
+
const isNewFile = !await this.hasHash(hash)
|
|
94
|
+
|
|
95
|
+
if (isNewFile) {
|
|
96
|
+
const stats = await this.getStats()
|
|
97
|
+
stats.count += 1
|
|
98
|
+
stats.size += info.size
|
|
99
|
+
await this.#kv.transaction(() => [
|
|
100
|
+
this.#wip.write.del(info.id),
|
|
101
|
+
this.#info.write.set(hash, info),
|
|
102
|
+
this.#stats.write.set(stats)
|
|
103
|
+
])
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
await this.#kv.transaction(() => [
|
|
107
|
+
this.#wip.write.del(info.id),
|
|
108
|
+
this.#trash.write.set(info.id, true),
|
|
109
|
+
])
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
|
|
2
2
|
import {hex} from "@e280/stz"
|
|
3
3
|
import {blake3} from "@noble/hashes/blake3.js"
|
|
4
|
-
import {Bucket, Id} from "../types.js"
|
|
4
|
+
import {Analysis, Bucket, Id} from "../types.js"
|
|
5
|
+
|
|
6
|
+
export async function save(
|
|
7
|
+
bucket: Bucket,
|
|
8
|
+
id: Id,
|
|
9
|
+
readable: ReadableStream<Uint8Array>,
|
|
10
|
+
): Promise<Analysis> {
|
|
5
11
|
|
|
6
|
-
export async function saveAndHash(bucket: Bucket, id: Id, readable: ReadableStream<Uint8Array>) {
|
|
7
12
|
let size = 0
|
|
8
13
|
const pipe = new TransformStream()
|
|
9
14
|
const done = bucket.write(id, pipe.readable)
|
package/s/node/disk-bucket.ts
CHANGED
package/s/test.ts
CHANGED
|
@@ -24,7 +24,7 @@ await science.run({
|
|
|
24
24
|
const dataDir = "data"
|
|
25
25
|
await rm(dataDir, {recursive: true, force: true})
|
|
26
26
|
const mammoth = new Mammoth(new DiskBucket(dataDir), new Kv())
|
|
27
|
-
const hash = await mammoth.write(blob().stream())
|
|
27
|
+
const {hash} = await mammoth.write(blob().stream())
|
|
28
28
|
const second = await mammoth.read(hash)
|
|
29
29
|
expect(bytes.eq(await second.bytes(), await blob().bytes()))
|
|
30
30
|
}),
|
|
@@ -32,15 +32,15 @@ await science.run({
|
|
|
32
32
|
"mammoth memory": {
|
|
33
33
|
".write and .read": test(async() => {
|
|
34
34
|
const {mammoth} = setup()
|
|
35
|
-
const hash = await mammoth.write(blob().stream())
|
|
35
|
+
const {hash} = await mammoth.write(blob().stream())
|
|
36
36
|
const second = await mammoth.read(hash)
|
|
37
37
|
expect(bytes.eq(await second.bytes(), await blob().bytes()))
|
|
38
38
|
}),
|
|
39
39
|
|
|
40
40
|
".write deduplication": test(async() => {
|
|
41
41
|
const {mammoth} = setup()
|
|
42
|
-
const hashA = await mammoth.write(blob().stream())
|
|
43
|
-
const hashB = await mammoth.write(blob().stream())
|
|
42
|
+
const {hash: hashA} = await mammoth.write(blob().stream())
|
|
43
|
+
const {hash: hashB} = await mammoth.write(blob().stream())
|
|
44
44
|
const keys = await collect(mammoth.hashes())
|
|
45
45
|
expect(hashA).is(hashB)
|
|
46
46
|
expect(keys.length).is(1)
|
|
@@ -49,8 +49,8 @@ await science.run({
|
|
|
49
49
|
|
|
50
50
|
".write empty file": test(async() => {
|
|
51
51
|
const {mammoth} = setup()
|
|
52
|
-
const hash = await mammoth.write(new Blob().stream())
|
|
53
|
-
expect(await mammoth.
|
|
52
|
+
const {hash} = await mammoth.write(new Blob().stream())
|
|
53
|
+
expect((await mammoth.info(hash)).size).is(0)
|
|
54
54
|
expect((await mammoth.read(hash)).size).is(0)
|
|
55
55
|
}),
|
|
56
56
|
|
|
@@ -65,9 +65,9 @@ await science.run({
|
|
|
65
65
|
".stats": test(async() => {
|
|
66
66
|
const {mammoth} = setup()
|
|
67
67
|
expect(await mammoth.stats()).deep({count: 0, size: 0})
|
|
68
|
-
const hashA = await mammoth.write(quickstream([0xC0, 0xFF, 0xEE]))
|
|
68
|
+
const {hash: hashA} = await mammoth.write(quickstream([0xC0, 0xFF, 0xEE]))
|
|
69
69
|
expect(await mammoth.stats()).deep({count: 1, size: 3})
|
|
70
|
-
const hashB = await mammoth.write(quickstream([0xB0, 0x0B, 0x1E, 0x5]))
|
|
70
|
+
const {hash: hashB} = await mammoth.write(quickstream([0xB0, 0x0B, 0x1E, 0x5]))
|
|
71
71
|
expect(await mammoth.stats()).deep({count: 2, size: 7})
|
|
72
72
|
await mammoth.delete(hashA)
|
|
73
73
|
expect(await mammoth.stats()).deep({count: 1, size: 4})
|
|
@@ -77,20 +77,21 @@ await science.run({
|
|
|
77
77
|
|
|
78
78
|
".has": test(async() => {
|
|
79
79
|
const {mammoth} = setup()
|
|
80
|
-
const hash = await mammoth.write(blob().stream())
|
|
80
|
+
const {hash} = await mammoth.write(blob().stream())
|
|
81
81
|
expect(await mammoth.has(hash)).is(true)
|
|
82
82
|
expect(await mammoth.has(randomId())).is(false)
|
|
83
83
|
}),
|
|
84
84
|
|
|
85
85
|
".size": test(async() => {
|
|
86
86
|
const {mammoth} = setup()
|
|
87
|
-
const
|
|
88
|
-
|
|
87
|
+
const b = blob()
|
|
88
|
+
const {hash} = await mammoth.write(b.stream())
|
|
89
|
+
expect((await mammoth.info(hash)).size).is(b.size)
|
|
89
90
|
}),
|
|
90
91
|
|
|
91
92
|
".delete": test(async() => {
|
|
92
93
|
const {mammoth} = setup()
|
|
93
|
-
const hash = await mammoth.write(blob().stream())
|
|
94
|
+
const {hash} = await mammoth.write(blob().stream())
|
|
94
95
|
await mammoth.delete(hash)
|
|
95
96
|
expect(await mammoth.has(hash)).is(false)
|
|
96
97
|
expect(await collect(mammoth.hashes())).deep([])
|
|
@@ -103,7 +104,7 @@ await science.run({
|
|
|
103
104
|
|
|
104
105
|
".keys": test(async() => {
|
|
105
106
|
const {mammoth} = setup()
|
|
106
|
-
const hash = await mammoth.write(blob().stream())
|
|
107
|
+
const {hash} = await mammoth.write(blob().stream())
|
|
107
108
|
const keys = await collect(mammoth.hashes())
|
|
108
109
|
expect(keys.length).is(1)
|
|
109
110
|
expect(keys[0]).is(hash)
|
package/x/browser/example.js
CHANGED
|
@@ -13,7 +13,7 @@ export async function example() {
|
|
|
13
13
|
const mammoth = new Mammoth(bucket, kv);
|
|
14
14
|
const myFile = new Blob([new Uint8Array([0xDE, 0xAD, 0xBE, 0xEF])]);
|
|
15
15
|
// write a file as an example
|
|
16
|
-
const hash = await mammoth.write(myFile.stream());
|
|
17
|
-
console.log(`mammoth stored file
|
|
16
|
+
const { hash } = await mammoth.write(myFile.stream());
|
|
17
|
+
console.log(`mammoth stored file "${hash}"`);
|
|
18
18
|
}
|
|
19
19
|
//# sourceMappingURL=example.js.map
|
package/x/browser/example.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"example.js","sourceRoot":"","sources":["../../s/browser/example.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,EAAE,EAAE,aAAa,EAAC,MAAM,UAAU,CAAA;AAC1C,OAAO,EAAC,OAAO,EAAC,MAAM,oBAAoB,CAAA;AAC1C,OAAO,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAA;AAE3C,MAAM,CAAC,KAAK,UAAU,OAAO;IAE5B,4BAA4B;IAC5B,MAAM,MAAM,GAAG,IAAI,aAAa,CAAC,MAAM,CAAC,YAAY,CAAC,CAAA;IACrD,MAAM,EAAE,GAAG,IAAI,EAAE,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,CAAA;IAE1C,6BAA6B;IAC7B,MAAM,IAAI,GAAG,MAAM,SAAS,CAAC,OAAO,CAAC,YAAY,EAAE,CAAA;IACnD,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,kBAAkB,CAAC,SAAS,EAAE,EAAC,MAAM,EAAE,IAAI,EAAC,CAAC,CAAA;IAC1E,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,SAAS,CAAC,CAAA;IAExC,oBAAoB;IACpB,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;IACvC,MAAM,MAAM,GAAG,IAAI,IAAI,CAAC,CAAC,IAAI,UAAU,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;IAEnE,6BAA6B;IAC7B,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"example.js","sourceRoot":"","sources":["../../s/browser/example.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,EAAE,EAAE,aAAa,EAAC,MAAM,UAAU,CAAA;AAC1C,OAAO,EAAC,OAAO,EAAC,MAAM,oBAAoB,CAAA;AAC1C,OAAO,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAA;AAE3C,MAAM,CAAC,KAAK,UAAU,OAAO;IAE5B,4BAA4B;IAC5B,MAAM,MAAM,GAAG,IAAI,aAAa,CAAC,MAAM,CAAC,YAAY,CAAC,CAAA;IACrD,MAAM,EAAE,GAAG,IAAI,EAAE,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,CAAA;IAE1C,6BAA6B;IAC7B,MAAM,IAAI,GAAG,MAAM,SAAS,CAAC,OAAO,CAAC,YAAY,EAAE,CAAA;IACnD,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,kBAAkB,CAAC,SAAS,EAAE,EAAC,MAAM,EAAE,IAAI,EAAC,CAAC,CAAA;IAC1E,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,SAAS,CAAC,CAAA;IAExC,oBAAoB;IACpB,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;IACvC,MAAM,MAAM,GAAG,IAAI,IAAI,CAAC,CAAC,IAAI,UAAU,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;IAEnE,6BAA6B;IAC7B,MAAM,EAAC,IAAI,EAAC,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAA;IACnD,OAAO,CAAC,GAAG,CAAC,wBAAwB,IAAI,GAAG,CAAC,CAAA;AAC7C,CAAC"}
|
|
@@ -3,7 +3,6 @@ export declare class OpfsBucket implements Bucket {
|
|
|
3
3
|
#private;
|
|
4
4
|
constructor(directory: FileSystemDirectoryHandle);
|
|
5
5
|
has(id: Id): Promise<boolean>;
|
|
6
|
-
size(id: Id): Promise<number>;
|
|
7
6
|
delete(id: Id): Promise<void>;
|
|
8
7
|
read(id: Id): Promise<File>;
|
|
9
8
|
write(id: Id, readable: ReadableStream<Uint8Array>): Promise<void>;
|
package/x/browser/opfs-bucket.js
CHANGED
|
@@ -15,11 +15,6 @@ export class OpfsBucket {
|
|
|
15
15
|
throw error;
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
-
async size(id) {
|
|
19
|
-
const handle = await this.#directory.getFileHandle(id);
|
|
20
|
-
const file = await handle.getFile();
|
|
21
|
-
return file.size;
|
|
22
|
-
}
|
|
23
18
|
async delete(id) {
|
|
24
19
|
try {
|
|
25
20
|
await this.#directory.removeEntry(id);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"opfs-bucket.js","sourceRoot":"","sources":["../../s/browser/opfs-bucket.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,UAAU,EAAC,MAAM,yBAAyB,CAAA;AAElD,MAAM,OAAO,UAAU;IACtB,UAAU,CAAA;IAEV,YAAY,SAAoC;QAC/C,IAAI,CAAC,UAAU,GAAG,SAAS,CAAA;IAC5B,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,EAAM;QACf,IAAI,CAAC;YACJ,MAAM,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC,CAAA;YACvC,OAAO,IAAI,CAAA;QACZ,CAAC;QACD,OAAO,KAAK,EAAE,CAAC;YACd,IAAI,UAAU,CAAC,KAAK,CAAC;gBAAE,OAAO,KAAK,CAAA;YACnC,MAAM,KAAK,CAAA;QACZ,CAAC;IACF,CAAC;IAED,KAAK,CAAC,
|
|
1
|
+
{"version":3,"file":"opfs-bucket.js","sourceRoot":"","sources":["../../s/browser/opfs-bucket.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,UAAU,EAAC,MAAM,yBAAyB,CAAA;AAElD,MAAM,OAAO,UAAU;IACtB,UAAU,CAAA;IAEV,YAAY,SAAoC;QAC/C,IAAI,CAAC,UAAU,GAAG,SAAS,CAAA;IAC5B,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,EAAM;QACf,IAAI,CAAC;YACJ,MAAM,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC,CAAA;YACvC,OAAO,IAAI,CAAA;QACZ,CAAC;QACD,OAAO,KAAK,EAAE,CAAC;YACd,IAAI,UAAU,CAAC,KAAK,CAAC;gBAAE,OAAO,KAAK,CAAA;YACnC,MAAM,KAAK,CAAA;QACZ,CAAC;IACF,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,EAAM;QAClB,IAAI,CAAC;YACJ,MAAM,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC,CAAA;QACtC,CAAC;QACD,OAAO,KAAK,EAAE,CAAC;YACd,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;gBACrB,MAAM,KAAK,CAAA;QACb,CAAC;IACF,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,EAAM;QAChB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC,CAAA;QACtD,OAAO,MAAM,CAAC,OAAO,EAAE,CAAA;IACxB,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,EAAM,EAAE,QAAoC;QACvD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,EAAE,EAAC,MAAM,EAAE,IAAI,EAAC,CAAC,CAAA;QACtE,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,cAAc,EAAE,CAAA;QAC9C,MAAM,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;IAChC,CAAC;CACD"}
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { hex, nap } from "@e280/stz";
|
|
2
2
|
import { blake3 } from "@noble/hashes/blake3.js";
|
|
3
3
|
import { relaxer } from "./utils/relaxer.js";
|
|
4
|
-
export async function
|
|
4
|
+
export async function analyze(readable) {
|
|
5
|
+
let size = 0;
|
|
5
6
|
const hasher = blake3.create();
|
|
6
7
|
const relax = relaxer();
|
|
7
8
|
for await (const chunk of readable) {
|
|
8
9
|
hasher.update(chunk);
|
|
10
|
+
size += chunk.byteLength;
|
|
9
11
|
if (relax())
|
|
10
12
|
await nap();
|
|
11
13
|
}
|
|
12
|
-
|
|
14
|
+
const hash = hex.fromBytes(hasher.digest());
|
|
15
|
+
return { hash, size };
|
|
13
16
|
}
|
|
14
|
-
//# sourceMappingURL=
|
|
17
|
+
//# sourceMappingURL=analyze.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyze.js","sourceRoot":"","sources":["../../s/core/analyze.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,GAAG,EAAE,GAAG,EAAC,MAAM,WAAW,CAAA;AAClC,OAAO,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAA;AAE9C,OAAO,EAAC,OAAO,EAAC,MAAM,oBAAoB,CAAA;AAE1C,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,QAAoC;IACjE,IAAI,IAAI,GAAG,CAAC,CAAA;IACZ,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,CAAA;IAC9B,MAAM,KAAK,GAAG,OAAO,EAAE,CAAA;IAEvB,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;QACpC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QACpB,IAAI,IAAI,KAAK,CAAC,UAAU,CAAA;QAExB,IAAI,KAAK,EAAE;YACV,MAAM,GAAG,EAAE,CAAA;IACb,CAAC;IAED,MAAM,IAAI,GAAG,GAAG,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAA;IAC3C,OAAO,EAAC,IAAI,EAAE,IAAI,EAAC,CAAA;AACpB,CAAC"}
|