@naturalcycles/cloud-storage-lib 1.0.1 → 1.4.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.
@@ -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 } = opt;
62
63
  return this.storage
63
64
  .bucket(bucketName)
64
65
  .getFilesStream({
@@ -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,14 @@ 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>;
40
+ saveStringFile(filePath: string, content: string): Promise<void>;
41
+ saveJsonFile(filePath: string, content: any): Promise<void>;
34
42
  saveFiles(entries: FileEntry[]): Promise<void>;
35
43
  /**
36
44
  * Should recursively delete all files in a folder, if path is a folder.
@@ -48,9 +56,9 @@ export declare class CommonStorageBucket {
48
56
  * Important difference between `prefix` and `path` is that `prefix` will
49
57
  * return all files from sub-directories too!
50
58
  */
51
- getFileNames(prefix: string): Promise<string[]>;
52
- getFileNamesStream(prefix: string, opt?: CommonStorageGetOptions): ReadableTyped<string>;
53
- 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>;
54
62
  getFileReadStream(filePath: string): Readable;
55
63
  getFileWriteStream(filePath: string): Writable;
56
64
  setFileVisibility(filePath: string, isPublic: boolean): Promise<void>;
@@ -73,6 +73,21 @@ 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
+ }
85
+ async saveStringFile(filePath, content) {
86
+ await this.cfg.storage.saveFile(this.cfg.bucketName, filePath, Buffer.from(content));
87
+ }
88
+ async saveJsonFile(filePath, content) {
89
+ await this.cfg.storage.saveFile(this.cfg.bucketName, filePath, Buffer.from(JSON.stringify(content)));
90
+ }
76
91
  async saveFiles(entries) {
77
92
  await (0, js_lib_1.pMap)(entries, async (f) => {
78
93
  await this.cfg.storage.saveFile(this.cfg.bucketName, f.filePath, f.content);
@@ -100,14 +115,14 @@ class CommonStorageBucket {
100
115
  * Important difference between `prefix` and `path` is that `prefix` will
101
116
  * return all files from sub-directories too!
102
117
  */
103
- async getFileNames(prefix) {
104
- 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);
105
120
  }
106
- getFileNamesStream(prefix, opt) {
107
- return this.cfg.storage.getFileNamesStream(this.cfg.bucketName, prefix, opt);
121
+ getFileNamesStream(opt) {
122
+ return this.cfg.storage.getFileNamesStream(this.cfg.bucketName, opt);
108
123
  }
109
- getFilesStream(prefix, opt) {
110
- return this.cfg.storage.getFilesStream(this.cfg.bucketName, prefix, opt);
124
+ getFilesStream(opt) {
125
+ return this.cfg.storage.getFilesStream(this.cfg.bucketName, opt);
111
126
  }
112
127
  getFileReadStream(filePath) {
113
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
@@ -12,6 +12,7 @@
12
12
  "devDependencies": {
13
13
  "@naturalcycles/dev-lib": "^12.1.3",
14
14
  "@types/node": "^17.0.5",
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.0.1",
38
+ "version": "1.4.0",
38
39
  "description": "",
39
40
  "author": "Natural Cycles Team",
40
41
  "license": "MIT"
package/readme.md CHANGED
@@ -3,7 +3,6 @@
3
3
  > CommonStorage implementation based on Google Cloud Storage
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/@naturalcycles/cloud-storage-lib/latest.svg)](https://www.npmjs.com/package/@naturalcycles/cloud-storage-lib)
6
- [![min.gz size](https://badgen.net/bundlephobia/minzip/@naturalcycles/cloud-storage-lib)](https://bundlephobia.com/result?p=@naturalcycles/cloud-storage-lib)
7
6
  [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier)
8
7
 
9
8
  Implements:
@@ -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 } = opt
91
+
96
92
  return this.storage
97
93
  .bucket(bucketName)
98
94
  .getFilesStream({
@@ -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,28 @@ 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
+
116
+ async saveStringFile(filePath: string, content: string): Promise<void> {
117
+ await this.cfg.storage.saveFile(this.cfg.bucketName, filePath, Buffer.from(content))
118
+ }
119
+
120
+ async saveJsonFile(filePath: string, content: any): Promise<void> {
121
+ await this.cfg.storage.saveFile(
122
+ this.cfg.bucketName,
123
+ filePath,
124
+ Buffer.from(JSON.stringify(content)),
125
+ )
126
+ }
127
+
106
128
  async saveFiles(entries: FileEntry[]): Promise<void> {
107
129
  await pMap(entries, async f => {
108
130
  await this.cfg.storage.saveFile(this.cfg.bucketName, f.filePath, f.content)
@@ -133,16 +155,16 @@ export class CommonStorageBucket {
133
155
  * Important difference between `prefix` and `path` is that `prefix` will
134
156
  * return all files from sub-directories too!
135
157
  */
136
- async getFileNames(prefix: string): Promise<string[]> {
137
- 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)
138
160
  }
139
161
 
140
- getFileNamesStream(prefix: string, opt?: CommonStorageGetOptions): ReadableTyped<string> {
141
- 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)
142
164
  }
143
165
 
144
- getFilesStream(prefix: string, opt?: CommonStorageGetOptions): ReadableTyped<FileEntry> {
145
- 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)
146
168
  }
147
169
 
148
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