|
|
@@ -2,25 +2,7 @@
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
-$baseDir = __DIR__;
|
|
|
-$configFile = $baseDir . "/config.php";
|
|
|
-
|
|
|
-if (is_file($configFile)) {
|
|
|
- require_once $configFile;
|
|
|
-}
|
|
|
-
|
|
|
-if (!defined("BACKUP_SERVER_RETENTION")) {
|
|
|
- define("BACKUP_SERVER_RETENTION", 30);
|
|
|
-}
|
|
|
-if (!defined("BACKUP_SERVER_BACKUP_DIR")) {
|
|
|
- define("BACKUP_SERVER_BACKUP_DIR", $baseDir . "/backups/");
|
|
|
-}
|
|
|
-if (!defined("BACKUP_SERVER_INDEX_FILE")) {
|
|
|
- define("BACKUP_SERVER_INDEX_FILE", rtrim((string) BACKUP_SERVER_BACKUP_DIR, "/\\") . "/index.json");
|
|
|
-}
|
|
|
-if (!defined("BACKUP_SERVER_SETTINGS_FILE")) {
|
|
|
- define("BACKUP_SERVER_SETTINGS_FILE", rtrim((string) BACKUP_SERVER_BACKUP_DIR, "/\\") . "/settings.json");
|
|
|
-}
|
|
|
+require_once __DIR__ . "/lib.php";
|
|
|
|
|
|
if (session_status() === PHP_SESSION_NONE) {
|
|
|
ini_set("session.use_strict_mode", "1");
|
|
|
@@ -75,139 +57,6 @@ function backupManageCsrfIsValid(string $token): bool
|
|
|
hash_equals($_SESSION["backup_server_csrf_token"], $token);
|
|
|
}
|
|
|
|
|
|
-function backupManageEnsureDirectory(string $dir): void
|
|
|
-{
|
|
|
- if (!is_dir($dir) && !mkdir($dir, 02775, true) && !is_dir($dir)) {
|
|
|
- throw new RuntimeException("Directory cannot be created: " . $dir);
|
|
|
- }
|
|
|
-
|
|
|
- @chmod($dir, 02775);
|
|
|
-}
|
|
|
-
|
|
|
-function backupManageReadJsonFile(string $file): array
|
|
|
-{
|
|
|
- if (!is_file($file)) {
|
|
|
- return [];
|
|
|
- }
|
|
|
-
|
|
|
- $decoded = json_decode((string) file_get_contents($file), true);
|
|
|
- if (!is_array($decoded)) {
|
|
|
- throw new RuntimeException("JSON file is invalid: " . basename($file));
|
|
|
- }
|
|
|
-
|
|
|
- return $decoded;
|
|
|
-}
|
|
|
-
|
|
|
-function backupManageWriteJsonFile(string $file, array $data): void
|
|
|
-{
|
|
|
- backupManageEnsureDirectory(dirname($file));
|
|
|
-
|
|
|
- $json = json_encode(
|
|
|
- $data,
|
|
|
- JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE,
|
|
|
- );
|
|
|
- if ($json === false) {
|
|
|
- throw new RuntimeException("JSON cannot be encoded.");
|
|
|
- }
|
|
|
-
|
|
|
- $tmpFile = $file . ".tmp";
|
|
|
- if (file_put_contents($tmpFile, $json . PHP_EOL, LOCK_EX) === false) {
|
|
|
- throw new RuntimeException("JSON cannot be written.");
|
|
|
- }
|
|
|
-
|
|
|
- @chmod($tmpFile, 0664);
|
|
|
- if (!rename($tmpFile, $file)) {
|
|
|
- @unlink($tmpFile);
|
|
|
- throw new RuntimeException("JSON cannot be saved.");
|
|
|
- }
|
|
|
-
|
|
|
- @chmod($file, 0664);
|
|
|
-}
|
|
|
-
|
|
|
-function backupManageReadIndex(): array
|
|
|
-{
|
|
|
- $index = backupManageReadJsonFile((string) BACKUP_SERVER_INDEX_FILE);
|
|
|
- $backups = isset($index["backups"]) && is_array($index["backups"])
|
|
|
- ? $index["backups"]
|
|
|
- : [];
|
|
|
-
|
|
|
- return ["backups" => array_values($backups)];
|
|
|
-}
|
|
|
-
|
|
|
-function backupManageWriteIndex(array $backups): void
|
|
|
-{
|
|
|
- backupManageWriteJsonFile((string) BACKUP_SERVER_INDEX_FILE, [
|
|
|
- "backups" => array_values($backups),
|
|
|
- ]);
|
|
|
-}
|
|
|
-
|
|
|
-function backupManageReadSettings(): array
|
|
|
-{
|
|
|
- $settings = backupManageReadJsonFile((string) BACKUP_SERVER_SETTINGS_FILE);
|
|
|
- $retention = isset($settings["retention"])
|
|
|
- ? max(1, (int) $settings["retention"])
|
|
|
- : max(1, (int) BACKUP_SERVER_RETENTION);
|
|
|
- $instances =
|
|
|
- isset($settings["instances"]) && is_array($settings["instances"])
|
|
|
- ? $settings["instances"]
|
|
|
- : [];
|
|
|
- $allowedInstances = [];
|
|
|
-
|
|
|
- foreach ($instances as $instance) {
|
|
|
- try {
|
|
|
- $allowedInstances[] = backupManageValidateInstance((string) $instance);
|
|
|
- } catch (Throwable $exception) {
|
|
|
- continue;
|
|
|
- }
|
|
|
- }
|
|
|
- $allowedInstances = array_values(array_unique($allowedInstances));
|
|
|
- sort($allowedInstances);
|
|
|
-
|
|
|
- return [
|
|
|
- "retention" => $retention,
|
|
|
- "instances" => $allowedInstances,
|
|
|
- ];
|
|
|
-}
|
|
|
-
|
|
|
-function backupManageWriteSettings(array $settings): void
|
|
|
-{
|
|
|
- $instances =
|
|
|
- isset($settings["instances"]) && is_array($settings["instances"])
|
|
|
- ? $settings["instances"]
|
|
|
- : backupManageReadSettings()["instances"];
|
|
|
- $allowedInstances = [];
|
|
|
-
|
|
|
- foreach ($instances as $instance) {
|
|
|
- $allowedInstances[] = backupManageValidateInstance((string) $instance);
|
|
|
- }
|
|
|
- $allowedInstances = array_values(array_unique($allowedInstances));
|
|
|
- sort($allowedInstances);
|
|
|
-
|
|
|
- backupManageWriteJsonFile((string) BACKUP_SERVER_SETTINGS_FILE, [
|
|
|
- "retention" => max(1, (int) ($settings["retention"] ?? BACKUP_SERVER_RETENTION)),
|
|
|
- "instances" => $allowedInstances,
|
|
|
- ]);
|
|
|
-}
|
|
|
-
|
|
|
-function backupManageInstanceDir(string $instance): string
|
|
|
-{
|
|
|
- return rtrim((string) BACKUP_SERVER_BACKUP_DIR, "/\\") . DIRECTORY_SEPARATOR . $instance;
|
|
|
-}
|
|
|
-
|
|
|
-function backupManageValidateInstance(string $instance): string
|
|
|
-{
|
|
|
- $instance = trim($instance);
|
|
|
- if (
|
|
|
- $instance === "" ||
|
|
|
- strlen($instance) > 120 ||
|
|
|
- preg_match('/^[A-Za-z0-9][A-Za-z0-9._-]*$/', $instance) !== 1
|
|
|
- ) {
|
|
|
- throw new RuntimeException("Invalid instance.");
|
|
|
- }
|
|
|
-
|
|
|
- return $instance;
|
|
|
-}
|
|
|
-
|
|
|
function backupManageValidateFilename(string $filename): string
|
|
|
{
|
|
|
$filename = basename(trim($filename));
|
|
|
@@ -234,7 +83,7 @@ function backupManageFormatBytes(int $bytes): string
|
|
|
|
|
|
function backupManageFindBackup(string $instance, string $filename): ?array
|
|
|
{
|
|
|
- foreach (backupManageReadIndex()["backups"] as $backup) {
|
|
|
+ foreach (backupServerReadIndex()["backups"] as $backup) {
|
|
|
if (!is_array($backup)) {
|
|
|
continue;
|
|
|
}
|
|
|
@@ -246,11 +95,6 @@ function backupManageFindBackup(string $instance, string $filename): ?array
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
-function backupManageBackupPath(string $instance, string $filename): string
|
|
|
-{
|
|
|
- return backupManageInstanceDir($instance) . DIRECTORY_SEPARATOR . $filename;
|
|
|
-}
|
|
|
-
|
|
|
function backupManageSendDownload(string $instance, string $filename): void
|
|
|
{
|
|
|
$backup = backupManageFindBackup($instance, $filename);
|
|
|
@@ -258,29 +102,46 @@ function backupManageSendDownload(string $instance, string $filename): void
|
|
|
throw new RuntimeException("Backup not found.");
|
|
|
}
|
|
|
|
|
|
- $path = backupManageBackupPath($instance, $filename);
|
|
|
- $size = filesize($path);
|
|
|
- $handle = fopen($path, "rb");
|
|
|
- if ($size === false || $handle === false) {
|
|
|
- throw new RuntimeException("Backup cannot be opened.");
|
|
|
+ $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;
|
|
|
}
|
|
|
|
|
|
- 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");
|
|
|
+ if (!empty($backup["s3_uploaded_at"])) {
|
|
|
+ if (!backupS3Enabled()) {
|
|
|
+ throw new RuntimeException("Backup is stored in S3, but S3 is not configured. See config.php.");
|
|
|
+ }
|
|
|
|
|
|
- fpassthru($handle);
|
|
|
- fclose($handle);
|
|
|
- exit;
|
|
|
+ $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 = backupManageReadIndex();
|
|
|
+ $index = backupServerReadIndex();
|
|
|
$kept = [];
|
|
|
- $found = false;
|
|
|
+ $found = null;
|
|
|
|
|
|
foreach ($index["backups"] as $backup) {
|
|
|
if (
|
|
|
@@ -288,97 +149,56 @@ function backupManageDeleteBackup(string $instance, string $filename): void
|
|
|
($backup["instance"] ?? "") === $instance &&
|
|
|
($backup["filename"] ?? "") === $filename
|
|
|
) {
|
|
|
- $found = true;
|
|
|
+ $found = $backup;
|
|
|
continue;
|
|
|
}
|
|
|
$kept[] = $backup;
|
|
|
}
|
|
|
|
|
|
- if (!$found) {
|
|
|
+ if ($found === null) {
|
|
|
throw new RuntimeException("Backup not found.");
|
|
|
}
|
|
|
|
|
|
- $path = backupManageBackupPath($instance, $filename);
|
|
|
- if (is_file($path)) {
|
|
|
- unlink($path);
|
|
|
- }
|
|
|
-
|
|
|
- backupManageWriteIndex($kept);
|
|
|
-}
|
|
|
-
|
|
|
-function backupManageApplyRetentionForInstance(string $instance): void
|
|
|
-{
|
|
|
- $index = backupManageReadIndex();
|
|
|
- $retention = backupManageReadSettings()["retention"];
|
|
|
- $instanceBackups = [];
|
|
|
- $otherBackups = [];
|
|
|
-
|
|
|
- foreach ($index["backups"] as $backup) {
|
|
|
- if (!is_array($backup)) {
|
|
|
- continue;
|
|
|
- }
|
|
|
- if (($backup["instance"] ?? "") === $instance) {
|
|
|
- $instanceBackups[] = $backup;
|
|
|
- } else {
|
|
|
- $otherBackups[] = $backup;
|
|
|
+ // 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.");
|
|
|
}
|
|
|
- }
|
|
|
-
|
|
|
- usort($instanceBackups, function ($left, $right) {
|
|
|
- return strcmp((string) ($right["uploaded_at"] ?? ""), (string) ($left["uploaded_at"] ?? ""));
|
|
|
- });
|
|
|
|
|
|
- $keep = array_slice($instanceBackups, 0, $retention);
|
|
|
- $remove = array_slice($instanceBackups, $retention);
|
|
|
-
|
|
|
- foreach ($remove as $backup) {
|
|
|
- $filename = basename((string) ($backup["filename"] ?? ""));
|
|
|
- if ($filename !== "") {
|
|
|
- $path = backupManageBackupPath($instance, $filename);
|
|
|
- if (is_file($path)) {
|
|
|
- @unlink($path);
|
|
|
- }
|
|
|
+ $key = (string) ($found["s3_key"] ?? "");
|
|
|
+ if ($key === "") {
|
|
|
+ $key = backupS3ObjectKey($instance, $filename);
|
|
|
}
|
|
|
+ backupS3DeleteObject($key);
|
|
|
}
|
|
|
|
|
|
- backupManageWriteIndex(array_merge($otherBackups, $keep));
|
|
|
-}
|
|
|
-
|
|
|
-function backupManageApplyRetentionAll(): void
|
|
|
-{
|
|
|
- $instances = [];
|
|
|
- foreach (backupManageReadIndex()["backups"] as $backup) {
|
|
|
- if (is_array($backup)) {
|
|
|
- $instance = (string) ($backup["instance"] ?? "");
|
|
|
- if ($instance !== "") {
|
|
|
- $instances[$instance] = true;
|
|
|
- }
|
|
|
- }
|
|
|
+ $path = backupServerBackupPath($instance, $filename);
|
|
|
+ if (is_file($path)) {
|
|
|
+ unlink($path);
|
|
|
}
|
|
|
|
|
|
- foreach (array_keys($instances) as $instance) {
|
|
|
- backupManageApplyRetentionForInstance($instance);
|
|
|
- }
|
|
|
+ backupServerWriteIndex($kept);
|
|
|
}
|
|
|
|
|
|
function backupManageAddInstance(string $instance): void
|
|
|
{
|
|
|
- $instance = backupManageValidateInstance($instance);
|
|
|
- $settings = backupManageReadSettings();
|
|
|
+ $instance = backupServerValidateInstance($instance);
|
|
|
+ $settings = backupServerGetSettings();
|
|
|
$settings["instances"][] = $instance;
|
|
|
- backupManageWriteSettings($settings);
|
|
|
+ backupServerWriteSettings($settings);
|
|
|
}
|
|
|
|
|
|
function backupManageRemoveInstance(string $instance): void
|
|
|
{
|
|
|
- $instance = backupManageValidateInstance($instance);
|
|
|
- $settings = backupManageReadSettings();
|
|
|
+ $instance = backupServerValidateInstance($instance);
|
|
|
+ $settings = backupServerGetSettings();
|
|
|
$settings["instances"] = array_values(
|
|
|
array_filter($settings["instances"], function ($existing) use ($instance) {
|
|
|
return $existing !== $instance;
|
|
|
}),
|
|
|
);
|
|
|
- backupManageWriteSettings($settings);
|
|
|
+ backupServerWriteSettings($settings);
|
|
|
}
|
|
|
|
|
|
function backupManageGroupBackupsByInstance(array $backups): array
|
|
|
@@ -390,11 +210,15 @@ function backupManageGroupBackupsByInstance(array $backups): array
|
|
|
}
|
|
|
$instance = (string) ($backup["instance"] ?? "");
|
|
|
$filename = basename((string) ($backup["filename"] ?? ""));
|
|
|
- if ($instance === "" || $filename === "" || !is_file(backupManageBackupPath($instance, $filename))) {
|
|
|
+ if ($instance === "" || $filename === "") {
|
|
|
continue;
|
|
|
}
|
|
|
$backup["filename"] = $filename;
|
|
|
- $backup["size"] = (int) (filesize(backupManageBackupPath($instance, $filename)) ?: ($backup["size"] ?? 0));
|
|
|
+ $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;
|
|
|
}
|
|
|
|
|
|
@@ -409,6 +233,42 @@ function backupManageGroupBackupsByInstance(array $backups): array
|
|
|
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"] ?? "");
|
|
|
|
|
|
@@ -432,11 +292,14 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
|
|
} else {
|
|
|
try {
|
|
|
if ($action === "update_retention") {
|
|
|
- $retention = max(1, (int) ($_POST["retention"] ?? BACKUP_SERVER_RETENTION));
|
|
|
- $settings = backupManageReadSettings();
|
|
|
- $settings["retention"] = $retention;
|
|
|
- backupManageWriteSettings($settings);
|
|
|
- backupManageApplyRetentionAll();
|
|
|
+ $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"] ?? ""));
|
|
|
@@ -446,15 +309,31 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
|
|
$messages[] = "Instance removed.";
|
|
|
} elseif ($action === "download") {
|
|
|
backupManageSendDownload(
|
|
|
- backupManageValidateInstance((string) ($_POST["instance"] ?? "")),
|
|
|
+ backupServerValidateInstance((string) ($_POST["instance"] ?? "")),
|
|
|
backupManageValidateFilename((string) ($_POST["filename"] ?? "")),
|
|
|
);
|
|
|
} elseif ($action === "delete") {
|
|
|
backupManageDeleteBackup(
|
|
|
- backupManageValidateInstance((string) ($_POST["instance"] ?? "")),
|
|
|
+ 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();
|
|
|
@@ -463,14 +342,36 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
- $settings = backupManageReadSettings();
|
|
|
- $groupedBackups = backupManageGroupBackupsByInstance(backupManageReadIndex()["backups"]);
|
|
|
+ $settings = backupServerGetSettings();
|
|
|
+ $groupedBackups = backupManageGroupBackupsByInstance(backupServerReadIndex()["backups"]);
|
|
|
} catch (Throwable $exception) {
|
|
|
- $settings = ["retention" => max(1, (int) BACKUP_SERVER_RETENTION)];
|
|
|
+ $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);
|
|
|
+
|
|
|
?>
|
|
|
<!DOCTYPE html>
|
|
|
<html lang="de">
|
|
|
@@ -510,12 +411,51 @@ try {
|
|
|
<input type="hidden" name="action" value="update_retention">
|
|
|
<input type="hidden" name="csrf_token" value="<?php echo backupManageEscape(backupManageCsrfToken()); ?>">
|
|
|
<p>
|
|
|
- <label for="retention">Backups retained per instance</label><br>
|
|
|
+ <label for="retention">Local backups retained per instance</label><br>
|
|
|
<input type="number" id="retention" name="retention" min="1" required value="<?php echo (int) $settings["retention"]; ?>">
|
|
|
</p>
|
|
|
+ <?php if ($s3Enabled): ?>
|
|
|
+ <p>
|
|
|
+ <label for="s3_retention">S3 backups retained per instance</label><br>
|
|
|
+ <input type="number" id="s3_retention" name="s3_retention" min="1" required value="<?php echo (int) $settings["s3_retention"]; ?>">
|
|
|
+ </p>
|
|
|
+ <?php endif; ?>
|
|
|
<button type="submit">Save retention</button>
|
|
|
</form>
|
|
|
|
|
|
+ <h2>S3 archive</h2>
|
|
|
+ <?php if (!$s3Enabled): ?>
|
|
|
+ <p>S3 storage is not configured. Set the <code>BACKUP_SERVER_S3_*</code> constants in <code>config.php</code> to enable it.</p>
|
|
|
+ <?php else: ?>
|
|
|
+ <p>
|
|
|
+ Endpoint: <code><?php echo backupManageEscape(BACKUP_SERVER_S3_ENDPOINT); ?></code>,
|
|
|
+ Bucket: <code><?php echo backupManageEscape(BACKUP_SERVER_S3_BUCKET); ?></code>
|
|
|
+ <?php if (trim((string) BACKUP_SERVER_S3_PREFIX, "/") !== ""): ?>
|
|
|
+ , Prefix: <code><?php echo backupManageEscape(trim((string) BACKUP_SERVER_S3_PREFIX, "/")); ?></code>
|
|
|
+ <?php endif; ?>
|
|
|
+ </p>
|
|
|
+ <p>Pending uploads: <?php echo (int) $s3PendingCount; ?></p>
|
|
|
+ <form method="POST">
|
|
|
+ <input type="hidden" name="action" value="s3_sync">
|
|
|
+ <input type="hidden" name="csrf_token" value="<?php echo backupManageEscape(backupManageCsrfToken()); ?>">
|
|
|
+ <button type="submit">Retry S3 uploads now</button>
|
|
|
+ </form>
|
|
|
+ <?php if (!empty($s3LastErrors)): ?>
|
|
|
+ <p><strong>Recent upload errors:</strong></p>
|
|
|
+ <ul>
|
|
|
+ <?php foreach ($s3LastErrors as $s3LastError): ?>
|
|
|
+ <li><?php echo backupManageEscape($s3LastError); ?></li>
|
|
|
+ <?php endforeach; ?>
|
|
|
+ </ul>
|
|
|
+ <?php endif; ?>
|
|
|
+ <?php if (!empty($s3LogTail)): ?>
|
|
|
+ <details>
|
|
|
+ <summary>S3 log (last <?php echo count($s3LogTail); ?> lines)</summary>
|
|
|
+ <pre><?php echo backupManageEscape(implode("\n", $s3LogTail)); ?></pre>
|
|
|
+ </details>
|
|
|
+ <?php endif; ?>
|
|
|
+ <?php endif; ?>
|
|
|
+
|
|
|
<h2>Upload endpoint</h2>
|
|
|
<p>Distributed instances should upload to <code>upload.php</code>.</p>
|
|
|
|
|
|
@@ -545,7 +485,7 @@ try {
|
|
|
<tr>
|
|
|
<td><?php echo backupManageEscape($instance); ?></td>
|
|
|
<td>
|
|
|
- <form method="POST" style="display:inline" onsubmit="return confirm('Remove this allowed instance? Existing backups remain visible.');">
|
|
|
+ <form method="POST" style="display:inline" onsubmit="return confirm('Remove this allowed instance? Existing backups remain visible; S3 objects remain until retention or manual delete.');">
|
|
|
<input type="hidden" name="action" value="remove_instance">
|
|
|
<input type="hidden" name="csrf_token" value="<?php echo backupManageEscape(backupManageCsrfToken()); ?>">
|
|
|
<input type="hidden" name="instance" value="<?php echo backupManageEscape($instance); ?>">
|
|
|
@@ -570,6 +510,7 @@ try {
|
|
|
<th>Uploaded</th>
|
|
|
<th>Filename</th>
|
|
|
<th>Size</th>
|
|
|
+ <th>Storage</th>
|
|
|
<th>SHA-256</th>
|
|
|
<th>Source IP</th>
|
|
|
<th>Actions</th>
|
|
|
@@ -581,6 +522,7 @@ try {
|
|
|
<td><?php echo backupManageEscape($backup["uploaded_at"] ?? ""); ?></td>
|
|
|
<td><?php echo backupManageEscape($backup["filename"] ?? ""); ?></td>
|
|
|
<td><?php echo backupManageEscape(backupManageFormatBytes((int) ($backup["size"] ?? 0))); ?></td>
|
|
|
+ <td title="<?php echo backupManageEscape($backup["s3_last_error"] ?? ""); ?>"><?php echo backupManageEscape(backupManageStorageLabel($backup)); ?></td>
|
|
|
<td><?php echo backupManageEscape($backup["sha256"] ?? ""); ?></td>
|
|
|
<td><?php echo backupManageEscape($backup["source_ip"] ?? ""); ?></td>
|
|
|
<td>
|
|
|
@@ -591,7 +533,7 @@ try {
|
|
|
<input type="hidden" name="filename" value="<?php echo backupManageEscape($backup["filename"] ?? ""); ?>">
|
|
|
<button type="submit">Download</button>
|
|
|
</form>
|
|
|
- <form method="POST" style="display:inline" onsubmit="return confirm('Delete this backup?');">
|
|
|
+ <form method="POST" style="display:inline" onsubmit="return confirm('Delete this backup from all stores?');">
|
|
|
<input type="hidden" name="action" value="delete">
|
|
|
<input type="hidden" name="csrf_token" value="<?php echo backupManageEscape(backupManageCsrfToken()); ?>">
|
|
|
<input type="hidden" name="instance" value="<?php echo backupManageEscape($instance); ?>">
|