@naturalcycles/cloud-storage-lib 1.9.2 → 1.11.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.
@@ -1,12 +1,10 @@
1
1
  /// <reference types="node" />
2
- /// <reference types="node" />
3
- import { Readable, Writable } from 'node:stream';
4
- import { Storage, StorageOptions } from '@google-cloud/storage';
5
- import { LocalTimeInput } from '@naturalcycles/js-lib';
6
- import { ReadableTyped } from '@naturalcycles/nodejs-lib';
7
- import { CommonStorage, CommonStorageGetOptions, FileEntry } from './commonStorage';
8
- import { GCPServiceAccount } from './model';
9
- export { Storage, type StorageOptions, };
2
+ import type { Storage, StorageOptions } from '@google-cloud/storage';
3
+ import { CommonLogger, LocalTimeInput } from '@naturalcycles/js-lib';
4
+ import type { ReadableBinary, ReadableTyped, WritableBinary } from '@naturalcycles/nodejs-lib';
5
+ import type { CommonStorage, CommonStorageGetOptions, FileEntry } from './commonStorage';
6
+ import type { GCPServiceAccount } from './model';
7
+ export { type Storage, type StorageOptions, };
10
8
  /**
11
9
  * This object is intentionally made to NOT extend StorageOptions,
12
10
  * because StorageOptions is complicated and provides just too many ways
@@ -18,9 +16,13 @@ export { Storage, type StorageOptions, };
18
16
  */
19
17
  export interface CloudStorageCfg {
20
18
  /**
21
- * It's optional, to allow automatic credentials in AppEngine, or GOOGLE_APPLICATION_CREDENTIALS.
19
+ * Default is console
20
+ */
21
+ logger?: CommonLogger;
22
+ /**
23
+ * Pass true for extra debugging
22
24
  */
23
- credentials?: GCPServiceAccount;
25
+ debug?: boolean;
24
26
  }
25
27
  /**
26
28
  * CloudStorage implementation of CommonStorage API.
@@ -29,13 +31,17 @@ export interface CloudStorageCfg {
29
31
  */
30
32
  export declare class CloudStorage implements CommonStorage {
31
33
  storage: Storage;
34
+ private constructor();
35
+ cfg: CloudStorageCfg & {
36
+ logger: CommonLogger;
37
+ };
38
+ static createFromGCPServiceAccount(credentials?: GCPServiceAccount, cfg?: CloudStorageCfg): CloudStorage;
39
+ static createFromStorageOptions(storageOptions?: StorageOptions, cfg?: CloudStorageCfg): CloudStorage;
32
40
  /**
33
41
  * Passing the pre-created Storage allows to instantiate it from both
34
42
  * GCP Storage and FirebaseStorage.
35
43
  */
36
- constructor(storage: Storage);
37
- static createFromGCPServiceAccount(cfg: CloudStorageCfg): CloudStorage;
38
- static createFromStorageOptions(storageOptions?: StorageOptions): CloudStorage;
44
+ static createFromStorage(storage: Storage, cfg?: CloudStorageCfg): CloudStorage;
39
45
  ping(bucketName?: string): Promise<void>;
40
46
  deletePath(bucketName: string, prefix: string): Promise<void>;
41
47
  deletePaths(bucketName: string, prefixes: string[]): Promise<void>;
@@ -48,16 +54,18 @@ export declare class CloudStorage implements CommonStorage {
48
54
  * Returns a Readable that is NOT object mode,
49
55
  * so you can e.g pipe it to fs.createWriteStream()
50
56
  */
51
- getFileReadStream(bucketName: string, filePath: string): Readable;
57
+ getFileReadStream(bucketName: string, filePath: string): ReadableBinary;
52
58
  saveFile(bucketName: string, filePath: string, content: Buffer): Promise<void>;
53
- getFileWriteStream(bucketName: string, filePath: string): Writable;
59
+ getFileWriteStream(bucketName: string, filePath: string): WritableBinary;
54
60
  uploadFile(localFilePath: string, bucketName: string, bucketFilePath: string): Promise<void>;
55
61
  setFileVisibility(bucketName: string, filePath: string, isPublic: boolean): Promise<void>;
56
62
  getFileVisibility(bucketName: string, filePath: string): Promise<boolean>;
57
63
  copyFile(fromBucket: string, fromPath: string, toPath: string, toBucket?: string): Promise<void>;
58
64
  moveFile(fromBucket: string, fromPath: string, toPath: string, toBucket?: string): Promise<void>;
59
65
  movePath(fromBucket: string, fromPrefix: string, toPrefix: string, toBucket?: string): Promise<void>;
60
- combine(bucketName: string, filePaths: string[], toPath: string, toBucket?: string): Promise<void>;
66
+ deleteFiles(bucketName: string, filePaths: string[]): Promise<void>;
67
+ combineFiles(bucketName: string, filePaths: string[], toPath: string, toBucket?: string, currentRecursionDepth?: number): Promise<void>;
68
+ combine(bucketName: string, prefix: string, toPath: string, toBucket?: string): Promise<void>;
61
69
  /**
62
70
  * Acquires a "signed url", which allows bearer to use it to download ('read') the file.
63
71
  *
@@ -1,37 +1,46 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CloudStorage = exports.Storage = void 0;
4
- const storage_1 = require("@google-cloud/storage");
5
- Object.defineProperty(exports, "Storage", { enumerable: true, get: function () { return storage_1.Storage; } });
3
+ exports.CloudStorage = void 0;
6
4
  const js_lib_1 = require("@naturalcycles/js-lib");
5
+ const MAX_RECURSION_DEPTH = 10;
6
+ const BATCH_SIZE = 32;
7
7
  /**
8
8
  * CloudStorage implementation of CommonStorage API.
9
9
  *
10
10
  * API: https://googleapis.dev/nodejs/storage/latest/index.html
11
11
  */
12
12
  class CloudStorage {
13
- /**
14
- * Passing the pre-created Storage allows to instantiate it from both
15
- * GCP Storage and FirebaseStorage.
16
- */
17
- constructor(storage) {
13
+ constructor(storage, cfg = {}) {
18
14
  this.storage = storage;
19
- }
20
- static createFromGCPServiceAccount(cfg) {
21
- const storage = new storage_1.Storage({
22
- credentials: cfg.credentials,
15
+ this.cfg = {
16
+ logger: console,
17
+ ...cfg,
18
+ };
19
+ }
20
+ static createFromGCPServiceAccount(credentials, cfg) {
21
+ const storageLib = require('@google-cloud/storage');
22
+ const storage = new storageLib.Storage({
23
+ credentials,
23
24
  // Explicitly passing it here to fix this error:
24
25
  // Error: Unable to detect a Project Id in the current environment.
25
26
  // To learn more about authentication and Google APIs, visit:
26
27
  // https://cloud.google.com/docs/authentication/getting-started
27
28
  // at /root/repo/node_modules/google-auth-library/build/src/auth/googleauth.js:95:31
28
- projectId: cfg.credentials?.project_id,
29
+ projectId: credentials?.project_id,
29
30
  });
30
- return new CloudStorage(storage);
31
+ return new CloudStorage(storage, cfg);
31
32
  }
32
- static createFromStorageOptions(storageOptions) {
33
- const storage = new storage_1.Storage(storageOptions);
34
- return new CloudStorage(storage);
33
+ static createFromStorageOptions(storageOptions, cfg) {
34
+ const storageLib = require('@google-cloud/storage');
35
+ const storage = new storageLib.Storage(storageOptions);
36
+ return new CloudStorage(storage, cfg);
37
+ }
38
+ /**
39
+ * Passing the pre-created Storage allows to instantiate it from both
40
+ * GCP Storage and FirebaseStorage.
41
+ */
42
+ static createFromStorage(storage, cfg) {
43
+ return new CloudStorage(storage, cfg);
35
44
  }
36
45
  async ping(bucketName) {
37
46
  await this.storage.bucket(bucketName || 'non-existing-for-sure').exists();
@@ -155,14 +164,48 @@ class CloudStorage {
155
164
  await file.move(this.storage.bucket(toBucket || fromBucket).file(newName));
156
165
  });
157
166
  }
158
- async combine(bucketName, filePaths, toPath, toBucket) {
159
- // todo: if (filePaths.length > 32) - use recursive algorithm
160
- (0, js_lib_1._assert)(filePaths.length <= 32, 'combine supports up to 32 input files');
161
- await this.storage
162
- .bucket(bucketName)
163
- .combine(filePaths, this.storage.bucket(toBucket || bucketName).file(toPath));
164
- // Delete original files
165
- await this.deletePaths(bucketName, filePaths);
167
+ async deleteFiles(bucketName, filePaths) {
168
+ await (0, js_lib_1.pMap)(filePaths, async (filePath) => {
169
+ await this.storage.bucket(bucketName).file(filePath).delete();
170
+ });
171
+ }
172
+ async combineFiles(bucketName, filePaths, toPath, toBucket, currentRecursionDepth = 0) {
173
+ (0, js_lib_1._assert)(currentRecursionDepth <= MAX_RECURSION_DEPTH, `combineFiles reached max recursion depth of ${MAX_RECURSION_DEPTH}`);
174
+ const { logger, debug } = this.cfg;
175
+ if (debug) {
176
+ logger.log(`[${currentRecursionDepth}] Will compose ${filePaths.length} files, by batches of ${BATCH_SIZE}`);
177
+ }
178
+ const intermediateFiles = [];
179
+ if (filePaths.length <= BATCH_SIZE) {
180
+ await this.storage
181
+ .bucket(bucketName)
182
+ .combine(filePaths, this.storage.bucket(toBucket || bucketName).file(toPath));
183
+ if (debug) {
184
+ logger.log(`[${currentRecursionDepth}] Composed into ${toPath}!`);
185
+ }
186
+ await this.deleteFiles(bucketName, filePaths);
187
+ return;
188
+ }
189
+ const started = Date.now();
190
+ await (0, js_lib_1.pMap)((0, js_lib_1._chunk)(filePaths, BATCH_SIZE), async (fileBatch, i) => {
191
+ if (debug) {
192
+ logger.log(`[${currentRecursionDepth}] Composing batch ${i + 1}...`);
193
+ }
194
+ const intermediateFile = `temp_${currentRecursionDepth}_${i}`;
195
+ await this.storage
196
+ .bucket(bucketName)
197
+ .combine(fileBatch, this.storage.bucket(toBucket || bucketName).file(intermediateFile));
198
+ intermediateFiles.push(intermediateFile);
199
+ await this.deleteFiles(bucketName, fileBatch);
200
+ });
201
+ if (debug) {
202
+ logger.log(`[${currentRecursionDepth}] Batch composed into ${intermediateFiles.length} files, in ${(0, js_lib_1._since)(started)}`);
203
+ }
204
+ await this.combineFiles(toBucket || bucketName, intermediateFiles, toPath, toBucket, currentRecursionDepth + 1);
205
+ }
206
+ async combine(bucketName, prefix, toPath, toBucket) {
207
+ const filePaths = await this.getFileNames(bucketName, { prefix });
208
+ await this.combineFiles(bucketName, filePaths, toPath, toBucket);
166
209
  }
167
210
  /**
168
211
  * Acquires a "signed url", which allows bearer to use it to download ('read') the file.
@@ -1,8 +1,6 @@
1
1
  /// <reference types="node" />
2
- /// <reference types="node" />
3
- import { Readable, Writable } from 'node:stream';
4
- import { LocalTimeInput } from '@naturalcycles/js-lib';
5
- import { ReadableTyped } from '@naturalcycles/nodejs-lib';
2
+ import type { LocalTimeInput } from '@naturalcycles/js-lib';
3
+ import type { ReadableBinary, ReadableTyped, WritableBinary } from '@naturalcycles/nodejs-lib';
6
4
  export interface FileEntry {
7
5
  filePath: string;
8
6
  content: Buffer;
@@ -56,6 +54,10 @@ export interface CommonStorage {
56
54
  */
57
55
  deletePath: (bucketName: string, prefix: string) => Promise<void>;
58
56
  deletePaths: (bucketName: string, prefixes: string[]) => Promise<void>;
57
+ /**
58
+ * Should delete all files by their paths.
59
+ */
60
+ deleteFiles: (bucketName: string, filePaths: string[]) => Promise<void>;
59
61
  /**
60
62
  * Returns an array of strings which are file paths.
61
63
  * Files that are not found by the path are not present in the map.
@@ -70,8 +72,8 @@ export interface CommonStorage {
70
72
  getFileNames: (bucketName: string, opt?: CommonStorageGetOptions) => Promise<string[]>;
71
73
  getFileNamesStream: (bucketName: string, opt?: CommonStorageGetOptions) => ReadableTyped<string>;
72
74
  getFilesStream: (bucketName: string, opt?: CommonStorageGetOptions) => ReadableTyped<FileEntry>;
73
- getFileReadStream: (bucketName: string, filePath: string) => Readable;
74
- getFileWriteStream: (bucketName: string, filePath: string) => Writable;
75
+ getFileReadStream: (bucketName: string, filePath: string) => ReadableBinary;
76
+ getFileWriteStream: (bucketName: string, filePath: string) => WritableBinary;
75
77
  /**
76
78
  * Upload local file to the bucket (by streaming it).
77
79
  */
@@ -95,7 +97,11 @@ export interface CommonStorage {
95
97
  *
96
98
  * @experimental
97
99
  */
98
- combine: (bucketName: string, filePaths: string[], toPath: string, toBucket?: string) => Promise<void>;
100
+ combineFiles: (bucketName: string, filePaths: string[], toPath: string, toBucket?: string) => Promise<void>;
101
+ /**
102
+ * Like `combineFiles`, but for a `prefix`.
103
+ */
104
+ combine: (bucketName: string, prefix: string, toPath: string, toBucket?: string) => Promise<void>;
99
105
  /**
100
106
  * Acquire a "signed url", which allows bearer to use it to download ('read') the file.
101
107
  *
@@ -1,8 +1,6 @@
1
1
  /// <reference types="node" />
2
- /// <reference types="node" />
3
- import { Readable, Writable } from 'node:stream';
4
2
  import { LocalTimeInput, StringMap } from '@naturalcycles/js-lib';
5
- import { ReadableTyped } from '@naturalcycles/nodejs-lib';
3
+ import { ReadableBinary, ReadableTyped, WritableBinary } from '@naturalcycles/nodejs-lib';
6
4
  import { CommonStorage, CommonStorageGetOptions, FileEntry } from './commonStorage';
7
5
  export declare class InMemoryCommonStorage implements CommonStorage {
8
6
  /**
@@ -18,17 +16,19 @@ export declare class InMemoryCommonStorage implements CommonStorage {
18
16
  saveFile(bucketName: string, filePath: string, content: Buffer): Promise<void>;
19
17
  deletePath(bucketName: string, prefix: string): Promise<void>;
20
18
  deletePaths(bucketName: string, prefixes: string[]): Promise<void>;
19
+ deleteFiles(bucketName: string, filePaths: string[]): Promise<void>;
21
20
  getFileNames(bucketName: string, opt?: CommonStorageGetOptions): Promise<string[]>;
22
21
  getFileNamesStream(bucketName: string, opt?: CommonStorageGetOptions): ReadableTyped<string>;
23
22
  getFilesStream(bucketName: string, opt?: CommonStorageGetOptions): ReadableTyped<FileEntry>;
24
- getFileReadStream(bucketName: string, filePath: string): Readable;
25
- getFileWriteStream(_bucketName: string, _filePath: string): Writable;
23
+ getFileReadStream(bucketName: string, filePath: string): ReadableBinary;
24
+ getFileWriteStream(_bucketName: string, _filePath: string): WritableBinary;
26
25
  uploadFile(localFilePath: string, bucketName: string, bucketFilePath: string): Promise<void>;
27
26
  setFileVisibility(bucketName: string, filePath: string, isPublic: boolean): Promise<void>;
28
27
  getFileVisibility(bucketName: string, filePath: string): Promise<boolean>;
29
28
  copyFile(fromBucket: string, fromPath: string, toPath: string, toBucket?: string): Promise<void>;
30
29
  moveFile(fromBucket: string, fromPath: string, toPath: string, toBucket?: string): Promise<void>;
31
30
  movePath(fromBucket: string, fromPrefix: string, toPrefix: string, toBucket?: string): Promise<void>;
32
- combine(bucketName: string, filePaths: string[], toPath: string, toBucket?: string): Promise<void>;
31
+ combine(bucketName: string, prefix: string, toPath: string, toBucket?: string): Promise<void>;
32
+ combineFiles(bucketName: string, filePaths: string[], toPath: string, toBucket?: string): Promise<void>;
33
33
  getSignedUrl(bucketName: string, filePath: string, expires: LocalTimeInput): Promise<string>;
34
34
  }
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.InMemoryCommonStorage = void 0;
4
- const node_stream_1 = require("node:stream");
5
4
  const js_lib_1 = require("@naturalcycles/js-lib");
6
5
  const nodejs_lib_1 = require("@naturalcycles/nodejs-lib");
7
6
  class InMemoryCommonStorage {
@@ -17,7 +16,7 @@ class InMemoryCommonStorage {
17
16
  return Object.keys(this.data);
18
17
  }
19
18
  getBucketNamesStream() {
20
- return node_stream_1.Readable.from(Object.keys(this.data));
19
+ return (0, nodejs_lib_1.readableFrom)(Object.keys(this.data));
21
20
  }
22
21
  async fileExists(bucketName, filePath) {
23
22
  return !!this.data[bucketName]?.[filePath];
@@ -39,6 +38,11 @@ class InMemoryCommonStorage {
39
38
  }
40
39
  });
41
40
  }
41
+ async deleteFiles(bucketName, filePaths) {
42
+ if (!this.data[bucketName])
43
+ return;
44
+ filePaths.forEach(filePath => delete this.data[bucketName][filePath]);
45
+ }
42
46
  async getFileNames(bucketName, opt = {}) {
43
47
  const { prefix = '', fullPaths = true } = opt;
44
48
  return Object.keys(this.data[bucketName] || {})
@@ -47,14 +51,14 @@ class InMemoryCommonStorage {
47
51
  }
48
52
  getFileNamesStream(bucketName, opt = {}) {
49
53
  const { prefix = '', fullPaths = true } = opt;
50
- return node_stream_1.Readable.from(Object.keys(this.data[bucketName] || {})
54
+ return (0, nodejs_lib_1.readableFrom)(Object.keys(this.data[bucketName] || {})
51
55
  .filter(filePath => filePath.startsWith(prefix))
52
56
  .slice(0, opt.limit)
53
57
  .map(n => (fullPaths ? n : (0, js_lib_1._substringAfterLast)(n, '/'))));
54
58
  }
55
59
  getFilesStream(bucketName, opt = {}) {
56
60
  const { prefix = '', fullPaths = true } = opt;
57
- return node_stream_1.Readable.from(Object.entries(this.data[bucketName] || {})
61
+ return (0, nodejs_lib_1.readableFrom)((0, js_lib_1._stringMapEntries)(this.data[bucketName] || {})
58
62
  .map(([filePath, content]) => ({
59
63
  filePath,
60
64
  content,
@@ -64,7 +68,7 @@ class InMemoryCommonStorage {
64
68
  .map(f => (fullPaths ? f : { ...f, filePath: (0, js_lib_1._substringAfterLast)(f.filePath, '/') })));
65
69
  }
66
70
  getFileReadStream(bucketName, filePath) {
67
- return node_stream_1.Readable.from(this.data[bucketName][filePath]);
71
+ return (0, nodejs_lib_1.readableFrom)(this.data[bucketName][filePath]);
68
72
  }
69
73
  getFileWriteStream(_bucketName, _filePath) {
70
74
  throw new Error('Method not implemented.');
@@ -104,7 +108,11 @@ class InMemoryCommonStorage {
104
108
  delete this.data[fromBucket][filePath];
105
109
  });
106
110
  }
107
- async combine(bucketName, filePaths, toPath, toBucket) {
111
+ async combine(bucketName, prefix, toPath, toBucket) {
112
+ const filePaths = await this.getFileNames(bucketName, { prefix });
113
+ await this.combineFiles(bucketName, filePaths, toPath, toBucket);
114
+ }
115
+ async combineFiles(bucketName, filePaths, toPath, toBucket) {
108
116
  if (!this.data[bucketName])
109
117
  return;
110
118
  const tob = toBucket || bucketName;
package/package.json CHANGED
@@ -35,7 +35,7 @@
35
35
  "engines": {
36
36
  "node": ">=18.12.0"
37
37
  },
38
- "version": "1.9.2",
38
+ "version": "1.11.0",
39
39
  "description": "CommonStorage implementation based on Google Cloud Storage",
40
40
  "author": "Natural Cycles Team",
41
41
  "license": "MIT"
@@ -1,23 +1,31 @@
1
- import { Readable, Writable } from 'node:stream'
2
- import { File, Storage, StorageOptions } from '@google-cloud/storage'
1
+ // eslint-disable-next-line import/no-duplicates
2
+ import type { File, Storage, StorageOptions } from '@google-cloud/storage'
3
+ // eslint-disable-next-line import/no-duplicates
4
+ import type * as StorageLib from '@google-cloud/storage'
3
5
  import {
4
6
  _assert,
7
+ _chunk,
8
+ _since,
5
9
  _substringAfterLast,
10
+ CommonLogger,
6
11
  localTime,
7
12
  LocalTimeInput,
8
13
  pMap,
9
14
  SKIP,
10
15
  } from '@naturalcycles/js-lib'
11
- import { ReadableTyped } from '@naturalcycles/nodejs-lib'
12
- import { CommonStorage, CommonStorageGetOptions, FileEntry } from './commonStorage'
13
- import { GCPServiceAccount } from './model'
16
+ import type { ReadableBinary, ReadableTyped, WritableBinary } from '@naturalcycles/nodejs-lib'
17
+ import type { CommonStorage, CommonStorageGetOptions, FileEntry } from './commonStorage'
18
+ import type { GCPServiceAccount } from './model'
14
19
 
15
20
  export {
16
21
  // This is the latest version, to be imported by consumers
17
- Storage,
22
+ type Storage,
18
23
  type StorageOptions,
19
24
  }
20
25
 
26
+ const MAX_RECURSION_DEPTH = 10
27
+ const BATCH_SIZE = 32
28
+
21
29
  /**
22
30
  * This object is intentionally made to NOT extend StorageOptions,
23
31
  * because StorageOptions is complicated and provides just too many ways
@@ -29,9 +37,14 @@ export {
29
37
  */
30
38
  export interface CloudStorageCfg {
31
39
  /**
32
- * It's optional, to allow automatic credentials in AppEngine, or GOOGLE_APPLICATION_CREDENTIALS.
40
+ * Default is console
41
+ */
42
+ logger?: CommonLogger
43
+
44
+ /**
45
+ * Pass true for extra debugging
33
46
  */
34
- credentials?: GCPServiceAccount
47
+ debug?: boolean
35
48
  }
36
49
 
37
50
  /**
@@ -40,29 +53,54 @@ export interface CloudStorageCfg {
40
53
  * API: https://googleapis.dev/nodejs/storage/latest/index.html
41
54
  */
42
55
  export class CloudStorage implements CommonStorage {
43
- /**
44
- * Passing the pre-created Storage allows to instantiate it from both
45
- * GCP Storage and FirebaseStorage.
46
- */
47
- constructor(public storage: Storage) {}
56
+ private constructor(
57
+ public storage: Storage,
58
+ cfg: CloudStorageCfg = {},
59
+ ) {
60
+ this.cfg = {
61
+ logger: console,
62
+ ...cfg,
63
+ }
64
+ }
65
+
66
+ cfg: CloudStorageCfg & {
67
+ logger: CommonLogger
68
+ }
69
+
70
+ static createFromGCPServiceAccount(
71
+ credentials?: GCPServiceAccount,
72
+ cfg?: CloudStorageCfg,
73
+ ): CloudStorage {
74
+ const storageLib = require('@google-cloud/storage') as typeof StorageLib
48
75
 
49
- static createFromGCPServiceAccount(cfg: CloudStorageCfg): CloudStorage {
50
- const storage = new Storage({
51
- credentials: cfg.credentials,
76
+ const storage = new storageLib.Storage({
77
+ credentials,
52
78
  // Explicitly passing it here to fix this error:
53
79
  // Error: Unable to detect a Project Id in the current environment.
54
80
  // To learn more about authentication and Google APIs, visit:
55
81
  // https://cloud.google.com/docs/authentication/getting-started
56
82
  // at /root/repo/node_modules/google-auth-library/build/src/auth/googleauth.js:95:31
57
- projectId: cfg.credentials?.project_id,
83
+ projectId: credentials?.project_id,
58
84
  })
59
85
 
60
- return new CloudStorage(storage)
86
+ return new CloudStorage(storage, cfg)
61
87
  }
62
88
 
63
- static createFromStorageOptions(storageOptions?: StorageOptions): CloudStorage {
64
- const storage = new Storage(storageOptions)
65
- return new CloudStorage(storage)
89
+ static createFromStorageOptions(
90
+ storageOptions?: StorageOptions,
91
+ cfg?: CloudStorageCfg,
92
+ ): CloudStorage {
93
+ const storageLib = require('@google-cloud/storage') as typeof StorageLib
94
+ const storage = new storageLib.Storage(storageOptions)
95
+ return new CloudStorage(storage, cfg)
96
+ }
97
+
98
+ /**
99
+ * Passing the pre-created Storage allows to instantiate it from both
100
+ * GCP Storage and FirebaseStorage.
101
+ */
102
+ static createFromStorage(storage: Storage, cfg?: CloudStorageCfg): CloudStorage {
103
+ return new CloudStorage(storage, cfg)
66
104
  }
67
105
 
68
106
  async ping(bucketName?: string): Promise<void> {
@@ -159,7 +197,7 @@ export class CloudStorage implements CommonStorage {
159
197
  * Returns a Readable that is NOT object mode,
160
198
  * so you can e.g pipe it to fs.createWriteStream()
161
199
  */
162
- getFileReadStream(bucketName: string, filePath: string): Readable {
200
+ getFileReadStream(bucketName: string, filePath: string): ReadableBinary {
163
201
  return this.storage.bucket(bucketName).file(filePath).createReadStream()
164
202
  }
165
203
 
@@ -167,7 +205,7 @@ export class CloudStorage implements CommonStorage {
167
205
  await this.storage.bucket(bucketName).file(filePath).save(content)
168
206
  }
169
207
 
170
- getFileWriteStream(bucketName: string, filePath: string): Writable {
208
+ getFileWriteStream(bucketName: string, filePath: string): WritableBinary {
171
209
  return this.storage.bucket(bucketName).file(filePath).createWriteStream()
172
210
  }
173
211
 
@@ -235,21 +273,81 @@ export class CloudStorage implements CommonStorage {
235
273
  })
236
274
  }
237
275
 
238
- async combine(
276
+ async deleteFiles(bucketName: string, filePaths: string[]): Promise<void> {
277
+ await pMap(filePaths, async filePath => {
278
+ await this.storage.bucket(bucketName).file(filePath).delete()
279
+ })
280
+ }
281
+
282
+ async combineFiles(
239
283
  bucketName: string,
240
284
  filePaths: string[],
241
285
  toPath: string,
242
286
  toBucket?: string,
287
+ currentRecursionDepth = 0, // not to be set publicly, only used internally
243
288
  ): Promise<void> {
244
- // todo: if (filePaths.length > 32) - use recursive algorithm
245
- _assert(filePaths.length <= 32, 'combine supports up to 32 input files')
289
+ _assert(
290
+ currentRecursionDepth <= MAX_RECURSION_DEPTH,
291
+ `combineFiles reached max recursion depth of ${MAX_RECURSION_DEPTH}`,
292
+ )
293
+ const { logger, debug } = this.cfg
246
294
 
247
- await this.storage
248
- .bucket(bucketName)
249
- .combine(filePaths, this.storage.bucket(toBucket || bucketName).file(toPath))
295
+ if (debug) {
296
+ logger.log(
297
+ `[${currentRecursionDepth}] Will compose ${filePaths.length} files, by batches of ${BATCH_SIZE}`,
298
+ )
299
+ }
300
+
301
+ const intermediateFiles: string[] = []
250
302
 
251
- // Delete original files
252
- await this.deletePaths(bucketName, filePaths)
303
+ if (filePaths.length <= BATCH_SIZE) {
304
+ await this.storage
305
+ .bucket(bucketName)
306
+ .combine(filePaths, this.storage.bucket(toBucket || bucketName).file(toPath))
307
+
308
+ if (debug) {
309
+ logger.log(`[${currentRecursionDepth}] Composed into ${toPath}!`)
310
+ }
311
+
312
+ await this.deleteFiles(bucketName, filePaths)
313
+ return
314
+ }
315
+
316
+ const started = Date.now()
317
+ await pMap(_chunk(filePaths, BATCH_SIZE), async (fileBatch, i) => {
318
+ if (debug) {
319
+ logger.log(`[${currentRecursionDepth}] Composing batch ${i + 1}...`)
320
+ }
321
+ const intermediateFile = `temp_${currentRecursionDepth}_${i}`
322
+ await this.storage
323
+ .bucket(bucketName)
324
+ .combine(fileBatch, this.storage.bucket(toBucket || bucketName).file(intermediateFile))
325
+ intermediateFiles.push(intermediateFile)
326
+ await this.deleteFiles(bucketName, fileBatch)
327
+ })
328
+ if (debug) {
329
+ logger.log(
330
+ `[${currentRecursionDepth}] Batch composed into ${intermediateFiles.length} files, in ${_since(started)}`,
331
+ )
332
+ }
333
+
334
+ await this.combineFiles(
335
+ toBucket || bucketName,
336
+ intermediateFiles,
337
+ toPath,
338
+ toBucket,
339
+ currentRecursionDepth + 1,
340
+ )
341
+ }
342
+
343
+ async combine(
344
+ bucketName: string,
345
+ prefix: string,
346
+ toPath: string,
347
+ toBucket?: string,
348
+ ): Promise<void> {
349
+ const filePaths = await this.getFileNames(bucketName, { prefix })
350
+ await this.combineFiles(bucketName, filePaths, toPath, toBucket)
253
351
  }
254
352
 
255
353
  /**
@@ -1,6 +1,5 @@
1
- import { Readable, Writable } from 'node:stream'
2
- import { LocalTimeInput } from '@naturalcycles/js-lib'
3
- import { ReadableTyped } from '@naturalcycles/nodejs-lib'
1
+ import type { LocalTimeInput } from '@naturalcycles/js-lib'
2
+ import type { ReadableBinary, ReadableTyped, WritableBinary } from '@naturalcycles/nodejs-lib'
4
3
 
5
4
  export interface FileEntry {
6
5
  filePath: string
@@ -69,6 +68,11 @@ export interface CommonStorage {
69
68
 
70
69
  deletePaths: (bucketName: string, prefixes: string[]) => Promise<void>
71
70
 
71
+ /**
72
+ * Should delete all files by their paths.
73
+ */
74
+ deleteFiles: (bucketName: string, filePaths: string[]) => Promise<void>
75
+
72
76
  /**
73
77
  * Returns an array of strings which are file paths.
74
78
  * Files that are not found by the path are not present in the map.
@@ -86,9 +90,9 @@ export interface CommonStorage {
86
90
 
87
91
  getFilesStream: (bucketName: string, opt?: CommonStorageGetOptions) => ReadableTyped<FileEntry>
88
92
 
89
- getFileReadStream: (bucketName: string, filePath: string) => Readable
93
+ getFileReadStream: (bucketName: string, filePath: string) => ReadableBinary
90
94
 
91
- getFileWriteStream: (bucketName: string, filePath: string) => Writable
95
+ getFileWriteStream: (bucketName: string, filePath: string) => WritableBinary
92
96
 
93
97
  /**
94
98
  * Upload local file to the bucket (by streaming it).
@@ -134,13 +138,18 @@ export interface CommonStorage {
134
138
  *
135
139
  * @experimental
136
140
  */
137
- combine: (
141
+ combineFiles: (
138
142
  bucketName: string,
139
143
  filePaths: string[],
140
144
  toPath: string,
141
145
  toBucket?: string,
142
146
  ) => Promise<void>
143
147
 
148
+ /**
149
+ * Like `combineFiles`, but for a `prefix`.
150
+ */
151
+ combine: (bucketName: string, prefix: string, toPath: string, toBucket?: string) => Promise<void>
152
+
144
153
  /**
145
154
  * Acquire a "signed url", which allows bearer to use it to download ('read') the file.
146
155
  *
@@ -1,4 +1,3 @@
1
- import { Readable, Writable } from 'node:stream'
2
1
  import {
3
2
  _assert,
4
3
  _isTruthy,
@@ -8,7 +7,14 @@ import {
8
7
  LocalTimeInput,
9
8
  StringMap,
10
9
  } from '@naturalcycles/js-lib'
11
- import { fs2, md5, ReadableTyped } from '@naturalcycles/nodejs-lib'
10
+ import {
11
+ fs2,
12
+ md5,
13
+ ReadableBinary,
14
+ readableFrom,
15
+ ReadableTyped,
16
+ WritableBinary,
17
+ } from '@naturalcycles/nodejs-lib'
12
18
  import { CommonStorage, CommonStorageGetOptions, FileEntry } from './commonStorage'
13
19
 
14
20
  export class InMemoryCommonStorage implements CommonStorage {
@@ -26,7 +32,7 @@ export class InMemoryCommonStorage implements CommonStorage {
26
32
  }
27
33
 
28
34
  getBucketNamesStream(): ReadableTyped<string> {
29
- return Readable.from(Object.keys(this.data))
35
+ return readableFrom(Object.keys(this.data))
30
36
  }
31
37
 
32
38
  async fileExists(bucketName: string, filePath: string): Promise<boolean> {
@@ -54,6 +60,11 @@ export class InMemoryCommonStorage implements CommonStorage {
54
60
  })
55
61
  }
56
62
 
63
+ async deleteFiles(bucketName: string, filePaths: string[]): Promise<void> {
64
+ if (!this.data[bucketName]) return
65
+ filePaths.forEach(filePath => delete this.data[bucketName]![filePath])
66
+ }
67
+
57
68
  async getFileNames(bucketName: string, opt: CommonStorageGetOptions = {}): Promise<string[]> {
58
69
  const { prefix = '', fullPaths = true } = opt
59
70
  return Object.keys(this.data[bucketName] || {})
@@ -64,7 +75,7 @@ export class InMemoryCommonStorage implements CommonStorage {
64
75
  getFileNamesStream(bucketName: string, opt: CommonStorageGetOptions = {}): ReadableTyped<string> {
65
76
  const { prefix = '', fullPaths = true } = opt
66
77
 
67
- return Readable.from(
78
+ return readableFrom(
68
79
  Object.keys(this.data[bucketName] || {})
69
80
  .filter(filePath => filePath.startsWith(prefix))
70
81
  .slice(0, opt.limit)
@@ -75,8 +86,8 @@ export class InMemoryCommonStorage implements CommonStorage {
75
86
  getFilesStream(bucketName: string, opt: CommonStorageGetOptions = {}): ReadableTyped<FileEntry> {
76
87
  const { prefix = '', fullPaths = true } = opt
77
88
 
78
- return Readable.from(
79
- Object.entries(this.data[bucketName] || {})
89
+ return readableFrom(
90
+ _stringMapEntries(this.data[bucketName] || {})
80
91
  .map(([filePath, content]) => ({
81
92
  filePath,
82
93
  content,
@@ -87,11 +98,11 @@ export class InMemoryCommonStorage implements CommonStorage {
87
98
  )
88
99
  }
89
100
 
90
- getFileReadStream(bucketName: string, filePath: string): Readable {
91
- return Readable.from(this.data[bucketName]![filePath]!)
101
+ getFileReadStream(bucketName: string, filePath: string): ReadableBinary {
102
+ return readableFrom(this.data[bucketName]![filePath]!)
92
103
  }
93
104
 
94
- getFileWriteStream(_bucketName: string, _filePath: string): Writable {
105
+ getFileWriteStream(_bucketName: string, _filePath: string): WritableBinary {
95
106
  throw new Error('Method not implemented.')
96
107
  }
97
108
 
@@ -156,6 +167,16 @@ export class InMemoryCommonStorage implements CommonStorage {
156
167
  }
157
168
 
158
169
  async combine(
170
+ bucketName: string,
171
+ prefix: string,
172
+ toPath: string,
173
+ toBucket?: string,
174
+ ): Promise<void> {
175
+ const filePaths = await this.getFileNames(bucketName, { prefix })
176
+ await this.combineFiles(bucketName, filePaths, toPath, toBucket)
177
+ }
178
+
179
+ async combineFiles(
159
180
  bucketName: string,
160
181
  filePaths: string[],
161
182
  toPath: string,