@milaboratories/pl-drivers 1.3.26 → 1.4.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 +6 -6
- 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@milaboratories/pl-drivers",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
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",
|
|
@@ -26,11 +26,11 @@
|
|
|
26
26
|
"tar-fs": "^3.0.6",
|
|
27
27
|
"undici": "^6.21.0",
|
|
28
28
|
"zod": "^3.23.8",
|
|
29
|
-
"@milaboratories/
|
|
30
|
-
"@milaboratories/pl-
|
|
31
|
-
"@milaboratories/
|
|
32
|
-
"@milaboratories/pl-
|
|
33
|
-
"@milaboratories/
|
|
29
|
+
"@milaboratories/ts-helpers": "^1.1.2",
|
|
30
|
+
"@milaboratories/pl-tree": "^1.4.18",
|
|
31
|
+
"@milaboratories/computable": "^2.3.3",
|
|
32
|
+
"@milaboratories/pl-client": "^2.6.3",
|
|
33
|
+
"@milaboratories/pl-model-common": "^1.8.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"typescript": "~5.5.4",
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { PlClient } from '@milaboratories/pl-client';
|
|
2
|
+
import { MiLogger } from '@milaboratories/ts-helpers';
|
|
3
|
+
import { GrpcTransport } from '@protobuf-ts/grpc-transport';
|
|
4
|
+
import { Dispatcher } from 'undici';
|
|
5
|
+
import { ClientDownload } from './download';
|
|
6
|
+
import { ClientLogs } from './logs';
|
|
7
|
+
import { ClientProgress } from './progress';
|
|
8
|
+
import { ClientUpload } from './upload';
|
|
9
|
+
import { ClientLs } from './ls_api';
|
|
10
|
+
import { LocalStorageProjection } from '../drivers/types';
|
|
11
|
+
|
|
12
|
+
export function createDownloadClient(
|
|
13
|
+
logger: MiLogger,
|
|
14
|
+
client: PlClient,
|
|
15
|
+
localProjections: LocalStorageProjection[]
|
|
16
|
+
) {
|
|
17
|
+
return client.getDriver({
|
|
18
|
+
name: 'DownloadBlob',
|
|
19
|
+
init: (_: PlClient, grpcTransport: GrpcTransport, httpDispatcher: Dispatcher) =>
|
|
20
|
+
new ClientDownload(grpcTransport, httpDispatcher, logger, localProjections)
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createLogsClient(client: PlClient, logger: MiLogger) {
|
|
25
|
+
return client.getDriver({
|
|
26
|
+
name: 'StreamLogs',
|
|
27
|
+
init: (_: PlClient, grpcTransport: GrpcTransport, httpDispatcher: Dispatcher) =>
|
|
28
|
+
new ClientLogs(grpcTransport, httpDispatcher, logger)
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createUploadProgressClient(client: PlClient, logger: MiLogger) {
|
|
33
|
+
return client.getDriver({
|
|
34
|
+
name: 'UploadProgress',
|
|
35
|
+
init: (_: PlClient, grpcTransport: GrpcTransport, httpDispatcher: Dispatcher) =>
|
|
36
|
+
new ClientProgress(grpcTransport, httpDispatcher, client, logger)
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createUploadBlobClient(client: PlClient, logger: MiLogger) {
|
|
41
|
+
return client.getDriver({
|
|
42
|
+
name: 'UploadBlob',
|
|
43
|
+
init: (_: PlClient, grpcTransport: GrpcTransport, httpDispatcher: Dispatcher) =>
|
|
44
|
+
new ClientUpload(grpcTransport, httpDispatcher, client, logger)
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function createLsFilesClient(client: PlClient, logger: MiLogger) {
|
|
49
|
+
return client.getDriver({
|
|
50
|
+
name: 'LsFiles',
|
|
51
|
+
init: (_client: PlClient, grpcTransport: GrpcTransport, _httpDispatcher: Dispatcher) =>
|
|
52
|
+
new ClientLs(grpcTransport, logger)
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -6,7 +6,7 @@ import { ConsoleLoggerAdapter } from '@milaboratories/ts-helpers';
|
|
|
6
6
|
import { GrpcTransport } from '@protobuf-ts/grpc-transport';
|
|
7
7
|
import { Dispatcher } from 'undici';
|
|
8
8
|
import { text } from 'node:stream/consumers';
|
|
9
|
-
import { ClientDownload,
|
|
9
|
+
import { ClientDownload, getFullPath, parseLocalUrl } from '../clients/download';
|
|
10
10
|
import { test, expect } from '@jest/globals';
|
|
11
11
|
|
|
12
12
|
test('should parse local file url even on Windows', () => {
|
|
@@ -15,12 +15,15 @@ test('should parse local file url even on Windows', () => {
|
|
|
15
15
|
const expectedFullPath =
|
|
16
16
|
'C:\\Users\\test\\67z\\2vy\\65i\\67z2vy65i0xwhjwsfsef_ex3k3hxe7qdc2cvtdfkdnhdp9kwlt7-7dmcy0kthe6u.json';
|
|
17
17
|
|
|
18
|
-
const got =
|
|
19
|
-
path.sep,
|
|
20
|
-
'\\'
|
|
21
|
-
); // for testing on *nix systems
|
|
18
|
+
const got = parseLocalUrl(url);
|
|
22
19
|
|
|
23
|
-
|
|
20
|
+
const fullPath = getFullPath(
|
|
21
|
+
got.storageId,
|
|
22
|
+
new Map([['main', 'C:\\Users\\test']]),
|
|
23
|
+
got.relativePath
|
|
24
|
+
).replace(path.sep, '\\'); // for testing on *nix systems
|
|
25
|
+
|
|
26
|
+
expect(fullPath).toEqual(expectedFullPath);
|
|
24
27
|
});
|
|
25
28
|
|
|
26
29
|
test('client download from a local file', async () => {
|
package/src/clients/download.ts
CHANGED
|
@@ -1,33 +1,27 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { addRTypeToMetadata } from '@milaboratories/pl-client';
|
|
2
|
+
import { ResourceInfo } from '@milaboratories/pl-tree';
|
|
3
|
+
import { MiLogger } from '@milaboratories/ts-helpers';
|
|
4
|
+
import { GrpcTransport } from '@protobuf-ts/grpc-transport';
|
|
5
|
+
import type { RpcOptions } from '@protobuf-ts/runtime-rpc';
|
|
2
6
|
import * as fs from 'node:fs';
|
|
3
7
|
import * as fsp from 'node:fs/promises';
|
|
4
8
|
import * as path from 'node:path';
|
|
5
|
-
import {
|
|
6
|
-
import { GrpcTransport } from '@protobuf-ts/grpc-transport';
|
|
7
|
-
import type { RpcOptions } from '@protobuf-ts/runtime-rpc';
|
|
8
|
-
import { MiLogger } from '@milaboratories/ts-helpers';
|
|
9
|
-
import { addRTypeToMetadata } from '@milaboratories/pl-client';
|
|
9
|
+
import { Readable } from 'node:stream';
|
|
10
10
|
import { Dispatcher } from 'undici';
|
|
11
|
-
import {
|
|
12
|
-
DownloadAPI_GetDownloadURL_HTTPHeader,
|
|
13
|
-
DownloadAPI_GetDownloadURL_Response
|
|
14
|
-
} from '../proto/github.com/milaboratory/pl/controllers/shared/grpc/downloadapi/protocol';
|
|
15
|
-
import { ResourceInfo } from '@milaboratories/pl-tree';
|
|
16
|
-
import { DownloadHelper, DownloadResponse } from '../helpers/download';
|
|
17
11
|
import { LocalStorageProjection } from '../drivers/types';
|
|
12
|
+
import { DownloadResponse, RemoteFileDownloader } from '../helpers/download';
|
|
18
13
|
import { validateAbsolute } from '../helpers/validate';
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
export class UnknownStorageError extends Error {}
|
|
23
|
-
|
|
24
|
-
export class WrongLocalFileUrl extends Error {}
|
|
14
|
+
import { DownloadAPI_GetDownloadURL_Response } from '../proto/github.com/milaboratory/pl/controllers/shared/grpc/downloadapi/protocol';
|
|
15
|
+
import { DownloadClient } from '../proto/github.com/milaboratory/pl/controllers/shared/grpc/downloadapi/protocol.client';
|
|
16
|
+
import { toHeadersMap } from './helpers';
|
|
25
17
|
|
|
26
18
|
/** Gets URLs for downloading from pl-core, parses them and reads or downloads
|
|
27
19
|
* files locally and from the web. */
|
|
28
20
|
export class ClientDownload {
|
|
29
21
|
public readonly grpcClient: DownloadClient;
|
|
30
|
-
private readonly
|
|
22
|
+
private readonly remoteFileDownloader: RemoteFileDownloader;
|
|
23
|
+
|
|
24
|
+
/** Helps to find a storage root directory by a storage id from URL scheme. */
|
|
31
25
|
private readonly localStorageIdsToRoot: Map<string, string>;
|
|
32
26
|
|
|
33
27
|
constructor(
|
|
@@ -37,80 +31,96 @@ export class ClientDownload {
|
|
|
37
31
|
/** Pl storages available locally */
|
|
38
32
|
localProjections: LocalStorageProjection[]
|
|
39
33
|
) {
|
|
40
|
-
for (const lp of localProjections) if (lp.localPath !== '') validateAbsolute(lp.localPath);
|
|
41
34
|
this.grpcClient = new DownloadClient(this.grpcTransport);
|
|
42
|
-
this.
|
|
43
|
-
this.localStorageIdsToRoot =
|
|
44
|
-
localProjections.map((lp) => [lp.storageId, lp.localPath])
|
|
45
|
-
);
|
|
35
|
+
this.remoteFileDownloader = new RemoteFileDownloader(httpClient);
|
|
36
|
+
this.localStorageIdsToRoot = newLocalStorageIdsToRoot(localProjections);
|
|
46
37
|
}
|
|
47
38
|
|
|
48
39
|
close() {}
|
|
49
40
|
|
|
50
|
-
async getUrl(
|
|
51
|
-
{ id, type }: ResourceInfo,
|
|
52
|
-
options?: RpcOptions,
|
|
53
|
-
signal?: AbortSignal
|
|
54
|
-
): Promise<DownloadAPI_GetDownloadURL_Response> {
|
|
55
|
-
const withAbort = options ?? {};
|
|
56
|
-
withAbort.abort = signal;
|
|
57
|
-
|
|
58
|
-
return await this.grpcClient.getDownloadURL(
|
|
59
|
-
{ resourceId: id },
|
|
60
|
-
addRTypeToMetadata(type, withAbort)
|
|
61
|
-
).response;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
41
|
async downloadBlob(
|
|
65
42
|
info: ResourceInfo,
|
|
66
43
|
options?: RpcOptions,
|
|
67
44
|
signal?: AbortSignal
|
|
68
45
|
): Promise<DownloadResponse> {
|
|
69
|
-
const { downloadUrl, headers } = await this.
|
|
46
|
+
const { downloadUrl, headers } = await this.grpcGetDownloadUrl(info, options, signal);
|
|
70
47
|
|
|
71
|
-
this.logger.info(`download from url ${downloadUrl}`);
|
|
48
|
+
this.logger.info(`download blob from url ${downloadUrl}`);
|
|
72
49
|
|
|
73
|
-
return
|
|
50
|
+
return isLocal(downloadUrl)
|
|
74
51
|
? await this.readLocalFile(downloadUrl)
|
|
75
|
-
: await this.
|
|
76
|
-
downloadUrl,
|
|
77
|
-
headersFromProto(headers),
|
|
78
|
-
signal
|
|
79
|
-
);
|
|
52
|
+
: await this.remoteFileDownloader.download(downloadUrl, toHeadersMap(headers), signal);
|
|
80
53
|
}
|
|
81
54
|
|
|
82
|
-
private isLocal = (url: string) => url.startsWith(storageProtocol);
|
|
83
|
-
|
|
84
55
|
async readLocalFile(url: string): Promise<DownloadResponse> {
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
const size = stat.size;
|
|
56
|
+
const { storageId, relativePath } = parseLocalUrl(url);
|
|
57
|
+
const fullPath = getFullPath(storageId, this.localStorageIdsToRoot, relativePath);
|
|
88
58
|
|
|
89
59
|
return {
|
|
90
60
|
content: Readable.toWeb(fs.createReadStream(fullPath)),
|
|
91
|
-
size
|
|
61
|
+
size: (await fsp.stat(fullPath)).size
|
|
92
62
|
};
|
|
93
63
|
}
|
|
64
|
+
|
|
65
|
+
private async grpcGetDownloadUrl(
|
|
66
|
+
{ id, type }: ResourceInfo,
|
|
67
|
+
options?: RpcOptions,
|
|
68
|
+
signal?: AbortSignal
|
|
69
|
+
): Promise<DownloadAPI_GetDownloadURL_Response> {
|
|
70
|
+
const withAbort = options ?? {};
|
|
71
|
+
withAbort.abort = signal;
|
|
72
|
+
|
|
73
|
+
return await this.grpcClient.getDownloadURL(
|
|
74
|
+
{ resourceId: id },
|
|
75
|
+
addRTypeToMetadata(type, withAbort)
|
|
76
|
+
).response;
|
|
77
|
+
}
|
|
94
78
|
}
|
|
95
79
|
|
|
96
|
-
export function
|
|
80
|
+
export function parseLocalUrl(url: string) {
|
|
97
81
|
const parsed = new URL(url);
|
|
98
82
|
if (parsed.pathname == '')
|
|
99
83
|
throw new WrongLocalFileUrl(`url for local filepath ${url} does not match url scheme`);
|
|
100
84
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
85
|
+
return {
|
|
86
|
+
storageId: parsed.host,
|
|
87
|
+
relativePath: decodeURIComponent(parsed.pathname.slice(1))
|
|
88
|
+
};
|
|
89
|
+
}
|
|
105
90
|
|
|
106
|
-
|
|
107
|
-
|
|
91
|
+
export function getFullPath(
|
|
92
|
+
storageId: string,
|
|
93
|
+
localStorageIdsToRoot: Map<string, string>,
|
|
94
|
+
relativePath: string
|
|
95
|
+
) {
|
|
96
|
+
const root = localStorageIdsToRoot.get(storageId);
|
|
97
|
+
if (root === undefined) throw new UnknownStorageError(`Unknown storage location: ${storageId}`);
|
|
98
|
+
|
|
99
|
+
if (root === '') return relativePath;
|
|
100
|
+
|
|
101
|
+
return path.join(root, relativePath);
|
|
102
|
+
}
|
|
108
103
|
|
|
109
|
-
|
|
104
|
+
const storageProtocol = 'storage://';
|
|
105
|
+
function isLocal(url: string) {
|
|
106
|
+
return url.startsWith(storageProtocol);
|
|
110
107
|
}
|
|
111
108
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
109
|
+
/** Throws when a local URL have invalid scheme. */
|
|
110
|
+
export class WrongLocalFileUrl extends Error {}
|
|
111
|
+
|
|
112
|
+
/** Happens when a storage for a local file can't be found. */
|
|
113
|
+
export class UnknownStorageError extends Error {}
|
|
114
|
+
|
|
115
|
+
export function newLocalStorageIdsToRoot(projections: LocalStorageProjection[]) {
|
|
116
|
+
const idToRoot: Map<string, string> = new Map();
|
|
117
|
+
for (const lp of projections) {
|
|
118
|
+
// Empty string means no prefix, i.e. any file on this machine can be got from the storage.
|
|
119
|
+
if (lp.localPath !== '') {
|
|
120
|
+
validateAbsolute(lp.localPath);
|
|
121
|
+
}
|
|
122
|
+
idToRoot.set(lp.storageId, lp.localPath);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return idToRoot;
|
|
116
126
|
}
|
package/src/clients/helpers.ts
CHANGED
|
@@ -1,54 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { GrpcTransport } from '@protobuf-ts/grpc-transport';
|
|
4
|
-
import { Dispatcher } from 'undici';
|
|
5
|
-
import { ClientDownload } from './download';
|
|
6
|
-
import { ClientLogs } from './logs';
|
|
7
|
-
import { ClientProgress } from './progress';
|
|
8
|
-
import { ClientUpload } from './upload';
|
|
9
|
-
import { ClientLs } from './ls_api';
|
|
10
|
-
import { LocalStorageProjection } from '../drivers/types';
|
|
11
|
-
|
|
12
|
-
export function createDownloadClient(
|
|
13
|
-
logger: MiLogger,
|
|
14
|
-
client: PlClient,
|
|
15
|
-
localProjections: LocalStorageProjection[]
|
|
16
|
-
) {
|
|
17
|
-
return client.getDriver({
|
|
18
|
-
name: 'DownloadBlob',
|
|
19
|
-
init: (_: PlClient, grpcTransport: GrpcTransport, httpDispatcher: Dispatcher) =>
|
|
20
|
-
new ClientDownload(grpcTransport, httpDispatcher, logger, localProjections)
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function createLogsClient(client: PlClient, logger: MiLogger) {
|
|
25
|
-
return client.getDriver({
|
|
26
|
-
name: 'StreamLogs',
|
|
27
|
-
init: (_: PlClient, grpcTransport: GrpcTransport, httpDispatcher: Dispatcher) =>
|
|
28
|
-
new ClientLogs(grpcTransport, httpDispatcher, logger)
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function createUploadProgressClient(client: PlClient, logger: MiLogger) {
|
|
33
|
-
return client.getDriver({
|
|
34
|
-
name: 'UploadProgress',
|
|
35
|
-
init: (_: PlClient, grpcTransport: GrpcTransport, httpDispatcher: Dispatcher) =>
|
|
36
|
-
new ClientProgress(grpcTransport, httpDispatcher, client, logger)
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function createUploadBlobClient(client: PlClient, logger: MiLogger) {
|
|
41
|
-
return client.getDriver({
|
|
42
|
-
name: 'UploadBlob',
|
|
43
|
-
init: (_: PlClient, grpcTransport: GrpcTransport, httpDispatcher: Dispatcher) =>
|
|
44
|
-
new ClientUpload(grpcTransport, httpDispatcher, client, logger)
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function createLsFilesClient(client: PlClient, logger: MiLogger) {
|
|
49
|
-
return client.getDriver({
|
|
50
|
-
name: 'LsFiles',
|
|
51
|
-
init: (_client: PlClient, grpcTransport: GrpcTransport, _httpDispatcher: Dispatcher) =>
|
|
52
|
-
new ClientLs(grpcTransport, logger)
|
|
53
|
-
});
|
|
1
|
+
export function toHeadersMap(headers: { name: string; value: string }[]): Record<string, string> {
|
|
2
|
+
return Object.fromEntries(headers.map(({ name, value }) => [name, value]));
|
|
54
3
|
}
|
package/src/clients/upload.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { PlClient, ResourceId, ResourceType, addRTypeToMetadata } from '@milaboratories/pl-client';
|
|
2
|
+
import { ResourceInfo } from '@milaboratories/pl-tree';
|
|
3
|
+
import { MiLogger } from '@milaboratories/ts-helpers';
|
|
2
4
|
import { GrpcTransport } from '@protobuf-ts/grpc-transport';
|
|
3
5
|
import type { RpcOptions } from '@protobuf-ts/runtime-rpc';
|
|
4
|
-
import
|
|
5
|
-
import { UploadClient } from '../proto/github.com/milaboratory/pl/controllers/shared/grpc/uploadapi/protocol.client';
|
|
6
|
-
import { MiLogger } from '@milaboratories/ts-helpers';
|
|
6
|
+
import * as fs from 'node:fs/promises';
|
|
7
7
|
import { Dispatcher, request } from 'undici';
|
|
8
8
|
import { uploadapi_GetPartURL_Response } from '../proto/github.com/milaboratory/pl/controllers/shared/grpc/uploadapi/protocol';
|
|
9
|
-
import {
|
|
9
|
+
import { UploadClient } from '../proto/github.com/milaboratory/pl/controllers/shared/grpc/uploadapi/protocol.client';
|
|
10
|
+
import { toHeadersMap } from './helpers';
|
|
11
|
+
import { IncomingHttpHeaders } from 'undici/types/header';
|
|
10
12
|
|
|
11
13
|
export class MTimeError extends Error {}
|
|
12
14
|
|
|
@@ -14,6 +16,7 @@ export class UnexpectedEOF extends Error {}
|
|
|
14
16
|
|
|
15
17
|
export class NetworkError extends Error {}
|
|
16
18
|
|
|
19
|
+
/** Happens when the file doesn't exist */
|
|
17
20
|
export class NoFileForUploading extends Error {}
|
|
18
21
|
|
|
19
22
|
/** Low-level client for grpc uploadapi.
|
|
@@ -33,133 +36,164 @@ export class ClientUpload {
|
|
|
33
36
|
|
|
34
37
|
close() {}
|
|
35
38
|
|
|
36
|
-
public async initUpload(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
public async initUpload(
|
|
40
|
+
{ id, type }: ResourceInfo,
|
|
41
|
+
options?: RpcOptions
|
|
42
|
+
): Promise<{
|
|
43
|
+
overall: bigint;
|
|
44
|
+
toUpload: bigint[];
|
|
45
|
+
}> {
|
|
46
|
+
const init = await this.grpcInit(id, type, options);
|
|
47
|
+
return {
|
|
48
|
+
overall: init.partsCount,
|
|
49
|
+
toUpload: this.partsToUpload(init.partsCount, init.uploadedParts)
|
|
50
|
+
};
|
|
39
51
|
}
|
|
40
52
|
|
|
41
53
|
public async partUpload(
|
|
42
54
|
{ id, type }: ResourceInfo,
|
|
43
55
|
path: string,
|
|
44
|
-
partNumber: bigint,
|
|
45
|
-
partsOverall: number,
|
|
46
56
|
expectedMTimeUnix: bigint,
|
|
57
|
+
partNumber: bigint,
|
|
47
58
|
options?: RpcOptions
|
|
48
59
|
) {
|
|
49
|
-
const info = await this.
|
|
50
|
-
{
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
addRTypeToMetadata(type, options)
|
|
56
|
-
).response;
|
|
60
|
+
const info = await this.grpcGetPartUrl(
|
|
61
|
+
{ id, type },
|
|
62
|
+
partNumber,
|
|
63
|
+
0n, // we update progress as a separate call later.
|
|
64
|
+
options
|
|
65
|
+
);
|
|
57
66
|
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
throw new MTimeError(
|
|
61
|
-
'file was modified, expected mtime: ' + expectedMTimeUnix + ', got: ' + mTime + '.'
|
|
62
|
-
);
|
|
63
|
-
}
|
|
67
|
+
const chunk = await readFileChunk(path, info.chunkStart, info.chunkEnd);
|
|
68
|
+
await checkExpectedMTime(path, expectedMTimeUnix);
|
|
64
69
|
|
|
65
|
-
const
|
|
70
|
+
const {
|
|
71
|
+
body: rawBody,
|
|
72
|
+
statusCode,
|
|
73
|
+
headers
|
|
74
|
+
} = await request(info.uploadUrl, {
|
|
75
|
+
dispatcher: this.httpClient,
|
|
76
|
+
body: chunk,
|
|
77
|
+
headers: toHeadersMap(info.headers),
|
|
78
|
+
method: info.method.toUpperCase() as Dispatcher.HttpMethod
|
|
79
|
+
});
|
|
66
80
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
81
|
+
// always read the body for resources to be garbage collected.
|
|
82
|
+
const body = await rawBody.text();
|
|
83
|
+
checkStatusCodeOk(statusCode, body, headers, info);
|
|
84
|
+
|
|
85
|
+
await this.grpcUpdateProgress({ id, type }, info.chunkEnd - info.chunkStart, options);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public async finalize(info: ResourceInfo, options?: RpcOptions) {
|
|
89
|
+
return await this.grpcFinalize(info, options);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Calculates parts that need to be uploaded from the parts that were
|
|
93
|
+
* already uploaded. */
|
|
94
|
+
private partsToUpload(partsCount: bigint, uploadedParts: bigint[]): bigint[] {
|
|
95
|
+
const toUpload: bigint[] = [];
|
|
96
|
+
const uploaded = new Set(uploadedParts);
|
|
73
97
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
`response is not ok, status code: ${resp.statusCode},` +
|
|
77
|
-
` body: ${body}, headers: ${resp.headers}, url: ${info.uploadUrl}`
|
|
78
|
-
);
|
|
98
|
+
for (let i = 1n; i <= partsCount; i++) {
|
|
99
|
+
if (!uploaded.has(i)) toUpload.push(i);
|
|
79
100
|
}
|
|
80
101
|
|
|
102
|
+
return toUpload;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private async grpcInit(id: ResourceId, type: ResourceType, options?: RpcOptions) {
|
|
106
|
+
return await this.grpcClient.init({ resourceId: id }, addRTypeToMetadata(type, options))
|
|
107
|
+
.response;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private async grpcGetPartUrl(
|
|
111
|
+
{ id, type }: ResourceInfo,
|
|
112
|
+
partNumber: bigint,
|
|
113
|
+
uploadedPartSize: bigint,
|
|
114
|
+
options?: RpcOptions
|
|
115
|
+
) {
|
|
116
|
+
return await this.grpcClient.getPartURL(
|
|
117
|
+
{ resourceId: id, partNumber, uploadedPartSize },
|
|
118
|
+
addRTypeToMetadata(type, options)
|
|
119
|
+
).response;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private async grpcUpdateProgress(
|
|
123
|
+
{ id, type }: ResourceInfo,
|
|
124
|
+
bytesProcessed: bigint,
|
|
125
|
+
options?: RpcOptions
|
|
126
|
+
) {
|
|
81
127
|
await this.grpcClient.updateProgress(
|
|
82
128
|
{
|
|
83
129
|
resourceId: id,
|
|
84
|
-
bytesProcessed
|
|
130
|
+
bytesProcessed
|
|
85
131
|
},
|
|
86
132
|
addRTypeToMetadata(type, options)
|
|
87
|
-
);
|
|
133
|
+
).response;
|
|
88
134
|
}
|
|
89
135
|
|
|
90
|
-
|
|
91
|
-
return await this.grpcClient.finalize({ resourceId: id }, addRTypeToMetadata(type, options))
|
|
136
|
+
private async grpcFinalize({ id, type }: ResourceInfo, options?: RpcOptions) {
|
|
137
|
+
return await this.grpcClient.finalize({ resourceId: id }, addRTypeToMetadata(type, options))
|
|
138
|
+
.response;
|
|
92
139
|
}
|
|
140
|
+
}
|
|
93
141
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return {
|
|
110
|
-
chunk: b.subarray(0, bytesRead),
|
|
111
|
-
mTime: BigInt(Math.floor(stat.mtimeMs / 1000))
|
|
112
|
-
};
|
|
113
|
-
} catch (e: any) {
|
|
114
|
-
if (e.code == 'ENOENT')
|
|
115
|
-
throw new NoFileForUploading(`there is no file ${path} for uploading`);
|
|
116
|
-
throw e;
|
|
117
|
-
} finally {
|
|
118
|
-
f?.close();
|
|
119
|
-
}
|
|
142
|
+
async function readFileChunk(path: string, chunkStart: bigint, chunkEnd: bigint): Promise<Buffer> {
|
|
143
|
+
let f: fs.FileHandle | undefined;
|
|
144
|
+
try {
|
|
145
|
+
f = await fs.open(path);
|
|
146
|
+
const len = Number(chunkEnd - chunkStart);
|
|
147
|
+
const pos = Number(chunkStart);
|
|
148
|
+
const b = Buffer.alloc(len);
|
|
149
|
+
const bytesRead = await readBytesFromPosition(f, b, len, pos);
|
|
150
|
+
|
|
151
|
+
return b.subarray(0, bytesRead);
|
|
152
|
+
} catch (e: any) {
|
|
153
|
+
if (e.code == 'ENOENT') throw new NoFileForUploading(`there is no file ${path} for uploading`);
|
|
154
|
+
throw e;
|
|
155
|
+
} finally {
|
|
156
|
+
f?.close();
|
|
120
157
|
}
|
|
158
|
+
}
|
|
121
159
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
bytesReadTotal += bytesRead;
|
|
160
|
+
/** Read len bytes from a given position.
|
|
161
|
+
* Without this, `FileHandle.read` can read less bytes than needed. */
|
|
162
|
+
async function readBytesFromPosition(f: fs.FileHandle, b: Buffer, len: number, position: number) {
|
|
163
|
+
let bytesReadTotal = 0;
|
|
164
|
+
while (bytesReadTotal < len) {
|
|
165
|
+
const { bytesRead } = await f.read(
|
|
166
|
+
b,
|
|
167
|
+
bytesReadTotal,
|
|
168
|
+
len - bytesReadTotal,
|
|
169
|
+
position + bytesReadTotal
|
|
170
|
+
);
|
|
171
|
+
if (bytesRead === 0) {
|
|
172
|
+
throw new UnexpectedEOF('file ended earlier than expected.');
|
|
137
173
|
}
|
|
138
|
-
|
|
139
|
-
return bytesReadTotal;
|
|
174
|
+
bytesReadTotal += bytesRead;
|
|
140
175
|
}
|
|
141
176
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
private partsToUpload(info: { partsCount: bigint; uploadedParts: bigint[] }): bigint[] {
|
|
145
|
-
const toUpload: bigint[] = [];
|
|
146
|
-
const uploaded = new Set(info.uploadedParts);
|
|
147
|
-
|
|
148
|
-
for (let i = 1n; i <= info.partsCount; i++) {
|
|
149
|
-
if (!uploaded.has(i)) toUpload.push(i);
|
|
150
|
-
}
|
|
177
|
+
return bytesReadTotal;
|
|
178
|
+
}
|
|
151
179
|
|
|
152
|
-
|
|
180
|
+
async function checkExpectedMTime(path: string, expectedMTimeUnix: bigint) {
|
|
181
|
+
const mTime = BigInt(Math.floor((await fs.stat(path)).mtimeMs / 1000));
|
|
182
|
+
if (mTime > expectedMTimeUnix) {
|
|
183
|
+
throw new MTimeError(`file was modified, expected mtime: ${expectedMTimeUnix}, got: ${mTime}.`);
|
|
153
184
|
}
|
|
185
|
+
}
|
|
154
186
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
187
|
+
function checkStatusCodeOk(
|
|
188
|
+
statusCode: number,
|
|
189
|
+
body: string,
|
|
190
|
+
headers: IncomingHttpHeaders,
|
|
191
|
+
info: uploadapi_GetPartURL_Response
|
|
192
|
+
) {
|
|
193
|
+
if (statusCode != 200) {
|
|
194
|
+
throw new NetworkError(
|
|
195
|
+
`response is not ok, status code: ${statusCode},` +
|
|
196
|
+
` body: ${body}, headers: ${headers}, url: ${info.uploadUrl}`
|
|
197
|
+
);
|
|
164
198
|
}
|
|
165
199
|
}
|