@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.
@@ -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
  *
@@ -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.storage.bucket(bucketName).deleteFiles({
42
- prefix,
43
- // to keep going in case error occurs, similar to THROW_AGGREGATED
44
- force: true,
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
- .pipe((0, nodejs_lib_1.transformMapSync)(f => this.normalizeFilename(f.name, fullPaths)));
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 js_lib_1.SKIP;
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 (0, nodejs_lib_1._pipeline)([
142
- this.storage.bucket(fromBucket).getFilesStream({
143
- prefix: fromPrefix,
144
- }),
145
- (0, nodejs_lib_1.writableForEach)(async (file) => {
146
- const { name } = file;
147
- const newName = toPrefix + name.slice(fromPrefix.length);
148
- await file.move(this.storage.bucket(toBucket || fromBucket).file(newName));
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;
@@ -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
- .pipe((0, nodejs_lib_1.transformMapSimple)(({ filePath, content }) => [
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 (0, nodejs_lib_1.readableToArray)(storage.getFileNamesStream(bucketName, { prefix: TEST_FOLDER }));
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 (0, nodejs_lib_1.readableToArray)(storage.getFileNamesStream(bucketName, { prefix: TEST_FOLDER }));
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
@@ -35,7 +35,7 @@
35
35
  "engines": {
36
36
  "node": ">=18.12.0"
37
37
  },
38
- "version": "1.8.0",
38
+ "version": "1.9.1",
39
39
  "description": "CommonStorage implementation based on Google Cloud Storage",
40
40
  "author": "Natural Cycles Team",
41
41
  "license": "MIT"
@@ -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.storage.bucket(bucketName).deleteFiles({
79
- prefix,
80
- // to keep going in case error occurs, similar to THROW_AGGREGATED
81
- force: true,
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 this.storage
109
- .bucket(bucketName)
110
- .getFilesStream({
111
+ return (
112
+ this.storage.bucket(bucketName).getFilesStream({
111
113
  prefix,
112
114
  maxResults: opt.limit || undefined,
113
- })
114
- .pipe(transformMapSync<File, string>(f => this.normalizeFilename(f.name, fullPaths)))
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 this.storage
121
- .bucket(bucketName)
122
- .getFilesStream({
126
+ return (
127
+ this.storage.bucket(bucketName).getFilesStream({
123
128
  prefix,
124
129
  maxResults: opt.limit || undefined,
125
- })
126
- .pipe(
127
- transformMap<File, FileEntry>(async f => {
128
- const filePath = this.normalizeFilename(f.name, fullPaths)
129
- if (filePath === SKIP) return SKIP
130
-
131
- const [content] = await f.download()
132
- return { filePath, content }
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 _pipeline([
219
- this.storage.bucket(fromBucket).getFilesStream({
221
+ await this.storage
222
+ .bucket(fromBucket)
223
+ .getFilesStream({
220
224
  prefix: fromPrefix,
221
- }),
222
- writableForEach<File>(async file => {
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
 
@@ -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, transformMapSimple } from '@naturalcycles/nodejs-lib'
4
- import { CommonStorage, FileEntry } from './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
- .pipe(
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 { _stringMapEntries, _substringAfterLast, StringMap } from '@naturalcycles/js-lib'
3
- import { fs2, ReadableTyped } from '@naturalcycles/nodejs-lib'
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 readableToArray(
73
- storage.getFileNamesStream(bucketName, { prefix: TEST_FOLDER }),
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 readableToArray(
103
- storage.getFileNamesStream(bucketName, { prefix: TEST_FOLDER }),
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> = {}