# PSA Orderform Backup Server This folder can be deployed as a central backup server for distributed PSA orderform instances. ## Setup 1. Copy `config.sample.php` to `config.php`. 2. Change `BACKUP_SERVER_PASSWORD`. 3. Ensure the `backups/` directory is writable by PHP. 4. Open `index.php` or `manage.php` and log in with the configured password. 5. Add every allowed distributed instance in the management UI. The upload endpoint is `upload.php`. It intentionally does not require authentication, but every upload must include an `instance` identifier that was added in the management UI. ## Client target Configure a distributed instance with a managed backup target: ```php define('BACKUP_REMOTE_TARGETS', [ [ 'name' => 'Managed Backup Server', 'type' => 'managed', 'url' => 'https://backup.example.org/upload.php', 'instance' => 'stadt-freising-prod', ], ]); ``` `url` must point directly to `upload.php`. `instance` may contain letters, numbers, dots, underscores, and dashes. ## Retention The server retains the latest backups per instance. The default is `30`. Retention can be changed in `config.php` with `BACKUP_SERVER_RETENTION` and in the management UI. The UI value is stored in `backups/settings.json` and takes precedence after it has been saved once. Retention is applied after every successful upload and after retention changes in the management UI. With S3 enabled (see below) this value controls only the **local** copies. The minimum is `1`, so at least the newest backup always stays on local disk. ## S3 archive (optional) The server can additionally archive every backup to an S3-compatible object storage (e.g. Hetzner Object Storage, MinIO). Local disk then acts as a small hot cache with the newest backups, while the bucket holds the complete archive. Enable it in `config.php`: ```php define("BACKUP_SERVER_S3_ENABLED", true); define("BACKUP_SERVER_S3_ENDPOINT", "https://fsn1.your-objectstorage.com"); define("BACKUP_SERVER_S3_REGION", "fsn1"); define("BACKUP_SERVER_S3_BUCKET", "my-backup-bucket"); define("BACKUP_SERVER_S3_PREFIX", "psa-backups"); define("BACKUP_SERVER_S3_ACCESS_KEY", "..."); define("BACKUP_SERVER_S3_SECRET_KEY", "..."); ``` By default the server uses **virtual-hosted-style** addressing (`https://./`), which Hetzner and most S3-compatible providers expect. If your provider requires **path-style** (`https:////`), set `BACKUP_SERVER_S3_PATH_STYLE` to `true`. Behavior: - Every received backup is stored locally first and then uploaded to S3 (AWS Signature V4, no SDK required). Objects are stored as `//`. - A local copy is only deleted after it fell out of the local retention window **and** its S3 copy is confirmed. While S3 is unreachable, local copies accumulate beyond the retention setting instead of being deleted. - S3 failures never fail a client upload. They are logged to `backups/s3.log` and shown in the management UI; failed uploads are retried on the next upload for that instance or via the "Retry S3 uploads now" button. - S3 has its own count-based retention ("S3 backups retained per instance", default `BACKUP_SERVER_S3_RETENTION` = 365). Backups that age out of S3 are deleted from the bucket. - S3-only backups remain listed in the management UI and are downloaded through the server, so the bucket can (and should) stay private. - When enabling S3 on an installation with existing backups, use "Retry S3 uploads now" once to backfill the archive; otherwise the first client upload per instance flushes the whole backlog within that request. Limitations: uploads to S3 hold the whole file in memory, so a single backup must fit into PHP's `memory_limit`. Downloads from S3 are streamed and have no such limit. Concurrent uploads for the same instance may race on `index.json` (pre-existing limitation). Troubleshooting: S3 errors are written to `backups/s3.log` with the provider's error code and, on a rejected request, a diagnostic showing the HTTP status chain, any redirect target, and the request id. `AccessDenied` or a redirect in the status chain usually means the addressing style is wrong — try flipping `BACKUP_SERVER_S3_PATH_STYLE`. `SignatureDoesNotMatch` usually means a wrong region or secret key. The server never follows S3 redirects, so a `3xx` in the log is reported rather than silently retried against the wrong host. ## Storage Backups are stored under: ```text backups//backup-YYYYmmdd-HHMMSS.zip ``` Metadata is stored in: ```text backups/index.json ``` Allowed instances and UI retention settings are stored in: ```text backups/settings.json ``` With the included `.htaccess`, ZIP, JSON, and log files as well as the internal includes (`lib.php`, `s3.php`) are not directly readable through Apache. Downloads should use the authenticated management UI.