@naturalcycles/cloud-storage-lib 1.1.0 → 1.4.1

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.
@@ -17,17 +17,19 @@ export interface CloudStorageCfg {
17
17
  credentials: GCPServiceAccount;
18
18
  }
19
19
  export declare class CloudStorage implements CommonStorage {
20
- cfg: CloudStorageCfg;
21
- constructor(cfg: CloudStorageCfg);
22
20
  storage: Storage;
21
+ /**
22
+ * Passing the pre-created Storage allows to instantiate it from both
23
+ * GCP Storage and FirebaseStorage.
24
+ */
25
+ constructor(storage: Storage);
26
+ static createFromGCPServiceAccount(cfg: CloudStorageCfg): CloudStorage;
23
27
  ping(bucketName?: string): Promise<void>;
24
- getBucketNames(opt?: CommonStorageGetOptions): Promise<string[]>;
25
- getBucketNamesStream(): ReadableTyped<string>;
26
28
  deletePath(bucketName: string, prefix: string): Promise<void>;
27
29
  fileExists(bucketName: string, filePath: string): Promise<boolean>;
28
- getFileNames(bucketName: string, prefix: string): Promise<string[]>;
29
- getFileNamesStream(bucketName: string, prefix: string, opt?: CommonStorageGetOptions): ReadableTyped<string>;
30
- getFilesStream(bucketName: string, prefix: string, opt?: CommonStorageGetOptions): ReadableTyped<FileEntry>;
30
+ getFileNames(bucketName: string, opt?: CommonStorageGetOptions): Promise<string[]>;
31
+ getFileNamesStream(bucketName: string, opt?: CommonStorageGetOptions): ReadableTyped<string>;
32
+ getFilesStream(bucketName: string, opt?: CommonStorageGetOptions): ReadableTyped<FileEntry>;
31
33
  getFile(bucketName: string, filePath: string): Promise<Buffer | null>;
32
34
  /**
33
35
  * Returns a Readable that is NOT object mode,
@@ -1,12 +1,19 @@
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
- constructor(cfg) {
8
- this.cfg = cfg;
9
- this.storage = new storage_1.Storage({
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.
@@ -15,23 +22,11 @@ class CloudStorage {
15
22
  // at /root/repo/node_modules/google-auth-library/build/src/auth/googleauth.js:95:31
16
23
  projectId: cfg.credentials.project_id,
17
24
  });
25
+ return new CloudStorage(storage);
18
26
  }
19
- // async createBucket(bucketName: string): Promise<void> {
20
- // const bucket = await this.storage.createBucket(bucketName)
21
- // console.log(bucket) // debugging
22
- // }
23
27
  async ping(bucketName) {
24
28
  await this.storage.bucket(bucketName || 'non-existing-for-sure').exists();
25
29
  }
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
30
  async deletePath(bucketName, prefix) {
36
31
  await this.storage.bucket(bucketName).deleteFiles({
37
32
  prefix,
@@ -43,22 +38,28 @@ class CloudStorage {
43
38
  const [exists] = await this.storage.bucket(bucketName).file(filePath).exists();
44
39
  return exists;
45
40
  }
46
- async getFileNames(bucketName, prefix) {
41
+ async getFileNames(bucketName, opt = {}) {
42
+ const { prefix, fullPaths = true } = opt;
47
43
  const [files] = await this.storage.bucket(bucketName).getFiles({
48
44
  prefix,
49
45
  });
50
- return files.map(f => f.name);
46
+ if (fullPaths) {
47
+ return files.map(f => f.name);
48
+ }
49
+ return files.map(f => (0, js_lib_1._substringAfterLast)(f.name, '/'));
51
50
  }
52
- getFileNamesStream(bucketName, prefix, opt = {}) {
51
+ getFileNamesStream(bucketName, opt = {}) {
52
+ const { prefix, fullPaths = true } = opt;
53
53
  return this.storage
54
54
  .bucket(bucketName)
55
55
  .getFilesStream({
56
56
  prefix,
57
57
  maxResults: opt.limit,
58
58
  })
59
- .pipe((0, nodejs_lib_1.transformMapSimple)(f => f.name));
59
+ .pipe((0, nodejs_lib_1.transformMapSimple)(f => fullPaths ? f.name : (0, js_lib_1._substringAfterLast)(f.name, '/')));
60
60
  }
61
- getFilesStream(bucketName, prefix, opt = {}) {
61
+ getFilesStream(bucketName, opt = {}) {
62
+ const { prefix, fullPaths = true } = opt;
62
63
  return this.storage
63
64
  .bucket(bucketName)
64
65
  .getFilesStream({
@@ -67,7 +68,7 @@ class CloudStorage {
67
68
  })
68
69
  .pipe((0, nodejs_lib_1.transformMap)(async (f) => {
69
70
  const [content] = await f.download();
70
- return { filePath: f.name, content };
71
+ return { filePath: fullPaths ? f.name : (0, js_lib_1._substringAfterLast)(f.name, '/'), content };
71
72
  }));
72
73
  }
73
74
  async getFile(bucketName, filePath) {
@@ -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, prefix: string): Promise<string[]>;
66
- getFileNamesStream(bucketName: string, prefix: string, opt?: CommonStorageGetOptions): ReadableTyped<string>;
67
- getFilesStream(bucketName: string, prefix: string, opt?: CommonStorageGetOptions): ReadableTyped<FileEntry>;
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,9 +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(prefix: string): Promise<string[]>;
54
- getFileNamesStream(prefix: string, opt?: CommonStorageGetOptions): ReadableTyped<string>;
55
- 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>;
56
62
  getFileReadStream(filePath: string): Readable;
57
63
  getFileWriteStream(filePath: string): Writable;
58
64
  setFileVisibility(filePath: string, isPublic: boolean): Promise<void>;
@@ -73,6 +73,15 @@ class CommonStorageBucket {
73
73
  async saveFile(filePath, content) {
74
74
  await this.cfg.storage.saveFile(this.cfg.bucketName, filePath, content);
75
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
+ }
76
85
  async saveStringFile(filePath, content) {
77
86
  await this.cfg.storage.saveFile(this.cfg.bucketName, filePath, Buffer.from(content));
78
87
  }
@@ -106,14 +115,14 @@ class CommonStorageBucket {
106
115
  * Important difference between `prefix` and `path` is that `prefix` will
107
116
  * return all files from sub-directories too!
108
117
  */
109
- async getFileNames(prefix) {
110
- return await this.cfg.storage.getFileNames(this.cfg.bucketName, prefix);
118
+ async getFileNames(opt) {
119
+ return await this.cfg.storage.getFileNames(this.cfg.bucketName, opt);
111
120
  }
112
- getFileNamesStream(prefix, opt) {
113
- return this.cfg.storage.getFileNamesStream(this.cfg.bucketName, prefix, opt);
121
+ getFileNamesStream(opt) {
122
+ return this.cfg.storage.getFileNamesStream(this.cfg.bucketName, opt);
114
123
  }
115
- getFilesStream(prefix, opt) {
116
- return this.cfg.storage.getFilesStream(this.cfg.bucketName, prefix, opt);
124
+ getFilesStream(opt) {
125
+ return this.cfg.storage.getFilesStream(this.cfg.bucketName, opt);
117
126
  }
118
127
  getFileReadStream(filePath) {
119
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
- const index = prefix.length + 1;
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, { limit })
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, { limit })
76
+ .getFilesStream(bucketName, { prefix, limit, fullPaths: false })
81
77
  .pipe((0, nodejs_lib_1.transformMapSimple)(({ filePath, content }) => [
82
- filePath.slice(index),
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, prefix: string): Promise<string[]>;
20
- getFileNamesStream(bucketName: string, prefix: string, opt?: CommonStorageGetOptions): ReadableTyped<string>;
21
- getFilesStream(bucketName: string, prefix: string, opt?: CommonStorageGetOptions): ReadableTyped<FileEntry>;
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, prefix) {
39
- return Object.keys(this.data[bucketName] || {}).filter(filePath => filePath.startsWith(prefix));
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, prefix, opt = {}) {
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, prefix, opt = {}) {
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]) => ({ 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
- const fileNames = await storage.getFileNames(bucketName, '');
51
- expect(fileNames).toEqual([]);
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('streamFileNames on root should return empty', async () => {
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 fileNames = await storage.getFileNames(bucketName, TEST_FOLDER);
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.5",
14
+ "@types/node": "^16.0.0",
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.1.0",
38
+ "version": "1.4.1",
38
39
  "description": "",
39
40
  "author": "Natural Cycles Team",
40
41
  "license": "MIT"
@@ -1,6 +1,7 @@
1
1
  import * as Buffer from 'buffer'
2
2
  import { Readable, Writable } from 'stream'
3
- import { Bucket, File, Storage } from '@google-cloud/storage'
3
+ import { _substringAfterLast } from '@naturalcycles/js-lib'
4
+ import { File, Storage } from '@google-cloud/storage'
4
5
  import { ReadableTyped, transformMap, transformMapSimple } from '@naturalcycles/nodejs-lib'
5
6
  import { CommonStorage, CommonStorageGetOptions, FileEntry } from './commonStorage'
6
7
  import { GCPServiceAccount } from './model'
@@ -19,8 +20,14 @@ export interface CloudStorageCfg {
19
20
  }
20
21
 
21
22
  export class CloudStorage implements CommonStorage {
22
- constructor(public cfg: CloudStorageCfg) {
23
- this.storage = new Storage({
23
+ /**
24
+ * Passing the pre-created Storage allows to instantiate it from both
25
+ * GCP Storage and FirebaseStorage.
26
+ */
27
+ constructor(public storage: Storage) {}
28
+
29
+ static createFromGCPServiceAccount(cfg: CloudStorageCfg): CloudStorage {
30
+ const storage = new Storage({
24
31
  credentials: cfg.credentials,
25
32
  // Explicitly passing it here to fix this error:
26
33
  // Error: Unable to detect a Project Id in the current environment.
@@ -29,31 +36,14 @@ export class CloudStorage implements CommonStorage {
29
36
  // at /root/repo/node_modules/google-auth-library/build/src/auth/googleauth.js:95:31
30
37
  projectId: cfg.credentials.project_id,
31
38
  })
32
- }
33
-
34
- storage: Storage
35
39
 
36
- // async createBucket(bucketName: string): Promise<void> {
37
- // const bucket = await this.storage.createBucket(bucketName)
38
- // console.log(bucket) // debugging
39
- // }
40
+ return new CloudStorage(storage)
41
+ }
40
42
 
41
43
  async ping(bucketName?: string): Promise<void> {
42
44
  await this.storage.bucket(bucketName || 'non-existing-for-sure').exists()
43
45
  }
44
46
 
45
- async getBucketNames(opt: CommonStorageGetOptions = {}): Promise<string[]> {
46
- const [buckets] = await this.storage.getBuckets({
47
- maxResults: opt.limit,
48
- })
49
-
50
- return buckets.map(b => b.name)
51
- }
52
-
53
- getBucketNamesStream(): ReadableTyped<string> {
54
- return this.storage.getBucketsStream().pipe(transformMapSimple<Bucket, string>(b => b.name))
55
- }
56
-
57
47
  async deletePath(bucketName: string, prefix: string): Promise<void> {
58
48
  await this.storage.bucket(bucketName).deleteFiles({
59
49
  prefix,
@@ -67,32 +57,38 @@ export class CloudStorage implements CommonStorage {
67
57
  return exists
68
58
  }
69
59
 
70
- async getFileNames(bucketName: string, prefix: string): Promise<string[]> {
60
+ async getFileNames(bucketName: string, opt: CommonStorageGetOptions = {}): Promise<string[]> {
61
+ const { prefix, fullPaths = true } = opt
71
62
  const [files] = await this.storage.bucket(bucketName).getFiles({
72
63
  prefix,
73
64
  })
74
- return files.map(f => f.name)
65
+
66
+ if (fullPaths) {
67
+ return files.map(f => f.name)
68
+ }
69
+
70
+ return files.map(f => _substringAfterLast(f.name, '/'))
75
71
  }
76
72
 
77
- getFileNamesStream(
78
- bucketName: string,
79
- prefix: string,
80
- opt: CommonStorageGetOptions = {},
81
- ): ReadableTyped<string> {
73
+ getFileNamesStream(bucketName: string, opt: CommonStorageGetOptions = {}): ReadableTyped<string> {
74
+ const { prefix, fullPaths = true } = opt
75
+
82
76
  return this.storage
83
77
  .bucket(bucketName)
84
78
  .getFilesStream({
85
79
  prefix,
86
80
  maxResults: opt.limit,
87
81
  })
88
- .pipe(transformMapSimple<File, string>(f => f.name))
82
+ .pipe(
83
+ transformMapSimple<File, string>(f =>
84
+ fullPaths ? f.name : _substringAfterLast(f.name, '/'),
85
+ ),
86
+ )
89
87
  }
90
88
 
91
- getFilesStream(
92
- bucketName: string,
93
- prefix: string,
94
- opt: CommonStorageGetOptions = {},
95
- ): ReadableTyped<FileEntry> {
89
+ getFilesStream(bucketName: string, opt: CommonStorageGetOptions = {}): ReadableTyped<FileEntry> {
90
+ const { prefix, fullPaths = true } = opt
91
+
96
92
  return this.storage
97
93
  .bucket(bucketName)
98
94
  .getFilesStream({
@@ -102,7 +98,7 @@ export class CloudStorage implements CommonStorage {
102
98
  .pipe(
103
99
  transformMap<File, FileEntry>(async f => {
104
100
  const [content] = await f.download()
105
- return { filePath: f.name, content }
101
+ return { filePath: fullPaths ? f.name : _substringAfterLast(f.name, '/'), content }
106
102
  }),
107
103
  )
108
104
  }
@@ -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, prefix: string): Promise<string[]>
80
-
81
- getFileNamesStream(
82
- bucketName: string,
83
- prefix: string,
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
 
@@ -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,16 +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(prefix: string): Promise<string[]> {
149
- return await this.cfg.storage.getFileNames(this.cfg.bucketName, prefix)
158
+ async getFileNames(opt?: CommonStorageGetOptions): Promise<string[]> {
159
+ return await this.cfg.storage.getFileNames(this.cfg.bucketName, opt)
150
160
  }
151
161
 
152
- getFileNamesStream(prefix: string, opt?: CommonStorageGetOptions): ReadableTyped<string> {
153
- return this.cfg.storage.getFileNamesStream(this.cfg.bucketName, prefix, opt)
162
+ getFileNamesStream(opt?: CommonStorageGetOptions): ReadableTyped<string> {
163
+ return this.cfg.storage.getFileNamesStream(this.cfg.bucketName, opt)
154
164
  }
155
165
 
156
- getFilesStream(prefix: string, opt?: CommonStorageGetOptions): ReadableTyped<FileEntry> {
157
- return this.cfg.storage.getFilesStream(this.cfg.bucketName, prefix, opt)
166
+ getFilesStream(opt?: CommonStorageGetOptions): ReadableTyped<FileEntry> {
167
+ return this.cfg.storage.getFilesStream(this.cfg.bucketName, opt)
158
168
  }
159
169
 
160
170
  getFileReadStream(filePath: string): Readable {
@@ -78,30 +78,26 @@ export class CommonStorageKeyValueDB implements CommonKeyValueDB {
78
78
 
79
79
  streamIds(table: string, limit?: number): ReadableTyped<string> {
80
80
  const { bucketName, prefix } = this.getBucketAndPrefix(table)
81
- const index = prefix.length + 1
82
81
 
83
- return this.cfg.storage
84
- .getFileNamesStream(bucketName, prefix, { limit })
85
- .pipe(transformMapSimple<string, string>(f => f.slice(index)))
82
+ return this.cfg.storage.getFileNamesStream(bucketName, { prefix, limit, fullPaths: false })
86
83
  }
87
84
 
88
85
  streamValues(table: string, limit?: number): ReadableTyped<Buffer> {
89
86
  const { bucketName, prefix } = this.getBucketAndPrefix(table)
90
87
 
91
88
  return this.cfg.storage
92
- .getFilesStream(bucketName, prefix, { limit })
89
+ .getFilesStream(bucketName, { prefix, limit })
93
90
  .pipe(transformMapSimple<FileEntry, Buffer>(f => f.content))
94
91
  }
95
92
 
96
93
  streamEntries(table: string, limit?: number): ReadableTyped<KeyValueDBTuple> {
97
94
  const { bucketName, prefix } = this.getBucketAndPrefix(table)
98
- const index = prefix.length + 1
99
95
 
100
96
  return this.cfg.storage
101
- .getFilesStream(bucketName, prefix, { limit })
97
+ .getFilesStream(bucketName, { prefix, limit, fullPaths: false })
102
98
  .pipe(
103
99
  transformMapSimple<FileEntry, KeyValueDBTuple>(({ filePath, content }) => [
104
- filePath.slice(index),
100
+ filePath,
105
101
  content,
106
102
  ]),
107
103
  )
@@ -110,6 +106,6 @@ export class CommonStorageKeyValueDB implements CommonKeyValueDB {
110
106
  async count(table: string): Promise<number> {
111
107
  const { bucketName, prefix } = this.getBucketAndPrefix(table)
112
108
 
113
- return (await this.cfg.storage.getFileNames(bucketName, prefix)).length
109
+ return (await this.cfg.storage.getFileNames(bucketName, { prefix })).length
114
110
  }
115
111
  }
@@ -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, prefix: string): Promise<string[]> {
46
- return Object.keys(this.data[bucketName] || {}).filter(filePath => filePath.startsWith(prefix))
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
- bucketName: string,
51
- prefix: string,
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
- bucketName: string,
63
- prefix: string,
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]) => ({ 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
- 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
- })
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
- const fileNames = await storage.getFileNames(bucketName, '')
63
- expect(fileNames).toEqual([])
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('streamFileNames on root should return empty', async () => {
72
- const fileNames = await readableToArray(storage.getFileNamesStream(bucketName, ''))
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 fileNames = await storage.getFileNames(bucketName, TEST_FOLDER)
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