@toa.io/extensions.storages 1.0.0-alpha.7 → 1.0.0-alpha.78

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 (47) hide show
  1. package/package.json +8 -8
  2. package/readme.md +32 -22
  3. package/schemas/fs.cos.yaml +2 -0
  4. package/schemas/s3.cos.yaml +1 -0
  5. package/schemas/tmp.cos.yaml +0 -1
  6. package/source/Entry.ts +1 -0
  7. package/source/Factory.ts +7 -4
  8. package/source/Provider.ts +5 -0
  9. package/source/Scanner.ts +46 -17
  10. package/source/Storage.test.ts +126 -76
  11. package/source/Storage.ts +92 -91
  12. package/source/deployment.ts +38 -7
  13. package/source/providers/FileSystem.test.ts +6 -0
  14. package/source/providers/FileSystem.ts +15 -2
  15. package/source/providers/Memory.ts +21 -7
  16. package/source/providers/S3.test.ts +1 -0
  17. package/source/providers/S3.ts +36 -6
  18. package/source/providers/index.test.ts +10 -0
  19. package/source/test/arny.jpg +0 -0
  20. package/source/test/sample.avi +0 -0
  21. package/source/test/sample.svg +4 -0
  22. package/source/test/sample.wav +0 -0
  23. package/transpiled/Entry.d.ts +1 -0
  24. package/transpiled/Factory.js +5 -3
  25. package/transpiled/Factory.js.map +1 -1
  26. package/transpiled/Provider.d.ts +3 -0
  27. package/transpiled/Provider.js +1 -0
  28. package/transpiled/Provider.js.map +1 -1
  29. package/transpiled/Scanner.d.ts +3 -1
  30. package/transpiled/Scanner.js +36 -15
  31. package/transpiled/Scanner.js.map +1 -1
  32. package/transpiled/Storage.d.ts +6 -6
  33. package/transpiled/Storage.js +66 -78
  34. package/transpiled/Storage.js.map +1 -1
  35. package/transpiled/deployment.d.ts +1 -1
  36. package/transpiled/deployment.js +30 -7
  37. package/transpiled/deployment.js.map +1 -1
  38. package/transpiled/providers/FileSystem.d.ts +4 -1
  39. package/transpiled/providers/FileSystem.js +10 -1
  40. package/transpiled/providers/FileSystem.js.map +1 -1
  41. package/transpiled/providers/Memory.d.ts +2 -0
  42. package/transpiled/providers/Memory.js +13 -1
  43. package/transpiled/providers/Memory.js.map +1 -1
  44. package/transpiled/providers/S3.d.ts +3 -0
  45. package/transpiled/providers/S3.js +23 -4
  46. package/transpiled/providers/S3.js.map +1 -1
  47. package/transpiled/tsconfig.tsbuildinfo +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/extensions.storages",
3
- "version": "1.0.0-alpha.7",
3
+ "version": "1.0.0-alpha.78",
4
4
  "description": "Toa Storages",
5
5
  "author": "temich <tema.gurtovoy@gmail.com>",
6
6
  "homepage": "https://github.com/toa-io/toa#readme",
@@ -24,20 +24,20 @@
24
24
  "test": "jest",
25
25
  "transpile": "npx tsc"
26
26
  },
27
- "devDependencies": {
28
- "@toa.io/streams": "1.0.0-alpha.7",
29
- "dotenv": "16.3.1"
27
+ "peerDependencies": {
28
+ "dotenv": "*"
30
29
  },
31
30
  "dependencies": {
32
31
  "@aws-sdk/client-s3": "3.481.0",
33
32
  "@aws-sdk/lib-storage": "3.481.0",
34
- "@toa.io/agent": "1.0.0-alpha.7",
35
- "@toa.io/generic": "1.0.0-alpha.7",
36
- "@toa.io/schemas": "1.0.0-alpha.7",
33
+ "@toa.io/agent": "1.0.0-alpha.71",
34
+ "@toa.io/generic": "1.0.0-alpha.63",
35
+ "@toa.io/schemas": "1.0.0-alpha.63",
37
36
  "error-value": "0.3.0",
38
37
  "matchacho": "0.6.0",
39
38
  "msgpackr": "1.10.1",
39
+ "openspan": "1.0.0-alpha.73",
40
40
  "smithy-node-native-fetch": "1.0.6"
41
41
  },
42
- "gitHead": "4f5ac0bc342d4b7bd469fbe5c74266f050b55c9f"
42
+ "gitHead": "8936ef7a76c95d83532ee7115ad2d698c3e2eac4"
43
43
  }
package/readme.md CHANGED
@@ -9,6 +9,7 @@ BLOBs are stored with the meta-information object (Entry) having the following p
9
9
  - `id` - checksum
10
10
  - `size` - size in bytes
11
11
  - `type` - MIME type
12
+ - `origin` - URL of the original BLOB (optional)
12
13
  - `created` - creation timestamp (UNIX time, ms)
13
14
  - `variants` - array of:
14
15
  - `name` - unique name
@@ -75,8 +76,6 @@ are: `image/jpeg`, `image/png`, `image/gif`, `image/webp`, `image/heic`, `image/
75
76
 
76
77
  See [source](source/Scanner.ts).
77
78
 
78
- If the entry already exists, it is returned and [revealed](#async-revealpath-string-maybevoid).
79
-
80
79
  #### `async get(path: string): Maybe<Entry>`
81
80
 
82
81
  Get an entry.
@@ -97,28 +96,28 @@ Fetch the BLOB specified by `path`. If the path does not exist, a `NOT_FOUND` er
97
96
 
98
97
  Delete the entry specified by `path`.
99
98
 
100
- #### `async list(path: string): string[]`
101
-
102
- Get ordered list of `id`s of entries in under the `path`.
99
+ #### `async move(path: string, to: string): Maybe<void>`
103
100
 
104
- #### `async permute(path: string, ids: string[]): Maybe<void>`
101
+ Moves the entry specified by `path` to the new path.
105
102
 
106
- Reorder entries under the `path`.
103
+ `to` may be an absolute or relative path (starting with `.`), if it ends with `/`, the entry is
104
+ moved to the `to` directory, otherwise, the entry is moved to the `to` path.
107
105
 
108
- Given list must be a permutation of the current list, otherwise a `PERMUTATION_MISMATCH` error is
109
- returned.
110
-
111
- #### `async diversify(path: string, name: string, stream: Readable): Maybe<void>`
106
+ The following examples are equivalent:
112
107
 
113
- Add or replace a `name` variant of the entry specified by `path`.
108
+ ```javascript
109
+ await storage.move('/path/to/eecd837c', '/path/to/sub/eecd837c')
110
+ await storage.move('/path/to/eecd837c', './sub/eecd837c')
111
+ await storage.move('/path/to/eecd837c', './sub/')
112
+ ```
114
113
 
115
- #### `async conceal(path: string): Maybe<void>`
114
+ #### `async entries(path: string): Entry[]`
116
115
 
117
- Remove the entry from the list.
116
+ Get a list of entries under the `path`.
118
117
 
119
- #### `async reveal(path: string): Maybe<void>`
118
+ #### `async diversify(path: string, name: string, stream: Readable): Maybe<void>`
120
119
 
121
- Restore the entry to the list.
120
+ Add or replace a `name` variant of the entry specified by `path`.
122
121
 
123
122
  #### `async annotate(path: string, key: string, value: any): Maybe<void>`
124
123
 
@@ -132,7 +131,7 @@ Custom providers are not supported.
132
131
 
133
132
  ### Amazon S3
134
133
 
135
- Annotation formats is like:
134
+ Annotation formats are like:
136
135
 
137
136
  ```yaml
138
137
  storages:
@@ -160,9 +159,20 @@ Annotation format is:
160
159
 
161
160
  ```yaml
162
161
  storages:
163
- photos@dev:
162
+ photos:
164
163
  provider: fs
165
- path: /var/my-storage
164
+ path: /var/my-photos
165
+ ```
166
+
167
+ [Kubernetes PVC](https://kubernetes.io/docs/concepts/storage/persistent-volumes/) can be mounted to
168
+ the storage:
169
+
170
+ ```yaml
171
+ storages:
172
+ photos:
173
+ provider: fs
174
+ path: /var/my-photos
175
+ claim: photos-pvc
166
176
  ```
167
177
 
168
178
  ### Temporary
@@ -205,10 +215,10 @@ Underlying directory structure:
205
215
  b4f577e0 # checksum
206
216
  /storage
207
217
  /path/to
208
- .list # list of entries
218
+ /.entries/
219
+ b4f577e0 # entry
209
220
  /b4f577e0
210
- .meta # entry
211
- thumbnail.jpeg # variant BLOBs
221
+ thumbnail.jpeg # variant
212
222
  thumbnail.webp
213
223
  ```
214
224
 
@@ -5,5 +5,7 @@ properties:
5
5
  path:
6
6
  type: string
7
7
  maxLength: 4096
8
+ claim:
9
+ type: string
8
10
  required: [provider, path]
9
11
  additionalProperties: false
@@ -14,3 +14,4 @@ properties:
14
14
  type: string
15
15
  format: uri
16
16
  required: [provider, bucket]
17
+ additionalProperties: false
@@ -1,7 +1,6 @@
1
1
  type: object
2
2
  properties:
3
3
  provider:
4
- type: string
5
4
  const: tmp
6
5
  directory:
7
6
  type: string
package/source/Entry.ts CHANGED
@@ -3,6 +3,7 @@ export interface Entry {
3
3
  size: number
4
4
  type: string
5
5
  created: number
6
+ origin?: string
6
7
  variants: Variant[]
7
8
  meta: Record<string, unknown>
8
9
  }
package/source/Factory.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import assert from 'node:assert'
2
+ import { console } from 'openspan'
2
3
  import { decode } from '@toa.io/generic'
3
4
  import { providers } from './providers'
4
5
  import { Storage, type Storages } from './Storage'
5
6
  import { Aspect } from './Aspect'
6
- import { SERIALIZATION_PREFIX } from './deployment'
7
+ import { ENV_PREFIX } from './deployment'
7
8
  import { validateAnnotation } from './Annotation'
8
9
  import type { Declaration } from './providers'
9
10
  import type { Annotation } from './Annotation'
@@ -13,9 +14,9 @@ export class Factory {
13
14
  private readonly annotation: Annotation
14
15
 
15
16
  public constructor () {
16
- const env = process.env.TOA_STORAGES
17
+ const env = process.env[ENV_PREFIX]
17
18
 
18
- assert.ok(env !== undefined, 'TOA_STORAGES is not defined')
19
+ assert.ok(env !== undefined, `${ENV_PREFIX} is not defined`)
19
20
 
20
21
  this.annotation = decode(env)
21
22
 
@@ -43,6 +44,8 @@ export class Factory {
43
44
  const secrets = this.resolveSecrets(name, Provider)
44
45
  const provider = new Provider(options, secrets)
45
46
 
47
+ console.debug('Storage created', { name, provider: providerId, options, path: provider.path })
48
+
46
49
  return new Storage(provider)
47
50
  }
48
51
 
@@ -54,7 +57,7 @@ export class Factory {
54
57
  const secrets: Record<string, string | undefined> = {}
55
58
 
56
59
  for (const secret of Class.SECRETS) {
57
- const variable = `${SERIALIZATION_PREFIX}_${storageName}_${secret.name}`.toUpperCase()
60
+ const variable = `${ENV_PREFIX}_${storageName}_${secret.name}`.toUpperCase()
58
61
  const value = process.env[variable]
59
62
 
60
63
  assert.ok(secret.optional === true || value !== undefined,
@@ -10,6 +10,7 @@ export interface ProviderSecret {
10
10
 
11
11
  export abstract class Provider<Options = void> {
12
12
  public static readonly SECRETS: readonly ProviderSecret[] = []
13
+ public readonly path: string | null = null
13
14
 
14
15
  public constructor (_: Options, secrets?: ProviderSecrets) {
15
16
  for (const { name, optional = false } of new.target.SECRETS)
@@ -18,11 +19,15 @@ export abstract class Provider<Options = void> {
18
19
 
19
20
  public abstract get (path: string): Promise<Readable | null>
20
21
 
22
+ public abstract list (path: string): Promise<string[]>
23
+
21
24
  public abstract put (path: string, filename: string, stream: Readable): Promise<void>
22
25
 
23
26
  public abstract delete (path: string): Promise<void>
24
27
 
25
28
  public abstract move (from: string, to: string): Promise<void>
29
+
30
+ public abstract moveDir (from: string, to: string): Promise<void>
26
31
  }
27
32
 
28
33
  export interface ProviderConstructor {
package/source/Scanner.ts CHANGED
@@ -11,8 +11,9 @@ export class Scanner extends PassThrough {
11
11
  private readonly hash = createHash('md5')
12
12
  private readonly claim?: string
13
13
  private readonly accept?: string
14
+ private readonly limit?: number
14
15
  private position = 0
15
- private detected = false
16
+ private completed = false
16
17
  private readonly chunks: Buffer[] = []
17
18
 
18
19
  public constructor (control?: ScanOptions) {
@@ -20,6 +21,7 @@ export class Scanner extends PassThrough {
20
21
 
21
22
  this.claim = control?.claim
22
23
  this.accept = control?.accept
24
+ this.limit = control?.limit
23
25
  }
24
26
 
25
27
  public digest (): string {
@@ -37,7 +39,7 @@ export class Scanner extends PassThrough {
37
39
  this.size += buffer.length
38
40
  this.hash.update(buffer)
39
41
 
40
- if (this.detected)
42
+ if (this.completed)
41
43
  return
42
44
 
43
45
  if (this.position + buffer.length > HEADER_SIZE)
@@ -48,13 +50,22 @@ export class Scanner extends PassThrough {
48
50
 
49
51
  if (this.position === HEADER_SIZE)
50
52
  this.complete()
53
+
54
+ if (this.limit !== undefined && this.size > this.limit)
55
+ this.interrupt(ERR_LIMIT_EXCEEDED)
51
56
  }
52
57
 
53
58
  private complete (): void {
54
59
  const header = Buffer.concat(this.chunks).toString('hex')
55
60
 
56
61
  const signature = SIGNATURES
57
- .find(({ hex, off }) => header.slice(off, off + hex.length) === hex)
62
+ .find(({ off, hex, expression }) => {
63
+ const sig = header.slice(off, off + hex.length)
64
+
65
+ return expression === undefined
66
+ ? sig === hex
67
+ : expression.test(sig)
68
+ })
58
69
 
59
70
  const type = signature?.type ?? this.claim
60
71
 
@@ -64,7 +75,7 @@ export class Scanner extends PassThrough {
64
75
  }
65
76
 
66
77
  this.verify(signature)
67
- this.detected = true
78
+ this.completed = true
68
79
  }
69
80
 
70
81
  private verify (signature: Signature | undefined): void {
@@ -90,6 +101,7 @@ export class Scanner extends PassThrough {
90
101
  }
91
102
 
92
103
  private interrupt (error: Error): void {
104
+ this.completed = true
93
105
  this.error = error
94
106
  this.end()
95
107
  }
@@ -97,21 +109,35 @@ export class Scanner extends PassThrough {
97
109
 
98
110
  // https://en.wikipedia.org/wiki/List_of_file_signatures
99
111
  const SIGNATURES: Signature[] = [
100
- { hex: 'ffd8ffe0', off: 0, type: 'image/jpeg' },
101
- { hex: 'ffd8ffe1', off: 0, type: 'image/jpeg' },
102
- { hex: 'ffd8ffee', off: 0, type: 'image/jpeg' },
103
- { hex: 'ffd8ffdb', off: 0, type: 'image/jpeg' },
104
- { hex: '89504e47', off: 0, type: 'image/png' },
105
- { hex: '47494638', off: 0, type: 'image/gif' },
106
- { hex: '52494646', off: 0, type: 'image/webp' },
107
- { hex: '4a584c200d0a870a', off: 8, type: 'image/jxl' },
108
- { hex: '6674797068656963', off: 8, type: 'image/heic' },
109
- { hex: '6674797061766966', off: 8, type: 'image/avif' }
112
+ { hex: 'ff d8 ff e0', off: 0, type: 'image/jpeg' },
113
+ { hex: 'ff d8 ff e1', off: 0, type: 'image/jpeg' },
114
+ { hex: 'ff d8 ff e2', off: 0, type: 'image/jpeg' },
115
+ { hex: 'ff d8 ff ee', off: 0, type: 'image/jpeg' },
116
+ { hex: 'ff d8 ff db', off: 0, type: 'image/jpeg' },
117
+ { hex: '89 50 4e 47', off: 0, type: 'image/png' },
118
+ { hex: '47 49 46 38', off: 0, type: 'image/gif' },
119
+ { hex: '52 49 46 46 ?? ?? ?? ?? 57 45 42 50', off: 0, type: 'image/webp' },
120
+ { hex: '4a 58 4c 20 0d 0a 87 0a', off: 8, type: 'image/jxl' },
121
+ { hex: '66 74 79 70 68 65 69 63', off: 8, type: 'image/heic' },
122
+ { hex: '66 74 79 70 61 76 69 66', off: 8, type: 'image/avif' },
123
+ { hex: '52 49 46 46 ?? ?? ?? ?? 41 56 49 20', off: 0, type: 'video/avi' },
124
+ { hex: '52 49 46 46 ?? ?? ?? ?? 57 41 56 45', off: 0, type: 'audio/wav' }
110
125
  /*
111
126
  When adding a new signature, include a copyright-free sample file in the `.tests` directory
112
127
  and update the 'signatures' test group in `Storage.test.ts`.
113
128
  */
114
- ]
129
+ ].map((signature: Signature) => {
130
+ signature.hex = signature.hex.replaceAll(' ', '')
131
+
132
+ if (signature.hex.includes('??')) {
133
+ const expression = signature.hex.replaceAll(/(?<wildcards>\?{1,24})/g,
134
+ (_, wildcards) => `[0-9a-f]{${wildcards.length}}`)
135
+
136
+ signature.expression = new RegExp(expression, 'i')
137
+ }
138
+
139
+ return signature
140
+ })
115
141
 
116
142
  const HEADER_SIZE = SIGNATURES
117
143
  .reduce((max, { off, hex }) => Math.max(max, off + hex.length), 0) / 2
@@ -120,14 +146,17 @@ const KNOWN_TYPES = new Set(SIGNATURES.map(({ type }) => type))
120
146
 
121
147
  const ERR_TYPE_MISMATCH = Err('TYPE_MISMATCH')
122
148
  const ERR_NOT_ACCEPTABLE = Err('NOT_ACCEPTABLE')
149
+ const ERR_LIMIT_EXCEEDED = Err('LIMIT_EXCEEDED')
123
150
 
124
151
  export interface ScanOptions {
125
152
  claim?: string
126
153
  accept?: string
154
+ limit?: number
127
155
  }
128
156
 
129
157
  interface Signature {
130
- hex: string
131
- off: number
132
158
  type: string
159
+ off: number
160
+ hex: string
161
+ expression?: RegExp
133
162
  }
@@ -13,7 +13,7 @@ import type { ProviderConstructor } from './Provider'
13
13
  let storage: Storage
14
14
  let dir: string
15
15
 
16
- const suite = suites[0]
16
+ const suite = suites[0] // replace with 2 to run tests for S3
17
17
 
18
18
  beforeAll(async () => {
19
19
  process.chdir(path.join(__dirname, 'test'))
@@ -103,18 +103,6 @@ describe('put', () => {
103
103
  })
104
104
 
105
105
  describe('existing entry', () => {
106
- it('should unhide existing', async () => {
107
- const stream = createReadStream('lenna.png')
108
- const path = `${dir}/${lenna.id}`
109
-
110
- await storage.conceal(path)
111
- await storage.put(dir, stream)
112
-
113
- const list = await storage.list(dir)
114
-
115
- expect(list).toContainEqual(lenna.id)
116
- })
117
-
118
106
  it('should preserve meta', async () => {
119
107
  const path = `${dir}/${lenna.id}`
120
108
  const stream = createReadStream('lenna.png')
@@ -146,86 +134,41 @@ describe('list', () => {
146
134
 
147
135
  expect(list).toEqual([albert.id, lenna.id])
148
136
  })
137
+ })
149
138
 
150
- it('should permutate', async () => {
151
- const error = await storage.permute(dir, [lenna.id, albert.id])
152
-
153
- expect(error).toBeUndefined()
154
-
155
- const list = await storage.list(dir)
156
-
157
- expect(list).toEqual([lenna.id, albert.id])
158
- })
159
-
160
- it('should return PERMUTATION_MISMATCH', async () => {
161
- const cases = [
162
- [lenna.id],
163
- [albert.id, lenna.id, 'unknown'],
164
- [lenna.id, lenna.id],
165
- [lenna.id, lenna.id, albert.id]
166
- ]
167
-
168
- for (const permutation of cases) {
169
- const error = await storage.permute(dir, permutation)
170
-
171
- expect(error).toBeInstanceOf(Error)
172
- expect(error).toHaveProperty('code', 'PERMUTATION_MISMATCH')
173
- }
174
- })
175
-
176
- it('should exclude concealed', async () => {
177
- const path = `${dir}/${lenna.id}`
178
-
179
- await storage.conceal(path)
139
+ describe('annotate', () => {
140
+ let lenna: Entry
180
141
 
181
- const entries = await storage.list(dir)
142
+ beforeEach(async () => {
143
+ const stream = createReadStream('lenna.png')
182
144
 
183
- expect(entries).toEqual([albert.id])
145
+ lenna = (await storage.put(dir, stream)) as Entry
184
146
  })
185
147
 
186
- it('should reveal', async () => {
148
+ it('should set meta property', async () => {
187
149
  const path = `${dir}/${lenna.id}`
188
150
 
189
- await storage.conceal(path)
190
- await storage.reveal(path)
191
- await storage.reveal(path) // test that no duplicates are created
192
-
193
- const entries = await storage.list(dir)
194
-
195
- expect(entries).toEqual([albert.id, lenna.id])
196
- })
197
-
198
- it('should return ERR_NOT_FOOUD if entry doesnt exist', async () => {
199
- const path = `${dir}/oopsie`
200
-
201
- const methods: Array<'reveal' | 'conceal'> = ['reveal', 'conceal']
151
+ await storage.annotate(path, 'foo', 'bar')
202
152
 
203
- for (const method of methods) {
204
- const error = await storage[method](path)
153
+ const state0 = (await storage.get(path)) as Entry
205
154
 
206
- expect(error).toBeInstanceOf(Error)
207
- expect(error).toHaveProperty('code', 'NOT_FOUND')
208
- }
209
- })
210
- })
155
+ expect(state0).toHaveProperty('meta.foo', 'bar')
211
156
 
212
- describe('annotate', () => {
213
- let lenna: Entry
157
+ await storage.annotate(path, 'foo')
214
158
 
215
- beforeEach(async () => {
216
- const stream = createReadStream('lenna.png')
159
+ const state1 = (await storage.get(path)) as Entry
217
160
 
218
- lenna = (await storage.put(dir, stream)) as Entry
161
+ expect(state1.meta).not.toHaveProperty('foo')
219
162
  })
220
163
 
221
- it('should set meta', async () => {
164
+ it('should set meta with object', async () => {
222
165
  const path = `${dir}/${lenna.id}`
223
166
 
224
- await storage.annotate(path, 'foo', 'bar')
167
+ await storage.annotate(path, { foo: 1, bar: 'baz' })
225
168
 
226
169
  const state0 = (await storage.get(path)) as Entry
227
170
 
228
- expect(state0).toHaveProperty('meta.foo', 'bar')
171
+ expect(state0.meta).toStrictEqual({ foo: 1, bar: 'baz' })
229
172
 
230
173
  await storage.annotate(path, 'foo')
231
174
 
@@ -380,6 +323,62 @@ describe('delete', () => {
380
323
  })
381
324
  })
382
325
 
326
+ describe('move', () => {
327
+ let lenna: Entry
328
+
329
+ beforeEach(async () => {
330
+ const stream = createReadStream('lenna.png')
331
+
332
+ lenna = (await storage.put(dir, stream)) as Entry
333
+ })
334
+
335
+ it('should move entry', async () => {
336
+ const path = `${dir}/${lenna.id}`
337
+ const to = `${dir}/lenna`
338
+
339
+ await storage.move(path, to)
340
+
341
+ const entry = await storage.get(to)
342
+
343
+ expect(entry).toMatchObject({ id: lenna.id, type: 'image/png' })
344
+ })
345
+
346
+ it('should move to subdirectory', async () => {
347
+ const path = `${dir}/${lenna.id}`
348
+ const to = `${dir}/sub/`
349
+
350
+ await storage.move(path, to)
351
+
352
+ const entry = await storage.get(to + lenna.id)
353
+
354
+ expect(entry).toMatchObject({ id: lenna.id, type: 'image/png' })
355
+ })
356
+
357
+ it('should move to relative path', async () => {
358
+ const path = `${dir}/${lenna.id}`
359
+ const to = './sub/'
360
+
361
+ await storage.move(path, to)
362
+
363
+ const entry = await storage.get(`${dir}/sub/${lenna.id}`)
364
+
365
+ expect(entry).toMatchObject({ id: lenna.id, type: 'image/png' })
366
+ })
367
+
368
+ it('should move variants', async () => {
369
+ const stream = createReadStream('sample.jpeg')
370
+
371
+ const path = `${dir}/${lenna.id}`
372
+
373
+ await storage.diversify(path, 'foo', stream)
374
+ await storage.move(path, `${dir}/lenna`)
375
+
376
+ const variant = await storage.fetch(`${dir}/lenna.foo`)
377
+
378
+ assert.ok(variant instanceof Readable)
379
+ })
380
+ })
381
+
383
382
  describe('signatures', () => {
384
383
  it.each(['jpeg', 'gif', 'webp', 'heic', 'jxl', 'avif'])('should detect image/%s',
385
384
  async (type) => {
@@ -389,9 +388,33 @@ describe('signatures', () => {
389
388
 
390
389
  expect(entry).toHaveProperty('type', 'image/' + type)
391
390
  })
391
+
392
+ it.each(['avi'])('should detect video/%s',
393
+ async (type) => {
394
+ const stream = createReadStream('sample.' + type)
395
+
396
+ const entry = (await storage.put(dir, stream)) as Entry
397
+
398
+ expect(entry).toHaveProperty('type', 'video/' + type)
399
+ })
400
+
401
+ it.each(['wav'])('should detect audio/%s',
402
+ async (type) => {
403
+ const stream = createReadStream('sample.' + type)
404
+ const entry = (await storage.put(dir, stream)) as Entry
405
+
406
+ expect(entry).toHaveProperty('type', 'audio/' + type)
407
+ })
408
+
409
+ it('should be ok with Arny', async () => {
410
+ const stream = createReadStream('arny.jpg')
411
+ const entry = (await storage.put(dir, stream)) as Entry
412
+
413
+ expect(entry).toHaveProperty('type', 'image/jpeg')
414
+ })
392
415
  })
393
416
 
394
- it("should return error if type doesn't match", async () => {
417
+ it('should return error if type doesn\'t match', async () => {
395
418
  const stream = createReadStream('sample.jpeg')
396
419
 
397
420
  const result = await storage.put(dir, stream, { claim: 'image/png' })
@@ -447,7 +470,6 @@ it('should accept wildcard types', async () => {
447
470
 
448
471
  it('should handle root entries', async () => {
449
472
  const stream = createReadStream('sample.jpeg')
450
-
451
473
  const result = (await storage.put('hello', stream)) as Entry
452
474
 
453
475
  expect(result).not.toBeInstanceOf(Error)
@@ -471,3 +493,31 @@ it('should store empty file', async () => {
471
493
 
472
494
  expect(buf).toHaveLength(0)
473
495
  })
496
+
497
+ it('should return error of stream size limit exceeded', async () => {
498
+ const stream = createReadStream('sample.jpeg')
499
+ const result = await storage.put(dir, stream, { limit: 1024 })
500
+
501
+ expect(result).toBeInstanceOf(Error)
502
+ expect(result).toHaveProperty('code', 'LIMIT_EXCEEDED')
503
+ })
504
+
505
+ it('should set origin', async () => {
506
+ const origin = 'https://example.com/image.jpeg'
507
+ const stream = createReadStream('sample.jpeg')
508
+ const result = (await storage.put('/origins', stream, { origin })) as Entry
509
+
510
+ assert.ok(!(result instanceof Error))
511
+
512
+ const stored = await storage.get('/origins/' + result.id)
513
+
514
+ assert.ok(!(stored instanceof Error))
515
+
516
+ expect(stored.origin).toBe(origin)
517
+ })
518
+
519
+ it('should expose path', () => {
520
+ const path = storage.path()
521
+
522
+ expect(typeof path === 'string' || path === null)
523
+ })