mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
B2 doesn't map onto the generic-S3 "paste an endpoint" model: its S3-compatible endpoint is region-scoped (s3.<region>.backblazeb2.com) and a public bucket is addressed virtual-hosted-style, so the public download URL is https://<bucket>.s3.<region>.backblazeb2.com/<key> — not derivable from a pasted generic endpoint. So B2 silently failed under "Custom S3". - s3-url: deriveB2Endpoint(region) -> https://s3.<region>.backblazeb2.com (empty for auto/blank/pasted-host so callers can require a real region); re-exported from s3Service. - storageService: PROVIDERS.B2 + resolveB2Endpoint (explicit endpoint wins, else derive) + PROVIDER_TESTS[B2] (derived endpoint, forcePathStyle false). - Public URL = S3-style virtual-hosted, auto-derived; an explicit Public Access URL (custom domain/CDN) overrides it. The save path now forwards the user's public URL to prepareS3Connection (also fixes generic-S3 ignoring a custom CDN URL when blank -> still derives, so no regression). - UI: "Backblaze B2" provider option + helper; raw Endpoint hidden for B2 (region drives it), region shows a us-west-004 example, force-path-style toggle hidden; save/test derive endpoint from region with a clear "enter your region" error. UploadModal shows B2-specific labels. - Tests: tests/storage-b2-provider.test.mjs (8). All s3/storage suites green (50); build + lint clean. Co-Authored-By: Claude Opus 4.8 <[email protected]>
189 lines
4.6 KiB
JavaScript
189 lines
4.6 KiB
JavaScript
import { ensureS3SdkLoaded, makeS3Client } from "../storage/s3-client.js";
|
|
import { ensureBucketCors, ensureBucketExists } from "../storage/s3-multipart.js";
|
|
import {
|
|
buildS3PublicUrl,
|
|
buildS3UrlFromBase,
|
|
deriveB2Endpoint,
|
|
normalizeS3PublicBaseUrl,
|
|
} from "../storage/s3-url.js";
|
|
|
|
export { deriveB2Endpoint };
|
|
import { userLogger } from "../utils/logger.js";
|
|
|
|
function normalizeEndpoint(endpoint) {
|
|
const trimmed = String(endpoint || "").trim();
|
|
if (!trimmed) {
|
|
return "";
|
|
}
|
|
if (/^https?:\/\//i.test(trimmed)) {
|
|
return trimmed.replace(/\/+$/, "");
|
|
}
|
|
return `https://${trimmed.replace(/\/+$/, "")}`;
|
|
}
|
|
|
|
function normalizeRegion(region) {
|
|
const trimmed = String(region || "").trim();
|
|
return trimmed || "auto";
|
|
}
|
|
|
|
function normalizeBucket(bucket) {
|
|
return String(bucket || "").trim();
|
|
}
|
|
|
|
export function derivePublicBaseUrl({
|
|
endpoint,
|
|
bucket,
|
|
forcePathStyle,
|
|
} = {}) {
|
|
return buildS3PublicUrl({ endpoint, bucket, forcePathStyle });
|
|
}
|
|
|
|
export function buildS3ObjectUrl({
|
|
publicBaseUrl,
|
|
endpoint,
|
|
bucket,
|
|
key,
|
|
forcePathStyle,
|
|
} = {}) {
|
|
const normalizedBase = normalizeS3PublicBaseUrl(publicBaseUrl);
|
|
if (!key) {
|
|
return "";
|
|
}
|
|
if (normalizedBase) {
|
|
return buildS3UrlFromBase({ publicBaseUrl: normalizedBase, key });
|
|
}
|
|
return buildS3PublicUrl({ endpoint, bucket, key, forcePathStyle });
|
|
}
|
|
|
|
export function validateS3Connection({
|
|
endpoint,
|
|
region,
|
|
accessKeyId,
|
|
secretAccessKey,
|
|
bucket,
|
|
forcePathStyle,
|
|
publicBaseUrl,
|
|
} = {}) {
|
|
const normalizedEndpoint = normalizeEndpoint(endpoint);
|
|
const normalizedBucket = normalizeBucket(bucket);
|
|
const normalizedRegion = normalizeRegion(region);
|
|
const normalizedAccessKeyId = String(accessKeyId || "").trim();
|
|
const normalizedSecretAccessKey = String(secretAccessKey || "").trim();
|
|
const normalizedPublicBaseUrl = normalizeS3PublicBaseUrl(publicBaseUrl);
|
|
|
|
if (!normalizedEndpoint) {
|
|
throw new Error("S3 endpoint is required.");
|
|
}
|
|
if (!normalizedRegion) {
|
|
throw new Error("S3 region is required.");
|
|
}
|
|
if (!normalizedAccessKeyId) {
|
|
throw new Error("S3 access key ID is required.");
|
|
}
|
|
if (!normalizedSecretAccessKey) {
|
|
throw new Error("S3 secret access key is required.");
|
|
}
|
|
if (!normalizedBucket) {
|
|
throw new Error("S3 bucket name is required.");
|
|
}
|
|
if (typeof forcePathStyle !== "boolean") {
|
|
throw new Error("S3 forcePathStyle setting must be true or false.");
|
|
}
|
|
|
|
const resolvedPublicBaseUrl =
|
|
normalizedPublicBaseUrl ||
|
|
derivePublicBaseUrl({
|
|
endpoint: normalizedEndpoint,
|
|
bucket: normalizedBucket,
|
|
forcePathStyle,
|
|
});
|
|
|
|
if (!resolvedPublicBaseUrl) {
|
|
throw new Error("Unable to derive a public base URL for the S3 bucket.");
|
|
}
|
|
|
|
return {
|
|
endpoint: normalizedEndpoint,
|
|
region: normalizedRegion,
|
|
accessKeyId: normalizedAccessKeyId,
|
|
secretAccessKey: normalizedSecretAccessKey,
|
|
bucket: normalizedBucket,
|
|
forcePathStyle,
|
|
publicBaseUrl: resolvedPublicBaseUrl,
|
|
};
|
|
}
|
|
|
|
export function getCorsOrigins() {
|
|
const origins = new Set();
|
|
if (typeof window !== "undefined" && window.location) {
|
|
const origin = window.location.origin;
|
|
if (origin && origin !== "null") {
|
|
origins.add(origin);
|
|
}
|
|
if (origin && origin.startsWith("http://localhost")) {
|
|
origins.add(origin.replace("http://", "https://"));
|
|
}
|
|
}
|
|
return Array.from(origins);
|
|
}
|
|
|
|
export async function prepareS3Connection({
|
|
endpoint,
|
|
region,
|
|
accessKeyId,
|
|
secretAccessKey,
|
|
bucket,
|
|
forcePathStyle,
|
|
origins,
|
|
publicBaseUrl,
|
|
createBucketIfMissing = true,
|
|
} = {}) {
|
|
const normalized = validateS3Connection({
|
|
endpoint,
|
|
region,
|
|
accessKeyId,
|
|
secretAccessKey,
|
|
bucket,
|
|
forcePathStyle,
|
|
publicBaseUrl,
|
|
});
|
|
|
|
await ensureS3SdkLoaded();
|
|
|
|
const s3 = makeS3Client({
|
|
endpoint: normalized.endpoint,
|
|
region: normalized.region,
|
|
accessKeyId: normalized.accessKeyId,
|
|
secretAccessKey: normalized.secretAccessKey,
|
|
forcePathStyle: normalized.forcePathStyle,
|
|
});
|
|
|
|
if (createBucketIfMissing) {
|
|
try {
|
|
await ensureBucketExists({
|
|
s3,
|
|
bucket: normalized.bucket,
|
|
region: normalized.region,
|
|
});
|
|
} catch (error) {
|
|
userLogger.warn("Failed to ensure S3 bucket exists:", error);
|
|
}
|
|
}
|
|
|
|
const allowedOrigins = Array.isArray(origins) ? origins : [];
|
|
if (allowedOrigins.length > 0) {
|
|
try {
|
|
await ensureBucketCors({
|
|
s3,
|
|
bucket: normalized.bucket,
|
|
origins: allowedOrigins,
|
|
region: normalized.region,
|
|
});
|
|
} catch (error) {
|
|
userLogger.warn("Failed to ensure S3 bucket CORS:", error);
|
|
}
|
|
}
|
|
|
|
return normalized;
|
|
}
|