@knime/hub-features 1.35.3 → 2.0.0
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/CHANGELOG.md +23 -0
- package/package.json +4 -4
- package/src/index.ts +0 -1
- package/src/useFileUpload/index.ts +0 -1
- package/src/useFileUpload/useFileUpload.ts +0 -348
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# @knime/hub-features
|
|
2
2
|
|
|
3
|
+
## 2.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- c0933b6: **Breaking:** removed `useFileUpload`. It is available from
|
|
8
|
+
`@knime/product-features/data-transfer`.
|
|
9
|
+
|
|
10
|
+
`@knime/utils` now comes from the registry rather than the private copy in this
|
|
11
|
+
repository.
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- Updated dependencies [c0933b6]
|
|
16
|
+
- @knime/components@2.0.0
|
|
17
|
+
|
|
18
|
+
## 1.35.4
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- Updated dependencies [97ad603]
|
|
23
|
+
- @knime/utils@1.11.2
|
|
24
|
+
- @knime/components@1.46.13
|
|
25
|
+
|
|
3
26
|
## 1.35.3
|
|
4
27
|
|
|
5
28
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knime/hub-features",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Vue components & composables for shared hub features",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -33,15 +33,15 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@floating-ui/vue": "1.1.8",
|
|
36
|
+
"@knime/utils": "2.0.0",
|
|
36
37
|
"@vueuse/components": "~13.8.0",
|
|
37
38
|
"@vueuse/core": "~13.8.0",
|
|
38
39
|
"@vueuse/shared": "~13.8.0",
|
|
39
40
|
"lodash-es": "4.18.1",
|
|
40
41
|
"ofetch": "^1.4.1",
|
|
41
42
|
"typescript": "^5.9.3",
|
|
42
|
-
"@knime/components": "
|
|
43
|
-
"@knime/styles": "1.15.4"
|
|
44
|
-
"@knime/utils": "1.11.1"
|
|
43
|
+
"@knime/components": "2.0.0",
|
|
44
|
+
"@knime/styles": "1.15.4"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"consola": "3.x",
|
package/src/index.ts
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./useFileUpload";
|
|
@@ -1,348 +0,0 @@
|
|
|
1
|
-
import { computed, reactive, readonly, ref } from "vue";
|
|
2
|
-
import { FetchError, type FetchOptions } from "ofetch";
|
|
3
|
-
|
|
4
|
-
import { useUploadManager } from "@knime/components";
|
|
5
|
-
import {
|
|
6
|
-
formatFileSize,
|
|
7
|
-
getFileMimeType,
|
|
8
|
-
knimeFileFormats,
|
|
9
|
-
promise,
|
|
10
|
-
} from "@knime/utils";
|
|
11
|
-
|
|
12
|
-
import { createHttpClient } from "../httpClient/createHttpClient";
|
|
13
|
-
import { rfcErrors } from "../rfcErrors";
|
|
14
|
-
|
|
15
|
-
const DEFAULT_MAX_UPLOAD_QUEUE_SIZE = 10;
|
|
16
|
-
const DEFAULT_UPLOAD_SIZE_LIMIT_BYTES = 5 * 1024 * 1024 * 1024;
|
|
17
|
-
|
|
18
|
-
const DEFAULT_RETRY_DELAY_MS = 50;
|
|
19
|
-
|
|
20
|
-
// Catalog & artifacts services currently have a limit for 5 GiB for
|
|
21
|
-
// upload and copy/move operations but it is not communicated to clients.
|
|
22
|
-
// Repeat the constant here to be able to still make a pre-check. See NXT-4510.
|
|
23
|
-
const getUploadSizeLimitBytes = () => DEFAULT_UPLOAD_SIZE_LIMIT_BYTES;
|
|
24
|
-
|
|
25
|
-
type UseFileUploadOptions = {
|
|
26
|
-
/**
|
|
27
|
-
* Max number of concurrent uploads allowed in the upload queue.
|
|
28
|
-
* When an upload is started with a number of files that exceed the pending
|
|
29
|
-
* uploads then only files up-to the max queue size will be processed and the rest
|
|
30
|
-
* will be ignored. This will also call the `onUploadQueueSizeExceeded` callback
|
|
31
|
-
*/
|
|
32
|
-
maxUploadQueueSize?: number;
|
|
33
|
-
onFileUploadComplete?: (payload: {
|
|
34
|
-
uploadId: string;
|
|
35
|
-
filePartIds: Record<number, string>;
|
|
36
|
-
parentId: string;
|
|
37
|
-
name: string;
|
|
38
|
-
}) => void;
|
|
39
|
-
onFileUploadFailed?: (payload: {
|
|
40
|
-
uploadId: string;
|
|
41
|
-
error: Error;
|
|
42
|
-
parentId: string;
|
|
43
|
-
}) => void;
|
|
44
|
-
onUploadQueueSizeExceeded?: (maxQueueSize: number) => void;
|
|
45
|
-
/**
|
|
46
|
-
* Options to customize the fetch client used internally to make http requests.
|
|
47
|
-
* e.g: headers, baseUrl, etc
|
|
48
|
-
*/
|
|
49
|
-
customFetchClientOptions?: FetchOptions;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* This composable handles uploads of data to a hub repository item (space or workflow group).
|
|
54
|
-
* It is a multi-step process which is described as follows:
|
|
55
|
-
*
|
|
56
|
-
* Step 1:
|
|
57
|
-
* First we need to prepare an upload. The items will be uploaded to a `parent` container
|
|
58
|
-
* identified by its id. Then, for each file we want to upload, we must provide paths relative
|
|
59
|
-
* to the parent that identify each file. Example:
|
|
60
|
-
* ```
|
|
61
|
-
* {
|
|
62
|
-
* "items": {
|
|
63
|
-
* "my-file.txt": { itemContentType: "text/plain", itemContentSize: 5000 },
|
|
64
|
-
* "folder/my-file.zip": { itemContentType: "application/zip", itemContentSize: 5000 },
|
|
65
|
-
* }
|
|
66
|
-
* }
|
|
67
|
-
* ```
|
|
68
|
-
* This describes 2 items, one placed in the parent's root and another inside a folder.
|
|
69
|
-
* The response for this would look like:
|
|
70
|
-
* ```
|
|
71
|
-
* {
|
|
72
|
-
* "items": {
|
|
73
|
-
* "my-file.txt": { uploadId: "id1" },
|
|
74
|
-
* "folder/my-file.zip": { uploadId: "id2" },
|
|
75
|
-
* }
|
|
76
|
-
* }
|
|
77
|
-
* ```
|
|
78
|
-
* Now, each corresponding `uploadId` uniquely identifies the upload process for
|
|
79
|
-
* their respective files.
|
|
80
|
-
*
|
|
81
|
-
* Step 2:
|
|
82
|
-
* Next, we need to start uploading the file parts. Technically, you _can_ just upload
|
|
83
|
-
* each file as one big file "part", but practicaly that is not recommended because a hiccup
|
|
84
|
-
* in the connection means you would have to start the upload from scratch; instead, we split
|
|
85
|
-
* the files in smaller parts.
|
|
86
|
-
* The upload manager helper will take care of splitting the file. However, the responsibility
|
|
87
|
-
* of this composable is to supply a function to resolve the upload URL. This upload URL is obtained
|
|
88
|
-
* by POSTing to an endpoint with the `uploadId` and the `partNumber` and this will return
|
|
89
|
-
* the data required to make an upload to a presigned URL; this will be called for each file part.
|
|
90
|
-
*
|
|
91
|
-
* Step 3:
|
|
92
|
-
* Completion:
|
|
93
|
-
* Once the upload of a single file is complete, the upload manager will trigger the `onFileUploadComplete`
|
|
94
|
-
* callback, which then will supply the ids of all the different parts that got uploaded for a given `uploadId`
|
|
95
|
-
* Then we need to "complete" the upload by sending a request which provides the `uploadId` and a list of
|
|
96
|
-
* the ids of all the parts. Note that some backends don't return the ids of the file parts after a successful
|
|
97
|
-
* upload of a part, in this case empty strings or null can be used instead of the part id.
|
|
98
|
-
*
|
|
99
|
-
* Cancellation:
|
|
100
|
-
* Alternative to completion, you can also cancel an upload. For this, the composable makes a DELETE request
|
|
101
|
-
* providing the corresponding `uploadId` which will effectively cancel it.
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* Usage example:
|
|
105
|
-
* ```
|
|
106
|
-
* const { start, uploadItems, cancel } = useFileUpload();
|
|
107
|
-
*
|
|
108
|
-
* const someEventHandler = (parentId: string, files: File[]) => {
|
|
109
|
-
* start(parentId, files);
|
|
110
|
-
* }
|
|
111
|
-
*
|
|
112
|
-
* <template>
|
|
113
|
-
* <!-- `uploadItems` will reactively update -->
|
|
114
|
-
* <div v-for="item in uploadItems" :key="item.id">
|
|
115
|
-
* <span>{{ item.name }} / {{ item.size }}</span>
|
|
116
|
-
* <hr />
|
|
117
|
-
* <span>Progress: {{ item.progress }}</span>
|
|
118
|
-
* </div>
|
|
119
|
-
* </template>
|
|
120
|
-
* ```
|
|
121
|
-
*/
|
|
122
|
-
export const useFileUpload = (options: UseFileUploadOptions = {}) => {
|
|
123
|
-
const $ofetch = createHttpClient(options.customFetchClientOptions);
|
|
124
|
-
|
|
125
|
-
const prepareUpload = (
|
|
126
|
-
parentId: string,
|
|
127
|
-
files: Array<File>,
|
|
128
|
-
): Promise<Array<{ uploadId: string; file: File }>> => {
|
|
129
|
-
const fileDictionary = Object.fromEntries(
|
|
130
|
-
files.map((file) => {
|
|
131
|
-
// strip extension for workflows
|
|
132
|
-
const name = knimeFileFormats.KNWF.getNameOrDefault(file, file.name);
|
|
133
|
-
|
|
134
|
-
return [name, file];
|
|
135
|
-
}),
|
|
136
|
-
);
|
|
137
|
-
|
|
138
|
-
// map file dictionary to body payload of the prepare upload request
|
|
139
|
-
const items: Record<string, { itemContentType: string }> =
|
|
140
|
-
Object.fromEntries(
|
|
141
|
-
Object.entries(fileDictionary).map(([name, file]) => [
|
|
142
|
-
name,
|
|
143
|
-
{
|
|
144
|
-
itemContentType: getFileMimeType(file),
|
|
145
|
-
itemContentSize: file.size,
|
|
146
|
-
},
|
|
147
|
-
]),
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
type PrepareUploadResponse = {
|
|
151
|
-
items: Record<string, { uploadId: string }>;
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
return $ofetch<PrepareUploadResponse>(`/repository/${parentId}/manifest`, {
|
|
155
|
-
method: "POST",
|
|
156
|
-
body: { items },
|
|
157
|
-
}).then(({ items }) => {
|
|
158
|
-
return Object.keys(items).map((name) => {
|
|
159
|
-
const { uploadId } = items[name];
|
|
160
|
-
const file = fileDictionary[name];
|
|
161
|
-
return { uploadId, file };
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
const resolveFilePartUploadURL = (uploadId: string, partNumber: number) => {
|
|
167
|
-
type UploadURLResponse = {
|
|
168
|
-
method: string;
|
|
169
|
-
url: string;
|
|
170
|
-
header: { Host: string };
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
return $ofetch<UploadURLResponse>(
|
|
174
|
-
`/uploads/${uploadId}/parts/?partNumber=${partNumber}`,
|
|
175
|
-
{ method: "POST" },
|
|
176
|
-
);
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
const completeUpload = (
|
|
180
|
-
uploadId: string,
|
|
181
|
-
partIds: Record<number, string>,
|
|
182
|
-
) => {
|
|
183
|
-
return $ofetch(`/uploads/${uploadId}`, {
|
|
184
|
-
method: "POST",
|
|
185
|
-
body: partIds,
|
|
186
|
-
});
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
const cancelUpload = (uploadId: string) => {
|
|
190
|
-
return $ofetch(`/uploads/${uploadId}`, {
|
|
191
|
-
method: "DELETE",
|
|
192
|
-
});
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
// contains upload ids for files that have a processing step which was not finished yet
|
|
196
|
-
const unprocessedUploads = reactive<Set<string>>(new Set());
|
|
197
|
-
const isKNWFFile = (file: File) => knimeFileFormats.KNWF.matches(file);
|
|
198
|
-
|
|
199
|
-
let useUploadManagerResult: ReturnType<typeof useUploadManager> | null = null;
|
|
200
|
-
|
|
201
|
-
const setProcessingCompleted = ({ uploadId }: { uploadId: string }) => {
|
|
202
|
-
unprocessedUploads.delete(uploadId);
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
const setProcessingFailed = ({
|
|
206
|
-
uploadId,
|
|
207
|
-
error,
|
|
208
|
-
}: {
|
|
209
|
-
uploadId: string;
|
|
210
|
-
error?: Error;
|
|
211
|
-
}) => {
|
|
212
|
-
unprocessedUploads.delete(uploadId);
|
|
213
|
-
useUploadManagerResult?.setFailed(
|
|
214
|
-
uploadId,
|
|
215
|
-
error ?? new Error("An error occurred when processing the file"),
|
|
216
|
-
);
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
useUploadManagerResult = useUploadManager({
|
|
220
|
-
resolveFilePartUploadURL,
|
|
221
|
-
|
|
222
|
-
onFileUploadComplete: ({ uploadId, filePartIds, parentId, name }) => {
|
|
223
|
-
promise
|
|
224
|
-
.retryPromise({ fn: () => completeUpload(uploadId, filePartIds) })
|
|
225
|
-
.then(() => {
|
|
226
|
-
options.onFileUploadComplete?.({
|
|
227
|
-
uploadId,
|
|
228
|
-
filePartIds,
|
|
229
|
-
parentId,
|
|
230
|
-
name,
|
|
231
|
-
});
|
|
232
|
-
})
|
|
233
|
-
.catch((error) => {
|
|
234
|
-
consola.error("Error attempting to complete upload", { error });
|
|
235
|
-
setProcessingFailed({ uploadId, error });
|
|
236
|
-
});
|
|
237
|
-
},
|
|
238
|
-
|
|
239
|
-
onFileUploadFailed: ({ uploadId, error, parentId }) => {
|
|
240
|
-
options?.onFileUploadFailed?.({ uploadId, error, parentId });
|
|
241
|
-
},
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
const getEnqueueableFiles = (files: File[]): File[] => {
|
|
245
|
-
const { totalPendingUploads } = useUploadManagerResult;
|
|
246
|
-
|
|
247
|
-
const { maxUploadQueueSize = DEFAULT_MAX_UPLOAD_QUEUE_SIZE } = options;
|
|
248
|
-
|
|
249
|
-
if (files.length + totalPendingUploads.value > maxUploadQueueSize) {
|
|
250
|
-
options.onUploadQueueSizeExceeded?.(maxUploadQueueSize);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// only keep as many files as the queue allows
|
|
254
|
-
return files.slice(0, maxUploadQueueSize - totalPendingUploads.value);
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
const { uploadState, ...uploadMangerResultRest } = useUploadManagerResult;
|
|
258
|
-
|
|
259
|
-
const uploadItems = computed(() => {
|
|
260
|
-
return Object.values(uploadState.value).map((uploadItem) => ({
|
|
261
|
-
...uploadItem,
|
|
262
|
-
status:
|
|
263
|
-
uploadItem.status === "complete" &&
|
|
264
|
-
unprocessedUploads.has(uploadItem.id)
|
|
265
|
-
? "processing"
|
|
266
|
-
: uploadItem.status,
|
|
267
|
-
}));
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
const prepareQueueSize = ref(0);
|
|
271
|
-
|
|
272
|
-
return {
|
|
273
|
-
...uploadMangerResultRest,
|
|
274
|
-
uploadItems,
|
|
275
|
-
unprocessedUploads: readonly(unprocessedUploads),
|
|
276
|
-
setProcessingCompleted,
|
|
277
|
-
setProcessingFailed,
|
|
278
|
-
|
|
279
|
-
isPreparingUpload: computed(() => prepareQueueSize.value > 0),
|
|
280
|
-
totalFilesBeingPrepared: computed(() => prepareQueueSize.value),
|
|
281
|
-
|
|
282
|
-
start: async (
|
|
283
|
-
parentId: string,
|
|
284
|
-
files: File[],
|
|
285
|
-
options: { isFileWithProcessing?: typeof isKNWFFile } = {},
|
|
286
|
-
) => {
|
|
287
|
-
const { isFileWithProcessing = isKNWFFile } = options;
|
|
288
|
-
const enqueableFiles = getEnqueueableFiles(files);
|
|
289
|
-
const uploadSizeLimitBytes = getUploadSizeLimitBytes();
|
|
290
|
-
const oversizedFiles = enqueableFiles.filter(
|
|
291
|
-
(file) => file.size > uploadSizeLimitBytes,
|
|
292
|
-
);
|
|
293
|
-
|
|
294
|
-
if (oversizedFiles.length > 0) {
|
|
295
|
-
throw new rfcErrors.RFCError({
|
|
296
|
-
title: "Upload size limit exceeded",
|
|
297
|
-
status: 413,
|
|
298
|
-
details: oversizedFiles.map(
|
|
299
|
-
(file) =>
|
|
300
|
-
`${file.name} (${formatFileSize(file.size)}) exceeds the upload limit of ${formatFileSize(uploadSizeLimitBytes)}.`,
|
|
301
|
-
),
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
try {
|
|
306
|
-
if (enqueableFiles.length === 0) {
|
|
307
|
-
return [];
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
prepareQueueSize.value += enqueableFiles.length;
|
|
311
|
-
|
|
312
|
-
const uploadPayload = await promise.retryPromise({
|
|
313
|
-
fn: () => prepareUpload(parentId, enqueableFiles),
|
|
314
|
-
excludeError: (error: FetchError) =>
|
|
315
|
-
// eslint-disable-next-line no-magic-numbers
|
|
316
|
-
Boolean(error.statusCode && error.statusCode < 500),
|
|
317
|
-
retryDelayMS: DEFAULT_RETRY_DELAY_MS,
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
uploadPayload.forEach(({ uploadId, file }) => {
|
|
321
|
-
if (isFileWithProcessing(file)) {
|
|
322
|
-
unprocessedUploads.add(uploadId);
|
|
323
|
-
}
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
const uploadIds = uploadPayload.map((upload) => upload.uploadId);
|
|
327
|
-
|
|
328
|
-
// accept floating promise, errors are handled inside the function
|
|
329
|
-
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
330
|
-
useUploadManagerResult.start(parentId, uploadPayload);
|
|
331
|
-
|
|
332
|
-
return uploadIds;
|
|
333
|
-
} catch (error) {
|
|
334
|
-
throw rfcErrors.tryParse(error) ?? error;
|
|
335
|
-
} finally {
|
|
336
|
-
// errors can only be thrown in the prepareUpload call, useUploadManagerResult.start does its own error handling
|
|
337
|
-
prepareQueueSize.value -= enqueableFiles.length;
|
|
338
|
-
}
|
|
339
|
-
},
|
|
340
|
-
|
|
341
|
-
cancel: (uploadId: string) => {
|
|
342
|
-
useUploadManagerResult.cancel(uploadId);
|
|
343
|
-
cancelUpload(uploadId).catch((error: unknown) => {
|
|
344
|
-
consola.error("There was a problem cancelling the upload", { error });
|
|
345
|
-
});
|
|
346
|
-
},
|
|
347
|
-
};
|
|
348
|
-
};
|