@maroonedsoftware/storage 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,6 +18,16 @@ pnpm add @aws-sdk/client-s3 @aws-sdk/lib-storage @aws-sdk/s3-request-presigner
18
18
  pnpm add @google-cloud/storage
19
19
  ```
20
20
 
21
+ ### Entry points
22
+
23
+ The cloud backends live behind subpath exports so importing the core never loads an SDK you didn't install:
24
+
25
+ | Import | Contents | Pulls in |
26
+ |--------|----------|----------|
27
+ | `@maroonedsoftware/storage` | `StorageProvider`, the errors, `DiskStorageProvider` | nothing extra |
28
+ | `@maroonedsoftware/storage/s3` | `S3StorageProvider`, `S3StorageProviderOptions` | `@aws-sdk/*` |
29
+ | `@maroonedsoftware/storage/gcs` | `GcsStorageProvider`, `GcsStorageProviderOptions` | `@google-cloud/storage` |
30
+
21
31
  ## Usage
22
32
 
23
33
  ### 1. Bind a provider in your DI container
@@ -33,7 +43,8 @@ container.bind(StorageProvider).toConstantValue(new DiskStorageProvider(new Disk
33
43
 
34
44
  ```typescript
35
45
  import { S3Client } from '@aws-sdk/client-s3';
36
- import { StorageProvider, S3StorageProvider, S3StorageProviderOptions } from '@maroonedsoftware/storage';
46
+ import { StorageProvider } from '@maroonedsoftware/storage';
47
+ import { S3StorageProvider, S3StorageProviderOptions } from '@maroonedsoftware/storage/s3';
37
48
 
38
49
  // AWS S3 — both the client and the options are injectable tokens
39
50
  container.bind(S3Client).toConstantValue(new S3Client({ region: 'us-east-1' }));
@@ -43,7 +54,8 @@ container.bind(StorageProvider).to(S3StorageProvider);
43
54
 
44
55
  ```typescript
45
56
  import { Storage } from '@google-cloud/storage';
46
- import { StorageProvider, GcsStorageProvider, GcsStorageProviderOptions } from '@maroonedsoftware/storage';
57
+ import { StorageProvider } from '@maroonedsoftware/storage';
58
+ import { GcsStorageProvider, GcsStorageProviderOptions } from '@maroonedsoftware/storage/gcs';
47
59
 
48
60
  // Google Cloud Storage
49
61
  container.bind(Storage).toConstantValue(new Storage());
@@ -168,5 +180,7 @@ export class InMemoryStorageProvider extends StorageProvider {
168
180
  The provider options are plain injectable classes, so the package stays decoupled from `@maroonedsoftware/appconfig`. To drive a bucket name from typed config, bridge it at bootstrap rather than importing AppConfig into the provider:
169
181
 
170
182
  ```typescript
183
+ import { S3StorageProviderOptions } from '@maroonedsoftware/storage/s3';
184
+
171
185
  registry.register(S3StorageProviderOptions).useFactory(c => new S3StorageProviderOptions({ bucket: c.get(StorageOptions).value.bucket }));
172
186
  ```
@@ -0,0 +1,71 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+
4
+ // src/storage.errors.ts
5
+ import { ServerkitError } from "@maroonedsoftware/errors";
6
+ var StorageError = class extends ServerkitError {
7
+ static {
8
+ __name(this, "StorageError");
9
+ }
10
+ constructor(message, options) {
11
+ super(message, options);
12
+ this.name = "StorageError";
13
+ }
14
+ };
15
+ var StorageObjectNotFoundError = class extends StorageError {
16
+ static {
17
+ __name(this, "StorageObjectNotFoundError");
18
+ }
19
+ key;
20
+ constructor(key, options) {
21
+ super(`storage object '${key}' not found`, options), this.key = key;
22
+ this.name = "StorageObjectNotFoundError";
23
+ }
24
+ };
25
+ var StorageAccessDeniedError = class extends StorageError {
26
+ static {
27
+ __name(this, "StorageAccessDeniedError");
28
+ }
29
+ key;
30
+ constructor(key, options) {
31
+ super(`access denied for storage object '${key}'`, options), this.key = key;
32
+ this.name = "StorageAccessDeniedError";
33
+ }
34
+ };
35
+ var StorageOperationNotSupportedError = class extends StorageError {
36
+ static {
37
+ __name(this, "StorageOperationNotSupportedError");
38
+ }
39
+ constructor(operation, options) {
40
+ super(`storage operation '${operation}' is not supported by this backend`, options);
41
+ this.name = "StorageOperationNotSupportedError";
42
+ }
43
+ };
44
+
45
+ // src/storage.provider.ts
46
+ import { Injectable } from "injectkit";
47
+ function _ts_decorate(decorators, target, key, desc) {
48
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
49
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
50
+ 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;
51
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
52
+ }
53
+ __name(_ts_decorate, "_ts_decorate");
54
+ var StorageProvider = class {
55
+ static {
56
+ __name(this, "StorageProvider");
57
+ }
58
+ };
59
+ StorageProvider = _ts_decorate([
60
+ Injectable()
61
+ ], StorageProvider);
62
+
63
+ export {
64
+ __name,
65
+ StorageError,
66
+ StorageObjectNotFoundError,
67
+ StorageAccessDeniedError,
68
+ StorageOperationNotSupportedError,
69
+ StorageProvider
70
+ };
71
+ //# sourceMappingURL=chunk-UBT2CAIO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/storage.errors.ts","../src/storage.provider.ts"],"sourcesContent":["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 { 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"],"mappings":";;;;AAAA,SAASA,sBAAsB;AAUxB,IAAMC,eAAN,cAA2BC,eAAAA;EAVlC,OAUkCA;;;EAChC,YAAYC,SAAiBC,SAA+B;AAC1D,UAAMD,SAASC,OAAAA;AACf,SAAKC,OAAO;EACd;AACF;AAOO,IAAMC,6BAAN,cAAyCL,aAAAA;EAtBhD,OAsBgDA;;;;EAC9C,YACWM,KACTH,SACA;AACA,UAAM,mBAAmBG,GAAAA,eAAkBH,OAAAA,GAAAA,KAHlCG,MAAAA;AAIT,SAAKF,OAAO;EACd;AACF;AAOO,IAAMG,2BAAN,cAAuCP,aAAAA;EArC9C,OAqC8CA;;;;EAC5C,YACWM,KACTH,SACA;AACA,UAAM,qCAAqCG,GAAAA,KAAQH,OAAAA,GAAAA,KAH1CG,MAAAA;AAIT,SAAKF,OAAO;EACd;AACF;AAOO,IAAMI,oCAAN,cAAgDR,aAAAA;EApDvD,OAoDuDA;;;EACrD,YAAYS,WAAmBN,SAA+B;AAC5D,UAAM,sBAAsBM,SAAAA,sCAA+CN,OAAAA;AAC3E,SAAKC,OAAO;EACd;AACF;;;ACzDA,SAASM,kBAAkB;;;;;;;;AAgHpB,IAAeC,kBAAf,MAAeA;SAAAA;;;AAmBtB;;;;","names":["ServerkitError","StorageError","ServerkitError","message","options","name","StorageObjectNotFoundError","key","StorageAccessDeniedError","StorageOperationNotSupportedError","operation","Injectable","StorageProvider"]}
@@ -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"}
package/dist/gcs.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './gcs.storage.provider.js';
2
+ //# sourceMappingURL=gcs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gcs.d.ts","sourceRoot":"","sources":["../src/gcs.ts"],"names":[],"mappings":"AAGA,cAAc,2BAA2B,CAAC"}
package/dist/gcs.js ADDED
@@ -0,0 +1,182 @@
1
+ import {
2
+ StorageAccessDeniedError,
3
+ StorageObjectNotFoundError,
4
+ StorageProvider,
5
+ __name
6
+ } from "./chunk-UBT2CAIO.js";
7
+
8
+ // src/gcs.storage.provider.ts
9
+ import { Injectable } from "injectkit";
10
+ import { Readable } from "stream";
11
+ import { pipeline } from "stream/promises";
12
+ import { DateTime } from "luxon";
13
+ import { Storage } from "@google-cloud/storage";
14
+ function _ts_decorate(decorators, target, key, desc) {
15
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
16
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
17
+ 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;
18
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
19
+ }
20
+ __name(_ts_decorate, "_ts_decorate");
21
+ function _ts_metadata(k, v) {
22
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
23
+ }
24
+ __name(_ts_metadata, "_ts_metadata");
25
+ var GcsStorageProviderOptions = class {
26
+ static {
27
+ __name(this, "GcsStorageProviderOptions");
28
+ }
29
+ /** Name of the bucket all keys live in. */
30
+ bucket;
31
+ constructor(init) {
32
+ this.bucket = init.bucket;
33
+ }
34
+ };
35
+ var GcsStorageProvider = class extends StorageProvider {
36
+ static {
37
+ __name(this, "GcsStorageProvider");
38
+ }
39
+ client;
40
+ options;
41
+ constructor(client, options) {
42
+ super(), this.client = client, this.options = options;
43
+ }
44
+ async write(key, body, options) {
45
+ const file = this.file(key);
46
+ const metadata = {
47
+ contentType: options?.contentType,
48
+ cacheControl: options?.cacheControl,
49
+ metadata: options?.metadata
50
+ };
51
+ if (body instanceof Readable) {
52
+ await pipeline(body, file.createWriteStream({
53
+ metadata
54
+ }));
55
+ return;
56
+ }
57
+ await file.save(body instanceof Buffer ? body : Buffer.from(body), {
58
+ metadata
59
+ });
60
+ }
61
+ async read(key, options) {
62
+ const exists = await this.exists(key);
63
+ if (!exists) {
64
+ throw new StorageObjectNotFoundError(key);
65
+ }
66
+ return this.file(key).createReadStream(options?.range ? {
67
+ start: options.range.start,
68
+ end: options.range.end
69
+ } : void 0);
70
+ }
71
+ async stat(key) {
72
+ try {
73
+ const [metadata] = await this.file(key).getMetadata();
74
+ return {
75
+ key,
76
+ size: typeof metadata.size === "string" ? Number(metadata.size) : metadata.size ?? 0,
77
+ contentType: metadata.contentType,
78
+ etag: metadata.etag,
79
+ lastModified: metadata.updated ? DateTime.fromISO(metadata.updated) : void 0,
80
+ metadata: metadata.metadata
81
+ };
82
+ } catch (error) {
83
+ throw this.mapError(key, error);
84
+ }
85
+ }
86
+ async exists(key) {
87
+ try {
88
+ const [exists] = await this.file(key).exists();
89
+ return exists;
90
+ } catch (error) {
91
+ throw this.mapError(key, error);
92
+ }
93
+ }
94
+ async delete(key) {
95
+ await this.file(key).delete({
96
+ ignoreNotFound: true
97
+ });
98
+ }
99
+ async copy(sourceKey, destinationKey) {
100
+ try {
101
+ await this.file(sourceKey).copy(this.file(destinationKey));
102
+ } catch (error) {
103
+ throw this.mapError(sourceKey, error);
104
+ }
105
+ }
106
+ async move(sourceKey, destinationKey) {
107
+ try {
108
+ await this.file(sourceKey).move(this.file(destinationKey));
109
+ } catch (error) {
110
+ throw this.mapError(sourceKey, error);
111
+ }
112
+ }
113
+ async list(options) {
114
+ const [files, nextQuery] = await this.client.bucket(this.options.bucket).getFiles({
115
+ prefix: options?.prefix,
116
+ maxResults: options?.limit,
117
+ pageToken: options?.cursor,
118
+ autoPaginate: false
119
+ });
120
+ const objects = files.map((file) => ({
121
+ key: file.name,
122
+ size: typeof file.metadata.size === "string" ? Number(file.metadata.size) : file.metadata.size ?? 0,
123
+ contentType: file.metadata.contentType,
124
+ etag: file.metadata.etag,
125
+ lastModified: file.metadata.updated ? DateTime.fromISO(file.metadata.updated) : void 0
126
+ }));
127
+ return {
128
+ objects,
129
+ cursor: nextQuery?.pageToken
130
+ };
131
+ }
132
+ async getSignedUrl(key, options) {
133
+ const [url] = await this.file(key).getSignedUrl({
134
+ version: "v4",
135
+ action: options.operation === "write" ? "write" : "read",
136
+ expires: DateTime.now().plus(options.expiresIn).toMillis(),
137
+ contentType: options.operation === "write" ? options.contentType : void 0
138
+ });
139
+ return url;
140
+ }
141
+ file(key) {
142
+ return this.client.bucket(this.options.bucket).file(key);
143
+ }
144
+ mapError(key, error) {
145
+ if (isNotFound(error)) {
146
+ return new StorageObjectNotFoundError(key, {
147
+ cause: error
148
+ });
149
+ }
150
+ if (isAccessDenied(error)) {
151
+ return new StorageAccessDeniedError(key, {
152
+ cause: error
153
+ });
154
+ }
155
+ return error;
156
+ }
157
+ };
158
+ GcsStorageProvider = _ts_decorate([
159
+ Injectable(),
160
+ _ts_metadata("design:type", Function),
161
+ _ts_metadata("design:paramtypes", [
162
+ typeof Storage === "undefined" ? Object : Storage,
163
+ typeof GcsStorageProviderOptions === "undefined" ? Object : GcsStorageProviderOptions
164
+ ])
165
+ ], GcsStorageProvider);
166
+ function statusCode(error) {
167
+ return typeof error === "object" && error !== null ? error.code : void 0;
168
+ }
169
+ __name(statusCode, "statusCode");
170
+ function isNotFound(error) {
171
+ return statusCode(error) === 404;
172
+ }
173
+ __name(isNotFound, "isNotFound");
174
+ function isAccessDenied(error) {
175
+ return statusCode(error) === 403;
176
+ }
177
+ __name(isAccessDenied, "isAccessDenied");
178
+ export {
179
+ GcsStorageProvider,
180
+ GcsStorageProviderOptions
181
+ };
182
+ //# sourceMappingURL=gcs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/gcs.storage.provider.ts"],"sourcesContent":["import { Injectable } from 'injectkit';\nimport { Readable } from 'node:stream';\nimport { pipeline } from 'node:stream/promises';\nimport { DateTime } from 'luxon';\nimport { Storage } from '@google-cloud/storage';\nimport type { File } 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","Readable","pipeline","DateTime","Storage","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,37 @@
1
+ import { Readable } from 'node:stream';
2
+ import { 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,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAGhD,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,4 @@
1
+ export * from './storage.provider.js';
2
+ export * from './storage.errors.js';
3
+ export * from './disk.storage.provider.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,4BAA4B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,234 @@
1
+ import {
2
+ StorageAccessDeniedError,
3
+ StorageError,
4
+ StorageObjectNotFoundError,
5
+ StorageOperationNotSupportedError,
6
+ StorageProvider,
7
+ __name
8
+ } from "./chunk-UBT2CAIO.js";
9
+
10
+ // src/disk.storage.provider.ts
11
+ import { Injectable } from "injectkit";
12
+ import { createReadStream, createWriteStream } from "fs";
13
+ import { copyFile, mkdir, readdir, rename, rm, stat as fsStat, unlink } from "fs/promises";
14
+ import { dirname, join, posix, relative, resolve, sep } from "path";
15
+ import { pipeline } from "stream/promises";
16
+ import { Readable } from "stream";
17
+ import { DateTime } from "luxon";
18
+ import mime from "mime-types";
19
+ function _ts_decorate(decorators, target, key, desc) {
20
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
21
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
22
+ 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;
23
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
24
+ }
25
+ __name(_ts_decorate, "_ts_decorate");
26
+ function _ts_metadata(k, v) {
27
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
28
+ }
29
+ __name(_ts_metadata, "_ts_metadata");
30
+ var DiskStorageProviderOptions = class {
31
+ static {
32
+ __name(this, "DiskStorageProviderOptions");
33
+ }
34
+ /** Root directory the provider stores objects under. */
35
+ rootDir;
36
+ /**
37
+ * Base URL objects are publicly served from. When set, {@link DiskStorageProvider.getSignedUrl}
38
+ * returns `${publicBaseUrl}/${key}`; when omitted, signing is unsupported.
39
+ */
40
+ publicBaseUrl;
41
+ constructor(init) {
42
+ this.rootDir = init.rootDir;
43
+ this.publicBaseUrl = init.publicBaseUrl;
44
+ }
45
+ };
46
+ var DiskStorageProvider = class extends StorageProvider {
47
+ static {
48
+ __name(this, "DiskStorageProvider");
49
+ }
50
+ options;
51
+ constructor(options) {
52
+ super(), this.options = options;
53
+ }
54
+ async write(key, body, _options) {
55
+ const path = this.resolveKey(key);
56
+ await mkdir(dirname(path), {
57
+ recursive: true
58
+ });
59
+ const source = body instanceof Readable ? body : Readable.from(body instanceof Buffer ? body : Buffer.from(body));
60
+ await pipeline(source, createWriteStream(path));
61
+ }
62
+ async read(key, options) {
63
+ const path = this.resolveKey(key);
64
+ await this.assertExists(key, path);
65
+ return options?.range ? createReadStream(path, {
66
+ start: options.range.start,
67
+ end: options.range.end
68
+ }) : createReadStream(path);
69
+ }
70
+ async stat(key) {
71
+ const path = this.resolveKey(key);
72
+ const stats = await this.assertExists(key, path);
73
+ const contentType = mime.lookup(path);
74
+ return {
75
+ key,
76
+ size: stats.size,
77
+ contentType: contentType === false ? void 0 : contentType,
78
+ lastModified: DateTime.fromJSDate(stats.mtime)
79
+ };
80
+ }
81
+ async exists(key) {
82
+ try {
83
+ const stats = await fsStat(this.resolveKey(key));
84
+ return stats.isFile();
85
+ } catch (error) {
86
+ if (isNotFound(error)) {
87
+ return false;
88
+ }
89
+ if (isAccessDenied(error)) {
90
+ throw new StorageAccessDeniedError(key, {
91
+ cause: error
92
+ });
93
+ }
94
+ throw error;
95
+ }
96
+ }
97
+ async delete(key) {
98
+ await rm(this.resolveKey(key), {
99
+ force: true
100
+ });
101
+ }
102
+ async copy(sourceKey, destinationKey) {
103
+ const source = this.resolveKey(sourceKey);
104
+ const destination = this.resolveKey(destinationKey);
105
+ await this.assertExists(sourceKey, source);
106
+ await mkdir(dirname(destination), {
107
+ recursive: true
108
+ });
109
+ await copyFile(source, destination);
110
+ }
111
+ async move(sourceKey, destinationKey) {
112
+ const source = this.resolveKey(sourceKey);
113
+ const destination = this.resolveKey(destinationKey);
114
+ await this.assertExists(sourceKey, source);
115
+ await mkdir(dirname(destination), {
116
+ recursive: true
117
+ });
118
+ try {
119
+ await rename(source, destination);
120
+ } catch (error) {
121
+ if (typeof error === "object" && error !== null && error.code === "EXDEV") {
122
+ await copyFile(source, destination);
123
+ await unlink(source);
124
+ return;
125
+ }
126
+ throw error;
127
+ }
128
+ }
129
+ /**
130
+ * Lists objects under the root, optionally filtered by prefix.
131
+ *
132
+ * Note: this walks the entire directory tree and `stat`s every matching file
133
+ * on each call — pagination slices an in-memory list, it does not make paging
134
+ * cheaper. Fine for development and modest trees (its intended use); avoid
135
+ * pointing it at very large directories.
136
+ */
137
+ async list(options) {
138
+ const prefix = options?.prefix ?? "";
139
+ const keys = [];
140
+ await this.walk(this.options.rootDir, keys);
141
+ const matched = keys.filter((key) => key.startsWith(prefix)).sort();
142
+ const start = options?.cursor ? matched.findIndex((key) => key > options.cursor) : 0;
143
+ const from = start === -1 ? matched.length : start;
144
+ const limit = options?.limit ?? matched.length;
145
+ const page = matched.slice(from, from + limit);
146
+ const objects = await Promise.all(page.map((key) => this.stat(key)));
147
+ const hasMore = from + limit < matched.length;
148
+ return {
149
+ objects,
150
+ cursor: hasMore && page.length > 0 ? page[page.length - 1] : void 0
151
+ };
152
+ }
153
+ async getSignedUrl(key, _options) {
154
+ if (!this.options.publicBaseUrl) {
155
+ throw new StorageOperationNotSupportedError("getSignedUrl");
156
+ }
157
+ this.resolveKey(key);
158
+ const encodedPath = key.replace(/^\/+/, "").split("/").map(encodeURIComponent).join("/");
159
+ return `${this.options.publicBaseUrl.replace(/\/+$/, "")}/${encodedPath}`;
160
+ }
161
+ /** Resolve a key to an absolute path, rejecting traversal that escapes the root. */
162
+ resolveKey(key) {
163
+ const path = resolve(this.options.rootDir, key);
164
+ const rel = relative(this.options.rootDir, path);
165
+ if (rel === "" || rel.startsWith("..") || rel.startsWith(`..${sep}`)) {
166
+ throw new StorageOperationNotSupportedError(`key '${key}' resolves outside the storage root`);
167
+ }
168
+ return path;
169
+ }
170
+ async assertExists(key, path) {
171
+ try {
172
+ const stats = await fsStat(path);
173
+ if (!stats.isFile()) {
174
+ throw new StorageObjectNotFoundError(key);
175
+ }
176
+ return stats;
177
+ } catch (error) {
178
+ if (isNotFound(error)) {
179
+ throw new StorageObjectNotFoundError(key, {
180
+ cause: error
181
+ });
182
+ }
183
+ if (isAccessDenied(error)) {
184
+ throw new StorageAccessDeniedError(key, {
185
+ cause: error
186
+ });
187
+ }
188
+ throw error;
189
+ }
190
+ }
191
+ /** Recursively collect file keys (root-relative, `/`-separated) under `dir`. */
192
+ async walk(dir, into) {
193
+ const entries = await readdir(dir, {
194
+ withFileTypes: true
195
+ });
196
+ for (const entry of entries) {
197
+ const full = join(dir, entry.name);
198
+ if (entry.isDirectory()) {
199
+ await this.walk(full, into);
200
+ } else if (entry.isFile()) {
201
+ into.push(relative(this.options.rootDir, full).split(sep).join(posix.sep));
202
+ }
203
+ }
204
+ }
205
+ };
206
+ DiskStorageProvider = _ts_decorate([
207
+ Injectable(),
208
+ _ts_metadata("design:type", Function),
209
+ _ts_metadata("design:paramtypes", [
210
+ typeof DiskStorageProviderOptions === "undefined" ? Object : DiskStorageProviderOptions
211
+ ])
212
+ ], DiskStorageProvider);
213
+ function isNotFound(error) {
214
+ return typeof error === "object" && error !== null && error.code === "ENOENT";
215
+ }
216
+ __name(isNotFound, "isNotFound");
217
+ function isAccessDenied(error) {
218
+ if (typeof error !== "object" || error === null) {
219
+ return false;
220
+ }
221
+ const code = error.code;
222
+ return code === "EACCES" || code === "EPERM";
223
+ }
224
+ __name(isAccessDenied, "isAccessDenied");
225
+ export {
226
+ DiskStorageProvider,
227
+ DiskStorageProviderOptions,
228
+ StorageAccessDeniedError,
229
+ StorageError,
230
+ StorageObjectNotFoundError,
231
+ StorageOperationNotSupportedError,
232
+ StorageProvider
233
+ };
234
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/disk.storage.provider.ts"],"sourcesContent":["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"],"mappings":"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","names":["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"]}
package/dist/s3.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './s3.storage.provider.js';
2
+ //# sourceMappingURL=s3.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"s3.d.ts","sourceRoot":"","sources":["../src/s3.ts"],"names":[],"mappings":"AAEA,cAAc,0BAA0B,CAAC"}
package/dist/s3.js ADDED
@@ -0,0 +1,222 @@
1
+ import {
2
+ StorageAccessDeniedError,
3
+ StorageObjectNotFoundError,
4
+ StorageProvider,
5
+ __name
6
+ } from "./chunk-UBT2CAIO.js";
7
+
8
+ // src/s3.storage.provider.ts
9
+ import { Injectable } from "injectkit";
10
+ import { Readable } from "stream";
11
+ import { DateTime } from "luxon";
12
+ import { CopyObjectCommand, DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
13
+ import { Upload } from "@aws-sdk/lib-storage";
14
+ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
15
+ function _ts_decorate(decorators, target, key, desc) {
16
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
17
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
18
+ 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;
19
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
20
+ }
21
+ __name(_ts_decorate, "_ts_decorate");
22
+ function _ts_metadata(k, v) {
23
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
24
+ }
25
+ __name(_ts_metadata, "_ts_metadata");
26
+ var S3StorageProviderOptions = class {
27
+ static {
28
+ __name(this, "S3StorageProviderOptions");
29
+ }
30
+ /** Name of the bucket all keys live in. */
31
+ bucket;
32
+ constructor(init) {
33
+ this.bucket = init.bucket;
34
+ }
35
+ };
36
+ var S3StorageProvider = class extends StorageProvider {
37
+ static {
38
+ __name(this, "S3StorageProvider");
39
+ }
40
+ client;
41
+ options;
42
+ constructor(client, options) {
43
+ super(), this.client = client, this.options = options;
44
+ }
45
+ async write(key, body, options) {
46
+ const shared = {
47
+ Bucket: this.options.bucket,
48
+ Key: key,
49
+ ContentType: options?.contentType,
50
+ ContentLength: options?.contentLength,
51
+ CacheControl: options?.cacheControl,
52
+ Metadata: options?.metadata
53
+ };
54
+ if (body instanceof Readable) {
55
+ const upload = new Upload({
56
+ client: this.client,
57
+ params: {
58
+ ...shared,
59
+ Body: body
60
+ }
61
+ });
62
+ await upload.done();
63
+ return;
64
+ }
65
+ await this.client.send(new PutObjectCommand({
66
+ ...shared,
67
+ Body: body
68
+ }));
69
+ }
70
+ async read(key, options) {
71
+ try {
72
+ const response = await this.client.send(new GetObjectCommand({
73
+ Bucket: this.options.bucket,
74
+ Key: key,
75
+ Range: toRangeHeader(options?.range)
76
+ }));
77
+ return response.Body;
78
+ } catch (error) {
79
+ throw this.mapError(key, error);
80
+ }
81
+ }
82
+ async stat(key) {
83
+ try {
84
+ const head = await this.client.send(new HeadObjectCommand({
85
+ Bucket: this.options.bucket,
86
+ Key: key
87
+ }));
88
+ return {
89
+ key,
90
+ size: head.ContentLength ?? 0,
91
+ contentType: head.ContentType,
92
+ etag: head.ETag,
93
+ lastModified: head.LastModified ? DateTime.fromJSDate(head.LastModified) : void 0,
94
+ metadata: head.Metadata
95
+ };
96
+ } catch (error) {
97
+ throw this.mapError(key, error);
98
+ }
99
+ }
100
+ async exists(key) {
101
+ try {
102
+ await this.client.send(new HeadObjectCommand({
103
+ Bucket: this.options.bucket,
104
+ Key: key
105
+ }));
106
+ return true;
107
+ } catch (error) {
108
+ if (isNotFound(error)) {
109
+ return false;
110
+ }
111
+ throw this.mapError(key, error);
112
+ }
113
+ }
114
+ async delete(key) {
115
+ await this.client.send(new DeleteObjectCommand({
116
+ Bucket: this.options.bucket,
117
+ Key: key
118
+ }));
119
+ }
120
+ /**
121
+ * Server-side copy via `CopyObjectCommand`.
122
+ *
123
+ * Note: S3's single-request `CopyObject` is capped at 5 GB. Copying a larger
124
+ * object requires a multipart copy (`UploadPartCopy`), which this provider
125
+ * does not yet implement — such copies will fail. {@link move} inherits the
126
+ * same limit since it delegates to `copy`.
127
+ */
128
+ async copy(sourceKey, destinationKey) {
129
+ try {
130
+ await this.client.send(new CopyObjectCommand({
131
+ Bucket: this.options.bucket,
132
+ Key: destinationKey,
133
+ // CopySource must be the URL-encoded `bucket/key` of the source object.
134
+ CopySource: `${this.options.bucket}/${encodeURIComponent(sourceKey)}`
135
+ }));
136
+ } catch (error) {
137
+ throw this.mapError(sourceKey, error);
138
+ }
139
+ }
140
+ async move(sourceKey, destinationKey) {
141
+ await this.copy(sourceKey, destinationKey);
142
+ await this.delete(sourceKey);
143
+ }
144
+ async list(options) {
145
+ const response = await this.client.send(new ListObjectsV2Command({
146
+ Bucket: this.options.bucket,
147
+ Prefix: options?.prefix,
148
+ MaxKeys: options?.limit,
149
+ ContinuationToken: options?.cursor
150
+ }));
151
+ const objects = (response.Contents ?? []).map((item) => ({
152
+ key: item.Key ?? "",
153
+ size: item.Size ?? 0,
154
+ etag: item.ETag,
155
+ lastModified: item.LastModified ? DateTime.fromJSDate(item.LastModified) : void 0
156
+ }));
157
+ return {
158
+ objects,
159
+ cursor: response.IsTruncated ? response.NextContinuationToken : void 0
160
+ };
161
+ }
162
+ async getSignedUrl(key, options) {
163
+ const command = options.operation === "write" ? new PutObjectCommand({
164
+ Bucket: this.options.bucket,
165
+ Key: key,
166
+ ContentType: options.contentType
167
+ }) : new GetObjectCommand({
168
+ Bucket: this.options.bucket,
169
+ Key: key
170
+ });
171
+ return getSignedUrl(this.client, command, {
172
+ expiresIn: Math.round(options.expiresIn.as("seconds"))
173
+ });
174
+ }
175
+ mapError(key, error) {
176
+ if (isNotFound(error)) {
177
+ return new StorageObjectNotFoundError(key, {
178
+ cause: error
179
+ });
180
+ }
181
+ if (isAccessDenied(error)) {
182
+ return new StorageAccessDeniedError(key, {
183
+ cause: error
184
+ });
185
+ }
186
+ return error;
187
+ }
188
+ };
189
+ S3StorageProvider = _ts_decorate([
190
+ Injectable(),
191
+ _ts_metadata("design:type", Function),
192
+ _ts_metadata("design:paramtypes", [
193
+ typeof S3Client === "undefined" ? Object : S3Client,
194
+ typeof S3StorageProviderOptions === "undefined" ? Object : S3StorageProviderOptions
195
+ ])
196
+ ], S3StorageProvider);
197
+ function errorName(error) {
198
+ return typeof error === "object" && error !== null ? error.name : void 0;
199
+ }
200
+ __name(errorName, "errorName");
201
+ function statusCode(error) {
202
+ return typeof error === "object" && error !== null ? error.$metadata?.httpStatusCode : void 0;
203
+ }
204
+ __name(statusCode, "statusCode");
205
+ function isNotFound(error) {
206
+ const name = errorName(error);
207
+ return name === "NoSuchKey" || name === "NotFound" || statusCode(error) === 404;
208
+ }
209
+ __name(isNotFound, "isNotFound");
210
+ function isAccessDenied(error) {
211
+ return errorName(error) === "AccessDenied" || statusCode(error) === 403;
212
+ }
213
+ __name(isAccessDenied, "isAccessDenied");
214
+ function toRangeHeader(range) {
215
+ return range ? `bytes=${range.start}-${range.end ?? ""}` : void 0;
216
+ }
217
+ __name(toRangeHeader, "toRangeHeader");
218
+ export {
219
+ S3StorageProvider,
220
+ S3StorageProviderOptions
221
+ };
222
+ //# sourceMappingURL=s3.js.map
package/dist/s3.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/s3.storage.provider.ts"],"sourcesContent":["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 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"],"mappings":"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","names":["Injectable","Readable","DateTime","CopyObjectCommand","DeleteObjectCommand","GetObjectCommand","HeadObjectCommand","ListObjectsV2Command","PutObjectCommand","S3Client","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"]}
@@ -0,0 +1,48 @@
1
+ import { Readable } from 'node:stream';
2
+ import { 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,QAAQ,EACT,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.3.0",
4
4
  "description": "Storage utilities for ServerKit.",
5
5
  "author": {
6
6
  "name": "Marooned Software",
@@ -25,6 +25,21 @@
25
25
  "main": "./dist/index.js",
26
26
  "module": "./dist/index.js",
27
27
  "types": "./dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js"
32
+ },
33
+ "./s3": {
34
+ "types": "./dist/s3.d.ts",
35
+ "import": "./dist/s3.js"
36
+ },
37
+ "./gcs": {
38
+ "types": "./dist/gcs.d.ts",
39
+ "import": "./dist/gcs.js"
40
+ },
41
+ "./package.json": "./package.json"
42
+ },
28
43
  "license": "MIT",
29
44
  "files": [
30
45
  "dist/**"
@@ -66,7 +81,7 @@
66
81
  "@repo/config-eslint": "0.2.1"
67
82
  },
68
83
  "scripts": {
69
- "build": "tsup src/index.ts --format esm --sourcemap && tsc --emitDeclarationOnly --declaration",
84
+ "build": "tsup src/index.ts src/s3.ts src/gcs.ts --format esm --sourcemap && tsc --emitDeclarationOnly --declaration",
70
85
  "build:ci": "eslint --max-warnings=0 && pnpm run build",
71
86
  "lint": "eslint --fix",
72
87
  "format": "prettier --write .",