BACKUP_UPDATE.md 8.1 KB

Backup and Update

The shop talks to a central Manage server (MANAGE_SERVER_URL) for three things: it uploads backup archives there, it fetches release packages from there, and it reports its status there. Everything is triggered from Admin → Einstellungen (admin/settings.php); this host has no cron.

Files

File Content
includes/manage.php entry point: config defaults, HTTP transport, logging, version, manageClientStatus()
includes/manage-zip.php pure-PHP ZIP writer used for backups
includes/manage-backup.php collecting sources, writing archives, retention, upload
includes/manage-update.php manifest, download, verification, deployment, migrations
includes/manage-heartbeat.php status report to the Manage server
includes/version.php the installed version, APP_VERSION
includes/after-update.php shopAfterUpdate(), the post-update callback
migrations/ migration scripts shipped inside a release, see migrations/README.md
scripts/create-release-zip.sh builds a release package
admin/settings.php the UI for all of it, plus admin account management

Runtime state lives under data/manage/ and is gitignored: backups/ (local archives + backup-index.json), updates/ (files the last update overwrote), work/ (update staging, cleared after each run), manage-client.log (JSONL), migrations.json, heartbeat.json.

config.php carries every MANAGE_* constant. See docs/CONFIG_REFERENCE.md; defaults live in includes/manage.php, so config.php only has to set what differs.

Backup

An archive contains data/*.json and assets/images/ — the operational data and the product images uploaded through the admin UI, which exist nowhere else. config.php is deliberately not in it: archives can be downloaded by anyone with a Manage server login, and that file holds the legacy ADMIN_USERS hashes, the order-history cookie secret and the instance token.

Three ways in:

  • Button on the settings page — trigger manual.
  • Automatically from admin/index.php once MANAGE_BACKUP_AUTO_INTERVAL_SECONDS (7 days) has passed since the last automatic run — trigger automatic. The call returns immediately when nothing is due, so the dashboard is only slow on the rare load that actually backs up.
  • Directly, manageBackupCreate('manual') after require_once includes/manage.php.

Each archive is written to data/manage/backups/, then uploaded. The last MANAGE_BACKUP_LOCAL_RETENTION (4) archives stay on disk; retention on the Manage server is configured there and is usually much higher.

A failed upload does not invalidate the archive. The error is stored in the index record, shown in the Upload column and logged; the local ZIP is complete either way. A failure that does throw means the archive never came into being.

Concurrent runs are prevented by a lock file — a second one fails immediately with Es läuft bereits ein Backup.

There is no restore

Backups are created and transferred, never played back. Restoring means downloading the ZIP from the settings page or the Manage server and unpacking it over data/ and assets/images/ by hand. This is deliberate: an automated restore button that runs while the shop is live is a footgun.

Update

The settings page shows the installed version against the current release. The Update ausrollen button then:

  1. fetches and validates the manifest,
  2. downloads the package and checks size and SHA-256 against it, deleting the file on mismatch,
  3. extracts it, rejecting any entry with .., an absolute path, a drive letter or a null byte,
  4. checks the package contains at least one MANAGE_UPDATE_SANITY_PATHS entry, so an unrelated ZIP cannot be rolled over the shop,
  5. copies every file over the shop root, copying each overwritten file aside into data/manage/updates/ first, skipping MANAGE_UPDATE_PROTECTED_PATHS,
  6. runs pending migrations, then shopAfterUpdate().

Take a backup first. The button does not do it for you — that stays a visible, deliberate act.

Limits, all deliberate

  • No rollback. The aside copies in data/manage/updates/ are for manual recovery, and only the most recent run is kept.
  • Deleted files are not removed. Deployment is an overlay; a file no longer in the new release stays behind. Removing it belongs in a migration.
  • No maintenance mode. The shop stays reachable while files are copied. Roll out during a quiet period.
  • Files can be live while the post-update step failed. The settings page reports the two separately. A failed migration leaves the rest pending; fix the cause and press Migrationen ausführen.

Protected paths

MANAGE_UPDATE_PROTECTED_PATHS is config.php, data/, .git/. Note that assets/images/ is not protected, and does not need to be: the updater only touches paths contained in the package, so uploaded images survive on their own, while a release can still ship or update its own images.

Cutting a release

./scripts/create-release-zip.sh v1.1.0

The script writes the version into includes/version.php, packs every git-tracked file minus config.php, data/, build/, scripts/ and .codex/, and prints size and SHA-256. Commit the version bump, then upload the ZIP in the Manage server under Releases.

Two things to get right:

  • PRODUCT at the top of the script must match MANAGE_PACKAGE_PREFIX on the Manage server.
  • The version format is strictly vX.Y.Z. Forgetting to bump it means the instance keeps reporting the old version and keeps being offered the same update.

Only committed content is packaged — the script warns on a dirty tree rather than refusing, because building from one is occasionally deliberate.

Heartbeat

Reports version, PHP version, free disk space, pending migrations and the time of the last backup. Without cron it rides along with page loads: at most once an hour on the settings page (manageHeartbeatSendIfDue()), and immediately after a backup or update, when those values have just changed. Status melden forces one.

It never breaks a page render — manageHeartbeatSendQuietly() swallows every error into the log.

Security notes

  • The token is the only secret between shop and server. It lives in config.php, which is gitignored and excluded from release packages. Rotate it on the Manage server if it leaks.
  • data/manage/ must not be web-readable. The root .htaccess denies all of data/; verify with curl -o /dev/null -w "%{http_code}\n" https://<host>/shop/data/manage/backups/ — anything but 403/404 is a problem. On nginx this needs a location rule instead.
  • Backup downloads validate the filename against backup-\d{8}-\d{6}(-\d+)?\.zip, so the form cannot be made to serve another path.
  • Every form on the settings page carries a CSRF token, and the page requires an admin session before anything else runs.
  • Transport is HTTPS with PHP's normal certificate verification, and redirects are not followed — a redirected request fails rather than sending the token somewhere else.
  • The checksum comes from the same server as the package. Whoever controls the Manage server can publish a package and its checksum; there is no signature layer. Secure the Manage server accordingly.

Troubleshooting

Message Cause
Der Manage-Client ist nicht konfiguriert. MANAGE_SERVER_URL, MANAGE_INSTANCE or MANAGE_TOKEN empty in config.php
Es ist kein gültiges Release veröffentlicht. no release published on the Manage server yet
Authentifizierung fehlgeschlagen. wrong or rotated token; instance id typo
Die Prüfsumme des Pakets stimmt nicht überein. corrupted download; nothing was deployed
Das Paket sieht nicht wie ein Release dieses Shops aus packaged with a wrapping directory, or the wrong ZIP
Die Datei konnte nicht ausgerollt werden: … PHP lacks write access in the shop root; the update stopped partway
Es läuft bereits ein Backup. a second run while one is in progress

data/manage/manage-client.log holds one JSON object per line for every backup, update, migration and failure.