#!/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/backup-before-deploy.php * VERSION: 09.21.07 * BRIEF: Snapshot Joomla directories before deployment for rollback capability */ declare(strict_types=1); require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; use MokoEnterprise\CliFramework; class BackupBeforeDeployCli 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('Snapshot Joomla directories before deployment for rollback capability'); $this->addArgument('--config', 'Path to sftp-config.json', ''); $this->addArgument('--output', 'Local output directory for snapshot', ''); } protected function run(): int { $configPath = $this->getArgument('--config'); $outputDir = $this->getArgument('--output'); if ($configPath === '') { $this->log('ERROR', 'Usage: backup-before-deploy.php --config [--output ] [--verbose]'); return 1; } if ($outputDir === '') { $outputDir = '/tmp/moko-snapshot-' . date('Ymd-His'); } $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; } // Create output directory if (!is_dir($outputDir)) { if (!mkdir($outputDir, 0755, true)) { $this->log('ERROR', "Could not create output directory: {$outputDir}"); return 1; } } $this->log('INFO', 'Starting pre-deploy snapshot...'); $this->log('INFO', "Source: {$user}@{$host}:{$remotePath}"); $this->log('INFO', "Output: {$outputDir}"); $failed = 0; foreach (self::JOOMLA_DIRS as $dir) { $remoteSource = "{$remotePath}/{$dir}/"; $localTarget = rtrim($outputDir, '/\\') . '/' . $dir . '/'; // Ensure local subdirectory exists if (!is_dir($localTarget)) { mkdir($localTarget, 0755, true); } $sshCmd = "ssh -p {$port}"; if ($sshKey !== '') { $sshCmd .= " -i " . escapeshellarg($sshKey); } $cmd = $this->buildRsyncCommand( $sshCmd, "{$user}@{$host}:{$remoteSource}", $localTarget ); $this->log('INFO', "Downloading: {$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', "Snapshot completed with {$failed} error(s)."); return 1; } $this->log('INFO', ''); $this->log('INFO', 'Snapshot completed successfully.'); $this->log('INFO', "SNAPSHOT_PATH={$outputDir}"); $this->log('INFO', ''); $this->log('INFO', 'To rollback, run:'); $this->log('INFO', " php rollback-joomla.php --config {$configPath} --snapshot-dir {$outputDir}"); 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', '--exclude=configuration.php']; if ($this->verbose) { $parts[] = '-v'; } $parts[] = '-e'; $parts[] = escapeshellarg($sshCmd); $parts[] = escapeshellarg($source); $parts[] = escapeshellarg($dest); return implode(' ', $parts); } } $app = new BackupBeforeDeployCli(); exit($app->execute());