@toa.io/extensions.storages 1.0.0-alpha.0 → 1.0.0-alpha.2
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 +12 -11
- package/readme.md +63 -19
- 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/Aspect.ts +6 -4
- package/source/Factory.ts +31 -28
- package/source/Provider.ts +30 -5
- package/source/Scanner.ts +3 -3
- package/source/Storage.test.ts +110 -105
- package/source/Storage.ts +13 -6
- package/source/deployment.ts +21 -29
- package/source/providers/Declaration.ts +10 -0
- package/source/providers/FileSystem.test.ts +1 -9
- package/source/providers/FileSystem.ts +20 -15
- package/source/providers/Memory.ts +41 -0
- package/source/providers/S3.test.ts +133 -0
- package/source/providers/S3.ts +114 -39
- package/source/providers/Temporary.ts +8 -6
- package/source/providers/Test.ts +8 -8
- package/source/providers/index.test.ts +24 -19
- package/source/providers/index.ts +10 -9
- package/source/providers/readme.md +1 -1
- package/source/schemas.test.ts +58 -0
- package/source/schemas.ts +15 -0
- package/source/test/util.ts +25 -54
- package/transpiled/Annotation.d.ts +3 -0
- package/transpiled/Annotation.js +57 -0
- package/transpiled/Annotation.js.map +1 -0
- package/transpiled/Aspect.d.ts +8 -0
- package/transpiled/Aspect.js +25 -0
- package/transpiled/Aspect.js.map +1 -0
- package/transpiled/Entry.d.ts +14 -0
- package/transpiled/Entry.js +3 -0
- package/transpiled/Entry.js.map +1 -0
- package/transpiled/Factory.d.ts +9 -0
- package/transpiled/Factory.js +53 -0
- package/transpiled/Factory.js.map +1 -0
- package/transpiled/Provider.d.ts +20 -0
- package/transpiled/Provider.js +36 -0
- package/transpiled/Provider.js.map +1 -0
- package/transpiled/Scanner.d.ts +26 -0
- package/transpiled/Scanner.js +98 -0
- package/transpiled/Scanner.js.map +1 -0
- package/transpiled/Storage.d.ts +32 -0
- package/transpiled/Storage.js +176 -0
- package/transpiled/Storage.js.map +1 -0
- package/transpiled/deployment.d.ts +5 -0
- package/transpiled/deployment.js +68 -0
- package/transpiled/deployment.js.map +1 -0
- package/transpiled/index.d.ts +4 -0
- package/transpiled/index.js +10 -0
- package/transpiled/index.js.map +1 -0
- package/transpiled/manifest.d.ts +1 -0
- package/transpiled/manifest.js +9 -0
- package/transpiled/manifest.js.map +1 -0
- 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 +15 -0
- package/transpiled/providers/FileSystem.js +44 -0
- package/transpiled/providers/FileSystem.js.map +1 -0
- package/transpiled/providers/Memory.d.ts +13 -0
- package/transpiled/providers/Memory.js +60 -0
- package/transpiled/providers/Memory.js.map +1 -0
- package/transpiled/providers/S3.d.ts +27 -0
- package/transpiled/providers/S3.js +154 -0
- package/transpiled/providers/S3.js.map +1 -0
- package/transpiled/providers/Temporary.d.ts +8 -0
- package/transpiled/providers/Temporary.js +14 -0
- package/transpiled/providers/Temporary.js.map +1 -0
- package/transpiled/providers/Test.d.ts +6 -0
- package/transpiled/providers/Test.js +15 -0
- package/transpiled/providers/Test.js.map +1 -0
- package/transpiled/providers/index.d.ts +13 -0
- package/transpiled/providers/index.js +16 -0
- package/transpiled/providers/index.js.map +1 -0
- 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 +29 -0
- package/transpiled/test/util.js +38 -0
- package/transpiled/test/util.js.map +1 -0
- package/transpiled/tsconfig.tsbuildinfo +1 -0
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.2",
|
|
4
4
|
"description": "Toa Storages",
|
|
5
5
|
"author": "temich <tema.gurtovoy@gmail.com>",
|
|
6
6
|
"homepage": "https://github.com/toa-io/toa#readme",
|
|
@@ -22,21 +22,22 @@
|
|
|
22
22
|
},
|
|
23
23
|
"scripts": {
|
|
24
24
|
"test": "jest",
|
|
25
|
-
"transpile": "
|
|
25
|
+
"transpile": "npx tsc"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@toa.io/streams": "1.0.0-alpha.
|
|
29
|
-
"@types/fs-extra": "11.0.3",
|
|
28
|
+
"@toa.io/streams": "1.0.0-alpha.2",
|
|
30
29
|
"dotenv": "16.3.1"
|
|
31
30
|
},
|
|
32
31
|
"dependencies": {
|
|
33
|
-
"@aws-sdk/client-s3": "3.
|
|
34
|
-
"@aws-sdk/lib-storage": "3.
|
|
35
|
-
"@toa.io/
|
|
32
|
+
"@aws-sdk/client-s3": "3.481.0",
|
|
33
|
+
"@aws-sdk/lib-storage": "3.481.0",
|
|
34
|
+
"@toa.io/agent": "1.0.0-alpha.2",
|
|
35
|
+
"@toa.io/generic": "1.0.0-alpha.2",
|
|
36
|
+
"@toa.io/schemas": "1.0.0-alpha.2",
|
|
36
37
|
"error-value": "0.3.0",
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
38
|
+
"matchacho": "0.6.0",
|
|
39
|
+
"msgpackr": "1.10.1",
|
|
40
|
+
"smithy-node-native-fetch": "1.0.6"
|
|
40
41
|
},
|
|
41
|
-
"gitHead": "
|
|
42
|
+
"gitHead": "7688e6e980a65c82ac2e459be4e355eebf406cd0"
|
|
42
43
|
}
|
package/readme.md
CHANGED
|
@@ -47,26 +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
|
-
If the `type` argument is specified and the value of the `claim` does not match the detected BLOB
|
|
65
|
-
a `TYPE_MISMATCH` error is returned.
|
|
66
|
-
If the BLOB type cannot be identified and the value of the `claim` is not in the list of known
|
|
67
|
-
value is used.
|
|
68
|
-
If the list of [acceptable types](https://datatracker.ietf.org/doc/html/rfc2616#section-14.1) is
|
|
69
|
-
the BLOB does not match any of its values, then a `NOT_ACCEPTABLE` error is
|
|
65
|
+
If the `type` argument is specified and the value of the `claim` does not match the detected BLOB
|
|
66
|
+
type, then a `TYPE_MISMATCH` error is returned.
|
|
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 value is used.
|
|
69
|
+
If the list of [acceptable types](https://datatracker.ietf.org/doc/html/rfc2616#section-14.1) is
|
|
70
|
+
passed and the type of the BLOB does not match any of its values, then a `NOT_ACCEPTABLE` error is
|
|
71
|
+
returned.
|
|
70
72
|
|
|
71
73
|
Known types
|
|
72
74
|
are: `image/jpeg`, `image/png`, `image/gif`, `image/webp`, `image/heic`, `image/jxl`, `image/avif`.
|
|
@@ -130,29 +132,63 @@ Custom providers are not supported.
|
|
|
130
132
|
|
|
131
133
|
### Amazon S3
|
|
132
134
|
|
|
133
|
-
Annotation
|
|
135
|
+
Annotation formats is like:
|
|
136
|
+
|
|
137
|
+
```yaml
|
|
138
|
+
storages:
|
|
139
|
+
photos:
|
|
140
|
+
provider: s3
|
|
141
|
+
bucket: my-bucket
|
|
142
|
+
region: eu-west-1
|
|
143
|
+
tmp:
|
|
144
|
+
provider: s3
|
|
145
|
+
endpoint: http://localhost:9000
|
|
146
|
+
bucket: my-bucket
|
|
147
|
+
```
|
|
134
148
|
|
|
135
|
-
|
|
149
|
+
Secrets for the AWS access key and secret key can be provided via SECRETS constructs property. If
|
|
150
|
+
missed standard AWS SDK credentials resolve chain will be used (that means environment variable,
|
|
151
|
+
shared config file, EC2 metadata service, etc.).
|
|
136
152
|
See [`toa conceal`](/runtime/cli/readme.md#conceal) for deployment
|
|
137
153
|
and [`toa env`](/runtime/cli/readme.md#env)
|
|
138
154
|
for local environment.
|
|
139
155
|
`endpoint` parameter is optional.
|
|
140
156
|
|
|
141
|
-
`s3://us-east-1/my-bucket?endpoint=http://s3.my-instance.com:4566`
|
|
142
|
-
|
|
143
157
|
### Filesystem
|
|
144
158
|
|
|
145
|
-
Annotation
|
|
159
|
+
Annotation format is:
|
|
146
160
|
|
|
147
|
-
|
|
161
|
+
```yaml
|
|
162
|
+
storages:
|
|
163
|
+
photos@dev:
|
|
164
|
+
provider: fs
|
|
165
|
+
path: /var/my-storage
|
|
166
|
+
```
|
|
148
167
|
|
|
149
168
|
### Temporary
|
|
150
169
|
|
|
151
170
|
Filesystem using OS temporary directory.
|
|
152
171
|
|
|
153
|
-
Annotation
|
|
172
|
+
Annotation format is:
|
|
154
173
|
|
|
155
|
-
|
|
174
|
+
```yaml
|
|
175
|
+
storages:
|
|
176
|
+
photos@dev:
|
|
177
|
+
provider: tmp
|
|
178
|
+
directory: my-app-tmp
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Memory
|
|
182
|
+
|
|
183
|
+
In-memory non-persistent storage.
|
|
184
|
+
|
|
185
|
+
Annotation value format is:
|
|
186
|
+
|
|
187
|
+
```yaml
|
|
188
|
+
storages:
|
|
189
|
+
photos@dev:
|
|
190
|
+
provider: mem
|
|
191
|
+
```
|
|
156
192
|
|
|
157
193
|
## Deduplication
|
|
158
194
|
|
|
@@ -199,8 +235,16 @@ provider-specific URLs as values.
|
|
|
199
235
|
|
|
200
236
|
```yaml
|
|
201
237
|
storages:
|
|
202
|
-
photos:
|
|
203
|
-
|
|
238
|
+
photos:
|
|
239
|
+
provider: s3
|
|
240
|
+
bucket: my-bucket
|
|
241
|
+
photos@dev:
|
|
242
|
+
provider: fs
|
|
243
|
+
path: /var/my-storage
|
|
244
|
+
tmp:
|
|
245
|
+
provider: s3
|
|
246
|
+
endpoint: http://localhost:9000
|
|
247
|
+
bucket: my-bucket
|
|
204
248
|
```
|
|
205
249
|
|
|
206
250
|
## Secrets
|
|
@@ -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/Aspect.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
1
2
|
import { Connector, type extensions } from '@toa.io/core'
|
|
2
3
|
import { type Storage, type Storages } from './Storage'
|
|
3
4
|
|
|
@@ -12,12 +13,13 @@ export class Aspect extends Connector implements extensions.Aspect {
|
|
|
12
13
|
this.storages = storages
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
public invoke (name: string, method: keyof Storage, ...args:
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
public invoke (name: string, method: keyof Storage, ...args: unknown[]): unknown {
|
|
17
|
+
const storage = this.storages[name]
|
|
18
|
+
|
|
19
|
+
assert.ok(storage !== undefined, `Storage '${name}' is not defined`)
|
|
18
20
|
|
|
19
21
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
20
22
|
// @ts-expect-error
|
|
21
|
-
return
|
|
23
|
+
return storage[method](...args)
|
|
22
24
|
}
|
|
23
25
|
}
|
package/source/Factory.ts
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import assert from 'node:assert'
|
|
2
|
+
import { decode } from '@toa.io/generic'
|
|
3
|
+
import { providers } from './providers'
|
|
3
4
|
import { Storage, type Storages } from './Storage'
|
|
4
5
|
import { Aspect } from './Aspect'
|
|
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'
|
|
5
11
|
|
|
6
12
|
export class Factory {
|
|
7
|
-
private readonly
|
|
13
|
+
private readonly annotation: Annotation
|
|
8
14
|
|
|
9
15
|
public constructor () {
|
|
10
16
|
const env = process.env.TOA_STORAGES
|
|
11
17
|
|
|
12
|
-
|
|
13
|
-
throw new Error('TOA_STORAGES is not defined')
|
|
18
|
+
assert.ok(env !== undefined, 'TOA_STORAGES is not defined')
|
|
14
19
|
|
|
15
|
-
this.
|
|
20
|
+
this.annotation = decode(env)
|
|
21
|
+
|
|
22
|
+
validateAnnotation(this.annotation)
|
|
16
23
|
}
|
|
17
24
|
|
|
18
25
|
public aspect (): Aspect {
|
|
@@ -21,43 +28,39 @@ export class Factory {
|
|
|
21
28
|
return new Aspect(storages)
|
|
22
29
|
}
|
|
23
30
|
|
|
24
|
-
public createStorage (name: string, ref: string): Storage {
|
|
25
|
-
const url = new URL(ref)
|
|
26
|
-
const Provider = providers[url.protocol]
|
|
27
|
-
|
|
28
|
-
if (Provider === undefined)
|
|
29
|
-
throw new Error(`No provider found for '${url.protocol}'`)
|
|
30
|
-
|
|
31
|
-
const secrets = this.resolveSecrets(name, Provider)
|
|
32
|
-
|
|
33
|
-
const provider = new Provider(url, secrets)
|
|
34
|
-
|
|
35
|
-
return new Storage(provider)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
31
|
private createStorages (): Storages {
|
|
39
32
|
const storages: Storages = {}
|
|
40
33
|
|
|
41
|
-
for (const [name,
|
|
42
|
-
storages[name] = this.createStorage(name,
|
|
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
|
|
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 {
|
|
48
51
|
if (Class.SECRETS === undefined)
|
|
49
52
|
return {}
|
|
50
53
|
|
|
51
|
-
const secrets: Record<string, string> = {}
|
|
54
|
+
const secrets: Record<string, string | undefined> = {}
|
|
52
55
|
|
|
53
56
|
for (const secret of Class.SECRETS) {
|
|
54
|
-
const variable =
|
|
57
|
+
const variable = `${SERIALIZATION_PREFIX}_${storageName}_${secret.name}`.toUpperCase()
|
|
55
58
|
const value = process.env[variable]
|
|
56
59
|
|
|
57
|
-
|
|
58
|
-
|
|
60
|
+
assert.ok(secret.optional === true || value !== undefined,
|
|
61
|
+
`'${variable}' is not defined`)
|
|
59
62
|
|
|
60
|
-
secrets[secret] = value
|
|
63
|
+
secrets[secret.name] = value
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
return secrets
|
package/source/Provider.ts
CHANGED
|
@@ -1,8 +1,33 @@
|
|
|
1
1
|
import { type Readable } from 'node:stream'
|
|
2
|
+
import * as assert from 'node:assert'
|
|
2
3
|
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
export type ProviderSecrets<K extends string = string> = Record<K | string, string | undefined>
|
|
5
|
+
|
|
6
|
+
export interface ProviderSecret {
|
|
7
|
+
readonly name: string
|
|
8
|
+
readonly optional?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export abstract class Provider<Options = void> {
|
|
12
|
+
public static readonly SECRETS: readonly ProviderSecret[] = []
|
|
13
|
+
|
|
14
|
+
public constructor (_: Options, secrets?: ProviderSecrets) {
|
|
15
|
+
for (const { name, optional = false } of new.target.SECRETS)
|
|
16
|
+
assert.ok(optional || secrets?.[name] !== undefined, `Missing secret '${name}'`)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public abstract get (path: string): Promise<Readable | null>
|
|
20
|
+
|
|
21
|
+
public abstract put (path: string, filename: string, stream: Readable): Promise<void>
|
|
22
|
+
|
|
23
|
+
public abstract delete (path: string): Promise<void>
|
|
24
|
+
|
|
25
|
+
public abstract move (from: string, to: string): Promise<void>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ProviderConstructor {
|
|
29
|
+
readonly SECRETS: readonly ProviderSecret[]
|
|
30
|
+
prototype: Provider
|
|
31
|
+
|
|
32
|
+
new (options: any, secrets?: ProviderSecrets): Provider
|
|
8
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
|
}
|