@e280/quay 0.0.0-2 → 0.0.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 +28 -0
- package/package.json +1 -1
- package/s/index.ts +4 -0
- package/s/logic/presets/media/schema.ts +3 -0
- package/s/logic/presets/media/store.test.ts +41 -0
- package/s/logic/presets/media/store.ts +163 -0
- package/s/tests.test.ts +3 -0
- package/x/index.d.ts +4 -0
- package/x/index.html +2 -2
- package/x/index.js +4 -0
- package/x/index.js.map +1 -1
- package/x/logic/presets/media/schema.d.ts +3 -0
- package/x/logic/presets/media/store.d.ts +75 -0
- package/x/logic/presets/media/store.js +129 -0
- package/x/logic/presets/media/store.js.map +1 -0
- package/x/logic/presets/media/store.test.d.ts +7 -0
- package/x/logic/presets/media/store.test.js +31 -0
- package/x/logic/presets/media/store.test.js.map +1 -0
- package/x/tests.test.d.ts +1 -1
- package/x/tests.test.js +3 -0
- package/x/tests.test.js.map +1 -1
package/README.md
CHANGED
|
@@ -115,3 +115,31 @@ new Quay.Brain({
|
|
|
115
115
|
|
|
116
116
|
In this way, you can setup sophisticated rules about what actions are permitted, and under whatever changing circumstances.
|
|
117
117
|
|
|
118
|
+
<br/>
|
|
119
|
+
|
|
120
|
+
## MediaStore
|
|
121
|
+
|
|
122
|
+
`MediaStore` is a small persistent media-bin preset.
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import {MediaStore, brain, register, components} from "@e280/quay"
|
|
126
|
+
|
|
127
|
+
const media = await MediaStore.open("my-project")
|
|
128
|
+
brain.setGroup("media", media)
|
|
129
|
+
|
|
130
|
+
register(components)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
```html
|
|
134
|
+
<div group="media">
|
|
135
|
+
<quay-dropzone></quay-dropzone>
|
|
136
|
+
<quay-browser></quay-browser>
|
|
137
|
+
</div>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The scope passed to `open()` separates media libraries.
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
const projectA = await MediaStore.open("project-a")
|
|
144
|
+
const projectB = await MediaStore.open("project-b")
|
|
145
|
+
```
|
package/package.json
CHANGED
package/s/index.ts
CHANGED
|
@@ -4,6 +4,10 @@ export {register, css} from "@benev/slate"
|
|
|
4
4
|
export * from "./cellar/index.js"
|
|
5
5
|
|
|
6
6
|
export * from "./dom/components.js"
|
|
7
|
+
export {brain} from "./dom/context.js"
|
|
8
|
+
export * from "./logic/presets/media/schema.js"
|
|
9
|
+
export * from "./logic/presets/media/group.js"
|
|
10
|
+
export * from "./logic/presets/media/store.js"
|
|
7
11
|
import {Permissions} from "./logic/permissions.js"
|
|
8
12
|
|
|
9
13
|
const Quay = {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
|
|
2
|
+
import {Txt} from "@e280/stz"
|
|
3
|
+
import {expect, Science, test} from "@e280/science"
|
|
4
|
+
|
|
5
|
+
import {MediaStore} from "./store.js"
|
|
6
|
+
|
|
7
|
+
delete (globalThis as any).localStorage
|
|
8
|
+
|
|
9
|
+
export default Science.suite({
|
|
10
|
+
"imports files into cellar and index": test(async() => {
|
|
11
|
+
const group = new MediaStore()
|
|
12
|
+
const file = new File([Txt.toBytes("hello")], "hello.txt", {type: "text/plain"})
|
|
13
|
+
|
|
14
|
+
const [record] = await group.importFiles([file])
|
|
15
|
+
|
|
16
|
+
expect(await group.cellar.has(record.hash)).is(true)
|
|
17
|
+
expect(group.findByHash(record.hash)?.specimen.label).is("hello.txt")
|
|
18
|
+
}),
|
|
19
|
+
|
|
20
|
+
"lists media records": test(async() => {
|
|
21
|
+
const store = new MediaStore()
|
|
22
|
+
const file = new File([Txt.toBytes("image")], "image.png", {type: "image/png"})
|
|
23
|
+
const record = await store.importFile(file)
|
|
24
|
+
const records = []
|
|
25
|
+
for await (const record of store.records())
|
|
26
|
+
records.push(record)
|
|
27
|
+
|
|
28
|
+
expect(records.some(r => r.hash === record.hash)).is(true)
|
|
29
|
+
}),
|
|
30
|
+
|
|
31
|
+
"removes media records and bytes": test(async() => {
|
|
32
|
+
const store = new MediaStore()
|
|
33
|
+
const file = new File([Txt.toBytes("image")], "image.png", {type: "image/png"})
|
|
34
|
+
const record = await store.importFile(file)
|
|
35
|
+
|
|
36
|
+
await store.remove(record.hash)
|
|
37
|
+
|
|
38
|
+
expect(await store.cellar.has(record.hash)).is(false)
|
|
39
|
+
expect(store.findByHash(record.hash)).is(undefined)
|
|
40
|
+
}),
|
|
41
|
+
})
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
|
|
2
|
+
import {Kv, StorageDriver} from "@e280/kv"
|
|
3
|
+
|
|
4
|
+
import {Cellar} from "../../../cellar/cellar.js"
|
|
5
|
+
import {MediaFormat} from "./schema.js"
|
|
6
|
+
import {MediaGroup} from "./group.js"
|
|
7
|
+
|
|
8
|
+
export type MediaRecord = {
|
|
9
|
+
hash: string
|
|
10
|
+
label: string
|
|
11
|
+
format: MediaFormat
|
|
12
|
+
mime: string
|
|
13
|
+
size: number
|
|
14
|
+
createdAt: number
|
|
15
|
+
updatedAt: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class MediaStore extends MediaGroup {
|
|
19
|
+
static async open(scope = "default") {
|
|
20
|
+
const group = new this()
|
|
21
|
+
group.cellar = await Cellar.opfs("media")
|
|
22
|
+
group.#index = mediaIndex(scope)
|
|
23
|
+
await group.#load()
|
|
24
|
+
return group
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
#objectUrls = new Map<string, string>()
|
|
28
|
+
#index: Kv<MediaRecord>
|
|
29
|
+
|
|
30
|
+
cellar = new Cellar()
|
|
31
|
+
|
|
32
|
+
constructor() {
|
|
33
|
+
super()
|
|
34
|
+
this.#index = mediaIndex("default")
|
|
35
|
+
this.on.upload.sub(({files, target}) => {
|
|
36
|
+
void this.importFiles(files, target)
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async *records() {
|
|
41
|
+
for await (const [, record] of this.#index.entries())
|
|
42
|
+
yield record
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async #load() {
|
|
46
|
+
for await (const record of this.records()) {
|
|
47
|
+
if (record.format === "image" && !this.#objectUrls.has(record.hash))
|
|
48
|
+
await this.#loadPreview(record)
|
|
49
|
+
this.#attachRecord(record, this.config.root)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async importFiles(files: File[], parent = this.config.root) {
|
|
54
|
+
return Promise.all(files.map(f => this.importFile(f, parent)))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async importFile(file: File, parent = this.config.root) {
|
|
58
|
+
const bytes = new Uint8Array(await file.arrayBuffer())
|
|
59
|
+
const cask = await this.cellar.save(bytes)
|
|
60
|
+
const existing = await this.#index.get(cask.hash)
|
|
61
|
+
const now = Date.now()
|
|
62
|
+
const record: MediaRecord = {
|
|
63
|
+
hash: cask.hash,
|
|
64
|
+
label: existing?.label ?? file.name,
|
|
65
|
+
format: mediaFormat(file.type),
|
|
66
|
+
mime: file.type,
|
|
67
|
+
size: file.size,
|
|
68
|
+
createdAt: existing?.createdAt ?? now,
|
|
69
|
+
updatedAt: now,
|
|
70
|
+
}
|
|
71
|
+
await this.#index.set(record.hash, record)
|
|
72
|
+
if (record.format === "image")
|
|
73
|
+
this.#setPreview(record.hash, URL.createObjectURL(file))
|
|
74
|
+
this.#attachRecord(record, parent)
|
|
75
|
+
return record
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async remove(hash: string) {
|
|
79
|
+
await this.#index.del(hash)
|
|
80
|
+
await this.cellar.delete(hash)
|
|
81
|
+
const item = this.findByHash(hash)
|
|
82
|
+
if (item) {
|
|
83
|
+
item.detach()
|
|
84
|
+
item.destroy()
|
|
85
|
+
}
|
|
86
|
+
this.#revokePreview(hash)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
findByHash(hash: string) {
|
|
90
|
+
for (const [item] of this.config.root.crawl()) {
|
|
91
|
+
if (item.isKind("file") && item.specimen.hash === hash)
|
|
92
|
+
return item
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
dispose() {
|
|
97
|
+
for (const url of this.#objectUrls.values())
|
|
98
|
+
URL.revokeObjectURL(url)
|
|
99
|
+
this.#objectUrls.clear()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
#attachRecord(record: MediaRecord, parent = this.config.root) {
|
|
103
|
+
const existing = this.findByHash(record.hash)
|
|
104
|
+
if (existing)
|
|
105
|
+
return existing
|
|
106
|
+
|
|
107
|
+
const item = this.config.codex.create("file", {
|
|
108
|
+
hash: record.hash,
|
|
109
|
+
label: record.label,
|
|
110
|
+
format: record.format,
|
|
111
|
+
mime: record.mime,
|
|
112
|
+
size: record.size,
|
|
113
|
+
previewUrl: this.#objectUrls.get(record.hash) ?? null,
|
|
114
|
+
})
|
|
115
|
+
parent.attach(item)
|
|
116
|
+
return item
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async #loadPreview(record: MediaRecord) {
|
|
120
|
+
const cask = await this.cellar.load(record.hash)
|
|
121
|
+
const blob = new Blob([cask.bytes], {type: record.mime || "image/*"})
|
|
122
|
+
return this.#setPreview(record.hash, URL.createObjectURL(blob))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#setPreview(hash: string, url: string) {
|
|
126
|
+
this.#revokePreview(hash)
|
|
127
|
+
this.#objectUrls.set(hash, url)
|
|
128
|
+
return url
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
#revokePreview(hash: string) {
|
|
132
|
+
const url = this.#objectUrls.get(hash)
|
|
133
|
+
if (url) {
|
|
134
|
+
URL.revokeObjectURL(url)
|
|
135
|
+
this.#objectUrls.delete(hash)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const mediaFormats = new Set(["video", "image", "audio"])
|
|
141
|
+
const memoryIndexes = new Map<string, Kv<MediaRecord>>()
|
|
142
|
+
|
|
143
|
+
function mediaFormat(mime: string): MediaFormat {
|
|
144
|
+
const [type] = mime.split("/")
|
|
145
|
+
return mediaFormats.has(type) ? type as MediaFormat : "other"
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function mediaIndex(scope: string) {
|
|
149
|
+
const storage = globalThis.localStorage
|
|
150
|
+
if (storage)
|
|
151
|
+
return new Kv<MediaRecord>(new StorageDriver(storage))
|
|
152
|
+
.namespace("quay.media")
|
|
153
|
+
.namespace(scope)
|
|
154
|
+
|
|
155
|
+
const existing = memoryIndexes.get(scope)
|
|
156
|
+
if (existing)
|
|
157
|
+
return existing
|
|
158
|
+
|
|
159
|
+
const index = new Kv<MediaRecord>()
|
|
160
|
+
memoryIndexes.set(scope, index)
|
|
161
|
+
return index
|
|
162
|
+
}
|
|
163
|
+
|
package/s/tests.test.ts
CHANGED
package/x/index.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
export { register, css } from "@benev/slate";
|
|
2
2
|
export * from "./cellar/index.js";
|
|
3
3
|
export * from "./dom/components.js";
|
|
4
|
+
export { brain } from "./dom/context.js";
|
|
5
|
+
export * from "./logic/presets/media/schema.js";
|
|
6
|
+
export * from "./logic/presets/media/group.js";
|
|
7
|
+
export * from "./logic/presets/media/store.js";
|
|
4
8
|
declare const Quay: {
|
|
5
9
|
permissions: {
|
|
6
10
|
all: import("./logic/permissions.js").Permission;
|
package/x/index.html
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<link rel="stylesheet" href="demo/main.css?v=c15dc8e4"/>
|
|
11
11
|
|
|
12
12
|
<link rel="icon" href="/assets/favicon.png"/>
|
|
13
|
-
<meta data-commit-hash="
|
|
13
|
+
<meta data-commit-hash="46d801819c2f1f3a00bffaa2e49b894e77f56786"/>
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
<meta name="theme-color" content="#eb6f1d">
|
|
@@ -123,7 +123,7 @@
|
|
|
123
123
|
<img alt="" src="/assets/favicon.png"/>
|
|
124
124
|
<div>
|
|
125
125
|
<h1>Quay</h1>
|
|
126
|
-
<span class=version>v0.0.0-
|
|
126
|
+
<span class=version>v0.0.0-7</span>
|
|
127
127
|
</div>
|
|
128
128
|
</header>
|
|
129
129
|
|
package/x/index.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
export { register, css } from "@benev/slate";
|
|
2
2
|
export * from "./cellar/index.js";
|
|
3
3
|
export * from "./dom/components.js";
|
|
4
|
+
export { brain } from "./dom/context.js";
|
|
5
|
+
export * from "./logic/presets/media/schema.js";
|
|
6
|
+
export * from "./logic/presets/media/group.js";
|
|
7
|
+
export * from "./logic/presets/media/store.js";
|
|
4
8
|
import { Permissions } from "./logic/permissions.js";
|
|
5
9
|
const Quay = {
|
|
6
10
|
permissions: Permissions,
|
package/x/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../s/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,QAAQ,EAAE,GAAG,EAAC,MAAM,cAAc,CAAA;AAE1C,cAAc,mBAAmB,CAAA;AAEjC,cAAc,qBAAqB,CAAA;AACnC,OAAO,EAAC,WAAW,EAAC,MAAM,wBAAwB,CAAA;AAElD,MAAM,IAAI,GAAG;IACZ,WAAW,EAAE,WAAW;CACxB,CAAA;AAED,eAAe,IAAI,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../s/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,QAAQ,EAAE,GAAG,EAAC,MAAM,cAAc,CAAA;AAE1C,cAAc,mBAAmB,CAAA;AAEjC,cAAc,qBAAqB,CAAA;AACnC,OAAO,EAAC,KAAK,EAAC,MAAM,kBAAkB,CAAA;AACtC,cAAc,iCAAiC,CAAA;AAC/C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,gCAAgC,CAAA;AAC9C,OAAO,EAAC,WAAW,EAAC,MAAM,wBAAwB,CAAA;AAElD,MAAM,IAAI,GAAG;IACZ,WAAW,EAAE,WAAW;CACxB,CAAA;AAED,eAAe,IAAI,CAAA"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Cellar } from "../../../cellar/cellar.js";
|
|
2
|
+
import { MediaFormat } from "./schema.js";
|
|
3
|
+
import { MediaGroup } from "./group.js";
|
|
4
|
+
export type MediaRecord = {
|
|
5
|
+
hash: string;
|
|
6
|
+
label: string;
|
|
7
|
+
format: MediaFormat;
|
|
8
|
+
mime: string;
|
|
9
|
+
size: number;
|
|
10
|
+
createdAt: number;
|
|
11
|
+
updatedAt: number;
|
|
12
|
+
};
|
|
13
|
+
export declare class MediaStore extends MediaGroup {
|
|
14
|
+
#private;
|
|
15
|
+
static open(scope?: string): Promise<MediaStore>;
|
|
16
|
+
cellar: Cellar;
|
|
17
|
+
constructor();
|
|
18
|
+
records(): AsyncGenerator<MediaRecord, void, unknown>;
|
|
19
|
+
importFiles(files: File[], parent?: import("../../aspects/codex/parts/codex-item.js").CodexItem<{
|
|
20
|
+
taxon: {
|
|
21
|
+
icon: import("@benev/slate").Content;
|
|
22
|
+
};
|
|
23
|
+
specimens: {
|
|
24
|
+
folder: {
|
|
25
|
+
label: string;
|
|
26
|
+
};
|
|
27
|
+
file: {
|
|
28
|
+
label: string;
|
|
29
|
+
format: MediaFormat;
|
|
30
|
+
previewUrl: string | null;
|
|
31
|
+
hash?: string;
|
|
32
|
+
mime?: string;
|
|
33
|
+
size?: number;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
}, "file" | "folder">): Promise<MediaRecord[]>;
|
|
37
|
+
importFile(file: File, parent?: import("../../aspects/codex/parts/codex-item.js").CodexItem<{
|
|
38
|
+
taxon: {
|
|
39
|
+
icon: import("@benev/slate").Content;
|
|
40
|
+
};
|
|
41
|
+
specimens: {
|
|
42
|
+
folder: {
|
|
43
|
+
label: string;
|
|
44
|
+
};
|
|
45
|
+
file: {
|
|
46
|
+
label: string;
|
|
47
|
+
format: MediaFormat;
|
|
48
|
+
previewUrl: string | null;
|
|
49
|
+
hash?: string;
|
|
50
|
+
mime?: string;
|
|
51
|
+
size?: number;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
}, "file" | "folder">): Promise<MediaRecord>;
|
|
55
|
+
remove(hash: string): Promise<void>;
|
|
56
|
+
findByHash(hash: string): import("../../aspects/codex/parts/codex-item.js").CodexItem<{
|
|
57
|
+
taxon: {
|
|
58
|
+
icon: import("@benev/slate").Content;
|
|
59
|
+
};
|
|
60
|
+
specimens: {
|
|
61
|
+
folder: {
|
|
62
|
+
label: string;
|
|
63
|
+
};
|
|
64
|
+
file: {
|
|
65
|
+
label: string;
|
|
66
|
+
format: MediaFormat;
|
|
67
|
+
previewUrl: string | null;
|
|
68
|
+
hash?: string;
|
|
69
|
+
mime?: string;
|
|
70
|
+
size?: number;
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
}, "file"> | undefined;
|
|
74
|
+
dispose(): void;
|
|
75
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { Kv, StorageDriver } from "@e280/kv";
|
|
2
|
+
import { Cellar } from "../../../cellar/cellar.js";
|
|
3
|
+
import { MediaGroup } from "./group.js";
|
|
4
|
+
export class MediaStore extends MediaGroup {
|
|
5
|
+
static async open(scope = "default") {
|
|
6
|
+
const group = new this();
|
|
7
|
+
group.cellar = await Cellar.opfs("media");
|
|
8
|
+
group.#index = mediaIndex(scope);
|
|
9
|
+
await group.#load();
|
|
10
|
+
return group;
|
|
11
|
+
}
|
|
12
|
+
#objectUrls = new Map();
|
|
13
|
+
#index;
|
|
14
|
+
cellar = new Cellar();
|
|
15
|
+
constructor() {
|
|
16
|
+
super();
|
|
17
|
+
this.#index = mediaIndex("default");
|
|
18
|
+
this.on.upload.sub(({ files, target }) => {
|
|
19
|
+
void this.importFiles(files, target);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
async *records() {
|
|
23
|
+
for await (const [, record] of this.#index.entries())
|
|
24
|
+
yield record;
|
|
25
|
+
}
|
|
26
|
+
async #load() {
|
|
27
|
+
for await (const record of this.records()) {
|
|
28
|
+
if (record.format === "image" && !this.#objectUrls.has(record.hash))
|
|
29
|
+
await this.#loadPreview(record);
|
|
30
|
+
this.#attachRecord(record, this.config.root);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async importFiles(files, parent = this.config.root) {
|
|
34
|
+
return Promise.all(files.map(f => this.importFile(f, parent)));
|
|
35
|
+
}
|
|
36
|
+
async importFile(file, parent = this.config.root) {
|
|
37
|
+
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
38
|
+
const cask = await this.cellar.save(bytes);
|
|
39
|
+
const existing = await this.#index.get(cask.hash);
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
const record = {
|
|
42
|
+
hash: cask.hash,
|
|
43
|
+
label: existing?.label ?? file.name,
|
|
44
|
+
format: mediaFormat(file.type),
|
|
45
|
+
mime: file.type,
|
|
46
|
+
size: file.size,
|
|
47
|
+
createdAt: existing?.createdAt ?? now,
|
|
48
|
+
updatedAt: now,
|
|
49
|
+
};
|
|
50
|
+
await this.#index.set(record.hash, record);
|
|
51
|
+
if (record.format === "image")
|
|
52
|
+
this.#setPreview(record.hash, URL.createObjectURL(file));
|
|
53
|
+
this.#attachRecord(record, parent);
|
|
54
|
+
return record;
|
|
55
|
+
}
|
|
56
|
+
async remove(hash) {
|
|
57
|
+
await this.#index.del(hash);
|
|
58
|
+
await this.cellar.delete(hash);
|
|
59
|
+
const item = this.findByHash(hash);
|
|
60
|
+
if (item) {
|
|
61
|
+
item.detach();
|
|
62
|
+
item.destroy();
|
|
63
|
+
}
|
|
64
|
+
this.#revokePreview(hash);
|
|
65
|
+
}
|
|
66
|
+
findByHash(hash) {
|
|
67
|
+
for (const [item] of this.config.root.crawl()) {
|
|
68
|
+
if (item.isKind("file") && item.specimen.hash === hash)
|
|
69
|
+
return item;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
dispose() {
|
|
73
|
+
for (const url of this.#objectUrls.values())
|
|
74
|
+
URL.revokeObjectURL(url);
|
|
75
|
+
this.#objectUrls.clear();
|
|
76
|
+
}
|
|
77
|
+
#attachRecord(record, parent = this.config.root) {
|
|
78
|
+
const existing = this.findByHash(record.hash);
|
|
79
|
+
if (existing)
|
|
80
|
+
return existing;
|
|
81
|
+
const item = this.config.codex.create("file", {
|
|
82
|
+
hash: record.hash,
|
|
83
|
+
label: record.label,
|
|
84
|
+
format: record.format,
|
|
85
|
+
mime: record.mime,
|
|
86
|
+
size: record.size,
|
|
87
|
+
previewUrl: this.#objectUrls.get(record.hash) ?? null,
|
|
88
|
+
});
|
|
89
|
+
parent.attach(item);
|
|
90
|
+
return item;
|
|
91
|
+
}
|
|
92
|
+
async #loadPreview(record) {
|
|
93
|
+
const cask = await this.cellar.load(record.hash);
|
|
94
|
+
const blob = new Blob([cask.bytes], { type: record.mime || "image/*" });
|
|
95
|
+
return this.#setPreview(record.hash, URL.createObjectURL(blob));
|
|
96
|
+
}
|
|
97
|
+
#setPreview(hash, url) {
|
|
98
|
+
this.#revokePreview(hash);
|
|
99
|
+
this.#objectUrls.set(hash, url);
|
|
100
|
+
return url;
|
|
101
|
+
}
|
|
102
|
+
#revokePreview(hash) {
|
|
103
|
+
const url = this.#objectUrls.get(hash);
|
|
104
|
+
if (url) {
|
|
105
|
+
URL.revokeObjectURL(url);
|
|
106
|
+
this.#objectUrls.delete(hash);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const mediaFormats = new Set(["video", "image", "audio"]);
|
|
111
|
+
const memoryIndexes = new Map();
|
|
112
|
+
function mediaFormat(mime) {
|
|
113
|
+
const [type] = mime.split("/");
|
|
114
|
+
return mediaFormats.has(type) ? type : "other";
|
|
115
|
+
}
|
|
116
|
+
function mediaIndex(scope) {
|
|
117
|
+
const storage = globalThis.localStorage;
|
|
118
|
+
if (storage)
|
|
119
|
+
return new Kv(new StorageDriver(storage))
|
|
120
|
+
.namespace("quay.media")
|
|
121
|
+
.namespace(scope);
|
|
122
|
+
const existing = memoryIndexes.get(scope);
|
|
123
|
+
if (existing)
|
|
124
|
+
return existing;
|
|
125
|
+
const index = new Kv();
|
|
126
|
+
memoryIndexes.set(scope, index);
|
|
127
|
+
return index;
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../../../s/logic/presets/media/store.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,EAAE,EAAE,aAAa,EAAC,MAAM,UAAU,CAAA;AAE1C,OAAO,EAAC,MAAM,EAAC,MAAM,2BAA2B,CAAA;AAEhD,OAAO,EAAC,UAAU,EAAC,MAAM,YAAY,CAAA;AAYrC,MAAM,OAAO,UAAW,SAAQ,UAAU;IACzC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,GAAG,SAAS;QAClC,MAAM,KAAK,GAAG,IAAI,IAAI,EAAE,CAAA;QACxB,KAAK,CAAC,MAAM,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QACzC,KAAK,CAAC,MAAM,GAAG,UAAU,CAAC,KAAK,CAAC,CAAA;QAChC,MAAM,KAAK,CAAC,KAAK,EAAE,CAAA;QACnB,OAAO,KAAK,CAAA;IACb,CAAC;IAED,WAAW,GAAG,IAAI,GAAG,EAAkB,CAAA;IACvC,MAAM,CAAiB;IAEvB,MAAM,GAAG,IAAI,MAAM,EAAE,CAAA;IAErB;QACC,KAAK,EAAE,CAAA;QACP,IAAI,CAAC,MAAM,GAAG,UAAU,CAAC,SAAS,CAAC,CAAA;QACnC,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,EAAC,KAAK,EAAE,MAAM,EAAC,EAAE,EAAE;YACtC,KAAK,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;QACrC,CAAC,CAAC,CAAA;IACH,CAAC;IAED,KAAK,CAAC,CAAC,OAAO;QACb,IAAI,KAAK,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE;YACnD,MAAM,MAAM,CAAA;IACd,CAAC;IAED,KAAK,CAAC,KAAK;QACV,IAAI,KAAK,EAAE,MAAM,MAAM,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC;YAC3C,IAAI,MAAM,CAAC,MAAM,KAAK,OAAO,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;gBAClE,MAAM,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,CAAA;YAChC,IAAI,CAAC,aAAa,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QAC7C,CAAC;IACF,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,KAAa,EAAE,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI;QACzD,OAAO,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAA;IAC/D,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,IAAU,EAAE,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI;QACrD,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC,CAAA;QACtD,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAC1C,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACjD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;QACtB,MAAM,MAAM,GAAgB;YAC3B,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,KAAK,EAAE,QAAQ,EAAE,KAAK,IAAI,IAAI,CAAC,IAAI;YACnC,MAAM,EAAE,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC;YAC9B,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,SAAS,EAAE,QAAQ,EAAE,SAAS,IAAI,GAAG;YACrC,SAAS,EAAE,GAAG;SACd,CAAA;QACD,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;QAC1C,IAAI,MAAM,CAAC,MAAM,KAAK,OAAO;YAC5B,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,CAAA;QACzD,IAAI,CAAC,aAAa,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAClC,OAAO,MAAM,CAAA;IACd,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,IAAY;QACxB,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QAC3B,MAAM,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QAC9B,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAA;QAClC,IAAI,IAAI,EAAE,CAAC;YACV,IAAI,CAAC,MAAM,EAAE,CAAA;YACb,IAAI,CAAC,OAAO,EAAE,CAAA;QACf,CAAC;QACD,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,CAAA;IAC1B,CAAC;IAED,UAAU,CAAC,IAAY;QACtB,KAAK,MAAM,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,CAAC;YAC/C,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,KAAK,IAAI;gBACrD,OAAO,IAAI,CAAA;QACb,CAAC;IACF,CAAC;IAED,OAAO;QACN,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,WAAW,CAAC,MAAM,EAAE;YAC1C,GAAG,CAAC,eAAe,CAAC,GAAG,CAAC,CAAA;QACzB,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,CAAA;IACzB,CAAC;IAED,aAAa,CAAC,MAAmB,EAAE,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI;QAC3D,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QAC7C,IAAI,QAAQ;YACX,OAAO,QAAQ,CAAA;QAEhB,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE;YAC7C,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,UAAU,EAAE,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,IAAI;SACrD,CAAC,CAAA;QACF,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QACnB,OAAO,IAAI,CAAA;IACZ,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,MAAmB;QACrC,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QAChD,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,EAAC,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,SAAS,EAAC,CAAC,CAAA;QACrE,OAAO,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,CAAA;IAChE,CAAC;IAED,WAAW,CAAC,IAAY,EAAE,GAAW;QACpC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,CAAA;QACzB,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;QAC/B,OAAO,GAAG,CAAA;IACX,CAAC;IAED,cAAc,CAAC,IAAY;QAC1B,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QACtC,IAAI,GAAG,EAAE,CAAC;YACT,GAAG,CAAC,eAAe,CAAC,GAAG,CAAC,CAAA;YACxB,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QAC9B,CAAC;IACF,CAAC;CACD;AAED,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAA;AACzD,MAAM,aAAa,GAAG,IAAI,GAAG,EAA2B,CAAA;AAExD,SAAS,WAAW,CAAC,IAAY;IAChC,MAAM,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAC9B,OAAO,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAmB,CAAC,CAAC,CAAC,OAAO,CAAA;AAC9D,CAAC;AAED,SAAS,UAAU,CAAC,KAAa;IAChC,MAAM,OAAO,GAAG,UAAU,CAAC,YAAY,CAAA;IACvC,IAAI,OAAO;QACV,OAAO,IAAI,EAAE,CAAc,IAAI,aAAa,CAAC,OAAO,CAAC,CAAC;aACpD,SAAS,CAAC,YAAY,CAAC;aACvB,SAAS,CAAC,KAAK,CAAC,CAAA;IAEnB,MAAM,QAAQ,GAAG,aAAa,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;IACzC,IAAI,QAAQ;QACX,OAAO,QAAQ,CAAA;IAEhB,MAAM,KAAK,GAAG,IAAI,EAAE,EAAe,CAAA;IACnC,aAAa,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;IAC/B,OAAO,KAAK,CAAA;AACb,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Txt } from "@e280/stz";
|
|
2
|
+
import { expect, Science, test } from "@e280/science";
|
|
3
|
+
import { MediaStore } from "./store.js";
|
|
4
|
+
delete globalThis.localStorage;
|
|
5
|
+
export default Science.suite({
|
|
6
|
+
"imports files into cellar and index": test(async () => {
|
|
7
|
+
const group = new MediaStore();
|
|
8
|
+
const file = new File([Txt.toBytes("hello")], "hello.txt", { type: "text/plain" });
|
|
9
|
+
const [record] = await group.importFiles([file]);
|
|
10
|
+
expect(await group.cellar.has(record.hash)).is(true);
|
|
11
|
+
expect(group.findByHash(record.hash)?.specimen.label).is("hello.txt");
|
|
12
|
+
}),
|
|
13
|
+
"lists media records": test(async () => {
|
|
14
|
+
const store = new MediaStore();
|
|
15
|
+
const file = new File([Txt.toBytes("image")], "image.png", { type: "image/png" });
|
|
16
|
+
const record = await store.importFile(file);
|
|
17
|
+
const records = [];
|
|
18
|
+
for await (const record of store.records())
|
|
19
|
+
records.push(record);
|
|
20
|
+
expect(records.some(r => r.hash === record.hash)).is(true);
|
|
21
|
+
}),
|
|
22
|
+
"removes media records and bytes": test(async () => {
|
|
23
|
+
const store = new MediaStore();
|
|
24
|
+
const file = new File([Txt.toBytes("image")], "image.png", { type: "image/png" });
|
|
25
|
+
const record = await store.importFile(file);
|
|
26
|
+
await store.remove(record.hash);
|
|
27
|
+
expect(await store.cellar.has(record.hash)).is(false);
|
|
28
|
+
expect(store.findByHash(record.hash)).is(undefined);
|
|
29
|
+
}),
|
|
30
|
+
});
|
|
31
|
+
//# sourceMappingURL=store.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.test.js","sourceRoot":"","sources":["../../../../s/logic/presets/media/store.test.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,GAAG,EAAC,MAAM,WAAW,CAAA;AAC7B,OAAO,EAAC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAC,MAAM,eAAe,CAAA;AAEnD,OAAO,EAAC,UAAU,EAAC,MAAM,YAAY,CAAA;AAErC,OAAQ,UAAkB,CAAC,YAAY,CAAA;AAEvC,eAAe,OAAO,CAAC,KAAK,CAAC;IAC5B,qCAAqC,EAAE,IAAI,CAAC,KAAK,IAAG,EAAE;QACrD,MAAM,KAAK,GAAG,IAAI,UAAU,EAAE,CAAA;QAC9B,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,EAAE,WAAW,EAAE,EAAC,IAAI,EAAE,YAAY,EAAC,CAAC,CAAA;QAEhF,MAAM,CAAC,MAAM,CAAC,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,CAAC,CAAA;QAEhD,MAAM,CAAC,MAAM,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAA;QACpD,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,WAAW,CAAC,CAAA;IACtE,CAAC,CAAC;IAEF,qBAAqB,EAAE,IAAI,CAAC,KAAK,IAAG,EAAE;QACrC,MAAM,KAAK,GAAG,IAAI,UAAU,EAAE,CAAA;QAC9B,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,EAAE,WAAW,EAAE,EAAC,IAAI,EAAE,WAAW,EAAC,CAAC,CAAA;QAC/E,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,CAAA;QAC3C,MAAM,OAAO,GAAG,EAAE,CAAA;QAClB,IAAI,KAAK,EAAE,MAAM,MAAM,IAAI,KAAK,CAAC,OAAO,EAAE;YACzC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QAErB,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAA;IAC3D,CAAC,CAAC;IAEF,iCAAiC,EAAE,IAAI,CAAC,KAAK,IAAG,EAAE;QACjD,MAAM,KAAK,GAAG,IAAI,UAAU,EAAE,CAAA;QAC9B,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,EAAE,WAAW,EAAE,EAAC,IAAI,EAAE,WAAW,EAAC,CAAC,CAAA;QAC/E,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,CAAA;QAE3C,MAAM,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QAE/B,MAAM,CAAC,MAAM,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAA;QACrD,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,SAAS,CAAC,CAAA;IACpD,CAAC,CAAC;CACF,CAAC,CAAA"}
|
package/x/tests.test.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
import "@benev/slate/x/node.js";
|
package/x/tests.test.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import "@benev/slate/x/node.js";
|
|
1
2
|
import { Science } from "@e280/science";
|
|
2
3
|
import cellar from "./cellar/cellar.test.js";
|
|
4
|
+
import media from "./logic/presets/media/store.test.js";
|
|
3
5
|
await Science.run({
|
|
4
6
|
cellar,
|
|
7
|
+
media
|
|
5
8
|
});
|
|
6
9
|
//# sourceMappingURL=tests.test.js.map
|
package/x/tests.test.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tests.test.js","sourceRoot":"","sources":["../s/tests.test.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,OAAO,EAAC,MAAM,eAAe,CAAA;AACrC,OAAO,MAAM,MAAM,yBAAyB,CAAA;
|
|
1
|
+
{"version":3,"file":"tests.test.js","sourceRoot":"","sources":["../s/tests.test.ts"],"names":[],"mappings":"AACA,OAAO,wBAAwB,CAAA;AAC/B,OAAO,EAAC,OAAO,EAAC,MAAM,eAAe,CAAA;AACrC,OAAO,MAAM,MAAM,yBAAyB,CAAA;AAC5C,OAAO,KAAK,MAAM,qCAAqC,CAAA;AAEvD,MAAM,OAAO,CAAC,GAAG,CAAC;IACjB,MAAM;IACN,KAAK;CACL,CAAC,CAAA"}
|