@furystack/s3-blob-store 1.0.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 +76 -0
- package/esm/define-s3-blob-store.d.ts +36 -0
- package/esm/define-s3-blob-store.d.ts.map +1 -0
- package/esm/define-s3-blob-store.js +41 -0
- package/esm/define-s3-blob-store.js.map +1 -0
- package/esm/define-s3-blob-store.spec.d.ts +2 -0
- package/esm/define-s3-blob-store.spec.d.ts.map +1 -0
- package/esm/define-s3-blob-store.spec.js +86 -0
- package/esm/define-s3-blob-store.spec.js.map +1 -0
- package/esm/ensure-bucket-lifecycle.d.ts +24 -0
- package/esm/ensure-bucket-lifecycle.d.ts.map +1 -0
- package/esm/ensure-bucket-lifecycle.js +37 -0
- package/esm/ensure-bucket-lifecycle.js.map +1 -0
- package/esm/index.d.ts +3 -0
- package/esm/index.d.ts.map +1 -0
- package/esm/index.js +3 -0
- package/esm/index.js.map +1 -0
- package/esm/s3-blob-store.d.ts +82 -0
- package/esm/s3-blob-store.d.ts.map +1 -0
- package/esm/s3-blob-store.integration.spec.d.ts +2 -0
- package/esm/s3-blob-store.integration.spec.d.ts.map +1 -0
- package/esm/s3-blob-store.integration.spec.js +71 -0
- package/esm/s3-blob-store.integration.spec.js.map +1 -0
- package/esm/s3-blob-store.js +283 -0
- package/esm/s3-blob-store.js.map +1 -0
- package/esm/s3-blob-store.unit.spec.d.ts +2 -0
- package/esm/s3-blob-store.unit.spec.d.ts.map +1 -0
- package/esm/s3-blob-store.unit.spec.js +892 -0
- package/esm/s3-blob-store.unit.spec.js.map +1 -0
- package/esm/s3-stream-helpers.d.ts +35 -0
- package/esm/s3-stream-helpers.d.ts.map +1 -0
- package/esm/s3-stream-helpers.js +127 -0
- package/esm/s3-stream-helpers.js.map +1 -0
- package/package.json +55 -0
- package/src/define-s3-blob-store.spec.ts +24 -0
- package/src/define-s3-blob-store.ts +43 -0
- package/src/ensure-bucket-lifecycle.ts +47 -0
- package/src/index.ts +2 -0
- package/src/s3-blob-store.integration.spec.ts +78 -0
- package/src/s3-blob-store.ts +378 -0
- package/src/s3-blob-store.unit.spec.ts +539 -0
- package/src/s3-stream-helpers.ts +132 -0
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# @furystack/s3-blob-store
|
|
2
|
+
|
|
3
|
+
S3-compatible adapter for `@furystack/blob-store`. Targets AWS S3,
|
|
4
|
+
MinIO, Cloudflare R2, Backblaze B2, and any backend that speaks the v3
|
|
5
|
+
S3 API. v1 uses single-part `PutObject` for uploads (5 GiB cap on AWS
|
|
6
|
+
S3). Apps with very-large blobs that need resumable multipart can
|
|
7
|
+
compose `@aws-sdk/lib-storage`'s `Upload` themselves on the underlying
|
|
8
|
+
client; v1.x will lift this into a multipart-aware `put` variant.
|
|
9
|
+
Presigned URLs ride on `@aws-sdk/s3-request-presigner`.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
yarn add @furystack/s3-blob-store
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The adapter has hard dependencies on the matching `@aws-sdk/*` v3
|
|
18
|
+
packages — they are listed as regular dependencies, not peers, so they
|
|
19
|
+
install transitively.
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
The caller owns the `S3Client` lifecycle; the adapter never closes it.
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { S3Client } from '@aws-sdk/client-s3'
|
|
27
|
+
import { BlobStore } from '@furystack/blob-store'
|
|
28
|
+
import { defineS3BlobStore } from '@furystack/s3-blob-store'
|
|
29
|
+
|
|
30
|
+
const client = new S3Client({
|
|
31
|
+
region: 'eu-central-1',
|
|
32
|
+
credentials: { accessKeyId, secretAccessKey },
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
injector.bind(
|
|
36
|
+
BlobStore,
|
|
37
|
+
defineS3BlobStore({
|
|
38
|
+
client,
|
|
39
|
+
bucket: 'my-app-blobs',
|
|
40
|
+
keyPrefix: 'tenant-a/',
|
|
41
|
+
}),
|
|
42
|
+
)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Lifecycle management
|
|
46
|
+
|
|
47
|
+
By default, on first `put`, the adapter installs a bucket lifecycle
|
|
48
|
+
rule that aborts incomplete multipart uploads after 24h to avoid
|
|
49
|
+
runaway storage costs from interrupted clients. Disable with
|
|
50
|
+
`manageLifecycle: false` (recommended for buckets the app lacks
|
|
51
|
+
`s3:PutLifecycleConfiguration` on). Surface failures via
|
|
52
|
+
`onLifecycleError`.
|
|
53
|
+
|
|
54
|
+
### Capabilities
|
|
55
|
+
|
|
56
|
+
- `presignedUrls: true`
|
|
57
|
+
- `multipart: false` (v1 — single-part `PutObject` only)
|
|
58
|
+
- `range: true`
|
|
59
|
+
- `crossNodeAccessible: true`
|
|
60
|
+
- `maxObjectBytes: 5 GiB` (AWS S3 single-part cap)
|
|
61
|
+
|
|
62
|
+
### Integration tests
|
|
63
|
+
|
|
64
|
+
The package ships unit tests against a stubbed `S3Client` plus an
|
|
65
|
+
integration suite that requires a reachable MinIO (or any S3-compatible)
|
|
66
|
+
endpoint. The repo's root `docker-compose.yml` exposes one on
|
|
67
|
+
`http://localhost:9000`. Run locally via:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
docker compose up -d
|
|
71
|
+
yarn vitest run --project Service packages/s3-blob-store
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Override the endpoint and credentials with `MINIO_URL`,
|
|
75
|
+
`MINIO_ACCESS_KEY`, `MINIO_SECRET_KEY` when targeting a different
|
|
76
|
+
backend.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { BlobStore } from '@furystack/blob-store';
|
|
2
|
+
import type { ServiceFactory } from '@furystack/inject';
|
|
3
|
+
import { type S3BlobStoreOptions } from './s3-blob-store.js';
|
|
4
|
+
/**
|
|
5
|
+
* Returns a {@link ServiceFactory} bound to the {@link BlobStore} token,
|
|
6
|
+
* resolving an {@link S3BlobStore} on first access. The caller still
|
|
7
|
+
* owns the lifecycle of the supplied `S3Client`.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { S3Client } from '@aws-sdk/client-s3'
|
|
12
|
+
* import { createInjector } from '@furystack/inject'
|
|
13
|
+
* import { BlobStore } from '@furystack/blob-store'
|
|
14
|
+
* import { defineS3BlobStore } from '@furystack/s3-blob-store'
|
|
15
|
+
*
|
|
16
|
+
* const client = new S3Client({
|
|
17
|
+
* region: 'eu-central-1',
|
|
18
|
+
* credentials: {
|
|
19
|
+
* accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
|
20
|
+
* secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
|
|
21
|
+
* },
|
|
22
|
+
* })
|
|
23
|
+
*
|
|
24
|
+
* await using injector = createInjector()
|
|
25
|
+
* injector.bind(
|
|
26
|
+
* BlobStore,
|
|
27
|
+
* defineS3BlobStore({
|
|
28
|
+
* client,
|
|
29
|
+
* bucket: 'my-app-blobs',
|
|
30
|
+
* keyPrefix: 'tenant-a/',
|
|
31
|
+
* }),
|
|
32
|
+
* )
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare const defineS3BlobStore: (options: S3BlobStoreOptions) => ServiceFactory<BlobStore>;
|
|
36
|
+
//# sourceMappingURL=define-s3-blob-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define-s3-blob-store.d.ts","sourceRoot":"","sources":["../src/define-s3-blob-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AACvD,OAAO,EAAe,KAAK,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAEzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,iBAAiB,GAAI,SAAS,kBAAkB,KAAG,cAAc,CAAC,SAAS,CAOvF,CAAA"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { S3BlobStore } from './s3-blob-store.js';
|
|
2
|
+
/**
|
|
3
|
+
* Returns a {@link ServiceFactory} bound to the {@link BlobStore} token,
|
|
4
|
+
* resolving an {@link S3BlobStore} on first access. The caller still
|
|
5
|
+
* owns the lifecycle of the supplied `S3Client`.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { S3Client } from '@aws-sdk/client-s3'
|
|
10
|
+
* import { createInjector } from '@furystack/inject'
|
|
11
|
+
* import { BlobStore } from '@furystack/blob-store'
|
|
12
|
+
* import { defineS3BlobStore } from '@furystack/s3-blob-store'
|
|
13
|
+
*
|
|
14
|
+
* const client = new S3Client({
|
|
15
|
+
* region: 'eu-central-1',
|
|
16
|
+
* credentials: {
|
|
17
|
+
* accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
|
18
|
+
* secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
|
|
19
|
+
* },
|
|
20
|
+
* })
|
|
21
|
+
*
|
|
22
|
+
* await using injector = createInjector()
|
|
23
|
+
* injector.bind(
|
|
24
|
+
* BlobStore,
|
|
25
|
+
* defineS3BlobStore({
|
|
26
|
+
* client,
|
|
27
|
+
* bucket: 'my-app-blobs',
|
|
28
|
+
* keyPrefix: 'tenant-a/',
|
|
29
|
+
* }),
|
|
30
|
+
* )
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export const defineS3BlobStore = (options) => {
|
|
34
|
+
return ({ onDispose }) => {
|
|
35
|
+
const store = new S3BlobStore(options);
|
|
36
|
+
// eslint-disable-next-line furystack/prefer-using-wrapper -- delegated to onDispose
|
|
37
|
+
onDispose(() => store[Symbol.dispose]());
|
|
38
|
+
return store;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
//# sourceMappingURL=define-s3-blob-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define-s3-blob-store.js","sourceRoot":"","sources":["../src/define-s3-blob-store.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAA2B,MAAM,oBAAoB,CAAA;AAEzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,OAA2B,EAA6B,EAAE;IAC1F,OAAO,CAAC,EAAE,SAAS,EAAE,EAAE,EAAE;QACvB,MAAM,KAAK,GAAG,IAAI,WAAW,CAAC,OAAO,CAAC,CAAA;QACtC,oFAAoF;QACpF,SAAS,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;QACxC,OAAO,KAAK,CAAA;IACd,CAAC,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define-s3-blob-store.spec.d.ts","sourceRoot":"","sources":["../src/define-s3-blob-store.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
|
|
2
|
+
if (value !== null && value !== void 0) {
|
|
3
|
+
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
4
|
+
var dispose, inner;
|
|
5
|
+
if (async) {
|
|
6
|
+
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
7
|
+
dispose = value[Symbol.asyncDispose];
|
|
8
|
+
}
|
|
9
|
+
if (dispose === void 0) {
|
|
10
|
+
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
11
|
+
dispose = value[Symbol.dispose];
|
|
12
|
+
if (async) inner = dispose;
|
|
13
|
+
}
|
|
14
|
+
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
15
|
+
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
16
|
+
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
17
|
+
}
|
|
18
|
+
else if (async) {
|
|
19
|
+
env.stack.push({ async: true });
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
};
|
|
23
|
+
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
24
|
+
return function (env) {
|
|
25
|
+
function fail(e) {
|
|
26
|
+
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
27
|
+
env.hasError = true;
|
|
28
|
+
}
|
|
29
|
+
var r, s = 0;
|
|
30
|
+
function next() {
|
|
31
|
+
while (r = env.stack.pop()) {
|
|
32
|
+
try {
|
|
33
|
+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
34
|
+
if (r.dispose) {
|
|
35
|
+
var result = r.dispose.call(r.value);
|
|
36
|
+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
37
|
+
}
|
|
38
|
+
else s |= 1;
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
fail(e);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
45
|
+
if (env.hasError) throw env.error;
|
|
46
|
+
}
|
|
47
|
+
return next();
|
|
48
|
+
};
|
|
49
|
+
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
50
|
+
var e = new Error(message);
|
|
51
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
|
+
});
|
|
53
|
+
import { describe, expect, it } from 'vitest';
|
|
54
|
+
import { BlobStore } from '@furystack/blob-store';
|
|
55
|
+
import { createInjector } from '@furystack/inject';
|
|
56
|
+
import { defineS3BlobStore } from './define-s3-blob-store.js';
|
|
57
|
+
import { S3BlobStore } from './s3-blob-store.js';
|
|
58
|
+
describe('defineS3BlobStore', () => {
|
|
59
|
+
it('binds an S3BlobStore on the BlobStore token', async () => {
|
|
60
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
61
|
+
try {
|
|
62
|
+
const injector = __addDisposableResource(env_1, createInjector(), true);
|
|
63
|
+
injector.bind(BlobStore, defineS3BlobStore({ client: {}, bucket: 'tests', name: 'bound' }));
|
|
64
|
+
const store = injector.get(BlobStore);
|
|
65
|
+
expect(store).toBeInstanceOf(S3BlobStore);
|
|
66
|
+
expect(store.storeName).toBe('bound');
|
|
67
|
+
}
|
|
68
|
+
catch (e_1) {
|
|
69
|
+
env_1.error = e_1;
|
|
70
|
+
env_1.hasError = true;
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
const result_1 = __disposeResources(env_1);
|
|
74
|
+
if (result_1)
|
|
75
|
+
await result_1;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
it('disposes the store when the injector tears down', async () => {
|
|
79
|
+
const injector = createInjector();
|
|
80
|
+
injector.bind(BlobStore, defineS3BlobStore({ client: {}, bucket: 'tests' }));
|
|
81
|
+
const store = injector.get(BlobStore);
|
|
82
|
+
await injector[Symbol.asyncDispose]();
|
|
83
|
+
await expect(store.head('any')).rejects.toMatchObject({ code: 'io-error' });
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
//# sourceMappingURL=define-s3-blob-store.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define-s3-blob-store.spec.js","sourceRoot":"","sources":["../src/define-s3-blob-store.spec.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAA;AAC7D,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAEhD,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;IACjC,EAAE,CAAC,6CAA6C,EAAE,KAAK,IAAI,EAAE;;;YAC3D,MAAY,QAAQ,kCAAG,cAAc,EAAE,OAAA,CAAA;YACvC,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,iBAAiB,CAAC,EAAE,MAAM,EAAE,EAAc,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,CAAA;YACvG,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;YACrC,MAAM,CAAC,KAAK,CAAC,CAAC,cAAc,CAAC,WAAW,CAAC,CAAA;YACzC,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;;;;;;;;;;;KACtC,CAAC,CAAA;IAEF,EAAE,CAAC,iDAAiD,EAAE,KAAK,IAAI,EAAE;QAC/D,MAAM,QAAQ,GAAG,cAAc,EAAE,CAAA;QACjC,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,iBAAiB,CAAC,EAAE,MAAM,EAAE,EAAc,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,CAAA;QACxF,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;QACrC,MAAM,QAAQ,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,CAAA;QACrC,MAAM,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAA;IAC7E,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type S3Client } from '@aws-sdk/client-s3';
|
|
2
|
+
/**
|
|
3
|
+
* Default rule id used when {@link S3BlobStoreOptions.manageLifecycle} is
|
|
4
|
+
* enabled. Stable, greppable, and easy to remove out-of-band via the AWS
|
|
5
|
+
* console / CLI.
|
|
6
|
+
*/
|
|
7
|
+
export declare const DEFAULT_LIFECYCLE_RULE_ID = "furystack-blob-store-abort-incomplete-multipart";
|
|
8
|
+
export type EnsureBucketLifecycleOptions = {
|
|
9
|
+
client: S3Client;
|
|
10
|
+
bucket: string;
|
|
11
|
+
ruleId: string;
|
|
12
|
+
/** Invoked when the lifecycle PUT fails (e.g. missing IAM permission). */
|
|
13
|
+
onError?: (error: unknown) => void;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Best-effort bootstrap of the bucket's "abort incomplete multipart" rule.
|
|
17
|
+
* Swallows errors and forwards them to `onError` — apps surface them via
|
|
18
|
+
* their own telemetry / logger sink.
|
|
19
|
+
*
|
|
20
|
+
* Idempotent at the call site: callers track whether the bootstrap has
|
|
21
|
+
* been attempted and only call this helper on first put.
|
|
22
|
+
*/
|
|
23
|
+
export declare const ensureBucketLifecycle: (options: EnsureBucketLifecycleOptions) => Promise<void>;
|
|
24
|
+
//# sourceMappingURL=ensure-bucket-lifecycle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ensure-bucket-lifecycle.d.ts","sourceRoot":"","sources":["../src/ensure-bucket-lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0C,KAAK,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAE1F;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,oDAAoD,CAAA;AAE1F,MAAM,MAAM,4BAA4B,GAAG;IACzC,MAAM,EAAE,QAAQ,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,0EAA0E;IAC1E,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;CACnC,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,GAAU,SAAS,4BAA4B,KAAG,OAAO,CAAC,IAAI,CAqB/F,CAAA"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { PutBucketLifecycleConfigurationCommand } from '@aws-sdk/client-s3';
|
|
2
|
+
/**
|
|
3
|
+
* Default rule id used when {@link S3BlobStoreOptions.manageLifecycle} is
|
|
4
|
+
* enabled. Stable, greppable, and easy to remove out-of-band via the AWS
|
|
5
|
+
* console / CLI.
|
|
6
|
+
*/
|
|
7
|
+
export const DEFAULT_LIFECYCLE_RULE_ID = 'furystack-blob-store-abort-incomplete-multipart';
|
|
8
|
+
/**
|
|
9
|
+
* Best-effort bootstrap of the bucket's "abort incomplete multipart" rule.
|
|
10
|
+
* Swallows errors and forwards them to `onError` — apps surface them via
|
|
11
|
+
* their own telemetry / logger sink.
|
|
12
|
+
*
|
|
13
|
+
* Idempotent at the call site: callers track whether the bootstrap has
|
|
14
|
+
* been attempted and only call this helper on first put.
|
|
15
|
+
*/
|
|
16
|
+
export const ensureBucketLifecycle = async (options) => {
|
|
17
|
+
const { client, bucket, ruleId, onError } = options;
|
|
18
|
+
try {
|
|
19
|
+
await client.send(new PutBucketLifecycleConfigurationCommand({
|
|
20
|
+
Bucket: bucket,
|
|
21
|
+
LifecycleConfiguration: {
|
|
22
|
+
Rules: [
|
|
23
|
+
{
|
|
24
|
+
ID: ruleId,
|
|
25
|
+
Status: 'Enabled',
|
|
26
|
+
Filter: {},
|
|
27
|
+
AbortIncompleteMultipartUpload: { DaysAfterInitiation: 1 },
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
onError?.(error);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
//# sourceMappingURL=ensure-bucket-lifecycle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ensure-bucket-lifecycle.js","sourceRoot":"","sources":["../src/ensure-bucket-lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sCAAsC,EAAiB,MAAM,oBAAoB,CAAA;AAE1F;;;;GAIG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,iDAAiD,CAAA;AAU1F;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,KAAK,EAAE,OAAqC,EAAiB,EAAE;IAClG,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAA;IACnD,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,IAAI,CACf,IAAI,sCAAsC,CAAC;YACzC,MAAM,EAAE,MAAM;YACd,sBAAsB,EAAE;gBACtB,KAAK,EAAE;oBACL;wBACE,EAAE,EAAE,MAAM;wBACV,MAAM,EAAE,SAAS;wBACjB,MAAM,EAAE,EAAE;wBACV,8BAA8B,EAAE,EAAE,mBAAmB,EAAE,CAAC,EAAE;qBAC3D;iBACF;aACF;SACF,CAAC,CACH,CAAA;IACH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,EAAE,CAAC,KAAK,CAAC,CAAA;IAClB,CAAC;AACH,CAAC,CAAA"}
|
package/esm/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAA;AAC7D,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,KAAK,kBAAkB,EAAE,MAAM,oBAAoB,CAAA"}
|
package/esm/index.js
ADDED
package/esm/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAA;AAC7D,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAA2B,MAAM,oBAAoB,CAAA"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { type S3Client } from '@aws-sdk/client-s3';
|
|
2
|
+
import { type BlobDownloadUrlOptions, type BlobGetResult, type BlobListOptions, type BlobListResult, type BlobMetadata, type BlobPutInput, type BlobPutOptions, type BlobRef, type BlobStore, type BlobStoreCapabilities, type BlobUploadUrl, type BlobUploadUrlOptions } from '@furystack/blob-store';
|
|
3
|
+
/**
|
|
4
|
+
* 5 TiB — the per-object limit AWS S3 enforces. Other S3-compatible
|
|
5
|
+
* backends (MinIO, R2) advertise smaller caps in practice; the value
|
|
6
|
+
* is a soft hint per
|
|
7
|
+
* {@link BlobStoreCapabilities.maxObjectBytes}.
|
|
8
|
+
*/
|
|
9
|
+
export declare const S3_MAX_OBJECT_BYTES = 5497558138880;
|
|
10
|
+
/**
|
|
11
|
+
* Options accepted by {@link S3BlobStore}.
|
|
12
|
+
*/
|
|
13
|
+
export type S3BlobStoreOptions = {
|
|
14
|
+
/**
|
|
15
|
+
* Caller-owned `S3Client` instance. The adapter never closes it;
|
|
16
|
+
* connection lifecycle stays with the application.
|
|
17
|
+
*/
|
|
18
|
+
client: S3Client;
|
|
19
|
+
/** Bucket every operation runs against. */
|
|
20
|
+
bucket: string;
|
|
21
|
+
/**
|
|
22
|
+
* Optional key prefix prepended to every blob key on the wire. Useful
|
|
23
|
+
* for tenant scoping inside a shared bucket. Stored without the
|
|
24
|
+
* prefix in {@link BlobRef.key} so apps round-trip cleanly.
|
|
25
|
+
*/
|
|
26
|
+
keyPrefix?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Adapter binding name. Defaults to `'s3'`. Set to a stable string
|
|
29
|
+
* when the same injector binds multiple S3 stores so {@link BlobRef}
|
|
30
|
+
* entries stay distinguishable.
|
|
31
|
+
*/
|
|
32
|
+
name?: string;
|
|
33
|
+
/**
|
|
34
|
+
* When true (default), installs a bucket lifecycle rule that aborts
|
|
35
|
+
* incomplete multipart uploads older than 24h on first put. Avoids
|
|
36
|
+
* orphan-multipart cost surprises. Disable for buckets the app
|
|
37
|
+
* lacks lifecycle permissions on.
|
|
38
|
+
*/
|
|
39
|
+
manageLifecycle?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Override the rule id used by `manageLifecycle`. Defaults to
|
|
42
|
+
* `'furystack-blob-store-abort-incomplete-multipart'` so the rule is
|
|
43
|
+
* easy to inspect and remove out-of-band.
|
|
44
|
+
*/
|
|
45
|
+
lifecycleRuleId?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Async callback invoked when the lifecycle setup attempt fails.
|
|
48
|
+
* Defaults to silent — apps wire this into their telemetry / logging
|
|
49
|
+
* sink to surface permission issues.
|
|
50
|
+
*/
|
|
51
|
+
onLifecycleError?: (error: unknown) => void;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* S3-compatible {@link BlobStore} adapter. Targets AWS S3, MinIO,
|
|
55
|
+
* Cloudflare R2, Backblaze B2, and any other backend exposing the v3
|
|
56
|
+
* S3 API. The caller owns the underlying `S3Client` instance.
|
|
57
|
+
*
|
|
58
|
+
* Multipart uploads run automatically via `@aws-sdk/lib-storage`'s
|
|
59
|
+
* `Upload`; single-part PUT is used for known-small payloads when
|
|
60
|
+
* `contentLength` is supplied at put time.
|
|
61
|
+
*/
|
|
62
|
+
export declare class S3BlobStore implements BlobStore {
|
|
63
|
+
#private;
|
|
64
|
+
readonly capabilities: BlobStoreCapabilities;
|
|
65
|
+
readonly storeName: string;
|
|
66
|
+
constructor(options: S3BlobStoreOptions);
|
|
67
|
+
put(key: string, payload: BlobPutInput, options?: BlobPutOptions): Promise<BlobRef>;
|
|
68
|
+
get(key: string): Promise<BlobGetResult>;
|
|
69
|
+
head(key: string): Promise<BlobMetadata | undefined>;
|
|
70
|
+
delete(key: string): Promise<void>;
|
|
71
|
+
list(prefix: string, options?: BlobListOptions): Promise<BlobListResult>;
|
|
72
|
+
getDownloadUrl(key: string, options: BlobDownloadUrlOptions): Promise<string>;
|
|
73
|
+
getUploadUrl(key: string, options: BlobUploadUrlOptions): Promise<BlobUploadUrl>;
|
|
74
|
+
[Symbol.dispose](): void;
|
|
75
|
+
/**
|
|
76
|
+
* Public helper for tests / advanced consumers: drains a `Body` from an
|
|
77
|
+
* S3 response into a single `Uint8Array`. Memory-bound — only safe
|
|
78
|
+
* when the caller knows the payload fits in RAM.
|
|
79
|
+
*/
|
|
80
|
+
static bodyToBuffer(body: unknown): Promise<Uint8Array>;
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=s3-blob-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"s3-blob-store.d.ts","sourceRoot":"","sources":["../src/s3-blob-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAUL,KAAK,QAAQ,EACd,MAAM,oBAAoB,CAAA;AAE3B,OAAO,EAGL,KAAK,sBAAsB,EAC3B,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,OAAO,EACZ,KAAK,SAAS,EACd,KAAK,qBAAqB,EAC1B,KAAK,aAAa,EAClB,KAAK,oBAAoB,EAC1B,MAAM,uBAAuB,CAAA;AAI9B;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,gBAAoB,CAAA;AAsBpD;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAC/B;;;OAGG;IACH,MAAM,EAAE,QAAQ,CAAA;IAChB,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAA;IACd;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;;OAKG;IACH,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;CAC5C,CAAA;AAoBD;;;;;;;;GAQG;AACH,qBAAa,WAAY,YAAW,SAAS;;IAC3C,SAAgB,YAAY,EAAE,qBAAqB,CAAe;IAClE,SAAgB,SAAS,EAAE,MAAM,CAAA;gBAWrB,OAAO,EAAE,kBAAkB;IAa1B,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,OAAO,CAAC;IA+CvF,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IA6BxC,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC;IAyBpD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAelC,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,cAAc,CAAC;IAoC5E,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,MAAM,CAAC;IAQ7E,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,aAAa,CAAC;IAuBtF,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI;IAI/B;;;;OAIG;WACiB,YAAY,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC;CA4BrE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"s3-blob-store.integration.spec.d.ts","sourceRoot":"","sources":["../src/s3-blob-store.integration.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { CreateBucketCommand, DeleteBucketCommand, S3Client } from '@aws-sdk/client-s3';
|
|
2
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
3
|
+
import { S3BlobStore } from './s3-blob-store.js';
|
|
4
|
+
const MINIO_URL = process.env.MINIO_URL ?? 'http://localhost:9000';
|
|
5
|
+
const ACCESS_KEY = process.env.MINIO_ACCESS_KEY ?? 'minioadmin';
|
|
6
|
+
const SECRET_KEY = process.env.MINIO_SECRET_KEY ?? 'minioadmin';
|
|
7
|
+
const decode = (data) => new TextDecoder().decode(data);
|
|
8
|
+
const collect = async (stream) => {
|
|
9
|
+
const reader = stream.getReader();
|
|
10
|
+
const chunks = [];
|
|
11
|
+
for (;;) {
|
|
12
|
+
const { done, value } = await reader.read();
|
|
13
|
+
if (done)
|
|
14
|
+
break;
|
|
15
|
+
chunks.push(value);
|
|
16
|
+
}
|
|
17
|
+
reader.releaseLock();
|
|
18
|
+
const total = chunks.reduce((acc, c) => acc + c.byteLength, 0);
|
|
19
|
+
const merged = new Uint8Array(total);
|
|
20
|
+
let offset = 0;
|
|
21
|
+
for (const chunk of chunks) {
|
|
22
|
+
merged.set(chunk, offset);
|
|
23
|
+
offset += chunk.byteLength;
|
|
24
|
+
}
|
|
25
|
+
return merged;
|
|
26
|
+
};
|
|
27
|
+
describe('S3BlobStore (integration, requires MinIO at $MINIO_URL or http://localhost:9000)', () => {
|
|
28
|
+
const bucket = `furystack-blob-test-${Date.now()}`;
|
|
29
|
+
let client;
|
|
30
|
+
let store;
|
|
31
|
+
beforeAll(async () => {
|
|
32
|
+
client = new S3Client({
|
|
33
|
+
endpoint: MINIO_URL,
|
|
34
|
+
region: 'us-east-1',
|
|
35
|
+
forcePathStyle: true,
|
|
36
|
+
credentials: { accessKeyId: ACCESS_KEY, secretAccessKey: SECRET_KEY },
|
|
37
|
+
});
|
|
38
|
+
await client.send(new CreateBucketCommand({ Bucket: bucket }));
|
|
39
|
+
store = new S3BlobStore({ client, bucket, manageLifecycle: false });
|
|
40
|
+
});
|
|
41
|
+
afterAll(async () => {
|
|
42
|
+
try {
|
|
43
|
+
const listing = await store.list('');
|
|
44
|
+
await Promise.all(listing.items.map((item) => store.delete(item.key)));
|
|
45
|
+
await client.send(new DeleteBucketCommand({ Bucket: bucket }));
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
store[Symbol.dispose]();
|
|
49
|
+
client.destroy();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
it('round-trips a buffer payload', async () => {
|
|
53
|
+
await store.put('greeting.txt', Buffer.from('hello-minio'), { contentType: 'text/plain' });
|
|
54
|
+
const result = await store.get('greeting.txt');
|
|
55
|
+
expect(result.contentType).toBe('text/plain');
|
|
56
|
+
expect(decode(await collect(result.stream))).toBe('hello-minio');
|
|
57
|
+
});
|
|
58
|
+
it('lists by prefix', async () => {
|
|
59
|
+
await store.put('list/a', Buffer.from('1'));
|
|
60
|
+
await store.put('list/b', Buffer.from('1'));
|
|
61
|
+
const result = await store.list('list/');
|
|
62
|
+
expect(result.items.map((i) => i.key).sort()).toEqual(['list/a', 'list/b']);
|
|
63
|
+
});
|
|
64
|
+
it('returns not-found on missing get', async () => {
|
|
65
|
+
await expect(store.get('absent')).rejects.toMatchObject({ code: 'not-found' });
|
|
66
|
+
});
|
|
67
|
+
it('returns undefined head for missing keys', async () => {
|
|
68
|
+
expect(await store.head('absent')).toBeUndefined();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
//# sourceMappingURL=s3-blob-store.integration.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"s3-blob-store.integration.spec.js","sourceRoot":"","sources":["../src/s3-blob-store.integration.spec.ts"],"names":[],"mappings":"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"}
|