@toa.io/extensions.storages 1.0.0-alpha.31 → 1.0.0-alpha.310
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 +38 -18
- package/readme.md +115 -47
- package/transpiled/Aspect.d.ts +8 -4
- package/transpiled/Aspect.js +12 -14
- package/transpiled/Aspect.js.map +1 -1
- package/transpiled/Entry.d.ts +14 -11
- package/transpiled/Entry.js +1 -2
- package/transpiled/Factory.d.ts +1 -1
- package/transpiled/Factory.js +29 -30
- package/transpiled/Factory.js.map +1 -1
- package/transpiled/Provider.d.ts +17 -19
- package/transpiled/Provider.js +10 -33
- package/transpiled/Provider.js.map +1 -1
- package/transpiled/Scanner.d.ts +5 -4
- package/transpiled/Scanner.js +33 -26
- package/transpiled/Scanner.js.map +1 -1
- package/transpiled/Secrets.d.ts +5 -0
- package/transpiled/Secrets.js +2 -0
- package/transpiled/Secrets.js.map +1 -0
- package/transpiled/Storage.d.ts +23 -22
- package/transpiled/Storage.js +79 -142
- package/transpiled/Storage.js.map +1 -1
- package/transpiled/errors.d.ts +7 -0
- package/transpiled/errors.js +4 -0
- package/transpiled/errors.js.map +1 -0
- package/transpiled/index.d.ts +4 -4
- package/transpiled/index.js +1 -9
- package/transpiled/index.js.map +1 -1
- package/transpiled/measurements.d.ts +3 -0
- package/transpiled/measurements.js +23 -0
- package/transpiled/measurements.js.map +1 -0
- package/transpiled/providers/Cloudinary.d.ts +50 -0
- package/transpiled/providers/Cloudinary.js +223 -0
- package/transpiled/providers/Cloudinary.js.map +1 -0
- package/transpiled/providers/Declaration.d.ts +9 -5
- package/transpiled/providers/Declaration.js +1 -2
- package/transpiled/providers/FileSystem.d.ts +16 -11
- package/transpiled/providers/FileSystem.js +64 -43
- package/transpiled/providers/FileSystem.js.map +1 -1
- package/transpiled/providers/S3.d.ts +14 -19
- package/transpiled/providers/S3.js +72 -123
- package/transpiled/providers/S3.js.map +1 -1
- package/transpiled/providers/Spaces.d.ts +12 -0
- package/transpiled/providers/Spaces.js +13 -0
- package/transpiled/providers/Spaces.js.map +1 -0
- package/transpiled/providers/Temporary.d.ts +2 -3
- package/transpiled/providers/Temporary.js +7 -11
- package/transpiled/providers/Temporary.js.map +1 -1
- package/transpiled/providers/Test.d.ts +4 -4
- package/transpiled/providers/Test.js +6 -12
- package/transpiled/providers/Test.js.map +1 -1
- package/transpiled/providers/index.d.ts +23 -11
- package/transpiled/providers/index.js +30 -14
- package/transpiled/providers/index.js.map +1 -1
- package/transpiled/providers/index.types.d.ts +5 -0
- package/transpiled/providers/index.types.js +2 -0
- package/transpiled/providers/index.types.js.map +1 -0
- package/transpiled/test/util.d.ts +113 -6
- package/transpiled/test/util.js +68 -15
- package/transpiled/test/util.js.map +1 -1
- package/schemas/annotation.cos.yaml +0 -10
- package/schemas/fs.cos.yaml +0 -9
- package/schemas/mem.cos.yaml +0 -6
- package/schemas/s3.cos.yaml +0 -16
- package/schemas/test.cos.yaml +0 -8
- package/schemas/tmp.cos.yaml +0 -9
- package/source/Annotation.ts +0 -39
- package/source/Aspect.ts +0 -25
- package/source/Entry.ts +0 -14
- package/source/Factory.ts +0 -68
- package/source/Provider.ts +0 -37
- package/source/Scanner.ts +0 -153
- package/source/Storage.test.ts +0 -472
- package/source/Storage.ts +0 -235
- package/source/deployment.ts +0 -57
- package/source/index.ts +0 -5
- package/source/manifest.ts +0 -8
- package/source/providers/Declaration.ts +0 -10
- package/source/providers/FileSystem.test.ts +0 -5
- package/source/providers/FileSystem.ts +0 -63
- package/source/providers/Memory.ts +0 -55
- package/source/providers/S3.test.ts +0 -134
- package/source/providers/S3.ts +0 -196
- package/source/providers/Temporary.ts +0 -16
- package/source/providers/Test.ts +0 -13
- package/source/providers/index.test.ts +0 -136
- package/source/providers/index.ts +0 -16
- package/source/providers/readme.md +0 -25
- package/source/schemas.test.ts +0 -58
- package/source/schemas.ts +0 -15
- package/source/test/.env.example +0 -1
- package/source/test/albert.jpg +0 -0
- package/source/test/empty.txt +0 -0
- package/source/test/lenna.ascii +0 -64
- package/source/test/lenna.png +0 -0
- package/source/test/sample.avi +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.svg +0 -4
- package/source/test/sample.wav +0 -0
- package/source/test/sample.webp +0 -0
- package/source/test/util.ts +0 -45
- package/transpiled/Annotation.d.ts +0 -3
- package/transpiled/Annotation.js +0 -57
- package/transpiled/Annotation.js.map +0 -1
- package/transpiled/deployment.d.ts +0 -5
- package/transpiled/deployment.js +0 -68
- package/transpiled/deployment.js.map +0 -1
- package/transpiled/manifest.d.ts +0 -1
- package/transpiled/manifest.js +0 -9
- package/transpiled/manifest.js.map +0 -1
- package/transpiled/providers/Memory.d.ts +0 -15
- package/transpiled/providers/Memory.js +0 -72
- package/transpiled/providers/Memory.js.map +0 -1
- package/transpiled/schemas.d.ts +0 -9
- package/transpiled/schemas.js +0 -14
- package/transpiled/schemas.js.map +0 -1
- package/transpiled/tsconfig.tsbuildinfo +0 -1
- package/tsconfig.json +0 -12
package/source/deployment.ts
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import * as assert from 'node:assert'
|
|
2
|
-
import { encode } from '@toa.io/generic'
|
|
3
|
-
import { providers } from './providers'
|
|
4
|
-
import { validateAnnotation } from './Annotation'
|
|
5
|
-
import type { Annotation } from './Annotation'
|
|
6
|
-
import type { Dependency, Variable } from '@toa.io/operations'
|
|
7
|
-
import type { context } from '@toa.io/norm'
|
|
8
|
-
|
|
9
|
-
export const SERIALIZATION_PREFIX = 'TOA_STORAGES'
|
|
10
|
-
|
|
11
|
-
export function deployment (instances: Instance[], annotation: unknown): Dependency {
|
|
12
|
-
validate(instances, annotation)
|
|
13
|
-
|
|
14
|
-
const value = encode(annotation)
|
|
15
|
-
const pointer: Variable = { name: SERIALIZATION_PREFIX, value }
|
|
16
|
-
const secrets = getSecrets(annotation)
|
|
17
|
-
|
|
18
|
-
return {
|
|
19
|
-
variables: { global: [pointer, ...secrets] }
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function validate (instances: Instance[], annotation: unknown): asserts annotation is Annotation {
|
|
24
|
-
validateAnnotation(annotation)
|
|
25
|
-
|
|
26
|
-
for (const instance of instances)
|
|
27
|
-
contains(instance, annotation)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function contains (instance: Instance, annotation: Annotation): void {
|
|
31
|
-
for (const name of instance.manifest)
|
|
32
|
-
assert.ok(name in annotation,
|
|
33
|
-
`Missing '${name}' storage annotation ` +
|
|
34
|
-
`declared in '${instance.component.locator.id}'`)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function getSecrets (annotation: Annotation): Variable[] {
|
|
38
|
-
const secrets: Variable[] = []
|
|
39
|
-
|
|
40
|
-
for (const [name, declaration] of Object.entries(annotation)) {
|
|
41
|
-
const Provider = providers[declaration.provider]
|
|
42
|
-
|
|
43
|
-
for (const secret of Provider.SECRETS)
|
|
44
|
-
secrets.push({
|
|
45
|
-
name: `${SERIALIZATION_PREFIX}_${name}_${secret.name}`.toUpperCase(),
|
|
46
|
-
secret: {
|
|
47
|
-
name: `toa-storages-${name}`,
|
|
48
|
-
key: secret.name,
|
|
49
|
-
optional: secret.optional
|
|
50
|
-
}
|
|
51
|
-
})
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return secrets
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export type Instance = context.Dependency<string[]>
|
package/source/index.ts
DELETED
package/source/manifest.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
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,63 +0,0 @@
|
|
|
1
|
-
import { type Readable } from 'node:stream'
|
|
2
|
-
import { dirname, join } from 'node:path'
|
|
3
|
-
import fs from 'node:fs/promises'
|
|
4
|
-
import { Provider } from '../Provider'
|
|
5
|
-
import type { ProviderSecrets } from '../Provider'
|
|
6
|
-
|
|
7
|
-
export interface FileSystemOptions {
|
|
8
|
-
path: string
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export class FileSystem extends Provider<FileSystemOptions> {
|
|
12
|
-
protected readonly path: string
|
|
13
|
-
|
|
14
|
-
public constructor (options: FileSystemOptions, secrets?: ProviderSecrets) {
|
|
15
|
-
super(options, secrets)
|
|
16
|
-
|
|
17
|
-
this.path = options.path
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
public async get (path: string): Promise<Readable | null> {
|
|
21
|
-
try {
|
|
22
|
-
const fd = await fs.open(join(this.path, path))
|
|
23
|
-
|
|
24
|
-
return fd.createReadStream()
|
|
25
|
-
} catch (err) {
|
|
26
|
-
if ((err as NodeJS.ErrnoException)?.code === 'ENOENT') return null
|
|
27
|
-
|
|
28
|
-
throw err
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
public async list (path: string): Promise<string[]> {
|
|
33
|
-
const dir = join(this.path, path)
|
|
34
|
-
|
|
35
|
-
return (await fs.readdir(dir, { withFileTypes: true }))
|
|
36
|
-
.filter((dirent) => dirent.isFile())
|
|
37
|
-
.map((dirent) => dirent.name)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
public async put (rel: string, filename: string, stream: Readable): Promise<void> {
|
|
41
|
-
const dir = join(this.path, rel)
|
|
42
|
-
const path = join(dir, filename)
|
|
43
|
-
|
|
44
|
-
await fs.mkdir(dir, { recursive: true })
|
|
45
|
-
await fs.writeFile(path, stream)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
public async delete (path: string): Promise<void> {
|
|
49
|
-
await fs.rm(join(this.path, path), { recursive: true, force: true })
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
public async move (from: string, to: string): Promise<void> {
|
|
53
|
-
from = join(this.path, from)
|
|
54
|
-
to = join(this.path, to)
|
|
55
|
-
|
|
56
|
-
await fs.mkdir(dirname(to), { recursive: true })
|
|
57
|
-
await fs.rename(from, to)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
public async moveDir (from: string, to: string): Promise<void> {
|
|
61
|
-
await this.move(from, to).catch(() => null)
|
|
62
|
-
}
|
|
63
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { Readable } from 'node:stream'
|
|
2
|
-
import { join, posix } from 'node:path'
|
|
3
|
-
import { buffer } from 'node:stream/consumers'
|
|
4
|
-
import * as assert from 'node:assert'
|
|
5
|
-
|
|
6
|
-
import { Provider } from '../Provider'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* In-memory provider
|
|
10
|
-
*/
|
|
11
|
-
export class InMemory extends Provider {
|
|
12
|
-
private readonly storage = new Map<string, Buffer>()
|
|
13
|
-
|
|
14
|
-
public async get (path: string): Promise<Readable | null> {
|
|
15
|
-
const data = this.storage.get(path)
|
|
16
|
-
|
|
17
|
-
if (data === undefined) return null
|
|
18
|
-
|
|
19
|
-
return Readable.from(data)
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
public async list (path: string): Promise<string[]> {
|
|
23
|
-
return Array.from(this.storage.keys())
|
|
24
|
-
.filter((f) => posix.dirname(f) === path)
|
|
25
|
-
.map((f) => posix.basename(f))
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
public async put (path: string, filename: string, stream: Readable): Promise<void> {
|
|
29
|
-
this.storage.set(join(path, filename), await buffer(stream))
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
public async delete (path: string): Promise<void> {
|
|
33
|
-
for (const f of this.storage.keys())
|
|
34
|
-
if (f.startsWith(path)) this.storage.delete(f)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
public async move (from: string, to: string): Promise<void> {
|
|
38
|
-
const buf = this.storage.get(from)
|
|
39
|
-
|
|
40
|
-
assert.ok(buf !== undefined, `File not found: ${from}`)
|
|
41
|
-
|
|
42
|
-
this.storage.set(to, buf)
|
|
43
|
-
this.storage.delete(from)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
public async moveDir (from: string, to: string): Promise<void> {
|
|
47
|
-
for (const f of this.storage.keys())
|
|
48
|
-
if (f.startsWith(from)) {
|
|
49
|
-
const toPath = to + f.slice(from.length)
|
|
50
|
-
|
|
51
|
-
this.storage.set(toPath, this.storage.get(f)!)
|
|
52
|
-
this.storage.delete(f)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import { Readable } from 'node:stream'
|
|
2
|
-
import { randomUUID } from 'node:crypto'
|
|
3
|
-
import streamConsumers from 'node:stream/consumers'
|
|
4
|
-
import { http, HttpResponse } from 'msw'
|
|
5
|
-
import { setupServer } from 'msw/node'
|
|
6
|
-
|
|
7
|
-
import { S3 } from './S3'
|
|
8
|
-
|
|
9
|
-
/* eslint-disable @typescript-eslint/no-non-null-assertion -- jest expect is not type guard */
|
|
10
|
-
|
|
11
|
-
describe('S3 storage provider', () => {
|
|
12
|
-
let provider: S3
|
|
13
|
-
let s3endpoint: ReturnType<typeof setupServer>
|
|
14
|
-
|
|
15
|
-
const AWS_REGION = 'us-east-1'
|
|
16
|
-
|
|
17
|
-
const TEST_BUCKET_NAME = 'test-bucket'
|
|
18
|
-
const S3_ENDPOINT = `https://${TEST_BUCKET_NAME}.s3.${AWS_REGION}.amazonaws.com`
|
|
19
|
-
|
|
20
|
-
beforeAll(async () => {
|
|
21
|
-
jest.useRealTimers()
|
|
22
|
-
s3endpoint = setupServer()
|
|
23
|
-
s3endpoint.listen({ onUnhandledRequest: 'error' })
|
|
24
|
-
|
|
25
|
-
provider = new S3({
|
|
26
|
-
bucket: TEST_BUCKET_NAME,
|
|
27
|
-
region: AWS_REGION
|
|
28
|
-
}, {
|
|
29
|
-
ACCESS_KEY_ID: 'test-key',
|
|
30
|
-
SECRET_ACCESS_KEY: 'test-key-secret'
|
|
31
|
-
})
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
afterEach(() => {
|
|
35
|
-
s3endpoint.resetHandlers()
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
afterAll(() => {
|
|
39
|
-
s3endpoint.close()
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
test('should be able to get file from S3 handling leading slash', async () => {
|
|
43
|
-
const testBody = randomUUID().repeat(10)
|
|
44
|
-
|
|
45
|
-
s3endpoint.use(http.get(`${S3_ENDPOINT}/some/absolute/path/filename`,
|
|
46
|
-
() => HttpResponse.text(testBody)))
|
|
47
|
-
|
|
48
|
-
const body = await provider.get('/some/absolute/path/filename')
|
|
49
|
-
|
|
50
|
-
expect(body).toBeInstanceOf(Readable)
|
|
51
|
-
expect((await streamConsumers.text(body!))).toBe(testBody)
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
test('should remove folder with all files', async () => {
|
|
55
|
-
const headHandler = jest.fn().mockReturnValue(HttpResponse.xml('', { status: 404 }))
|
|
56
|
-
|
|
57
|
-
const listHandler = jest.fn().mockReturnValueOnce(HttpResponse.xml(`
|
|
58
|
-
<ListBucketResult>
|
|
59
|
-
<NextContinuationToken>someToken</NextContinuationToken>
|
|
60
|
-
<KeyCount>1001</KeyCount>
|
|
61
|
-
<MaxKeys>1000</MaxKeys>
|
|
62
|
-
<IsTruncated>true</IsTruncated>
|
|
63
|
-
<Contents>
|
|
64
|
-
<Key>happy_face1.jpg</Key>
|
|
65
|
-
</Contents>
|
|
66
|
-
</ListBucketResult>
|
|
67
|
-
`, {
|
|
68
|
-
status: 200
|
|
69
|
-
})).mockReturnValueOnce(HttpResponse.xml(`
|
|
70
|
-
<ListBucketResult>
|
|
71
|
-
<KeyCount>1001</KeyCount>
|
|
72
|
-
<MaxKeys>1000</MaxKeys>
|
|
73
|
-
<Contents>
|
|
74
|
-
<Key>happy_face2.jpg</Key>
|
|
75
|
-
</Contents>
|
|
76
|
-
<Contents>
|
|
77
|
-
<Key>happy_face3.jpg</Key>
|
|
78
|
-
</Contents>
|
|
79
|
-
</ListBucketResult>
|
|
80
|
-
`, {
|
|
81
|
-
status: 200
|
|
82
|
-
}))
|
|
83
|
-
|
|
84
|
-
const deleteHandler = jest.fn().mockReturnValue(HttpResponse.xml(`
|
|
85
|
-
<DeleteResult>
|
|
86
|
-
<Deleted>
|
|
87
|
-
<Key>happy_face1.jpg</Key>
|
|
88
|
-
</Deleted>
|
|
89
|
-
<Deleted>
|
|
90
|
-
<Key>happy_face2.jpg</Key>
|
|
91
|
-
</Deleted>
|
|
92
|
-
<Deleted>
|
|
93
|
-
<Key>happy_face3.jpg</Key>
|
|
94
|
-
</Deleted>
|
|
95
|
-
</DeleteResult>
|
|
96
|
-
`, {
|
|
97
|
-
status: 200
|
|
98
|
-
}))
|
|
99
|
-
|
|
100
|
-
s3endpoint.use(http.head(`${S3_ENDPOINT}/some/absolute/path_to_remove`,
|
|
101
|
-
headHandler),
|
|
102
|
-
http.get(`${S3_ENDPOINT}/`,
|
|
103
|
-
listHandler),
|
|
104
|
-
http.post(`${S3_ENDPOINT}/`,
|
|
105
|
-
deleteHandler))
|
|
106
|
-
|
|
107
|
-
await provider.delete('/some/absolute/path_to_remove')
|
|
108
|
-
|
|
109
|
-
expect(headHandler).toHaveBeenCalledTimes(1)
|
|
110
|
-
expect(listHandler).toHaveBeenCalledTimes(2)
|
|
111
|
-
expect(deleteHandler).toHaveBeenCalledTimes(1)
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
test('should be able to upload a file to S3', async () => {
|
|
115
|
-
const body = Readable.from('test content')
|
|
116
|
-
|
|
117
|
-
const handler = jest.fn().mockReturnValue(HttpResponse.xml('', {
|
|
118
|
-
status: 200,
|
|
119
|
-
headers: {
|
|
120
|
-
ETag: 'test-etag',
|
|
121
|
-
'x-amz-id-2': 'test_id_2',
|
|
122
|
-
'x-amz-request-id': 'test_request_id',
|
|
123
|
-
Server: 'AmazonS3'
|
|
124
|
-
}
|
|
125
|
-
}))
|
|
126
|
-
|
|
127
|
-
s3endpoint.use(http.put(`${S3_ENDPOINT}/some/absolute/path/filename`,
|
|
128
|
-
handler))
|
|
129
|
-
|
|
130
|
-
await provider.put('/some/absolute/path', 'filename', body)
|
|
131
|
-
expect(handler).toHaveBeenCalledTimes(1)
|
|
132
|
-
expect(body.readableEnded).toBe(true)
|
|
133
|
-
})
|
|
134
|
-
})
|
package/source/providers/S3.ts
DELETED
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
import { Readable } from 'node:stream'
|
|
2
|
-
import { Blob } from 'node:buffer'
|
|
3
|
-
import { join } from 'node:path/posix'
|
|
4
|
-
import assert from 'node:assert'
|
|
5
|
-
import { posix } from 'node:path'
|
|
6
|
-
import { Upload } from '@aws-sdk/lib-storage'
|
|
7
|
-
import {
|
|
8
|
-
S3Client,
|
|
9
|
-
GetObjectCommand,
|
|
10
|
-
CopyObjectCommand,
|
|
11
|
-
DeleteObjectsCommand,
|
|
12
|
-
paginateListObjectsV2,
|
|
13
|
-
HeadObjectCommand,
|
|
14
|
-
NotFound,
|
|
15
|
-
DeleteObjectCommand,
|
|
16
|
-
NoSuchKey,
|
|
17
|
-
type S3ClientConfigType,
|
|
18
|
-
type ObjectIdentifier
|
|
19
|
-
} from '@aws-sdk/client-s3'
|
|
20
|
-
import * as nodeNativeFetch from 'smithy-node-native-fetch'
|
|
21
|
-
import { Provider, type ProviderSecret, type ProviderSecrets } from '../Provider'
|
|
22
|
-
import type { ReadableStream } from 'node:stream/web'
|
|
23
|
-
|
|
24
|
-
export interface S3Options {
|
|
25
|
-
bucket: string
|
|
26
|
-
region?: string
|
|
27
|
-
prefix?: string
|
|
28
|
-
endpoint?: string
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
type S3Secrets = ProviderSecrets<'ACCESS_KEY_ID' | 'SECRET_ACCESS_KEY'>
|
|
32
|
-
|
|
33
|
-
export class S3 extends Provider<S3Options> {
|
|
34
|
-
public static override readonly SECRETS: readonly ProviderSecret[] = [
|
|
35
|
-
{ name: 'ACCESS_KEY_ID', optional: true },
|
|
36
|
-
{ name: 'SECRET_ACCESS_KEY', optional: true }
|
|
37
|
-
]
|
|
38
|
-
|
|
39
|
-
protected readonly bucket: string
|
|
40
|
-
protected readonly client: S3Client
|
|
41
|
-
|
|
42
|
-
public constructor (options: S3Options, secrets?: S3Secrets) {
|
|
43
|
-
super(options)
|
|
44
|
-
|
|
45
|
-
this.bucket = options.bucket
|
|
46
|
-
|
|
47
|
-
const s3Config: S3ClientConfigType = {
|
|
48
|
-
retryMode: 'adaptive',
|
|
49
|
-
...nodeNativeFetch
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (options.endpoint !== undefined) {
|
|
53
|
-
s3Config.forcePathStyle = options.endpoint.startsWith('http://')
|
|
54
|
-
s3Config.endpoint = options.endpoint
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (options.region !== undefined) s3Config.region = options.region
|
|
58
|
-
|
|
59
|
-
if (typeof secrets?.ACCESS_KEY_ID === 'string') {
|
|
60
|
-
assert.ok(secrets.SECRET_ACCESS_KEY !== undefined,
|
|
61
|
-
'SECRET_ACCESS_KEY is required if ACCESS_KEY_ID is provided')
|
|
62
|
-
|
|
63
|
-
s3Config.credentials = {
|
|
64
|
-
accessKeyId: secrets.ACCESS_KEY_ID,
|
|
65
|
-
secretAccessKey: secrets.SECRET_ACCESS_KEY
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
this.client = new S3Client(s3Config)
|
|
70
|
-
|
|
71
|
-
this.client.middlewareStack.add((next, _context) => async (args) => {
|
|
72
|
-
if ('Key' in args.input && typeof args.input.Key === 'string')
|
|
73
|
-
// removes leading slash
|
|
74
|
-
|
|
75
|
-
args.input.Key = args.input.Key.replace(/^\//, '')
|
|
76
|
-
|
|
77
|
-
if ('Prefix' in args.input && typeof args.input.Prefix === 'string')
|
|
78
|
-
// removes leading slash and ensures finishing slash
|
|
79
|
-
|
|
80
|
-
args.input.Prefix = args.input.Prefix.replace(/^\/|\/$/g, '') + '/'
|
|
81
|
-
|
|
82
|
-
return next(args)
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
step: 'initialize',
|
|
86
|
-
priority: 'high',
|
|
87
|
-
name: 'normalizesSlashesInPath'
|
|
88
|
-
})
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
public async get (Key: string): Promise<Readable | null> {
|
|
92
|
-
try {
|
|
93
|
-
const fileResponse = await this.client.send(new GetObjectCommand({
|
|
94
|
-
Bucket: this.bucket,
|
|
95
|
-
Key
|
|
96
|
-
}))
|
|
97
|
-
|
|
98
|
-
if (fileResponse.Body === undefined) return null // should never happen
|
|
99
|
-
|
|
100
|
-
if (fileResponse.Body instanceof Readable) return fileResponse.Body
|
|
101
|
-
|
|
102
|
-
return Readable.fromWeb((fileResponse.Body instanceof Blob
|
|
103
|
-
? fileResponse.Body.stream()
|
|
104
|
-
: fileResponse.Body) as ReadableStream) // types mismatch between Node 20 and aws-sdk
|
|
105
|
-
} catch (err) {
|
|
106
|
-
if (err instanceof NotFound || err instanceof NoSuchKey)
|
|
107
|
-
return null
|
|
108
|
-
else
|
|
109
|
-
throw err
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
public async list (prefix: string): Promise<string[]> {
|
|
114
|
-
return (await this.listObjects(prefix))
|
|
115
|
-
.map(({ Key }) => posix.basename(Key!))
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
public async put (path: string, filename: string, stream: Readable): Promise<void> {
|
|
119
|
-
await new Upload({
|
|
120
|
-
client: this.client,
|
|
121
|
-
params: {
|
|
122
|
-
Bucket: this.bucket,
|
|
123
|
-
Key: join(path, filename),
|
|
124
|
-
Body: stream
|
|
125
|
-
}
|
|
126
|
-
}).done()
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Deletes either a single object or "directory" - all objects
|
|
131
|
-
* with given prefix (prefix will be enforced to finish with `/`)
|
|
132
|
-
* @param Key - key name or path prefix
|
|
133
|
-
*/
|
|
134
|
-
public async delete (Key: string): Promise<void> {
|
|
135
|
-
const { client, bucket: Bucket } = this
|
|
136
|
-
|
|
137
|
-
// checking if given key is a single file
|
|
138
|
-
if (!Key.endsWith('/'))
|
|
139
|
-
try {
|
|
140
|
-
// DeleteObject on S3 returns no error if object does not exist
|
|
141
|
-
await client.send(new HeadObjectCommand({ Bucket, Key }))
|
|
142
|
-
await client.send(new DeleteObjectCommand({ Bucket, Key }))
|
|
143
|
-
|
|
144
|
-
return
|
|
145
|
-
} catch (err) {
|
|
146
|
-
assert.ok(err instanceof NotFound || err instanceof NoSuchKey, err as Error)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const objectsToRemove: ObjectIdentifier[] = await this.listObjects(Key)
|
|
150
|
-
|
|
151
|
-
// Removing all objects in parallel in batches
|
|
152
|
-
await Promise.all((function * () {
|
|
153
|
-
while (objectsToRemove.length > 0)
|
|
154
|
-
yield client.send(new DeleteObjectsCommand({
|
|
155
|
-
Bucket,
|
|
156
|
-
Delete: {
|
|
157
|
-
Objects: objectsToRemove.splice(0,
|
|
158
|
-
1000 /* max batch size for DeleteObjects */)
|
|
159
|
-
}
|
|
160
|
-
}))
|
|
161
|
-
})())
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
public async move (from: string, keyTo: string): Promise<void> {
|
|
165
|
-
await this.client.send(new CopyObjectCommand({
|
|
166
|
-
Bucket: this.bucket,
|
|
167
|
-
Key: keyTo,
|
|
168
|
-
CopySource: join(this.bucket, from)
|
|
169
|
-
}))
|
|
170
|
-
|
|
171
|
-
await this.client.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: from }))
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
public async moveDir (from: string, to: string): Promise<void> {
|
|
175
|
-
const objects: ObjectIdentifier[] = await this.listObjects(from)
|
|
176
|
-
|
|
177
|
-
await Promise.all(objects.map(async ({ Key }) => {
|
|
178
|
-
const ent = posix.basename(Key!)
|
|
179
|
-
const source = posix.join(from, ent)
|
|
180
|
-
const target = posix.join(to, ent)
|
|
181
|
-
|
|
182
|
-
return this.move(source, target)
|
|
183
|
-
}))
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
private async listObjects (Prefix: string): Promise<ObjectIdentifier[]> {
|
|
187
|
-
const { client, bucket: Bucket } = this
|
|
188
|
-
const objects: ObjectIdentifier[] = []
|
|
189
|
-
|
|
190
|
-
for await (const page of paginateListObjectsV2({ client }, { Bucket, Prefix }))
|
|
191
|
-
for (const { Key } of page.Contents ?? [])
|
|
192
|
-
objects.push({ Key })
|
|
193
|
-
|
|
194
|
-
return objects
|
|
195
|
-
}
|
|
196
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { tmpdir } from 'node:os'
|
|
2
|
-
import { join } from 'node:path'
|
|
3
|
-
import { FileSystem } from './FileSystem'
|
|
4
|
-
import type { ProviderSecrets } from '../Provider'
|
|
5
|
-
|
|
6
|
-
export interface TemporaryOptions {
|
|
7
|
-
directory: string
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export class Temporary extends FileSystem {
|
|
11
|
-
public constructor (options: TemporaryOptions, secrets?: ProviderSecrets) {
|
|
12
|
-
const path = join(tmpdir(), options.directory)
|
|
13
|
-
|
|
14
|
-
super({ path }, secrets)
|
|
15
|
-
}
|
|
16
|
-
}
|
package/source/providers/Test.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { Temporary, type TemporaryOptions } from './Temporary'
|
|
2
|
-
import type { ProviderSecret, ProviderSecrets } from '../Provider'
|
|
3
|
-
|
|
4
|
-
export class Test extends Temporary {
|
|
5
|
-
public static override readonly SECRETS: readonly ProviderSecret[] = [
|
|
6
|
-
{ name: 'USERNAME' },
|
|
7
|
-
{ name: 'PASSWORD' }
|
|
8
|
-
]
|
|
9
|
-
|
|
10
|
-
public constructor (options: TemporaryOptions, secrets?: ProviderSecrets) {
|
|
11
|
-
super(options, secrets)
|
|
12
|
-
}
|
|
13
|
-
}
|