| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315 |
- <?php
- 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");
- }
- 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 backupUploadEnsureDirectory(string $dir): void
- {
- if (!is_dir($dir) && !mkdir($dir, 02775, true) && !is_dir($dir)) {
- throw new RuntimeException("Directory cannot be created.");
- }
- @chmod($dir, 02775);
- }
- function backupUploadNormalizeInstance(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 identifier.");
- }
- return $instance;
- }
- function backupUploadReadJsonFile(string $file): array
- {
- if (!is_file($file)) {
- return [];
- }
- $decoded = json_decode((string) file_get_contents($file), true);
- return is_array($decoded) ? $decoded : [];
- }
- function backupUploadWriteJsonFile(string $file, array $data): void
- {
- backupUploadEnsureDirectory(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 backupUploadReadIndex(): array
- {
- $index = backupUploadReadJsonFile((string) BACKUP_SERVER_INDEX_FILE);
- $backups = isset($index["backups"]) && is_array($index["backups"])
- ? $index["backups"]
- : [];
- return ["backups" => array_values($backups)];
- }
- function backupUploadWriteIndex(array $backups): void
- {
- backupUploadWriteJsonFile((string) BACKUP_SERVER_INDEX_FILE, [
- "backups" => array_values($backups),
- ]);
- }
- function backupUploadGetRetention(): int
- {
- $settings = backupUploadReadJsonFile((string) BACKUP_SERVER_SETTINGS_FILE);
- if (isset($settings["retention"])) {
- return max(1, (int) $settings["retention"]);
- }
- return max(1, (int) BACKUP_SERVER_RETENTION);
- }
- function backupUploadGetAllowedInstances(): array
- {
- $settings = backupUploadReadJsonFile((string) BACKUP_SERVER_SETTINGS_FILE);
- $instances =
- isset($settings["instances"]) && is_array($settings["instances"])
- ? $settings["instances"]
- : [];
- $allowed = [];
- foreach ($instances as $instance) {
- $instance = (string) $instance;
- if (
- $instance !== "" &&
- preg_match('/^[A-Za-z0-9][A-Za-z0-9._-]*$/', $instance) === 1
- ) {
- $allowed[$instance] = true;
- }
- }
- return $allowed;
- }
- function backupUploadInstanceIsAllowed(string $instance): bool
- {
- $allowed = backupUploadGetAllowedInstances();
- return isset($allowed[$instance]);
- }
- function backupUploadInstanceDir(string $instance): string
- {
- return rtrim((string) BACKUP_SERVER_BACKUP_DIR, "/\\") . DIRECTORY_SEPARATOR . $instance;
- }
- 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;
- }
- function backupUploadApplyRetention(string $instance): void
- {
- $index = backupUploadReadIndex();
- $retention = backupUploadGetRetention();
- $instanceBackups = [];
- $otherBackups = [];
- foreach ($index["backups"] as $backup) {
- if (!is_array($backup)) {
- continue;
- }
- if (($backup["instance"] ?? "") === $instance) {
- $instanceBackups[] = $backup;
- } else {
- $otherBackups[] = $backup;
- }
- }
- 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);
- $instanceDir = backupUploadInstanceDir($instance);
- foreach ($remove as $backup) {
- $filename = basename((string) ($backup["filename"] ?? ""));
- if ($filename !== "" && is_file($instanceDir . DIRECTORY_SEPARATOR . $filename)) {
- @unlink($instanceDir . DIRECTORY_SEPARATOR . $filename);
- }
- }
- backupUploadWriteIndex(array_merge($otherBackups, $keep));
- }
- if ($_SERVER["REQUEST_METHOD"] !== "POST") {
- backupUploadRespond(405, ["success" => false, "error" => "POST required."]);
- }
- try {
- $instance = backupUploadNormalizeInstance((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 = backupUploadInstanceDir($instance);
- backupUploadEnsureDirectory($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 = backupUploadReadIndex();
- $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;
- backupUploadWriteIndex($index["backups"]);
- backupUploadApplyRetention($instance);
- backupUploadRespond(200, [
- "success" => true,
- "instance" => $instance,
- "filename" => $filename,
- "size" => $size,
- "sha256" => $sha256,
- "retention" => backupUploadGetRetention(),
- ]);
- } catch (Throwable $exception) {
- backupUploadRespond(400, [
- "success" => false,
- "error" => $exception->getMessage(),
- ]);
- }
|