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;
}
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 = `
<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>
`;
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();

432
server.js
View file

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