458 lines
14 KiB
PHP
458 lines
14 KiB
PHP
<?php
|
|
|
|
namespace Evoweb\DeployerConfig\Config;
|
|
|
|
use Deployer\Host\Localhost;
|
|
use Deployer\Task\Context;
|
|
use RuntimeException;
|
|
|
|
use function Deployer\run;
|
|
use function Deployer\runLocally;
|
|
use function Deployer\test;
|
|
use function Deployer\writeln;
|
|
|
|
class Rsync extends AbstractConfiguration
|
|
{
|
|
protected array $defaultConfig = [
|
|
'exclude' => [
|
|
'.git',
|
|
'deploy.php',
|
|
'deploy.lock',
|
|
'current',
|
|
'shared/',
|
|
'var/*',
|
|
],
|
|
'exclude-file' => false,
|
|
'include' => [
|
|
'var/',
|
|
],
|
|
'include-file' => false,
|
|
'filter' => [],
|
|
'filter-file' => false,
|
|
'filter-perdir' => false,
|
|
'flags' => 'rzlc',
|
|
'options' => ['delete', 'delete-after'],
|
|
'timeout' => 600,
|
|
];
|
|
|
|
protected array $tasks = [
|
|
'rsync:warmup' => ['methodName' => 'warmup'],
|
|
'rsync:update_code' => ['methodName' => 'updateCode'],
|
|
'rsync:remote' => ['methodName' => 'remote'],
|
|
'rsync:remote_additional_targets' => ['methodName' => 'remoteAdditionalTargets'],
|
|
'rsync:switch_current' => ['methodName' => 'switchCurrent'],
|
|
'rsync:switch_current_additional_targets' => ['methodName' => 'switchCurrentAdditionalTargets'],
|
|
];
|
|
|
|
public function __construct()
|
|
{
|
|
$this->setDefaultConfiguration();
|
|
$this->initializeTasks();
|
|
}
|
|
|
|
protected function setDefaultConfiguration(): void
|
|
{
|
|
$this->set('port', 22);
|
|
|
|
$this->set('rsync', []);
|
|
|
|
$this->set('rsync_default', $this->defaultConfig);
|
|
|
|
$this->set('rsync_src', '{{deploy_path}}');
|
|
|
|
$this->set('rsync_dest', '{{user}}@{{hostname}}:\'{{remote_path}}/\'');
|
|
|
|
$this->set('rsync_config', $this->rsyncConfig(...));
|
|
|
|
$this->set('rsync_flags', $this->rsyncFlags(...));
|
|
|
|
$this->set('rsync_excludes', $this->rsyncExcludes(...));
|
|
$this->set('rsync_excludes_download', $this->rsyncExcludesDownload(...));
|
|
$this->set('rsync_excludes_upload', $this->rsyncExcludesUpload(...));
|
|
|
|
$this->set('rsync_includes', $this->rsyncIncludes(...));
|
|
|
|
$this->set('rsync_filter', $this->rsyncFilter(...));
|
|
|
|
$this->set('rsync_options', $this->rsyncOptions(...));
|
|
|
|
$this->set('rsync_timeout', $this->rsyncTimeout(...));
|
|
}
|
|
|
|
/**
|
|
* Warmup remote Rsync target
|
|
*/
|
|
public function warmup(): void
|
|
{
|
|
if (!test('[ -d "{{deploy_path}}" ]')) {
|
|
runLocally('mkdir -p {{deploy_path}}');
|
|
}
|
|
|
|
if (test('[ -d "{{deploy_path}}" ]')) {
|
|
$identityFile = $this->get('identityFile') ? ' -i ' . $this->get('identityFile') : '';
|
|
run(
|
|
'rsync \
|
|
-e \'ssh -p {{port}}' . $identityFile . '\' \
|
|
{{rsync_flags}} \
|
|
{{rsync_options}} \
|
|
{{rsync_timeout}} \
|
|
--exclude=' . escapeshellarg('shared') . ' \
|
|
{{rsync_excludes_download}} \
|
|
{{rsync_includes}} \
|
|
{{rsync_filter}} \
|
|
{{rsync_dest}} {{deploy_path}}'
|
|
);
|
|
} else {
|
|
writeln('<comment>No destination folder found.</comment>');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Copy repository content to release folder
|
|
*/
|
|
public function updateCode(): void
|
|
{
|
|
$pathConfigName = 'ignore_update_code_paths';
|
|
if ($this->hasArray($pathConfigName)) {
|
|
$excludes = '';
|
|
$ignoreUpdateCodePaths = $this->get($pathConfigName);
|
|
foreach ($ignoreUpdateCodePaths as $path) {
|
|
$excludes .= ' --exclude=\'' . $path . '\'';
|
|
}
|
|
$identityFile = $this->get('identityFile') ? ' -i ' . $this->get('identityFile') : '';
|
|
run(
|
|
'rsync \
|
|
-e \'ssh -p {{port}}' . $identityFile . '\' \
|
|
{{rsync_flags}} \
|
|
{{rsync_options}} \
|
|
{{rsync_timeout}} \
|
|
--exclude=' . escapeshellarg('shared') . ' \
|
|
' . $excludes . ' \
|
|
{{rsync_includes}} \
|
|
{{rsync_filter}} \
|
|
{{CI_PROJECT_DIR}}/ \
|
|
{{release_path}}'
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Rsync local->remote
|
|
*/
|
|
public function remote(): void
|
|
{
|
|
$config = $this->get('rsync_config');
|
|
$server = Context::get()->getHost();
|
|
$identityFile = $this->get('identityFile') ? ' -i ' . $this->get('identityFile') : '';
|
|
if ($server instanceof Localhost) {
|
|
runLocally(
|
|
'rsync \
|
|
-e \'ssh -p {{port}}' . $identityFile . '\' \
|
|
{{rsync_flags}} \
|
|
{{rsync_options}} \
|
|
{{rsync_timeout}} \
|
|
{{rsync_includes}} \
|
|
--exclude=' . escapeshellarg('shared/') . ' \
|
|
{{rsync_excludes_upload}} \
|
|
{{rsync_filter}} \
|
|
\'{{rsync_src}}/\' {{rsync_dest}}',
|
|
$config
|
|
);
|
|
return;
|
|
}
|
|
|
|
runLocally(
|
|
'rsync \
|
|
-e \'ssh -p {{port}}' . $identityFile . '\' \
|
|
{{rsync_flags}} \
|
|
{{rsync_options}} \
|
|
{{rsync_timeout}} \
|
|
{{rsync_includes}} \
|
|
--exclude=' . escapeshellarg('shared/') . ' \
|
|
{{rsync_excludes_upload}} \
|
|
{{rsync_filter}} \
|
|
\'{{rsync_src}}/\' {{user}}@{{hostname}}:\'{{remote_path}}/\'',
|
|
$config
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Rsync local->additional targets
|
|
*/
|
|
public function remoteAdditionalTargets(): void
|
|
{
|
|
$configName = 'additional_remote_targets';
|
|
if ($this->hasArray($configName)) {
|
|
$config = $this->get('rsync_config');
|
|
$identityFile = $this->get('identityFile') ? ' -i ' . $this->get('identityFile') : '';
|
|
$targets = $this->get($configName);
|
|
foreach ($targets as $target) {
|
|
$port = $target['port'] ?? '{{port}}';
|
|
$user = $target['user'] ?? '{{user}}';
|
|
$remotePath = $target['remote_path'] ?: '{{remote_path}}';
|
|
runLocally(
|
|
'rsync \
|
|
-e \'ssh -p ' . $port . $identityFile . '\' \
|
|
{{rsync_flags}} \
|
|
{{rsync_options}} \
|
|
{{rsync_timeout}} \
|
|
{{rsync_includes}} \
|
|
--exclude=' . escapeshellarg('shared/') . ' \
|
|
{{rsync_excludes_upload}} \
|
|
{{rsync_filter}} \
|
|
\'{{rsync_src}}/\' ' . $user . '@' . $target['hostname'] . ':\'' . $remotePath . '/\'',
|
|
$config
|
|
);
|
|
}
|
|
} else {
|
|
writeln('No additional remote target configured');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sync current after release was uploaded
|
|
*/
|
|
public function switchCurrent(): void
|
|
{
|
|
$config = $this->get('rsync_config');
|
|
$server = Context::get()->getHost();
|
|
$identityFile = $this->get('identityFile') ? ' -i ' . $this->get('identityFile') : '';
|
|
if ($server instanceof Localhost) {
|
|
runLocally(
|
|
'rsync \
|
|
-e \'ssh -p {{port}}' . $identityFile . '\' \
|
|
{{rsync_flags}} \
|
|
{{rsync_options}} \
|
|
\'{{rsync_src}}/current\' {{rsync_dest}}',
|
|
$config
|
|
);
|
|
return;
|
|
}
|
|
|
|
runLocally(
|
|
'rsync \
|
|
-e \'ssh -p {{port}}' . $identityFile . '\' \
|
|
{{rsync_flags}} \
|
|
{{rsync_options}} \
|
|
\'{{rsync_src}}/current\' {{user}}@{{hostname}}:\'{{remote_path}}/\'',
|
|
$config
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Sync current after release was uploaded
|
|
*/
|
|
public function switchCurrentAdditionalTargets(): void
|
|
{
|
|
$configName = 'additional_remote_targets';
|
|
if ($this->hasArray($configName)) {
|
|
$config = $this->get('rsync_config');
|
|
$identityFile = $this->get('identityFile') ? ' -i ' . $this->get('identityFile') : '';
|
|
$targets = $this->get($configName);
|
|
foreach ($targets as $target) {
|
|
$port = $target['port'] ?? '{{port}}';
|
|
$user = $target['user'] ?? '{{user}}';
|
|
$remotePath = $target['remote_path'] ?: '{{remote_path}}';
|
|
runLocally(
|
|
'rsync \
|
|
-e \'ssh -p ' . $port . $identityFile . '\' \
|
|
{{rsync_flags}} \
|
|
{{rsync_options}} \
|
|
\'{{rsync_src}}/current\'' . ' ' . $user . '@' . $target['hostname'] . ':\'' . $remotePath . '/\'',
|
|
$config
|
|
);
|
|
}
|
|
} else {
|
|
writeln('No additional remote target configured');
|
|
}
|
|
}
|
|
|
|
protected function getSource(): string
|
|
{
|
|
$source = $this->get('rsync_src');
|
|
while (is_callable($source)) {
|
|
$source = $source();
|
|
}
|
|
|
|
if (!trim($source)) {
|
|
// if $src is not set here rsync is going to do a directory listing
|
|
// exiting with code 0, since only doing a directory listing clearly
|
|
// is not what we want to achieve we need to throw an exception
|
|
throw new RuntimeException('You need to specify a source path.');
|
|
}
|
|
|
|
return $source;
|
|
}
|
|
|
|
protected function getDestination(): string
|
|
{
|
|
$destination = $this->get('rsync_dest');
|
|
while (is_callable($destination)) {
|
|
$destination = $destination();
|
|
}
|
|
|
|
if (!trim($destination)) {
|
|
// if $dst is not set here we are going to sync to root
|
|
// and even worse - depending on rsync flags and permission -
|
|
// might end up deleting everything we have write permission to
|
|
throw new RuntimeException('You need to specify a destination path.');
|
|
}
|
|
|
|
return $destination;
|
|
}
|
|
|
|
public function rsyncConfig(): array
|
|
{
|
|
$default = $this->get('rsync_default');
|
|
$config = $this->get('rsync');
|
|
return Rsync::arrayMergeRecursiveDistinct($default, $config);
|
|
}
|
|
|
|
public function rsyncFlags(): string
|
|
{
|
|
$config = $this->get('rsync_config');
|
|
$flags = $config['flags'] ?? '';
|
|
return $flags ? ' -' . $flags : '';
|
|
}
|
|
|
|
public function rsyncExcludes(): string
|
|
{
|
|
$config = $this->get('rsync_config');
|
|
$excludes = $config['exclude'] ?? [];
|
|
$excludeFile = $config['exclude-file'] ?? '';
|
|
$excludesRsync = '';
|
|
foreach ($excludes as $exclude) {
|
|
$excludesRsync .= ' --exclude=' . escapeshellarg($exclude);
|
|
}
|
|
|
|
if (
|
|
!empty($excludeFile)
|
|
&& file_exists($excludeFile)
|
|
&& is_file($excludeFile)
|
|
&& is_readable($excludeFile)
|
|
) {
|
|
$excludesRsync .= ' --exclude-from=' . escapeshellarg($excludeFile);
|
|
}
|
|
|
|
return $excludesRsync;
|
|
}
|
|
|
|
public function rsyncExcludesDownload(): string
|
|
{
|
|
$config = $this->get('rsync_config');
|
|
$excludes = $config['exclude_download'] ?? $config['exclude'] ?? [];
|
|
$excludeFile = $config['exclude-file'] ?? '';
|
|
$excludesRsync = '';
|
|
foreach ($excludes as $exclude) {
|
|
$excludesRsync .= ' --exclude=' . escapeshellarg($exclude);
|
|
}
|
|
|
|
if (
|
|
!empty($excludeFile)
|
|
&& file_exists($excludeFile)
|
|
&& is_file($excludeFile)
|
|
&& is_readable($excludeFile)
|
|
) {
|
|
$excludesRsync .= ' --exclude-from=' . escapeshellarg($excludeFile);
|
|
}
|
|
|
|
return $excludesRsync;
|
|
}
|
|
|
|
public function rsyncExcludesUpload(): string
|
|
{
|
|
$config = $this->get('rsync_config');
|
|
$excludes = $config['exclude_upload'] ?? $config['exclude'] ?? [];
|
|
$excludeFile = $config['exclude-file'] ?? '';
|
|
$excludesRsync = '';
|
|
foreach ($excludes as $exclude) {
|
|
$excludesRsync .= ' --exclude=' . escapeshellarg($exclude);
|
|
}
|
|
|
|
if (
|
|
!empty($excludeFile)
|
|
&& file_exists($excludeFile)
|
|
&& is_file($excludeFile)
|
|
&& is_readable($excludeFile)
|
|
) {
|
|
$excludesRsync .= ' --exclude-from=' . escapeshellarg($excludeFile);
|
|
}
|
|
|
|
return $excludesRsync;
|
|
}
|
|
|
|
public function rsyncIncludes(): string
|
|
{
|
|
$config = $this->get('rsync_config');
|
|
$includes = $config['include'] ?? [];
|
|
$includeFile = $config['include-file'] ?? '';
|
|
$includesRsync = '';
|
|
foreach ($includes as $include) {
|
|
$includesRsync .= ' --include=' . escapeshellarg($include);
|
|
}
|
|
if (
|
|
!empty($includeFile)
|
|
&& file_exists($includeFile)
|
|
&& is_file($includeFile)
|
|
&& is_readable($includeFile)
|
|
) {
|
|
$includesRsync .= ' --include-from=' . escapeshellarg($includeFile);
|
|
}
|
|
|
|
return $includesRsync;
|
|
}
|
|
|
|
public function rsyncFilter(): string
|
|
{
|
|
$config = $this->get('rsync_config');
|
|
$filters = $config['filter'] ?? [];
|
|
$filterFile = $config['filter-file'] ?? '';
|
|
$filterPerDir = $config['filter-perdir'] ?? '';
|
|
$filtersRsync = '';
|
|
foreach ($filters as $filter) {
|
|
$filtersRsync .= " --filter='$filter'";
|
|
}
|
|
if (!empty($filterFile)) {
|
|
$filtersRsync .= " --filter='merge $filterFile'";
|
|
}
|
|
if (!empty($filterPerDir)) {
|
|
$filtersRsync .= " --filter='dir-merge $filterPerDir'";
|
|
}
|
|
return $filtersRsync;
|
|
}
|
|
|
|
public function rsyncOptions(): string
|
|
{
|
|
$config = $this->get('rsync_config');
|
|
$options = $config['options'] ?? [];
|
|
$optionsRsync = [];
|
|
foreach ($options as $option) {
|
|
$optionsRsync[] = '--' . $option;
|
|
}
|
|
return ' ' . implode(' ', $optionsRsync);
|
|
}
|
|
|
|
public function rsyncTimeout(): string
|
|
{
|
|
$config = $this->get('rsync_config');
|
|
$timeout = $config['timeout'] ?? 0;
|
|
|
|
return $timeout ? ' --timeout=' . $timeout : '';
|
|
}
|
|
|
|
public static function arrayMergeRecursiveDistinct(array $array1, array &$array2): array
|
|
{
|
|
$merged = $array1;
|
|
|
|
foreach ($array2 as $key => &$value) {
|
|
if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
|
|
$merged[$key] = self::arrayMergeRecursiveDistinct($merged[$key], $value);
|
|
} else {
|
|
$merged[$key] = $value;
|
|
}
|
|
}
|
|
|
|
return $merged;
|
|
}
|
|
}
|