@milaboratories/pl-drivers 1.5.57 → 1.5.59

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.
@@ -19,17 +19,19 @@ import type {
19
19
  RemoteBlobHandleAndSize,
20
20
  StreamingApiResponse,
21
21
  } from '@milaboratories/pl-model-common';
22
+ import { newRangeBytesOpt, type RangeBytes, validateRangeBytes } from '@milaboratories/pl-model-common';
22
23
  import type {
23
24
  PlTreeEntry,
24
25
  ResourceInfo,
25
- ResourceSnapshot } from '@milaboratories/pl-tree';
26
+ ResourceSnapshot
27
+ } from '@milaboratories/pl-tree';
26
28
  import {
27
29
  isPlTreeEntry,
28
30
  makeResourceSnapshot,
29
31
  treeEntryToResourceInfo,
30
32
  } from '@milaboratories/pl-tree';
31
33
  import type { MiLogger, Signer } from '@milaboratories/ts-helpers';
32
- import { CallersCounter, TaskProcessor } from '@milaboratories/ts-helpers';
34
+ import { CallersCounter, mapGet, notEmpty, TaskProcessor } from '@milaboratories/ts-helpers';
33
35
  import Denque from 'denque';
34
36
  import * as fs from 'fs';
35
37
  import { randomUUID } from 'node:crypto';
@@ -41,21 +43,22 @@ import { Readable, Writable } from 'node:stream';
41
43
  import { buffer } from 'node:stream/consumers';
42
44
  import type { ClientDownload } from '../../clients/download';
43
45
  import type { ClientLogs } from '../../clients/logs';
44
- import { DownloadBlobTask, nonRecoverableError } from './download_blob_task';
45
- import { FilesCache } from '../helpers/files_cache';
46
46
  import {
47
47
  isLocalBlobHandle,
48
48
  newLocalHandle,
49
49
  parseLocalHandle,
50
50
  } from '../helpers/download_local_handle';
51
- import { getSize, OnDemandBlobResourceSnapshot } from '../types';
52
51
  import {
53
52
  isRemoteBlobHandle,
54
53
  newRemoteHandle,
55
54
  parseRemoteHandle,
56
55
  } from '../helpers/download_remote_handle';
57
- import { getResourceInfoFromLogHandle, newLogHandle } from '../helpers/logs_handle';
58
56
  import { Updater, WrongResourceTypeError } from '../helpers/helpers';
57
+ import { getResourceInfoFromLogHandle, newLogHandle } from '../helpers/logs_handle';
58
+ import { getSize, OnDemandBlobResourceSnapshot } from '../types';
59
+ import { blobKey, pathToKey } from './blob_key';
60
+ import { DownloadBlobTask, nonRecoverableError } from './download_blob_task';
61
+ import { FilesCache } from '../helpers/files_cache';
59
62
 
60
63
  export type DownloadDriverOps = {
61
64
  /**
@@ -75,7 +78,7 @@ export type DownloadDriverOps = {
75
78
  * and notifies every watcher when a file were downloaded. */
76
79
  export class DownloadDriver implements BlobDriver {
77
80
  /** Represents a unique key to the path of a blob as a map. */
78
- private idToDownload: Map<string, DownloadBlobTask> = new Map();
81
+ private keyToDownload: Map<string, DownloadBlobTask> = new Map();
79
82
 
80
83
  /** Writes and removes files to a hard drive and holds a counter for every
81
84
  * file that should be kept. */
@@ -84,7 +87,7 @@ export class DownloadDriver implements BlobDriver {
84
87
  /** Downloads files and writes them to the local dir. */
85
88
  private downloadQueue: TaskProcessor;
86
89
 
87
- private idToOnDemand: Map<string, OnDemandBlobHolder> = new Map();
90
+ private keyToOnDemand: Map<string, OnDemandBlobHolder> = new Map();
88
91
 
89
92
  private idToLastLines: Map<string, LastLinesGetter> = new Map();
90
93
  private idToProgressLog: Map<string, LastLinesGetter> = new Map();
@@ -105,37 +108,75 @@ export class DownloadDriver implements BlobDriver {
105
108
  this.saveDir = path.resolve(saveDir);
106
109
  }
107
110
 
111
+ static async init(
112
+ logger: MiLogger,
113
+ clientDownload: ClientDownload,
114
+ clientLogs: ClientLogs,
115
+ saveDir: string,
116
+ signer: Signer,
117
+ ops: DownloadDriverOps,
118
+ ): Promise<DownloadDriver> {
119
+ const driver = new DownloadDriver(logger, clientDownload, clientLogs, saveDir, signer, ops);
120
+ await driver.initCache();
121
+
122
+ return driver;
123
+ }
124
+
125
+ private async initCache() {
126
+ let files: string[];
127
+ try {
128
+ files = await fsp.readdir(this.saveDir);
129
+ } catch (e: unknown) {
130
+ if (typeof e === 'object' && e !== null && (e as { code?: string }).code === 'ENOENT') {
131
+ return;
132
+ }
133
+
134
+ throw e;
135
+ }
136
+
137
+ // for (const file of files) {
138
+ // const { size } = await fsp.stat(path.resolve(this.saveDir, file));
139
+
140
+ // const blobInfo = pathToBlobInfo(file);
141
+ // if (blobInfo == undefined) {
142
+ // continue;
143
+ // }
144
+
145
+ // this.cache.addCache({
146
+ // path: path.resolve(this.saveDir, file),
147
+ // baseKey: blobKey(blobInfo.resourceId),
148
+ // key: blobKey(blobInfo.resourceId, blobInfo.range),
149
+ // counter: new CallersCounter(),
150
+ // range,
151
+ // });
152
+ // }
153
+ }
154
+
108
155
  /** Gets a blob or part of the blob by its resource id or downloads a blob and sets it in a cache. */
109
156
  public getDownloadedBlob(
110
157
  res: ResourceInfo | PlTreeEntry,
111
158
  ctx: ComputableCtx,
112
- fromBytes?: number,
113
- toBytes?: number,
114
159
  ): LocalBlobHandleAndSize | undefined;
115
160
  public getDownloadedBlob(
116
161
  res: ResourceInfo | PlTreeEntry,
117
- ctx?: undefined,
118
- fromBytes?: number,
119
- toBytes?: number,
120
162
  ): ComputableStableDefined<LocalBlobHandleAndSize>;
121
163
  public getDownloadedBlob(
122
164
  res: ResourceInfo | PlTreeEntry,
123
165
  ctx?: ComputableCtx,
124
- fromBytes?: number,
125
- toBytes?: number,
126
166
  ): Computable<LocalBlobHandleAndSize | undefined> | LocalBlobHandleAndSize | undefined {
127
167
  if (ctx === undefined) {
128
- return Computable.make((ctx) => this.getDownloadedBlob(res, ctx, fromBytes, toBytes));
168
+ return Computable.make((ctx) => this.getDownloadedBlob(res, ctx));
129
169
  }
130
170
 
131
171
  const rInfo = treeEntryToResourceInfo(res, ctx);
132
- const key = blobKey(rInfo.id, fromBytes, toBytes);
133
172
 
134
173
  const callerId = randomUUID();
135
- ctx.addOnDestroy(() => this.releaseBlob(key, callerId));
174
+ ctx.addOnDestroy(() => this.releaseBlob(rInfo, callerId));
136
175
 
137
- const result = this.getDownloadedBlobNoCtx(ctx.watcher, rInfo as ResourceSnapshot, callerId, fromBytes, toBytes);
138
- if (result == undefined) ctx.markUnstable('download blob is still undefined');
176
+ const result = this.getDownloadedBlobNoCtx(ctx.watcher, rInfo as ResourceSnapshot, callerId);
177
+ if (result == undefined) {
178
+ ctx.markUnstable('download blob is still undefined');
179
+ }
139
180
 
140
181
  return result;
141
182
  }
@@ -144,42 +185,54 @@ export class DownloadDriver implements BlobDriver {
144
185
  w: Watcher,
145
186
  rInfo: ResourceSnapshot,
146
187
  callerId: string,
147
- fromBytes?: number,
148
- toBytes?: number,
149
188
  ): LocalBlobHandleAndSize | undefined {
150
189
  validateDownloadableResourceType('getDownloadedBlob', rInfo.type);
151
190
 
152
- let task = this.idToDownload.get(blobKey(rInfo.id, fromBytes, toBytes));
153
-
154
- if (task === undefined) {
155
- // schedule the blob downloading
156
- const newTask = this.setNewDownloadTask(rInfo);
157
- this.downloadQueue.push({
158
- fn: () => this.downloadBlob(newTask, callerId),
159
- recoverableErrorPredicate: (e) => !nonRecoverableError(e),
160
- });
161
- task = newTask;
162
- }
191
+ // We don't need to request files with wider limits,
192
+ // PFrame's engine does it disk-optimally by itself.
163
193
 
194
+ const task = this.getOrSetNewTask(rInfo, callerId);
164
195
  task.attach(w, callerId);
196
+
165
197
  const result = task.getBlob();
166
- if (!result.done) return undefined;
167
- if (result.result.ok) return result.result.value;
198
+ if (!result.done) {
199
+ return undefined;
200
+ }
201
+ if (result.result.ok) {
202
+ return result.result.value;
203
+ }
168
204
  throw result.result.error;
169
205
  }
170
206
 
171
- private setNewDownloadTask(rInfo: ResourceSnapshot, fromBytes?: number, toBytes?: number) {
172
- const fPath = path.resolve(this.saveDir, blobKey(rInfo.id, fromBytes, toBytes));
173
- const result = new DownloadBlobTask(
207
+ private getOrSetNewTask(
208
+ rInfo: ResourceSnapshot,
209
+ callerId: string,
210
+ ): DownloadBlobTask {
211
+ const key = blobKey(rInfo.id);
212
+
213
+ const inMemoryTask = this.keyToDownload.get(key);
214
+ if (inMemoryTask) {
215
+ return inMemoryTask;
216
+ }
217
+
218
+ // schedule the blob downloading, then it'll be added to the cache.
219
+ const fPath = path.resolve(this.saveDir, key);
220
+
221
+ const newTask = new DownloadBlobTask(
174
222
  this.logger,
175
223
  this.clientDownload,
176
224
  rInfo,
177
- fPath,
178
225
  newLocalHandle(fPath, this.signer),
226
+ fPath,
179
227
  );
180
- this.idToDownload.set(blobKey(rInfo.id, fromBytes, toBytes), result);
228
+ this.keyToDownload.set(key, newTask);
181
229
 
182
- return result;
230
+ this.downloadQueue.push({
231
+ fn: () => this.downloadBlob(newTask, callerId),
232
+ recoverableErrorPredicate: (e) => !nonRecoverableError(e),
233
+ });
234
+
235
+ return newTask;
183
236
  }
184
237
 
185
238
  private async downloadBlob(task: DownloadBlobTask, callerId: string) {
@@ -192,11 +245,19 @@ export class DownloadDriver implements BlobDriver {
192
245
 
193
246
  /** Gets on demand blob. */
194
247
  public getOnDemandBlob(
195
- res: OnDemandBlobResourceSnapshot | PlTreeEntry
248
+ res: OnDemandBlobResourceSnapshot | PlTreeEntry,
196
249
  ): Computable<RemoteBlobHandleAndSize>;
197
250
  public getOnDemandBlob(
198
251
  res: OnDemandBlobResourceSnapshot | PlTreeEntry,
199
- ctx: ComputableCtx
252
+ ctx?: undefined,
253
+ fromBytes?: number,
254
+ toBytes?: number,
255
+ ): Computable<RemoteBlobHandleAndSize>;
256
+ public getOnDemandBlob(
257
+ res: OnDemandBlobResourceSnapshot | PlTreeEntry,
258
+ ctx: ComputableCtx,
259
+ fromBytes?: number,
260
+ toBytes?: number,
200
261
  ): RemoteBlobHandleAndSize;
201
262
  public getOnDemandBlob(
202
263
  res: OnDemandBlobResourceSnapshot | PlTreeEntry,
@@ -224,11 +285,11 @@ export class DownloadDriver implements BlobDriver {
224
285
  ): RemoteBlobHandleAndSize {
225
286
  validateDownloadableResourceType('getOnDemandBlob', info.type);
226
287
 
227
- let blob = this.idToOnDemand.get(blobKey(info.id));
288
+ let blob = this.keyToOnDemand.get(blobKey(info.id));
228
289
 
229
290
  if (blob === undefined) {
230
291
  blob = new OnDemandBlobHolder(getSize(info), newRemoteHandle(info, this.signer));
231
- this.idToOnDemand.set(blobKey(info.id), blob);
292
+ this.keyToOnDemand.set(blobKey(info.id), blob);
232
293
  }
233
294
 
234
295
  blob.attach(callerId);
@@ -243,13 +304,23 @@ export class DownloadDriver implements BlobDriver {
243
304
  }
244
305
 
245
306
  /** Gets a content of a blob by a handle. */
246
- public async getContent(handle: LocalBlobHandle | RemoteBlobHandle): Promise<Uint8Array> {
307
+ public async getContent(handle: LocalBlobHandle | RemoteBlobHandle, range?: RangeBytes): Promise<Uint8Array> {
308
+ if (range) {
309
+ validateRangeBytes(range, `getContent`);
310
+ }
311
+
247
312
  if (isLocalBlobHandle(handle)) {
248
- return await read(this.getLocalPath(handle));
313
+ return await read(this.getLocalPath(handle), range);
249
314
  }
250
315
  if (isRemoteBlobHandle(handle)) {
251
316
  const result = parseRemoteHandle(handle, this.signer);
252
- const { content } = await this.clientDownload.downloadBlob(result);
317
+ const { content } = await this.clientDownload.downloadBlob(
318
+ { id: result.id, type: result.type },
319
+ undefined,
320
+ undefined,
321
+ range?.from,
322
+ range?.to,
323
+ );
253
324
 
254
325
  return await buffer(content);
255
326
  }
@@ -262,12 +333,17 @@ export class DownloadDriver implements BlobDriver {
262
333
  * Uses downloaded blob handle under the hood, so stores corresponding blob in file system.
263
334
  */
264
335
  public getComputableContent(
265
- res: ResourceInfo | PlTreeEntry
266
- ): ComputableStableDefined<Uint8Array>{
336
+ res: ResourceInfo | PlTreeEntry,
337
+ range?: RangeBytes,
338
+ ): ComputableStableDefined<Uint8Array> {
339
+ if (range) {
340
+ validateRangeBytes(range, `getComputableContent`);
341
+ }
342
+
267
343
  return Computable.make((ctx) =>
268
344
  this.getDownloadedBlob(res, ctx), {
269
- postprocessValue: (v) => v ? this.getContent(v.handle) : undefined
270
- }
345
+ postprocessValue: (v) => v ? this.getContent(v.handle, range) : undefined
346
+ }
271
347
  ).withStableType()
272
348
  }
273
349
 
@@ -291,7 +367,7 @@ export class DownloadDriver implements BlobDriver {
291
367
 
292
368
  const r = treeEntryToResourceInfo(res, ctx);
293
369
  const callerId = randomUUID();
294
- ctx.addOnDestroy(() => this.releaseBlob(blobKey(r.id), callerId));
370
+ ctx.addOnDestroy(() => this.releaseBlob(r, callerId));
295
371
 
296
372
  const result = this.getLastLogsNoCtx(ctx.watcher, r as ResourceSnapshot, lines, callerId);
297
373
  if (result == undefined)
@@ -347,7 +423,7 @@ export class DownloadDriver implements BlobDriver {
347
423
 
348
424
  const r = treeEntryToResourceInfo(res, ctx);
349
425
  const callerId = randomUUID();
350
- ctx.addOnDestroy(() => this.releaseBlob(blobKey(r.id), callerId));
426
+ ctx.addOnDestroy(() => this.releaseBlob(r, callerId));
351
427
 
352
428
  const result = this.getProgressLogNoCtx(
353
429
  ctx.watcher,
@@ -452,22 +528,25 @@ export class DownloadDriver implements BlobDriver {
452
528
  };
453
529
  }
454
530
 
455
- private async releaseBlob(blobKey: string, callerId: string) {
456
- const task = this.idToDownload.get(blobKey);
457
- if (task == undefined) return;
531
+ private async releaseBlob(rInfo: ResourceInfo, callerId: string) {
532
+ const task = this.keyToDownload.get(blobKey(rInfo.id));
533
+ if (task == undefined) {
534
+ return;
535
+ }
536
+
537
+ if (this.cache.existsFile(blobKey(rInfo.id))) {
538
+ const toDelete = this.cache.removeFile(blobKey(rInfo.id), callerId);
458
539
 
459
- if (this.cache.existsFile(task.path)) {
460
- const toDelete = this.cache.removeFile(task.path, callerId);
461
540
  await Promise.all(
462
- toDelete.map(async (task) => {
463
- await fsp.rm(task.path);
541
+ toDelete.map(async (cachedFile) => {
542
+ await fsp.rm(cachedFile.path);
464
543
 
465
- this.cache.removeCache(task);
544
+ this.cache.removeCache(cachedFile);
466
545
 
467
546
  this.removeTask(
468
- task,
469
- `the task ${stringifyWithResourceId(task.info())} was removed`
470
- + `from cache along with ${stringifyWithResourceId(toDelete.map((d) => d.info()))}`,
547
+ mapGet(this.keyToDownload, pathToKey(cachedFile.path)),
548
+ `the task ${stringifyWithResourceId(cachedFile)} was removed`
549
+ + `from cache along with ${stringifyWithResourceId(toDelete.map((d) => d.path))}`,
471
550
  );
472
551
  }),
473
552
  );
@@ -486,22 +565,22 @@ export class DownloadDriver implements BlobDriver {
486
565
  private removeTask(task: DownloadBlobTask, reason: string) {
487
566
  task.abort(reason);
488
567
  task.change.markChanged();
489
- this.idToDownload.delete(blobKey(task.rInfo.id));
568
+ this.keyToDownload.delete(pathToKey(task.path));
490
569
  this.idToLastLines.delete(blobKey(task.rInfo.id));
491
570
  this.idToProgressLog.delete(blobKey(task.rInfo.id));
492
571
  }
493
572
 
494
573
  private async releaseOnDemandBlob(blobId: ResourceId, callerId: string) {
495
- const deleted = this.idToOnDemand.get(blobKey(blobId))?.release(callerId) ?? false;
496
- if (deleted) this.idToOnDemand.delete(blobKey(blobId));
574
+ const deleted = this.keyToOnDemand.get(blobKey(blobId))?.release(callerId) ?? false;
575
+ if (deleted) this.keyToOnDemand.delete(blobKey(blobId));
497
576
  }
498
577
 
499
578
  /** Removes all files from a hard drive. */
500
579
  async releaseAll() {
501
580
  this.downloadQueue.stop();
502
581
 
503
- this.idToDownload.forEach((task, blobId) => {
504
- this.idToDownload.delete(blobId);
582
+ this.keyToDownload.forEach((task, key) => {
583
+ this.keyToDownload.delete(key);
505
584
  task.change.markChanged();
506
585
  });
507
586
  }
@@ -605,8 +684,16 @@ function getLastLines(fPath: string, nLines: number, patternToSearch?: string):
605
684
  });
606
685
  }
607
686
 
608
- async function read(path: string): Promise<Uint8Array> {
609
- return await buffer(Readable.toWeb(fs.createReadStream(path)));
687
+ async function read(path: string, range?: RangeBytes): Promise<Uint8Array> {
688
+ const ops: { start?: number; end?: number } = {};
689
+ if (range) {
690
+ ops.start = range.from;
691
+ ops.end = range.to - 1;
692
+ }
693
+
694
+ const stream = fs.createReadStream(path, ops);
695
+
696
+ return await buffer(Readable.toWeb(stream));
610
697
  }
611
698
 
612
699
  function validateDownloadableResourceType(methodName: string, rType: ResourceType) {
@@ -618,12 +705,3 @@ function validateDownloadableResourceType(methodName: string, rType: ResourceTyp
618
705
  throw new WrongResourceTypeError(message);
619
706
  }
620
707
  }
621
-
622
- /** Returns a file name and the unique key of the file.*/
623
- function blobKey(rId: ResourceId, fromBytes?: number, toBytes?: number): string {
624
- if (fromBytes !== undefined && toBytes !== undefined) {
625
- return `${BigInt(rId)}_${fromBytes}-${toBytes}`;
626
- }
627
-
628
- return `${BigInt(rId)}`;
629
- }
@@ -7,10 +7,10 @@ import type {
7
7
  MiLogger,
8
8
  } from '@milaboratories/ts-helpers';
9
9
  import {
10
- CallersCounter,
11
10
  ensureDirExists,
12
11
  fileExists,
13
12
  createPathAtomically,
13
+ CallersCounter,
14
14
  } from '@milaboratories/ts-helpers';
15
15
  import fs from 'node:fs';
16
16
  import * as fsp from 'node:fs/promises';
@@ -20,32 +20,32 @@ import type { ClientDownload } from '../../clients/download';
20
20
  import { UnknownStorageError, WrongLocalFileUrl } from '../../clients/download';
21
21
  import { NetworkError400 } from '../../helpers/download';
22
22
 
23
- /** Downloads a blob. */
23
+ /** Downloads a blob and holds callers and watchers for the blob. */
24
24
  export class DownloadBlobTask {
25
- readonly counter = new CallersCounter();
26
25
  readonly change = new ChangeSource();
27
26
  private readonly signalCtl = new AbortController();
28
- private error: any | undefined;
27
+ public readonly counter = new CallersCounter();
28
+ private error: unknown | undefined;
29
29
  private done = false;
30
- /** Represents a size in bytes of the downloaded blob. */
31
- size = 0;
30
+ public size = 0;
31
+ private state: DownloadState = {};
32
32
 
33
33
  constructor(
34
34
  private readonly logger: MiLogger,
35
35
  private readonly clientDownload: ClientDownload,
36
36
  readonly rInfo: ResourceSnapshot,
37
- readonly path: string,
38
37
  private readonly handle: LocalBlobHandle,
38
+ readonly path: string,
39
39
  ) {}
40
40
 
41
- /** Returns a simple object that describes this task. */
41
+ /** Returns a simple object that describes this task for debugging purposes. */
42
42
  public info() {
43
43
  return {
44
44
  rInfo: this.rInfo,
45
- path: this.path,
45
+ fPath: this.path,
46
46
  done: this.done,
47
- size: this.size,
48
47
  error: this.error,
48
+ state: this.state,
49
49
  };
50
50
  }
51
51
 
@@ -60,6 +60,7 @@ export class DownloadBlobTask {
60
60
  this.setDone(size);
61
61
  this.change.markChanged();
62
62
  } catch (e: any) {
63
+ this.logger.error(`task failed: ${e}, ${JSON.stringify(this.state)}`);
63
64
  if (nonRecoverableError(e)) {
64
65
  this.setError(e);
65
66
  this.change.markChanged();
@@ -72,21 +73,36 @@ export class DownloadBlobTask {
72
73
  }
73
74
 
74
75
  private async ensureDownloaded() {
75
- await ensureDirExists(path.dirname(this.path));
76
-
77
- if (await fileExists(this.path)) {
78
- this.logger.info(`a blob was already downloaded: ${this.path}`);
79
- const stat = await fsp.stat(this.path);
80
- return stat.size;
76
+ this.state = {};
77
+ this.state.filePath = this.path;
78
+ await ensureDirExists(path.dirname(this.state.filePath));
79
+ this.state.dirExists = true;
80
+
81
+ if (await fileExists(this.state.filePath)) {
82
+ this.state.fileExists = true;
83
+ this.logger.info(`a blob was already downloaded: ${this.state.filePath}`);
84
+ const stat = await fsp.stat(this.state.filePath);
85
+ this.state.fileSize = stat.size;
86
+
87
+ return this.state.fileSize;
81
88
  }
82
89
 
83
- const { content, size } = await this.clientDownload.downloadBlob(this.rInfo);
90
+ const { content, size } = await this.clientDownload.downloadBlob(
91
+ this.rInfo,
92
+ {},
93
+ undefined,
94
+ );
95
+ this.state.fileSize = size;
96
+ this.state.downloaded = true;
84
97
 
85
- await createPathAtomically(this.logger, this.path, async (fPath: string) => {
98
+ await createPathAtomically(this.logger, this.state.filePath, async (fPath: string) => {
86
99
  const f = Writable.toWeb(fs.createWriteStream(fPath, { flags: 'wx' }));
87
100
  await content.pipeTo(f);
101
+ this.state.tempWritten = true;
88
102
  });
89
103
 
104
+ this.state.done = true;
105
+
90
106
  return size;
91
107
  }
92
108
 
@@ -102,21 +118,9 @@ export class DownloadBlobTask {
102
118
  } {
103
119
  if (!this.done) return { done: false };
104
120
 
105
- if (this.error)
106
- return {
107
- done: true,
108
- result: { ok: false, error: this.error },
109
- };
110
-
111
121
  return {
112
- done: true,
113
- result: {
114
- ok: true,
115
- value: {
116
- handle: this.handle,
117
- size: this.size,
118
- },
119
- },
122
+ done: this.done,
123
+ result: getDownloadedBlobResponse(this.handle, this.size, this.error),
120
124
  };
121
125
  }
122
126
 
@@ -125,7 +129,7 @@ export class DownloadBlobTask {
125
129
  this.size = sizeBytes;
126
130
  }
127
131
 
128
- private setError(e: any) {
132
+ private setError(e: unknown) {
129
133
  this.done = true;
130
134
  this.error = e;
131
135
  }
@@ -147,3 +151,35 @@ export function nonRecoverableError(e: any) {
147
151
  /** The downloading task was aborted by a signal.
148
152
  * It may happen when the computable is done, for example. */
149
153
  class DownloadAborted extends Error {}
154
+
155
+ export function getDownloadedBlobResponse(
156
+ handle: LocalBlobHandle | undefined,
157
+ size: number,
158
+ error?: unknown,
159
+ ): ValueOrError<LocalBlobHandleAndSize> {
160
+ if (error) {
161
+ return { ok: false, error };
162
+ }
163
+
164
+ if (!handle) {
165
+ return { ok: false, error: new Error('No file or handle provided') };
166
+ }
167
+
168
+ return {
169
+ ok: true,
170
+ value: {
171
+ handle,
172
+ size,
173
+ },
174
+ };
175
+ }
176
+
177
+ type DownloadState = {
178
+ filePath?: string;
179
+ dirExists?: boolean;
180
+ fileExists?: boolean;
181
+ fileSize?: number;
182
+ downloaded?: boolean;
183
+ tempWritten?: boolean;
184
+ done?: boolean;
185
+ }
@@ -3,19 +3,20 @@
3
3
 
4
4
  import type { Signer } from '@milaboratories/ts-helpers';
5
5
  import type { OnDemandBlobResourceSnapshot } from '../types';
6
- import type { RemoteBlobHandle } from '@milaboratories/pl-model-common';
7
- import type { ResourceInfo } from '@milaboratories/pl-tree';
8
- import { bigintToResourceId } from '@milaboratories/pl-client';
6
+ import type { RemoteBlobHandle, RangeBytes } from '@milaboratories/pl-model-common';
7
+ import { bigintToResourceId, ResourceId, ResourceType } from '@milaboratories/pl-client';
8
+ import { ResourceInfo } from '@milaboratories/pl-tree';
9
9
 
10
- // https://regex101.com/r/rvbPZt/1
10
+ // https://regex101.com/r/Q4YdTa/4
11
11
  const remoteHandleRegex
12
- = /^blob\+remote:\/\/download\/(?<content>(?<resourceType>.*)\/(?<resourceVersion>.*)\/(?<resourceId>.*))#(?<signature>.*)$/;
12
+ = /^blob\+remote:\/\/download\/(?<content>(?<resourceType>.+)\/(?<resourceVersion>.+?)\/(?<resourceId>\d+?))#(?<signature>.*)$/;
13
13
 
14
14
  export function newRemoteHandle(
15
15
  rInfo: OnDemandBlobResourceSnapshot,
16
16
  signer: Signer,
17
17
  ): RemoteBlobHandle {
18
- const content = `${rInfo.type.name}/${rInfo.type.version}/${BigInt(rInfo.id)}`;
18
+ let content = `${rInfo.type.name}/${rInfo.type.version}/${BigInt(rInfo.id)}`;
19
+
19
20
  return `blob+remote://download/${content}#${signer.sign(content)}` as RemoteBlobHandle;
20
21
  }
21
22
 
@@ -6,6 +6,7 @@ import * as os from 'node:os';
6
6
  import * as path from 'node:path';
7
7
  import { test, expect } from 'vitest';
8
8
  import { isImportFileHandleIndex, isImportFileHandleUpload } from '@milaboratories/pl-model-common';
9
+ import * as env from '../test_env';
9
10
 
10
11
  const assetsPath = path.resolve('../../../assets');
11
12
 
@@ -21,10 +22,10 @@ test('should ok when get all storages from ls driver', async () => {
21
22
  const got = await driver.getStorageList();
22
23
 
23
24
  expect(got.length).toBeGreaterThanOrEqual(1);
24
- expect(got.find((se) => se.name == 'library')?.handle).toContain('library');
25
- expect(got.find((se) => se.name == 'library')?.initialFullPath).toEqual('');
26
- expect(got.find((se) => se.name == 'local')?.handle).toContain('/');
27
- expect(got.find((se) => se.name == 'local')?.initialFullPath).toEqual(os.homedir());
25
+ expect(got.find((se) => se.name == env.libraryStorage)?.handle).toContain(env.libraryStorage);
26
+ expect(got.find((se) => se.name == env.libraryStorage)?.initialFullPath).toEqual('');
27
+ // expect(got.find((se) => se.name == 'local')?.handle).toContain('/');
28
+ // expect(got.find((se) => se.name == 'local')?.initialFullPath).toEqual(os.homedir());
28
29
 
29
30
  console.log('got all storage entries: ', got);
30
31
  });
@@ -39,7 +40,7 @@ test('should ok when list files from remote storage in ls driver', async () => {
39
40
  });
40
41
 
41
42
  const storages = await driver.getStorageList();
42
- const library = storages.find((se) => se.name == 'library')!.handle;
43
+ const library = storages.find((se) => se.name == env.libraryStorage)!.handle;
43
44
 
44
45
  const topLevelDir = await driver.listFiles(library, '');
45
46
  expect(topLevelDir.entries.length).toBeGreaterThan(1);
@@ -47,7 +48,9 @@ test('should ok when list files from remote storage in ls driver', async () => {
47
48
  const testDir = topLevelDir.entries.find((d) => d.name.includes('ls_dir_structure'));
48
49
  expect(testDir).toBeDefined();
49
50
  expect(testDir!.type).toEqual('dir');
50
- expect(testDir!.fullPath).toEqual('/ls_dir_structure_test');
51
+
52
+ const fullPath = testDir!.fullPath.startsWith('/') ? testDir!.fullPath.slice(1) : testDir!.fullPath;
53
+ expect(fullPath).toEqual('ls_dir_structure_test');
51
54
  expect(testDir!.name).toEqual('ls_dir_structure_test');
52
55
 
53
56
  const secondDirs = await driver.listFiles(library, testDir!.fullPath);