#!/usr/bin/env php * * This file is part of a Moko Consulting project. * * SPDX-License-Identifier: GPL-3.0-or-later * * FILE INFORMATION * DEFGROUP: MokoStandards.Scripts.Deploy * INGROUP: MokoStandards * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /deploy/backup-before-deploy.php * VERSION: 01.00.00 * BRIEF: Snapshot Joomla directories before deployment for rollback capability */ declare(strict_types=1); class BackupBeforeDeploy { private bool $verbose = false; private string $configPath = ''; private string $outputDir = ''; private const JOOMLA_DIRS = [ 'administrator/components', 'administrator/language', 'administrator/modules', 'administrator/templates', 'components', 'language', 'layouts', 'libraries', 'media', 'modules', 'plugins', 'templates', ]; public function run(): int { $this->parseArgs(); if ($this->configPath === '') { $this->log('Usage: backup-before-deploy.php --config [--output ] [--verbose]'); return 1; } if ($this->outputDir === '') { $this->outputDir = '/tmp/moko-snapshot-' . date('Ymd-His'); } $config = $this->loadConfig($this->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($this->outputDir)) { if (!mkdir($this->outputDir, 0755, true)) { $this->log("ERROR: Could not create output directory: {$this->outputDir}"); return 1; } } $this->log('Starting pre-deploy snapshot...'); $this->log("Source: {$user}@{$host}:{$remotePath}"); $this->log("Output: {$this->outputDir}"); $failed = 0; foreach (self::JOOMLA_DIRS as $dir) { $remoteSource = "{$remotePath}/{$dir}/"; $localTarget = rtrim($this->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("Downloading: {$dir}"); if ($this->verbose) { $this->log("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(" {$line}"); } $failed++; } else { if ($this->verbose) { foreach ($output as $line) { $this->log(" {$line}"); } } } } if ($failed > 0) { $this->log("Snapshot completed with {$failed} error(s)."); return 1; } $this->log(''); $this->log('Snapshot completed successfully.'); $this->log("SNAPSHOT_PATH={$this->outputDir}"); $this->log(''); $this->log('To rollback, run:'); $this->log(" php rollback-joomla.php --config {$this->configPath} --snapshot-dir {$this->outputDir}"); return 0; } private function parseArgs(): void { $args = $_SERVER['argv'] ?? []; $count = count($args); for ($i = 1; $i < $count; $i++) { switch ($args[$i]) { case '--config': $this->configPath = $args[++$i] ?? ''; break; case '--output': $this->outputDir = $args[++$i] ?? ''; break; case '--verbose': $this->verbose = true; break; } } } 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); } private function log(string $message): void { $timestamp = date('Y-m-d H:i:s'); fwrite(STDERR, "[{$timestamp}] {$message}" . PHP_EOL); } } $app = new BackupBeforeDeploy(); exit($app->run());