@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 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>]
@@ -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
+ }
@@ -0,0 +1,14 @@
1
+ export interface Entry {
2
+ id: string
3
+ size: number
4
+ type: string
5
+ created: number
6
+ variants: Variant[]
7
+ meta: Record<string, unknown>
8
+ }
9
+
10
+ interface Variant {
11
+ name: string
12
+ size: number
13
+ type: string
14
+ }
@@ -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
+ }