@naturalcycles/cloud-storage-lib 1.6.4 → 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.
- package/dist/cloudStorage.d.ts +20 -0
- package/dist/cloudStorage.js +58 -4
- package/dist/commonStorage.d.ts +7 -0
- package/dist/inMemoryCommonStorage.d.ts +1 -0
- package/dist/inMemoryCommonStorage.js +11 -0
- package/package.json +4 -4
- package/src/cloudStorage.ts +86 -10
- package/src/commonStorage.ts +14 -0
- package/src/inMemoryCommonStorage.ts +18 -1
package/dist/cloudStorage.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/cloudStorage.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
|
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;
|
package/dist/commonStorage.d.ts
CHANGED
|
@@ -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,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naturalcycles/cloud-storage-lib",
|
|
3
3
|
"scripts": {
|
|
4
|
-
"prepare": "husky
|
|
4
|
+
"prepare": "husky"
|
|
5
5
|
},
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@google-cloud/storage": "^7.0.0",
|
|
8
|
-
"@naturalcycles/db-lib": "^
|
|
8
|
+
"@naturalcycles/db-lib": "^9.1.0",
|
|
9
9
|
"@naturalcycles/js-lib": "^14.41.0",
|
|
10
10
|
"@naturalcycles/nodejs-lib": "^13.1.0"
|
|
11
11
|
},
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"engines": {
|
|
36
36
|
"node": ">=18.12.0"
|
|
37
37
|
},
|
|
38
|
-
"version": "1.
|
|
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
|
}
|
package/src/cloudStorage.ts
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import { Readable, Writable } from 'node:stream'
|
|
2
2
|
import { File, Storage, StorageOptions } from '@google-cloud/storage'
|
|
3
|
-
import {
|
|
4
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|
package/src/commonStorage.ts
CHANGED
|
@@ -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
|
}
|