@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.
- package/dist/cloudStorage.d.ts +24 -16
- package/dist/cloudStorage.js +68 -25
- package/dist/commonStorage.d.ts +13 -7
- package/dist/inMemoryCommonStorage.d.ts +6 -6
- package/dist/inMemoryCommonStorage.js +14 -6
- package/package.json +1 -1
- package/src/cloudStorage.ts +129 -31
- package/src/commonStorage.ts +15 -6
- package/src/inMemoryCommonStorage.ts +30 -9
package/dist/cloudStorage.d.ts
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
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
|
-
*
|
|
19
|
+
* Default is console
|
|
20
|
+
*/
|
|
21
|
+
logger?: CommonLogger;
|
|
22
|
+
/**
|
|
23
|
+
* Pass true for extra debugging
|
|
22
24
|
*/
|
|
23
|
-
|
|
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
|
-
|
|
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):
|
|
57
|
+
getFileReadStream(bucketName: string, filePath: string): ReadableBinary;
|
|
52
58
|
saveFile(bucketName: string, filePath: string, content: Buffer): Promise<void>;
|
|
53
|
-
getFileWriteStream(bucketName: string, filePath: string):
|
|
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
|
-
|
|
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
|
*
|
package/dist/cloudStorage.js
CHANGED
|
@@ -1,37 +1,46 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CloudStorage =
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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:
|
|
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
|
|
34
|
-
|
|
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
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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.
|
package/dist/commonStorage.d.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
|
-
|
|
3
|
-
import {
|
|
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) =>
|
|
74
|
-
getFileWriteStream: (bucketName: string, filePath: string) =>
|
|
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
|
-
|
|
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):
|
|
25
|
-
getFileWriteStream(_bucketName: string, _filePath: string):
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
package/src/cloudStorage.ts
CHANGED
|
@@ -1,23 +1,31 @@
|
|
|
1
|
-
|
|
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
|
-
*
|
|
40
|
+
* Default is console
|
|
41
|
+
*/
|
|
42
|
+
logger?: CommonLogger
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Pass true for extra debugging
|
|
33
46
|
*/
|
|
34
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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:
|
|
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(
|
|
64
|
-
|
|
65
|
-
|
|
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):
|
|
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):
|
|
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
|
|
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
|
-
|
|
245
|
-
|
|
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
|
-
|
|
248
|
-
.
|
|
249
|
-
|
|
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
|
-
|
|
252
|
-
|
|
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
|
/**
|
package/src/commonStorage.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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) =>
|
|
93
|
+
getFileReadStream: (bucketName: string, filePath: string) => ReadableBinary
|
|
90
94
|
|
|
91
|
-
getFileWriteStream: (bucketName: string, filePath: string) =>
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
79
|
-
|
|
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):
|
|
91
|
-
return
|
|
101
|
+
getFileReadStream(bucketName: string, filePath: string): ReadableBinary {
|
|
102
|
+
return readableFrom(this.data[bucketName]![filePath]!)
|
|
92
103
|
}
|
|
93
104
|
|
|
94
|
-
getFileWriteStream(_bucketName: string, _filePath: string):
|
|
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,
|