diff --git a/Classes/Command/AbstractCommandController.php b/Classes/Command/AbstractCommandController.php index f20ae5a..1069a22 100644 --- a/Classes/Command/AbstractCommandController.php +++ b/Classes/Command/AbstractCommandController.php @@ -9,10 +9,10 @@ use Neos\Flow\Annotations as Flow; use Neos\Flow\Core\Bootstrap; use Neos\Flow\Cli\CommandController; +use Sitegeist\MagicWand\Domain\Service\ConfigurationService; abstract class AbstractCommandController extends CommandController { - const HIDE_RESULT = 1; const HIDE_COMMAND = 2; @@ -44,6 +44,12 @@ abstract class AbstractCommandController extends CommandController */ protected $flowCommand; + /** + * @Flow\Inject + * @var ConfigurationService + */ + protected $configurationService; + /** * @param string $commands * @param array $arguments @@ -53,11 +59,11 @@ protected function executeLocalShellCommand($command, $arguments = [], $options { $customizedCommand = call_user_func_array('sprintf', array_merge([$command], $arguments)); if (!in_array(self::HIDE_COMMAND, $options)) { - $this->outputLine($customizedCommand); + $this->renderLine($customizedCommand); } $customizedCommandResult = shell_exec($customizedCommand); if (is_string($customizedCommandResult) && !in_array(self::HIDE_RESULT, $options)) { - $this->outputLine($customizedCommandResult); + $this->renderLine($customizedCommandResult); } return $customizedCommandResult; } @@ -87,19 +93,19 @@ protected function executeLocalFlowCommand($command, $arguments = [], $options = /** * @param $line */ - protected function outputHeadLine($line = '', $arguments = []) + protected function renderHeadLine($line = '', $arguments = []) { $this->headlineNumber++; - $this->outputLine(); - $this->outputLine('' . $this->headlineNumber . '. ' . $line . '', $arguments); - $this->outputLine(); + $this->renderLine(); + $this->renderLine('' . $this->headlineNumber . '. ' . $line . '', $arguments); + $this->renderLine(); } /** * @param string $line * @param array $arguments */ - protected function outputLine(string $line = '', array $arguments = []) + protected function renderLine(string $line = '', array $arguments = []) { $filteredLine = $line; foreach ($this->secrets as $secret) { diff --git a/Classes/Command/CloneCommandController.php b/Classes/Command/CloneCommandController.php index 0c56527..2ee5052 100644 --- a/Classes/Command/CloneCommandController.php +++ b/Classes/Command/CloneCommandController.php @@ -10,6 +10,7 @@ use Neos\Utility\Arrays; use Neos\Flow\Core\Bootstrap; use Sitegeist\MagicWand\DBAL\SimpleDBAL; +use Symfony\Component\Yaml\Yaml; /** * @Flow\Scope("singleton") @@ -48,19 +49,11 @@ public function listCommand() { if ($this->clonePresets) { foreach ($this->clonePresets as $presetName => $presetConfiguration) { - $this->outputHeadLine($presetName); - foreach ($presetConfiguration as $key => $value) { - if (is_array($value)) { - $this->outputLine(' - ' . $key . ':'); - - foreach ($value as $line) { - $this->outputLine(' ' . $line); - } - - continue; - } - - $this->outputLine(' - ' . $key . ': ' . $value); + $this->renderHeadLine($presetName); + $presetConfigurationAsYaml = Yaml::dump($presetConfiguration); + $lines = explode(PHP_EOL, $presetConfigurationAsYaml); + foreach ($lines as $line) { + $this->renderLine($line); } } } @@ -75,7 +68,7 @@ public function listCommand() public function defaultCommand(bool $yes = false, bool $keepDb = false) : void { if ($this->defaultPreset === null || $this->defaultPreset === '') { - $this->outputLine('There is no default preset configured!'); + $this->renderLine('There is no default preset configured!'); $this->quit(1); } @@ -93,31 +86,35 @@ public function presetCommand($presetName, $yes = false, $keepDb = false) { if (count($this->clonePresets) > 0) { if ($this->clonePresets && array_key_exists($presetName, $this->clonePresets)) { - $this->outputLine('Clone by preset ' . $presetName); - $this->remoteHostCommand( - $this->clonePresets[$presetName]['host'], - $this->clonePresets[$presetName]['user'], - $this->clonePresets[$presetName]['port'], - $this->clonePresets[$presetName]['path'], - $this->clonePresets[$presetName]['context'], - (isset($this->clonePresets[$presetName]['postClone']) ? - $this->clonePresets[$presetName]['postClone'] : null + + $this->configurationService->setCurrentPreset($presetName); + $configuration = $this->configurationService->getCurrentConfiguration(); + + $this->renderLine('Clone by preset ' . $presetName); + $this->cloneRemoteHost( + $configuration['host'], + $configuration['user'], + $configuration['port'], + $configuration['path'], + $configuration['context'], + (isset($configuration['postClone']) ? + $configuration['postClone'] : null ), $yes, $keepDb, - (isset($this->clonePresets[$presetName]['flowCommand']) ? - $this->clonePresets[$presetName]['flowCommand'] : null + (isset($configuration['flowCommand']) ? + $configuration['flowCommand'] : null ), - (isset($this->clonePresets[$presetName]['sshOptions']) ? - $this->clonePresets[$presetName]['sshOptions'] : '' + (isset($configuration['sshOptions']) ? + $configuration['sshOptions'] : '' ) ); } else { - $this->outputLine('The preset ' . $presetName . ' was not found!'); + $this->renderLine('The preset ' . $presetName . ' was not found!'); $this->quit(1); } } else { - $this->outputLine('No presets found!'); + $this->renderLine('No presets found!'); $this->quit(1); } } @@ -136,7 +133,7 @@ public function presetCommand($presetName, $yes = false, $keepDb = false) * @param string $remoteFlowCommand the flow command to execute on the remote system * @param string $sshOptions additional options for the ssh command */ - public function remoteHostCommand( + protected function cloneRemoteHost( $host, $user, $port, @@ -155,12 +152,12 @@ public function remoteHostCommand( } // read local configuration - $this->outputHeadLine('Read local configuration'); + $this->renderHeadLine('Read local configuration'); $localDataPersistentPath = FLOW_PATH_ROOT . 'Data/Persistent'; // read remote configuration - $this->outputHeadLine('Fetch remote configuration'); + $this->renderHeadLine('Fetch remote configuration'); $remotePersistenceConfigurationYaml = $this->executeLocalShellCommand( 'ssh -p %s %s %s@%s "cd %s; FLOW_CONTEXT=%s ' . $remoteFlowCommand @@ -188,15 +185,15 @@ public function remoteHostCommand( ################# if (!$yes) { - $this->outputLine("Are you sure you want to do this? Type 'yes' to continue: "); + $this->renderLine("Are you sure you want to do this? Type 'yes' to continue: "); $handle = fopen("php://stdin", "r"); $line = fgets($handle); if (trim($line) != 'yes') { - $this->outputLine('exit'); + $this->renderLine('exit'); $this->quit(1); } else { - $this->outputLine(); - $this->outputLine(); + $this->renderLine(); + $this->renderLine(); } } @@ -238,7 +235,7 @@ public function remoteHostCommand( ######################## if ($keepDb == false) { - $this->outputHeadLine('Drop and Recreate DB'); + $this->renderHeadLine('Drop and Recreate DB'); $emptyLocalDbSql = $this->dbal->flushDbSql($this->databaseConfiguration['driver'], $this->databaseConfiguration['dbname']); @@ -257,14 +254,14 @@ public function remoteHostCommand( ] ); } else { - $this->outputHeadLine('Skipped (Drop and Recreate DB)'); + $this->renderHeadLine('Skipped (Drop and Recreate DB)'); } ###################### # Transfer Database # ###################### - $this->outputHeadLine('Transfer Database'); + $this->renderHeadLine('Transfer Database'); $this->executeLocalShellCommand( 'ssh -p %s %s %s@%s -- %s | %s', [ @@ -295,24 +292,41 @@ public function remoteHostCommand( # Transfer Files # ################## - $this->outputHeadLine('Transfer Files'); - $this->executeLocalShellCommand( - 'rsync -e "ssh -p %s %s" -kLr %s@%s:%s/* %s', - [ - $port, - addslashes($sshOptions), - $user, - $host, - $remoteDataPersistentPath, - $localDataPersistentPath - ] - ); + $resourceProxyConfiguration = $this->configurationService->getCurrentConfigurationByPath('resourceProxy'); + + if (!$resourceProxyConfiguration) { + $this->renderHeadLine('Transfer Files'); + $this->executeLocalShellCommand( + 'rsync -e "ssh -p %s %s" -kLr %s@%s:%s/* %s', + [ + $port, + addslashes($sshOptions), + $user, + $host, + $remoteDataPersistentPath, + $localDataPersistentPath + ] + ); + } else { + $this->renderHeadLine('Transfer Files - without Resources because a resourceProxyConfiguration is found'); + $this->executeLocalShellCommand( + 'rsync -e "ssh -p %s %s" --exclude "Resources/*" -kLr %s@%s:%s/* %s', + [ + $port, + addslashes($sshOptions), + $user, + $host, + $remoteDataPersistentPath, + $localDataPersistentPath + ] + ); + } ######################### # Transfer Translations # ######################### - $this->outputHeadLine('Transfer Translations'); + $this->renderHeadLine('Transfer Translations'); $remoteDataTranslationsPath = $path . '/Data/Translations'; $localDataTranslationsPath = FLOW_PATH_ROOT . 'Data/Translations'; @@ -348,14 +362,14 @@ public function remoteHostCommand( # Clear Caches # ################ - $this->outputHeadLine('Clear Caches'); + $this->renderHeadLine('Clear Caches'); $this->executeLocalFlowCommand('flow:cache:flush'); ################## # Set DB charset # ################## - if ($this->databaseConfiguration['driver'] == 'pdo_mysql' && $remotePersistenceConfiguration['charset'] != 'utf8mb4') { - $this->outputHeadLine('Set DB charset'); + if ($this->databaseConfiguration['driver'] == 'pdo_mysql' && $remotePersistenceConfiguration['charset'] != 'utf8mb4' ) { + $this->renderHeadLine('Set DB charset'); $this->executeLocalFlowCommand('database:setcharset'); } @@ -363,14 +377,14 @@ public function remoteHostCommand( # Migrate DB # ############## - $this->outputHeadLine('Migrate cloned DB'); + $this->renderHeadLine('Migrate cloned DB'); $this->executeLocalFlowCommand('doctrine:migrate'); ##################### # Publish Resources # ##################### - $this->outputHeadLine('Publish Resources'); + $this->renderHeadLine('Publish Resources'); $this->executeLocalFlowCommand('resource:publish'); ############## @@ -378,7 +392,7 @@ public function remoteHostCommand( ############## if ($postClone) { - $this->outputHeadLine('Execute post_clone commands'); + $this->renderHeadLine('Execute post_clone commands'); if (is_array($postClone)) { foreach ($postClone as $postCloneCommand) { $this->executeLocalShellCommandWithFlowContext($postCloneCommand); @@ -395,8 +409,8 @@ public function remoteHostCommand( $endTimestamp = time(); $duration = $endTimestamp - $startTimestamp; - $this->outputHeadLine('Done'); - $this->outputLine('Successfully cloned in %s seconds', [$duration]); + $this->renderHeadLine('Done'); + $this->renderLine('Successfully cloned in %s seconds', [$duration]); } /** @@ -406,22 +420,22 @@ public function remoteHostCommand( */ protected function checkConfiguration($remotePersistenceConfiguration) { - $this->outputHeadLine('Check Configuration'); + $this->renderHeadLine('Check Configuration'); if (!$this->dbal->driverIsSupported($remotePersistenceConfiguration['driver']) && !$this->dbal->driverIsSupported($this->databaseConfiguration['driver'])) { - $this->outputLine(sprintf('ERROR: Only pdo_pgsql and pdo_mysql drivers are supported! Remote: "%s" Local: "%s" configured.', $remotePersistenceConfiguration['driver'], $this->databaseConfiguration['driver'])); + $this->renderLine(sprintf('ERROR: Only pdo_pgsql and pdo_mysql drivers are supported! Remote: "%s" Local: "%s" configured.', $remotePersistenceConfiguration['driver'], $this->databaseConfiguration['driver'])); $this->quit(1); } if ($remotePersistenceConfiguration['driver'] !== $this->databaseConfiguration['driver']) { - $this->outputLine('ERROR: Remote and local databases must use the same driver!'); + $this->renderLine('ERROR: Remote and local databases must use the same driver!'); $this->quit(1); } if (in_array($remotePersistenceConfiguration['charset'], ['utf8', 'utf8mb4']) && in_array($this->databaseConfiguration['charset'], ['utf8', 'utf8mb4'])) { // we accept utf8 and utf8mb4 being similar enough } else if ($remotePersistenceConfiguration['charset'] != $this->databaseConfiguration['charset']) { - $this->outputLine(sprintf('ERROR: Remote and local databases must use the same charset! Remote: "%s", Local: "%s" configured.', $remotePersistenceConfiguration['charset'], $this->databaseConfiguration['charset'])); + $this->renderLine(sprintf('ERROR: Remote and local databases must use the same charset! Remote: "%s", Local: "%s" configured.', $remotePersistenceConfiguration['charset'], $this->databaseConfiguration['charset'])); $this->quit(1); } - $this->outputLine(' - Configuration seems ok ...'); + $this->renderLine(' - Configuration seems ok ...'); } } diff --git a/Classes/Command/StashCommandController.php b/Classes/Command/StashCommandController.php index 39ab8c7..81f5c63 100644 --- a/Classes/Command/StashCommandController.php +++ b/Classes/Command/StashCommandController.php @@ -15,9 +15,6 @@ */ class StashCommandController extends AbstractCommandController { - - - /** * Creates a new stash entry with the given name. * @@ -32,10 +29,7 @@ public function createCommand($name) # Build Paths # ####################### - $basePath = sprintf( - FLOW_PATH_ROOT . 'Data/MagicWandStash/%s', - $name - ); + $basePath = $this->getStashEntryPath($name); $databaseDestination = $basePath . '/database.sql'; $persistentDestination = $basePath . '/persistent/'; @@ -55,11 +49,29 @@ public function createCommand($name) $this->addSecret($this->databaseConfiguration['user']); $this->addSecret($this->databaseConfiguration['password']); + ###################### + # Write Manifest # + ###################### + $this->renderHeadLine('Write Manifest'); + $presetName = $this->configurationService->getCurrentPreset(); + $presetConfiguration = $this->configurationService->getCurrentConfiguration(); + $cloneTimestamp = $this->configurationService->getMostRecentCloneTimeStamp(); + $stashTimestamp = time(); + + $this->writeStashEntryManifest($name, [ + 'preset' => [ + 'name' => $presetName, + 'configuration' => $presetConfiguration + ], + 'cloned_at' => $cloneTimestamp, + 'stashed_at' => $stashTimestamp + ]); + ###################### # Backup Database # ###################### - $this->outputHeadLine('Backup Database'); + $this->renderHeadLine('Backup Database'); $this->executeLocalShellCommand( 'mysqldump --single-transaction --add-drop-table --host="%s" --user="%s" --password="%s" %s > %s', [ @@ -75,7 +87,7 @@ public function createCommand($name) # Backup Persistent Resources # ############################### - $this->outputHeadLine('Backup Persistent Resources'); + $this->renderHeadLine('Backup Persistent Resources'); $this->executeLocalShellCommand( 'cp -al %s %s', [ @@ -91,8 +103,8 @@ public function createCommand($name) $endTimestamp = time(); $duration = $endTimestamp - $startTimestamp; - $this->outputHeadLine('Done'); - $this->outputLine('Successfuly stashed %s in %s seconds', [$name, $duration]); + $this->renderHeadLine('Done'); + $this->renderLine('Successfuly stashed %s in %s seconds', [$name, $duration]); } /** @@ -102,10 +114,12 @@ public function createCommand($name) */ public function listCommand() { + $head = ['Name', 'Stashed At', 'From Preset', 'Cloned At']; + $rows = []; $basePath = sprintf(FLOW_PATH_ROOT . 'Data/MagicWandStash'); if (!is_dir($basePath)) { - $this->outputLine('Stash is empty.'); + $this->renderLine('Stash is empty.'); $this->quit(1); } @@ -114,15 +128,26 @@ public function listCommand() foreach ($baseDir as $entry) { if (!in_array($entry, ['.', '..'])) { - $this->outputLine(' • %s', [$entry->getFilename()]); + $stashEntryName = $entry->getFilename(); + $manifest = $this->readStashEntryManifest($stashEntryName) ?: []; + + $rows[] = [ + $stashEntryName, + $manifest['stashed_at'] ? date('Y-m-d H:i:s', $manifest['stashed_at']) : 'N/A', + isset($manifest['preset']['name']) ? $manifest['preset']['name'] : 'N/A', + $manifest['cloned_at'] ? date('Y-m-d H:i:s', $manifest['cloned_at']) : 'N/A', + ]; + $anyEntry = true; } } if (!$anyEntry) { - $this->outputLine('Stash is empty.'); + $this->renderLine('Stash is empty.'); $this->quit(1); } + + $this->output->outputTable($rows, $head); } /** @@ -144,8 +169,8 @@ public function clearCommand() $endTimestamp = time(); $duration = $endTimestamp - $startTimestamp; - $this->outputHeadLine('Done'); - $this->outputLine('Cleanup successful in %s seconds', [$duration]); + $this->renderHeadLine('Done'); + $this->renderLine('Cleanup successful in %s seconds', [$duration]); } /** @@ -158,7 +183,7 @@ public function clearCommand() */ public function restoreCommand($name, $yes = false, $keepDb = false) { - $basePath = sprintf(FLOW_PATH_ROOT . 'Data/MagicWandStash/%s', $name); + $basePath = $this->getStashEntryPath($name); $this->restoreStashEntry($basePath, $name, $yes, true, $keepDb); } @@ -174,21 +199,21 @@ public function removeCommand($name, $yes = false) $directory = FLOW_PATH_ROOT . 'Data/MagicWandStash/' . $name; if (!is_dir($directory)) { - $this->outputLine('%s does not exist', [$name]); + $this->renderLine('%s does not exist', [$name]); $this->quit(1); } if (!$yes) { - $this->outputLine("Are you sure you want to do this? Type 'yes' to continue: "); + $this->renderLine("Are you sure you want to do this? Type 'yes' to continue: "); $handle = fopen("php://stdin", "r"); $line = fgets($handle); if (trim($line) != 'yes') { - $this->outputLine('exit'); + $this->renderLine('exit'); $this->quit(1); } else { - $this->outputLine(); - $this->outputLine(); + $this->renderLine(); + $this->renderLine(); } } @@ -208,8 +233,8 @@ public function removeCommand($name, $yes = false) $endTimestamp = time(); $duration = $endTimestamp - $startTimestamp; - $this->outputHeadLine('Done'); - $this->outputLine('Cleanup removed stash %s in %s seconds', [$name, $duration]); + $this->renderHeadLine('Done'); + $this->renderLine('Cleanup removed stash %s in %s seconds', [$name, $duration]); } /** @@ -224,7 +249,7 @@ public function removeCommand($name, $yes = false) protected function restoreStashEntry($source, $name, $force = false, $preserve = true, $keepDb = false) { if (!is_dir($source)) { - $this->outputLine('%s does not exist', [$name]); + $this->renderLine('%s does not exist', [$name]); $this->quit(1); } @@ -233,16 +258,16 @@ protected function restoreStashEntry($source, $name, $force = false, $preserve = ################# if (!$force) { - $this->outputLine("Are you sure you want to do this? Type 'yes' to continue: "); + $this->renderLine("Are you sure you want to do this? Type 'yes' to continue: "); $handle = fopen("php://stdin", "r"); $line = fgets($handle); if (trim($line) != 'yes') { - $this->outputLine('exit'); + $this->renderLine('exit'); $this->quit(1); } else { - $this->outputLine(); - $this->outputLine(); + $this->renderLine(); + $this->renderLine(); } } @@ -270,14 +295,14 @@ protected function restoreStashEntry($source, $name, $force = false, $preserve = ######################## if ($keepDb == false) { - $this->outputHeadLine('Drop and Recreate DB'); + $this->renderHeadLine('Drop and Recreate DB'); $emptyLocalDbSql = 'DROP DATABASE `' . $this->databaseConfiguration['dbname'] . '`; CREATE DATABASE `' . $this->databaseConfiguration['dbname'] . '` collate utf8_unicode_ci;'; - + $this->executeLocalShellCommand( 'echo %s | mysql --host=%s --user=%s --password=%s', [ @@ -288,14 +313,14 @@ protected function restoreStashEntry($source, $name, $force = false, $preserve = ] ); } else { - $this->outputHeadLine('Skipped (Drop and Recreate DB)'); + $this->renderHeadLine('Skipped (Drop and Recreate DB)'); } ###################### # Restore Database # ###################### - $this->outputHeadLine('Restore Database'); + $this->renderHeadLine('Restore Database'); $this->executeLocalShellCommand( 'mysql --host="%s" --user="%s" --password="%s" %s < %s', [ @@ -311,7 +336,7 @@ protected function restoreStashEntry($source, $name, $force = false, $preserve = # Restore Persistent Resources # ################################ - $this->outputHeadLine('Restore Persistent Resources'); + $this->renderHeadLine('Restore Persistent Resources'); $this->executeLocalShellCommand( 'rm -rf %s && cp -al %s %1$s', [ @@ -329,7 +354,7 @@ protected function restoreStashEntry($source, $name, $force = false, $preserve = # Clear Caches # ################ - $this->outputHeadLine('Clear Caches'); + $this->renderHeadLine('Clear Caches'); $this->executeLocalFlowCommand('flow:cache:flush'); @@ -337,16 +362,23 @@ protected function restoreStashEntry($source, $name, $force = false, $preserve = # Migrate DB # ############## - $this->outputHeadLine('Migrate DB'); + $this->renderHeadLine('Migrate DB'); $this->executeLocalFlowCommand('doctrine:migrate'); ##################### # Publish Resources # ##################### - $this->outputHeadLine('Publish Resources'); + $this->renderHeadLine('Publish Resources'); $this->executeLocalFlowCommand('resource:publish'); + ############################# + # Restore Clone Information # + ############################# + if($manifest = $this->readStashEntryManifest($name)) { + $this->configurationService->setCurrentStashEntry($name, $manifest); + } + ################# # Final Message # ################# @@ -354,8 +386,8 @@ protected function restoreStashEntry($source, $name, $force = false, $preserve = $endTimestamp = time(); $duration = $endTimestamp - $startTimestamp; - $this->outputHeadLine('Done'); - $this->outputLine('Successfuly restored %s in %s seconds', [$name, $duration]); + $this->renderHeadLine('Done'); + $this->renderLine('Successfuly restored %s in %s seconds', [$name, $duration]); } /** @@ -363,13 +395,66 @@ protected function restoreStashEntry($source, $name, $force = false, $preserve = */ protected function checkConfiguration() { - $this->outputHeadLine('Check Configuration'); + $this->renderHeadLine('Check Configuration'); if ($this->databaseConfiguration['driver'] !== 'pdo_mysql') { - $this->outputLine(' only mysql is supported'); + $this->renderLine(' only mysql is supported'); $this->quit(1); } - $this->outputLine(' - Configuration seems ok ...'); + $this->renderLine(' - Configuration seems ok ...'); + } + + /** + * @param string $stashEntryName + * @return string + */ + protected function getStashEntryPath(string $stashEntryName): string + { + return sprintf( + FLOW_PATH_ROOT . 'Data/MagicWandStash/%s', + $stashEntryName + ); + } + + /** + * @param string $stashEntryName + * @return array|null + */ + protected function readStashEntryManifest(string $stashEntryName): ?array + { + $manifestDestination = $this->getStashEntryPath($stashEntryName) . '/manifest.json'; + + if (file_exists($manifestDestination)) { + if ($manifest = json_decode(file_get_contents($manifestDestination), true)) { + if (is_array($manifest)) { + return $manifest; + } + } + + $this->outputLine('Manifest file has been corrupted.'); + } + + return null; + } + + /** + * @param string $stashEntryName + * @param array $manifest + * @return void + */ + protected function writeStashEntryManifest(string $stashEntryName, array $manifest): void + { + $manifestDestination = $this->getStashEntryPath($stashEntryName) . '/manifest.json'; + + // Create directory, if not exists + if (!file_exists(dirname($manifestDestination))) { + FileUtils::createDirectoryRecursively(dirname($manifestDestination)); + } + + // Write manifest file + file_put_contents($manifestDestination, json_encode($manifest, JSON_PRETTY_PRINT)); + + $this->outputLine('Wrote "%s"', [$manifestDestination]); } } diff --git a/Classes/Controller/ResourceController.php b/Classes/Controller/ResourceController.php new file mode 100644 index 0000000..d6fe7ec --- /dev/null +++ b/Classes/Controller/ResourceController.php @@ -0,0 +1,46 @@ +resourceRepository->findByIdentifier($resourceIdentifier); + if ($resource) { + $sourceStream = $resource->getStream(); + if ($sourceStream !== false) { + fclose($sourceStream); + $this->redirectToUri($this->resourceManager->getPublicPersistentResourceUri($resource), 0, 302); + } else { + throw new ResourceNotFoundException(sprintf('Could not read stream of resource with id %s ', $resourceIdentifier)); + } + } + + throw new ResourceNotFoundException(sprintf('Could not find any resource with id %s in local database', $resourceIdentifier)); + } +} diff --git a/Classes/Domain/Service/ConfigurationService.php b/Classes/Domain/Service/ConfigurationService.php new file mode 100644 index 0000000..b461d46 --- /dev/null +++ b/Classes/Domain/Service/ConfigurationService.php @@ -0,0 +1,128 @@ +clonePresetInformationCache->get('current'); + + if ($clonePresetInformation && is_array($clonePresetInformation) && isset($clonePresetInformation['presetName'])) { + return $clonePresetInformation['presetName']; + } + + return null; + } + + /** + * @return integer + */ + public function getMostRecentCloneTimeStamp(): ?int + { + $clonePresetInformation = $this->clonePresetInformationCache->get('current'); + + if ($clonePresetInformation && is_array($clonePresetInformation) && isset($clonePresetInformation['cloned_at'])) { + return intval($clonePresetInformation['cloned_at']); + } + + return null; + } + + /** + * @return array + */ + public function getCurrentConfiguration(): array + { + if ($presetName = $this->getCurrentPreset()) { + if (is_array($this->clonePresets) && array_key_exists($presetName, $this->clonePresets)) { + return $this->clonePresets[$presetName]; + } + } + + return []; + } + + /** + * @return mixed + */ + public function getCurrentConfigurationByPath($path) + { + $currentConfiguration = $this->getCurrentConfiguration(); + return Arrays::getValueByPath($currentConfiguration, $path); + } + + /** + * @return boolean + */ + public function hasCurrentPreset(): bool + { + if ($this->clonePresetInformationCache->has('current')) { + return true; + } + + $clonePresetInformation = $this->clonePresetInformationCache->get('current'); + + if ($clonePresetInformation && is_array($clonePresetInformation) && isset($clonePresetInformation['presetName'])) { + return true; + } + + return false; + } + + /** + * @param $presetName string + * @return void + * @throws \Neos\Cache\Exception + */ + public function setCurrentPreset(string $presetName): void + { + $this->clonePresetInformationCache->set('current', [ + 'presetName' => $presetName, + 'cloned_at' => time() + ]); + } + + /** + * @param string $stashEntryName + * @param array $stashEntryManifest + * @return void + * @throws \Neos\Cache\Exception + */ + public function setCurrentStashEntry(string $stashEntryName, array $stashEntryManifest): void + { + if (!isset($stashEntryManifest['preset']['name'])) { + return; + } + + if (!isset($stashEntryManifest['cloned_at'])) { + return; + } + + $presetName = $stashEntryManifest['preset']['name']; + $clonedAt = $stashEntryManifest['cloned_at']; + + $this->clonePresetInformationCache->set('current', [ + 'presetName' => $presetName, + 'cloned_at' => $clonedAt + ]); + } +} diff --git a/Classes/ResourceManagement/ProxyAwareFileSystemSymlinkTarget.php b/Classes/ResourceManagement/ProxyAwareFileSystemSymlinkTarget.php new file mode 100644 index 0000000..30f4516 --- /dev/null +++ b/Classes/ResourceManagement/ProxyAwareFileSystemSymlinkTarget.php @@ -0,0 +1,119 @@ +bootstrap->getActiveRequestHandler(); + if ($requestHandler instanceof HttpRequestHandlerInterface) { + $request = new ActionRequest($requestHandler->getHttpRequest()); + $this->uriBuilder->setRequest($request); + } + parent::initializeObject(); + } + + /** + * Publishes the whole collection to this target + * + * @param CollectionInterface $collection The collection to publish + * @param callable $callback Function called after each resource publishing + * @return void + */ + public function publishCollection(CollectionInterface $collection, callable $callback = null) + { + if (!$this->configurationService->getCurrentConfigurationByPath('resourceProxy')) { + return parent::publishCollection($collection, $callback); + } + + /** + * @var ProxyAwareWritableFileSystemStorage $storage + */ + $storage = $collection->getStorage(); + if (!$storage instanceof ProxyAwareWritableFileSystemStorage) { + return parent::publishCollection($collection, $callback); + } + + foreach ($collection->getObjects($callback) as $object) { + /** @var StorageObject $object */ + if ($storage->resourceIsPresentInStorage($object) === false) { + // this storage ignores resources that are not yet in the filesystem as they + // are optimistically created during read operations + continue; + } + $sourceStream = $object->getStream(); + $this->publishFile($sourceStream, $this->getRelativePublicationPathAndFilename($object)); + fclose($sourceStream); + } + } + + /** + * @param PersistentResource $resource + * @return string + * @throws Exception + */ + public function getPublicPersistentResourceUri(PersistentResource $resource) + { + if (!$this->configurationService->getCurrentConfigurationByPath('resourceProxy')) { + return parent::getPublicPersistentResourceUri($resource); + } + + $collection = $this->resourceManager->getCollection($resource->getCollectionName()); + $storage = $collection->getStorage(); + + if (!$storage instanceof ProxyAwareWritableFileSystemStorage) { + return parent::getPublicPersistentResourceUri($resource); + } + + if ($storage->resourceIsPresentInStorage($resource)) { + return parent::getPublicPersistentResourceUri($resource); + } + + // build uri to resoucre controller that will fetch and publish + // the resource asynchronously + return $this->uriBuilder->uriFor( + 'index', + ['resourceIdentifier' => $resource], + 'Resource', + 'Sitegeist.MagicWand' + ); + } +} diff --git a/Classes/ResourceManagement/ProxyAwareWritableFileSystemStorage.php b/Classes/ResourceManagement/ProxyAwareWritableFileSystemStorage.php new file mode 100644 index 0000000..54652da --- /dev/null +++ b/Classes/ResourceManagement/ProxyAwareWritableFileSystemStorage.php @@ -0,0 +1,93 @@ +getStoragePathAndFilenameByHash($resource->getSha1()); + return file_exists($path); + } + + /** + * @param PersistentResource $resource + * @return bool|resource + */ + public function getStreamByResource(PersistentResource $resource) + { + if ($this->resourceIsPresentInStorage($resource)) { + return parent::getStreamByResource($resource); + } + + $resourceProxyConfiguration = $this->configurationService->getCurrentConfigurationByPath('resourceProxy'); + if (!$resourceProxyConfiguration) { + return parent::getStreamByResource($resource); + } + + $collection = $this->resourceManager->getCollection($resource->getCollectionName()); + $target = $collection->getTarget(); + if (!$target instanceof ProxyAwareFileSystemSymlinkTarget) { + return parent::getStreamByResource($resource); + } + + $curlEngine = new CurlEngine(); + $curlOptions = $resourceProxyConfiguration['curlOptions'] ?? []; + foreach($curlOptions as $key => $value) { + $curlEngine->setOption(constant($key), $value); + } + + $browser = new Browser(); + $browser->setRequestEngine($curlEngine); + + $subdivideHashPathSegment = $resourceProxyConfiguration['subdivideHashPathSegment'] ?? false; + if ($subdivideHashPathSegment) { + $sha1Hash = $resource->getSha1(); + $uri = $resourceProxyConfiguration['baseUri'] .'/_Resources/Persistent/' . $sha1Hash[0] . '/' . $sha1Hash[1] . '/' . $sha1Hash[2] . '/' . $sha1Hash[3] . '/' . $sha1Hash . '/' . $resource->getSha1() . '/' . rawurlencode($resource->getFilename()); + } else { + $uri = $resourceProxyConfiguration['baseUri'] .'/_Resources/Persistent/' . $resource->getSha1() . '/' . rawurlencode($resource->getFilename()); + } + + $response = $browser->request($uri); + + if ($response->getStatusCode() == 200 ) { + $stream = $response->getBody()->detach(); + $targetPathAndFilename = $this->getStoragePathAndFilenameByHash($resource->getSha1()); + if (!file_exists(dirname($targetPathAndFilename))) { + Files::createDirectoryRecursively(dirname($targetPathAndFilename)); + } + file_put_contents($targetPathAndFilename, stream_get_contents($stream)); + $this->fixFilePermissions($targetPathAndFilename); + $target->publishResource($resource, $collection); + return $stream; + } + + throw new ResourceNotFoundException( + sprintf('Resource from uri %s returned status %s', $uri, $response->getStatusCode()) + ); + } +} diff --git a/Classes/ResourceManagement/ResourceNotFoundException.php b/Classes/ResourceManagement/ResourceNotFoundException.php new file mode 100644 index 0000000..2828942 --- /dev/null +++ b/Classes/ResourceManagement/ResourceNotFoundException.php @@ -0,0 +1,15 @@ +indexAction())' + +roles: + 'Neos.Flow:Everybody': + privileges: + - + privilegeTarget: 'Sitegeist.MagicWand:ProxyAssets' + permission: GRANT diff --git a/Configuration/Routes.yaml b/Configuration/Routes.yaml new file mode 100644 index 0000000..3cdc8db --- /dev/null +++ b/Configuration/Routes.yaml @@ -0,0 +1,11 @@ +## +# MagicWand-ResourceProxy + +- + name: 'MagicWand-ResourceProxy' + uriPattern: 'magicwand/resourceproxy/{resourceIdentifier}' + defaults: + '@package': 'Sitegeist.MagicWand' + '@controller': 'Resource' + '@action': 'index' + httpMethods: ['GET'] diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml index 643d138..fe0b015 100644 --- a/Configuration/Settings.yaml +++ b/Configuration/Settings.yaml @@ -1,5 +1,6 @@ Sitegeist: MagicWand: + # the local flow command flowCommand: './flow' # # preset which is used by the clone:default command @@ -7,21 +8,55 @@ Sitegeist: clonePresets: [] # # the name of the preset for referencing on the clone:preset command # master: +# # # hostname or ip of the server to clone from # host: ~ +# # # ssh username # user: ~ +# # # ssh port # port: ~ +# # # ssh options # sshOptions: ~ +# # # path on the remote server # path: ~ +# # # flow-context on the remote server # context: Production +# # # the flow cli command on the remote server # # default is the main flowCommand-Setting # flowCommand: ~ +# # # commands to execute after cloning # postClone: # - './flow help' +# +# # informations to access the resources of the cloned setup via http +# # if this is configured the rsync of the persistent resources is skipped +# # and instead resources are fetched and imported on the fly once read +# resourceProxy: +# baseUri: http://vour.server.tld +# subdivideHashPathSegment: false +# curlOptions: +# CURLOPT_USERPWD: very:secure + +Neos: + Flow: + resource: + + storages: + defaultPersistentResourcesStorage: + storage: 'Sitegeist\MagicWand\ResourceManagement\ProxyAwareWritableFileSystemStorage' + + targets: + localWebDirectoryPersistentResourcesTarget: + target: 'Sitegeist\MagicWand\ResourceManagement\ProxyAwareFileSystemSymlinkTarget' + + mvc: + routes: + Sitegeist.MagicWand: + position: 'before Neos.Neos' diff --git a/README.md b/README.md index a218b15..4485ff7 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ section of your composer.json**. ## Easy and fast cloning of Flow and Neos Installations -The CLI commands `clone:list`, `clone:preset` and `clone:remotehost` help to +The CLI commands `clone:list`, `clone:preset` to help to clone a remote Flow/Neos setup into the local Flow/Neos installation that executes the command. **Attention: These commands will empty the local database and resources of your local Flow installation. @@ -28,9 +28,6 @@ using the commands.** # clone from remote host with the information stored in the master preset ./flow clone:preset master - -# clone remote host with the information stored in the master preset -./flow clone:remotehost --host=host --user=user --port=port --path=path --context=context ``` ### Settings.yaml @@ -69,6 +66,17 @@ Sitegeist: # commands to execute after cloning like ./flow user:create ... postClone: [] + + # informations to access the resources of the cloned setup via http + # if this is configured the rsync of the persistent resources is skipped + # and instead resources are fetched and imported on the fly once read + resourceProxy: + baseUri: http://vour.server.tld + # define wether or not the remote uses subdivideHashPathSegments + subdivideHashPathSegment: false + # curl options + curlOptions: + CURLOPT_USERPWD: very:secure ``` The settings should be added to the global `Settings.yaml` of the project, so that every diff --git a/composer.json b/composer.json index 23979aa..fe2bc0f 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ } ], "require": { - "neos/flow": "^5.0 || dev-master" + "neos/flow": "~4.0 || ~5.0 || dev-master" }, "autoload": { "psr-4": {