Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 4 additions & 144 deletions app/api/cron/backup/route.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,8 @@
import { NextResponse } from 'next/server';
import { v4 as uuidv4 } from 'uuid';
import {
getConfig,
getActiveProjects,
getRunHistory,
addRun,
updateRun,
updateProjectActivity,
verifyCronSecret,
createBackup,
getScheduledBackups,
generateBackupEnvironmentId,
enforceRetention,
type BackupRun,
type BackupType,
handleCronBackup,
type CronBackupResponse,
type CronBackupResult,
} from '@casperjuel/datocms-backup-api';

export const dynamic = 'force-dynamic';
Expand All @@ -31,137 +18,10 @@ export async function GET(): Promise<NextResponse<CronBackupResponse | { error:
);
}

const results: CronBackupResult[] = [];
const timestamp = new Date().toISOString();

console.log(`[CRON] Backup cron started at ${timestamp}`);

try {
const projectIds = await getActiveProjects();
console.log(`[CRON] Found ${projectIds.length} active projects`);

for (const projectId of projectIds) {
try {
const config = await getConfig(projectId);

if (!config) {
console.log(`[CRON] No config found for project ${projectId}, skipping`);
continue;
}

const { runs } = await getRunHistory(projectId, 100, 0);
const lastRuns: Record<BackupType, Date | null> = {
daily: null,
weekly: null,
monthly: null,
manual: null,
};

for (const run of runs) {
// Track when each type was last triggered (any status)
// We only care if we already started one, not whether it succeeded
if (!lastRuns[run.type]) {
lastRuns[run.type] = new Date(run.startedAt);
}
}

const scheduledBackups = getScheduledBackups(config, lastRuns);
console.log(`[CRON] Project ${projectId}: ${scheduledBackups.length} backups due`);

// Only run ONE backup per cron to avoid timeout (backups take ~200s each)
// Priority: daily > weekly > monthly
const backupToRun = scheduledBackups[0];
if (!backupToRun) continue;

const backupsToProcess = [backupToRun];
for (const backup of backupsToProcess) {
const targetEnvironmentId = generateBackupEnvironmentId(backup.schedule.prefix);

const run: BackupRun = {
id: uuidv4(),
projectId,
type: backup.type,
status: 'in_progress',
sourceEnvironment: config.sourceEnvironment,
targetEnvironment: targetEnvironmentId,
startedAt: new Date().toISOString(),
metadata: {
triggeredBy: 'cron',
},
};

await addRun(run);

const startTime = Date.now();
const result = await createBackup(
config.apiToken,
config.sourceEnvironment,
targetEnvironmentId
);
const duration = Date.now() - startTime;

const completedRun: BackupRun = {
...run,
status: result.success ? 'completed' : 'failed',
completedAt: new Date().toISOString(),
duration,
error: result.error,
metadata: {
...run.metadata,
environmentId: result.environmentId,
},
};

try {
await updateRun(completedRun);
} catch (updateError) {
console.error(`[CRON] Failed to update run status for ${run.id}:`, updateError);
}

results.push({
projectId,
type: backup.type,
runId: run.id,
status: result.success ? 'started' : 'error',
error: result.error,
});

console.log(
`[CRON] Project ${projectId}: ${backup.type} backup ${result.success ? 'completed' : 'failed'}`
);

if (result.success) {
try {
const retentionResult = await enforceRetention(config, backup.type);
console.log(
`[CRON] Project ${projectId}: cleaned up ${retentionResult.deletedEnvironments.length} old environments`
);
} catch (error) {
console.error(`[CRON] Retention cleanup error:`, error);
}
}
}

await updateProjectActivity(projectId);
} catch (error) {
console.error(`[CRON] Error processing project ${projectId}:`, error);
results.push({
projectId,
type: 'daily',
runId: '',
status: 'error',
error: error instanceof Error ? error.message : 'Unknown error',
});
}
}

console.log(`[CRON] Backup cron completed. Processed ${results.length} backups`);

return NextResponse.json({
success: true,
executed: results,
timestamp,
});
// All logic is in the package - just call the handler
const result = await handleCronBackup();
return NextResponse.json(result);
} catch (error) {
console.error('[CRON] Critical error:', error);
return NextResponse.json(
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
"private": true,
"scripts": {
"dev": "next dev --port 3001",
"build": "next build",
"build": "npm update @casperjuel/datocms-backup-api && next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@casperjuel/datocms-backup-api": "^1.0.3",
"@casperjuel/datocms-backup-api": "^1.0.8",
"@vercel/kv": "^3.0.0",
"next": "^15.0.0",
"react": "^19.0.0",
Expand Down