@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.
- package/package.json +8 -8
- package/readme.md +32 -22
- 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 +1 -0
- package/source/Factory.ts +7 -4
- package/source/Provider.ts +5 -0
- package/source/Scanner.ts +46 -17
- package/source/Storage.test.ts +126 -76
- package/source/Storage.ts +92 -91
- package/source/deployment.ts +38 -7
- package/source/providers/FileSystem.test.ts +6 -0
- package/source/providers/FileSystem.ts +15 -2
- package/source/providers/Memory.ts +21 -7
- package/source/providers/S3.test.ts +1 -0
- package/source/providers/S3.ts +36 -6
- package/source/providers/index.test.ts +10 -0
- package/source/test/arny.jpg +0 -0
- package/source/test/sample.avi +0 -0
- package/source/test/sample.svg +4 -0
- package/source/test/sample.wav +0 -0
- package/transpiled/Entry.d.ts +1 -0
- package/transpiled/Factory.js +5 -3
- package/transpiled/Factory.js.map +1 -1
- package/transpiled/Provider.d.ts +3 -0
- package/transpiled/Provider.js +1 -0
- package/transpiled/Provider.js.map +1 -1
- package/transpiled/Scanner.d.ts +3 -1
- package/transpiled/Scanner.js +36 -15
- package/transpiled/Scanner.js.map +1 -1
- package/transpiled/Storage.d.ts +6 -6
- package/transpiled/Storage.js +66 -78
- package/transpiled/Storage.js.map +1 -1
- package/transpiled/deployment.d.ts +1 -1
- package/transpiled/deployment.js +30 -7
- package/transpiled/deployment.js.map +1 -1
- package/transpiled/providers/FileSystem.d.ts +4 -1
- package/transpiled/providers/FileSystem.js +10 -1
- package/transpiled/providers/FileSystem.js.map +1 -1
- package/transpiled/providers/Memory.d.ts +2 -0
- package/transpiled/providers/Memory.js +13 -1
- package/transpiled/providers/Memory.js.map +1 -1
- package/transpiled/providers/S3.d.ts +3 -0
- package/transpiled/providers/S3.js +23 -4
- package/transpiled/providers/S3.js.map +1 -1
- 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.
|
|
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
|
-
"
|
|
28
|
-
"
|
|
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.
|
|
35
|
-
"@toa.io/generic": "1.0.0-alpha.
|
|
36
|
-
"@toa.io/schemas": "1.0.0-alpha.
|
|
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": "
|
|
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
|
|
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
|
-
|
|
101
|
+
Moves the entry specified by `path` to the new path.
|
|
105
102
|
|
|
106
|
-
|
|
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
|
-
|
|
109
|
-
returned.
|
|
110
|
-
|
|
111
|
-
#### `async diversify(path: string, name: string, stream: Readable): Maybe<void>`
|
|
106
|
+
The following examples are equivalent:
|
|
112
107
|
|
|
113
|
-
|
|
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
|
|
114
|
+
#### `async entries(path: string): Entry[]`
|
|
116
115
|
|
|
117
|
-
|
|
116
|
+
Get a list of entries under the `path`.
|
|
118
117
|
|
|
119
|
-
#### `async
|
|
118
|
+
#### `async diversify(path: string, name: string, stream: Readable): Maybe<void>`
|
|
120
119
|
|
|
121
|
-
|
|
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
|
|
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
|
|
162
|
+
photos:
|
|
164
163
|
provider: fs
|
|
165
|
-
path: /var/my-
|
|
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
|
-
|
|
218
|
+
/.entries/
|
|
219
|
+
b4f577e0 # entry
|
|
209
220
|
/b4f577e0
|
|
210
|
-
.
|
|
211
|
-
thumbnail.jpeg # variant BLOBs
|
|
221
|
+
thumbnail.jpeg # variant
|
|
212
222
|
thumbnail.webp
|
|
213
223
|
```
|
|
214
224
|
|
package/schemas/fs.cos.yaml
CHANGED
package/schemas/s3.cos.yaml
CHANGED
package/schemas/tmp.cos.yaml
CHANGED
package/source/Entry.ts
CHANGED
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 {
|
|
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
|
|
17
|
+
const env = process.env[ENV_PREFIX]
|
|
17
18
|
|
|
18
|
-
assert.ok(env !== undefined,
|
|
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 = `${
|
|
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,
|
package/source/Provider.ts
CHANGED
|
@@ -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
|
|
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.
|
|
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,
|
|
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.
|
|
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: '
|
|
101
|
-
{ hex: '
|
|
102
|
-
{ hex: '
|
|
103
|
-
{ hex: '
|
|
104
|
-
{ hex: '
|
|
105
|
-
{ hex: '
|
|
106
|
-
{ hex: '
|
|
107
|
-
{ hex: '
|
|
108
|
-
{ hex: '
|
|
109
|
-
{ hex: '
|
|
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
|
}
|
package/source/Storage.test.ts
CHANGED
|
@@ -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
|
-
|
|
151
|
-
|
|
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
|
-
|
|
142
|
+
beforeEach(async () => {
|
|
143
|
+
const stream = createReadStream('lenna.png')
|
|
182
144
|
|
|
183
|
-
|
|
145
|
+
lenna = (await storage.put(dir, stream)) as Entry
|
|
184
146
|
})
|
|
185
147
|
|
|
186
|
-
it('should
|
|
148
|
+
it('should set meta property', async () => {
|
|
187
149
|
const path = `${dir}/${lenna.id}`
|
|
188
150
|
|
|
189
|
-
await storage.
|
|
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
|
-
|
|
204
|
-
const error = await storage[method](path)
|
|
153
|
+
const state0 = (await storage.get(path)) as Entry
|
|
205
154
|
|
|
206
|
-
|
|
207
|
-
expect(error).toHaveProperty('code', 'NOT_FOUND')
|
|
208
|
-
}
|
|
209
|
-
})
|
|
210
|
-
})
|
|
155
|
+
expect(state0).toHaveProperty('meta.foo', 'bar')
|
|
211
156
|
|
|
212
|
-
|
|
213
|
-
let lenna: Entry
|
|
157
|
+
await storage.annotate(path, 'foo')
|
|
214
158
|
|
|
215
|
-
|
|
216
|
-
const stream = createReadStream('lenna.png')
|
|
159
|
+
const state1 = (await storage.get(path)) as Entry
|
|
217
160
|
|
|
218
|
-
|
|
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,
|
|
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).
|
|
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(
|
|
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
|
+
})
|