#!/usr/bin/env php Site Backups). * * Usage: * php cli/backup.php --action=run # one full backup + prune * php cli/backup.php --action=prune # apply retention only * php cli/backup.php --action=status # show current state * php cli/backup.php --action=list # list backup files * php cli/backup.php --action=upload-remote --file=example.com-backup-... * * Cron example (one nightly run): * 0 3 * * * php /path/to/site/cli/backup.php --action=run * * Exit codes: * 0 success (or status/list completed) * 1 failure (config missing, ZIP error, etc.) * 2 invalid arguments * 3 another run is in progress (lock contention) */ // Command-line tools must never run as a web request, even where a server ignores .htaccess. if (PHP_SAPI !== 'cli' && isset($_SERVER['REQUEST_METHOD'])) { http_response_code(404); exit; } $projectRoot = dirname(__DIR__); if (!file_exists($projectRoot . '/route.php')) { fwrite(STDERR, "Error: Run this script from the Raiju project root.\n"); fwrite(STDERR, " php /path/to/raiju/cli/backup.php --action=run\n"); exit(1); } // ── CLI argument parsing ────────────────────────────────────────────── $opts = []; for ($i = 1; $i < count($argv); $i++) { $a = $argv[$i]; if (preg_match('/^--([a-z-]+)=(.+)$/', $a, $m)) { $opts[$m[1]] = $m[2]; } elseif (preg_match('/^--([a-z-]+)$/', $a, $m)) { $opts[$m[1]] = true; } } if (isset($opts['help']) || !isset($opts['action'])) { echo <<= 1024 * 1024 * 1024) return number_format($bytes / 1024 / 1024 / 1024, 2) . ' GB'; if ($bytes >= 1024 * 1024) return number_format($bytes / 1024 / 1024, 1) . ' MB'; if ($bytes >= 1024) return number_format($bytes / 1024, 1) . ' KB'; return $bytes . ' B'; } // ── Actions ─────────────────────────────────────────────────────────── switch ($action) { case 'run': $tier = $opts['tier'] ?? null; $config = backupConfig(); if (!$config['enabled'] && !isset($opts['tier'])) { // Allow manual --tier override even when scheduled backups are off. fwrite(STDERR, "Backups are disabled in settings. Enable via dashboard or use --tier=manual to force one run.\n"); exit(0); } try { $skipRemote = !empty($GLOBALS['opts']['skip-remote']); $result = backupWithLock(function() use ($tier, $skipRemote) { $started = time(); if (!($GLOBALS['quiet'] ?? false)) { echo "[" . date('c', $started) . "] Backup run starting (tier=" . ($tier ?? 'auto') . ")...\n"; } return backupRun($started, $tier, !$skipRemote); }); } catch (BackupLockException $e) { fwrite(STDERR, $e->getMessage() . "\n"); exit(3); } $created = $result['created']; if ($created['ok']) { echo "Created: {$created['file']} (" . fmtSize($created['size']) . ", tier={$created['tier']})\n"; if (!empty($result['deleted'])) { echo "Pruned: " . count($result['deleted']) . " older backup(s)\n"; if (!$quiet) { foreach ($result['deleted'] as $f) echo " - $f\n"; } } if (!$quiet && !empty($result['remote'])) { echo "Remote uploads:\n"; foreach ($result['remote'] as $remote) { echo " - {$remote['name']}: " . ($remote['ok'] ? 'ok' : 'failed') . " ({$remote['message']})\n"; } } $status = backupStatus(); echo "Total stored: {$status['count']} backups, " . fmtSize($status['total_bytes']) . "\n"; exit(0); } else { fwrite(STDERR, "Backup failed: " . $created['message'] . "\n"); exit(1); } case 'prune': $deleted = backupWithLock(fn() => backupPrune()); echo "Pruned: " . count($deleted) . " backup(s)\n"; if (!$quiet) { foreach ($deleted as $f) echo " - $f\n"; } $status = backupStatus(); echo "Remaining: {$status['count']} backups, " . fmtSize($status['total_bytes']) . "\n"; exit(0); case 'status': $s = backupStatus(); echo "Scheduled backups: " . ($s['enabled'] ? 'enabled' : 'disabled') . "\n"; echo "Retention:\n"; foreach ($s['retention'] as $tier => $n) { echo " $tier: keep $n\n"; } echo "Storage limit: " . ($s['storage_limit_mb'] ? $s['storage_limit_mb'] . ' MB' : 'unlimited') . "\n"; echo "\n"; echo "Stored: {$s['count']} backups, " . fmtSize($s['total_bytes']) . "\n"; echo "Newest: " . ($s['newest'] ? date('c', $s['newest']) : '-') . "\n"; echo "Oldest: " . ($s['oldest'] ? date('c', $s['oldest']) : '-') . "\n"; echo "Last run: " . ($s['last_run'] ?: '-') . " (" . ($s['last_status'] ?: '-') . ")\n"; if ($s['last_message']) { echo "Last message: {$s['last_message']}\n"; } echo "Remote targets: " . count($s['remote_targets']) . "\n"; foreach ($s['remote_targets'] as $target) { echo " {$target['id']} ({$target['type']}): " . ($target['enabled'] ? 'enabled' : 'disabled') . ', last=' . ($target['last_status'] ?: '-') . "\n"; } exit(0); case 'list': $list = backupListAll(); if (empty($list)) { echo "No backups found.\n"; exit(0); } echo str_pad('FILE', 60) . str_pad('TIER', 10) . str_pad('SIZE', 12) . "DATE\n"; echo str_repeat('-', 100) . "\n"; foreach ($list as $b) { echo str_pad($b['file'], 60) . str_pad($b['tier'], 10) . str_pad(fmtSize($b['size']), 12) . date('Y-m-d H:i:s', $b['mtime']) . "\n"; } exit(0); case 'upload-remote': $file = $opts['file'] ?? ''; if (!backupIsPoolFilename($file)) { fwrite(STDERR, "Invalid or missing --file value.\n"); exit(2); } if (!is_file(BACKUP_PATH . $file)) { fwrite(STDERR, "Backup file not found: $file\n"); exit(1); } try { $results = backupWithLock(fn() => backupUploadRemoteTargets($file, $opts['target'] ?? null)); } catch (BackupLockException $e) { fwrite(STDERR, $e->getMessage() . "\n"); exit(3); } $failed = 0; foreach ($results as $remote) { echo "{$remote['name']}: " . ($remote['ok'] ? 'ok' : 'failed') . " ({$remote['message']})\n"; if (!$remote['ok']) $failed++; } exit($failed > 0 ? 1 : 0); default: fwrite(STDERR, "Unknown action: $action (try --help)\n"); exit(2); }