@toa.io/extensions.storages 1.0.0-alpha.282 → 1.0.0-alpha.286
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/package.json +4 -4
- package/readme.md +1 -1
- package/source/Annotation.ts +25 -12
- package/source/Aspect.ts +4 -3
- package/source/Factory.ts +10 -9
- package/source/Provider.ts +13 -9
- package/source/Scanner.ts +32 -34
- package/source/Storage.test.ts +19 -20
- package/source/Storage.ts +15 -17
- package/source/context.ts +14 -0
- package/source/deployment.ts +14 -11
- package/source/index.ts +3 -0
- package/source/manifest.ts +3 -5
- package/source/providers/Cloudinary.ts +76 -76
- package/source/providers/Declaration.ts +1 -1
- package/source/providers/FileSystem.ts +15 -21
- package/source/providers/S3.ts +77 -65
- package/source/providers/Spaces.ts +9 -6
- package/source/providers/Temporary.ts +1 -1
- package/source/providers/Test.ts +1 -1
- package/source/providers/index.test.ts +85 -85
- package/source/schemas.test.ts +3 -4
- package/source/test/util.ts +7 -2
- package/transpiled/Annotation.js +6 -2
- package/transpiled/Annotation.js.map +1 -1
- package/transpiled/Aspect.d.ts +2 -1
- package/transpiled/Aspect.js.map +1 -1
- package/transpiled/Factory.js.map +1 -1
- package/transpiled/Provider.d.ts +1 -1
- package/transpiled/Provider.js.map +1 -1
- package/transpiled/Scanner.js +3 -7
- package/transpiled/Scanner.js.map +1 -1
- package/transpiled/Storage.js +3 -2
- package/transpiled/Storage.js.map +1 -1
- package/transpiled/context.d.ts +3 -0
- package/transpiled/context.js +12 -0
- package/transpiled/context.js.map +1 -0
- package/transpiled/deployment.js.map +1 -1
- package/transpiled/index.d.ts +1 -0
- package/transpiled/index.js +2 -0
- package/transpiled/index.js.map +1 -1
- package/transpiled/manifest.js.map +1 -1
- package/transpiled/providers/Cloudinary.d.ts +1 -1
- package/transpiled/providers/Cloudinary.js +8 -2
- package/transpiled/providers/Cloudinary.js.map +1 -1
- package/transpiled/providers/FileSystem.d.ts +1 -1
- package/transpiled/providers/FileSystem.js +1 -4
- package/transpiled/providers/FileSystem.js.map +1 -1
- package/transpiled/providers/S3.d.ts +1 -1
- package/transpiled/providers/S3.js.map +1 -1
- package/transpiled/providers/Spaces.js.map +1 -1
- package/transpiled/providers/Temporary.js.map +1 -1
- package/transpiled/providers/Test.js.map +1 -1
- package/transpiled/test/util.js +1 -1
- package/transpiled/test/util.js.map +1 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/CHANGELOG.md +0 -74
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toa.io/extensions.storages",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.286",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Toa Storages",
|
|
6
6
|
"author": "temich <tema.gurtovoy@gmail.com>",
|
|
@@ -27,10 +27,10 @@
|
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@aws-sdk/client-s3": "3.1125.0",
|
|
29
29
|
"@aws-sdk/lib-storage": "3.1125.0",
|
|
30
|
-
"@toa.io/schemas": "1.0.0-alpha.
|
|
30
|
+
"@toa.io/schemas": "1.0.0-alpha.286",
|
|
31
31
|
"cloudinary": "2.11.0",
|
|
32
32
|
"negotiator": "1.1.0",
|
|
33
|
-
"openspan": "1.0.0-alpha.
|
|
33
|
+
"openspan": "1.0.0-alpha.286"
|
|
34
34
|
},
|
|
35
|
-
"gitHead": "
|
|
35
|
+
"gitHead": "81f3d0688f17668b902be336d1749cc715edcdb6"
|
|
36
36
|
}
|
package/readme.md
CHANGED
|
@@ -29,7 +29,7 @@ The Storages extension provides `storages` aspect,
|
|
|
29
29
|
containing named Storage instances, according to the annotation.
|
|
30
30
|
|
|
31
31
|
```javascript
|
|
32
|
-
async function effect
|
|
32
|
+
async function effect(_, context) {
|
|
33
33
|
await context.storages.photos.get('/path/to/b4f577e0.thumbnail.jpeg')
|
|
34
34
|
}
|
|
35
35
|
```
|
package/source/Annotation.ts
CHANGED
|
@@ -6,7 +6,9 @@ import type { Schema } from '@toa.io/schemas'
|
|
|
6
6
|
|
|
7
7
|
export type Annotation = Record<string, Declaration>
|
|
8
8
|
|
|
9
|
-
export function validateAnnotation
|
|
9
|
+
export function validateAnnotation(
|
|
10
|
+
annotation: unknown
|
|
11
|
+
): asserts annotation is Annotation {
|
|
10
12
|
try {
|
|
11
13
|
schemas.annotation.validate(annotation)
|
|
12
14
|
} catch (error) {
|
|
@@ -20,19 +22,30 @@ export function validateAnnotation (annotation: unknown): asserts annotation is
|
|
|
20
22
|
/*
|
|
21
23
|
It is required because `oneOf` schema is used for the annotation validation.
|
|
22
24
|
*/
|
|
23
|
-
function explain
|
|
24
|
-
assert.ok(
|
|
25
|
-
'
|
|
25
|
+
function explain(annotation: unknown): void {
|
|
26
|
+
assert.ok(
|
|
27
|
+
typeof annotation === 'object' && annotation !== null,
|
|
28
|
+
'TOA_STORAGES is not an object'
|
|
29
|
+
)
|
|
26
30
|
|
|
27
31
|
for (const declaration of Object.values(annotation)) {
|
|
28
|
-
assert.ok(
|
|
29
|
-
declaration
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
assert.ok(
|
|
33
|
+
typeof declaration === 'object' &&
|
|
34
|
+
declaration !== null &&
|
|
35
|
+
declaration.provider in providers,
|
|
36
|
+
`Unknown provider '${declaration.provider}'`
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
assert.ok(
|
|
40
|
+
declaration.provider in schemas,
|
|
41
|
+
`No schema for provider '${declaration.provider}'`
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
const provider = declaration.provider as keyof typeof providers
|
|
45
|
+
|
|
46
|
+
// the declaration names the provider
|
|
47
|
+
// oxlint-disable-next-line import/namespace
|
|
48
|
+
const schema: Schema<Declaration> = schemas[provider]
|
|
36
49
|
|
|
37
50
|
schema.validate(declaration, `Storage '${declaration.provider}' annotation`)
|
|
38
51
|
}
|
package/source/Aspect.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import assert from 'node:assert'
|
|
2
|
-
import { Connector
|
|
2
|
+
import { Connector } from '@toa.io/core'
|
|
3
|
+
import type { extensions } from '@toa.io/core/types'
|
|
3
4
|
import { type Storage, type Storages } from './Storage.js'
|
|
4
5
|
|
|
5
6
|
export class Aspect extends Connector implements extensions.Aspect {
|
|
@@ -7,13 +8,13 @@ export class Aspect extends Connector implements extensions.Aspect {
|
|
|
7
8
|
|
|
8
9
|
private readonly storages: Storages
|
|
9
10
|
|
|
10
|
-
public constructor
|
|
11
|
+
public constructor(storages: Storages) {
|
|
11
12
|
super()
|
|
12
13
|
|
|
13
14
|
this.storages = storages
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
public invoke
|
|
17
|
+
public invoke(name: string, method: keyof Storage, ...args: unknown[]): unknown {
|
|
17
18
|
const storage = this.storages[name]
|
|
18
19
|
|
|
19
20
|
assert.ok(storage !== undefined, `Storage '${name}' is not defined`)
|
package/source/Factory.ts
CHANGED
|
@@ -13,7 +13,7 @@ import type { Secrets } from './Secrets.js'
|
|
|
13
13
|
export class Factory {
|
|
14
14
|
private readonly annotation: Annotation
|
|
15
15
|
|
|
16
|
-
public constructor
|
|
16
|
+
public constructor() {
|
|
17
17
|
const env = process.env[ENV_PREFIX]
|
|
18
18
|
|
|
19
19
|
assert.ok(env !== undefined, `${ENV_PREFIX} is not defined`)
|
|
@@ -23,13 +23,13 @@ export class Factory {
|
|
|
23
23
|
validateAnnotation(this.annotation)
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
public aspect
|
|
26
|
+
public aspect(): Aspect {
|
|
27
27
|
const storages = this.createStorages()
|
|
28
28
|
|
|
29
29
|
return new Aspect(storages)
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
private createStorages
|
|
32
|
+
private createStorages(): Storages {
|
|
33
33
|
const storages: Storages = {}
|
|
34
34
|
|
|
35
35
|
for (const [name, declaration] of Object.entries(this.annotation))
|
|
@@ -38,7 +38,7 @@ export class Factory {
|
|
|
38
38
|
return storages
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
private createStorage
|
|
41
|
+
private createStorage(name: string, declaration: Declaration): Storage {
|
|
42
42
|
const { provider: id, ...options } = declaration
|
|
43
43
|
const Provider: Constructor = providers[id]
|
|
44
44
|
const secrets = this.resolveSecrets(name, Provider)
|
|
@@ -53,9 +53,8 @@ export class Factory {
|
|
|
53
53
|
return new Storage(provider, { name, provider: id })
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
private resolveSecrets
|
|
57
|
-
if (Class.SECRETS === undefined)
|
|
58
|
-
return {}
|
|
56
|
+
private resolveSecrets(storageName: string, Class: Constructor): Secrets {
|
|
57
|
+
if (Class.SECRETS === undefined) return {}
|
|
59
58
|
|
|
60
59
|
const secrets: Record<string, string | undefined> = {}
|
|
61
60
|
|
|
@@ -63,8 +62,10 @@ export class Factory {
|
|
|
63
62
|
const variable = `${ENV_PREFIX}_${storageName}_${secret.name}`.toUpperCase()
|
|
64
63
|
const value = process.env[variable]
|
|
65
64
|
|
|
66
|
-
assert.ok(
|
|
67
|
-
|
|
65
|
+
assert.ok(
|
|
66
|
+
secret.optional === true || value !== undefined,
|
|
67
|
+
`'${variable}' is not defined`
|
|
68
|
+
)
|
|
68
69
|
|
|
69
70
|
secrets[secret.name] = value
|
|
70
71
|
}
|
package/source/Provider.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as assert from 'node:assert'
|
|
2
2
|
import type { Metadata, Stream } from './Entry.js'
|
|
3
3
|
import type { Readable } from 'node:stream'
|
|
4
|
-
import type { Maybe } from '@toa.io/types'
|
|
4
|
+
import type { Maybe } from '@toa.io/core/types'
|
|
5
5
|
import type { Secret, Secrets } from './Secrets.js'
|
|
6
6
|
|
|
7
7
|
export abstract class Provider<Options = unknown> {
|
|
@@ -9,24 +9,28 @@ export abstract class Provider<Options = unknown> {
|
|
|
9
9
|
public readonly root?: string
|
|
10
10
|
public readonly options: Options
|
|
11
11
|
|
|
12
|
-
protected constructor
|
|
12
|
+
protected constructor(options: Options, secrets?: Secrets) {
|
|
13
13
|
this.options = options
|
|
14
14
|
|
|
15
15
|
new.target.SECRETS?.forEach(({ name, optional }) =>
|
|
16
|
-
assert.ok(
|
|
16
|
+
assert.ok(
|
|
17
|
+
optional === true || secrets?.[name] !== undefined,
|
|
18
|
+
`Missing secret '${name}'`
|
|
19
|
+
)
|
|
20
|
+
)
|
|
17
21
|
}
|
|
18
22
|
|
|
19
|
-
public abstract get
|
|
23
|
+
public abstract get(path: string, options?: unknown): Promise<Maybe<Stream>>
|
|
20
24
|
|
|
21
|
-
public abstract head
|
|
25
|
+
public abstract head(path: string): Promise<Maybe<Metadata>>
|
|
22
26
|
|
|
23
|
-
public abstract put
|
|
27
|
+
public abstract put(path: string, stream: Readable): Promise<void>
|
|
24
28
|
|
|
25
|
-
public abstract commit
|
|
29
|
+
public abstract commit(path: string, metadata: Metadata): Promise<void>
|
|
26
30
|
|
|
27
|
-
public abstract delete
|
|
31
|
+
public abstract delete(path: string): Promise<void>
|
|
28
32
|
|
|
29
|
-
public abstract move
|
|
33
|
+
public abstract move(from: string, to: string): Promise<Maybe<void>>
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
export interface Constructor<Options = any> {
|
package/source/Scanner.ts
CHANGED
|
@@ -15,7 +15,7 @@ export class Scanner extends PassThrough {
|
|
|
15
15
|
private completed = false
|
|
16
16
|
private readonly chunks: Buffer[] = []
|
|
17
17
|
|
|
18
|
-
public constructor
|
|
18
|
+
public constructor(control?: ScanOptions) {
|
|
19
19
|
super()
|
|
20
20
|
|
|
21
21
|
this.claim = control?.claim
|
|
@@ -23,11 +23,15 @@ export class Scanner extends PassThrough {
|
|
|
23
23
|
this.limit = control?.limit
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
public digest
|
|
26
|
+
public digest(): string {
|
|
27
27
|
return this.hash.digest('hex')
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
public override _transform
|
|
30
|
+
public override _transform(
|
|
31
|
+
buffer: Buffer,
|
|
32
|
+
encoding: BufferEncoding,
|
|
33
|
+
callback: TransformCallback
|
|
34
|
+
): void {
|
|
31
35
|
super._transform(buffer, encoding, callback)
|
|
32
36
|
|
|
33
37
|
this.process(buffer)
|
|
@@ -37,8 +41,7 @@ export class Scanner extends PassThrough {
|
|
|
37
41
|
this.size += buffer.length
|
|
38
42
|
this.hash.update(buffer)
|
|
39
43
|
|
|
40
|
-
if (this.completed)
|
|
41
|
-
return
|
|
44
|
+
if (this.completed) return
|
|
42
45
|
|
|
43
46
|
if (this.position + buffer.length > HEADER_SIZE)
|
|
44
47
|
buffer = buffer.subarray(0, HEADER_SIZE - this.position)
|
|
@@ -46,24 +49,20 @@ export class Scanner extends PassThrough {
|
|
|
46
49
|
this.chunks.push(buffer)
|
|
47
50
|
this.position += buffer.length
|
|
48
51
|
|
|
49
|
-
if (this.position === HEADER_SIZE)
|
|
50
|
-
this.complete()
|
|
52
|
+
if (this.position === HEADER_SIZE) this.complete()
|
|
51
53
|
|
|
52
54
|
if (this.limit !== undefined && this.size > this.limit)
|
|
53
55
|
this.interrupt(ERR_LIMIT_EXCEEDED)
|
|
54
56
|
}
|
|
55
57
|
|
|
56
|
-
private complete
|
|
58
|
+
private complete(): void {
|
|
57
59
|
const header = Buffer.concat(this.chunks).toString('hex')
|
|
58
60
|
|
|
59
|
-
const signature = SIGNATURES
|
|
60
|
-
.
|
|
61
|
-
const sig = header.slice(off, off + hex.length)
|
|
61
|
+
const signature = SIGNATURES.find(({ off, hex, expression }) => {
|
|
62
|
+
const sig = header.slice(off, off + hex.length)
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
: expression.test(sig)
|
|
66
|
-
})
|
|
64
|
+
return expression === undefined ? sig === hex : expression.test(sig)
|
|
65
|
+
})
|
|
67
66
|
|
|
68
67
|
const type = signature?.type ?? this.claim
|
|
69
68
|
|
|
@@ -76,33 +75,30 @@ export class Scanner extends PassThrough {
|
|
|
76
75
|
this.completed = true
|
|
77
76
|
}
|
|
78
77
|
|
|
79
|
-
private verify
|
|
80
|
-
if (this.claim === undefined || this.claim === 'application/octet-stream')
|
|
81
|
-
return
|
|
78
|
+
private verify(signature: Signature | undefined): void {
|
|
79
|
+
if (this.claim === undefined || this.claim === 'application/octet-stream') return
|
|
82
80
|
|
|
83
|
-
const mismatch =
|
|
84
|
-
|
|
85
|
-
|
|
81
|
+
const mismatch =
|
|
82
|
+
signature === undefined
|
|
83
|
+
? KNOWN_TYPES.has(this.claim)
|
|
84
|
+
: this.claim !== signature.type
|
|
86
85
|
|
|
87
|
-
if (mismatch)
|
|
88
|
-
this.interrupt(ERR_TYPE_MISMATCH)
|
|
86
|
+
if (mismatch) this.interrupt(ERR_TYPE_MISMATCH)
|
|
89
87
|
}
|
|
90
88
|
|
|
91
|
-
private match
|
|
92
|
-
if (this.accept === undefined)
|
|
93
|
-
return
|
|
89
|
+
private match(type: string): void {
|
|
90
|
+
if (this.accept === undefined) return
|
|
94
91
|
|
|
95
92
|
const unacceptable = this.negotiate(this.accept, [type]) === null
|
|
96
93
|
|
|
97
|
-
if (unacceptable)
|
|
98
|
-
this.interrupt(ERR_NOT_ACCEPTABLE)
|
|
94
|
+
if (unacceptable) this.interrupt(ERR_NOT_ACCEPTABLE)
|
|
99
95
|
}
|
|
100
96
|
|
|
101
|
-
private negotiate
|
|
97
|
+
private negotiate(accept: string, type: string[]): string | null {
|
|
102
98
|
return new Negotiator({ headers: { accept } }).mediaType(type) ?? null
|
|
103
99
|
}
|
|
104
100
|
|
|
105
|
-
private interrupt
|
|
101
|
+
private interrupt(error: Error): void {
|
|
106
102
|
this.completed = true
|
|
107
103
|
this.error = error
|
|
108
104
|
this.destroy(error)
|
|
@@ -132,8 +128,10 @@ const SIGNATURES: Signature[] = [
|
|
|
132
128
|
signature.hex = signature.hex.replaceAll(' ', '')
|
|
133
129
|
|
|
134
130
|
if (signature.hex.includes('??')) {
|
|
135
|
-
const expression = signature.hex.replaceAll(
|
|
136
|
-
(
|
|
131
|
+
const expression = signature.hex.replaceAll(
|
|
132
|
+
/(?<wildcards>\?{1,24})/g,
|
|
133
|
+
(_, wildcards) => `[0-9a-f]{${wildcards.length}}`
|
|
134
|
+
)
|
|
137
135
|
|
|
138
136
|
signature.expression = new RegExp(expression, 'i')
|
|
139
137
|
}
|
|
@@ -141,8 +139,8 @@ const SIGNATURES: Signature[] = [
|
|
|
141
139
|
return signature
|
|
142
140
|
})
|
|
143
141
|
|
|
144
|
-
const HEADER_SIZE =
|
|
145
|
-
.reduce((max, { off, hex }) => Math.max(max, off + hex.length), 0) / 2
|
|
142
|
+
const HEADER_SIZE =
|
|
143
|
+
SIGNATURES.reduce((max, { off, hex }) => Math.max(max, off + hex.length), 0) / 2
|
|
146
144
|
|
|
147
145
|
const KNOWN_TYPES = new Set(SIGNATURES.map(({ type }) => type))
|
|
148
146
|
|
package/source/Storage.test.ts
CHANGED
|
@@ -41,7 +41,7 @@ describe('put', () => {
|
|
|
41
41
|
const stream = createReadStream('lenna.png')
|
|
42
42
|
|
|
43
43
|
startCreation = Date.now()
|
|
44
|
-
lenna = await storage.put(dir, stream) as Entry
|
|
44
|
+
lenna = (await storage.put(dir, stream)) as Entry
|
|
45
45
|
path = `${dir}/${lenna.id}`
|
|
46
46
|
|
|
47
47
|
assert(!(lenna instanceof Error))
|
|
@@ -76,7 +76,7 @@ describe('put', () => {
|
|
|
76
76
|
})
|
|
77
77
|
|
|
78
78
|
it('should create metadata', async () => {
|
|
79
|
-
const entry = await storage.head(path) as Entry
|
|
79
|
+
const entry = (await storage.head(path)) as Entry
|
|
80
80
|
|
|
81
81
|
assert.partialDeepStrictEqual(entry, {
|
|
82
82
|
type: 'image/png',
|
|
@@ -89,7 +89,7 @@ describe('put', () => {
|
|
|
89
89
|
|
|
90
90
|
it('should set timestamp', async () => {
|
|
91
91
|
const now = Date.now() + 1000 // Cloudinary has 1s resolution
|
|
92
|
-
const entry = await storage.get(path) as Stream
|
|
92
|
+
const entry = (await storage.get(path)) as Stream
|
|
93
93
|
const created = new Date(entry.created).getTime()
|
|
94
94
|
|
|
95
95
|
assert.ok(created <= now)
|
|
@@ -100,14 +100,13 @@ describe('put', () => {
|
|
|
100
100
|
const stream = createReadStream('lenna.png')
|
|
101
101
|
const id = Math.random().toString(36).slice(2)
|
|
102
102
|
|
|
103
|
-
const entry = await storage.put(dir, stream, { id }) as Entry
|
|
103
|
+
const entry = (await storage.put(dir, stream, { id })) as Entry
|
|
104
104
|
|
|
105
105
|
assert.strictEqual(entry.id, id)
|
|
106
106
|
|
|
107
107
|
const check = await storage.head(`${dir}/${id}`)
|
|
108
108
|
|
|
109
|
-
if (check instanceof Error)
|
|
110
|
-
throw check
|
|
109
|
+
if (check instanceof Error) throw check
|
|
111
110
|
|
|
112
111
|
assert.strictEqual(check.id, id)
|
|
113
112
|
})
|
|
@@ -120,13 +119,13 @@ describe('get, head', () => {
|
|
|
120
119
|
beforeEach(async () => {
|
|
121
120
|
const stream = createReadStream('lenna.png')
|
|
122
121
|
|
|
123
|
-
lenna = await storage.put(dir, stream) as Entry
|
|
122
|
+
lenna = (await storage.put(dir, stream)) as Entry
|
|
124
123
|
|
|
125
124
|
path = `${dir}/${lenna.id}`
|
|
126
125
|
})
|
|
127
126
|
|
|
128
127
|
it('should get', async () => {
|
|
129
|
-
const entry = await storage.get(path) as Stream
|
|
128
|
+
const entry = (await storage.get(path)) as Stream
|
|
130
129
|
const stored = await buffer(entry.stream)
|
|
131
130
|
const buf = await buffer(createReadStream('lenna.png'))
|
|
132
131
|
|
|
@@ -134,26 +133,26 @@ describe('get, head', () => {
|
|
|
134
133
|
})
|
|
135
134
|
|
|
136
135
|
it('should get entry', async () => {
|
|
137
|
-
const entry = await storage.head(path) as Entry
|
|
136
|
+
const entry = (await storage.head(path)) as Entry
|
|
138
137
|
|
|
139
138
|
assert.strictEqual(entry.id, lenna.id)
|
|
140
139
|
})
|
|
141
140
|
|
|
142
141
|
if (suite.provider === 'cloudinary') {
|
|
143
142
|
it('should return cloudinary url', async () => {
|
|
144
|
-
const entry = await storage.head(`${path}.jpeg`) as Entry
|
|
143
|
+
const entry = (await storage.head(`${path}.jpeg`)) as Entry
|
|
145
144
|
|
|
146
145
|
assert.match(entry.attributes.url, /^https:\/\//)
|
|
147
146
|
})
|
|
148
147
|
|
|
149
148
|
it('should support conditions', async () => {
|
|
150
|
-
const entry = await storage.head(`${path}.vertical.jpeg`) as Entry
|
|
149
|
+
const entry = (await storage.head(`${path}.vertical.jpeg`)) as Entry
|
|
151
150
|
|
|
152
151
|
assert.strictEqual(entry.attributes.url.includes('/if_w_gt_h/a_90/if_end/'), true)
|
|
153
152
|
})
|
|
154
153
|
|
|
155
154
|
it('should return entry id when requested with extensions', async () => {
|
|
156
|
-
const entry = await storage.head(`${path}.jpeg`) as Entry
|
|
155
|
+
const entry = (await storage.head(`${path}.jpeg`)) as Entry
|
|
157
156
|
|
|
158
157
|
assert.strictEqual(entry.id, lenna.id)
|
|
159
158
|
})
|
|
@@ -166,7 +165,7 @@ describe('delete', () => {
|
|
|
166
165
|
beforeEach(async () => {
|
|
167
166
|
const stream = createReadStream('lenna.png')
|
|
168
167
|
|
|
169
|
-
lenna = await storage.put(dir, stream) as Entry
|
|
168
|
+
lenna = (await storage.put(dir, stream)) as Entry
|
|
170
169
|
})
|
|
171
170
|
|
|
172
171
|
it('should delete entry', async () => {
|
|
@@ -193,21 +192,21 @@ describe('signatures', () => {
|
|
|
193
192
|
for (const type of ['jpeg', 'gif', 'webp', 'heic', 'jxl', 'avif'])
|
|
194
193
|
it(`should detect image/${type}`, async () => {
|
|
195
194
|
const stream = createReadStream('sample.' + type)
|
|
196
|
-
const entry = await storage.put(dir, stream) as Entry
|
|
195
|
+
const entry = (await storage.put(dir, stream)) as Entry
|
|
197
196
|
|
|
198
197
|
assert.deepStrictEqual(entry['type'], 'image/' + type)
|
|
199
198
|
})
|
|
200
199
|
|
|
201
200
|
for (const type of ['avi'])
|
|
202
|
-
|
|
201
|
+
it(`should detect video/${type}`, async () => {
|
|
203
202
|
const stream = createReadStream('sample.' + type)
|
|
204
|
-
const entry = await storage.put(dir, stream) as Entry
|
|
203
|
+
const entry = (await storage.put(dir, stream)) as Entry
|
|
205
204
|
|
|
206
205
|
assert.deepStrictEqual(entry['type'], 'video/' + type)
|
|
207
206
|
})
|
|
208
207
|
|
|
209
208
|
for (const type of ['wav'])
|
|
210
|
-
|
|
209
|
+
it(`should detect audio/${type}`, async () => {
|
|
211
210
|
const stream = createReadStream('sample.' + type)
|
|
212
211
|
const entry = (await storage.put(dir, stream)) as Entry
|
|
213
212
|
|
|
@@ -222,7 +221,7 @@ describe('signatures', () => {
|
|
|
222
221
|
})
|
|
223
222
|
})
|
|
224
223
|
|
|
225
|
-
it(
|
|
224
|
+
it("should return error if type doesn't match", async () => {
|
|
226
225
|
const stream = createReadStream('sample.jpeg')
|
|
227
226
|
|
|
228
227
|
const result = await storage.put(dir, stream, { claim: 'image/png' })
|
|
@@ -278,7 +277,7 @@ it('should accept wildcard types', async () => {
|
|
|
278
277
|
|
|
279
278
|
it('should handle root entries', async () => {
|
|
280
279
|
const stream = createReadStream('sample.jpeg')
|
|
281
|
-
const result = await storage.put('/', stream) as Entry
|
|
280
|
+
const result = (await storage.put('/', stream)) as Entry
|
|
282
281
|
|
|
283
282
|
assert.ok(!(result instanceof Error))
|
|
284
283
|
|
|
@@ -299,7 +298,7 @@ if (suite.provider !== 'cloudinary')
|
|
|
299
298
|
it('should add origin attribute', async () => {
|
|
300
299
|
const origin = 'https://example.com/image.jpeg'
|
|
301
300
|
const stream = createReadStream('sample.jpeg')
|
|
302
|
-
const result = await storage.put('/origins', stream, { origin }) as Entry
|
|
301
|
+
const result = (await storage.put('/origins', stream, { origin })) as Entry
|
|
303
302
|
|
|
304
303
|
assert.ok(!(result instanceof Error))
|
|
305
304
|
|
package/source/Storage.ts
CHANGED
|
@@ -11,16 +11,16 @@ export class Storage<T extends Provider = Provider> {
|
|
|
11
11
|
private readonly provider: T
|
|
12
12
|
private readonly scope?: Scope
|
|
13
13
|
|
|
14
|
-
public constructor
|
|
14
|
+
public constructor(provider: T, scope?: Scope) {
|
|
15
15
|
this.provider = provider
|
|
16
16
|
this.scope = scope
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
public options
|
|
19
|
+
public options(): T['options'] {
|
|
20
20
|
return this.provider.options
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
public async put
|
|
23
|
+
public async put(path: string, stream: Readable, options?: Options): Maybe<Entry> {
|
|
24
24
|
return await console.span(this.span('put', path), async () => {
|
|
25
25
|
const scanner = new Scanner(options)
|
|
26
26
|
const pipe = stream.pipe(scanner).on('error', () => undefined)
|
|
@@ -31,14 +31,14 @@ export class Storage<T extends Provider = Provider> {
|
|
|
31
31
|
* Provider can return or throw an error.
|
|
32
32
|
* If thrown error is TYPE_MISMATCH from the Scanner, it should be returned.
|
|
33
33
|
*/
|
|
34
|
-
const error: Error | undefined = await this.provider
|
|
34
|
+
const error: Error | undefined = await this.provider
|
|
35
|
+
.put(location, pipe)
|
|
35
36
|
.catch((error: any) => {
|
|
36
37
|
if (error === scanner.error) return error
|
|
37
38
|
else throw error
|
|
38
39
|
})
|
|
39
40
|
|
|
40
|
-
if (error instanceof Error)
|
|
41
|
-
return error
|
|
41
|
+
if (error instanceof Error) return error
|
|
42
42
|
|
|
43
43
|
const metadata: Entry = {
|
|
44
44
|
id,
|
|
@@ -49,8 +49,7 @@ export class Storage<T extends Provider = Provider> {
|
|
|
49
49
|
attributes: options?.attributes ?? {}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
if (options?.origin !== undefined)
|
|
53
|
-
metadata.attributes.origin = options.origin
|
|
52
|
+
if (options?.origin !== undefined) metadata.attributes.origin = options.origin
|
|
54
53
|
|
|
55
54
|
await this.provider.commit(location, metadata)
|
|
56
55
|
|
|
@@ -58,7 +57,7 @@ export class Storage<T extends Provider = Provider> {
|
|
|
58
57
|
})
|
|
59
58
|
}
|
|
60
59
|
|
|
61
|
-
public async get
|
|
60
|
+
public async get(path: string, options?: unknown): Maybe<Stream> {
|
|
62
61
|
return await console.span(this.span('get', dirname(path)), async () => {
|
|
63
62
|
const location = this.locate(path)
|
|
64
63
|
|
|
@@ -66,14 +65,13 @@ export class Storage<T extends Provider = Provider> {
|
|
|
66
65
|
})
|
|
67
66
|
}
|
|
68
67
|
|
|
69
|
-
public async head
|
|
68
|
+
public async head(path: string): Promise<Maybe<Entry>> {
|
|
70
69
|
return await console.span(this.span('head', dirname(path)), async () => {
|
|
71
70
|
const id = basename(path).split('.')[0]
|
|
72
71
|
const location = this.locate(path)
|
|
73
72
|
const metadata = await this.provider.head(location)
|
|
74
73
|
|
|
75
|
-
if (metadata instanceof Error)
|
|
76
|
-
return metadata
|
|
74
|
+
if (metadata instanceof Error) return metadata
|
|
77
75
|
|
|
78
76
|
return {
|
|
79
77
|
id,
|
|
@@ -82,7 +80,7 @@ export class Storage<T extends Provider = Provider> {
|
|
|
82
80
|
})
|
|
83
81
|
}
|
|
84
82
|
|
|
85
|
-
public async delete
|
|
83
|
+
public async delete(path: string): Maybe<void> {
|
|
86
84
|
return await console.span(this.span('delete', dirname(path)), async () => {
|
|
87
85
|
const location = this.locate(path)
|
|
88
86
|
|
|
@@ -90,20 +88,20 @@ export class Storage<T extends Provider = Provider> {
|
|
|
90
88
|
})
|
|
91
89
|
}
|
|
92
90
|
|
|
93
|
-
public path
|
|
91
|
+
public path(): string | null {
|
|
94
92
|
return this.provider.root ?? null
|
|
95
93
|
}
|
|
96
94
|
|
|
97
|
-
private locate
|
|
95
|
+
private locate(...rel: string[]): string {
|
|
98
96
|
return join(ENTRIES, ...rel)
|
|
99
97
|
}
|
|
100
98
|
|
|
101
|
-
private span
|
|
99
|
+
private span(method: string, path: string): SpanOptions {
|
|
102
100
|
return {
|
|
103
101
|
name: `${method} ${this.scope?.name ?? 'storage'}`,
|
|
104
102
|
kind: 'client',
|
|
105
103
|
attributes: {
|
|
106
|
-
...this.scope === undefined ? {} : { provider: this.scope.provider },
|
|
104
|
+
...(this.scope === undefined ? {} : { provider: this.scope.provider }),
|
|
107
105
|
path
|
|
108
106
|
}
|
|
109
107
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Contribution } from '@toa.io/core/types'
|
|
2
|
+
|
|
3
|
+
/** What this extension puts on the context of a component that declares it. */
|
|
4
|
+
export function context(declaration: string[]): Contribution | null {
|
|
5
|
+
if (!Array.isArray(declaration) || declaration.length === 0) return null
|
|
6
|
+
|
|
7
|
+
const names = declaration.map((name) => JSON.stringify(name)).join(' | ')
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
name: 'storages',
|
|
11
|
+
type: `Record<${names}, Storage>`,
|
|
12
|
+
imports: { '@toa.io/extensions.storages': ['Storage'] }
|
|
13
|
+
}
|
|
14
|
+
}
|