mirror of
https://gitlab.com/Zer0interrupt/recipe-manager.git
synced 2026-11-01 23:24:11 -05:00
fix: eliminate Google Drive API burst rate-limit errors and cache inspected backup buffers
This commit is contained in:
parent
906d448dba
commit
b44c4ba333
2 changed files with 163 additions and 26 deletions
|
|
@ -3116,6 +3116,8 @@ function getBackupFormPayload() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let lastGdriveFoldersFetchAt = 0;
|
||||||
|
|
||||||
function renderGdriveConnectionState(cfg) {
|
function renderGdriveConnectionState(cfg) {
|
||||||
const isConnected = Boolean(cfg.gdriveConnected);
|
const isConnected = Boolean(cfg.gdriveConnected);
|
||||||
|
|
||||||
|
|
@ -3152,7 +3154,16 @@ function renderGdriveConnectionState(cfg) {
|
||||||
if (elements.backupGdriveFolderSelect) {
|
if (elements.backupGdriveFolderSelect) {
|
||||||
elements.backupGdriveFolderSelect.disabled = !isConnected;
|
elements.backupGdriveFolderSelect.disabled = !isConnected;
|
||||||
if (!isConnected) {
|
if (!isConnected) {
|
||||||
|
lastGdriveFoldersFetchAt = 0;
|
||||||
elements.backupGdriveFolderSelect.innerHTML = `<option value="">Authorize with Google first to select a folder...</option>`;
|
elements.backupGdriveFolderSelect.innerHTML = `<option value="">Authorize with Google first to select a folder...</option>`;
|
||||||
|
} else if (cfg.gdriveFolderId && (!elements.backupGdriveFolderSelect.value || elements.backupGdriveFolderSelect.options.length <= 1)) {
|
||||||
|
elements.backupGdriveFolderSelect.innerHTML = '';
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = cfg.gdriveFolderId;
|
||||||
|
opt.dataset.folderName = cfg.gdriveFolderName || 'ForkFolio Backups';
|
||||||
|
opt.textContent = `📁 ${cfg.gdriveFolderName || 'ForkFolio Backups'}`;
|
||||||
|
opt.selected = true;
|
||||||
|
elements.backupGdriveFolderSelect.appendChild(opt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (elements.btnGdriveNewFolder) {
|
if (elements.btnGdriveNewFolder) {
|
||||||
|
|
@ -3162,7 +3173,7 @@ function renderGdriveConnectionState(cfg) {
|
||||||
elements.btnGdriveRefreshFolders.disabled = !isConnected;
|
elements.btnGdriveRefreshFolders.disabled = !isConnected;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isConnected) {
|
if (isConnected && Date.now() - lastGdriveFoldersFetchAt > 60 * 1000) {
|
||||||
loadGoogleDriveFolders(false, cfg.gdriveFolderId, cfg.gdriveFolderName);
|
loadGoogleDriveFolders(false, cfg.gdriveFolderId, cfg.gdriveFolderName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3170,6 +3181,7 @@ function renderGdriveConnectionState(cfg) {
|
||||||
async function loadGoogleDriveFolders(showFeedback = false, fallbackId = '', fallbackName = 'ForkFolio Backups') {
|
async function loadGoogleDriveFolders(showFeedback = false, fallbackId = '', fallbackName = 'ForkFolio Backups') {
|
||||||
if (!elements.backupGdriveFolderSelect) return;
|
if (!elements.backupGdriveFolderSelect) return;
|
||||||
try {
|
try {
|
||||||
|
lastGdriveFoldersFetchAt = Date.now();
|
||||||
if (showFeedback) {
|
if (showFeedback) {
|
||||||
showGdriveInlineStatus('Loading folders from your Google Drive...', 'info');
|
showGdriveInlineStatus('Loading folders from your Google Drive...', 'info');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
175
server.js
175
server.js
|
|
@ -2498,6 +2498,54 @@ async function s3ListBackupObjects(cfg) {
|
||||||
// GOOGLE DRIVE REST API V3 CONNECTION CLIENT
|
// GOOGLE DRIVE REST API V3 CONNECTION CLIENT
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
|
|
||||||
|
// In-memory Google OAuth2 access token cache + request serializer/retry helper to avoid shared client rate limits
|
||||||
|
let cachedGdriveToken = {
|
||||||
|
accessToken: '',
|
||||||
|
refreshToken: '',
|
||||||
|
expiresAt: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
let gdriveRequestQueue = Promise.resolve();
|
||||||
|
|
||||||
|
function sleepMs(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function gdriveFetch(url, options = {}, maxRetries = 4) {
|
||||||
|
const runRequest = async () => {
|
||||||
|
let lastRes = null;
|
||||||
|
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);
|
||||||
|
await sleepMs(delay);
|
||||||
|
}
|
||||||
|
lastRes = await fetch(url, options);
|
||||||
|
if (lastRes.ok) {
|
||||||
|
return lastRes;
|
||||||
|
}
|
||||||
|
// Retry on rate limit (403 userRateLimitExceeded / rateLimitExceeded / 429) or transient 5xx
|
||||||
|
if (lastRes.status === 429 || lastRes.status >= 500) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (lastRes.status === 403 && attempt < maxRetries) {
|
||||||
|
const clonedText = await lastRes.clone().text().catch(() => '');
|
||||||
|
if (/rateLimit|quota|Queries per|userRateLimit/i.test(clonedText)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lastRes;
|
||||||
|
}
|
||||||
|
return lastRes;
|
||||||
|
};
|
||||||
|
|
||||||
|
const next = gdriveRequestQueue.then(async () => {
|
||||||
|
await sleepMs(120);
|
||||||
|
return runRequest();
|
||||||
|
});
|
||||||
|
gdriveRequestQueue = next.catch(() => {});
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
async function getGoogleDriveAccessToken(cfg) {
|
async function getGoogleDriveAccessToken(cfg) {
|
||||||
const clientId = (cfg.gdriveClientId && cfg.gdriveClientId.trim()) || DEFAULT_GDRIVE_CLIENT_ID;
|
const clientId = (cfg.gdriveClientId && cfg.gdriveClientId.trim()) || DEFAULT_GDRIVE_CLIENT_ID;
|
||||||
const clientSecret = (cfg.gdriveClientSecret && cfg.gdriveClientSecret.trim()) || DEFAULT_GDRIVE_CLIENT_SECRET;
|
const clientSecret = (cfg.gdriveClientSecret && cfg.gdriveClientSecret.trim()) || DEFAULT_GDRIVE_CLIENT_SECRET;
|
||||||
|
|
@ -2505,6 +2553,16 @@ async function getGoogleDriveAccessToken(cfg) {
|
||||||
if (!refreshToken) {
|
if (!refreshToken) {
|
||||||
throw new Error('Google Drive is not connected yet. Click "Authorize with Google" to connect your account.');
|
throw new Error('Google Drive is not connected yet. Click "Authorize with Google" to connect your account.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
if (
|
||||||
|
cachedGdriveToken.accessToken &&
|
||||||
|
cachedGdriveToken.refreshToken === refreshToken &&
|
||||||
|
cachedGdriveToken.expiresAt > now + 60 * 1000
|
||||||
|
) {
|
||||||
|
return cachedGdriveToken.accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
client_id: clientId,
|
client_id: clientId,
|
||||||
client_secret: clientSecret,
|
client_secret: clientSecret,
|
||||||
|
|
@ -2520,12 +2578,19 @@ async function getGoogleDriveAccessToken(cfg) {
|
||||||
if (!res.ok || !data.access_token) {
|
if (!res.ok || !data.access_token) {
|
||||||
throw new Error(data.error_description || data.error || `Google OAuth token exchange failed (${res.status}).`);
|
throw new Error(data.error_description || data.error || `Google OAuth token exchange failed (${res.status}).`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const expiresInSec = Number(data.expires_in) || 3500;
|
||||||
|
cachedGdriveToken = {
|
||||||
|
accessToken: data.access_token,
|
||||||
|
refreshToken,
|
||||||
|
expiresAt: now + expiresInSec * 1000
|
||||||
|
};
|
||||||
return data.access_token;
|
return data.access_token;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getGoogleDriveUserEmail(accessToken) {
|
async function getGoogleDriveUserEmail(accessToken) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('https://www.googleapis.com/drive/v3/about?fields=user(emailAddress,displayName)', {
|
const res = await gdriveFetch('https://www.googleapis.com/drive/v3/about?fields=user(emailAddress,displayName)', {
|
||||||
headers: { Authorization: `Bearer ${accessToken}` }
|
headers: { Authorization: `Bearer ${accessToken}` }
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
|
@ -2536,10 +2601,13 @@ async function getGoogleDriveUserEmail(accessToken) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureGoogleDriveFolder(accessToken, cfg) {
|
async function ensureGoogleDriveFolder(accessToken, cfg, verifyRemote = false) {
|
||||||
if (cfg.gdriveFolderId && cfg.gdriveFolderId.trim()) {
|
if (cfg.gdriveFolderId && cfg.gdriveFolderId.trim()) {
|
||||||
const folderId = cfg.gdriveFolderId.trim();
|
const folderId = cfg.gdriveFolderId.trim();
|
||||||
const checkRes = await fetch(`https://www.googleapis.com/drive/v3/files/${encodeURIComponent(folderId)}?fields=id,name,trashed`, {
|
if (!verifyRemote) {
|
||||||
|
return folderId;
|
||||||
|
}
|
||||||
|
const checkRes = await gdriveFetch(`https://www.googleapis.com/drive/v3/files/${encodeURIComponent(folderId)}?fields=id,name,trashed`, {
|
||||||
headers: { Authorization: `Bearer ${accessToken}` }
|
headers: { Authorization: `Bearer ${accessToken}` }
|
||||||
});
|
});
|
||||||
if (checkRes.ok) {
|
if (checkRes.ok) {
|
||||||
|
|
@ -2550,12 +2618,15 @@ async function ensureGoogleDriveFolder(accessToken, cfg) {
|
||||||
}
|
}
|
||||||
return f.id;
|
return f.id;
|
||||||
}
|
}
|
||||||
|
} else if (checkRes.status !== 404) {
|
||||||
|
// Keep configured folderId on non-404 responses (e.g. temporary quota limit) rather than recreating
|
||||||
|
return folderId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search for existing "ForkFolio Backups" folder or create one
|
// Search for existing "ForkFolio Backups" folder or create one
|
||||||
const q = encodeURIComponent("mimeType='application/vnd.google-apps.folder' and name='ForkFolio Backups' and trashed=false");
|
const q = encodeURIComponent("mimeType='application/vnd.google-apps.folder' and name='ForkFolio Backups' and trashed=false");
|
||||||
const searchRes = await fetch(`https://www.googleapis.com/drive/v3/files?q=${q}&fields=files(id,name)`, {
|
const searchRes = await gdriveFetch(`https://www.googleapis.com/drive/v3/files?q=${q}&fields=files(id,name)`, {
|
||||||
headers: { Authorization: `Bearer ${accessToken}` }
|
headers: { Authorization: `Bearer ${accessToken}` }
|
||||||
});
|
});
|
||||||
if (searchRes.ok) {
|
if (searchRes.ok) {
|
||||||
|
|
@ -2569,7 +2640,7 @@ async function ensureGoogleDriveFolder(accessToken, cfg) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create folder
|
// Create folder
|
||||||
const createRes = await fetch('https://www.googleapis.com/drive/v3/files?fields=id,name', {
|
const createRes = await gdriveFetch('https://www.googleapis.com/drive/v3/files?fields=id,name', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
|
@ -2582,7 +2653,7 @@ async function ensureGoogleDriveFolder(accessToken, cfg) {
|
||||||
});
|
});
|
||||||
const created = await createRes.json().catch(() => ({}));
|
const created = await createRes.json().catch(() => ({}));
|
||||||
if (!createRes.ok || !created.id) {
|
if (!createRes.ok || !created.id) {
|
||||||
throw new Error('Failed to verify or create Google Drive backup folder.');
|
throw new Error(created.error?.message || 'Failed to verify or create Google Drive backup folder.');
|
||||||
}
|
}
|
||||||
db.prepare('UPDATE backup_config SET gdriveFolderId = ?, gdriveFolderName = ? WHERE id = 1').run(created.id, created.name || 'ForkFolio Backups');
|
db.prepare('UPDATE backup_config SET gdriveFolderId = ?, gdriveFolderName = ? WHERE id = 1').run(created.id, created.name || 'ForkFolio Backups');
|
||||||
return created.id;
|
return created.id;
|
||||||
|
|
@ -2590,7 +2661,7 @@ async function ensureGoogleDriveFolder(accessToken, cfg) {
|
||||||
|
|
||||||
async function gdriveListFolders(accessToken) {
|
async function gdriveListFolders(accessToken) {
|
||||||
const q = encodeURIComponent("mimeType='application/vnd.google-apps.folder' and trashed=false");
|
const q = encodeURIComponent("mimeType='application/vnd.google-apps.folder' and trashed=false");
|
||||||
const res = await fetch(`https://www.googleapis.com/drive/v3/files?q=${q}&pageSize=100&orderBy=name&fields=files(id,name,parents)`, {
|
const res = await gdriveFetch(`https://www.googleapis.com/drive/v3/files?q=${q}&pageSize=100&orderBy=name&fields=files(id,name,parents)`, {
|
||||||
headers: { Authorization: `Bearer ${accessToken}` }
|
headers: { Authorization: `Bearer ${accessToken}` }
|
||||||
});
|
});
|
||||||
const data = await res.json().catch(() => ({}));
|
const data = await res.json().catch(() => ({}));
|
||||||
|
|
@ -2602,7 +2673,7 @@ async function gdriveListFolders(accessToken) {
|
||||||
|
|
||||||
async function gdriveCreateFolder(accessToken, folderName) {
|
async function gdriveCreateFolder(accessToken, folderName) {
|
||||||
const cleanName = (folderName || '').trim() || 'ForkFolio Backups';
|
const cleanName = (folderName || '').trim() || 'ForkFolio Backups';
|
||||||
const res = await fetch('https://www.googleapis.com/drive/v3/files?fields=id,name', {
|
const res = await gdriveFetch('https://www.googleapis.com/drive/v3/files?fields=id,name', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
|
@ -2628,7 +2699,7 @@ async function gdriveListBackupFiles(accessToken, folderId) {
|
||||||
clauses.push("name contains 'forkfolio-backup'");
|
clauses.push("name contains 'forkfolio-backup'");
|
||||||
}
|
}
|
||||||
const q = encodeURIComponent(clauses.join(' and '));
|
const q = encodeURIComponent(clauses.join(' and '));
|
||||||
const res = await fetch(
|
const res = await gdriveFetch(
|
||||||
`https://www.googleapis.com/drive/v3/files?q=${q}&pageSize=150&orderBy=createdTime desc&fields=files(id,name,size,createdTime,modifiedTime,mimeType,parents)`,
|
`https://www.googleapis.com/drive/v3/files?q=${q}&pageSize=150&orderBy=createdTime desc&fields=files(id,name,size,createdTime,modifiedTime,mimeType,parents)`,
|
||||||
{ headers: { Authorization: `Bearer ${accessToken}` } }
|
{ headers: { Authorization: `Bearer ${accessToken}` } }
|
||||||
);
|
);
|
||||||
|
|
@ -2662,7 +2733,7 @@ async function gdriveUploadFile(accessToken, folderId, filename, buffer, mimeTyp
|
||||||
const epilogue = Buffer.from(`\r\n--${boundary}--`, 'utf8');
|
const epilogue = Buffer.from(`\r\n--${boundary}--`, 'utf8');
|
||||||
const multipartBody = Buffer.concat([preamble, buffer, epilogue]);
|
const multipartBody = Buffer.concat([preamble, buffer, epilogue]);
|
||||||
|
|
||||||
const res = await fetch('https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id,name,size', {
|
const res = await gdriveFetch('https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id,name,size', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
|
@ -2679,7 +2750,7 @@ async function gdriveUploadFile(accessToken, folderId, filename, buffer, mimeTyp
|
||||||
}
|
}
|
||||||
|
|
||||||
async function gdriveDownloadFile(accessToken, fileId) {
|
async function gdriveDownloadFile(accessToken, fileId) {
|
||||||
const res = await fetch(`https://www.googleapis.com/drive/v3/files/${encodeURIComponent(fileId)}?alt=media`, {
|
const res = await gdriveFetch(`https://www.googleapis.com/drive/v3/files/${encodeURIComponent(fileId)}?alt=media`, {
|
||||||
headers: { Authorization: `Bearer ${accessToken}` }
|
headers: { Authorization: `Bearer ${accessToken}` }
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
|
@ -2691,7 +2762,7 @@ async function gdriveDownloadFile(accessToken, fileId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function gdriveDeleteFile(accessToken, fileId) {
|
async function gdriveDeleteFile(accessToken, fileId) {
|
||||||
const res = await fetch(`https://www.googleapis.com/drive/v3/files/${encodeURIComponent(fileId)}`, {
|
const res = await gdriveFetch(`https://www.googleapis.com/drive/v3/files/${encodeURIComponent(fileId)}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: { Authorization: `Bearer ${accessToken}` }
|
headers: { Authorization: `Bearer ${accessToken}` }
|
||||||
});
|
});
|
||||||
|
|
@ -3797,8 +3868,7 @@ async function discoverAndSyncCloudBackups() {
|
||||||
if (cfg.gdriveConnected) {
|
if (cfg.gdriveConnected) {
|
||||||
try {
|
try {
|
||||||
const accessToken = await getGoogleDriveAccessToken(cfg);
|
const accessToken = await getGoogleDriveAccessToken(cfg);
|
||||||
const folderId = await ensureGoogleDriveFolder(accessToken, cfg);
|
const driveFiles = await gdriveListBackupFiles(accessToken, cfg.gdriveFolderId || '');
|
||||||
const driveFiles = await gdriveListBackupFiles(accessToken, folderId);
|
|
||||||
|
|
||||||
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, '');
|
||||||
|
|
@ -3836,7 +3906,12 @@ async function discoverAndSyncCloudBackups() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (gdErr) {
|
} catch (gdErr) {
|
||||||
providerErrors.push(`Google Drive: ${gdErr.message}`);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3969,6 +4044,14 @@ 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
|
||||||
|
};
|
||||||
|
|
||||||
// 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) => {
|
||||||
if (!req.user.isAdmin) {
|
if (!req.user.isAdmin) {
|
||||||
|
|
@ -3979,16 +4062,29 @@ app.post('/api/admin/backups/inspect', requireAuth, async (req, res) => {
|
||||||
const cfg = getBackupConfig();
|
const cfg = getBackupConfig();
|
||||||
let fileBuf = null;
|
let fileBuf = null;
|
||||||
let resolvedName = filename || 'backup.db';
|
let resolvedName = filename || 'backup.db';
|
||||||
|
let cacheKey = '';
|
||||||
|
|
||||||
const rawBase64 = base64Data || dataBase64;
|
const rawBase64 = base64Data || dataBase64;
|
||||||
if (rawBase64) {
|
if (rawBase64) {
|
||||||
fileBuf = Buffer.from(rawBase64, 'base64');
|
fileBuf = Buffer.from(rawBase64, 'base64');
|
||||||
} else if (provider === 'gdrive' && fileId) {
|
} else if (provider === 'gdrive' && fileId) {
|
||||||
const token = await getGoogleDriveAccessToken(cfg);
|
cacheKey = `gdrive:${fileId}`;
|
||||||
fileBuf = await gdriveDownloadFile(token, 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);
|
||||||
|
}
|
||||||
} else if (provider === 's3' && objectKey) {
|
} else if (provider === 's3' && objectKey) {
|
||||||
fileBuf = await s3GetObject(cfg, objectKey);
|
cacheKey = `s3:${objectKey}`;
|
||||||
resolvedName = filename || 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);
|
const row = db.prepare('SELECT * FROM backup_history WHERE id = ?').get(historyId);
|
||||||
if (!row) return res.status(404).json({ error: 'Backup record not found.' });
|
if (!row) return res.status(404).json({ error: 'Backup record not found.' });
|
||||||
|
|
@ -3998,7 +4094,11 @@ app.post('/api/admin/backups/inspect', requireAuth, async (req, res) => {
|
||||||
const s3Target = s3List.find(o => o.format === preferredFormat) || s3List.find(o => o.format === 'db') || s3List[0];
|
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];
|
const gdTarget = gdList.find(f => f.format === preferredFormat) || gdList.find(f => f.format === 'db') || gdList[0];
|
||||||
|
|
||||||
if (gdTarget && (cfg.gdriveEnabled || cfg.gdriveConnected)) {
|
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);
|
const token = await getGoogleDriveAccessToken(cfg);
|
||||||
fileBuf = await gdriveDownloadFile(token, gdTarget.id);
|
fileBuf = await gdriveDownloadFile(token, gdTarget.id);
|
||||||
resolvedName = gdTarget.name || resolvedName;
|
resolvedName = gdTarget.name || resolvedName;
|
||||||
|
|
@ -4012,6 +4112,15 @@ app.post('/api/admin/backups/inspect', requireAuth, async (req, res) => {
|
||||||
return res.status(400).json({ error: 'Backup source is required to inspect contents.' });
|
return res.status(400).json({ error: 'Backup source is required to inspect contents.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cacheKey && fileBuf) {
|
||||||
|
lastInspectedBackupCache = {
|
||||||
|
cacheKey,
|
||||||
|
buffer: fileBuf,
|
||||||
|
filename: resolvedName,
|
||||||
|
expiresAt: Date.now() + 10 * 60 * 1000
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const parsedData = parseBackupBufferToData(fileBuf, resolvedName);
|
const parsedData = parseBackupBufferToData(fileBuf, resolvedName);
|
||||||
const inspection = inspectBackupUsersFromData(parsedData);
|
const inspection = inspectBackupUsersFromData(parsedData);
|
||||||
res.json({ success: true, ...inspection });
|
res.json({ success: true, ...inspection });
|
||||||
|
|
@ -4033,11 +4142,23 @@ app.post('/api/admin/backups/restore-cloud-file', requireAuth, async (req, res)
|
||||||
let resolvedName = filename || 'cloud-backup.db';
|
let resolvedName = filename || 'cloud-backup.db';
|
||||||
|
|
||||||
if (provider === 'gdrive' && fileId) {
|
if (provider === 'gdrive' && fileId) {
|
||||||
const token = await getGoogleDriveAccessToken(cfg);
|
const cacheKey = `gdrive:${fileId}`;
|
||||||
fileBuf = await gdriveDownloadFile(token, 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);
|
||||||
|
}
|
||||||
} else if (provider === 's3' && objectKey) {
|
} else if (provider === 's3' && objectKey) {
|
||||||
fileBuf = await s3GetObject(cfg, objectKey);
|
const cacheKey = `s3:${objectKey}`;
|
||||||
resolvedName = filename || 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 {
|
} 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.' });
|
||||||
}
|
}
|
||||||
|
|
@ -4111,8 +4232,12 @@ 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}`;
|
||||||
|
|
||||||
if (gdTarget && (cfg.gdriveEnabled || cfg.gdriveConnected)) {
|
if (lastInspectedBackupCache.cacheKey === cacheKey && lastInspectedBackupCache.buffer && lastInspectedBackupCache.expiresAt > Date.now()) {
|
||||||
|
fileBuf = lastInspectedBackupCache.buffer;
|
||||||
|
filename = lastInspectedBackupCache.filename || '';
|
||||||
|
} 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 || '';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue