@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
@@ -0,0 +1,126 @@
1
+ import { ChangeSource, Watcher } from '@milaboratories/computable';
2
+ import { LocalBlobHandle, LocalBlobHandleAndSize } from '@milaboratories/pl-model-common';
3
+ import { ResourceSnapshot } from '@milaboratories/pl-tree';
4
+ import {
5
+ CallersCounter,
6
+ ValueOrError,
7
+ ensureDirExists,
8
+ fileExists,
9
+ createPathAtomically,
10
+ MiLogger
11
+ } from '@milaboratories/ts-helpers';
12
+ import fs from 'node:fs';
13
+ import * as fsp from 'node:fs/promises';
14
+ import * as path from 'node:path';
15
+ import { Writable } from 'node:stream';
16
+ import { ClientDownload, UnknownStorageError, WrongLocalFileUrl } from '../clients/download';
17
+ import { NetworkError400 } from '../helpers/download';
18
+
19
+ /** Downloads a blob. */
20
+ export class DownloadBlobTask {
21
+ readonly counter = new CallersCounter();
22
+ readonly change = new ChangeSource();
23
+ private readonly signalCtl = new AbortController();
24
+ private error: any | undefined;
25
+ private done = false;
26
+ /** Represents a size in bytes of the downloaded blob. */
27
+ size = 0;
28
+
29
+ constructor(
30
+ private readonly logger: MiLogger,
31
+ private readonly clientDownload: ClientDownload,
32
+ readonly rInfo: ResourceSnapshot,
33
+ readonly path: string,
34
+ private readonly handle: LocalBlobHandle
35
+ ) {}
36
+
37
+ public attach(w: Watcher, callerId: string) {
38
+ this.counter.inc(callerId);
39
+ if (!this.done) this.change.attachWatcher(w);
40
+ }
41
+
42
+ public async download() {
43
+ 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
+
56
+ this.setDone(size);
57
+ this.change.markChanged();
58
+ } catch (e: any) {
59
+ if (nonRecoverableError(e)) {
60
+ this.setError(e);
61
+ this.change.markChanged();
62
+ // Just in case we were half-way extracting an archive.
63
+ await fsp.rm(this.path);
64
+ }
65
+
66
+ throw e;
67
+ }
68
+ }
69
+
70
+ public abort(reason: string) {
71
+ this.signalCtl.abort(new DownloadAborted(reason));
72
+ }
73
+
74
+ public getBlob():
75
+ | { done: false }
76
+ | {
77
+ done: true;
78
+ result: ValueOrError<LocalBlobHandleAndSize>;
79
+ } {
80
+ if (!this.done) return { done: false };
81
+
82
+ if (this.error)
83
+ return {
84
+ done: true,
85
+ result: { ok: false, error: this.error }
86
+ };
87
+
88
+ return {
89
+ done: true,
90
+ result: {
91
+ ok: true,
92
+ value: {
93
+ handle: this.handle,
94
+ size: this.size
95
+ }
96
+ }
97
+ };
98
+ }
99
+
100
+ private setDone(sizeBytes: number) {
101
+ this.done = true;
102
+ this.size = sizeBytes;
103
+ }
104
+
105
+ private setError(e: any) {
106
+ this.done = true;
107
+ this.error = e;
108
+ }
109
+ }
110
+
111
+ export function nonRecoverableError(e: any) {
112
+ return (
113
+ e instanceof DownloadAborted ||
114
+ e instanceof NetworkError400 ||
115
+ e instanceof UnknownStorageError ||
116
+ e instanceof WrongLocalFileUrl ||
117
+ // file that we downloads from was moved or deleted.
118
+ e?.code == 'ENOENT' ||
119
+ // A resource was deleted.
120
+ (e.name == 'RpcError' && (e.code == 'NOT_FOUND' || e.code == 'ABORTED'))
121
+ );
122
+ }
123
+
124
+ /** The downloading task was aborted by a signal.
125
+ * It may happen when the computable is done, for example. */
126
+ class DownloadAborted extends Error {}
@@ -38,7 +38,7 @@ test('should download a tar archive and extracts its content and then deleted',
38
38
 
39
39
  c.resetState();
40
40
  });
41
- });
41
+ }, 15000);
42
42
 
43
43
  test('should show a error when 404 status code', async () => {
44
44
  try {
@@ -1,20 +1,22 @@
1
+ import { ChangeSource, Computable, ComputableCtx, Watcher } from '@milaboratories/computable';
1
2
  import {
2
3
  CallersCounter,
3
4
  MiLogger,
4
5
  TaskProcessor,
5
- notEmpty,
6
- fileExists
6
+ createPathAtomically,
7
+ ensureDirExists,
8
+ fileExists,
9
+ notEmpty
7
10
  } from '@milaboratories/ts-helpers';
11
+ import { createHash, randomUUID } from 'node:crypto';
8
12
  import * as fsp from 'node:fs/promises';
9
13
  import * as path from 'node:path';
10
- import { Writable, Transform } from 'node:stream';
11
- import { ChangeSource, Computable, ComputableCtx, Watcher } from '@milaboratories/computable';
12
- import { randomUUID, createHash } from 'node:crypto';
14
+ import { Transform, Writable } from 'node:stream';
13
15
  import * as zlib from 'node:zlib';
14
16
  import * as tar from 'tar-fs';
15
- import { FilesCache } from './helpers/files_cache';
16
17
  import { Dispatcher } from 'undici';
17
- import { DownloadHelper, NetworkError400 } from '../helpers/download';
18
+ import { NetworkError400, RemoteFileDownloader } from '../helpers/download';
19
+ import { FilesCache } from './helpers/files_cache';
18
20
 
19
21
  export interface DownloadUrlSyncReader {
20
22
  /** Returns a Computable that (when the time will come)
@@ -39,14 +41,14 @@ export type DownloadUrlDriverOps = {
39
41
  /** Downloads .tar or .tar.gz archives by given URLs
40
42
  * and extracts them into saveDir. */
41
43
  export class DownloadUrlDriver implements DownloadUrlSyncReader {
42
- private readonly downloadHelper: DownloadHelper;
44
+ private readonly downloadHelper: RemoteFileDownloader;
43
45
 
44
- private urlToDownload: Map<string, Download> = new Map();
46
+ private urlToDownload: Map<string, DownloadByUrlTask> = new Map();
45
47
  private downloadQueue: TaskProcessor;
46
48
 
47
49
  /** Writes and removes files to a hard drive and holds a counter for every
48
50
  * file that should be kept. */
49
- private cache: FilesCache<Download>;
51
+ private cache: FilesCache<DownloadByUrlTask>;
50
52
 
51
53
  constructor(
52
54
  private readonly logger: MiLogger,
@@ -60,7 +62,7 @@ export class DownloadUrlDriver implements DownloadUrlSyncReader {
60
62
  ) {
61
63
  this.downloadQueue = new TaskProcessor(this.logger, this.opts.nConcurrentDownloads);
62
64
  this.cache = new FilesCache(this.opts.cacheSoftSizeBytes);
63
- this.downloadHelper = new DownloadHelper(httpClient);
65
+ this.downloadHelper = new RemoteFileDownloader(httpClient);
64
66
  }
65
67
 
66
68
  /** Use to get a path result inside a computable context */
@@ -109,7 +111,7 @@ export class DownloadUrlDriver implements DownloadUrlSyncReader {
109
111
  }
110
112
 
111
113
  /** Downloads and extracts a tar archive if it wasn't downloaded yet. */
112
- async downloadUrl(task: Download, callerId: string) {
114
+ async downloadUrl(task: DownloadByUrlTask, callerId: string) {
113
115
  await task.download(this.downloadHelper, this.opts.withGunzip);
114
116
  // Might be undefined if a error happened
115
117
  if (task.getPath()?.path != undefined) this.cache.addCache(task, callerId);
@@ -159,14 +161,14 @@ export class DownloadUrlDriver implements DownloadUrlSyncReader {
159
161
  }
160
162
 
161
163
  private setNewTask(w: Watcher, url: URL, callerId: string) {
162
- const result = new Download(this.getFilePath(url), url);
164
+ const result = new DownloadByUrlTask(this.logger, this.getFilePath(url), url);
163
165
  result.attach(w, callerId);
164
166
  this.urlToDownload.set(url.toString(), result);
165
167
 
166
168
  return result;
167
169
  }
168
170
 
169
- private removeTask(task: Download, reason: string) {
171
+ private removeTask(task: DownloadByUrlTask, reason: string) {
170
172
  task.abort(reason);
171
173
  task.change.markChanged();
172
174
  this.urlToDownload.delete(task.url.toString());
@@ -178,15 +180,17 @@ export class DownloadUrlDriver implements DownloadUrlSyncReader {
178
180
  }
179
181
  }
180
182
 
181
- class Download {
183
+ /** Downloads and extracts an archive to a directory. */
184
+ class DownloadByUrlTask {
182
185
  readonly counter = new CallersCounter();
183
186
  readonly change = new ChangeSource();
184
- readonly signalCtl = new AbortController();
187
+ private readonly signalCtl = new AbortController();
185
188
  error: string | undefined;
186
189
  done = false;
187
- sizeBytes = 0;
190
+ size = 0;
188
191
 
189
192
  constructor(
193
+ private readonly logger: MiLogger,
190
194
  readonly path: string,
191
195
  readonly url: URL
192
196
  ) {}
@@ -196,17 +200,15 @@ class Download {
196
200
  if (!this.done) this.change.attachWatcher(w);
197
201
  }
198
202
 
199
- async download(clientDownload: DownloadHelper, withGunzip: boolean) {
203
+ async download(clientDownload: RemoteFileDownloader, withGunzip: boolean) {
200
204
  try {
201
- const sizeBytes = await this.downloadAndUntar(
202
- clientDownload,
203
- withGunzip,
204
- this.signalCtl.signal
205
- );
206
- this.setDone(sizeBytes);
205
+ const size = await this.downloadAndUntar(clientDownload, withGunzip, this.signalCtl.signal);
206
+ this.setDone(size);
207
+ this.change.markChanged();
207
208
  } catch (e: any) {
208
209
  if (e instanceof URLAborted || e instanceof NetworkError400) {
209
210
  this.setError(e);
211
+ this.change.markChanged();
210
212
  // Just in case we were half-way extracting an archive.
211
213
  await rmRFDir(this.path);
212
214
  return;
@@ -217,23 +219,29 @@ class Download {
217
219
  }
218
220
 
219
221
  private async downloadAndUntar(
220
- clientDownload: DownloadHelper,
222
+ clientDownload: RemoteFileDownloader,
221
223
  withGunzip: boolean,
222
224
  signal: AbortSignal
223
225
  ): Promise<number> {
226
+ await ensureDirExists(path.dirname(this.path));
227
+
224
228
  if (await fileExists(this.path)) {
225
229
  return await dirSize(this.path);
226
230
  }
227
231
 
228
- const resp = await clientDownload.downloadRemoteFile(this.url.toString(), {}, signal);
229
- let content = resp.content;
232
+ const resp = await clientDownload.download(this.url.toString(), {}, signal);
230
233
 
234
+ let content = resp.content;
231
235
  if (withGunzip) {
232
236
  const gunzip = Transform.toWeb(zlib.createGunzip());
233
237
  content = content.pipeThrough(gunzip, { signal });
234
238
  }
235
- const untar = Writable.toWeb(tar.extract(this.path));
236
- await content.pipeTo(untar, { signal });
239
+
240
+ await createPathAtomically(this.logger, this.path, async (fPath: string) => {
241
+ await fsp.mkdir(fPath); // throws if a directory already exists.
242
+ const untar = Writable.toWeb(tar.extract(fPath));
243
+ await content.pipeTo(untar, { signal });
244
+ });
237
245
 
238
246
  return resp.size;
239
247
  }
@@ -246,22 +254,21 @@ class Download {
246
254
  return undefined;
247
255
  }
248
256
 
249
- private setDone(sizeBytes: number) {
257
+ private setDone(size: number) {
250
258
  this.done = true;
251
- this.sizeBytes = sizeBytes;
252
- this.change.markChanged();
253
- }
254
-
255
- abort(reason: string) {
256
- this.signalCtl.abort(new URLAborted(reason));
259
+ this.size = size;
257
260
  }
258
261
 
259
262
  private setError(e: any) {
260
263
  this.error = String(e);
261
- this.change.markChanged();
264
+ }
265
+
266
+ abort(reason: string) {
267
+ this.signalCtl.abort(new URLAborted(reason));
262
268
  }
263
269
  }
264
270
 
271
+ /** Throws when a downloading aborts. */
265
272
  class URLAborted extends Error {}
266
273
 
267
274
  /** Gets a directory size by calculating sizes recursively. */
@@ -0,0 +1,29 @@
1
+ /** Handle of locally downloaded blob. This handle is issued only after the
2
+ * blob's content is downloaded locally, and ready for quick access. */
3
+
4
+ import { LocalBlobHandle } from '@milaboratories/pl-model-common';
5
+ import { Signer } from '@milaboratories/ts-helpers';
6
+
7
+ // https://regex101.com/r/kfnBVX/1
8
+ const localHandleRegex = /^blob\+local:\/\/download\/(?<path>.*)#(?<signature>.*)$/;
9
+
10
+ export function newLocalHandle(path: string, signer: Signer): LocalBlobHandle {
11
+ return `blob+local://download/${path}#${signer.sign(path)}` as LocalBlobHandle;
12
+ }
13
+
14
+ export function isLocalBlobHandle(handle: string): handle is LocalBlobHandle {
15
+ return Boolean(handle.match(localHandleRegex));
16
+ }
17
+
18
+ export function parseLocalHandle(handle: LocalBlobHandle, signer: Signer) {
19
+ const parsed = handle.match(localHandleRegex);
20
+
21
+ if (parsed === null) {
22
+ throw new Error(`Local handle is malformed: ${handle}, matches: ${parsed}`);
23
+ }
24
+
25
+ const { path, signature } = parsed.groups!;
26
+ signer.verify(path, signature, `Signature verification failed for: ${handle}`);
27
+
28
+ return { path, signature };
29
+ }
@@ -0,0 +1,40 @@
1
+ /** Handle of remote blob. This handle is issued as soon as the data becomes
2
+ * available on the remote server. */
3
+
4
+ import { Signer } from '@milaboratories/ts-helpers';
5
+ import { OnDemandBlobResourceSnapshot } from '../types';
6
+ import { RemoteBlobHandle } from '@milaboratories/pl-model-common';
7
+ import { ResourceInfo } from '@milaboratories/pl-tree';
8
+ import { bigintToResourceId } from '@milaboratories/pl-client';
9
+
10
+ // https://regex101.com/r/rvbPZt/1
11
+ const remoteHandleRegex =
12
+ /^blob\+remote:\/\/download\/(?<content>(?<resourceType>.*)\/(?<resourceVersion>.*)\/(?<resourceId>.*))#(?<signature>.*)$/;
13
+
14
+ export function newRemoteHandle(
15
+ rInfo: OnDemandBlobResourceSnapshot,
16
+ signer: Signer
17
+ ): RemoteBlobHandle {
18
+ const content = `${rInfo.type.name}/${rInfo.type.version}/${BigInt(rInfo.id)}`;
19
+ return `blob+remote://download/${content}#${signer.sign(content)}` as RemoteBlobHandle;
20
+ }
21
+
22
+ export function isRemoteBlobHandle(handle: string): handle is RemoteBlobHandle {
23
+ return Boolean(handle.match(remoteHandleRegex));
24
+ }
25
+
26
+ export function parseRemoteHandle(handle: RemoteBlobHandle, signer: Signer): ResourceInfo {
27
+ const parsed = handle.match(remoteHandleRegex);
28
+ if (parsed === null) {
29
+ throw new Error(`Remote handle is malformed: ${handle}, matches: ${parsed}`);
30
+ }
31
+
32
+ const { content, resourceType, resourceVersion, resourceId, signature } = parsed.groups!;
33
+
34
+ signer.verify(content, signature, `Signature verification failed for ${handle}`);
35
+
36
+ return {
37
+ id: bigintToResourceId(BigInt(resourceId)),
38
+ type: { name: resourceType, version: resourceVersion }
39
+ };
40
+ }
@@ -1,22 +1,23 @@
1
- import { CachedFile, FilesCache } from './files_cache';
1
+ import { expect, test } from '@jest/globals';
2
2
  import { CallersCounter } from '@milaboratories/ts-helpers';
3
+ import { CachedFile, FilesCache } from './files_cache';
3
4
 
4
5
  test('should delete blob3 when add 3 blobs, exceed a soft limit and nothing holds blob3', () => {
5
6
  const cache = new FilesCache(20);
6
7
  const callerId1 = 'callerId1';
7
8
  const blob1: CachedFile = {
8
9
  path: 'path1',
9
- sizeBytes: 5,
10
+ size: 5,
10
11
  counter: new CallersCounter()
11
12
  };
12
13
  const blob2: CachedFile = {
13
14
  path: 'path2',
14
- sizeBytes: 10,
15
+ size: 10,
15
16
  counter: new CallersCounter()
16
17
  };
17
18
  const blob3: CachedFile = {
18
19
  path: 'path3',
19
- sizeBytes: 10,
20
+ size: 10,
20
21
  counter: new CallersCounter()
21
22
  };
22
23
 
@@ -51,12 +52,12 @@ test('regression should allow to add empty files', () => {
51
52
  const callerId1 = 'callerId1';
52
53
  const blob1: CachedFile = {
53
54
  path: 'path1',
54
- sizeBytes: 0,
55
+ size: 0,
55
56
  counter: new CallersCounter()
56
57
  };
57
58
  const blob2: CachedFile = {
58
59
  path: 'path2',
59
- sizeBytes: 2,
60
+ size: 2,
60
61
  counter: new CallersCounter()
61
62
  };
62
63
 
@@ -3,7 +3,8 @@ import { CallersCounter, mapEntries, mapGet } from '@milaboratories/ts-helpers';
3
3
  type PathLike = string;
4
4
 
5
5
  export interface CachedFile {
6
- sizeBytes: number;
6
+ /** Size in bytes. */
7
+ size: number;
7
8
  path: PathLike;
8
9
  counter: CallersCounter;
9
10
  }
@@ -50,7 +51,7 @@ export class FilesCache<T extends CachedFile> {
50
51
  .forEach(([path, _]) => {
51
52
  if (this.totalSizeBytes - freedBytes <= this.softSizeBytes) return;
52
53
  const file = mapGet(this.cache, path);
53
- freedBytes += file.sizeBytes;
54
+ freedBytes += file.size;
54
55
  result.push(file);
55
56
  });
56
57
 
@@ -62,13 +63,13 @@ export class FilesCache<T extends CachedFile> {
62
63
  this.cache.set(file.path, file);
63
64
  file.counter.inc(callerId);
64
65
 
65
- if (file.sizeBytes < 0) throw new Error(`empty sizeBytes: ${file}`);
66
+ if (file.size < 0) throw new Error(`empty sizeBytes: ${file}`);
66
67
 
67
- if (created) this.totalSizeBytes += file.sizeBytes;
68
+ if (created) this.totalSizeBytes += file.size;
68
69
  }
69
70
 
70
71
  removeCache(file: T) {
71
72
  this.cache.delete(file.path);
72
- this.totalSizeBytes -= file.sizeBytes;
73
+ this.totalSizeBytes -= file.size;
73
74
  }
74
75
  }
@@ -1,37 +1,14 @@
1
- import type { RpcOptions } from '@protobuf-ts/runtime-rpc';
2
- import { ClientLogs } from '../../clients/logs';
3
1
  import {
4
- PlClient,
5
- ResourceId,
6
2
  BasicResourceData,
3
+ getField,
7
4
  isNullResourceId,
8
- valErr,
9
- getField
5
+ PlClient,
6
+ ResourceId,
7
+ valErr
10
8
  } from '@milaboratories/pl-client';
11
- import { scheduler } from 'node:timers/promises';
12
- import { ResourceInfo } from '@milaboratories/pl-tree';
13
-
14
- // TODO: remove this when we switch to refreshState.
15
-
16
- /** It's an Updater but for tasks that happens in a while loop with sleeping between. */
17
- export class LongUpdater {
18
- private updater: Updater;
19
-
20
- constructor(
21
- private readonly onUpdate: () => Promise<boolean>,
22
- private readonly sleepMs: number
23
- ) {
24
- this.updater = new Updater(async () => {
25
- while (true) {
26
- const done = await this.onUpdate();
27
- if (done) return;
28
- await scheduler.wait(this.sleepMs);
29
- }
30
- });
31
- }
32
9
 
33
- schedule = () => this.updater.schedule();
34
- }
10
+ /** Throws when a driver gets a resource with a wrong resource type. */
11
+ export class WrongResourceTypeError extends Error {}
35
12
 
36
13
  /** Updater incorporates a pattern when someone wants to run a callback
37
14
  * that updates something only when it's not already running. */
@@ -54,74 +31,3 @@ export class Updater {
54
31
  }
55
32
  }
56
33
  }
57
-
58
- // TODO: remove all the code below to the computable that calculates Mixcr logs.
59
-
60
- export async function getStream(
61
- client: PlClient,
62
- streamManagerId: ResourceId
63
- ): Promise<BasicResourceData | undefined> {
64
- return client.withReadTx('LogsDriverGetStream', async (tx) => {
65
- const sm = await tx.getResourceData(streamManagerId, true);
66
- const stream = await valErr(tx, getField(sm, 'stream'));
67
- if (stream.error != '') {
68
- throw new Error(`while getting stream: ${stream.error}`);
69
- }
70
- if (isNullResourceId(stream.valueId)) return undefined;
71
-
72
- return await tx.getResourceData(stream.valueId, false);
73
- });
74
- }
75
-
76
- export type MixcrProgressResponse = { found: false } | ({ found: true } & MixcrProgressLine);
77
-
78
- export type MixcrProgressLine = {
79
- stage: string; // Building pre-clones from tag groups
80
- progress: string; // 35.3%
81
- eta: string; // ETA: 00:00:07
82
- };
83
-
84
- /** Is set by a template code.
85
- * Mixcr adds this prefix to every log line that contains a progress. */
86
- const mixcrProgressPrefix = '8C7#F1328%9E089B3D22';
87
- const mixcrProgressRegex = /(?<stage>.*):\s*(?<progress>[\d.]+%)\s.*(?<eta>ETA:.*)/g;
88
-
89
- export function lineToProgress(line: string): MixcrProgressLine | undefined {
90
- const noPrefix = line.replace(mixcrProgressPrefix, '');
91
- const parsed = noPrefix.match(mixcrProgressRegex);
92
-
93
- if (parsed == null || parsed.length != 4) {
94
- return undefined;
95
- }
96
-
97
- const [_, stage, progress, eta] = parsed;
98
-
99
- return {
100
- stage, // For example, 'Building pre-clones from tag groups'
101
- progress, // 35.3%
102
- eta // ETA: 00:00:07
103
- };
104
- }
105
-
106
- export async function mixcrProgressFromLogs(
107
- rInfo: ResourceInfo,
108
- client: ClientLogs,
109
- options?: RpcOptions
110
- ): Promise<MixcrProgressResponse> {
111
- const lastLines = await client.lastLines(rInfo, 1, 0n, mixcrProgressPrefix, options);
112
- if (lastLines.data == null || lastLines.data.length == 0) {
113
- return { found: false };
114
- }
115
-
116
- const line = lastLines.data.toString().split(/\r?\n/)[0];
117
- if (line == undefined) {
118
- return { found: false };
119
- }
120
-
121
- const progress = lineToProgress(line);
122
- if (progress === undefined) {
123
- return { found: false };
124
- }
125
-
126
- return { found: true, ...progress };
127
- }
@@ -0,0 +1,52 @@
1
+ /** Handle of logs. This handle should be passed
2
+ * to the driver for retrieving logs. */
3
+
4
+ import { ResourceInfo } from '@milaboratories/pl-tree';
5
+ import * as sdk from '@milaboratories/pl-model-common';
6
+ import { bigintToResourceId } from '@milaboratories/pl-client';
7
+
8
+ export function newLogHandle(live: boolean, rInfo: ResourceInfo): sdk.AnyLogHandle {
9
+ if (live) {
10
+ return `log+live://log/${rInfo.type.name}/${rInfo.type.version}/${BigInt(rInfo.id)}` as sdk.LiveLogHandle;
11
+ }
12
+
13
+ return `log+ready://log/${rInfo.type.name}/${rInfo.type.version}/${BigInt(rInfo.id)}` as sdk.ReadyLogHandle;
14
+ }
15
+
16
+ /** Handle of the live logs of a program.
17
+ * The resource that represents a log can be deleted,
18
+ * in this case the handle should be refreshed. */
19
+
20
+ export const liveHandleRegex =
21
+ /^log\+live:\/\/log\/(?<resourceType>.*)\/(?<resourceVersion>.*)\/(?<resourceId>.*)$/;
22
+
23
+ export function isLiveLogHandle(handle: string): handle is sdk.LiveLogHandle {
24
+ return liveHandleRegex.test(handle);
25
+ }
26
+
27
+ /** Handle of the ready logs of a program. */
28
+
29
+ export const readyHandleRegex =
30
+ /^log\+ready:\/\/log\/(?<resourceType>.*)\/(?<resourceVersion>.*)\/(?<resourceId>.*)$/;
31
+
32
+ export function isReadyLogHandle(handle: string): handle is sdk.ReadyLogHandle {
33
+ return readyHandleRegex.test(handle);
34
+ }
35
+
36
+ export function getResourceInfoFromLogHandle(handle: sdk.AnyLogHandle): ResourceInfo {
37
+ let parsed: RegExpMatchArray | null;
38
+
39
+ if (isLiveLogHandle(handle)) {
40
+ parsed = handle.match(liveHandleRegex);
41
+ } else if (isReadyLogHandle(handle)) {
42
+ parsed = handle.match(readyHandleRegex);
43
+ } else throw new Error(`Log handle is malformed: ${handle}`);
44
+ if (parsed == null) throw new Error(`Log handle wasn't parsed: ${handle}`);
45
+
46
+ const { resourceType, resourceVersion, resourceId } = parsed.groups!;
47
+
48
+ return {
49
+ id: bigintToResourceId(BigInt(resourceId)),
50
+ type: { name: resourceType, version: resourceVersion }
51
+ };
52
+ }
@@ -0,0 +1,43 @@
1
+ import * as sdk from '@milaboratories/pl-model-common';
2
+ import { Signer } from '@milaboratories/ts-helpers';
3
+ import { ImportFileHandleIndexData, ImportFileHandleUploadData } from '../types';
4
+
5
+ export function createIndexImportHandle(
6
+ storageName: string,
7
+ path: string
8
+ ): sdk.ImportFileHandleIndex {
9
+ const data: ImportFileHandleIndexData = {
10
+ storageId: storageName,
11
+ path: path
12
+ };
13
+
14
+ return `index://index/${encodeURIComponent(JSON.stringify(data))}`;
15
+ }
16
+
17
+ export function createUploadImportHandle(
18
+ localPath: string,
19
+ signer: Signer,
20
+ sizeBytes: bigint,
21
+ modificationTimeSeconds: bigint
22
+ ): sdk.ImportFileHandleUpload {
23
+ const data: ImportFileHandleUploadData = {
24
+ localPath,
25
+ pathSignature: signer.sign(localPath),
26
+ sizeBytes: String(sizeBytes),
27
+ modificationTime: String(modificationTimeSeconds)
28
+ };
29
+
30
+ return `upload://upload/${encodeURIComponent(JSON.stringify(data))}`;
31
+ }
32
+
33
+ export function parseUploadHandle(handle: sdk.ImportFileHandleUpload): ImportFileHandleUploadData {
34
+ const url = new URL(handle);
35
+ return ImportFileHandleUploadData.parse(
36
+ JSON.parse(decodeURIComponent(url.pathname.substring(1)))
37
+ );
38
+ }
39
+
40
+ export function parseIndexHandle(handle: sdk.ImportFileHandleIndex): ImportFileHandleIndexData {
41
+ const url = new URL(handle);
42
+ return ImportFileHandleIndexData.parse(JSON.parse(decodeURIComponent(url.pathname.substring(1))));
43
+ }