@nixxie-cms/storage 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,391 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { mkdir, writeFile, readFile, rm, stat, readdir } from 'node:fs/promises';
3
+ import { resolve, sep, dirname, join, relative, posix } from 'node:path';
4
+
5
+ function loadAzure() {
6
+ try {
7
+ return require('@azure/storage-blob');
8
+ } catch {
9
+ throw new Error('@azure/storage-blob is required for the Azure driver. Run: npm install @azure/storage-blob');
10
+ }
11
+ }
12
+
13
+ /** Azure Blob Storage backend. */
14
+ class AzureStorage {
15
+ constructor(config) {
16
+ this.config = config;
17
+ this.prefix = config.prefix ? config.prefix.replace(/\/$/, '') + '/' : '';
18
+ const {
19
+ BlobServiceClient
20
+ } = loadAzure();
21
+ const service = BlobServiceClient.fromConnectionString(config.connectionString);
22
+ this.container = service.getContainerClient(config.container);
23
+ }
24
+ blob(key) {
25
+ return this.container.getBlockBlobClient(`${this.prefix}${key}`);
26
+ }
27
+ async put(key, data, options) {
28
+ const body = typeof data === 'string' ? Buffer.from(data) : Buffer.from(data);
29
+ await this.blob(key).uploadData(body, {
30
+ blobHTTPHeaders: options !== null && options !== void 0 && options.contentType ? {
31
+ blobContentType: options.contentType
32
+ } : undefined,
33
+ metadata: options === null || options === void 0 ? void 0 : options.metadata
34
+ });
35
+ return {
36
+ key,
37
+ url: this.url(key),
38
+ size: body.byteLength,
39
+ contentType: options === null || options === void 0 ? void 0 : options.contentType
40
+ };
41
+ }
42
+ async get(key) {
43
+ try {
44
+ return await this.blob(key).downloadToBuffer();
45
+ } catch (err) {
46
+ if ((err === null || err === void 0 ? void 0 : err.statusCode) === 404) return undefined;
47
+ throw err;
48
+ }
49
+ }
50
+ async delete(key) {
51
+ await this.blob(key).deleteIfExists();
52
+ }
53
+ async has(key) {
54
+ return Boolean(await this.blob(key).exists());
55
+ }
56
+ url(key) {
57
+ if (this.config.publicBaseUrl) return `${this.config.publicBaseUrl.replace(/\/$/, '')}/${this.prefix}${key}`;
58
+ return this.blob(key).url;
59
+ }
60
+ async signedUrl(key, options) {
61
+ var _credential, _options$expiresIn;
62
+ const {
63
+ BlobSASPermissions,
64
+ generateBlobSASQueryParameters
65
+ } = loadAzure();
66
+ const blob = this.blob(key);
67
+ const credential = (_credential = this.container.credential) !== null && _credential !== void 0 ? _credential : blob.credential;
68
+ const expiresOn = new Date(Date.now() + ((_options$expiresIn = options === null || options === void 0 ? void 0 : options.expiresIn) !== null && _options$expiresIn !== void 0 ? _options$expiresIn : 900) * 1000);
69
+ const permissions = BlobSASPermissions.parse((options === null || options === void 0 ? void 0 : options.operation) === 'put' ? 'cw' : 'r');
70
+ try {
71
+ const sas = generateBlobSASQueryParameters({
72
+ containerName: this.config.container,
73
+ blobName: `${this.prefix}${key}`,
74
+ permissions,
75
+ expiresOn
76
+ }, credential).toString();
77
+ return `${blob.url}?${sas}`;
78
+ } catch {
79
+ // Falls back to the plain URL when SAS generation is unavailable (e.g. token credential).
80
+ return blob.url;
81
+ }
82
+ }
83
+ async list(prefix = '') {
84
+ const out = [];
85
+ for await (const item of this.container.listBlobsFlat({
86
+ prefix: `${this.prefix}${prefix}`
87
+ })) {
88
+ const name = item.name;
89
+ out.push(this.prefix && name.startsWith(this.prefix) ? name.slice(this.prefix.length) : name);
90
+ }
91
+ return out;
92
+ }
93
+ }
94
+
95
+ function loadGcs() {
96
+ try {
97
+ return require('@google-cloud/storage');
98
+ } catch {
99
+ throw new Error('@google-cloud/storage is required for the GCS driver. Run: npm install @google-cloud/storage');
100
+ }
101
+ }
102
+
103
+ /** Google Cloud Storage backend. */
104
+ class GcsStorage {
105
+ constructor(config) {
106
+ this.config = config;
107
+ this.prefix = config.prefix ? config.prefix.replace(/\/$/, '') + '/' : '';
108
+ const {
109
+ Storage
110
+ } = loadGcs();
111
+ const client = new Storage({
112
+ projectId: config.projectId,
113
+ keyFilename: config.keyFilename
114
+ });
115
+ this.bucket = client.bucket(config.bucket);
116
+ }
117
+ file(key) {
118
+ return this.bucket.file(`${this.prefix}${key}`);
119
+ }
120
+ async put(key, data, options) {
121
+ const body = typeof data === 'string' ? Buffer.from(data) : Buffer.from(data);
122
+ const file = this.file(key);
123
+ await file.save(body, {
124
+ contentType: options === null || options === void 0 ? void 0 : options.contentType,
125
+ metadata: options !== null && options !== void 0 && options.metadata ? {
126
+ metadata: options.metadata
127
+ } : undefined
128
+ });
129
+ if (options !== null && options !== void 0 && options.public) await file.makePublic();
130
+ return {
131
+ key,
132
+ url: this.url(key),
133
+ size: body.byteLength,
134
+ contentType: options === null || options === void 0 ? void 0 : options.contentType
135
+ };
136
+ }
137
+ async get(key) {
138
+ try {
139
+ const [contents] = await this.file(key).download();
140
+ return contents;
141
+ } catch (err) {
142
+ if ((err === null || err === void 0 ? void 0 : err.code) === 404) return undefined;
143
+ throw err;
144
+ }
145
+ }
146
+ async delete(key) {
147
+ await this.file(key).delete({
148
+ ignoreNotFound: true
149
+ });
150
+ }
151
+ async has(key) {
152
+ const [exists] = await this.file(key).exists();
153
+ return Boolean(exists);
154
+ }
155
+ url(key) {
156
+ if (this.config.publicBaseUrl) return `${this.config.publicBaseUrl.replace(/\/$/, '')}/${this.prefix}${key}`;
157
+ return `https://storage.googleapis.com/${this.config.bucket}/${this.prefix}${key}`;
158
+ }
159
+ async signedUrl(key, options) {
160
+ var _options$expiresIn;
161
+ const [url] = await this.file(key).getSignedUrl({
162
+ action: (options === null || options === void 0 ? void 0 : options.operation) === 'put' ? 'write' : 'read',
163
+ expires: Date.now() + ((_options$expiresIn = options === null || options === void 0 ? void 0 : options.expiresIn) !== null && _options$expiresIn !== void 0 ? _options$expiresIn : 900) * 1000,
164
+ contentType: (options === null || options === void 0 ? void 0 : options.operation) === 'put' ? options === null || options === void 0 ? void 0 : options.contentType : undefined
165
+ });
166
+ return url;
167
+ }
168
+ async list(prefix = '') {
169
+ const [files] = await this.bucket.getFiles({
170
+ prefix: `${this.prefix}${prefix}`
171
+ });
172
+ return files.map(f => f.name).map(k => this.prefix && k.startsWith(this.prefix) ? k.slice(this.prefix.length) : k);
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Filesystem-backed storage. Good for development and single-node deployments.
178
+ * `signedUrl()` falls back to the plain public URL since the local disk cannot sign.
179
+ */
180
+ class LocalStorage {
181
+ constructor(config) {
182
+ var _config$baseDir, _config$baseUrl;
183
+ this.baseDir = (_config$baseDir = config.baseDir) !== null && _config$baseDir !== void 0 ? _config$baseDir : '.nixxie-storage';
184
+ this.baseUrl = ((_config$baseUrl = config.baseUrl) !== null && _config$baseUrl !== void 0 ? _config$baseUrl : '/files').replace(/\/$/, '');
185
+ }
186
+ path(key) {
187
+ // Confine resolved paths to baseDir so keys like "../../etc/passwd" (or absolute paths)
188
+ // cannot escape the storage root.
189
+ const root = resolve(this.baseDir);
190
+ const full = resolve(root, key);
191
+ if (full !== root && !full.startsWith(root + sep)) {
192
+ throw new Error(`Invalid storage key (path traversal detected): ${key}`);
193
+ }
194
+ return full;
195
+ }
196
+ async put(key, data, options) {
197
+ const filePath = this.path(key);
198
+ await mkdir(dirname(filePath), {
199
+ recursive: true
200
+ });
201
+ const buffer = typeof data === 'string' ? Buffer.from(data) : Buffer.from(data);
202
+ await writeFile(filePath, buffer);
203
+ return {
204
+ key,
205
+ url: this.url(key),
206
+ size: buffer.byteLength,
207
+ contentType: options === null || options === void 0 ? void 0 : options.contentType
208
+ };
209
+ }
210
+ async get(key) {
211
+ const filePath = this.path(key);
212
+ if (!existsSync(filePath)) return undefined;
213
+ return readFile(filePath);
214
+ }
215
+ async delete(key) {
216
+ await rm(this.path(key), {
217
+ force: true
218
+ });
219
+ }
220
+ async has(key) {
221
+ try {
222
+ await stat(this.path(key));
223
+ return true;
224
+ } catch {
225
+ return false;
226
+ }
227
+ }
228
+ url(key) {
229
+ return `${this.baseUrl}/${key.split(sep).join('/')}`;
230
+ }
231
+ async signedUrl(key, _options) {
232
+ return this.url(key);
233
+ }
234
+ async list(prefix = '') {
235
+ const root = this.baseDir;
236
+ const out = [];
237
+ const walk = async dir => {
238
+ let entries;
239
+ try {
240
+ entries = await readdir(dir, {
241
+ withFileTypes: true
242
+ });
243
+ } catch {
244
+ return;
245
+ }
246
+ for (const entry of entries) {
247
+ const full = join(dir, entry.name);
248
+ if (entry.isDirectory()) await walk(full);else out.push(relative(root, full).split(sep).join(posix.sep));
249
+ }
250
+ };
251
+ await walk(root);
252
+ return out.filter(k => k.startsWith(prefix));
253
+ }
254
+ }
255
+
256
+ function loadS3() {
257
+ try {
258
+ return {
259
+ s3: require('@aws-sdk/client-s3'),
260
+ presigner: require('@aws-sdk/s3-request-presigner')
261
+ };
262
+ } catch {
263
+ throw new Error('@aws-sdk/client-s3 and @aws-sdk/s3-request-presigner are required for the S3 driver. Run: npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner');
264
+ }
265
+ }
266
+
267
+ /** AWS S3 (and S3-compatible: R2, MinIO, Spaces) storage backend. */
268
+ class S3Storage {
269
+ constructor(config) {
270
+ this.config = config;
271
+ this.prefix = config.prefix ? config.prefix.replace(/\/$/, '') + '/' : '';
272
+ const {
273
+ s3,
274
+ presigner
275
+ } = loadS3();
276
+ this.s3 = s3;
277
+ this.presigner = presigner;
278
+ this.client = new s3.S3Client({
279
+ region: config.region,
280
+ endpoint: config.endpoint,
281
+ forcePathStyle: config.forcePathStyle,
282
+ credentials: config.accessKeyId && config.secretAccessKey ? {
283
+ accessKeyId: config.accessKeyId,
284
+ secretAccessKey: config.secretAccessKey
285
+ } : undefined
286
+ });
287
+ }
288
+ k(key) {
289
+ return `${this.prefix}${key}`;
290
+ }
291
+ async put(key, data, options) {
292
+ const body = typeof data === 'string' ? Buffer.from(data) : Buffer.from(data);
293
+ await this.client.send(new this.s3.PutObjectCommand({
294
+ Bucket: this.config.bucket,
295
+ Key: this.k(key),
296
+ Body: body,
297
+ ContentType: options === null || options === void 0 ? void 0 : options.contentType,
298
+ ACL: options !== null && options !== void 0 && options.public ? 'public-read' : undefined,
299
+ Metadata: options === null || options === void 0 ? void 0 : options.metadata
300
+ }));
301
+ return {
302
+ key,
303
+ url: this.url(key),
304
+ size: body.byteLength,
305
+ contentType: options === null || options === void 0 ? void 0 : options.contentType
306
+ };
307
+ }
308
+ async get(key) {
309
+ try {
310
+ var _res$Body;
311
+ const res = await this.client.send(new this.s3.GetObjectCommand({
312
+ Bucket: this.config.bucket,
313
+ Key: this.k(key)
314
+ }));
315
+ const bytes = await ((_res$Body = res.Body) === null || _res$Body === void 0 ? void 0 : _res$Body.transformToByteArray());
316
+ return bytes ? Buffer.from(bytes) : undefined;
317
+ } catch (err) {
318
+ var _err$$metadata;
319
+ if ((err === null || err === void 0 ? void 0 : err.name) === 'NoSuchKey' || (err === null || err === void 0 || (_err$$metadata = err.$metadata) === null || _err$$metadata === void 0 ? void 0 : _err$$metadata.httpStatusCode) === 404) return undefined;
320
+ throw err;
321
+ }
322
+ }
323
+ async delete(key) {
324
+ await this.client.send(new this.s3.DeleteObjectCommand({
325
+ Bucket: this.config.bucket,
326
+ Key: this.k(key)
327
+ }));
328
+ }
329
+ async has(key) {
330
+ try {
331
+ await this.client.send(new this.s3.HeadObjectCommand({
332
+ Bucket: this.config.bucket,
333
+ Key: this.k(key)
334
+ }));
335
+ return true;
336
+ } catch {
337
+ return false;
338
+ }
339
+ }
340
+ url(key) {
341
+ var _this$config$region;
342
+ if (this.config.publicBaseUrl) return `${this.config.publicBaseUrl.replace(/\/$/, '')}/${this.k(key)}`;
343
+ const host = this.config.endpoint ? `${this.config.endpoint.replace(/\/$/, '')}/${this.config.bucket}` : `https://${this.config.bucket}.s3.${(_this$config$region = this.config.region) !== null && _this$config$region !== void 0 ? _this$config$region : 'us-east-1'}.amazonaws.com`;
344
+ return `${host}/${this.k(key)}`;
345
+ }
346
+ async signedUrl(key, options) {
347
+ var _options$expiresIn;
348
+ const command = (options === null || options === void 0 ? void 0 : options.operation) === 'put' ? new this.s3.PutObjectCommand({
349
+ Bucket: this.config.bucket,
350
+ Key: this.k(key),
351
+ ContentType: options.contentType
352
+ }) : new this.s3.GetObjectCommand({
353
+ Bucket: this.config.bucket,
354
+ Key: this.k(key)
355
+ });
356
+ return this.presigner.getSignedUrl(this.client, command, {
357
+ expiresIn: (_options$expiresIn = options === null || options === void 0 ? void 0 : options.expiresIn) !== null && _options$expiresIn !== void 0 ? _options$expiresIn : 900
358
+ });
359
+ }
360
+ async list(prefix = '') {
361
+ var _res$Contents;
362
+ const res = await this.client.send(new this.s3.ListObjectsV2Command({
363
+ Bucket: this.config.bucket,
364
+ Prefix: this.k(prefix)
365
+ }));
366
+ return ((_res$Contents = res.Contents) !== null && _res$Contents !== void 0 ? _res$Contents : []).map(o => {
367
+ var _o$Key;
368
+ return (_o$Key = o.Key) !== null && _o$Key !== void 0 ? _o$Key : '';
369
+ }).map(k => this.prefix && k.startsWith(this.prefix) ? k.slice(this.prefix.length) : k).filter(Boolean);
370
+ }
371
+ }
372
+
373
+ function createStorage(config) {
374
+ switch (config.driver) {
375
+ case 'local':
376
+ return new LocalStorage(config);
377
+ case 's3':
378
+ return new S3Storage(config);
379
+ case 'gcs':
380
+ return new GcsStorage(config);
381
+ case 'azure':
382
+ return new AzureStorage(config);
383
+ default:
384
+ {
385
+ const exhaustive = config;
386
+ throw new Error(`Unknown storage driver: ${exhaustive.driver}`);
387
+ }
388
+ }
389
+ }
390
+
391
+ export { AzureStorage, GcsStorage, LocalStorage, S3Storage, createStorage };
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@nixxie-cms/storage",
3
+ "version": "1.0.1",
4
+ "license": "MIT",
5
+ "main": "dist/nixxie-cms-storage.cjs.js",
6
+ "module": "dist/nixxie-cms-storage.esm.js",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/nixxie-cms-storage.cjs.js",
10
+ "module": "./dist/nixxie-cms-storage.esm.js",
11
+ "default": "./dist/nixxie-cms-storage.cjs.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "dependencies": {
16
+ "@babel/runtime": "^7.24.7"
17
+ },
18
+ "devDependencies": {
19
+ "@aws-sdk/client-s3": "^3.1037.0",
20
+ "@aws-sdk/s3-request-presigner": "^3.1037.0",
21
+ "@nixxie-cms/core": "^1.0.1"
22
+ },
23
+ "peerDependencies": {
24
+ "@nixxie-cms/core": "^1.0.1"
25
+ },
26
+ "optionalDependencies": {
27
+ "@aws-sdk/client-s3": "^3.1037.0",
28
+ "@aws-sdk/s3-request-presigner": "^3.1037.0"
29
+ },
30
+ "preconstruct": {
31
+ "entrypoints": [
32
+ "index.ts"
33
+ ]
34
+ },
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/nixxiecms/nixxie/tree/main/packages/storage"
38
+ }
39
+ }
@@ -0,0 +1,104 @@
1
+ import type {
2
+ NixxiePutOptions,
3
+ NixxieSignedUrlOptions,
4
+ NixxieStorageService,
5
+ NixxieStoredFile,
6
+ } from '@nixxie-cms/core'
7
+ import type { AzureStorageConfig } from './types'
8
+
9
+ function loadAzure(): any {
10
+ try {
11
+ return require('@azure/storage-blob')
12
+ } catch {
13
+ throw new Error(
14
+ '@azure/storage-blob is required for the Azure driver. Run: npm install @azure/storage-blob'
15
+ )
16
+ }
17
+ }
18
+
19
+ /** Azure Blob Storage backend. */
20
+ export class AzureStorage implements NixxieStorageService {
21
+ private config: AzureStorageConfig
22
+ private prefix: string
23
+ private container: any
24
+
25
+ constructor(config: AzureStorageConfig) {
26
+ this.config = config
27
+ this.prefix = config.prefix ? config.prefix.replace(/\/$/, '') + '/' : ''
28
+ const { BlobServiceClient } = loadAzure()
29
+ const service = BlobServiceClient.fromConnectionString(config.connectionString)
30
+ this.container = service.getContainerClient(config.container)
31
+ }
32
+
33
+ private blob(key: string): any {
34
+ return this.container.getBlockBlobClient(`${this.prefix}${key}`)
35
+ }
36
+
37
+ async put(
38
+ key: string,
39
+ data: Buffer | Uint8Array | string,
40
+ options?: NixxiePutOptions
41
+ ): Promise<NixxieStoredFile> {
42
+ const body = typeof data === 'string' ? Buffer.from(data) : Buffer.from(data)
43
+ await this.blob(key).uploadData(body, {
44
+ blobHTTPHeaders: options?.contentType ? { blobContentType: options.contentType } : undefined,
45
+ metadata: options?.metadata,
46
+ })
47
+ return { key, url: this.url(key), size: body.byteLength, contentType: options?.contentType }
48
+ }
49
+
50
+ async get(key: string): Promise<Buffer | undefined> {
51
+ try {
52
+ return (await this.blob(key).downloadToBuffer()) as Buffer
53
+ } catch (err: any) {
54
+ if (err?.statusCode === 404) return undefined
55
+ throw err
56
+ }
57
+ }
58
+
59
+ async delete(key: string): Promise<void> {
60
+ await this.blob(key).deleteIfExists()
61
+ }
62
+
63
+ async has(key: string): Promise<boolean> {
64
+ return Boolean(await this.blob(key).exists())
65
+ }
66
+
67
+ url(key: string): string {
68
+ if (this.config.publicBaseUrl)
69
+ return `${this.config.publicBaseUrl.replace(/\/$/, '')}/${this.prefix}${key}`
70
+ return this.blob(key).url as string
71
+ }
72
+
73
+ async signedUrl(key: string, options?: NixxieSignedUrlOptions): Promise<string> {
74
+ const { BlobSASPermissions, generateBlobSASQueryParameters } = loadAzure()
75
+ const blob = this.blob(key)
76
+ const credential = (this.container as any).credential ?? blob.credential
77
+ const expiresOn = new Date(Date.now() + (options?.expiresIn ?? 900) * 1000)
78
+ const permissions = BlobSASPermissions.parse(options?.operation === 'put' ? 'cw' : 'r')
79
+ try {
80
+ const sas = generateBlobSASQueryParameters(
81
+ {
82
+ containerName: this.config.container,
83
+ blobName: `${this.prefix}${key}`,
84
+ permissions,
85
+ expiresOn,
86
+ },
87
+ credential
88
+ ).toString()
89
+ return `${blob.url}?${sas}`
90
+ } catch {
91
+ // Falls back to the plain URL when SAS generation is unavailable (e.g. token credential).
92
+ return blob.url as string
93
+ }
94
+ }
95
+
96
+ async list(prefix = ''): Promise<string[]> {
97
+ const out: string[] = []
98
+ for await (const item of this.container.listBlobsFlat({ prefix: `${this.prefix}${prefix}` })) {
99
+ const name = item.name as string
100
+ out.push(this.prefix && name.startsWith(this.prefix) ? name.slice(this.prefix.length) : name)
101
+ }
102
+ return out
103
+ }
104
+ }
@@ -0,0 +1,92 @@
1
+ import type {
2
+ NixxiePutOptions,
3
+ NixxieSignedUrlOptions,
4
+ NixxieStorageService,
5
+ NixxieStoredFile,
6
+ } from '@nixxie-cms/core'
7
+ import type { GcsStorageConfig } from './types'
8
+
9
+ function loadGcs(): any {
10
+ try {
11
+ return require('@google-cloud/storage')
12
+ } catch {
13
+ throw new Error(
14
+ '@google-cloud/storage is required for the GCS driver. Run: npm install @google-cloud/storage'
15
+ )
16
+ }
17
+ }
18
+
19
+ /** Google Cloud Storage backend. */
20
+ export class GcsStorage implements NixxieStorageService {
21
+ private config: GcsStorageConfig
22
+ private prefix: string
23
+ private bucket: any
24
+
25
+ constructor(config: GcsStorageConfig) {
26
+ this.config = config
27
+ this.prefix = config.prefix ? config.prefix.replace(/\/$/, '') + '/' : ''
28
+ const { Storage } = loadGcs()
29
+ const client = new Storage({ projectId: config.projectId, keyFilename: config.keyFilename })
30
+ this.bucket = client.bucket(config.bucket)
31
+ }
32
+
33
+ private file(key: string): any {
34
+ return this.bucket.file(`${this.prefix}${key}`)
35
+ }
36
+
37
+ async put(
38
+ key: string,
39
+ data: Buffer | Uint8Array | string,
40
+ options?: NixxiePutOptions
41
+ ): Promise<NixxieStoredFile> {
42
+ const body = typeof data === 'string' ? Buffer.from(data) : Buffer.from(data)
43
+ const file = this.file(key)
44
+ await file.save(body, {
45
+ contentType: options?.contentType,
46
+ metadata: options?.metadata ? { metadata: options.metadata } : undefined,
47
+ })
48
+ if (options?.public) await file.makePublic()
49
+ return { key, url: this.url(key), size: body.byteLength, contentType: options?.contentType }
50
+ }
51
+
52
+ async get(key: string): Promise<Buffer | undefined> {
53
+ try {
54
+ const [contents] = await this.file(key).download()
55
+ return contents as Buffer
56
+ } catch (err: any) {
57
+ if (err?.code === 404) return undefined
58
+ throw err
59
+ }
60
+ }
61
+
62
+ async delete(key: string): Promise<void> {
63
+ await this.file(key).delete({ ignoreNotFound: true })
64
+ }
65
+
66
+ async has(key: string): Promise<boolean> {
67
+ const [exists] = await this.file(key).exists()
68
+ return Boolean(exists)
69
+ }
70
+
71
+ url(key: string): string {
72
+ if (this.config.publicBaseUrl)
73
+ return `${this.config.publicBaseUrl.replace(/\/$/, '')}/${this.prefix}${key}`
74
+ return `https://storage.googleapis.com/${this.config.bucket}/${this.prefix}${key}`
75
+ }
76
+
77
+ async signedUrl(key: string, options?: NixxieSignedUrlOptions): Promise<string> {
78
+ const [url] = await this.file(key).getSignedUrl({
79
+ action: options?.operation === 'put' ? 'write' : 'read',
80
+ expires: Date.now() + (options?.expiresIn ?? 900) * 1000,
81
+ contentType: options?.operation === 'put' ? options?.contentType : undefined,
82
+ })
83
+ return url as string
84
+ }
85
+
86
+ async list(prefix = ''): Promise<string[]> {
87
+ const [files] = await this.bucket.getFiles({ prefix: `${this.prefix}${prefix}` })
88
+ return files
89
+ .map((f: any) => f.name as string)
90
+ .map((k: string) => (this.prefix && k.startsWith(this.prefix) ? k.slice(this.prefix.length) : k))
91
+ }
92
+ }