@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.
- package/dist/clients/constructors.d.ts +14 -0
- package/dist/clients/constructors.d.ts.map +1 -0
- package/dist/clients/download.d.ts +19 -14
- package/dist/clients/download.d.ts.map +1 -1
- package/dist/clients/helpers.d.ts +4 -13
- package/dist/clients/helpers.d.ts.map +1 -1
- package/dist/clients/upload.d.ts +15 -13
- package/dist/clients/upload.d.ts.map +1 -1
- package/dist/drivers/{download_and_logs_blob.d.ts → download_blob.d.ts} +13 -40
- package/dist/drivers/download_blob.d.ts.map +1 -0
- package/dist/drivers/download_blob_task.d.ts +34 -0
- package/dist/drivers/download_blob_task.d.ts.map +1 -0
- package/dist/drivers/download_url.d.ts +11 -9
- package/dist/drivers/download_url.d.ts.map +1 -1
- package/dist/drivers/helpers/download_local_handle.d.ts +9 -0
- package/dist/drivers/helpers/download_local_handle.d.ts.map +1 -0
- package/dist/drivers/helpers/download_remote_handle.d.ts +8 -0
- package/dist/drivers/helpers/download_remote_handle.d.ts.map +1 -0
- package/dist/drivers/helpers/files_cache.d.ts +2 -1
- package/dist/drivers/helpers/files_cache.d.ts.map +1 -1
- package/dist/drivers/helpers/helpers.d.ts +2 -24
- package/dist/drivers/helpers/helpers.d.ts.map +1 -1
- package/dist/drivers/helpers/logs_handle.d.ts +13 -0
- package/dist/drivers/helpers/logs_handle.d.ts.map +1 -0
- package/dist/drivers/helpers/ls_remote_import_handle.d.ts +8 -0
- package/dist/drivers/helpers/ls_remote_import_handle.d.ts.map +1 -0
- package/dist/drivers/logs.d.ts +1 -5
- package/dist/drivers/logs.d.ts.map +1 -1
- package/dist/drivers/logs_stream.d.ts.map +1 -1
- package/dist/drivers/ls.d.ts.map +1 -1
- package/dist/drivers/types.d.ts +47 -4
- package/dist/drivers/types.d.ts.map +1 -1
- package/dist/drivers/upload.d.ts +2 -28
- package/dist/drivers/upload.d.ts.map +1 -1
- package/dist/drivers/upload_task.d.ts +41 -0
- package/dist/drivers/upload_task.d.ts.map +1 -0
- package/dist/helpers/download.d.ts +2 -2
- package/dist/helpers/download.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1537 -1526
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -7
- package/src/clients/constructors.ts +54 -0
- package/src/clients/download.test.ts +9 -6
- package/src/clients/download.ts +74 -64
- package/src/clients/helpers.ts +2 -53
- package/src/clients/upload.ts +136 -102
- package/src/drivers/download_blob.test.ts +12 -11
- package/src/drivers/{download_and_logs_blob.ts → download_blob.ts} +154 -290
- package/src/drivers/download_blob_task.ts +126 -0
- package/src/drivers/download_url.test.ts +1 -1
- package/src/drivers/download_url.ts +44 -37
- package/src/drivers/helpers/download_local_handle.ts +29 -0
- package/src/drivers/helpers/download_remote_handle.ts +40 -0
- package/src/drivers/helpers/files_cache.test.ts +7 -6
- package/src/drivers/helpers/files_cache.ts +6 -5
- package/src/drivers/helpers/helpers.ts +6 -100
- package/src/drivers/helpers/logs_handle.ts +52 -0
- package/src/drivers/helpers/ls_remote_import_handle.ts +43 -0
- package/src/drivers/logs.test.ts +14 -14
- package/src/drivers/logs.ts +3 -43
- package/src/drivers/logs_stream.ts +32 -6
- package/src/drivers/ls.test.ts +1 -2
- package/src/drivers/ls.ts +26 -28
- package/src/drivers/types.ts +48 -0
- package/src/drivers/upload.test.ts +8 -18
- package/src/drivers/upload.ts +38 -271
- package/src/drivers/upload_task.ts +251 -0
- package/src/helpers/download.ts +18 -15
- package/src/index.ts +2 -2
- package/dist/drivers/download_and_logs_blob.d.ts.map +0 -1
- package/dist/drivers/helpers/ls_list_entry.d.ts +0 -44
- package/dist/drivers/helpers/ls_list_entry.d.ts.map +0 -1
- package/src/drivers/helpers/ls_list_entry.test.ts +0 -55
- package/src/drivers/helpers/ls_list_entry.ts +0 -147
|
@@ -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 {}
|
|
@@ -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
|
-
|
|
6
|
-
|
|
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 {
|
|
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 {
|
|
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:
|
|
44
|
+
private readonly downloadHelper: RemoteFileDownloader;
|
|
43
45
|
|
|
44
|
-
private urlToDownload: Map<string,
|
|
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<
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
203
|
+
async download(clientDownload: RemoteFileDownloader, withGunzip: boolean) {
|
|
200
204
|
try {
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
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:
|
|
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.
|
|
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
|
-
|
|
236
|
-
await
|
|
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(
|
|
257
|
+
private setDone(size: number) {
|
|
250
258
|
this.done = true;
|
|
251
|
-
this.
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
10
|
+
size: 5,
|
|
10
11
|
counter: new CallersCounter()
|
|
11
12
|
};
|
|
12
13
|
const blob2: CachedFile = {
|
|
13
14
|
path: 'path2',
|
|
14
|
-
|
|
15
|
+
size: 10,
|
|
15
16
|
counter: new CallersCounter()
|
|
16
17
|
};
|
|
17
18
|
const blob3: CachedFile = {
|
|
18
19
|
path: 'path3',
|
|
19
|
-
|
|
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
|
-
|
|
55
|
+
size: 0,
|
|
55
56
|
counter: new CallersCounter()
|
|
56
57
|
};
|
|
57
58
|
const blob2: CachedFile = {
|
|
58
59
|
path: 'path2',
|
|
59
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
66
|
+
if (file.size < 0) throw new Error(`empty sizeBytes: ${file}`);
|
|
66
67
|
|
|
67
|
-
if (created) this.totalSizeBytes += file.
|
|
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.
|
|
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
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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
|
+
}
|