05_BACKUP_SOURCES.md 6.5 KB

Backup Sources and Targets

Overview

What gets backed up lives in MANAGE_BACKUP_SOURCES. A MySQL dump can be added optionally. Where it gets backed up to is controlled by MANAGE_BACKUP_UPLOAD and MANAGE_BACKUP_REMOTE_TARGETS.

Relevant files:

  • manage-client/lib/backup.php – collecting, archiving, retention, upload
  • manage-client/lib/zip.php – ZIP creation without an external library
  • manage-client/lib/mysql.php – optional database dump
  • manage-client/lib/remote.php – extra targets

Declaring sources

All paths are relative to MANAGE_APP_ROOT. Every entry uses exactly one of three forms:

define("MANAGE_BACKUP_SOURCES", [
    // Non-recursive glob pattern
    ["as" => "data", "glob" => "data/*.json"],

    // Directory, recursive
    ["as" => "data/uploads", "dir" => "data/uploads"],

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

as is the prefix inside the archive. Without as, the entry lands at the top level — for dir, the directory name is used as the prefix.

The example above produces:

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

What gets skipped automatically

  • files whose name starts with a dot
  • files with the extensions .tmp and .part
  • unreadable files

If two sources produce the same archive path, the first one wins. An archive never contains two entries with the same name.

What does not belong in it

  • config.php with real credentials — a backup gets transferred to the server and downloaded there; see 10_SECURITY
  • the backup directory itself
  • logs and cache directories
  • the application files: those come from the release package, not from the backup

A backup secures operational data, not a complete system image.

Database dump

Only active when MANAGE_BACKUP_DATABASE is set:

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

    // Optional: back up only the structure of these tables, not their rows.
    "skip_data_tables" => ["sessions", "cache"],

    // Optional: name inside the archive, if SELECT DATABASE() returns nothing.
    "name" => "myproject",
]);

The dump lands in the archive as database/<name>.sql and contains, for every table, DROP TABLE IF EXISTS, the CREATE TABLE from SHOW CREATE TABLE, and the rows as individual INSERT statements. SET FOREIGN_KEY_CHECKS=0 sits at the top, set back to 1 at the end.

Properties and limits:

  • PDO only, no mysqldump call — on many shared hosts that's the only option available, but it only works for MySQL and MariaDB.
  • Rows are read in blocks of 500 and written immediately, so memory use doesn't grow with table size.
  • Binary values are written as 0x… literals, so the dump stays valid ASCII.
  • Views are backed up as structure, but without data.
  • The dump is not transactionally consistent. For an application with concurrent writes, the backup should run during a quiet period.

Manual restore:

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

Compression

MANAGE_BACKUP_COMPRESS (default true) turns on deflate. The difference is large for SQL dumps and JSON, practically zero for already-compressed images. If zlib is missing, the client keeps writing uncompressed instead of aborting.

Limits of the ZIP format without Zip64: 4 GB per file, 4 GB per archive, 65535 entries. If a limit is exceeded, the backup aborts with a clear message.

Targets

Manage server

Default target, active via MANAGE_BACKUP_UPLOAD (default true). Nothing else needs configuring: MANAGE_SERVER_URL, MANAGE_INSTANCE and MANAGE_TOKEN apply. The server re-checks the submitted checksum and rejects a mismatching upload.

For purely local backups:

define("MANAGE_BACKUP_UPLOAD", false);

Extra targets

Every target is attempted independently. A failure invalidates neither the local archive nor the other targets.

S3-compatible storage:

define("MANAGE_BACKUP_REMOTE_TARGETS", [
    [
        "name"       => "S3 Archive",
        "type"       => "s3",
        "bucket"     => "example-bucket",
        "region"     => "eu-central-1",
        "prefix"     => "myproject",
        "access_key" => "AKIA…",
        "secret_key" => "…",
        // For S3-compatible providers:
        // "endpoint" => "https://fsn1.your-objectstorage.com",
    ],
]);

Signing uses AWS Signature V4 over plain PHP HTTPS streams; no library is required. The archive is loaded fully into memory for signing — a backup larger than memory_limit cannot use this target.

SFTP (needs the PHP ssh2 extension):

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

With a key instead of a password:

[
    "type"        => "sftp",
    "host"        => "backup.example.org",
    "username"    => "backup-user",
    "public_key"  => "/path/to/backup.pub",
    "private_key" => "/path/to/backup",
    "password"    => "",        // key passphrase, empty otherwise
    "path"        => "/backups/myproject",
]

The target directory must exist and be writable; it is not created.

Custom uploader:

[
    "name"     => "Custom Target",
    "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
    // Success: return true or an array.
    // Failure: throw an exception, or return ["success" => false, "error" => "…"].
    return ["remote_path" => "…"];
}

Logging and secrets

Failed uploads are logged with HTTP status and a response excerpt. Credentials are excluded from that: only an allowlist of non-sensitive keys is taken from the target configuration (name, type, url, bucket, region, prefix, endpoint, host, port, username, path, file, callback, timeout). access_key, secret_key and password never appear in the log.

Next