@milaboratories/pl-drivers 1.5.4 → 1.5.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@milaboratories/pl-drivers",
3
- "version": "1.5.4",
3
+ "version": "1.5.6",
4
4
  "description": "Drivers and a low-level clients for log streaming, downloading and uploading files from and to pl",
5
5
  "types": "./dist/index.d.ts",
6
6
  "main": "./dist/index.js",
@@ -28,9 +28,9 @@
28
28
  "zod": "~3.23.8",
29
29
  "@milaboratories/ts-helpers": "^1.1.3",
30
30
  "@milaboratories/computable": "^2.3.4",
31
+ "@milaboratories/pl-client": "^2.7.1",
31
32
  "@milaboratories/pl-tree": "^1.4.20",
32
- "@milaboratories/pl-model-common": "^1.10.1",
33
- "@milaboratories/pl-client": "^2.7.1"
33
+ "@milaboratories/pl-model-common": "^1.10.2"
34
34
  },
35
35
  "devDependencies": {
36
36
  "typescript": "~5.5.4",
@@ -20,7 +20,7 @@ import { OnDemandBlobResourceSnapshot } from './types';
20
20
 
21
21
  const fileName = 'answer_to_the_ultimate_question.txt';
22
22
 
23
- test.skip('should download a blob and read its content', async () => {
23
+ test('should download a blob and read its content', async () => {
24
24
  await TestHelpers.withTempRoot(async (client) => {
25
25
  const logger = new ConsoleLoggerAdapter();
26
26
  const dir = await fsp.mkdtemp(path.join(os.tmpdir(), 'test-download-1-'));
@@ -49,7 +49,55 @@ test.skip('should download a blob and read its content', async () => {
49
49
  });
50
50
  });
51
51
 
52
- test.skip('should get on demand blob without downloading a blob', async () => {
52
+ test('should not redownload a blob a file already exists', async () => {
53
+ await TestHelpers.withTempRoot(async (client) => {
54
+ const logger = new ConsoleLoggerAdapter();
55
+ const dir = await fsp.mkdtemp(path.join(os.tmpdir(), 'test-download-1-'));
56
+
57
+ const signer = new HmacSha256Signer(HmacSha256Signer.generateSecret());
58
+
59
+ const driver = new DownloadDriver(
60
+ logger,
61
+ createDownloadClient(logger, client, []),
62
+ createLogsClient(client, logger),
63
+ dir,
64
+ signer,
65
+ { cacheSoftSizeBytes: 700 * 1024, nConcurrentDownloads: 10 }
66
+ );
67
+
68
+ console.log('Download the first time');
69
+ const downloadable = await makeDownloadableBlobFromAssets(client, fileName);
70
+ const c = driver.getDownloadedBlob(downloadable);
71
+ await c.getValue();
72
+ await c.awaitChange();
73
+ const blob = await c.getValue();
74
+ expect(blob).toBeDefined();
75
+ expect(blob!.size).toBe(3);
76
+ expect((await driver.getContent(blob!.handle))?.toString()).toBe('42\n');
77
+
78
+ await driver.releaseAll();
79
+
80
+ const driver2 = new DownloadDriver(
81
+ logger,
82
+ createDownloadClient(logger, client, []),
83
+ createLogsClient(client, logger),
84
+ dir,
85
+ signer,
86
+ { cacheSoftSizeBytes: 700 * 1024, nConcurrentDownloads: 10 }
87
+ );
88
+
89
+ console.log('Download the second time');
90
+ const c2 = driver2.getDownloadedBlob(downloadable);
91
+ await c2.getValue();
92
+ await c2.awaitChange();
93
+ const blob2 = await c2.getValue();
94
+ expect(blob2).toBeDefined();
95
+ expect(blob2!.size).toBe(3);
96
+ expect((await driver.getContent(blob2!.handle))?.toString()).toBe('42\n');
97
+ });
98
+ });
99
+
100
+ test('should get on demand blob without downloading a blob', async () => {
53
101
  await TestHelpers.withTempRoot(async (client) => {
54
102
  const logger = new ConsoleLoggerAdapter();
55
103
  const dir = await fsp.mkdtemp(path.join(os.tmpdir(), 'test-download-2-'));
@@ -74,7 +122,7 @@ test.skip('should get on demand blob without downloading a blob', async () => {
74
122
  });
75
123
  });
76
124
 
77
- test.skip('should get undefined when releasing a blob from a small cache and the blob was deleted.', async () => {
125
+ test('should get undefined when releasing a blob from a small cache and the blob was deleted.', async () => {
78
126
  await TestHelpers.withTempRoot(async (client) => {
79
127
  const logger = new ConsoleLoggerAdapter();
80
128
  const dir = await fsp.mkdtemp(path.join(os.tmpdir(), 'test-download-3-'));
@@ -111,7 +159,7 @@ test.skip('should get undefined when releasing a blob from a small cache and the
111
159
  });
112
160
  });
113
161
 
114
- test.skip('should get the blob when releasing a blob, but a cache is big enough and it keeps a file on the local drive.', async () => {
162
+ test('should get the blob when releasing a blob, but a cache is big enough and it keeps a file on the local drive.', async () => {
115
163
  await TestHelpers.withTempRoot(async (client) => {
116
164
  const logger = new ConsoleLoggerAdapter();
117
165
  const dir = await fsp.mkdtemp(path.join(os.tmpdir(), 'test-download-4-'));
@@ -5,7 +5,7 @@ import {
5
5
  ComputableStableDefined,
6
6
  Watcher
7
7
  } from '@milaboratories/computable';
8
- import { ResourceId, ResourceType } from '@milaboratories/pl-client';
8
+ import { ResourceId, ResourceType, stringifyWithResourceId } from '@milaboratories/pl-client';
9
9
  import {
10
10
  AnyLogHandle,
11
11
  BlobDriver,
@@ -435,8 +435,8 @@ export class DownloadDriver implements BlobDriver {
435
435
 
436
436
  this.removeTask(
437
437
  task,
438
- `the task ${task.path} was removed` +
439
- `from cache along with ${toDelete.map((d) => d.path)}`
438
+ `the task ${stringifyWithResourceId(task.info())} was removed` +
439
+ `from cache along with ${stringifyWithResourceId(toDelete.map((d) => d.info()))}`
440
440
  );
441
441
  })
442
442
  );
@@ -444,7 +444,10 @@ export class DownloadDriver implements BlobDriver {
444
444
  // The task is still in a downloading queue.
445
445
  const deleted = task.counter.dec(callerId);
446
446
  if (deleted) {
447
- this.removeTask(task, `the task ${task.path} was removed from cache`);
447
+ this.removeTask(
448
+ task,
449
+ `the task ${stringifyWithResourceId(task.info())} was removed from cache`
450
+ );
448
451
  }
449
452
  }
450
453
  }
@@ -34,6 +34,17 @@ export class DownloadBlobTask {
34
34
  private readonly handle: LocalBlobHandle
35
35
  ) {}
36
36
 
37
+ /** Returns a simple object that describes this task. */
38
+ public info() {
39
+ return {
40
+ rInfo: this.rInfo,
41
+ path: this.path,
42
+ done: this.done,
43
+ size: this.size,
44
+ error: this.error
45
+ };
46
+ }
47
+
37
48
  public attach(w: Watcher, callerId: string) {
38
49
  this.counter.inc(callerId);
39
50
  if (!this.done) this.change.attachWatcher(w);
@@ -41,18 +52,7 @@ export class DownloadBlobTask {
41
52
 
42
53
  public async download() {
43
54
  try {
44
- await ensureDirExists(path.dirname(this.path));
45
- const { content, size } = await this.clientDownload.downloadBlob(this.rInfo);
46
-
47
- if (await fileExists(this.path)) {
48
- await content.cancel(`the file already exists.`); // finalize body
49
- } else {
50
- await createPathAtomically(this.logger, this.path, async (fPath: string) => {
51
- const f = Writable.toWeb(fs.createWriteStream(fPath, { flags: 'wx' }));
52
- await content.pipeTo(f);
53
- });
54
- }
55
-
55
+ const size = await this.ensureDownloaded();
56
56
  this.setDone(size);
57
57
  this.change.markChanged();
58
58
  } catch (e: any) {
@@ -67,6 +67,25 @@ export class DownloadBlobTask {
67
67
  }
68
68
  }
69
69
 
70
+ private async ensureDownloaded() {
71
+ await ensureDirExists(path.dirname(this.path));
72
+
73
+ if (await fileExists(this.path)) {
74
+ this.logger.info(`a blob was already downloaded: ${this.path}`);
75
+ const stat = await fsp.stat(this.path);
76
+ return stat.size;
77
+ }
78
+
79
+ const { content, size } = await this.clientDownload.downloadBlob(this.rInfo);
80
+
81
+ await createPathAtomically(this.logger, this.path, async (fPath: string) => {
82
+ const f = Writable.toWeb(fs.createWriteStream(fPath, { flags: 'wx' }));
83
+ await content.pipeTo(f);
84
+ });
85
+
86
+ return size;
87
+ }
88
+
70
89
  public abort(reason: string) {
71
90
  this.signalCtl.abort(new DownloadAborted(reason));
72
91
  }
@@ -17,6 +17,8 @@ import * as tar from 'tar-fs';
17
17
  import { Dispatcher } from 'undici';
18
18
  import { NetworkError400, RemoteFileDownloader } from '../helpers/download';
19
19
  import { FilesCache } from './helpers/files_cache';
20
+ import { stringifyWithResourceId } from '@milaboratories/pl-client';
21
+ import { DownloadBlobTask } from './download_blob_task';
20
22
 
21
23
  export interface DownloadUrlSyncReader {
22
24
  /** Returns a Computable that (when the time will come)
@@ -128,21 +130,25 @@ export class DownloadUrlDriver implements DownloadUrlSyncReader {
128
130
  const toDelete = this.cache.removeFile(task.path, callerId);
129
131
 
130
132
  await Promise.all(
131
- toDelete.map(async (task) => {
133
+ toDelete.map(async (task: DownloadByUrlTask) => {
132
134
  await rmRFDir(task.path);
133
135
  this.cache.removeCache(task);
134
136
 
135
137
  this.removeTask(
136
138
  task,
137
- `the task ${JSON.stringify(task)} was removed` +
138
- `from cache along with ${JSON.stringify(toDelete)}`
139
+ `the task ${stringifyWithResourceId(task.info())} was removed` +
140
+ `from cache along with ${stringifyWithResourceId(toDelete.map((t) => t.info()))}`
139
141
  );
140
142
  })
141
143
  );
142
144
  } else {
143
145
  // The task is still in a downloading queue.
144
146
  const deleted = task.counter.dec(callerId);
145
- if (deleted) this.removeTask(task, `the task ${JSON.stringify(task)} was removed from cache`);
147
+ if (deleted)
148
+ this.removeTask(
149
+ task,
150
+ `the task ${stringifyWithResourceId(task.info())} was removed from cache`
151
+ );
146
152
  }
147
153
  }
148
154
 
@@ -155,7 +161,10 @@ export class DownloadUrlDriver implements DownloadUrlSyncReader {
155
161
  await rmRFDir(task.path);
156
162
  this.cache.removeCache(task);
157
163
 
158
- this.removeTask(task, `the task ${task} was released when the driver was closed`);
164
+ this.removeTask(
165
+ task,
166
+ `the task ${stringifyWithResourceId(task.info())} was released when the driver was closed`
167
+ );
159
168
  })
160
169
  );
161
170
  }
@@ -195,6 +204,16 @@ class DownloadByUrlTask {
195
204
  readonly url: URL
196
205
  ) {}
197
206
 
207
+ public info() {
208
+ return {
209
+ url: this.url.toString(),
210
+ path: this.path,
211
+ done: this.done,
212
+ size: this.size,
213
+ error: this.error
214
+ };
215
+ }
216
+
198
217
  attach(w: Watcher, callerId: string) {
199
218
  this.counter.inc(callerId);
200
219
  if (!this.done) this.change.attachWatcher(w);