@milaboratories/pl-drivers 1.3.26 → 1.5.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.
Files changed (78) hide show
  1. package/dist/clients/constructors.d.ts +14 -0
  2. package/dist/clients/constructors.d.ts.map +1 -0
  3. package/dist/clients/download.d.ts +19 -14
  4. package/dist/clients/download.d.ts.map +1 -1
  5. package/dist/clients/helpers.d.ts +4 -13
  6. package/dist/clients/helpers.d.ts.map +1 -1
  7. package/dist/clients/upload.d.ts +15 -13
  8. package/dist/clients/upload.d.ts.map +1 -1
  9. package/dist/drivers/{download_and_logs_blob.d.ts → download_blob.d.ts} +13 -40
  10. package/dist/drivers/download_blob.d.ts.map +1 -0
  11. package/dist/drivers/download_blob_task.d.ts +34 -0
  12. package/dist/drivers/download_blob_task.d.ts.map +1 -0
  13. package/dist/drivers/download_url.d.ts +11 -9
  14. package/dist/drivers/download_url.d.ts.map +1 -1
  15. package/dist/drivers/helpers/download_local_handle.d.ts +9 -0
  16. package/dist/drivers/helpers/download_local_handle.d.ts.map +1 -0
  17. package/dist/drivers/helpers/download_remote_handle.d.ts +8 -0
  18. package/dist/drivers/helpers/download_remote_handle.d.ts.map +1 -0
  19. package/dist/drivers/helpers/files_cache.d.ts +2 -1
  20. package/dist/drivers/helpers/files_cache.d.ts.map +1 -1
  21. package/dist/drivers/helpers/helpers.d.ts +2 -24
  22. package/dist/drivers/helpers/helpers.d.ts.map +1 -1
  23. package/dist/drivers/helpers/logs_handle.d.ts +13 -0
  24. package/dist/drivers/helpers/logs_handle.d.ts.map +1 -0
  25. package/dist/drivers/helpers/ls_remote_import_handle.d.ts +8 -0
  26. package/dist/drivers/helpers/ls_remote_import_handle.d.ts.map +1 -0
  27. package/dist/drivers/logs.d.ts +1 -5
  28. package/dist/drivers/logs.d.ts.map +1 -1
  29. package/dist/drivers/logs_stream.d.ts.map +1 -1
  30. package/dist/drivers/ls.d.ts.map +1 -1
  31. package/dist/drivers/types.d.ts +47 -4
  32. package/dist/drivers/types.d.ts.map +1 -1
  33. package/dist/drivers/upload.d.ts +2 -28
  34. package/dist/drivers/upload.d.ts.map +1 -1
  35. package/dist/drivers/upload_task.d.ts +41 -0
  36. package/dist/drivers/upload_task.d.ts.map +1 -0
  37. package/dist/helpers/download.d.ts +2 -2
  38. package/dist/helpers/download.d.ts.map +1 -1
  39. package/dist/index.d.ts +2 -2
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +2 -2
  42. package/dist/index.js.map +1 -1
  43. package/dist/index.mjs +1537 -1526
  44. package/dist/index.mjs.map +1 -1
  45. package/package.json +7 -7
  46. package/src/clients/constructors.ts +54 -0
  47. package/src/clients/download.test.ts +9 -6
  48. package/src/clients/download.ts +74 -64
  49. package/src/clients/helpers.ts +2 -53
  50. package/src/clients/upload.ts +136 -102
  51. package/src/drivers/download_blob.test.ts +12 -11
  52. package/src/drivers/{download_and_logs_blob.ts → download_blob.ts} +154 -290
  53. package/src/drivers/download_blob_task.ts +126 -0
  54. package/src/drivers/download_url.test.ts +1 -1
  55. package/src/drivers/download_url.ts +44 -37
  56. package/src/drivers/helpers/download_local_handle.ts +29 -0
  57. package/src/drivers/helpers/download_remote_handle.ts +40 -0
  58. package/src/drivers/helpers/files_cache.test.ts +7 -6
  59. package/src/drivers/helpers/files_cache.ts +6 -5
  60. package/src/drivers/helpers/helpers.ts +6 -100
  61. package/src/drivers/helpers/logs_handle.ts +52 -0
  62. package/src/drivers/helpers/ls_remote_import_handle.ts +43 -0
  63. package/src/drivers/logs.test.ts +14 -14
  64. package/src/drivers/logs.ts +3 -43
  65. package/src/drivers/logs_stream.ts +32 -6
  66. package/src/drivers/ls.test.ts +1 -2
  67. package/src/drivers/ls.ts +26 -28
  68. package/src/drivers/types.ts +48 -0
  69. package/src/drivers/upload.test.ts +8 -18
  70. package/src/drivers/upload.ts +38 -271
  71. package/src/drivers/upload_task.ts +251 -0
  72. package/src/helpers/download.ts +18 -15
  73. package/src/index.ts +2 -2
  74. package/dist/drivers/download_and_logs_blob.d.ts.map +0 -1
  75. package/dist/drivers/helpers/ls_list_entry.d.ts +0 -44
  76. package/dist/drivers/helpers/ls_list_entry.d.ts.map +0 -1
  77. package/src/drivers/helpers/ls_list_entry.test.ts +0 -55
  78. package/src/drivers/helpers/ls_list_entry.ts +0 -147
@@ -1,56 +1,28 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { ResourceId, stringifyWithResourceId } from '@milaboratories/pl-client';
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 { ProgressStatus, ClientProgress } from '../clients/progress';
19
- import { ClientUpload, MTimeError, NoFileForUploading, UnexpectedEOF } from '../clients/upload';
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 { ImportFileHandleUploadData } from './types';
33
-
34
- /** Options from BlobUpload resource that have to be passed to getProgress. */
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
- else return makeResourceSnapshot(node, IndexResourceSnapshot);
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, ProgressUpdater> = new Map();
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
- const blobExists =
156
- 'blob' in res.fields ? res.fields.blob !== undefined : res.fields.incarnation !== undefined;
123
+ validateResourceType('getProgressId', res.type);
157
124
 
158
- const value = this.idToProgress.get(res.id);
125
+ const task = this.idToProgress.get(res.id);
159
126
 
160
- if (value != undefined) {
161
- value.attach(w, callerId);
162
- return value.mustGetProgress(blobExists);
127
+ if (task != undefined) {
128
+ task.setDoneIfOutputSet(res);
129
+ return task.getProgress(w, callerId);
163
130
  }
164
131
 
165
- const newValue = new ProgressUpdater(
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, newValue);
175
- newValue.attach(w, callerId);
141
+ this.idToProgress.set(res.id, newTask);
176
142
 
177
- if (newValue.progress.isUpload && newValue.progress.isUploadSignMatch)
143
+ if (newTask.shouldScheduleUpload())
178
144
  this.uploadQueue.push({
179
- fn: () => newValue.uploadBlobTask(),
145
+ fn: () => newTask.uploadBlobTask(),
180
146
  recoverableErrorPredicate: (e) => !nonRecoverableError(e)
181
147
  });
182
148
 
183
- return newValue.mustGetProgress(blobExists);
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 value = this.idToProgress.get(id);
189
- if (value === undefined) return;
155
+ const task = this.idToProgress.get(id);
156
+ if (task === undefined) return;
190
157
 
191
- const deleted = value.decCounter(callerId);
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<ProgressUpdater> {
213
+ private getAllNotDoneProgresses(): Array<UploadTask> {
247
214
  return Array.from(this.idToProgress.entries())
248
- .filter(([_, p]) => !isProgressStable(p.progress))
215
+ .filter(([_, p]) => !isProgressDone(p.progress))
249
216
  .map(([_, p]) => p);
250
217
  }
251
218
  }
252
219
 
253
- /** Holds all info needed to upload a file and a status of uploadong
254
- * and indexing. Also, has a method to update a status of the progress.
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
+ }
@@ -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 DownloadHelper {
14
+ export class RemoteFileDownloader {
15
15
  constructor(public readonly httpClient: Dispatcher) {}
16
16
 
17
- async downloadRemoteFile(
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
+