@toa.io/extensions.storages 0.24.0-alpha.21 → 0.24.0-alpha.23
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 +6 -5
- package/readme.md +10 -12
- package/schemas/annotation.cos.yaml +10 -0
- package/schemas/fs.cos.yaml +9 -0
- package/schemas/mem.cos.yaml +6 -0
- package/schemas/s3.cos.yaml +16 -0
- package/schemas/test.cos.yaml +8 -0
- package/schemas/tmp.cos.yaml +9 -0
- package/source/Annotation.ts +39 -0
- package/source/Factory.ts +26 -23
- package/source/Provider.ts +8 -6
- package/source/Scanner.ts +3 -3
- package/source/Storage.test.ts +4 -3
- package/source/Storage.ts +13 -6
- package/source/deployment.ts +12 -23
- package/source/providers/Declaration.ts +10 -0
- package/source/providers/FileSystem.ts +5 -5
- package/source/providers/S3.test.ts +4 -5
- package/source/providers/S3.ts +17 -12
- package/source/providers/Temporary.ts +6 -4
- package/source/providers/Test.ts +3 -3
- package/source/providers/index.test.ts +5 -5
- package/source/providers/index.ts +5 -3
- package/source/schemas.test.ts +58 -0
- package/source/schemas.ts +15 -0
- package/source/test/util.ts +14 -8
- package/transpiled/Annotation.d.ts +3 -0
- package/transpiled/Annotation.js +57 -0
- package/transpiled/Annotation.js.map +1 -0
- package/transpiled/Factory.d.ts +2 -3
- package/transpiled/Factory.js +16 -15
- package/transpiled/Factory.js.map +1 -1
- package/transpiled/Provider.d.ts +3 -5
- package/transpiled/Provider.js +2 -2
- package/transpiled/Provider.js.map +1 -1
- package/transpiled/Scanner.d.ts +2 -2
- package/transpiled/Scanner.js +2 -2
- package/transpiled/Scanner.js.map +1 -1
- package/transpiled/Storage.d.ts +6 -2
- package/transpiled/Storage.js +5 -5
- package/transpiled/Storage.js.map +1 -1
- package/transpiled/deployment.d.ts +1 -8
- package/transpiled/deployment.js +7 -14
- package/transpiled/deployment.js.map +1 -1
- package/transpiled/providers/Declaration.d.ts +14 -0
- package/transpiled/providers/Declaration.js +3 -0
- package/transpiled/providers/Declaration.js.map +1 -0
- package/transpiled/providers/FileSystem.d.ts +2 -1
- package/transpiled/providers/FileSystem.js +3 -5
- package/transpiled/providers/FileSystem.js.map +1 -1
- package/transpiled/providers/S3.d.ts +4 -2
- package/transpiled/providers/S3.js +13 -12
- package/transpiled/providers/S3.js.map +1 -1
- package/transpiled/providers/Temporary.d.ts +3 -2
- package/transpiled/providers/Temporary.js +3 -2
- package/transpiled/providers/Temporary.js.map +1 -1
- package/transpiled/providers/Test.d.ts +2 -2
- package/transpiled/providers/Test.js +2 -2
- package/transpiled/providers/Test.js.map +1 -1
- package/transpiled/providers/index.d.ts +3 -2
- package/transpiled/providers/index.js +3 -3
- package/transpiled/providers/index.js.map +1 -1
- package/transpiled/schemas.d.ts +9 -0
- package/transpiled/schemas.js +14 -0
- package/transpiled/schemas.js.map +1 -0
- package/transpiled/test/util.d.ts +28 -17
- package/transpiled/test/util.js +9 -5
- package/transpiled/test/util.js.map +1 -1
- package/transpiled/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toa.io/extensions.storages",
|
|
3
|
-
"version": "0.24.0-alpha.
|
|
3
|
+
"version": "0.24.0-alpha.23",
|
|
4
4
|
"description": "Toa Storages",
|
|
5
5
|
"author": "temich <tema.gurtovoy@gmail.com>",
|
|
6
6
|
"homepage": "https://github.com/toa-io/toa#readme",
|
|
@@ -25,18 +25,19 @@
|
|
|
25
25
|
"transpile": "npx tsc"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@toa.io/streams": "0.24.0-alpha.
|
|
28
|
+
"@toa.io/streams": "0.24.0-alpha.23",
|
|
29
29
|
"dotenv": "16.3.1"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@aws-sdk/client-s3": "3.481.0",
|
|
33
33
|
"@aws-sdk/lib-storage": "3.481.0",
|
|
34
|
-
"@toa.io/
|
|
35
|
-
"@toa.io/
|
|
34
|
+
"@toa.io/agent": "0.24.0-alpha.23",
|
|
35
|
+
"@toa.io/generic": "0.24.0-alpha.23",
|
|
36
|
+
"@toa.io/schemas": "0.24.0-alpha.23",
|
|
36
37
|
"error-value": "0.3.0",
|
|
37
38
|
"matchacho": "0.6.0",
|
|
38
39
|
"msgpackr": "1.10.1",
|
|
39
40
|
"smithy-node-native-fetch": "1.0.6"
|
|
40
41
|
},
|
|
41
|
-
"gitHead": "
|
|
42
|
+
"gitHead": "df9de3cbb530e8f985a660bca0bf65bd027dbb01"
|
|
42
43
|
}
|
package/readme.md
CHANGED
|
@@ -47,29 +47,28 @@ async function effect (_, context) {
|
|
|
47
47
|
|
|
48
48
|
> `Maybe<T> = T | Error`
|
|
49
49
|
|
|
50
|
-
#### `async put(path: string, stream: Readable,
|
|
50
|
+
#### `async put(path: string, stream: Readable, options?: Options): Maybe<Entry>`
|
|
51
51
|
|
|
52
52
|
```
|
|
53
|
-
interface
|
|
53
|
+
interface Options {
|
|
54
54
|
claim?: string
|
|
55
55
|
accept?: string
|
|
56
|
+
meta?: Record<string, string>
|
|
56
57
|
}
|
|
57
58
|
```
|
|
58
59
|
|
|
59
|
-
Add a BLOB to the storage and create an entry under specified `path
|
|
60
|
+
Add a BLOB to the storage and create an entry under specified `path`, with given meta-information.
|
|
60
61
|
|
|
61
62
|
BLOB type is identified
|
|
62
63
|
using [magick numbers](https://github.com/sindresorhus/file-type).
|
|
63
64
|
|
|
64
65
|
If the `type` argument is specified and the value of the `claim` does not match the detected BLOB
|
|
65
|
-
type, then
|
|
66
|
-
a `TYPE_MISMATCH` error is returned.
|
|
66
|
+
type, then a `TYPE_MISMATCH` error is returned.
|
|
67
67
|
If the BLOB type cannot be identified and the value of the `claim` is not in the list of known
|
|
68
|
-
types, then the given
|
|
69
|
-
value is used.
|
|
68
|
+
types, then the given value is used.
|
|
70
69
|
If the list of [acceptable types](https://datatracker.ietf.org/doc/html/rfc2616#section-14.1) is
|
|
71
|
-
passed and the type of
|
|
72
|
-
|
|
70
|
+
passed and the type of the BLOB does not match any of its values, then a `NOT_ACCEPTABLE` error is
|
|
71
|
+
returned.
|
|
73
72
|
|
|
74
73
|
Known types
|
|
75
74
|
are: `image/jpeg`, `image/png`, `image/gif`, `image/webp`, `image/heic`, `image/jxl`, `image/avif`.
|
|
@@ -155,7 +154,6 @@ and [`toa env`](/runtime/cli/readme.md#env)
|
|
|
155
154
|
for local environment.
|
|
156
155
|
`endpoint` parameter is optional.
|
|
157
156
|
|
|
158
|
-
|
|
159
157
|
### Filesystem
|
|
160
158
|
|
|
161
159
|
Annotation format is:
|
|
@@ -177,7 +175,7 @@ Annotation format is:
|
|
|
177
175
|
storages:
|
|
178
176
|
photos@dev:
|
|
179
177
|
provider: tmp
|
|
180
|
-
|
|
178
|
+
directory: my-app-tmp
|
|
181
179
|
```
|
|
182
180
|
|
|
183
181
|
### Memory
|
|
@@ -189,7 +187,7 @@ Annotation value format is:
|
|
|
189
187
|
```yaml
|
|
190
188
|
storages:
|
|
191
189
|
photos@dev:
|
|
192
|
-
provider:
|
|
190
|
+
provider: mem
|
|
193
191
|
```
|
|
194
192
|
|
|
195
193
|
## Deduplication
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
2
|
+
import { providers } from './providers'
|
|
3
|
+
import * as schemas from './schemas'
|
|
4
|
+
import type { Declaration } from './providers'
|
|
5
|
+
import type { Schema } from '@toa.io/schemas'
|
|
6
|
+
|
|
7
|
+
export type Annotation = Record<string, Declaration>
|
|
8
|
+
|
|
9
|
+
export function validateAnnotation (annotation: unknown): asserts annotation is Annotation {
|
|
10
|
+
try {
|
|
11
|
+
schemas.annotation.validate(annotation)
|
|
12
|
+
} catch (error) {
|
|
13
|
+
explain(annotation)
|
|
14
|
+
|
|
15
|
+
// if all declarations are valid, re-throw the error
|
|
16
|
+
throw error
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
It is required because `oneOf` schema is used for the annotation validation.
|
|
22
|
+
*/
|
|
23
|
+
function explain (annotation: unknown): void {
|
|
24
|
+
assert.ok(typeof annotation === 'object' && annotation !== null,
|
|
25
|
+
'TOA_STORAGES is not an object')
|
|
26
|
+
|
|
27
|
+
for (const declaration of Object.values(annotation)) {
|
|
28
|
+
assert.ok(typeof declaration === 'object' && declaration !== null &&
|
|
29
|
+
declaration.provider in providers,
|
|
30
|
+
`Unknown provider '${declaration.provider}'`)
|
|
31
|
+
|
|
32
|
+
assert.ok(declaration.provider in schemas,
|
|
33
|
+
`No schema for provider '${declaration.provider}'`)
|
|
34
|
+
|
|
35
|
+
const schema: Schema<Declaration> = schemas[declaration.provider as keyof typeof providers]
|
|
36
|
+
|
|
37
|
+
schema.validate(declaration, `Storage '${declaration.provider}' annotation`)
|
|
38
|
+
}
|
|
39
|
+
}
|
package/source/Factory.ts
CHANGED
|
@@ -3,18 +3,23 @@ import { decode } from '@toa.io/generic'
|
|
|
3
3
|
import { providers } from './providers'
|
|
4
4
|
import { Storage, type Storages } from './Storage'
|
|
5
5
|
import { Aspect } from './Aspect'
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
6
|
+
import { SERIALIZATION_PREFIX } from './deployment'
|
|
7
|
+
import { validateAnnotation } from './Annotation'
|
|
8
|
+
import type { Declaration } from './providers'
|
|
9
|
+
import type { Annotation } from './Annotation'
|
|
10
|
+
import type { ProviderConstructor, ProviderSecrets } from './Provider'
|
|
8
11
|
|
|
9
12
|
export class Factory {
|
|
10
|
-
private readonly
|
|
13
|
+
private readonly annotation: Annotation
|
|
11
14
|
|
|
12
15
|
public constructor () {
|
|
13
16
|
const env = process.env.TOA_STORAGES
|
|
14
17
|
|
|
15
|
-
|
|
18
|
+
assert.ok(env !== undefined, 'TOA_STORAGES is not defined')
|
|
16
19
|
|
|
17
|
-
this.
|
|
20
|
+
this.annotation = decode(env)
|
|
21
|
+
|
|
22
|
+
validateAnnotation(this.annotation)
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
public aspect (): Aspect {
|
|
@@ -23,35 +28,33 @@ export class Factory {
|
|
|
23
28
|
return new Aspect(storages)
|
|
24
29
|
}
|
|
25
30
|
|
|
26
|
-
public createStorage (componentName: string, { provider: providerId, ...props }: any): Storage {
|
|
27
|
-
validateProviderId(providerId)
|
|
28
|
-
|
|
29
|
-
const Provider = providers[providerId]
|
|
30
|
-
|
|
31
|
-
const secrets = this.resolveSecrets(componentName, Provider)
|
|
32
|
-
|
|
33
|
-
const provider = new Provider({ ...props, secrets })
|
|
34
|
-
|
|
35
|
-
return new Storage(provider)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
31
|
private createStorages (): Storages {
|
|
39
32
|
const storages: Storages = {}
|
|
40
33
|
|
|
41
|
-
for (const [
|
|
42
|
-
storages[
|
|
34
|
+
for (const [name, declaration] of Object.entries(this.annotation))
|
|
35
|
+
storages[name] = this.createStorage(name, declaration)
|
|
43
36
|
|
|
44
37
|
return storages
|
|
45
38
|
}
|
|
46
39
|
|
|
47
|
-
private
|
|
48
|
-
|
|
49
|
-
|
|
40
|
+
private createStorage (name: string, declaration: Declaration): Storage {
|
|
41
|
+
const { provider: providerId, ...options } = declaration
|
|
42
|
+
const Provider: ProviderConstructor = providers[providerId]
|
|
43
|
+
const secrets = this.resolveSecrets(name, Provider)
|
|
44
|
+
const provider = new Provider(options, secrets)
|
|
45
|
+
|
|
46
|
+
return new Storage(provider)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private resolveSecrets (storageName: string,
|
|
50
|
+
Class: ProviderConstructor): ProviderSecrets {
|
|
51
|
+
if (Class.SECRETS === undefined)
|
|
52
|
+
return {}
|
|
50
53
|
|
|
51
54
|
const secrets: Record<string, string | undefined> = {}
|
|
52
55
|
|
|
53
56
|
for (const secret of Class.SECRETS) {
|
|
54
|
-
const variable = `${SERIALIZATION_PREFIX}_${
|
|
57
|
+
const variable = `${SERIALIZATION_PREFIX}_${storageName}_${secret.name}`.toUpperCase()
|
|
55
58
|
const value = process.env[variable]
|
|
56
59
|
|
|
57
60
|
assert.ok(secret.optional === true || value !== undefined,
|
package/source/Provider.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { type Readable } from 'node:stream'
|
|
2
2
|
import * as assert from 'node:assert'
|
|
3
3
|
|
|
4
|
-
export
|
|
5
|
-
secrets?: Record<string, string>
|
|
6
|
-
}
|
|
4
|
+
export type ProviderSecrets<K extends string = string> = Record<K | string, string | undefined>
|
|
7
5
|
|
|
8
6
|
export interface ProviderSecret {
|
|
9
7
|
readonly name: string
|
|
@@ -13,19 +11,23 @@ export interface ProviderSecret {
|
|
|
13
11
|
export abstract class Provider<Options = void> {
|
|
14
12
|
public static readonly SECRETS: readonly ProviderSecret[] = []
|
|
15
13
|
|
|
16
|
-
public constructor (
|
|
14
|
+
public constructor (_: Options, secrets?: ProviderSecrets) {
|
|
17
15
|
for (const { name, optional = false } of new.target.SECRETS)
|
|
18
|
-
assert.ok(optional ||
|
|
16
|
+
assert.ok(optional || secrets?.[name] !== undefined, `Missing secret '${name}'`)
|
|
19
17
|
}
|
|
20
18
|
|
|
21
19
|
public abstract get (path: string): Promise<Readable | null>
|
|
20
|
+
|
|
22
21
|
public abstract put (path: string, filename: string, stream: Readable): Promise<void>
|
|
22
|
+
|
|
23
23
|
public abstract delete (path: string): Promise<void>
|
|
24
|
+
|
|
24
25
|
public abstract move (from: string, to: string): Promise<void>
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
export interface ProviderConstructor {
|
|
28
29
|
readonly SECRETS: readonly ProviderSecret[]
|
|
29
30
|
prototype: Provider
|
|
30
|
-
|
|
31
|
+
|
|
32
|
+
new (options: any, secrets?: ProviderSecrets): Provider
|
|
31
33
|
}
|
package/source/Scanner.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { PassThrough, type TransformCallback } from 'node:stream'
|
|
2
2
|
import { createHash } from 'node:crypto'
|
|
3
|
-
import { negotiate } from '@toa.io/
|
|
3
|
+
import { negotiate } from '@toa.io/agent'
|
|
4
4
|
import { Err } from 'error-value'
|
|
5
5
|
|
|
6
6
|
export class Scanner extends PassThrough {
|
|
@@ -15,7 +15,7 @@ export class Scanner extends PassThrough {
|
|
|
15
15
|
private detected = false
|
|
16
16
|
private readonly chunks: Buffer[] = []
|
|
17
17
|
|
|
18
|
-
public constructor (control?:
|
|
18
|
+
public constructor (control?: ScanOptions) {
|
|
19
19
|
super()
|
|
20
20
|
|
|
21
21
|
this.claim = control?.claim
|
|
@@ -121,7 +121,7 @@ const KNOWN_TYPES = new Set(SIGNATURES.map(({ type }) => type))
|
|
|
121
121
|
const ERR_TYPE_MISMATCH = Err('TYPE_MISMATCH')
|
|
122
122
|
const ERR_NOT_ACCEPTABLE = Err('NOT_ACCEPTABLE')
|
|
123
123
|
|
|
124
|
-
export interface
|
|
124
|
+
export interface ScanOptions {
|
|
125
125
|
claim?: string
|
|
126
126
|
accept?: string
|
|
127
127
|
}
|
package/source/Storage.test.ts
CHANGED
|
@@ -8,11 +8,12 @@ import { Storage } from './Storage'
|
|
|
8
8
|
import { suites } from './test/util'
|
|
9
9
|
import { type Entry } from './Entry'
|
|
10
10
|
import { providers } from './providers'
|
|
11
|
+
import type { ProviderConstructor } from './Provider'
|
|
11
12
|
|
|
12
13
|
let storage: Storage
|
|
13
14
|
let dir: string
|
|
14
15
|
|
|
15
|
-
const
|
|
16
|
+
const suite = suites[0]
|
|
16
17
|
|
|
17
18
|
beforeAll(async () => {
|
|
18
19
|
process.chdir(path.join(__dirname, 'test'))
|
|
@@ -21,8 +22,8 @@ beforeAll(async () => {
|
|
|
21
22
|
beforeEach(() => {
|
|
22
23
|
dir = '/' + randomUUID()
|
|
23
24
|
|
|
24
|
-
const Provider = providers[
|
|
25
|
-
const provider = new Provider(
|
|
25
|
+
const Provider: ProviderConstructor = providers[suite.provider]
|
|
26
|
+
const provider = new Provider(suite.options)
|
|
26
27
|
|
|
27
28
|
storage = new Storage(provider)
|
|
28
29
|
})
|
package/source/Storage.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { decode, encode } from 'msgpackr'
|
|
|
4
4
|
import { buffer, newid } from '@toa.io/generic'
|
|
5
5
|
import { Err } from 'error-value'
|
|
6
6
|
import { Scanner } from './Scanner'
|
|
7
|
-
import type {
|
|
7
|
+
import type { ScanOptions } from './Scanner'
|
|
8
8
|
import type { Provider } from './Provider'
|
|
9
9
|
import type { Entry } from './Entry'
|
|
10
10
|
|
|
@@ -15,8 +15,8 @@ export class Storage {
|
|
|
15
15
|
this.provider = provider
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
public async put (path: string, stream: Readable,
|
|
19
|
-
const scanner = new Scanner(
|
|
18
|
+
public async put (path: string, stream: Readable, options?: Options): Maybe<Entry> {
|
|
19
|
+
const scanner = new Scanner(options)
|
|
20
20
|
const pipe = stream.pipe(scanner)
|
|
21
21
|
const tempname = await this.transit(pipe)
|
|
22
22
|
|
|
@@ -27,7 +27,7 @@ export class Storage {
|
|
|
27
27
|
|
|
28
28
|
await this.persist(tempname, id)
|
|
29
29
|
|
|
30
|
-
return await this.create(path, id, scanner.size, scanner.type)
|
|
30
|
+
return await this.create(path, id, scanner.size, scanner.type, options?.meta)
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
public async get (path: string): Maybe<Entry> {
|
|
@@ -160,14 +160,15 @@ export class Storage {
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
// eslint-disable-next-line max-params
|
|
163
|
-
private async create
|
|
163
|
+
private async create
|
|
164
|
+
(path: string, id: string, size: number, type: string, meta: Meta = {}): Promise<Entry> {
|
|
164
165
|
const entry: Entry = {
|
|
165
166
|
id,
|
|
166
167
|
size,
|
|
167
168
|
type,
|
|
168
169
|
created: Date.now(),
|
|
169
170
|
variants: [],
|
|
170
|
-
meta
|
|
171
|
+
meta
|
|
171
172
|
}
|
|
172
173
|
|
|
173
174
|
const metafile = posix.join(path, entry.id)
|
|
@@ -235,4 +236,10 @@ interface Path {
|
|
|
235
236
|
variant: string | null
|
|
236
237
|
}
|
|
237
238
|
|
|
239
|
+
type Meta = Record<string, string>
|
|
240
|
+
|
|
241
|
+
interface Options extends ScanOptions {
|
|
242
|
+
meta?: Meta
|
|
243
|
+
}
|
|
244
|
+
|
|
238
245
|
export type Storages = Record<string, Storage>
|
package/source/deployment.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import * as assert from 'node:assert'
|
|
2
2
|
import { encode } from '@toa.io/generic'
|
|
3
3
|
import { providers } from './providers'
|
|
4
|
+
import { validateAnnotation } from './Annotation'
|
|
5
|
+
import type { Annotation } from './Annotation'
|
|
4
6
|
import type { Dependency, Variable } from '@toa.io/operations'
|
|
5
7
|
import type { context } from '@toa.io/norm'
|
|
6
8
|
|
|
7
9
|
export const SERIALIZATION_PREFIX = 'TOA_STORAGES'
|
|
8
10
|
|
|
9
|
-
export function deployment (instances: Instance[], annotation:
|
|
11
|
+
export function deployment (instances: Instance[], annotation: unknown): Dependency {
|
|
10
12
|
validate(instances, annotation)
|
|
11
13
|
|
|
12
14
|
const value = encode(annotation)
|
|
@@ -18,16 +20,11 @@ export function deployment (instances: Instance[], annotation: Annotation): Depe
|
|
|
18
20
|
}
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
function validate (instances: Instance[],
|
|
22
|
-
annotation
|
|
23
|
-
assert.ok(annotation !== undefined,
|
|
24
|
-
`Storages annotation is required by: '${instances
|
|
25
|
-
.map((i) => i.component.locator.id)
|
|
26
|
-
.join("', '")}'`)
|
|
23
|
+
function validate (instances: Instance[], annotation: unknown): asserts annotation is Annotation {
|
|
24
|
+
validateAnnotation(annotation)
|
|
27
25
|
|
|
28
|
-
for (const instance of instances)
|
|
29
|
-
|
|
30
|
-
for (const { provider } of Object.values(annotation)) validateProviderId(provider)
|
|
26
|
+
for (const instance of instances)
|
|
27
|
+
contains(instance, annotation)
|
|
31
28
|
}
|
|
32
29
|
|
|
33
30
|
function contains (instance: Instance, annotation: Annotation): void {
|
|
@@ -37,21 +34,17 @@ function contains (instance: Instance, annotation: Annotation): void {
|
|
|
37
34
|
`declared in '${instance.component.locator.id}'`)
|
|
38
35
|
}
|
|
39
36
|
|
|
40
|
-
|
|
41
|
-
assert.ok(typeof id === 'string' && id in providers, `Unknown storage provider '${id}'`)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function getSecrets (annotation: ValidatedAnnotation): Variable[] {
|
|
37
|
+
function getSecrets (annotation: Annotation): Variable[] {
|
|
45
38
|
const secrets: Variable[] = []
|
|
46
39
|
|
|
47
|
-
for (const [
|
|
48
|
-
const Provider = providers[
|
|
40
|
+
for (const [name, declaration] of Object.entries(annotation)) {
|
|
41
|
+
const Provider = providers[declaration.provider]
|
|
49
42
|
|
|
50
43
|
for (const secret of Provider.SECRETS)
|
|
51
44
|
secrets.push({
|
|
52
|
-
name: `${SERIALIZATION_PREFIX}_${
|
|
45
|
+
name: `${SERIALIZATION_PREFIX}_${name}_${secret.name}`.toUpperCase(),
|
|
53
46
|
secret: {
|
|
54
|
-
name: `toa-storages-${
|
|
47
|
+
name: `toa-storages-${name}`,
|
|
55
48
|
key: secret.name,
|
|
56
49
|
optional: secret.optional
|
|
57
50
|
}
|
|
@@ -61,8 +54,4 @@ function getSecrets (annotation: ValidatedAnnotation): Variable[] {
|
|
|
61
54
|
return secrets
|
|
62
55
|
}
|
|
63
56
|
|
|
64
|
-
type Annotation = Record<string, { [k: string]: unknown, provider: string }>
|
|
65
|
-
type ValidatedAnnotation = Readonly<
|
|
66
|
-
Record<string, { [k: string]: unknown, provider: keyof typeof providers }>
|
|
67
|
-
>
|
|
68
57
|
export type Instance = context.Dependency<string[]>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { S3Options } from './S3'
|
|
2
|
+
import type { FileSystemOptions } from './FileSystem'
|
|
3
|
+
import type { TemporaryOptions } from './Temporary'
|
|
4
|
+
|
|
5
|
+
export type Declaration =
|
|
6
|
+
({ provider: 's3' } & S3Options)
|
|
7
|
+
| ({ provider: 'fs' } & FileSystemOptions)
|
|
8
|
+
| ({ provider: 'tmp' } & TemporaryOptions)
|
|
9
|
+
| ({ provider: 'mem' })
|
|
10
|
+
| ({ provider: 'test' } & TemporaryOptions)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { type Readable } from 'node:stream'
|
|
2
2
|
import { dirname, join } from 'node:path'
|
|
3
3
|
import fs from 'node:fs/promises'
|
|
4
|
-
import assert from 'node:assert'
|
|
5
4
|
import { Provider } from '../Provider'
|
|
5
|
+
import type { ProviderSecrets } from '../Provider'
|
|
6
6
|
|
|
7
7
|
export interface FileSystemOptions {
|
|
8
8
|
path: string
|
|
@@ -11,10 +11,10 @@ export interface FileSystemOptions {
|
|
|
11
11
|
export class FileSystem extends Provider<FileSystemOptions> {
|
|
12
12
|
protected readonly path: string
|
|
13
13
|
|
|
14
|
-
public constructor (
|
|
15
|
-
super(
|
|
16
|
-
|
|
17
|
-
this.path =
|
|
14
|
+
public constructor (options: FileSystemOptions, secrets?: ProviderSecrets) {
|
|
15
|
+
super(options, secrets)
|
|
16
|
+
|
|
17
|
+
this.path = options.path
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
public async get (path: string): Promise<Readable | null> {
|
|
@@ -24,11 +24,10 @@ describe('S3 storage provider', () => {
|
|
|
24
24
|
|
|
25
25
|
provider = new S3({
|
|
26
26
|
bucket: TEST_BUCKET_NAME,
|
|
27
|
-
region: AWS_REGION
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
27
|
+
region: AWS_REGION
|
|
28
|
+
}, {
|
|
29
|
+
ACCESS_KEY_ID: 'test-key',
|
|
30
|
+
SECRET_ACCESS_KEY: 'test-key-secret'
|
|
32
31
|
})
|
|
33
32
|
})
|
|
34
33
|
|
package/source/providers/S3.ts
CHANGED
|
@@ -22,11 +22,13 @@ import type { ReadableStream } from 'node:stream/web'
|
|
|
22
22
|
|
|
23
23
|
export interface S3Options {
|
|
24
24
|
bucket: string
|
|
25
|
-
prefix?: string
|
|
26
25
|
region?: string
|
|
26
|
+
prefix?: string
|
|
27
27
|
endpoint?: string
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
type S3Secrets = ProviderSecrets<'ACCESS_KEY_ID' | 'SECRET_ACCESS_KEY'>
|
|
31
|
+
|
|
30
32
|
export class S3 extends Provider<S3Options> {
|
|
31
33
|
public static override readonly SECRETS: readonly ProviderSecret[] = [
|
|
32
34
|
{ name: 'ACCESS_KEY_ID', optional: true },
|
|
@@ -36,29 +38,32 @@ export class S3 extends Provider<S3Options> {
|
|
|
36
38
|
protected readonly bucket: string
|
|
37
39
|
protected readonly client: S3Client
|
|
38
40
|
|
|
39
|
-
public constructor (
|
|
40
|
-
super(
|
|
41
|
+
public constructor (options: S3Options, secrets?: S3Secrets) {
|
|
42
|
+
super(options)
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
this.bucket = props.bucket
|
|
44
|
+
this.bucket = options.bucket
|
|
44
45
|
|
|
45
46
|
const s3Config: S3ClientConfigType = {
|
|
46
47
|
retryMode: 'adaptive',
|
|
47
48
|
...nodeNativeFetch
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
if (
|
|
51
|
-
s3Config.forcePathStyle =
|
|
52
|
-
s3Config.endpoint =
|
|
51
|
+
if (options.endpoint !== undefined) {
|
|
52
|
+
s3Config.forcePathStyle = options.endpoint.startsWith('http://')
|
|
53
|
+
s3Config.endpoint = options.endpoint
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
if (
|
|
56
|
+
if (options.region !== undefined) s3Config.region = options.region
|
|
57
|
+
|
|
58
|
+
if (typeof secrets?.ACCESS_KEY_ID === 'string') {
|
|
59
|
+
assert.ok(secrets.SECRET_ACCESS_KEY !== undefined,
|
|
60
|
+
'SECRET_ACCESS_KEY is required if ACCESS_KEY_ID is provided')
|
|
56
61
|
|
|
57
|
-
if (typeof props.secrets?.ACCESS_KEY_ID === 'string')
|
|
58
62
|
s3Config.credentials = {
|
|
59
|
-
accessKeyId:
|
|
60
|
-
secretAccessKey:
|
|
63
|
+
accessKeyId: secrets.ACCESS_KEY_ID,
|
|
64
|
+
secretAccessKey: secrets.SECRET_ACCESS_KEY
|
|
61
65
|
}
|
|
66
|
+
}
|
|
62
67
|
|
|
63
68
|
this.client = new S3Client(s3Config)
|
|
64
69
|
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { tmpdir } from 'node:os'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
|
-
|
|
4
3
|
import { FileSystem } from './FileSystem'
|
|
4
|
+
import type { ProviderSecrets } from '../Provider'
|
|
5
5
|
|
|
6
6
|
export interface TemporaryOptions {
|
|
7
|
-
|
|
7
|
+
directory: string
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export class Temporary extends FileSystem {
|
|
11
|
-
public constructor (
|
|
12
|
-
|
|
11
|
+
public constructor (options: TemporaryOptions, secrets?: ProviderSecrets) {
|
|
12
|
+
const path = join(tmpdir(), options.directory)
|
|
13
|
+
|
|
14
|
+
super({ path }, secrets)
|
|
13
15
|
}
|
|
14
16
|
}
|
package/source/providers/Test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Temporary, type TemporaryOptions } from './Temporary'
|
|
2
|
-
import type { ProviderSecret } from '../Provider'
|
|
2
|
+
import type { ProviderSecret, ProviderSecrets } from '../Provider'
|
|
3
3
|
|
|
4
4
|
export class Test extends Temporary {
|
|
5
5
|
public static override readonly SECRETS: readonly ProviderSecret[] = [
|
|
@@ -7,7 +7,7 @@ export class Test extends Temporary {
|
|
|
7
7
|
{ name: 'PASSWORD' }
|
|
8
8
|
]
|
|
9
9
|
|
|
10
|
-
public constructor (
|
|
11
|
-
super(
|
|
10
|
+
public constructor (options: TemporaryOptions, secrets?: ProviderSecrets) {
|
|
11
|
+
super(options, secrets)
|
|
12
12
|
}
|
|
13
13
|
}
|