|
|
@@ -15,6 +15,22 @@ if (!defined("BACKUP_REMOTE_TARGETS")) {
|
|
|
define("BACKUP_REMOTE_TARGETS", []);
|
|
|
}
|
|
|
|
|
|
+class BackupRemoteUploadException extends RuntimeException
|
|
|
+{
|
|
|
+ private array $debugContext;
|
|
|
+
|
|
|
+ public function __construct(string $message, array $debugContext = [])
|
|
|
+ {
|
|
|
+ parent::__construct($message);
|
|
|
+ $this->debugContext = $debugContext;
|
|
|
+ }
|
|
|
+
|
|
|
+ public function getDebugContext(): array
|
|
|
+ {
|
|
|
+ return $this->debugContext;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
function backupGetDirectory(): string
|
|
|
{
|
|
|
return rtrim((string) BACKUP_DIR, "/\\") . DIRECTORY_SEPARATOR;
|
|
|
@@ -422,6 +438,87 @@ function backupGetTargetLabel(array $target, int $index): string
|
|
|
return $type . "-" . ($index + 1);
|
|
|
}
|
|
|
|
|
|
+function backupGetSafeTargetContext(array $target): array
|
|
|
+{
|
|
|
+ $safe = [];
|
|
|
+ $allowedKeys = [
|
|
|
+ "name",
|
|
|
+ "type",
|
|
|
+ "url",
|
|
|
+ "instance",
|
|
|
+ "bucket",
|
|
|
+ "region",
|
|
|
+ "prefix",
|
|
|
+ "endpoint",
|
|
|
+ "host",
|
|
|
+ "port",
|
|
|
+ "username",
|
|
|
+ "path",
|
|
|
+ "file",
|
|
|
+ "callback",
|
|
|
+ "timeout",
|
|
|
+ ];
|
|
|
+
|
|
|
+ foreach ($allowedKeys as $key) {
|
|
|
+ if (array_key_exists($key, $target)) {
|
|
|
+ $safe[$key] = is_scalar($target[$key]) ? (string) $target[$key] : gettype($target[$key]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return $safe;
|
|
|
+}
|
|
|
+
|
|
|
+function backupGetHttpUserAgent(): string
|
|
|
+{
|
|
|
+ $version = defined("APP_VERSION") ? trim((string) APP_VERSION) : "";
|
|
|
+ if ($version === "") {
|
|
|
+ $version = "unknown";
|
|
|
+ }
|
|
|
+
|
|
|
+ return "PSA-Orderform-Backup/" . $version;
|
|
|
+}
|
|
|
+
|
|
|
+function backupFormatResponseExcerpt($response): string
|
|
|
+{
|
|
|
+ if (!is_string($response) || $response === "") {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+
|
|
|
+ $response = preg_replace('/\s+/', " ", trim($response));
|
|
|
+ if (!is_string($response)) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+
|
|
|
+ return substr($response, 0, 500);
|
|
|
+}
|
|
|
+
|
|
|
+function backupFormatHeaderExcerpt(array $headers): array
|
|
|
+{
|
|
|
+ $result = [];
|
|
|
+ foreach ($headers as $header) {
|
|
|
+ $header = trim((string) $header);
|
|
|
+ if ($header === "") {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $result[] = substr($header, 0, 500);
|
|
|
+ if (count($result) >= 20) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return $result;
|
|
|
+}
|
|
|
+
|
|
|
+function backupGetLastPhpErrorMessage(): string
|
|
|
+{
|
|
|
+ $error = error_get_last();
|
|
|
+ if (!is_array($error)) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+
|
|
|
+ return substr(trim((string) ($error["message"] ?? "")), 0, 500);
|
|
|
+}
|
|
|
+
|
|
|
function backupRemoteCapabilities(): array
|
|
|
{
|
|
|
$targets = backupGetRemoteTargets();
|
|
|
@@ -546,19 +643,21 @@ function backupUploadToS3(string $archivePath, array $metadata, array $target):
|
|
|
"http" => [
|
|
|
"method" => "PUT",
|
|
|
"timeout" => (int) ($target["timeout"] ?? 120),
|
|
|
- "ignore_errors" => false,
|
|
|
+ "ignore_errors" => true,
|
|
|
"header" =>
|
|
|
"Content-Type: application/zip\r\n" .
|
|
|
"Content-Length: " . strlen($payload) . "\r\n" .
|
|
|
"Host: " . $host . "\r\n" .
|
|
|
"X-Amz-Date: " . $now . "\r\n" .
|
|
|
"X-Amz-Content-Sha256: " . $payloadHash . "\r\n" .
|
|
|
- "Authorization: " . $authorization . "\r\n",
|
|
|
+ "Authorization: " . $authorization . "\r\n" .
|
|
|
+ "User-Agent: " . backupGetHttpUserAgent() . "\r\n",
|
|
|
"content" => $payload,
|
|
|
],
|
|
|
]);
|
|
|
|
|
|
$response = @file_get_contents($url, false, $context);
|
|
|
+ $phpError = $response === false ? backupGetLastPhpErrorMessage() : "";
|
|
|
if (function_exists("http_get_last_response_headers")) {
|
|
|
$lastHeaders = http_get_last_response_headers();
|
|
|
$headers = is_array($lastHeaders) ? $lastHeaders : [];
|
|
|
@@ -571,8 +670,18 @@ function backupUploadToS3(string $archivePath, array $metadata, array $target):
|
|
|
$status = backupGetHttpStatusFromHeaders($headers);
|
|
|
|
|
|
if ($response === false || $status < 200 || $status >= 300) {
|
|
|
- throw new RuntimeException(
|
|
|
+ throw new BackupRemoteUploadException(
|
|
|
"S3-Upload fehlgeschlagen" . ($status > 0 ? " (HTTP " . $status . ")" : "") . ".",
|
|
|
+ [
|
|
|
+ "http_status" => $status,
|
|
|
+ "response_excerpt" => backupFormatResponseExcerpt($response),
|
|
|
+ "response_headers" => backupFormatHeaderExcerpt($headers),
|
|
|
+ "php_error" => $phpError,
|
|
|
+ "bucket" => $bucket,
|
|
|
+ "region" => $region,
|
|
|
+ "key" => $key,
|
|
|
+ "endpoint" => $endpoint,
|
|
|
+ ],
|
|
|
);
|
|
|
}
|
|
|
|
|
|
@@ -732,6 +841,8 @@ function backupUploadToManaged(string $archivePath, array $metadata, array $targ
|
|
|
"header" =>
|
|
|
"Content-Type: multipart/form-data; boundary=" .
|
|
|
$boundary .
|
|
|
+ "\r\nAccept: application/json\r\nUser-Agent: " .
|
|
|
+ backupGetHttpUserAgent() .
|
|
|
"\r\nContent-Length: " .
|
|
|
strlen($body) .
|
|
|
"\r\n",
|
|
|
@@ -740,6 +851,7 @@ function backupUploadToManaged(string $archivePath, array $metadata, array $targ
|
|
|
]);
|
|
|
|
|
|
$response = @file_get_contents($url, false, $context);
|
|
|
+ $phpError = $response === false ? backupGetLastPhpErrorMessage() : "";
|
|
|
if (function_exists("http_get_last_response_headers")) {
|
|
|
$lastHeaders = http_get_last_response_headers();
|
|
|
$headers = is_array($lastHeaders) ? $lastHeaders : [];
|
|
|
@@ -751,16 +863,32 @@ function backupUploadToManaged(string $archivePath, array $metadata, array $targ
|
|
|
}
|
|
|
$status = backupGetHttpStatusFromHeaders($headers);
|
|
|
if ($response === false || $status < 200 || $status >= 300) {
|
|
|
- throw new RuntimeException(
|
|
|
+ throw new BackupRemoteUploadException(
|
|
|
"Managed-Backup-Upload fehlgeschlagen" . ($status > 0 ? " (HTTP " . $status . ")" : "") . ".",
|
|
|
+ [
|
|
|
+ "http_status" => $status,
|
|
|
+ "response_excerpt" => backupFormatResponseExcerpt($response),
|
|
|
+ "response_headers" => backupFormatHeaderExcerpt($headers),
|
|
|
+ "php_error" => $phpError,
|
|
|
+ "url" => $url,
|
|
|
+ "instance" => $instance,
|
|
|
+ ],
|
|
|
);
|
|
|
}
|
|
|
|
|
|
$decoded = json_decode($response, true);
|
|
|
if (!is_array($decoded) || empty($decoded["success"])) {
|
|
|
$error = is_array($decoded) ? trim((string) ($decoded["error"] ?? "")) : "";
|
|
|
- throw new RuntimeException(
|
|
|
+ throw new BackupRemoteUploadException(
|
|
|
"Managed-Backup-Upload wurde abgelehnt" . ($error !== "" ? ": " . $error : "."),
|
|
|
+ [
|
|
|
+ "http_status" => $status,
|
|
|
+ "response_excerpt" => backupFormatResponseExcerpt($response),
|
|
|
+ "response_headers" => backupFormatHeaderExcerpt($headers),
|
|
|
+ "url" => $url,
|
|
|
+ "instance" => $instance,
|
|
|
+ "server_error" => $error,
|
|
|
+ ],
|
|
|
);
|
|
|
}
|
|
|
|
|
|
@@ -810,6 +938,7 @@ function backupUploadRemotes(string $archivePath, array $metadata): array
|
|
|
$type = trim((string) ($target["type"] ?? ""));
|
|
|
$label = backupGetTargetLabel($target, (int) $index);
|
|
|
$startedAt = date("c");
|
|
|
+ $safeTargetContext = backupGetSafeTargetContext($target);
|
|
|
|
|
|
try {
|
|
|
if ($type === "s3") {
|
|
|
@@ -824,7 +953,7 @@ function backupUploadRemotes(string $archivePath, array $metadata): array
|
|
|
throw new RuntimeException("Unbekannter Backup-Zieltyp: " . $type);
|
|
|
}
|
|
|
|
|
|
- $results[] = array_merge(
|
|
|
+ $result = array_merge(
|
|
|
[
|
|
|
"target" => $label,
|
|
|
"type" => $type,
|
|
|
@@ -834,14 +963,39 @@ function backupUploadRemotes(string $archivePath, array $metadata): array
|
|
|
],
|
|
|
is_array($extra) ? $extra : [],
|
|
|
);
|
|
|
+ $results[] = $result;
|
|
|
+
|
|
|
+ logAccess("Backup remote upload succeeded", [
|
|
|
+ "target" => $label,
|
|
|
+ "type" => $type,
|
|
|
+ "filename" => $metadata["filename"] ?? basename($archivePath),
|
|
|
+ "remote_path" => (string) ($result["remote_path"] ?? ""),
|
|
|
+ ]);
|
|
|
} catch (Throwable $exception) {
|
|
|
- $results[] = [
|
|
|
+ $debugContext = $exception instanceof BackupRemoteUploadException
|
|
|
+ ? $exception->getDebugContext()
|
|
|
+ : [];
|
|
|
+ $result = [
|
|
|
"target" => $label,
|
|
|
"type" => $type !== "" ? $type : "unknown",
|
|
|
"success" => false,
|
|
|
"started_at" => $startedAt,
|
|
|
"error" => $exception->getMessage(),
|
|
|
];
|
|
|
+ if (!empty($debugContext)) {
|
|
|
+ $result["debug"] = $debugContext;
|
|
|
+ }
|
|
|
+ $results[] = $result;
|
|
|
+
|
|
|
+ logError("Backup remote upload failed", [
|
|
|
+ "target" => $label,
|
|
|
+ "type" => $type !== "" ? $type : "unknown",
|
|
|
+ "target_config" => $safeTargetContext,
|
|
|
+ "filename" => $metadata["filename"] ?? basename($archivePath),
|
|
|
+ "sha256" => $metadata["sha256"] ?? "",
|
|
|
+ "error" => $exception->getMessage(),
|
|
|
+ "debug" => $debugContext,
|
|
|
+ ]);
|
|
|
}
|
|
|
}
|
|
|
|