@lenso/storage 0.0.0-stage → 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 +144 -2
- package/dist/fetch.d.ts +18 -0
- package/dist/fetch.js +98 -0
- package/dist/files.d.ts +68 -0
- package/dist/files.js +237 -0
- package/dist/index-0fz1t930.js +64 -0
- package/dist/index-fa6z0g4w.js +89 -0
- package/dist/index.d.ts +116 -0
- package/dist/index.js +20 -0
- package/dist/local.d.ts +11 -0
- package/dist/local.js +457 -0
- package/dist/postgres.d.ts +567 -0
- package/dist/postgres.js +45 -0
- package/dist/r2.d.ts +52 -0
- package/dist/r2.js +188 -0
- package/dist/s3.d.ts +16 -0
- package/dist/s3.js +386 -0
- package/dist/sqlite.d.ts +602 -0
- package/dist/sqlite.js +45 -0
- package/dist/stream.d.ts +8 -0
- package/migrations/postgres/0001_files.sql +19 -0
- package/migrations/sqlite/0001_files.sql +19 -0
- package/package.json +84 -4
package/dist/r2.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import {
|
|
2
|
+
StorageError2,
|
|
3
|
+
createStoragePlugin2,
|
|
4
|
+
validateKey2,
|
|
5
|
+
validateList2,
|
|
6
|
+
validateRange2,
|
|
7
|
+
unsupportedSigning2
|
|
8
|
+
} from "./index-0fz1t930.js";
|
|
9
|
+
import {
|
|
10
|
+
validateUpload,
|
|
11
|
+
checkedUpload
|
|
12
|
+
} from "./index-fa6z0g4w.js";
|
|
13
|
+
|
|
14
|
+
// src/r2.ts
|
|
15
|
+
function metadata(object) {
|
|
16
|
+
return {
|
|
17
|
+
key: object.key,
|
|
18
|
+
size: object.size,
|
|
19
|
+
contentType: object.httpMetadata?.contentType ?? "application/octet-stream",
|
|
20
|
+
etag: object.httpEtag,
|
|
21
|
+
versionId: object.version,
|
|
22
|
+
lastModified: object.uploaded,
|
|
23
|
+
customMetadata: object.customMetadata
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function failure(error, signal) {
|
|
27
|
+
if (error instanceof StorageError2)
|
|
28
|
+
return error;
|
|
29
|
+
if (signal?.aborted)
|
|
30
|
+
return new StorageError2("aborted", "Storage operation cancelled", { cause: error });
|
|
31
|
+
const value = error;
|
|
32
|
+
const status = value?.status ?? value?.statusCode ?? value?.code;
|
|
33
|
+
const code = status === 403 || status === 10003 || value?.name === "AccessDenied" ? "forbidden" : status === 404 || value?.name === "NoSuchKey" ? "not-found" : status === 409 || status === 412 ? "conflict" : "provider";
|
|
34
|
+
return new StorageError2(code, "R2 storage operation failed", { cause: error });
|
|
35
|
+
}
|
|
36
|
+
function checkSignal(signal) {
|
|
37
|
+
if (signal?.aborted)
|
|
38
|
+
throw new StorageError2("aborted", "Storage operation cancelled", { cause: signal.reason });
|
|
39
|
+
}
|
|
40
|
+
function createR2StoragePlugin(options) {
|
|
41
|
+
return createStoragePlugin2({
|
|
42
|
+
id: options.id,
|
|
43
|
+
setup() {
|
|
44
|
+
const binding = options.binding;
|
|
45
|
+
return {
|
|
46
|
+
capabilities: {
|
|
47
|
+
provider: "r2-binding",
|
|
48
|
+
signedUpload: false,
|
|
49
|
+
signedDownload: false,
|
|
50
|
+
rangeRead: true,
|
|
51
|
+
conditionalRead: true,
|
|
52
|
+
pagination: "opaque",
|
|
53
|
+
uploadCancellation: "stream-only",
|
|
54
|
+
uploadRequiresSize: true,
|
|
55
|
+
maxUploadBytes: 5 * 1024 ** 3
|
|
56
|
+
},
|
|
57
|
+
async put(input) {
|
|
58
|
+
validateKey2(input.key);
|
|
59
|
+
checkSignal(input.signal);
|
|
60
|
+
validateUpload(input);
|
|
61
|
+
if (input.size === undefined)
|
|
62
|
+
throw new StorageError2("invalid-input", "R2 binding uploads require size");
|
|
63
|
+
if (input.size > 5 * 1024 ** 3)
|
|
64
|
+
throw new StorageError2("too-large", "R2 binding single PUT cannot exceed 5 GiB");
|
|
65
|
+
const FixedLengthStream = globalThis.FixedLengthStream;
|
|
66
|
+
if (!FixedLengthStream)
|
|
67
|
+
throw new StorageError2("unsupported", "R2 uploads require the Workers FixedLengthStream runtime");
|
|
68
|
+
const fixed = new FixedLengthStream(input.size);
|
|
69
|
+
const upload = checkedUpload(input);
|
|
70
|
+
const controller = new AbortController;
|
|
71
|
+
const abort = () => controller.abort(input.signal?.reason);
|
|
72
|
+
input.signal?.addEventListener("abort", abort, { once: true });
|
|
73
|
+
if (input.signal?.aborted)
|
|
74
|
+
abort();
|
|
75
|
+
const pumping = upload.body.pipeTo(fixed.writable, { signal: controller.signal });
|
|
76
|
+
pumping.catch(() => {});
|
|
77
|
+
try {
|
|
78
|
+
const object = await binding.put(input.key, fixed.readable, {
|
|
79
|
+
onlyIf: new Headers({ "If-None-Match": "*" }),
|
|
80
|
+
httpMetadata: { contentType: input.contentType ?? "application/octet-stream" },
|
|
81
|
+
customMetadata: input.customMetadata
|
|
82
|
+
});
|
|
83
|
+
if (!object)
|
|
84
|
+
throw new StorageError2("conflict", "Object already exists");
|
|
85
|
+
await pumping;
|
|
86
|
+
return metadata(object);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
controller.abort();
|
|
89
|
+
try {
|
|
90
|
+
await upload.cancel(error);
|
|
91
|
+
} catch (cleanupError) {
|
|
92
|
+
await pumping.catch(() => {});
|
|
93
|
+
throw new StorageError2("provider", "R2 upload failed and source cleanup failed", {
|
|
94
|
+
cause: new AggregateError([error, cleanupError], "Upload and cleanup failed")
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
const pumpError = await pumping.catch((value) => value);
|
|
98
|
+
if (pumpError instanceof StorageError2)
|
|
99
|
+
throw failure(pumpError, input.signal);
|
|
100
|
+
if (upload.size() > input.size) {
|
|
101
|
+
throw new StorageError2("invalid-input", "Upload size differs from declared size", {
|
|
102
|
+
cause: error
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
throw failure(error, input.signal);
|
|
106
|
+
} finally {
|
|
107
|
+
input.signal?.removeEventListener("abort", abort);
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
async get(key, input = {}) {
|
|
111
|
+
validateKey2(key);
|
|
112
|
+
validateRange2(input, true);
|
|
113
|
+
checkSignal(input.signal);
|
|
114
|
+
try {
|
|
115
|
+
const object = await binding.get(key, {
|
|
116
|
+
range: input.range,
|
|
117
|
+
onlyIf: input.ifMatch === undefined ? undefined : new Headers({ "If-Match": input.ifMatch })
|
|
118
|
+
});
|
|
119
|
+
if (!object)
|
|
120
|
+
throw new StorageError2("not-found", "Object not found");
|
|
121
|
+
if (!object.body)
|
|
122
|
+
throw new StorageError2("conflict", "Object condition failed");
|
|
123
|
+
return {
|
|
124
|
+
metadata: metadata(object),
|
|
125
|
+
body: object.body,
|
|
126
|
+
range: input.range ? {
|
|
127
|
+
offset: object.range?.offset ?? input.range.offset,
|
|
128
|
+
length: object.range?.length ?? Math.max(0, Math.min(input.range.length ?? object.size, object.size - input.range.offset))
|
|
129
|
+
} : undefined
|
|
130
|
+
};
|
|
131
|
+
} catch (error) {
|
|
132
|
+
throw failure(error, input.signal);
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
async head(key, input = {}) {
|
|
136
|
+
validateKey2(key);
|
|
137
|
+
checkSignal(input.signal);
|
|
138
|
+
try {
|
|
139
|
+
const object = await binding.head(key);
|
|
140
|
+
return object ? metadata(object) : null;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
throw failure(error, input.signal);
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
async delete(key, input = {}) {
|
|
146
|
+
validateKey2(key);
|
|
147
|
+
checkSignal(input.signal);
|
|
148
|
+
try {
|
|
149
|
+
await binding.delete(key);
|
|
150
|
+
return { outcome: "absent-or-deleted" };
|
|
151
|
+
} catch (error) {
|
|
152
|
+
const mapped = failure(error, input.signal);
|
|
153
|
+
if (mapped.code === "not-found")
|
|
154
|
+
return { outcome: "not-found" };
|
|
155
|
+
throw mapped;
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
async list(input = {}) {
|
|
159
|
+
const limit = validateList2(input);
|
|
160
|
+
checkSignal(input.signal);
|
|
161
|
+
try {
|
|
162
|
+
const page = await binding.list({
|
|
163
|
+
prefix: input.prefix,
|
|
164
|
+
cursor: input.cursor,
|
|
165
|
+
limit,
|
|
166
|
+
include: ["httpMetadata", "customMetadata"]
|
|
167
|
+
});
|
|
168
|
+
return {
|
|
169
|
+
objects: page.objects.map(metadata),
|
|
170
|
+
cursor: page.truncated ? page.cursor : undefined
|
|
171
|
+
};
|
|
172
|
+
} catch (error) {
|
|
173
|
+
throw failure(error, input.signal);
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
async signUpload() {
|
|
177
|
+
return unsupportedSigning2();
|
|
178
|
+
},
|
|
179
|
+
async signDownload() {
|
|
180
|
+
return unsupportedSigning2();
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
export {
|
|
187
|
+
createR2StoragePlugin
|
|
188
|
+
};
|
package/dist/s3.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { S3Client, type S3ClientConfig } from "@aws-sdk/client-s3";
|
|
2
|
+
import { type ObjectStorage } from "./index";
|
|
3
|
+
export interface S3StorageOptions {
|
|
4
|
+
id: string;
|
|
5
|
+
bucket: string;
|
|
6
|
+
client?: S3Client;
|
|
7
|
+
clientConfig?: S3ClientConfig;
|
|
8
|
+
/** Disable multipart for endpoints without conditional CompleteMultipartUpload. Requires size <= 5 GiB. */
|
|
9
|
+
multipart?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Requires native If-None-Match on PutObject AND CompleteMultipartUpload.
|
|
13
|
+
* Compatible endpoints must enforce these conditions, not merely accept the headers.
|
|
14
|
+
* R2 documents conditional PutObject but not conditional multipart completion.
|
|
15
|
+
*/
|
|
16
|
+
export declare function createS3StoragePlugin(options: S3StorageOptions): import("@lenso/core/plugin").Plugin<ObjectStorage>;
|
package/dist/s3.js
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import {
|
|
2
|
+
StorageError2,
|
|
3
|
+
createStoragePlugin2,
|
|
4
|
+
validateKey2,
|
|
5
|
+
validateList2,
|
|
6
|
+
validateRange2,
|
|
7
|
+
validateExpiry2
|
|
8
|
+
} from "./index-0fz1t930.js";
|
|
9
|
+
import {
|
|
10
|
+
validateUpload,
|
|
11
|
+
checkedUpload
|
|
12
|
+
} from "./index-fa6z0g4w.js";
|
|
13
|
+
|
|
14
|
+
// src/s3.ts
|
|
15
|
+
import {
|
|
16
|
+
AbortMultipartUploadCommand,
|
|
17
|
+
CompleteMultipartUploadCommand,
|
|
18
|
+
DeleteObjectCommand,
|
|
19
|
+
CreateMultipartUploadCommand,
|
|
20
|
+
GetObjectCommand,
|
|
21
|
+
HeadObjectCommand,
|
|
22
|
+
ListObjectsV2Command,
|
|
23
|
+
PutObjectCommand,
|
|
24
|
+
S3Client
|
|
25
|
+
} from "@aws-sdk/client-s3";
|
|
26
|
+
import { Readable } from "node:stream";
|
|
27
|
+
import { finished } from "node:stream/promises";
|
|
28
|
+
import { Upload } from "@aws-sdk/lib-storage";
|
|
29
|
+
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
30
|
+
function failure(error, signal) {
|
|
31
|
+
if (error instanceof StorageError2)
|
|
32
|
+
return error;
|
|
33
|
+
if (signal?.aborted)
|
|
34
|
+
return new StorageError2("aborted", "Storage operation cancelled", { cause: error });
|
|
35
|
+
const value = error;
|
|
36
|
+
const status = value?.$metadata?.httpStatusCode;
|
|
37
|
+
const code = status === 403 || value?.name === "AccessDenied" ? "forbidden" : status === 404 || value?.name === "NoSuchKey" || value?.name === "NotFound" ? "not-found" : status === 409 || status === 412 ? "conflict" : value?.name === "AbortError" ? "aborted" : "provider";
|
|
38
|
+
return new StorageError2(code, "S3 storage operation failed", { cause: error });
|
|
39
|
+
}
|
|
40
|
+
function metadata(key, object) {
|
|
41
|
+
return {
|
|
42
|
+
key,
|
|
43
|
+
size: object.ContentLength ?? 0,
|
|
44
|
+
contentType: object.ContentType ?? "application/octet-stream",
|
|
45
|
+
etag: object.ETag,
|
|
46
|
+
versionId: object.VersionId,
|
|
47
|
+
lastModified: object.LastModified,
|
|
48
|
+
customMetadata: object.Metadata
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function createS3StoragePlugin(options) {
|
|
52
|
+
return createStoragePlugin2({
|
|
53
|
+
id: options.id,
|
|
54
|
+
async setup(context) {
|
|
55
|
+
const client = options.client ?? new S3Client({ requestChecksumCalculation: "WHEN_REQUIRED", ...options.clientConfig });
|
|
56
|
+
if (!options.client)
|
|
57
|
+
context.onCleanup(() => client.destroy());
|
|
58
|
+
const active = new Map;
|
|
59
|
+
let stopped = false;
|
|
60
|
+
context.onCleanup(async () => {
|
|
61
|
+
stopped = true;
|
|
62
|
+
for (const controller of active.keys())
|
|
63
|
+
controller.abort();
|
|
64
|
+
await Promise.allSettled(active.values());
|
|
65
|
+
});
|
|
66
|
+
const endpoint = await client.config.endpoint?.();
|
|
67
|
+
const r2Endpoint = endpoint?.hostname.endsWith(".r2.cloudflarestorage.com") ?? false;
|
|
68
|
+
const multipart = !r2Endpoint && options.multipart !== false;
|
|
69
|
+
const maxUploadBytes = multipart ? 5 * 1024 ** 2 * 1e4 : 5 * 1024 ** 3;
|
|
70
|
+
const Bucket = options.bucket;
|
|
71
|
+
const service = {
|
|
72
|
+
capabilities: {
|
|
73
|
+
provider: "s3",
|
|
74
|
+
signedUpload: true,
|
|
75
|
+
signedDownload: true,
|
|
76
|
+
rangeRead: true,
|
|
77
|
+
conditionalRead: true,
|
|
78
|
+
pagination: "opaque",
|
|
79
|
+
uploadCancellation: "abort",
|
|
80
|
+
uploadRequiresSize: !multipart,
|
|
81
|
+
maxUploadBytes
|
|
82
|
+
},
|
|
83
|
+
async put(input) {
|
|
84
|
+
validateKey2(input.key);
|
|
85
|
+
if (stopped)
|
|
86
|
+
throw new StorageError2("aborted", "Storage plugin stopped");
|
|
87
|
+
try {
|
|
88
|
+
validateUpload(input);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
throw failure(error, input.signal);
|
|
91
|
+
}
|
|
92
|
+
if (!multipart && input.size === undefined)
|
|
93
|
+
throw new StorageError2("invalid-input", "Single PUT uploads require size");
|
|
94
|
+
if (input.size !== undefined && input.size > maxUploadBytes)
|
|
95
|
+
throw new StorageError2("too-large", "Upload exceeds this adapter's size limit");
|
|
96
|
+
if (!multipart && await client.config.requestChecksumCalculation() !== "WHEN_REQUIRED") {
|
|
97
|
+
throw new StorageError2("unsupported", "Single PUT streaming requires requestChecksumCalculation WHEN_REQUIRED");
|
|
98
|
+
}
|
|
99
|
+
const controller = new AbortController;
|
|
100
|
+
const abort = () => controller.abort(input.signal?.reason);
|
|
101
|
+
input.signal?.addEventListener("abort", abort, { once: true });
|
|
102
|
+
if (input.signal?.aborted)
|
|
103
|
+
abort();
|
|
104
|
+
const upload = checkedUpload({
|
|
105
|
+
...input,
|
|
106
|
+
signal: controller.signal,
|
|
107
|
+
maxBytes: Math.min(input.maxBytes ?? maxUploadBytes, maxUploadBytes)
|
|
108
|
+
});
|
|
109
|
+
let uploadId;
|
|
110
|
+
const uploadClient = new Proxy(client, {
|
|
111
|
+
get(target, property, receiver) {
|
|
112
|
+
if (property !== "send")
|
|
113
|
+
return Reflect.get(target, property, receiver);
|
|
114
|
+
return async (command, requestOptions) => {
|
|
115
|
+
if (command instanceof PutObjectCommand || command instanceof CompleteMultipartUploadCommand) {
|
|
116
|
+
if (command.input.IfNoneMatch !== "*") {
|
|
117
|
+
throw new StorageError2("unsupported", "SDK must preserve create-only upload conditions");
|
|
118
|
+
}
|
|
119
|
+
if (r2Endpoint && command instanceof CompleteMultipartUploadCommand) {
|
|
120
|
+
throw new StorageError2("unsupported", "R2 S3 conditional multipart completion is not documented; use the R2 binding");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const result = await Reflect.apply(target.send, target, [
|
|
124
|
+
command,
|
|
125
|
+
{
|
|
126
|
+
...requestOptions,
|
|
127
|
+
...command instanceof AbortMultipartUploadCommand ? {} : { abortSignal: controller.signal }
|
|
128
|
+
}
|
|
129
|
+
]);
|
|
130
|
+
if (command instanceof CreateMultipartUploadCommand)
|
|
131
|
+
uploadId = result.UploadId;
|
|
132
|
+
if (command instanceof AbortMultipartUploadCommand || command instanceof CompleteMultipartUploadCommand)
|
|
133
|
+
uploadId = undefined;
|
|
134
|
+
return result;
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
const task = (async () => {
|
|
139
|
+
let body;
|
|
140
|
+
try {
|
|
141
|
+
if (!multipart) {
|
|
142
|
+
body = Readable.from(async function* () {
|
|
143
|
+
const reader = upload.body.getReader();
|
|
144
|
+
try {
|
|
145
|
+
while (true) {
|
|
146
|
+
const chunk = await reader.read();
|
|
147
|
+
if (chunk.done)
|
|
148
|
+
break;
|
|
149
|
+
yield chunk.value;
|
|
150
|
+
}
|
|
151
|
+
} finally {
|
|
152
|
+
reader.releaseLock();
|
|
153
|
+
}
|
|
154
|
+
}(), { objectMode: false });
|
|
155
|
+
}
|
|
156
|
+
const result = multipart ? await new Upload({
|
|
157
|
+
client: uploadClient,
|
|
158
|
+
params: {
|
|
159
|
+
Bucket,
|
|
160
|
+
Key: input.key,
|
|
161
|
+
Body: upload.body,
|
|
162
|
+
ContentType: input.contentType ?? "application/octet-stream",
|
|
163
|
+
Metadata: input.customMetadata,
|
|
164
|
+
IfNoneMatch: "*"
|
|
165
|
+
},
|
|
166
|
+
queueSize: 1,
|
|
167
|
+
partSize: 5 * 1024 * 1024,
|
|
168
|
+
leavePartsOnError: false
|
|
169
|
+
}).done() : await client.send(new PutObjectCommand({
|
|
170
|
+
Bucket,
|
|
171
|
+
Key: input.key,
|
|
172
|
+
Body: body,
|
|
173
|
+
ContentLength: input.size,
|
|
174
|
+
ContentType: input.contentType ?? "application/octet-stream",
|
|
175
|
+
Metadata: input.customMetadata,
|
|
176
|
+
IfNoneMatch: "*"
|
|
177
|
+
}), { abortSignal: controller.signal });
|
|
178
|
+
if (body)
|
|
179
|
+
await finished(body, { cleanup: true });
|
|
180
|
+
return {
|
|
181
|
+
key: input.key,
|
|
182
|
+
size: upload.size(),
|
|
183
|
+
contentType: input.contentType ?? "application/octet-stream",
|
|
184
|
+
etag: result.ETag,
|
|
185
|
+
versionId: result.VersionId,
|
|
186
|
+
customMetadata: input.customMetadata
|
|
187
|
+
};
|
|
188
|
+
} catch (error) {
|
|
189
|
+
body?.destroy();
|
|
190
|
+
const cleanupErrors = [];
|
|
191
|
+
try {
|
|
192
|
+
await upload.cancel(error);
|
|
193
|
+
} catch (cleanupError) {
|
|
194
|
+
cleanupErrors.push(cleanupError);
|
|
195
|
+
}
|
|
196
|
+
if (uploadId) {
|
|
197
|
+
try {
|
|
198
|
+
await client.send(new AbortMultipartUploadCommand({ Bucket, Key: input.key, UploadId: uploadId }));
|
|
199
|
+
} catch (cleanupError) {
|
|
200
|
+
cleanupErrors.push(cleanupError);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (cleanupErrors.length)
|
|
204
|
+
throw new StorageError2("provider", "S3 upload failed and cleanup failed", {
|
|
205
|
+
cause: new AggregateError([error, ...cleanupErrors], "Upload and cleanup failed")
|
|
206
|
+
});
|
|
207
|
+
throw failure(error, controller.signal);
|
|
208
|
+
} finally {
|
|
209
|
+
input.signal?.removeEventListener("abort", abort);
|
|
210
|
+
active.delete(controller);
|
|
211
|
+
}
|
|
212
|
+
})();
|
|
213
|
+
active.set(controller, task);
|
|
214
|
+
return task;
|
|
215
|
+
},
|
|
216
|
+
async head(key, input = {}) {
|
|
217
|
+
validateKey2(key);
|
|
218
|
+
try {
|
|
219
|
+
const result = await client.send(new HeadObjectCommand({ Bucket, Key: key }), {
|
|
220
|
+
abortSignal: input.signal
|
|
221
|
+
});
|
|
222
|
+
return metadata(key, result);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
const mapped = failure(error, input.signal);
|
|
225
|
+
if (mapped.code === "not-found")
|
|
226
|
+
return null;
|
|
227
|
+
throw mapped;
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
async get(key, input = {}) {
|
|
231
|
+
validateKey2(key);
|
|
232
|
+
validateRange2(input, true);
|
|
233
|
+
let responseBody;
|
|
234
|
+
try {
|
|
235
|
+
const range = input.range;
|
|
236
|
+
const result = await client.send(new GetObjectCommand({
|
|
237
|
+
Bucket,
|
|
238
|
+
Key: key,
|
|
239
|
+
IfMatch: input.ifMatch,
|
|
240
|
+
Range: range ? `bytes=${range.offset}-${range.length === undefined ? "" : range.offset + range.length - 1}` : undefined
|
|
241
|
+
}), { abortSignal: input.signal });
|
|
242
|
+
if (!result.Body)
|
|
243
|
+
throw new StorageError2("provider", "S3 response has no body");
|
|
244
|
+
responseBody = result.Body.transformToWebStream();
|
|
245
|
+
const matched = result.ContentRange?.match(/^bytes (\d+)-(\d+)\/(\d+)$/);
|
|
246
|
+
if (range && (!matched || Number(matched[1]) !== range.offset)) {
|
|
247
|
+
throw new StorageError2("unsupported", "Provider did not honor the requested range");
|
|
248
|
+
}
|
|
249
|
+
if (input.ifMatch !== undefined && input.ifMatch !== "*" && result.ETag !== input.ifMatch) {
|
|
250
|
+
throw new StorageError2("conflict", "Provider returned an object outside the requested etag condition");
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
metadata: metadata(key, {
|
|
254
|
+
...result,
|
|
255
|
+
ContentLength: matched ? Number(matched[3]) : result.ContentLength
|
|
256
|
+
}),
|
|
257
|
+
body: responseBody,
|
|
258
|
+
range: matched ? {
|
|
259
|
+
offset: Number(matched[1]),
|
|
260
|
+
length: Number(matched[2]) - Number(matched[1]) + 1
|
|
261
|
+
} : undefined
|
|
262
|
+
};
|
|
263
|
+
} catch (error) {
|
|
264
|
+
try {
|
|
265
|
+
await responseBody?.cancel(error);
|
|
266
|
+
} catch (cleanupError) {
|
|
267
|
+
throw new StorageError2("provider", "S3 download failed and cleanup failed", {
|
|
268
|
+
cause: new AggregateError([error, cleanupError])
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
throw failure(error, input.signal);
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
async delete(key, input = {}) {
|
|
275
|
+
validateKey2(key);
|
|
276
|
+
try {
|
|
277
|
+
await client.send(new DeleteObjectCommand({ Bucket, Key: key }), {
|
|
278
|
+
abortSignal: input.signal
|
|
279
|
+
});
|
|
280
|
+
return { outcome: "absent-or-deleted" };
|
|
281
|
+
} catch (error) {
|
|
282
|
+
const mapped = failure(error, input.signal);
|
|
283
|
+
if (mapped.code === "not-found")
|
|
284
|
+
return { outcome: "not-found" };
|
|
285
|
+
throw mapped;
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
async list(input = {}) {
|
|
289
|
+
const limit = validateList2(input);
|
|
290
|
+
try {
|
|
291
|
+
const page = await client.send(new ListObjectsV2Command({
|
|
292
|
+
Bucket,
|
|
293
|
+
Prefix: input.prefix,
|
|
294
|
+
ContinuationToken: input.cursor,
|
|
295
|
+
MaxKeys: limit
|
|
296
|
+
}), { abortSignal: input.signal });
|
|
297
|
+
const objects = [];
|
|
298
|
+
for (const item of page.Contents ?? []) {
|
|
299
|
+
if (!item.Key)
|
|
300
|
+
continue;
|
|
301
|
+
const object = await service.head(item.Key, { signal: input.signal });
|
|
302
|
+
if (object)
|
|
303
|
+
objects.push(object);
|
|
304
|
+
}
|
|
305
|
+
return { objects, cursor: page.IsTruncated ? page.NextContinuationToken : undefined };
|
|
306
|
+
} catch (error) {
|
|
307
|
+
throw failure(error, input.signal);
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
async signUpload(input) {
|
|
311
|
+
validateKey2(input.key);
|
|
312
|
+
validateExpiry2(input.expiresIn);
|
|
313
|
+
validateUpload({
|
|
314
|
+
key: input.key,
|
|
315
|
+
body: new ReadableStream,
|
|
316
|
+
maxBytes: input.maxBytes,
|
|
317
|
+
contentType: input.contentType
|
|
318
|
+
});
|
|
319
|
+
try {
|
|
320
|
+
const signingClient = new Proxy(client, {
|
|
321
|
+
get(target, property, receiver) {
|
|
322
|
+
if (property === "config") {
|
|
323
|
+
return {
|
|
324
|
+
...target.config,
|
|
325
|
+
requestChecksumCalculation: async () => "WHEN_REQUIRED"
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
return Reflect.get(target, property, receiver);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
const url = await getSignedUrl(signingClient, new PutObjectCommand({
|
|
332
|
+
Bucket,
|
|
333
|
+
Key: input.key,
|
|
334
|
+
ContentType: input.contentType,
|
|
335
|
+
IfNoneMatch: "*"
|
|
336
|
+
}), {
|
|
337
|
+
expiresIn: input.expiresIn,
|
|
338
|
+
signableHeaders: new Set(["content-type", "if-none-match"])
|
|
339
|
+
});
|
|
340
|
+
return {
|
|
341
|
+
url,
|
|
342
|
+
method: "PUT",
|
|
343
|
+
headers: { "content-type": input.contentType, "if-none-match": "*" },
|
|
344
|
+
expiresAt: new Date(Date.now() + input.expiresIn * 1000),
|
|
345
|
+
conditions: {
|
|
346
|
+
contentType: input.contentType,
|
|
347
|
+
createOnly: true,
|
|
348
|
+
maxBytes: input.maxBytes,
|
|
349
|
+
sizeEnforcement: input.maxBytes === undefined ? "none" : "completion-check"
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
} catch (error) {
|
|
353
|
+
throw failure(error);
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
async signDownload(input) {
|
|
357
|
+
validateKey2(input.key);
|
|
358
|
+
validateExpiry2(input.expiresIn);
|
|
359
|
+
try {
|
|
360
|
+
const url = await getSignedUrl(client, new GetObjectCommand({
|
|
361
|
+
Bucket,
|
|
362
|
+
Key: input.key,
|
|
363
|
+
IfMatch: input.ifMatch
|
|
364
|
+
}), { expiresIn: input.expiresIn, signableHeaders: new Set(["if-match"]) });
|
|
365
|
+
const headers = {};
|
|
366
|
+
if (input.ifMatch !== undefined)
|
|
367
|
+
headers["if-match"] = input.ifMatch;
|
|
368
|
+
return {
|
|
369
|
+
url,
|
|
370
|
+
method: "GET",
|
|
371
|
+
headers,
|
|
372
|
+
expiresAt: new Date(Date.now() + input.expiresIn * 1000),
|
|
373
|
+
conditions: { ifMatch: input.ifMatch, sizeEnforcement: "none" }
|
|
374
|
+
};
|
|
375
|
+
} catch (error) {
|
|
376
|
+
throw failure(error);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
return service;
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
export {
|
|
385
|
+
createS3StoragePlugin
|
|
386
|
+
};
|