@naturalcycles/cloud-storage-lib 1.6.5 → 1.7.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.
@@ -2,6 +2,7 @@
2
2
  /// <reference types="node" />
3
3
  import { Readable, Writable } from 'node:stream';
4
4
  import { Storage, StorageOptions } from '@google-cloud/storage';
5
+ import { LocalTimeInput } from '@naturalcycles/js-lib';
5
6
  import { ReadableTyped } from '@naturalcycles/nodejs-lib';
6
7
  import { CommonStorage, CommonStorageGetOptions, FileEntry } from './commonStorage';
7
8
  import { GCPServiceAccount } from './model';
@@ -21,6 +22,11 @@ export interface CloudStorageCfg {
21
22
  */
22
23
  credentials?: GCPServiceAccount;
23
24
  }
25
+ /**
26
+ * CloudStorage implementation of CommonStorage API.
27
+ *
28
+ * API: https://googleapis.dev/nodejs/storage/latest/index.html
29
+ */
24
30
  export declare class CloudStorage implements CommonStorage {
25
31
  storage: Storage;
26
32
  /**
@@ -48,4 +54,18 @@ export declare class CloudStorage implements CommonStorage {
48
54
  getFileVisibility(bucketName: string, filePath: string): Promise<boolean>;
49
55
  copyFile(fromBucket: string, fromPath: string, toPath: string, toBucket?: string): Promise<void>;
50
56
  moveFile(fromBucket: string, fromPath: string, toPath: string, toBucket?: string): Promise<void>;
57
+ movePath(fromBucket: string, fromPrefix: string, toPrefix: string, toBucket?: string): Promise<void>;
58
+ /**
59
+ * Acquires a "signed url", which allows bearer to use it to download ('read') the file.
60
+ *
61
+ * expires: 'v4' supports maximum duration of 7 days from now.
62
+ *
63
+ * @experimental - not tested yet
64
+ */
65
+ getSignedUrl(bucketName: string, filePath: string, expires: LocalTimeInput): Promise<string>;
66
+ /**
67
+ * Returns SKIP if fileName is a folder.
68
+ * If !fullPaths - strip away the folder prefix.
69
+ */
70
+ private normalizeFilename;
51
71
  }
@@ -5,6 +5,11 @@ 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
7
  const nodejs_lib_1 = require("@naturalcycles/nodejs-lib");
8
+ /**
9
+ * CloudStorage implementation of CommonStorage API.
10
+ *
11
+ * API: https://googleapis.dev/nodejs/storage/latest/index.html
12
+ */
8
13
  class CloudStorage {
9
14
  /**
10
15
  * Passing the pre-created Storage allows to instantiate it from both
@@ -49,9 +54,11 @@ class CloudStorage {
49
54
  prefix,
50
55
  });
51
56
  if (fullPaths) {
52
- return files.map(f => f.name);
57
+ // Paths that end with `/` are "folders", which are "virtual" in CloudStorage
58
+ // It doesn't make sense to return or do anything with them
59
+ return files.map(f => f.name).filter(s => !s.endsWith('/'));
53
60
  }
54
- return files.map(f => (0, js_lib_1._substringAfterLast)(f.name, '/'));
61
+ return files.map(f => (0, js_lib_1._substringAfterLast)(f.name, '/')).filter(Boolean);
55
62
  }
56
63
  getFileNamesStream(bucketName, opt = {}) {
57
64
  const { prefix, fullPaths = true } = opt;
@@ -61,7 +68,7 @@ class CloudStorage {
61
68
  prefix,
62
69
  maxResults: opt.limit || undefined,
63
70
  })
64
- .pipe((0, nodejs_lib_1.transformMapSimple)(f => fullPaths ? f.name : (0, js_lib_1._substringAfterLast)(f.name, '/')));
71
+ .pipe((0, nodejs_lib_1.transformMapSync)(f => this.normalizeFilename(f.name, fullPaths)));
65
72
  }
66
73
  getFilesStream(bucketName, opt = {}) {
67
74
  const { prefix, fullPaths = true } = opt;
@@ -72,8 +79,11 @@ class CloudStorage {
72
79
  maxResults: opt.limit || undefined,
73
80
  })
74
81
  .pipe((0, nodejs_lib_1.transformMap)(async (f) => {
82
+ const filePath = this.normalizeFilename(f.name, fullPaths);
83
+ if (filePath === js_lib_1.SKIP)
84
+ return js_lib_1.SKIP;
75
85
  const [content] = await f.download();
76
- return { filePath: fullPaths ? f.name : (0, js_lib_1._substringAfterLast)(f.name, '/'), content };
86
+ return { filePath, content };
77
87
  }));
78
88
  }
79
89
  async getFile(bucketName, filePath) {
@@ -120,5 +130,49 @@ class CloudStorage {
120
130
  .file(fromPath)
121
131
  .move(this.storage.bucket(toBucket || fromBucket).file(toPath));
122
132
  }
133
+ async movePath(fromBucket, fromPrefix, toPrefix, toBucket) {
134
+ (0, js_lib_1._assert)(fromPrefix.endsWith('/'), 'fromPrefix should end with `/`');
135
+ (0, js_lib_1._assert)(toPrefix.endsWith('/'), 'toPrefix should end with `/`');
136
+ await (0, nodejs_lib_1._pipeline)([
137
+ this.storage.bucket(fromBucket).getFilesStream({
138
+ prefix: fromPrefix,
139
+ }),
140
+ (0, nodejs_lib_1.writableForEach)(async (file) => {
141
+ const { name } = file;
142
+ const newName = toPrefix + name.slice(fromPrefix.length);
143
+ await file.move(this.storage.bucket(toBucket || fromBucket).file(newName));
144
+ }),
145
+ ]);
146
+ }
147
+ /**
148
+ * Acquires a "signed url", which allows bearer to use it to download ('read') the file.
149
+ *
150
+ * expires: 'v4' supports maximum duration of 7 days from now.
151
+ *
152
+ * @experimental - not tested yet
153
+ */
154
+ async getSignedUrl(bucketName, filePath, expires) {
155
+ const [url] = await this.storage
156
+ .bucket(bucketName)
157
+ .file(filePath)
158
+ .getSignedUrl({
159
+ action: 'read',
160
+ expires: (0, js_lib_1.localTime)(expires).unixMillis(),
161
+ });
162
+ return url;
163
+ }
164
+ /**
165
+ * Returns SKIP if fileName is a folder.
166
+ * If !fullPaths - strip away the folder prefix.
167
+ */
168
+ normalizeFilename(fileName, fullPaths) {
169
+ if (fullPaths) {
170
+ if (fileName.endsWith('/'))
171
+ return js_lib_1.SKIP; // skip folders
172
+ return fileName;
173
+ }
174
+ fileName = (0, js_lib_1._substringAfterLast)(fileName, '/');
175
+ return fileName || js_lib_1.SKIP; // skip folders
176
+ }
123
177
  }
124
178
  exports.CloudStorage = CloudStorage;
@@ -74,4 +74,11 @@ export interface CommonStorage {
74
74
  getFileVisibility: (bucketName: string, filePath: string) => Promise<boolean>;
75
75
  copyFile: (fromBucket: string, fromPath: string, toPath: string, toBucket?: string) => Promise<void>;
76
76
  moveFile: (fromBucket: string, fromPath: string, toPath: string, toBucket?: string) => Promise<void>;
77
+ /**
78
+ * Allows to move "directory" with all its contents.
79
+ *
80
+ * Prefixes should end with `/` to work properly,
81
+ * otherwise some folder that starts with the same prefix will be included.
82
+ */
83
+ movePath: (fromBucket: string, fromPrefix: string, toPrefix: string, toBucket?: string) => Promise<void>;
77
84
  }
@@ -26,4 +26,5 @@ export declare class InMemoryCommonStorage implements CommonStorage {
26
26
  getFileVisibility(bucketName: string, filePath: string): Promise<boolean>;
27
27
  copyFile(fromBucket: string, fromPath: string, toPath: string, toBucket?: string): Promise<void>;
28
28
  moveFile(fromBucket: string, fromPath: string, toPath: string, toBucket?: string): Promise<void>;
29
+ movePath(fromBucket: string, fromPrefix: string, toPrefix: string, toBucket?: string): Promise<void>;
29
30
  }
@@ -85,5 +85,16 @@ class InMemoryCommonStorage {
85
85
  this.data[tob][toPath] = this.data[fromBucket][fromPath];
86
86
  delete this.data[fromBucket][fromPath];
87
87
  }
88
+ async movePath(fromBucket, fromPrefix, toPrefix, toBucket) {
89
+ const tob = toBucket || fromBucket;
90
+ this.data[fromBucket] ||= {};
91
+ this.data[tob] ||= {};
92
+ (0, js_lib_1._stringMapEntries)(this.data[fromBucket]).forEach(([filePath, v]) => {
93
+ if (!filePath.startsWith(fromPrefix))
94
+ return;
95
+ this.data[tob][toPrefix + filePath.slice(fromPrefix.length)] = v;
96
+ delete this.data[fromBucket][filePath];
97
+ });
98
+ }
88
99
  }
89
100
  exports.InMemoryCommonStorage = InMemoryCommonStorage;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/cloud-storage-lib",
3
3
  "scripts": {
4
- "prepare": "husky install"
4
+ "prepare": "husky"
5
5
  },
6
6
  "dependencies": {
7
7
  "@google-cloud/storage": "^7.0.0",
@@ -35,8 +35,8 @@
35
35
  "engines": {
36
36
  "node": ">=18.12.0"
37
37
  },
38
- "version": "1.6.5",
39
- "description": "",
38
+ "version": "1.7.0",
39
+ "description": "CommonStorage implementation based on Google Cloud Storage",
40
40
  "author": "Natural Cycles Team",
41
41
  "license": "MIT"
42
42
  }
@@ -1,7 +1,19 @@
1
1
  import { Readable, Writable } from 'node:stream'
2
2
  import { File, Storage, StorageOptions } from '@google-cloud/storage'
3
- import { _substringAfterLast } from '@naturalcycles/js-lib'
4
- import { ReadableTyped, transformMap, transformMapSimple } from '@naturalcycles/nodejs-lib'
3
+ import {
4
+ _assert,
5
+ _substringAfterLast,
6
+ localTime,
7
+ LocalTimeInput,
8
+ SKIP,
9
+ } from '@naturalcycles/js-lib'
10
+ import {
11
+ _pipeline,
12
+ ReadableTyped,
13
+ transformMap,
14
+ transformMapSync,
15
+ writableForEach,
16
+ } from '@naturalcycles/nodejs-lib'
5
17
  import { CommonStorage, CommonStorageGetOptions, FileEntry } from './commonStorage'
6
18
  import { GCPServiceAccount } from './model'
7
19
 
@@ -27,6 +39,11 @@ export interface CloudStorageCfg {
27
39
  credentials?: GCPServiceAccount
28
40
  }
29
41
 
42
+ /**
43
+ * CloudStorage implementation of CommonStorage API.
44
+ *
45
+ * API: https://googleapis.dev/nodejs/storage/latest/index.html
46
+ */
30
47
  export class CloudStorage implements CommonStorage {
31
48
  /**
32
49
  * Passing the pre-created Storage allows to instantiate it from both
@@ -77,10 +94,12 @@ export class CloudStorage implements CommonStorage {
77
94
  })
78
95
 
79
96
  if (fullPaths) {
80
- return files.map(f => f.name)
97
+ // Paths that end with `/` are "folders", which are "virtual" in CloudStorage
98
+ // It doesn't make sense to return or do anything with them
99
+ return files.map(f => f.name).filter(s => !s.endsWith('/'))
81
100
  }
82
101
 
83
- return files.map(f => _substringAfterLast(f.name, '/'))
102
+ return files.map(f => _substringAfterLast(f.name, '/')).filter(Boolean)
84
103
  }
85
104
 
86
105
  getFileNamesStream(bucketName: string, opt: CommonStorageGetOptions = {}): ReadableTyped<string> {
@@ -92,11 +111,7 @@ export class CloudStorage implements CommonStorage {
92
111
  prefix,
93
112
  maxResults: opt.limit || undefined,
94
113
  })
95
- .pipe(
96
- transformMapSimple<File, string>(f =>
97
- fullPaths ? f.name : _substringAfterLast(f.name, '/'),
98
- ),
99
- )
114
+ .pipe(transformMapSync<File, string>(f => this.normalizeFilename(f.name, fullPaths)))
100
115
  }
101
116
 
102
117
  getFilesStream(bucketName: string, opt: CommonStorageGetOptions = {}): ReadableTyped<FileEntry> {
@@ -110,8 +125,11 @@ export class CloudStorage implements CommonStorage {
110
125
  })
111
126
  .pipe(
112
127
  transformMap<File, FileEntry>(async f => {
128
+ const filePath = this.normalizeFilename(f.name, fullPaths)
129
+ if (filePath === SKIP) return SKIP
130
+
113
131
  const [content] = await f.download()
114
- return { filePath: fullPaths ? f.name : _substringAfterLast(f.name, '/'), content }
132
+ return { filePath, content }
115
133
  }),
116
134
  )
117
135
  }
@@ -177,4 +195,62 @@ export class CloudStorage implements CommonStorage {
177
195
  .file(fromPath)
178
196
  .move(this.storage.bucket(toBucket || fromBucket).file(toPath))
179
197
  }
198
+
199
+ async movePath(
200
+ fromBucket: string,
201
+ fromPrefix: string,
202
+ toPrefix: string,
203
+ toBucket?: string,
204
+ ): Promise<void> {
205
+ _assert(fromPrefix.endsWith('/'), 'fromPrefix should end with `/`')
206
+ _assert(toPrefix.endsWith('/'), 'toPrefix should end with `/`')
207
+
208
+ await _pipeline([
209
+ this.storage.bucket(fromBucket).getFilesStream({
210
+ prefix: fromPrefix,
211
+ }),
212
+ writableForEach<File>(async file => {
213
+ const { name } = file
214
+ const newName = toPrefix + name.slice(fromPrefix.length)
215
+ await file.move(this.storage.bucket(toBucket || fromBucket).file(newName))
216
+ }),
217
+ ])
218
+ }
219
+
220
+ /**
221
+ * Acquires a "signed url", which allows bearer to use it to download ('read') the file.
222
+ *
223
+ * expires: 'v4' supports maximum duration of 7 days from now.
224
+ *
225
+ * @experimental - not tested yet
226
+ */
227
+ async getSignedUrl(
228
+ bucketName: string,
229
+ filePath: string,
230
+ expires: LocalTimeInput,
231
+ ): Promise<string> {
232
+ const [url] = await this.storage
233
+ .bucket(bucketName)
234
+ .file(filePath)
235
+ .getSignedUrl({
236
+ action: 'read',
237
+ expires: localTime(expires).unixMillis(),
238
+ })
239
+
240
+ return url
241
+ }
242
+
243
+ /**
244
+ * Returns SKIP if fileName is a folder.
245
+ * If !fullPaths - strip away the folder prefix.
246
+ */
247
+ private normalizeFilename(fileName: string, fullPaths: boolean): string | typeof SKIP {
248
+ if (fullPaths) {
249
+ if (fileName.endsWith('/')) return SKIP // skip folders
250
+ return fileName
251
+ }
252
+
253
+ fileName = _substringAfterLast(fileName, '/')
254
+ return fileName || SKIP // skip folders
255
+ }
180
256
  }
@@ -97,10 +97,24 @@ export interface CommonStorage {
97
97
  toPath: string,
98
98
  toBucket?: string,
99
99
  ) => Promise<void>
100
+
100
101
  moveFile: (
101
102
  fromBucket: string,
102
103
  fromPath: string,
103
104
  toPath: string,
104
105
  toBucket?: string,
105
106
  ) => Promise<void>
107
+
108
+ /**
109
+ * Allows to move "directory" with all its contents.
110
+ *
111
+ * Prefixes should end with `/` to work properly,
112
+ * otherwise some folder that starts with the same prefix will be included.
113
+ */
114
+ movePath: (
115
+ fromBucket: string,
116
+ fromPrefix: string,
117
+ toPrefix: string,
118
+ toBucket?: string,
119
+ ) => Promise<void>
106
120
  }
@@ -1,5 +1,5 @@
1
1
  import { Readable, Writable } from 'node:stream'
2
- import { _substringAfterLast, StringMap } from '@naturalcycles/js-lib'
2
+ import { _stringMapEntries, _substringAfterLast, StringMap } from '@naturalcycles/js-lib'
3
3
  import { ReadableTyped } from '@naturalcycles/nodejs-lib'
4
4
  import { CommonStorage, CommonStorageGetOptions, FileEntry } from './commonStorage'
5
5
 
@@ -116,4 +116,21 @@ export class InMemoryCommonStorage implements CommonStorage {
116
116
  this.data[tob]![toPath] = this.data[fromBucket]![fromPath]
117
117
  delete this.data[fromBucket]![fromPath]
118
118
  }
119
+
120
+ async movePath(
121
+ fromBucket: string,
122
+ fromPrefix: string,
123
+ toPrefix: string,
124
+ toBucket?: string,
125
+ ): Promise<void> {
126
+ const tob = toBucket || fromBucket
127
+ this.data[fromBucket] ||= {}
128
+ this.data[tob] ||= {}
129
+
130
+ _stringMapEntries(this.data[fromBucket]!).forEach(([filePath, v]) => {
131
+ if (!filePath.startsWith(fromPrefix)) return
132
+ this.data[tob]![toPrefix + filePath.slice(fromPrefix.length)] = v
133
+ delete this.data[fromBucket]![filePath]
134
+ })
135
+ }
119
136
  }