@balena/pinejs 21.4.0-build-large-file-uploads-2-56700d672d31db4406ba01ca349d69af5c8611e7-1 → 21.4.0-build-add-actions-example-c20002c2bf051233b333158f3b7a0719f6cf6e4e-1
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/.versionbot/CHANGELOG.yml +3 -3
- package/CHANGELOG.md +1 -1
- package/out/actions/index.d.ts +32 -0
- package/out/actions/index.js +67 -0
- package/out/actions/index.js.map +1 -0
- package/out/config-loader/config-loader.js +7 -6
- package/out/config-loader/config-loader.js.map +1 -1
- package/out/sbvr-api/sbvr-utils.d.ts +1 -1
- package/out/sbvr-api/sbvr-utils.js +9 -2
- package/out/sbvr-api/sbvr-utils.js.map +1 -1
- package/out/webresource-handler/actions/beginUpload.d.ts +3 -0
- package/out/webresource-handler/actions/beginUpload.js +99 -0
- package/out/webresource-handler/actions/beginUpload.js.map +1 -0
- package/out/webresource-handler/actions/canceUpload.d.ts +3 -0
- package/out/webresource-handler/actions/canceUpload.js +59 -0
- package/out/webresource-handler/actions/canceUpload.js.map +1 -0
- package/out/webresource-handler/actions/commitUpload.d.ts +3 -0
- package/out/webresource-handler/actions/commitUpload.js +85 -0
- package/out/webresource-handler/actions/commitUpload.js.map +1 -0
- package/out/webresource-handler/index.d.ts +1 -1
- package/out/webresource-handler/index.js +11 -6
- package/out/webresource-handler/index.js.map +1 -1
- package/out/webresource-handler/multiparUpload.d.ts +4 -0
- package/out/webresource-handler/multiparUpload.js +14 -0
- package/out/webresource-handler/multiparUpload.js.map +1 -0
- package/package.json +2 -2
- package/src/actions/index.ts +142 -0
- package/src/config-loader/config-loader.ts +8 -8
- package/src/sbvr-api/sbvr-utils.ts +11 -2
- package/src/webresource-handler/actions/beginUpload.ts +160 -0
- package/src/webresource-handler/actions/canceUpload.ts +93 -0
- package/src/webresource-handler/actions/commitUpload.ts +116 -0
- package/src/webresource-handler/index.ts +16 -14
- package/src/webresource-handler/multiparUpload.ts +23 -0
- package/out/webresource-handler/multipartUpload.d.ts +0 -12
- package/out/webresource-handler/multipartUpload.js +0 -297
- package/out/webresource-handler/multipartUpload.js.map +0 -1
- package/src/webresource-handler/multipartUpload.ts +0 -424
@@ -0,0 +1,160 @@
|
|
1
|
+
import type { AnyObject } from 'pinejs-client-core';
|
2
|
+
import type {
|
3
|
+
ODataActionArgs,
|
4
|
+
ODataActionRequest,
|
5
|
+
} from '../../actions/index.js';
|
6
|
+
import { api, type Response } from '../../sbvr-api/sbvr-utils.js';
|
7
|
+
import type { MultipartUploadHandler } from '../multiparUpload.js';
|
8
|
+
import { getMultipartUploadHandler } from '../multiparUpload.js';
|
9
|
+
import type { BeginMultipartUploadPayload } from '../index.js';
|
10
|
+
import { getWebResourceFields } from '../index.js';
|
11
|
+
import { BadRequestError, NotImplementedError } from '../../sbvr-api/errors.js';
|
12
|
+
import { permissions, sbvrUtils } from '../../server-glue/module.js';
|
13
|
+
import { TransactionClosedError } from '../../database-layer/db.js';
|
14
|
+
import { randomUUID } from 'crypto';
|
15
|
+
import type { WebResourceType as WebResource } from '@balena/sbvr-types';
|
16
|
+
|
17
|
+
type FakeWebResourcePatch = {
|
18
|
+
[key: string]: Omit<WebResource, 'href'> & {
|
19
|
+
href: 'fake_patch';
|
20
|
+
};
|
21
|
+
};
|
22
|
+
|
23
|
+
export const beginUploadAction = async ({
|
24
|
+
request,
|
25
|
+
tx,
|
26
|
+
affectedIds,
|
27
|
+
req,
|
28
|
+
}: ODataActionArgs): Promise<Response> => {
|
29
|
+
const affectedId = affectedIds[0];
|
30
|
+
if (typeof affectedId !== 'number') {
|
31
|
+
throw new NotImplementedError(
|
32
|
+
'multipart upload do not yet support non-numeric ids',
|
33
|
+
);
|
34
|
+
}
|
35
|
+
|
36
|
+
const handler = getMultipartUploadHandler();
|
37
|
+
const { fieldName, beginUploadPayload } = parseBeginUpload(request, handler);
|
38
|
+
|
39
|
+
await runFakeDbPatch(request, {
|
40
|
+
[fieldName]: { ...beginUploadPayload, href: 'fake_patch' },
|
41
|
+
});
|
42
|
+
|
43
|
+
const { fileKey, uploadId, uploadParts } =
|
44
|
+
await handler.multipartUpload.begin(fieldName, beginUploadPayload);
|
45
|
+
const uuid = randomUUID();
|
46
|
+
await api.webresource.post({
|
47
|
+
resource: 'multipart_upload',
|
48
|
+
body: {
|
49
|
+
uuid,
|
50
|
+
resource_name: request.resourceName,
|
51
|
+
field_name: fieldName,
|
52
|
+
resource_id: affectedId,
|
53
|
+
upload_id: uploadId,
|
54
|
+
file_key: fileKey,
|
55
|
+
status: 'pending',
|
56
|
+
...beginUploadPayload,
|
57
|
+
expiry_date: Date.now() + 7 * 24 * 60 * 60 * 1000, // 7 days in ms
|
58
|
+
is_created_by__actor: req.user?.actor,
|
59
|
+
},
|
60
|
+
passthrough: {
|
61
|
+
req: permissions.root,
|
62
|
+
tx,
|
63
|
+
},
|
64
|
+
});
|
65
|
+
|
66
|
+
return {
|
67
|
+
body: { [fieldName]: { uuid, uploadParts } },
|
68
|
+
statusCode: 200,
|
69
|
+
};
|
70
|
+
};
|
71
|
+
|
72
|
+
const parseBeginUpload = (
|
73
|
+
request: ODataActionRequest,
|
74
|
+
webResourceHandler: MultipartUploadHandler,
|
75
|
+
) => {
|
76
|
+
const fieldNames = Object.keys(request.values);
|
77
|
+
if (fieldNames.length !== 1) {
|
78
|
+
throw new BadRequestError(
|
79
|
+
'You can only get upload url for one field at a time',
|
80
|
+
);
|
81
|
+
}
|
82
|
+
|
83
|
+
const [fieldName] = fieldNames;
|
84
|
+
const webResourceFields = getWebResourceFields(request, false);
|
85
|
+
if (!webResourceFields.includes(fieldName)) {
|
86
|
+
throw new BadRequestError(
|
87
|
+
`The provided field '${fieldName}' is not a valid webresource`,
|
88
|
+
);
|
89
|
+
}
|
90
|
+
|
91
|
+
const beginUploadPayload = parseBeginUploadPayload(
|
92
|
+
request.values[fieldName],
|
93
|
+
webResourceHandler,
|
94
|
+
);
|
95
|
+
if (beginUploadPayload == null) {
|
96
|
+
throw new BadRequestError('Invalid file metadata');
|
97
|
+
}
|
98
|
+
|
99
|
+
return {
|
100
|
+
beginUploadPayload,
|
101
|
+
fieldName,
|
102
|
+
};
|
103
|
+
};
|
104
|
+
|
105
|
+
const parseBeginUploadPayload = (
|
106
|
+
payload: AnyObject,
|
107
|
+
webResourceHandler: MultipartUploadHandler,
|
108
|
+
): BeginMultipartUploadPayload | null => {
|
109
|
+
if (payload == null || typeof payload !== 'object') {
|
110
|
+
return null;
|
111
|
+
}
|
112
|
+
|
113
|
+
let { filename, content_type, size, chunk_size } = payload;
|
114
|
+
if (
|
115
|
+
typeof filename !== 'string' ||
|
116
|
+
typeof content_type !== 'string' ||
|
117
|
+
typeof size !== 'number' ||
|
118
|
+
(chunk_size != null && typeof chunk_size !== 'number') ||
|
119
|
+
(chunk_size != null &&
|
120
|
+
chunk_size < webResourceHandler.multipartUpload.getMinimumPartSize())
|
121
|
+
) {
|
122
|
+
return null;
|
123
|
+
}
|
124
|
+
|
125
|
+
chunk_size ??= webResourceHandler.multipartUpload.getDefaultPartSize();
|
126
|
+
|
127
|
+
return { filename, content_type, size, chunk_size };
|
128
|
+
};
|
129
|
+
|
130
|
+
// TODO: comment explaining why run this fake transaction is important
|
131
|
+
const runFakeDbPatch = async (
|
132
|
+
request: ODataActionRequest,
|
133
|
+
fakeDbPatch: FakeWebResourcePatch,
|
134
|
+
) => {
|
135
|
+
try {
|
136
|
+
await sbvrUtils.db.transaction(async (fakeTx) => {
|
137
|
+
const newUrl = request.url.slice(1).replace(/\/beginUpload$/, '');
|
138
|
+
await api[request.vocabulary].request({
|
139
|
+
method: 'PATCH',
|
140
|
+
url: newUrl,
|
141
|
+
body: fakeDbPatch,
|
142
|
+
// TODO: comment explaining why root is necessary
|
143
|
+
passthrough: { tx: fakeTx, req: permissions.root },
|
144
|
+
});
|
145
|
+
|
146
|
+
await fakeTx.rollback();
|
147
|
+
});
|
148
|
+
} catch (e) {
|
149
|
+
if (
|
150
|
+
// calling rollback() always makes it throw, so we capture and move on in case
|
151
|
+
// the error was just because the tx was (on purpose) rolledback
|
152
|
+
!(
|
153
|
+
e instanceof TransactionClosedError &&
|
154
|
+
e.message === 'Transaction has been rolled back.'
|
155
|
+
)
|
156
|
+
) {
|
157
|
+
throw e;
|
158
|
+
}
|
159
|
+
}
|
160
|
+
};
|
@@ -0,0 +1,93 @@
|
|
1
|
+
import type {
|
2
|
+
ODataActionArgs,
|
3
|
+
ODataActionRequest,
|
4
|
+
} from '../../actions/index.js';
|
5
|
+
import type { Tx } from '../../database-layer/db.js';
|
6
|
+
import {
|
7
|
+
BadRequestError,
|
8
|
+
NotImplementedError,
|
9
|
+
UnauthorizedError,
|
10
|
+
} from '../../sbvr-api/errors.js';
|
11
|
+
import { api, type Response } from '../../sbvr-api/sbvr-utils.js';
|
12
|
+
import { permissions } from '../../server-glue/module.js';
|
13
|
+
import { getMultipartUploadHandler } from '../multiparUpload.js';
|
14
|
+
|
15
|
+
export const cancelUploadAction = async ({
|
16
|
+
request,
|
17
|
+
tx,
|
18
|
+
affectedIds,
|
19
|
+
}: ODataActionArgs): Promise<Response> => {
|
20
|
+
const affectedId = affectedIds[0];
|
21
|
+
if (typeof affectedId !== 'number') {
|
22
|
+
throw new NotImplementedError(
|
23
|
+
'multipart upload do not yet support non-numeric ids',
|
24
|
+
);
|
25
|
+
}
|
26
|
+
const { id, fileKey, uploadId } = await getOngoingUpload(
|
27
|
+
request,
|
28
|
+
affectedId,
|
29
|
+
tx,
|
30
|
+
);
|
31
|
+
const handler = getMultipartUploadHandler();
|
32
|
+
|
33
|
+
await api.webresource.patch({
|
34
|
+
resource: 'multipart_upload',
|
35
|
+
body: {
|
36
|
+
status: 'cancelled',
|
37
|
+
},
|
38
|
+
id,
|
39
|
+
passthrough: {
|
40
|
+
tx: tx,
|
41
|
+
req: permissions.root,
|
42
|
+
},
|
43
|
+
});
|
44
|
+
|
45
|
+
// Note that different then beginUpload/commitUpload where we first do the action on the external service
|
46
|
+
// and then reflect it on the DB, for cancel upload it is the other way around
|
47
|
+
// as the worst case scenario is having a canceled upload which is marked on the DB as something else
|
48
|
+
await handler.multipartUpload.cancel({ fileKey, uploadId });
|
49
|
+
|
50
|
+
return {
|
51
|
+
statusCode: 204,
|
52
|
+
};
|
53
|
+
};
|
54
|
+
|
55
|
+
const getOngoingUpload = async (
|
56
|
+
request: ODataActionRequest,
|
57
|
+
affectedId: number,
|
58
|
+
tx: Tx,
|
59
|
+
) => {
|
60
|
+
const { uuid } = request.values;
|
61
|
+
if (!uuid || typeof uuid !== 'string') {
|
62
|
+
throw new BadRequestError('Invalid uuid type');
|
63
|
+
}
|
64
|
+
|
65
|
+
const [multipartUpload] = await api.webresource.get({
|
66
|
+
resource: 'multipart_upload',
|
67
|
+
options: {
|
68
|
+
$select: ['id', 'file_key', 'upload_id'],
|
69
|
+
$filter: {
|
70
|
+
// TODO: swap this into the ID to see if it works
|
71
|
+
uuid,
|
72
|
+
status: 'pending',
|
73
|
+
expiry_date: { $gt: { $now: {} } },
|
74
|
+
resource_name: request.resourceName,
|
75
|
+
resource_id: affectedId,
|
76
|
+
},
|
77
|
+
},
|
78
|
+
passthrough: {
|
79
|
+
tx,
|
80
|
+
req: permissions.rootRead,
|
81
|
+
},
|
82
|
+
});
|
83
|
+
|
84
|
+
if (multipartUpload == null) {
|
85
|
+
throw new UnauthorizedError();
|
86
|
+
}
|
87
|
+
|
88
|
+
return {
|
89
|
+
id: multipartUpload.id,
|
90
|
+
fileKey: multipartUpload.file_key,
|
91
|
+
uploadId: multipartUpload.upload_id,
|
92
|
+
};
|
93
|
+
};
|
@@ -0,0 +1,116 @@
|
|
1
|
+
import type {
|
2
|
+
ODataActionArgs,
|
3
|
+
ODataActionRequest,
|
4
|
+
} from '../../actions/index.js';
|
5
|
+
import type { Tx } from '../../database-layer/db.js';
|
6
|
+
import {
|
7
|
+
BadRequestError,
|
8
|
+
NotImplementedError,
|
9
|
+
UnauthorizedError,
|
10
|
+
} from '../../sbvr-api/errors.js';
|
11
|
+
import type { Response } from '../../sbvr-api/sbvr-utils.js';
|
12
|
+
import { api } from '../../sbvr-api/sbvr-utils.js';
|
13
|
+
import { permissions } from '../../server-glue/module.js';
|
14
|
+
import { getMultipartUploadHandler } from '../multiparUpload.js';
|
15
|
+
|
16
|
+
export const commitUploadAction = async ({
|
17
|
+
request,
|
18
|
+
tx,
|
19
|
+
affectedIds,
|
20
|
+
api: applicationApi,
|
21
|
+
}: ODataActionArgs): Promise<Response> => {
|
22
|
+
const affectedId = affectedIds[0];
|
23
|
+
if (typeof affectedId !== 'number') {
|
24
|
+
throw new NotImplementedError(
|
25
|
+
'multipart upload do not yet support non-numeric ids',
|
26
|
+
);
|
27
|
+
}
|
28
|
+
|
29
|
+
const multipartUpload = await getOngoingUpload(request, affectedId, tx);
|
30
|
+
const handler = getMultipartUploadHandler();
|
31
|
+
|
32
|
+
const webresource = await handler.multipartUpload.commit({
|
33
|
+
fileKey: multipartUpload.fileKey,
|
34
|
+
uploadId: multipartUpload.uploadId,
|
35
|
+
filename: multipartUpload.filename,
|
36
|
+
providerCommitData: multipartUpload.providerCommitData,
|
37
|
+
});
|
38
|
+
|
39
|
+
await Promise.all([
|
40
|
+
api.webresource.patch({
|
41
|
+
resource: 'multipart_upload',
|
42
|
+
body: {
|
43
|
+
status: 'completed',
|
44
|
+
},
|
45
|
+
options: {
|
46
|
+
$filter: {
|
47
|
+
uuid: multipartUpload.uuid,
|
48
|
+
},
|
49
|
+
},
|
50
|
+
passthrough: {
|
51
|
+
tx: tx,
|
52
|
+
req: permissions.root,
|
53
|
+
},
|
54
|
+
}),
|
55
|
+
applicationApi.patch({
|
56
|
+
resource: request.resourceName,
|
57
|
+
id: affectedId,
|
58
|
+
body: {
|
59
|
+
[multipartUpload.fieldName]: webresource,
|
60
|
+
},
|
61
|
+
passthrough: {
|
62
|
+
tx: tx,
|
63
|
+
// Root is needed as, if you are not root, you are not allowed to directly modify the actual metadata
|
64
|
+
req: permissions.root,
|
65
|
+
},
|
66
|
+
}),
|
67
|
+
]);
|
68
|
+
|
69
|
+
return {
|
70
|
+
body: webresource,
|
71
|
+
statusCode: 200,
|
72
|
+
};
|
73
|
+
};
|
74
|
+
|
75
|
+
const getOngoingUpload = async (
|
76
|
+
request: ODataActionRequest,
|
77
|
+
affectedId: number,
|
78
|
+
tx: Tx,
|
79
|
+
) => {
|
80
|
+
const { uuid, providerCommitData } = request.values;
|
81
|
+
if (!uuid || typeof uuid !== 'string') {
|
82
|
+
throw new BadRequestError('Invalid uuid type');
|
83
|
+
}
|
84
|
+
|
85
|
+
const [multipartUpload] = await api.webresource.get({
|
86
|
+
resource: 'multipart_upload',
|
87
|
+
options: {
|
88
|
+
$select: ['id', 'file_key', 'upload_id', 'field_name', 'filename'],
|
89
|
+
$filter: {
|
90
|
+
// TODO: swap this into the ID to see if it works
|
91
|
+
uuid,
|
92
|
+
status: 'pending',
|
93
|
+
expiry_date: { $gt: { $now: {} } },
|
94
|
+
resource_name: request.resourceName,
|
95
|
+
resource_id: affectedId,
|
96
|
+
},
|
97
|
+
},
|
98
|
+
passthrough: {
|
99
|
+
tx,
|
100
|
+
req: permissions.rootRead,
|
101
|
+
},
|
102
|
+
});
|
103
|
+
|
104
|
+
if (multipartUpload == null) {
|
105
|
+
throw new UnauthorizedError();
|
106
|
+
}
|
107
|
+
|
108
|
+
return {
|
109
|
+
uuid,
|
110
|
+
providerCommitData,
|
111
|
+
fileKey: multipartUpload.file_key,
|
112
|
+
uploadId: multipartUpload.upload_id,
|
113
|
+
filename: multipartUpload.filename,
|
114
|
+
fieldName: multipartUpload.field_name,
|
115
|
+
};
|
116
|
+
};
|
@@ -18,13 +18,13 @@ import { TypedError } from 'typed-error';
|
|
18
18
|
import type { Resolvable } from '../sbvr-api/common-types.js';
|
19
19
|
import type WebresourceModel from './webresource.js';
|
20
20
|
import { importSBVR } from '../server-glue/sbvr-loader.js';
|
21
|
-
import {
|
22
|
-
|
23
|
-
|
24
|
-
} from './
|
21
|
+
import { addAction } from '../actions/index.js';
|
22
|
+
import { beginUploadAction } from './actions/beginUpload.js';
|
23
|
+
import { isMultipartUploadAvailable } from './multiparUpload.js';
|
24
|
+
import { commitUploadAction } from './actions/commitUpload.js';
|
25
|
+
import { cancelUploadAction } from './actions/canceUpload.js';
|
25
26
|
|
26
27
|
export * from './handlers/index.js';
|
27
|
-
export type { BeginUploadResponse } from './multipartUpload.js';
|
28
28
|
|
29
29
|
export interface IncomingFile {
|
30
30
|
fieldname: string;
|
@@ -337,9 +337,6 @@ const throwIfWebresourceNotInMultipart = (
|
|
337
337
|
{ req, request }: HookArgs,
|
338
338
|
) => {
|
339
339
|
if (
|
340
|
-
request.custom.isAction !== 'beginUpload' &&
|
341
|
-
request.custom.isAction !== 'commitUpload' &&
|
342
|
-
request.custom.isAction !== 'cancelUpload' &&
|
343
340
|
req.user !== permissions.root.user &&
|
344
341
|
!req.is?.('multipart') &&
|
345
342
|
webResourceFields.some((field) => request.values[field] != null)
|
@@ -537,14 +534,19 @@ export const setupUploadHooks = (
|
|
537
534
|
resourceName,
|
538
535
|
getCreateWebResourceHooks(handler),
|
539
536
|
);
|
537
|
+
};
|
540
538
|
|
539
|
+
export const setupUploadActions = (
|
540
|
+
handler: WebResourceHandler,
|
541
|
+
apiRoot: string,
|
542
|
+
resourceName: string,
|
543
|
+
) => {
|
541
544
|
if (isMultipartUploadAvailable(handler)) {
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
);
|
545
|
+
addAction(apiRoot, resourceName, 'beginUpload', beginUploadAction);
|
546
|
+
|
547
|
+
addAction(apiRoot, resourceName, 'commitUpload', commitUploadAction);
|
548
|
+
|
549
|
+
addAction(apiRoot, resourceName, 'cancelUpload', cancelUploadAction);
|
548
550
|
}
|
549
551
|
};
|
550
552
|
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import { webResource as webResourceEnv } from '../config-loader/env.js';
|
2
|
+
import { NotImplementedError } from '../sbvr-api/errors.js';
|
3
|
+
import type { WebResourceHandler } from './index.js';
|
4
|
+
import { getWebresourceHandler } from './index.js';
|
5
|
+
|
6
|
+
export type MultipartUploadHandler = WebResourceHandler &
|
7
|
+
Required<Pick<WebResourceHandler, 'multipartUpload'>>;
|
8
|
+
|
9
|
+
export const isMultipartUploadAvailable = (
|
10
|
+
handler: WebResourceHandler | undefined,
|
11
|
+
): handler is MultipartUploadHandler => {
|
12
|
+
return (
|
13
|
+
webResourceEnv.multipartUploadEnabled && handler?.multipartUpload != null
|
14
|
+
);
|
15
|
+
};
|
16
|
+
|
17
|
+
export const getMultipartUploadHandler = () => {
|
18
|
+
const handler = getWebresourceHandler();
|
19
|
+
if (!isMultipartUploadAvailable(handler)) {
|
20
|
+
throw new NotImplementedError('Multipart uploads not available');
|
21
|
+
}
|
22
|
+
return handler;
|
23
|
+
};
|
@@ -1,12 +0,0 @@
|
|
1
|
-
import type { UploadPart, WebResourceHandler } from './index.js';
|
2
|
-
import { sbvrUtils } from '../server-glue/module.js';
|
3
|
-
export interface BeginUploadResponse {
|
4
|
-
[fieldName: string]: {
|
5
|
-
uuid: string;
|
6
|
-
uploadParts: UploadPart[];
|
7
|
-
};
|
8
|
-
}
|
9
|
-
type MultipartUploadHandler = WebResourceHandler & Required<Pick<WebResourceHandler, 'multipartUpload'>>;
|
10
|
-
export declare const isMultipartUploadAvailable: (webResourceHandler: WebResourceHandler) => webResourceHandler is MultipartUploadHandler;
|
11
|
-
export declare const multipartUploadHooks: (webResourceHandler: MultipartUploadHandler) => sbvrUtils.Hooks;
|
12
|
-
export {};
|