| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169 |
- <?php
- declare(strict_types=1);
- require_once __DIR__ . "/lib.php";
- header("Content-Type: application/json; charset=utf-8");
- header("Cache-Control: no-store");
- header("X-Content-Type-Options: nosniff");
- function backupUploadRespond(int $status, array $payload): void
- {
- http_response_code($status);
- echo json_encode(
- $payload,
- JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE,
- );
- exit;
- }
- function backupUploadInstanceIsAllowed(string $instance): bool
- {
- return in_array($instance, backupServerGetSettings()["instances"], true);
- }
- function backupUploadIsZipFile(string $path): bool
- {
- $handle = fopen($path, "rb");
- if ($handle === false) {
- return false;
- }
- $signature = fread($handle, 4);
- fclose($handle);
- return $signature === "PK\x03\x04" ||
- $signature === "PK\x05\x06" ||
- $signature === "PK\x07\x08";
- }
- function backupUploadChooseFilename(string $clientFilename, string $instanceDir): string
- {
- $clientFilename = trim($clientFilename);
- if ($clientFilename === "") {
- $filename = "backup-" . gmdate("Ymd-His") . ".zip";
- } elseif (
- basename($clientFilename) !== $clientFilename ||
- preg_match('/^backup-\d{8}-\d{6}(?:-\d+)?\.zip$/', $clientFilename) !== 1
- ) {
- throw new RuntimeException("Invalid backup filename.");
- } else {
- $filename = $clientFilename;
- }
- $base = substr($filename, 0, -4);
- $counter = 2;
- while (is_file($instanceDir . DIRECTORY_SEPARATOR . $filename)) {
- $filename = $base . "-" . $counter . ".zip";
- $counter++;
- }
- return $filename;
- }
- if ($_SERVER["REQUEST_METHOD"] !== "POST") {
- backupUploadRespond(405, ["success" => false, "error" => "POST required."]);
- }
- try {
- $instance = backupServerValidateInstance((string) ($_POST["instance"] ?? ""));
- if (!backupUploadInstanceIsAllowed($instance)) {
- throw new RuntimeException("Instance is not allowed.");
- }
- $file = $_FILES["backup"] ?? null;
- if (!is_array($file)) {
- throw new RuntimeException("Backup file is missing.");
- }
- if (($file["error"] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
- throw new RuntimeException("Upload failed with error code " . (string) ($file["error"] ?? "unknown") . ".");
- }
- $tmpName = (string) ($file["tmp_name"] ?? "");
- if ($tmpName === "" || !is_uploaded_file($tmpName)) {
- throw new RuntimeException("Upload is invalid.");
- }
- if (!backupUploadIsZipFile($tmpName)) {
- throw new RuntimeException("Uploaded file must be a ZIP file.");
- }
- $instanceDir = backupServerInstanceDir($instance);
- backupServerEnsureDirectory($instanceDir);
- $requestedFilename = (string) ($_POST["filename"] ?? "");
- $clientFilename = $requestedFilename !== "" ? $requestedFilename : (string) ($file["name"] ?? "");
- $filename = backupUploadChooseFilename($requestedFilename, $instanceDir);
- $targetPath = $instanceDir . DIRECTORY_SEPARATOR . $filename;
- if (!move_uploaded_file($tmpName, $targetPath)) {
- throw new RuntimeException("Uploaded backup cannot be stored.");
- }
- @chmod($targetPath, 0664);
- $size = filesize($targetPath);
- $sha256 = strtolower(hash_file("sha256", $targetPath) ?: "");
- if ($size === false || $size <= 0 || !preg_match('/^[a-f0-9]{64}$/', $sha256)) {
- @unlink($targetPath);
- throw new RuntimeException("Stored backup could not be verified.");
- }
- $postedSha256 = strtolower(trim((string) ($_POST["sha256"] ?? "")));
- if ($postedSha256 !== "" && (!preg_match('/^[a-f0-9]{64}$/', $postedSha256) || $postedSha256 !== $sha256)) {
- @unlink($targetPath);
- throw new RuntimeException("Backup checksum mismatch.");
- }
- $index = backupServerReadIndex();
- $record = [
- "instance" => $instance,
- "filename" => $filename,
- "client_filename" => basename($clientFilename),
- "size" => $size,
- "sha256" => $sha256,
- "uploaded_at" => date(DATE_ATOM),
- "source_ip" => $_SERVER["REMOTE_ADDR"] ?? "unknown",
- ];
- $index["backups"][] = $record;
- backupServerWriteIndex($index["backups"]);
- // S3 problems must never fail the upload: the local copy exists, and the
- // sync is retried on the next upload or via the management UI.
- $s3Enabled = backupS3Enabled();
- $s3Result = ["uploaded" => 0, "pending" => 0, "error" => null];
- if ($s3Enabled) {
- try {
- $s3Result = backupServerSyncInstanceS3($instance);
- } catch (Throwable $exception) {
- $s3Result = ["uploaded" => 0, "pending" => 1, "error" => $exception->getMessage()];
- backupServerLog("S3 sync crashed", [
- "instance" => $instance,
- "error" => $exception->getMessage(),
- ]);
- }
- }
- backupServerApplyRetention($instance);
- backupUploadRespond(200, [
- "success" => true,
- "instance" => $instance,
- "filename" => $filename,
- "size" => $size,
- "sha256" => $sha256,
- "retention" => backupServerGetSettings()["retention"],
- "s3" => [
- "enabled" => $s3Enabled,
- "uploaded" => $s3Enabled && $s3Result["pending"] === 0,
- "pending" => $s3Result["pending"],
- ],
- ]);
- } catch (Throwable $exception) {
- backupUploadRespond(400, [
- "success" => false,
- "error" => $exception->getMessage(),
- ]);
- }
|