@balena/pinejs 17.0.0-build-wip-large-file-uploads-8d9c57891b422476172efba74cacc2a0fea09248-1 → 17.0.0-build-v17-13acf75c74cc64a383aca6e91ba9582f40f60ea1-2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,286 +0,0 @@
1
- import type { WebResourceType as WebResource } from '@balena/sbvr-types';
2
- import { randomUUID } from 'node:crypto';
3
- import type { AnyObject } from 'pinejs-client-core';
4
- import type { WebResourceHandler } from '.';
5
- import { getWebResourceFields } from '.';
6
- import { api } from '../sbvr-api/sbvr-utils';
7
- import type { ODataRequest } from '../sbvr-api/uri-parser';
8
- import { errors, permissions, sbvrUtils } from '../server-glue/module';
9
-
10
- export interface BeginUploadPayload {
11
- filename: string;
12
- content_type: string;
13
- size: number;
14
- chunk_size: number;
15
- }
16
-
17
- type BeginUploadDbCheck = BeginUploadPayload & WebResource;
18
-
19
- export interface UploadUrl {
20
- url: string;
21
- chunkSize: number;
22
- partNumber: number;
23
- }
24
-
25
- export interface BeginUploadHandlerResponse {
26
- uploadUrls: UploadUrl[];
27
- fileKey: string;
28
- uploadId: string;
29
- }
30
-
31
- export interface PendingUpload extends BeginUploadPayload {
32
- fieldName: string;
33
- fileKey: string;
34
- uploadId: string;
35
- }
36
-
37
- export interface BeginUploadResponse {
38
- [fieldName: string]: {
39
- key: string;
40
- uploadUrls: UploadUrl[];
41
- };
42
- }
43
- export interface CommitUploadHandlerPayload {
44
- fileKey: string;
45
- uploadId: string;
46
- filename: string;
47
- multipartUploadChecksums?: AnyObject;
48
- }
49
-
50
- const MB = 1024 * 1024;
51
-
52
- export const multipartUploadHooks = (
53
- webResourceHandler: WebResourceHandler,
54
- ): sbvrUtils.Hooks => {
55
- return {
56
- POSTPARSE: async ({ req, request, tx, api: vocabularyApi }) => {
57
- if (request.odataQuery.property?.resource === 'beginUpload') {
58
- const uploadParams = parseBeginUpload(request);
59
-
60
- await vocabularyApi.post({
61
- url: request.url.substring(1).replace('beginUpload', 'canAccess'),
62
- body: { method: 'PATCH' },
63
- });
64
-
65
- // This transaction is necessary because beginUpload requests
66
- // will rollback the transaction (in order to first validate)
67
- // The metadata requested. If we don't pass any transaction
68
- // It will use the default transaction handler which will error out
69
- // on any rollback.
70
- tx = await sbvrUtils.db.transaction();
71
- req.tx = tx;
72
- request.tx = tx;
73
-
74
- request.method = 'PATCH';
75
- request.values = uploadParams;
76
- request.odataQuery.resource = request.resourceName;
77
- delete request.odataQuery.property;
78
- request.custom.isAction = 'beginUpload';
79
- } else if (request.odataQuery.property?.resource === 'commitUpload') {
80
- const commitPayload = await parseCommitUpload(request);
81
-
82
- await vocabularyApi.post({
83
- url: request.url.substring(1).replace('commitUpload', 'canAccess'),
84
- body: { method: 'PATCH' },
85
- });
86
-
87
- const webresource = await webResourceHandler.commitUpload({
88
- fileKey: commitPayload.metadata.fileKey,
89
- uploadId: commitPayload.metadata.uploadId,
90
- filename: commitPayload.metadata.filename,
91
- multipartUploadChecksums: commitPayload.additionalCommitInfo,
92
- });
93
-
94
- await api.webresource.patch({
95
- resource: 'multipart_upload',
96
- body: {
97
- status: 'completed',
98
- },
99
- options: {
100
- $filter: {
101
- uuid: commitPayload.key,
102
- },
103
- },
104
- passthrough: {
105
- req: permissions.root,
106
- tx: tx,
107
- },
108
- });
109
-
110
- request.method = 'PATCH';
111
- request.values = {
112
- [commitPayload.metadata.fieldName]: webresource,
113
- };
114
- request.odataQuery.resource = request.resourceName;
115
- delete request.odataQuery.property;
116
- request.custom.isAction = 'commitUpload';
117
- request.custom.commitUploadPayload = webresource;
118
- }
119
- },
120
- PRERESPOND: async ({ req, request, response, tx }) => {
121
- if (request.custom.isAction === 'beginUpload') {
122
- await tx.rollback();
123
-
124
- response.statusCode = 200;
125
- response.body = await beginUpload(
126
- webResourceHandler,
127
- request,
128
- req.user?.actor,
129
- );
130
- } else if (request.custom.isAction === 'commitUpload') {
131
- response.body = await webResourceHandler.onPreRespond(
132
- request.custom.commitUploadPayload,
133
- );
134
- }
135
- },
136
- };
137
- };
138
-
139
- export const beginUpload = async (
140
- webResourceHandler: WebResourceHandler,
141
- odataRequest: ODataRequest,
142
- actorId?: number,
143
- ): Promise<BeginUploadResponse> => {
144
- const payload = odataRequest.values as { [x: string]: BeginUploadPayload };
145
- const fieldName = Object.keys(payload)[0];
146
- const metadata = payload[fieldName];
147
-
148
- const { fileKey, uploadId, uploadUrls } =
149
- await webResourceHandler.beginUpload(fieldName, metadata);
150
- const uuid = randomUUID();
151
-
152
- try {
153
- await api.webresource.post({
154
- resource: 'multipart_upload',
155
- body: {
156
- uuid,
157
- resource_name: odataRequest.resourceName,
158
- field_name: fieldName,
159
- resource_id: odataRequest.affectedIds?.[0],
160
- upload_id: uploadId,
161
- file_key: fileKey,
162
- status: 'pending',
163
- filename: metadata.filename,
164
- content_type: metadata.content_type,
165
- size: metadata.size,
166
- chunk_size: metadata.chunk_size,
167
- expiry_date: Date.now() + 7 * 24 * 60 * 60 * 1000, // 7 days in ms
168
- is_created_by__actor: actorId,
169
- },
170
- passthrough: {
171
- req: permissions.root,
172
- },
173
- });
174
- } catch (err) {
175
- console.error('failed to start multipart upload', err);
176
- throw new errors.BadRequestError('Failed to start multipart upload');
177
- }
178
-
179
- return { [fieldName]: { key: uuid, uploadUrls } };
180
- };
181
-
182
- const parseBeginUpload = (request: ODataRequest) => {
183
- if (request.odataQuery.key == null) {
184
- throw new errors.BadRequestError();
185
- }
186
-
187
- const fieldNames = Object.keys(request.values);
188
- if (fieldNames.length !== 1) {
189
- throw new errors.BadRequestError(
190
- 'You can only get upload url for one field at a time',
191
- );
192
- }
193
-
194
- const [fieldName] = fieldNames;
195
- const webResourceFields = getWebResourceFields(request, false);
196
- if (!webResourceFields.includes(fieldName)) {
197
- throw new errors.BadRequestError(
198
- `You must provide a valid webresource field from: ${JSON.stringify(webResourceFields)}`,
199
- );
200
- }
201
-
202
- const beginUploadPayload = parseBeginUploadPayload(request.values[fieldName]);
203
- if (beginUploadPayload == null) {
204
- throw new errors.BadRequestError('Invalid file metadata');
205
- }
206
-
207
- const uploadMetadataCheck: BeginUploadDbCheck = {
208
- ...beginUploadPayload,
209
- href: 'metadata_check',
210
- };
211
-
212
- return { [fieldName]: uploadMetadataCheck };
213
- };
214
-
215
- const parseBeginUploadPayload = (
216
- payload: AnyObject,
217
- ): BeginUploadPayload | null => {
218
- if (typeof payload !== 'object') {
219
- return null;
220
- }
221
-
222
- let { filename, content_type, size, chunk_size } = payload;
223
- if (
224
- typeof filename !== 'string' ||
225
- typeof content_type !== 'string' ||
226
- typeof size !== 'number' ||
227
- (chunk_size != null && typeof chunk_size !== 'number') ||
228
- (chunk_size != null && chunk_size < 5 * MB)
229
- ) {
230
- return null;
231
- }
232
-
233
- if (chunk_size == null) {
234
- chunk_size = 5 * MB;
235
- }
236
- return { filename, content_type, size, chunk_size };
237
- };
238
-
239
- const parseCommitUpload = async (request: ODataRequest) => {
240
- if (request.odataQuery.key == null) {
241
- throw new errors.BadRequestError();
242
- }
243
-
244
- const { key, additionalCommitInfo } = request.values;
245
- if (typeof key !== 'string') {
246
- throw new errors.BadRequestError('Invalid key type');
247
- }
248
-
249
- // TODO: actor permissions
250
- const [multipartUpload] = (await api.webresource.get({
251
- resource: 'multipart_upload',
252
- options: {
253
- $select: ['id', 'file_key', 'upload_id', 'field_name', 'filename'],
254
- $filter: {
255
- uuid: key,
256
- status: 'pending',
257
- expiry_date: { $gt: { $now: {} } },
258
- },
259
- },
260
- passthrough: {
261
- req: permissions.root,
262
- tx: request.tx,
263
- },
264
- })) as [
265
- {
266
- id: number;
267
- file_key: string;
268
- upload_id: string;
269
- field_name: string;
270
- filename: string;
271
- }?,
272
- ];
273
-
274
- if (multipartUpload == null) {
275
- throw new errors.BadRequestError(`Invalid upload for key ${key}`);
276
- }
277
-
278
- const metadata = {
279
- fileKey: multipartUpload.file_key,
280
- uploadId: multipartUpload.upload_id,
281
- filename: multipartUpload.filename,
282
- fieldName: multipartUpload.field_name,
283
- };
284
-
285
- return { key, additionalCommitInfo, metadata };
286
- };
@@ -1,63 +0,0 @@
1
- Vocabulary: Auth
2
-
3
- Term: actor
4
- Term: expiry date
5
- Concept Type: Date Time (Type)
6
-
7
- Vocabulary: webresource
8
-
9
- Term: uuid
10
- Concept Type: Short Text (Type)
11
- Term: resource name
12
- Concept Type: Short Text (Type)
13
- Term: field name
14
- Concept Type: Short Text (Type)
15
- Term: resource id
16
- Concept Type: Integer (Type)
17
- Term: upload id
18
- Concept Type: Short Text (Type)
19
- Term: file key
20
- Concept Type: Short Text (Type)
21
- Term: status
22
- Concept Type: Short Text (Type)
23
- Term: filename
24
- Concept Type: Short Text (Type)
25
- Term: content type
26
- Concept Type: Short Text (Type)
27
- Term: size
28
- Concept Type: Integer (Type)
29
- Term: chunk size
30
- Concept Type: Integer (Type)
31
- Term: valid until date
32
- Concept Type: Date Time (Type)
33
-
34
- Term: multipart upload
35
- Fact type: multipart upload has uuid
36
- Necessity: each multipart upload has exactly one uuid
37
- Necessity: each uuid is of exactly one multipart upload
38
- Fact type: multipart upload has resource name
39
- Necessity: each multipart upload has exactly one resource name
40
- Fact type: multipart upload has field name
41
- Necessity: each multipart upload has exactly one field name
42
- Fact type: multipart upload has resource id
43
- Necessity: each multipart upload has exactly one resource id
44
- Fact type: multipart upload has upload id
45
- Necessity: each multipart upload has exactly one upload id
46
- Fact type: multipart upload has file key
47
- Necessity: each multipart upload has exactly one file key
48
- Fact type: multipart upload has status
49
- Necessity: each multipart upload has exactly one status
50
- Definition: "pending" or "completed" or "cancelled"
51
- Fact type: multipart upload has filename
52
- Necessity: each multipart upload has exactly one filename
53
- Fact type: multipart upload has content type
54
- Necessity: each multipart upload has exactly one content type
55
- Fact type: multipart upload has size
56
- Necessity: each multipart upload has exactly one size
57
- Fact type: multipart upload has chunk size
58
- Necessity: each multipart upload has exactly one chunk size
59
- Fact type: multipart upload has expiry date (Auth)
60
- Necessity: each multipart upload has exactly one expiry date (Auth)
61
- Fact type: multipart upload is created by actor (Auth)
62
- Necessity: each multipart upload is created by at most one actor (Auth)
63
- Reference Type: informative