= 1073741824) { return number_format($bytes / 1073741824, 2, ",", ".") . " GB"; } if ($bytes >= 1048576) { return number_format($bytes / 1048576, 2, ",", ".") . " MB"; } if ($bytes >= 1024) { return number_format($bytes / 1024, 1, ",", ".") . " KB"; } return $bytes . " B"; } function backupManageFindBackup(string $instance, string $filename): ?array { foreach (backupServerReadIndex()["backups"] as $backup) { if (!is_array($backup)) { continue; } if (($backup["instance"] ?? "") === $instance && ($backup["filename"] ?? "") === $filename) { return $backup; } } return null; } function backupManageSendDownload(string $instance, string $filename): void { $backup = backupManageFindBackup($instance, $filename); if ($backup === null) { throw new RuntimeException("Backup not found."); } $path = backupServerBackupPath($instance, $filename); if (is_file($path)) { $size = filesize($path); $handle = fopen($path, "rb"); if ($size === false || $handle === false) { throw new RuntimeException("Backup cannot be opened."); } header("Content-Type: application/zip"); header("Content-Disposition: attachment; filename=\"" . addcslashes($instance . "-" . $filename, "\"\\") . "\""); header("Content-Length: " . (string) $size); header("Cache-Control: private, no-store"); header("X-Content-Type-Options: nosniff"); fpassthru($handle); fclose($handle); exit; } if (!empty($backup["s3_uploaded_at"])) { if (!backupS3Enabled()) { throw new RuntimeException("Backup is stored in S3, but S3 is not configured. See config.php."); } $key = (string) ($backup["s3_key"] ?? ""); if ($key === "") { $key = backupS3ObjectKey($instance, $filename); } backupS3SendObjectToOutput($key, $instance . "-" . $filename, (int) ($backup["size"] ?? 0)); } throw new RuntimeException("Backup not found in any store."); } function backupManageDeleteBackup(string $instance, string $filename): void { $index = backupServerReadIndex(); $kept = []; $found = null; foreach ($index["backups"] as $backup) { if ( is_array($backup) && ($backup["instance"] ?? "") === $instance && ($backup["filename"] ?? "") === $filename ) { $found = $backup; continue; } $kept[] = $backup; } if ($found === null) { throw new RuntimeException("Backup not found."); } // Delete the S3 object first: if that fails, nothing is changed, so no // object is ever stranded in the bucket without an index record. if (!empty($found["s3_uploaded_at"])) { if (!backupS3Enabled()) { throw new RuntimeException("Backup has an S3 copy, but S3 is not configured. See config.php."); } $key = (string) ($found["s3_key"] ?? ""); if ($key === "") { $key = backupS3ObjectKey($instance, $filename); } backupS3DeleteObject($key); } $path = backupServerBackupPath($instance, $filename); if (is_file($path)) { unlink($path); } backupServerWriteIndex($kept); } function backupManageAddInstance(string $instance): void { $instance = backupServerValidateInstance($instance); $settings = backupServerGetSettings(); $settings["instances"][] = $instance; backupServerWriteSettings($settings); } function backupManageRemoveInstance(string $instance): void { $instance = backupServerValidateInstance($instance); $settings = backupServerGetSettings(); $settings["instances"] = array_values( array_filter($settings["instances"], function ($existing) use ($instance) { return $existing !== $instance; }), ); backupServerWriteSettings($settings); } function backupManageGroupBackupsByInstance(array $backups): array { $grouped = []; foreach ($backups as $backup) { if (!is_array($backup)) { continue; } $instance = (string) ($backup["instance"] ?? ""); $filename = basename((string) ($backup["filename"] ?? "")); if ($instance === "" || $filename === "") { continue; } $backup["filename"] = $filename; $path = backupServerBackupPath($instance, $filename); $backup["local_exists"] = is_file($path); $backup["size"] = $backup["local_exists"] ? (int) (filesize($path) ?: ($backup["size"] ?? 0)) : (int) ($backup["size"] ?? 0); $grouped[$instance][] = $backup; } ksort($grouped); foreach ($grouped as &$records) { usort($records, function ($left, $right) { return strcmp((string) ($right["uploaded_at"] ?? ""), (string) ($left["uploaded_at"] ?? "")); }); } unset($records); return $grouped; } function backupManageStorageLabel(array $backup): string { $local = !empty($backup["local_exists"]); $inS3 = !empty($backup["s3_uploaded_at"]); if ($local && $inS3) { return "Local + S3"; } if ($local && backupS3Enabled() && empty($backup["s3_expired"])) { return "Local (S3 pending)"; } if ($local) { return "Local"; } if ($inS3) { return "S3 only"; } return "Missing"; } function backupManageLogTail(int $lines): array { $file = (string) BACKUP_SERVER_LOG_FILE; if (!is_file($file)) { return []; } $content = @file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); if (!is_array($content)) { return []; } return array_slice($content, -$lines); } if ($_SERVER["REQUEST_METHOD"] === "POST") { $action = (string) ($_POST["action"] ?? ""); if ($action === "login") { if (!backupManagePasswordConfigured()) { $errors[] = "No password is configured."; } elseif (backupManagePasswordMatches((string) ($_POST["password"] ?? ""))) { session_regenerate_id(true); $_SESSION["backup_server_logged_in"] = true; $messages[] = "Logged in."; } else { $errors[] = "Wrong password."; } } elseif ($action === "logout") { unset($_SESSION["backup_server_logged_in"], $_SESSION["backup_server_csrf_token"]); $messages[] = "Logged out."; } elseif (!backupManageIsLoggedIn()) { $errors[] = "Login required."; } elseif (!backupManageCsrfIsValid((string) ($_POST["csrf_token"] ?? ""))) { $errors[] = "Invalid token. Please reload the page and try again."; } else { try { if ($action === "update_retention") { $settings = backupServerGetSettings(); $settings["retention"] = max(1, (int) ($_POST["retention"] ?? BACKUP_SERVER_RETENTION)); if (isset($_POST["s3_retention"])) { $settings["s3_retention"] = max(1, (int) $_POST["s3_retention"]); } backupServerWriteSettings($settings); // May issue S3 deletes for backups that now age out of the archive. backupServerApplyRetentionAll(); $messages[] = "Retention updated."; } elseif ($action === "add_instance") { backupManageAddInstance((string) ($_POST["instance"] ?? "")); $messages[] = "Instance added."; } elseif ($action === "remove_instance") { backupManageRemoveInstance((string) ($_POST["instance"] ?? "")); $messages[] = "Instance removed."; } elseif ($action === "download") { backupManageSendDownload( backupServerValidateInstance((string) ($_POST["instance"] ?? "")), backupManageValidateFilename((string) ($_POST["filename"] ?? "")), ); } elseif ($action === "delete") { backupManageDeleteBackup( backupServerValidateInstance((string) ($_POST["instance"] ?? "")), backupManageValidateFilename((string) ($_POST["filename"] ?? "")), ); $messages[] = "Backup deleted."; } elseif ($action === "s3_sync") { if (!backupS3Enabled()) { throw new RuntimeException("S3 is not configured. See config.php."); } $uploaded = 0; $pending = 0; foreach (backupServerIndexInstances() as $syncInstance) { $result = backupServerSyncInstanceS3($syncInstance); $uploaded += $result["uploaded"]; $pending += $result["pending"]; if ($result["error"] !== null) { $errors[] = "S3 upload for " . $syncInstance . " failed: " . $result["error"]; } backupServerApplyRetention($syncInstance); } $messages[] = "S3 sync finished: " . $uploaded . " uploaded, " . $pending . " still pending."; } } catch (Throwable $exception) { $errors[] = $exception->getMessage(); } } } try { $settings = backupServerGetSettings(); $groupedBackups = backupManageGroupBackupsByInstance(backupServerReadIndex()["backups"]); } catch (Throwable $exception) { $settings = [ "retention" => max(1, (int) BACKUP_SERVER_RETENTION), "s3_retention" => max(1, (int) BACKUP_SERVER_S3_RETENTION), "instances" => [], ]; $groupedBackups = []; $errors[] = $exception->getMessage(); } $s3Enabled = backupS3Enabled(); $s3PendingCount = 0; $s3LastErrors = []; if ($s3Enabled) { foreach ($groupedBackups as $instanceBackups) { foreach ($instanceBackups as $backup) { if (!empty($backup["local_exists"]) && empty($backup["s3_uploaded_at"])) { $s3PendingCount++; } if (!empty($backup["s3_last_error"]) && count($s3LastErrors) < 5) { $s3LastErrors[] = ($backup["instance"] ?? "") . "/" . ($backup["filename"] ?? "") . ": " . $backup["s3_last_error"]; } } } } $s3LogTail = backupManageLogTail(20); ?>
Error:
S3 storage is not configured. Set the BACKUP_SERVER_S3_* constants in config.php to enable it.
Endpoint: ,
Bucket:
, Prefix:
Pending uploads:
Recent upload errors:
Distributed instances should upload to upload.php.
No instances allowed. Uploads will be rejected until an instance is added.
| Instance | Actions |
|---|---|
No backups uploaded.
$backups): ?>| Uploaded | Filename | Size | Storage | SHA-256 | Source IP | Actions |
|---|---|---|---|---|---|---|
| "> |