@toa.io/extensions.storages 1.0.0-alpha.13 → 1.0.0-alpha.143

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 (90) hide show
  1. package/package.json +13 -14
  2. package/readme.md +94 -49
  3. package/schemas/annotation.cos.yaml +1 -0
  4. package/schemas/cloudinary.cos.yaml +30 -0
  5. package/schemas/fs.cos.yaml +2 -0
  6. package/schemas/s3.cos.yaml +1 -0
  7. package/schemas/tmp.cos.yaml +0 -1
  8. package/source/Entry.ts +11 -11
  9. package/source/Factory.ts +16 -9
  10. package/source/Provider.ts +19 -19
  11. package/source/Scanner.ts +57 -24
  12. package/source/Secrets.ts +6 -0
  13. package/source/Storage.test.ts +103 -279
  14. package/source/Storage.ts +55 -204
  15. package/source/deployment.ts +48 -15
  16. package/source/errors.ts +3 -0
  17. package/source/index.ts +1 -1
  18. package/source/manifest.ts +7 -6
  19. package/source/providers/Cloudinary.ts +274 -0
  20. package/source/providers/Declaration.ts +2 -1
  21. package/source/providers/FileSystem.ts +79 -25
  22. package/source/providers/S3.ts +81 -80
  23. package/source/providers/Temporary.ts +2 -3
  24. package/source/providers/Test.ts +4 -4
  25. package/source/providers/index.test.ts +102 -76
  26. package/source/providers/index.ts +9 -4
  27. package/source/schemas.ts +1 -0
  28. package/source/test/.env.example +4 -0
  29. package/source/test/arny.jpg +0 -0
  30. package/source/test/lenna.48x48.jpeg +0 -0
  31. package/source/test/sample.avi +0 -0
  32. package/source/test/sample.svg +4 -0
  33. package/source/test/sample.wav +0 -0
  34. package/source/test/util.ts +54 -12
  35. package/transpiled/Entry.d.ts +12 -10
  36. package/transpiled/Factory.js +11 -5
  37. package/transpiled/Factory.js.map +1 -1
  38. package/transpiled/Provider.d.ts +15 -15
  39. package/transpiled/Provider.js +3 -3
  40. package/transpiled/Provider.js.map +1 -1
  41. package/transpiled/Scanner.d.ts +5 -2
  42. package/transpiled/Scanner.js +49 -22
  43. package/transpiled/Scanner.js.map +1 -1
  44. package/transpiled/Secrets.d.ts +5 -0
  45. package/transpiled/Secrets.js +3 -0
  46. package/transpiled/Secrets.js.map +1 -0
  47. package/transpiled/Storage.d.ts +10 -19
  48. package/transpiled/Storage.js +47 -153
  49. package/transpiled/Storage.js.map +1 -1
  50. package/transpiled/deployment.d.ts +1 -1
  51. package/transpiled/deployment.js +40 -15
  52. package/transpiled/deployment.js.map +1 -1
  53. package/transpiled/errors.d.ts +2 -0
  54. package/transpiled/errors.js +6 -0
  55. package/transpiled/errors.js.map +1 -0
  56. package/transpiled/index.d.ts +1 -1
  57. package/transpiled/manifest.js +5 -2
  58. package/transpiled/manifest.js.map +1 -1
  59. package/transpiled/providers/Cloudinary.d.ts +42 -0
  60. package/transpiled/providers/Cloudinary.js +189 -0
  61. package/transpiled/providers/Cloudinary.js.map +1 -0
  62. package/transpiled/providers/Declaration.d.ts +3 -2
  63. package/transpiled/providers/FileSystem.d.ts +15 -7
  64. package/transpiled/providers/FileSystem.js +64 -24
  65. package/transpiled/providers/FileSystem.js.map +1 -1
  66. package/transpiled/providers/S3.d.ts +14 -14
  67. package/transpiled/providers/S3.js +62 -60
  68. package/transpiled/providers/S3.js.map +1 -1
  69. package/transpiled/providers/Temporary.d.ts +1 -2
  70. package/transpiled/providers/Temporary.js +2 -2
  71. package/transpiled/providers/Temporary.js.map +1 -1
  72. package/transpiled/providers/Test.d.ts +3 -3
  73. package/transpiled/providers/Test.js +2 -2
  74. package/transpiled/providers/Test.js.map +1 -1
  75. package/transpiled/providers/index.d.ts +6 -2
  76. package/transpiled/providers/index.js +2 -2
  77. package/transpiled/providers/index.js.map +1 -1
  78. package/transpiled/schemas.d.ts +1 -0
  79. package/transpiled/schemas.js +2 -1
  80. package/transpiled/schemas.js.map +1 -1
  81. package/transpiled/test/util.d.ts +70 -5
  82. package/transpiled/test/util.js +43 -4
  83. package/transpiled/test/util.js.map +1 -1
  84. package/transpiled/tsconfig.tsbuildinfo +1 -1
  85. package/source/providers/FileSystem.test.ts +0 -5
  86. package/source/providers/Memory.ts +0 -41
  87. package/source/providers/S3.test.ts +0 -133
  88. package/transpiled/providers/Memory.d.ts +0 -13
  89. package/transpiled/providers/Memory.js +0 -60
  90. package/transpiled/providers/Memory.js.map +0 -1
@@ -1,10 +1,11 @@
1
1
  import type { S3Options } from './S3'
2
+ import type { CloudinaryOptions } from './Cloudinary'
2
3
  import type { FileSystemOptions } from './FileSystem'
3
4
  import type { TemporaryOptions } from './Temporary'
4
5
 
5
6
  export type Declaration =
6
7
  ({ provider: 's3' } & S3Options)
8
+ | ({ provider: 'cloudinary' } & CloudinaryOptions)
7
9
  | ({ provider: 'fs' } & FileSystemOptions)
8
10
  | ({ provider: 'tmp' } & TemporaryOptions)
9
- | ({ provider: 'mem' })
10
11
  | ({ provider: 'test' } & TemporaryOptions)
@@ -1,51 +1,105 @@
1
- import { type Readable } from 'node:stream'
2
- import { dirname, join } from 'node:path'
3
1
  import fs from 'node:fs/promises'
2
+ import { createReadStream } from 'node:fs'
3
+ import { dirname, join } from 'node:path'
4
4
  import { Provider } from '../Provider'
5
- import type { ProviderSecrets } from '../Provider'
5
+ import { ERR_NOT_FOUND } from '../errors'
6
+ import type { Readable } from 'node:stream'
7
+ import type { Maybe } from '@toa.io/types'
8
+ import type { Metadata, Stream } from '../Entry'
6
9
 
7
10
  export interface FileSystemOptions {
8
11
  path: string
12
+ claim?: string
9
13
  }
10
14
 
11
15
  export class FileSystem extends Provider<FileSystemOptions> {
12
- protected readonly path: string
16
+ public override readonly root: string
13
17
 
14
- public constructor (options: FileSystemOptions, secrets?: ProviderSecrets) {
15
- super(options, secrets)
18
+ public constructor (options: FileSystemOptions) {
19
+ super(options)
16
20
 
17
- this.path = options.path
21
+ this.root = options.path
18
22
  }
19
23
 
20
- public async get (path: string): Promise<Readable | null> {
21
- try {
22
- const fd = await fs.open(join(this.path, path))
24
+ public async get (rel: string): Promise<Maybe<Stream>> {
25
+ const path = this.blob(rel)
26
+ const metadata = await this.head(rel)
23
27
 
24
- return fd.createReadStream()
25
- } catch (err) {
26
- if ((err as NodeJS.ErrnoException)?.code === 'ENOENT') return null
28
+ if (metadata instanceof Error)
29
+ return metadata
27
30
 
28
- throw err
29
- }
31
+ const stream = createReadStream(path)
32
+
33
+ return { stream, ...metadata }
34
+ }
35
+
36
+ public async head (rel: string): Promise<Maybe<Metadata>> {
37
+ const path = this.meta(rel)
38
+
39
+ return this.try(async () => {
40
+ const contents = await fs.readFile(path, 'utf8')
41
+
42
+ return JSON.parse(contents)
43
+ })
30
44
  }
31
45
 
32
- public async put (rel: string, filename: string, stream: Readable): Promise<void> {
33
- const dir = join(this.path, rel)
34
- const path = join(dir, filename)
46
+ public async put (rel: string, stream: Readable): Promise<void> {
47
+ const path = this.blob(rel)
48
+ const dir = dirname(path)
35
49
 
36
- await fs.mkdir(dirname(path), { recursive: true })
50
+ await fs.mkdir(dir, { recursive: true })
37
51
  await fs.writeFile(path, stream)
38
52
  }
39
53
 
54
+ public async commit (rel: string, metadata: Metadata): Promise<void> {
55
+ const path = this.meta(rel)
56
+
57
+ await fs.writeFile(path, JSON.stringify(metadata), 'utf8')
58
+ }
59
+
40
60
  public async delete (path: string): Promise<void> {
41
- await fs.rm(join(this.path, path), { recursive: true, force: true })
61
+ await Promise.all([
62
+ fs.rm(this.blob(path), { force: true }),
63
+ fs.rm(this.meta(path), { force: true })
64
+ ])
42
65
  }
43
66
 
44
- public async move (from: string, to: string): Promise<void> {
45
- from = join(this.path, from)
46
- to = join(this.path, to)
67
+ public async move (from: string, to: string): Promise<Maybe<void>> {
68
+ const bf = this.blob(from)
69
+ const bt = this.blob(to)
70
+ const mf = this.meta(from)
71
+ const mt = this.meta(to)
47
72
 
48
- await fs.mkdir(dirname(to), { recursive: true })
49
- await fs.rename(from, to)
73
+ await fs.mkdir(dirname(bt), { recursive: true })
74
+
75
+ return await this.try(async () => {
76
+ await Promise.all([
77
+ fs.rename(bf, bt),
78
+ fs.rename(mf, mt)
79
+ ])
80
+ })
81
+ }
82
+
83
+ private blob (rel: string): string {
84
+ return this.join(rel, '.blob')
85
+ }
86
+
87
+ private meta (rel: string): string {
88
+ return this.join(rel, '.meta')
89
+ }
90
+
91
+ private join (rel: string, ext: string): string {
92
+ return join(this.root, rel) + ext
93
+ }
94
+
95
+ private async try<T = void> (action: () => Promise<T>): Promise<Maybe<T>> {
96
+ try {
97
+ return await action()
98
+ } catch (err: NodeJS.ErrnoException | any) {
99
+ if (err?.code === 'ENOENT')
100
+ return ERR_NOT_FOUND
101
+ else
102
+ throw err
103
+ }
50
104
  }
51
105
  }
@@ -3,22 +3,15 @@ import { Blob } from 'node:buffer'
3
3
  import { join } from 'node:path/posix'
4
4
  import assert from 'node:assert'
5
5
  import { Upload } from '@aws-sdk/lib-storage'
6
- import {
7
- S3Client,
8
- GetObjectCommand,
9
- CopyObjectCommand,
10
- DeleteObjectsCommand,
11
- paginateListObjectsV2,
12
- HeadObjectCommand,
13
- NotFound,
14
- DeleteObjectCommand,
15
- NoSuchKey,
16
- type S3ClientConfigType,
17
- type ObjectIdentifier
18
- } from '@aws-sdk/client-s3'
6
+ import * as s3 from '@aws-sdk/client-s3'
19
7
  import * as nodeNativeFetch from 'smithy-node-native-fetch'
20
- import { Provider, type ProviderSecret, type ProviderSecrets } from '../Provider'
8
+ import { console } from 'openspan'
9
+ import { Provider } from '../Provider'
10
+ import { ERR_NOT_FOUND } from '../errors'
21
11
  import type { ReadableStream } from 'node:stream/web'
12
+ import type { Maybe } from '@toa.io/types'
13
+ import type { Metadata, Stream } from '../Entry'
14
+ import type { Secret, Secrets } from '../Secrets'
22
15
 
23
16
  export interface S3Options {
24
17
  bucket: string
@@ -27,23 +20,23 @@ export interface S3Options {
27
20
  endpoint?: string
28
21
  }
29
22
 
30
- type S3Secrets = ProviderSecrets<'ACCESS_KEY_ID' | 'SECRET_ACCESS_KEY'>
23
+ type S3Secrets = Secrets<'ACCESS_KEY_ID' | 'SECRET_ACCESS_KEY'>
31
24
 
32
25
  export class S3 extends Provider<S3Options> {
33
- public static override readonly SECRETS: readonly ProviderSecret[] = [
26
+ public static override readonly SECRETS: readonly Secret[] = [
34
27
  { name: 'ACCESS_KEY_ID', optional: true },
35
28
  { name: 'SECRET_ACCESS_KEY', optional: true }
36
29
  ]
37
30
 
38
- protected readonly bucket: string
39
- protected readonly client: S3Client
31
+ private readonly bucket: string
32
+ private readonly client: s3.S3Client
40
33
 
41
34
  public constructor (options: S3Options, secrets?: S3Secrets) {
42
- super(options)
35
+ super(options, secrets)
43
36
 
44
37
  this.bucket = options.bucket
45
38
 
46
- const s3Config: S3ClientConfigType = {
39
+ const s3Config: s3.S3ClientConfigType = {
47
40
  retryMode: 'adaptive',
48
41
  ...nodeNativeFetch
49
42
  }
@@ -53,7 +46,8 @@ export class S3 extends Provider<S3Options> {
53
46
  s3Config.endpoint = options.endpoint
54
47
  }
55
48
 
56
- if (options.region !== undefined) s3Config.region = options.region
49
+ if (options.region !== undefined)
50
+ s3Config.region = options.region
57
51
 
58
52
  if (typeof secrets?.ACCESS_KEY_ID === 'string') {
59
53
  assert.ok(secrets.SECRET_ACCESS_KEY !== undefined,
@@ -65,15 +59,15 @@ export class S3 extends Provider<S3Options> {
65
59
  }
66
60
  }
67
61
 
68
- this.client = new S3Client(s3Config)
62
+ this.client = new s3.S3Client(s3Config)
69
63
 
70
64
  this.client.middlewareStack.add((next, _context) => async (args) => {
71
- if ('Key' in args.input && typeof args.input.Key === 'string')
72
65
  // removes leading slash
66
+ if ('Key' in args.input && typeof args.input.Key === 'string')
73
67
  args.input.Key = args.input.Key.replace(/^\//, '')
74
68
 
75
- if ('Prefix' in args.input && typeof args.input.Prefix === 'string')
76
69
  // removes leading slash and ensures finishing slash
70
+ if ('Prefix' in args.input && typeof args.input.Prefix === 'string')
77
71
  args.input.Prefix = args.input.Prefix.replace(/^\/|\/$/g, '') + '/'
78
72
 
79
73
  return next(args)
@@ -85,82 +79,89 @@ export class S3 extends Provider<S3Options> {
85
79
  })
86
80
  }
87
81
 
88
- public async get (Key: string): Promise<Readable | null> {
89
- try {
90
- const fileResponse = await this.client.send(new GetObjectCommand({
82
+ public async get (Key: string): Promise<Maybe<Stream>> {
83
+ return await this.try<Stream>(async () => {
84
+ const entry = await this.client.send(new s3.GetObjectCommand({
91
85
  Bucket: this.bucket,
92
86
  Key
93
87
  }))
94
88
 
95
- if (fileResponse.Body === undefined) return null // should never happen
89
+ const stream = entry.Body instanceof Readable
90
+ ? entry.Body
91
+ : Readable.fromWeb((entry.Body instanceof Blob
92
+ ? entry.Body.stream()
93
+ : entry.Body) as ReadableStream)
96
94
 
97
- if (fileResponse.Body instanceof Readable) return fileResponse.Body
95
+ if (entry.Metadata?.value === undefined)
96
+ return ERR_NOT_FOUND
98
97
 
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
104
- else throw err
105
- }
98
+ const metadata = JSON.parse(entry.Metadata.value)
99
+
100
+ return { stream, ...metadata }
101
+ })
102
+ }
103
+
104
+ public async head (Key: string): Promise<Maybe<Metadata>> {
105
+ return await this.try<Metadata>(async () => {
106
+ const entry = await this.client.send(new s3.HeadObjectCommand({
107
+ Bucket: this.bucket,
108
+ Key
109
+ }))
110
+
111
+ if (entry.Metadata?.value === undefined)
112
+ return ERR_NOT_FOUND
113
+
114
+ return JSON.parse(entry.Metadata.value)
115
+ })
106
116
  }
107
117
 
108
- public async put (path: string, filename: string, stream: Readable): Promise<void> {
118
+ public async put (Key: string, stream: Readable): Promise<void> {
109
119
  await new Upload({
110
120
  client: this.client,
111
121
  params: {
112
122
  Bucket: this.bucket,
113
- Key: join(path, filename),
123
+ Key,
114
124
  Body: stream
115
125
  }
116
126
  }).done()
117
127
  }
118
128
 
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
126
-
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 }))
133
-
134
- return
135
- } catch (err) {
136
- assert.ok(err instanceof NotFound || err instanceof NoSuchKey, err as Error)
137
- }
129
+ public async commit (Key: string, metadata: object): Promise<void> {
130
+ await this.client.send(new s3.CopyObjectCommand({
131
+ Bucket: this.bucket,
132
+ Key,
133
+ CopySource: join(this.bucket, Key),
134
+ Metadata: { value: JSON.stringify(metadata) },
135
+ MetadataDirective: 'REPLACE'
136
+ }))
138
137
 
139
- const objectsToRemove: ObjectIdentifier[] = []
140
-
141
- for await (const page of paginateListObjectsV2({ client }, { Bucket, Prefix: Key }))
142
- for (const { Key } of page.Contents ?? []) objectsToRemove.push({ Key })
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
- })())
138
+ console.debug('Uploaded to S3', { bucket: this.bucket, path: Key, metadata })
155
139
  }
156
140
 
157
- public async move (from: string, keyTo: string): Promise<void> {
158
- await this.client.send(new CopyObjectCommand({
159
- Bucket: this.bucket,
160
- Key: keyTo,
161
- CopySource: join(this.bucket, from)
162
- }))
141
+ public async delete (Key: string): Promise<void> {
142
+ await this.client.send(new s3.DeleteObjectCommand({ Bucket: this.bucket, Key }))
143
+ }
163
144
 
164
- await this.client.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: from }))
145
+ public async move (from: string, keyTo: string): Promise<Maybe<void>> {
146
+ return await this.try(async () => {
147
+ await this.client.send(new s3.CopyObjectCommand({
148
+ Bucket: this.bucket,
149
+ Key: keyTo,
150
+ CopySource: join(this.bucket, from)
151
+ }))
152
+
153
+ await this.client.send(new s3.DeleteObjectCommand({ Bucket: this.bucket, Key: from }))
154
+ })
155
+ }
156
+
157
+ private async try<T = void> (action: () => Promise<T>): Promise<Maybe<T>> {
158
+ try {
159
+ return await action()
160
+ } catch (err: any) {
161
+ if (err?.name === 'NotFound' || err?.name === 'NoSuchKey')
162
+ return ERR_NOT_FOUND
163
+ else
164
+ throw err
165
+ }
165
166
  }
166
167
  }
@@ -1,16 +1,15 @@
1
1
  import { tmpdir } from 'node:os'
2
2
  import { join } from 'node:path'
3
3
  import { FileSystem } from './FileSystem'
4
- import type { ProviderSecrets } from '../Provider'
5
4
 
6
5
  export interface TemporaryOptions {
7
6
  directory: string
8
7
  }
9
8
 
10
9
  export class Temporary extends FileSystem {
11
- public constructor (options: TemporaryOptions, secrets?: ProviderSecrets) {
10
+ public constructor (options: TemporaryOptions) {
12
11
  const path = join(tmpdir(), options.directory)
13
12
 
14
- super({ path }, secrets)
13
+ super({ path })
15
14
  }
16
15
  }
@@ -1,13 +1,13 @@
1
1
  import { Temporary, type TemporaryOptions } from './Temporary'
2
- import type { ProviderSecret, ProviderSecrets } from '../Provider'
2
+ import type { Secret } from '../Secrets'
3
3
 
4
4
  export class Test extends Temporary {
5
- public static override readonly SECRETS: readonly ProviderSecret[] = [
5
+ public static override readonly SECRETS: readonly Secret[] = [
6
6
  { name: 'USERNAME' },
7
7
  { name: 'PASSWORD' }
8
8
  ]
9
9
 
10
- public constructor (options: TemporaryOptions, secrets?: ProviderSecrets) {
11
- super(options, secrets)
10
+ public constructor (options: TemporaryOptions) {
11
+ super(options)
12
12
  }
13
13
  }
@@ -1,126 +1,152 @@
1
- import { type Readable } from 'node:stream'
2
- import { buffer } from 'node:stream/consumers'
3
- import { readFile } from 'node:fs/promises'
1
+ import assert from 'node:assert'
4
2
  import { createReadStream } from 'node:fs'
5
- import path from 'node:path'
6
- import { randomUUID } from 'node:crypto'
3
+ import { resolve } from 'node:path'
7
4
  import { suites } from '../test/util'
8
5
  import { providers } from './index'
9
- import type { ProviderConstructor } from '../Provider'
6
+ import type { Constructor } from '../Provider'
7
+ import type { Metadata, Stream } from '../Entry'
8
+
9
+ const sample = resolve(__dirname, '../test/sample.jpeg')
10
+ const lenna = resolve(__dirname, '../test/lenna.png')
11
+
12
+ const metadata: Metadata = {
13
+ type: 'image/jpeg',
14
+ size: 73444,
15
+ checksum: 'd41d8cd98f00b204e9800998ecf8427e',
16
+ created: new Date().toISOString(),
17
+ attributes: {}
18
+ }
10
19
 
11
20
  describe.each(suites)('$provider', (suite) => {
21
+ const id = Math.random().toString(36).substring(7)
12
22
  const it = suite.run ? global.it : global.it.skip
13
- const Provider: ProviderConstructor = providers[suite.provider]
23
+ const Provider: Constructor = providers[suite.provider]
14
24
  const provider = new Provider(suite.options, suite.secrets)
15
25
 
16
- let dir: string
26
+ describe('put, get, head', () => {
27
+ let entry: Stream
17
28
 
18
- beforeAll(async () => {
19
- process.chdir(path.resolve(__dirname, '../test/'))
20
- })
29
+ beforeAll(async () => {
30
+ await provider.put(id, createReadStream(sample))
31
+ await provider.commit(id, metadata)
21
32
 
22
- beforeEach(() => {
23
- dir = '/' + randomUUID()
24
- })
33
+ entry = await provider.get(id) as Stream
34
+ })
25
35
 
26
- it('should be', async () => {
27
- expect(provider).toBeInstanceOf(Provider)
28
- })
36
+ afterAll(() => entry.stream.destroy())
29
37
 
30
- it('should return null if file not found', async () => {
31
- const result = await provider.get(randomUUID())
38
+ it('should create metadata', async () => {
39
+ expect(entry.size).toEqual(metadata.size)
40
+ })
32
41
 
33
- expect(result).toBeNull()
34
- })
42
+ if (suite.provider !== 'cloudinary')
43
+ it('should store attributes', async () => {
44
+ const path = '/path/to/file'
35
45
 
36
- it('should create entry', async () => {
37
- const stream = createReadStream('lenna.png')
46
+ await provider.put(path, createReadStream(sample))
47
+ await provider.commit(path, { ...metadata, attributes: { foo: 'bar' } })
38
48
 
39
- await provider.put(dir, 'lenna.png', stream)
49
+ const entry = await provider.get(path) as Stream
40
50
 
41
- const readable = await provider.get(dir + '/lenna.png') as Readable
42
- const output = await buffer(readable)
43
- const lenna = await readFile('lenna.png')
51
+ expect(entry.attributes).toEqual({ foo: 'bar' })
44
52
 
45
- expect(output.compare(lenna)).toBe(0)
46
- })
53
+ entry.stream.destroy()
54
+ })
47
55
 
48
- it('should overwrite existing entry', async () => {
49
- const stream0 = createReadStream('lenna.png')
50
- const stream1 = createReadStream('albert.jpg')
56
+ it('should overwrite', async () => {
57
+ const path = Math.random().toString(36).substring(7)
51
58
 
52
- await provider.put(dir, 'lenna.png', stream0)
53
- await provider.put(dir, 'lenna.png', stream1)
59
+ await provider.put(path, createReadStream(sample))
60
+ await provider.commit(path, metadata)
54
61
 
55
- const readable = await provider.get(dir + '/lenna.png') as Readable
56
- const output = await buffer(readable)
57
- const albert = await readFile('albert.jpg')
62
+ if (suite.provider === 'cloudinary')
63
+ await new Promise((resolve) => setTimeout(resolve, 5_000))
58
64
 
59
- expect(output.compare(albert)).toBe(0)
60
- })
65
+ await expect(provider.put(path, createReadStream(lenna)))
66
+ .resolves.not.toThrow()
61
67
 
62
- it('should get by path', async () => {
63
- const stream = createReadStream('lenna.png')
68
+ const meta = { ...metadata, size: 473831 } // lenna size
64
69
 
65
- await provider.put(dir, 'lenna.png', stream)
70
+ await provider.commit(path, meta)
66
71
 
67
- const result = await provider.get('/bar/lenna.png')
72
+ if (suite.provider === 'cloudinary')
73
+ await new Promise((resolve) => setTimeout(resolve, 10_000))
68
74
 
69
- expect(result).toBeNull()
70
- })
75
+ const overwritten = await provider.get(path) as Stream
71
76
 
72
- describe('danger', () => {
73
- /*
77
+ expect(overwritten.size).toEqual(meta.size)
74
78
 
75
- WHEN MAKING CHANGES TO DELETION,
76
- ALWAYS RUN TESTS IN STEP-BY-STEP DEBUGGING MODE
79
+ overwritten.stream.destroy()
80
+ })
77
81
 
78
- YOU MAY EVENTUALLY DELETE YOUR ENTIRE FILE SYSTEM
82
+ it('should return error if not found', async () => {
83
+ const error = await provider.get(Math.random().toString(36).substring(7)) as any
79
84
 
80
- Sincerely yours, Murphy
85
+ expect(error).toBeInstanceOf(Error)
86
+ expect(error.code).toBe('NOT_FOUND')
87
+ })
88
+
89
+ it('should return entry', async () => {
90
+ const entry = await provider.head(id)
91
+
92
+ assert.ok(!(entry instanceof Error))
81
93
 
82
- */
94
+ expect(entry.size).toEqual(metadata.size)
95
+ })
96
+ })
83
97
 
84
- it('should delete entry', async () => {
85
- const stream = createReadStream('lenna.png')
98
+ describe('delete', () => {
99
+ const path = '/path/to/' + Math.random().toString(36).substring(7)
86
100
 
87
- await provider.put(dir, 'lenna.png', stream)
88
- await provider.delete(dir + '/lenna.png')
101
+ it('should remove file', async () => {
102
+ await provider.put(path, createReadStream(sample))
103
+ await provider.commit(path, metadata)
104
+ await provider.delete(path)
89
105
 
90
- const result = await provider.get(dir + '/lenna.png')
106
+ const error = await provider.get(path) as any
91
107
 
92
- expect(result).toBeNull()
108
+ expect(error).toBeInstanceOf(Error)
109
+ expect(error.code).toBe('NOT_FOUND')
93
110
  })
94
111
 
95
- it('should not throw if path does not exists', async () => {
96
- await expect(provider.delete(dir + '/whatever')).resolves.not.toThrow()
112
+ it('should not return error if not found', async () => {
113
+ const empty = await provider.delete(Math.random().toString(36).substring(7))
114
+
115
+ expect(empty).toBeUndefined()
97
116
  })
117
+ })
98
118
 
99
- it('should delete directory', async () => {
100
- const stream = createReadStream('lenna.png')
119
+ describe('move', () => {
120
+ it.each([true, false])('should move (commit: %s)', async (committed) => {
121
+ const from = Math.random().toString(36).substring(7)
122
+ const to = Math.random().toString(36).substring(7)
101
123
 
102
- await provider.put(dir, 'lenna.png', stream)
103
- await provider.delete(dir)
124
+ await provider.put(from, createReadStream(sample))
104
125
 
105
- const result = await provider.get(dir + '/lenna.png')
126
+ if (committed)
127
+ await provider.commit(from, metadata)
106
128
 
107
- expect(result).toBeNull()
108
- })
129
+ await provider.move(from, to)
109
130
 
110
- it('should move an entry', async () => {
111
- const stream = createReadStream('lenna.png')
112
- const dir2 = '/' + randomUUID()
131
+ if (!committed)
132
+ await provider.commit(to, metadata)
113
133
 
114
- await provider.put(dir, 'lenna.png', stream)
115
- await provider.move(dir + '/lenna.png', dir2 + '/lenna2.png')
134
+ const error = await provider.get(from) as any
116
135
 
117
- const result = await provider.get(dir2 + '/lenna2.png') as Readable
136
+ expect(error).toBeInstanceOf(Error)
118
137
 
119
- expect(result).not.toBeNull()
138
+ const entry = await provider.get(to) as Stream
120
139
 
121
- const nope = await provider.get(dir + '/lenna.png')
140
+ expect(entry.stream).toBeDefined()
141
+ })
122
142
 
123
- expect(nope).toBeNull()
143
+ it('should return error if not found', async () => {
144
+ const error = await provider.move(Math.random().toString(36).substring(7), 'whatever') as any
145
+
146
+ expect(error).toBeInstanceOf(Error)
147
+ expect(error.code).toBe('NOT_FOUND')
124
148
  })
125
149
  })
126
150
  })
151
+
152
+ jest.setTimeout(30_000)