mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-01 10:22:25 +00:00
Merge pull request #1706 from PR0M3TH3AN/codex/remove-or-gate-quickr2section-feature
Remove deprecated quick R2 upload path
This commit is contained in:
3 files changed
+5
-426
No files matched your search
+1
-1
@@ -6,7 +6,7 @@
|
||||
|
||||
> **Security Note:** This integration requires storing encrypted credentials in the browser. See [`README.md`](../README.md) and [`docs/storage-tab-design.md`](./storage-tab-design.md) for security warnings and alternative upload modes for untrusted environments.
|
||||
|
||||
The **Storage Tab** (powered by `StorageService`) is the single supported configuration path for R2 credentials. `r2Service` is responsible for:
|
||||
The **Storage Tab** (powered by `StorageService`) is the single supported configuration path for R2 credentials. The deprecated quick-upload panel (`quickR2Section` / `js/r2-quick.js`) has been removed to avoid conflicting credential sources. `r2Service` is responsible for:
|
||||
|
||||
- Uploading the selected video/HLS asset via multipart S3 uploads.
|
||||
- Verifying public access via the configured Public Bucket URL.
|
||||
|
||||
@@ -64,7 +64,6 @@ import getAuthProvider, {
|
||||
import hashtagPreferences, {
|
||||
HASHTAG_PREFERENCES_EVENTS,
|
||||
} from "./services/hashtagPreferencesService.js";
|
||||
import { initQuickR2Upload } from "./r2-quick.js";
|
||||
import { getSidebarLoadingMarkup } from "./sidebarLoading.js";
|
||||
import { subscriptions } from "./subscriptions.js";
|
||||
import {
|
||||
@@ -655,15 +654,10 @@ class Application {
|
||||
}
|
||||
});
|
||||
|
||||
const uploadModalPromise = this.uploadModal
|
||||
.load()
|
||||
.catch((error) => {
|
||||
devLogger.error("initUploadModal failed:", error);
|
||||
this.showError(`Failed to initialize upload modal: ${error.message}`);
|
||||
})
|
||||
.finally(() => {
|
||||
initQuickR2Upload(this);
|
||||
});
|
||||
const uploadModalPromise = this.uploadModal.load().catch((error) => {
|
||||
devLogger.error("initUploadModal failed:", error);
|
||||
this.showError(`Failed to initialize upload modal: ${error.message}`);
|
||||
});
|
||||
|
||||
const editModalPromise = this.editModal.load().catch((error) => {
|
||||
devLogger.error("Failed to load edit modal:", error);
|
||||
|
||||
-415
@@ -1,415 +0,0 @@
|
||||
import { buildR2Key, buildPublicUrl } from "./r2.js";
|
||||
import {
|
||||
sanitizeBucketName,
|
||||
ensureBucket,
|
||||
putCors,
|
||||
attachCustomDomainAndWait,
|
||||
setManagedDomain,
|
||||
} from "./storage/r2-mgmt.js";
|
||||
import { makeR2Client } from "./storage/r2-s3.js";
|
||||
import { multipartUpload } from "./storage/s3-multipart.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,
|
||||
createBucketIfMissing: true,
|
||||
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;
|
||||
Reference in new issue
Block a user