@naturalcycles/cloud-storage-lib 1.6.5 → 1.8.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
  /**
@@ -44,8 +50,23 @@ export declare class CloudStorage implements CommonStorage {
44
50
  getFileReadStream(bucketName: string, filePath: string): Readable;
45
51
  saveFile(bucketName: string, filePath: string, content: Buffer): Promise<void>;
46
52
  getFileWriteStream(bucketName: string, filePath: string): Writable;
53
+ uploadFile(localFilePath: string, bucketName: string, bucketFilePath: string): Promise<void>;
47
54
  setFileVisibility(bucketName: string, filePath: string, isPublic: boolean): Promise<void>;
48
55
  getFileVisibility(bucketName: string, filePath: string): Promise<boolean>;
49
56
  copyFile(fromBucket: string, fromPath: string, toPath: string, toBucket?: string): Promise<void>;
50
57
  moveFile(fromBucket: string, fromPath: string, toPath: string, toBucket?: string): Promise<void>;
58
+ movePath(fromBucket: string, fromPrefix: string, toPrefix: string, toBucket?: string): Promise<void>;
59
+ /**
60
+ * Acquires a "signed url", which allows bearer to use it to download ('read') the file.
61
+ *
62
+ * expires: 'v4' supports maximum duration of 7 days from now.
63
+ *
64
+ * @experimental - not tested yet
65
+ */
66
+ getSignedUrl(bucketName: string, filePath: string, expires: LocalTimeInput): Promise<string>;
67
+ /**
68
+ * Returns SKIP if fileName is a folder.
69
+ * If !fullPaths - strip away the folder prefix.
70
+ */
71
+ private normalizeFilename;
51
72
  }
@@ -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) {
@@ -101,6 +111,11 @@ class CloudStorage {
101
111
  getFileWriteStream(bucketName, filePath) {
102
112
  return this.storage.bucket(bucketName).file(filePath).createWriteStream();
103
113
  }
114
+ async uploadFile(localFilePath, bucketName, bucketFilePath) {
115
+ await this.storage.bucket(bucketName).upload(localFilePath, {
116
+ destination: bucketFilePath,
117
+ });
118
+ }
104
119
  async setFileVisibility(bucketName, filePath, isPublic) {
105
120
  await this.storage.bucket(bucketName).file(filePath)[isPublic ? 'makePublic' : 'makePrivate']();
106
121
  }
@@ -120,5 +135,49 @@ class CloudStorage {
120
135
  .file(fromPath)
121
136
  .move(this.storage.bucket(toBucket || fromBucket).file(toPath));
122
137
  }
138
+ async movePath(fromBucket, fromPrefix, toPrefix, toBucket) {
139
+ (0, js_lib_1._assert)(fromPrefix.endsWith('/'), 'fromPrefix should end with `/`');
140
+ (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
+ ]);
151
+ }
152
+ /**
153
+ * Acquires a "signed url", which allows bearer to use it to download ('read') the file.
154
+ *
155
+ * expires: 'v4' supports maximum duration of 7 days from now.
156
+ *
157
+ * @experimental - not tested yet
158
+ */
159
+ async getSignedUrl(bucketName, filePath, expires) {
160
+ const [url] = await this.storage
161
+ .bucket(bucketName)
162
+ .file(filePath)
163
+ .getSignedUrl({
164
+ action: 'read',
165
+ expires: (0, js_lib_1.localTime)(expires).unixMillis(),
166
+ });
167
+ return url;
168
+ }
169
+ /**
170
+ * Returns SKIP if fileName is a folder.
171
+ * If !fullPaths - strip away the folder prefix.
172
+ */
173
+ normalizeFilename(fileName, fullPaths) {
174
+ if (fullPaths) {
175
+ if (fileName.endsWith('/'))
176
+ return js_lib_1.SKIP; // skip folders
177
+ return fileName;
178
+ }
179
+ fileName = (0, js_lib_1._substringAfterLast)(fileName, '/');
180
+ return fileName || js_lib_1.SKIP; // skip folders
181
+ }
123
182
  }
124
183
  exports.CloudStorage = CloudStorage;
@@ -70,8 +70,19 @@ export interface CommonStorage {
70
70
  getFilesStream: (bucketName: string, opt?: CommonStorageGetOptions) => ReadableTyped<FileEntry>;
71
71
  getFileReadStream: (bucketName: string, filePath: string) => Readable;
72
72
  getFileWriteStream: (bucketName: string, filePath: string) => Writable;
73
+ /**
74
+ * Upload local file to the bucket (by streaming it).
75
+ */
76
+ uploadFile: (localFilePath: string, bucketName: string, bucketFilePath: string) => Promise<void>;
73
77
  setFileVisibility: (bucketName: string, filePath: string, isPublic: boolean) => Promise<void>;
74
78
  getFileVisibility: (bucketName: string, filePath: string) => Promise<boolean>;
75
79
  copyFile: (fromBucket: string, fromPath: string, toPath: string, toBucket?: string) => Promise<void>;
76
80
  moveFile: (fromBucket: string, fromPath: string, toPath: string, toBucket?: string) => Promise<void>;
81
+ /**
82
+ * Allows to move "directory" with all its contents.
83
+ *
84
+ * Prefixes should end with `/` to work properly,
85
+ * otherwise some folder that starts with the same prefix will be included.
86
+ */
87
+ movePath: (fromBucket: string, fromPrefix: string, toPrefix: string, toBucket?: string) => Promise<void>;
77
88
  }
@@ -22,8 +22,10 @@ export declare class InMemoryCommonStorage implements CommonStorage {
22
22
  getFilesStream(bucketName: string, opt?: CommonStorageGetOptions): ReadableTyped<FileEntry>;
23
23
  getFileReadStream(bucketName: string, filePath: string): Readable;
24
24
  getFileWriteStream(_bucketName: string, _filePath: string): Writable;
25
+ uploadFile(localFilePath: string, bucketName: string, bucketFilePath: string): Promise<void>;
25
26
  setFileVisibility(bucketName: string, filePath: string, isPublic: boolean): Promise<void>;
26
27
  getFileVisibility(bucketName: string, filePath: string): Promise<boolean>;
27
28
  copyFile(fromBucket: string, fromPath: string, toPath: string, toBucket?: string): Promise<void>;
28
29
  moveFile(fromBucket: string, fromPath: string, toPath: string, toBucket?: string): Promise<void>;
30
+ movePath(fromBucket: string, fromPrefix: string, toPrefix: string, toBucket?: string): Promise<void>;
29
31
  }
@@ -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
  /**
@@ -65,6 +66,10 @@ class InMemoryCommonStorage {
65
66
  getFileWriteStream(_bucketName, _filePath) {
66
67
  throw new Error('Method not implemented.');
67
68
  }
69
+ async uploadFile(localFilePath, bucketName, bucketFilePath) {
70
+ this.data[bucketName] ||= {};
71
+ this.data[bucketName][bucketFilePath] = await nodejs_lib_1.fs2.readBufferAsync(localFilePath);
72
+ }
68
73
  async setFileVisibility(bucketName, filePath, isPublic) {
69
74
  this.publicMap[bucketName] ||= {};
70
75
  this.publicMap[bucketName][filePath] = isPublic;
@@ -85,5 +90,16 @@ class InMemoryCommonStorage {
85
90
  this.data[tob][toPath] = this.data[fromBucket][fromPath];
86
91
  delete this.data[fromBucket][fromPath];
87
92
  }
93
+ async movePath(fromBucket, fromPrefix, toPrefix, toBucket) {
94
+ const tob = toBucket || fromBucket;
95
+ this.data[fromBucket] ||= {};
96
+ this.data[tob] ||= {};
97
+ (0, js_lib_1._stringMapEntries)(this.data[fromBucket]).forEach(([filePath, v]) => {
98
+ if (!filePath.startsWith(fromPrefix))
99
+ return;
100
+ this.data[tob][toPrefix + filePath.slice(fromPrefix.length)] = v;
101
+ delete this.data[fromBucket][filePath];
102
+ });
103
+ }
88
104
  }
89
105
  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.8.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
  }
@@ -145,6 +163,16 @@ export class CloudStorage implements CommonStorage {
145
163
  return this.storage.bucket(bucketName).file(filePath).createWriteStream()
146
164
  }
147
165
 
166
+ async uploadFile(
167
+ localFilePath: string,
168
+ bucketName: string,
169
+ bucketFilePath: string,
170
+ ): Promise<void> {
171
+ await this.storage.bucket(bucketName).upload(localFilePath, {
172
+ destination: bucketFilePath,
173
+ })
174
+ }
175
+
148
176
  async setFileVisibility(bucketName: string, filePath: string, isPublic: boolean): Promise<void> {
149
177
  await this.storage.bucket(bucketName).file(filePath)[isPublic ? 'makePublic' : 'makePrivate']()
150
178
  }
@@ -177,4 +205,62 @@ export class CloudStorage implements CommonStorage {
177
205
  .file(fromPath)
178
206
  .move(this.storage.bucket(toBucket || fromBucket).file(toPath))
179
207
  }
208
+
209
+ async movePath(
210
+ fromBucket: string,
211
+ fromPrefix: string,
212
+ toPrefix: string,
213
+ toBucket?: string,
214
+ ): Promise<void> {
215
+ _assert(fromPrefix.endsWith('/'), 'fromPrefix should end with `/`')
216
+ _assert(toPrefix.endsWith('/'), 'toPrefix should end with `/`')
217
+
218
+ await _pipeline([
219
+ this.storage.bucket(fromBucket).getFilesStream({
220
+ prefix: fromPrefix,
221
+ }),
222
+ writableForEach<File>(async file => {
223
+ const { name } = file
224
+ const newName = toPrefix + name.slice(fromPrefix.length)
225
+ await file.move(this.storage.bucket(toBucket || fromBucket).file(newName))
226
+ }),
227
+ ])
228
+ }
229
+
230
+ /**
231
+ * Acquires a "signed url", which allows bearer to use it to download ('read') the file.
232
+ *
233
+ * expires: 'v4' supports maximum duration of 7 days from now.
234
+ *
235
+ * @experimental - not tested yet
236
+ */
237
+ async getSignedUrl(
238
+ bucketName: string,
239
+ filePath: string,
240
+ expires: LocalTimeInput,
241
+ ): Promise<string> {
242
+ const [url] = await this.storage
243
+ .bucket(bucketName)
244
+ .file(filePath)
245
+ .getSignedUrl({
246
+ action: 'read',
247
+ expires: localTime(expires).unixMillis(),
248
+ })
249
+
250
+ return url
251
+ }
252
+
253
+ /**
254
+ * Returns SKIP if fileName is a folder.
255
+ * If !fullPaths - strip away the folder prefix.
256
+ */
257
+ private normalizeFilename(fileName: string, fullPaths: boolean): string | typeof SKIP {
258
+ if (fullPaths) {
259
+ if (fileName.endsWith('/')) return SKIP // skip folders
260
+ return fileName
261
+ }
262
+
263
+ fileName = _substringAfterLast(fileName, '/')
264
+ return fileName || SKIP // skip folders
265
+ }
180
266
  }
@@ -87,6 +87,11 @@ export interface CommonStorage {
87
87
 
88
88
  getFileWriteStream: (bucketName: string, filePath: string) => Writable
89
89
 
90
+ /**
91
+ * Upload local file to the bucket (by streaming it).
92
+ */
93
+ uploadFile: (localFilePath: string, bucketName: string, bucketFilePath: string) => Promise<void>
94
+
90
95
  setFileVisibility: (bucketName: string, filePath: string, isPublic: boolean) => Promise<void>
91
96
 
92
97
  getFileVisibility: (bucketName: string, filePath: string) => Promise<boolean>
@@ -97,10 +102,24 @@ export interface CommonStorage {
97
102
  toPath: string,
98
103
  toBucket?: string,
99
104
  ) => Promise<void>
105
+
100
106
  moveFile: (
101
107
  fromBucket: string,
102
108
  fromPath: string,
103
109
  toPath: string,
104
110
  toBucket?: string,
105
111
  ) => Promise<void>
112
+
113
+ /**
114
+ * Allows to move "directory" with all its contents.
115
+ *
116
+ * Prefixes should end with `/` to work properly,
117
+ * otherwise some folder that starts with the same prefix will be included.
118
+ */
119
+ movePath: (
120
+ fromBucket: string,
121
+ fromPrefix: string,
122
+ toPrefix: string,
123
+ toBucket?: string,
124
+ ) => Promise<void>
106
125
  }
@@ -1,6 +1,6 @@
1
1
  import { Readable, Writable } from 'node:stream'
2
- import { _substringAfterLast, StringMap } from '@naturalcycles/js-lib'
3
- import { ReadableTyped } from '@naturalcycles/nodejs-lib'
2
+ import { _stringMapEntries, _substringAfterLast, StringMap } from '@naturalcycles/js-lib'
3
+ import { fs2, ReadableTyped } from '@naturalcycles/nodejs-lib'
4
4
  import { CommonStorage, CommonStorageGetOptions, FileEntry } from './commonStorage'
5
5
 
6
6
  export class InMemoryCommonStorage implements CommonStorage {
@@ -83,6 +83,15 @@ export class InMemoryCommonStorage implements CommonStorage {
83
83
  throw new Error('Method not implemented.')
84
84
  }
85
85
 
86
+ async uploadFile(
87
+ localFilePath: string,
88
+ bucketName: string,
89
+ bucketFilePath: string,
90
+ ): Promise<void> {
91
+ this.data[bucketName] ||= {}
92
+ this.data[bucketName]![bucketFilePath] = await fs2.readBufferAsync(localFilePath)
93
+ }
94
+
86
95
  async setFileVisibility(bucketName: string, filePath: string, isPublic: boolean): Promise<void> {
87
96
  this.publicMap[bucketName] ||= {}
88
97
  this.publicMap[bucketName]![filePath] = isPublic
@@ -116,4 +125,21 @@ export class InMemoryCommonStorage implements CommonStorage {
116
125
  this.data[tob]![toPath] = this.data[fromBucket]![fromPath]
117
126
  delete this.data[fromBucket]![fromPath]
118
127
  }
128
+
129
+ async movePath(
130
+ fromBucket: string,
131
+ fromPrefix: string,
132
+ toPrefix: string,
133
+ toBucket?: string,
134
+ ): Promise<void> {
135
+ const tob = toBucket || fromBucket
136
+ this.data[fromBucket] ||= {}
137
+ this.data[tob] ||= {}
138
+
139
+ _stringMapEntries(this.data[fromBucket]!).forEach(([filePath, v]) => {
140
+ if (!filePath.startsWith(fromPrefix)) return
141
+ this.data[tob]![toPrefix + filePath.slice(fromPrefix.length)] = v
142
+ delete this.data[fromBucket]![filePath]
143
+ })
144
+ }
119
145
  }