@toa.io/extensions.storages 1.0.0-alpha.21 → 1.0.0-alpha.219

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 (91) hide show
  1. package/package.json +14 -14
  2. package/readme.md +96 -51
  3. package/schemas/annotation.cos.yaml +1 -0
  4. package/schemas/cloudinary.cos.yaml +37 -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 +13 -11
  9. package/source/Factory.ts +16 -9
  10. package/source/Provider.ts +22 -19
  11. package/source/Scanner.ts +58 -26
  12. package/source/Secrets.ts +6 -0
  13. package/source/Storage.test.ts +118 -274
  14. package/source/Storage.ts +62 -207
  15. package/source/deployment.ts +52 -16
  16. package/source/errors.ts +3 -0
  17. package/source/index.ts +3 -1
  18. package/source/manifest.ts +7 -6
  19. package/source/providers/Cloudinary.ts +320 -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 +10 -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/plank.mp4 +0 -0
  32. package/source/test/sample.avi +0 -0
  33. package/source/test/sample.wav +0 -0
  34. package/source/test/sport.mp4 +0 -0
  35. package/source/test/util.ts +62 -12
  36. package/transpiled/Entry.d.ts +15 -11
  37. package/transpiled/Factory.js +11 -5
  38. package/transpiled/Factory.js.map +1 -1
  39. package/transpiled/Provider.d.ts +17 -16
  40. package/transpiled/Provider.js +6 -4
  41. package/transpiled/Provider.js.map +1 -1
  42. package/transpiled/Scanner.d.ts +6 -2
  43. package/transpiled/Scanner.js +49 -22
  44. package/transpiled/Scanner.js.map +1 -1
  45. package/transpiled/Secrets.d.ts +5 -0
  46. package/transpiled/Secrets.js +3 -0
  47. package/transpiled/Secrets.js.map +1 -0
  48. package/transpiled/Storage.d.ts +13 -21
  49. package/transpiled/Storage.js +52 -155
  50. package/transpiled/Storage.js.map +1 -1
  51. package/transpiled/deployment.d.ts +1 -1
  52. package/transpiled/deployment.js +43 -16
  53. package/transpiled/deployment.js.map +1 -1
  54. package/transpiled/errors.d.ts +2 -0
  55. package/transpiled/errors.js +6 -0
  56. package/transpiled/errors.js.map +1 -0
  57. package/transpiled/index.d.ts +3 -1
  58. package/transpiled/manifest.js +5 -2
  59. package/transpiled/manifest.js.map +1 -1
  60. package/transpiled/providers/Cloudinary.d.ts +51 -0
  61. package/transpiled/providers/Cloudinary.js +223 -0
  62. package/transpiled/providers/Cloudinary.js.map +1 -0
  63. package/transpiled/providers/Declaration.d.ts +3 -2
  64. package/transpiled/providers/FileSystem.d.ts +15 -7
  65. package/transpiled/providers/FileSystem.js +64 -24
  66. package/transpiled/providers/FileSystem.js.map +1 -1
  67. package/transpiled/providers/S3.d.ts +14 -14
  68. package/transpiled/providers/S3.js +62 -60
  69. package/transpiled/providers/S3.js.map +1 -1
  70. package/transpiled/providers/Temporary.d.ts +1 -2
  71. package/transpiled/providers/Temporary.js +2 -2
  72. package/transpiled/providers/Temporary.js.map +1 -1
  73. package/transpiled/providers/Test.d.ts +3 -3
  74. package/transpiled/providers/Test.js +2 -2
  75. package/transpiled/providers/Test.js.map +1 -1
  76. package/transpiled/providers/index.d.ts +7 -2
  77. package/transpiled/providers/index.js +2 -2
  78. package/transpiled/providers/index.js.map +1 -1
  79. package/transpiled/schemas.d.ts +1 -0
  80. package/transpiled/schemas.js +2 -1
  81. package/transpiled/schemas.js.map +1 -1
  82. package/transpiled/test/util.d.ts +89 -5
  83. package/transpiled/test/util.js +51 -4
  84. package/transpiled/test/util.js.map +1 -1
  85. package/transpiled/tsconfig.tsbuildinfo +1 -1
  86. package/source/providers/FileSystem.test.ts +0 -5
  87. package/source/providers/Memory.ts +0 -41
  88. package/source/providers/S3.test.ts +0 -133
  89. package/transpiled/providers/Memory.d.ts +0 -13
  90. package/transpiled/providers/Memory.js +0 -60
  91. package/transpiled/providers/Memory.js.map +0 -1
package/source/Storage.ts CHANGED
@@ -1,245 +1,100 @@
1
- import { Readable } from 'node:stream'
2
- import { posix } from 'node:path'
3
- import { decode, encode } from 'msgpackr'
4
- import { buffer, newid } from '@toa.io/generic'
5
- import { Err } from 'error-value'
1
+ import { basename, join } from 'node:path'
2
+ import { randomUUID } from 'node:crypto'
6
3
  import { Scanner } from './Scanner'
4
+ import type { Readable } from 'node:stream'
5
+ import type { Attributes, Entry, Stream } from './Entry'
7
6
  import type { ScanOptions } from './Scanner'
8
7
  import type { Provider } from './Provider'
9
- import type { Entry } from './Entry'
10
8
 
11
- export class Storage {
12
- private readonly provider: Provider
9
+ export class Storage<T extends Provider = Provider> {
10
+ private readonly provider: T
13
11
 
14
- public constructor (provider: Provider) {
12
+ public constructor (provider: T) {
15
13
  this.provider = provider
16
14
  }
17
15
 
18
- public async put (path: string, stream: Readable, options?: Options): Maybe<Entry> {
19
- const scanner = new Scanner(options)
20
- const pipe = stream.pipe(scanner)
21
- const tempname = await this.transit(pipe)
22
-
23
- if (scanner.error !== null)
24
- return scanner.error
25
-
26
- const id = scanner.digest()
27
-
28
- await this.persist(tempname, id)
29
-
30
- return await this.create(path, id, scanner.size, scanner.type, options?.meta)
31
- }
32
-
33
- public async get (path: string): Maybe<Entry> {
34
- const metapath = posix.join(ENTRIES, path, META)
35
- const result = await this.provider.get(metapath)
36
-
37
- if (result === null) return ERR_NOT_FOUND
38
- else return decode(await buffer(result))
16
+ public options (): T['options'] {
17
+ return this.provider.options
39
18
  }
40
19
 
41
- public async fetch (path: string): Maybe<Readable> {
42
- const { rel, id, variant } = this.parse(path)
43
-
44
- if (variant === null && rel !== '') {
45
- const entry = await this.get(path)
46
-
47
- if (entry instanceof Error)
48
- return entry
20
+ public async put (path: string, stream: Readable, options?: Options): Maybe<Entry> {
21
+ const scanner = new Scanner(options)
22
+ const pipe = stream.pipe(scanner).on('error', () => undefined)
23
+ const id = options?.id ?? randomUUID().replace(/-/g, '')
24
+ const location = this.locate(path, id)
25
+
26
+ /**
27
+ * Provider can return or throw an error.
28
+ * If thrown error is TYPE_MISMATCH from the Scanner, it should be returned.
29
+ */
30
+ const error: Error | undefined = await this.provider.put(location, pipe).catch((error: any) => {
31
+ if (error === scanner.error) return error
32
+ else throw error
33
+ })
34
+
35
+ if (error instanceof Error)
36
+ return error
37
+
38
+ const metadata: Entry = {
39
+ id,
40
+ size: scanner.size,
41
+ type: scanner.type,
42
+ checksum: scanner.digest(),
43
+ created: new Date().toISOString(),
44
+ attributes: options?.attributes ?? {}
49
45
  }
50
46
 
51
- const blob = variant === null
52
- ? posix.join(BLOBs, id)
53
- : posix.join(ENTRIES, rel, id, variant)
47
+ if (options?.origin !== undefined)
48
+ metadata.attributes.origin = options.origin
54
49
 
55
- const stream = await this.provider.get(blob)
50
+ await this.provider.commit(location, metadata)
56
51
 
57
- if (stream === null) return ERR_NOT_FOUND
58
- else return stream
52
+ return metadata
59
53
  }
60
54
 
61
- public async delete (path: string): Maybe<void> {
62
- const entry = await this.get(path)
63
-
64
- if (entry instanceof Error)
65
- return entry
55
+ public async get (path: string, options?: unknown): Maybe<Stream> {
56
+ const location = this.locate(path)
66
57
 
67
- await this.conceal(path)
68
- await this.provider.delete(posix.join(ENTRIES, path))
58
+ return await this.provider.get(location, options)
69
59
  }
70
60
 
71
- public async list (path: string): Promise<string[]> {
72
- const listfile = posix.join(ENTRIES, path, LIST)
73
- const stream = await this.provider.get(listfile)
74
-
75
- return stream === null ? [] : decode(await buffer(stream))
76
- }
77
-
78
- public async permute (path: string, ids: string[]): Maybe<void> {
79
- const unique = new Set(ids)
80
- const dir = posix.join(ENTRIES, path)
81
- const list = await this.list(path)
82
-
83
- if (list.length !== ids.length || unique.size !== ids.length)
84
- return ERR_PERMUTATION_MISMATCH
85
-
86
- for (const id of ids)
87
- if (!list.includes(id))
88
- return ERR_PERMUTATION_MISMATCH
89
-
90
- await this.provider.put(dir, LIST, Readable.from(encode(ids)))
91
- }
92
-
93
- public async conceal (path: string): Maybe<void> {
94
- const { id, rel } = this.parse(path)
95
- const dir = posix.join(ENTRIES, rel)
96
- const list = await this.list(rel)
97
- const index = list.indexOf(id)
98
-
99
- if (index === -1)
100
- return ERR_NOT_FOUND
101
-
102
- list.splice(index, 1)
103
-
104
- await this.provider.put(dir, LIST, Readable.from(encode(list)))
105
- }
106
-
107
- public async reveal (path: string): Maybe<void> {
108
- const { id, rel } = this.parse(path)
109
-
110
- return await this.enroll(rel, id)
111
- }
112
-
113
- public async diversify (path: string, name: string, stream: Readable): Maybe<void> {
114
- const scanner = new Scanner()
115
- const pipe = stream.pipe(scanner)
116
-
117
- await this.provider.put(posix.join(ENTRIES, path), name, pipe)
118
-
119
- if (scanner.error !== null)
120
- return scanner.error
121
-
122
- const { size, type } = scanner
123
- const entry = await this.get(path)
61
+ public async head (path: string): Promise<Maybe<Entry>> {
62
+ const id = basename(path).split('.')[0]
63
+ const location = this.locate(path)
64
+ const metadata = await this.provider.head(location)
124
65
 
125
- if (entry instanceof Error)
126
- return entry
66
+ if (metadata instanceof Error)
67
+ return metadata
127
68
 
128
- entry.variants = entry.variants.filter((variant) => variant.name !== name)
129
- entry.variants.push({ name, size, type })
130
-
131
- await this.save(path, entry)
132
- }
133
-
134
- public async annotate (path: string, key: string, value?: unknown): Maybe<void> {
135
- const entry = await this.get(path)
136
-
137
- if (entry instanceof Error)
138
- return entry
139
-
140
- // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
141
- if (value === undefined) delete entry.meta[key]
142
- else entry.meta[key] = value
143
-
144
- await this.save(path, entry)
145
- }
146
-
147
- private async transit (stream: Readable): Promise<string> {
148
- const tempname = newid()
149
-
150
- await this.provider.put(TEMP, tempname, stream)
151
-
152
- return tempname
153
- }
154
-
155
- private async persist (tempname: string, id: string): Promise<void> {
156
- const temp = posix.join(TEMP, tempname)
157
- const blob = posix.join(BLOBs, id)
158
-
159
- await this.provider.move(temp, blob)
160
- }
161
-
162
- // eslint-disable-next-line max-params
163
- private async create
164
- (path: string, id: string, size: number, type: string, meta: Meta = {}): Promise<Entry> {
165
- const entry: Entry = {
69
+ return {
166
70
  id,
167
- size,
168
- type,
169
- created: Date.now(),
170
- variants: [],
171
- meta
71
+ ...metadata
172
72
  }
173
-
174
- const metafile = posix.join(path, entry.id)
175
- const existing = await this.get(metafile)
176
-
177
- if (existing instanceof Error)
178
- await this.save(metafile, entry)
179
-
180
- await this.enroll(path, id, true)
181
-
182
- return entry
183
73
  }
184
74
 
185
- private async save (rel: string, entry: Entry): Promise<void> {
186
- const buffer = encode(entry)
187
- const stream = Readable.from(buffer)
75
+ public async delete (path: string): Maybe<void> {
76
+ const location = this.locate(path)
188
77
 
189
- await this.provider.put(posix.join(ENTRIES, rel), META, stream)
78
+ return this.provider.delete(location)
190
79
  }
191
80
 
192
- private async enroll (path: string, id: string, addition: boolean = false): Maybe<void> {
193
- const dir = posix.join(ENTRIES, path)
194
- const list = await this.list(path)
195
- const index = list.indexOf(id)
196
-
197
- if (index !== -1)
198
- if (addition) list.splice(index, 1)
199
- else return
200
- else if (!addition) {
201
- const entry = await this.get(posix.join(path, id))
202
-
203
- if (entry instanceof Error)
204
- return entry
205
- }
206
-
207
- list.push(id)
208
-
209
- await this.provider.put(dir, LIST, Readable.from(encode(list)))
81
+ public path (): string | null {
82
+ return this.provider.root ?? null
210
83
  }
211
84
 
212
- private parse (path: string): Path {
213
- const [last, ...segments] = path.split('/').reverse()
214
- const [id, ...rest] = last.split('.')
215
- const variant = rest.length > 0 ? rest.join('.') : null
216
- const rel = segments.reverse().join('/')
217
-
218
- return { rel, id, variant }
85
+ private locate (...rel: string[]): string {
86
+ return join(ENTRIES, ...rel)
219
87
  }
220
88
  }
221
89
 
222
- const ERR_NOT_FOUND = Err('NOT_FOUND')
223
- const ERR_PERMUTATION_MISMATCH = Err('PERMUTATION_MISMATCH')
224
-
225
- const TEMP = '/temp'
226
- const BLOBs = '/blobs'
227
- const ENTRIES = '/entries'
228
- const LIST = '.list'
229
- const META = '.meta'
230
-
231
- type Maybe<T> = Promise<T | Error>
232
-
233
- interface Path {
234
- rel: string
235
- id: string
236
- variant: string | null
237
- }
238
-
239
- type Meta = Record<string, string>
90
+ const ENTRIES = '/'
240
91
 
241
92
  interface Options extends ScanOptions {
242
- meta?: Meta
93
+ id?: string
94
+ origin?: string
95
+ attributes?: Attributes
243
96
  }
244
97
 
98
+ type Maybe<T> = Promise<T | Error>
99
+
245
100
  export type Storages = Record<string, Storage>
@@ -3,28 +3,35 @@ import { encode } from '@toa.io/generic'
3
3
  import { providers } from './providers'
4
4
  import { validateAnnotation } from './Annotation'
5
5
  import type { Annotation } from './Annotation'
6
- import type { Dependency, Variable } from '@toa.io/operations'
6
+ import type { Dependency, Variable, Mounts } from '@toa.io/operations'
7
7
  import type { context } from '@toa.io/norm'
8
8
 
9
- export const SERIALIZATION_PREFIX = 'TOA_STORAGES'
9
+ export const ENV_PREFIX = 'TOA_STORAGES'
10
10
 
11
11
  export function deployment (instances: Instance[], annotation: unknown): Dependency {
12
12
  validate(instances, annotation)
13
13
 
14
14
  const value = encode(annotation)
15
- const pointer: Variable = { name: SERIALIZATION_PREFIX, value }
15
+ const pointer: Variable = { name: ENV_PREFIX, value }
16
16
  const secrets = getSecrets(annotation)
17
+ const mounts = getMounts(instances, annotation)
17
18
 
18
- return {
19
- variables: { global: [pointer, ...secrets] }
20
- }
19
+ const dependency: Dependency = { variables: { global: [pointer, ...secrets] } }
20
+
21
+ if (mounts !== null)
22
+ dependency.mounts = mounts
23
+
24
+ return dependency
21
25
  }
22
26
 
23
27
  function validate (instances: Instance[], annotation: unknown): asserts annotation is Annotation {
24
28
  validateAnnotation(annotation)
25
29
 
26
- for (const instance of instances)
30
+ for (const instance of instances) {
31
+ instance.manifest ??= []
32
+
27
33
  contains(instance, annotation)
34
+ }
28
35
  }
29
36
 
30
37
  function contains (instance: Instance, annotation: Annotation): void {
@@ -40,18 +47,47 @@ function getSecrets (annotation: Annotation): Variable[] {
40
47
  for (const [name, declaration] of Object.entries(annotation)) {
41
48
  const Provider = providers[declaration.provider]
42
49
 
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
- })
50
+ if (Provider.SECRETS !== undefined)
51
+ // eslint-disable-next-line max-depth
52
+ for (const secret of Provider.SECRETS)
53
+ secrets.push({
54
+ name: `${ENV_PREFIX}_${name}_${secret.name}`.toUpperCase(),
55
+ secret: {
56
+ name: `toa-storages-${name}`,
57
+ key: secret.name,
58
+ optional: secret.optional
59
+ }
60
+ })
52
61
  }
53
62
 
54
63
  return secrets
55
64
  }
56
65
 
66
+ function getMounts (instances: Instance[], annotation: Annotation): Mounts | null {
67
+ let mounts: Mounts | null = null
68
+
69
+ for (const { locator, manifest } of instances)
70
+ for (const name of manifest) {
71
+ const declaration = annotation[name]
72
+
73
+ // eslint-disable-next-line max-depth
74
+ if (declaration.provider !== 'fs')
75
+ continue
76
+
77
+ // eslint-disable-next-line max-depth
78
+ if (declaration.claim !== undefined) {
79
+ mounts ??= {}
80
+ mounts[locator.label] ??= []
81
+
82
+ mounts[locator.label].push({
83
+ name,
84
+ path: declaration.path,
85
+ claim: declaration.claim
86
+ })
87
+ }
88
+ }
89
+
90
+ return mounts
91
+ }
92
+
57
93
  export type Instance = context.Dependency<string[]>
@@ -0,0 +1,3 @@
1
+ import { Err } from 'error-value'
2
+
3
+ export const ERR_NOT_FOUND = new Err('NOT_FOUND')
package/source/index.ts CHANGED
@@ -2,4 +2,6 @@ export { Factory } from './Factory'
2
2
  export { deployment } from './deployment'
3
3
  export { manifest } from './manifest'
4
4
 
5
- export type { Entry } from './Entry'
5
+ export type { Entry, Stream } from './Entry'
6
+ export type { Storage } from './Storage'
7
+ export type * from './providers'
@@ -1,8 +1,9 @@
1
- import { match } from 'matchacho'
2
-
3
1
  export function manifest (manifest: string | string[] | null): string[] {
4
- return match(manifest,
5
- String, (name: string) => [name],
6
- Array, manifest,
7
- null, [])
2
+ if (manifest === null)
3
+ return []
4
+
5
+ if (typeof manifest === 'string')
6
+ return [manifest]
7
+
8
+ return manifest
8
9
  }