@naturalcycles/cloud-storage-lib 1.8.0 → 1.9.1
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 +2 -0
- package/dist/cloudStorage.js +42 -28
- package/dist/commonStorage.d.ts +17 -0
- package/dist/commonStorageKeyValueDB.js +2 -8
- package/dist/inMemoryCommonStorage.d.ts +4 -1
- package/dist/inMemoryCommonStorage.js +19 -1
- package/dist/testing/commonStorageTest.js +6 -3
- package/package.json +1 -1
- package/src/cloudStorage.ts +56 -35
- package/src/commonStorage.ts +25 -0
- package/src/commonStorageKeyValueDB.ts +4 -11
- package/src/inMemoryCommonStorage.ts +43 -3
- package/src/testing/commonStorageTest.ts +6 -7
package/dist/cloudStorage.d.ts
CHANGED
|
@@ -38,6 +38,7 @@ export declare class CloudStorage implements CommonStorage {
|
|
|
38
38
|
static createFromStorageOptions(storageOptions?: StorageOptions): CloudStorage;
|
|
39
39
|
ping(bucketName?: string): Promise<void>;
|
|
40
40
|
deletePath(bucketName: string, prefix: string): Promise<void>;
|
|
41
|
+
deletePaths(bucketName: string, prefixes: string[]): Promise<void>;
|
|
41
42
|
fileExists(bucketName: string, filePath: string): Promise<boolean>;
|
|
42
43
|
getFileNames(bucketName: string, opt?: CommonStorageGetOptions): Promise<string[]>;
|
|
43
44
|
getFileNamesStream(bucketName: string, opt?: CommonStorageGetOptions): ReadableTyped<string>;
|
|
@@ -56,6 +57,7 @@ export declare class CloudStorage implements CommonStorage {
|
|
|
56
57
|
copyFile(fromBucket: string, fromPath: string, toPath: string, toBucket?: string): Promise<void>;
|
|
57
58
|
moveFile(fromBucket: string, fromPath: string, toPath: string, toBucket?: string): Promise<void>;
|
|
58
59
|
movePath(fromBucket: string, fromPrefix: string, toPrefix: string, toBucket?: string): Promise<void>;
|
|
60
|
+
combine(bucketName: string, filePaths: string[], toPath: string, toBucket?: string): Promise<void>;
|
|
59
61
|
/**
|
|
60
62
|
* Acquires a "signed url", which allows bearer to use it to download ('read') the file.
|
|
61
63
|
*
|
package/dist/cloudStorage.js
CHANGED
|
@@ -4,7 +4,6 @@ exports.CloudStorage = exports.Storage = void 0;
|
|
|
4
4
|
const storage_1 = require("@google-cloud/storage");
|
|
5
5
|
Object.defineProperty(exports, "Storage", { enumerable: true, get: function () { return storage_1.Storage; } });
|
|
6
6
|
const js_lib_1 = require("@naturalcycles/js-lib");
|
|
7
|
-
const nodejs_lib_1 = require("@naturalcycles/nodejs-lib");
|
|
8
7
|
/**
|
|
9
8
|
* CloudStorage implementation of CommonStorage API.
|
|
10
9
|
*
|
|
@@ -38,10 +37,16 @@ class CloudStorage {
|
|
|
38
37
|
await this.storage.bucket(bucketName || 'non-existing-for-sure').exists();
|
|
39
38
|
}
|
|
40
39
|
async deletePath(bucketName, prefix) {
|
|
41
|
-
await this.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
await this.deletePaths(bucketName, [prefix]);
|
|
41
|
+
}
|
|
42
|
+
async deletePaths(bucketName, prefixes) {
|
|
43
|
+
const bucket = this.storage.bucket(bucketName);
|
|
44
|
+
await (0, js_lib_1.pMap)(prefixes, async (prefix) => {
|
|
45
|
+
await bucket.deleteFiles({
|
|
46
|
+
prefix,
|
|
47
|
+
// to keep going in case error occurs, similar to THROW_AGGREGATED
|
|
48
|
+
force: true,
|
|
49
|
+
});
|
|
45
50
|
});
|
|
46
51
|
}
|
|
47
52
|
async fileExists(bucketName, filePath) {
|
|
@@ -62,29 +67,28 @@ class CloudStorage {
|
|
|
62
67
|
}
|
|
63
68
|
getFileNamesStream(bucketName, opt = {}) {
|
|
64
69
|
const { prefix, fullPaths = true } = opt;
|
|
65
|
-
return this.storage
|
|
66
|
-
.bucket(bucketName)
|
|
67
|
-
.getFilesStream({
|
|
70
|
+
return this.storage.bucket(bucketName).getFilesStream({
|
|
68
71
|
prefix,
|
|
69
72
|
maxResults: opt.limit || undefined,
|
|
70
|
-
})
|
|
71
|
-
|
|
73
|
+
}).flatMap(f => {
|
|
74
|
+
const r = this.normalizeFilename(f.name, fullPaths);
|
|
75
|
+
if (r === js_lib_1.SKIP)
|
|
76
|
+
return [];
|
|
77
|
+
return [r];
|
|
78
|
+
});
|
|
72
79
|
}
|
|
73
80
|
getFilesStream(bucketName, opt = {}) {
|
|
74
81
|
const { prefix, fullPaths = true } = opt;
|
|
75
|
-
return this.storage
|
|
76
|
-
.bucket(bucketName)
|
|
77
|
-
.getFilesStream({
|
|
82
|
+
return this.storage.bucket(bucketName).getFilesStream({
|
|
78
83
|
prefix,
|
|
79
84
|
maxResults: opt.limit || undefined,
|
|
80
|
-
})
|
|
81
|
-
.pipe((0, nodejs_lib_1.transformMap)(async (f) => {
|
|
85
|
+
}).flatMap(async (f) => {
|
|
82
86
|
const filePath = this.normalizeFilename(f.name, fullPaths);
|
|
83
87
|
if (filePath === js_lib_1.SKIP)
|
|
84
|
-
return
|
|
88
|
+
return [];
|
|
85
89
|
const [content] = await f.download();
|
|
86
|
-
return { filePath, content };
|
|
87
|
-
})
|
|
90
|
+
return [{ filePath, content }];
|
|
91
|
+
});
|
|
88
92
|
}
|
|
89
93
|
async getFile(bucketName, filePath) {
|
|
90
94
|
const [buf] = await this.storage
|
|
@@ -138,16 +142,25 @@ class CloudStorage {
|
|
|
138
142
|
async movePath(fromBucket, fromPrefix, toPrefix, toBucket) {
|
|
139
143
|
(0, js_lib_1._assert)(fromPrefix.endsWith('/'), 'fromPrefix should end with `/`');
|
|
140
144
|
(0, js_lib_1._assert)(toPrefix.endsWith('/'), 'toPrefix should end with `/`');
|
|
141
|
-
await
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
145
|
+
await this.storage
|
|
146
|
+
.bucket(fromBucket)
|
|
147
|
+
.getFilesStream({
|
|
148
|
+
prefix: fromPrefix,
|
|
149
|
+
})
|
|
150
|
+
.forEach(async (file) => {
|
|
151
|
+
const { name } = file;
|
|
152
|
+
const newName = toPrefix + name.slice(fromPrefix.length);
|
|
153
|
+
await file.move(this.storage.bucket(toBucket || fromBucket).file(newName));
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
async combine(bucketName, filePaths, toPath, toBucket) {
|
|
157
|
+
// todo: if (filePaths.length > 32) - use recursive algorithm
|
|
158
|
+
(0, js_lib_1._assert)(filePaths.length <= 32, 'combine supports up to 32 input files');
|
|
159
|
+
await this.storage
|
|
160
|
+
.bucket(bucketName)
|
|
161
|
+
.combine(filePaths, this.storage.bucket(toBucket || bucketName).file(toPath));
|
|
162
|
+
// Delete original files
|
|
163
|
+
await this.deletePaths(bucketName, filePaths);
|
|
151
164
|
}
|
|
152
165
|
/**
|
|
153
166
|
* Acquires a "signed url", which allows bearer to use it to download ('read') the file.
|
|
@@ -162,6 +175,7 @@ class CloudStorage {
|
|
|
162
175
|
.file(filePath)
|
|
163
176
|
.getSignedUrl({
|
|
164
177
|
action: 'read',
|
|
178
|
+
version: 'v4',
|
|
165
179
|
expires: (0, js_lib_1.localTime)(expires).unixMillis(),
|
|
166
180
|
});
|
|
167
181
|
return url;
|
package/dist/commonStorage.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
/// <reference types="node" />
|
|
3
3
|
import { Readable, Writable } from 'node:stream';
|
|
4
|
+
import { LocalTimeInput } from '@naturalcycles/js-lib';
|
|
4
5
|
import { ReadableTyped } from '@naturalcycles/nodejs-lib';
|
|
5
6
|
export interface FileEntry {
|
|
6
7
|
filePath: string;
|
|
@@ -54,6 +55,7 @@ export interface CommonStorage {
|
|
|
54
55
|
* Should recursively delete all files in a folder, if path is a folder.
|
|
55
56
|
*/
|
|
56
57
|
deletePath: (bucketName: string, prefix: string) => Promise<void>;
|
|
58
|
+
deletePaths: (bucketName: string, prefixes: string[]) => Promise<void>;
|
|
57
59
|
/**
|
|
58
60
|
* Returns an array of strings which are file paths.
|
|
59
61
|
* Files that are not found by the path are not present in the map.
|
|
@@ -85,4 +87,19 @@ export interface CommonStorage {
|
|
|
85
87
|
* otherwise some folder that starts with the same prefix will be included.
|
|
86
88
|
*/
|
|
87
89
|
movePath: (fromBucket: string, fromPrefix: string, toPrefix: string, toBucket?: string) => Promise<void>;
|
|
90
|
+
/**
|
|
91
|
+
* Combine (compose) multiple input files into a single output file.
|
|
92
|
+
* Should support unlimited number of input files, using recursive algorithm if necessary.
|
|
93
|
+
*
|
|
94
|
+
* After the output file is created, all input files should be deleted.
|
|
95
|
+
*
|
|
96
|
+
* @experimental
|
|
97
|
+
*/
|
|
98
|
+
combine: (bucketName: string, filePaths: string[], toPath: string, toBucket?: string) => Promise<void>;
|
|
99
|
+
/**
|
|
100
|
+
* Acquire a "signed url", which allows bearer to use it to download ('read') the file.
|
|
101
|
+
*
|
|
102
|
+
* @experimental
|
|
103
|
+
*/
|
|
104
|
+
getSignedUrl: (bucketName: string, filePath: string, expires: LocalTimeInput) => Promise<string>;
|
|
88
105
|
}
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.CommonStorageKeyValueDB = void 0;
|
|
4
4
|
const js_lib_1 = require("@naturalcycles/js-lib");
|
|
5
|
-
const nodejs_lib_1 = require("@naturalcycles/nodejs-lib");
|
|
6
5
|
/**
|
|
7
6
|
* CommonKeyValueDB, backed up by a CommonStorage implementation.
|
|
8
7
|
*
|
|
@@ -66,18 +65,13 @@ class CommonStorageKeyValueDB {
|
|
|
66
65
|
}
|
|
67
66
|
streamValues(table, limit) {
|
|
68
67
|
const { bucketName, prefix } = this.getBucketAndPrefix(table);
|
|
69
|
-
return this.cfg.storage
|
|
70
|
-
.getFilesStream(bucketName, { prefix, limit })
|
|
71
|
-
.pipe((0, nodejs_lib_1.transformMapSimple)(f => f.content));
|
|
68
|
+
return this.cfg.storage.getFilesStream(bucketName, { prefix, limit }).map(f => f.content);
|
|
72
69
|
}
|
|
73
70
|
streamEntries(table, limit) {
|
|
74
71
|
const { bucketName, prefix } = this.getBucketAndPrefix(table);
|
|
75
72
|
return this.cfg.storage
|
|
76
73
|
.getFilesStream(bucketName, { prefix, limit, fullPaths: false })
|
|
77
|
-
.
|
|
78
|
-
filePath,
|
|
79
|
-
content,
|
|
80
|
-
]));
|
|
74
|
+
.map(f => [f.filePath, f.content]);
|
|
81
75
|
}
|
|
82
76
|
async count(table) {
|
|
83
77
|
const { bucketName, prefix } = this.getBucketAndPrefix(table);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
/// <reference types="node" />
|
|
3
3
|
import { Readable, Writable } from 'node:stream';
|
|
4
|
-
import { StringMap } from '@naturalcycles/js-lib';
|
|
4
|
+
import { LocalTimeInput, StringMap } from '@naturalcycles/js-lib';
|
|
5
5
|
import { ReadableTyped } from '@naturalcycles/nodejs-lib';
|
|
6
6
|
import { CommonStorage, CommonStorageGetOptions, FileEntry } from './commonStorage';
|
|
7
7
|
export declare class InMemoryCommonStorage implements CommonStorage {
|
|
@@ -17,6 +17,7 @@ export declare class InMemoryCommonStorage implements CommonStorage {
|
|
|
17
17
|
getFile(bucketName: string, filePath: string): Promise<Buffer | null>;
|
|
18
18
|
saveFile(bucketName: string, filePath: string, content: Buffer): Promise<void>;
|
|
19
19
|
deletePath(bucketName: string, prefix: string): Promise<void>;
|
|
20
|
+
deletePaths(bucketName: string, prefixes: string[]): Promise<void>;
|
|
20
21
|
getFileNames(bucketName: string, opt?: CommonStorageGetOptions): Promise<string[]>;
|
|
21
22
|
getFileNamesStream(bucketName: string, opt?: CommonStorageGetOptions): ReadableTyped<string>;
|
|
22
23
|
getFilesStream(bucketName: string, opt?: CommonStorageGetOptions): ReadableTyped<FileEntry>;
|
|
@@ -28,4 +29,6 @@ export declare class InMemoryCommonStorage implements CommonStorage {
|
|
|
28
29
|
copyFile(fromBucket: string, fromPath: string, toPath: string, toBucket?: string): Promise<void>;
|
|
29
30
|
moveFile(fromBucket: string, fromPath: string, toPath: string, toBucket?: string): Promise<void>;
|
|
30
31
|
movePath(fromBucket: string, fromPrefix: string, toPrefix: string, toBucket?: string): Promise<void>;
|
|
32
|
+
combine(bucketName: string, filePaths: string[], toPath: string, toBucket?: string): Promise<void>;
|
|
33
|
+
getSignedUrl(bucketName: string, filePath: string, expires: LocalTimeInput): Promise<string>;
|
|
31
34
|
}
|
|
@@ -30,8 +30,11 @@ class InMemoryCommonStorage {
|
|
|
30
30
|
this.data[bucketName][filePath] = content;
|
|
31
31
|
}
|
|
32
32
|
async deletePath(bucketName, prefix) {
|
|
33
|
+
await this.deletePaths(bucketName, [prefix]);
|
|
34
|
+
}
|
|
35
|
+
async deletePaths(bucketName, prefixes) {
|
|
33
36
|
Object.keys(this.data[bucketName] || {}).forEach(filePath => {
|
|
34
|
-
if (filePath.startsWith(prefix)) {
|
|
37
|
+
if (prefixes.some(prefix => filePath.startsWith(prefix))) {
|
|
35
38
|
delete this.data[bucketName][filePath];
|
|
36
39
|
}
|
|
37
40
|
});
|
|
@@ -101,5 +104,20 @@ class InMemoryCommonStorage {
|
|
|
101
104
|
delete this.data[fromBucket][filePath];
|
|
102
105
|
});
|
|
103
106
|
}
|
|
107
|
+
async combine(bucketName, filePaths, toPath, toBucket) {
|
|
108
|
+
if (!this.data[bucketName])
|
|
109
|
+
return;
|
|
110
|
+
const tob = toBucket || bucketName;
|
|
111
|
+
this.data[tob] ||= {};
|
|
112
|
+
this.data[tob][toPath] = Buffer.concat(filePaths.map(p => this.data[bucketName][p]).filter(js_lib_1._isTruthy));
|
|
113
|
+
// delete source files
|
|
114
|
+
filePaths.forEach(p => delete this.data[bucketName][p]);
|
|
115
|
+
}
|
|
116
|
+
async getSignedUrl(bucketName, filePath, expires) {
|
|
117
|
+
const buf = this.data[bucketName]?.[filePath];
|
|
118
|
+
(0, js_lib_1._assert)(buf, `getSignedUrl file not found: ${bucketName}/${filePath}`);
|
|
119
|
+
const signature = (0, nodejs_lib_1.md5)(buf);
|
|
120
|
+
return `https://testurl.com/${bucketName}/${filePath}?expires=${(0, js_lib_1.localTime)(expires).unix()}&signature=${signature}`;
|
|
121
|
+
}
|
|
104
122
|
}
|
|
105
123
|
exports.InMemoryCommonStorage = InMemoryCommonStorage;
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.runCommonStorageTest = void 0;
|
|
4
4
|
const js_lib_1 = require("@naturalcycles/js-lib");
|
|
5
|
-
const nodejs_lib_1 = require("@naturalcycles/nodejs-lib");
|
|
6
5
|
const TEST_FOLDER = 'test/subdir';
|
|
7
6
|
const TEST_ITEMS = (0, js_lib_1._range)(10).map(n => ({
|
|
8
7
|
id: `id_${n + 1}`,
|
|
@@ -56,7 +55,9 @@ function runCommonStorageTest(storage, bucketName) {
|
|
|
56
55
|
expect(fileNames).toEqual([]);
|
|
57
56
|
});
|
|
58
57
|
test(`streamFileNames on ${TEST_FOLDER} should return empty`, async () => {
|
|
59
|
-
const fileNames = await
|
|
58
|
+
const fileNames = await storage
|
|
59
|
+
.getFileNamesStream(bucketName, { prefix: TEST_FOLDER })
|
|
60
|
+
.toArray();
|
|
60
61
|
expect(fileNames).toEqual([]);
|
|
61
62
|
});
|
|
62
63
|
test(`exists should return empty array`, async () => {
|
|
@@ -76,7 +77,9 @@ function runCommonStorageTest(storage, bucketName) {
|
|
|
76
77
|
expect(fileNamesShort.sort()).toEqual(TEST_FILES.map(f => (0, js_lib_1._substringAfterLast)(f.filePath, '/')).sort());
|
|
77
78
|
const fileNames = await storage.getFileNames(bucketName, { prefix: TEST_FOLDER });
|
|
78
79
|
expect(fileNames.sort()).toEqual(TEST_FILES.map(f => f.filePath).sort());
|
|
79
|
-
const streamedFileNames = await
|
|
80
|
+
const streamedFileNames = await storage
|
|
81
|
+
.getFileNamesStream(bucketName, { prefix: TEST_FOLDER })
|
|
82
|
+
.toArray();
|
|
80
83
|
expect(streamedFileNames.sort()).toEqual(TEST_FILES.map(f => f.filePath).sort());
|
|
81
84
|
const filesMap = {};
|
|
82
85
|
await (0, js_lib_1.pMap)(fileNames, async (filePath) => {
|
package/package.json
CHANGED
package/src/cloudStorage.ts
CHANGED
|
@@ -5,15 +5,10 @@ import {
|
|
|
5
5
|
_substringAfterLast,
|
|
6
6
|
localTime,
|
|
7
7
|
LocalTimeInput,
|
|
8
|
+
pMap,
|
|
8
9
|
SKIP,
|
|
9
10
|
} from '@naturalcycles/js-lib'
|
|
10
|
-
import {
|
|
11
|
-
_pipeline,
|
|
12
|
-
ReadableTyped,
|
|
13
|
-
transformMap,
|
|
14
|
-
transformMapSync,
|
|
15
|
-
writableForEach,
|
|
16
|
-
} from '@naturalcycles/nodejs-lib'
|
|
11
|
+
import { ReadableTyped } from '@naturalcycles/nodejs-lib'
|
|
17
12
|
import { CommonStorage, CommonStorageGetOptions, FileEntry } from './commonStorage'
|
|
18
13
|
import { GCPServiceAccount } from './model'
|
|
19
14
|
|
|
@@ -75,10 +70,18 @@ export class CloudStorage implements CommonStorage {
|
|
|
75
70
|
}
|
|
76
71
|
|
|
77
72
|
async deletePath(bucketName: string, prefix: string): Promise<void> {
|
|
78
|
-
await this.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
73
|
+
await this.deletePaths(bucketName, [prefix])
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async deletePaths(bucketName: string, prefixes: string[]): Promise<void> {
|
|
77
|
+
const bucket = this.storage.bucket(bucketName)
|
|
78
|
+
|
|
79
|
+
await pMap(prefixes, async prefix => {
|
|
80
|
+
await bucket.deleteFiles({
|
|
81
|
+
prefix,
|
|
82
|
+
// to keep going in case error occurs, similar to THROW_AGGREGATED
|
|
83
|
+
force: true,
|
|
84
|
+
})
|
|
82
85
|
})
|
|
83
86
|
}
|
|
84
87
|
|
|
@@ -105,33 +108,33 @@ export class CloudStorage implements CommonStorage {
|
|
|
105
108
|
getFileNamesStream(bucketName: string, opt: CommonStorageGetOptions = {}): ReadableTyped<string> {
|
|
106
109
|
const { prefix, fullPaths = true } = opt
|
|
107
110
|
|
|
108
|
-
return
|
|
109
|
-
.bucket(bucketName)
|
|
110
|
-
.getFilesStream({
|
|
111
|
+
return (
|
|
112
|
+
this.storage.bucket(bucketName).getFilesStream({
|
|
111
113
|
prefix,
|
|
112
114
|
maxResults: opt.limit || undefined,
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
+
}) as ReadableTyped<File>
|
|
116
|
+
).flatMap(f => {
|
|
117
|
+
const r = this.normalizeFilename(f.name, fullPaths)
|
|
118
|
+
if (r === SKIP) return []
|
|
119
|
+
return [r]
|
|
120
|
+
})
|
|
115
121
|
}
|
|
116
122
|
|
|
117
123
|
getFilesStream(bucketName: string, opt: CommonStorageGetOptions = {}): ReadableTyped<FileEntry> {
|
|
118
124
|
const { prefix, fullPaths = true } = opt
|
|
119
125
|
|
|
120
|
-
return
|
|
121
|
-
.bucket(bucketName)
|
|
122
|
-
.getFilesStream({
|
|
126
|
+
return (
|
|
127
|
+
this.storage.bucket(bucketName).getFilesStream({
|
|
123
128
|
prefix,
|
|
124
129
|
maxResults: opt.limit || undefined,
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}),
|
|
134
|
-
)
|
|
130
|
+
}) as ReadableTyped<File>
|
|
131
|
+
).flatMap(async f => {
|
|
132
|
+
const filePath = this.normalizeFilename(f.name, fullPaths)
|
|
133
|
+
if (filePath === SKIP) return []
|
|
134
|
+
|
|
135
|
+
const [content] = await f.download()
|
|
136
|
+
return [{ filePath, content }] as FileEntry[]
|
|
137
|
+
})
|
|
135
138
|
}
|
|
136
139
|
|
|
137
140
|
async getFile(bucketName: string, filePath: string): Promise<Buffer | null> {
|
|
@@ -215,16 +218,33 @@ export class CloudStorage implements CommonStorage {
|
|
|
215
218
|
_assert(fromPrefix.endsWith('/'), 'fromPrefix should end with `/`')
|
|
216
219
|
_assert(toPrefix.endsWith('/'), 'toPrefix should end with `/`')
|
|
217
220
|
|
|
218
|
-
await
|
|
219
|
-
|
|
221
|
+
await this.storage
|
|
222
|
+
.bucket(fromBucket)
|
|
223
|
+
.getFilesStream({
|
|
220
224
|
prefix: fromPrefix,
|
|
221
|
-
})
|
|
222
|
-
|
|
225
|
+
})
|
|
226
|
+
.forEach(async file => {
|
|
223
227
|
const { name } = file
|
|
224
228
|
const newName = toPrefix + name.slice(fromPrefix.length)
|
|
225
229
|
await file.move(this.storage.bucket(toBucket || fromBucket).file(newName))
|
|
226
|
-
})
|
|
227
|
-
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async combine(
|
|
234
|
+
bucketName: string,
|
|
235
|
+
filePaths: string[],
|
|
236
|
+
toPath: string,
|
|
237
|
+
toBucket?: string,
|
|
238
|
+
): Promise<void> {
|
|
239
|
+
// todo: if (filePaths.length > 32) - use recursive algorithm
|
|
240
|
+
_assert(filePaths.length <= 32, 'combine supports up to 32 input files')
|
|
241
|
+
|
|
242
|
+
await this.storage
|
|
243
|
+
.bucket(bucketName)
|
|
244
|
+
.combine(filePaths, this.storage.bucket(toBucket || bucketName).file(toPath))
|
|
245
|
+
|
|
246
|
+
// Delete original files
|
|
247
|
+
await this.deletePaths(bucketName, filePaths)
|
|
228
248
|
}
|
|
229
249
|
|
|
230
250
|
/**
|
|
@@ -244,6 +264,7 @@ export class CloudStorage implements CommonStorage {
|
|
|
244
264
|
.file(filePath)
|
|
245
265
|
.getSignedUrl({
|
|
246
266
|
action: 'read',
|
|
267
|
+
version: 'v4',
|
|
247
268
|
expires: localTime(expires).unixMillis(),
|
|
248
269
|
})
|
|
249
270
|
|
package/src/commonStorage.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Readable, Writable } from 'node:stream'
|
|
2
|
+
import { LocalTimeInput } from '@naturalcycles/js-lib'
|
|
2
3
|
import { ReadableTyped } from '@naturalcycles/nodejs-lib'
|
|
3
4
|
|
|
4
5
|
export interface FileEntry {
|
|
@@ -66,6 +67,8 @@ export interface CommonStorage {
|
|
|
66
67
|
*/
|
|
67
68
|
deletePath: (bucketName: string, prefix: string) => Promise<void>
|
|
68
69
|
|
|
70
|
+
deletePaths: (bucketName: string, prefixes: string[]) => Promise<void>
|
|
71
|
+
|
|
69
72
|
/**
|
|
70
73
|
* Returns an array of strings which are file paths.
|
|
71
74
|
* Files that are not found by the path are not present in the map.
|
|
@@ -122,4 +125,26 @@ export interface CommonStorage {
|
|
|
122
125
|
toPrefix: string,
|
|
123
126
|
toBucket?: string,
|
|
124
127
|
) => Promise<void>
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Combine (compose) multiple input files into a single output file.
|
|
131
|
+
* Should support unlimited number of input files, using recursive algorithm if necessary.
|
|
132
|
+
*
|
|
133
|
+
* After the output file is created, all input files should be deleted.
|
|
134
|
+
*
|
|
135
|
+
* @experimental
|
|
136
|
+
*/
|
|
137
|
+
combine: (
|
|
138
|
+
bucketName: string,
|
|
139
|
+
filePaths: string[],
|
|
140
|
+
toPath: string,
|
|
141
|
+
toBucket?: string,
|
|
142
|
+
) => Promise<void>
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Acquire a "signed url", which allows bearer to use it to download ('read') the file.
|
|
146
|
+
*
|
|
147
|
+
* @experimental
|
|
148
|
+
*/
|
|
149
|
+
getSignedUrl: (bucketName: string, filePath: string, expires: LocalTimeInput) => Promise<string>
|
|
125
150
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CommonDBCreateOptions, CommonKeyValueDB, KeyValueDBTuple } from '@naturalcycles/db-lib'
|
|
2
2
|
import { pMap, StringMap } from '@naturalcycles/js-lib'
|
|
3
|
-
import { ReadableTyped
|
|
4
|
-
import { CommonStorage
|
|
3
|
+
import { ReadableTyped } from '@naturalcycles/nodejs-lib'
|
|
4
|
+
import { CommonStorage } from './commonStorage'
|
|
5
5
|
|
|
6
6
|
export interface CommonStorageKeyValueDBCfg {
|
|
7
7
|
storage: CommonStorage
|
|
@@ -84,9 +84,7 @@ export class CommonStorageKeyValueDB implements CommonKeyValueDB {
|
|
|
84
84
|
streamValues(table: string, limit?: number): ReadableTyped<Buffer> {
|
|
85
85
|
const { bucketName, prefix } = this.getBucketAndPrefix(table)
|
|
86
86
|
|
|
87
|
-
return this.cfg.storage
|
|
88
|
-
.getFilesStream(bucketName, { prefix, limit })
|
|
89
|
-
.pipe(transformMapSimple<FileEntry, Buffer>(f => f.content))
|
|
87
|
+
return this.cfg.storage.getFilesStream(bucketName, { prefix, limit }).map(f => f.content)
|
|
90
88
|
}
|
|
91
89
|
|
|
92
90
|
streamEntries(table: string, limit?: number): ReadableTyped<KeyValueDBTuple> {
|
|
@@ -94,12 +92,7 @@ export class CommonStorageKeyValueDB implements CommonKeyValueDB {
|
|
|
94
92
|
|
|
95
93
|
return this.cfg.storage
|
|
96
94
|
.getFilesStream(bucketName, { prefix, limit, fullPaths: false })
|
|
97
|
-
.
|
|
98
|
-
transformMapSimple<FileEntry, KeyValueDBTuple>(({ filePath, content }) => [
|
|
99
|
-
filePath,
|
|
100
|
-
content,
|
|
101
|
-
]),
|
|
102
|
-
)
|
|
95
|
+
.map(f => [f.filePath, f.content] satisfies KeyValueDBTuple)
|
|
103
96
|
}
|
|
104
97
|
|
|
105
98
|
async count(table: string): Promise<number> {
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { Readable, Writable } from 'node:stream'
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
_assert,
|
|
4
|
+
_isTruthy,
|
|
5
|
+
_stringMapEntries,
|
|
6
|
+
_substringAfterLast,
|
|
7
|
+
localTime,
|
|
8
|
+
LocalTimeInput,
|
|
9
|
+
StringMap,
|
|
10
|
+
} from '@naturalcycles/js-lib'
|
|
11
|
+
import { fs2, md5, ReadableTyped } from '@naturalcycles/nodejs-lib'
|
|
4
12
|
import { CommonStorage, CommonStorageGetOptions, FileEntry } from './commonStorage'
|
|
5
13
|
|
|
6
14
|
export class InMemoryCommonStorage implements CommonStorage {
|
|
@@ -35,8 +43,12 @@ export class InMemoryCommonStorage implements CommonStorage {
|
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
async deletePath(bucketName: string, prefix: string): Promise<void> {
|
|
46
|
+
await this.deletePaths(bucketName, [prefix])
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async deletePaths(bucketName: string, prefixes: string[]): Promise<void> {
|
|
38
50
|
Object.keys(this.data[bucketName] || {}).forEach(filePath => {
|
|
39
|
-
if (filePath.startsWith(prefix)) {
|
|
51
|
+
if (prefixes.some(prefix => filePath.startsWith(prefix))) {
|
|
40
52
|
delete this.data[bucketName]![filePath]
|
|
41
53
|
}
|
|
42
54
|
})
|
|
@@ -142,4 +154,32 @@ export class InMemoryCommonStorage implements CommonStorage {
|
|
|
142
154
|
delete this.data[fromBucket]![filePath]
|
|
143
155
|
})
|
|
144
156
|
}
|
|
157
|
+
|
|
158
|
+
async combine(
|
|
159
|
+
bucketName: string,
|
|
160
|
+
filePaths: string[],
|
|
161
|
+
toPath: string,
|
|
162
|
+
toBucket?: string,
|
|
163
|
+
): Promise<void> {
|
|
164
|
+
if (!this.data[bucketName]) return
|
|
165
|
+
const tob = toBucket || bucketName
|
|
166
|
+
this.data[tob] ||= {}
|
|
167
|
+
this.data[tob]![toPath] = Buffer.concat(
|
|
168
|
+
filePaths.map(p => this.data[bucketName]![p]).filter(_isTruthy),
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
// delete source files
|
|
172
|
+
filePaths.forEach(p => delete this.data[bucketName]![p])
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async getSignedUrl(
|
|
176
|
+
bucketName: string,
|
|
177
|
+
filePath: string,
|
|
178
|
+
expires: LocalTimeInput,
|
|
179
|
+
): Promise<string> {
|
|
180
|
+
const buf = this.data[bucketName]?.[filePath]
|
|
181
|
+
_assert(buf, `getSignedUrl file not found: ${bucketName}/${filePath}`)
|
|
182
|
+
const signature = md5(buf)
|
|
183
|
+
return `https://testurl.com/${bucketName}/${filePath}?expires=${localTime(expires).unix()}&signature=${signature}`
|
|
184
|
+
}
|
|
145
185
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { _range, _substringAfterLast, pMap, StringMap } from '@naturalcycles/js-lib'
|
|
2
|
-
import { readableToArray } from '@naturalcycles/nodejs-lib'
|
|
3
2
|
import { CommonStorage, FileEntry } from '../commonStorage'
|
|
4
3
|
|
|
5
4
|
const TEST_FOLDER = 'test/subdir'
|
|
@@ -69,9 +68,9 @@ export function runCommonStorageTest(storage: CommonStorage, bucketName: string)
|
|
|
69
68
|
})
|
|
70
69
|
|
|
71
70
|
test(`streamFileNames on ${TEST_FOLDER} should return empty`, async () => {
|
|
72
|
-
const fileNames = await
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
const fileNames = await storage
|
|
72
|
+
.getFileNamesStream(bucketName, { prefix: TEST_FOLDER })
|
|
73
|
+
.toArray()
|
|
75
74
|
expect(fileNames).toEqual([])
|
|
76
75
|
})
|
|
77
76
|
|
|
@@ -99,9 +98,9 @@ export function runCommonStorageTest(storage: CommonStorage, bucketName: string)
|
|
|
99
98
|
const fileNames = await storage.getFileNames(bucketName, { prefix: TEST_FOLDER })
|
|
100
99
|
expect(fileNames.sort()).toEqual(TEST_FILES.map(f => f.filePath).sort())
|
|
101
100
|
|
|
102
|
-
const streamedFileNames = await
|
|
103
|
-
|
|
104
|
-
|
|
101
|
+
const streamedFileNames = await storage
|
|
102
|
+
.getFileNamesStream(bucketName, { prefix: TEST_FOLDER })
|
|
103
|
+
.toArray()
|
|
105
104
|
expect(streamedFileNames.sort()).toEqual(TEST_FILES.map(f => f.filePath).sort())
|
|
106
105
|
|
|
107
106
|
const filesMap: StringMap<Buffer> = {}
|