@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.
- package/dist/clients/download.d.ts +6 -2
- package/dist/clients/download.d.ts.map +1 -1
- package/dist/clients/helpers.d.ts +2 -1
- package/dist/clients/helpers.d.ts.map +1 -1
- package/dist/drivers/download_blob/blob_key.d.ts +5 -0
- package/dist/drivers/download_blob/blob_key.d.ts.map +1 -0
- package/dist/drivers/download_blob/download_blob.d.ts +12 -9
- package/dist/drivers/download_blob/download_blob.d.ts.map +1 -1
- package/dist/drivers/download_blob/download_blob_task.d.ts +20 -9
- package/dist/drivers/download_blob/download_blob_task.d.ts.map +1 -1
- package/dist/drivers/helpers/download_remote_handle.d.ts.map +1 -1
- package/dist/drivers/types.d.ts +2 -1
- package/dist/drivers/types.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1846 -1802
- package/dist/index.mjs.map +1 -1
- package/dist/test_env.d.ts +6 -0
- package/dist/test_env.d.ts.map +1 -0
- package/package.json +10 -8
- package/src/clients/download.ts +7 -4
- package/src/clients/helpers.ts +3 -3
- package/src/drivers/download_blob/blob_key.ts +15 -0
- package/src/drivers/download_blob/download_blob.test.ts +122 -72
- package/src/drivers/download_blob/download_blob.ts +159 -81
- package/src/drivers/download_blob/download_blob_task.ts +69 -33
- package/src/drivers/helpers/download_remote_handle.ts +7 -6
- package/src/drivers/ls.test.ts +9 -6
- package/src/drivers/types.ts +13 -2
- package/src/drivers/upload.test.ts +3 -2
- package/src/test_env.ts +7 -0
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
174
|
+
ctx.addOnDestroy(() => this.releaseBlob(rInfo, callerId));
|
|
136
175
|
|
|
137
|
-
const result = this.getDownloadedBlobNoCtx(ctx.watcher, rInfo as ResourceSnapshot, callerId
|
|
138
|
-
if (result == 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
|
-
|
|
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)
|
|
167
|
-
|
|
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
|
|
172
|
-
|
|
173
|
-
|
|
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.
|
|
228
|
+
this.keyToDownload.set(key, newTask);
|
|
181
229
|
|
|
182
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
456
|
-
const task = this.
|
|
457
|
-
if (task == undefined)
|
|
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 (
|
|
463
|
-
await fsp.rm(
|
|
541
|
+
toDelete.map(async (cachedFile) => {
|
|
542
|
+
await fsp.rm(cachedFile.path);
|
|
464
543
|
|
|
465
|
-
this.cache.removeCache(
|
|
544
|
+
this.cache.removeCache(cachedFile);
|
|
466
545
|
|
|
467
546
|
this.removeTask(
|
|
468
|
-
|
|
469
|
-
`the task ${stringifyWithResourceId(
|
|
470
|
-
+ `from cache along with ${stringifyWithResourceId(toDelete.map((d) => d.
|
|
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.
|
|
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.
|
|
496
|
-
if (deleted) this.
|
|
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.
|
|
504
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
27
|
+
public readonly counter = new CallersCounter();
|
|
28
|
+
private error: unknown | undefined;
|
|
29
29
|
private done = false;
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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(
|
|
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.
|
|
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:
|
|
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:
|
|
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
|
|
8
|
-
import {
|
|
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/
|
|
10
|
+
// https://regex101.com/r/Q4YdTa/4
|
|
11
11
|
const remoteHandleRegex
|
|
12
|
-
= /^blob\+remote:\/\/download\/(?<content>(?<resourceType
|
|
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
|
-
|
|
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
|
|
package/src/drivers/ls.test.ts
CHANGED
|
@@ -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 ==
|
|
25
|
-
expect(got.find((se) => se.name ==
|
|
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 ==
|
|
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
|
-
|
|
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);
|