@naturalcycles/cloud-storage-lib 1.7.0 → 1.9.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.
@@ -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>;
@@ -50,11 +51,13 @@ export declare class CloudStorage implements CommonStorage {
50
51
  getFileReadStream(bucketName: string, filePath: string): Readable;
51
52
  saveFile(bucketName: string, filePath: string, content: Buffer): Promise<void>;
52
53
  getFileWriteStream(bucketName: string, filePath: string): Writable;
54
+ uploadFile(localFilePath: string, bucketName: string, bucketFilePath: string): Promise<void>;
53
55
  setFileVisibility(bucketName: string, filePath: string, isPublic: boolean): Promise<void>;
54
56
  getFileVisibility(bucketName: string, filePath: string): Promise<boolean>;
55
57
  copyFile(fromBucket: string, fromPath: string, toPath: string, toBucket?: string): Promise<void>;
56
58
  moveFile(fromBucket: string, fromPath: string, toPath: string, toBucket?: string): Promise<void>;
57
59
  movePath(fromBucket: string, fromPrefix: string, toPrefix: string, toBucket?: string): Promise<void>;
60
+ combine(bucketName: string, filePaths: string[], toPath: string, toBucket?: string): Promise<void>;
58
61
  /**
59
62
  * Acquires a "signed url", which allows bearer to use it to download ('read') the file.
60
63
  *
@@ -38,10 +38,16 @@ class CloudStorage {
38
38
  await this.storage.bucket(bucketName || 'non-existing-for-sure').exists();
39
39
  }
40
40
  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,
41
+ await this.deletePaths(bucketName, [prefix]);
42
+ }
43
+ async deletePaths(bucketName, prefixes) {
44
+ const bucket = this.storage.bucket(bucketName);
45
+ await (0, js_lib_1.pMap)(prefixes, async (prefix) => {
46
+ await bucket.deleteFiles({
47
+ prefix,
48
+ // to keep going in case error occurs, similar to THROW_AGGREGATED
49
+ force: true,
50
+ });
45
51
  });
46
52
  }
47
53
  async fileExists(bucketName, filePath) {
@@ -111,6 +117,11 @@ class CloudStorage {
111
117
  getFileWriteStream(bucketName, filePath) {
112
118
  return this.storage.bucket(bucketName).file(filePath).createWriteStream();
113
119
  }
120
+ async uploadFile(localFilePath, bucketName, bucketFilePath) {
121
+ await this.storage.bucket(bucketName).upload(localFilePath, {
122
+ destination: bucketFilePath,
123
+ });
124
+ }
114
125
  async setFileVisibility(bucketName, filePath, isPublic) {
115
126
  await this.storage.bucket(bucketName).file(filePath)[isPublic ? 'makePublic' : 'makePrivate']();
116
127
  }
@@ -144,6 +155,15 @@ class CloudStorage {
144
155
  }),
145
156
  ]);
146
157
  }
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);
166
+ }
147
167
  /**
148
168
  * Acquires a "signed url", which allows bearer to use it to download ('read') the file.
149
169
  *
@@ -157,6 +177,7 @@ class CloudStorage {
157
177
  .file(filePath)
158
178
  .getSignedUrl({
159
179
  action: 'read',
180
+ version: 'v4',
160
181
  expires: (0, js_lib_1.localTime)(expires).unixMillis(),
161
182
  });
162
183
  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.
@@ -70,6 +72,10 @@ export interface CommonStorage {
70
72
  getFilesStream: (bucketName: string, opt?: CommonStorageGetOptions) => ReadableTyped<FileEntry>;
71
73
  getFileReadStream: (bucketName: string, filePath: string) => Readable;
72
74
  getFileWriteStream: (bucketName: string, filePath: string) => Writable;
75
+ /**
76
+ * Upload local file to the bucket (by streaming it).
77
+ */
78
+ uploadFile: (localFilePath: string, bucketName: string, bucketFilePath: string) => Promise<void>;
73
79
  setFileVisibility: (bucketName: string, filePath: string, isPublic: boolean) => Promise<void>;
74
80
  getFileVisibility: (bucketName: string, filePath: string) => Promise<boolean>;
75
81
  copyFile: (fromBucket: string, fromPath: string, toPath: string, toBucket?: string) => Promise<void>;
@@ -81,4 +87,19 @@ export interface CommonStorage {
81
87
  * otherwise some folder that starts with the same prefix will be included.
82
88
  */
83
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>;
84
105
  }
@@ -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,14 +17,18 @@ 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>;
23
24
  getFileReadStream(bucketName: string, filePath: string): Readable;
24
25
  getFileWriteStream(_bucketName: string, _filePath: string): Writable;
26
+ uploadFile(localFilePath: string, bucketName: string, bucketFilePath: string): Promise<void>;
25
27
  setFileVisibility(bucketName: string, filePath: string, isPublic: boolean): Promise<void>;
26
28
  getFileVisibility(bucketName: string, filePath: string): Promise<boolean>;
27
29
  copyFile(fromBucket: string, fromPath: string, toPath: string, toBucket?: string): Promise<void>;
28
30
  moveFile(fromBucket: string, fromPath: string, toPath: string, toBucket?: string): Promise<void>;
29
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>;
30
34
  }
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.InMemoryCommonStorage = void 0;
4
4
  const node_stream_1 = require("node:stream");
5
5
  const js_lib_1 = require("@naturalcycles/js-lib");
6
+ const nodejs_lib_1 = require("@naturalcycles/nodejs-lib");
6
7
  class InMemoryCommonStorage {
7
8
  constructor() {
8
9
  /**
@@ -29,8 +30,11 @@ class InMemoryCommonStorage {
29
30
  this.data[bucketName][filePath] = content;
30
31
  }
31
32
  async deletePath(bucketName, prefix) {
33
+ await this.deletePaths(bucketName, [prefix]);
34
+ }
35
+ async deletePaths(bucketName, prefixes) {
32
36
  Object.keys(this.data[bucketName] || {}).forEach(filePath => {
33
- if (filePath.startsWith(prefix)) {
37
+ if (prefixes.some(prefix => filePath.startsWith(prefix))) {
34
38
  delete this.data[bucketName][filePath];
35
39
  }
36
40
  });
@@ -65,6 +69,10 @@ class InMemoryCommonStorage {
65
69
  getFileWriteStream(_bucketName, _filePath) {
66
70
  throw new Error('Method not implemented.');
67
71
  }
72
+ async uploadFile(localFilePath, bucketName, bucketFilePath) {
73
+ this.data[bucketName] ||= {};
74
+ this.data[bucketName][bucketFilePath] = await nodejs_lib_1.fs2.readBufferAsync(localFilePath);
75
+ }
68
76
  async setFileVisibility(bucketName, filePath, isPublic) {
69
77
  this.publicMap[bucketName] ||= {};
70
78
  this.publicMap[bucketName][filePath] = isPublic;
@@ -96,5 +104,20 @@ class InMemoryCommonStorage {
96
104
  delete this.data[fromBucket][filePath];
97
105
  });
98
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
+ }
99
122
  }
100
123
  exports.InMemoryCommonStorage = InMemoryCommonStorage;
package/package.json CHANGED
@@ -35,7 +35,7 @@
35
35
  "engines": {
36
36
  "node": ">=18.12.0"
37
37
  },
38
- "version": "1.7.0",
38
+ "version": "1.9.0",
39
39
  "description": "CommonStorage implementation based on Google Cloud Storage",
40
40
  "author": "Natural Cycles Team",
41
41
  "license": "MIT"
@@ -5,6 +5,7 @@ import {
5
5
  _substringAfterLast,
6
6
  localTime,
7
7
  LocalTimeInput,
8
+ pMap,
8
9
  SKIP,
9
10
  } from '@naturalcycles/js-lib'
10
11
  import {
@@ -75,10 +76,18 @@ export class CloudStorage implements CommonStorage {
75
76
  }
76
77
 
77
78
  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,
79
+ await this.deletePaths(bucketName, [prefix])
80
+ }
81
+
82
+ async deletePaths(bucketName: string, prefixes: string[]): Promise<void> {
83
+ const bucket = this.storage.bucket(bucketName)
84
+
85
+ await pMap(prefixes, async prefix => {
86
+ await bucket.deleteFiles({
87
+ prefix,
88
+ // to keep going in case error occurs, similar to THROW_AGGREGATED
89
+ force: true,
90
+ })
82
91
  })
83
92
  }
84
93
 
@@ -163,6 +172,16 @@ export class CloudStorage implements CommonStorage {
163
172
  return this.storage.bucket(bucketName).file(filePath).createWriteStream()
164
173
  }
165
174
 
175
+ async uploadFile(
176
+ localFilePath: string,
177
+ bucketName: string,
178
+ bucketFilePath: string,
179
+ ): Promise<void> {
180
+ await this.storage.bucket(bucketName).upload(localFilePath, {
181
+ destination: bucketFilePath,
182
+ })
183
+ }
184
+
166
185
  async setFileVisibility(bucketName: string, filePath: string, isPublic: boolean): Promise<void> {
167
186
  await this.storage.bucket(bucketName).file(filePath)[isPublic ? 'makePublic' : 'makePrivate']()
168
187
  }
@@ -217,6 +236,23 @@ export class CloudStorage implements CommonStorage {
217
236
  ])
218
237
  }
219
238
 
239
+ async combine(
240
+ bucketName: string,
241
+ filePaths: string[],
242
+ toPath: string,
243
+ toBucket?: string,
244
+ ): Promise<void> {
245
+ // todo: if (filePaths.length > 32) - use recursive algorithm
246
+ _assert(filePaths.length <= 32, 'combine supports up to 32 input files')
247
+
248
+ await this.storage
249
+ .bucket(bucketName)
250
+ .combine(filePaths, this.storage.bucket(toBucket || bucketName).file(toPath))
251
+
252
+ // Delete original files
253
+ await this.deletePaths(bucketName, filePaths)
254
+ }
255
+
220
256
  /**
221
257
  * Acquires a "signed url", which allows bearer to use it to download ('read') the file.
222
258
  *
@@ -234,6 +270,7 @@ export class CloudStorage implements CommonStorage {
234
270
  .file(filePath)
235
271
  .getSignedUrl({
236
272
  action: 'read',
273
+ version: 'v4',
237
274
  expires: localTime(expires).unixMillis(),
238
275
  })
239
276
 
@@ -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.
@@ -87,6 +90,11 @@ export interface CommonStorage {
87
90
 
88
91
  getFileWriteStream: (bucketName: string, filePath: string) => Writable
89
92
 
93
+ /**
94
+ * Upload local file to the bucket (by streaming it).
95
+ */
96
+ uploadFile: (localFilePath: string, bucketName: string, bucketFilePath: string) => Promise<void>
97
+
90
98
  setFileVisibility: (bucketName: string, filePath: string, isPublic: boolean) => Promise<void>
91
99
 
92
100
  getFileVisibility: (bucketName: string, filePath: string) => Promise<boolean>
@@ -117,4 +125,26 @@ export interface CommonStorage {
117
125
  toPrefix: string,
118
126
  toBucket?: string,
119
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>
120
150
  }
@@ -1,6 +1,14 @@
1
1
  import { Readable, Writable } from 'node:stream'
2
- import { _stringMapEntries, _substringAfterLast, StringMap } from '@naturalcycles/js-lib'
3
- import { 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
  })
@@ -83,6 +95,15 @@ export class InMemoryCommonStorage implements CommonStorage {
83
95
  throw new Error('Method not implemented.')
84
96
  }
85
97
 
98
+ async uploadFile(
99
+ localFilePath: string,
100
+ bucketName: string,
101
+ bucketFilePath: string,
102
+ ): Promise<void> {
103
+ this.data[bucketName] ||= {}
104
+ this.data[bucketName]![bucketFilePath] = await fs2.readBufferAsync(localFilePath)
105
+ }
106
+
86
107
  async setFileVisibility(bucketName: string, filePath: string, isPublic: boolean): Promise<void> {
87
108
  this.publicMap[bucketName] ||= {}
88
109
  this.publicMap[bucketName]![filePath] = isPublic
@@ -133,4 +154,32 @@ export class InMemoryCommonStorage implements CommonStorage {
133
154
  delete this.data[fromBucket]![filePath]
134
155
  })
135
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
+ }
136
185
  }