feat: add 6h automated backup to Dropbox via Vercel cron

This commit is contained in:
Christian Vidal Wolf
2026-05-15 08:06:54 +02:00
parent cf531cfa40
commit b911ada50b
2 changed files with 141 additions and 1 deletions
+136
View File
@@ -0,0 +1,136 @@
const SUPABASE_URL = 'https://hwithddwaapyhnfwcesj.supabase.co';
const SUPABASE_ANON_KEY = process.env.SUPABASE_SERVICE_KEY || 'sb_publishable_fGXkh0bSrAOqSk2jWKAzSg_NJD9YPCv';
const DROPBOX_APP_KEY = process.env.DROPBOX_APP_KEY;
const DROPBOX_APP_SECRET = process.env.DROPBOX_APP_SECRET;
const DROPBOX_REFRESH_TOKEN = process.env.DROPBOX_REFRESH_TOKEN;
const BACKUP_SECRET = process.env.BACKUP_SECRET;
const DROPBOX_BACKUP_FOLDER = '/CrazeBackups';
const MAX_BACKUP_AGE_DAYS = 7;
async function getDropboxToken() {
const res = await fetch('https://api.dropboxapi.com/oauth2/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: DROPBOX_REFRESH_TOKEN,
client_id: DROPBOX_APP_KEY,
client_secret: DROPBOX_APP_SECRET,
}),
});
const data = await res.json();
if (!data.access_token) throw new Error('Dropbox token failed: ' + JSON.stringify(data));
return data.access_token;
}
async function fetchSupabaseTable(table) {
const PAGE_SIZE = 1000;
const rows = [];
let offset = 0;
for (let page = 0; page < 50; page++) {
const res = await fetch(
`${SUPABASE_URL}/rest/v1/${table}?select=*&limit=${PAGE_SIZE}&offset=${offset}`,
{ headers: { apikey: SUPABASE_ANON_KEY, Authorization: `Bearer ${SUPABASE_ANON_KEY}` } }
);
if (!res.ok) throw new Error(`Supabase ${table} fetch failed: ${res.status}`);
const batch = await res.json();
rows.push(...batch);
if (batch.length < PAGE_SIZE) break;
offset += PAGE_SIZE;
}
return rows;
}
async function uploadToDropbox(token, path, content) {
const res = await fetch('https://content.dropboxapi.com/2/files/upload', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/octet-stream',
'Dropbox-API-Arg': JSON.stringify({ path, mode: 'overwrite', autorename: false }),
},
body: content,
});
if (!res.ok) {
const txt = await res.text();
throw new Error(`Dropbox upload failed (${res.status}): ${txt}`);
}
return res.json();
}
async function deleteOldBackups(token) {
const listRes = await fetch('https://api.dropboxapi.com/2/files/list_folder', {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ path: DROPBOX_BACKUP_FOLDER, limit: 200 }),
});
if (!listRes.ok) return; // folder may not exist yet — skip
const list = await listRes.json();
const cutoff = Date.now() - MAX_BACKUP_AGE_DAYS * 24 * 60 * 60 * 1000;
for (const entry of list.entries || []) {
if (entry['.tag'] !== 'file') continue;
const modified = new Date(entry.server_modified).getTime();
if (modified < cutoff) {
await fetch('https://api.dropboxapi.com/2/files/delete_v2', {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ path: entry.path_lower }),
});
}
}
}
export default async function handler(req, res) {
// Allow cron (no origin) or requests with valid secret
const authHeader = req.headers['authorization'];
const isCron = !req.headers.origin;
const hasSecret = BACKUP_SECRET && authHeader === `Bearer ${BACKUP_SECRET}`;
if (!isCron && !hasSecret) {
return res.status(403).json({ error: 'Forbidden' });
}
if (req.method !== 'GET' && req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
const [syncedRows, history] = await Promise.all([
fetchSupabaseTable('synced_rows'),
fetchSupabaseTable('item_history'),
]);
const now = new Date();
const timestamp = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
const filename = `backup_${timestamp}.json`;
const path = `${DROPBOX_BACKUP_FOLDER}/${filename}`;
const payload = JSON.stringify({
created_at: now.toISOString(),
synced_rows_count: syncedRows.length,
history_count: history.length,
synced_rows: syncedRows,
history,
});
const token = await getDropboxToken();
await Promise.all([
uploadToDropbox(token, path, payload),
deleteOldBackups(token),
]);
console.log(`[backup] OK — ${filename} | rows: ${syncedRows.length} | history: ${history.length}`);
return res.json({
success: true,
filename,
synced_rows_count: syncedRows.length,
history_count: history.length,
});
} catch (err) {
console.error('[backup] error:', err.message);
return res.status(500).json({ success: false, error: err.message });
}
}
+5 -1
View File
@@ -3,7 +3,11 @@
{ "source": "/api/dropbox-proxy", "destination": "api/dropbox-proxy.js" }, { "source": "/api/dropbox-proxy", "destination": "api/dropbox-proxy.js" },
{ "source": "/api/dropbox-sync", "destination": "api/dropbox-sync.js" }, { "source": "/api/dropbox-sync", "destination": "api/dropbox-sync.js" },
{ "source": "/api/bc-proxy", "destination": "api/bc-proxy.js" }, { "source": "/api/bc-proxy", "destination": "api/bc-proxy.js" },
{ "source": "/api/bc-export", "destination": "api/bc-export.js" } { "source": "/api/bc-export", "destination": "api/bc-export.js" },
{ "source": "/api/backup", "destination": "api/backup.js" }
],
"crons": [
{ "path": "/api/backup", "schedule": "0 0,6,12,18 * * *" }
], ],
"headers": [ "headers": [
{ {