@naturalcycles/cloud-storage-lib 1.2.0 → 1.5.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 +15 -9
- package/dist/cloudStorage.js +28 -23
- package/dist/commonStorage.d.ts +8 -8
- package/dist/commonStorageBucket.d.ts +9 -7
- package/dist/commonStorageBucket.js +17 -22
- package/dist/commonStorageKeyValueDB.js +5 -9
- package/dist/inMemoryCommonStorage.d.ts +3 -3
- package/dist/inMemoryCommonStorage.js +18 -7
- package/dist/testing/commonStorageTest.js +25 -19
- package/package.json +3 -2
- package/src/cloudStorage.ts +41 -38
- package/src/commonStorage.ts +11 -20
- package/src/commonStorageBucket.ts +17 -27
- package/src/commonStorageKeyValueDB.ts +5 -10
- package/src/inMemoryCommonStorage.ts +20 -16
- package/src/testing/commonStorageTest.ts +31 -21
package/dist/cloudStorage.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
import { Readable, Writable } from 'stream';
|
|
3
|
-
import { Storage } from '@google-cloud/storage';
|
|
3
|
+
import { Storage, StorageOptions } from '@google-cloud/storage';
|
|
4
4
|
import { ReadableTyped } from '@naturalcycles/nodejs-lib';
|
|
5
5
|
import { CommonStorage, CommonStorageGetOptions, FileEntry } from './commonStorage';
|
|
6
6
|
import { GCPServiceAccount } from './model';
|
|
@@ -14,20 +14,26 @@ import { GCPServiceAccount } from './model';
|
|
|
14
14
|
* (either personal one or non-personal).
|
|
15
15
|
*/
|
|
16
16
|
export interface CloudStorageCfg {
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* It's optional, to allow automatic credentials in AppEngine, or GOOGLE_APPLICATION_CREDENTIALS.
|
|
19
|
+
*/
|
|
20
|
+
credentials?: GCPServiceAccount;
|
|
18
21
|
}
|
|
19
22
|
export declare class CloudStorage implements CommonStorage {
|
|
20
|
-
cfg: CloudStorageCfg;
|
|
21
|
-
constructor(cfg: CloudStorageCfg);
|
|
22
23
|
storage: Storage;
|
|
24
|
+
/**
|
|
25
|
+
* Passing the pre-created Storage allows to instantiate it from both
|
|
26
|
+
* GCP Storage and FirebaseStorage.
|
|
27
|
+
*/
|
|
28
|
+
constructor(storage: Storage);
|
|
29
|
+
static createFromGCPServiceAccount(cfg: CloudStorageCfg): CloudStorage;
|
|
30
|
+
static createFromStorageOptions(storageOptions?: StorageOptions): CloudStorage;
|
|
23
31
|
ping(bucketName?: string): Promise<void>;
|
|
24
|
-
getBucketNames(opt?: CommonStorageGetOptions): Promise<string[]>;
|
|
25
|
-
getBucketNamesStream(): ReadableTyped<string>;
|
|
26
32
|
deletePath(bucketName: string, prefix: string): Promise<void>;
|
|
27
33
|
fileExists(bucketName: string, filePath: string): Promise<boolean>;
|
|
28
|
-
getFileNames(bucketName: string,
|
|
29
|
-
getFileNamesStream(bucketName: string,
|
|
30
|
-
getFilesStream(bucketName: string,
|
|
34
|
+
getFileNames(bucketName: string, opt?: CommonStorageGetOptions): Promise<string[]>;
|
|
35
|
+
getFileNamesStream(bucketName: string, opt?: CommonStorageGetOptions): ReadableTyped<string>;
|
|
36
|
+
getFilesStream(bucketName: string, opt?: CommonStorageGetOptions): ReadableTyped<FileEntry>;
|
|
31
37
|
getFile(bucketName: string, filePath: string): Promise<Buffer | null>;
|
|
32
38
|
/**
|
|
33
39
|
* Returns a Readable that is NOT object mode,
|
package/dist/cloudStorage.js
CHANGED
|
@@ -1,37 +1,36 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.CloudStorage = void 0;
|
|
4
|
+
const js_lib_1 = require("@naturalcycles/js-lib");
|
|
4
5
|
const storage_1 = require("@google-cloud/storage");
|
|
5
6
|
const nodejs_lib_1 = require("@naturalcycles/nodejs-lib");
|
|
6
7
|
class CloudStorage {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Passing the pre-created Storage allows to instantiate it from both
|
|
10
|
+
* GCP Storage and FirebaseStorage.
|
|
11
|
+
*/
|
|
12
|
+
constructor(storage) {
|
|
13
|
+
this.storage = storage;
|
|
14
|
+
}
|
|
15
|
+
static createFromGCPServiceAccount(cfg) {
|
|
16
|
+
const storage = new storage_1.Storage({
|
|
10
17
|
credentials: cfg.credentials,
|
|
11
18
|
// Explicitly passing it here to fix this error:
|
|
12
19
|
// Error: Unable to detect a Project Id in the current environment.
|
|
13
20
|
// To learn more about authentication and Google APIs, visit:
|
|
14
21
|
// https://cloud.google.com/docs/authentication/getting-started
|
|
15
22
|
// at /root/repo/node_modules/google-auth-library/build/src/auth/googleauth.js:95:31
|
|
16
|
-
projectId: cfg.credentials
|
|
23
|
+
projectId: cfg.credentials?.project_id,
|
|
17
24
|
});
|
|
25
|
+
return new CloudStorage(storage);
|
|
26
|
+
}
|
|
27
|
+
static createFromStorageOptions(storageOptions) {
|
|
28
|
+
const storage = new storage_1.Storage(storageOptions);
|
|
29
|
+
return new CloudStorage(storage);
|
|
18
30
|
}
|
|
19
|
-
// async createBucket(bucketName: string): Promise<void> {
|
|
20
|
-
// const bucket = await this.storage.createBucket(bucketName)
|
|
21
|
-
// console.log(bucket) // debugging
|
|
22
|
-
// }
|
|
23
31
|
async ping(bucketName) {
|
|
24
32
|
await this.storage.bucket(bucketName || 'non-existing-for-sure').exists();
|
|
25
33
|
}
|
|
26
|
-
async getBucketNames(opt = {}) {
|
|
27
|
-
const [buckets] = await this.storage.getBuckets({
|
|
28
|
-
maxResults: opt.limit,
|
|
29
|
-
});
|
|
30
|
-
return buckets.map(b => b.name);
|
|
31
|
-
}
|
|
32
|
-
getBucketNamesStream() {
|
|
33
|
-
return this.storage.getBucketsStream().pipe((0, nodejs_lib_1.transformMapSimple)(b => b.name));
|
|
34
|
-
}
|
|
35
34
|
async deletePath(bucketName, prefix) {
|
|
36
35
|
await this.storage.bucket(bucketName).deleteFiles({
|
|
37
36
|
prefix,
|
|
@@ -43,22 +42,28 @@ class CloudStorage {
|
|
|
43
42
|
const [exists] = await this.storage.bucket(bucketName).file(filePath).exists();
|
|
44
43
|
return exists;
|
|
45
44
|
}
|
|
46
|
-
async getFileNames(bucketName,
|
|
45
|
+
async getFileNames(bucketName, opt = {}) {
|
|
46
|
+
const { prefix, fullPaths = true } = opt;
|
|
47
47
|
const [files] = await this.storage.bucket(bucketName).getFiles({
|
|
48
48
|
prefix,
|
|
49
49
|
});
|
|
50
|
-
|
|
50
|
+
if (fullPaths) {
|
|
51
|
+
return files.map(f => f.name);
|
|
52
|
+
}
|
|
53
|
+
return files.map(f => (0, js_lib_1._substringAfterLast)(f.name, '/'));
|
|
51
54
|
}
|
|
52
|
-
getFileNamesStream(bucketName,
|
|
55
|
+
getFileNamesStream(bucketName, opt = {}) {
|
|
56
|
+
const { prefix, fullPaths = true } = opt;
|
|
53
57
|
return this.storage
|
|
54
58
|
.bucket(bucketName)
|
|
55
59
|
.getFilesStream({
|
|
56
60
|
prefix,
|
|
57
61
|
maxResults: opt.limit,
|
|
58
62
|
})
|
|
59
|
-
.pipe((0, nodejs_lib_1.transformMapSimple)(f => f.name));
|
|
63
|
+
.pipe((0, nodejs_lib_1.transformMapSimple)(f => fullPaths ? f.name : (0, js_lib_1._substringAfterLast)(f.name, '/')));
|
|
60
64
|
}
|
|
61
|
-
getFilesStream(bucketName,
|
|
65
|
+
getFilesStream(bucketName, opt = {}) {
|
|
66
|
+
const { prefix, fullPaths = true } = opt;
|
|
62
67
|
return this.storage
|
|
63
68
|
.bucket(bucketName)
|
|
64
69
|
.getFilesStream({
|
|
@@ -67,7 +72,7 @@ class CloudStorage {
|
|
|
67
72
|
})
|
|
68
73
|
.pipe((0, nodejs_lib_1.transformMap)(async (f) => {
|
|
69
74
|
const [content] = await f.download();
|
|
70
|
-
return { filePath: f.name, content };
|
|
75
|
+
return { filePath: fullPaths ? f.name : (0, js_lib_1._substringAfterLast)(f.name, '/'), content };
|
|
71
76
|
}));
|
|
72
77
|
}
|
|
73
78
|
async getFile(bucketName, filePath) {
|
package/dist/commonStorage.d.ts
CHANGED
|
@@ -10,6 +10,11 @@ export interface CommonStorageGetOptions {
|
|
|
10
10
|
* Will filter resulting files based on `prefix`.
|
|
11
11
|
*/
|
|
12
12
|
prefix?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Defaults to true.
|
|
15
|
+
* Set to false to return file names instead of full paths.
|
|
16
|
+
*/
|
|
17
|
+
fullPaths?: boolean;
|
|
13
18
|
/**
|
|
14
19
|
* Limits the number of results.
|
|
15
20
|
*
|
|
@@ -35,11 +40,6 @@ export interface CommonStorage {
|
|
|
35
40
|
* Pass `bucketName` in case you only have permissions to operate on that bucket.
|
|
36
41
|
*/
|
|
37
42
|
ping(bucketName?: string): Promise<void>;
|
|
38
|
-
/**
|
|
39
|
-
* Often needs a special permission.
|
|
40
|
-
*/
|
|
41
|
-
getBucketNames(opt?: CommonStorageGetOptions): Promise<string[]>;
|
|
42
|
-
getBucketNamesStream(): ReadableTyped<string>;
|
|
43
43
|
/**
|
|
44
44
|
* Creates a new bucket by given name.
|
|
45
45
|
* todo: check what to do if it already exists
|
|
@@ -62,9 +62,9 @@ export interface CommonStorage {
|
|
|
62
62
|
* Important difference between `prefix` and `path` is that `prefix` will
|
|
63
63
|
* return all files from sub-directories too!
|
|
64
64
|
*/
|
|
65
|
-
getFileNames(bucketName: string,
|
|
66
|
-
getFileNamesStream(bucketName: string,
|
|
67
|
-
getFilesStream(bucketName: string,
|
|
65
|
+
getFileNames(bucketName: string, opt?: CommonStorageGetOptions): Promise<string[]>;
|
|
66
|
+
getFileNamesStream(bucketName: string, opt?: CommonStorageGetOptions): ReadableTyped<string>;
|
|
67
|
+
getFilesStream(bucketName: string, opt?: CommonStorageGetOptions): ReadableTyped<FileEntry>;
|
|
68
68
|
getFileReadStream(bucketName: string, filePath: string): Readable;
|
|
69
69
|
getFileWriteStream(bucketName: string, filePath: string): Writable;
|
|
70
70
|
setFileVisibility(bucketName: string, filePath: string, isPublic: boolean): Promise<void>;
|
|
@@ -31,6 +31,12 @@ export declare class CommonStorageBucket {
|
|
|
31
31
|
content: T;
|
|
32
32
|
}[]>;
|
|
33
33
|
saveFile(filePath: string, content: Buffer): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Convenience method that does:
|
|
36
|
+
* await saveFile
|
|
37
|
+
* await setFileVisibility
|
|
38
|
+
*/
|
|
39
|
+
savePublicFile(filePath: string, content: Buffer): Promise<void>;
|
|
34
40
|
saveStringFile(filePath: string, content: string): Promise<void>;
|
|
35
41
|
saveJsonFile(filePath: string, content: any): Promise<void>;
|
|
36
42
|
saveFiles(entries: FileEntry[]): Promise<void>;
|
|
@@ -50,13 +56,9 @@ export declare class CommonStorageBucket {
|
|
|
50
56
|
* Important difference between `prefix` and `path` is that `prefix` will
|
|
51
57
|
* return all files from sub-directories too!
|
|
52
58
|
*/
|
|
53
|
-
getFileNames(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
getFileNamesStream(prefix: string, opt?: CommonStorageGetOptions & {
|
|
57
|
-
fullPath?: boolean;
|
|
58
|
-
}): ReadableTyped<string>;
|
|
59
|
-
getFilesStream(prefix: string, opt?: CommonStorageGetOptions): ReadableTyped<FileEntry>;
|
|
59
|
+
getFileNames(opt?: CommonStorageGetOptions): Promise<string[]>;
|
|
60
|
+
getFileNamesStream(opt?: CommonStorageGetOptions): ReadableTyped<string>;
|
|
61
|
+
getFilesStream(opt?: CommonStorageGetOptions): ReadableTyped<FileEntry>;
|
|
60
62
|
getFileReadStream(filePath: string): Readable;
|
|
61
63
|
getFileWriteStream(filePath: string): Writable;
|
|
62
64
|
setFileVisibility(filePath: string, isPublic: boolean): Promise<void>;
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.CommonStorageBucket = 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
|
* Convenience wrapper around CommonStorage for a given Bucket.
|
|
8
7
|
*
|
|
@@ -74,6 +73,15 @@ class CommonStorageBucket {
|
|
|
74
73
|
async saveFile(filePath, content) {
|
|
75
74
|
await this.cfg.storage.saveFile(this.cfg.bucketName, filePath, content);
|
|
76
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Convenience method that does:
|
|
78
|
+
* await saveFile
|
|
79
|
+
* await setFileVisibility
|
|
80
|
+
*/
|
|
81
|
+
async savePublicFile(filePath, content) {
|
|
82
|
+
await this.cfg.storage.saveFile(this.cfg.bucketName, filePath, content);
|
|
83
|
+
await this.cfg.storage.setFileVisibility(this.cfg.bucketName, filePath, true);
|
|
84
|
+
}
|
|
77
85
|
async saveStringFile(filePath, content) {
|
|
78
86
|
await this.cfg.storage.saveFile(this.cfg.bucketName, filePath, Buffer.from(content));
|
|
79
87
|
}
|
|
@@ -107,27 +115,14 @@ class CommonStorageBucket {
|
|
|
107
115
|
* Important difference between `prefix` and `path` is that `prefix` will
|
|
108
116
|
* return all files from sub-directories too!
|
|
109
117
|
*/
|
|
110
|
-
async getFileNames(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return
|
|
118
|
-
}
|
|
119
|
-
getFileNamesStream(prefix, opt = {}) {
|
|
120
|
-
const { fullPath = true } = opt;
|
|
121
|
-
if (fullPath || !prefix) {
|
|
122
|
-
return this.cfg.storage.getFileNamesStream(this.cfg.bucketName, prefix, opt);
|
|
123
|
-
}
|
|
124
|
-
const start = `${prefix}/`.length;
|
|
125
|
-
return this.cfg.storage
|
|
126
|
-
.getFileNamesStream(this.cfg.bucketName, prefix, opt)
|
|
127
|
-
.pipe((0, nodejs_lib_1.transformMapSimple)(f => f.slice(start)));
|
|
128
|
-
}
|
|
129
|
-
getFilesStream(prefix, opt) {
|
|
130
|
-
return this.cfg.storage.getFilesStream(this.cfg.bucketName, prefix, opt);
|
|
118
|
+
async getFileNames(opt) {
|
|
119
|
+
return await this.cfg.storage.getFileNames(this.cfg.bucketName, opt);
|
|
120
|
+
}
|
|
121
|
+
getFileNamesStream(opt) {
|
|
122
|
+
return this.cfg.storage.getFileNamesStream(this.cfg.bucketName, opt);
|
|
123
|
+
}
|
|
124
|
+
getFilesStream(opt) {
|
|
125
|
+
return this.cfg.storage.getFilesStream(this.cfg.bucketName, opt);
|
|
131
126
|
}
|
|
132
127
|
getFileReadStream(filePath) {
|
|
133
128
|
return this.cfg.storage.getFileReadStream(this.cfg.bucketName, filePath);
|
|
@@ -62,30 +62,26 @@ class CommonStorageKeyValueDB {
|
|
|
62
62
|
}
|
|
63
63
|
streamIds(table, limit) {
|
|
64
64
|
const { bucketName, prefix } = this.getBucketAndPrefix(table);
|
|
65
|
-
|
|
66
|
-
return this.cfg.storage
|
|
67
|
-
.getFileNamesStream(bucketName, prefix, { limit })
|
|
68
|
-
.pipe((0, nodejs_lib_1.transformMapSimple)(f => f.slice(index)));
|
|
65
|
+
return this.cfg.storage.getFileNamesStream(bucketName, { prefix, limit, fullPaths: false });
|
|
69
66
|
}
|
|
70
67
|
streamValues(table, limit) {
|
|
71
68
|
const { bucketName, prefix } = this.getBucketAndPrefix(table);
|
|
72
69
|
return this.cfg.storage
|
|
73
|
-
.getFilesStream(bucketName, prefix,
|
|
70
|
+
.getFilesStream(bucketName, { prefix, limit })
|
|
74
71
|
.pipe((0, nodejs_lib_1.transformMapSimple)(f => f.content));
|
|
75
72
|
}
|
|
76
73
|
streamEntries(table, limit) {
|
|
77
74
|
const { bucketName, prefix } = this.getBucketAndPrefix(table);
|
|
78
|
-
const index = prefix.length + 1;
|
|
79
75
|
return this.cfg.storage
|
|
80
|
-
.getFilesStream(bucketName, prefix,
|
|
76
|
+
.getFilesStream(bucketName, { prefix, limit, fullPaths: false })
|
|
81
77
|
.pipe((0, nodejs_lib_1.transformMapSimple)(({ filePath, content }) => [
|
|
82
|
-
filePath
|
|
78
|
+
filePath,
|
|
83
79
|
content,
|
|
84
80
|
]));
|
|
85
81
|
}
|
|
86
82
|
async count(table) {
|
|
87
83
|
const { bucketName, prefix } = this.getBucketAndPrefix(table);
|
|
88
|
-
return (await this.cfg.storage.getFileNames(bucketName, prefix)).length;
|
|
84
|
+
return (await this.cfg.storage.getFileNames(bucketName, { prefix })).length;
|
|
89
85
|
}
|
|
90
86
|
}
|
|
91
87
|
exports.CommonStorageKeyValueDB = CommonStorageKeyValueDB;
|
|
@@ -16,9 +16,9 @@ export declare class InMemoryCommonStorage implements CommonStorage {
|
|
|
16
16
|
getFile(bucketName: string, filePath: string): Promise<Buffer | null>;
|
|
17
17
|
saveFile(bucketName: string, filePath: string, content: Buffer): Promise<void>;
|
|
18
18
|
deletePath(bucketName: string, prefix: string): Promise<void>;
|
|
19
|
-
getFileNames(bucketName: string,
|
|
20
|
-
getFileNamesStream(bucketName: string,
|
|
21
|
-
getFilesStream(bucketName: string,
|
|
19
|
+
getFileNames(bucketName: string, opt?: CommonStorageGetOptions): Promise<string[]>;
|
|
20
|
+
getFileNamesStream(bucketName: string, opt?: CommonStorageGetOptions): ReadableTyped<string>;
|
|
21
|
+
getFilesStream(bucketName: string, opt?: CommonStorageGetOptions): ReadableTyped<FileEntry>;
|
|
22
22
|
getFileReadStream(bucketName: string, filePath: string): Readable;
|
|
23
23
|
getFileWriteStream(_bucketName: string, _filePath: string): Writable;
|
|
24
24
|
setFileVisibility(bucketName: string, filePath: string, isPublic: boolean): Promise<void>;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.InMemoryCommonStorage = void 0;
|
|
4
4
|
const stream_1 = require("stream");
|
|
5
|
+
const js_lib_1 = require("@naturalcycles/js-lib");
|
|
5
6
|
class InMemoryCommonStorage {
|
|
6
7
|
constructor() {
|
|
7
8
|
/**
|
|
@@ -35,19 +36,29 @@ class InMemoryCommonStorage {
|
|
|
35
36
|
}
|
|
36
37
|
});
|
|
37
38
|
}
|
|
38
|
-
async getFileNames(bucketName,
|
|
39
|
-
|
|
39
|
+
async getFileNames(bucketName, opt = {}) {
|
|
40
|
+
const { prefix = '', fullPaths = true } = opt;
|
|
41
|
+
return Object.keys(this.data[bucketName] || {})
|
|
42
|
+
.filter(filePath => filePath.startsWith(prefix))
|
|
43
|
+
.map(f => (fullPaths ? f : (0, js_lib_1._substringAfterLast)(f, '/')));
|
|
40
44
|
}
|
|
41
|
-
getFileNamesStream(bucketName,
|
|
45
|
+
getFileNamesStream(bucketName, opt = {}) {
|
|
46
|
+
const { prefix = '', fullPaths = true } = opt;
|
|
42
47
|
return stream_1.Readable.from(Object.keys(this.data[bucketName] || {})
|
|
43
48
|
.filter(filePath => filePath.startsWith(prefix))
|
|
44
|
-
.slice(0, opt.limit)
|
|
49
|
+
.slice(0, opt.limit)
|
|
50
|
+
.map(n => (fullPaths ? n : (0, js_lib_1._substringAfterLast)(n, '/'))));
|
|
45
51
|
}
|
|
46
|
-
getFilesStream(bucketName,
|
|
52
|
+
getFilesStream(bucketName, opt = {}) {
|
|
53
|
+
const { prefix = '', fullPaths = true } = opt;
|
|
47
54
|
return stream_1.Readable.from(Object.entries(this.data[bucketName] || {})
|
|
48
|
-
.map(([filePath, content]) => ({
|
|
55
|
+
.map(([filePath, content]) => ({
|
|
56
|
+
filePath,
|
|
57
|
+
content,
|
|
58
|
+
}))
|
|
49
59
|
.filter(f => f.filePath.startsWith(prefix))
|
|
50
|
-
.slice(0, opt.limit)
|
|
60
|
+
.slice(0, opt.limit)
|
|
61
|
+
.map(f => (fullPaths ? f : { ...f, filePath: (0, js_lib_1._substringAfterLast)(f.filePath, '/') })));
|
|
51
62
|
}
|
|
52
63
|
getFileReadStream(bucketName, filePath) {
|
|
53
64
|
return stream_1.Readable.from(this.data[bucketName][filePath]);
|
|
@@ -33,29 +33,30 @@ function runCommonStorageTest(storage, bucketName) {
|
|
|
33
33
|
// await storage.createBucket(bucketName)
|
|
34
34
|
// })
|
|
35
35
|
test('ping', async () => {
|
|
36
|
-
await storage.ping();
|
|
37
|
-
});
|
|
38
|
-
test('listBuckets', async () => {
|
|
39
|
-
const buckets = await storage.getBucketNames();
|
|
40
|
-
console.log(buckets);
|
|
41
|
-
});
|
|
42
|
-
test('streamBuckets', async () => {
|
|
43
|
-
const buckets = await (0, nodejs_lib_1.readableToArray)(storage.getBucketNamesStream());
|
|
44
|
-
console.log(buckets);
|
|
36
|
+
await storage.ping(bucketName);
|
|
45
37
|
});
|
|
38
|
+
// test('listBuckets', async () => {
|
|
39
|
+
// const buckets = await storage.getBucketNames()
|
|
40
|
+
// console.log(buckets)
|
|
41
|
+
// })
|
|
42
|
+
//
|
|
43
|
+
// test('streamBuckets', async () => {
|
|
44
|
+
// const buckets = await readableToArray(storage.getBucketNamesStream())
|
|
45
|
+
// console.log(buckets)
|
|
46
|
+
// })
|
|
46
47
|
test('prepare: clear bucket', async () => {
|
|
47
48
|
await (0, js_lib_1.pMap)(TEST_FILES.map(f => f.filePath), async (filePath) => await storage.deletePath(bucketName, filePath));
|
|
48
49
|
});
|
|
49
|
-
test('listFileNames on root should return empty', async () => {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
})
|
|
50
|
+
// test('listFileNames on root should return empty', async () => {
|
|
51
|
+
// const fileNames = await storage.getFileNames(bucketName)
|
|
52
|
+
// expect(fileNames).toEqual([])
|
|
53
|
+
// })
|
|
53
54
|
test(`listFileNames on ${TEST_FOLDER} should return empty`, async () => {
|
|
54
|
-
const fileNames = await storage.getFileNames(bucketName, TEST_FOLDER);
|
|
55
|
+
const fileNames = await storage.getFileNames(bucketName, { prefix: TEST_FOLDER });
|
|
55
56
|
expect(fileNames).toEqual([]);
|
|
56
57
|
});
|
|
57
|
-
test(
|
|
58
|
-
const fileNames = await (0, nodejs_lib_1.readableToArray)(storage.getFileNamesStream(bucketName,
|
|
58
|
+
test(`streamFileNames on ${TEST_FOLDER} should return empty`, async () => {
|
|
59
|
+
const fileNames = await (0, nodejs_lib_1.readableToArray)(storage.getFileNamesStream(bucketName, { prefix: TEST_FOLDER }));
|
|
59
60
|
expect(fileNames).toEqual([]);
|
|
60
61
|
});
|
|
61
62
|
test(`exists should return empty array`, async () => {
|
|
@@ -68,9 +69,14 @@ function runCommonStorageTest(storage, bucketName) {
|
|
|
68
69
|
const testFilesMap = Object.fromEntries(TEST_FILES.map(f => [f.filePath, f.content]));
|
|
69
70
|
// It's done in the same test to ensure "strong consistency"
|
|
70
71
|
await (0, js_lib_1.pMap)(TEST_FILES, async (f) => await storage.saveFile(bucketName, f.filePath, f.content));
|
|
71
|
-
const
|
|
72
|
+
const fileNamesShort = await storage.getFileNames(bucketName, {
|
|
73
|
+
prefix: TEST_FOLDER,
|
|
74
|
+
fullPaths: false,
|
|
75
|
+
});
|
|
76
|
+
expect(fileNamesShort.sort()).toEqual(TEST_FILES.map(f => (0, js_lib_1._substringAfterLast)(f.filePath, '/')).sort());
|
|
77
|
+
const fileNames = await storage.getFileNames(bucketName, { prefix: TEST_FOLDER });
|
|
72
78
|
expect(fileNames.sort()).toEqual(TEST_FILES.map(f => f.filePath).sort());
|
|
73
|
-
const streamedFileNames = await (0, nodejs_lib_1.readableToArray)(storage.getFileNamesStream(bucketName, TEST_FOLDER));
|
|
79
|
+
const streamedFileNames = await (0, nodejs_lib_1.readableToArray)(storage.getFileNamesStream(bucketName, { prefix: TEST_FOLDER }));
|
|
74
80
|
expect(streamedFileNames.sort()).toEqual(TEST_FILES.map(f => f.filePath).sort());
|
|
75
81
|
const filesMap = {};
|
|
76
82
|
await (0, js_lib_1.pMap)(fileNames, async (filePath) => {
|
|
@@ -83,7 +89,7 @@ function runCommonStorageTest(storage, bucketName) {
|
|
|
83
89
|
});
|
|
84
90
|
});
|
|
85
91
|
test('cleanup', async () => {
|
|
86
|
-
await storage.deletePath(bucketName,
|
|
92
|
+
await storage.deletePath(bucketName, TEST_FOLDER);
|
|
87
93
|
});
|
|
88
94
|
// Cannot update access control for an object when uniform bucket-level access is enabled. Read more at https://cloud.google.com/storage/docs/uniform-bucket-level-access
|
|
89
95
|
/*
|
package/package.json
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
},
|
|
12
12
|
"devDependencies": {
|
|
13
13
|
"@naturalcycles/dev-lib": "^12.1.3",
|
|
14
|
-
"@types/node": "^17.0.
|
|
14
|
+
"@types/node": "^17.0.8",
|
|
15
|
+
"firebase-admin": "^10.0.1",
|
|
15
16
|
"jest": "^27.1.0"
|
|
16
17
|
},
|
|
17
18
|
"files": [
|
|
@@ -34,7 +35,7 @@
|
|
|
34
35
|
"engines": {
|
|
35
36
|
"node": ">=14.16.0"
|
|
36
37
|
},
|
|
37
|
-
"version": "1.
|
|
38
|
+
"version": "1.5.0",
|
|
38
39
|
"description": "",
|
|
39
40
|
"author": "Natural Cycles Team",
|
|
40
41
|
"license": "MIT"
|
package/src/cloudStorage.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import * as Buffer from 'buffer'
|
|
2
1
|
import { Readable, Writable } from 'stream'
|
|
3
|
-
import {
|
|
2
|
+
import { _substringAfterLast } from '@naturalcycles/js-lib'
|
|
3
|
+
import { File, Storage, StorageOptions } from '@google-cloud/storage'
|
|
4
4
|
import { ReadableTyped, transformMap, transformMapSimple } from '@naturalcycles/nodejs-lib'
|
|
5
5
|
import { CommonStorage, CommonStorageGetOptions, FileEntry } from './commonStorage'
|
|
6
6
|
import { GCPServiceAccount } from './model'
|
|
@@ -15,43 +15,40 @@ import { GCPServiceAccount } from './model'
|
|
|
15
15
|
* (either personal one or non-personal).
|
|
16
16
|
*/
|
|
17
17
|
export interface CloudStorageCfg {
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* It's optional, to allow automatic credentials in AppEngine, or GOOGLE_APPLICATION_CREDENTIALS.
|
|
20
|
+
*/
|
|
21
|
+
credentials?: GCPServiceAccount
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
export class CloudStorage implements CommonStorage {
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Passing the pre-created Storage allows to instantiate it from both
|
|
27
|
+
* GCP Storage and FirebaseStorage.
|
|
28
|
+
*/
|
|
29
|
+
constructor(public storage: Storage) {}
|
|
30
|
+
|
|
31
|
+
static createFromGCPServiceAccount(cfg: CloudStorageCfg): CloudStorage {
|
|
32
|
+
const storage = new Storage({
|
|
24
33
|
credentials: cfg.credentials,
|
|
25
34
|
// Explicitly passing it here to fix this error:
|
|
26
35
|
// Error: Unable to detect a Project Id in the current environment.
|
|
27
36
|
// To learn more about authentication and Google APIs, visit:
|
|
28
37
|
// https://cloud.google.com/docs/authentication/getting-started
|
|
29
38
|
// at /root/repo/node_modules/google-auth-library/build/src/auth/googleauth.js:95:31
|
|
30
|
-
projectId: cfg.credentials
|
|
39
|
+
projectId: cfg.credentials?.project_id,
|
|
31
40
|
})
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
storage: Storage
|
|
35
|
-
|
|
36
|
-
// async createBucket(bucketName: string): Promise<void> {
|
|
37
|
-
// const bucket = await this.storage.createBucket(bucketName)
|
|
38
|
-
// console.log(bucket) // debugging
|
|
39
|
-
// }
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
await this.storage.bucket(bucketName || 'non-existing-for-sure').exists()
|
|
42
|
+
return new CloudStorage(storage)
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
return buckets.map(b => b.name)
|
|
45
|
+
static createFromStorageOptions(storageOptions?: StorageOptions): CloudStorage {
|
|
46
|
+
const storage = new Storage(storageOptions)
|
|
47
|
+
return new CloudStorage(storage)
|
|
51
48
|
}
|
|
52
49
|
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
async ping(bucketName?: string): Promise<void> {
|
|
51
|
+
await this.storage.bucket(bucketName || 'non-existing-for-sure').exists()
|
|
55
52
|
}
|
|
56
53
|
|
|
57
54
|
async deletePath(bucketName: string, prefix: string): Promise<void> {
|
|
@@ -67,32 +64,38 @@ export class CloudStorage implements CommonStorage {
|
|
|
67
64
|
return exists
|
|
68
65
|
}
|
|
69
66
|
|
|
70
|
-
async getFileNames(bucketName: string,
|
|
67
|
+
async getFileNames(bucketName: string, opt: CommonStorageGetOptions = {}): Promise<string[]> {
|
|
68
|
+
const { prefix, fullPaths = true } = opt
|
|
71
69
|
const [files] = await this.storage.bucket(bucketName).getFiles({
|
|
72
70
|
prefix,
|
|
73
71
|
})
|
|
74
|
-
|
|
72
|
+
|
|
73
|
+
if (fullPaths) {
|
|
74
|
+
return files.map(f => f.name)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return files.map(f => _substringAfterLast(f.name, '/'))
|
|
75
78
|
}
|
|
76
79
|
|
|
77
|
-
getFileNamesStream(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
opt: CommonStorageGetOptions = {},
|
|
81
|
-
): ReadableTyped<string> {
|
|
80
|
+
getFileNamesStream(bucketName: string, opt: CommonStorageGetOptions = {}): ReadableTyped<string> {
|
|
81
|
+
const { prefix, fullPaths = true } = opt
|
|
82
|
+
|
|
82
83
|
return this.storage
|
|
83
84
|
.bucket(bucketName)
|
|
84
85
|
.getFilesStream({
|
|
85
86
|
prefix,
|
|
86
87
|
maxResults: opt.limit,
|
|
87
88
|
})
|
|
88
|
-
.pipe(
|
|
89
|
+
.pipe(
|
|
90
|
+
transformMapSimple<File, string>(f =>
|
|
91
|
+
fullPaths ? f.name : _substringAfterLast(f.name, '/'),
|
|
92
|
+
),
|
|
93
|
+
)
|
|
89
94
|
}
|
|
90
95
|
|
|
91
|
-
getFilesStream(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
opt: CommonStorageGetOptions = {},
|
|
95
|
-
): ReadableTyped<FileEntry> {
|
|
96
|
+
getFilesStream(bucketName: string, opt: CommonStorageGetOptions = {}): ReadableTyped<FileEntry> {
|
|
97
|
+
const { prefix, fullPaths = true } = opt
|
|
98
|
+
|
|
96
99
|
return this.storage
|
|
97
100
|
.bucket(bucketName)
|
|
98
101
|
.getFilesStream({
|
|
@@ -102,7 +105,7 @@ export class CloudStorage implements CommonStorage {
|
|
|
102
105
|
.pipe(
|
|
103
106
|
transformMap<File, FileEntry>(async f => {
|
|
104
107
|
const [content] = await f.download()
|
|
105
|
-
return { filePath: f.name, content }
|
|
108
|
+
return { filePath: fullPaths ? f.name : _substringAfterLast(f.name, '/'), content }
|
|
106
109
|
}),
|
|
107
110
|
)
|
|
108
111
|
}
|
package/src/commonStorage.ts
CHANGED
|
@@ -14,6 +14,12 @@ export interface CommonStorageGetOptions {
|
|
|
14
14
|
*/
|
|
15
15
|
prefix?: string
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Defaults to true.
|
|
19
|
+
* Set to false to return file names instead of full paths.
|
|
20
|
+
*/
|
|
21
|
+
fullPaths?: boolean
|
|
22
|
+
|
|
17
23
|
/**
|
|
18
24
|
* Limits the number of results.
|
|
19
25
|
*
|
|
@@ -41,13 +47,6 @@ export interface CommonStorage {
|
|
|
41
47
|
*/
|
|
42
48
|
ping(bucketName?: string): Promise<void>
|
|
43
49
|
|
|
44
|
-
/**
|
|
45
|
-
* Often needs a special permission.
|
|
46
|
-
*/
|
|
47
|
-
getBucketNames(opt?: CommonStorageGetOptions): Promise<string[]>
|
|
48
|
-
|
|
49
|
-
getBucketNamesStream(): ReadableTyped<string>
|
|
50
|
-
|
|
51
50
|
/**
|
|
52
51
|
* Creates a new bucket by given name.
|
|
53
52
|
* todo: check what to do if it already exists
|
|
@@ -76,19 +75,11 @@ export interface CommonStorage {
|
|
|
76
75
|
* Important difference between `prefix` and `path` is that `prefix` will
|
|
77
76
|
* return all files from sub-directories too!
|
|
78
77
|
*/
|
|
79
|
-
getFileNames(bucketName: string,
|
|
80
|
-
|
|
81
|
-
getFileNamesStream(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
opt?: CommonStorageGetOptions,
|
|
85
|
-
): ReadableTyped<string>
|
|
86
|
-
|
|
87
|
-
getFilesStream(
|
|
88
|
-
bucketName: string,
|
|
89
|
-
prefix: string,
|
|
90
|
-
opt?: CommonStorageGetOptions,
|
|
91
|
-
): ReadableTyped<FileEntry>
|
|
78
|
+
getFileNames(bucketName: string, opt?: CommonStorageGetOptions): Promise<string[]>
|
|
79
|
+
|
|
80
|
+
getFileNamesStream(bucketName: string, opt?: CommonStorageGetOptions): ReadableTyped<string>
|
|
81
|
+
|
|
82
|
+
getFilesStream(bucketName: string, opt?: CommonStorageGetOptions): ReadableTyped<FileEntry>
|
|
92
83
|
|
|
93
84
|
getFileReadStream(bucketName: string, filePath: string): Readable
|
|
94
85
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Readable, Writable } from 'stream'
|
|
2
2
|
import { AppError, pMap } from '@naturalcycles/js-lib'
|
|
3
|
-
import { ReadableTyped
|
|
3
|
+
import { ReadableTyped } from '@naturalcycles/nodejs-lib'
|
|
4
4
|
import { CommonStorage, CommonStorageGetOptions, FileEntry } from './commonStorage'
|
|
5
5
|
|
|
6
6
|
export interface CommonStorageBucketCfg {
|
|
@@ -103,6 +103,16 @@ export class CommonStorageBucket {
|
|
|
103
103
|
await this.cfg.storage.saveFile(this.cfg.bucketName, filePath, content)
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Convenience method that does:
|
|
108
|
+
* await saveFile
|
|
109
|
+
* await setFileVisibility
|
|
110
|
+
*/
|
|
111
|
+
async savePublicFile(filePath: string, content: Buffer): Promise<void> {
|
|
112
|
+
await this.cfg.storage.saveFile(this.cfg.bucketName, filePath, content)
|
|
113
|
+
await this.cfg.storage.setFileVisibility(this.cfg.bucketName, filePath, true)
|
|
114
|
+
}
|
|
115
|
+
|
|
106
116
|
async saveStringFile(filePath: string, content: string): Promise<void> {
|
|
107
117
|
await this.cfg.storage.saveFile(this.cfg.bucketName, filePath, Buffer.from(content))
|
|
108
118
|
}
|
|
@@ -145,36 +155,16 @@ export class CommonStorageBucket {
|
|
|
145
155
|
* Important difference between `prefix` and `path` is that `prefix` will
|
|
146
156
|
* return all files from sub-directories too!
|
|
147
157
|
*/
|
|
148
|
-
async getFileNames(
|
|
149
|
-
|
|
150
|
-
const names = await this.cfg.storage.getFileNames(this.cfg.bucketName, prefix)
|
|
151
|
-
|
|
152
|
-
if (!fullPath && prefix) {
|
|
153
|
-
const start = `${prefix}/`.length
|
|
154
|
-
return names.map(n => n.slice(start))
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return names
|
|
158
|
+
async getFileNames(opt?: CommonStorageGetOptions): Promise<string[]> {
|
|
159
|
+
return await this.cfg.storage.getFileNames(this.cfg.bucketName, opt)
|
|
158
160
|
}
|
|
159
161
|
|
|
160
|
-
getFileNamesStream(
|
|
161
|
-
|
|
162
|
-
opt: CommonStorageGetOptions & { fullPath?: boolean } = {},
|
|
163
|
-
): ReadableTyped<string> {
|
|
164
|
-
const { fullPath = true } = opt
|
|
165
|
-
|
|
166
|
-
if (fullPath || !prefix) {
|
|
167
|
-
return this.cfg.storage.getFileNamesStream(this.cfg.bucketName, prefix, opt)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const start = `${prefix}/`.length
|
|
171
|
-
return this.cfg.storage
|
|
172
|
-
.getFileNamesStream(this.cfg.bucketName, prefix, opt)
|
|
173
|
-
.pipe(transformMapSimple<string, string>(f => f.slice(start)))
|
|
162
|
+
getFileNamesStream(opt?: CommonStorageGetOptions): ReadableTyped<string> {
|
|
163
|
+
return this.cfg.storage.getFileNamesStream(this.cfg.bucketName, opt)
|
|
174
164
|
}
|
|
175
165
|
|
|
176
|
-
getFilesStream(
|
|
177
|
-
return this.cfg.storage.getFilesStream(this.cfg.bucketName,
|
|
166
|
+
getFilesStream(opt?: CommonStorageGetOptions): ReadableTyped<FileEntry> {
|
|
167
|
+
return this.cfg.storage.getFilesStream(this.cfg.bucketName, opt)
|
|
178
168
|
}
|
|
179
169
|
|
|
180
170
|
getFileReadStream(filePath: string): Readable {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import * as Buffer from 'buffer'
|
|
2
1
|
import { CommonDBCreateOptions, CommonKeyValueDB, KeyValueDBTuple } from '@naturalcycles/db-lib'
|
|
3
2
|
import { pMap, StringMap } from '@naturalcycles/js-lib'
|
|
4
3
|
import { ReadableTyped, transformMapSimple } from '@naturalcycles/nodejs-lib'
|
|
@@ -78,30 +77,26 @@ export class CommonStorageKeyValueDB implements CommonKeyValueDB {
|
|
|
78
77
|
|
|
79
78
|
streamIds(table: string, limit?: number): ReadableTyped<string> {
|
|
80
79
|
const { bucketName, prefix } = this.getBucketAndPrefix(table)
|
|
81
|
-
const index = prefix.length + 1
|
|
82
80
|
|
|
83
|
-
return this.cfg.storage
|
|
84
|
-
.getFileNamesStream(bucketName, prefix, { limit })
|
|
85
|
-
.pipe(transformMapSimple<string, string>(f => f.slice(index)))
|
|
81
|
+
return this.cfg.storage.getFileNamesStream(bucketName, { prefix, limit, fullPaths: false })
|
|
86
82
|
}
|
|
87
83
|
|
|
88
84
|
streamValues(table: string, limit?: number): ReadableTyped<Buffer> {
|
|
89
85
|
const { bucketName, prefix } = this.getBucketAndPrefix(table)
|
|
90
86
|
|
|
91
87
|
return this.cfg.storage
|
|
92
|
-
.getFilesStream(bucketName, prefix,
|
|
88
|
+
.getFilesStream(bucketName, { prefix, limit })
|
|
93
89
|
.pipe(transformMapSimple<FileEntry, Buffer>(f => f.content))
|
|
94
90
|
}
|
|
95
91
|
|
|
96
92
|
streamEntries(table: string, limit?: number): ReadableTyped<KeyValueDBTuple> {
|
|
97
93
|
const { bucketName, prefix } = this.getBucketAndPrefix(table)
|
|
98
|
-
const index = prefix.length + 1
|
|
99
94
|
|
|
100
95
|
return this.cfg.storage
|
|
101
|
-
.getFilesStream(bucketName, prefix,
|
|
96
|
+
.getFilesStream(bucketName, { prefix, limit, fullPaths: false })
|
|
102
97
|
.pipe(
|
|
103
98
|
transformMapSimple<FileEntry, KeyValueDBTuple>(({ filePath, content }) => [
|
|
104
|
-
filePath
|
|
99
|
+
filePath,
|
|
105
100
|
content,
|
|
106
101
|
]),
|
|
107
102
|
)
|
|
@@ -110,6 +105,6 @@ export class CommonStorageKeyValueDB implements CommonKeyValueDB {
|
|
|
110
105
|
async count(table: string): Promise<number> {
|
|
111
106
|
const { bucketName, prefix } = this.getBucketAndPrefix(table)
|
|
112
107
|
|
|
113
|
-
return (await this.cfg.storage.getFileNames(bucketName, prefix)).length
|
|
108
|
+
return (await this.cfg.storage.getFileNames(bucketName, { prefix })).length
|
|
114
109
|
}
|
|
115
110
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Readable, Writable } from 'stream'
|
|
2
|
-
import { StringMap } from '@naturalcycles/js-lib'
|
|
2
|
+
import { _substringAfterLast, StringMap } from '@naturalcycles/js-lib'
|
|
3
3
|
import { ReadableTyped } from '@naturalcycles/nodejs-lib'
|
|
4
4
|
import { CommonStorage, CommonStorageGetOptions, FileEntry } from './commonStorage'
|
|
5
5
|
|
|
@@ -42,32 +42,36 @@ export class InMemoryCommonStorage implements CommonStorage {
|
|
|
42
42
|
})
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
async getFileNames(bucketName: string,
|
|
46
|
-
|
|
45
|
+
async getFileNames(bucketName: string, opt: CommonStorageGetOptions = {}): Promise<string[]> {
|
|
46
|
+
const { prefix = '', fullPaths = true } = opt
|
|
47
|
+
return Object.keys(this.data[bucketName] || {})
|
|
48
|
+
.filter(filePath => filePath.startsWith(prefix))
|
|
49
|
+
.map(f => (fullPaths ? f : _substringAfterLast(f, '/')))
|
|
47
50
|
}
|
|
48
51
|
|
|
49
|
-
getFileNamesStream(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
opt: CommonStorageGetOptions = {},
|
|
53
|
-
): ReadableTyped<string> {
|
|
52
|
+
getFileNamesStream(bucketName: string, opt: CommonStorageGetOptions = {}): ReadableTyped<string> {
|
|
53
|
+
const { prefix = '', fullPaths = true } = opt
|
|
54
|
+
|
|
54
55
|
return Readable.from(
|
|
55
56
|
Object.keys(this.data[bucketName] || {})
|
|
56
57
|
.filter(filePath => filePath.startsWith(prefix))
|
|
57
|
-
.slice(0, opt.limit)
|
|
58
|
+
.slice(0, opt.limit)
|
|
59
|
+
.map(n => (fullPaths ? n : _substringAfterLast(n, '/'))),
|
|
58
60
|
)
|
|
59
61
|
}
|
|
60
62
|
|
|
61
|
-
getFilesStream(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
opt: CommonStorageGetOptions = {},
|
|
65
|
-
): ReadableTyped<FileEntry> {
|
|
63
|
+
getFilesStream(bucketName: string, opt: CommonStorageGetOptions = {}): ReadableTyped<FileEntry> {
|
|
64
|
+
const { prefix = '', fullPaths = true } = opt
|
|
65
|
+
|
|
66
66
|
return Readable.from(
|
|
67
67
|
Object.entries(this.data[bucketName] || {})
|
|
68
|
-
.map(([filePath, content]) => ({
|
|
68
|
+
.map(([filePath, content]) => ({
|
|
69
|
+
filePath,
|
|
70
|
+
content,
|
|
71
|
+
}))
|
|
69
72
|
.filter(f => f.filePath.startsWith(prefix))
|
|
70
|
-
.slice(0, opt.limit)
|
|
73
|
+
.slice(0, opt.limit)
|
|
74
|
+
.map(f => (fullPaths ? f : { ...f, filePath: _substringAfterLast(f.filePath, '/') })),
|
|
71
75
|
)
|
|
72
76
|
}
|
|
73
77
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _range, pMap, StringMap } from '@naturalcycles/js-lib'
|
|
1
|
+
import { _range, _substringAfterLast, pMap, StringMap } from '@naturalcycles/js-lib'
|
|
2
2
|
import { readableToArray } from '@naturalcycles/nodejs-lib'
|
|
3
3
|
import { CommonStorage, FileEntry } from '../commonStorage'
|
|
4
4
|
|
|
@@ -38,18 +38,18 @@ export function runCommonStorageTest(storage: CommonStorage, bucketName: string)
|
|
|
38
38
|
// })
|
|
39
39
|
|
|
40
40
|
test('ping', async () => {
|
|
41
|
-
await storage.ping()
|
|
41
|
+
await storage.ping(bucketName)
|
|
42
42
|
})
|
|
43
43
|
|
|
44
|
-
test('listBuckets', async () => {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
test('streamBuckets', async () => {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
})
|
|
44
|
+
// test('listBuckets', async () => {
|
|
45
|
+
// const buckets = await storage.getBucketNames()
|
|
46
|
+
// console.log(buckets)
|
|
47
|
+
// })
|
|
48
|
+
//
|
|
49
|
+
// test('streamBuckets', async () => {
|
|
50
|
+
// const buckets = await readableToArray(storage.getBucketNamesStream())
|
|
51
|
+
// console.log(buckets)
|
|
52
|
+
// })
|
|
53
53
|
|
|
54
54
|
test('prepare: clear bucket', async () => {
|
|
55
55
|
await pMap(
|
|
@@ -58,18 +58,20 @@ export function runCommonStorageTest(storage: CommonStorage, bucketName: string)
|
|
|
58
58
|
)
|
|
59
59
|
})
|
|
60
60
|
|
|
61
|
-
test('listFileNames on root should return empty', async () => {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
})
|
|
61
|
+
// test('listFileNames on root should return empty', async () => {
|
|
62
|
+
// const fileNames = await storage.getFileNames(bucketName)
|
|
63
|
+
// expect(fileNames).toEqual([])
|
|
64
|
+
// })
|
|
65
65
|
|
|
66
66
|
test(`listFileNames on ${TEST_FOLDER} should return empty`, async () => {
|
|
67
|
-
const fileNames = await storage.getFileNames(bucketName, TEST_FOLDER)
|
|
67
|
+
const fileNames = await storage.getFileNames(bucketName, { prefix: TEST_FOLDER })
|
|
68
68
|
expect(fileNames).toEqual([])
|
|
69
69
|
})
|
|
70
70
|
|
|
71
|
-
test(
|
|
72
|
-
const fileNames = await readableToArray(
|
|
71
|
+
test(`streamFileNames on ${TEST_FOLDER} should return empty`, async () => {
|
|
72
|
+
const fileNames = await readableToArray(
|
|
73
|
+
storage.getFileNamesStream(bucketName, { prefix: TEST_FOLDER }),
|
|
74
|
+
)
|
|
73
75
|
expect(fileNames).toEqual([])
|
|
74
76
|
})
|
|
75
77
|
|
|
@@ -86,11 +88,19 @@ export function runCommonStorageTest(storage: CommonStorage, bucketName: string)
|
|
|
86
88
|
// It's done in the same test to ensure "strong consistency"
|
|
87
89
|
await pMap(TEST_FILES, async f => await storage.saveFile(bucketName, f.filePath, f.content))
|
|
88
90
|
|
|
89
|
-
const
|
|
91
|
+
const fileNamesShort = await storage.getFileNames(bucketName, {
|
|
92
|
+
prefix: TEST_FOLDER,
|
|
93
|
+
fullPaths: false,
|
|
94
|
+
})
|
|
95
|
+
expect(fileNamesShort.sort()).toEqual(
|
|
96
|
+
TEST_FILES.map(f => _substringAfterLast(f.filePath, '/')).sort(),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
const fileNames = await storage.getFileNames(bucketName, { prefix: TEST_FOLDER })
|
|
90
100
|
expect(fileNames.sort()).toEqual(TEST_FILES.map(f => f.filePath).sort())
|
|
91
101
|
|
|
92
102
|
const streamedFileNames = await readableToArray(
|
|
93
|
-
storage.getFileNamesStream(bucketName, TEST_FOLDER),
|
|
103
|
+
storage.getFileNamesStream(bucketName, { prefix: TEST_FOLDER }),
|
|
94
104
|
)
|
|
95
105
|
expect(streamedFileNames.sort()).toEqual(TEST_FILES.map(f => f.filePath).sort())
|
|
96
106
|
|
|
@@ -109,7 +119,7 @@ export function runCommonStorageTest(storage: CommonStorage, bucketName: string)
|
|
|
109
119
|
})
|
|
110
120
|
|
|
111
121
|
test('cleanup', async () => {
|
|
112
|
-
await storage.deletePath(bucketName,
|
|
122
|
+
await storage.deletePath(bucketName, TEST_FOLDER)
|
|
113
123
|
})
|
|
114
124
|
|
|
115
125
|
// Cannot update access control for an object when uniform bucket-level access is enabled. Read more at https://cloud.google.com/storage/docs/uniform-bucket-level-access
|