mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
414 lines
12 KiB
JavaScript
414 lines
12 KiB
JavaScript
import { buildR2Key, buildPublicUrl } from "./r2.js";
|
|
import {
|
|
sanitizeBucketName,
|
|
ensureBucket,
|
|
putCors,
|
|
attachCustomDomainAndWait,
|
|
setManagedDomain,
|
|
} from "./storage/r2-mgmt.js";
|
|
import { makeR2Client, multipartUpload } from "./storage/r2-s3.js";
|
|
import { userLogger } from "./utils/logger.js";
|
|
|
|
const STORAGE_KEY = "bitvid:quickR2Settings";
|
|
const STATUS_CLASSNAMES = [
|
|
"text-status-neutral",
|
|
"text-status-info",
|
|
"text-status-success",
|
|
"text-status-danger",
|
|
"text-status-warning",
|
|
];
|
|
const STATUS_TONES = {
|
|
info: "text-status-info",
|
|
success: "text-status-success",
|
|
error: "text-status-danger",
|
|
warning: "text-status-warning",
|
|
};
|
|
|
|
function sanitizeDomain(value) {
|
|
if (!value) {
|
|
return "";
|
|
}
|
|
return String(value)
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/^https?:\/\//, "")
|
|
.replace(/\/.*$/, "");
|
|
}
|
|
|
|
function loadSavedSettings() {
|
|
try {
|
|
const raw = localStorage.getItem(STORAGE_KEY);
|
|
if (!raw) {
|
|
return null;
|
|
}
|
|
const parsed = JSON.parse(raw);
|
|
if (!parsed || typeof parsed !== "object") {
|
|
return null;
|
|
}
|
|
return parsed;
|
|
} catch (err) {
|
|
userLogger.warn("Failed to read quick R2 settings:", err);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function persistSettings(settings) {
|
|
try {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
|
|
} catch (err) {
|
|
userLogger.warn("Failed to store quick R2 settings:", err);
|
|
}
|
|
}
|
|
|
|
function clearSavedSettings() {
|
|
try {
|
|
localStorage.removeItem(STORAGE_KEY);
|
|
} catch (err) {
|
|
userLogger.warn("Failed to clear quick R2 settings:", err);
|
|
}
|
|
}
|
|
|
|
function setStatus(el, message, tone = "info") {
|
|
if (!el) {
|
|
return;
|
|
}
|
|
STATUS_CLASSNAMES.forEach((cls) => el.classList.remove(cls));
|
|
const className = STATUS_TONES[tone] || "text-status-neutral";
|
|
if (className) {
|
|
el.classList.add(className);
|
|
}
|
|
el.textContent = message || "";
|
|
}
|
|
|
|
function setUploadingState(elements, uploading) {
|
|
const disabled = Boolean(uploading);
|
|
elements.forEach((el) => {
|
|
if (el) {
|
|
el.disabled = disabled;
|
|
}
|
|
});
|
|
}
|
|
|
|
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://")) {
|
|
origins.add(origin.replace("http://", "https://"));
|
|
}
|
|
}
|
|
origins.add("http://localhost:8000");
|
|
origins.add("http://127.0.0.1:8000");
|
|
return Array.from(origins);
|
|
}
|
|
|
|
function buildQuickKey(title, file, npub) {
|
|
const now = new Date();
|
|
const datePart = now.toISOString().slice(0, 10);
|
|
const baseName = (title || file?.name || "video")
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9.-]+/g, "-")
|
|
.replace(/^-+|-+$/g, "");
|
|
const safeBase = baseName || "video";
|
|
const originalName = typeof file?.name === "string" ? file.name : "";
|
|
const extMatch = originalName.match(/\.([^.]+)$/);
|
|
const ext = extMatch ? extMatch[1].toLowerCase() : "mp4";
|
|
const sanitizedNpub = String(npub || "user").replace(/[^a-z0-9]/gi, "");
|
|
return `videos/${datePart}/${sanitizedNpub || "user"}-${safeBase}.${ext}`;
|
|
}
|
|
|
|
export function initQuickR2Upload(app) {
|
|
const section = document.getElementById("quickR2Section");
|
|
if (!section) {
|
|
return;
|
|
}
|
|
|
|
const elements = {
|
|
accountId: document.getElementById("quickR2AccountId"),
|
|
accessKeyId: document.getElementById("quickR2AccessKeyId"),
|
|
secretAccessKey: document.getElementById("quickR2SecretAccessKey"),
|
|
apiToken: document.getElementById("quickR2ApiToken"),
|
|
customDomain: document.getElementById("quickR2CustomDomain"),
|
|
zoneId: document.getElementById("quickR2ZoneId"),
|
|
remember: document.getElementById("quickR2Remember"),
|
|
allowManaged: document.getElementById("quickR2AllowManaged"),
|
|
file: document.getElementById("quickR2File"),
|
|
uploadButton: document.getElementById("quickR2UploadButton"),
|
|
status: document.getElementById("quickR2Status"),
|
|
hostedUrl: document.getElementById("uploadUrl"),
|
|
titleInput: document.getElementById("uploadTitle"),
|
|
form: document.getElementById("uploadForm"),
|
|
};
|
|
|
|
const saved = loadSavedSettings();
|
|
if (saved && saved.remember) {
|
|
if (elements.accountId) elements.accountId.value = saved.accountId || "";
|
|
if (elements.accessKeyId)
|
|
elements.accessKeyId.value = saved.accessKeyId || "";
|
|
if (elements.secretAccessKey)
|
|
elements.secretAccessKey.value = saved.secretAccessKey || "";
|
|
if (elements.apiToken) elements.apiToken.value = saved.apiToken || "";
|
|
if (elements.customDomain)
|
|
elements.customDomain.value = saved.customDomain || "";
|
|
if (elements.zoneId) elements.zoneId.value = saved.zoneId || "";
|
|
if (elements.allowManaged)
|
|
elements.allowManaged.checked = Boolean(saved.allowManaged ?? true);
|
|
if (elements.remember) elements.remember.checked = true;
|
|
}
|
|
|
|
function maybePersist() {
|
|
if (!elements.remember) {
|
|
return;
|
|
}
|
|
if (elements.remember.checked) {
|
|
persistSettings({
|
|
remember: true,
|
|
accountId: elements.accountId?.value?.trim() || "",
|
|
accessKeyId: elements.accessKeyId?.value?.trim() || "",
|
|
secretAccessKey: elements.secretAccessKey?.value?.trim() || "",
|
|
apiToken: elements.apiToken?.value?.trim() || "",
|
|
customDomain: elements.customDomain?.value?.trim() || "",
|
|
zoneId: elements.zoneId?.value?.trim() || "",
|
|
allowManaged: Boolean(elements.allowManaged?.checked ?? true),
|
|
});
|
|
} else {
|
|
clearSavedSettings();
|
|
}
|
|
}
|
|
|
|
elements.remember?.addEventListener("change", () => {
|
|
if (!elements.remember.checked) {
|
|
clearSavedSettings();
|
|
}
|
|
});
|
|
|
|
async function handleUpload() {
|
|
const file = elements.file?.files?.[0] || null;
|
|
if (!file) {
|
|
setStatus(elements.status, "Choose a file to upload.", "error");
|
|
return;
|
|
}
|
|
|
|
const accountId = elements.accountId?.value?.trim();
|
|
const accessKeyId = elements.accessKeyId?.value?.trim();
|
|
const secretAccessKey = elements.secretAccessKey?.value?.trim();
|
|
const apiToken = elements.apiToken?.value?.trim();
|
|
const customDomain = sanitizeDomain(
|
|
elements.customDomain?.value || ""
|
|
);
|
|
const zoneId = elements.zoneId?.value?.trim();
|
|
const allowManaged = Boolean(elements.allowManaged?.checked ?? true);
|
|
|
|
if (!accountId || !accessKeyId || !secretAccessKey) {
|
|
setStatus(
|
|
elements.status,
|
|
"Account ID and S3 keys are required for Cloudflare R2.",
|
|
"error"
|
|
);
|
|
return;
|
|
}
|
|
|
|
const npub =
|
|
typeof app?.safeEncodeNpub === "function" ? app.safeEncodeNpub(app.pubkey) : null;
|
|
if (!npub) {
|
|
setStatus(
|
|
elements.status,
|
|
"Unable to derive your npub. Connect a Nostr key first.",
|
|
"error"
|
|
);
|
|
return;
|
|
}
|
|
|
|
maybePersist();
|
|
|
|
const bucket = sanitizeBucketName(npub);
|
|
const inputsToDisable = [
|
|
elements.accountId,
|
|
elements.accessKeyId,
|
|
elements.secretAccessKey,
|
|
elements.apiToken,
|
|
elements.customDomain,
|
|
elements.zoneId,
|
|
elements.remember,
|
|
elements.allowManaged,
|
|
elements.file,
|
|
elements.uploadButton,
|
|
];
|
|
|
|
setUploadingState(inputsToDisable, true);
|
|
setStatus(elements.status, `Preparing bucket ${bucket}…`, "info");
|
|
|
|
try {
|
|
if (apiToken) {
|
|
try {
|
|
await ensureBucket({ accountId, bucket, token: apiToken });
|
|
} catch (err) {
|
|
throw new Error(
|
|
err?.message ? `Bucket setup failed: ${err.message}` : "Bucket setup failed."
|
|
);
|
|
}
|
|
|
|
try {
|
|
await putCors({
|
|
accountId,
|
|
bucket,
|
|
token: apiToken,
|
|
origins: getCorsOrigins(),
|
|
});
|
|
} catch (corsErr) {
|
|
userLogger.warn("Quick R2 CORS update failed:", corsErr);
|
|
}
|
|
}
|
|
|
|
let publicBaseUrl = "";
|
|
let usedManagedFallback = false;
|
|
|
|
if (customDomain) {
|
|
if (apiToken && zoneId) {
|
|
try {
|
|
setStatus(
|
|
elements.status,
|
|
`Attaching ${customDomain}…`,
|
|
"info"
|
|
);
|
|
const custom = await attachCustomDomainAndWait({
|
|
accountId,
|
|
bucket,
|
|
token: apiToken,
|
|
zoneId,
|
|
domain: customDomain,
|
|
pollInterval: 1500,
|
|
timeoutMs: 120000,
|
|
});
|
|
if (custom?.active && custom?.url) {
|
|
publicBaseUrl = custom.url;
|
|
try {
|
|
await setManagedDomain({
|
|
accountId,
|
|
bucket,
|
|
token: apiToken,
|
|
enabled: false,
|
|
});
|
|
} catch (disableErr) {
|
|
userLogger.warn("Quick R2 managed domain disable failed:", disableErr);
|
|
}
|
|
} else {
|
|
setStatus(
|
|
elements.status,
|
|
`Custom domain pending (${custom?.status || "unknown"}).`,
|
|
"warning"
|
|
);
|
|
}
|
|
} catch (err) {
|
|
userLogger.warn("Quick R2 custom domain error:", err);
|
|
setStatus(
|
|
elements.status,
|
|
err?.message
|
|
? `Custom domain setup failed: ${err.message}`
|
|
: "Custom domain setup failed.",
|
|
"warning"
|
|
);
|
|
}
|
|
}
|
|
|
|
if (!publicBaseUrl) {
|
|
publicBaseUrl = `https://${customDomain}`;
|
|
}
|
|
}
|
|
|
|
if (!publicBaseUrl && apiToken && allowManaged) {
|
|
try {
|
|
setStatus(elements.status, "Enabling managed r2.dev domain…", "info");
|
|
const managed = await setManagedDomain({
|
|
accountId,
|
|
bucket,
|
|
token: apiToken,
|
|
enabled: true,
|
|
});
|
|
if (managed?.url) {
|
|
publicBaseUrl = managed.url;
|
|
usedManagedFallback = true;
|
|
}
|
|
} catch (err) {
|
|
userLogger.warn("Quick R2 managed domain enable failed:", err);
|
|
setStatus(
|
|
elements.status,
|
|
err?.message
|
|
? `Managed domain failed: ${err.message}`
|
|
: "Managed domain failed.",
|
|
"warning"
|
|
);
|
|
}
|
|
}
|
|
|
|
if (!publicBaseUrl) {
|
|
throw new Error(
|
|
"No public domain configured. Provide a custom domain or allow the managed r2.dev fallback."
|
|
);
|
|
}
|
|
|
|
const s3 = makeR2Client({ accountId, accessKeyId, secretAccessKey });
|
|
const title = elements.titleInput?.value?.trim() || "";
|
|
const key = buildQuickKey(title, file, npub) || buildR2Key(npub, file);
|
|
const hostedUrl = buildPublicUrl(publicBaseUrl, key);
|
|
|
|
let lastPercent = -1;
|
|
setStatus(elements.status, `Uploading… 0%`, usedManagedFallback ? "warning" : "info");
|
|
|
|
await multipartUpload({
|
|
s3,
|
|
bucket,
|
|
key,
|
|
file,
|
|
contentType: file.type,
|
|
onProgress: (fraction) => {
|
|
if (typeof fraction !== "number" || !isFinite(fraction)) {
|
|
return;
|
|
}
|
|
const percent = Math.max(0, Math.min(100, Math.round(fraction * 100)));
|
|
if (percent !== lastPercent) {
|
|
lastPercent = percent;
|
|
setStatus(
|
|
elements.status,
|
|
`Uploading… ${percent}%`,
|
|
usedManagedFallback ? "warning" : "info"
|
|
);
|
|
}
|
|
},
|
|
});
|
|
|
|
if (elements.hostedUrl) {
|
|
elements.hostedUrl.value = hostedUrl;
|
|
}
|
|
|
|
setStatus(
|
|
elements.status,
|
|
`Upload complete. Hosted URL set to ${hostedUrl}.`,
|
|
"success"
|
|
);
|
|
|
|
if (elements.form && typeof elements.form.requestSubmit === "function") {
|
|
elements.form.requestSubmit();
|
|
}
|
|
} catch (err) {
|
|
userLogger.error("Quick R2 upload failed:", err);
|
|
setStatus(
|
|
elements.status,
|
|
err?.message ? `Upload failed: ${err.message}` : "Upload failed.",
|
|
"error"
|
|
);
|
|
} finally {
|
|
setUploadingState(inputsToDisable, false);
|
|
}
|
|
}
|
|
|
|
elements.uploadButton?.addEventListener("click", () => {
|
|
handleUpload();
|
|
});
|
|
}
|
|
|
|
export default initQuickR2Upload;
|