@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.
@@ -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
- credentials: GCPServiceAccount;
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, prefix: string): Promise<string[]>;
29
- getFileNamesStream(bucketName: string, prefix: string, opt?: CommonStorageGetOptions): ReadableTyped<string>;
30
- getFilesStream(bucketName: string, prefix: string, opt?: CommonStorageGetOptions): ReadableTyped<FileEntry>;
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,
@@ -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
- 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.
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.project_id,
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, prefix) {
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
- return files.map(f => f.name);
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, prefix, opt = {}) {
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, prefix, opt = {}) {
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) {
@@ -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,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(prefix: string, opt?: {
54
- fullPath?: boolean;
55
- }): Promise<string[]>;
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(prefix, opt = {}) {
111
- const { fullPath = true } = opt;
112
- const names = await this.cfg.storage.getFileNames(this.cfg.bucketName, prefix);
113
- if (!fullPath && prefix) {
114
- const start = `${prefix}/`.length;
115
- return names.map(n => n.slice(start));
116
- }
117
- return names;
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
- 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": "^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.2.0",
38
+ "version": "1.5.0",
38
39
  "description": "",
39
40
  "author": "Natural Cycles Team",
40
41
  "license": "MIT"
@@ -1,6 +1,6 @@
1
- import * as Buffer from 'buffer'
2
1
  import { Readable, Writable } from 'stream'
3
- import { Bucket, File, Storage } from '@google-cloud/storage'
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
- credentials: GCPServiceAccount
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
- constructor(public cfg: CloudStorageCfg) {
23
- this.storage = new Storage({
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.project_id,
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
- async ping(bucketName?: string): Promise<void> {
42
- await this.storage.bucket(bucketName || 'non-existing-for-sure').exists()
42
+ return new CloudStorage(storage)
43
43
  }
44
44
 
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)
45
+ static createFromStorageOptions(storageOptions?: StorageOptions): CloudStorage {
46
+ const storage = new Storage(storageOptions)
47
+ return new CloudStorage(storage)
51
48
  }
52
49
 
53
- getBucketNamesStream(): ReadableTyped<string> {
54
- return this.storage.getBucketsStream().pipe(transformMapSimple<Bucket, string>(b => b.name))
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, prefix: string): Promise<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
- return files.map(f => f.name)
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
- bucketName: string,
79
- prefix: string,
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(transformMapSimple<File, string>(f => f.name))
89
+ .pipe(
90
+ transformMapSimple<File, string>(f =>
91
+ fullPaths ? f.name : _substringAfterLast(f.name, '/'),
92
+ ),
93
+ )
89
94
  }
90
95
 
91
- getFilesStream(
92
- bucketName: string,
93
- prefix: string,
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
  }
@@ -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
 
@@ -1,6 +1,6 @@
1
1
  import { Readable, Writable } from 'stream'
2
2
  import { AppError, pMap } from '@naturalcycles/js-lib'
3
- import { ReadableTyped, transformMapSimple } from '@naturalcycles/nodejs-lib'
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(prefix: string, opt: { fullPath?: boolean } = {}): Promise<string[]> {
149
- const { fullPath = true } = opt
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
- prefix: string,
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(prefix: string, opt?: CommonStorageGetOptions): ReadableTyped<FileEntry> {
177
- 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)
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, { limit })
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, { limit })
96
+ .getFilesStream(bucketName, { prefix, limit, fullPaths: false })
102
97
  .pipe(
103
98
  transformMapSimple<FileEntry, KeyValueDBTuple>(({ filePath, content }) => [
104
- filePath.slice(index),
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, 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