From 496e89fd2e8473bf75483063dcba06f0244dc495 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucian=20V=C4=83c=C4=83roiu?= Date: Mon, 17 Nov 2025 20:04:57 +0200 Subject: [PATCH 1/3] feat: add site:shared push pull commands for shared files --- app/Console/Site/SiteSharedPullCommand.php | 210 +++++++++++++++++++++ app/Console/Site/SiteSharedPushCommand.php | 193 +++++++++++++++++++ 2 files changed, 403 insertions(+) create mode 100644 app/Console/Site/SiteSharedPullCommand.php create mode 100644 app/Console/Site/SiteSharedPushCommand.php diff --git a/app/Console/Site/SiteSharedPullCommand.php b/app/Console/Site/SiteSharedPullCommand.php new file mode 100644 index 00000000..aa063aa3 --- /dev/null +++ b/app/Console/Site/SiteSharedPullCommand.php @@ -0,0 +1,210 @@ +addOption('site', null, InputOption::VALUE_REQUIRED, 'Site domain') + ->addOption('remote', null, InputOption::VALUE_REQUIRED, 'Remote filename (relative to shared/)') + ->addOption('local', null, InputOption::VALUE_REQUIRED, 'Local destination file path'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + parent::execute($input, $output); + + $this->heading('Download Shared File'); + + $site = $this->selectSite(); + if (is_int($site)) { + return $site; + } + + $server = $this->getServerForSite($site); + if (is_int($server)) { + return $server; + } + + $remoteRelative = $this->resolveRemotePath(); + if ($remoteRelative === null) { + return Command::FAILURE; + } + + $remotePath = $this->buildSharedPath($site, $remoteRelative); + + try { + if (! $this->remoteFileExists($server, $remotePath)) { + $this->nay("Remote file not found: {$remoteRelative}"); + + return Command::FAILURE; + } + } catch (\RuntimeException $e) { + $this->nay($e->getMessage()); + + return Command::FAILURE; + } + + $localPath = $this->resolveLocalPath($remoteRelative); + if ($localPath === null) { + return Command::FAILURE; + } + + if ($this->fs->exists($localPath)) { + /** @var bool $overwrite */ + $overwrite = $this->io->promptConfirm( + label: "Local file {$localPath} exists. Overwrite?", + default: false + ); + + if (! $overwrite) { + $this->io->warning('Download cancelled.'); + + return Command::SUCCESS; + } + } + + $this->io->info("Downloading {$remotePath} to {$localPath}"); + $this->io->writeln(''); + + try { + $this->ssh->downloadFile($server, $remotePath, $localPath); + } catch (\RuntimeException $e) { + $this->nay($e->getMessage()); + + return Command::FAILURE; + } + + $this->yay('Shared file downloaded'); + + $this->showCommandReplay('site:shared:pull', [ + 'site' => $site->domain, + 'remote' => $remoteRelative, + 'local' => $localPath, + ]); + + return Command::SUCCESS; + } + + private function resolveRemotePath(): ?string + { + /** @var string|null $remoteInput */ + $remoteInput = $this->io->getOptionOrPrompt( + 'remote', + fn (): string => $this->io->promptText( + label: 'Remote filename (relative to shared/):', + placeholder: '.env', + required: true + ) + ); + + $normalized = $this->normalizeRelativePath($remoteInput ?? ''); + + if ($normalized === null) { + return null; + } + + return $normalized; + } + + private function resolveLocalPath(string $remoteRelative): ?string + { + $default = basename($remoteRelative) ?: $remoteRelative; + + /** @var string $localInput */ + $localInput = $this->io->getOptionOrPrompt( + 'local', + fn (): string => $this->io->promptText( + label: 'Local destination path:', + default: $default, + required: true + ) + ); + + try { + return $this->fs->expandPath($localInput); + } catch (\RuntimeException $e) { + $this->nay($e->getMessage()); + + return null; + } + } + + private function normalizeRelativePath(string $path): ?string + { + $cleaned = trim(str_replace('\\', '/', $path)); + $cleaned = preg_replace('#/+#', '/', $cleaned); + + if ($cleaned === null) { + $this->nay('Remote filename is required.'); + + return null; + } + + $cleaned = ltrim($cleaned, '/'); + + if ($cleaned === '' || str_contains($cleaned, '..')) { + $this->nay('Remote filename must be relative to the shared/ directory and cannot contain "..".'); + + return null; + } + + return $cleaned; + } + + private function buildSharedPath(SiteDTO $site, string $relative = ''): string + { + $sharedRoot = $this->getSiteSharedPath($site); + + if ($relative === '') { + return $sharedRoot; + } + + return rtrim($sharedRoot, '/').'/'.ltrim($relative, '/'); + } + + private function remoteFileExists(ServerDTO $server, string $remotePath): bool + { + $result = $this->ssh->executeCommand( + $server, + sprintf('test -f %s', escapeshellarg($remotePath)) + ); + + if ($result['exit_code'] === 0) { + return true; + } + + if ($result['exit_code'] === 1) { + return false; + } + + $output = trim((string) $result['output']); + $message = $output === '' ? "Failed checking remote file: {$remotePath}" : $output; + + throw new \RuntimeException($message); + } +} diff --git a/app/Console/Site/SiteSharedPushCommand.php b/app/Console/Site/SiteSharedPushCommand.php new file mode 100644 index 00000000..cf7d8635 --- /dev/null +++ b/app/Console/Site/SiteSharedPushCommand.php @@ -0,0 +1,193 @@ +addOption('site', null, InputOption::VALUE_REQUIRED, 'Site domain') + ->addOption('local', null, InputOption::VALUE_REQUIRED, 'Local file path to upload') + ->addOption('remote', null, InputOption::VALUE_REQUIRED, 'Remote filename (relative to shared/)'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + parent::execute($input, $output); + + $this->heading('Upload Shared File'); + + $site = $this->selectSite(); + if (is_int($site)) { + return $site; + } + + $server = $this->getServerForSite($site); + if (is_int($server)) { + return $server; + } + + $localPath = $this->resolveLocalPath(); + if ($localPath === null) { + return Command::FAILURE; + } + + $remoteRelative = $this->resolveRemotePath($localPath); + if ($remoteRelative === null) { + return Command::FAILURE; + } + + $remotePath = $this->buildSharedPath($site, $remoteRelative); + $remoteDir = dirname($remotePath); + + $this->io->info("Uploading {$localPath} to {$remotePath}"); + $this->io->writeln(''); + + try { + $this->runRemoteCommand($server, sprintf('mkdir -p %s', escapeshellarg($remoteDir))); + $this->ssh->uploadFile($server, $localPath, $remotePath); + $this->runRemoteCommand($server, sprintf('chmod 640 %s', escapeshellarg($remotePath))); + + if ($server->username !== 'deployer') { + $this->runRemoteCommand( + $server, + sprintf('chown deployer:deployer %s', escapeshellarg($remotePath)) + ); + } + } catch (\RuntimeException $e) { + $this->nay($e->getMessage()); + + return Command::FAILURE; + } + + $this->yay('Shared file uploaded'); + + $this->showCommandReplay('site:shared:push', [ + 'site' => $site->domain, + 'local' => $localPath, + 'remote' => $remoteRelative, + ]); + + return Command::SUCCESS; + } + + private function resolveLocalPath(): ?string + { + /** @var string $localInput */ + $localInput = $this->io->getOptionOrPrompt( + 'local', + fn (): string => $this->io->promptText( + label: 'Local file path:', + placeholder: '.env.production', + required: true + ) + ); + + try { + $expanded = $this->fs->expandPath($localInput); + } catch (\RuntimeException $e) { + $this->nay($e->getMessage()); + + return null; + } + + if (! $this->fs->exists($expanded) || ! is_file($expanded)) { + $this->nay("Local file not found: {$expanded}"); + + return null; + } + + return $expanded; + } + + private function resolveRemotePath(string $localPath): ?string + { + $defaultName = basename($localPath); + + /** @var string|null $remoteInput */ + $remoteInput = $this->io->getOptionOrPrompt( + 'remote', + fn (): string => $this->io->promptText( + label: 'Remote filename (relative to shared/):', + placeholder: $defaultName === '' ? '.env' : $defaultName, + default: $defaultName === '' ? '.env' : $defaultName, + required: true + ) + ); + + $normalized = $this->normalizeRelativePath($remoteInput ?? ''); + + if ($normalized === null) { + return null; + } + + return $normalized; + } + + private function normalizeRelativePath(string $path): ?string + { + $cleaned = trim(str_replace('\\', '/', $path)); + $cleaned = preg_replace('#/+#', '/', $cleaned); + + if ($cleaned === null) { + $this->nay('Remote filename is required.'); + + return null; + } + + $cleaned = ltrim($cleaned, '/'); + + if ($cleaned === '' || str_contains($cleaned, '..')) { + $this->nay('Remote filename must be relative to the shared/ directory and cannot contain "..".'); + + return null; + } + + return $cleaned; + } + + private function buildSharedPath(SiteDTO $site, string $relative = ''): string + { + $sharedRoot = $this->getSiteSharedPath($site); + + if ($relative === '') { + return $sharedRoot; + } + + return rtrim($sharedRoot, '/').'/'.ltrim($relative, '/'); + } + + private function runRemoteCommand(ServerDTO $server, string $command): void + { + $result = $this->ssh->executeCommand($server, $command); + if ($result['exit_code'] !== 0) { + $output = trim((string) $result['output']); + $message = $output === '' ? "Remote command failed: {$command}" : $output; + + throw new \RuntimeException($message); + } + } +} From 00e3663c9ab98c6d6fe2309bf6d32d826e0c1cbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucian=20V=C4=83c=C4=83roiu?= Date: Mon, 17 Nov 2025 20:06:59 +0200 Subject: [PATCH 2/3] fixup --- app/Traits/ServersTrait.php | 59 +++++++++++++++++++++++-------------- app/Traits/SitesTrait.php | 34 ++++++++++++++++----- 2 files changed, 63 insertions(+), 30 deletions(-) diff --git a/app/Traits/ServersTrait.php b/app/Traits/ServersTrait.php index d6b3a48b..22c0209e 100644 --- a/app/Traits/ServersTrait.php +++ b/app/Traits/ServersTrait.php @@ -41,7 +41,7 @@ trait ServersTrait * Automatically displays server info and validates that the server is running a supported distribution (Debian/Ubuntu) * and has sufficient permissions (root or sudo). * - * @param ServerDTO $server Server to get information for + * @param ServerDTO $server Server to get information for * @return array|int Returns parsed server info or failure code on failure */ protected function serverInfo(ServerDTO $server): array|int @@ -73,7 +73,7 @@ protected function serverInfo(ServerDTO $server): array|int /** * Validate that server is running a supported distribution. * - * @param array $info Server information array from server-info playbook + * @param array $info Server information array from server-info playbook * @return array|int Returns validated server info or failure code */ protected function validateServerDistribution(array $info): array|int @@ -90,7 +90,7 @@ protected function validateServerDistribution(array $info): array|int $distroName = $distribution->displayName(); - if (!$distribution->isSupported()) { + if (! $distribution->isSupported()) { $this->nay("Unsupported distribution: {$distroName}. Only Debian and Ubuntu are supported."); return Command::FAILURE; @@ -102,14 +102,14 @@ protected function validateServerDistribution(array $info): array|int /** * Validate that server has sufficient permissions (root or sudo). * - * @param array $info Server information array from server-info playbook + * @param array $info Server information array from server-info playbook * @return array|int Returns validated server info or failure code */ protected function validateServerPermissions(array $info): array|int { $permissions = $info['permissions'] ?? null; - if (!is_string($permissions) || !in_array($permissions, ['root', 'sudo'])) { + if (! is_string($permissions) || ! in_array($permissions, ['root', 'sudo'])) { $this->nay('Server requires root or passwordless sudo permissions'); $this->io->writeln([ '', @@ -130,7 +130,7 @@ protected function validateServerPermissions(array $info): array|int /** * Display formatted server information. * - * @param array $info + * @param array $info */ protected function displayServerInfo(array $info): void { @@ -310,7 +310,7 @@ protected function displayServerInfo(array $info): void // Display PHP-FPM information if available (multiple versions) if (isset($info['php_fpm']) && is_array($info['php_fpm']) && count($info['php_fpm']) > 0) { foreach ($info['php_fpm'] as $version => $fpmData) { - if (!is_array($fpmData) || !is_string($version)) { + if (! is_array($fpmData) || ! is_string($version)) { continue; } @@ -427,7 +427,7 @@ private function formatUptime(int $seconds): string /** * Display a warning to add a server if no servers are available. Otherwise, return all servers. * - * @param array|null $servers Optional pre-fetched servers; if null, fetches from repository + * @param array|null $servers Optional pre-fetched servers; if null, fetches from repository * @return array|int Returns array of servers or Command::FAILURE if no servers available */ protected function ensureServersAvailable(?array $servers = null): array|int @@ -458,7 +458,7 @@ protected function ensureServersAvailable(?array $servers = null): array|int /** * Select a server from inventory by name option or interactive prompt. * - * @param array|null $servers Optional pre-fetched servers; if null, fetches from repository + * @param array|null $servers Optional pre-fetched servers; if null, fetches from repository * @return ServerDTO|int Returns ServerDTO on success, or Command::FAILURE on error */ protected function selectServer(?array $servers = null): ServerDTO|int @@ -526,6 +526,22 @@ protected function displayServerDeets(ServerDTO $server): void $this->io->writeln(''); } + /** + * Resolve the server associated with a site, handling error output. + */ + protected function getServerForSite(SiteDTO $site): ServerDTO|int + { + $server = $this->servers->findByName($site->server); + + if ($server === null) { + $this->nay("Server '{$site->server}' not found in inventory"); + + return Command::FAILURE; + } + + return $server; + } + /** * Verify SSH connection to a server with proper error handling. * @@ -563,8 +579,8 @@ protected function verifySSHConnection(ServerDTO $server): int $this->io->writeln([ '', 'The server will be added to the inventory regardless. You can either:', - ' • Wait a minute and run server:info --server=' . $server->name . ' to check again', - ' • Or run server:install --server=' . $server->name . ' to install software when ready', + ' • Wait a minute and run server:info --server='.$server->name.' to check again', + ' • Or run server:install --server='.$server->name.' to install software when ready', '', ]); @@ -589,13 +605,13 @@ protected function isDigitalOceanServer(ServerDTO $server): bool // ---- /** - * Validate server name format and uniqueness. - * - * @return string|null Error message if invalid, null if valid - */ + * Validate server name format and uniqueness. + * + * @return string|null Error message if invalid, null if valid + */ protected function validateServerName(mixed $name): ?string { - if (!is_string($name)) { + if (! is_string($name)) { return 'Server name must be a string'; } @@ -605,7 +621,7 @@ protected function validateServerName(mixed $name): ?string } // Validate format: alphanumeric, hyphens, underscores only - if (!preg_match('/^[a-zA-Z0-9_-]+$/', $name)) { + if (! preg_match('/^[a-zA-Z0-9_-]+$/', $name)) { return 'Server name can only contain letters, numbers, hyphens, and underscores'; } @@ -625,7 +641,7 @@ protected function validateServerName(mixed $name): ?string */ protected function validateServerHost(mixed $host): ?string { - if (!is_string($host)) { + if (! is_string($host)) { return 'Host must be a string'; } @@ -633,7 +649,7 @@ protected function validateServerHost(mixed $host): ?string $isValidIp = filter_var($host, FILTER_VALIDATE_IP) !== false; $isValidDomain = filter_var($host, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) !== false; - if (!$isValidIp && !$isValidDomain) { + if (! $isValidIp && ! $isValidDomain) { return 'Must be a valid IP address or domain name (e.g., 192.168.1.100, example.com)'; } @@ -653,11 +669,11 @@ protected function validateServerHost(mixed $host): ?string */ protected function validateServerPort(mixed $portString): ?string { - if (!is_string($portString)) { + if (! is_string($portString)) { return 'Port must be a string'; } - if (!ctype_digit($portString)) { + if (! ctype_digit($portString)) { return 'Port must be a number'; } @@ -668,5 +684,4 @@ protected function validateServerPort(mixed $portString): ?string return null; } - } diff --git a/app/Traits/SitesTrait.php b/app/Traits/SitesTrait.php index 42d1c59f..9045e289 100644 --- a/app/Traits/SitesTrait.php +++ b/app/Traits/SitesTrait.php @@ -34,7 +34,7 @@ trait SitesTrait /** * Display a warning to add a site if no sites are available. Otherwise, return all sites. * - * @param array|null $sites Optional pre-fetched sites; if null, fetches from repository + * @param array|null $sites Optional pre-fetched sites; if null, fetches from repository * @return array|int Returns array of sites or Command::SUCCESS if no sites available */ protected function ensureSitesAvailable(?array $sites = null): array|int @@ -64,7 +64,7 @@ protected function ensureSitesAvailable(?array $sites = null): array|int /** * Select a site from inventory by domain option or interactive prompt. * - * @param array|null $sites Optional pre-fetched sites; if null, fetches from repository + * @param array|null $sites Optional pre-fetched sites; if null, fetches from repository * @return SiteDTO|int Returns SiteDTO on success, or Command::SUCCESS if empty inventory, or Command::FAILURE if not found */ protected function selectSite(?array $sites = null): SiteDTO|int @@ -78,13 +78,15 @@ protected function selectSite(?array $sites = null): SiteDTO|int return $allSites; } + $optionName = $this->getDefinition()->hasOption('site') ? 'site' : 'domain'; + // // Extract site domains and prompt for selection $siteDomains = array_map(fn (SiteDTO $site) => $site->domain, $allSites); $domain = (string) $this->io->getOptionOrPrompt( - 'domain', + $optionName, fn () => $this->io->promptSelect( label: 'Select site:', options: $siteDomains, @@ -133,13 +135,13 @@ protected function displaySiteDeets(SiteDTO $site): void */ protected function validateSiteDomain(mixed $domain): ?string { - if (!is_string($domain)) { + if (! is_string($domain)) { return 'Domain must be a string'; } // Check format $isValid = filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) !== false; - if (!$isValid) { + if (! $isValid) { return 'Must be a valid domain name (e.g., example.com, subdomain.example.com)'; } @@ -159,7 +161,7 @@ protected function validateSiteDomain(mixed $domain): ?string */ protected function validateSiteBranch(mixed $branch): ?string { - if (!is_string($branch)) { + if (! is_string($branch)) { return 'Branch name must be a string'; } @@ -177,7 +179,7 @@ protected function validateSiteBranch(mixed $branch): ?string */ protected function validateSiteRepo(mixed $repo): ?string { - if (!is_string($repo)) { + if (! is_string($repo)) { return 'Repository URL must be a string'; } @@ -197,10 +199,26 @@ protected function validateSiteRepo(mixed $repo): ?string } } - if (!$hasValidPrefix) { + if (! $hasValidPrefix) { return 'Repository URL must start with git@, https://, http://, or ssh://'; } return null; } + + /** + * Get the remote root path for a site. + */ + protected function getSiteRootPath(SiteDTO $site): string + { + return '/home/deployer/sites/'.$site->domain; + } + + /** + * Get the remote shared directory path for a site. + */ + protected function getSiteSharedPath(SiteDTO $site): string + { + return $this->getSiteRootPath($site).'/shared'; + } } From ae7d90b18870695670373a9c63d082b2deab32dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucian=20V=C4=83c=C4=83roiu?= Date: Mon, 17 Nov 2025 20:19:55 +0200 Subject: [PATCH 3/3] fixup --- app/Console/Site/SiteSharedPullCommand.php | 62 +++++++++++++++++++++- app/Console/Site/SiteSharedPushCommand.php | 52 +++++++++++++++++- app/Traits/SitesTrait.php | 4 +- 3 files changed, 111 insertions(+), 7 deletions(-) diff --git a/app/Console/Site/SiteSharedPullCommand.php b/app/Console/Site/SiteSharedPullCommand.php index aa063aa3..2dc53d74 100644 --- a/app/Console/Site/SiteSharedPullCommand.php +++ b/app/Console/Site/SiteSharedPullCommand.php @@ -7,6 +7,7 @@ use Bigpixelrocket\DeployerPHP\Contracts\BaseCommand; use Bigpixelrocket\DeployerPHP\DTOs\ServerDTO; use Bigpixelrocket\DeployerPHP\DTOs\SiteDTO; +use Bigpixelrocket\DeployerPHP\Traits\PlaybooksTrait; use Bigpixelrocket\DeployerPHP\Traits\ServersTrait; use Bigpixelrocket\DeployerPHP\Traits\SitesTrait; use Symfony\Component\Console\Attribute\AsCommand; @@ -21,35 +22,72 @@ )] class SiteSharedPullCommand extends BaseCommand { + use PlaybooksTrait; use ServersTrait; use SitesTrait; + // ---- + // Configuration + // ---- + protected function configure(): void { parent::configure(); $this - ->addOption('site', null, InputOption::VALUE_REQUIRED, 'Site domain') + ->addOption('domain', null, InputOption::VALUE_REQUIRED, 'Site domain') ->addOption('remote', null, InputOption::VALUE_REQUIRED, 'Remote filename (relative to shared/)') ->addOption('local', null, InputOption::VALUE_REQUIRED, 'Local destination file path'); } + // ---- + // Execution + // ---- + protected function execute(InputInterface $input, OutputInterface $output): int { parent::execute($input, $output); $this->heading('Download Shared File'); + // + // Select site + // ---- + $site = $this->selectSite(); + if (is_int($site)) { return $site; } + $this->displaySiteDeets($site); + + // + // Get server for site + // ---- + $server = $this->getServerForSite($site); + if (is_int($server)) { return $server; } + $this->displayServerDeets($server); + + // + // Verify server connection + // ---- + + $info = $this->serverInfo($server); + + if (is_int($info)) { + return $info; + } + + // + // Resolve paths + // ---- + $remoteRelative = $this->resolveRemotePath(); if ($remoteRelative === null) { return Command::FAILURE; @@ -57,6 +95,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int $remotePath = $this->buildSharedPath($site, $remoteRelative); + // + // Verify remote file exists + // ---- + try { if (! $this->remoteFileExists($server, $remotePath)) { $this->nay("Remote file not found: {$remoteRelative}"); @@ -74,6 +116,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::FAILURE; } + // + // Check local file overwrite + // ---- + if ($this->fs->exists($localPath)) { /** @var bool $overwrite */ $overwrite = $this->io->promptConfirm( @@ -88,6 +134,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int } } + // + // Download file + // ---- + $this->io->info("Downloading {$remotePath} to {$localPath}"); $this->io->writeln(''); @@ -101,8 +151,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->yay('Shared file downloaded'); + // + // Show command replay + // ---- + $this->showCommandReplay('site:shared:pull', [ - 'site' => $site->domain, + 'domain' => $site->domain, 'remote' => $remoteRelative, 'local' => $localPath, ]); @@ -110,6 +164,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::SUCCESS; } + // ---- + // Helpers + // ---- + private function resolveRemotePath(): ?string { /** @var string|null $remoteInput */ diff --git a/app/Console/Site/SiteSharedPushCommand.php b/app/Console/Site/SiteSharedPushCommand.php index cf7d8635..d5f26743 100644 --- a/app/Console/Site/SiteSharedPushCommand.php +++ b/app/Console/Site/SiteSharedPushCommand.php @@ -7,6 +7,7 @@ use Bigpixelrocket\DeployerPHP\Contracts\BaseCommand; use Bigpixelrocket\DeployerPHP\DTOs\ServerDTO; use Bigpixelrocket\DeployerPHP\DTOs\SiteDTO; +use Bigpixelrocket\DeployerPHP\Traits\PlaybooksTrait; use Bigpixelrocket\DeployerPHP\Traits\ServersTrait; use Bigpixelrocket\DeployerPHP\Traits\SitesTrait; use Symfony\Component\Console\Attribute\AsCommand; @@ -21,35 +22,70 @@ )] class SiteSharedPushCommand extends BaseCommand { + use PlaybooksTrait; use ServersTrait; use SitesTrait; + // ---- + // Configuration + // ---- + protected function configure(): void { parent::configure(); $this - ->addOption('site', null, InputOption::VALUE_REQUIRED, 'Site domain') + ->addOption('domain', null, InputOption::VALUE_REQUIRED, 'Site domain') ->addOption('local', null, InputOption::VALUE_REQUIRED, 'Local file path to upload') ->addOption('remote', null, InputOption::VALUE_REQUIRED, 'Remote filename (relative to shared/)'); } + // ---- + // Execution + // ---- + protected function execute(InputInterface $input, OutputInterface $output): int { parent::execute($input, $output); $this->heading('Upload Shared File'); + // + // Select site + // ---- + $site = $this->selectSite(); if (is_int($site)) { return $site; } + $this->displaySiteDeets($site); + + // + // Get server for site + // ---- + $server = $this->getServerForSite($site); if (is_int($server)) { return $server; } + $this->displayServerDeets($server); + + // + // Verify server connection + // ---- + + $info = $this->serverInfo($server); + + if (is_int($info)) { + return $info; + } + + // + // Resolve paths + // ---- + $localPath = $this->resolveLocalPath(); if ($localPath === null) { return Command::FAILURE; @@ -63,6 +99,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int $remotePath = $this->buildSharedPath($site, $remoteRelative); $remoteDir = dirname($remotePath); + // + // Upload file + // ---- + $this->io->info("Uploading {$localPath} to {$remotePath}"); $this->io->writeln(''); @@ -85,8 +125,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->yay('Shared file uploaded'); + // + // Show command replay + // ---- + $this->showCommandReplay('site:shared:push', [ - 'site' => $site->domain, + 'domain' => $site->domain, 'local' => $localPath, 'remote' => $remoteRelative, ]); @@ -94,6 +138,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::SUCCESS; } + // ---- + // Helpers + // ---- + private function resolveLocalPath(): ?string { /** @var string $localInput */ diff --git a/app/Traits/SitesTrait.php b/app/Traits/SitesTrait.php index 9045e289..d854f6fa 100644 --- a/app/Traits/SitesTrait.php +++ b/app/Traits/SitesTrait.php @@ -78,15 +78,13 @@ protected function selectSite(?array $sites = null): SiteDTO|int return $allSites; } - $optionName = $this->getDefinition()->hasOption('site') ? 'site' : 'domain'; - // // Extract site domains and prompt for selection $siteDomains = array_map(fn (SiteDTO $site) => $site->domain, $allSites); $domain = (string) $this->io->getOptionOrPrompt( - $optionName, + 'domain', fn () => $this->io->promptSelect( label: 'Select site:', options: $siteDomains,