@maroonedsoftware/storage 0.2.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/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 +1 -1
- package/dist/gcs.storage.provider.d.ts.map +1 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -451
- package/dist/index.js.map +1 -1
- 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 +1 -1
- package/dist/s3.storage.provider.d.ts.map +1 -1
- 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"]}
|
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"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Readable } from 'node:stream';
|
|
2
|
-
import
|
|
2
|
+
import { Storage } from '@google-cloud/storage';
|
|
3
3
|
import type { SignedUrlOptions, StorageListOptions, StorageListResult, StorageObjectMetadata, StorageReadOptions, StorageWriteOptions } from './storage.provider.js';
|
|
4
4
|
import { StorageProvider } from './storage.provider.js';
|
|
5
5
|
/**
|
|
@@ -1 +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,
|
|
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
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
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"}
|