Maintenance
'
. 'The backup and update client is not installed: manage-client/ '
. 'is missing. See docs/SETUP.md, section 5a.
';
admin_footer();
exit;
}
require_once APP_ROOT . '/manage-client/lib/client.php';
$messages = [];
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
csrf_verify();
// Archiving media/ and uploading it runs well past the default limit.
@set_time_limit(0);
$action = (string)($_POST['action'] ?? '');
try {
if ($action === 'download') {
// Validates the filename itself and throws on anything that is not
// a backup of this installation.
$path = manageBackupPath((string)($_POST['filename'] ?? ''));
$size = filesize($path);
$fh = fopen($path, 'rb');
if ($size === false || $fh === false) {
throw new RuntimeException('Backup file could not be opened.');
}
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="' . addcslashes(basename($path), '"\\') . '"');
header('Content-Length: ' . $size);
header('Cache-Control: private, no-store');
header('X-Content-Type-Options: nosniff');
fpassthru($fh);
fclose($fh);
exit;
}
if ($action === 'backup') {
$messages = array_merge($messages, maintenance_backup('manual'));
manageHeartbeatSendQuietly();
} elseif ($action === 'update') {
// Deliberately a line here rather than a feature of the client: it
// stays visible that the update takes a backup first, and the
// update stops if that backup cannot be written.
if (!empty($_POST['backup_first'])) {
$messages = array_merge($messages, maintenance_backup('update'));
}
$result = manageUpdateApply(['force' => !empty($_POST['force'])]);
$messages[] = sprintf(
'Update deployed: %s → %s. %d files copied, %d replaced files saved to %s.',
$result['from_version'] !== '' ? $result['from_version'] : 'unknown',
$result['to_version'],
$result['copied'],
$result['backed_up'],
$result['backup_dir'],
);
// Files are deployed even when the hook failed; that difference is
// the whole point of reporting it separately.
$hook = is_array($result['hook'] ?? null) ? $result['hook'] : [];
$applied = $hook['migrations']['applied'] ?? [];
if ($applied !== []) {
$messages[] = 'Migrations run: ' . implode(', ', $applied);
}
if ($hook !== [] && empty($hook['success'])) {
$errors[] = empty($hook['failed_migration'])
? 'The files are deployed, but the post-update step failed: '
. (string)($hook['error'] ?? 'unknown')
: 'The files are deployed, but migration "' . (string)$hook['failed_migration']
. '" failed: ' . (string)($hook['error'] ?? 'unknown')
. ' Remaining migrations were not attempted — fix the cause, then use "Run migrations" below.';
}
manageHeartbeatSendQuietly();
} elseif ($action === 'migrate') {
$report = manageUpdateRunMigrations();
if ($report['applied'] !== []) {
$messages[] = 'Migrations run: ' . implode(', ', $report['applied']);
}
if (!$report['success']) {
$errors[] = 'Migration "' . (string)$report['failed'] . '" failed: ' . (string)$report['error'];
} elseif ($report['applied'] === []) {
$messages[] = 'No pending migrations.';
}
} elseif ($action === 'heartbeat') {
$result = manageHeartbeatSend();
$messages[] = 'Status reported. Current release: '
. (($result['latest'] ?? '') !== '' ? (string)$result['latest'] : 'none') . '.';
}
} catch (Throwable $e) {
$errors[] = $e->getMessage();
}
}
/**
* One backup, reported as message lines. A failed upload is a warning, not a
* failure: the archive exists locally either way.
*/
function maintenance_backup(string $trigger): array
{
$record = manageBackupCreate($trigger);
$lines = [sprintf(
'Backup created: %s — %d files, %s.',
$record['filename'],
$record['file_count'],
manageFormatBytes((int)$record['size']),
)];
foreach ($record['remote_uploads'] as $upload) {
if (empty($upload['success'])) {
$lines[] = 'Upload to ' . (string)$upload['target'] . ' failed: '
. (string)($upload['error'] ?? 'unknown') . ' The local copy is intact.';
}
}
return $lines;
}
// Never throws: an unreachable manage server still renders the page.
$status = manageClientStatus();
$update = $status['update'];
$capabilities = manageRemoteCapabilities();
admin_header('Maintenance', 'maintenance');
flash_render();
?>
Maintenance
= e($line) ?>
= e($line) ?>
Not connected to the manage server. Create an instance there, then fill in
MANAGE_INSTANCE and MANAGE_TOKEN in
manage-client/config.php. Backups can still be made locally.
| Installed version |
= e($status['version'] !== '' ? $status['version'] : 'unknown') ?> |
PHP = e($status['php_version']) ?> |
| Current release |
= e($update['latest']) ?> available
= e($update['latest'] !== '' ? $update['latest'] : '—') ?>
—
|
Check failed: = e($status['update_error']) ?>
Back up first, then deploy.
Up to date.
= e($status['instance'] !== '' ? $status['instance'] : 'no instance configured') ?>
|
| Last backup |
= e($status['last_backup_at'] ?? 'never') ?> |
= count($status['backups']) ?> kept locally |
| Release migrations |
= count($status['pending_migrations']) ?> pending |
= $status['pending_migrations'] === []
? 'Nothing to run.'
: e(implode(', ', array_column($status['pending_migrations'], 'id'))) ?>
|
Backup
Archives data/ and media/ — galleries, showreel,
front page, settings — and uploads it to the manage server. Gallery photos
are not included: they live in the S3 bucket, which is their own backup.
Nor are config/ credentials, because a backup can be
downloaded again from the server.
Update
Version = e($update['latest']) ?> is ready
(published = e($update['manifest']['published_at']) ?>)
.
Files are replaced while the site stays online, and there is no rollback:
the replaced files are copied to data/manage/updates/ for
manual recovery. config/, data/ and
media/ are never touched. Deleted files are not removed —
an update overlays what is there.
Afterwards, check Data migration.
Pending release migrations
Shipped with a release and normally run by the update itself. These are
left over — usually because one failed, or because the update ran with
migrations skipped.
Local backups
No backup has been made yet.
| File | Created | Trigger |
Files | Size | Upload | |
| = e((string)($backup['filename'] ?? '')) ?> |
= e((string)($backup['created_at'] ?? '')) ?> |
= e((string)($backup['trigger'] ?? '')) ?> |
= (int)($backup['file_count'] ?? 0) ?> |
= e(manageFormatBytes((int)($backup['size'] ?? 0))) ?> |
—
= e((string)($upload['target'] ?? '?')) ?>= empty($upload['success']) ? ' failed' : '' ?>
|
|
Kept locally: = (int)MANAGE_BACKUP_LOCAL_RETENTION ?>. Older ones are
deleted here after each new backup; the manage server keeps its own,
longer history.
$capability) {
if ($capability['configured'] && !$capability['available']) {
$unsupported[] = $type;
}
}
?>
Configured backup targets this server cannot use:
= e(implode(', ', $unsupported)) ?>. Those uploads will fail.
= e($line) ?>
Report status
Sends version, PHP version, free disk space and the time of the last
backup to the manage server. Normally an hourly cron job; this is the
manual version of it.
Same operations from the shell:
php manage-client/bin/manage-client.php status|check|backup|update|migrate|heartbeat.
See docs/SETUP.md.