@naturalcycles/cloud-storage-lib 1.0.0 → 1.3.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.
@@ -25,9 +25,9 @@ export declare class CloudStorage implements CommonStorage {
25
25
  getBucketNamesStream(): ReadableTyped<string>;
26
26
  deletePath(bucketName: string, prefix: string): Promise<void>;
27
27
  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>;
28
+ getFileNames(bucketName: string, opt?: CommonStorageGetOptions): Promise<string[]>;
29
+ getFileNamesStream(bucketName: string, opt?: CommonStorageGetOptions): ReadableTyped<string>;
30
+ getFilesStream(bucketName: string, opt?: CommonStorageGetOptions): ReadableTyped<FileEntry>;
31
31
  getFile(bucketName: string, filePath: string): Promise<Buffer | null>;
32
32
  /**
33
33
  * Returns a Readable that is NOT object mode,
@@ -1,6 +1,7 @@
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 {
@@ -43,22 +44,28 @@ class CloudStorage {
43
44
  const [exists] = await this.storage.bucket(bucketName).file(filePath).exists();
44
45
  return exists;
45
46
  }
46
- async getFileNames(bucketName, prefix) {
47
+ async getFileNames(bucketName, opt = {}) {
48
+ const { prefix, fullPaths = true } = opt;
47
49
  const [files] = await this.storage.bucket(bucketName).getFiles({
48
50
  prefix,
49
51
  });
50
- return files.map(f => f.name);
52
+ if (fullPaths) {
53
+ return files.map(f => f.name);
54
+ }
55
+ return files.map(f => (0, js_lib_1._substringAfterLast)(f.name, '/'));
51
56
  }
52
- getFileNamesStream(bucketName, prefix, opt = {}) {
57
+ getFileNamesStream(bucketName, opt = {}) {
58
+ const { prefix, fullPaths = true } = opt;
53
59
  return this.storage
54
60
  .bucket(bucketName)
55
61
  .getFilesStream({
56
62
  prefix,
57
63
  maxResults: opt.limit,
58
64
  })
59
- .pipe((0, nodejs_lib_1.transformMapSimple)(f => f.name));
65
+ .pipe((0, nodejs_lib_1.transformMapSimple)(f => fullPaths ? f.name : (0, js_lib_1._substringAfterLast)(f.name, '/')));
60
66
  }
61
- getFilesStream(bucketName, prefix, opt = {}) {
67
+ getFilesStream(bucketName, opt = {}) {
68
+ const { prefix } = opt;
62
69
  return this.storage
63
70
  .bucket(bucketName)
64
71
  .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
  *
@@ -62,9 +67,9 @@ export interface CommonStorage {
62
67
  * Important difference between `prefix` and `path` is that `prefix` will
63
68
  * return all files from sub-directories too!
64
69
  */
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>;
70
+ getFileNames(bucketName: string, opt?: CommonStorageGetOptions): Promise<string[]>;
71
+ getFileNamesStream(bucketName: string, opt?: CommonStorageGetOptions): ReadableTyped<string>;
72
+ getFilesStream(bucketName: string, opt?: CommonStorageGetOptions): ReadableTyped<FileEntry>;
68
73
  getFileReadStream(bucketName: string, filePath: string): Readable;
69
74
  getFileWriteStream(bucketName: string, filePath: string): Writable;
70
75
  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);
@@ -29,4 +29,5 @@ export declare class CommonStorageKeyValueDB implements CommonKeyValueDB {
29
29
  streamIds(table: string, limit?: number): ReadableTyped<string>;
30
30
  streamValues(table: string, limit?: number): ReadableTyped<Buffer>;
31
31
  streamEntries(table: string, limit?: number): ReadableTyped<KeyValueDBTuple>;
32
+ count(table: string): Promise<number>;
32
33
  }
@@ -62,26 +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
  }
82
+ async count(table) {
83
+ const { bucketName, prefix } = this.getBucketAndPrefix(table);
84
+ return (await this.cfg.storage.getFileNames(bucketName, { prefix })).length;
85
+ }
86
86
  }
87
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]);
@@ -47,15 +47,15 @@ function runCommonStorageTest(storage, bucketName) {
47
47
  await (0, js_lib_1.pMap)(TEST_FILES.map(f => f.filePath), async (filePath) => await storage.deletePath(bucketName, filePath));
48
48
  });
49
49
  test('listFileNames on root should return empty', async () => {
50
- const fileNames = await storage.getFileNames(bucketName, '');
50
+ const fileNames = await storage.getFileNames(bucketName);
51
51
  expect(fileNames).toEqual([]);
52
52
  });
53
53
  test(`listFileNames on ${TEST_FOLDER} should return empty`, async () => {
54
- const fileNames = await storage.getFileNames(bucketName, TEST_FOLDER);
54
+ const fileNames = await storage.getFileNames(bucketName, { prefix: TEST_FOLDER });
55
55
  expect(fileNames).toEqual([]);
56
56
  });
57
57
  test('streamFileNames on root should return empty', async () => {
58
- const fileNames = await (0, nodejs_lib_1.readableToArray)(storage.getFileNamesStream(bucketName, ''));
58
+ const fileNames = await (0, nodejs_lib_1.readableToArray)(storage.getFileNamesStream(bucketName));
59
59
  expect(fileNames).toEqual([]);
60
60
  });
61
61
  test(`exists should return empty array`, async () => {
@@ -68,9 +68,14 @@ function runCommonStorageTest(storage, bucketName) {
68
68
  const testFilesMap = Object.fromEntries(TEST_FILES.map(f => [f.filePath, f.content]));
69
69
  // It's done in the same test to ensure "strong consistency"
70
70
  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);
71
+ const fileNamesShort = await storage.getFileNames(bucketName, {
72
+ prefix: TEST_FOLDER,
73
+ fullPaths: false,
74
+ });
75
+ expect(fileNamesShort.sort()).toEqual(TEST_FILES.map(f => (0, js_lib_1._substringAfterLast)(f.filePath, '/')).sort());
76
+ const fileNames = await storage.getFileNames(bucketName, { prefix: TEST_FOLDER });
72
77
  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));
78
+ const streamedFileNames = await (0, nodejs_lib_1.readableToArray)(storage.getFileNamesStream(bucketName, { prefix: TEST_FOLDER }));
74
79
  expect(streamedFileNames.sort()).toEqual(TEST_FILES.map(f => f.filePath).sort());
75
80
  const filesMap = {};
76
81
  await (0, js_lib_1.pMap)(fileNames, async (filePath) => {
package/package.json CHANGED
@@ -34,7 +34,7 @@
34
34
  "engines": {
35
35
  "node": ">=14.16.0"
36
36
  },
37
- "version": "1.0.0",
37
+ "version": "1.3.0",
38
38
  "description": "",
39
39
  "author": "Natural Cycles Team",
40
40
  "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,5 +1,6 @@
1
1
  import * as Buffer from 'buffer'
2
2
  import { Readable, Writable } from 'stream'
3
+ import { _substringAfterLast } from '@naturalcycles/js-lib'
3
4
  import { Bucket, File, Storage } from '@google-cloud/storage'
4
5
  import { ReadableTyped, transformMap, transformMapSimple } from '@naturalcycles/nodejs-lib'
5
6
  import { CommonStorage, CommonStorageGetOptions, FileEntry } from './commonStorage'
@@ -67,32 +68,38 @@ export class CloudStorage implements CommonStorage {
67
68
  return exists
68
69
  }
69
70
 
70
- async getFileNames(bucketName: string, prefix: string): Promise<string[]> {
71
+ async getFileNames(bucketName: string, opt: CommonStorageGetOptions = {}): Promise<string[]> {
72
+ const { prefix, fullPaths = true } = opt
71
73
  const [files] = await this.storage.bucket(bucketName).getFiles({
72
74
  prefix,
73
75
  })
74
- return files.map(f => f.name)
76
+
77
+ if (fullPaths) {
78
+ return files.map(f => f.name)
79
+ }
80
+
81
+ return files.map(f => _substringAfterLast(f.name, '/'))
75
82
  }
76
83
 
77
- getFileNamesStream(
78
- bucketName: string,
79
- prefix: string,
80
- opt: CommonStorageGetOptions = {},
81
- ): ReadableTyped<string> {
84
+ getFileNamesStream(bucketName: string, opt: CommonStorageGetOptions = {}): ReadableTyped<string> {
85
+ const { prefix, fullPaths = true } = opt
86
+
82
87
  return this.storage
83
88
  .bucket(bucketName)
84
89
  .getFilesStream({
85
90
  prefix,
86
91
  maxResults: opt.limit,
87
92
  })
88
- .pipe(transformMapSimple<File, string>(f => f.name))
93
+ .pipe(
94
+ transformMapSimple<File, string>(f =>
95
+ fullPaths ? f.name : _substringAfterLast(f.name, '/'),
96
+ ),
97
+ )
89
98
  }
90
99
 
91
- getFilesStream(
92
- bucketName: string,
93
- prefix: string,
94
- opt: CommonStorageGetOptions = {},
95
- ): ReadableTyped<FileEntry> {
100
+ getFilesStream(bucketName: string, opt: CommonStorageGetOptions = {}): ReadableTyped<FileEntry> {
101
+ const { prefix } = opt
102
+
96
103
  return this.storage
97
104
  .bucket(bucketName)
98
105
  .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
  *
@@ -76,19 +82,11 @@ export interface CommonStorage {
76
82
  * Important difference between `prefix` and `path` is that `prefix` will
77
83
  * return all files from sub-directories too!
78
84
  */
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>
85
+ getFileNames(bucketName: string, opt?: CommonStorageGetOptions): Promise<string[]>
86
+
87
+ getFileNamesStream(bucketName: string, opt?: CommonStorageGetOptions): ReadableTyped<string>
88
+
89
+ getFilesStream(bucketName: string, opt?: CommonStorageGetOptions): ReadableTyped<FileEntry>
92
90
 
93
91
  getFileReadStream(bucketName: string, filePath: string): Readable
94
92
 
@@ -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,32 +78,34 @@ 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
  )
108
104
  }
105
+
106
+ async count(table: string): Promise<number> {
107
+ const { bucketName, prefix } = this.getBucketAndPrefix(table)
108
+
109
+ return (await this.cfg.storage.getFileNames(bucketName, { prefix })).length
110
+ }
109
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
 
@@ -59,17 +59,17 @@ export function runCommonStorageTest(storage: CommonStorage, bucketName: string)
59
59
  })
60
60
 
61
61
  test('listFileNames on root should return empty', async () => {
62
- const fileNames = await storage.getFileNames(bucketName, '')
62
+ const fileNames = await storage.getFileNames(bucketName)
63
63
  expect(fileNames).toEqual([])
64
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
71
  test('streamFileNames on root should return empty', async () => {
72
- const fileNames = await readableToArray(storage.getFileNamesStream(bucketName, ''))
72
+ const fileNames = await readableToArray(storage.getFileNamesStream(bucketName))
73
73
  expect(fileNames).toEqual([])
74
74
  })
75
75
 
@@ -86,11 +86,19 @@ export function runCommonStorageTest(storage: CommonStorage, bucketName: string)
86
86
  // It's done in the same test to ensure "strong consistency"
87
87
  await pMap(TEST_FILES, async f => await storage.saveFile(bucketName, f.filePath, f.content))
88
88
 
89
- const fileNames = await storage.getFileNames(bucketName, TEST_FOLDER)
89
+ const fileNamesShort = await storage.getFileNames(bucketName, {
90
+ prefix: TEST_FOLDER,
91
+ fullPaths: false,
92
+ })
93
+ expect(fileNamesShort.sort()).toEqual(
94
+ TEST_FILES.map(f => _substringAfterLast(f.filePath, '/')).sort(),
95
+ )
96
+
97
+ const fileNames = await storage.getFileNames(bucketName, { prefix: TEST_FOLDER })
90
98
  expect(fileNames.sort()).toEqual(TEST_FILES.map(f => f.filePath).sort())
91
99
 
92
100
  const streamedFileNames = await readableToArray(
93
- storage.getFileNamesStream(bucketName, TEST_FOLDER),
101
+ storage.getFileNamesStream(bucketName, { prefix: TEST_FOLDER }),
94
102
  )
95
103
  expect(streamedFileNames.sort()).toEqual(TEST_FILES.map(f => f.filePath).sort())
96
104