From 736de603c4fb983678c79cf16e23f7a81dce9d40 Mon Sep 17 00:00:00 2001 From: Laurent Muller Date: Mon, 25 Mar 2024 13:10:11 +0100 Subject: [PATCH] Implemented requireCompressMark option (#164). --- deployment.sample.ini | 3 + src/Deployment/CliRunner.php | 613 ++++++++++++++++++----------------- 2 files changed, 311 insertions(+), 305 deletions(-) diff --git a/deployment.sample.ini b/deployment.sample.ini index 31909388..6b2713c9 100644 --- a/deployment.sample.ini +++ b/deployment.sample.ini @@ -59,6 +59,9 @@ after[] = local: git reset HEAD --hard ; reverts all changes in working direct ; files to preprocess (defaults to none) preprocess = *.js *.css +; compression mark (default to yes) +requireCompressMark = no + ; file which contains hashes of all uploaded files (defaults to .htdeployment) deploymentFile = .deployment diff --git a/src/Deployment/CliRunner.php b/src/Deployment/CliRunner.php index 7faf6841..85a3df1f 100644 --- a/src/Deployment/CliRunner.php +++ b/src/Deployment/CliRunner.php @@ -1,305 +1,308 @@ - '', - 'passivemode' => true, - 'include' => '', - 'ignore' => '', - 'allowdelete' => true, - 'purge' => '', - 'before' => '', - 'afterupload' => '', - 'after' => '', - 'preprocess' => false, - ]; - - /** @var string[] */ - public array $ignoreMasks = ['*.bak', '.svn', '.git*', 'Thumbs.db', '.DS_Store', '.idea']; - private Logger $logger; - private string $configFile; - - /** test|generate|null */ - private ?string $mode; - - /** @var array[] */ - private array $batches = []; - - /** @var resource */ - private $lock; - - - public function run(): ?int - { - $this->logger = new Logger('php://memory'); - $this->setupPhp(); - - $config = $this->loadConfig(); - if (!$config) { - return 1; - } - - $this->logger = new Logger($config['log']); - $this->logger->useColors = (bool) $config['colors']; - $this->logger->showProgress = (bool) $config['progress']; - - if (!is_dir($tempDir = $config['tempdir'])) { - $this->logger->log("Creating temporary directory $tempDir"); - mkdir($tempDir, 0777, true); - } - - $time = time(); - $this->logger->log('Started at ' . date('[Y/m/d H:i]')); - $this->logger->log("Config file is $this->configFile"); - $res = 0; - - foreach ($this->batches as $name => $batch) { - $this->logger->log("\nDeploying $name"); - - $deployment = $this->createDeployer($batch); - $deployment->tempDir = $tempDir; - - if ($this->mode === 'generate') { - $this->logger->log('Scanning files'); - $localPaths = $deployment->collectPaths(); - $this->logger->log('Saved ' . $deployment->writeDeploymentFile($localPaths)); - continue; - } - - if ($deployment->testMode) { - $this->logger->log('Test mode', 'lime'); - } else { - $this->logger->log('Live mode', 'aqua'); - } - if (!$deployment->allowDelete) { - $this->logger->log('Deleting disabled'); - } - - try { - $deployment->deploy(); - } catch (JobException | ServerException $e) { - $this->logger->log("Error: {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}\n\n$e", 'red'); - $res = 1; - } - $this->logger->log("\n\n"); - } - - $time = time() - $time; - $this->logger->log('Finished at ' . date('[Y/m/d H:i]') . " (in $time seconds)\n----------------------------------------------\n\n", 'lime'); - return $res; - } - - - private function createDeployer(array $config): Deployer - { - if ( - empty($config['remote']) - || !($urlParts = parse_url($config['remote'])) - || !isset($urlParts['scheme']) - ) { - throw new \Exception("Missing or invalid 'remote' URL in config."); - } - if (isset($config['user'])) { - $urlParts['user'] = urlencode($config['user']); - } - if (isset($config['password'])) { - $urlParts['pass'] = urlencode($config['password']); - } - - if ($urlParts['scheme'] === 'ftp') { - $this->logger->log('Note: connection is not encrypted', 'white/red'); - } - - if ($urlParts['scheme'] === 'phpsec') { - $server = new PhpsecServer(Helpers::buildUrl($urlParts), $config['publickey'] ?? null, $config['privatekey'] ?? null, $config['passphrase'] ?? null); - } elseif ($urlParts['scheme'] === 'sftp') { - $server = new SshServer(Helpers::buildUrl($urlParts), $config['publickey'] ?? null, $config['privatekey'] ?? null, $config['passphrase'] ?? null); - } elseif ($urlParts['scheme'] === 'file') { - $server = new FileServer($config['remote']); - } else { - $server = new FtpServer(Helpers::buildUrl($urlParts), (bool) $config['passivemode']); - } - $server->filePermissions = empty($config['filepermissions']) - ? null - : octdec($config['filepermissions']); - $server->dirPermissions = empty($config['dirpermissions']) - ? null - : octdec($config['dirpermissions']); - - $server = new RetryServer($server, $this->logger); - - if (!preg_match('#/|\\\\|[a-z]:#iA', $config['local'])) { - $config['local'] = dirname($this->configFile) . '/' . $config['local']; - } - - $deployment = new Deployer($server, $config['local'], $this->logger); - - if ($config['preprocess']) { - $deployment->preprocessMasks = $config['preprocess'] == 1 - ? ['*.js', '*.css'] - : self::toArray($config['preprocess']); // intentionally == - $preprocessor = new Preprocessor($this->logger); - $deployment->addFilter('js', [$preprocessor, 'expandApacheImports']); - $deployment->addFilter('js', [$preprocessor, 'compressJs'], true); - $deployment->addFilter('css', [$preprocessor, 'expandApacheImports']); - $deployment->addFilter('css', [$preprocessor, 'expandCssImports']); - $deployment->addFilter('css', [$preprocessor, 'compressCss'], true); - } - - $deployment->includeMasks = self::toArray($config['include'], true); - $deployment->ignoreMasks = array_merge(self::toArray($config['ignore']), $this->ignoreMasks); - $deployment->deploymentFile = empty($config['deploymentfile']) - ? $deployment->deploymentFile - : $config['deploymentfile']; - $deployment->allowDelete = (bool) $config['allowdelete']; - $deployment->toPurge = self::toArray($config['purge'], true); - $deployment->runBefore = self::toArray($config['before'], true); - $deployment->runAfterUpload = self::toArray($config['afterupload'], true); - $deployment->runAfter = self::toArray($config['after'], true); - $deployment->testMode = !empty($config['test']) || $this->mode === 'test'; - - return $deployment; - } - - - private function setupPhp(): void - { - set_time_limit(0); - date_default_timezone_set('Europe/Prague'); - - set_error_handler(function ($severity, $message, $file, $line) { - if (($severity & error_reporting()) !== $severity) { - return false; - } - - if (ini_get('html_errors')) { - $message = html_entity_decode(strip_tags($message)); - } - - throw new \ErrorException($message, 0, $severity, $file, $line); - }); - - set_exception_handler(function (\Throwable $e): void { - $this->logger->log("Error: {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}\n\n$e", 'red'); - exit(1); - }); - - if (extension_loaded('pcntl')) { - pcntl_signal(SIGINT, function (): void { - pcntl_signal(SIGINT, SIG_DFL); - throw new \Exception('Terminated'); - }); - pcntl_async_signals(true); - - } elseif (function_exists('sapi_windows_set_ctrl_handler')) { - sapi_windows_set_ctrl_handler(function () { - throw new \Exception('Terminated'); - }); - } - } - - - private function loadConfig(): ?array - { - $cmd = new CommandLine( - <<<'XX' - - FTP deployment v3.6 - ------------------- - Usage: - deployment [-t | --test] - - Options: - -t | --test Run in test-mode. - --section Only deploys the named section. - --generate Only generates deployment file. - --no-progress Hide the progress indicators. - - XX, - [ - 'config' => [CommandLine::RealPath => true], - ], - ); - - if ($cmd->isEmpty()) { - $cmd->help(); - return null; - } - - $options = $cmd->parse(); - $this->mode = $options['--generate'] - ? 'generate' - : ($options['--test'] ? 'test' : null); - $this->configFile = $options['config']; - - $config = $this->loadConfigFile($options['config']); - if (!$config) { - throw new \Exception('Missing config.'); - } - - if (!flock($this->lock = fopen($options['config'], 'r'), LOCK_EX | LOCK_NB)) { - throw new \Exception('It seems that you are in the middle of another deployment.'); - } - - $this->batches = isset($config['remote']) && is_string($config['remote']) - ? ['' => $config] - : array_filter($config, 'is_array'); - - if (isset($options['--section'])) { - $section = $options['--section']; - if ($section === '') { - throw new \Exception('Missing section name.'); - } elseif (!isset($this->batches[$section])) { - throw new \Exception("Unknown section '$section'."); - } - $this->batches = [$section => $this->batches[$section]]; - } - - foreach ($this->batches as &$batch) { - $batch = array_change_key_case($batch, CASE_LOWER) + $this->defaults; - } - - $config = array_change_key_case($config, CASE_LOWER) + [ - 'log' => preg_replace('#\.\w+$#', '.log', $this->configFile), - 'tempdir' => sys_get_temp_dir() . '/deployment', - 'progress' => true, - 'colors' => (PHP_SAPI === 'cli' && ((function_exists('posix_isatty') && posix_isatty(STDOUT)) - || getenv('ConEmuANSI') === 'ON' || getenv('ANSICON') !== false)), - ]; - $config['progress'] = $options['--no-progress'] ? false : $config['progress']; - return $config; - } - - - protected function loadConfigFile(string $file): array - { - if (pathinfo($file, PATHINFO_EXTENSION) == 'php') { - return include $file; - } else { - return parse_ini_file($file, true); - } - } - - - public static function toArray($val, bool $lines = false): array - { - return is_array($val) - ? array_filter($val) - : preg_split($lines ? '#\s*\n\s*#' : '#\s+#', $val, -1, PREG_SPLIT_NO_EMPTY); - } -} + '', + 'passivemode' => true, + 'include' => '', + 'ignore' => '', + 'allowdelete' => true, + 'purge' => '', + 'before' => '', + 'afterupload' => '', + 'after' => '', + 'preprocess' => false, + ]; + + /** @var string[] */ + public array $ignoreMasks = ['*.bak', '.svn', '.git*', 'Thumbs.db', '.DS_Store', '.idea']; + private Logger $logger; + private string $configFile; + + /** test|generate|null */ + private ?string $mode; + + /** @var array[] */ + private array $batches = []; + + /** @var resource */ + private $lock; + + + public function run(): ?int + { + $this->logger = new Logger('php://memory'); + $this->setupPhp(); + + $config = $this->loadConfig(); + if (!$config) { + return 1; + } + + $this->logger = new Logger($config['log']); + $this->logger->useColors = (bool) $config['colors']; + $this->logger->showProgress = (bool) $config['progress']; + + if (!is_dir($tempDir = $config['tempdir'])) { + $this->logger->log("Creating temporary directory $tempDir"); + mkdir($tempDir, 0777, true); + } + + $time = time(); + $this->logger->log('Started at ' . date('[Y/m/d H:i]')); + $this->logger->log("Config file is $this->configFile"); + $res = 0; + + foreach ($this->batches as $name => $batch) { + $this->logger->log("\nDeploying $name"); + + $deployment = $this->createDeployer($batch); + $deployment->tempDir = $tempDir; + + if ($this->mode === 'generate') { + $this->logger->log('Scanning files'); + $localPaths = $deployment->collectPaths(); + $this->logger->log('Saved ' . $deployment->writeDeploymentFile($localPaths)); + continue; + } + + if ($deployment->testMode) { + $this->logger->log('Test mode', 'lime'); + } else { + $this->logger->log('Live mode', 'aqua'); + } + if (!$deployment->allowDelete) { + $this->logger->log('Deleting disabled'); + } + + try { + $deployment->deploy(); + } catch (JobException | ServerException $e) { + $this->logger->log("Error: {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}\n\n$e", 'red'); + $res = 1; + } + $this->logger->log("\n\n"); + } + + $time = time() - $time; + $this->logger->log('Finished at ' . date('[Y/m/d H:i]') . " (in $time seconds)\n----------------------------------------------\n\n", 'lime'); + return $res; + } + + + private function createDeployer(array $config): Deployer + { + if ( + empty($config['remote']) + || !($urlParts = parse_url($config['remote'])) + || !isset($urlParts['scheme']) + ) { + throw new \Exception("Missing or invalid 'remote' URL in config."); + } + if (isset($config['user'])) { + $urlParts['user'] = urlencode($config['user']); + } + if (isset($config['password'])) { + $urlParts['pass'] = urlencode($config['password']); + } + + if ($urlParts['scheme'] === 'ftp') { + $this->logger->log('Note: connection is not encrypted', 'white/red'); + } + + if ($urlParts['scheme'] === 'phpsec') { + $server = new PhpsecServer(Helpers::buildUrl($urlParts), $config['publickey'] ?? null, $config['privatekey'] ?? null, $config['passphrase'] ?? null); + } elseif ($urlParts['scheme'] === 'sftp') { + $server = new SshServer(Helpers::buildUrl($urlParts), $config['publickey'] ?? null, $config['privatekey'] ?? null, $config['passphrase'] ?? null); + } elseif ($urlParts['scheme'] === 'file') { + $server = new FileServer($config['remote']); + } else { + $server = new FtpServer(Helpers::buildUrl($urlParts), (bool) $config['passivemode']); + } + $server->filePermissions = empty($config['filepermissions']) + ? null + : octdec($config['filepermissions']); + $server->dirPermissions = empty($config['dirpermissions']) + ? null + : octdec($config['dirpermissions']); + + $server = new RetryServer($server, $this->logger); + + if (!preg_match('#/|\\\\|[a-z]:#iA', $config['local'])) { + $config['local'] = dirname($this->configFile) . '/' . $config['local']; + } + + $deployment = new Deployer($server, $config['local'], $this->logger); + + if ($config['preprocess']) { + $deployment->preprocessMasks = $config['preprocess'] == 1 + ? ['*.js', '*.css'] + : self::toArray($config['preprocess']); // intentionally == + $preprocessor = new Preprocessor($this->logger); + if (isset($config['requireCompressMark'])) { + $preprocessor->requireCompressMark = 'no' !== $config['requireCompressMark']; + } + $deployment->addFilter('js', [$preprocessor, 'expandApacheImports']); + $deployment->addFilter('js', [$preprocessor, 'compressJs'], true); + $deployment->addFilter('css', [$preprocessor, 'expandApacheImports']); + $deployment->addFilter('css', [$preprocessor, 'expandCssImports']); + $deployment->addFilter('css', [$preprocessor, 'compressCss'], true); + } + + $deployment->includeMasks = self::toArray($config['include'], true); + $deployment->ignoreMasks = array_merge(self::toArray($config['ignore']), $this->ignoreMasks); + $deployment->deploymentFile = empty($config['deploymentfile']) + ? $deployment->deploymentFile + : $config['deploymentfile']; + $deployment->allowDelete = (bool) $config['allowdelete']; + $deployment->toPurge = self::toArray($config['purge'], true); + $deployment->runBefore = self::toArray($config['before'], true); + $deployment->runAfterUpload = self::toArray($config['afterupload'], true); + $deployment->runAfter = self::toArray($config['after'], true); + $deployment->testMode = !empty($config['test']) || $this->mode === 'test'; + + return $deployment; + } + + + private function setupPhp(): void + { + set_time_limit(0); + date_default_timezone_set('Europe/Prague'); + + set_error_handler(function ($severity, $message, $file, $line) { + if (($severity & error_reporting()) !== $severity) { + return false; + } + + if (ini_get('html_errors')) { + $message = html_entity_decode(strip_tags($message)); + } + + throw new \ErrorException($message, 0, $severity, $file, $line); + }); + + set_exception_handler(function (\Throwable $e): void { + $this->logger->log("Error: {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}\n\n$e", 'red'); + exit(1); + }); + + if (extension_loaded('pcntl')) { + pcntl_signal(SIGINT, function (): void { + pcntl_signal(SIGINT, SIG_DFL); + throw new \Exception('Terminated'); + }); + pcntl_async_signals(true); + + } elseif (function_exists('sapi_windows_set_ctrl_handler')) { + sapi_windows_set_ctrl_handler(function () { + throw new \Exception('Terminated'); + }); + } + } + + + private function loadConfig(): ?array + { + $cmd = new CommandLine( + <<<'XX' + + FTP deployment v3.6 + ------------------- + Usage: + deployment [-t | --test] + + Options: + -t | --test Run in test-mode. + --section Only deploys the named section. + --generate Only generates deployment file. + --no-progress Hide the progress indicators. + + XX, + [ + 'config' => [CommandLine::RealPath => true], + ], + ); + + if ($cmd->isEmpty()) { + $cmd->help(); + return null; + } + + $options = $cmd->parse(); + $this->mode = $options['--generate'] + ? 'generate' + : ($options['--test'] ? 'test' : null); + $this->configFile = $options['config']; + + $config = $this->loadConfigFile($options['config']); + if (!$config) { + throw new \Exception('Missing config.'); + } + + if (!flock($this->lock = fopen($options['config'], 'r'), LOCK_EX | LOCK_NB)) { + throw new \Exception('It seems that you are in the middle of another deployment.'); + } + + $this->batches = isset($config['remote']) && is_string($config['remote']) + ? ['' => $config] + : array_filter($config, 'is_array'); + + if (isset($options['--section'])) { + $section = $options['--section']; + if ($section === '') { + throw new \Exception('Missing section name.'); + } elseif (!isset($this->batches[$section])) { + throw new \Exception("Unknown section '$section'."); + } + $this->batches = [$section => $this->batches[$section]]; + } + + foreach ($this->batches as &$batch) { + $batch = array_change_key_case($batch, CASE_LOWER) + $this->defaults; + } + + $config = array_change_key_case($config, CASE_LOWER) + [ + 'log' => preg_replace('#\.\w+$#', '.log', $this->configFile), + 'tempdir' => sys_get_temp_dir() . '/deployment', + 'progress' => true, + 'colors' => (PHP_SAPI === 'cli' && ((function_exists('posix_isatty') && posix_isatty(STDOUT)) + || getenv('ConEmuANSI') === 'ON' || getenv('ANSICON') !== false)), + ]; + $config['progress'] = $options['--no-progress'] ? false : $config['progress']; + return $config; + } + + + protected function loadConfigFile(string $file): array + { + if (pathinfo($file, PATHINFO_EXTENSION) == 'php') { + return include $file; + } else { + return parse_ini_file($file, true); + } + } + + + public static function toArray($val, bool $lines = false): array + { + return is_array($val) + ? array_filter($val) + : preg_split($lines ? '#\s*\n\s*#' : '#\s+#', $val, -1, PREG_SPLIT_NO_EMPTY); + } +}