= 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); ?> Backup Management

Backup Management

Error:


Settings


">


">

S3 archive

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:

S3 log (last lines)

Upload endpoint

Distributed instances should upload to upload.php.

Allowed instances


No instances allowed. Uploads will be rejected until an instance is added.

Instance Actions

Backups

No backups uploaded.

$backups): ?>

Uploaded Filename Size Storage SHA-256 Source IP Actions
">
">
">