@milaboratories/pl-drivers 1.3.26 → 1.4.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/dist/clients/constructors.d.ts +14 -0
- package/dist/clients/constructors.d.ts.map +1 -0
- package/dist/clients/download.d.ts +19 -14
- package/dist/clients/download.d.ts.map +1 -1
- package/dist/clients/helpers.d.ts +4 -13
- package/dist/clients/helpers.d.ts.map +1 -1
- package/dist/clients/upload.d.ts +15 -13
- package/dist/clients/upload.d.ts.map +1 -1
- package/dist/drivers/{download_and_logs_blob.d.ts → download_blob.d.ts} +13 -40
- package/dist/drivers/download_blob.d.ts.map +1 -0
- package/dist/drivers/download_blob_task.d.ts +34 -0
- package/dist/drivers/download_blob_task.d.ts.map +1 -0
- package/dist/drivers/download_url.d.ts +11 -9
- package/dist/drivers/download_url.d.ts.map +1 -1
- package/dist/drivers/helpers/download_local_handle.d.ts +9 -0
- package/dist/drivers/helpers/download_local_handle.d.ts.map +1 -0
- package/dist/drivers/helpers/download_remote_handle.d.ts +8 -0
- package/dist/drivers/helpers/download_remote_handle.d.ts.map +1 -0
- package/dist/drivers/helpers/files_cache.d.ts +2 -1
- package/dist/drivers/helpers/files_cache.d.ts.map +1 -1
- package/dist/drivers/helpers/helpers.d.ts +2 -24
- package/dist/drivers/helpers/helpers.d.ts.map +1 -1
- package/dist/drivers/helpers/logs_handle.d.ts +13 -0
- package/dist/drivers/helpers/logs_handle.d.ts.map +1 -0
- package/dist/drivers/helpers/ls_remote_import_handle.d.ts +8 -0
- package/dist/drivers/helpers/ls_remote_import_handle.d.ts.map +1 -0
- package/dist/drivers/logs.d.ts +1 -5
- package/dist/drivers/logs.d.ts.map +1 -1
- package/dist/drivers/logs_stream.d.ts.map +1 -1
- package/dist/drivers/ls.d.ts.map +1 -1
- package/dist/drivers/types.d.ts +47 -4
- package/dist/drivers/types.d.ts.map +1 -1
- package/dist/drivers/upload.d.ts +2 -28
- package/dist/drivers/upload.d.ts.map +1 -1
- package/dist/drivers/upload_task.d.ts +41 -0
- package/dist/drivers/upload_task.d.ts.map +1 -0
- package/dist/helpers/download.d.ts +2 -2
- package/dist/helpers/download.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1537 -1526
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
- package/src/clients/constructors.ts +54 -0
- package/src/clients/download.test.ts +9 -6
- package/src/clients/download.ts +74 -64
- package/src/clients/helpers.ts +2 -53
- package/src/clients/upload.ts +136 -102
- package/src/drivers/download_blob.test.ts +12 -11
- package/src/drivers/{download_and_logs_blob.ts → download_blob.ts} +154 -290
- package/src/drivers/download_blob_task.ts +126 -0
- package/src/drivers/download_url.test.ts +1 -1
- package/src/drivers/download_url.ts +44 -37
- package/src/drivers/helpers/download_local_handle.ts +29 -0
- package/src/drivers/helpers/download_remote_handle.ts +40 -0
- package/src/drivers/helpers/files_cache.test.ts +7 -6
- package/src/drivers/helpers/files_cache.ts +6 -5
- package/src/drivers/helpers/helpers.ts +6 -100
- package/src/drivers/helpers/logs_handle.ts +52 -0
- package/src/drivers/helpers/ls_remote_import_handle.ts +43 -0
- package/src/drivers/logs.test.ts +14 -14
- package/src/drivers/logs.ts +3 -43
- package/src/drivers/logs_stream.ts +32 -6
- package/src/drivers/ls.test.ts +1 -2
- package/src/drivers/ls.ts +26 -28
- package/src/drivers/types.ts +48 -0
- package/src/drivers/upload.test.ts +8 -18
- package/src/drivers/upload.ts +38 -271
- package/src/drivers/upload_task.ts +251 -0
- package/src/helpers/download.ts +18 -15
- package/src/index.ts +2 -2
- package/dist/drivers/download_and_logs_blob.d.ts.map +0 -1
- package/dist/drivers/helpers/ls_list_entry.d.ts +0 -44
- package/dist/drivers/helpers/ls_list_entry.d.ts.map +0 -1
- package/src/drivers/helpers/ls_list_entry.test.ts +0 -55
- package/src/drivers/helpers/ls_list_entry.ts +0 -147
package/src/drivers/upload.ts
CHANGED
|
@@ -1,56 +1,28 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { ResourceId,
|
|
2
|
+
import { ResourceId, ResourceType } from '@milaboratories/pl-client';
|
|
3
3
|
import {
|
|
4
4
|
Watcher,
|
|
5
|
-
ChangeSource,
|
|
6
5
|
ComputableCtx,
|
|
7
6
|
Computable,
|
|
8
7
|
PollingComputableHooks
|
|
9
8
|
} from '@milaboratories/computable';
|
|
10
|
-
import {
|
|
11
|
-
MiLogger,
|
|
12
|
-
asyncPool,
|
|
13
|
-
TaskProcessor,
|
|
14
|
-
CallersCounter,
|
|
15
|
-
Signer
|
|
16
|
-
} from '@milaboratories/ts-helpers';
|
|
9
|
+
import { MiLogger, asyncPool, TaskProcessor, Signer } from '@milaboratories/ts-helpers';
|
|
17
10
|
import * as sdk from '@milaboratories/pl-model-common';
|
|
18
|
-
import {
|
|
19
|
-
import { ClientUpload
|
|
11
|
+
import { ClientProgress } from '../clients/progress';
|
|
12
|
+
import { ClientUpload } from '../clients/upload';
|
|
20
13
|
import {
|
|
21
|
-
InferSnapshot,
|
|
22
14
|
isPlTreeEntry,
|
|
23
15
|
isPlTreeEntryAccessor,
|
|
24
16
|
makeResourceSnapshot,
|
|
25
17
|
PlTreeEntry,
|
|
26
18
|
PlTreeEntryAccessor,
|
|
27
|
-
PlTreeNodeAccessor
|
|
28
|
-
rsSchema
|
|
19
|
+
PlTreeNodeAccessor
|
|
29
20
|
} from '@milaboratories/pl-tree';
|
|
30
21
|
import { scheduler } from 'node:timers/promises';
|
|
31
22
|
import { PollingOps } from './helpers/polling_ops';
|
|
32
|
-
import {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
/** ResourceSnapshot that can be passed to GetProgressID */
|
|
37
|
-
export const UploadResourceSnapshot = rsSchema({
|
|
38
|
-
data: ImportFileHandleUploadData,
|
|
39
|
-
fields: {
|
|
40
|
-
blob: false
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
export const IndexResourceSnapshot = rsSchema({
|
|
45
|
-
fields: {
|
|
46
|
-
incarnation: false
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
export type UploadResourceSnapshot = InferSnapshot<typeof UploadResourceSnapshot>;
|
|
51
|
-
export type IndexResourceSnapshot = InferSnapshot<typeof IndexResourceSnapshot>;
|
|
52
|
-
|
|
53
|
-
export type ImportResourceSnapshot = UploadResourceSnapshot | IndexResourceSnapshot;
|
|
23
|
+
import { ImportResourceSnapshot, IndexResourceSnapshot, UploadResourceSnapshot } from './types';
|
|
24
|
+
import { nonRecoverableError, UploadTask } from './upload_task';
|
|
25
|
+
import { WrongResourceTypeError } from './helpers/helpers';
|
|
54
26
|
|
|
55
27
|
export function makeBlobImportSnapshot(
|
|
56
28
|
entryOrAccessor: PlTreeEntry | PlTreeNodeAccessor | PlTreeEntryAccessor,
|
|
@@ -61,9 +33,10 @@ export function makeBlobImportSnapshot(
|
|
|
61
33
|
: isPlTreeEntryAccessor(entryOrAccessor)
|
|
62
34
|
? entryOrAccessor.node()
|
|
63
35
|
: entryOrAccessor;
|
|
36
|
+
|
|
64
37
|
if (node.resourceType.name.startsWith('BlobUpload'))
|
|
65
38
|
return makeResourceSnapshot(node, UploadResourceSnapshot);
|
|
66
|
-
|
|
39
|
+
return makeResourceSnapshot(node, IndexResourceSnapshot);
|
|
67
40
|
}
|
|
68
41
|
|
|
69
42
|
export type UploadDriverOps = PollingOps & {
|
|
@@ -81,7 +54,7 @@ export type UploadDriverOps = PollingOps & {
|
|
|
81
54
|
* Handles both Index and Upload blobs,
|
|
82
55
|
* the client needs to pass concrete blobs from `handle` field. */
|
|
83
56
|
export class UploadDriver {
|
|
84
|
-
private readonly idToProgress: Map<ResourceId,
|
|
57
|
+
private readonly idToProgress: Map<ResourceId, UploadTask> = new Map();
|
|
85
58
|
|
|
86
59
|
/** Holds a queue that upload blobs. */
|
|
87
60
|
private readonly uploadQueue: TaskProcessor;
|
|
@@ -138,11 +111,6 @@ export class UploadDriver {
|
|
|
138
111
|
ctx.addOnDestroy(() => this.release(rInfo.id, callerId));
|
|
139
112
|
|
|
140
113
|
const result = this.getProgressIdNoCtx(ctx.watcher, rInfo, callerId);
|
|
141
|
-
if (!isProgressStable(result)) {
|
|
142
|
-
ctx.markUnstable(
|
|
143
|
-
`upload/index progress was got, but it's not stable: ${JSON.stringify(result)}`
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
114
|
|
|
147
115
|
return result;
|
|
148
116
|
}
|
|
@@ -152,17 +120,16 @@ export class UploadDriver {
|
|
|
152
120
|
res: ImportResourceSnapshot,
|
|
153
121
|
callerId: string
|
|
154
122
|
): sdk.ImportProgress {
|
|
155
|
-
|
|
156
|
-
'blob' in res.fields ? res.fields.blob !== undefined : res.fields.incarnation !== undefined;
|
|
123
|
+
validateResourceType('getProgressId', res.type);
|
|
157
124
|
|
|
158
|
-
const
|
|
125
|
+
const task = this.idToProgress.get(res.id);
|
|
159
126
|
|
|
160
|
-
if (
|
|
161
|
-
|
|
162
|
-
return
|
|
127
|
+
if (task != undefined) {
|
|
128
|
+
task.setDoneIfOutputSet(res);
|
|
129
|
+
return task.getProgress(w, callerId);
|
|
163
130
|
}
|
|
164
131
|
|
|
165
|
-
const
|
|
132
|
+
const newTask = new UploadTask(
|
|
166
133
|
this.logger,
|
|
167
134
|
this.clientBlob,
|
|
168
135
|
this.clientProgress,
|
|
@@ -171,24 +138,24 @@ export class UploadDriver {
|
|
|
171
138
|
res
|
|
172
139
|
);
|
|
173
140
|
|
|
174
|
-
this.idToProgress.set(res.id,
|
|
175
|
-
newValue.attach(w, callerId);
|
|
141
|
+
this.idToProgress.set(res.id, newTask);
|
|
176
142
|
|
|
177
|
-
if (
|
|
143
|
+
if (newTask.shouldScheduleUpload())
|
|
178
144
|
this.uploadQueue.push({
|
|
179
|
-
fn: () =>
|
|
145
|
+
fn: () => newTask.uploadBlobTask(),
|
|
180
146
|
recoverableErrorPredicate: (e) => !nonRecoverableError(e)
|
|
181
147
|
});
|
|
182
148
|
|
|
183
|
-
|
|
149
|
+
newTask.setDoneIfOutputSet(res);
|
|
150
|
+
return newTask.getProgress(w, callerId);
|
|
184
151
|
}
|
|
185
152
|
|
|
186
153
|
/** Decrement counters for the file and remove an uploading if counter == 0. */
|
|
187
154
|
private async release(id: ResourceId, callerId: string) {
|
|
188
|
-
const
|
|
189
|
-
if (
|
|
155
|
+
const task = this.idToProgress.get(id);
|
|
156
|
+
if (task === undefined) return;
|
|
190
157
|
|
|
191
|
-
const deleted =
|
|
158
|
+
const deleted = task.decCounter(callerId);
|
|
192
159
|
if (deleted) this.idToProgress.delete(id);
|
|
193
160
|
}
|
|
194
161
|
|
|
@@ -243,227 +210,27 @@ export class UploadDriver {
|
|
|
243
210
|
this.currentLoop = undefined;
|
|
244
211
|
}
|
|
245
212
|
|
|
246
|
-
private getAllNotDoneProgresses(): Array<
|
|
213
|
+
private getAllNotDoneProgresses(): Array<UploadTask> {
|
|
247
214
|
return Array.from(this.idToProgress.entries())
|
|
248
|
-
.filter(([_, p]) => !
|
|
215
|
+
.filter(([_, p]) => !isProgressDone(p.progress))
|
|
249
216
|
.map(([_, p]) => p);
|
|
250
217
|
}
|
|
251
218
|
}
|
|
252
219
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
* And holds a change source. */
|
|
256
|
-
class ProgressUpdater {
|
|
257
|
-
private readonly change: ChangeSource = new ChangeSource();
|
|
258
|
-
private readonly counter: CallersCounter = new CallersCounter();
|
|
259
|
-
|
|
260
|
-
public progress: sdk.ImportProgress;
|
|
261
|
-
/** If this is upload progress this field will be defined */
|
|
262
|
-
private uploadData?: ImportFileHandleUploadData;
|
|
263
|
-
public uploadingTerminallyFailed?: boolean;
|
|
264
|
-
|
|
265
|
-
constructor(
|
|
266
|
-
private readonly logger: MiLogger,
|
|
267
|
-
private readonly clientBlob: ClientUpload,
|
|
268
|
-
private readonly clientProgress: ClientProgress,
|
|
269
|
-
private readonly nConcurrentPartsUpload: number,
|
|
270
|
-
signer: Signer,
|
|
271
|
-
public readonly res: ImportResourceSnapshot
|
|
272
|
-
) {
|
|
273
|
-
const isUpload = res.type.name.startsWith('BlobUpload');
|
|
274
|
-
let isUploadSignMatch: boolean | undefined;
|
|
275
|
-
if (isUpload) {
|
|
276
|
-
this.uploadData = ImportFileHandleUploadData.parse(res.data);
|
|
277
|
-
isUploadSignMatch = isSignMatch(
|
|
278
|
-
signer,
|
|
279
|
-
this.uploadData.localPath,
|
|
280
|
-
this.uploadData.pathSignature
|
|
281
|
-
);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
this.progress = {
|
|
285
|
-
done: false,
|
|
286
|
-
status: undefined,
|
|
287
|
-
isUpload: isUpload,
|
|
288
|
-
isUploadSignMatch: isUploadSignMatch,
|
|
289
|
-
lastError: undefined
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
public mustGetProgress(blobExists: boolean) {
|
|
294
|
-
// We provide a deep copy of progress,
|
|
295
|
-
// since we do not want to pass a mutable object
|
|
296
|
-
// to API, it led to bugs before.
|
|
297
|
-
|
|
298
|
-
// We do not use '...' cloning syntax
|
|
299
|
-
// for the compiler to fail here if we change API.
|
|
300
|
-
const progress: sdk.ImportProgress = {
|
|
301
|
-
done: this.progress.done,
|
|
302
|
-
isUpload: this.progress.isUpload,
|
|
303
|
-
isUploadSignMatch: this.progress.isUploadSignMatch,
|
|
304
|
-
lastError: this.progress.lastError
|
|
305
|
-
};
|
|
306
|
-
if (this.progress.status)
|
|
307
|
-
progress.status = {
|
|
308
|
-
progress: this.progress.status.progress,
|
|
309
|
-
bytesProcessed: this.progress.status.bytesProcessed,
|
|
310
|
-
bytesTotal: this.progress.status.bytesTotal
|
|
311
|
-
};
|
|
312
|
-
|
|
313
|
-
if (blobExists) {
|
|
314
|
-
this.setDone(blobExists);
|
|
315
|
-
return progress;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
if (this.uploadingTerminallyFailed) {
|
|
319
|
-
this.logger.error(`Uploading terminally failed: ${this.progress.lastError}`);
|
|
320
|
-
throw new Error(this.progress.lastError);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
return progress;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
public attach(w: Watcher, callerId: string) {
|
|
327
|
-
this.change.attachWatcher(w);
|
|
328
|
-
this.counter.inc(callerId);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
public decCounter(callerId: string) {
|
|
332
|
-
return this.counter.dec(callerId);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/** Uploads a blob if it's not BlobIndex. */
|
|
336
|
-
async uploadBlobTask() {
|
|
337
|
-
try {
|
|
338
|
-
await this.uploadBlob();
|
|
339
|
-
} catch (e: any) {
|
|
340
|
-
this.setLastError(e);
|
|
341
|
-
|
|
342
|
-
if (isResourceWasDeletedError(e)) {
|
|
343
|
-
this.logger.warn(`resource was deleted while uploading a blob: ${e}`);
|
|
344
|
-
this.change.markChanged();
|
|
345
|
-
this.setDone(true);
|
|
346
|
-
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
this.logger.error(`error while uploading a blob: ${e}`);
|
|
351
|
-
this.change.markChanged();
|
|
352
|
-
|
|
353
|
-
if (nonRecoverableError(e)) this.terminateWithError(e);
|
|
354
|
-
|
|
355
|
-
throw e;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/** Uploads a blob using client. */
|
|
360
|
-
private async uploadBlob() {
|
|
361
|
-
if (this.counter.isZero()) return;
|
|
362
|
-
const parts = await this.clientBlob.initUpload(this.res);
|
|
363
|
-
|
|
364
|
-
this.logger.info(`start to upload blob ${this.res.id}, parts count: ${parts.length}`);
|
|
365
|
-
|
|
366
|
-
const partUploadFn = (part: bigint) => async () => {
|
|
367
|
-
if (this.counter.isZero()) return;
|
|
368
|
-
await this.clientBlob.partUpload(
|
|
369
|
-
this.res,
|
|
370
|
-
this.uploadData!.localPath,
|
|
371
|
-
part,
|
|
372
|
-
parts.length,
|
|
373
|
-
BigInt(this.uploadData!.modificationTime)
|
|
374
|
-
);
|
|
375
|
-
};
|
|
376
|
-
|
|
377
|
-
await asyncPool(this.nConcurrentPartsUpload, parts.map(partUploadFn));
|
|
378
|
-
|
|
379
|
-
if (this.counter.isZero()) return;
|
|
380
|
-
await this.clientBlob.finalizeUpload(this.res);
|
|
381
|
-
|
|
382
|
-
this.logger.info(`uploading of resource ${this.res.id} finished.`);
|
|
383
|
-
this.change.markChanged();
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
private terminateWithError(e: unknown) {
|
|
387
|
-
this.progress.lastError = String(e);
|
|
388
|
-
this.progress.done = false;
|
|
389
|
-
this.uploadingTerminallyFailed = true;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
private setLastError(e: unknown) {
|
|
393
|
-
this.progress.lastError = String(e);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
private setDone(done: boolean) {
|
|
397
|
-
this.progress.done = done;
|
|
398
|
-
if (done) this.progress.lastError = undefined;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
async updateStatus() {
|
|
402
|
-
try {
|
|
403
|
-
const status = await this.clientProgress.getStatus(this.res);
|
|
404
|
-
|
|
405
|
-
const oldStatus = this.progress.status;
|
|
406
|
-
this.progress.status = protoToStatus(status);
|
|
407
|
-
this.setDone(status.done);
|
|
408
|
-
|
|
409
|
-
if (status.done || status.progress != oldStatus?.progress) this.change.markChanged();
|
|
410
|
-
} catch (e: any) {
|
|
411
|
-
this.setLastError(e);
|
|
412
|
-
|
|
413
|
-
if (e.name == 'RpcError' && e.code == 'DEADLINE_EXCEEDED') {
|
|
414
|
-
this.logger.warn(`deadline exceeded while getting a status of BlobImport`);
|
|
415
|
-
return;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
if (isResourceWasDeletedError(e)) {
|
|
419
|
-
this.logger.warn(
|
|
420
|
-
`resource was not found while updating a status of BlobImport: ${e}, ${stringifyWithResourceId(this.res)}`
|
|
421
|
-
);
|
|
422
|
-
this.change.markChanged();
|
|
423
|
-
this.setDone(true);
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
this.logger.error(`error while updating a status of BlobImport: ${e}`);
|
|
428
|
-
this.change.markChanged();
|
|
429
|
-
this.terminateWithError(e);
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
function isProgressStable(p: sdk.ImportProgress) {
|
|
435
|
-
return p.done && p.status !== undefined && p.status !== null && p.status.progress >= 1.0;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
function protoToStatus(proto: ProgressStatus): sdk.ImportStatus {
|
|
439
|
-
return {
|
|
440
|
-
progress: proto.progress ?? 0,
|
|
441
|
-
bytesProcessed: Number(proto.bytesProcessed),
|
|
442
|
-
bytesTotal: Number(proto.bytesTotal)
|
|
443
|
-
};
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
function isSignMatch(signer: Signer, path: string, signature: string): boolean {
|
|
447
|
-
try {
|
|
448
|
-
signer.verify(path, signature);
|
|
449
|
-
return true;
|
|
450
|
-
} catch (e) {
|
|
451
|
-
return false;
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
function nonRecoverableError(e: any) {
|
|
456
|
-
return e instanceof MTimeError || e instanceof UnexpectedEOF || e instanceof NoFileForUploading;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
function isResourceWasDeletedError(e: any) {
|
|
460
|
-
return (
|
|
461
|
-
e.name == 'RpcError' &&
|
|
462
|
-
(e.code == 'NOT_FOUND' || e.code == 'ABORTED' || e.code == 'ALREADY_EXISTS')
|
|
463
|
-
);
|
|
220
|
+
function isProgressDone(p: sdk.ImportProgress) {
|
|
221
|
+
return p.done && (p.status?.progress ?? 0.0) >= 1.0;
|
|
464
222
|
}
|
|
465
223
|
|
|
466
224
|
type ScheduledRefresh = {
|
|
467
225
|
resolve: () => void;
|
|
468
226
|
reject: (err: any) => void;
|
|
469
227
|
};
|
|
228
|
+
|
|
229
|
+
function validateResourceType(methodName: string, rType: ResourceType) {
|
|
230
|
+
if (!rType.name.startsWith('BlobUpload') && !rType.name.startsWith('BlobIndex')) {
|
|
231
|
+
throw new WrongResourceTypeError(
|
|
232
|
+
`${methodName}: wrong resource type: ${rType.name}, ` +
|
|
233
|
+
`expected: a resource of either type 'BlobUpload' or 'BlobIndex'.`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { ChangeSource, Watcher } from '@milaboratories/computable';
|
|
2
|
+
import { stringifyWithResourceId } from '@milaboratories/pl-client';
|
|
3
|
+
import * as sdk from '@milaboratories/pl-model-common';
|
|
4
|
+
import { asyncPool, CallersCounter, MiLogger, Signer } from '@milaboratories/ts-helpers';
|
|
5
|
+
import { ClientProgress, ProgressStatus } from '../clients/progress';
|
|
6
|
+
import { ClientUpload, MTimeError, NoFileForUploading, UnexpectedEOF } from '../clients/upload';
|
|
7
|
+
import { ImportFileHandleUploadData, ImportResourceSnapshot } from './types';
|
|
8
|
+
import assert from 'node:assert';
|
|
9
|
+
|
|
10
|
+
/** Holds all info needed to upload a file and a status of uploading
|
|
11
|
+
* and indexing. Also, has a method to update a status of the progress.
|
|
12
|
+
* And holds a change source. */
|
|
13
|
+
export class UploadTask {
|
|
14
|
+
private readonly change: ChangeSource = new ChangeSource();
|
|
15
|
+
private readonly counter: CallersCounter = new CallersCounter();
|
|
16
|
+
|
|
17
|
+
/** If this is upload progress this field will be defined */
|
|
18
|
+
private uploadData?: ImportFileHandleUploadData;
|
|
19
|
+
public progress: sdk.ImportProgress;
|
|
20
|
+
|
|
21
|
+
/** If failed, then getting a progress is terminally failed. */
|
|
22
|
+
public failed?: boolean;
|
|
23
|
+
|
|
24
|
+
constructor(
|
|
25
|
+
private readonly logger: MiLogger,
|
|
26
|
+
private readonly clientBlob: ClientUpload,
|
|
27
|
+
private readonly clientProgress: ClientProgress,
|
|
28
|
+
private readonly nConcurrentPartsUpload: number,
|
|
29
|
+
signer: Signer,
|
|
30
|
+
public readonly res: ImportResourceSnapshot
|
|
31
|
+
) {
|
|
32
|
+
const { uploadData, progress } = newProgress(res, signer);
|
|
33
|
+
this.uploadData = uploadData;
|
|
34
|
+
this.progress = progress;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public getProgress(w: Watcher, callerId: string) {
|
|
38
|
+
this.incCounter(w, callerId);
|
|
39
|
+
|
|
40
|
+
if (this.failed) {
|
|
41
|
+
this.logger.error(`Uploading terminally failed: ${this.progress.lastError}`);
|
|
42
|
+
throw new Error(this.progress.lastError);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return cloneProgress(this.progress);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public shouldScheduleUpload(): boolean {
|
|
49
|
+
return this.progress.isUpload && this.progress.isUploadSignMatch!;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Uploads a blob if it's not BlobIndex. */
|
|
53
|
+
public async uploadBlobTask() {
|
|
54
|
+
assert(isUpload(this.res), 'the upload operation can be done only for BlobUploads');
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
if (this.isComputableDone()) return;
|
|
58
|
+
const parts = await this.clientBlob.initUpload(this.res);
|
|
59
|
+
this.logger.info(
|
|
60
|
+
`started to upload blob ${this.res.id},` +
|
|
61
|
+
` parts overall: ${parts.overall}, parts remained: ${parts.toUpload.length}`
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const partUploadFn = (part: bigint) => async () => {
|
|
65
|
+
if (this.isComputableDone()) return;
|
|
66
|
+
await this.clientBlob.partUpload(
|
|
67
|
+
this.res,
|
|
68
|
+
this.uploadData!.localPath,
|
|
69
|
+
BigInt(this.uploadData!.modificationTime),
|
|
70
|
+
part
|
|
71
|
+
);
|
|
72
|
+
this.logger.info(`uploaded chunk ${part}/${parts.overall} of resource: ${this.res.id}`);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
await asyncPool(this.nConcurrentPartsUpload, parts.toUpload.map(partUploadFn));
|
|
76
|
+
|
|
77
|
+
if (this.isComputableDone()) return;
|
|
78
|
+
await this.clientBlob.finalize(this.res);
|
|
79
|
+
|
|
80
|
+
this.logger.info(`uploading of resource ${this.res.id} finished.`);
|
|
81
|
+
this.change.markChanged();
|
|
82
|
+
} catch (e: any) {
|
|
83
|
+
this.setRetriableError(e);
|
|
84
|
+
|
|
85
|
+
if (isResourceWasDeletedError(e)) {
|
|
86
|
+
this.logger.warn(`resource was deleted while uploading a blob: ${e}`);
|
|
87
|
+
this.change.markChanged();
|
|
88
|
+
this.setDone(true);
|
|
89
|
+
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
this.logger.error(`error while uploading a blob: ${e}`);
|
|
94
|
+
this.change.markChanged();
|
|
95
|
+
|
|
96
|
+
if (nonRecoverableError(e)) {
|
|
97
|
+
this.setTerminalError(e);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
throw e;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
public async updateStatus() {
|
|
106
|
+
try {
|
|
107
|
+
const status = await this.clientProgress.getStatus(this.res);
|
|
108
|
+
|
|
109
|
+
const oldStatus = this.progress.status;
|
|
110
|
+
this.progress.status = protoToStatus(status);
|
|
111
|
+
this.setDone(status.done);
|
|
112
|
+
|
|
113
|
+
if (status.done || status.progress != oldStatus?.progress) this.change.markChanged();
|
|
114
|
+
} catch (e: any) {
|
|
115
|
+
this.setRetriableError(e);
|
|
116
|
+
|
|
117
|
+
if (e.name == 'RpcError' && e.code == 'DEADLINE_EXCEEDED') {
|
|
118
|
+
this.logger.warn(`deadline exceeded while getting a status of BlobImport`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (isResourceWasDeletedError(e)) {
|
|
123
|
+
this.logger.warn(
|
|
124
|
+
`resource was not found while updating a status of BlobImport: ${e}, ${stringifyWithResourceId(this.res)}`
|
|
125
|
+
);
|
|
126
|
+
this.change.markChanged();
|
|
127
|
+
this.setDone(true);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
this.logger.error(`error while updating a status of BlobImport: ${e}`);
|
|
132
|
+
this.change.markChanged();
|
|
133
|
+
this.setTerminalError(e);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Set non-terminal error, that task can be retried. */
|
|
138
|
+
private setRetriableError(e: unknown) {
|
|
139
|
+
this.progress.lastError = String(e);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Set a terminal error, the task will throw a error instead of a progress. */
|
|
143
|
+
private setTerminalError(e: unknown) {
|
|
144
|
+
this.progress.lastError = String(e);
|
|
145
|
+
this.progress.done = false;
|
|
146
|
+
this.failed = true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
public setDoneIfOutputSet(res: ImportResourceSnapshot) {
|
|
150
|
+
if (isImportResourceOutputSet(res)) this.setDone(true);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private setDone(done: boolean) {
|
|
154
|
+
this.progress.done = done;
|
|
155
|
+
if (done) this.progress.lastError = undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
public incCounter(w: Watcher, callerId: string) {
|
|
159
|
+
this.change.attachWatcher(w);
|
|
160
|
+
this.counter.inc(callerId);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
public decCounter(callerId: string) {
|
|
164
|
+
return this.counter.dec(callerId);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private isComputableDone() {
|
|
168
|
+
return this.counter.isZero();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function newProgress(res: ImportResourceSnapshot, signer: Signer) {
|
|
173
|
+
let isUploadSignMatch: boolean | undefined;
|
|
174
|
+
let uploadData: ImportFileHandleUploadData | undefined;
|
|
175
|
+
if (isUpload(res)) {
|
|
176
|
+
uploadData = ImportFileHandleUploadData.parse(res.data);
|
|
177
|
+
isUploadSignMatch = isSignMatch(signer, uploadData.localPath, uploadData.pathSignature);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
uploadData: uploadData,
|
|
182
|
+
progress: {
|
|
183
|
+
done: false,
|
|
184
|
+
status: undefined,
|
|
185
|
+
isUpload: isUpload(res),
|
|
186
|
+
isUploadSignMatch: isUploadSignMatch,
|
|
187
|
+
lastError: undefined
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Creates a deep copy of progress,
|
|
193
|
+
* since we do not want to pass a mutable object
|
|
194
|
+
* to API, it led to bugs before.
|
|
195
|
+
* We do not use '...' cloning syntax
|
|
196
|
+
* for the compiler to fail here if we change API. */
|
|
197
|
+
function cloneProgress(progress: sdk.ImportProgress): sdk.ImportProgress {
|
|
198
|
+
const cloned: sdk.ImportProgress = {
|
|
199
|
+
done: progress.done,
|
|
200
|
+
isUpload: progress.isUpload,
|
|
201
|
+
isUploadSignMatch: progress.isUploadSignMatch,
|
|
202
|
+
lastError: progress.lastError
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
if (progress.status)
|
|
206
|
+
cloned.status = {
|
|
207
|
+
progress: progress.status.progress,
|
|
208
|
+
bytesProcessed: progress.status.bytesProcessed,
|
|
209
|
+
bytesTotal: progress.status.bytesTotal
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
return progress;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function isImportResourceOutputSet(res: ImportResourceSnapshot) {
|
|
216
|
+
return 'blob' in res.fields
|
|
217
|
+
? res.fields.blob !== undefined
|
|
218
|
+
: res.fields.incarnation !== undefined;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function isUpload(res: ImportResourceSnapshot) {
|
|
222
|
+
return res.type.name.startsWith('BlobUpload');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function isSignMatch(signer: Signer, path: string, signature: string): boolean {
|
|
226
|
+
try {
|
|
227
|
+
signer.verify(path, signature);
|
|
228
|
+
return true;
|
|
229
|
+
} catch (e) {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function protoToStatus(proto: ProgressStatus): sdk.ImportStatus {
|
|
235
|
+
return {
|
|
236
|
+
progress: proto.progress ?? 0,
|
|
237
|
+
bytesProcessed: Number(proto.bytesProcessed),
|
|
238
|
+
bytesTotal: Number(proto.bytesTotal)
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function isResourceWasDeletedError(e: any) {
|
|
243
|
+
return (
|
|
244
|
+
e.name == 'RpcError' &&
|
|
245
|
+
(e.code == 'NOT_FOUND' || e.code == 'ABORTED' || e.code == 'ALREADY_EXISTS')
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function nonRecoverableError(e: any) {
|
|
250
|
+
return e instanceof MTimeError || e instanceof UnexpectedEOF || e instanceof NoFileForUploading;
|
|
251
|
+
}
|
package/src/helpers/download.ts
CHANGED
|
@@ -11,10 +11,10 @@ export interface DownloadResponse {
|
|
|
11
11
|
/** Throws when a status code of the downloading URL was in range [400, 500). */
|
|
12
12
|
export class NetworkError400 extends Error {}
|
|
13
13
|
|
|
14
|
-
export class
|
|
14
|
+
export class RemoteFileDownloader {
|
|
15
15
|
constructor(public readonly httpClient: Dispatcher) {}
|
|
16
16
|
|
|
17
|
-
async
|
|
17
|
+
async download(
|
|
18
18
|
url: string,
|
|
19
19
|
reqHeaders: Record<string, string>,
|
|
20
20
|
signal?: AbortSignal
|
|
@@ -26,19 +26,7 @@ export class DownloadHelper {
|
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
const webBody = Readable.toWeb(body);
|
|
29
|
-
|
|
30
|
-
if (statusCode != 200) {
|
|
31
|
-
const textBody = await text(webBody);
|
|
32
|
-
const beginning = textBody.substring(0, Math.min(textBody.length, 1000));
|
|
33
|
-
|
|
34
|
-
if (400 <= statusCode && statusCode < 500) {
|
|
35
|
-
throw new NetworkError400(
|
|
36
|
-
`Http error: statusCode: ${statusCode} url: ${url.toString()}, beginning of body: ${beginning}`
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
throw new Error(`Http error: statusCode: ${statusCode} url: ${url.toString()}`);
|
|
41
|
-
}
|
|
29
|
+
await checkStatusCodeOk(statusCode, webBody, url);
|
|
42
30
|
|
|
43
31
|
return {
|
|
44
32
|
content: webBody,
|
|
@@ -46,3 +34,18 @@ export class DownloadHelper {
|
|
|
46
34
|
};
|
|
47
35
|
}
|
|
48
36
|
}
|
|
37
|
+
|
|
38
|
+
async function checkStatusCodeOk(statusCode: number, webBody: ReadableStream<any>, url: string) {
|
|
39
|
+
if (statusCode != 200) {
|
|
40
|
+
const beginning = (await text(webBody)).substring(0, 1000);
|
|
41
|
+
|
|
42
|
+
if (400 <= statusCode && statusCode < 500) {
|
|
43
|
+
throw new NetworkError400(
|
|
44
|
+
`Http error: statusCode: ${statusCode} ` +
|
|
45
|
+
`url: ${url.toString()}, beginning of body: ${beginning}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
throw new Error(`Http error: statusCode: ${statusCode} url: ${url.toString()}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|