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": {