@toa.io/extensions.exposition 1.0.0-alpha.60 → 1.0.0-alpha.62
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/components/octets.storage/manifest.toa.yaml +1 -0
- package/components/octets.storage/operations/get.js +2 -2
- package/components/octets.storage/operations/store.js +31 -23
- package/documentation/octets.md +21 -4
- package/features/cors.feature +1 -1
- package/features/methods.feature +47 -0
- package/features/octets.download.feature +73 -1
- package/features/octets.feature +34 -0
- package/features/steps/Parameters.ts +1 -1
- package/package.json +5 -5
- package/schemas/method.cos.yaml +1 -1
- package/schemas/octets/store.cos.yaml +25 -4
- package/source/HTTP/Server.ts +1 -1
- package/source/HTTP/exceptions.ts +6 -0
- package/source/RTD/syntax/types.ts +1 -1
- package/source/directives/cors/CORS.ts +1 -1
- package/source/directives/io/Input.ts +2 -2
- package/source/directives/io/Output.ts +1 -1
- package/source/directives/octets/Context.ts +3 -2
- package/source/directives/octets/Fetch.ts +1 -1
- package/source/directives/octets/Store.ts +13 -4
- package/source/directives/octets/bytes.test.ts +30 -0
- package/source/directives/octets/bytes.ts +18 -0
- package/source/directives/octets/schemas.ts +0 -2
- package/transpiled/HTTP/Server.js +1 -1
- package/transpiled/HTTP/Server.js.map +1 -1
- package/transpiled/HTTP/exceptions.d.ts +3 -0
- package/transpiled/HTTP/exceptions.js +7 -1
- package/transpiled/HTTP/exceptions.js.map +1 -1
- package/transpiled/RTD/syntax/types.js +1 -1
- package/transpiled/RTD/syntax/types.js.map +1 -1
- package/transpiled/directives/cors/CORS.js +1 -1
- package/transpiled/directives/cors/CORS.js.map +1 -1
- package/transpiled/directives/io/Input.js.map +1 -1
- package/transpiled/directives/io/Output.js.map +1 -1
- package/transpiled/directives/octets/Context.js +4 -24
- package/transpiled/directives/octets/Context.js.map +1 -1
- package/transpiled/directives/octets/Fetch.js +1 -1
- package/transpiled/directives/octets/Fetch.js.map +1 -1
- package/transpiled/directives/octets/Store.d.ts +3 -0
- package/transpiled/directives/octets/Store.js +9 -3
- package/transpiled/directives/octets/Store.js.map +1 -1
- package/transpiled/directives/octets/bytes.d.ts +1 -0
- package/transpiled/directives/octets/bytes.js +21 -0
- package/transpiled/directives/octets/bytes.js.map +1 -0
- package/transpiled/directives/octets/schemas.d.ts +0 -2
- package/transpiled/directives/octets/schemas.js +1 -3
- package/transpiled/directives/octets/schemas.js.map +1 -1
- package/transpiled/tsconfig.tsbuildinfo +1 -1
- package/schemas/octets/context.cos.yaml +0 -1
|
@@ -5,16 +5,38 @@ const { Err } = require('error-value')
|
|
|
5
5
|
const { match } = require('matchacho')
|
|
6
6
|
|
|
7
7
|
async function store (input, context) {
|
|
8
|
-
const { storage, request, accept, trust } = input
|
|
8
|
+
const { storage, request, accept, limit, trust } = input
|
|
9
9
|
const path = request.url
|
|
10
10
|
const claim = request.headers['content-type']
|
|
11
11
|
const meta = parseMeta(request.headers['content-meta'])
|
|
12
|
-
const
|
|
12
|
+
const location = request.headers['content-location']
|
|
13
|
+
|
|
14
|
+
/** @type {Readable} */
|
|
15
|
+
let body = request
|
|
16
|
+
|
|
17
|
+
const options = { claim, accept, meta }
|
|
18
|
+
|
|
19
|
+
if (location !== undefined) {
|
|
20
|
+
const length = Number.parseInt(request.headers['content-length'])
|
|
21
|
+
|
|
22
|
+
if (length !== 0)
|
|
23
|
+
return ERR_LENGTH
|
|
24
|
+
|
|
25
|
+
if (!trusted(location, trust))
|
|
26
|
+
return ERR_UNTRUSTED
|
|
27
|
+
|
|
28
|
+
body = await download(location)
|
|
29
|
+
|
|
30
|
+
if (body instanceof Error)
|
|
31
|
+
return body
|
|
32
|
+
|
|
33
|
+
options.origin = location
|
|
34
|
+
}
|
|
13
35
|
|
|
14
|
-
if (
|
|
15
|
-
|
|
36
|
+
if (limit !== undefined)
|
|
37
|
+
options.limit = limit
|
|
16
38
|
|
|
17
|
-
return context.storages[storage].put(path, body,
|
|
39
|
+
return context.storages[storage].put(path, body, options)
|
|
18
40
|
}
|
|
19
41
|
|
|
20
42
|
/**
|
|
@@ -41,25 +63,10 @@ function parseMeta (values) {
|
|
|
41
63
|
}
|
|
42
64
|
|
|
43
65
|
/**
|
|
44
|
-
* @param {
|
|
45
|
-
* @
|
|
46
|
-
* @return {import('node:stream').Readable | Error}
|
|
66
|
+
* @param {string} location
|
|
67
|
+
* @return {Readable | Error}
|
|
47
68
|
*/
|
|
48
|
-
async function download (
|
|
49
|
-
/** @type {string | undefined} */
|
|
50
|
-
const location = request.headers['content-location']
|
|
51
|
-
|
|
52
|
-
if (location === undefined)
|
|
53
|
-
return request
|
|
54
|
-
|
|
55
|
-
const length = Number.parseInt(request.headers['content-length'])
|
|
56
|
-
|
|
57
|
-
if (length !== 0)
|
|
58
|
-
return ERR_LENGTH
|
|
59
|
-
|
|
60
|
-
if (!trusted(location, trust))
|
|
61
|
-
return ERR_UNTRUSTED
|
|
62
|
-
|
|
69
|
+
async function download (location) {
|
|
63
70
|
const response = await fetch(location)
|
|
64
71
|
|
|
65
72
|
if (!response.ok)
|
|
@@ -111,3 +118,4 @@ const ERR_UNAVAILABLE = Err('LOCATION_UNAVAILABLE', 'Location is not available')
|
|
|
111
118
|
exports.effect = store
|
|
112
119
|
|
|
113
120
|
/** @typedef {Array<string | RegExp>} Trust */
|
|
121
|
+
/** @typedef {import('node:stream').Readable} Readable */
|
package/documentation/octets.md
CHANGED
|
@@ -25,10 +25,7 @@ the request is rejected with a `415 Unsupported Media Type` response.
|
|
|
25
25
|
|
|
26
26
|
The value of the directive is `null` or an object with the following properties:
|
|
27
27
|
|
|
28
|
-
- `limit`:
|
|
29
|
-
a [string with units](https://www.npmjs.com/package/bytes#bytesparsestringnumber-value-numbernull))
|
|
30
|
-
to limit the size of the uploaded content
|
|
31
|
-
(default is 64MB, which should be enough for everyone ©).
|
|
28
|
+
- `limit`: [maximum size](#stream-size-limit) of the incoming stream.
|
|
32
29
|
- `accept`: a media type or an array of media types that are acceptable.
|
|
33
30
|
If the `accept` property is not specified, any media type is acceptable (which is the default).
|
|
34
31
|
- `workflow`: [workflow](#workflows) to be executed once the content is successfully stored.
|
|
@@ -70,6 +67,23 @@ meta:
|
|
|
70
67
|
|
|
71
68
|
If the Entry already exists, the `content-meta` header is ignored.
|
|
72
69
|
|
|
70
|
+
### Stream size limit
|
|
71
|
+
|
|
72
|
+
The `limit` property can be used to set the maximum size of the incoming stream in bytes.
|
|
73
|
+
|
|
74
|
+
The property value can be specified as a number
|
|
75
|
+
(representing bytes) or a string that combines a number with a unit (e.g., `1MB`).
|
|
76
|
+
Both [binary and decimal prefixes](https://en.wikipedia.org/wiki/Binary_prefix) are supported.
|
|
77
|
+
If the prefix or unit is specified _incorrectly_ (e.g., `1mb`),
|
|
78
|
+
it will default to a binary prefix interpretation.
|
|
79
|
+
|
|
80
|
+
- `1b`, `1B`: 1 byte
|
|
81
|
+
- `1KB`: 1000 bytes
|
|
82
|
+
- `1KiB`: 1024 bytes
|
|
83
|
+
- `1kb`: 1024 bytes
|
|
84
|
+
|
|
85
|
+
The default value is `64MiB`.
|
|
86
|
+
|
|
73
87
|
### Downloading external content
|
|
74
88
|
|
|
75
89
|
The `octets:store` directive can be used to download external content:
|
|
@@ -85,6 +99,9 @@ Requests with `content-location` header must have an empty body (`content-length
|
|
|
85
99
|
Target origin must be allowed by the `trust` property,
|
|
86
100
|
which can contain a list of trusted origins or regular expressions to match the full URL.
|
|
87
101
|
|
|
102
|
+
URL of the downloaded content is stored in the `origin` property of
|
|
103
|
+
the [Entry](/extensions/storages/readme.md#entry).
|
|
104
|
+
|
|
88
105
|
```yaml
|
|
89
106
|
/images:
|
|
90
107
|
octets:context: images
|
package/features/cors.feature
CHANGED
|
@@ -20,7 +20,7 @@ Feature: CORS Support
|
|
|
20
20
|
"""
|
|
21
21
|
204 No Content
|
|
22
22
|
access-control-allow-origin: https://hello.world
|
|
23
|
-
access-control-allow-methods: GET, POST, PUT, PATCH, DELETE
|
|
23
|
+
access-control-allow-methods: GET, POST, PUT, PATCH, DELETE, LOCK, UNLOCK
|
|
24
24
|
access-control-allow-headers: accept, authorization, content-type, etag, if-match, if-none-match
|
|
25
25
|
access-control-allow-credentials: true
|
|
26
26
|
access-control-max-age: 3600
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
Feature: Supported methods
|
|
2
|
+
|
|
3
|
+
Scenario Outline: <method> is supported
|
|
4
|
+
Given the annotation:
|
|
5
|
+
"""yaml
|
|
6
|
+
/:
|
|
7
|
+
<method>:
|
|
8
|
+
dev:stub:
|
|
9
|
+
hello: world
|
|
10
|
+
"""
|
|
11
|
+
And the `greeter` is running with the following manifest:
|
|
12
|
+
"""yaml
|
|
13
|
+
exposition:
|
|
14
|
+
/:
|
|
15
|
+
<method>:
|
|
16
|
+
dev:stub:
|
|
17
|
+
hello: world
|
|
18
|
+
"""
|
|
19
|
+
Examples:
|
|
20
|
+
| method |
|
|
21
|
+
| GET |
|
|
22
|
+
| POST |
|
|
23
|
+
| PUT |
|
|
24
|
+
| DELETE |
|
|
25
|
+
| PATCH |
|
|
26
|
+
| LOCK |
|
|
27
|
+
| UNLOCK |
|
|
28
|
+
|
|
29
|
+
Scenario: CORS allowed methods
|
|
30
|
+
Given the annotation:
|
|
31
|
+
"""yaml
|
|
32
|
+
/:
|
|
33
|
+
GET:
|
|
34
|
+
dev:stub:
|
|
35
|
+
hello: world
|
|
36
|
+
"""
|
|
37
|
+
When the following request is received:
|
|
38
|
+
"""
|
|
39
|
+
OPTIONS / HTTP/1.1
|
|
40
|
+
host: nex.toa.io
|
|
41
|
+
origin: https://hello.world
|
|
42
|
+
"""
|
|
43
|
+
Then the following reply is sent:
|
|
44
|
+
"""
|
|
45
|
+
204 No Content
|
|
46
|
+
access-control-allow-methods: GET, POST, PUT, PATCH, DELETE, LOCK, UNLOCK
|
|
47
|
+
"""
|
|
@@ -14,7 +14,8 @@ Feature: Download and store
|
|
|
14
14
|
- https://github.com
|
|
15
15
|
/*:
|
|
16
16
|
GET:
|
|
17
|
-
octets:fetch:
|
|
17
|
+
octets:fetch:
|
|
18
|
+
meta: true
|
|
18
19
|
"""
|
|
19
20
|
|
|
20
21
|
When the following request is received:
|
|
@@ -43,6 +44,24 @@ Feature: Download and store
|
|
|
43
44
|
200 OK
|
|
44
45
|
content-type: image/png
|
|
45
46
|
content-length: 1288
|
|
47
|
+
etag: "${{ id }}"
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
# origin is stored in the entry
|
|
51
|
+
When the following request is received:
|
|
52
|
+
"""
|
|
53
|
+
GET /${{ id }} HTTP/1.1
|
|
54
|
+
host: nex.toa.io
|
|
55
|
+
accept: application/vnd.toa.octets.entry+yaml
|
|
56
|
+
"""
|
|
57
|
+
Then the following reply is sent:
|
|
58
|
+
"""
|
|
59
|
+
200 OK
|
|
60
|
+
content-type: application/yaml
|
|
61
|
+
|
|
62
|
+
id: ${{ id }}
|
|
63
|
+
type: image/png
|
|
64
|
+
origin: https://avatars.githubusercontent.com/u/92763022?s=48&v=4
|
|
46
65
|
"""
|
|
47
66
|
|
|
48
67
|
# untrusted location
|
|
@@ -115,3 +134,56 @@ Feature: Download and store
|
|
|
115
134
|
| type | location |
|
|
116
135
|
| origin | https://avatars.githubusercontent.com |
|
|
117
136
|
| expression | /^https://avatars\.githubusercontent\.com/ |
|
|
137
|
+
|
|
138
|
+
Scenario: Download size limit
|
|
139
|
+
Given the annotation:
|
|
140
|
+
"""yaml
|
|
141
|
+
/:
|
|
142
|
+
io:output: true
|
|
143
|
+
auth:anonymous: true
|
|
144
|
+
octets:context: octets
|
|
145
|
+
POST:
|
|
146
|
+
octets:store:
|
|
147
|
+
limit: 1kb
|
|
148
|
+
trust:
|
|
149
|
+
- https://avatars.githubusercontent.com
|
|
150
|
+
"""
|
|
151
|
+
When the following request is received:
|
|
152
|
+
"""
|
|
153
|
+
POST / HTTP/1.1
|
|
154
|
+
host: nex.toa.io
|
|
155
|
+
content-location: https://avatars.githubusercontent.com/u/92763022?s=48&v=4
|
|
156
|
+
content-length: 0
|
|
157
|
+
accept: text/plain
|
|
158
|
+
"""
|
|
159
|
+
Then the following reply is sent:
|
|
160
|
+
"""
|
|
161
|
+
413 Request Entity Too Large
|
|
162
|
+
|
|
163
|
+
Size limit is 1kb
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
Scenario: Allow `content-location` request header
|
|
167
|
+
Given the annotation:
|
|
168
|
+
"""yaml
|
|
169
|
+
/:
|
|
170
|
+
io:output: true
|
|
171
|
+
auth:anonymous: true
|
|
172
|
+
octets:context: octets
|
|
173
|
+
POST:
|
|
174
|
+
octets:store:
|
|
175
|
+
limit: 1kb
|
|
176
|
+
trust:
|
|
177
|
+
- https://avatars.githubusercontent.com
|
|
178
|
+
"""
|
|
179
|
+
When the following request is received:
|
|
180
|
+
"""
|
|
181
|
+
OPTIONS / HTTP/1.1
|
|
182
|
+
host: nex.toa.io
|
|
183
|
+
origin: https://hello.world
|
|
184
|
+
"""
|
|
185
|
+
Then the following reply is sent:
|
|
186
|
+
"""
|
|
187
|
+
204 No Content
|
|
188
|
+
access-control-allow-headers: accept, authorization, content-type, etag, if-match, if-none-match, content-meta, content-location
|
|
189
|
+
"""
|
package/features/octets.feature
CHANGED
|
@@ -34,6 +34,14 @@ Feature: Octets directive family
|
|
|
34
34
|
/*:
|
|
35
35
|
GET:
|
|
36
36
|
octets:fetch: ~
|
|
37
|
+
/limit-1kb:
|
|
38
|
+
POST:
|
|
39
|
+
octets:store:
|
|
40
|
+
limit: 1kb
|
|
41
|
+
/limit-100kb:
|
|
42
|
+
POST:
|
|
43
|
+
octets:store:
|
|
44
|
+
limit: 100kb
|
|
37
45
|
"""
|
|
38
46
|
|
|
39
47
|
Scenario: Basic storage operations
|
|
@@ -146,6 +154,32 @@ Feature: Octets directive family
|
|
|
146
154
|
201 Created
|
|
147
155
|
"""
|
|
148
156
|
|
|
157
|
+
Scenario: Size limit
|
|
158
|
+
When the stream of `albert.jpg` is received with the following headers:
|
|
159
|
+
"""
|
|
160
|
+
POST /limit-1kb/ HTTP/1.1
|
|
161
|
+
host: nex.toa.io
|
|
162
|
+
content-type: image/jpeg
|
|
163
|
+
accept: text/plain
|
|
164
|
+
"""
|
|
165
|
+
Then the following reply is sent:
|
|
166
|
+
"""
|
|
167
|
+
413 Request Entity Too Large
|
|
168
|
+
|
|
169
|
+
Size limit is 1kb
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
When the stream of `albert.jpg` is received with the following headers:
|
|
173
|
+
"""
|
|
174
|
+
POST /limit-100kb/ HTTP/1.1
|
|
175
|
+
host: nex.toa.io
|
|
176
|
+
content-type: image/jpeg
|
|
177
|
+
"""
|
|
178
|
+
Then the following reply is sent:
|
|
179
|
+
"""
|
|
180
|
+
201 Created
|
|
181
|
+
"""
|
|
182
|
+
|
|
149
183
|
Scenario Outline: Detecting `<type>`
|
|
150
184
|
When the stream of `sample.<ext>` is received with the following headers:
|
|
151
185
|
"""
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toa.io/extensions.exposition",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.62",
|
|
4
4
|
"description": "Toa Exposition",
|
|
5
5
|
"author": "temich <tema.gurtovoy@gmail.com>",
|
|
6
6
|
"homepage": "https://github.com/toa-io/toa#readme",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@toa.io/core": "1.0.0-alpha.59",
|
|
21
21
|
"@toa.io/generic": "1.0.0-alpha.59",
|
|
22
|
-
"@toa.io/schemas": "1.0.0-alpha.
|
|
22
|
+
"@toa.io/schemas": "1.0.0-alpha.61",
|
|
23
23
|
"bcryptjs": "2.4.3",
|
|
24
24
|
"error-value": "0.3.0",
|
|
25
25
|
"js-yaml": "4.1.0",
|
|
@@ -50,12 +50,12 @@
|
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@swc/core": "1.6.6",
|
|
52
52
|
"@swc/helpers": "0.5.11",
|
|
53
|
-
"@toa.io/agent": "1.0.0-alpha.
|
|
54
|
-
"@toa.io/extensions.storages": "1.0.0-alpha.
|
|
53
|
+
"@toa.io/agent": "1.0.0-alpha.62",
|
|
54
|
+
"@toa.io/extensions.storages": "1.0.0-alpha.62",
|
|
55
55
|
"@types/bcryptjs": "2.4.3",
|
|
56
56
|
"@types/cors": "2.8.13",
|
|
57
57
|
"@types/negotiator": "0.6.1",
|
|
58
58
|
"jest-esbuild": "0.3.0"
|
|
59
59
|
},
|
|
60
|
-
"gitHead": "
|
|
60
|
+
"gitHead": "0ddfa7893b630214d50334264a2220b3bb6b455e"
|
|
61
61
|
}
|
package/schemas/method.cos.yaml
CHANGED
|
@@ -1,4 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
type: object
|
|
2
|
+
nullable: true
|
|
3
|
+
properties:
|
|
4
|
+
accept:
|
|
5
|
+
anyOf:
|
|
6
|
+
- type: string
|
|
7
|
+
- type: array
|
|
8
|
+
items:
|
|
9
|
+
type: string
|
|
10
|
+
limit:
|
|
11
|
+
type: string
|
|
12
|
+
pattern: ^(\d+(\.\d+)?)([kmgtKMBGT]i?)?[bB]?$
|
|
13
|
+
trust:
|
|
14
|
+
type: array
|
|
15
|
+
items:
|
|
16
|
+
type: string
|
|
17
|
+
workflow:
|
|
18
|
+
anyOf:
|
|
19
|
+
- &unit
|
|
20
|
+
type: object
|
|
21
|
+
patternProperties:
|
|
22
|
+
^.+$:
|
|
23
|
+
type: string
|
|
24
|
+
- type: array
|
|
25
|
+
items: *unit
|
package/source/HTTP/Server.ts
CHANGED
|
@@ -142,7 +142,7 @@ export const PORT = 8000
|
|
|
142
142
|
export const DELAY = 3 // seconds
|
|
143
143
|
|
|
144
144
|
const DEFAULTS: Omit<Properties, 'authorities'> = {
|
|
145
|
-
methods: new Set<string>(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', '
|
|
145
|
+
methods: new Set<string>(['OPTIONS', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'LOCK', 'UNLOCK']),
|
|
146
146
|
debug: false,
|
|
147
147
|
trace: false,
|
|
148
148
|
port: PORT,
|
|
@@ -14,7 +14,7 @@ export class CORS implements Interceptor {
|
|
|
14
14
|
])
|
|
15
15
|
|
|
16
16
|
private readonly headers = new Headers({
|
|
17
|
-
'access-control-allow-methods': 'GET, POST, PUT, PATCH, DELETE',
|
|
17
|
+
'access-control-allow-methods': 'GET, POST, PUT, PATCH, DELETE, LOCK, UNLOCK',
|
|
18
18
|
'access-control-allow-credentials': 'true',
|
|
19
19
|
'access-control-allow-headers': Array.from(this.requestHeaders).join(', '),
|
|
20
20
|
'access-control-max-age': '3600',
|
|
@@ -12,7 +12,7 @@ export class Input implements Directive {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
public static validate (permissions: unknown): asserts permissions is Permissions {
|
|
15
|
-
schemas.input.validate(permissions, 'Incorrect \'io:input\' format')
|
|
15
|
+
schemas.input.validate<Permissions>(permissions, 'Incorrect \'io:input\' format')
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
public attach (context: Context): void {
|
|
@@ -21,7 +21,7 @@ export class Input implements Directive {
|
|
|
21
21
|
|
|
22
22
|
private check (body: unknown): unknown {
|
|
23
23
|
try {
|
|
24
|
-
schemas.message.validate(body)
|
|
24
|
+
schemas.message.validate<Message | Message[]>(body)
|
|
25
25
|
} catch {
|
|
26
26
|
throw new BadRequest('Invalid request body')
|
|
27
27
|
}
|
|
@@ -48,7 +48,7 @@ export class Output implements Directive {
|
|
|
48
48
|
return
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
schemas.message.validate(message.body,
|
|
51
|
+
schemas.message.validate<Message>(message.body,
|
|
52
52
|
'\'io:output\' expects response to be an object or array of objects')
|
|
53
53
|
|
|
54
54
|
if (Array.isArray(message.body))
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import assert from 'node:assert'
|
|
2
2
|
import { Directive } from './Directive'
|
|
3
3
|
import type { Output } from '../../io'
|
|
4
4
|
|
|
@@ -8,7 +8,8 @@ export class Context extends Directive {
|
|
|
8
8
|
|
|
9
9
|
public constructor (value: unknown) {
|
|
10
10
|
super()
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
assert.ok(typeof value === 'string', 'Directive \'octets:context\' must must be a string')
|
|
12
13
|
|
|
13
14
|
this.storage = value
|
|
14
15
|
}
|
|
@@ -5,6 +5,7 @@ import { cors } from '../cors'
|
|
|
5
5
|
import * as schemas from './schemas'
|
|
6
6
|
import { Workflow } from './workflows'
|
|
7
7
|
import { Directive } from './Directive'
|
|
8
|
+
import { toBytes } from './bytes'
|
|
8
9
|
import type { Readable } from 'stream'
|
|
9
10
|
import type { Parameter } from '../../RTD'
|
|
10
11
|
import type { Unit } from './workflows'
|
|
@@ -19,6 +20,8 @@ export class Store extends Directive {
|
|
|
19
20
|
public readonly targeted = false
|
|
20
21
|
|
|
21
22
|
private readonly accept?: string
|
|
23
|
+
private readonly limit: number
|
|
24
|
+
private readonly limitString: string
|
|
22
25
|
private readonly trust?: Array<string | RegExp>
|
|
23
26
|
private readonly workflow?: Workflow
|
|
24
27
|
private readonly discovery: Record<string, Promise<Component>> = {}
|
|
@@ -27,7 +30,8 @@ export class Store extends Directive {
|
|
|
27
30
|
public constructor
|
|
28
31
|
(options: Options | null, discovery: Promise<Component>, remotes: Remotes) {
|
|
29
32
|
super()
|
|
30
|
-
|
|
33
|
+
|
|
34
|
+
schemas.store.validate<Options>(options)
|
|
31
35
|
|
|
32
36
|
this.accept = match(options?.accept,
|
|
33
37
|
String, (value: string) => value,
|
|
@@ -41,9 +45,12 @@ export class Store extends Directive {
|
|
|
41
45
|
this.trust = options.trust.map((value: string) =>
|
|
42
46
|
value.startsWith('/') ? new RegExp(value.slice(1, -1)) : value)
|
|
43
47
|
|
|
48
|
+
this.limitString = options?.limit ?? '64MiB'
|
|
49
|
+
this.limit = toBytes(this.limitString)
|
|
44
50
|
this.discovery.storage = discovery
|
|
45
51
|
|
|
46
52
|
cors.allow('content-meta')
|
|
53
|
+
cors.allow('content-location')
|
|
47
54
|
}
|
|
48
55
|
|
|
49
56
|
public async apply (storage: string, input: Input, parameters: Parameter[]): Promise<Output> {
|
|
@@ -53,15 +60,14 @@ export class Store extends Directive {
|
|
|
53
60
|
input: {
|
|
54
61
|
storage,
|
|
55
62
|
request: input.request,
|
|
63
|
+
accept: this.accept,
|
|
64
|
+
limit: this.limit,
|
|
56
65
|
trust: this.trust
|
|
57
66
|
}
|
|
58
67
|
}
|
|
59
68
|
|
|
60
69
|
const meta = input.request.headers['content-meta']
|
|
61
70
|
|
|
62
|
-
if (this.accept !== undefined)
|
|
63
|
-
request.input.accept = this.accept
|
|
64
|
-
|
|
65
71
|
if (meta !== undefined)
|
|
66
72
|
request.input.meta = this.meta(meta)
|
|
67
73
|
|
|
@@ -97,6 +103,7 @@ export class Store extends Directive {
|
|
|
97
103
|
throw match(error.code,
|
|
98
104
|
'NOT_ACCEPTABLE', () => new http.UnsupportedMediaType(),
|
|
99
105
|
'TYPE_MISMATCH', () => new http.BadRequest(),
|
|
106
|
+
'LIMIT_EXCEEDED', () => new http.RequestEntityTooLarge(`Size limit is ${this.limitString}`),
|
|
100
107
|
'LOCATION_UNTRUSTED', () => new http.Forbidden(error.message),
|
|
101
108
|
'LOCATION_LENGTH', () => new http.BadRequest(error.message),
|
|
102
109
|
'LOCATION_UNAVAILABLE', () => new http.NotFound(error.message),
|
|
@@ -122,6 +129,7 @@ export class Store extends Directive {
|
|
|
122
129
|
|
|
123
130
|
export interface Options {
|
|
124
131
|
accept?: string | string[]
|
|
132
|
+
limit?: string
|
|
125
133
|
workflow?: Unit[] | Unit
|
|
126
134
|
trust?: string[]
|
|
127
135
|
}
|
|
@@ -131,6 +139,7 @@ interface StoreRequest {
|
|
|
131
139
|
storage: string
|
|
132
140
|
request: Input['request']
|
|
133
141
|
accept?: string
|
|
142
|
+
limit?: number
|
|
134
143
|
trust?: Array<string | RegExp>
|
|
135
144
|
meta?: Record<string, string>
|
|
136
145
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { toBytes } from './bytes'
|
|
2
|
+
|
|
3
|
+
it('should parse bytes', async () => {
|
|
4
|
+
expect(toBytes('10')).toBe(10)
|
|
5
|
+
expect(toBytes('10B')).toBe(10)
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
it('should parse binary prefix', async () => {
|
|
9
|
+
expect(toBytes('10KiB')).toBe(10240)
|
|
10
|
+
expect(toBytes('10MiB')).toBe(10485760)
|
|
11
|
+
expect(toBytes('10GiB')).toBe(10737418240)
|
|
12
|
+
expect(toBytes('10TiB')).toBe(10995116277760)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('should parse decimal prefix', async () => {
|
|
16
|
+
expect(toBytes('10kB')).toBe(10000)
|
|
17
|
+
expect(toBytes('10MB')).toBe(10000000)
|
|
18
|
+
expect(toBytes('10GB')).toBe(10000000000)
|
|
19
|
+
expect(toBytes('10TB')).toBe(10000000000000)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('should parse incorrect value as binary', async () => {
|
|
23
|
+
expect(toBytes('10b')).toBe(10)
|
|
24
|
+
expect(toBytes('10kb')).toBe(10240)
|
|
25
|
+
expect(toBytes('10kib')).toBe(10240)
|
|
26
|
+
expect(toBytes('10mb')).toBe(10485760)
|
|
27
|
+
expect(toBytes('10gb')).toBe(10737418240)
|
|
28
|
+
expect(toBytes('10tib')).toBe(10995116277760)
|
|
29
|
+
expect(toBytes('10Mb')).toBe(10485760)
|
|
30
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
2
|
+
|
|
3
|
+
export function toBytes (input: string): number {
|
|
4
|
+
const match = RX.exec(input)
|
|
5
|
+
|
|
6
|
+
assert.ok(match !== null, `Invalid bytes format: ${input}`)
|
|
7
|
+
|
|
8
|
+
const value = parseFloat(match.groups!.value)
|
|
9
|
+
const prefix = match.groups!.prefix?.[0].toLowerCase() ?? ''
|
|
10
|
+
const binary = match.groups!.binary !== undefined || match.groups!.unit === 'b'
|
|
11
|
+
const base = binary ? 1024 : 1000
|
|
12
|
+
const power = POWERS.indexOf(prefix)
|
|
13
|
+
|
|
14
|
+
return value * Math.pow(base, power)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const POWERS = ['', 'k', 'm', 'g', 't']
|
|
18
|
+
const RX = /^(?<value>(\d+)(\.\d+)?)(?<prefix>[kmgt](?<binary>i)?)?(?<unit>b)?$/i
|
|
@@ -10,10 +10,8 @@ import type { Unit } from './workflows'
|
|
|
10
10
|
const path = resolve(__dirname, '../../../schemas/octets')
|
|
11
11
|
const namespace = schemas.namespace(path)
|
|
12
12
|
|
|
13
|
-
export const context: Schema<string> = namespace.schema('context')
|
|
14
13
|
export const store: Schema<StoreOptions | null> = namespace.schema('store')
|
|
15
14
|
export const fetch: Schema<FetchOptions | null> = namespace.schema('fetch')
|
|
16
15
|
export const remove: Schema<DeleteOptions | null> = namespace.schema('delete')
|
|
17
16
|
export const list: Schema<ListOptions | null> = namespace.schema('list')
|
|
18
|
-
export const permute: Schema<null> = namespace.schema('permute')
|
|
19
17
|
export const workflow: Schema<Unit[] | Unit> = namespace.schema('workflow')
|
|
@@ -134,7 +134,7 @@ async function adam(request) {
|
|
|
134
134
|
exports.PORT = 8000;
|
|
135
135
|
exports.DELAY = 3; // seconds
|
|
136
136
|
const DEFAULTS = {
|
|
137
|
-
methods: new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', '
|
|
137
|
+
methods: new Set(['OPTIONS', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'LOCK', 'UNLOCK']),
|
|
138
138
|
debug: false,
|
|
139
139
|
trace: false,
|
|
140
140
|
port: exports.PORT,
|