@maroonedsoftware/storage 0.1.0 → 0.2.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.
@@ -0,0 +1,57 @@
1
+ import { Readable } from 'node:stream';
2
+ import type { SignedUrlOptions, StorageListOptions, StorageListResult, StorageObjectMetadata, StorageReadOptions, StorageWriteOptions } from './storage.provider.js';
3
+ import { StorageProvider } from './storage.provider.js';
4
+ /**
5
+ * Construction options for {@link DiskStorageProvider}.
6
+ *
7
+ * A class (not an interface) so it can serve as an InjectKit token — register
8
+ * an instance and the container can construct {@link DiskStorageProvider}.
9
+ */
10
+ export declare class DiskStorageProviderOptions {
11
+ /** Root directory the provider stores objects under. */
12
+ readonly rootDir: string;
13
+ /**
14
+ * Base URL objects are publicly served from. When set, {@link DiskStorageProvider.getSignedUrl}
15
+ * returns `${publicBaseUrl}/${key}`; when omitted, signing is unsupported.
16
+ */
17
+ readonly publicBaseUrl?: string;
18
+ constructor(init: {
19
+ rootDir: string;
20
+ publicBaseUrl?: string;
21
+ });
22
+ }
23
+ /**
24
+ * {@link StorageProvider} backed by the local filesystem rooted at a directory.
25
+ *
26
+ * Keys map to paths under the root; nested keys create intermediate directories
27
+ * on write. User metadata is not persisted (the filesystem has no native slot),
28
+ * and `getSignedUrl` requires a configured `publicBaseUrl`. Useful for local
29
+ * development and as an in-process test double.
30
+ */
31
+ export declare class DiskStorageProvider extends StorageProvider {
32
+ private readonly options;
33
+ constructor(options: DiskStorageProviderOptions);
34
+ write(key: string, body: Readable | Buffer | string, _options?: StorageWriteOptions): Promise<void>;
35
+ read(key: string, options?: StorageReadOptions): Promise<Readable>;
36
+ stat(key: string): Promise<StorageObjectMetadata>;
37
+ exists(key: string): Promise<boolean>;
38
+ delete(key: string): Promise<void>;
39
+ copy(sourceKey: string, destinationKey: string): Promise<void>;
40
+ move(sourceKey: string, destinationKey: string): Promise<void>;
41
+ /**
42
+ * Lists objects under the root, optionally filtered by prefix.
43
+ *
44
+ * Note: this walks the entire directory tree and `stat`s every matching file
45
+ * on each call — pagination slices an in-memory list, it does not make paging
46
+ * cheaper. Fine for development and modest trees (its intended use); avoid
47
+ * pointing it at very large directories.
48
+ */
49
+ list(options?: StorageListOptions): Promise<StorageListResult>;
50
+ getSignedUrl(key: string, _options: SignedUrlOptions): Promise<string>;
51
+ /** Resolve a key to an absolute path, rejecting traversal that escapes the root. */
52
+ private resolveKey;
53
+ private assertExists;
54
+ /** Recursively collect file keys (root-relative, `/`-separated) under `dir`. */
55
+ private walk;
56
+ }
57
+ //# sourceMappingURL=disk.storage.provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"disk.storage.provider.d.ts","sourceRoot":"","sources":["../src/disk.storage.provider.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAIvC,OAAO,KAAK,EACV,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EACjB,qBAAqB,EACrB,kBAAkB,EAClB,mBAAmB,EACpB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAExD;;;;;GAKG;AACH,qBAAa,0BAA0B;IACrC,wDAAwD;IACxD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;gBAEpB,IAAI,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE;CAI9D;AAED;;;;;;;GAOG;AACH,qBACa,mBAAoB,SAAQ,eAAe;IAC1C,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,0BAA0B;IAI1D,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,MAAM,GAAG,MAAM,EAAE,QAAQ,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAOnG,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,QAAQ,CAAC;IAOlE,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAYjD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAerC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKlC,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ9D,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAmBpE;;;;;;;OAOG;IACG,IAAI,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAoB9D,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC;IAe5E,oFAAoF;IACpF,OAAO,CAAC,UAAU;YASJ,YAAY;IAkB1B,gFAAgF;YAClE,IAAI;CAWnB"}
@@ -0,0 +1,37 @@
1
+ import { Readable } from 'node:stream';
2
+ import type { Storage } from '@google-cloud/storage';
3
+ import type { SignedUrlOptions, StorageListOptions, StorageListResult, StorageObjectMetadata, StorageReadOptions, StorageWriteOptions } from './storage.provider.js';
4
+ import { StorageProvider } from './storage.provider.js';
5
+ /**
6
+ * Construction options for {@link GcsStorageProvider}.
7
+ *
8
+ * A class (not an interface) so it can serve as an InjectKit token — register
9
+ * an instance and the container can construct {@link GcsStorageProvider}.
10
+ */
11
+ export declare class GcsStorageProviderOptions {
12
+ /** Name of the bucket all keys live in. */
13
+ readonly bucket: string;
14
+ constructor(init: {
15
+ bucket: string;
16
+ });
17
+ }
18
+ /**
19
+ * {@link StorageProvider} backed by a Google Cloud Storage bucket.
20
+ */
21
+ export declare class GcsStorageProvider extends StorageProvider {
22
+ private readonly client;
23
+ private readonly options;
24
+ constructor(client: Storage, options: GcsStorageProviderOptions);
25
+ write(key: string, body: Readable | Buffer | string, options?: StorageWriteOptions): Promise<void>;
26
+ read(key: string, options?: StorageReadOptions): Promise<Readable>;
27
+ stat(key: string): Promise<StorageObjectMetadata>;
28
+ exists(key: string): Promise<boolean>;
29
+ delete(key: string): Promise<void>;
30
+ copy(sourceKey: string, destinationKey: string): Promise<void>;
31
+ move(sourceKey: string, destinationKey: string): Promise<void>;
32
+ list(options?: StorageListOptions): Promise<StorageListResult>;
33
+ getSignedUrl(key: string, options: SignedUrlOptions): Promise<string>;
34
+ private file;
35
+ private mapError;
36
+ }
37
+ //# sourceMappingURL=gcs.storage.provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gcs.storage.provider.d.ts","sourceRoot":"","sources":["../src/gcs.storage.provider.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAGvC,OAAO,KAAK,EAAQ,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,KAAK,EACV,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EACjB,qBAAqB,EACrB,kBAAkB,EAClB,mBAAmB,EACpB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAExD;;;;;GAKG;AACH,qBAAa,yBAAyB;IACpC,2CAA2C;IAC3C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;gBAEZ,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE;CAGrC;AAED;;GAEG;AACH,qBACa,kBAAmB,SAAQ,eAAe;IAEnD,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBADP,MAAM,EAAE,OAAO,EACf,OAAO,EAAE,yBAAyB;IAK/C,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,MAAM,GAAG,MAAM,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBlG,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,QAAQ,CAAC;IAWlE,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAgBjD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAWrC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlC,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ9D,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAS9D,IAAI,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAsB9D,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC;IAU3E,OAAO,CAAC,IAAI;IAIZ,OAAO,CAAC,QAAQ;CASjB"}
@@ -0,0 +1,6 @@
1
+ export * from './storage.provider.js';
2
+ export * from './storage.errors.js';
3
+ export * from './disk.storage.provider.js';
4
+ export * from './s3.storage.provider.js';
5
+ export * from './gcs.storage.provider.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,0BAA0B,CAAC;AACzC,cAAc,2BAA2B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,672 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+
4
+ // src/storage.provider.ts
5
+ import { Injectable } from "injectkit";
6
+ function _ts_decorate(decorators, target, key, desc) {
7
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
8
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
9
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
10
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
11
+ }
12
+ __name(_ts_decorate, "_ts_decorate");
13
+ var StorageProvider = class {
14
+ static {
15
+ __name(this, "StorageProvider");
16
+ }
17
+ };
18
+ StorageProvider = _ts_decorate([
19
+ Injectable()
20
+ ], StorageProvider);
21
+
22
+ // src/storage.errors.ts
23
+ import { ServerkitError } from "@maroonedsoftware/errors";
24
+ var StorageError = class extends ServerkitError {
25
+ static {
26
+ __name(this, "StorageError");
27
+ }
28
+ constructor(message, options) {
29
+ super(message, options);
30
+ this.name = "StorageError";
31
+ }
32
+ };
33
+ var StorageObjectNotFoundError = class extends StorageError {
34
+ static {
35
+ __name(this, "StorageObjectNotFoundError");
36
+ }
37
+ key;
38
+ constructor(key, options) {
39
+ super(`storage object '${key}' not found`, options), this.key = key;
40
+ this.name = "StorageObjectNotFoundError";
41
+ }
42
+ };
43
+ var StorageAccessDeniedError = class extends StorageError {
44
+ static {
45
+ __name(this, "StorageAccessDeniedError");
46
+ }
47
+ key;
48
+ constructor(key, options) {
49
+ super(`access denied for storage object '${key}'`, options), this.key = key;
50
+ this.name = "StorageAccessDeniedError";
51
+ }
52
+ };
53
+ var StorageOperationNotSupportedError = class extends StorageError {
54
+ static {
55
+ __name(this, "StorageOperationNotSupportedError");
56
+ }
57
+ constructor(operation, options) {
58
+ super(`storage operation '${operation}' is not supported by this backend`, options);
59
+ this.name = "StorageOperationNotSupportedError";
60
+ }
61
+ };
62
+
63
+ // src/disk.storage.provider.ts
64
+ import { Injectable as Injectable2 } from "injectkit";
65
+ import { createReadStream, createWriteStream } from "fs";
66
+ import { copyFile, mkdir, readdir, rename, rm, stat as fsStat, unlink } from "fs/promises";
67
+ import { dirname, join, posix, relative, resolve, sep } from "path";
68
+ import { pipeline } from "stream/promises";
69
+ import { Readable } from "stream";
70
+ import { DateTime } from "luxon";
71
+ import mime from "mime-types";
72
+ function _ts_decorate2(decorators, target, key, desc) {
73
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
74
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
75
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
76
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
77
+ }
78
+ __name(_ts_decorate2, "_ts_decorate");
79
+ function _ts_metadata(k, v) {
80
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
81
+ }
82
+ __name(_ts_metadata, "_ts_metadata");
83
+ var DiskStorageProviderOptions = class {
84
+ static {
85
+ __name(this, "DiskStorageProviderOptions");
86
+ }
87
+ /** Root directory the provider stores objects under. */
88
+ rootDir;
89
+ /**
90
+ * Base URL objects are publicly served from. When set, {@link DiskStorageProvider.getSignedUrl}
91
+ * returns `${publicBaseUrl}/${key}`; when omitted, signing is unsupported.
92
+ */
93
+ publicBaseUrl;
94
+ constructor(init) {
95
+ this.rootDir = init.rootDir;
96
+ this.publicBaseUrl = init.publicBaseUrl;
97
+ }
98
+ };
99
+ var DiskStorageProvider = class extends StorageProvider {
100
+ static {
101
+ __name(this, "DiskStorageProvider");
102
+ }
103
+ options;
104
+ constructor(options) {
105
+ super(), this.options = options;
106
+ }
107
+ async write(key, body, _options) {
108
+ const path = this.resolveKey(key);
109
+ await mkdir(dirname(path), {
110
+ recursive: true
111
+ });
112
+ const source = body instanceof Readable ? body : Readable.from(body instanceof Buffer ? body : Buffer.from(body));
113
+ await pipeline(source, createWriteStream(path));
114
+ }
115
+ async read(key, options) {
116
+ const path = this.resolveKey(key);
117
+ await this.assertExists(key, path);
118
+ return options?.range ? createReadStream(path, {
119
+ start: options.range.start,
120
+ end: options.range.end
121
+ }) : createReadStream(path);
122
+ }
123
+ async stat(key) {
124
+ const path = this.resolveKey(key);
125
+ const stats = await this.assertExists(key, path);
126
+ const contentType = mime.lookup(path);
127
+ return {
128
+ key,
129
+ size: stats.size,
130
+ contentType: contentType === false ? void 0 : contentType,
131
+ lastModified: DateTime.fromJSDate(stats.mtime)
132
+ };
133
+ }
134
+ async exists(key) {
135
+ try {
136
+ const stats = await fsStat(this.resolveKey(key));
137
+ return stats.isFile();
138
+ } catch (error) {
139
+ if (isNotFound(error)) {
140
+ return false;
141
+ }
142
+ if (isAccessDenied(error)) {
143
+ throw new StorageAccessDeniedError(key, {
144
+ cause: error
145
+ });
146
+ }
147
+ throw error;
148
+ }
149
+ }
150
+ async delete(key) {
151
+ await rm(this.resolveKey(key), {
152
+ force: true
153
+ });
154
+ }
155
+ async copy(sourceKey, destinationKey) {
156
+ const source = this.resolveKey(sourceKey);
157
+ const destination = this.resolveKey(destinationKey);
158
+ await this.assertExists(sourceKey, source);
159
+ await mkdir(dirname(destination), {
160
+ recursive: true
161
+ });
162
+ await copyFile(source, destination);
163
+ }
164
+ async move(sourceKey, destinationKey) {
165
+ const source = this.resolveKey(sourceKey);
166
+ const destination = this.resolveKey(destinationKey);
167
+ await this.assertExists(sourceKey, source);
168
+ await mkdir(dirname(destination), {
169
+ recursive: true
170
+ });
171
+ try {
172
+ await rename(source, destination);
173
+ } catch (error) {
174
+ if (typeof error === "object" && error !== null && error.code === "EXDEV") {
175
+ await copyFile(source, destination);
176
+ await unlink(source);
177
+ return;
178
+ }
179
+ throw error;
180
+ }
181
+ }
182
+ /**
183
+ * Lists objects under the root, optionally filtered by prefix.
184
+ *
185
+ * Note: this walks the entire directory tree and `stat`s every matching file
186
+ * on each call — pagination slices an in-memory list, it does not make paging
187
+ * cheaper. Fine for development and modest trees (its intended use); avoid
188
+ * pointing it at very large directories.
189
+ */
190
+ async list(options) {
191
+ const prefix = options?.prefix ?? "";
192
+ const keys = [];
193
+ await this.walk(this.options.rootDir, keys);
194
+ const matched = keys.filter((key) => key.startsWith(prefix)).sort();
195
+ const start = options?.cursor ? matched.findIndex((key) => key > options.cursor) : 0;
196
+ const from = start === -1 ? matched.length : start;
197
+ const limit = options?.limit ?? matched.length;
198
+ const page = matched.slice(from, from + limit);
199
+ const objects = await Promise.all(page.map((key) => this.stat(key)));
200
+ const hasMore = from + limit < matched.length;
201
+ return {
202
+ objects,
203
+ cursor: hasMore && page.length > 0 ? page[page.length - 1] : void 0
204
+ };
205
+ }
206
+ async getSignedUrl(key, _options) {
207
+ if (!this.options.publicBaseUrl) {
208
+ throw new StorageOperationNotSupportedError("getSignedUrl");
209
+ }
210
+ this.resolveKey(key);
211
+ const encodedPath = key.replace(/^\/+/, "").split("/").map(encodeURIComponent).join("/");
212
+ return `${this.options.publicBaseUrl.replace(/\/+$/, "")}/${encodedPath}`;
213
+ }
214
+ /** Resolve a key to an absolute path, rejecting traversal that escapes the root. */
215
+ resolveKey(key) {
216
+ const path = resolve(this.options.rootDir, key);
217
+ const rel = relative(this.options.rootDir, path);
218
+ if (rel === "" || rel.startsWith("..") || rel.startsWith(`..${sep}`)) {
219
+ throw new StorageOperationNotSupportedError(`key '${key}' resolves outside the storage root`);
220
+ }
221
+ return path;
222
+ }
223
+ async assertExists(key, path) {
224
+ try {
225
+ const stats = await fsStat(path);
226
+ if (!stats.isFile()) {
227
+ throw new StorageObjectNotFoundError(key);
228
+ }
229
+ return stats;
230
+ } catch (error) {
231
+ if (isNotFound(error)) {
232
+ throw new StorageObjectNotFoundError(key, {
233
+ cause: error
234
+ });
235
+ }
236
+ if (isAccessDenied(error)) {
237
+ throw new StorageAccessDeniedError(key, {
238
+ cause: error
239
+ });
240
+ }
241
+ throw error;
242
+ }
243
+ }
244
+ /** Recursively collect file keys (root-relative, `/`-separated) under `dir`. */
245
+ async walk(dir, into) {
246
+ const entries = await readdir(dir, {
247
+ withFileTypes: true
248
+ });
249
+ for (const entry of entries) {
250
+ const full = join(dir, entry.name);
251
+ if (entry.isDirectory()) {
252
+ await this.walk(full, into);
253
+ } else if (entry.isFile()) {
254
+ into.push(relative(this.options.rootDir, full).split(sep).join(posix.sep));
255
+ }
256
+ }
257
+ }
258
+ };
259
+ DiskStorageProvider = _ts_decorate2([
260
+ Injectable2(),
261
+ _ts_metadata("design:type", Function),
262
+ _ts_metadata("design:paramtypes", [
263
+ typeof DiskStorageProviderOptions === "undefined" ? Object : DiskStorageProviderOptions
264
+ ])
265
+ ], DiskStorageProvider);
266
+ function isNotFound(error) {
267
+ return typeof error === "object" && error !== null && error.code === "ENOENT";
268
+ }
269
+ __name(isNotFound, "isNotFound");
270
+ function isAccessDenied(error) {
271
+ if (typeof error !== "object" || error === null) {
272
+ return false;
273
+ }
274
+ const code = error.code;
275
+ return code === "EACCES" || code === "EPERM";
276
+ }
277
+ __name(isAccessDenied, "isAccessDenied");
278
+
279
+ // src/s3.storage.provider.ts
280
+ import { Injectable as Injectable3 } from "injectkit";
281
+ import { Readable as Readable2 } from "stream";
282
+ import { DateTime as DateTime2 } from "luxon";
283
+ import { CopyObjectCommand, DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand } from "@aws-sdk/client-s3";
284
+ import { Upload } from "@aws-sdk/lib-storage";
285
+ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
286
+ function _ts_decorate3(decorators, target, key, desc) {
287
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
288
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
289
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
290
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
291
+ }
292
+ __name(_ts_decorate3, "_ts_decorate");
293
+ function _ts_metadata2(k, v) {
294
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
295
+ }
296
+ __name(_ts_metadata2, "_ts_metadata");
297
+ var S3StorageProviderOptions = class {
298
+ static {
299
+ __name(this, "S3StorageProviderOptions");
300
+ }
301
+ /** Name of the bucket all keys live in. */
302
+ bucket;
303
+ constructor(init) {
304
+ this.bucket = init.bucket;
305
+ }
306
+ };
307
+ var S3StorageProvider = class extends StorageProvider {
308
+ static {
309
+ __name(this, "S3StorageProvider");
310
+ }
311
+ client;
312
+ options;
313
+ constructor(client, options) {
314
+ super(), this.client = client, this.options = options;
315
+ }
316
+ async write(key, body, options) {
317
+ const shared = {
318
+ Bucket: this.options.bucket,
319
+ Key: key,
320
+ ContentType: options?.contentType,
321
+ ContentLength: options?.contentLength,
322
+ CacheControl: options?.cacheControl,
323
+ Metadata: options?.metadata
324
+ };
325
+ if (body instanceof Readable2) {
326
+ const upload = new Upload({
327
+ client: this.client,
328
+ params: {
329
+ ...shared,
330
+ Body: body
331
+ }
332
+ });
333
+ await upload.done();
334
+ return;
335
+ }
336
+ await this.client.send(new PutObjectCommand({
337
+ ...shared,
338
+ Body: body
339
+ }));
340
+ }
341
+ async read(key, options) {
342
+ try {
343
+ const response = await this.client.send(new GetObjectCommand({
344
+ Bucket: this.options.bucket,
345
+ Key: key,
346
+ Range: toRangeHeader(options?.range)
347
+ }));
348
+ return response.Body;
349
+ } catch (error) {
350
+ throw this.mapError(key, error);
351
+ }
352
+ }
353
+ async stat(key) {
354
+ try {
355
+ const head = await this.client.send(new HeadObjectCommand({
356
+ Bucket: this.options.bucket,
357
+ Key: key
358
+ }));
359
+ return {
360
+ key,
361
+ size: head.ContentLength ?? 0,
362
+ contentType: head.ContentType,
363
+ etag: head.ETag,
364
+ lastModified: head.LastModified ? DateTime2.fromJSDate(head.LastModified) : void 0,
365
+ metadata: head.Metadata
366
+ };
367
+ } catch (error) {
368
+ throw this.mapError(key, error);
369
+ }
370
+ }
371
+ async exists(key) {
372
+ try {
373
+ await this.client.send(new HeadObjectCommand({
374
+ Bucket: this.options.bucket,
375
+ Key: key
376
+ }));
377
+ return true;
378
+ } catch (error) {
379
+ if (isNotFound2(error)) {
380
+ return false;
381
+ }
382
+ throw this.mapError(key, error);
383
+ }
384
+ }
385
+ async delete(key) {
386
+ await this.client.send(new DeleteObjectCommand({
387
+ Bucket: this.options.bucket,
388
+ Key: key
389
+ }));
390
+ }
391
+ /**
392
+ * Server-side copy via `CopyObjectCommand`.
393
+ *
394
+ * Note: S3's single-request `CopyObject` is capped at 5 GB. Copying a larger
395
+ * object requires a multipart copy (`UploadPartCopy`), which this provider
396
+ * does not yet implement — such copies will fail. {@link move} inherits the
397
+ * same limit since it delegates to `copy`.
398
+ */
399
+ async copy(sourceKey, destinationKey) {
400
+ try {
401
+ await this.client.send(new CopyObjectCommand({
402
+ Bucket: this.options.bucket,
403
+ Key: destinationKey,
404
+ // CopySource must be the URL-encoded `bucket/key` of the source object.
405
+ CopySource: `${this.options.bucket}/${encodeURIComponent(sourceKey)}`
406
+ }));
407
+ } catch (error) {
408
+ throw this.mapError(sourceKey, error);
409
+ }
410
+ }
411
+ async move(sourceKey, destinationKey) {
412
+ await this.copy(sourceKey, destinationKey);
413
+ await this.delete(sourceKey);
414
+ }
415
+ async list(options) {
416
+ const response = await this.client.send(new ListObjectsV2Command({
417
+ Bucket: this.options.bucket,
418
+ Prefix: options?.prefix,
419
+ MaxKeys: options?.limit,
420
+ ContinuationToken: options?.cursor
421
+ }));
422
+ const objects = (response.Contents ?? []).map((item) => ({
423
+ key: item.Key ?? "",
424
+ size: item.Size ?? 0,
425
+ etag: item.ETag,
426
+ lastModified: item.LastModified ? DateTime2.fromJSDate(item.LastModified) : void 0
427
+ }));
428
+ return {
429
+ objects,
430
+ cursor: response.IsTruncated ? response.NextContinuationToken : void 0
431
+ };
432
+ }
433
+ async getSignedUrl(key, options) {
434
+ const command = options.operation === "write" ? new PutObjectCommand({
435
+ Bucket: this.options.bucket,
436
+ Key: key,
437
+ ContentType: options.contentType
438
+ }) : new GetObjectCommand({
439
+ Bucket: this.options.bucket,
440
+ Key: key
441
+ });
442
+ return getSignedUrl(this.client, command, {
443
+ expiresIn: Math.round(options.expiresIn.as("seconds"))
444
+ });
445
+ }
446
+ mapError(key, error) {
447
+ if (isNotFound2(error)) {
448
+ return new StorageObjectNotFoundError(key, {
449
+ cause: error
450
+ });
451
+ }
452
+ if (isAccessDenied2(error)) {
453
+ return new StorageAccessDeniedError(key, {
454
+ cause: error
455
+ });
456
+ }
457
+ return error;
458
+ }
459
+ };
460
+ S3StorageProvider = _ts_decorate3([
461
+ Injectable3(),
462
+ _ts_metadata2("design:type", Function),
463
+ _ts_metadata2("design:paramtypes", [
464
+ typeof S3Client === "undefined" ? Object : S3Client,
465
+ typeof S3StorageProviderOptions === "undefined" ? Object : S3StorageProviderOptions
466
+ ])
467
+ ], S3StorageProvider);
468
+ function errorName(error) {
469
+ return typeof error === "object" && error !== null ? error.name : void 0;
470
+ }
471
+ __name(errorName, "errorName");
472
+ function statusCode(error) {
473
+ return typeof error === "object" && error !== null ? error.$metadata?.httpStatusCode : void 0;
474
+ }
475
+ __name(statusCode, "statusCode");
476
+ function isNotFound2(error) {
477
+ const name = errorName(error);
478
+ return name === "NoSuchKey" || name === "NotFound" || statusCode(error) === 404;
479
+ }
480
+ __name(isNotFound2, "isNotFound");
481
+ function isAccessDenied2(error) {
482
+ return errorName(error) === "AccessDenied" || statusCode(error) === 403;
483
+ }
484
+ __name(isAccessDenied2, "isAccessDenied");
485
+ function toRangeHeader(range) {
486
+ return range ? `bytes=${range.start}-${range.end ?? ""}` : void 0;
487
+ }
488
+ __name(toRangeHeader, "toRangeHeader");
489
+
490
+ // src/gcs.storage.provider.ts
491
+ import { Injectable as Injectable4 } from "injectkit";
492
+ import { Readable as Readable3 } from "stream";
493
+ import { pipeline as pipeline2 } from "stream/promises";
494
+ import { DateTime as DateTime3 } from "luxon";
495
+ function _ts_decorate4(decorators, target, key, desc) {
496
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
497
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
498
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
499
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
500
+ }
501
+ __name(_ts_decorate4, "_ts_decorate");
502
+ function _ts_metadata3(k, v) {
503
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
504
+ }
505
+ __name(_ts_metadata3, "_ts_metadata");
506
+ var GcsStorageProviderOptions = class {
507
+ static {
508
+ __name(this, "GcsStorageProviderOptions");
509
+ }
510
+ /** Name of the bucket all keys live in. */
511
+ bucket;
512
+ constructor(init) {
513
+ this.bucket = init.bucket;
514
+ }
515
+ };
516
+ var GcsStorageProvider = class extends StorageProvider {
517
+ static {
518
+ __name(this, "GcsStorageProvider");
519
+ }
520
+ client;
521
+ options;
522
+ constructor(client, options) {
523
+ super(), this.client = client, this.options = options;
524
+ }
525
+ async write(key, body, options) {
526
+ const file = this.file(key);
527
+ const metadata = {
528
+ contentType: options?.contentType,
529
+ cacheControl: options?.cacheControl,
530
+ metadata: options?.metadata
531
+ };
532
+ if (body instanceof Readable3) {
533
+ await pipeline2(body, file.createWriteStream({
534
+ metadata
535
+ }));
536
+ return;
537
+ }
538
+ await file.save(body instanceof Buffer ? body : Buffer.from(body), {
539
+ metadata
540
+ });
541
+ }
542
+ async read(key, options) {
543
+ const exists = await this.exists(key);
544
+ if (!exists) {
545
+ throw new StorageObjectNotFoundError(key);
546
+ }
547
+ return this.file(key).createReadStream(options?.range ? {
548
+ start: options.range.start,
549
+ end: options.range.end
550
+ } : void 0);
551
+ }
552
+ async stat(key) {
553
+ try {
554
+ const [metadata] = await this.file(key).getMetadata();
555
+ return {
556
+ key,
557
+ size: typeof metadata.size === "string" ? Number(metadata.size) : metadata.size ?? 0,
558
+ contentType: metadata.contentType,
559
+ etag: metadata.etag,
560
+ lastModified: metadata.updated ? DateTime3.fromISO(metadata.updated) : void 0,
561
+ metadata: metadata.metadata
562
+ };
563
+ } catch (error) {
564
+ throw this.mapError(key, error);
565
+ }
566
+ }
567
+ async exists(key) {
568
+ try {
569
+ const [exists] = await this.file(key).exists();
570
+ return exists;
571
+ } catch (error) {
572
+ throw this.mapError(key, error);
573
+ }
574
+ }
575
+ async delete(key) {
576
+ await this.file(key).delete({
577
+ ignoreNotFound: true
578
+ });
579
+ }
580
+ async copy(sourceKey, destinationKey) {
581
+ try {
582
+ await this.file(sourceKey).copy(this.file(destinationKey));
583
+ } catch (error) {
584
+ throw this.mapError(sourceKey, error);
585
+ }
586
+ }
587
+ async move(sourceKey, destinationKey) {
588
+ try {
589
+ await this.file(sourceKey).move(this.file(destinationKey));
590
+ } catch (error) {
591
+ throw this.mapError(sourceKey, error);
592
+ }
593
+ }
594
+ async list(options) {
595
+ const [files, nextQuery] = await this.client.bucket(this.options.bucket).getFiles({
596
+ prefix: options?.prefix,
597
+ maxResults: options?.limit,
598
+ pageToken: options?.cursor,
599
+ autoPaginate: false
600
+ });
601
+ const objects = files.map((file) => ({
602
+ key: file.name,
603
+ size: typeof file.metadata.size === "string" ? Number(file.metadata.size) : file.metadata.size ?? 0,
604
+ contentType: file.metadata.contentType,
605
+ etag: file.metadata.etag,
606
+ lastModified: file.metadata.updated ? DateTime3.fromISO(file.metadata.updated) : void 0
607
+ }));
608
+ return {
609
+ objects,
610
+ cursor: nextQuery?.pageToken
611
+ };
612
+ }
613
+ async getSignedUrl(key, options) {
614
+ const [url] = await this.file(key).getSignedUrl({
615
+ version: "v4",
616
+ action: options.operation === "write" ? "write" : "read",
617
+ expires: DateTime3.now().plus(options.expiresIn).toMillis(),
618
+ contentType: options.operation === "write" ? options.contentType : void 0
619
+ });
620
+ return url;
621
+ }
622
+ file(key) {
623
+ return this.client.bucket(this.options.bucket).file(key);
624
+ }
625
+ mapError(key, error) {
626
+ if (isNotFound3(error)) {
627
+ return new StorageObjectNotFoundError(key, {
628
+ cause: error
629
+ });
630
+ }
631
+ if (isAccessDenied3(error)) {
632
+ return new StorageAccessDeniedError(key, {
633
+ cause: error
634
+ });
635
+ }
636
+ return error;
637
+ }
638
+ };
639
+ GcsStorageProvider = _ts_decorate4([
640
+ Injectable4(),
641
+ _ts_metadata3("design:type", Function),
642
+ _ts_metadata3("design:paramtypes", [
643
+ typeof Storage === "undefined" ? Object : Storage,
644
+ typeof GcsStorageProviderOptions === "undefined" ? Object : GcsStorageProviderOptions
645
+ ])
646
+ ], GcsStorageProvider);
647
+ function statusCode2(error) {
648
+ return typeof error === "object" && error !== null ? error.code : void 0;
649
+ }
650
+ __name(statusCode2, "statusCode");
651
+ function isNotFound3(error) {
652
+ return statusCode2(error) === 404;
653
+ }
654
+ __name(isNotFound3, "isNotFound");
655
+ function isAccessDenied3(error) {
656
+ return statusCode2(error) === 403;
657
+ }
658
+ __name(isAccessDenied3, "isAccessDenied");
659
+ export {
660
+ DiskStorageProvider,
661
+ DiskStorageProviderOptions,
662
+ GcsStorageProvider,
663
+ GcsStorageProviderOptions,
664
+ S3StorageProvider,
665
+ S3StorageProviderOptions,
666
+ StorageAccessDeniedError,
667
+ StorageError,
668
+ StorageObjectNotFoundError,
669
+ StorageOperationNotSupportedError,
670
+ StorageProvider
671
+ };
672
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/storage.provider.ts","../src/storage.errors.ts","../src/disk.storage.provider.ts","../src/s3.storage.provider.ts","../src/gcs.storage.provider.ts"],"sourcesContent":["import { Injectable } from 'injectkit';\nimport { Readable } from 'node:stream';\nimport { DateTime, Duration } from 'luxon';\n\n/**\n * Options applied when writing an object. All fields are optional; backends\n * that cannot honour a given field ignore it (documented per provider).\n */\nexport interface StorageWriteOptions {\n /** MIME type stored alongside the object (e.g. `image/png`). */\n contentType?: string;\n /** Byte length of the body, when known ahead of time. */\n contentLength?: number;\n /** `Cache-Control` header persisted with the object (cloud backends). */\n cacheControl?: string;\n /** Arbitrary user metadata. Unsupported on the disk backend (ignored). */\n metadata?: Record<string, string>;\n}\n\n/**\n * Options for {@link StorageProvider.read}.\n */\nexport interface StorageReadOptions {\n /**\n * Restrict the read to a byte range. `start` and `end` are both inclusive\n * (HTTP `Range` semantics); omit `end` to read from `start` to the end of the\n * object. Useful for media streaming and resumable downloads.\n */\n range?: { start: number; end?: number };\n}\n\n/**\n * Metadata describing a stored object, returned by {@link StorageProvider.stat}\n * and {@link StorageProvider.list}.\n */\nexport interface StorageObjectMetadata {\n /** The object's key. */\n key: string;\n /** Size in bytes. */\n size: number;\n /** MIME type, when known. */\n contentType?: string;\n /** Backend entity tag, when provided. */\n etag?: string;\n /** Last modification time, when known. */\n lastModified?: DateTime;\n /** User metadata, when the backend supports and returns it. */\n metadata?: Record<string, string>;\n}\n\n/**\n * Options for {@link StorageProvider.list}.\n */\nexport interface StorageListOptions {\n /** Restrict results to keys beginning with this prefix. */\n prefix?: string;\n /** Maximum number of objects to return in this page. */\n limit?: number;\n /** Opaque continuation token from a previous {@link StorageListResult}. */\n cursor?: string;\n}\n\n/**\n * A single page of {@link StorageProvider.list} results.\n */\nexport interface StorageListResult {\n /** Objects in this page. */\n objects: StorageObjectMetadata[];\n /** Continuation token for the next page, or `undefined` when exhausted. */\n cursor?: string;\n}\n\n/** Whether a signed URL grants read (download) or write (upload) access. */\nexport type SignedUrlOperation = 'read' | 'write';\n\n/**\n * Options for {@link StorageProvider.getSignedUrl}.\n */\nexport interface SignedUrlOptions {\n /** The operation the URL authorises. */\n operation: SignedUrlOperation;\n /** How long the URL remains valid. */\n expiresIn: Duration;\n /** Content type the client must use when uploading (`write` URLs). */\n contentType?: string;\n}\n\n/**\n * Backend-agnostic object storage. Implementations wrap a concrete backend\n * (local disk, AWS S3, Google Cloud Storage). Bind a concrete provider to this\n * token in the DI container so consumers depend only on the abstraction:\n *\n * ```ts\n * container.bind(StorageProvider).toConstantValue(new DiskStorageProvider({ rootDir: '/var/data' }));\n * ```\n *\n * Keys are hierarchical, `/`-separated paths (e.g. `users/42/avatar.png`), not\n * flat filenames.\n *\n * ## Behaviour contract\n * - {@link read} / {@link stat} on a missing key throw `StorageObjectNotFoundError`.\n * - {@link exists} never throws for a missing key — it returns `false`.\n * - Operations that hit a permission failure throw `StorageAccessDeniedError`.\n * - {@link delete} is idempotent — deleting a missing key is a no-op.\n * - {@link copy} / {@link move} throw `StorageObjectNotFoundError` when the\n * source is missing, and overwrite the destination if it already exists. Both\n * operate within this backend only (same bucket / root) — cross-backend or\n * cross-bucket transfers are out of scope.\n * - {@link getSignedUrl} throws `StorageOperationNotSupportedError` on backends\n * that cannot sign URLs.\n */\n@Injectable()\nexport abstract class StorageProvider {\n /** Write `body` to `key`, overwriting any existing object. */\n abstract write(key: string, body: Readable | Buffer | string, options?: StorageWriteOptions): Promise<void>;\n /** Open a readable stream for `key`, optionally for a byte range. Throws if the key does not exist. */\n abstract read(key: string, options?: StorageReadOptions): Promise<Readable>;\n /** Fetch metadata for `key` without reading its body. Throws if absent. */\n abstract stat(key: string): Promise<StorageObjectMetadata>;\n /** Resolve to `true` if `key` exists, `false` otherwise. Never throws for absence. */\n abstract exists(key: string): Promise<boolean>;\n /** Delete `key`. A no-op if the key does not exist. */\n abstract delete(key: string): Promise<void>;\n /** Copy `sourceKey` to `destinationKey` within this backend, overwriting the destination. Throws if the source is missing. */\n abstract copy(sourceKey: string, destinationKey: string): Promise<void>;\n /** Move/rename `sourceKey` to `destinationKey` within this backend, overwriting the destination. Throws if the source is missing. */\n abstract move(sourceKey: string, destinationKey: string): Promise<void>;\n /** List a single page of objects, optionally filtered by prefix. */\n abstract list(options?: StorageListOptions): Promise<StorageListResult>;\n /** Generate a time-limited signed URL for direct client read/write access. */\n abstract getSignedUrl(key: string, options: SignedUrlOptions): Promise<string>;\n}\n","import { ServerkitError } from '@maroonedsoftware/errors';\n\n/**\n * Base class for all errors thrown by a {@link StorageProvider}.\n *\n * Catch this to handle any storage failure generically; catch a subclass to\n * distinguish specific conditions (missing object, unsupported operation).\n * Extends `ServerkitError`, so `errorMiddleware` renders it as a 500 with any\n * attached `details`.\n */\nexport class StorageError extends ServerkitError {\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = 'StorageError';\n }\n}\n\n/**\n * Thrown when {@link StorageProvider.read} or {@link StorageProvider.stat} is\n * called for a key that does not exist. The offending key is available on\n * `key` for callers that want to surface it.\n */\nexport class StorageObjectNotFoundError extends StorageError {\n constructor(\n readonly key: string,\n options?: { cause?: unknown },\n ) {\n super(`storage object '${key}' not found`, options);\n this.name = 'StorageObjectNotFoundError';\n }\n}\n\n/**\n * Thrown when the backend rejects an operation for permission reasons (an S3 or\n * GCS `403`, a local-filesystem `EACCES`/`EPERM`). The offending key is\n * available on `key`.\n */\nexport class StorageAccessDeniedError extends StorageError {\n constructor(\n readonly key: string,\n options?: { cause?: unknown },\n ) {\n super(`access denied for storage object '${key}'`, options);\n this.name = 'StorageAccessDeniedError';\n }\n}\n\n/**\n * Thrown when an operation is not supported by the active backend — for\n * example {@link StorageProvider.getSignedUrl} on a disk provider that has no\n * public base URL configured.\n */\nexport class StorageOperationNotSupportedError extends StorageError {\n constructor(operation: string, options?: { cause?: unknown }) {\n super(`storage operation '${operation}' is not supported by this backend`, options);\n this.name = 'StorageOperationNotSupportedError';\n }\n}\n","import { Injectable } from 'injectkit';\nimport { createReadStream, createWriteStream } from 'node:fs';\nimport { copyFile, mkdir, readdir, rename, rm, stat as fsStat, unlink } from 'node:fs/promises';\nimport { dirname, join, posix, relative, resolve, sep } from 'node:path';\nimport { pipeline } from 'node:stream/promises';\nimport { Readable } from 'node:stream';\nimport { DateTime } from 'luxon';\nimport mime from 'mime-types';\nimport { StorageAccessDeniedError, StorageObjectNotFoundError, StorageOperationNotSupportedError } from './storage.errors.js';\nimport type {\n SignedUrlOptions,\n StorageListOptions,\n StorageListResult,\n StorageObjectMetadata,\n StorageReadOptions,\n StorageWriteOptions,\n} from './storage.provider.js';\nimport { StorageProvider } from './storage.provider.js';\n\n/**\n * Construction options for {@link DiskStorageProvider}.\n *\n * A class (not an interface) so it can serve as an InjectKit token — register\n * an instance and the container can construct {@link DiskStorageProvider}.\n */\nexport class DiskStorageProviderOptions {\n /** Root directory the provider stores objects under. */\n readonly rootDir: string;\n /**\n * Base URL objects are publicly served from. When set, {@link DiskStorageProvider.getSignedUrl}\n * returns `${publicBaseUrl}/${key}`; when omitted, signing is unsupported.\n */\n readonly publicBaseUrl?: string;\n\n constructor(init: { rootDir: string; publicBaseUrl?: string }) {\n this.rootDir = init.rootDir;\n this.publicBaseUrl = init.publicBaseUrl;\n }\n}\n\n/**\n * {@link StorageProvider} backed by the local filesystem rooted at a directory.\n *\n * Keys map to paths under the root; nested keys create intermediate directories\n * on write. User metadata is not persisted (the filesystem has no native slot),\n * and `getSignedUrl` requires a configured `publicBaseUrl`. Useful for local\n * development and as an in-process test double.\n */\n@Injectable()\nexport class DiskStorageProvider extends StorageProvider {\n constructor(private readonly options: DiskStorageProviderOptions) {\n super();\n }\n\n async write(key: string, body: Readable | Buffer | string, _options?: StorageWriteOptions): Promise<void> {\n const path = this.resolveKey(key);\n await mkdir(dirname(path), { recursive: true });\n const source = body instanceof Readable ? body : Readable.from(body instanceof Buffer ? body : Buffer.from(body));\n await pipeline(source, createWriteStream(path));\n }\n\n async read(key: string, options?: StorageReadOptions): Promise<Readable> {\n const path = this.resolveKey(key);\n await this.assertExists(key, path);\n // `start`/`end` map directly to fs's inclusive byte offsets.\n return options?.range ? createReadStream(path, { start: options.range.start, end: options.range.end }) : createReadStream(path);\n }\n\n async stat(key: string): Promise<StorageObjectMetadata> {\n const path = this.resolveKey(key);\n const stats = await this.assertExists(key, path);\n const contentType = mime.lookup(path);\n return {\n key,\n size: stats.size,\n contentType: contentType === false ? undefined : contentType,\n lastModified: DateTime.fromJSDate(stats.mtime),\n };\n }\n\n async exists(key: string): Promise<boolean> {\n try {\n const stats = await fsStat(this.resolveKey(key));\n return stats.isFile();\n } catch (error) {\n if (isNotFound(error)) {\n return false;\n }\n if (isAccessDenied(error)) {\n throw new StorageAccessDeniedError(key, { cause: error });\n }\n throw error;\n }\n }\n\n async delete(key: string): Promise<void> {\n // `force: true` makes this idempotent — no error when the path is absent.\n await rm(this.resolveKey(key), { force: true });\n }\n\n async copy(sourceKey: string, destinationKey: string): Promise<void> {\n const source = this.resolveKey(sourceKey);\n const destination = this.resolveKey(destinationKey);\n await this.assertExists(sourceKey, source);\n await mkdir(dirname(destination), { recursive: true });\n await copyFile(source, destination);\n }\n\n async move(sourceKey: string, destinationKey: string): Promise<void> {\n const source = this.resolveKey(sourceKey);\n const destination = this.resolveKey(destinationKey);\n await this.assertExists(sourceKey, source);\n await mkdir(dirname(destination), { recursive: true });\n try {\n // Atomic and cheap on the same filesystem.\n await rename(source, destination);\n } catch (error) {\n // `rename` cannot cross filesystem boundaries — fall back to copy + delete.\n if (typeof error === 'object' && error !== null && (error as NodeJS.ErrnoException).code === 'EXDEV') {\n await copyFile(source, destination);\n await unlink(source);\n return;\n }\n throw error;\n }\n }\n\n /**\n * Lists objects under the root, optionally filtered by prefix.\n *\n * Note: this walks the entire directory tree and `stat`s every matching file\n * on each call — pagination slices an in-memory list, it does not make paging\n * cheaper. Fine for development and modest trees (its intended use); avoid\n * pointing it at very large directories.\n */\n async list(options?: StorageListOptions): Promise<StorageListResult> {\n const prefix = options?.prefix ?? '';\n const keys: string[] = [];\n await this.walk(this.options.rootDir, keys);\n const matched = keys.filter(key => key.startsWith(prefix)).sort();\n\n // Cursor is the last key returned by the previous page; resume after it.\n const start = options?.cursor ? matched.findIndex(key => key > options.cursor!) : 0;\n const from = start === -1 ? matched.length : start;\n const limit = options?.limit ?? matched.length;\n const page = matched.slice(from, from + limit);\n\n const objects = await Promise.all(page.map(key => this.stat(key)));\n const hasMore = from + limit < matched.length;\n return {\n objects,\n cursor: hasMore && page.length > 0 ? page[page.length - 1] : undefined,\n };\n }\n\n async getSignedUrl(key: string, _options: SignedUrlOptions): Promise<string> {\n if (!this.options.publicBaseUrl) {\n throw new StorageOperationNotSupportedError('getSignedUrl');\n }\n // Normalise the key against the root for traversal safety, then join to the base URL.\n this.resolveKey(key);\n // Encode each path segment so keys with spaces or reserved characters yield a valid URL.\n const encodedPath = key\n .replace(/^\\/+/, '')\n .split('/')\n .map(encodeURIComponent)\n .join('/');\n return `${this.options.publicBaseUrl.replace(/\\/+$/, '')}/${encodedPath}`;\n }\n\n /** Resolve a key to an absolute path, rejecting traversal that escapes the root. */\n private resolveKey(key: string): string {\n const path = resolve(this.options.rootDir, key);\n const rel = relative(this.options.rootDir, path);\n if (rel === '' || rel.startsWith('..') || rel.startsWith(`..${sep}`)) {\n throw new StorageOperationNotSupportedError(`key '${key}' resolves outside the storage root`);\n }\n return path;\n }\n\n private async assertExists(key: string, path: string) {\n try {\n const stats = await fsStat(path);\n if (!stats.isFile()) {\n throw new StorageObjectNotFoundError(key);\n }\n return stats;\n } catch (error) {\n if (isNotFound(error)) {\n throw new StorageObjectNotFoundError(key, { cause: error });\n }\n if (isAccessDenied(error)) {\n throw new StorageAccessDeniedError(key, { cause: error });\n }\n throw error;\n }\n }\n\n /** Recursively collect file keys (root-relative, `/`-separated) under `dir`. */\n private async walk(dir: string, into: string[]): Promise<void> {\n const entries = await readdir(dir, { withFileTypes: true });\n for (const entry of entries) {\n const full = join(dir, entry.name);\n if (entry.isDirectory()) {\n await this.walk(full, into);\n } else if (entry.isFile()) {\n into.push(relative(this.options.rootDir, full).split(sep).join(posix.sep));\n }\n }\n }\n}\n\nfunction isNotFound(error: unknown): boolean {\n return typeof error === 'object' && error !== null && (error as NodeJS.ErrnoException).code === 'ENOENT';\n}\n\nfunction isAccessDenied(error: unknown): boolean {\n if (typeof error !== 'object' || error === null) {\n return false;\n }\n const code = (error as NodeJS.ErrnoException).code;\n return code === 'EACCES' || code === 'EPERM';\n}\n","import { Injectable } from 'injectkit';\nimport { Readable } from 'node:stream';\nimport { DateTime } from 'luxon';\nimport {\n CopyObjectCommand,\n DeleteObjectCommand,\n GetObjectCommand,\n HeadObjectCommand,\n ListObjectsV2Command,\n PutObjectCommand,\n type S3Client,\n} from '@aws-sdk/client-s3';\nimport { Upload } from '@aws-sdk/lib-storage';\nimport { getSignedUrl } from '@aws-sdk/s3-request-presigner';\nimport { StorageAccessDeniedError, StorageObjectNotFoundError } from './storage.errors.js';\nimport type {\n SignedUrlOptions,\n StorageListOptions,\n StorageListResult,\n StorageObjectMetadata,\n StorageReadOptions,\n StorageWriteOptions,\n} from './storage.provider.js';\nimport { StorageProvider } from './storage.provider.js';\n\n/**\n * Construction options for {@link S3StorageProvider}.\n *\n * A class (not an interface) so it can serve as an InjectKit token — register\n * an instance and the container can construct {@link S3StorageProvider}.\n */\nexport class S3StorageProviderOptions {\n /** Name of the bucket all keys live in. */\n readonly bucket: string;\n\n constructor(init: { bucket: string }) {\n this.bucket = init.bucket;\n }\n}\n\n/**\n * {@link StorageProvider} backed by an AWS S3 (or S3-compatible) bucket.\n *\n * Streaming writes go through `@aws-sdk/lib-storage`'s multipart `Upload`;\n * buffer/string writes use a single `PutObject`. Signed URLs are produced with\n * `@aws-sdk/s3-request-presigner`.\n */\n@Injectable()\nexport class S3StorageProvider extends StorageProvider {\n constructor(\n private readonly client: S3Client,\n private readonly options: S3StorageProviderOptions,\n ) {\n super();\n }\n\n async write(key: string, body: Readable | Buffer | string, options?: StorageWriteOptions): Promise<void> {\n const shared = {\n Bucket: this.options.bucket,\n Key: key,\n ContentType: options?.contentType,\n ContentLength: options?.contentLength,\n CacheControl: options?.cacheControl,\n Metadata: options?.metadata,\n };\n\n if (body instanceof Readable) {\n // lib-storage streams the body, automatically switching to multipart for large objects.\n const upload = new Upload({ client: this.client, params: { ...shared, Body: body } });\n await upload.done();\n return;\n }\n\n await this.client.send(new PutObjectCommand({ ...shared, Body: body }));\n }\n\n async read(key: string, options?: StorageReadOptions): Promise<Readable> {\n try {\n const response = await this.client.send(\n new GetObjectCommand({ Bucket: this.options.bucket, Key: key, Range: toRangeHeader(options?.range) }),\n );\n return response.Body as Readable;\n } catch (error) {\n throw this.mapError(key, error);\n }\n }\n\n async stat(key: string): Promise<StorageObjectMetadata> {\n try {\n const head = await this.client.send(new HeadObjectCommand({ Bucket: this.options.bucket, Key: key }));\n return {\n key,\n size: head.ContentLength ?? 0,\n contentType: head.ContentType,\n etag: head.ETag,\n lastModified: head.LastModified ? DateTime.fromJSDate(head.LastModified) : undefined,\n metadata: head.Metadata,\n };\n } catch (error) {\n throw this.mapError(key, error);\n }\n }\n\n async exists(key: string): Promise<boolean> {\n try {\n await this.client.send(new HeadObjectCommand({ Bucket: this.options.bucket, Key: key }));\n return true;\n } catch (error) {\n if (isNotFound(error)) {\n return false;\n }\n // A 403 is a permission failure, not absence — surface it.\n throw this.mapError(key, error);\n }\n }\n\n async delete(key: string): Promise<void> {\n // S3 DeleteObject is idempotent — deleting a missing key succeeds.\n await this.client.send(new DeleteObjectCommand({ Bucket: this.options.bucket, Key: key }));\n }\n\n /**\n * Server-side copy via `CopyObjectCommand`.\n *\n * Note: S3's single-request `CopyObject` is capped at 5 GB. Copying a larger\n * object requires a multipart copy (`UploadPartCopy`), which this provider\n * does not yet implement — such copies will fail. {@link move} inherits the\n * same limit since it delegates to `copy`.\n */\n async copy(sourceKey: string, destinationKey: string): Promise<void> {\n try {\n await this.client.send(\n new CopyObjectCommand({\n Bucket: this.options.bucket,\n Key: destinationKey,\n // CopySource must be the URL-encoded `bucket/key` of the source object.\n CopySource: `${this.options.bucket}/${encodeURIComponent(sourceKey)}`,\n }),\n );\n } catch (error) {\n throw this.mapError(sourceKey, error);\n }\n }\n\n async move(sourceKey: string, destinationKey: string): Promise<void> {\n // S3 has no native move — copy then delete the source.\n await this.copy(sourceKey, destinationKey);\n await this.delete(sourceKey);\n }\n\n async list(options?: StorageListOptions): Promise<StorageListResult> {\n const response = await this.client.send(\n new ListObjectsV2Command({\n Bucket: this.options.bucket,\n Prefix: options?.prefix,\n MaxKeys: options?.limit,\n ContinuationToken: options?.cursor,\n }),\n );\n\n const objects: StorageObjectMetadata[] = (response.Contents ?? []).map(item => ({\n key: item.Key ?? '',\n size: item.Size ?? 0,\n etag: item.ETag,\n lastModified: item.LastModified ? DateTime.fromJSDate(item.LastModified) : undefined,\n }));\n\n return {\n objects,\n cursor: response.IsTruncated ? response.NextContinuationToken : undefined,\n };\n }\n\n async getSignedUrl(key: string, options: SignedUrlOptions): Promise<string> {\n const command =\n options.operation === 'write'\n ? new PutObjectCommand({ Bucket: this.options.bucket, Key: key, ContentType: options.contentType })\n : new GetObjectCommand({ Bucket: this.options.bucket, Key: key });\n return getSignedUrl(this.client, command, { expiresIn: Math.round(options.expiresIn.as('seconds')) });\n }\n\n private mapError(key: string, error: unknown): unknown {\n if (isNotFound(error)) {\n return new StorageObjectNotFoundError(key, { cause: error });\n }\n if (isAccessDenied(error)) {\n return new StorageAccessDeniedError(key, { cause: error });\n }\n return error;\n }\n}\n\nfunction errorName(error: unknown): string | undefined {\n return typeof error === 'object' && error !== null ? (error as { name?: string }).name : undefined;\n}\n\nfunction statusCode(error: unknown): number | undefined {\n return typeof error === 'object' && error !== null ? (error as { $metadata?: { httpStatusCode?: number } }).$metadata?.httpStatusCode : undefined;\n}\n\nfunction isNotFound(error: unknown): boolean {\n const name = errorName(error);\n return name === 'NoSuchKey' || name === 'NotFound' || statusCode(error) === 404;\n}\n\nfunction isAccessDenied(error: unknown): boolean {\n return errorName(error) === 'AccessDenied' || statusCode(error) === 403;\n}\n\n/** Build an HTTP `Range` header (`bytes=start-end`) from an inclusive byte range. */\nfunction toRangeHeader(range?: { start: number; end?: number }): string | undefined {\n return range ? `bytes=${range.start}-${range.end ?? ''}` : undefined;\n}\n","import { Injectable } from 'injectkit';\nimport { Readable } from 'node:stream';\nimport { pipeline } from 'node:stream/promises';\nimport { DateTime } from 'luxon';\nimport type { File, Storage } from '@google-cloud/storage';\nimport { StorageAccessDeniedError, StorageObjectNotFoundError } from './storage.errors.js';\nimport type {\n SignedUrlOptions,\n StorageListOptions,\n StorageListResult,\n StorageObjectMetadata,\n StorageReadOptions,\n StorageWriteOptions,\n} from './storage.provider.js';\nimport { StorageProvider } from './storage.provider.js';\n\n/**\n * Construction options for {@link GcsStorageProvider}.\n *\n * A class (not an interface) so it can serve as an InjectKit token — register\n * an instance and the container can construct {@link GcsStorageProvider}.\n */\nexport class GcsStorageProviderOptions {\n /** Name of the bucket all keys live in. */\n readonly bucket: string;\n\n constructor(init: { bucket: string }) {\n this.bucket = init.bucket;\n }\n}\n\n/**\n * {@link StorageProvider} backed by a Google Cloud Storage bucket.\n */\n@Injectable()\nexport class GcsStorageProvider extends StorageProvider {\n constructor(\n private readonly client: Storage,\n private readonly options: GcsStorageProviderOptions,\n ) {\n super();\n }\n\n async write(key: string, body: Readable | Buffer | string, options?: StorageWriteOptions): Promise<void> {\n const file = this.file(key);\n const metadata = {\n contentType: options?.contentType,\n cacheControl: options?.cacheControl,\n metadata: options?.metadata,\n };\n\n if (body instanceof Readable) {\n await pipeline(body, file.createWriteStream({ metadata }));\n return;\n }\n\n await file.save(body instanceof Buffer ? body : Buffer.from(body), { metadata });\n }\n\n async read(key: string, options?: StorageReadOptions): Promise<Readable> {\n // GCS surfaces a missing object on the stream's 'error' event rather than as\n // a rejected promise, so pre-check existence to honour the not-found contract.\n const exists = await this.exists(key);\n if (!exists) {\n throw new StorageObjectNotFoundError(key);\n }\n // `start`/`end` are inclusive byte offsets, matching the read contract.\n return this.file(key).createReadStream(options?.range ? { start: options.range.start, end: options.range.end } : undefined);\n }\n\n async stat(key: string): Promise<StorageObjectMetadata> {\n try {\n const [metadata] = await this.file(key).getMetadata();\n return {\n key,\n size: typeof metadata.size === 'string' ? Number(metadata.size) : (metadata.size ?? 0),\n contentType: metadata.contentType,\n etag: metadata.etag,\n lastModified: metadata.updated ? DateTime.fromISO(metadata.updated) : undefined,\n metadata: metadata.metadata as Record<string, string> | undefined,\n };\n } catch (error) {\n throw this.mapError(key, error);\n }\n }\n\n async exists(key: string): Promise<boolean> {\n try {\n const [exists] = await this.file(key).exists();\n return exists;\n } catch (error) {\n // `exists()` reports absence as `false`; a thrown error is a real failure\n // (e.g. a permission denial) and should surface.\n throw this.mapError(key, error);\n }\n }\n\n async delete(key: string): Promise<void> {\n await this.file(key).delete({ ignoreNotFound: true });\n }\n\n async copy(sourceKey: string, destinationKey: string): Promise<void> {\n try {\n await this.file(sourceKey).copy(this.file(destinationKey));\n } catch (error) {\n throw this.mapError(sourceKey, error);\n }\n }\n\n async move(sourceKey: string, destinationKey: string): Promise<void> {\n try {\n // GCS `move` is a server-side copy followed by a delete of the source.\n await this.file(sourceKey).move(this.file(destinationKey));\n } catch (error) {\n throw this.mapError(sourceKey, error);\n }\n }\n\n async list(options?: StorageListOptions): Promise<StorageListResult> {\n const [files, nextQuery] = await this.client.bucket(this.options.bucket).getFiles({\n prefix: options?.prefix,\n maxResults: options?.limit,\n pageToken: options?.cursor,\n autoPaginate: false,\n });\n\n const objects: StorageObjectMetadata[] = files.map(file => ({\n key: file.name,\n size: typeof file.metadata.size === 'string' ? Number(file.metadata.size) : (file.metadata.size ?? 0),\n contentType: file.metadata.contentType,\n etag: file.metadata.etag,\n lastModified: file.metadata.updated ? DateTime.fromISO(file.metadata.updated) : undefined,\n }));\n\n return {\n objects,\n cursor: (nextQuery as { pageToken?: string } | undefined)?.pageToken,\n };\n }\n\n async getSignedUrl(key: string, options: SignedUrlOptions): Promise<string> {\n const [url] = await this.file(key).getSignedUrl({\n version: 'v4',\n action: options.operation === 'write' ? 'write' : 'read',\n expires: DateTime.now().plus(options.expiresIn).toMillis(),\n contentType: options.operation === 'write' ? options.contentType : undefined,\n });\n return url;\n }\n\n private file(key: string): File {\n return this.client.bucket(this.options.bucket).file(key);\n }\n\n private mapError(key: string, error: unknown): unknown {\n if (isNotFound(error)) {\n return new StorageObjectNotFoundError(key, { cause: error });\n }\n if (isAccessDenied(error)) {\n return new StorageAccessDeniedError(key, { cause: error });\n }\n return error;\n }\n}\n\nfunction statusCode(error: unknown): number | undefined {\n return typeof error === 'object' && error !== null ? (error as { code?: number }).code : undefined;\n}\n\nfunction isNotFound(error: unknown): boolean {\n return statusCode(error) === 404;\n}\n\nfunction isAccessDenied(error: unknown): boolean {\n return statusCode(error) === 403;\n}\n"],"mappings":"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","names":["Injectable","StorageProvider","ServerkitError","StorageError","ServerkitError","message","options","name","StorageObjectNotFoundError","key","StorageAccessDeniedError","StorageOperationNotSupportedError","operation","Injectable","createReadStream","createWriteStream","copyFile","mkdir","readdir","rename","rm","stat","fsStat","unlink","dirname","join","posix","relative","resolve","sep","pipeline","Readable","DateTime","mime","DiskStorageProviderOptions","rootDir","publicBaseUrl","init","DiskStorageProvider","StorageProvider","options","write","key","body","_options","path","resolveKey","mkdir","dirname","recursive","source","Readable","from","Buffer","pipeline","createWriteStream","read","assertExists","range","createReadStream","start","end","stat","stats","contentType","mime","lookup","size","undefined","lastModified","DateTime","fromJSDate","mtime","exists","fsStat","isFile","error","isNotFound","isAccessDenied","StorageAccessDeniedError","cause","delete","rm","force","copy","sourceKey","destinationKey","destination","copyFile","move","rename","code","unlink","list","prefix","keys","walk","matched","filter","startsWith","sort","cursor","findIndex","length","limit","page","slice","objects","Promise","all","map","hasMore","getSignedUrl","StorageOperationNotSupportedError","encodedPath","replace","split","encodeURIComponent","join","resolve","rel","relative","sep","StorageObjectNotFoundError","dir","into","entries","readdir","withFileTypes","entry","full","name","isDirectory","push","posix","Injectable","Readable","DateTime","CopyObjectCommand","DeleteObjectCommand","GetObjectCommand","HeadObjectCommand","ListObjectsV2Command","PutObjectCommand","Upload","getSignedUrl","S3StorageProviderOptions","bucket","init","S3StorageProvider","StorageProvider","client","options","write","key","body","shared","Bucket","Key","ContentType","contentType","ContentLength","contentLength","CacheControl","cacheControl","Metadata","metadata","Readable","upload","Upload","params","Body","done","send","PutObjectCommand","read","response","GetObjectCommand","Range","toRangeHeader","range","error","mapError","stat","head","HeadObjectCommand","size","etag","ETag","lastModified","LastModified","DateTime","fromJSDate","undefined","exists","isNotFound","delete","DeleteObjectCommand","copy","sourceKey","destinationKey","CopyObjectCommand","CopySource","encodeURIComponent","move","list","ListObjectsV2Command","Prefix","prefix","MaxKeys","limit","ContinuationToken","cursor","objects","Contents","map","item","Size","IsTruncated","NextContinuationToken","getSignedUrl","command","operation","expiresIn","Math","round","as","StorageObjectNotFoundError","cause","isAccessDenied","StorageAccessDeniedError","errorName","name","statusCode","$metadata","httpStatusCode","start","end","Injectable","Readable","pipeline","DateTime","GcsStorageProviderOptions","bucket","init","GcsStorageProvider","StorageProvider","client","options","write","key","body","file","metadata","contentType","cacheControl","Readable","pipeline","createWriteStream","save","Buffer","from","read","exists","StorageObjectNotFoundError","createReadStream","range","start","end","undefined","stat","getMetadata","size","Number","etag","lastModified","updated","DateTime","fromISO","error","mapError","delete","ignoreNotFound","copy","sourceKey","destinationKey","move","list","files","nextQuery","getFiles","prefix","maxResults","limit","pageToken","cursor","autoPaginate","objects","map","name","getSignedUrl","url","version","action","operation","expires","now","plus","expiresIn","toMillis","isNotFound","cause","isAccessDenied","StorageAccessDeniedError","statusCode","code"]}
@@ -0,0 +1,48 @@
1
+ import { Readable } from 'node:stream';
2
+ import { type S3Client } from '@aws-sdk/client-s3';
3
+ import type { SignedUrlOptions, StorageListOptions, StorageListResult, StorageObjectMetadata, StorageReadOptions, StorageWriteOptions } from './storage.provider.js';
4
+ import { StorageProvider } from './storage.provider.js';
5
+ /**
6
+ * Construction options for {@link S3StorageProvider}.
7
+ *
8
+ * A class (not an interface) so it can serve as an InjectKit token — register
9
+ * an instance and the container can construct {@link S3StorageProvider}.
10
+ */
11
+ export declare class S3StorageProviderOptions {
12
+ /** Name of the bucket all keys live in. */
13
+ readonly bucket: string;
14
+ constructor(init: {
15
+ bucket: string;
16
+ });
17
+ }
18
+ /**
19
+ * {@link StorageProvider} backed by an AWS S3 (or S3-compatible) bucket.
20
+ *
21
+ * Streaming writes go through `@aws-sdk/lib-storage`'s multipart `Upload`;
22
+ * buffer/string writes use a single `PutObject`. Signed URLs are produced with
23
+ * `@aws-sdk/s3-request-presigner`.
24
+ */
25
+ export declare class S3StorageProvider extends StorageProvider {
26
+ private readonly client;
27
+ private readonly options;
28
+ constructor(client: S3Client, options: S3StorageProviderOptions);
29
+ write(key: string, body: Readable | Buffer | string, options?: StorageWriteOptions): Promise<void>;
30
+ read(key: string, options?: StorageReadOptions): Promise<Readable>;
31
+ stat(key: string): Promise<StorageObjectMetadata>;
32
+ exists(key: string): Promise<boolean>;
33
+ delete(key: string): Promise<void>;
34
+ /**
35
+ * Server-side copy via `CopyObjectCommand`.
36
+ *
37
+ * Note: S3's single-request `CopyObject` is capped at 5 GB. Copying a larger
38
+ * object requires a multipart copy (`UploadPartCopy`), which this provider
39
+ * does not yet implement — such copies will fail. {@link move} inherits the
40
+ * same limit since it delegates to `copy`.
41
+ */
42
+ copy(sourceKey: string, destinationKey: string): Promise<void>;
43
+ move(sourceKey: string, destinationKey: string): Promise<void>;
44
+ list(options?: StorageListOptions): Promise<StorageListResult>;
45
+ getSignedUrl(key: string, options: SignedUrlOptions): Promise<string>;
46
+ private mapError;
47
+ }
48
+ //# sourceMappingURL=s3.storage.provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"s3.storage.provider.d.ts","sourceRoot":"","sources":["../src/s3.storage.provider.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC,OAAO,EAOL,KAAK,QAAQ,EACd,MAAM,oBAAoB,CAAC;AAI5B,OAAO,KAAK,EACV,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EACjB,qBAAqB,EACrB,kBAAkB,EAClB,mBAAmB,EACpB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAExD;;;;;GAKG;AACH,qBAAa,wBAAwB;IACnC,2CAA2C;IAC3C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;gBAEZ,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE;CAGrC;AAED;;;;;;GAMG;AACH,qBACa,iBAAkB,SAAQ,eAAe;IAElD,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBADP,MAAM,EAAE,QAAQ,EAChB,OAAO,EAAE,wBAAwB;IAK9C,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,MAAM,GAAG,MAAM,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBlG,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,QAAQ,CAAC;IAWlE,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAgBjD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAarC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKxC;;;;;;;OAOG;IACG,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAe9D,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAM9D,IAAI,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAuB9D,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC;IAQ3E,OAAO,CAAC,QAAQ;CASjB"}
@@ -0,0 +1,47 @@
1
+ import { ServerkitError } from '@maroonedsoftware/errors';
2
+ /**
3
+ * Base class for all errors thrown by a {@link StorageProvider}.
4
+ *
5
+ * Catch this to handle any storage failure generically; catch a subclass to
6
+ * distinguish specific conditions (missing object, unsupported operation).
7
+ * Extends `ServerkitError`, so `errorMiddleware` renders it as a 500 with any
8
+ * attached `details`.
9
+ */
10
+ export declare class StorageError extends ServerkitError {
11
+ constructor(message: string, options?: {
12
+ cause?: unknown;
13
+ });
14
+ }
15
+ /**
16
+ * Thrown when {@link StorageProvider.read} or {@link StorageProvider.stat} is
17
+ * called for a key that does not exist. The offending key is available on
18
+ * `key` for callers that want to surface it.
19
+ */
20
+ export declare class StorageObjectNotFoundError extends StorageError {
21
+ readonly key: string;
22
+ constructor(key: string, options?: {
23
+ cause?: unknown;
24
+ });
25
+ }
26
+ /**
27
+ * Thrown when the backend rejects an operation for permission reasons (an S3 or
28
+ * GCS `403`, a local-filesystem `EACCES`/`EPERM`). The offending key is
29
+ * available on `key`.
30
+ */
31
+ export declare class StorageAccessDeniedError extends StorageError {
32
+ readonly key: string;
33
+ constructor(key: string, options?: {
34
+ cause?: unknown;
35
+ });
36
+ }
37
+ /**
38
+ * Thrown when an operation is not supported by the active backend — for
39
+ * example {@link StorageProvider.getSignedUrl} on a disk provider that has no
40
+ * public base URL configured.
41
+ */
42
+ export declare class StorageOperationNotSupportedError extends StorageError {
43
+ constructor(operation: string, options?: {
44
+ cause?: unknown;
45
+ });
46
+ }
47
+ //# sourceMappingURL=storage.errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.errors.d.ts","sourceRoot":"","sources":["../src/storage.errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAE1D;;;;;;;GAOG;AACH,qBAAa,YAAa,SAAQ,cAAc;gBAClC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAI3D;AAED;;;;GAIG;AACH,qBAAa,0BAA2B,SAAQ,YAAY;IAExD,QAAQ,CAAC,GAAG,EAAE,MAAM;gBAAX,GAAG,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAKhC;AAED;;;;GAIG;AACH,qBAAa,wBAAyB,SAAQ,YAAY;IAEtD,QAAQ,CAAC,GAAG,EAAE,MAAM;gBAAX,GAAG,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAKhC;AAED;;;;GAIG;AACH,qBAAa,iCAAkC,SAAQ,YAAY;gBACrD,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAI7D"}
@@ -0,0 +1,126 @@
1
+ import { Readable } from 'node:stream';
2
+ import { DateTime, Duration } from 'luxon';
3
+ /**
4
+ * Options applied when writing an object. All fields are optional; backends
5
+ * that cannot honour a given field ignore it (documented per provider).
6
+ */
7
+ export interface StorageWriteOptions {
8
+ /** MIME type stored alongside the object (e.g. `image/png`). */
9
+ contentType?: string;
10
+ /** Byte length of the body, when known ahead of time. */
11
+ contentLength?: number;
12
+ /** `Cache-Control` header persisted with the object (cloud backends). */
13
+ cacheControl?: string;
14
+ /** Arbitrary user metadata. Unsupported on the disk backend (ignored). */
15
+ metadata?: Record<string, string>;
16
+ }
17
+ /**
18
+ * Options for {@link StorageProvider.read}.
19
+ */
20
+ export interface StorageReadOptions {
21
+ /**
22
+ * Restrict the read to a byte range. `start` and `end` are both inclusive
23
+ * (HTTP `Range` semantics); omit `end` to read from `start` to the end of the
24
+ * object. Useful for media streaming and resumable downloads.
25
+ */
26
+ range?: {
27
+ start: number;
28
+ end?: number;
29
+ };
30
+ }
31
+ /**
32
+ * Metadata describing a stored object, returned by {@link StorageProvider.stat}
33
+ * and {@link StorageProvider.list}.
34
+ */
35
+ export interface StorageObjectMetadata {
36
+ /** The object's key. */
37
+ key: string;
38
+ /** Size in bytes. */
39
+ size: number;
40
+ /** MIME type, when known. */
41
+ contentType?: string;
42
+ /** Backend entity tag, when provided. */
43
+ etag?: string;
44
+ /** Last modification time, when known. */
45
+ lastModified?: DateTime;
46
+ /** User metadata, when the backend supports and returns it. */
47
+ metadata?: Record<string, string>;
48
+ }
49
+ /**
50
+ * Options for {@link StorageProvider.list}.
51
+ */
52
+ export interface StorageListOptions {
53
+ /** Restrict results to keys beginning with this prefix. */
54
+ prefix?: string;
55
+ /** Maximum number of objects to return in this page. */
56
+ limit?: number;
57
+ /** Opaque continuation token from a previous {@link StorageListResult}. */
58
+ cursor?: string;
59
+ }
60
+ /**
61
+ * A single page of {@link StorageProvider.list} results.
62
+ */
63
+ export interface StorageListResult {
64
+ /** Objects in this page. */
65
+ objects: StorageObjectMetadata[];
66
+ /** Continuation token for the next page, or `undefined` when exhausted. */
67
+ cursor?: string;
68
+ }
69
+ /** Whether a signed URL grants read (download) or write (upload) access. */
70
+ export type SignedUrlOperation = 'read' | 'write';
71
+ /**
72
+ * Options for {@link StorageProvider.getSignedUrl}.
73
+ */
74
+ export interface SignedUrlOptions {
75
+ /** The operation the URL authorises. */
76
+ operation: SignedUrlOperation;
77
+ /** How long the URL remains valid. */
78
+ expiresIn: Duration;
79
+ /** Content type the client must use when uploading (`write` URLs). */
80
+ contentType?: string;
81
+ }
82
+ /**
83
+ * Backend-agnostic object storage. Implementations wrap a concrete backend
84
+ * (local disk, AWS S3, Google Cloud Storage). Bind a concrete provider to this
85
+ * token in the DI container so consumers depend only on the abstraction:
86
+ *
87
+ * ```ts
88
+ * container.bind(StorageProvider).toConstantValue(new DiskStorageProvider({ rootDir: '/var/data' }));
89
+ * ```
90
+ *
91
+ * Keys are hierarchical, `/`-separated paths (e.g. `users/42/avatar.png`), not
92
+ * flat filenames.
93
+ *
94
+ * ## Behaviour contract
95
+ * - {@link read} / {@link stat} on a missing key throw `StorageObjectNotFoundError`.
96
+ * - {@link exists} never throws for a missing key — it returns `false`.
97
+ * - Operations that hit a permission failure throw `StorageAccessDeniedError`.
98
+ * - {@link delete} is idempotent — deleting a missing key is a no-op.
99
+ * - {@link copy} / {@link move} throw `StorageObjectNotFoundError` when the
100
+ * source is missing, and overwrite the destination if it already exists. Both
101
+ * operate within this backend only (same bucket / root) — cross-backend or
102
+ * cross-bucket transfers are out of scope.
103
+ * - {@link getSignedUrl} throws `StorageOperationNotSupportedError` on backends
104
+ * that cannot sign URLs.
105
+ */
106
+ export declare abstract class StorageProvider {
107
+ /** Write `body` to `key`, overwriting any existing object. */
108
+ abstract write(key: string, body: Readable | Buffer | string, options?: StorageWriteOptions): Promise<void>;
109
+ /** Open a readable stream for `key`, optionally for a byte range. Throws if the key does not exist. */
110
+ abstract read(key: string, options?: StorageReadOptions): Promise<Readable>;
111
+ /** Fetch metadata for `key` without reading its body. Throws if absent. */
112
+ abstract stat(key: string): Promise<StorageObjectMetadata>;
113
+ /** Resolve to `true` if `key` exists, `false` otherwise. Never throws for absence. */
114
+ abstract exists(key: string): Promise<boolean>;
115
+ /** Delete `key`. A no-op if the key does not exist. */
116
+ abstract delete(key: string): Promise<void>;
117
+ /** Copy `sourceKey` to `destinationKey` within this backend, overwriting the destination. Throws if the source is missing. */
118
+ abstract copy(sourceKey: string, destinationKey: string): Promise<void>;
119
+ /** Move/rename `sourceKey` to `destinationKey` within this backend, overwriting the destination. Throws if the source is missing. */
120
+ abstract move(sourceKey: string, destinationKey: string): Promise<void>;
121
+ /** List a single page of objects, optionally filtered by prefix. */
122
+ abstract list(options?: StorageListOptions): Promise<StorageListResult>;
123
+ /** Generate a time-limited signed URL for direct client read/write access. */
124
+ abstract getSignedUrl(key: string, options: SignedUrlOptions): Promise<string>;
125
+ }
126
+ //# sourceMappingURL=storage.provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.provider.d.ts","sourceRoot":"","sources":["../src/storage.provider.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maroonedsoftware/storage",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Storage utilities for ServerKit.",
5
5
  "author": {
6
6
  "name": "Marooned Software",