05_BACKUP_SOURCES.md 6.8 KB

Backup-Quellen und Ziele

Überblick

Was gesichert wird, steht in MANAGE_BACKUP_SOURCES. Optional kommt ein MySQL-Dump dazu. Wohin gesichert wird, steuern MANAGE_BACKUP_UPLOAD und MANAGE_BACKUP_REMOTE_TARGETS.

Relevante Dateien:

  • manage-client/lib/backup.php – Sammeln, Archivieren, Aufbewahrung, Upload
  • manage-client/lib/zip.php – ZIP-Erzeugung ohne externe Bibliothek
  • manage-client/lib/mysql.php – optionaler Datenbank-Dump
  • manage-client/lib/remote.php – zusätzliche Ziele

Quellen deklarieren

Alle Pfade sind relativ zu MANAGE_APP_ROOT. Jeder Eintrag verwendet genau eine der drei Formen:

define("MANAGE_BACKUP_SOURCES", [
    // Nicht-rekursives Glob-Muster
    ["as" => "data", "glob" => "data/*.json"],

    // Verzeichnis, rekursiv
    ["as" => "data/uploads", "dir" => "data/uploads"],

    // Einzelne Datei
    ["as" => "config", "file" => "config.dist.php"],
]);

as ist das Präfix im Archiv. Ohne as landet der Eintrag auf oberster Ebene – bei dir wird der Verzeichnisname als Präfix verwendet.

Das obige Beispiel erzeugt:

data/orders.json
data/settings.json
data/uploads/2026/bild.jpg
config/config.dist.php

Was automatisch übersprungen wird

  • Dateien, deren Name mit einem Punkt beginnt
  • Dateien mit den Endungen .tmp und .part
  • nicht lesbare Dateien

Ergeben zwei Quellen denselben Archivpfad, gewinnt die erste. Ein Archiv enthält nie zwei Einträge mit gleichem Namen.

Was nicht hineingehört

  • config.php mit echten Zugangsdaten – ein Backup wird an den Server übertragen und dort heruntergeladen; siehe 10_SECURITY
  • das Backup-Verzeichnis selbst
  • Protokolle und Cache-Verzeichnisse
  • die Anwendungsdateien: Die kommen aus dem Release-Paket, nicht aus dem Backup

Ein Backup sichert Betriebsdaten, kein vollständiges Systemabbild.

Datenbank-Dump

Nur aktiv, wenn MANAGE_BACKUP_DATABASE gesetzt ist:

define("MANAGE_BACKUP_DATABASE", [
    "dsn"      => "mysql:host=localhost;dbname=meinprojekt;charset=utf8mb4",
    "user"     => "meinprojekt",
    "password" => "…",

    // Optional: von diesen Tabellen nur die Struktur sichern, nicht die Zeilen.
    "skip_data_tables" => ["sessions", "cache"],

    // Optional: Name im Archiv, falls SELECT DATABASE() nichts liefert.
    "name" => "meinprojekt",
]);

Der Dump landet im Archiv als database/<name>.sql und enthält für jede Tabelle DROP TABLE IF EXISTS, das CREATE TABLE aus SHOW CREATE TABLE und die Zeilen als einzelne INSERT-Anweisungen. SET FOREIGN_KEY_CHECKS=0 steht am Anfang, am Ende wird wieder auf 1 gesetzt.

Eigenschaften und Grenzen:

  • Nur PDO, kein mysqldump-Aufruf – das ist auf vielen Shared-Hostern die einzige Möglichkeit, funktioniert aber nur für MySQL und MariaDB.
  • Die Zeilen werden in Blöcken von 500 gelesen und sofort geschrieben, sodass der Speicherbedarf nicht mit der Tabellengröße wächst.
  • Binärwerte werden als 0x…-Literal geschrieben, damit der Dump gültiges ASCII bleibt.
  • Views werden als Struktur gesichert, aber nicht mit Daten.
  • Der Dump ist nicht transaktional konsistent. Für eine Anwendung mit gleichzeitigem Schreibverkehr sollte das Backup in einer ruhigen Zeit laufen.

Wiederherstellung von Hand:

unzip -p backup-20260820-092104.zip database/meinprojekt.sql | mysql meinprojekt

Kompression

MANAGE_BACKUP_COMPRESS (Standard true) schaltet Deflate ein. Der Unterschied ist bei SQL-Dumps und JSON groß, bei bereits komprimierten Bildern praktisch null. Fehlt zlib, schreibt der Client unkomprimiert weiter statt abzubrechen.

Grenzen des ZIP-Formats ohne Zip64: 4 GB pro Datei, 4 GB pro Archiv, 65535 Einträge. Wird eine Grenze überschritten, bricht das Backup mit einer klaren Meldung ab.

Ziele

Manage-Server

Standardziel, aktiv über MANAGE_BACKUP_UPLOAD (Standard true). Konfiguriert wird nichts weiter: Es gelten MANAGE_SERVER_URL, MANAGE_INSTANCE und MANAGE_TOKEN. Der Server prüft die mitgesendete Prüfsumme erneut und lehnt abweichende Uploads ab.

Für rein lokale Backups:

define("MANAGE_BACKUP_UPLOAD", false);

Zusätzliche Ziele

Jedes Ziel wird unabhängig versucht. Ein Fehlschlag macht weder das lokale Archiv noch die anderen Ziele ungültig.

S3-kompatibler Speicher:

define("MANAGE_BACKUP_REMOTE_TARGETS", [
    [
        "name"       => "S3 Archiv",
        "type"       => "s3",
        "bucket"     => "example-bucket",
        "region"     => "eu-central-1",
        "prefix"     => "meinprojekt",
        "access_key" => "AKIA…",
        "secret_key" => "…",
        // Für S3-kompatible Anbieter:
        // "endpoint" => "https://fsn1.your-objectstorage.com",
    ],
]);

Signiert wird mit AWS Signature V4 über normale PHP-HTTPS-Streams; eine Bibliothek wird nicht benötigt. Das Archiv wird für die Signatur vollständig in den Speicher geladen – ein Backup größer als memory_limit kann dieses Ziel nicht nutzen.

SFTP (benötigt die PHP-Erweiterung ssh2):

[
    "name"     => "SFTP Backup",
    "type"     => "sftp",
    "host"     => "backup.example.org",
    "port"     => 22,
    "username" => "backup-user",
    "password" => "…",
    "path"     => "/backups/meinprojekt",
]

Mit Schlüssel statt Passwort:

[
    "type"        => "sftp",
    "host"        => "backup.example.org",
    "username"    => "backup-user",
    "public_key"  => "/pfad/zu/backup.pub",
    "private_key" => "/pfad/zu/backup",
    "password"    => "",        // Passphrase des Schlüssels, sonst leer
    "path"        => "/backups/meinprojekt",
]

Das Zielverzeichnis muss existieren und beschreibbar sein; es wird nicht angelegt.

Eigener Uploader:

[
    "name"     => "Eigenes Ziel",
    "type"     => "custom",
    "file"     => MANAGE_APP_ROOT . "/includes/backup-uploader.php",
    "callback" => "myProjectUploadBackup",
]
function myProjectUploadBackup(string $archivePath, array $metadata, array $target)
{
    // $metadata: filename, created_at, trigger, sha256, file_count, source_bytes
    // Erfolg: true oder ein Array zurückgeben.
    // Fehler: Exception werfen oder ["success" => false, "error" => "…"].
    return ["remote_path" => "…"];
}

Protokollierung und Geheimnisse

Fehlgeschlagene Uploads werden mit HTTP-Status und Antwortauszug protokolliert. Zugangsdaten sind davon ausgenommen: Aus der Zielkonfiguration wird nur eine Positivliste unkritischer Schlüssel übernommen (name, type, url, bucket, region, prefix, endpoint, host, port, username, path, file, callback, timeout). access_key, secret_key und password erscheinen nie im Protokoll.

Weiter