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, uploadmanage-client/lib/zip.php – ZIP creation without an external librarymanage-client/lib/mysql.php – optional database dumpmanage-client/lib/remote.php – extra targetsAll 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
.tmp and .partIf two sources produce the same archive path, the first one wins. An archive never contains two entries with the same name.
config.php with real credentials — a backup gets transferred to the
server and downloaded there; see 10_SECURITYA backup secures operational data, not a complete system image.
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:
mysqldump call — on many shared hosts that's the only
option available, but it only works for MySQL and MariaDB.0x… literals, so the dump stays valid
ASCII.Manual restore:
unzip -p backup-20260820-092104.zip database/myproject.sql | mysql myproject
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.
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);
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" => "…"];
}
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.
manageBackupCreate() and its return values