diff --git a/deploy/backup-before-deploy.php b/deploy/backup-before-deploy.php new file mode 100644 index 0000000..7139cdf --- /dev/null +++ b/deploy/backup-before-deploy.php @@ -0,0 +1,212 @@ +#!/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());