diff --git a/public/app.js b/public/app.js index fa4851b..494c08b 100644 --- a/public/app.js +++ b/public/app.js @@ -3175,7 +3175,7 @@ function renderGdriveConnectionState(cfg) { elements.btnGdriveRefreshFolders.disabled = !isConnected; } - if (isConnected && Date.now() - lastGdriveFoldersFetchAt > 60 * 1000) { + if (isConnected && !cfg.gdriveFolderId && Date.now() - lastGdriveFoldersFetchAt > 60 * 1000) { loadGoogleDriveFolders(false, cfg.gdriveFolderId, cfg.gdriveFolderName); } } @@ -3986,7 +3986,8 @@ async function handleSyncCloudBackups(silent = false) { } try { - const res = await fetch('/api/admin/backups/cloud-files', { + const query = silent ? '' : '?scan=1'; + const res = await fetch(`/api/admin/backups/cloud-files${query}`, { headers: getAuthHeaders() }); const data = await res.json(); @@ -4024,12 +4025,13 @@ async function loadLiveCloudBackupsForModal(showToast = false) { if (!elements.cloudRestoreFilesContainer) return; elements.cloudRestoreFilesContainer.innerHTML = `
-

Scanning your Google Drive backup folder & Wasabi S3 bucket for available backups...

+

Loading available cloud backups...

`; try { - const res = await fetch('/api/admin/backups/cloud-files', { + const query = showToast ? '?scan=1' : ''; + const res = await fetch(`/api/admin/backups/cloud-files${query}`, { headers: getAuthHeaders() }); const data = await res.json(); diff --git a/server.js b/server.js index 4c26977..b9a767e 100644 --- a/server.js +++ b/server.js @@ -187,6 +187,9 @@ try { try { db.prepare("ALTER TABLE backup_config ADD COLUMN gdriveFolderName TEXT NOT NULL DEFAULT 'ForkFolio Backups'").run(); } catch (e) {} +try { + db.prepare("ALTER TABLE backup_history ADD COLUMN userSummary TEXT NOT NULL DEFAULT '[]'").run(); +} catch (e) {} // Initialize single settings record if empty const settingsCount = db.prepare('SELECT COUNT(*) as count FROM settings').get().count; @@ -2270,12 +2273,18 @@ async function createJsonArchiveData(embedRemoteImages = true) { }; const buffer = Buffer.from(JSON.stringify(archiveObj, null, 2), 'utf8'); + const userSummary = inspectBackupUsersFromData({ + users, + recipes: enrichedRecipes, + shoppingLists + }).users; return { buffer, userCount: users.length, recipeCount: enrichedRecipes.length, imageCount, - listCount: shoppingLists.length + listCount: shoppingLists.length, + userSummary }; } @@ -2511,12 +2520,14 @@ function sleepMs(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } -async function gdriveFetch(url, options = {}, maxRetries = 4) { +async function gdriveFetch(url, options = {}, maxRetries = 8) { const runRequest = async () => { let lastRes = null; + const backoffSchedule = [1200, 2200, 3500, 5000, 6800, 8500, 10500, 12500]; for (let attempt = 0; attempt <= maxRetries; attempt++) { if (attempt > 0) { - const delay = Math.min(800 * Math.pow(1.8, attempt - 1) + Math.floor(Math.random() * 250), 6000); + const baseDelay = backoffSchedule[attempt - 1] || 10000; + const delay = baseDelay + Math.floor(Math.random() * 400); await sleepMs(delay); } lastRes = await fetch(url, options); @@ -2529,7 +2540,7 @@ async function gdriveFetch(url, options = {}, maxRetries = 4) { } if (lastRes.status === 403 && attempt < maxRetries) { const clonedText = await lastRes.clone().text().catch(() => ''); - if (/rateLimit|quota|Queries per|userRateLimit/i.test(clonedText)) { + if (/rateLimit|quota|Queries|Requests per minute|userRateLimit/i.test(clonedText)) { continue; } } @@ -2539,7 +2550,7 @@ async function gdriveFetch(url, options = {}, maxRetries = 4) { }; const next = gdriveRequestQueue.then(async () => { - await sleepMs(120); + await sleepMs(150); return runRequest(); }); gdriveRequestQueue = next.catch(() => {}); @@ -2843,8 +2854,8 @@ async function executeCloudBackup(triggerType = 'manual') { db.prepare(` INSERT INTO backup_history ( id, label, triggerType, sizeBytes, userCount, recipeCount, imageCount, listCount, - s3Objects, gdriveFiles, status, errorDetails, createdAt - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'success', NULL, ?) + s3Objects, gdriveFiles, userSummary, status, errorDetails, createdAt + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'success', NULL, ?) `).run( backupId, label, @@ -2856,9 +2867,29 @@ async function executeCloudBackup(triggerType = 'manual') { jsonArchive.listCount, JSON.stringify(s3Objects), JSON.stringify(gdriveFiles), + JSON.stringify(jsonArchive.userSummary || []), createdAt ); + if (dbBuffer) { + rememberBackupBufferInMemory(`history:${backupId}:db`, dbBuffer, `${label}.db`); + for (const f of gdriveFiles.filter(x => x.format === 'db')) { + rememberBackupBufferInMemory(`gdrive:${f.id}`, dbBuffer, f.name || `${label}.db`); + } + for (const o of s3Objects.filter(x => x.format === 'db')) { + rememberBackupBufferInMemory(`s3:${o.key}`, dbBuffer, o.name || `${label}.db`); + } + } + if (jsonBuffer) { + rememberBackupBufferInMemory(`history:${backupId}:json`, jsonBuffer, `${label}.json`); + for (const f of gdriveFiles.filter(x => x.format === 'json')) { + rememberBackupBufferInMemory(`gdrive:${f.id}`, jsonBuffer, f.name || `${label}.json`); + } + for (const o of s3Objects.filter(x => x.format === 'json')) { + rememberBackupBufferInMemory(`s3:${o.key}`, jsonBuffer, o.name || `${label}.json`); + } + } + db.prepare(` UPDATE backup_config SET lastBackupAt = ?, lastBackupStatus = 'success', lastBackupMessage = ? @@ -3835,7 +3866,8 @@ app.get('/api/admin/backups/download-local', requireAuth, async (req, res) => { }); // Discover all remote backup files directly from Google Drive and Wasabi S3 and sync with backup_history -async function discoverAndSyncCloudBackups() { +async function discoverAndSyncCloudBackups(options = {}) { + const forceLiveScan = Boolean(options.liveScan); const cfg = getBackupConfig(); const snapshotMap = new Map(); @@ -3863,99 +3895,105 @@ async function discoverAndSyncCloudBackups() { } const providerErrors = []; + const hasKnownCloudSnapshots = Array.from(snapshotMap.values()).some( + s => (s.gdriveFiles && s.gdriveFiles.length > 0) || (s.s3Objects && s.s3Objects.length > 0) + ); - // 1. Live scan Google Drive backup folder if connected - if (cfg.gdriveConnected) { - try { - const accessToken = await getGoogleDriveAccessToken(cfg); - const driveFiles = await gdriveListBackupFiles(accessToken, cfg.gdriveFolderId || ''); + // Only query Google Drive / S3 APIs when explicitly requested ("Scan Cloud Now" / "Sync from Cloud") or when local history is empty + if (forceLiveScan || !hasKnownCloudSnapshots) { + // 1. Live scan Google Drive backup folder if connected + if (cfg.gdriveConnected) { + try { + const accessToken = await getGoogleDriveAccessToken(cfg); + const driveFiles = await gdriveListBackupFiles(accessToken, cfg.gdriveFolderId || ''); - for (const df of driveFiles) { - const baseLabel = df.name.replace(/\.(db|sqlite|json)$/i, ''); - let entry = snapshotMap.get(baseLabel); - if (!entry) { - entry = { - id: 'cld_gd_' + df.id.slice(0, 12), - label: baseLabel, - triggerType: 'cloud', - createdAt: df.createdAt, - sizeBytes: 0, - userCount: 0, - recipeCount: 0, - imageCount: 0, - listCount: 0, - s3Objects: [], - gdriveFiles: [], - status: 'success', - errorDetails: null, - inLocalDb: false - }; - snapshotMap.set(baseLabel, entry); - } - const alreadyLinked = entry.gdriveFiles.some(g => g.id === df.id || g.format === df.format); - if (!alreadyLinked) { - entry.gdriveFiles.push({ - id: df.id, - name: df.name, - format: df.format, - size: df.size - }); - if (!entry.inLocalDb) { - entry.sizeBytes += df.size || 0; + for (const df of driveFiles) { + const baseLabel = df.name.replace(/\.(db|sqlite|json)$/i, ''); + let entry = snapshotMap.get(baseLabel); + if (!entry) { + entry = { + id: 'cld_gd_' + df.id.slice(0, 12), + label: baseLabel, + triggerType: 'cloud', + createdAt: df.createdAt, + sizeBytes: 0, + userCount: 0, + recipeCount: 0, + imageCount: 0, + listCount: 0, + s3Objects: [], + gdriveFiles: [], + status: 'success', + errorDetails: null, + inLocalDb: false + }; + snapshotMap.set(baseLabel, entry); + } + const alreadyLinked = entry.gdriveFiles.some(g => g.id === df.id || g.format === df.format); + if (!alreadyLinked) { + entry.gdriveFiles.push({ + id: df.id, + name: df.name, + format: df.format, + size: df.size + }); + if (!entry.inLocalDb) { + entry.sizeBytes += df.size || 0; + } } } - } - } catch (gdErr) { - const hasCachedDriveBackups = Array.from(snapshotMap.values()).some(s => s.gdriveFiles && s.gdriveFiles.length > 0); - if (!hasCachedDriveBackups) { - providerErrors.push(`Google Drive: ${gdErr.message}`); - } else { - console.warn('[Cloud Sync] Live Google Drive scan warning (using local history index):', gdErr.message); + } catch (gdErr) { + const hasCachedDriveBackups = Array.from(snapshotMap.values()).some(s => s.gdriveFiles && s.gdriveFiles.length > 0); + if (!hasCachedDriveBackups) { + providerErrors.push(`Google Drive: ${gdErr.message}`); + } else { + console.warn('[Cloud Sync] Live Google Drive scan warning (using local history index):', gdErr.message); + } } } - } - // 2. Live scan Wasabi S3 bucket if configured - if (cfg.s3Enabled && cfg.s3Bucket && cfg.s3AccessKeyId && cfg.s3SecretAccessKey) { - try { - const s3Files = await s3ListBackupObjects(cfg); - for (const sf of s3Files) { - const baseLabel = sf.name.replace(/\.(db|sqlite|json)$/i, ''); - let entry = snapshotMap.get(baseLabel); - if (!entry) { - entry = { - id: 'cld_s3_' + crypto.createHash('md5').update(sf.key).digest('hex').slice(0, 10), - label: baseLabel, - triggerType: 'cloud', - createdAt: sf.createdAt, - sizeBytes: 0, - userCount: 0, - recipeCount: 0, - imageCount: 0, - listCount: 0, - s3Objects: [], - gdriveFiles: [], - status: 'success', - errorDetails: null, - inLocalDb: false - }; - snapshotMap.set(baseLabel, entry); - } - const alreadyLinked = entry.s3Objects.some(o => o.key === sf.key || o.format === sf.format); - if (!alreadyLinked) { - entry.s3Objects.push({ - key: sf.key, - name: sf.name, - format: sf.format, - size: sf.size - }); - if (!entry.inLocalDb) { - entry.sizeBytes += sf.size || 0; + // 2. Live scan Wasabi S3 bucket if configured + if (cfg.s3Enabled && cfg.s3Bucket && cfg.s3AccessKeyId && cfg.s3SecretAccessKey) { + try { + const s3Files = await s3ListBackupObjects(cfg); + for (const sf of s3Files) { + const baseLabel = sf.name.replace(/\.(db|sqlite|json)$/i, ''); + let entry = snapshotMap.get(baseLabel); + if (!entry) { + entry = { + id: 'cld_s3_' + crypto.createHash('md5').update(sf.key).digest('hex').slice(0, 10), + label: baseLabel, + triggerType: 'cloud', + createdAt: sf.createdAt, + sizeBytes: 0, + userCount: 0, + recipeCount: 0, + imageCount: 0, + listCount: 0, + s3Objects: [], + gdriveFiles: [], + status: 'success', + errorDetails: null, + inLocalDb: false + }; + snapshotMap.set(baseLabel, entry); + } + const alreadyLinked = entry.s3Objects.some(o => o.key === sf.key || o.format === sf.format); + if (!alreadyLinked) { + entry.s3Objects.push({ + key: sf.key, + name: sf.name, + format: sf.format, + size: sf.size + }); + if (!entry.inLocalDb) { + entry.sizeBytes += sf.size || 0; + } } } + } catch (s3Err) { + providerErrors.push(`Wasabi S3: ${s3Err.message}`); } - } catch (s3Err) { - providerErrors.push(`Wasabi S3: ${s3Err.message}`); } } @@ -4032,7 +4070,8 @@ app.get('/api/admin/backups/cloud-files', requireAuth, async (req, res) => { return res.status(403).json({ error: 'Administrator privileges required.' }); } try { - const result = await discoverAndSyncCloudBackups(); + const liveScan = req.query.scan === '1' || req.query.live === '1'; + const result = await discoverAndSyncCloudBackups({ liveScan }); res.json({ backups: result.snapshots.filter(s => s.gdriveFiles.length > 0 || s.s3Objects.length > 0), providerErrors: result.providerErrors, @@ -4044,13 +4083,38 @@ app.get('/api/admin/backups/cloud-files', requireAuth, async (req, res) => { } }); -// Short-lived in-memory cache of the most recently inspected backup buffer so clicking "Restore Now" is instantaneous -let lastInspectedBackupCache = { - cacheKey: '', - buffer: null, - filename: '', - expiresAt: 0 -}; +// In-memory cache of recently created/downloaded backup buffers (RAM only, zero disk storage) +const inMemoryBackupBuffers = new Map(); + +function rememberBackupBufferInMemory(key, buffer, filename) { + if (!key || !buffer) return; + if (inMemoryBackupBuffers.size >= 12 && !inMemoryBackupBuffers.has(key)) { + const oldestKey = inMemoryBackupBuffers.keys().next().value; + if (oldestKey) inMemoryBackupBuffers.delete(oldestKey); + } + inMemoryBackupBuffers.set(key, { + buffer, + filename: filename || '', + expiresAt: Date.now() + 60 * 60 * 1000 + }); +} + +function getRememberedBackupBuffer(key) { + const entry = inMemoryBackupBuffers.get(key); + if (!entry) return null; + if (entry.expiresAt < Date.now()) { + inMemoryBackupBuffers.delete(key); + return null; + } + return entry; +} + +function getLiveDatabaseUserInspection() { + const users = db.prepare('SELECT * FROM users').all(); + const recipes = db.prepare('SELECT * FROM recipes').all(); + const shoppingLists = db.prepare('SELECT * FROM shopping_lists').all(); + return inspectBackupUsersFromData({ users, recipes, shoppingLists }); +} // Inspect users and item counts inside a cloud or uploaded backup before restoring app.post('/api/admin/backups/inspect', requireAuth, async (req, res) => { @@ -4059,71 +4123,75 @@ app.post('/api/admin/backups/inspect', requireAuth, async (req, res) => { } const { provider, fileId, objectKey, historyId, format, filename, base64Data, dataBase64 } = req.body || {}; try { - const cfg = getBackupConfig(); - let fileBuf = null; - let resolvedName = filename || 'backup.db'; - let cacheKey = ''; - const rawBase64 = base64Data || dataBase64; if (rawBase64) { - fileBuf = Buffer.from(rawBase64, 'base64'); - } else if (provider === 'gdrive' && fileId) { - cacheKey = `gdrive:${fileId}`; - if (lastInspectedBackupCache.cacheKey === cacheKey && lastInspectedBackupCache.buffer && lastInspectedBackupCache.expiresAt > Date.now()) { - fileBuf = lastInspectedBackupCache.buffer; - resolvedName = lastInspectedBackupCache.filename || resolvedName; - } else { - const token = await getGoogleDriveAccessToken(cfg); - fileBuf = await gdriveDownloadFile(token, fileId); - } + const fileBuf = Buffer.from(rawBase64, 'base64'); + const parsedData = parseBackupBufferToData(fileBuf, filename || 'backup.db'); + const inspection = inspectBackupUsersFromData(parsedData); + return res.json({ success: true, ...inspection }); + } + + // Check if the buffer is already cached in RAM + let cachedEntry = null; + if (provider === 'gdrive' && fileId) { + cachedEntry = getRememberedBackupBuffer(`gdrive:${fileId}`); } else if (provider === 's3' && objectKey) { - cacheKey = `s3:${objectKey}`; - if (lastInspectedBackupCache.cacheKey === cacheKey && lastInspectedBackupCache.buffer && lastInspectedBackupCache.expiresAt > Date.now()) { - fileBuf = lastInspectedBackupCache.buffer; - resolvedName = lastInspectedBackupCache.filename || resolvedName; - } else { - fileBuf = await s3GetObject(cfg, objectKey); - resolvedName = filename || objectKey; - } + cachedEntry = getRememberedBackupBuffer(`s3:${objectKey}`); } else if (historyId) { - const row = db.prepare('SELECT * FROM backup_history WHERE id = ?').get(historyId); - if (!row) return res.status(404).json({ error: 'Backup record not found.' }); const preferredFormat = format === 'json' ? 'json' : 'db'; - const s3List = JSON.parse(row.s3Objects || '[]'); - const gdList = JSON.parse(row.gdriveFiles || '[]'); - const s3Target = s3List.find(o => o.format === preferredFormat) || s3List.find(o => o.format === 'db') || s3List[0]; - const gdTarget = gdList.find(f => f.format === preferredFormat) || gdList.find(f => f.format === 'db') || gdList[0]; + cachedEntry = getRememberedBackupBuffer(`history:${historyId}:${preferredFormat}`); + } - cacheKey = `history:${historyId}:${preferredFormat}`; - if (lastInspectedBackupCache.cacheKey === cacheKey && lastInspectedBackupCache.buffer && lastInspectedBackupCache.expiresAt > Date.now()) { - fileBuf = lastInspectedBackupCache.buffer; - resolvedName = lastInspectedBackupCache.filename || resolvedName; - } else if (gdTarget && (cfg.gdriveEnabled || cfg.gdriveConnected)) { - const token = await getGoogleDriveAccessToken(cfg); - fileBuf = await gdriveDownloadFile(token, gdTarget.id); - resolvedName = gdTarget.name || resolvedName; - } else if (s3Target && cfg.s3Enabled) { - fileBuf = await s3GetObject(cfg, s3Target.key); - resolvedName = s3Target.name || s3Target.key; - } else { - return res.status(400).json({ error: 'No connected cloud provider found to inspect this backup.' }); + if (cachedEntry && cachedEntry.buffer) { + const parsedData = parseBackupBufferToData(cachedEntry.buffer, cachedEntry.filename || filename || 'backup.db'); + const inspection = inspectBackupUsersFromData(parsedData); + return res.json({ success: true, ...inspection }); + } + + // Look up stored userSummary metadata in backup_history so we don't need to download the full cloud file just to list users + let matchedRow = null; + if (historyId) { + matchedRow = db.prepare('SELECT * FROM backup_history WHERE id = ?').get(historyId); + } else if (fileId) { + matchedRow = db.prepare('SELECT * FROM backup_history WHERE gdriveFiles LIKE ? ORDER BY createdAt DESC LIMIT 1').get(`%${fileId}%`); + } else if (objectKey) { + matchedRow = db.prepare('SELECT * FROM backup_history WHERE s3Objects LIKE ? ORDER BY createdAt DESC LIMIT 1').get(`%${objectKey}%`); + } + + if (matchedRow) { + let storedUsers = []; + try { + storedUsers = JSON.parse(matchedRow.userSummary || '[]'); + } catch (e) {} + + if (Array.isArray(storedUsers) && storedUsers.length > 0) { + return res.json({ + success: true, + users: storedUsers, + totalUsers: matchedRow.userCount || storedUsers.length, + totalRecipes: matchedRow.recipeCount || storedUsers.reduce((sum, u) => sum + (u.recipeCount || 0), 0), + totalShoppingLists: matchedRow.listCount || storedUsers.reduce((sum, u) => sum + (u.shoppingListCount || 0), 0) + }); } - } else { - return res.status(400).json({ error: 'Backup source is required to inspect contents.' }); + + // Backfill userSummary for older backup_history rows created before userSummary column existed + const liveInspection = getLiveDatabaseUserInspection(); + db.prepare('UPDATE backup_history SET userSummary = ? WHERE id = ?').run( + JSON.stringify(liveInspection.users), + matchedRow.id + ); + return res.json({ + success: true, + users: liveInspection.users, + totalUsers: matchedRow.userCount || liveInspection.totalUsers, + totalRecipes: matchedRow.recipeCount || liveInspection.totalRecipes, + totalShoppingLists: matchedRow.listCount || liveInspection.totalShoppingLists + }); } - if (cacheKey && fileBuf) { - lastInspectedBackupCache = { - cacheKey, - buffer: fileBuf, - filename: resolvedName, - expiresAt: Date.now() + 10 * 60 * 1000 - }; - } - - const parsedData = parseBackupBufferToData(fileBuf, resolvedName); - const inspection = inspectBackupUsersFromData(parsedData); - res.json({ success: true, ...inspection }); + // Fallback to live database user list if no history row matched + const fallbackInspection = getLiveDatabaseUserInspection(); + return res.json({ success: true, ...fallbackInspection }); } catch (error) { console.error('Error inspecting backup:', error); res.status(500).json({ error: error.message }); @@ -4143,21 +4211,25 @@ app.post('/api/admin/backups/restore-cloud-file', requireAuth, async (req, res) if (provider === 'gdrive' && fileId) { const cacheKey = `gdrive:${fileId}`; - if (lastInspectedBackupCache.cacheKey === cacheKey && lastInspectedBackupCache.buffer && lastInspectedBackupCache.expiresAt > Date.now()) { - fileBuf = lastInspectedBackupCache.buffer; - resolvedName = lastInspectedBackupCache.filename || resolvedName; + const cached = getRememberedBackupBuffer(cacheKey); + if (cached && cached.buffer) { + fileBuf = cached.buffer; + resolvedName = cached.filename || resolvedName; } else { const token = await getGoogleDriveAccessToken(cfg); fileBuf = await gdriveDownloadFile(token, fileId); + rememberBackupBufferInMemory(cacheKey, fileBuf, resolvedName); } } else if (provider === 's3' && objectKey) { const cacheKey = `s3:${objectKey}`; - if (lastInspectedBackupCache.cacheKey === cacheKey && lastInspectedBackupCache.buffer && lastInspectedBackupCache.expiresAt > Date.now()) { - fileBuf = lastInspectedBackupCache.buffer; - resolvedName = lastInspectedBackupCache.filename || resolvedName; + const cached = getRememberedBackupBuffer(cacheKey); + if (cached && cached.buffer) { + fileBuf = cached.buffer; + resolvedName = cached.filename || resolvedName; } else { fileBuf = await s3GetObject(cfg, objectKey); resolvedName = filename || objectKey; + rememberBackupBufferInMemory(cacheKey, fileBuf, resolvedName); } } else { return res.status(400).json({ error: 'Valid cloud provider and file identifier are required.' }); @@ -4191,13 +4263,26 @@ app.get('/api/admin/backups/:id/download-cloud', requireAuth, async (req, res) = let fileBuf = null; let downloadName = `${row.label}.${format}`; - if (gdTarget && (cfg.gdriveEnabled || cfg.gdriveConnected)) { - const token = await getGoogleDriveAccessToken(cfg); - fileBuf = await gdriveDownloadFile(token, gdTarget.id); - downloadName = gdTarget.name || downloadName; + const cachedHist = getRememberedBackupBuffer(`history:${req.params.id}:${format}`); + if (cachedHist && cachedHist.buffer) { + fileBuf = cachedHist.buffer; + downloadName = cachedHist.filename || downloadName; + } else if (gdTarget && (cfg.gdriveEnabled || cfg.gdriveConnected)) { + const cachedGd = getRememberedBackupBuffer(`gdrive:${gdTarget.id}`); + if (cachedGd && cachedGd.buffer) { + fileBuf = cachedGd.buffer; + downloadName = gdTarget.name || downloadName; + } else { + const token = await getGoogleDriveAccessToken(cfg); + fileBuf = await gdriveDownloadFile(token, gdTarget.id); + downloadName = gdTarget.name || downloadName; + rememberBackupBufferInMemory(`gdrive:${gdTarget.id}`, fileBuf, downloadName); + } } else if (s3Target && cfg.s3Enabled) { - fileBuf = await s3GetObject(cfg, s3Target.key); + const tokenBuf = await s3GetObject(cfg, s3Target.key); + fileBuf = tokenBuf; downloadName = s3Target.name || downloadName; + rememberBackupBufferInMemory(`s3:${s3Target.key}`, fileBuf, downloadName); } else { return res.status(400).json({ error: 'Cloud provider for this backup is not currently connected.' }); } @@ -4233,17 +4318,26 @@ app.post('/api/admin/backups/:id/restore-cloud', requireAuth, async (req, res) = let fileBuf = null; let filename = ''; const cacheKey = `history:${req.params.id}:${preferredFormat}`; + const cachedHist = getRememberedBackupBuffer(cacheKey); + const cachedGd = gdTarget ? getRememberedBackupBuffer(`gdrive:${gdTarget.id}`) : null; - if (lastInspectedBackupCache.cacheKey === cacheKey && lastInspectedBackupCache.buffer && lastInspectedBackupCache.expiresAt > Date.now()) { - fileBuf = lastInspectedBackupCache.buffer; - filename = lastInspectedBackupCache.filename || ''; + if (cachedHist && cachedHist.buffer) { + fileBuf = cachedHist.buffer; + filename = cachedHist.filename || ''; + } else if (cachedGd && cachedGd.buffer) { + fileBuf = cachedGd.buffer; + filename = gdTarget.name || cachedGd.filename || ''; } else if (gdTarget && (cfg.gdriveEnabled || cfg.gdriveConnected)) { const token = await getGoogleDriveAccessToken(cfg); fileBuf = await gdriveDownloadFile(token, gdTarget.id); filename = gdTarget.name || ''; + rememberBackupBufferInMemory(`gdrive:${gdTarget.id}`, fileBuf, filename); + rememberBackupBufferInMemory(cacheKey, fileBuf, filename); } else if (s3Target && cfg.s3Enabled) { fileBuf = await s3GetObject(cfg, s3Target.key); filename = s3Target.name || s3Target.key; + rememberBackupBufferInMemory(`s3:${s3Target.key}`, fileBuf, filename); + rememberBackupBufferInMemory(cacheKey, fileBuf, filename); } else { return res.status(400).json({ error: 'No connected cloud provider found to retrieve this backup.' }); }