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.' });
}