mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Migrate legacy R2 settings to storage service
This commit is contained in:
1 parent
758841180d
commit
1e2f6b989a
4 files changed
+123
-195
No files matched your search
+2
-2
@@ -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.
|
||||
|
||||
While the new **Storage Tab** (powered by `StorageService`) manages the secure storage of credentials, `r2Service` is responsible for:
|
||||
The **Storage Tab** (powered by `StorageService`) is the single supported configuration path for R2 credentials. `r2Service` is responsible for:
|
||||
|
||||
- Uploading the selected video/HLS asset via multipart S3 uploads.
|
||||
- Verifying public access via the configured Public Bucket URL.
|
||||
@@ -69,7 +69,7 @@ The app will attempt to upload a small test file to verify the credentials and p
|
||||
|
||||
## Configuration keys
|
||||
|
||||
R2 credentials are no longer stored in plain `bitvidSettings`. They are encapsulated within the `StorageService` data model.
|
||||
R2 credentials are no longer stored in plain `bitvidSettings`. They are encapsulated within the `StorageService` data model. Legacy `r2Settings` are migrated into `StorageService` on first unlock and then cleared.
|
||||
|
||||
**R2 Connection Profile (Encrypted):**
|
||||
- `provider`: `cloudflare_r2`
|
||||
|
||||
@@ -92,7 +92,12 @@ function openSettingsDb() {
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadR2Settings() {
|
||||
/**
|
||||
* Legacy R2 settings loader (deprecated).
|
||||
* Used only for one-time migration into StorageService.
|
||||
* @deprecated
|
||||
*/
|
||||
export async function loadLegacyR2Settings() {
|
||||
try {
|
||||
const db = await openSettingsDb();
|
||||
if (db) {
|
||||
@@ -126,36 +131,12 @@ export async function loadR2Settings() {
|
||||
return normalizeSettings(null);
|
||||
}
|
||||
|
||||
export async function saveR2Settings(settings) {
|
||||
const normalized = normalizeSettings(settings);
|
||||
|
||||
let db = null;
|
||||
try {
|
||||
db = await openSettingsDb();
|
||||
} catch (err) {
|
||||
userLogger.warn("Unable to open IndexedDB, continuing with fallback:", err);
|
||||
}
|
||||
|
||||
if (db) {
|
||||
await new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(STORE_NAME, "readwrite");
|
||||
const store = tx.objectStore(STORE_NAME);
|
||||
const req = store.put(normalized, SETTINGS_KEY);
|
||||
req.onsuccess = () => resolve();
|
||||
req.onerror = () =>
|
||||
reject(req.error || new Error("Failed to save R2 settings"));
|
||||
});
|
||||
} else if (typeof localStorage !== "undefined") {
|
||||
localStorage.setItem(
|
||||
LOCALSTORAGE_FALLBACK_KEY,
|
||||
JSON.stringify(normalized)
|
||||
);
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export async function clearR2Settings() {
|
||||
/**
|
||||
* Legacy R2 settings clearer (deprecated).
|
||||
* Used only after successful migration into StorageService.
|
||||
* @deprecated
|
||||
*/
|
||||
export async function clearLegacyR2Settings() {
|
||||
let cleared = false;
|
||||
try {
|
||||
const db = await openSettingsDb();
|
||||
|
||||
+28
-162
@@ -17,13 +17,8 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
loadR2Settings,
|
||||
saveR2Settings,
|
||||
clearR2Settings,
|
||||
buildR2Key,
|
||||
buildPublicUrl,
|
||||
mergeBucketEntry,
|
||||
sanitizeBaseDomain,
|
||||
} from "../r2.js";
|
||||
import {
|
||||
sanitizeBucketName,
|
||||
@@ -290,101 +285,24 @@ class R2Service {
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
try {
|
||||
const settings = await loadR2Settings();
|
||||
this.setSettings(settings);
|
||||
this.populateCloudflareSettingsInputs(settings);
|
||||
return settings;
|
||||
} catch (err) {
|
||||
userLogger.error("Failed to load Cloudflare settings:", err);
|
||||
this.setSettings(createDefaultSettings());
|
||||
this.populateCloudflareSettingsInputs(this.getSettings());
|
||||
this.setCloudflareSettingsStatus(
|
||||
"Failed to load saved settings.",
|
||||
"error"
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
const settings = createDefaultSettings();
|
||||
this.setSettings(settings);
|
||||
this.populateCloudflareSettingsInputs(settings);
|
||||
this.setCloudflareSettingsStatus(
|
||||
"Legacy Cloudflare settings have been retired. Use the Storage tab.",
|
||||
"warning"
|
||||
);
|
||||
return settings;
|
||||
}
|
||||
|
||||
async handleCloudflareSettingsSubmit(formValues = {}, { quiet = false } = {}) {
|
||||
// Legacy support wrapper: redirects to storageService if possible, or warns.
|
||||
// For now, we allow saving to legacy for backward compatibility if explicit flow isn't used,
|
||||
// but we prefer to use storageService.
|
||||
|
||||
const accountId = String(formValues.accountId || "").trim();
|
||||
const accessKeyId = String(formValues.accessKeyId || "").trim();
|
||||
const secretAccessKey = String(formValues.secretAccessKey || "").trim();
|
||||
const baseDomain = sanitizeBaseDomain(formValues.baseDomain || ""); // This is the Public Bucket URL
|
||||
|
||||
if (baseDomain.includes(".r2.cloudflarestorage.com")) {
|
||||
if (!quiet) {
|
||||
this.setCloudflareSettingsStatus(
|
||||
"It looks like you entered the S3 API URL. Please use your Public Bucket URL (e.g. https://pub-xxx.r2.dev or your custom domain).",
|
||||
"error"
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!accountId || !accessKeyId || !secretAccessKey) {
|
||||
if (!quiet) {
|
||||
this.setCloudflareSettingsStatus(
|
||||
"Account ID, Access Key ID, and Secret are required.",
|
||||
"error"
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!baseDomain) {
|
||||
if (!quiet) {
|
||||
this.setCloudflareSettingsStatus(
|
||||
"Public Bucket URL is required (e.g., https://pub-xxx.r2.dev).",
|
||||
"error"
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to save to StorageService if available and unlocked
|
||||
// We can't easily know the active pubkey here unless passed or inferred from context.
|
||||
// Since this method is legacy, we'll default to legacy behavior but warn.
|
||||
|
||||
let buckets = { ...(this.getSettings().buckets || {}) };
|
||||
const previousAccount = this.getSettings().accountId || "";
|
||||
const previousBaseDomain = this.getSettings().baseDomain || "";
|
||||
|
||||
if (previousAccount !== accountId || previousBaseDomain !== baseDomain) {
|
||||
buckets = {};
|
||||
}
|
||||
|
||||
const updatedSettings = {
|
||||
accountId,
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
baseDomain,
|
||||
buckets,
|
||||
};
|
||||
|
||||
try {
|
||||
const saved = await saveR2Settings(updatedSettings);
|
||||
this.setSettings(saved);
|
||||
this.populateCloudflareSettingsInputs(saved);
|
||||
if (!quiet) {
|
||||
this.setCloudflareSettingsStatus("Settings saved locally.", "success");
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
userLogger.error("Failed to save Cloudflare settings:", err);
|
||||
if (!quiet) {
|
||||
this.setCloudflareSettingsStatus(
|
||||
"Failed to save settings. Check console for details.",
|
||||
"error"
|
||||
);
|
||||
}
|
||||
return false;
|
||||
if (!quiet) {
|
||||
this.setCloudflareSettingsStatus(
|
||||
"Legacy settings are disabled. Configure Cloudflare R2 in the Storage tab.",
|
||||
"error"
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async saveSettings(formValues = {}, options = {}) {
|
||||
@@ -462,33 +380,17 @@ class R2Service {
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fallback to legacy
|
||||
const legacy = this.cloudflareSettings;
|
||||
if (
|
||||
legacy &&
|
||||
legacy.accountId &&
|
||||
legacy.accessKeyId &&
|
||||
legacy.secretAccessKey
|
||||
) {
|
||||
return { ...legacy, isLegacy: true };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async handleCloudflareClearSettings() {
|
||||
try {
|
||||
await clearR2Settings();
|
||||
const refreshed = await loadR2Settings();
|
||||
this.setSettings(refreshed);
|
||||
this.populateCloudflareSettingsInputs(refreshed);
|
||||
this.setCloudflareSettingsStatus("Settings cleared.", "success");
|
||||
return true;
|
||||
} catch (err) {
|
||||
userLogger.error("Failed to clear Cloudflare settings:", err);
|
||||
this.setCloudflareSettingsStatus("Failed to clear settings.", "error");
|
||||
return false;
|
||||
}
|
||||
this.setSettings(createDefaultSettings());
|
||||
this.populateCloudflareSettingsInputs(this.getSettings());
|
||||
this.setCloudflareSettingsStatus(
|
||||
"Legacy settings are no longer supported. Clear settings in the Storage tab.",
|
||||
"warning"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
async clearSettings() {
|
||||
@@ -607,35 +509,6 @@ class R2Service {
|
||||
lastUpdated: Date.now(),
|
||||
};
|
||||
|
||||
// If using legacy settings, we save the bucket mapping.
|
||||
// If using StorageService (settings.isLegacy === false), we do NOT save to legacy store
|
||||
// to avoid partial state or overwriting. We just return the manual entry.
|
||||
if (settings.isLegacy !== false) {
|
||||
const currentSettings = this.getSettings();
|
||||
if (!this.cloudflareSettings) {
|
||||
this.cloudflareSettings = currentSettings;
|
||||
}
|
||||
let entry = currentSettings.buckets?.[npub];
|
||||
let savedEntry = entry;
|
||||
|
||||
if (
|
||||
!entry ||
|
||||
entry.bucket !== manualEntry.bucket ||
|
||||
entry.publicBaseUrl !== manualEntry.publicBaseUrl
|
||||
) {
|
||||
const updatedSettings = await saveR2Settings(
|
||||
mergeBucketEntry(currentSettings, npub, manualEntry)
|
||||
);
|
||||
this.setSettings(updatedSettings);
|
||||
savedEntry = updatedSettings.buckets?.[npub] || manualEntry;
|
||||
}
|
||||
return {
|
||||
entry: savedEntry,
|
||||
usedManagedFallback: false,
|
||||
customDomainStatus: "manual",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
entry: manualEntry,
|
||||
usedManagedFallback: false,
|
||||
@@ -797,7 +670,7 @@ class R2Service {
|
||||
bucket: effectiveSettings.bucket || "",
|
||||
baseDomain: effectiveSettings.baseDomain || "",
|
||||
publicBaseUrl: effectiveSettings.publicBaseUrl || "",
|
||||
isLegacy: effectiveSettings.isLegacy !== false,
|
||||
legacyFallback: Boolean(effectiveSettings.isLegacy),
|
||||
});
|
||||
|
||||
if (!accountId || !accessKeyId || !secretAccessKey) {
|
||||
@@ -858,19 +731,12 @@ class R2Service {
|
||||
forcedTorrentKey = "",
|
||||
forcedTorrentUrl = "",
|
||||
} = {}) {
|
||||
// If no explicit credentials, we might save settingsInput to legacy DB.
|
||||
// If explicitCredentials ARE provided, we skip saving and use them directly.
|
||||
if (!explicitCredentials && settingsInput) {
|
||||
const saved = await this.handleCloudflareSettingsSubmit(settingsInput, {
|
||||
quiet: true,
|
||||
});
|
||||
if (!saved) {
|
||||
this.setCloudflareUploadStatus(
|
||||
"Fix your R2 settings before uploading.",
|
||||
"error"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (settingsInput) {
|
||||
this.setCloudflareUploadStatus(
|
||||
"Legacy Cloudflare settings are disabled. Configure R2 in the Storage tab.",
|
||||
"error"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!npub) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { userLogger } from "../utils/logger.js";
|
||||
import { clearLegacyR2Settings, loadLegacyR2Settings } from "../r2.js";
|
||||
import { bytesToHex } from "../../vendor/crypto-helpers.bundle.min.js";
|
||||
import { testS3Connection } from "../storage/r2-s3.js";
|
||||
|
||||
@@ -109,6 +110,18 @@ function buildS3TestConfig(config, overrides = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function encodeNpub(pubkey) {
|
||||
if (!pubkey) return null;
|
||||
try {
|
||||
if (typeof window !== "undefined" && window.NostrTools?.nip19?.npubEncode) {
|
||||
return window.NostrTools.nip19.npubEncode(pubkey);
|
||||
}
|
||||
} catch (err) {
|
||||
userLogger.warn("[StorageService] Failed to encode npub for migration:", err);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const PROVIDER_TESTS = {
|
||||
[PROVIDERS.R2]: (config) =>
|
||||
testS3Connection(
|
||||
@@ -404,6 +417,8 @@ export class StorageService {
|
||||
|
||||
this.masterKeys.set(pubkey, masterKey);
|
||||
userLogger.log(`[StorageService] Unlocked storage for ${pubkey.slice(0, 8)}...`);
|
||||
|
||||
await this._migrateLegacyR2Settings(pubkey);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -587,6 +602,72 @@ export class StorageService {
|
||||
|
||||
return this.testAccess(connection.provider, connection);
|
||||
}
|
||||
|
||||
async _migrateLegacyR2Settings(pubkey) {
|
||||
let legacySettings = null;
|
||||
try {
|
||||
legacySettings = await loadLegacyR2Settings();
|
||||
} catch (err) {
|
||||
userLogger.warn("[StorageService] Legacy R2 settings load failed:", err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!legacySettings ||
|
||||
!legacySettings.accountId ||
|
||||
!legacySettings.accessKeyId ||
|
||||
!legacySettings.secretAccessKey ||
|
||||
!legacySettings.baseDomain
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const existingConnections = await this.listConnections(pubkey);
|
||||
const hasR2 = existingConnections.some(
|
||||
(connection) => connection.provider === PROVIDERS.R2
|
||||
);
|
||||
if (hasR2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const npub = encodeNpub(pubkey);
|
||||
const legacyEntry =
|
||||
npub && legacySettings.buckets ? legacySettings.buckets[npub] : null;
|
||||
const publicBaseUrl =
|
||||
legacyEntry?.publicBaseUrl || legacySettings.baseDomain || "";
|
||||
|
||||
const payload = {
|
||||
provider: PROVIDERS.R2,
|
||||
accountId: legacySettings.accountId,
|
||||
accessKeyId: legacySettings.accessKeyId,
|
||||
secretAccessKey: legacySettings.secretAccessKey,
|
||||
bucket: legacyEntry?.bucket || "",
|
||||
};
|
||||
const meta = {
|
||||
provider: PROVIDERS.R2,
|
||||
label: "Cloudflare R2 (Migrated)",
|
||||
publicBaseUrl,
|
||||
baseDomain: publicBaseUrl,
|
||||
defaultForUploads: existingConnections.length === 0,
|
||||
migratedFromLegacy: true,
|
||||
};
|
||||
|
||||
const baseId = "cloudflare-r2-migrated";
|
||||
let connectionId = baseId;
|
||||
let suffix = 1;
|
||||
while (existingConnections.some((conn) => conn.id === connectionId)) {
|
||||
connectionId = `${baseId}-${suffix}`;
|
||||
suffix += 1;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.saveConnection(pubkey, connectionId, payload, meta);
|
||||
await clearLegacyR2Settings();
|
||||
userLogger.log("[StorageService] Migrated legacy R2 settings.");
|
||||
} catch (err) {
|
||||
userLogger.warn("[StorageService] Legacy R2 migration failed:", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const storageService = new StorageService();
|
||||
|
||||
Reference in new issue
Block a user