Add generic S3 client loader and update tests

This commit is contained in:
thePR0M3TH3AN committed 2026-01-23 18:49:34 -05:00
1 parent f634b5a929
commit 9412afb1af
3 files changed
+197 -95

No files matched your search

+67 -3
View File
@@ -34,10 +34,74 @@ export const PROVIDERS = Object.freeze({
GENERIC: "generic_s3",
});
function resolveConnectionMeta(config) {
return config?.meta || {};
}
function resolveBucket(config) {
return config.bucket || resolveConnectionMeta(config).bucket;
}
function resolveRegion(config) {
return config.region || resolveConnectionMeta(config).region;
}
function resolveEndpoint(config) {
return config.endpoint || resolveConnectionMeta(config).endpoint;
}
function resolveR2Endpoint(config) {
const accountId =
config.accountId || resolveConnectionMeta(config).accountId || "";
if (!accountId) {
return resolveEndpoint(config);
}
return `https://${accountId}.r2.cloudflarestorage.com`;
}
function resolveForcePathStyle(config, fallback = false) {
const meta = resolveConnectionMeta(config);
if (typeof config.forcePathStyle === "boolean") {
return config.forcePathStyle;
}
if (typeof meta.forcePathStyle === "boolean") {
return meta.forcePathStyle;
}
return fallback;
}
function buildS3TestConfig(config, overrides = {}) {
return {
accessKeyId: config.accessKeyId,
secretAccessKey: config.secretAccessKey,
bucket: resolveBucket(config),
region: resolveRegion(config),
endpoint: resolveEndpoint(config),
forcePathStyle: resolveForcePathStyle(config),
...overrides,
};
}
const PROVIDER_TESTS = {
[PROVIDERS.R2]: testS3Connection,
[PROVIDERS.S3]: testS3Connection,
[PROVIDERS.GENERIC]: testS3Connection,
[PROVIDERS.R2]: (config) =>
testS3Connection(
buildS3TestConfig(config, {
endpoint: resolveR2Endpoint(config),
forcePathStyle: true,
})
),
[PROVIDERS.S3]: (config) =>
testS3Connection(
buildS3TestConfig(config, {
forcePathStyle: resolveForcePathStyle(config, false),
})
),
[PROVIDERS.GENERIC]: (config) =>
testS3Connection(
buildS3TestConfig(config, {
forcePathStyle: resolveForcePathStyle(config, true),
})
),
};
function hexToBytes(hex) {
+18 -92
View File
@@ -1,81 +1,28 @@
// js/storage/r2-s3.js
const AWS_SDK_SPECIFIER = "https://esm.sh/@aws-sdk/[email protected]?bundle";
const disableNetworkImports = Boolean(
globalThis?.__BITVID_DISABLE_NETWORK_IMPORTS__
);
let awsSdk = null;
let awsSdkLoadError = null;
let awsSdkLoadPromise = null;
function assignAwsSdk(module) {
if (!module) {
return;
}
awsSdk = {
S3Client: module.S3Client,
CreateBucketCommand: module.CreateBucketCommand,
CreateMultipartUploadCommand: module.CreateMultipartUploadCommand,
UploadPartCommand: module.UploadPartCommand,
CompleteMultipartUploadCommand: module.CompleteMultipartUploadCommand,
AbortMultipartUploadCommand: module.AbortMultipartUploadCommand,
ListBucketsCommand: module.ListBucketsCommand,
HeadBucketCommand: module.HeadBucketCommand,
PutBucketCorsCommand: module.PutBucketCorsCommand,
DeleteObjectCommand: module.DeleteObjectCommand,
};
}
async function loadAwsSdkModule() {
if (awsSdk || awsSdkLoadError) {
return awsSdk;
}
if (!awsSdkLoadPromise) {
if (disableNetworkImports) {
awsSdkLoadError = new Error(
"Cloudflare R2 SDK network import has been disabled."
);
return null;
}
awsSdkLoadPromise = import(AWS_SDK_SPECIFIER)
.then((module) => {
assignAwsSdk(module);
return awsSdk;
})
.catch((error) => {
awsSdkLoadError = error || new Error("Failed to load R2 SDK module");
throw awsSdkLoadError;
});
}
try {
return await awsSdkLoadPromise;
} catch (error) {
awsSdkLoadError = error;
throw error;
}
}
import {
ensureS3SdkLoaded,
getS3SdkLoadError,
isS3SdkAvailable,
makeS3Client,
requireAwsSdk,
} from "./s3-client.js";
export async function testS3Connection(config) {
if (!config) {
throw new Error("Configuration required for testing S3 connection.");
}
await ensureR2SdkLoaded();
await ensureS3SdkLoaded();
const { ListBucketsCommand, HeadBucketCommand } = requireAwsSdk();
const s3 = makeR2Client({
accountId: config.accountId,
accessKeyId: config.accessKeyId,
secretAccessKey: config.secretAccessKey,
const s3 = makeS3Client({
endpoint: config.endpoint,
region: config.region,
accessKeyId: config.accessKeyId,
secretAccessKey: config.secretAccessKey,
forcePathStyle: config.forcePathStyle,
});
// If bucket is specified, we check that specific bucket (permissions + existence).
@@ -108,37 +55,16 @@ export async function testS3Connection(config) {
}
}
function requireAwsSdk() {
if (awsSdk) {
return awsSdk;
}
if (awsSdkLoadError) {
throw awsSdkLoadError;
}
throw new Error(
"Cloudflare R2 SDK has not finished loading. Call ensureR2SdkLoaded() first."
);
}
export function ensureR2SdkLoaded() {
return loadAwsSdkModule();
return ensureS3SdkLoaded();
}
export function getR2SdkLoadError() {
return awsSdkLoadError;
return getS3SdkLoadError();
}
export function isR2SdkAvailable() {
return Boolean(awsSdk);
}
if (!disableNetworkImports) {
// Begin loading immediately in environments that support network imports.
loadAwsSdkModule().catch(() => {
// Errors are captured for later inspection by getR2SdkLoadError().
});
return isS3SdkAvailable();
}
function computeCacheControl(key) {
@@ -173,14 +99,14 @@ export function makeR2Client({
throw new Error("Missing S3/R2 credentials");
}
const { S3Client } = requireAwsSdk();
const finalEndpoint =
endpoint || `https://${accountId}.r2.cloudflarestorage.com`;
return new S3Client({
return makeS3Client({
region: region || "auto",
endpoint: finalEndpoint,
credentials: { accessKeyId, secretAccessKey },
accessKeyId,
secretAccessKey,
forcePathStyle: true,
});
}
+112
View File
@@ -0,0 +1,112 @@
// js/storage/s3-client.js
const AWS_SDK_SPECIFIER = "https://esm.sh/@aws-sdk/[email protected]?bundle";
const disableNetworkImports = Boolean(
globalThis?.__BITVID_DISABLE_NETWORK_IMPORTS__
);
let awsSdk = null;
let awsSdkLoadError = null;
let awsSdkLoadPromise = null;
function assignAwsSdk(module) {
if (!module) {
return;
}
awsSdk = {
S3Client: module.S3Client,
CreateBucketCommand: module.CreateBucketCommand,
CreateMultipartUploadCommand: module.CreateMultipartUploadCommand,
UploadPartCommand: module.UploadPartCommand,
CompleteMultipartUploadCommand: module.CompleteMultipartUploadCommand,
AbortMultipartUploadCommand: module.AbortMultipartUploadCommand,
ListBucketsCommand: module.ListBucketsCommand,
HeadBucketCommand: module.HeadBucketCommand,
PutBucketCorsCommand: module.PutBucketCorsCommand,
DeleteObjectCommand: module.DeleteObjectCommand,
};
}
async function loadAwsSdkModule() {
if (awsSdk || awsSdkLoadError) {
return awsSdk;
}
if (!awsSdkLoadPromise) {
if (disableNetworkImports) {
awsSdkLoadError = new Error("S3 SDK network import has been disabled.");
return null;
}
awsSdkLoadPromise = import(AWS_SDK_SPECIFIER)
.then((module) => {
assignAwsSdk(module);
return awsSdk;
})
.catch((error) => {
awsSdkLoadError = error || new Error("Failed to load S3 SDK module");
throw awsSdkLoadError;
});
}
try {
return await awsSdkLoadPromise;
} catch (error) {
awsSdkLoadError = error;
throw error;
}
}
export function requireAwsSdk() {
if (awsSdk) {
return awsSdk;
}
if (awsSdkLoadError) {
throw awsSdkLoadError;
}
throw new Error("S3 SDK has not finished loading. Call ensureS3SdkLoaded() first.");
}
export function ensureS3SdkLoaded() {
return loadAwsSdkModule();
}
export function getS3SdkLoadError() {
return awsSdkLoadError;
}
export function isS3SdkAvailable() {
return Boolean(awsSdk);
}
export function makeS3Client({
endpoint,
region,
accessKeyId,
secretAccessKey,
forcePathStyle,
}) {
if (!accessKeyId || !secretAccessKey) {
throw new Error("Missing S3 credentials");
}
const { S3Client } = requireAwsSdk();
return new S3Client({
region: region || "auto",
endpoint,
credentials: { accessKeyId, secretAccessKey },
forcePathStyle: Boolean(forcePathStyle),
});
}
if (!disableNetworkImports) {
// Begin loading immediately in environments that support network imports.
loadAwsSdkModule().catch(() => {
// Errors are captured for later inspection by getS3SdkLoadError().
});
}