瀏覽代碼

adding backup docs

Josef Straßl 1 月之前
父節點
當前提交
53bffa59e9
共有 4 個文件被更改,包括 261 次插入3 次删除
  1. 65 2
      admin/settings.php
  2. 194 0
      docs/BACKUP_CONFIGURATION.md
  3. 1 0
      docs/CONFIG_REFERENCE.md
  4. 1 1
      includes/version.php

+ 65 - 2
admin/settings.php

@@ -123,6 +123,47 @@ function settingsGetBackupCapabilityLabel(array $capability): string
     return !empty($capability["available"]) ? "bereit" : "nicht verfügbar";
 }
 
+function settingsFindBackupByFilename(string $filename): ?array
+{
+    if ($filename === "" || basename($filename) !== $filename) {
+        return null;
+    }
+
+    foreach (backupListBackups() as $backup) {
+        if (($backup["filename"] ?? "") === $filename) {
+            return $backup;
+        }
+    }
+
+    return null;
+}
+
+function settingsSendBackupDownload(array $backup): void
+{
+    $filename = basename((string) ($backup["filename"] ?? ""));
+    $path = backupGetDirectory() . $filename;
+
+    if ($filename === "" || !is_file($path) || !is_readable($path)) {
+        throw new RuntimeException("Backup-Datei wurde nicht gefunden.");
+    }
+    $size = filesize($path);
+    if ($size === false) {
+        throw new RuntimeException("Backup-Dateigröße konnte nicht gelesen werden.");
+    }
+
+    logAccess("Backup downloaded", [
+        "filename" => $filename,
+    ]);
+
+    header("Content-Type: application/zip");
+    header('Content-Disposition: attachment; filename="' . $filename . '"');
+    header("Content-Length: " . (string) $size);
+    header("X-Content-Type-Options: nosniff");
+
+    readfile($path);
+    exit();
+}
+
 if (empty($_SESSION['admin_logged_in'])) {
     header("Location: login.php");
     exit();
@@ -175,6 +216,22 @@ if ($_SERVER['REQUEST_METHOD'] === "POST" && isset($_POST['save_settings'])) {
             $messageType = "error";
         }
     }
+} elseif ($_SERVER['REQUEST_METHOD'] === "POST" && isset($_POST['download_backup'])) {
+    if (!validateCsrfToken($_POST['csrf_token'] ?? "")) {
+        $message = "Ungültiges Token. Bitte versuchen Sie es erneut.";
+        $messageType = "error";
+    } else {
+        try {
+            $backup = settingsFindBackupByFilename((string) ($_POST["backup_filename"] ?? ""));
+            if ($backup === null) {
+                throw new RuntimeException("Backup wurde nicht gefunden.");
+            }
+            settingsSendBackupDownload($backup);
+        } catch (Throwable $exception) {
+            $message = "Backup konnte nicht heruntergeladen werden: " . $exception->getMessage();
+            $messageType = "error";
+        }
+    }
 } elseif ($_SERVER['REQUEST_METHOD'] === "GET") {
     try {
         $backup = backupCreateAutomaticIfDue();
@@ -269,23 +326,29 @@ include __DIR__ . "/../includes/header.php";
             <table class="responsive-table">
                 <thead>
                     <tr>
-                        <th>Datei</th>
                         <th>Erstellt</th>
                         <th>Auslöser</th>
                         <th>Größe</th>
                         <th>Dateien</th>
                         <th>Remote</th>
+                        <th>Aktionen</th>
                     </tr>
                 </thead>
                 <tbody>
                     <?php foreach ($backups as $backup): ?>
                         <tr>
-                            <td data-label="Datei"><?php echo escape($backup["filename"] ?? ""); ?></td>
                             <td data-label="Erstellt"><?php echo escape(settingsFormatBackupDate((string) ($backup["created_at"] ?? ""))); ?></td>
                             <td data-label="Auslöser"><?php echo (($backup["trigger"] ?? "") === "automatic") ? "Automatisch" : "Manuell"; ?></td>
                             <td data-label="Größe"><?php echo escape(backupFormatBytes((int) ($backup["size"] ?? 0))); ?></td>
                             <td data-label="Dateien"><?php echo (int) ($backup["file_count"] ?? 0); ?></td>
                             <td data-label="Remote"><?php echo escape(settingsGetBackupUploadLabel($backup)); ?></td>
+                            <td data-label="Aktionen">
+                                <form method="POST" class="inline-form">
+                                    <?php echo csrfField(); ?>
+                                    <input type="hidden" name="backup_filename" value="<?php echo escape($backup["filename"] ?? ""); ?>">
+                                    <button type="submit" name="download_backup" class="btn btn-secondary btn-small">Download</button>
+                                </form>
+                            </td>
                         </tr>
                     <?php endforeach; ?>
                 </tbody>

+ 194 - 0
docs/BACKUP_CONFIGURATION.md

@@ -0,0 +1,194 @@
+# Backup-Konfiguration
+
+Backups werden in `config.php` konfiguriert. Ohne Remote-Konfiguration funktionieren sie sofort: Ein Admin kann unter **Einstellungen** lokale ZIP-Backups erstellen.
+
+## Inhalt des Backups
+
+Das Backup-ZIP enthält nur Betriebsdaten:
+
+- `data/*.json`
+- `data/uploads/**`
+
+Nicht enthalten sind App-Dateien, `config.php`, bestehende Backups, Updater-Arbeitsdateien, Logs, Rate-Limit-Daten und temporäre Dateien.
+
+Lokale Backup-ZIPs liegen in `data/backups/`. Mit der Standard-`.htaccess` ist dieses Verzeichnis nicht öffentlich lesbar.
+
+## Lokale Grundkonfiguration
+
+Diese Defaults sind bereits in `includes/backup.php` hinterlegt. Wenn eine Installation explizite Werte benötigt, werden sie in `config.php` gesetzt:
+
+```php
+define('BACKUP_DIR', DATA_DIR . 'backups/');
+define('BACKUP_LOCAL_RETENTION', 4);
+define('BACKUP_AUTO_INTERVAL_SECONDS', 604800);
+define('BACKUP_REMOTE_TARGETS', []);
+```
+
+`BACKUP_LOCAL_RETENTION` legt fest, wie viele lokale ZIP-Dateien behalten werden. Ältere Backups werden nach einem erfolgreichen neuen Backup gelöscht.
+
+`BACKUP_AUTO_INTERVAL_SECONDS` steuert automatische Backups durch Admin-Aktivität. Der Standard ist wöchentlich (`604800`). Mit `0` werden automatische Backups deaktiviert.
+
+Es gibt keinen Cron-Endpunkt. Automatische Backups laufen nur, wenn ein Admin die Einstellungsseite öffnet und das Intervall abgelaufen ist.
+
+## Bedienung im Admin
+
+Pfad: **Admin > Einstellungen > Backups**.
+
+- **Backup erstellen** erstellt sofort ein manuelles Backup.
+- **Letzte Backups** zeigt lokale Backups mit Erstellzeit, Auslöser, Größe, Dateianzahl, Remote-Status und Download-Aktion.
+- Fehlgeschlagene Remote-Uploads machen das lokale Backup nicht ungültig. Der Fehler wird in den Backup-Metadaten gespeichert.
+
+## S3-Ziel
+
+S3-Uploads nutzen eingebaute PHP-HTTPS-Streams und AWS Signature V4. Es ist keine Bibliothek erforderlich.
+
+```php
+define('BACKUP_REMOTE_TARGETS', [
+    [
+        'name' => 'S3 Backup',
+        'type' => 's3',
+        'bucket' => 'example-bucket',
+        'region' => 'eu-central-1',
+        'prefix' => 'psa-orderform',
+        'access_key' => 'AKIA...',
+        'secret_key' => '...',
+    ],
+]);
+```
+
+Für S3-kompatiblen Speicher kann `endpoint` ergänzt werden:
+
+```php
+'endpoint' => 'https://s3.example.org',
+```
+
+Der Objektpfad besteht aus `prefix` plus generiertem ZIP-Dateinamen.
+
+## SFTP-Ziel
+
+SFTP benötigt die optionale PHP-SSH2-Erweiterung. Wenn die Erweiterung fehlt, funktionieren lokale Backups weiterhin; der SFTP-Upload wird als nicht verfügbar beziehungsweise fehlgeschlagen gemeldet.
+
+Passwort-Anmeldung:
+
+```php
+define('BACKUP_REMOTE_TARGETS', [
+    [
+        'name' => 'SFTP Backup',
+        'type' => 'sftp',
+        'host' => 'backup.example.org',
+        'port' => 22,
+        'username' => 'backup-user',
+        'password' => '...',
+        'path' => '/backups/psa-orderform',
+    ],
+]);
+```
+
+Key-Anmeldung:
+
+```php
+define('BACKUP_REMOTE_TARGETS', [
+    [
+        'name' => 'SFTP Backup',
+        'type' => 'sftp',
+        'host' => 'backup.example.org',
+        'port' => 22,
+        'username' => 'backup-user',
+        'public_key' => __DIR__ . '/keys/backup.pub',
+        'private_key' => __DIR__ . '/keys/backup',
+        'password' => '',
+        'path' => '/backups/psa-orderform',
+    ],
+]);
+```
+
+Das Remote-Verzeichnis muss bereits existieren und für den konfigurierten Benutzer beschreibbar sein.
+
+## Custom-Uploader
+
+Ein Custom-Uploader ist ein PHP-Callback, der in `config.php` konfiguriert wird.
+
+```php
+define('BACKUP_REMOTE_TARGETS', [
+    [
+        'name' => 'Custom Backup',
+        'type' => 'custom',
+        'file' => __DIR__ . '/custom-backup-uploader.php',
+        'callback' => 'uploadPsaOrderformBackup',
+    ],
+]);
+```
+
+Beispiel-Callback:
+
+```php
+<?php
+
+function uploadPsaOrderformBackup(string $archivePath, array $metadata, array $target)
+{
+    $targetDir = __DIR__ . '/external-backups';
+    if (!is_dir($targetDir) && !mkdir($targetDir, 02775, true) && !is_dir($targetDir)) {
+        return [
+            'success' => false,
+            'error' => 'Target directory cannot be created.',
+        ];
+    }
+
+    $remotePath = $targetDir . '/' . basename($archivePath);
+    if (!copy($archivePath, $remotePath)) {
+        return [
+            'success' => false,
+            'error' => 'Copy failed.',
+        ];
+    }
+
+    return [
+        'remote_path' => $remotePath,
+    ];
+}
+```
+
+Der Callback erhält:
+
+- `$archivePath`: absoluter Pfad zur lokalen ZIP-Datei.
+- `$metadata`: Dateiname, Erstellzeitpunkt, Auslöser und Prüfsumme.
+- `$target`: das konfigurierte Ziel-Array.
+
+Für Erfolg kann `true` oder ein Array zurückgegeben werden. Für Fehler kann eine Exception geworfen werden. Ein Array mit `'success' => false` wird als fehlgeschlagener Remote-Upload protokolliert.
+
+## Mehrere Remote-Ziele
+
+Remote-Ziele können kombiniert werden:
+
+```php
+define('BACKUP_REMOTE_TARGETS', [
+    [
+        'name' => 'Primary S3',
+        'type' => 's3',
+        'bucket' => 'example-bucket',
+        'region' => 'eu-central-1',
+        'prefix' => 'psa-orderform',
+        'access_key' => 'AKIA...',
+        'secret_key' => '...',
+    ],
+    [
+        'name' => 'Secondary SFTP',
+        'type' => 'sftp',
+        'host' => 'backup.example.org',
+        'port' => 22,
+        'username' => 'backup-user',
+        'password' => '...',
+        'path' => '/backups/psa-orderform',
+    ],
+]);
+```
+
+Jedes Ziel wird unabhängig versucht.
+
+## Betriebshinweise
+
+- Remote-Zugangsdaten gehören in `config.php`, nicht in `data/settings.json`.
+- `data/` muss für PHP beschreibbar sein.
+- Auf Apache-Hosting muss `.htaccess` aktiv bleiben, damit `data/backups/` nicht direkt erreichbar ist.
+- Remote-Ziele sollten nach Konfigurationsänderungen mit einem manuellen Backup getestet werden.
+- Lokale Backups sollten nur über den authentifizierten Adminbereich heruntergeladen werden.

+ 1 - 0
docs/CONFIG_REFERENCE.md

@@ -65,6 +65,7 @@ Der Startseiten-Introtext wird unter **FAQ** gepflegt (`startpage_intro_text` in
 - Backups enthalten `data/*.json` und `data/uploads/**`; App-Dateien, Logs, Updates, Rate-Limits und bestehende Backups werden ausgeschlossen.
 - SFTP benötigt die optionale PHP-SSH2-Erweiterung. Ohne Erweiterung bleibt das lokale Backup gültig, der Remote-Upload wird als fehlgeschlagen protokolliert.
 - Zugangsdaten für Remote-Ziele gehören in `config.php`, nicht in `data/settings.json`.
+- Details und Beispiele: [Backup-Konfiguration](BACKUP_CONFIGURATION.md).
 
 ## Hinweis
 

+ 1 - 1
includes/version.php

@@ -1,3 +1,3 @@
 <?php
 
-define("APP_VERSION", "v1.3.3");
+define("APP_VERSION", "v1.3.5");