@haverstack/blob-adapter-s3 0.1.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/LICENSE +7 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +91 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
CC0 1.0 Universal (CC0 1.0) Public Domain Dedication
|
|
2
|
+
|
|
3
|
+
To the extent possible under law, the Haverstack contributors have waived all
|
|
4
|
+
copyright and related or neighboring rights to Haverstack. This work is
|
|
5
|
+
published from the United States.
|
|
6
|
+
|
|
7
|
+
https://creativecommons.org/publicdomain/zero/1.0/
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Haverstack — S3 Blob Adapter
|
|
3
|
+
* -------------------------------------------------------
|
|
4
|
+
* Implements StackBlobAdapter over the S3 API, storing
|
|
5
|
+
* content-addressed blobs keyed by the SHA-256 hash of their
|
|
6
|
+
* bytes. Pass `endpoint` + `forcePathStyle` to target an
|
|
7
|
+
* S3-compatible store (e.g. Cloudflare R2) instead of AWS S3
|
|
8
|
+
* itself — the rest of the adapter is identical either way.
|
|
9
|
+
*
|
|
10
|
+
* Unlike blob-adapter-disk, no temp-file-plus-rename dance is
|
|
11
|
+
* needed: S3's PutObject is atomic per key, so two callers
|
|
12
|
+
* writing the same content-addressed key concurrently just
|
|
13
|
+
* write the same bytes twice — never a torn object.
|
|
14
|
+
*/
|
|
15
|
+
import { S3Client, type S3ClientConfig } from '@aws-sdk/client-s3';
|
|
16
|
+
import type { FileId } from '@haverstack/core';
|
|
17
|
+
import type { StackBlobAdapter, BlobFileInfo } from '@haverstack/core/adapter';
|
|
18
|
+
export type S3BlobAdapterOptions = {
|
|
19
|
+
/** Bucket to store blobs in. */
|
|
20
|
+
bucket: string;
|
|
21
|
+
/**
|
|
22
|
+
* A pre-configured S3Client, for callers who need control over retry
|
|
23
|
+
* policy, credential providers, request middleware, etc. When omitted,
|
|
24
|
+
* a client is built from the remaining options.
|
|
25
|
+
*/
|
|
26
|
+
client?: S3Client;
|
|
27
|
+
/** AWS region. Ignored when `client` is provided. */
|
|
28
|
+
region?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Custom endpoint — set this together with `forcePathStyle` to target an
|
|
31
|
+
* S3-compatible store (e.g. Cloudflare R2's S3 endpoint) instead of AWS
|
|
32
|
+
* S3. Ignored when `client` is provided.
|
|
33
|
+
*/
|
|
34
|
+
endpoint?: string;
|
|
35
|
+
/** Ignored when `client` is provided. */
|
|
36
|
+
forcePathStyle?: boolean;
|
|
37
|
+
/** Ignored when `client` is provided. */
|
|
38
|
+
credentials?: S3ClientConfig['credentials'];
|
|
39
|
+
};
|
|
40
|
+
export declare class S3BlobAdapter implements StackBlobAdapter {
|
|
41
|
+
private readonly client;
|
|
42
|
+
private readonly bucket;
|
|
43
|
+
constructor(options: S3BlobAdapterOptions);
|
|
44
|
+
private objectExists;
|
|
45
|
+
putAttachment(data: Uint8Array): Promise<FileId>;
|
|
46
|
+
getAttachment(fileId: FileId): Promise<Uint8Array>;
|
|
47
|
+
deleteAttachment(fileId: FileId): Promise<void>;
|
|
48
|
+
listFiles(): Promise<BlobFileInfo[]>;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,EACL,QAAQ,EAQR,KAAK,cAAc,EACpB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAU/E,MAAM,MAAM,oBAAoB,GAAG;IACjC,gCAAgC;IAChC,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,MAAM,CAAC,EAAE,QAAQ,CAAC;IAClB,qDAAqD;IACrD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yCAAyC;IACzC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,yCAAyC;IACzC,WAAW,CAAC,EAAE,cAAc,CAAC,aAAa,CAAC,CAAC;CAC7C,CAAC;AAEF,qBAAa,aAAc,YAAW,gBAAgB;IACpD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAW;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;gBAEpB,OAAO,EAAE,oBAAoB;YAY3B,YAAY;IAUpB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC;IAUhD,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAelD,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ/C,SAAS,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;CAgB3C"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Haverstack — S3 Blob Adapter
|
|
3
|
+
* -------------------------------------------------------
|
|
4
|
+
* Implements StackBlobAdapter over the S3 API, storing
|
|
5
|
+
* content-addressed blobs keyed by the SHA-256 hash of their
|
|
6
|
+
* bytes. Pass `endpoint` + `forcePathStyle` to target an
|
|
7
|
+
* S3-compatible store (e.g. Cloudflare R2) instead of AWS S3
|
|
8
|
+
* itself — the rest of the adapter is identical either way.
|
|
9
|
+
*
|
|
10
|
+
* Unlike blob-adapter-disk, no temp-file-plus-rename dance is
|
|
11
|
+
* needed: S3's PutObject is atomic per key, so two callers
|
|
12
|
+
* writing the same content-addressed key concurrently just
|
|
13
|
+
* write the same bytes twice — never a torn object.
|
|
14
|
+
*/
|
|
15
|
+
import { createHash } from 'node:crypto';
|
|
16
|
+
import { S3Client, PutObjectCommand, GetObjectCommand, HeadObjectCommand, DeleteObjectCommand, ListObjectsV2Command, NoSuchKey, NotFound, } from '@aws-sdk/client-s3';
|
|
17
|
+
import { StackNotFoundError, StackQueryError } from '@haverstack/core';
|
|
18
|
+
const SHA256_HEX_RE = /^[0-9a-f]{64}$/;
|
|
19
|
+
const assertFileId = (fileId) => {
|
|
20
|
+
if (!SHA256_HEX_RE.test(fileId)) {
|
|
21
|
+
throw new StackQueryError(`Invalid fileId: expected 64-character lowercase hex string`);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
export class S3BlobAdapter {
|
|
25
|
+
client;
|
|
26
|
+
bucket;
|
|
27
|
+
constructor(options) {
|
|
28
|
+
this.bucket = options.bucket;
|
|
29
|
+
this.client =
|
|
30
|
+
options.client ??
|
|
31
|
+
new S3Client({
|
|
32
|
+
region: options.region,
|
|
33
|
+
endpoint: options.endpoint,
|
|
34
|
+
forcePathStyle: options.forcePathStyle,
|
|
35
|
+
credentials: options.credentials,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
async objectExists(fileId) {
|
|
39
|
+
try {
|
|
40
|
+
await this.client.send(new HeadObjectCommand({ Bucket: this.bucket, Key: fileId }));
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
if (err instanceof NotFound)
|
|
45
|
+
return false;
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async putAttachment(data) {
|
|
50
|
+
const fileId = createHash('sha256').update(data).digest('hex');
|
|
51
|
+
if (!(await this.objectExists(fileId))) {
|
|
52
|
+
await this.client.send(new PutObjectCommand({ Bucket: this.bucket, Key: fileId, Body: data }));
|
|
53
|
+
}
|
|
54
|
+
return fileId;
|
|
55
|
+
}
|
|
56
|
+
async getAttachment(fileId) {
|
|
57
|
+
assertFileId(fileId);
|
|
58
|
+
try {
|
|
59
|
+
const response = await this.client.send(new GetObjectCommand({ Bucket: this.bucket, Key: fileId }));
|
|
60
|
+
return await response.Body.transformToByteArray();
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
if (err instanceof NoSuchKey) {
|
|
64
|
+
throw new StackNotFoundError(`Attachment not found: "${fileId}"`);
|
|
65
|
+
}
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async deleteAttachment(fileId) {
|
|
70
|
+
assertFileId(fileId);
|
|
71
|
+
// S3's DeleteObject is idempotent — a missing key is treated as
|
|
72
|
+
// already-deleted rather than an error, so no non-fatal catch is
|
|
73
|
+
// needed here the way blob-adapter-disk needs one for fs semantics.
|
|
74
|
+
await this.client.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: fileId }));
|
|
75
|
+
}
|
|
76
|
+
async listFiles() {
|
|
77
|
+
const files = [];
|
|
78
|
+
let continuationToken;
|
|
79
|
+
do {
|
|
80
|
+
const response = await this.client.send(new ListObjectsV2Command({ Bucket: this.bucket, ContinuationToken: continuationToken }));
|
|
81
|
+
for (const obj of response.Contents ?? []) {
|
|
82
|
+
if (obj.Key && SHA256_HEX_RE.test(obj.Key) && obj.Size !== undefined && obj.LastModified) {
|
|
83
|
+
files.push({ fileId: obj.Key, size: obj.Size, modifiedAt: obj.LastModified });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
continuationToken = response.IsTruncated ? response.NextContinuationToken : undefined;
|
|
87
|
+
} while (continuationToken);
|
|
88
|
+
return files;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@haverstack/blob-adapter-s3",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "S3 (and S3-compatible) blob adapter for Haverstack",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"import": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/haverstack/core.git",
|
|
20
|
+
"directory": "packages/blob-adapter-s3"
|
|
21
|
+
},
|
|
22
|
+
"license": "CC0-1.0",
|
|
23
|
+
"keywords": [
|
|
24
|
+
"haverstack",
|
|
25
|
+
"s3",
|
|
26
|
+
"r2",
|
|
27
|
+
"blob",
|
|
28
|
+
"adapter",
|
|
29
|
+
"personal data",
|
|
30
|
+
"storage"
|
|
31
|
+
],
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@aws-sdk/client-s3": "^3.700.0",
|
|
34
|
+
"@haverstack/core": "^0.15.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^22.0.0",
|
|
38
|
+
"aws-sdk-client-mock": "^4.0.0",
|
|
39
|
+
"typescript": "^5.5.0",
|
|
40
|
+
"vitest": "^2.0.0"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsc -p tsconfig.build.json",
|
|
44
|
+
"test": "vitest run",
|
|
45
|
+
"typecheck": "tsc --noEmit",
|
|
46
|
+
"lint": "eslint src tests"
|
|
47
|
+
}
|
|
48
|
+
}
|