@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 +16 -2
- package/dist/chunk-UBT2CAIO.js +71 -0
- package/dist/chunk-UBT2CAIO.js.map +1 -0
- package/dist/disk.storage.provider.d.ts +57 -0
- package/dist/disk.storage.provider.d.ts.map +1 -0
- package/dist/gcs.d.ts +2 -0
- package/dist/gcs.d.ts.map +1 -0
- package/dist/gcs.js +182 -0
- package/dist/gcs.js.map +1 -0
- package/dist/gcs.storage.provider.d.ts +37 -0
- package/dist/gcs.storage.provider.d.ts.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +234 -0
- package/dist/index.js.map +1 -0
- package/dist/s3.d.ts +2 -0
- package/dist/s3.d.ts.map +1 -0
- package/dist/s3.js +222 -0
- package/dist/s3.js.map +1 -0
- package/dist/s3.storage.provider.d.ts +48 -0
- package/dist/s3.storage.provider.d.ts.map +1 -0
- package/dist/storage.errors.d.ts +47 -0
- package/dist/storage.errors.d.ts.map +1 -0
- package/dist/storage.provider.d.ts +126 -0
- package/dist/storage.provider.d.ts.map +1 -0
- package/package.json +17 -2
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
|
|
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
|
|
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 @@
|
|
|
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
|
package/dist/gcs.js.map
ADDED
|
@@ -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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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
package/dist/s3.d.ts.map
ADDED
|
@@ -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.
|
|
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 .",
|