@milaboratories/pl-drivers 1.3.25 → 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.
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 +5 -5
  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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@milaboratories/pl-drivers",
3
- "version": "1.3.25",
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,10 +26,10 @@
26
26
  "tar-fs": "^3.0.6",
27
27
  "undici": "^6.21.0",
28
28
  "zod": "^3.23.8",
29
- "@milaboratories/ts-helpers": "^1.1.1",
30
- "@milaboratories/computable": "^2.3.2",
31
- "@milaboratories/pl-tree": "^1.4.16",
32
- "@milaboratories/pl-client": "^2.6.1",
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
33
  "@milaboratories/pl-model-common": "^1.8.0"
34
34
  },
35
35
  "devDependencies": {
@@ -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, parseLocalFileUrl } from '../clients/download';
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 = parseLocalFileUrl(url, new Map([['main', 'C:\\Users\\test']])).replace(
19
- path.sep,
20
- '\\'
21
- ); // for testing on *nix systems
18
+ const got = parseLocalUrl(url);
22
19
 
23
- expect(got).toEqual(expectedFullPath);
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 () => {
@@ -1,33 +1,27 @@
1
- import { Readable } from 'node:stream';
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 { DownloadClient } from '../proto/github.com/milaboratory/pl/controllers/shared/grpc/downloadapi/protocol.client';
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
- const storageProtocol = 'storage://';
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 downloadHelper: DownloadHelper;
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.downloadHelper = new DownloadHelper(httpClient);
43
- this.localStorageIdsToRoot = new Map(
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.getUrl(info, options, signal);
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 this.isLocal(downloadUrl)
50
+ return isLocal(downloadUrl)
74
51
  ? await this.readLocalFile(downloadUrl)
75
- : await this.downloadHelper.downloadRemoteFile(
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 fullPath = parseLocalFileUrl(url, this.localStorageIdsToRoot);
86
- const stat = await fsp.stat(fullPath);
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 parseLocalFileUrl(url: string, localStorageIdsToRoot: Map<string, string>): string {
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
- const storageId = parsed.host;
102
- const storageRoot = localStorageIdsToRoot.get(storageId);
103
- if (storageRoot === undefined)
104
- throw new UnknownStorageError(`Unknown storage location: ${storageId}`);
85
+ return {
86
+ storageId: parsed.host,
87
+ relativePath: decodeURIComponent(parsed.pathname.slice(1))
88
+ };
89
+ }
105
90
 
106
- const localPath = decodeURIComponent(parsed.pathname.slice(1));
107
- const fullPath = storageRoot === '' ? localPath : path.join(storageRoot, localPath);
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
- return fullPath;
104
+ const storageProtocol = 'storage://';
105
+ function isLocal(url: string) {
106
+ return url.startsWith(storageProtocol);
110
107
  }
111
108
 
112
- export function headersFromProto(
113
- headers: DownloadAPI_GetDownloadURL_HTTPHeader[]
114
- ): Record<string, string> {
115
- return Object.fromEntries(headers.map(({ name, value }) => [name, value]));
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
  }
@@ -1,54 +1,3 @@
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
- });
1
+ export function toHeadersMap(headers: { name: string; value: string }[]): Record<string, string> {
2
+ return Object.fromEntries(headers.map(({ name, value }) => [name, value]));
54
3
  }
@@ -1,12 +1,14 @@
1
- import * as fs from 'node:fs/promises';
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 { PlClient, addRTypeToMetadata } from '@milaboratories/pl-client';
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 { ResourceInfo } from '@milaboratories/pl-tree';
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({ id, type }: ResourceInfo, options?: RpcOptions): Promise<bigint[]> {
37
- const init = await this.grpcClient.init({ resourceId: id }, addRTypeToMetadata(type, options));
38
- return this.partsToUpload(init.response);
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.grpcClient.getPartURL(
50
- {
51
- resourceId: id,
52
- partNumber: partNumber,
53
- uploadedPartSize: 0n // we update progress as a separate call later.
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 { chunk, mTime } = await this.readChunk(path, info.chunkStart, info.chunkEnd);
59
- if (mTime > expectedMTimeUnix) {
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 resp = await request(info.uploadUrl, this.prepareUploadOpts(info, chunk));
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
- const body = await resp.body.text();
68
- this.logger.info(
69
- `uploaded chunk ${partNumber} from ${partsOverall} of resource: ${id},` +
70
- ` response: '${body.toString()}', ` +
71
- `status code: ${resp.statusCode}`
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
- if (resp.statusCode != 200) {
75
- throw new NetworkError(
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: info.chunkEnd - info.chunkStart
130
+ bytesProcessed
85
131
  },
86
132
  addRTypeToMetadata(type, options)
87
- );
133
+ ).response;
88
134
  }
89
135
 
90
- public async finalizeUpload({ id, type }: ResourceInfo, options?: RpcOptions) {
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
- private async readChunk(
95
- path: string,
96
- chunkStart: bigint,
97
- chunkEnd: bigint
98
- ): Promise<{ chunk: Buffer; mTime: bigint }> {
99
- let f: fs.FileHandle | undefined;
100
- try {
101
- f = await fs.open(path);
102
- const len = Number(chunkEnd - chunkStart);
103
- const pos = Number(chunkStart);
104
- const b = Buffer.alloc(len);
105
- const bytesRead = await this.readBytesFromPosition(f, b, len, pos);
106
-
107
- const stat = await fs.stat(path);
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
- /** Read len bytes from a given position. The reason the method exists
123
- is that FileHandle.read can read less bytes than it's needed. */
124
- async readBytesFromPosition(f: fs.FileHandle, b: Buffer, len: number, position: number) {
125
- let bytesReadTotal = 0;
126
- while (bytesReadTotal < len) {
127
- const { bytesRead } = await f.read(
128
- b,
129
- bytesReadTotal,
130
- len - bytesReadTotal,
131
- position + bytesReadTotal
132
- );
133
- if (bytesRead === 0) {
134
- throw new UnexpectedEOF('file ended earlier than expected.');
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
- /** Calculates parts that need to be uploaded from the parts that were
143
- * already uploaded. */
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
- return toUpload;
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
- private prepareUploadOpts(info: uploadapi_GetPartURL_Response, chunk: Buffer): any {
156
- const headers = info.headers.map(({ name, value }) => [name, value]);
157
-
158
- return {
159
- dispatcher: this.httpClient,
160
- body: chunk,
161
- headers: Object.fromEntries(headers),
162
- method: info.method.toUpperCase() as Dispatcher.HttpMethod
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
  }