@toa.io/extensions.storages 0.20.0-alpha.3
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/LICENSE +22 -0
- package/package.json +38 -0
- package/readme.md +195 -0
- package/source/.test/albert.jpg +0 -0
- package/source/.test/empty.txt +0 -0
- package/source/.test/lenna.ascii +64 -0
- package/source/.test/lenna.png +0 -0
- package/source/.test/sample.avif +0 -0
- package/source/.test/sample.gif +0 -0
- package/source/.test/sample.heic +0 -0
- package/source/.test/sample.jpeg +0 -0
- package/source/.test/sample.jxl +0 -0
- package/source/.test/sample.webp +0 -0
- package/source/.test/util.ts +50 -0
- package/source/Aspect.ts +23 -0
- package/source/Entry.ts +14 -0
- package/source/Factory.ts +65 -0
- package/source/Provider.ts +8 -0
- package/source/Scanner.ts +109 -0
- package/source/Storage.test.ts +445 -0
- package/source/Storage.ts +236 -0
- package/source/deployment.ts +61 -0
- package/source/index.ts +3 -0
- package/source/manifest.ts +8 -0
- package/source/providers/FileSystem.test.ts +13 -0
- package/source/providers/FileSystem.ts +46 -0
- package/source/providers/Temporary.ts +14 -0
- package/source/providers/Test.ts +13 -0
- package/source/providers/index.test.ts +116 -0
- package/source/providers/index.ts +13 -0
- package/source/providers/readme.md +20 -0
- package/tsconfig.json +12 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2020-present Artem Gurtovoi
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@toa.io/extensions.storages",
|
|
3
|
+
"version": "0.20.0-alpha.3",
|
|
4
|
+
"description": "Toa Storages",
|
|
5
|
+
"author": "temich <tema.gurtovoy@gmail.com>",
|
|
6
|
+
"homepage": "https://github.com/toa-io/toa#readme",
|
|
7
|
+
"main": "transpiled/index.js",
|
|
8
|
+
"types": "transpiled/index.d.ts",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/toa-io/toa.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/toa-io/toa/issues"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"jest": {
|
|
20
|
+
"preset": "ts-jest",
|
|
21
|
+
"testEnvironment": "node"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"test": "jest",
|
|
25
|
+
"transpile": "rm -rf transpiled && npx tsc"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@toa.io/match": "0.3.0",
|
|
29
|
+
"@toa.io/streams": "0.1.0-alpha.2",
|
|
30
|
+
"@types/fs-extra": "11.0.3"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@toa.io/generic": "0.20.0-alpha.2",
|
|
34
|
+
"fs-extra": "11.1.1",
|
|
35
|
+
"msgpackr": "1.9.9"
|
|
36
|
+
},
|
|
37
|
+
"gitHead": "ed28dc0d2823022fbb1188bb28b994bc827a4432"
|
|
38
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# Toa Storages
|
|
2
|
+
|
|
3
|
+
Shared BLOB storage.
|
|
4
|
+
|
|
5
|
+
## Entry
|
|
6
|
+
|
|
7
|
+
BLOBs are stored with the meta-information object (Entry) having the following properties:
|
|
8
|
+
|
|
9
|
+
- `id` - checksum
|
|
10
|
+
- `size` - size in bytes
|
|
11
|
+
- `type` - MIME type
|
|
12
|
+
- `created` - creation timestamp (UNIX time, ms)
|
|
13
|
+
- `variants` - array of:
|
|
14
|
+
- `name` - unique name
|
|
15
|
+
- `size` - size in bytes
|
|
16
|
+
- `type` - variant MIME type
|
|
17
|
+
- `meta` - object with application-specific information, empty by default
|
|
18
|
+
|
|
19
|
+
### Example
|
|
20
|
+
|
|
21
|
+
```yaml
|
|
22
|
+
id: eecd837c
|
|
23
|
+
type: image/jpeg
|
|
24
|
+
created: 1698004822358
|
|
25
|
+
variants:
|
|
26
|
+
- name: thumbnail.jpeg
|
|
27
|
+
type: image/jpeg
|
|
28
|
+
- name: thumbnail.webp
|
|
29
|
+
type: image/webp
|
|
30
|
+
meta:
|
|
31
|
+
face: true
|
|
32
|
+
nudity: false
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Aspect
|
|
36
|
+
|
|
37
|
+
The Storages extension provides `storages` aspect,
|
|
38
|
+
containing named Storage instances, according to the annotation.
|
|
39
|
+
|
|
40
|
+
```javascript
|
|
41
|
+
async function effect (_, context) {
|
|
42
|
+
await context.storages.photos.fetch('/path/to/b4f577e0.thumbnail.jpeg')
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Storage interface
|
|
47
|
+
|
|
48
|
+
> `Maybe<T> = T | Error`
|
|
49
|
+
|
|
50
|
+
#### `async put(path: string, stream: Readable, type?: string): Maybe<Entry>`
|
|
51
|
+
|
|
52
|
+
Add a BLOB to the storage and create an entry under specified `path`.
|
|
53
|
+
|
|
54
|
+
BLOB type is identified
|
|
55
|
+
using [magick numbers](https://github.com/sindresorhus/file-type).
|
|
56
|
+
If the `type` argument is specified and does not match the BLOB type, then a `TYPE_MISMATCH` error
|
|
57
|
+
is returned.
|
|
58
|
+
If the BLOB type cannot be identified
|
|
59
|
+
and the value of `type` is not in the list of known types, then the given value is used.
|
|
60
|
+
|
|
61
|
+
Known types
|
|
62
|
+
are: `image/jpeg`, `image/png`, `image/gif`, `image/webp`, `image/heic`, `image/jxl`, `image/avif`.
|
|
63
|
+
|
|
64
|
+
See [source](source/Scanner.ts).
|
|
65
|
+
|
|
66
|
+
If the entry already exists, it is returned and [revealed](#async-revealpath-string-maybevoid).
|
|
67
|
+
|
|
68
|
+
#### `async get(path: string): Maybe<Entry>`
|
|
69
|
+
|
|
70
|
+
Get an entry.
|
|
71
|
+
|
|
72
|
+
If the entry does not exist, a `NOT_FOUND` error is returned.
|
|
73
|
+
|
|
74
|
+
#### `async fetch(path: string): Maybe<Readable>`
|
|
75
|
+
|
|
76
|
+
Fetch the BLOB specified by `path`. If the path does not exist, a `NOT_FOUND` error is returned.
|
|
77
|
+
|
|
78
|
+
`path` can be an entry id, or a path to the entry, or a path to a variant of the entry.
|
|
79
|
+
|
|
80
|
+
- `eecd837c` - fetch the BLOB by `id`
|
|
81
|
+
- `/path/to/eecd837c` - fetch the BLOB by path
|
|
82
|
+
- `/path/to/eecd837c.thumbnail.jpeg` - fetch the `thumbnail.jpeg` variant of the BLOB
|
|
83
|
+
|
|
84
|
+
#### `async delete(path: string): Maybe<void>`
|
|
85
|
+
|
|
86
|
+
Delete the entry specified by `path`.
|
|
87
|
+
|
|
88
|
+
#### `async list(path: string): string[]`
|
|
89
|
+
|
|
90
|
+
Get ordered list of `id`s of entries in under the `path`.
|
|
91
|
+
|
|
92
|
+
#### `async reorder(path: string, ids: string[]): Maybe<void>`
|
|
93
|
+
|
|
94
|
+
Reorder entries under the `path`.
|
|
95
|
+
|
|
96
|
+
Given list must be a permutation of the current list, otherwise a `PERMUTATION_MISMATCH` error is
|
|
97
|
+
returned.
|
|
98
|
+
|
|
99
|
+
#### `async diversify(path: string, name: string, stream: Readable): Maybe<void>`
|
|
100
|
+
|
|
101
|
+
Add or replace a `name` variant of the entry specified by `path`.
|
|
102
|
+
|
|
103
|
+
#### `async conceal(path: string): Maybe<void>`
|
|
104
|
+
|
|
105
|
+
Remove the entry from the list.
|
|
106
|
+
|
|
107
|
+
#### `async reveal(path: string): Maybe<void>`
|
|
108
|
+
|
|
109
|
+
Restore the entry to the list.
|
|
110
|
+
|
|
111
|
+
#### `async annotate(path: string, key: string, value: any): Maybe<void>`
|
|
112
|
+
|
|
113
|
+
Set a `key` property in the `meta` of the entry specified by `path`.
|
|
114
|
+
|
|
115
|
+
## Providers
|
|
116
|
+
|
|
117
|
+
Storage uses underlying providers to store BLOBs and entries.
|
|
118
|
+
|
|
119
|
+
Custom providers are not supported yet.
|
|
120
|
+
|
|
121
|
+
### Amazon S3
|
|
122
|
+
|
|
123
|
+
Annotation value format is `s3://{region}/{bucket}`.
|
|
124
|
+
|
|
125
|
+
Requires secrets for the access key and secret key.
|
|
126
|
+
|
|
127
|
+
`s3://us-east-1/my-bucket`
|
|
128
|
+
|
|
129
|
+
### Filesystem
|
|
130
|
+
|
|
131
|
+
Annotation value format is `file:///{path}`.
|
|
132
|
+
|
|
133
|
+
`file:///var/my-storage`
|
|
134
|
+
|
|
135
|
+
### Temporary
|
|
136
|
+
|
|
137
|
+
Filesystem using OS temporary directory.
|
|
138
|
+
|
|
139
|
+
Annotation value format is `tmp:///{path}`.
|
|
140
|
+
|
|
141
|
+
`tmp:///my-storage`
|
|
142
|
+
|
|
143
|
+
## Deduplication
|
|
144
|
+
|
|
145
|
+
BLOBs are stored in the underlying storage with their checksum as the key, ensuring that identical BLOBs
|
|
146
|
+
are stored only once.
|
|
147
|
+
Variants, on the other hand, are not deduplicated across different entries.
|
|
148
|
+
|
|
149
|
+
Underlying directory structure:
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
/temp
|
|
153
|
+
c28f4dfd # random id
|
|
154
|
+
/blobs
|
|
155
|
+
b4f577e0 # checksum
|
|
156
|
+
/storage
|
|
157
|
+
/path/to
|
|
158
|
+
.list # list of entries
|
|
159
|
+
/b4f577e0
|
|
160
|
+
.meta # entry
|
|
161
|
+
thumbnail.jpeg # variant BLOBs
|
|
162
|
+
thumbnail.webp
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Manifest
|
|
166
|
+
|
|
167
|
+
Storage extension can be enabled by adding `storages` key to the component manifest.
|
|
168
|
+
|
|
169
|
+
```yaml
|
|
170
|
+
storages: [photos, videos]
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Value of the `storages` key is an array of storage names, that should be declared in the context.
|
|
174
|
+
|
|
175
|
+
It the names are unknown, `null` declaration can be used:
|
|
176
|
+
|
|
177
|
+
```yaml
|
|
178
|
+
storages: ~
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Annotation
|
|
182
|
+
|
|
183
|
+
The `storages` context annotation is an object with keys that reference the storage name and
|
|
184
|
+
provider-specific URLs as values.
|
|
185
|
+
|
|
186
|
+
```yaml
|
|
187
|
+
storages:
|
|
188
|
+
photos: s3://us-east-1/my-bucket
|
|
189
|
+
photos@dev: file:///var/my-storage
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Secrets
|
|
193
|
+
|
|
194
|
+
Secrets declared by storage providers can be deployed by [`toa conceal`](/runtime/cli/readme.md#conceal),
|
|
195
|
+
or set locally by [`toa env`](/runtime/cli/readme.md#env).
|
|
Binary file
|
|
File without changes
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
***************...............******.*********************************.***.*.....********************@@@@*....................**
|
|
2
|
+
***************..................**********************************.****.*.*......********************@@@@*................**.
|
|
3
|
+
***************..................**************************************.****.......********************@@@@*..........****..
|
|
4
|
+
***************.................*.***********.****.....*...*..****.**..............**********************@@@@.........**.
|
|
5
|
+
***************..................*.**..**.**.****.......*....********...............**********************@@@@*.....**.
|
|
6
|
+
***************......................***..*******..*...........*..**.*.***..........***********************@@@@*.....
|
|
7
|
+
***************.....................*****..***.******************....**.*...........************************@@@@*..
|
|
8
|
+
*****..********..........................***...*****************@@@*................*************************@@@@*
|
|
9
|
+
****...********........................*.......*.****************@@@@@*.............**************************@@.
|
|
10
|
+
***....********.......................*........*****************@@@@@**@@*..........*******..*****************. . *
|
|
11
|
+
**.....********.....................*...........*************@****@@@@@@@@@.........*******...*************** . .**
|
|
12
|
+
*......********...............................*.**************@@@@@@@@@@@@@@@.......*******. ..***********. ...****
|
|
13
|
+
.......********..............................***************@@@@@@@@@@@@@@@@@@@.....*******. ..*********. .******
|
|
14
|
+
.......********.................**..........*********..***@@@*@**@@@@@@@@@@@@@@@*...*******. .******* .*******
|
|
15
|
+
.......********.................@.........********...*****@***@@@@@@@@@@@@@@@@@@@*..*******.. .@@@@@@ .*********
|
|
16
|
+
.......********................@*........**..**...************@@@@**@@@@@@@@@@@@@@*.*******. @@*@@@@@@* .**********
|
|
17
|
+
.......********................@*...........***.****************@@*@@@@@@@**@@@@@@@@******..*@*@@@@@@@@@* ************
|
|
18
|
+
.......********...............***.............***********************@@@***@@@@@@@**@****@@**@@@@@@@@@@@. .*************
|
|
19
|
+
.......********...............@**...........****.*************************************@@@@@@@@@@@@@@@@@@ .**************
|
|
20
|
+
.......********...............@****.......****.***********************************@**@@@@@@@@@@@@@****@ .***************
|
|
21
|
+
.......********...............@*****....****.**********************************@**@@@@@@@@@@@@@@******* ..****************
|
|
22
|
+
.......********...............@@*****.****..***********...****.*************@**@@***@@@@@@@@@@@***..*@ .*****************
|
|
23
|
+
.......********...............@@@****.....***.***.*****.....***..... @**@@@@@@@@@@@@@@@@@*....*@* .******************
|
|
24
|
+
.......********...............@@@*.**...***..**.**..*.... .*... .. .**@**@*@****@@@@@@*.....**@* .*******************
|
|
25
|
+
.......********................@*****..**...*....*..... .. ... ***@*********@@@@* ..******** .********************
|
|
26
|
+
.......********.................@@**.**..*...*....... . . . .. *.*@@*************....*.***@. .*********************
|
|
27
|
+
.......********..................@@**...*..*.*...... ... ...*************@@@@@*....***. .**********************
|
|
28
|
+
.......********..................@@......**.. .. . *. *.****@********@@@@@@@*.. ..** .**********************
|
|
29
|
+
.......********............*.....*@.....*.. . . . .. *.***************@@@@@@@@*. .** . .***********************
|
|
30
|
+
.......********...................*..***. . . . ...****************@@@@@@@@@*.. .*. . .************************
|
|
31
|
+
.......********.................*..*.*.... . .. . .. *******@*..*********@@@@@@@@@* .*. . .*************************
|
|
32
|
+
........*******...............*** ...... .. . . .. ..*****@*.*****...*******@**..... ** ... .*************************
|
|
33
|
+
........********...........******.. ..... ... . ****@***. . .******@@*. .*. .**************************
|
|
34
|
+
........*******............****.. . . ..* . ****@@*.. ... @@....***@@.....*. ..*... ***************************
|
|
35
|
+
........********......* .***@ .... .....*. .***@*..***...*******.***@@*****. . ..*.. .***************************
|
|
36
|
+
...... .********.......**.... ..... ....... .**@@...******************@@*****... . *.. .***********************@@***
|
|
37
|
+
...... .********.....****.. ..... ...... ..*@@.....****************.*@@******.. . ** *********************@@@@**@@
|
|
38
|
+
..... .********............. .. . .*. ..*@@ ...********************@@*****.. ** .*****************.**@@@@@@@@@
|
|
39
|
+
..... .********..*.*.. ... ..*** . ...**.. .*@* ...****************..**@@*****.. .. **. *******************@@@@@@@@@@@
|
|
40
|
+
.... .********........ .... .*.. ..*... ..**. .....************..**.**@@*****. . .** .******************@@@@@@@@@@@@
|
|
41
|
+
..... .********.......... *. ..*. . ... **** .....************.*. ...******. ** ******************@@@@@@@@@@@@@
|
|
42
|
+
.......*********.....* .. . ....... .* . ..**.** ......*****************@@****. .. *...******************@@@@@@@@@@@@@
|
|
43
|
+
.......*********......... . ... **.. ..**..* . .....****************@*@@**** ... ...******************@@@@@@@@@@@@@@
|
|
44
|
+
. . ..*********..*... .... ..........@@*..* . . .....*******..*****..**.*.** ... . .*.****************.*@@@@@@@@@@@@@@
|
|
45
|
+
. ..*********........ .. *.... ..**@*.*. . ........******...****@@**** .. . .******************.*@@@@@@@@@@@@@@
|
|
46
|
+
..*********..*.. . . ...*....*.*.*** . ........*********...*.***. ..... ******************.*@@@@@@@@@@@@@@
|
|
47
|
+
...*********....... . *..*...*..*@ .......**************.. .. ..... *****************..@@@@@@@@@@@@@@@
|
|
48
|
+
...*********.... . . *. ....**.. . . . ....************. . ...... *****************.*@@@@@@@@@@@@@@*
|
|
49
|
+
...********.*.* . .. .*...*..****. .. . .......*.**********. . ........**...***********..*@@@@@@@@@*.
|
|
50
|
+
.. ...*******@.... . . ..*.**.....** .. . ......************@@@@*. .......****..............@@@@@@@@** ...
|
|
51
|
+
**.... .********.. .. .. .........*. ... . . .. .....*.************@@@@@@@. ......*********.........*@@@@@*. ......
|
|
52
|
+
.****. .*********. . . * .....**..*.. . . ... ....****************@@@@@@@.. .. *************...*.*@@@@@*. ........
|
|
53
|
+
.***. .*********. . . . .......*** *@ . . .... ..*******************@@@@@@@* ******************@@@@@*...........
|
|
54
|
+
**** ********.. . . .......**...* .. . ... ..********************@@@@@@@* *************..*@@@@@@@............
|
|
55
|
+
.*** ********* .. . . * .**..**. . .... ..********************@@@@@@@@. .************..*@@@@@@@*............
|
|
56
|
+
.***. .********. .. . .**. ...*.... ........*********************@@@@@@@@ .***********..*@@@@@@@*.............
|
|
57
|
+
.***. .*******. .. . . ......*.. . ..... ..**********************@@@@@@@@*.***************@@@@@*..............
|
|
58
|
+
***. .********. . .. ....* ........************************@@@@@@@@*.....************@@*...............
|
|
59
|
+
**** .*******.. . . . . ..* ... ...... .**************************@@@@@@@@...........*****@@@*..............
|
|
60
|
+
...****..*******.. . .*... ..... ..***************************@@@@@@@@........ *@**@@.. ............
|
|
61
|
+
...****..******... . .. . . .... ..... ....****************************@@@@@@@@...........*@@**. ............. .
|
|
62
|
+
...***..*******. . ... . . ... ......********************************@@@@@@*..........**.................. .
|
|
63
|
+
.*@**.******. . . .* .. .. .. ........**********************************@@@@@@@.........................*...
|
|
64
|
+
.@@@*.******. . . ...* .. . ... ...........**********************************@@@@@@*......*....*..........*... ..
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import fs from 'node:fs/promises'
|
|
4
|
+
import { createReadStream } from 'node:fs'
|
|
5
|
+
import { Readable } from 'node:stream'
|
|
6
|
+
|
|
7
|
+
const suites: Suite[] = [
|
|
8
|
+
{
|
|
9
|
+
run: true,
|
|
10
|
+
ref: `file:///${join(tmpdir(), 'toa-storages-file')}`,
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
run: true,
|
|
14
|
+
ref: 'tmp:///toa-storages-temp',
|
|
15
|
+
},
|
|
16
|
+
// add more providers here, use `run` as a condition to run the test
|
|
17
|
+
// e.g.: `run: process.env.ACCESS_KEY_ID !== undefined`
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
function map (suite: Suite): Case{
|
|
21
|
+
const url = new URL(suite.ref)
|
|
22
|
+
|
|
23
|
+
return [url.protocol, url, suite.secrets ?? {}]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const cases = suites.filter(({ run }) => run).map(map)
|
|
27
|
+
|
|
28
|
+
export function rnd (): string{
|
|
29
|
+
return Math.random().toString(36).slice(2)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function open (rel: string): Promise<Readable>{
|
|
33
|
+
const path = join(__dirname, rel)
|
|
34
|
+
|
|
35
|
+
return createReadStream(path)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function read (rel: string): Promise<Buffer>{
|
|
39
|
+
const path = join(__dirname, rel)
|
|
40
|
+
|
|
41
|
+
return fs.readFile(path)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface Suite{
|
|
45
|
+
run: boolean
|
|
46
|
+
ref: string
|
|
47
|
+
secrets?: Record<string, string>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type Case = [string, URL, Record<string, string>]
|
package/source/Aspect.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Connector, type extensions } from '@toa.io/core'
|
|
2
|
+
import { type Storage, type Storages } from './Storage'
|
|
3
|
+
|
|
4
|
+
export class Aspect extends Connector implements extensions.Aspect {
|
|
5
|
+
public readonly name = 'storages'
|
|
6
|
+
|
|
7
|
+
private readonly storages: Storages
|
|
8
|
+
|
|
9
|
+
public constructor (storages: Storages) {
|
|
10
|
+
super()
|
|
11
|
+
|
|
12
|
+
this.storages = storages
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public invoke (name: string, method: keyof Storage, ...args: any[]): any {
|
|
16
|
+
if (!(name in this.storages))
|
|
17
|
+
throw new Error(`Storage '${name}' is not defined`)
|
|
18
|
+
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
20
|
+
// @ts-expect-error
|
|
21
|
+
return this.storages[name][method](...args)
|
|
22
|
+
}
|
|
23
|
+
}
|
package/source/Entry.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { decode } from 'msgpackr'
|
|
2
|
+
import { type ProviderClass, providers } from './providers'
|
|
3
|
+
import { Storage, type Storages } from './Storage'
|
|
4
|
+
import { Aspect } from './Aspect'
|
|
5
|
+
|
|
6
|
+
export class Factory {
|
|
7
|
+
private readonly declaration: Record<string, string>
|
|
8
|
+
|
|
9
|
+
public constructor () {
|
|
10
|
+
const env = process.env.TOA_STORAGES
|
|
11
|
+
|
|
12
|
+
if (env === undefined)
|
|
13
|
+
throw new Error('TOA_STORAGES is not defined')
|
|
14
|
+
|
|
15
|
+
this.declaration = decode(Buffer.from(env, 'base64')) as Record<string, string>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public aspect (): Aspect {
|
|
19
|
+
const storages = this.createStorages()
|
|
20
|
+
|
|
21
|
+
return new Aspect(storages)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public createStorage (name: string, ref: string): Storage {
|
|
25
|
+
const url = new URL(ref)
|
|
26
|
+
const Provider = providers[url.protocol]
|
|
27
|
+
|
|
28
|
+
if (Provider === undefined)
|
|
29
|
+
throw new Error(`No provider found for '${url.protocol}'`)
|
|
30
|
+
|
|
31
|
+
const secrets = this.resolveSecrets(name, Provider)
|
|
32
|
+
|
|
33
|
+
const provider = new Provider(url, secrets)
|
|
34
|
+
|
|
35
|
+
return new Storage(provider)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private createStorages (): Storages {
|
|
39
|
+
const storages: Storages = {}
|
|
40
|
+
|
|
41
|
+
for (const [name, ref] of Object.entries(this.declaration))
|
|
42
|
+
storages[name] = this.createStorage(name, ref)
|
|
43
|
+
|
|
44
|
+
return storages
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private resolveSecrets (name: string, Class: ProviderClass): Record<string, string> {
|
|
48
|
+
if (Class.SECRETS === undefined)
|
|
49
|
+
return {}
|
|
50
|
+
|
|
51
|
+
const secrets: Record<string, string> = {}
|
|
52
|
+
|
|
53
|
+
for (const secret of Class.SECRETS) {
|
|
54
|
+
const variable = `TOA_STORAGES_${name.toUpperCase()}_${secret.toUpperCase()}`
|
|
55
|
+
const value = process.env[variable]
|
|
56
|
+
|
|
57
|
+
if (value === undefined)
|
|
58
|
+
throw new Error(`${variable} is not defined`)
|
|
59
|
+
|
|
60
|
+
secrets[secret] = value
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return secrets
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Readable } from 'node:stream'
|
|
2
|
+
|
|
3
|
+
export interface Provider {
|
|
4
|
+
get: (path: string) => Promise<Readable | null>
|
|
5
|
+
put: (path: string, filename: string, stream: Readable) => Promise<void>
|
|
6
|
+
delete: (path: string) => Promise<void>
|
|
7
|
+
move: (from: string, to: string) => Promise<void>
|
|
8
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { PassThrough, type TransformCallback } from 'node:stream'
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
|
|
4
|
+
export class Scanner extends PassThrough {
|
|
5
|
+
public size = 0
|
|
6
|
+
public type = 'application/octet-stream'
|
|
7
|
+
public error: Error | null = null
|
|
8
|
+
|
|
9
|
+
private readonly hash = createHash('md5')
|
|
10
|
+
private readonly assertion: string | undefined
|
|
11
|
+
private position = 0
|
|
12
|
+
private detected = false
|
|
13
|
+
private readonly chunks: Buffer[] = []
|
|
14
|
+
|
|
15
|
+
public constructor (assertion?: string) {
|
|
16
|
+
super()
|
|
17
|
+
|
|
18
|
+
this.assertion = assertion
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public digest (): string {
|
|
22
|
+
return this.hash.digest('hex')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public override _transform
|
|
26
|
+
(buffer: Buffer, encoding: BufferEncoding, callback: TransformCallback): void {
|
|
27
|
+
super._transform(buffer, encoding, callback)
|
|
28
|
+
|
|
29
|
+
this.process(buffer)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private readonly process = (buffer: Buffer): void => {
|
|
33
|
+
this.size += buffer.length
|
|
34
|
+
this.hash.update(buffer)
|
|
35
|
+
|
|
36
|
+
if (this.detected)
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
if (this.position + buffer.length > HEADER_SIZE)
|
|
40
|
+
buffer = buffer.subarray(0, HEADER_SIZE - this.position)
|
|
41
|
+
|
|
42
|
+
this.chunks.push(buffer)
|
|
43
|
+
this.position += buffer.length
|
|
44
|
+
|
|
45
|
+
if (this.position === HEADER_SIZE)
|
|
46
|
+
this.complete()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private complete (): void {
|
|
50
|
+
this.detected = true
|
|
51
|
+
|
|
52
|
+
const header = Buffer.concat(this.chunks).toString('hex')
|
|
53
|
+
|
|
54
|
+
const signature = SIGNATURES
|
|
55
|
+
.find(({ hex, off }) => header.slice(off, off + hex.length) === hex)
|
|
56
|
+
|
|
57
|
+
this.verify(signature)
|
|
58
|
+
|
|
59
|
+
const value = signature?.type ?? this.assertion
|
|
60
|
+
|
|
61
|
+
if (value !== undefined)
|
|
62
|
+
this.type = value
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private verify (signature: Signature | undefined): void {
|
|
66
|
+
if (this.assertion === undefined || this.assertion === 'application/octet-stream')
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
const mismatch = signature === undefined
|
|
70
|
+
? KNOWN_TYPES.has(this.assertion)
|
|
71
|
+
: this.assertion !== signature.type
|
|
72
|
+
|
|
73
|
+
if (mismatch) {
|
|
74
|
+
this.error = ERR_TYPE_MISMATCH
|
|
75
|
+
this.end()
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// https://en.wikipedia.org/wiki/List_of_file_signatures
|
|
81
|
+
const SIGNATURES: Signature[] = [
|
|
82
|
+
{ hex: 'ffd8ffe0', off: 0, type: 'image/jpeg' },
|
|
83
|
+
{ hex: 'ffd8ffe1', off: 0, type: 'image/jpeg' },
|
|
84
|
+
{ hex: 'ffd8ffee', off: 0, type: 'image/jpeg' },
|
|
85
|
+
{ hex: 'ffd8ffdb', off: 0, type: 'image/jpeg' },
|
|
86
|
+
{ hex: '89504e47', off: 0, type: 'image/png' },
|
|
87
|
+
{ hex: '47494638', off: 0, type: 'image/gif' },
|
|
88
|
+
{ hex: '52494646', off: 0, type: 'image/webp' },
|
|
89
|
+
{ hex: '4a584c200d0a870a', off: 8, type: 'image/jxl' },
|
|
90
|
+
{ hex: '6674797068656963', off: 8, type: 'image/heic' },
|
|
91
|
+
{ hex: '6674797061766966', off: 8, type: 'image/avif' }
|
|
92
|
+
/*
|
|
93
|
+
When adding a new signature, include a copyright-free sample file in the `.tests` directory
|
|
94
|
+
and update the 'signatures' test group in `Storage.test.ts`.
|
|
95
|
+
*/
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
const HEADER_SIZE = SIGNATURES
|
|
99
|
+
.reduce((max, { off, hex }) => Math.max(max, off + hex.length), 0) / 2
|
|
100
|
+
|
|
101
|
+
const KNOWN_TYPES = new Set(SIGNATURES.map(({ type }) => type))
|
|
102
|
+
|
|
103
|
+
const ERR_TYPE_MISMATCH = new Error('TYPE_MISMATCH')
|
|
104
|
+
|
|
105
|
+
interface Signature {
|
|
106
|
+
hex: string
|
|
107
|
+
off: number
|
|
108
|
+
type: string
|
|
109
|
+
}
|