@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.
Files changed (88) hide show
  1. package/package.json +12 -11
  2. package/readme.md +63 -19
  3. package/schemas/annotation.cos.yaml +10 -0
  4. package/schemas/fs.cos.yaml +9 -0
  5. package/schemas/mem.cos.yaml +6 -0
  6. package/schemas/s3.cos.yaml +16 -0
  7. package/schemas/test.cos.yaml +8 -0
  8. package/schemas/tmp.cos.yaml +9 -0
  9. package/source/Annotation.ts +39 -0
  10. package/source/Aspect.ts +6 -4
  11. package/source/Factory.ts +31 -28
  12. package/source/Provider.ts +30 -5
  13. package/source/Scanner.ts +3 -3
  14. package/source/Storage.test.ts +110 -105
  15. package/source/Storage.ts +13 -6
  16. package/source/deployment.ts +21 -29
  17. package/source/providers/Declaration.ts +10 -0
  18. package/source/providers/FileSystem.test.ts +1 -9
  19. package/source/providers/FileSystem.ts +20 -15
  20. package/source/providers/Memory.ts +41 -0
  21. package/source/providers/S3.test.ts +133 -0
  22. package/source/providers/S3.ts +114 -39
  23. package/source/providers/Temporary.ts +8 -6
  24. package/source/providers/Test.ts +8 -8
  25. package/source/providers/index.test.ts +24 -19
  26. package/source/providers/index.ts +10 -9
  27. package/source/providers/readme.md +1 -1
  28. package/source/schemas.test.ts +58 -0
  29. package/source/schemas.ts +15 -0
  30. package/source/test/util.ts +25 -54
  31. package/transpiled/Annotation.d.ts +3 -0
  32. package/transpiled/Annotation.js +57 -0
  33. package/transpiled/Annotation.js.map +1 -0
  34. package/transpiled/Aspect.d.ts +8 -0
  35. package/transpiled/Aspect.js +25 -0
  36. package/transpiled/Aspect.js.map +1 -0
  37. package/transpiled/Entry.d.ts +14 -0
  38. package/transpiled/Entry.js +3 -0
  39. package/transpiled/Entry.js.map +1 -0
  40. package/transpiled/Factory.d.ts +9 -0
  41. package/transpiled/Factory.js +53 -0
  42. package/transpiled/Factory.js.map +1 -0
  43. package/transpiled/Provider.d.ts +20 -0
  44. package/transpiled/Provider.js +36 -0
  45. package/transpiled/Provider.js.map +1 -0
  46. package/transpiled/Scanner.d.ts +26 -0
  47. package/transpiled/Scanner.js +98 -0
  48. package/transpiled/Scanner.js.map +1 -0
  49. package/transpiled/Storage.d.ts +32 -0
  50. package/transpiled/Storage.js +176 -0
  51. package/transpiled/Storage.js.map +1 -0
  52. package/transpiled/deployment.d.ts +5 -0
  53. package/transpiled/deployment.js +68 -0
  54. package/transpiled/deployment.js.map +1 -0
  55. package/transpiled/index.d.ts +4 -0
  56. package/transpiled/index.js +10 -0
  57. package/transpiled/index.js.map +1 -0
  58. package/transpiled/manifest.d.ts +1 -0
  59. package/transpiled/manifest.js +9 -0
  60. package/transpiled/manifest.js.map +1 -0
  61. package/transpiled/providers/Declaration.d.ts +14 -0
  62. package/transpiled/providers/Declaration.js +3 -0
  63. package/transpiled/providers/Declaration.js.map +1 -0
  64. package/transpiled/providers/FileSystem.d.ts +15 -0
  65. package/transpiled/providers/FileSystem.js +44 -0
  66. package/transpiled/providers/FileSystem.js.map +1 -0
  67. package/transpiled/providers/Memory.d.ts +13 -0
  68. package/transpiled/providers/Memory.js +60 -0
  69. package/transpiled/providers/Memory.js.map +1 -0
  70. package/transpiled/providers/S3.d.ts +27 -0
  71. package/transpiled/providers/S3.js +154 -0
  72. package/transpiled/providers/S3.js.map +1 -0
  73. package/transpiled/providers/Temporary.d.ts +8 -0
  74. package/transpiled/providers/Temporary.js +14 -0
  75. package/transpiled/providers/Temporary.js.map +1 -0
  76. package/transpiled/providers/Test.d.ts +6 -0
  77. package/transpiled/providers/Test.js +15 -0
  78. package/transpiled/providers/Test.js.map +1 -0
  79. package/transpiled/providers/index.d.ts +13 -0
  80. package/transpiled/providers/index.js +16 -0
  81. package/transpiled/providers/index.js.map +1 -0
  82. package/transpiled/schemas.d.ts +9 -0
  83. package/transpiled/schemas.js +14 -0
  84. package/transpiled/schemas.js.map +1 -0
  85. package/transpiled/test/util.d.ts +29 -0
  86. package/transpiled/test/util.js +38 -0
  87. package/transpiled/test/util.js.map +1 -0
  88. package/transpiled/tsconfig.tsbuildinfo +1 -0
@@ -1,13 +1,5 @@
1
1
  import { FileSystem } from './FileSystem'
2
2
 
3
3
  it('should be ok', async () => {
4
- const url = new URL('file:///path')
5
-
6
- expect(() => new FileSystem(url)).not.toThrow()
7
- })
8
-
9
- it('should throw if url contains host', async () => {
10
- const url = new URL('file://host/path')
11
-
12
- expect(() => new FileSystem(url)).toThrow()
4
+ expect(() => new FileSystem({ path: 'path' })).not.toThrow()
13
5
  })
@@ -1,46 +1,51 @@
1
1
  import { type Readable } from 'node:stream'
2
2
  import { dirname, join } from 'node:path'
3
- import { createReadStream } from 'node:fs'
4
3
  import fs from 'node:fs/promises'
5
- import fse from 'fs-extra'
6
- import { type Provider } from '../Provider'
4
+ import { Provider } from '../Provider'
5
+ import type { ProviderSecrets } from '../Provider'
7
6
 
8
- export class FileSystem implements Provider {
7
+ export interface FileSystemOptions {
8
+ path: string
9
+ }
10
+
11
+ export class FileSystem extends Provider<FileSystemOptions> {
9
12
  protected readonly path: string
10
13
 
11
- public constructor (url: URL) {
12
- if (url.host !== '')
13
- throw new Error('File system URL must not contain host')
14
+ public constructor (options: FileSystemOptions, secrets?: ProviderSecrets) {
15
+ super(options, secrets)
14
16
 
15
- this.path = url.pathname
17
+ this.path = options.path
16
18
  }
17
19
 
18
20
  public async get (path: string): Promise<Readable | null> {
19
- path = join(this.path, path)
21
+ try {
22
+ const fd = await fs.open(join(this.path, path))
20
23
 
21
- if (!await fse.exists(path))
22
- return null
24
+ return fd.createReadStream()
25
+ } catch (err) {
26
+ if ((err as NodeJS.ErrnoException)?.code === 'ENOENT') return null
23
27
 
24
- return createReadStream(path)
28
+ throw err
29
+ }
25
30
  }
26
31
 
27
32
  public async put (rel: string, filename: string, stream: Readable): Promise<void> {
28
33
  const dir = join(this.path, rel)
29
34
  const path = join(dir, filename)
30
35
 
31
- await fse.ensureDir(dir)
36
+ await fs.mkdir(dirname(path), { recursive: true })
32
37
  await fs.writeFile(path, stream)
33
38
  }
34
39
 
35
40
  public async delete (path: string): Promise<void> {
36
- await fse.remove(join(this.path, path))
41
+ await fs.rm(join(this.path, path), { recursive: true, force: true })
37
42
  }
38
43
 
39
44
  public async move (from: string, to: string): Promise<void> {
40
45
  from = join(this.path, from)
41
46
  to = join(this.path, to)
42
47
 
43
- await fse.ensureDir(dirname(to))
48
+ await fs.mkdir(dirname(to), { recursive: true })
44
49
  await fs.rename(from, to)
45
50
  }
46
51
  }
@@ -0,0 +1,41 @@
1
+ import { Readable } from 'node:stream'
2
+ import { join } 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 override 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 override async put (path: string, filename: string, stream: Readable): Promise<void> {
23
+ this.storage.set(join(path, filename), await buffer(stream))
24
+ }
25
+
26
+ public override async delete (path: string): Promise<void> {
27
+ for (const f of this.storage.keys())
28
+ if (f.startsWith(path)) this.storage.delete(f)
29
+ }
30
+
31
+ public override async move (from: string, to: string): Promise<void> {
32
+ assert.notEqual(from, to, 'Source and destination are the same')
33
+
34
+ const buf = this.storage.get(from)
35
+
36
+ assert.ok(buf !== undefined, `File not found: ${from}`)
37
+
38
+ this.storage.set(to, buf)
39
+ this.storage.delete(from)
40
+ }
41
+ }
@@ -0,0 +1,133 @@
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
+ expect(headHandler).toHaveBeenCalledTimes(1)
109
+ expect(listHandler).toHaveBeenCalledTimes(2)
110
+ expect(deleteHandler).toHaveBeenCalledTimes(1)
111
+ })
112
+
113
+ test('should be able to upload a file to S3', async () => {
114
+ const body = Readable.from('test content')
115
+
116
+ const handler = jest.fn().mockReturnValue(HttpResponse.xml('', {
117
+ status: 200,
118
+ headers: {
119
+ ETag: 'test-etag',
120
+ 'x-amz-id-2': 'test_id_2',
121
+ 'x-amz-request-id': 'test_request_id',
122
+ Server: 'AmazonS3'
123
+ }
124
+ }))
125
+
126
+ s3endpoint.use(http.put(`${S3_ENDPOINT}/some/absolute/path/filename`,
127
+ handler))
128
+
129
+ await provider.put('/some/absolute/path', 'filename', body)
130
+ expect(handler).toHaveBeenCalledTimes(1)
131
+ expect(body.readableEnded).toBe(true)
132
+ })
133
+ })
@@ -1,91 +1,166 @@
1
- import { type Readable } from 'node:stream'
1
+ import { Readable } from 'node:stream'
2
+ import { Blob } from 'node:buffer'
2
3
  import { join } from 'node:path/posix'
4
+ import assert from 'node:assert'
3
5
  import { Upload } from '@aws-sdk/lib-storage'
4
6
  import {
5
7
  S3Client,
6
8
  GetObjectCommand,
7
9
  CopyObjectCommand,
8
- ListObjectsV2Command,
9
10
  DeleteObjectsCommand,
10
- type S3ClientConfigType
11
+ paginateListObjectsV2,
12
+ HeadObjectCommand,
13
+ NotFound,
14
+ DeleteObjectCommand,
15
+ NoSuchKey,
16
+ type S3ClientConfigType,
17
+ type ObjectIdentifier
11
18
  } from '@aws-sdk/client-s3'
12
- import { type Provider } from '../Provider'
19
+ import * as nodeNativeFetch from 'smithy-node-native-fetch'
20
+ import { Provider, type ProviderSecret, type ProviderSecrets } from '../Provider'
21
+ import type { ReadableStream } from 'node:stream/web'
22
+
23
+ export interface S3Options {
24
+ bucket: string
25
+ region?: string
26
+ prefix?: string
27
+ endpoint?: string
28
+ }
29
+
30
+ type S3Secrets = ProviderSecrets<'ACCESS_KEY_ID' | 'SECRET_ACCESS_KEY'>
31
+
32
+ export class S3 extends Provider<S3Options> {
33
+ public static override readonly SECRETS: readonly ProviderSecret[] = [
34
+ { name: 'ACCESS_KEY_ID', optional: true },
35
+ { name: 'SECRET_ACCESS_KEY', optional: true }
36
+ ]
13
37
 
14
- export class S3 implements Provider {
15
38
  protected readonly bucket: string
16
39
  protected readonly client: S3Client
17
40
 
18
- public constructor (url: URL, secrets: Record<string, string>) {
19
- this.bucket = url.pathname.split('/')[1]
41
+ public constructor (options: S3Options, secrets?: S3Secrets) {
42
+ super(options)
20
43
 
21
- if (this.bucket === undefined || this.bucket === '')
22
- throw new Error('S3 bucket not specified')
44
+ this.bucket = options.bucket
23
45
 
24
46
  const s3Config: S3ClientConfigType = {
25
- credentials: {
47
+ retryMode: 'adaptive',
48
+ ...nodeNativeFetch
49
+ }
50
+
51
+ if (options.endpoint !== undefined) {
52
+ s3Config.forcePathStyle = options.endpoint.startsWith('http://')
53
+ s3Config.endpoint = options.endpoint
54
+ }
55
+
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')
61
+
62
+ s3Config.credentials = {
26
63
  accessKeyId: secrets.ACCESS_KEY_ID,
27
64
  secretAccessKey: secrets.SECRET_ACCESS_KEY
28
- },
29
- region: url.host,
30
- endpoint: url.searchParams.get('endpoint') ?? undefined
65
+ }
31
66
  }
32
67
 
33
68
  this.client = new S3Client(s3Config)
69
+
70
+ this.client.middlewareStack.add((next, _context) => async (args) => {
71
+ if ('Key' in args.input && typeof args.input.Key === 'string')
72
+ // removes leading slash
73
+ args.input.Key = args.input.Key.replace(/^\//, '')
74
+
75
+ if ('Prefix' in args.input && typeof args.input.Prefix === 'string')
76
+ // removes leading slash and ensures finishing slash
77
+ args.input.Prefix = args.input.Prefix.replace(/^\/|\/$/g, '') + '/'
78
+
79
+ return next(args)
80
+ },
81
+ {
82
+ step: 'initialize',
83
+ priority: 'high',
84
+ name: 'normalizesSlashesInPath'
85
+ })
34
86
  }
35
87
 
36
- public async get (key: string): Promise<Readable | null> {
88
+ public async get (Key: string): Promise<Readable | null> {
37
89
  try {
38
90
  const fileResponse = await this.client.send(new GetObjectCommand({
39
91
  Bucket: this.bucket,
40
- Key: key
92
+ Key
41
93
  }))
42
94
 
43
- return fileResponse.Body as Readable
44
- } catch (err: any) {
45
- if (err?.Code === 'NoSuchKey') return null
95
+ if (fileResponse.Body === undefined) return null // should never happen
96
+
97
+ if (fileResponse.Body instanceof Readable) return fileResponse.Body
98
+
99
+ return Readable.fromWeb((fileResponse.Body instanceof Blob
100
+ ? fileResponse.Body.stream()
101
+ : fileResponse.Body) as ReadableStream) // types mismatch between Node 20 and aws-sdk
102
+ } catch (err) {
103
+ if (err instanceof NotFound || err instanceof NoSuchKey) return null
46
104
  else throw err
47
105
  }
48
106
  }
49
107
 
50
108
  public async put (path: string, filename: string, stream: Readable): Promise<void> {
51
- const key = join(path, filename)
52
-
53
109
  await new Upload({
54
110
  client: this.client,
55
111
  params: {
56
112
  Bucket: this.bucket,
57
- Key: key,
113
+ Key: join(path, filename),
58
114
  Body: stream
59
115
  }
60
116
  }).done()
61
117
  }
62
118
 
63
- public async delete (key: string): Promise<void> {
64
- const listResponse = await this.client.send(new ListObjectsV2Command({
65
- Bucket: this.bucket,
66
- Prefix: key.substring(1)
67
- }))
119
+ /**
120
+ * Deletes either a single object or "directory" - all objects
121
+ * with given prefix (prefix will be enforced to finish with `/`)
122
+ * @param Key - key name or path prefix
123
+ */
124
+ public async delete (Key: string): Promise<void> {
125
+ const { client, bucket: Bucket } = this
68
126
 
69
- if (listResponse.Contents !== undefined && listResponse.Contents.length > 0)
70
- await this.client.send(new DeleteObjectsCommand({
71
- Bucket: this.bucket,
72
- Delete: {
73
- Objects: listResponse.Contents.map(({ Key }) => ({ Key }))
74
- }
75
- }))
76
- }
127
+ // checking if given key is a single file
128
+ if (!Key.endsWith('/'))
129
+ try {
130
+ // DeleteObject on S3 returns no error if object does not exist
131
+ await client.send(new HeadObjectCommand({ Bucket, Key }))
132
+ await client.send(new DeleteObjectCommand({ Bucket, Key }))
77
133
 
78
- public async move (from: string, keyTo: string): Promise<void> {
79
- const keyFrom = join(this.bucket, from)
134
+ return
135
+ } catch (err) {
136
+ assert.ok(err instanceof NotFound || err instanceof NoSuchKey, err as Error)
137
+ }
138
+
139
+ const objectsToRemove: ObjectIdentifier[] = []
80
140
 
81
- await new Promise((resolve) => setTimeout(resolve, 500))
141
+ for await (const page of paginateListObjectsV2({ client }, { Bucket, Prefix: Key }))
142
+ for (const { Key } of page.Contents ?? []) objectsToRemove.push({ Key })
82
143
 
144
+ // Removing all objects in parallel in batches
145
+ await Promise.all((function * () {
146
+ while (objectsToRemove.length > 0)
147
+ yield client.send(new DeleteObjectsCommand({
148
+ Bucket,
149
+ Delete: {
150
+ Objects: objectsToRemove.splice(0,
151
+ 1000 /* max batch size for DeleteObjects */)
152
+ }
153
+ }))
154
+ })())
155
+ }
156
+
157
+ public async move (from: string, keyTo: string): Promise<void> {
83
158
  await this.client.send(new CopyObjectCommand({
84
159
  Bucket: this.bucket,
85
160
  Key: keyTo,
86
- CopySource: keyFrom
161
+ CopySource: join(this.bucket, from)
87
162
  }))
88
163
 
89
- await this.delete(from)
164
+ await this.client.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: from }))
90
165
  }
91
166
  }
@@ -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
- export class Temporary extends FileSystem {
7
- protected override readonly path: string
6
+ export interface TemporaryOptions {
7
+ directory: string
8
+ }
8
9
 
9
- public constructor (url: URL) {
10
- super(url)
10
+ export class Temporary extends FileSystem {
11
+ public constructor (options: TemporaryOptions, secrets?: ProviderSecrets) {
12
+ const path = join(tmpdir(), options.directory)
11
13
 
12
- this.path = join(tmpdir(), url.pathname)
14
+ super({ path }, secrets)
13
15
  }
14
16
  }
@@ -1,13 +1,13 @@
1
- import assert from 'node:assert'
2
- import { Temporary } from './Temporary'
1
+ import { Temporary, type TemporaryOptions } from './Temporary'
2
+ import type { ProviderSecret, ProviderSecrets } from '../Provider'
3
3
 
4
4
  export class Test extends Temporary {
5
- public static readonly SECRETS = ['USERNAME', 'PASSWORD']
5
+ public static override readonly SECRETS: readonly ProviderSecret[] = [
6
+ { name: 'USERNAME' },
7
+ { name: 'PASSWORD' }
8
+ ]
6
9
 
7
- public constructor (url: URL, secrets: Record<string, string>) {
8
- super(url)
9
-
10
- assert(secrets.USERNAME !== undefined, 'Missing USERNAME')
11
- assert(secrets.PASSWORD !== undefined, 'Missing PASSWORD')
10
+ public constructor (options: TemporaryOptions, secrets?: ProviderSecrets) {
11
+ super(options, secrets)
12
12
  }
13
13
  }
@@ -1,21 +1,26 @@
1
1
  import { type Readable } from 'node:stream'
2
- import { buffer } from '@toa.io/streams'
3
- import { cases, open, rnd, read } from '../test/util'
2
+ import { buffer } from 'node:stream/consumers'
3
+ import { readFile } from 'node:fs/promises'
4
+ import { createReadStream } from 'node:fs'
5
+ import path from 'node:path'
6
+ import { randomUUID } from 'node:crypto'
7
+ import { suites } from '../test/util'
4
8
  import { providers } from './index'
9
+ import type { ProviderConstructor } from '../Provider'
5
10
 
6
- describe.each(cases)('%s', (...suite) => {
7
- const [protocol, url, secrets, init] = suite
8
- const Provider = providers[protocol]
9
- const provider = new Provider(url, secrets)
11
+ describe.each(suites)('$provider', (suite) => {
12
+ const it = suite.run ? global.it : global.it.skip
13
+ const Provider: ProviderConstructor = providers[suite.provider]
14
+ const provider = new Provider(suite.options, suite.secrets)
10
15
 
11
16
  let dir: string
12
17
 
13
18
  beforeAll(async () => {
14
- await init?.(url, secrets)
19
+ process.chdir(path.resolve(__dirname, '../test/'))
15
20
  })
16
21
 
17
22
  beforeEach(() => {
18
- dir = '/' + rnd()
23
+ dir = '/' + randomUUID()
19
24
  })
20
25
 
21
26
  it('should be', async () => {
@@ -23,39 +28,39 @@ describe.each(cases)('%s', (...suite) => {
23
28
  })
24
29
 
25
30
  it('should return null if file not found', async () => {
26
- const result = await provider.get(rnd())
31
+ const result = await provider.get(randomUUID())
27
32
 
28
33
  expect(result).toBeNull()
29
34
  })
30
35
 
31
36
  it('should create entry', async () => {
32
- const stream = open('lenna.png')
37
+ const stream = createReadStream('lenna.png')
33
38
 
34
39
  await provider.put(dir, 'lenna.png', stream)
35
40
 
36
41
  const readable = await provider.get(dir + '/lenna.png') as Readable
37
42
  const output = await buffer(readable)
38
- const lenna = await read('lenna.png')
43
+ const lenna = await readFile('lenna.png')
39
44
 
40
45
  expect(output.compare(lenna)).toBe(0)
41
46
  })
42
47
 
43
48
  it('should overwrite existing entry', async () => {
44
- const stream0 = open('lenna.png')
45
- const stream1 = open('albert.jpg')
49
+ const stream0 = createReadStream('lenna.png')
50
+ const stream1 = createReadStream('albert.jpg')
46
51
 
47
52
  await provider.put(dir, 'lenna.png', stream0)
48
53
  await provider.put(dir, 'lenna.png', stream1)
49
54
 
50
55
  const readable = await provider.get(dir + '/lenna.png') as Readable
51
56
  const output = await buffer(readable)
52
- const albert = await read('albert.jpg')
57
+ const albert = await readFile('albert.jpg')
53
58
 
54
59
  expect(output.compare(albert)).toBe(0)
55
60
  })
56
61
 
57
62
  it('should get by path', async () => {
58
- const stream = open('lenna.png')
63
+ const stream = createReadStream('lenna.png')
59
64
 
60
65
  await provider.put(dir, 'lenna.png', stream)
61
66
 
@@ -77,7 +82,7 @@ describe.each(cases)('%s', (...suite) => {
77
82
  */
78
83
 
79
84
  it('should delete entry', async () => {
80
- const stream = open('lenna.png')
85
+ const stream = createReadStream('lenna.png')
81
86
 
82
87
  await provider.put(dir, 'lenna.png', stream)
83
88
  await provider.delete(dir + '/lenna.png')
@@ -92,7 +97,7 @@ describe.each(cases)('%s', (...suite) => {
92
97
  })
93
98
 
94
99
  it('should delete directory', async () => {
95
- const stream = open('lenna.png')
100
+ const stream = createReadStream('lenna.png')
96
101
 
97
102
  await provider.put(dir, 'lenna.png', stream)
98
103
  await provider.delete(dir)
@@ -103,8 +108,8 @@ describe.each(cases)('%s', (...suite) => {
103
108
  })
104
109
 
105
110
  it('should move an entry', async () => {
106
- const stream = open('lenna.png')
107
- const dir2 = '/' + rnd()
111
+ const stream = createReadStream('lenna.png')
112
+ const dir2 = '/' + randomUUID()
108
113
 
109
114
  await provider.put(dir, 'lenna.png', stream)
110
115
  await provider.move(dir + '/lenna.png', dir2 + '/lenna2.png')
@@ -1,15 +1,16 @@
1
- import { type Provider } from '../Provider'
2
1
  import { FileSystem } from './FileSystem'
3
2
  import { S3 } from './S3'
4
3
  import { Temporary } from './Temporary'
5
4
  import { Test } from './Test'
5
+ import { InMemory } from './Memory'
6
+ import type { ProviderConstructor } from '../Provider'
6
7
 
7
- export const providers: Record<string, ProviderClass> = {
8
- 'file:': FileSystem,
9
- 'tmp:': Temporary,
10
- 'test:': Test,
11
- 's3:': S3
12
- }
8
+ export const providers = {
9
+ s3: S3,
10
+ fs: FileSystem,
11
+ tmp: Temporary,
12
+ mem: InMemory,
13
+ test: Test
14
+ } as const satisfies Record<string, ProviderConstructor>
13
15
 
14
- export type ProviderClass =
15
- (new (url: URL, secrets: Record<string, string>) => Provider) & { SECRETS?: string[] }
16
+ export type { Declaration } from './Declaration'
@@ -1,6 +1,6 @@
1
1
  # Developing a provider
2
2
 
3
- 1. Add an implementaion of the [Provider](../Provider.ts) interface under this directory.
3
+ 1. Add an implementation of the [Provider](../Provider.ts) interface under this directory.
4
4
  2. Add an entry to the provider map in [`index.ts`](./index.ts).
5
5
  3. Add a suite to the `suites` object in [`util.ts`](../.test/util.ts).
6
6
  4. Run `$ npm test` in the [`storages` directory](../..).