@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.
- package/package.json +13 -14
- package/readme.md +94 -49
- package/schemas/annotation.cos.yaml +1 -0
- package/schemas/cloudinary.cos.yaml +30 -0
- package/schemas/fs.cos.yaml +2 -0
- package/schemas/s3.cos.yaml +1 -0
- package/schemas/tmp.cos.yaml +0 -1
- package/source/Entry.ts +11 -11
- package/source/Factory.ts +16 -9
- package/source/Provider.ts +19 -19
- package/source/Scanner.ts +57 -24
- package/source/Secrets.ts +6 -0
- package/source/Storage.test.ts +103 -279
- package/source/Storage.ts +55 -204
- package/source/deployment.ts +48 -15
- package/source/errors.ts +3 -0
- package/source/index.ts +1 -1
- package/source/manifest.ts +7 -6
- package/source/providers/Cloudinary.ts +274 -0
- package/source/providers/Declaration.ts +2 -1
- package/source/providers/FileSystem.ts +79 -25
- package/source/providers/S3.ts +81 -80
- package/source/providers/Temporary.ts +2 -3
- package/source/providers/Test.ts +4 -4
- package/source/providers/index.test.ts +102 -76
- package/source/providers/index.ts +9 -4
- package/source/schemas.ts +1 -0
- package/source/test/.env.example +4 -0
- package/source/test/arny.jpg +0 -0
- package/source/test/lenna.48x48.jpeg +0 -0
- package/source/test/sample.avi +0 -0
- package/source/test/sample.svg +4 -0
- package/source/test/sample.wav +0 -0
- package/source/test/util.ts +54 -12
- package/transpiled/Entry.d.ts +12 -10
- package/transpiled/Factory.js +11 -5
- package/transpiled/Factory.js.map +1 -1
- package/transpiled/Provider.d.ts +15 -15
- package/transpiled/Provider.js +3 -3
- package/transpiled/Provider.js.map +1 -1
- package/transpiled/Scanner.d.ts +5 -2
- package/transpiled/Scanner.js +49 -22
- package/transpiled/Scanner.js.map +1 -1
- package/transpiled/Secrets.d.ts +5 -0
- package/transpiled/Secrets.js +3 -0
- package/transpiled/Secrets.js.map +1 -0
- package/transpiled/Storage.d.ts +10 -19
- package/transpiled/Storage.js +47 -153
- package/transpiled/Storage.js.map +1 -1
- package/transpiled/deployment.d.ts +1 -1
- package/transpiled/deployment.js +40 -15
- package/transpiled/deployment.js.map +1 -1
- package/transpiled/errors.d.ts +2 -0
- package/transpiled/errors.js +6 -0
- package/transpiled/errors.js.map +1 -0
- package/transpiled/index.d.ts +1 -1
- package/transpiled/manifest.js +5 -2
- package/transpiled/manifest.js.map +1 -1
- package/transpiled/providers/Cloudinary.d.ts +42 -0
- package/transpiled/providers/Cloudinary.js +189 -0
- package/transpiled/providers/Cloudinary.js.map +1 -0
- package/transpiled/providers/Declaration.d.ts +3 -2
- package/transpiled/providers/FileSystem.d.ts +15 -7
- package/transpiled/providers/FileSystem.js +64 -24
- package/transpiled/providers/FileSystem.js.map +1 -1
- package/transpiled/providers/S3.d.ts +14 -14
- package/transpiled/providers/S3.js +62 -60
- package/transpiled/providers/S3.js.map +1 -1
- package/transpiled/providers/Temporary.d.ts +1 -2
- package/transpiled/providers/Temporary.js +2 -2
- package/transpiled/providers/Temporary.js.map +1 -1
- package/transpiled/providers/Test.d.ts +3 -3
- package/transpiled/providers/Test.js +2 -2
- package/transpiled/providers/Test.js.map +1 -1
- package/transpiled/providers/index.d.ts +6 -2
- package/transpiled/providers/index.js +2 -2
- package/transpiled/providers/index.js.map +1 -1
- package/transpiled/schemas.d.ts +1 -0
- package/transpiled/schemas.js +2 -1
- package/transpiled/schemas.js.map +1 -1
- package/transpiled/test/util.d.ts +70 -5
- package/transpiled/test/util.js +43 -4
- package/transpiled/test/util.js.map +1 -1
- package/transpiled/tsconfig.tsbuildinfo +1 -1
- package/source/providers/FileSystem.test.ts +0 -5
- package/source/providers/Memory.ts +0 -41
- package/source/providers/S3.test.ts +0 -133
- package/transpiled/providers/Memory.d.ts +0 -13
- package/transpiled/providers/Memory.js +0 -60
- 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
|
|
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
|
-
|
|
16
|
+
public override readonly root: string
|
|
13
17
|
|
|
14
|
-
public constructor (options: FileSystemOptions
|
|
15
|
-
super(options
|
|
18
|
+
public constructor (options: FileSystemOptions) {
|
|
19
|
+
super(options)
|
|
16
20
|
|
|
17
|
-
this.
|
|
21
|
+
this.root = options.path
|
|
18
22
|
}
|
|
19
23
|
|
|
20
|
-
public async get (
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
if ((err as NodeJS.ErrnoException)?.code === 'ENOENT') return null
|
|
28
|
+
if (metadata instanceof Error)
|
|
29
|
+
return metadata
|
|
27
30
|
|
|
28
|
-
|
|
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,
|
|
33
|
-
const
|
|
34
|
-
const
|
|
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(
|
|
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
|
|
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
|
-
|
|
46
|
-
|
|
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(
|
|
49
|
-
|
|
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
|
}
|
package/source/providers/S3.ts
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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
|
|
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
|
-
|
|
39
|
-
|
|
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)
|
|
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<
|
|
89
|
-
try {
|
|
90
|
-
const
|
|
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
|
-
|
|
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 (
|
|
95
|
+
if (entry.Metadata?.value === undefined)
|
|
96
|
+
return ERR_NOT_FOUND
|
|
98
97
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
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 (
|
|
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
|
|
123
|
+
Key,
|
|
114
124
|
Body: stream
|
|
115
125
|
}
|
|
116
126
|
}).done()
|
|
117
127
|
}
|
|
118
128
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
|
158
|
-
await this.client.send(new
|
|
159
|
-
|
|
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
|
-
|
|
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
|
|
10
|
+
public constructor (options: TemporaryOptions) {
|
|
12
11
|
const path = join(tmpdir(), options.directory)
|
|
13
12
|
|
|
14
|
-
super({ path }
|
|
13
|
+
super({ path })
|
|
15
14
|
}
|
|
16
15
|
}
|
package/source/providers/Test.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { Temporary, type TemporaryOptions } from './Temporary'
|
|
2
|
-
import type {
|
|
2
|
+
import type { Secret } from '../Secrets'
|
|
3
3
|
|
|
4
4
|
export class Test extends Temporary {
|
|
5
|
-
public static override readonly SECRETS: readonly
|
|
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
|
|
11
|
-
super(options
|
|
10
|
+
public constructor (options: TemporaryOptions) {
|
|
11
|
+
super(options)
|
|
12
12
|
}
|
|
13
13
|
}
|
|
@@ -1,126 +1,152 @@
|
|
|
1
|
-
import
|
|
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
|
|
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 {
|
|
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:
|
|
23
|
+
const Provider: Constructor = providers[suite.provider]
|
|
14
24
|
const provider = new Provider(suite.options, suite.secrets)
|
|
15
25
|
|
|
16
|
-
|
|
26
|
+
describe('put, get, head', () => {
|
|
27
|
+
let entry: Stream
|
|
17
28
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
29
|
+
beforeAll(async () => {
|
|
30
|
+
await provider.put(id, createReadStream(sample))
|
|
31
|
+
await provider.commit(id, metadata)
|
|
21
32
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
})
|
|
33
|
+
entry = await provider.get(id) as Stream
|
|
34
|
+
})
|
|
25
35
|
|
|
26
|
-
|
|
27
|
-
expect(provider).toBeInstanceOf(Provider)
|
|
28
|
-
})
|
|
36
|
+
afterAll(() => entry.stream.destroy())
|
|
29
37
|
|
|
30
|
-
|
|
31
|
-
|
|
38
|
+
it('should create metadata', async () => {
|
|
39
|
+
expect(entry.size).toEqual(metadata.size)
|
|
40
|
+
})
|
|
32
41
|
|
|
33
|
-
|
|
34
|
-
|
|
42
|
+
if (suite.provider !== 'cloudinary')
|
|
43
|
+
it('should store attributes', async () => {
|
|
44
|
+
const path = '/path/to/file'
|
|
35
45
|
|
|
36
|
-
|
|
37
|
-
|
|
46
|
+
await provider.put(path, createReadStream(sample))
|
|
47
|
+
await provider.commit(path, { ...metadata, attributes: { foo: 'bar' } })
|
|
38
48
|
|
|
39
|
-
|
|
49
|
+
const entry = await provider.get(path) as Stream
|
|
40
50
|
|
|
41
|
-
|
|
42
|
-
const output = await buffer(readable)
|
|
43
|
-
const lenna = await readFile('lenna.png')
|
|
51
|
+
expect(entry.attributes).toEqual({ foo: 'bar' })
|
|
44
52
|
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
entry.stream.destroy()
|
|
54
|
+
})
|
|
47
55
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const stream1 = createReadStream('albert.jpg')
|
|
56
|
+
it('should overwrite', async () => {
|
|
57
|
+
const path = Math.random().toString(36).substring(7)
|
|
51
58
|
|
|
52
|
-
|
|
53
|
-
|
|
59
|
+
await provider.put(path, createReadStream(sample))
|
|
60
|
+
await provider.commit(path, metadata)
|
|
54
61
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const albert = await readFile('albert.jpg')
|
|
62
|
+
if (suite.provider === 'cloudinary')
|
|
63
|
+
await new Promise((resolve) => setTimeout(resolve, 5_000))
|
|
58
64
|
|
|
59
|
-
|
|
60
|
-
|
|
65
|
+
await expect(provider.put(path, createReadStream(lenna)))
|
|
66
|
+
.resolves.not.toThrow()
|
|
61
67
|
|
|
62
|
-
|
|
63
|
-
const stream = createReadStream('lenna.png')
|
|
68
|
+
const meta = { ...metadata, size: 473831 } // lenna size
|
|
64
69
|
|
|
65
|
-
|
|
70
|
+
await provider.commit(path, meta)
|
|
66
71
|
|
|
67
|
-
|
|
72
|
+
if (suite.provider === 'cloudinary')
|
|
73
|
+
await new Promise((resolve) => setTimeout(resolve, 10_000))
|
|
68
74
|
|
|
69
|
-
|
|
70
|
-
})
|
|
75
|
+
const overwritten = await provider.get(path) as Stream
|
|
71
76
|
|
|
72
|
-
|
|
73
|
-
/*
|
|
77
|
+
expect(overwritten.size).toEqual(meta.size)
|
|
74
78
|
|
|
75
|
-
|
|
76
|
-
|
|
79
|
+
overwritten.stream.destroy()
|
|
80
|
+
})
|
|
77
81
|
|
|
78
|
-
|
|
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
|
-
|
|
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
|
-
|
|
85
|
-
|
|
98
|
+
describe('delete', () => {
|
|
99
|
+
const path = '/path/to/' + Math.random().toString(36).substring(7)
|
|
86
100
|
|
|
87
|
-
|
|
88
|
-
await provider.
|
|
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
|
|
106
|
+
const error = await provider.get(path) as any
|
|
91
107
|
|
|
92
|
-
expect(
|
|
108
|
+
expect(error).toBeInstanceOf(Error)
|
|
109
|
+
expect(error.code).toBe('NOT_FOUND')
|
|
93
110
|
})
|
|
94
111
|
|
|
95
|
-
it('should not
|
|
96
|
-
await
|
|
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
|
-
|
|
100
|
-
|
|
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(
|
|
103
|
-
await provider.delete(dir)
|
|
124
|
+
await provider.put(from, createReadStream(sample))
|
|
104
125
|
|
|
105
|
-
|
|
126
|
+
if (committed)
|
|
127
|
+
await provider.commit(from, metadata)
|
|
106
128
|
|
|
107
|
-
|
|
108
|
-
})
|
|
129
|
+
await provider.move(from, to)
|
|
109
130
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const dir2 = '/' + randomUUID()
|
|
131
|
+
if (!committed)
|
|
132
|
+
await provider.commit(to, metadata)
|
|
113
133
|
|
|
114
|
-
await provider.
|
|
115
|
-
await provider.move(dir + '/lenna.png', dir2 + '/lenna2.png')
|
|
134
|
+
const error = await provider.get(from) as any
|
|
116
135
|
|
|
117
|
-
|
|
136
|
+
expect(error).toBeInstanceOf(Error)
|
|
118
137
|
|
|
119
|
-
|
|
138
|
+
const entry = await provider.get(to) as Stream
|
|
120
139
|
|
|
121
|
-
|
|
140
|
+
expect(entry.stream).toBeDefined()
|
|
141
|
+
})
|
|
122
142
|
|
|
123
|
-
|
|
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)
|