#!/usr/bin/env php * * This file is part of a Moko Consulting project. * * SPDX-License-Identifier: GPL-3.0-or-later * * FILE INFORMATION * DEFGROUP: MokoPlatform.Scripts.Deploy * INGROUP: MokoPlatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /deploy/rollback-joomla.php * VERSION: 09.23.00 * BRIEF: Rollback a Joomla deployment by restoring from a pre-deploy snapshot */ declare(strict_types=1); require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; use MokoEnterprise\CliFramework; class RollbackJoomlaCli extends CliFramework { private const JOOMLA_DIRS = [ 'administrator/components', 'administrator/language', 'administrator/modules', 'administrator/templates', 'components', 'language', 'layouts', 'libraries', 'media', 'modules', 'plugins', 'templates', ]; protected function configure(): void { $this->setDescription('Rollback a Joomla deployment by restoring from a pre-deploy snapshot'); $this->addArgument('--config', 'Path to sftp-config.json', ''); $this->addArgument('--snapshot-dir', 'Path to snapshot directory', ''); } protected function run(): int { $configPath = $this->getArgument('--config'); $snapshotDir = $this->getArgument('--snapshot-dir'); if ($configPath === '' || $snapshotDir === '') { $this->log('ERROR', 'Usage: rollback-joomla.php --config --snapshot-dir [--dry-run] [--verbose]'); return 1; } if (!is_dir($snapshotDir)) { $this->log('ERROR', "Snapshot directory does not exist: {$snapshotDir}"); return 1; } $config = $this->loadConfig($configPath); if ($config === null) { return 1; } $host = $config['host'] ?? ''; $user = $config['user'] ?? ''; $port = (int) ($config['port'] ?? 22); $remotePath = rtrim($config['remote_path'] ?? '', '/'); $sshKey = $config['ssh_key_file'] ?? ''; if ($host === '' || $user === '' || $remotePath === '') { $this->log('ERROR', 'Config must contain host, user, and remote_path.'); return 1; } $this->log('INFO', 'Starting Joomla rollback from snapshot...'); $this->log('INFO', "Snapshot: {$snapshotDir}"); $this->log('INFO', "Target: {$user}@{$host}:{$remotePath}"); if ($this->dryRun) { $this->log('INFO', '*** DRY RUN — no changes will be made ***'); } $failed = 0; foreach (self::JOOMLA_DIRS as $dir) { $localDir = rtrim($snapshotDir, '/\\') . '/' . $dir . '/'; if (!is_dir($localDir)) { if ($this->verbose) { $this->log('INFO', "SKIP: {$dir} (not present in snapshot)"); } continue; } $remoteTarget = "{$remotePath}/{$dir}/"; $sshCmd = "ssh -p {$port}"; if ($sshKey !== '') { $sshCmd .= " -i " . escapeshellarg($sshKey); } $cmd = $this->buildRsyncCommand($sshCmd, $localDir, "{$user}@{$host}:{$remoteTarget}"); $this->log('INFO', "Restoring: {$dir}"); if ($this->verbose) { $this->log('INFO', "CMD: {$cmd}"); } $output = []; $exitCode = 0; exec($cmd, $output, $exitCode); if ($exitCode !== 0) { $this->log('ERROR', "rsync failed for {$dir} (exit code {$exitCode})"); foreach ($output as $line) { $this->log('ERROR', " {$line}"); } $failed++; } else { if ($this->verbose) { foreach ($output as $line) { $this->log('INFO', " {$line}"); } } } } if ($failed > 0) { $this->log('ERROR', "Rollback completed with {$failed} error(s)."); return 1; } $this->log('INFO', 'Rollback completed successfully.'); return 0; } private function loadConfig(string $path): ?array { if (!is_file($path)) { $this->log('ERROR', "Config file not found: {$path}"); return null; } $raw = file_get_contents($path); if ($raw === false) { $this->log('ERROR', "Could not read config file: {$path}"); return null; } // Strip // comments (sftp-config.json style) $cleaned = preg_replace('#^\s*//.*$#m', '', $raw); $config = json_decode($cleaned, true); if (!is_array($config)) { $this->log('ERROR', 'Invalid JSON in config file.'); return null; } return $config; } private function buildRsyncCommand(string $sshCmd, string $source, string $dest): string { $parts = ['rsync', '-rlptz', '--delete', '--exclude=configuration.php']; if ($this->dryRun) { $parts[] = '--dry-run'; } if ($this->verbose) { $parts[] = '-v'; } $parts[] = '-e'; $parts[] = escapeshellarg($sshCmd); $parts[] = escapeshellarg($source); $parts[] = escapeshellarg($dest); return implode(' ', $parts); } } $app = new RollbackJoomlaCli(); exit($app->execute());