@balena/pinejs 20.1.0-build-add-large-file-uploads-interfaces-1aa101d2ee3103f42cf7f7bc29d703a8958011e8-1 → 20.1.0-build-large-file-uploads-2-b83400458570d926f7a6abddeb7a149e89f52abe-2
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/.pinejs-cache.json +1 -1
- package/.versionbot/CHANGELOG.yml +2950 -2
- package/CHANGELOG.md +1091 -1
- package/out/config-loader/env.d.ts +4 -0
- package/out/config-loader/env.js +4 -0
- package/out/config-loader/env.js.map +1 -1
- package/out/sbvr-api/abstract-sql.js +19 -9
- package/out/sbvr-api/abstract-sql.js.map +1 -1
- package/out/server-glue/module.js +2 -0
- package/out/server-glue/module.js.map +1 -1
- package/out/webresource-handler/index.d.ts +10 -0
- package/out/webresource-handler/index.js +53 -18
- package/out/webresource-handler/index.js.map +1 -1
- package/out/webresource-handler/multipartUpload.d.ts +17 -0
- package/out/webresource-handler/multipartUpload.js +259 -0
- package/out/webresource-handler/multipartUpload.js.map +1 -0
- package/out/webresource-handler/webresource.d.ts +42 -0
- package/out/webresource-handler/webresource.js +2 -0
- package/out/webresource-handler/webresource.js.map +1 -0
- package/out/webresource-handler/webresource.sbvr +60 -0
- package/package.json +8 -8
- package/src/config-loader/env.ts +11 -0
- package/src/sbvr-api/abstract-sql.ts +28 -9
- package/src/server-glue/module.ts +2 -0
- package/src/webresource-handler/index.ts +93 -24
- package/src/webresource-handler/multipartUpload.ts +371 -0
- package/src/webresource-handler/webresource.sbvr +60 -0
- package/src/webresource-handler/webresource.ts +48 -0
@@ -11,9 +11,16 @@ import {
|
|
11
11
|
odataNameToSqlName,
|
12
12
|
sqlNameToODataName,
|
13
13
|
} from '@balena/odata-to-abstract-sql';
|
14
|
+
import type { ConfigLoader } from '../server-glue/module.js';
|
14
15
|
import { errors, permissions } from '../server-glue/module.js';
|
15
16
|
import type { WebResourceType as WebResource } from '@balena/sbvr-types';
|
16
17
|
import { TypedError } from 'typed-error';
|
18
|
+
import type WebresourceModel from './webresource.js';
|
19
|
+
import { importSBVR } from '../server-glue/sbvr-loader.js';
|
20
|
+
import {
|
21
|
+
isMultipartUploadAvailable,
|
22
|
+
multipartUploadHooks,
|
23
|
+
} from './multipartUpload.js';
|
17
24
|
|
18
25
|
export * from './handlers/index.js';
|
19
26
|
|
@@ -102,30 +109,27 @@ export const setupWebresourceHandler = (handler: WebResourceHandler): void => {
|
|
102
109
|
export const getWebresourceHandler = (): WebResourceHandler | undefined => {
|
103
110
|
return configuredWebResourceHandler;
|
104
111
|
};
|
112
|
+
const notValidUpload = () => Promise.resolve(false);
|
105
113
|
|
106
|
-
const
|
107
|
-
fieldname: string,
|
114
|
+
const getRequestUploadValidator = async (
|
108
115
|
req: Express.Request,
|
109
|
-
|
116
|
+
odataRequest: uriParser.ParsedODataRequest,
|
117
|
+
): Promise<(fieldName: string) => Promise<boolean>> => {
|
110
118
|
if (req.method !== 'POST' && req.method !== 'PATCH') {
|
111
|
-
return
|
119
|
+
return notValidUpload;
|
112
120
|
}
|
113
121
|
|
114
122
|
const apiRoot = getApiRoot(req);
|
115
123
|
if (apiRoot == null) {
|
116
|
-
return
|
124
|
+
return notValidUpload;
|
117
125
|
}
|
118
126
|
const model = getModel(apiRoot);
|
119
|
-
const odataRequest = uriParser.parseOData({
|
120
|
-
url: req.url,
|
121
|
-
method: req.method,
|
122
|
-
});
|
123
127
|
const sqlResourceName = sbvrUtils.resolveSynonym(odataRequest);
|
124
128
|
|
125
129
|
const table = model.abstractSql.tables[sqlResourceName];
|
126
130
|
|
127
131
|
if (table == null) {
|
128
|
-
return
|
132
|
+
return notValidUpload;
|
129
133
|
}
|
130
134
|
|
131
135
|
const permission = req.method === 'POST' ? 'create' : 'update';
|
@@ -140,20 +144,22 @@ const isFileInValidPath = async (
|
|
140
144
|
);
|
141
145
|
|
142
146
|
if (!hasPermissions) {
|
143
|
-
return
|
147
|
+
return notValidUpload;
|
144
148
|
}
|
145
149
|
|
146
|
-
|
147
|
-
|
148
|
-
(
|
149
|
-
field
|
150
|
-
|
150
|
+
return async (fieldname: string) => {
|
151
|
+
const dbFieldName = odataNameToSqlName(fieldname);
|
152
|
+
return table.fields.some(
|
153
|
+
(field) =>
|
154
|
+
field.fieldName === dbFieldName && field.dataType === 'WebResource',
|
155
|
+
);
|
156
|
+
};
|
151
157
|
};
|
152
158
|
|
153
159
|
export const getUploaderMiddlware = (
|
154
160
|
handler: WebResourceHandler,
|
155
161
|
): Express.RequestHandler => {
|
156
|
-
return (req, res, next) => {
|
162
|
+
return async (req, res, next) => {
|
157
163
|
if (!req.is('multipart')) {
|
158
164
|
next();
|
159
165
|
return;
|
@@ -164,6 +170,20 @@ export const getUploaderMiddlware = (
|
|
164
170
|
const bb = busboy({ headers: req.headers });
|
165
171
|
let isAborting = false;
|
166
172
|
|
173
|
+
const parsedOdataRequest = uriParser.parseOData({
|
174
|
+
url: req.url,
|
175
|
+
method: req.method,
|
176
|
+
});
|
177
|
+
const webResourcesFieldNames = getWebResourceFields(
|
178
|
+
parsedOdataRequest,
|
179
|
+
false,
|
180
|
+
);
|
181
|
+
|
182
|
+
const isValidUpload = await getRequestUploadValidator(
|
183
|
+
req,
|
184
|
+
parsedOdataRequest,
|
185
|
+
);
|
186
|
+
|
167
187
|
const finishFileUpload = () => {
|
168
188
|
req.unpipe(bb);
|
169
189
|
req.on('readable', req.read.bind(req));
|
@@ -191,7 +211,7 @@ export const getUploaderMiddlware = (
|
|
191
211
|
completeUploads.push(
|
192
212
|
(async () => {
|
193
213
|
try {
|
194
|
-
if (!(await
|
214
|
+
if (!(await isValidUpload(fieldname))) {
|
195
215
|
filestream.resume();
|
196
216
|
return;
|
197
217
|
}
|
@@ -213,7 +233,10 @@ export const getUploaderMiddlware = (
|
|
213
233
|
uploadedFilePaths.push(result.filename);
|
214
234
|
} catch (err: any) {
|
215
235
|
filestream.resume();
|
216
|
-
|
236
|
+
bb.emit(
|
237
|
+
'error',
|
238
|
+
new errors.BadRequestError(err.message ?? 'Error uploading file'),
|
239
|
+
);
|
217
240
|
}
|
218
241
|
})(),
|
219
242
|
);
|
@@ -223,6 +246,14 @@ export const getUploaderMiddlware = (
|
|
223
246
|
// This receives the form fields and transforms them into a standard JSON body
|
224
247
|
// This is a similar behavior as previous multer library did
|
225
248
|
bb.on('field', (name, val) => {
|
249
|
+
if (webResourcesFieldNames.includes(name)) {
|
250
|
+
isAborting = true;
|
251
|
+
bb.emit(
|
252
|
+
'error',
|
253
|
+
new errors.BadRequestError('WebResource field must be a blob.'),
|
254
|
+
);
|
255
|
+
return;
|
256
|
+
}
|
226
257
|
req.body[name] = val;
|
227
258
|
});
|
228
259
|
|
@@ -248,7 +279,7 @@ export const getUploaderMiddlware = (
|
|
248
279
|
}
|
249
280
|
});
|
250
281
|
|
251
|
-
bb.on('error', async (err) => {
|
282
|
+
bb.on('error', async (err: Error) => {
|
252
283
|
finishFileUpload();
|
253
284
|
await clearFiles();
|
254
285
|
|
@@ -260,15 +291,17 @@ export const getUploaderMiddlware = (
|
|
260
291
|
);
|
261
292
|
}
|
262
293
|
|
263
|
-
|
264
|
-
|
294
|
+
if (!sbvrUtils.handleHttpErrors(req, res, err)) {
|
295
|
+
getLogger(getApiRoot(req)).error('Error uploading file', err);
|
296
|
+
next(err);
|
297
|
+
}
|
265
298
|
});
|
266
299
|
req.pipe(bb);
|
267
300
|
};
|
268
301
|
};
|
269
302
|
|
270
|
-
const getWebResourceFields = (
|
271
|
-
request: uriParser.
|
303
|
+
export const getWebResourceFields = (
|
304
|
+
request: uriParser.ParsedODataRequest,
|
272
305
|
useTranslations = true,
|
273
306
|
): string[] => {
|
274
307
|
// Translations will use modifyFields(translated) rather than fields(original) so we need to
|
@@ -300,6 +333,9 @@ const throwIfWebresourceNotInMultipart = (
|
|
300
333
|
{ req, request }: HookArgs,
|
301
334
|
) => {
|
302
335
|
if (
|
336
|
+
request.custom.isAction !== 'beginUpload' &&
|
337
|
+
request.custom.isAction !== 'commitUpload' &&
|
338
|
+
request.custom.isAction !== 'cancelUpload' &&
|
303
339
|
!req.is?.('multipart') &&
|
304
340
|
webResourceFields.some((field) => request.values[field] != null)
|
305
341
|
) {
|
@@ -496,4 +532,37 @@ export const setupUploadHooks = (
|
|
496
532
|
resourceName,
|
497
533
|
getCreateWebResourceHooks(handler),
|
498
534
|
);
|
535
|
+
|
536
|
+
if (isMultipartUploadAvailable(handler)) {
|
537
|
+
sbvrUtils.addPureHook(
|
538
|
+
'POST',
|
539
|
+
apiRoot,
|
540
|
+
resourceName,
|
541
|
+
multipartUploadHooks(handler),
|
542
|
+
);
|
543
|
+
}
|
544
|
+
};
|
545
|
+
|
546
|
+
const initSql = `
|
547
|
+
CREATE INDEX IF NOT EXISTS idx_multipart_upload_uuid ON "multipart upload" (uuid);
|
548
|
+
CREATE INDEX IF NOT EXISTS idx_multipart_upload_status ON "multipart upload" (status);
|
549
|
+
`;
|
550
|
+
|
551
|
+
const modelText = await importSBVR('./webresource.sbvr', import.meta);
|
552
|
+
|
553
|
+
declare module '../sbvr-api/sbvr-utils.js' {
|
554
|
+
export interface API {
|
555
|
+
webresource: PinejsClient<WebresourceModel>;
|
556
|
+
}
|
557
|
+
}
|
558
|
+
|
559
|
+
export const config: ConfigLoader.Config = {
|
560
|
+
models: [
|
561
|
+
{
|
562
|
+
modelName: 'webresource',
|
563
|
+
apiRoot: 'webresource',
|
564
|
+
modelText,
|
565
|
+
initSql,
|
566
|
+
},
|
567
|
+
],
|
499
568
|
};
|
@@ -0,0 +1,371 @@
|
|
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 {
|
5
|
+
BeginMultipartUploadPayload,
|
6
|
+
UploadPart,
|
7
|
+
WebResourceHandler,
|
8
|
+
} from './index.js';
|
9
|
+
import { getWebResourceFields } from './index.js';
|
10
|
+
import type { PinejsClient } from '../sbvr-api/sbvr-utils.js';
|
11
|
+
import { api } from '../sbvr-api/sbvr-utils.js';
|
12
|
+
import type { ODataRequest } from '../sbvr-api/uri-parser.js';
|
13
|
+
import { errors, sbvrUtils } from '../server-glue/module.js';
|
14
|
+
import { webResource as webResourceEnv } from '../config-loader/env.js';
|
15
|
+
import * as permissions from '../sbvr-api/permissions.js';
|
16
|
+
|
17
|
+
type BeginUploadDbCheck = BeginMultipartUploadPayload & WebResource;
|
18
|
+
|
19
|
+
export interface PendingUpload extends BeginMultipartUploadPayload {
|
20
|
+
fieldName: string;
|
21
|
+
fileKey: string;
|
22
|
+
uploadId: string;
|
23
|
+
}
|
24
|
+
|
25
|
+
export interface BeginUploadResponse {
|
26
|
+
[fieldName: string]: {
|
27
|
+
uuid: string;
|
28
|
+
uploadParts: UploadPart[];
|
29
|
+
};
|
30
|
+
}
|
31
|
+
|
32
|
+
// Is WebResourceHandler but with beginMultipartUpload and commitMultipartUpload as not optional
|
33
|
+
type MultipartUploadHandler = WebResourceHandler &
|
34
|
+
Required<Pick<WebResourceHandler, 'multipartUpload'>>;
|
35
|
+
|
36
|
+
const MB = 1024 * 1024;
|
37
|
+
|
38
|
+
export const isMultipartUploadAvailable = (
|
39
|
+
webResourceHandler: WebResourceHandler,
|
40
|
+
): webResourceHandler is MultipartUploadHandler => {
|
41
|
+
return (
|
42
|
+
webResourceEnv.multipartUploadEnabled &&
|
43
|
+
webResourceHandler.multipartUpload != null
|
44
|
+
);
|
45
|
+
};
|
46
|
+
|
47
|
+
export const multipartUploadHooks = (
|
48
|
+
webResourceHandler: MultipartUploadHandler,
|
49
|
+
): sbvrUtils.Hooks => {
|
50
|
+
return {
|
51
|
+
POSTPARSE: async ({ req, request, tx, api: applicationApi }) => {
|
52
|
+
if (request.odataQuery.property?.resource === 'beginUpload') {
|
53
|
+
const uploadParams = await validateBeginUpload(request, applicationApi);
|
54
|
+
|
55
|
+
// This transaction is necessary because beginUpload requests
|
56
|
+
// will rollback the transaction (in order to first validate)
|
57
|
+
// The metadata requested. If we don't pass any transaction
|
58
|
+
// It will use the default transaction handler which will error out
|
59
|
+
// on any rollback.
|
60
|
+
tx = await sbvrUtils.db.transaction();
|
61
|
+
req.tx = tx;
|
62
|
+
request.tx = tx;
|
63
|
+
|
64
|
+
request.method = 'PATCH';
|
65
|
+
request.values = uploadParams;
|
66
|
+
request.odataQuery.resource = request.resourceName;
|
67
|
+
delete request.odataQuery.property;
|
68
|
+
request.custom.isAction = 'beginUpload';
|
69
|
+
} else if (request.odataQuery.property?.resource === 'commitUpload') {
|
70
|
+
const commitPayload = await validateCommitUpload(
|
71
|
+
request,
|
72
|
+
applicationApi,
|
73
|
+
);
|
74
|
+
|
75
|
+
const webresource = await webResourceHandler.multipartUpload.commit({
|
76
|
+
fileKey: commitPayload.metadata.fileKey,
|
77
|
+
uploadId: commitPayload.metadata.uploadId,
|
78
|
+
filename: commitPayload.metadata.filename,
|
79
|
+
providerCommitData: commitPayload.providerCommitData,
|
80
|
+
});
|
81
|
+
|
82
|
+
await api.webresource.patch({
|
83
|
+
resource: 'multipart_upload',
|
84
|
+
body: {
|
85
|
+
status: 'completed',
|
86
|
+
},
|
87
|
+
options: {
|
88
|
+
$filter: {
|
89
|
+
uuid: commitPayload.uuid,
|
90
|
+
},
|
91
|
+
},
|
92
|
+
passthrough: {
|
93
|
+
tx: tx,
|
94
|
+
req: permissions.root,
|
95
|
+
},
|
96
|
+
});
|
97
|
+
|
98
|
+
request.method = 'PATCH';
|
99
|
+
request.values = {
|
100
|
+
[commitPayload.metadata.fieldName]: webresource,
|
101
|
+
};
|
102
|
+
request.odataQuery.resource = request.resourceName;
|
103
|
+
delete request.odataQuery.property;
|
104
|
+
request.custom.isAction = 'commitUpload';
|
105
|
+
request.custom.commitUploadPayload = webresource;
|
106
|
+
} else if (request.odataQuery.property?.resource === 'cancelUpload') {
|
107
|
+
const { uuid, fileKey, uploadId } = await validateCancelPayload(
|
108
|
+
request,
|
109
|
+
applicationApi,
|
110
|
+
);
|
111
|
+
|
112
|
+
await webResourceHandler.multipartUpload.cancel({ fileKey, uploadId });
|
113
|
+
|
114
|
+
await api.webresource.patch({
|
115
|
+
resource: 'multipart_upload',
|
116
|
+
body: {
|
117
|
+
status: 'cancelled',
|
118
|
+
},
|
119
|
+
options: {
|
120
|
+
$filter: { uuid },
|
121
|
+
},
|
122
|
+
passthrough: {
|
123
|
+
tx: tx,
|
124
|
+
req: permissions.root,
|
125
|
+
},
|
126
|
+
});
|
127
|
+
|
128
|
+
request.method = 'GET';
|
129
|
+
request.odataQuery.resource = request.resourceName;
|
130
|
+
delete request.odataQuery.property;
|
131
|
+
request.custom.isAction = 'cancelUpload';
|
132
|
+
}
|
133
|
+
},
|
134
|
+
PRERESPOND: async ({ req, request, response, tx }) => {
|
135
|
+
if (request.custom.isAction === 'beginUpload') {
|
136
|
+
// In the case where the transaction has failed because it had invalid payload
|
137
|
+
// such as breaking a db constraint, this hook wouldn't have been called
|
138
|
+
// and would rather throw with the rule it failed to validate
|
139
|
+
// We rollback here as the patch was just a way to validate the upload payload
|
140
|
+
await tx.rollback();
|
141
|
+
|
142
|
+
response.statusCode = 200;
|
143
|
+
response.body = await beginUpload({
|
144
|
+
webResourceHandler,
|
145
|
+
odataRequest: request,
|
146
|
+
actorId: req.user?.actor,
|
147
|
+
});
|
148
|
+
} else if (request.custom.isAction === 'commitUpload') {
|
149
|
+
response.body = await webResourceHandler.onPreRespond(
|
150
|
+
request.custom.commitUploadPayload,
|
151
|
+
);
|
152
|
+
} else if (request.custom.isAction === 'cancelUpload') {
|
153
|
+
response.statusCode = 204;
|
154
|
+
delete response.body;
|
155
|
+
}
|
156
|
+
},
|
157
|
+
};
|
158
|
+
};
|
159
|
+
|
160
|
+
const beginUpload = async ({
|
161
|
+
webResourceHandler,
|
162
|
+
odataRequest,
|
163
|
+
actorId,
|
164
|
+
}: {
|
165
|
+
webResourceHandler: MultipartUploadHandler;
|
166
|
+
odataRequest: ODataRequest;
|
167
|
+
actorId?: number;
|
168
|
+
}): Promise<BeginUploadResponse> => {
|
169
|
+
const payload = odataRequest.values as {
|
170
|
+
[x: string]: BeginMultipartUploadPayload;
|
171
|
+
};
|
172
|
+
const fieldName = Object.keys(payload)[0];
|
173
|
+
const metadata = payload[fieldName];
|
174
|
+
const { fileKey, uploadId, uploadParts } =
|
175
|
+
await webResourceHandler.multipartUpload.begin(fieldName, metadata);
|
176
|
+
const uuid = randomUUID();
|
177
|
+
|
178
|
+
return await sbvrUtils.db.transaction(async (tx) => {
|
179
|
+
try {
|
180
|
+
await api.webresource.post({
|
181
|
+
resource: 'multipart_upload',
|
182
|
+
body: {
|
183
|
+
uuid,
|
184
|
+
resource_name: odataRequest.resourceName,
|
185
|
+
field_name: fieldName,
|
186
|
+
resource_id: odataRequest.affectedIds?.[0],
|
187
|
+
upload_id: uploadId,
|
188
|
+
file_key: fileKey,
|
189
|
+
status: 'pending',
|
190
|
+
filename: metadata.filename,
|
191
|
+
content_type: metadata.content_type,
|
192
|
+
size: metadata.size,
|
193
|
+
chunk_size: metadata.chunk_size,
|
194
|
+
expiry_date: Date.now() + 7 * 24 * 60 * 60 * 1000, // 7 days in ms
|
195
|
+
is_created_by__actor: actorId,
|
196
|
+
},
|
197
|
+
passthrough: {
|
198
|
+
req: permissions.root,
|
199
|
+
tx,
|
200
|
+
},
|
201
|
+
});
|
202
|
+
return { [fieldName]: { uuid, uploadParts } };
|
203
|
+
} catch (err) {
|
204
|
+
console.error('failed to start multipart upload', err);
|
205
|
+
throw new errors.BadRequestError('Failed to start multipart upload');
|
206
|
+
}
|
207
|
+
});
|
208
|
+
};
|
209
|
+
|
210
|
+
const validateBeginUpload = async (
|
211
|
+
request: ODataRequest,
|
212
|
+
applicationApi: PinejsClient,
|
213
|
+
) => {
|
214
|
+
if (request.odataQuery.key == null) {
|
215
|
+
throw new errors.BadRequestError();
|
216
|
+
}
|
217
|
+
|
218
|
+
await applicationApi.request({
|
219
|
+
method: 'POST',
|
220
|
+
url: request.url.substring(1).replace('beginUpload', 'canAccess'),
|
221
|
+
body: { method: 'PATCH' },
|
222
|
+
});
|
223
|
+
|
224
|
+
const fieldNames = Object.keys(request.values);
|
225
|
+
if (fieldNames.length !== 1) {
|
226
|
+
throw new errors.BadRequestError(
|
227
|
+
'You can only get upload url for one field at a time',
|
228
|
+
);
|
229
|
+
}
|
230
|
+
|
231
|
+
const [fieldName] = fieldNames;
|
232
|
+
const webResourceFields = getWebResourceFields(request, false);
|
233
|
+
if (!webResourceFields.includes(fieldName)) {
|
234
|
+
throw new errors.BadRequestError(
|
235
|
+
`You must provide a valid webresource field from: ${JSON.stringify(webResourceFields)}`,
|
236
|
+
);
|
237
|
+
}
|
238
|
+
|
239
|
+
const beginUploadPayload = parseBeginUploadPayload(request.values[fieldName]);
|
240
|
+
if (beginUploadPayload == null) {
|
241
|
+
throw new errors.BadRequestError('Invalid file metadata');
|
242
|
+
}
|
243
|
+
|
244
|
+
const uploadMetadataCheck: BeginUploadDbCheck = {
|
245
|
+
...beginUploadPayload,
|
246
|
+
// This is "probe" request. We don't actually store anything on the application table yet
|
247
|
+
// We just avoid creating the application record if it would fail anyway for some other db constraint
|
248
|
+
href: 'metadata_check_probe',
|
249
|
+
};
|
250
|
+
|
251
|
+
return { [fieldName]: uploadMetadataCheck };
|
252
|
+
};
|
253
|
+
|
254
|
+
const parseBeginUploadPayload = (
|
255
|
+
payload: AnyObject,
|
256
|
+
): BeginMultipartUploadPayload | null => {
|
257
|
+
if (typeof payload !== 'object') {
|
258
|
+
return null;
|
259
|
+
}
|
260
|
+
|
261
|
+
let { filename, content_type, size, chunk_size } = payload;
|
262
|
+
if (
|
263
|
+
typeof filename !== 'string' ||
|
264
|
+
typeof content_type !== 'string' ||
|
265
|
+
typeof size !== 'number' ||
|
266
|
+
(chunk_size != null && typeof chunk_size !== 'number') ||
|
267
|
+
(chunk_size != null && chunk_size < 5 * MB)
|
268
|
+
) {
|
269
|
+
return null;
|
270
|
+
}
|
271
|
+
|
272
|
+
if (chunk_size == null) {
|
273
|
+
chunk_size = 5 * MB;
|
274
|
+
}
|
275
|
+
return { filename, content_type, size, chunk_size };
|
276
|
+
};
|
277
|
+
|
278
|
+
const validateCommitUpload = async (
|
279
|
+
request: ODataRequest,
|
280
|
+
applicationApi: PinejsClient,
|
281
|
+
) => {
|
282
|
+
if (request.odataQuery.key == null) {
|
283
|
+
throw new errors.BadRequestError();
|
284
|
+
}
|
285
|
+
|
286
|
+
await applicationApi.request({
|
287
|
+
method: 'POST',
|
288
|
+
url: request.url.substring(1).replace('commitUpload', 'canAccess'),
|
289
|
+
body: { method: 'PATCH' },
|
290
|
+
});
|
291
|
+
|
292
|
+
const { uuid, providerCommitData } = request.values;
|
293
|
+
if (typeof uuid !== 'string') {
|
294
|
+
throw new errors.BadRequestError('Invalid uuid type');
|
295
|
+
}
|
296
|
+
|
297
|
+
const [multipartUpload] = await api.webresource.get({
|
298
|
+
resource: 'multipart_upload',
|
299
|
+
options: {
|
300
|
+
$select: ['id', 'file_key', 'upload_id', 'field_name', 'filename'],
|
301
|
+
$filter: {
|
302
|
+
uuid,
|
303
|
+
status: 'pending',
|
304
|
+
expiry_date: { $gt: { $now: {} } },
|
305
|
+
},
|
306
|
+
},
|
307
|
+
passthrough: {
|
308
|
+
tx: request.tx,
|
309
|
+
req: permissions.rootRead,
|
310
|
+
},
|
311
|
+
});
|
312
|
+
|
313
|
+
if (multipartUpload == null) {
|
314
|
+
throw new errors.BadRequestError(`Invalid upload for uuid ${uuid}`);
|
315
|
+
}
|
316
|
+
|
317
|
+
const metadata = {
|
318
|
+
fileKey: multipartUpload.file_key,
|
319
|
+
uploadId: multipartUpload.upload_id,
|
320
|
+
filename: multipartUpload.filename,
|
321
|
+
fieldName: multipartUpload.field_name,
|
322
|
+
};
|
323
|
+
|
324
|
+
return { uuid, providerCommitData, metadata };
|
325
|
+
};
|
326
|
+
|
327
|
+
const validateCancelPayload = async (
|
328
|
+
request: ODataRequest,
|
329
|
+
applicationApi: PinejsClient,
|
330
|
+
) => {
|
331
|
+
if (request.odataQuery.key == null) {
|
332
|
+
throw new errors.BadRequestError();
|
333
|
+
}
|
334
|
+
|
335
|
+
await applicationApi.request({
|
336
|
+
method: 'POST',
|
337
|
+
url: request.url.substring(1).replace('cancelUpload', 'canAccess'),
|
338
|
+
body: { method: 'PATCH' },
|
339
|
+
});
|
340
|
+
|
341
|
+
const { uuid } = request.values;
|
342
|
+
if (typeof uuid !== 'string') {
|
343
|
+
throw new errors.BadRequestError('Invalid uuid type');
|
344
|
+
}
|
345
|
+
|
346
|
+
const [multipartUpload] = await api.webresource.get({
|
347
|
+
resource: 'multipart_upload',
|
348
|
+
options: {
|
349
|
+
$select: ['id', 'file_key', 'upload_id'],
|
350
|
+
$filter: {
|
351
|
+
uuid,
|
352
|
+
status: 'pending',
|
353
|
+
expiry_date: { $gt: { $now: {} } },
|
354
|
+
},
|
355
|
+
},
|
356
|
+
passthrough: {
|
357
|
+
tx: request.tx,
|
358
|
+
req: permissions.rootRead,
|
359
|
+
},
|
360
|
+
});
|
361
|
+
|
362
|
+
if (multipartUpload == null) {
|
363
|
+
throw new errors.BadRequestError(`Invalid upload for uuid ${uuid}`);
|
364
|
+
}
|
365
|
+
|
366
|
+
return {
|
367
|
+
uuid,
|
368
|
+
fileKey: multipartUpload.file_key,
|
369
|
+
uploadId: multipartUpload.upload_id,
|
370
|
+
};
|
371
|
+
};
|
@@ -0,0 +1,60 @@
|
|
1
|
+
Vocabulary: webresource
|
2
|
+
|
3
|
+
Term: actor
|
4
|
+
Concept Type: Integer (Type)
|
5
|
+
Term: expiry date
|
6
|
+
Concept Type: Date Time (Type)
|
7
|
+
Term: uuid
|
8
|
+
Concept Type: Short Text (Type)
|
9
|
+
Term: resource name
|
10
|
+
Concept Type: Short Text (Type)
|
11
|
+
Term: field name
|
12
|
+
Concept Type: Short Text (Type)
|
13
|
+
Term: resource id
|
14
|
+
Concept Type: Integer (Type)
|
15
|
+
Term: upload id
|
16
|
+
Concept Type: Short Text (Type)
|
17
|
+
Term: file key
|
18
|
+
Concept Type: Short Text (Type)
|
19
|
+
Term: status
|
20
|
+
Concept Type: Short Text (Type)
|
21
|
+
Term: filename
|
22
|
+
Concept Type: Short Text (Type)
|
23
|
+
Term: content type
|
24
|
+
Concept Type: Short Text (Type)
|
25
|
+
Term: size
|
26
|
+
Concept Type: Integer (Type)
|
27
|
+
Term: chunk size
|
28
|
+
Concept Type: Integer (Type)
|
29
|
+
Term: valid until date
|
30
|
+
Concept Type: Date Time (Type)
|
31
|
+
|
32
|
+
Term: multipart upload
|
33
|
+
Fact type: multipart upload has uuid
|
34
|
+
Necessity: each multipart upload has exactly one uuid
|
35
|
+
Necessity: each uuid is of exactly one multipart upload
|
36
|
+
Fact type: multipart upload has resource name
|
37
|
+
Necessity: each multipart upload has exactly one resource name
|
38
|
+
Fact type: multipart upload has field name
|
39
|
+
Necessity: each multipart upload has exactly one field name
|
40
|
+
Fact type: multipart upload has resource id
|
41
|
+
Necessity: each multipart upload has exactly one resource id
|
42
|
+
Fact type: multipart upload has upload id
|
43
|
+
Necessity: each multipart upload has exactly one upload id
|
44
|
+
Fact type: multipart upload has file key
|
45
|
+
Necessity: each multipart upload has exactly one file key
|
46
|
+
Fact type: multipart upload has status
|
47
|
+
Necessity: each multipart upload has exactly one status
|
48
|
+
Definition: "pending" or "completed" or "cancelled"
|
49
|
+
Fact type: multipart upload has filename
|
50
|
+
Necessity: each multipart upload has exactly one filename
|
51
|
+
Fact type: multipart upload has content type
|
52
|
+
Necessity: each multipart upload has exactly one content type
|
53
|
+
Fact type: multipart upload has size
|
54
|
+
Necessity: each multipart upload has exactly one size
|
55
|
+
Fact type: multipart upload has chunk size
|
56
|
+
Necessity: each multipart upload has exactly one chunk size
|
57
|
+
Fact type: multipart upload has expiry date
|
58
|
+
Necessity: each multipart upload has exactly one expiry date
|
59
|
+
Fact type: multipart upload is created by actor
|
60
|
+
Necessity: each multipart upload is created by at most one actor
|
@@ -0,0 +1,48 @@
|
|
1
|
+
// These types were generated by @balena/abstract-sql-to-typescript v5.1.0
|
2
|
+
|
3
|
+
import type { Types } from '@balena/abstract-sql-to-typescript';
|
4
|
+
|
5
|
+
export interface MultipartUpload {
|
6
|
+
Read: {
|
7
|
+
created_at: Types['Date Time']['Read'];
|
8
|
+
modified_at: Types['Date Time']['Read'];
|
9
|
+
id: Types['Serial']['Read'];
|
10
|
+
uuid: Types['Short Text']['Read'];
|
11
|
+
resource_name: Types['Short Text']['Read'];
|
12
|
+
field_name: Types['Short Text']['Read'];
|
13
|
+
resource_id: Types['Integer']['Read'];
|
14
|
+
upload_id: Types['Short Text']['Read'];
|
15
|
+
file_key: Types['Short Text']['Read'];
|
16
|
+
status: 'pending' | 'completed' | 'cancelled';
|
17
|
+
filename: Types['Short Text']['Read'];
|
18
|
+
content_type: Types['Short Text']['Read'];
|
19
|
+
size: Types['Integer']['Read'];
|
20
|
+
chunk_size: Types['Integer']['Read'];
|
21
|
+
expiry_date: Types['Date Time']['Read'];
|
22
|
+
is_created_by__actor: Types['Integer']['Read'] | null;
|
23
|
+
};
|
24
|
+
Write: {
|
25
|
+
created_at: Types['Date Time']['Write'];
|
26
|
+
modified_at: Types['Date Time']['Write'];
|
27
|
+
id: Types['Serial']['Write'];
|
28
|
+
uuid: Types['Short Text']['Write'];
|
29
|
+
resource_name: Types['Short Text']['Write'];
|
30
|
+
field_name: Types['Short Text']['Write'];
|
31
|
+
resource_id: Types['Integer']['Write'];
|
32
|
+
upload_id: Types['Short Text']['Write'];
|
33
|
+
file_key: Types['Short Text']['Write'];
|
34
|
+
status: 'pending' | 'completed' | 'cancelled';
|
35
|
+
filename: Types['Short Text']['Write'];
|
36
|
+
content_type: Types['Short Text']['Write'];
|
37
|
+
size: Types['Integer']['Write'];
|
38
|
+
chunk_size: Types['Integer']['Write'];
|
39
|
+
expiry_date: Types['Date Time']['Write'];
|
40
|
+
is_created_by__actor: Types['Integer']['Write'] | null;
|
41
|
+
};
|
42
|
+
}
|
43
|
+
|
44
|
+
export default interface $Model {
|
45
|
+
multipart_upload: MultipartUpload;
|
46
|
+
|
47
|
+
|
48
|
+
}
|