fix: serve backup user inspection from metadata and avoid Google Drive quota bursts

This commit is contained in:
Chris 2026-10-31 01:36:47 -04:00
parent c34ce4122b
commit c7fb78a139
2 changed files with 269 additions and 173 deletions

View file

@ -3175,7 +3175,7 @@ function renderGdriveConnectionState(cfg) {
elements.btnGdriveRefreshFolders.disabled = !isConnected; 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); loadGoogleDriveFolders(false, cfg.gdriveFolderId, cfg.gdriveFolderName);
} }
} }
@ -3986,7 +3986,8 @@ async function handleSyncCloudBackups(silent = false) {
} }
try { 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() headers: getAuthHeaders()
}); });
const data = await res.json(); const data = await res.json();
@ -4024,12 +4025,13 @@ async function loadLiveCloudBackupsForModal(showToast = false) {
if (!elements.cloudRestoreFilesContainer) return; if (!elements.cloudRestoreFilesContainer) return;
elements.cloudRestoreFilesContainer.innerHTML = ` elements.cloudRestoreFilesContainer.innerHTML = `
<div style="text-align: center; padding: 32px; color: var(--text-muted);"> <div style="text-align: center; padding: 32px; color: var(--text-muted);">
<p>Scanning your Google Drive backup folder & Wasabi S3 bucket for available backups...</p> <p>Loading available cloud backups...</p>
</div> </div>
`; `;
try { 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() headers: getAuthHeaders()
}); });
const data = await res.json(); const data = await res.json();

432
server.js
View file

@ -187,6 +187,9 @@ try {
try { try {
db.prepare("ALTER TABLE backup_config ADD COLUMN gdriveFolderName TEXT NOT NULL DEFAULT 'ForkFolio Backups'").run(); db.prepare("ALTER TABLE backup_config ADD COLUMN gdriveFolderName TEXT NOT NULL DEFAULT 'ForkFolio Backups'").run();
} catch (e) {} } 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 // Initialize single settings record if empty
const settingsCount = db.prepare('SELECT COUNT(*) as count FROM settings').get().count; 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 buffer = Buffer.from(JSON.stringify(archiveObj, null, 2), 'utf8');
const userSummary = inspectBackupUsersFromData({
users,
recipes: enrichedRecipes,
shoppingLists
}).users;
return { return {
buffer, buffer,
userCount: users.length, userCount: users.length,
recipeCount: enrichedRecipes.length, recipeCount: enrichedRecipes.length,
imageCount, imageCount,
listCount: shoppingLists.length listCount: shoppingLists.length,
userSummary
}; };
} }
@ -2511,12 +2520,14 @@ function sleepMs(ms) {
return new Promise(resolve => setTimeout(resolve, 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 () => { const runRequest = async () => {
let lastRes = null; let lastRes = null;
const backoffSchedule = [1200, 2200, 3500, 5000, 6800, 8500, 10500, 12500];
for (let attempt = 0; attempt <= maxRetries; attempt++) { for (let attempt = 0; attempt <= maxRetries; attempt++) {
if (attempt > 0) { 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); await sleepMs(delay);
} }
lastRes = await fetch(url, options); lastRes = await fetch(url, options);
@ -2529,7 +2540,7 @@ async function gdriveFetch(url, options = {}, maxRetries = 4) {
} }
if (lastRes.status === 403 && attempt < maxRetries) { if (lastRes.status === 403 && attempt < maxRetries) {
const clonedText = await lastRes.clone().text().catch(() => ''); 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; continue;
} }
} }
@ -2539,7 +2550,7 @@ async function gdriveFetch(url, options = {}, maxRetries = 4) {
}; };
const next = gdriveRequestQueue.then(async () => { const next = gdriveRequestQueue.then(async () => {
await sleepMs(120); await sleepMs(150);
return runRequest(); return runRequest();
}); });
gdriveRequestQueue = next.catch(() => {}); gdriveRequestQueue = next.catch(() => {});
@ -2843,8 +2854,8 @@ async function executeCloudBackup(triggerType = 'manual') {
db.prepare(` db.prepare(`
INSERT INTO backup_history ( INSERT INTO backup_history (
id, label, triggerType, sizeBytes, userCount, recipeCount, imageCount, listCount, id, label, triggerType, sizeBytes, userCount, recipeCount, imageCount, listCount,
s3Objects, gdriveFiles, status, errorDetails, createdAt s3Objects, gdriveFiles, userSummary, status, errorDetails, createdAt
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'success', NULL, ?) ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'success', NULL, ?)
`).run( `).run(
backupId, backupId,
label, label,
@ -2856,9 +2867,29 @@ async function executeCloudBackup(triggerType = 'manual') {
jsonArchive.listCount, jsonArchive.listCount,
JSON.stringify(s3Objects), JSON.stringify(s3Objects),
JSON.stringify(gdriveFiles), JSON.stringify(gdriveFiles),
JSON.stringify(jsonArchive.userSummary || []),
createdAt 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(` db.prepare(`
UPDATE backup_config UPDATE backup_config
SET lastBackupAt = ?, lastBackupStatus = 'success', lastBackupMessage = ? 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 // 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 cfg = getBackupConfig();
const snapshotMap = new Map(); const snapshotMap = new Map();
@ -3863,99 +3895,105 @@ async function discoverAndSyncCloudBackups() {
} }
const providerErrors = []; 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 // Only query Google Drive / S3 APIs when explicitly requested ("Scan Cloud Now" / "Sync from Cloud") or when local history is empty
if (cfg.gdriveConnected) { if (forceLiveScan || !hasKnownCloudSnapshots) {
try { // 1. Live scan Google Drive backup folder if connected
const accessToken = await getGoogleDriveAccessToken(cfg); if (cfg.gdriveConnected) {
const driveFiles = await gdriveListBackupFiles(accessToken, cfg.gdriveFolderId || ''); try {
const accessToken = await getGoogleDriveAccessToken(cfg);
const driveFiles = await gdriveListBackupFiles(accessToken, cfg.gdriveFolderId || '');
for (const df of driveFiles) { for (const df of driveFiles) {
const baseLabel = df.name.replace(/\.(db|sqlite|json)$/i, ''); const baseLabel = df.name.replace(/\.(db|sqlite|json)$/i, '');
let entry = snapshotMap.get(baseLabel); let entry = snapshotMap.get(baseLabel);
if (!entry) { if (!entry) {
entry = { entry = {
id: 'cld_gd_' + df.id.slice(0, 12), id: 'cld_gd_' + df.id.slice(0, 12),
label: baseLabel, label: baseLabel,
triggerType: 'cloud', triggerType: 'cloud',
createdAt: df.createdAt, createdAt: df.createdAt,
sizeBytes: 0, sizeBytes: 0,
userCount: 0, userCount: 0,
recipeCount: 0, recipeCount: 0,
imageCount: 0, imageCount: 0,
listCount: 0, listCount: 0,
s3Objects: [], s3Objects: [],
gdriveFiles: [], gdriveFiles: [],
status: 'success', status: 'success',
errorDetails: null, errorDetails: null,
inLocalDb: false inLocalDb: false
}; };
snapshotMap.set(baseLabel, entry); snapshotMap.set(baseLabel, entry);
} }
const alreadyLinked = entry.gdriveFiles.some(g => g.id === df.id || g.format === df.format); const alreadyLinked = entry.gdriveFiles.some(g => g.id === df.id || g.format === df.format);
if (!alreadyLinked) { if (!alreadyLinked) {
entry.gdriveFiles.push({ entry.gdriveFiles.push({
id: df.id, id: df.id,
name: df.name, name: df.name,
format: df.format, format: df.format,
size: df.size size: df.size
}); });
if (!entry.inLocalDb) { if (!entry.inLocalDb) {
entry.sizeBytes += df.size || 0; entry.sizeBytes += df.size || 0;
}
} }
} }
} } catch (gdErr) {
} catch (gdErr) { const hasCachedDriveBackups = Array.from(snapshotMap.values()).some(s => s.gdriveFiles && s.gdriveFiles.length > 0);
const hasCachedDriveBackups = Array.from(snapshotMap.values()).some(s => s.gdriveFiles && s.gdriveFiles.length > 0); if (!hasCachedDriveBackups) {
if (!hasCachedDriveBackups) { providerErrors.push(`Google Drive: ${gdErr.message}`);
providerErrors.push(`Google Drive: ${gdErr.message}`); } else {
} else { console.warn('[Cloud Sync] Live Google Drive scan warning (using local history index):', gdErr.message);
console.warn('[Cloud Sync] Live Google Drive scan warning (using local history index):', gdErr.message); }
} }
} }
}
// 2. Live scan Wasabi S3 bucket if configured // 2. Live scan Wasabi S3 bucket if configured
if (cfg.s3Enabled && cfg.s3Bucket && cfg.s3AccessKeyId && cfg.s3SecretAccessKey) { if (cfg.s3Enabled && cfg.s3Bucket && cfg.s3AccessKeyId && cfg.s3SecretAccessKey) {
try { try {
const s3Files = await s3ListBackupObjects(cfg); const s3Files = await s3ListBackupObjects(cfg);
for (const sf of s3Files) { for (const sf of s3Files) {
const baseLabel = sf.name.replace(/\.(db|sqlite|json)$/i, ''); const baseLabel = sf.name.replace(/\.(db|sqlite|json)$/i, '');
let entry = snapshotMap.get(baseLabel); let entry = snapshotMap.get(baseLabel);
if (!entry) { if (!entry) {
entry = { entry = {
id: 'cld_s3_' + crypto.createHash('md5').update(sf.key).digest('hex').slice(0, 10), id: 'cld_s3_' + crypto.createHash('md5').update(sf.key).digest('hex').slice(0, 10),
label: baseLabel, label: baseLabel,
triggerType: 'cloud', triggerType: 'cloud',
createdAt: sf.createdAt, createdAt: sf.createdAt,
sizeBytes: 0, sizeBytes: 0,
userCount: 0, userCount: 0,
recipeCount: 0, recipeCount: 0,
imageCount: 0, imageCount: 0,
listCount: 0, listCount: 0,
s3Objects: [], s3Objects: [],
gdriveFiles: [], gdriveFiles: [],
status: 'success', status: 'success',
errorDetails: null, errorDetails: null,
inLocalDb: false inLocalDb: false
}; };
snapshotMap.set(baseLabel, entry); snapshotMap.set(baseLabel, entry);
} }
const alreadyLinked = entry.s3Objects.some(o => o.key === sf.key || o.format === sf.format); const alreadyLinked = entry.s3Objects.some(o => o.key === sf.key || o.format === sf.format);
if (!alreadyLinked) { if (!alreadyLinked) {
entry.s3Objects.push({ entry.s3Objects.push({
key: sf.key, key: sf.key,
name: sf.name, name: sf.name,
format: sf.format, format: sf.format,
size: sf.size size: sf.size
}); });
if (!entry.inLocalDb) { if (!entry.inLocalDb) {
entry.sizeBytes += sf.size || 0; 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.' }); return res.status(403).json({ error: 'Administrator privileges required.' });
} }
try { try {
const result = await discoverAndSyncCloudBackups(); const liveScan = req.query.scan === '1' || req.query.live === '1';
const result = await discoverAndSyncCloudBackups({ liveScan });
res.json({ res.json({
backups: result.snapshots.filter(s => s.gdriveFiles.length > 0 || s.s3Objects.length > 0), backups: result.snapshots.filter(s => s.gdriveFiles.length > 0 || s.s3Objects.length > 0),
providerErrors: result.providerErrors, 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 // In-memory cache of recently created/downloaded backup buffers (RAM only, zero disk storage)
let lastInspectedBackupCache = { const inMemoryBackupBuffers = new Map();
cacheKey: '',
buffer: null, function rememberBackupBufferInMemory(key, buffer, filename) {
filename: '', if (!key || !buffer) return;
expiresAt: 0 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 // Inspect users and item counts inside a cloud or uploaded backup before restoring
app.post('/api/admin/backups/inspect', requireAuth, async (req, res) => { 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 || {}; const { provider, fileId, objectKey, historyId, format, filename, base64Data, dataBase64 } = req.body || {};
try { try {
const cfg = getBackupConfig();
let fileBuf = null;
let resolvedName = filename || 'backup.db';
let cacheKey = '';
const rawBase64 = base64Data || dataBase64; const rawBase64 = base64Data || dataBase64;
if (rawBase64) { if (rawBase64) {
fileBuf = Buffer.from(rawBase64, 'base64'); const fileBuf = Buffer.from(rawBase64, 'base64');
} else if (provider === 'gdrive' && fileId) { const parsedData = parseBackupBufferToData(fileBuf, filename || 'backup.db');
cacheKey = `gdrive:${fileId}`; const inspection = inspectBackupUsersFromData(parsedData);
if (lastInspectedBackupCache.cacheKey === cacheKey && lastInspectedBackupCache.buffer && lastInspectedBackupCache.expiresAt > Date.now()) { return res.json({ success: true, ...inspection });
fileBuf = lastInspectedBackupCache.buffer; }
resolvedName = lastInspectedBackupCache.filename || resolvedName;
} else { // Check if the buffer is already cached in RAM
const token = await getGoogleDriveAccessToken(cfg); let cachedEntry = null;
fileBuf = await gdriveDownloadFile(token, fileId); if (provider === 'gdrive' && fileId) {
} cachedEntry = getRememberedBackupBuffer(`gdrive:${fileId}`);
} else if (provider === 's3' && objectKey) { } else if (provider === 's3' && objectKey) {
cacheKey = `s3:${objectKey}`; cachedEntry = getRememberedBackupBuffer(`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;
}
} else if (historyId) { } 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 preferredFormat = format === 'json' ? 'json' : 'db';
const s3List = JSON.parse(row.s3Objects || '[]'); cachedEntry = getRememberedBackupBuffer(`history:${historyId}:${preferredFormat}`);
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];
cacheKey = `history:${historyId}:${preferredFormat}`; if (cachedEntry && cachedEntry.buffer) {
if (lastInspectedBackupCache.cacheKey === cacheKey && lastInspectedBackupCache.buffer && lastInspectedBackupCache.expiresAt > Date.now()) { const parsedData = parseBackupBufferToData(cachedEntry.buffer, cachedEntry.filename || filename || 'backup.db');
fileBuf = lastInspectedBackupCache.buffer; const inspection = inspectBackupUsersFromData(parsedData);
resolvedName = lastInspectedBackupCache.filename || resolvedName; return res.json({ success: true, ...inspection });
} else if (gdTarget && (cfg.gdriveEnabled || cfg.gdriveConnected)) { }
const token = await getGoogleDriveAccessToken(cfg);
fileBuf = await gdriveDownloadFile(token, gdTarget.id); // Look up stored userSummary metadata in backup_history so we don't need to download the full cloud file just to list users
resolvedName = gdTarget.name || resolvedName; let matchedRow = null;
} else if (s3Target && cfg.s3Enabled) { if (historyId) {
fileBuf = await s3GetObject(cfg, s3Target.key); matchedRow = db.prepare('SELECT * FROM backup_history WHERE id = ?').get(historyId);
resolvedName = s3Target.name || s3Target.key; } else if (fileId) {
} else { matchedRow = db.prepare('SELECT * FROM backup_history WHERE gdriveFiles LIKE ? ORDER BY createdAt DESC LIMIT 1').get(`%${fileId}%`);
return res.status(400).json({ error: 'No connected cloud provider found to inspect this backup.' }); } 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) { // Fallback to live database user list if no history row matched
lastInspectedBackupCache = { const fallbackInspection = getLiveDatabaseUserInspection();
cacheKey, return res.json({ success: true, ...fallbackInspection });
buffer: fileBuf,
filename: resolvedName,
expiresAt: Date.now() + 10 * 60 * 1000
};
}
const parsedData = parseBackupBufferToData(fileBuf, resolvedName);
const inspection = inspectBackupUsersFromData(parsedData);
res.json({ success: true, ...inspection });
} catch (error) { } catch (error) {
console.error('Error inspecting backup:', error); console.error('Error inspecting backup:', error);
res.status(500).json({ error: error.message }); 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) { if (provider === 'gdrive' && fileId) {
const cacheKey = `gdrive:${fileId}`; const cacheKey = `gdrive:${fileId}`;
if (lastInspectedBackupCache.cacheKey === cacheKey && lastInspectedBackupCache.buffer && lastInspectedBackupCache.expiresAt > Date.now()) { const cached = getRememberedBackupBuffer(cacheKey);
fileBuf = lastInspectedBackupCache.buffer; if (cached && cached.buffer) {
resolvedName = lastInspectedBackupCache.filename || resolvedName; fileBuf = cached.buffer;
resolvedName = cached.filename || resolvedName;
} else { } else {
const token = await getGoogleDriveAccessToken(cfg); const token = await getGoogleDriveAccessToken(cfg);
fileBuf = await gdriveDownloadFile(token, fileId); fileBuf = await gdriveDownloadFile(token, fileId);
rememberBackupBufferInMemory(cacheKey, fileBuf, resolvedName);
} }
} else if (provider === 's3' && objectKey) { } else if (provider === 's3' && objectKey) {
const cacheKey = `s3:${objectKey}`; const cacheKey = `s3:${objectKey}`;
if (lastInspectedBackupCache.cacheKey === cacheKey && lastInspectedBackupCache.buffer && lastInspectedBackupCache.expiresAt > Date.now()) { const cached = getRememberedBackupBuffer(cacheKey);
fileBuf = lastInspectedBackupCache.buffer; if (cached && cached.buffer) {
resolvedName = lastInspectedBackupCache.filename || resolvedName; fileBuf = cached.buffer;
resolvedName = cached.filename || resolvedName;
} else { } else {
fileBuf = await s3GetObject(cfg, objectKey); fileBuf = await s3GetObject(cfg, objectKey);
resolvedName = filename || objectKey; resolvedName = filename || objectKey;
rememberBackupBufferInMemory(cacheKey, fileBuf, resolvedName);
} }
} else { } else {
return res.status(400).json({ error: 'Valid cloud provider and file identifier are required.' }); 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 fileBuf = null;
let downloadName = `${row.label}.${format}`; let downloadName = `${row.label}.${format}`;
if (gdTarget && (cfg.gdriveEnabled || cfg.gdriveConnected)) { const cachedHist = getRememberedBackupBuffer(`history:${req.params.id}:${format}`);
const token = await getGoogleDriveAccessToken(cfg); if (cachedHist && cachedHist.buffer) {
fileBuf = await gdriveDownloadFile(token, gdTarget.id); fileBuf = cachedHist.buffer;
downloadName = gdTarget.name || downloadName; 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) { } else if (s3Target && cfg.s3Enabled) {
fileBuf = await s3GetObject(cfg, s3Target.key); const tokenBuf = await s3GetObject(cfg, s3Target.key);
fileBuf = tokenBuf;
downloadName = s3Target.name || downloadName; downloadName = s3Target.name || downloadName;
rememberBackupBufferInMemory(`s3:${s3Target.key}`, fileBuf, downloadName);
} else { } else {
return res.status(400).json({ error: 'Cloud provider for this backup is not currently connected.' }); 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 fileBuf = null;
let filename = ''; let filename = '';
const cacheKey = `history:${req.params.id}:${preferredFormat}`; 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()) { if (cachedHist && cachedHist.buffer) {
fileBuf = lastInspectedBackupCache.buffer; fileBuf = cachedHist.buffer;
filename = lastInspectedBackupCache.filename || ''; filename = cachedHist.filename || '';
} else if (cachedGd && cachedGd.buffer) {
fileBuf = cachedGd.buffer;
filename = gdTarget.name || cachedGd.filename || '';
} else if (gdTarget && (cfg.gdriveEnabled || cfg.gdriveConnected)) { } else if (gdTarget && (cfg.gdriveEnabled || cfg.gdriveConnected)) {
const token = await getGoogleDriveAccessToken(cfg); const token = await getGoogleDriveAccessToken(cfg);
fileBuf = await gdriveDownloadFile(token, gdTarget.id); fileBuf = await gdriveDownloadFile(token, gdTarget.id);
filename = gdTarget.name || ''; filename = gdTarget.name || '';
rememberBackupBufferInMemory(`gdrive:${gdTarget.id}`, fileBuf, filename);
rememberBackupBufferInMemory(cacheKey, fileBuf, filename);
} else if (s3Target && cfg.s3Enabled) { } else if (s3Target && cfg.s3Enabled) {
fileBuf = await s3GetObject(cfg, s3Target.key); fileBuf = await s3GetObject(cfg, s3Target.key);
filename = s3Target.name || s3Target.key; filename = s3Target.name || s3Target.key;
rememberBackupBufferInMemory(`s3:${s3Target.key}`, fileBuf, filename);
rememberBackupBufferInMemory(cacheKey, fileBuf, filename);
} else { } else {
return res.status(400).json({ error: 'No connected cloud provider found to retrieve this backup.' }); return res.status(400).json({ error: 'No connected cloud provider found to retrieve this backup.' });
} }