mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-28 16:32:28 +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]>
94 lines
2.8 KiB
JavaScript
94 lines
2.8 KiB
JavaScript
function normalizeEndpoint(endpoint) {
|
|
const trimmed = String(endpoint || "").trim();
|
|
if (!trimmed) {
|
|
return "";
|
|
}
|
|
if (/^https?:\/\//i.test(trimmed)) {
|
|
return trimmed.replace(/\/+$/, "");
|
|
}
|
|
return `https://${trimmed.replace(/\/+$/, "")}`;
|
|
}
|
|
|
|
function normalizeBucket(bucket) {
|
|
return String(bucket || "").trim();
|
|
}
|
|
|
|
function normalizeKey(key) {
|
|
return String(key || "").replace(/^\/+/, "").trim();
|
|
}
|
|
|
|
// Backblaze B2's S3-compatible endpoint is region-scoped:
|
|
// region "us-west-004" -> https://s3.us-west-004.backblazeb2.com
|
|
// Unlike generic S3, the user supplies a region (not a pasted endpoint) and bitvid
|
|
// derives the endpoint. Returns "" for a missing/placeholder region so callers can
|
|
// surface a "region required" error. B2 buckets are addressed virtual-hosted-style,
|
|
// so the public URL then derives to https://<bucket>.s3.<region>.backblazeb2.com via
|
|
// buildS3PublicUrl({ forcePathStyle: false }).
|
|
export function deriveB2Endpoint(region) {
|
|
const normalizedRegion = String(region || "").trim().toLowerCase();
|
|
if (!normalizedRegion || normalizedRegion === "auto") {
|
|
return "";
|
|
}
|
|
// Defend against a user pasting a full endpoint/host into the region box.
|
|
if (normalizedRegion.includes("/") || normalizedRegion.includes(".")) {
|
|
return "";
|
|
}
|
|
return `https://s3.${normalizedRegion}.backblazeb2.com`;
|
|
}
|
|
|
|
export function normalizeS3PublicBaseUrl(publicBaseUrl) {
|
|
const trimmed = String(publicBaseUrl || "").trim();
|
|
if (!trimmed) {
|
|
return "";
|
|
}
|
|
return trimmed.replace(/\/+$/, "");
|
|
}
|
|
|
|
export function buildS3PublicUrl({
|
|
endpoint,
|
|
bucket,
|
|
key,
|
|
forcePathStyle,
|
|
} = {}) {
|
|
const normalizedEndpoint = normalizeEndpoint(endpoint);
|
|
const normalizedBucket = normalizeBucket(bucket);
|
|
const normalizedKey = normalizeKey(key);
|
|
|
|
if (!normalizedEndpoint || !normalizedBucket) {
|
|
return "";
|
|
}
|
|
|
|
const url = new URL(normalizedEndpoint);
|
|
url.hash = "";
|
|
url.search = "";
|
|
|
|
const basePath = url.pathname.replace(/\/+$/, "");
|
|
const prefix = basePath === "/" ? "" : basePath;
|
|
|
|
if (forcePathStyle) {
|
|
url.pathname = `${prefix}/${normalizedBucket}`;
|
|
} else if (!url.hostname.startsWith(`${normalizedBucket}.`)) {
|
|
url.hostname = `${normalizedBucket}.${url.hostname}`;
|
|
}
|
|
|
|
if (normalizedKey) {
|
|
const pathBase = url.pathname.replace(/\/+$/, "");
|
|
const pathPrefix = pathBase === "/" ? "" : pathBase;
|
|
url.pathname = `${pathPrefix}/${normalizedKey}`;
|
|
}
|
|
|
|
return url.toString().replace(/\/+$/, "");
|
|
}
|
|
|
|
export function buildS3UrlFromBase({ publicBaseUrl, key } = {}) {
|
|
const normalizedBase = normalizeS3PublicBaseUrl(publicBaseUrl);
|
|
const normalizedKey = normalizeKey(key);
|
|
if (!normalizedBase || !normalizedKey) {
|
|
return "";
|
|
}
|
|
const baseWithSlash = normalizedBase.endsWith("/")
|
|
? normalizedBase
|
|
: `${normalizedBase}/`;
|
|
return new URL(normalizedKey, baseWithSlash).toString();
|
|
}
|