@toa.io/extensions.storages 0.20.0-alpha.3
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/LICENSE +22 -0
- package/package.json +38 -0
- package/readme.md +195 -0
- package/source/.test/albert.jpg +0 -0
- package/source/.test/empty.txt +0 -0
- package/source/.test/lenna.ascii +64 -0
- package/source/.test/lenna.png +0 -0
- package/source/.test/sample.avif +0 -0
- package/source/.test/sample.gif +0 -0
- package/source/.test/sample.heic +0 -0
- package/source/.test/sample.jpeg +0 -0
- package/source/.test/sample.jxl +0 -0
- package/source/.test/sample.webp +0 -0
- package/source/.test/util.ts +50 -0
- package/source/Aspect.ts +23 -0
- package/source/Entry.ts +14 -0
- package/source/Factory.ts +65 -0
- package/source/Provider.ts +8 -0
- package/source/Scanner.ts +109 -0
- package/source/Storage.test.ts +445 -0
- package/source/Storage.ts +236 -0
- package/source/deployment.ts +61 -0
- package/source/index.ts +3 -0
- package/source/manifest.ts +8 -0
- package/source/providers/FileSystem.test.ts +13 -0
- package/source/providers/FileSystem.ts +46 -0
- package/source/providers/Temporary.ts +14 -0
- package/source/providers/Test.ts +13 -0
- package/source/providers/index.test.ts +116 -0
- package/source/providers/index.ts +13 -0
- package/source/providers/readme.md +20 -0
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import { Readable } from 'node:stream'
|
|
2
|
+
import { match } from '@toa.io/match'
|
|
3
|
+
import { buffer } from '@toa.io/generic'
|
|
4
|
+
import { Storage } from './Storage'
|
|
5
|
+
import { cases, open, rnd } from './.test/util'
|
|
6
|
+
import { type Entry } from './Entry'
|
|
7
|
+
import { providers } from './providers'
|
|
8
|
+
|
|
9
|
+
let storage: Storage
|
|
10
|
+
let dir: string
|
|
11
|
+
|
|
12
|
+
describe.each(cases)('%s', (_, url, secrets) => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
dir = '/' + rnd()
|
|
15
|
+
|
|
16
|
+
const Provider = providers[url.protocol]
|
|
17
|
+
const provider = new Provider(url, secrets)
|
|
18
|
+
|
|
19
|
+
storage = new Storage(provider)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('should be', async () => {
|
|
23
|
+
expect(storage).toBeInstanceOf(Storage)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('should return error if entry is not found', async () => {
|
|
27
|
+
const result = await storage.get('not-found')
|
|
28
|
+
|
|
29
|
+
match(result,
|
|
30
|
+
Error, (error: Error) => expect(error.message).toBe('NOT_FOUND'))
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
describe('put', () => {
|
|
34
|
+
let lenna: Entry
|
|
35
|
+
|
|
36
|
+
beforeEach(async () => {
|
|
37
|
+
const stream = await open('lenna.png')
|
|
38
|
+
|
|
39
|
+
lenna = await storage.put(dir, stream) as Entry
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should not return error', async () => {
|
|
43
|
+
expect(lenna).not.toBeInstanceOf(Error)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('should return entry id', async () => {
|
|
47
|
+
expect(lenna.id).toBeDefined()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('should return id as checksum', async () => {
|
|
51
|
+
const stream = await open('lenna.png')
|
|
52
|
+
const dir2 = '/' + rnd()
|
|
53
|
+
const copy = await storage.put(dir2, stream) as Entry
|
|
54
|
+
|
|
55
|
+
expect(copy.id).toBe(lenna.id)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('should detect file type', async () => {
|
|
59
|
+
expect(lenna.type).toBe('image/png')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('should count size', async () => {
|
|
63
|
+
expect(lenna.size).toBe(473831)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('should return entry', async () => {
|
|
67
|
+
expect(lenna).toMatchObject({
|
|
68
|
+
id: lenna.id,
|
|
69
|
+
type: 'image/png',
|
|
70
|
+
variants: [],
|
|
71
|
+
meta: {}
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('should create entry', async () => {
|
|
76
|
+
const entry = await storage.get(`${dir}/${lenna.id}`)
|
|
77
|
+
|
|
78
|
+
match(entry,
|
|
79
|
+
{
|
|
80
|
+
id: lenna.id,
|
|
81
|
+
type: 'image/png',
|
|
82
|
+
variants: [],
|
|
83
|
+
meta: {}
|
|
84
|
+
}, undefined)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('should set timestamp', async () => {
|
|
88
|
+
const now = Date.now()
|
|
89
|
+
const entry = await storage.get(`${dir}/${lenna.id}`) as Entry
|
|
90
|
+
|
|
91
|
+
expect(entry.created).toBeLessThanOrEqual(now)
|
|
92
|
+
expect(entry.created).toBeGreaterThan(now - 100)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
describe('existing entry', () => {
|
|
96
|
+
it('should unhide existing', async () => {
|
|
97
|
+
const stream = await open('lenna.png')
|
|
98
|
+
const path = `${dir}/${lenna.id}`
|
|
99
|
+
|
|
100
|
+
await storage.conceal(path)
|
|
101
|
+
await storage.put(dir, stream)
|
|
102
|
+
|
|
103
|
+
const list = await storage.list(dir)
|
|
104
|
+
|
|
105
|
+
expect(list).toContainEqual(lenna.id)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('should preserve meta', async () => {
|
|
109
|
+
const path = `${dir}/${lenna.id}`
|
|
110
|
+
const stream = await open('lenna.png')
|
|
111
|
+
|
|
112
|
+
await storage.annotate(path, 'foo', 'bar')
|
|
113
|
+
await storage.put(dir, stream)
|
|
114
|
+
|
|
115
|
+
const entry = await storage.get(path) as Entry
|
|
116
|
+
|
|
117
|
+
expect(entry.meta).toMatchObject({ foo: 'bar' })
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
describe('list', () => {
|
|
123
|
+
let albert: Entry
|
|
124
|
+
let lenna: Entry
|
|
125
|
+
|
|
126
|
+
beforeEach(async () => {
|
|
127
|
+
const stream0 = await open('albert.jpg')
|
|
128
|
+
const stream1 = await open('lenna.png')
|
|
129
|
+
|
|
130
|
+
albert = await storage.put(dir, stream0) as Entry
|
|
131
|
+
lenna = await storage.put(dir, stream1) as Entry
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('should list entries', async () => {
|
|
135
|
+
const list = await storage.list(dir)
|
|
136
|
+
|
|
137
|
+
expect(list).toMatchObject([albert.id, lenna.id])
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('should permutate', async () => {
|
|
141
|
+
const error = await storage.reorder(dir, [lenna.id, albert.id])
|
|
142
|
+
|
|
143
|
+
expect(error).toBeUndefined()
|
|
144
|
+
|
|
145
|
+
const list = await storage.list(dir)
|
|
146
|
+
|
|
147
|
+
expect(list).toMatchObject([lenna.id, albert.id])
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('should return PERMUTATION_MISMATCH', async () => {
|
|
151
|
+
const cases = [
|
|
152
|
+
[lenna.id],
|
|
153
|
+
[albert.id, lenna.id, 'unknown'],
|
|
154
|
+
[lenna.id, lenna.id],
|
|
155
|
+
[lenna.id, lenna.id, albert.id]
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
for (const permutation of cases) {
|
|
159
|
+
const error = await storage.reorder(dir, permutation)
|
|
160
|
+
|
|
161
|
+
expect(error).toBeInstanceOf(Error)
|
|
162
|
+
expect(error).toMatchObject({ message: 'PERMUTATION_MISMATCH' })
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('should exclude concealed', async () => {
|
|
167
|
+
const path = `${dir}/${lenna.id}`
|
|
168
|
+
|
|
169
|
+
await storage.conceal(path)
|
|
170
|
+
|
|
171
|
+
const entries = await storage.list(dir)
|
|
172
|
+
|
|
173
|
+
expect(entries).toMatchObject([albert.id])
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('should reveal', async () => {
|
|
177
|
+
const path = `${dir}/${lenna.id}`
|
|
178
|
+
|
|
179
|
+
await storage.conceal(path)
|
|
180
|
+
await storage.reveal(path)
|
|
181
|
+
await storage.reveal(path) // test that no duplicates are created
|
|
182
|
+
|
|
183
|
+
const entries = await storage.list(dir)
|
|
184
|
+
|
|
185
|
+
expect(entries).toMatchObject([albert.id, lenna.id])
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('should return ERR_NOT_FOOUD if entry doesnt exist', async () => {
|
|
189
|
+
const path = `${dir}/oopsie`
|
|
190
|
+
|
|
191
|
+
const methods: Array<'reveal' | 'conceal'> = ['reveal', 'conceal']
|
|
192
|
+
|
|
193
|
+
for (const method of methods) {
|
|
194
|
+
const error = await storage[method](path)
|
|
195
|
+
|
|
196
|
+
expect(error).toBeInstanceOf(Error)
|
|
197
|
+
expect(error).toMatchObject({ message: 'NOT_FOUND' })
|
|
198
|
+
}
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
describe('annotate', () => {
|
|
203
|
+
let lenna: Entry
|
|
204
|
+
|
|
205
|
+
beforeEach(async () => {
|
|
206
|
+
const stream = await open('lenna.png')
|
|
207
|
+
|
|
208
|
+
lenna = await storage.put(dir, stream) as Entry
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('should set meta', async () => {
|
|
212
|
+
const path = `${dir}/${lenna.id}`
|
|
213
|
+
|
|
214
|
+
await storage.annotate(path, 'foo', 'bar')
|
|
215
|
+
|
|
216
|
+
const state0 = await storage.get(path) as Entry
|
|
217
|
+
|
|
218
|
+
expect(state0.meta).toMatchObject({ foo: 'bar' })
|
|
219
|
+
|
|
220
|
+
await storage.annotate(path, 'foo')
|
|
221
|
+
|
|
222
|
+
const state1 = await storage.get(path) as Entry
|
|
223
|
+
|
|
224
|
+
expect('foo' in state1.meta).toBe(false)
|
|
225
|
+
})
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
describe('variants', () => {
|
|
229
|
+
let lenna: Entry
|
|
230
|
+
|
|
231
|
+
beforeEach(async () => {
|
|
232
|
+
const stream = await open('lenna.png')
|
|
233
|
+
|
|
234
|
+
lenna = await storage.put(dir, stream) as Entry
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('should add variant', async () => {
|
|
238
|
+
const stream = await open('sample.jpeg')
|
|
239
|
+
|
|
240
|
+
const path = `${dir}/${lenna.id}`
|
|
241
|
+
|
|
242
|
+
await storage.diversify(path, 'foo', stream)
|
|
243
|
+
|
|
244
|
+
const state = await storage.get(path) as Entry
|
|
245
|
+
|
|
246
|
+
expect(state.variants).toMatchObject([{ name: 'foo', size: 73444, type: 'image/jpeg' }])
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('should replace variant', async () => {
|
|
250
|
+
const stream0 = await open('sample.jpeg')
|
|
251
|
+
const stream1 = await open('sample.webp')
|
|
252
|
+
const path = `${dir}/${lenna.id}`
|
|
253
|
+
|
|
254
|
+
await storage.diversify(path, 'foo', stream0)
|
|
255
|
+
await storage.diversify(path, 'foo', stream1)
|
|
256
|
+
|
|
257
|
+
const state = await storage.get(path) as Entry
|
|
258
|
+
|
|
259
|
+
expect(state.variants).toMatchObject([{ name: 'foo', type: 'image/webp' }])
|
|
260
|
+
})
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
describe('fetch', () => {
|
|
264
|
+
let lenna: Entry
|
|
265
|
+
|
|
266
|
+
beforeEach(async () => {
|
|
267
|
+
const stream = await open('lenna.png')
|
|
268
|
+
|
|
269
|
+
lenna = await storage.put(dir, stream) as Entry
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it('should fetch', async () => {
|
|
273
|
+
const path = `${dir}/${lenna.id}`
|
|
274
|
+
const stream = await storage.fetch(path)
|
|
275
|
+
|
|
276
|
+
const stored: Buffer = await match(stream,
|
|
277
|
+
Readable, async (stream: Readable) => await buffer(stream))
|
|
278
|
+
|
|
279
|
+
const buf = await buffer(await open('lenna.png'))
|
|
280
|
+
|
|
281
|
+
expect(stored.compare(buf)).toBe(0)
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
it('should fetch blob by id', async () => {
|
|
285
|
+
const stream = await open('lenna.ascii')
|
|
286
|
+
const entry = await storage.put(dir, stream) as Entry
|
|
287
|
+
const stored = await storage.fetch(entry.id)
|
|
288
|
+
|
|
289
|
+
if (stored instanceof Error)
|
|
290
|
+
throw stored
|
|
291
|
+
|
|
292
|
+
const buf = await buffer(stored)
|
|
293
|
+
const expected = await buffer(await open('lenna.ascii'))
|
|
294
|
+
|
|
295
|
+
expect(buf.compare(expected)).toBe(0)
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it('should fetch variant', async () => {
|
|
299
|
+
const stream = await open('sample.jpeg')
|
|
300
|
+
|
|
301
|
+
const buf = await buffer(stream)
|
|
302
|
+
const path = `${dir}/${lenna.id}`
|
|
303
|
+
|
|
304
|
+
await storage.diversify(path, '100x100.jpeg', Readable.from(buf))
|
|
305
|
+
|
|
306
|
+
const variant = await storage.fetch(`${path}.100x100.jpeg`)
|
|
307
|
+
|
|
308
|
+
const stored = await match(variant,
|
|
309
|
+
Readable, async (stream: Readable) => await buffer(stream))
|
|
310
|
+
|
|
311
|
+
expect(stored.compare(buf)).toBe(0)
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
it('should not fetch blob by id and fake path', async () => {
|
|
315
|
+
const stored = await storage.fetch(`fake/${lenna.id}`)
|
|
316
|
+
|
|
317
|
+
match(stored,
|
|
318
|
+
Error, (error: Error) => expect(error.message).toBe('NOT_FOUND'))
|
|
319
|
+
})
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
describe('delete', () => {
|
|
323
|
+
let lenna: Entry
|
|
324
|
+
|
|
325
|
+
beforeEach(async () => {
|
|
326
|
+
const stream = await open('lenna.png')
|
|
327
|
+
|
|
328
|
+
lenna = await storage.put(dir, stream) as Entry
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
it('should remove from the list', async () => {
|
|
332
|
+
await storage.delete(`${dir}/${lenna.id}`)
|
|
333
|
+
|
|
334
|
+
const list = await storage.list(dir)
|
|
335
|
+
|
|
336
|
+
expect(list).not.toContain(lenna.id)
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
it('should delete entry', async () => {
|
|
340
|
+
await storage.delete(`${dir}/${lenna.id}`)
|
|
341
|
+
|
|
342
|
+
const result = await storage.get(`${dir}/${lenna.id}`)
|
|
343
|
+
|
|
344
|
+
match(result,
|
|
345
|
+
Error, (error: Error) => expect(error.message).toBe('NOT_FOUND'))
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
it('should delete variants', async () => {
|
|
349
|
+
const stream = await open('sample.jpeg')
|
|
350
|
+
|
|
351
|
+
const path = `${dir}/${lenna.id}`
|
|
352
|
+
|
|
353
|
+
await storage.diversify(path, 'foo', stream)
|
|
354
|
+
await storage.delete(`${dir}/${lenna.id}`)
|
|
355
|
+
|
|
356
|
+
const variant = await storage.fetch(`${path}.foo`)
|
|
357
|
+
|
|
358
|
+
match(variant,
|
|
359
|
+
Error, (error: Error) => expect(error.message).toBe('NOT_FOUND'))
|
|
360
|
+
|
|
361
|
+
stream.destroy()
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
it('should throw if path is not an entry', async () => {
|
|
365
|
+
const result = await storage.delete(dir)
|
|
366
|
+
|
|
367
|
+
expect(result).toBeInstanceOf(Error)
|
|
368
|
+
expect(result).toMatchObject({ message: 'NOT_FOUND' })
|
|
369
|
+
})
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
describe('signatures', () => {
|
|
373
|
+
it.each(['jpeg', 'gif', 'webp', 'heic', 'jxl', 'avif'])('should detect image/%s',
|
|
374
|
+
async (type) => {
|
|
375
|
+
const stream = await open('sample.' + type)
|
|
376
|
+
|
|
377
|
+
const entry = await storage.put(dir, stream) as Entry
|
|
378
|
+
|
|
379
|
+
expect(entry.type).toBe('image/' + type)
|
|
380
|
+
})
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
it('should return error if type doesnt match', async () => {
|
|
384
|
+
const stream = await open('sample.jpeg')
|
|
385
|
+
|
|
386
|
+
const result = await storage.put(dir, stream, 'image/png')
|
|
387
|
+
|
|
388
|
+
match(result,
|
|
389
|
+
Error, (error: Error) => expect(error.message).toBe('TYPE_MISMATCH'))
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
it('should trust unknown types', async () => {
|
|
393
|
+
const stream = await open('lenna.ascii')
|
|
394
|
+
|
|
395
|
+
const result = await storage.put(dir, stream, 'text/plain')
|
|
396
|
+
|
|
397
|
+
expect(result).not.toBeInstanceOf(Error)
|
|
398
|
+
expect(result).toMatchObject({ type: 'text/plain' })
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
it('should return error if type is identifiable', async () => {
|
|
402
|
+
const stream = await open('lenna.ascii')
|
|
403
|
+
|
|
404
|
+
const result = await storage.put(dir, stream, 'image/jpeg')
|
|
405
|
+
|
|
406
|
+
expect(result).toBeInstanceOf(Error)
|
|
407
|
+
expect(result).toMatchObject({ message: 'TYPE_MISMATCH' })
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
it('should not return error if type application/octet-stream', async () => {
|
|
411
|
+
const stream = await open('sample.jpeg')
|
|
412
|
+
|
|
413
|
+
const result = await storage.put(dir, stream, 'application/octet-stream')
|
|
414
|
+
|
|
415
|
+
expect(result).not.toBeInstanceOf(Error)
|
|
416
|
+
expect(result).toMatchObject({ type: 'image/jpeg' })
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
it('should handle root entries', async () => {
|
|
420
|
+
const stream = await open('sample.jpeg')
|
|
421
|
+
|
|
422
|
+
const result = await storage.put('hello', stream) as Entry
|
|
423
|
+
|
|
424
|
+
expect(result).not.toBeInstanceOf(Error)
|
|
425
|
+
|
|
426
|
+
const stored = await storage.fetch(result.id)
|
|
427
|
+
|
|
428
|
+
expect(stored).not.toBeInstanceOf(Error)
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
it('should store empty file', async () => {
|
|
432
|
+
const stream = await open('empty.txt')
|
|
433
|
+
const result = await storage.put('empty', stream) as Entry
|
|
434
|
+
|
|
435
|
+
expect(result.size).toBe(0)
|
|
436
|
+
|
|
437
|
+
const stored = await storage.fetch(result.id) as Readable
|
|
438
|
+
|
|
439
|
+
expect(stored).not.toBeInstanceOf(Error)
|
|
440
|
+
|
|
441
|
+
const buf = await buffer(stored)
|
|
442
|
+
|
|
443
|
+
expect(buf.length).toBe(0)
|
|
444
|
+
})
|
|
445
|
+
})
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { Readable } from 'node:stream'
|
|
2
|
+
import { posix } from 'node:path'
|
|
3
|
+
import { decode, encode } from 'msgpackr'
|
|
4
|
+
import { buffer, newid } from '@toa.io/generic'
|
|
5
|
+
import { Scanner } from './Scanner'
|
|
6
|
+
import type { Provider } from './Provider'
|
|
7
|
+
import type { Entry } from './Entry'
|
|
8
|
+
|
|
9
|
+
export class Storage {
|
|
10
|
+
private readonly provider: Provider
|
|
11
|
+
|
|
12
|
+
public constructor (provider: Provider) {
|
|
13
|
+
this.provider = provider
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public async put (path: string, stream: Readable, type?: string): Maybe<Entry> {
|
|
17
|
+
const scanner = new Scanner(type)
|
|
18
|
+
const pipe = stream.pipe(scanner)
|
|
19
|
+
const tempname = await this.transit(pipe)
|
|
20
|
+
|
|
21
|
+
if (scanner.error !== null)
|
|
22
|
+
return scanner.error
|
|
23
|
+
|
|
24
|
+
const id = scanner.digest()
|
|
25
|
+
|
|
26
|
+
await this.persist(tempname, id)
|
|
27
|
+
|
|
28
|
+
return await this.create(path, id, scanner.size, scanner.type)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public async get (path: string): Maybe<Entry> {
|
|
32
|
+
const metapath = posix.join(ENTRIES, path, META)
|
|
33
|
+
const result = await this.provider.get(metapath)
|
|
34
|
+
|
|
35
|
+
if (result === null) return ERR_NOT_FOUND
|
|
36
|
+
else return decode(await buffer(result))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public async fetch (path: string): Maybe<Readable> {
|
|
40
|
+
const { rel, id, variant } = this.parse(path)
|
|
41
|
+
|
|
42
|
+
if (variant === null && rel !== '') {
|
|
43
|
+
const entry = await this.get(path)
|
|
44
|
+
|
|
45
|
+
if (entry instanceof Error)
|
|
46
|
+
return entry
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const blob = variant === null
|
|
50
|
+
? posix.join(BLOBs, id)
|
|
51
|
+
: posix.join(ENTRIES, rel, id, variant)
|
|
52
|
+
|
|
53
|
+
const stream = await this.provider.get(blob)
|
|
54
|
+
|
|
55
|
+
if (stream === null) return ERR_NOT_FOUND
|
|
56
|
+
else return stream
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public async delete (path: string): Maybe<void> {
|
|
60
|
+
const entry = await this.get(path)
|
|
61
|
+
|
|
62
|
+
if (entry instanceof Error)
|
|
63
|
+
return entry
|
|
64
|
+
|
|
65
|
+
await this.conceal(path)
|
|
66
|
+
await this.provider.delete(posix.join(ENTRIES, path))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public async list (path: string): Promise<string[]> {
|
|
70
|
+
const listfile = posix.join(ENTRIES, path, LIST)
|
|
71
|
+
const stream = await this.provider.get(listfile)
|
|
72
|
+
|
|
73
|
+
return stream === null ? [] : decode(await buffer(stream))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public async reorder (path: string, ids: string[]): Maybe<void> {
|
|
77
|
+
const unique = new Set(ids)
|
|
78
|
+
const dir = posix.join(ENTRIES, path)
|
|
79
|
+
const list = await this.list(path)
|
|
80
|
+
|
|
81
|
+
if (list.length !== ids.length || unique.size !== ids.length)
|
|
82
|
+
return ERR_PERMUTATION_MISMATCH
|
|
83
|
+
|
|
84
|
+
for (const id of ids)
|
|
85
|
+
if (!list.includes(id))
|
|
86
|
+
return ERR_PERMUTATION_MISMATCH
|
|
87
|
+
|
|
88
|
+
await this.provider.put(dir, LIST, Readable.from(encode(ids)))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
public async conceal (path: string): Maybe<void> {
|
|
92
|
+
const { id, rel } = this.parse(path)
|
|
93
|
+
const dir = posix.join(ENTRIES, rel)
|
|
94
|
+
const list = await this.list(rel)
|
|
95
|
+
const index = list.indexOf(id)
|
|
96
|
+
|
|
97
|
+
if (index === -1)
|
|
98
|
+
return ERR_NOT_FOUND
|
|
99
|
+
|
|
100
|
+
list.splice(index, 1)
|
|
101
|
+
|
|
102
|
+
await this.provider.put(dir, LIST, Readable.from(encode(list)))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
public async reveal (path: string): Maybe<void> {
|
|
106
|
+
const { id, rel } = this.parse(path)
|
|
107
|
+
|
|
108
|
+
return await this.enroll(rel, id)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
public async diversify (path: string, name: string, stream: Readable): Maybe<void> {
|
|
112
|
+
const scanner = new Scanner()
|
|
113
|
+
const pipe = stream.pipe(scanner)
|
|
114
|
+
|
|
115
|
+
await this.provider.put(posix.join(ENTRIES, path), name, pipe)
|
|
116
|
+
|
|
117
|
+
if (scanner.error !== null)
|
|
118
|
+
return scanner.error
|
|
119
|
+
|
|
120
|
+
const { size, type } = scanner
|
|
121
|
+
const entry = await this.get(path)
|
|
122
|
+
|
|
123
|
+
if (entry instanceof Error)
|
|
124
|
+
return entry
|
|
125
|
+
|
|
126
|
+
entry.variants = entry.variants.filter((variant) => variant.name !== name)
|
|
127
|
+
entry.variants.push({ name, size, type })
|
|
128
|
+
|
|
129
|
+
await this.save(path, entry)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
public async annotate (path: string, key: string, value?: unknown): Maybe<void> {
|
|
133
|
+
const entry = await this.get(path)
|
|
134
|
+
|
|
135
|
+
if (entry instanceof Error)
|
|
136
|
+
return entry
|
|
137
|
+
|
|
138
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
139
|
+
if (value === undefined) delete entry.meta[key]
|
|
140
|
+
else entry.meta[key] = value
|
|
141
|
+
|
|
142
|
+
await this.save(path, entry)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private async transit (stream: Readable): Promise<string> {
|
|
146
|
+
const tempname = newid()
|
|
147
|
+
|
|
148
|
+
await this.provider.put(TEMP, tempname, stream)
|
|
149
|
+
|
|
150
|
+
return tempname
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private async persist (tempname: string, id: string): Promise<void> {
|
|
154
|
+
const temp = posix.join(TEMP, tempname)
|
|
155
|
+
const blob = posix.join(BLOBs, id)
|
|
156
|
+
|
|
157
|
+
await this.provider.move(temp, blob)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// eslint-disable-next-line max-params
|
|
161
|
+
private async create (path: string, id: string, size: number, type: string): Promise<Entry> {
|
|
162
|
+
const entry: Entry = {
|
|
163
|
+
id,
|
|
164
|
+
size,
|
|
165
|
+
type,
|
|
166
|
+
created: Date.now(),
|
|
167
|
+
variants: [],
|
|
168
|
+
meta: {}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const metafile = posix.join(path, entry.id)
|
|
172
|
+
const existing = await this.get(metafile)
|
|
173
|
+
|
|
174
|
+
if (existing instanceof Error)
|
|
175
|
+
await this.save(metafile, entry)
|
|
176
|
+
|
|
177
|
+
await this.enroll(path, id, true)
|
|
178
|
+
|
|
179
|
+
return entry
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private async save (rel: string, entry: Entry): Promise<void> {
|
|
183
|
+
const buffer = encode(entry)
|
|
184
|
+
const stream = Readable.from(buffer)
|
|
185
|
+
|
|
186
|
+
await this.provider.put(posix.join(ENTRIES, rel), META, stream)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private async enroll (path: string, id: string, addition: boolean = false): Maybe<void> {
|
|
190
|
+
const dir = posix.join(ENTRIES, path)
|
|
191
|
+
const list = await this.list(path)
|
|
192
|
+
const index = list.indexOf(id)
|
|
193
|
+
|
|
194
|
+
if (index !== -1)
|
|
195
|
+
if (addition) list.splice(index, 1)
|
|
196
|
+
else return
|
|
197
|
+
else if (!addition) {
|
|
198
|
+
const entry = await this.get(posix.join(path, id))
|
|
199
|
+
|
|
200
|
+
if (entry instanceof Error)
|
|
201
|
+
return entry
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
list.push(id)
|
|
205
|
+
|
|
206
|
+
await this.provider.put(dir, LIST, Readable.from(encode(list)))
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private parse (path: string): Path {
|
|
210
|
+
const [last, ...segments] = path.split('/').reverse()
|
|
211
|
+
const [id, ...rest] = last.split('.')
|
|
212
|
+
const variant = rest.length > 0 ? rest.join('.') : null
|
|
213
|
+
const rel = segments.reverse().join('/')
|
|
214
|
+
|
|
215
|
+
return { rel, id, variant }
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const ERR_NOT_FOUND = new Error('NOT_FOUND')
|
|
220
|
+
const ERR_PERMUTATION_MISMATCH = new Error('PERMUTATION_MISMATCH')
|
|
221
|
+
|
|
222
|
+
const TEMP = '/temp'
|
|
223
|
+
const BLOBs = '/blobs'
|
|
224
|
+
const ENTRIES = '/entries'
|
|
225
|
+
const LIST = '.list'
|
|
226
|
+
const META = '.meta'
|
|
227
|
+
|
|
228
|
+
type Maybe<T> = Promise<T | Error>
|
|
229
|
+
|
|
230
|
+
interface Path {
|
|
231
|
+
rel: string
|
|
232
|
+
id: string
|
|
233
|
+
variant: string | null
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export type Storages = Record<string, Storage>
|