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(), ]); }