@anabranch/storage-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 +21 -0
- package/README.md +36 -0
- package/esm/connector.d.ts +23 -0
- package/esm/connector.d.ts.map +1 -0
- package/esm/connector.js +28 -0
- package/esm/index.d.ts +105 -0
- package/esm/index.d.ts.map +1 -0
- package/esm/index.js +103 -0
- package/esm/package.json +3 -0
- package/esm/s3.d.ts +24 -0
- package/esm/s3.d.ts.map +1 -0
- package/esm/s3.js +167 -0
- package/package.json +28 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Frodi Karlsson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# @anabranch/storage-s3
|
|
2
|
+
|
|
3
|
+
S3 storage adapter for the anabranch ecosystem. Works with AWS S3, Minio,
|
|
4
|
+
LocalStack, and other S3-compatible APIs.
|
|
5
|
+
|
|
6
|
+
## Usage
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { createS3 } from "@anabranch/storage-s3";
|
|
10
|
+
import { Storage } from "@anabranch/storage";
|
|
11
|
+
|
|
12
|
+
const connector = createS3({
|
|
13
|
+
bucket: "my-bucket",
|
|
14
|
+
region: "us-east-1",
|
|
15
|
+
credentials: {
|
|
16
|
+
accessKeyId: "...",
|
|
17
|
+
secretAccessKey: "...",
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const storage = new Storage(await connector.connect());
|
|
22
|
+
|
|
23
|
+
await storage.put("hello.txt", "world").run();
|
|
24
|
+
|
|
25
|
+
const content = await storage.get("hello.txt").run();
|
|
26
|
+
console.log(await new Response(content.body).text());
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Features
|
|
30
|
+
|
|
31
|
+
- **Concurrent Operations**: Leverage anabranch streams for high-throughput
|
|
32
|
+
listing and bulk transfers.
|
|
33
|
+
- **Retry & Timeouts**: Composable `Task` primitives allow for robust network
|
|
34
|
+
resilience.
|
|
35
|
+
- **Prefix Isolation**: Easily scope storage access to a specific path.
|
|
36
|
+
- **Presigned URLs**: Support for temporary GET/PUT access.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { StorageConnector, StorageOptions } from "@anabranch/storage";
|
|
2
|
+
/** Options for creating an S3 storage connector. */
|
|
3
|
+
export interface S3StorageOptions extends StorageOptions {
|
|
4
|
+
bucket: string;
|
|
5
|
+
region?: string;
|
|
6
|
+
endpoint?: string;
|
|
7
|
+
credentials?: {
|
|
8
|
+
accessKeyId: string;
|
|
9
|
+
secretAccessKey: string;
|
|
10
|
+
sessionToken?: string;
|
|
11
|
+
};
|
|
12
|
+
forcePathStyle?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Whether to use connection pooling. Defaults to true.
|
|
15
|
+
* Disable this in tests to avoid TCP resource leaks.
|
|
16
|
+
*/
|
|
17
|
+
pooled?: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Creates a {@link StorageConnector} for AWS S3 and compatible services.
|
|
21
|
+
*/
|
|
22
|
+
export declare function createS3(options: S3StorageOptions): StorageConnector;
|
|
23
|
+
//# sourceMappingURL=connector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connector.d.ts","sourceRoot":"","sources":["../src/connector.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAG3E,oDAAoD;AACpD,MAAM,WAAW,gBAAiB,SAAQ,cAAc;IACtD,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE;QACZ,WAAW,EAAE,MAAM,CAAC;QACpB,eAAe,EAAE,MAAM,CAAC;QACxB,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,CAAC;IACF,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,gBAAgB,GAAG,gBAAgB,CAwBpE"}
|
package/esm/connector.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { S3Client } from "@aws-sdk/client-s3";
|
|
2
|
+
import { NodeHttpHandler } from "@smithy/node-http-handler";
|
|
3
|
+
import { Agent } from "node:https";
|
|
4
|
+
import { Agent as HttpAgent } from "node:http";
|
|
5
|
+
import { S3Adapter } from "./s3.js";
|
|
6
|
+
/**
|
|
7
|
+
* Creates a {@link StorageConnector} for AWS S3 and compatible services.
|
|
8
|
+
*/
|
|
9
|
+
export function createS3(options) {
|
|
10
|
+
const pooled = options.pooled ?? true;
|
|
11
|
+
const client = new S3Client({
|
|
12
|
+
region: options.region ?? "us-east-1",
|
|
13
|
+
endpoint: options.endpoint,
|
|
14
|
+
credentials: options.credentials,
|
|
15
|
+
forcePathStyle: options.forcePathStyle,
|
|
16
|
+
requestHandler: pooled ? undefined : new NodeHttpHandler({
|
|
17
|
+
httpAgent: new HttpAgent({ keepAlive: false }),
|
|
18
|
+
httpsAgent: new Agent({ keepAlive: false }),
|
|
19
|
+
}),
|
|
20
|
+
});
|
|
21
|
+
return {
|
|
22
|
+
connect: () => Promise.resolve(new S3Adapter(client, options.bucket, options.prefix ?? "")),
|
|
23
|
+
end: () => {
|
|
24
|
+
client.destroy();
|
|
25
|
+
return Promise.resolve();
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
package/esm/index.d.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @anabranch/storage-s3
|
|
3
|
+
*
|
|
4
|
+
* S3 adapter for @anabranch/storage using @aws-sdk/client-s3.
|
|
5
|
+
* Supports presigned URLs, multipart uploads, and all storage operations.
|
|
6
|
+
*
|
|
7
|
+
* ## Connector vs Adapter
|
|
8
|
+
*
|
|
9
|
+
* A **StorageConnector** produces connected **StorageAdapter** instances. Use
|
|
10
|
+
* `createS3()` for production code to properly manage S3 connections.
|
|
11
|
+
*
|
|
12
|
+
* ## Core Types
|
|
13
|
+
*
|
|
14
|
+
* - {@linkcode StorageConnector} - Connection factory for S3
|
|
15
|
+
* - {@linkcode StorageAdapter} - Low-level storage operations (put, get, delete, head, list)
|
|
16
|
+
* - {@linkcode PresignableAdapter} - Extended interface with presign() for presigned URLs
|
|
17
|
+
*
|
|
18
|
+
* ## Error Types
|
|
19
|
+
*
|
|
20
|
+
* All errors are typed for catchable handling:
|
|
21
|
+
* - {@linkcode StorageObjectNotFound} - Object does not exist
|
|
22
|
+
* - {@linkcode StoragePutFailed} - Put operation failed
|
|
23
|
+
* - {@linkcode StorageGetFailed} - Get operation failed
|
|
24
|
+
* - {@linkcode StorageDeleteFailed} - Delete operation failed
|
|
25
|
+
* - {@linkcode StorageHeadFailed} - Head operation failed
|
|
26
|
+
* - {@linkcode StorageListFailed} - List operation failed
|
|
27
|
+
* - {@linkcode StoragePresignFailed} - Presign operation failed
|
|
28
|
+
* - {@linkcode StoragePresignNotSupported} - Adapter doesn't support presigning
|
|
29
|
+
*
|
|
30
|
+
* @example Concurrent uploads with retry and backpressure
|
|
31
|
+
* ```ts
|
|
32
|
+
* import { Task, createS3 } from "@anabranch/storage-s3";
|
|
33
|
+
* import { Storage } from "@anabranch/storage";
|
|
34
|
+
*
|
|
35
|
+
* const connector = createS3({ bucket: "uploads", region: "us-east-1" });
|
|
36
|
+
* const storage = await Storage.connect(connector).run();
|
|
37
|
+
*
|
|
38
|
+
* const files = ["a.txt", "b.txt", "c.txt"];
|
|
39
|
+
*
|
|
40
|
+
* await Task.all(
|
|
41
|
+
* files.map((file) =>
|
|
42
|
+
* storage.put(file, Deno.readFileSync(`./${file}`))
|
|
43
|
+
* .retry({ attempts: 3, delay: (i) => 500 * Math.pow(2, i) })
|
|
44
|
+
* .timeout(30_000)
|
|
45
|
+
* )
|
|
46
|
+
* ).run();
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* @example Upload with presigned URL and result handling
|
|
50
|
+
* ```ts
|
|
51
|
+
* import { Task, createS3 } from "@anabranch/storage-s3";
|
|
52
|
+
* import { Storage } from "@anabranch/storage";
|
|
53
|
+
*
|
|
54
|
+
* const connector = createS3({ bucket: "images", region: "us-east-1" });
|
|
55
|
+
* const storage = await Storage.connect(connector).run();
|
|
56
|
+
*
|
|
57
|
+
* const result = await storage
|
|
58
|
+
* .presign("photo.jpg", { method: "PUT", expiresIn: 3600 })
|
|
59
|
+
* .result();
|
|
60
|
+
*
|
|
61
|
+
* if (result.type === "error") {
|
|
62
|
+
* console.error("Failed to generate upload URL:", result.error);
|
|
63
|
+
* return;
|
|
64
|
+
* }
|
|
65
|
+
*
|
|
66
|
+
* const uploadUrl = result.value;
|
|
67
|
+
*
|
|
68
|
+
* await Task.of(async () => {
|
|
69
|
+
* const response = await fetch(uploadUrl, {
|
|
70
|
+
* method: "PUT",
|
|
71
|
+
* body: await Deno.readFile("photo.jpg"),
|
|
72
|
+
* headers: { "Content-Type": "image/jpeg" },
|
|
73
|
+
* });
|
|
74
|
+
* if (!response.ok) throw new Error("Upload failed");
|
|
75
|
+
* })
|
|
76
|
+
* .retry({ attempts: 3 })
|
|
77
|
+
* .timeout(60_000)
|
|
78
|
+
* .run();
|
|
79
|
+
* ```
|
|
80
|
+
*
|
|
81
|
+
* @example Process list results with concurrency limits
|
|
82
|
+
* ```ts
|
|
83
|
+
* const storage = await Storage.connect(
|
|
84
|
+
* createS3({ bucket: "logs", prefix: "archive/" })
|
|
85
|
+
* ).run();
|
|
86
|
+
*
|
|
87
|
+
* const { successes, errors } = await storage.list()
|
|
88
|
+
* .withConcurrency(10)
|
|
89
|
+
* .map(async (entry) => {
|
|
90
|
+
* const obj = await storage.get(entry.key).run();
|
|
91
|
+
* const text = await new Response(obj.body).text();
|
|
92
|
+
* return { key: entry.key, lines: text.split("\n").length };
|
|
93
|
+
* })
|
|
94
|
+
* .tapErr((err) => console.error("Failed processing:", err))
|
|
95
|
+
* .partition();
|
|
96
|
+
*
|
|
97
|
+
* console.log(`Processed ${successes.length} files with ${errors.length} errors`);
|
|
98
|
+
* ```
|
|
99
|
+
*
|
|
100
|
+
* @module
|
|
101
|
+
*/
|
|
102
|
+
export { S3Adapter } from "./s3.js";
|
|
103
|
+
export { createS3 } from "./connector.js";
|
|
104
|
+
export type { S3StorageOptions } from "./connector.js";
|
|
105
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoGG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,YAAY,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC"}
|
package/esm/index.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @anabranch/storage-s3
|
|
3
|
+
*
|
|
4
|
+
* S3 adapter for @anabranch/storage using @aws-sdk/client-s3.
|
|
5
|
+
* Supports presigned URLs, multipart uploads, and all storage operations.
|
|
6
|
+
*
|
|
7
|
+
* ## Connector vs Adapter
|
|
8
|
+
*
|
|
9
|
+
* A **StorageConnector** produces connected **StorageAdapter** instances. Use
|
|
10
|
+
* `createS3()` for production code to properly manage S3 connections.
|
|
11
|
+
*
|
|
12
|
+
* ## Core Types
|
|
13
|
+
*
|
|
14
|
+
* - {@linkcode StorageConnector} - Connection factory for S3
|
|
15
|
+
* - {@linkcode StorageAdapter} - Low-level storage operations (put, get, delete, head, list)
|
|
16
|
+
* - {@linkcode PresignableAdapter} - Extended interface with presign() for presigned URLs
|
|
17
|
+
*
|
|
18
|
+
* ## Error Types
|
|
19
|
+
*
|
|
20
|
+
* All errors are typed for catchable handling:
|
|
21
|
+
* - {@linkcode StorageObjectNotFound} - Object does not exist
|
|
22
|
+
* - {@linkcode StoragePutFailed} - Put operation failed
|
|
23
|
+
* - {@linkcode StorageGetFailed} - Get operation failed
|
|
24
|
+
* - {@linkcode StorageDeleteFailed} - Delete operation failed
|
|
25
|
+
* - {@linkcode StorageHeadFailed} - Head operation failed
|
|
26
|
+
* - {@linkcode StorageListFailed} - List operation failed
|
|
27
|
+
* - {@linkcode StoragePresignFailed} - Presign operation failed
|
|
28
|
+
* - {@linkcode StoragePresignNotSupported} - Adapter doesn't support presigning
|
|
29
|
+
*
|
|
30
|
+
* @example Concurrent uploads with retry and backpressure
|
|
31
|
+
* ```ts
|
|
32
|
+
* import { Task, createS3 } from "@anabranch/storage-s3";
|
|
33
|
+
* import { Storage } from "@anabranch/storage";
|
|
34
|
+
*
|
|
35
|
+
* const connector = createS3({ bucket: "uploads", region: "us-east-1" });
|
|
36
|
+
* const storage = await Storage.connect(connector).run();
|
|
37
|
+
*
|
|
38
|
+
* const files = ["a.txt", "b.txt", "c.txt"];
|
|
39
|
+
*
|
|
40
|
+
* await Task.all(
|
|
41
|
+
* files.map((file) =>
|
|
42
|
+
* storage.put(file, Deno.readFileSync(`./${file}`))
|
|
43
|
+
* .retry({ attempts: 3, delay: (i) => 500 * Math.pow(2, i) })
|
|
44
|
+
* .timeout(30_000)
|
|
45
|
+
* )
|
|
46
|
+
* ).run();
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* @example Upload with presigned URL and result handling
|
|
50
|
+
* ```ts
|
|
51
|
+
* import { Task, createS3 } from "@anabranch/storage-s3";
|
|
52
|
+
* import { Storage } from "@anabranch/storage";
|
|
53
|
+
*
|
|
54
|
+
* const connector = createS3({ bucket: "images", region: "us-east-1" });
|
|
55
|
+
* const storage = await Storage.connect(connector).run();
|
|
56
|
+
*
|
|
57
|
+
* const result = await storage
|
|
58
|
+
* .presign("photo.jpg", { method: "PUT", expiresIn: 3600 })
|
|
59
|
+
* .result();
|
|
60
|
+
*
|
|
61
|
+
* if (result.type === "error") {
|
|
62
|
+
* console.error("Failed to generate upload URL:", result.error);
|
|
63
|
+
* return;
|
|
64
|
+
* }
|
|
65
|
+
*
|
|
66
|
+
* const uploadUrl = result.value;
|
|
67
|
+
*
|
|
68
|
+
* await Task.of(async () => {
|
|
69
|
+
* const response = await fetch(uploadUrl, {
|
|
70
|
+
* method: "PUT",
|
|
71
|
+
* body: await Deno.readFile("photo.jpg"),
|
|
72
|
+
* headers: { "Content-Type": "image/jpeg" },
|
|
73
|
+
* });
|
|
74
|
+
* if (!response.ok) throw new Error("Upload failed");
|
|
75
|
+
* })
|
|
76
|
+
* .retry({ attempts: 3 })
|
|
77
|
+
* .timeout(60_000)
|
|
78
|
+
* .run();
|
|
79
|
+
* ```
|
|
80
|
+
*
|
|
81
|
+
* @example Process list results with concurrency limits
|
|
82
|
+
* ```ts
|
|
83
|
+
* const storage = await Storage.connect(
|
|
84
|
+
* createS3({ bucket: "logs", prefix: "archive/" })
|
|
85
|
+
* ).run();
|
|
86
|
+
*
|
|
87
|
+
* const { successes, errors } = await storage.list()
|
|
88
|
+
* .withConcurrency(10)
|
|
89
|
+
* .map(async (entry) => {
|
|
90
|
+
* const obj = await storage.get(entry.key).run();
|
|
91
|
+
* const text = await new Response(obj.body).text();
|
|
92
|
+
* return { key: entry.key, lines: text.split("\n").length };
|
|
93
|
+
* })
|
|
94
|
+
* .tapErr((err) => console.error("Failed processing:", err))
|
|
95
|
+
* .partition();
|
|
96
|
+
*
|
|
97
|
+
* console.log(`Processed ${successes.length} files with ${errors.length} errors`);
|
|
98
|
+
* ```
|
|
99
|
+
*
|
|
100
|
+
* @module
|
|
101
|
+
*/
|
|
102
|
+
export { S3Adapter } from "./s3.js";
|
|
103
|
+
export { createS3 } from "./connector.js";
|
package/esm/package.json
ADDED
package/esm/s3.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { S3Client } from "@aws-sdk/client-s3";
|
|
2
|
+
import type { BodyInput, PresignableAdapter, PresignOptions, PutOptions, StorageEntry, StorageMetadata, StorageObject } from "@anabranch/storage";
|
|
3
|
+
/**
|
|
4
|
+
* Adapter for AWS S3 and compatible services (Minio, LocalStack, etc.).
|
|
5
|
+
*/
|
|
6
|
+
export declare class S3Adapter implements PresignableAdapter {
|
|
7
|
+
private readonly client;
|
|
8
|
+
private readonly bucket;
|
|
9
|
+
private readonly prefix;
|
|
10
|
+
constructor(client: S3Client, bucket: string, prefix: string);
|
|
11
|
+
put(key: string, body: BodyInput, options?: PutOptions): Promise<void>;
|
|
12
|
+
get(key: string): Promise<StorageObject>;
|
|
13
|
+
/**
|
|
14
|
+
* Deletes an object from S3. This operation is idempotent; it returns
|
|
15
|
+
* successfully even if the object does not exist.
|
|
16
|
+
*/
|
|
17
|
+
delete(key: string): Promise<void>;
|
|
18
|
+
head(key: string): Promise<StorageMetadata>;
|
|
19
|
+
list(prefix?: string): AsyncIterable<StorageEntry>;
|
|
20
|
+
presign(key: string, options: PresignOptions): Promise<string>;
|
|
21
|
+
close(): Promise<void>;
|
|
22
|
+
private isNotFound;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=s3.d.ts.map
|
package/esm/s3.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"s3.d.ts","sourceRoot":"","sources":["../src/s3.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,QAAQ,EACT,MAAM,oBAAoB,CAAC;AAE5B,OAAO,KAAK,EACV,SAAS,EACT,kBAAkB,EAClB,cAAc,EACd,UAAU,EACV,YAAY,EACZ,eAAe,EACf,aAAa,EACd,MAAM,oBAAoB,CAAC;AAW5B;;GAEG;AACH,qBAAa,SAAU,YAAW,kBAAkB;IAEhD,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAFN,MAAM,EAAE,QAAQ,EAChB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM;IAG3B,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAqBtE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAqC9C;;;OAGG;IACG,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBlC,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IA8BjD,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,aAAa,CAAC,YAAY,CAAC;IA0C5C,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC;IAmBpE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAItB,OAAO,CAAC,UAAU;CAWnB"}
|
package/esm/s3.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand, } from "@aws-sdk/client-s3";
|
|
2
|
+
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
3
|
+
import { StorageDeleteFailed, StorageGetFailed, StorageHeadFailed, StorageListFailed, StorageObjectNotFound, StoragePresignFailed, StoragePutFailed, } from "@anabranch/storage";
|
|
4
|
+
/**
|
|
5
|
+
* Adapter for AWS S3 and compatible services (Minio, LocalStack, etc.).
|
|
6
|
+
*/
|
|
7
|
+
export class S3Adapter {
|
|
8
|
+
constructor(client, bucket, prefix) {
|
|
9
|
+
Object.defineProperty(this, "client", {
|
|
10
|
+
enumerable: true,
|
|
11
|
+
configurable: true,
|
|
12
|
+
writable: true,
|
|
13
|
+
value: client
|
|
14
|
+
});
|
|
15
|
+
Object.defineProperty(this, "bucket", {
|
|
16
|
+
enumerable: true,
|
|
17
|
+
configurable: true,
|
|
18
|
+
writable: true,
|
|
19
|
+
value: bucket
|
|
20
|
+
});
|
|
21
|
+
Object.defineProperty(this, "prefix", {
|
|
22
|
+
enumerable: true,
|
|
23
|
+
configurable: true,
|
|
24
|
+
writable: true,
|
|
25
|
+
value: prefix
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
async put(key, body, options) {
|
|
29
|
+
const fullKey = this.prefix + key;
|
|
30
|
+
try {
|
|
31
|
+
await this.client.send(new PutObjectCommand({
|
|
32
|
+
Bucket: this.bucket,
|
|
33
|
+
Key: fullKey,
|
|
34
|
+
Body: body,
|
|
35
|
+
ContentType: options?.contentType,
|
|
36
|
+
Metadata: options?.custom,
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
throw new StoragePutFailed(key, error instanceof Error ? error.message : String(error), error);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async get(key) {
|
|
44
|
+
const fullKey = this.prefix + key;
|
|
45
|
+
try {
|
|
46
|
+
const response = await this.client.send(new GetObjectCommand({
|
|
47
|
+
Bucket: this.bucket,
|
|
48
|
+
Key: fullKey,
|
|
49
|
+
}));
|
|
50
|
+
if (!response.Body) {
|
|
51
|
+
throw new Error("Empty body in S3 response");
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
body: response.Body.transformToWebStream(),
|
|
55
|
+
metadata: {
|
|
56
|
+
key,
|
|
57
|
+
size: response.ContentLength ?? 0,
|
|
58
|
+
etag: response.ETag,
|
|
59
|
+
lastModified: response.LastModified ?? new Date(),
|
|
60
|
+
contentType: response.ContentType,
|
|
61
|
+
custom: response.Metadata,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (this.isNotFound(error)) {
|
|
67
|
+
throw new StorageObjectNotFound(key);
|
|
68
|
+
}
|
|
69
|
+
throw new StorageGetFailed(key, error instanceof Error ? error.message : String(error), error);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Deletes an object from S3. This operation is idempotent; it returns
|
|
74
|
+
* successfully even if the object does not exist.
|
|
75
|
+
*/
|
|
76
|
+
async delete(key) {
|
|
77
|
+
const fullKey = this.prefix + key;
|
|
78
|
+
try {
|
|
79
|
+
await this.client.send(new DeleteObjectCommand({
|
|
80
|
+
Bucket: this.bucket,
|
|
81
|
+
Key: fullKey,
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
throw new StorageDeleteFailed(key, error instanceof Error ? error.message : String(error), error);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async head(key) {
|
|
89
|
+
const fullKey = this.prefix + key;
|
|
90
|
+
try {
|
|
91
|
+
const response = await this.client.send(new HeadObjectCommand({
|
|
92
|
+
Bucket: this.bucket,
|
|
93
|
+
Key: fullKey,
|
|
94
|
+
}));
|
|
95
|
+
return {
|
|
96
|
+
key,
|
|
97
|
+
size: response.ContentLength ?? 0,
|
|
98
|
+
etag: response.ETag,
|
|
99
|
+
lastModified: response.LastModified ?? new Date(),
|
|
100
|
+
contentType: response.ContentType,
|
|
101
|
+
custom: response.Metadata,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (this.isNotFound(error)) {
|
|
106
|
+
throw new StorageObjectNotFound(key);
|
|
107
|
+
}
|
|
108
|
+
throw new StorageHeadFailed(key, error instanceof Error ? error.message : String(error), error);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
list(prefix) {
|
|
112
|
+
const searchPrefix = this.prefix + (prefix ?? "");
|
|
113
|
+
const client = this.client;
|
|
114
|
+
const bucket = this.bucket;
|
|
115
|
+
const rootPrefix = this.prefix;
|
|
116
|
+
return (async function* () {
|
|
117
|
+
let continuationToken;
|
|
118
|
+
try {
|
|
119
|
+
do {
|
|
120
|
+
const response = await client.send(new ListObjectsV2Command({
|
|
121
|
+
Bucket: bucket,
|
|
122
|
+
Prefix: searchPrefix,
|
|
123
|
+
ContinuationToken: continuationToken,
|
|
124
|
+
}));
|
|
125
|
+
if (response.Contents) {
|
|
126
|
+
for (const item of response.Contents) {
|
|
127
|
+
if (item.Key) {
|
|
128
|
+
yield {
|
|
129
|
+
key: item.Key.slice(rootPrefix.length),
|
|
130
|
+
size: item.Size ?? 0,
|
|
131
|
+
lastModified: item.LastModified ?? new Date(),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
continuationToken = response.NextContinuationToken;
|
|
137
|
+
} while (continuationToken);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
throw new StorageListFailed(searchPrefix, error instanceof Error ? error.message : String(error), error);
|
|
141
|
+
}
|
|
142
|
+
})();
|
|
143
|
+
}
|
|
144
|
+
async presign(key, options) {
|
|
145
|
+
const fullKey = this.prefix + key;
|
|
146
|
+
try {
|
|
147
|
+
const command = options.method === "PUT"
|
|
148
|
+
? new PutObjectCommand({ Bucket: this.bucket, Key: fullKey })
|
|
149
|
+
: new GetObjectCommand({ Bucket: this.bucket, Key: fullKey });
|
|
150
|
+
return await getSignedUrl(this.client, command, {
|
|
151
|
+
expiresIn: options.expiresIn,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
throw new StoragePresignFailed(key, error instanceof Error ? error.message : String(error), error);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
close() {
|
|
159
|
+
return Promise.resolve();
|
|
160
|
+
}
|
|
161
|
+
isNotFound(error) {
|
|
162
|
+
const err = error;
|
|
163
|
+
return (err.name === "NoSuchKey" ||
|
|
164
|
+
err.name === "NotFound" ||
|
|
165
|
+
err.$metadata?.httpStatusCode === 404);
|
|
166
|
+
}
|
|
167
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@anabranch/storage-s3",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "S3 storage adapter for the anabranch ecosystem",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/frodi-karlsson/anabranch.git"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/frodi-karlsson/anabranch.git"
|
|
12
|
+
},
|
|
13
|
+
"module": "./esm/index.js",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"import": "./esm/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"scripts": {},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@anabranch/storage": "^0",
|
|
22
|
+
"@aws-sdk/client-s3": "^3",
|
|
23
|
+
"@aws-sdk/s3-request-presigner": "^3",
|
|
24
|
+
"@smithy/node-http-handler": "*",
|
|
25
|
+
"anabranch": "^0"
|
|
26
|
+
},
|
|
27
|
+
"_generatedBy": "dnt@dev"
|
|
28
|
+
}
|