@unifiedcommerce/adapter-s3 0.2.0 → 0.2.2
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/package.json +2 -1
- package/src/index.ts +158 -0
package/package.json
CHANGED
package/src/index.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DeleteObjectCommand,
|
|
3
|
+
GetObjectCommand,
|
|
4
|
+
ListObjectsV2Command,
|
|
5
|
+
PutObjectCommand,
|
|
6
|
+
S3Client,
|
|
7
|
+
type S3ClientConfig,
|
|
8
|
+
} from "@aws-sdk/client-s3";
|
|
9
|
+
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
10
|
+
import { Err, Ok, type Result, type StorageAdapter } from "@unifiedcommerce/core";
|
|
11
|
+
|
|
12
|
+
interface S3ClientLike {
|
|
13
|
+
send(command: unknown): Promise<any>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface S3StorageAdapterOptions {
|
|
17
|
+
bucket: string;
|
|
18
|
+
region: string;
|
|
19
|
+
endpoint?: string;
|
|
20
|
+
forcePathStyle?: boolean;
|
|
21
|
+
publicBaseUrl?: string;
|
|
22
|
+
signedUrlExpiresIn?: number;
|
|
23
|
+
credentials?: S3ClientConfig["credentials"];
|
|
24
|
+
client?: S3ClientLike;
|
|
25
|
+
signUrl?: (client: S3ClientLike, command: unknown, options: { expiresIn: number }) => Promise<string>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function toArrayBuffer(data: ArrayBuffer | ReadableStream): Promise<ArrayBuffer> {
|
|
29
|
+
if (data instanceof ArrayBuffer) return data;
|
|
30
|
+
return new Response(data).arrayBuffer();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function objectUrl(options: S3StorageAdapterOptions, key: string): string {
|
|
34
|
+
if (options.publicBaseUrl) {
|
|
35
|
+
return `${options.publicBaseUrl.replace(/\/$/, "")}/${key}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (options.endpoint) {
|
|
39
|
+
const endpoint = options.endpoint.replace(/\/$/, "");
|
|
40
|
+
if (options.forcePathStyle) {
|
|
41
|
+
return `${endpoint}/${options.bucket}/${key}`;
|
|
42
|
+
}
|
|
43
|
+
return `${endpoint}/${key}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return `https://${options.bucket}.s3.${options.region}.amazonaws.com/${key}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function s3StorageAdapter(options: S3StorageAdapterOptions): StorageAdapter {
|
|
50
|
+
const client: S3ClientLike =
|
|
51
|
+
options.client ??
|
|
52
|
+
new S3Client({
|
|
53
|
+
region: options.region,
|
|
54
|
+
...(options.endpoint ? { endpoint: options.endpoint } : {}),
|
|
55
|
+
...(options.forcePathStyle !== undefined ? { forcePathStyle: options.forcePathStyle } : {}),
|
|
56
|
+
...(options.credentials ? { credentials: options.credentials } : {}),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const signUrl = options.signUrl ?? (async (currentClient, command, config) => getSignedUrl(currentClient as any, command as any, config));
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
providerId: "s3",
|
|
63
|
+
|
|
64
|
+
async upload(key, data, contentType): Promise<Result<{ key: string; url: string; contentType: string; size?: number }>> {
|
|
65
|
+
try {
|
|
66
|
+
const body = await toArrayBuffer(data);
|
|
67
|
+
|
|
68
|
+
await client.send(
|
|
69
|
+
new PutObjectCommand({
|
|
70
|
+
Bucket: options.bucket,
|
|
71
|
+
Key: key,
|
|
72
|
+
Body: new Uint8Array(body),
|
|
73
|
+
ContentType: contentType,
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
return Ok({
|
|
78
|
+
key,
|
|
79
|
+
url: objectUrl(options, key),
|
|
80
|
+
contentType,
|
|
81
|
+
size: body.byteLength,
|
|
82
|
+
});
|
|
83
|
+
} catch (error) {
|
|
84
|
+
return Err({
|
|
85
|
+
code: "S3_UPLOAD_FAILED",
|
|
86
|
+
message: error instanceof Error ? error.message : "Failed to upload to S3.",
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
async getUrl(key): Promise<Result<string>> {
|
|
92
|
+
return Ok(objectUrl(options, key));
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
async getSignedUrl(key, expiresIn): Promise<Result<string>> {
|
|
96
|
+
try {
|
|
97
|
+
const ttl = expiresIn > 0 ? expiresIn : options.signedUrlExpiresIn ?? 900;
|
|
98
|
+
const url = await signUrl(
|
|
99
|
+
client,
|
|
100
|
+
new GetObjectCommand({
|
|
101
|
+
Bucket: options.bucket,
|
|
102
|
+
Key: key,
|
|
103
|
+
}),
|
|
104
|
+
{ expiresIn: ttl },
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return Ok(url);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return Err({
|
|
110
|
+
code: "S3_SIGNED_URL_FAILED",
|
|
111
|
+
message: error instanceof Error ? error.message : "Failed to generate signed URL.",
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
async delete(key): Promise<Result<void>> {
|
|
117
|
+
try {
|
|
118
|
+
await client.send(
|
|
119
|
+
new DeleteObjectCommand({
|
|
120
|
+
Bucket: options.bucket,
|
|
121
|
+
Key: key,
|
|
122
|
+
}),
|
|
123
|
+
);
|
|
124
|
+
return Ok(undefined);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
return Err({
|
|
127
|
+
code: "S3_DELETE_FAILED",
|
|
128
|
+
message: error instanceof Error ? error.message : "Failed to delete object from S3.",
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
async list(prefix): Promise<Result<Array<{ key: string; url: string; contentType: string; size?: number }>>> {
|
|
134
|
+
try {
|
|
135
|
+
const listed = await client.send(
|
|
136
|
+
new ListObjectsV2Command({
|
|
137
|
+
Bucket: options.bucket,
|
|
138
|
+
Prefix: prefix,
|
|
139
|
+
}),
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const items = (listed.Contents ?? []).map((item: any) => ({
|
|
143
|
+
key: String(item.Key ?? ""),
|
|
144
|
+
url: objectUrl(options, String(item.Key ?? "")),
|
|
145
|
+
contentType: "application/octet-stream",
|
|
146
|
+
size: typeof item.Size === "number" ? item.Size : undefined,
|
|
147
|
+
}));
|
|
148
|
+
|
|
149
|
+
return Ok(items.filter((item: { key: string }) => item.key.length > 0));
|
|
150
|
+
} catch (error) {
|
|
151
|
+
return Err({
|
|
152
|
+
code: "S3_LIST_FAILED",
|
|
153
|
+
message: error instanceof Error ? error.message : "Failed to list S3 objects.",
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|