Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit d1e5a59

Browse files
committedMar 30, 2024·
Refactor
1 parent 1a0a104 commit d1e5a59

13 files changed

+204
-299
lines changed
 

‎composer.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
],
1414
"require": {
1515
"php": "^8.2",
16-
"illuminate/support": "^11.0"
16+
"illuminate/support": "^11.0",
17+
"laravel/prompts": "^0.1.17"
1718
},
1819
"require-dev": {
1920
"nunomaduro/collision": "^8.1",

‎config/sync.php

+1-5
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,7 @@
5151
*/
5252

5353
'options' => [
54-
// '--archive',
55-
// '--itemize-changes',
56-
// '--verbose',
57-
// '--human-readable',
58-
// '--progress'
54+
'--archive',
5955
],
6056

6157
];

‎src/CommandGenerator.php

-101
This file was deleted.

‎src/Commands/BaseCommand.php

+111-23
Original file line numberDiff line numberDiff line change
@@ -2,63 +2,151 @@
22

33
namespace Aerni\Sync\Commands;
44

5-
use Facades\Aerni\Sync\CommandGenerator;
6-
use Facades\Aerni\Sync\Config;
5+
use Aerni\Sync\SyncCommand;
6+
use Illuminate\Support\Arr;
7+
use Aerni\Sync\PathGenerator;
78
use Illuminate\Console\Command;
9+
use Illuminate\Validation\Rule;
10+
use Illuminate\Support\Collection;
11+
use function Laravel\Prompts\select;
12+
use Illuminate\Support\Facades\Validator;
13+
use Symfony\Component\Console\Output\OutputInterface;
14+
use Illuminate\Contracts\Console\PromptsForMissingInput;
815

9-
class BaseCommand extends Command
16+
class BaseCommand extends Command implements PromptsForMissingInput
1017
{
1118
public function __construct()
1219
{
13-
$baseSignature = "
14-
{operation : Choose if you want to 'push' or 'pull'}
20+
$baseSignature = '
21+
{operation : Choose if you want to push or pull}
1522
{remote : The remote you want to sync with}
1623
{recipe : The recipe defining the paths to sync}
1724
{--O|option=* : An rsync option to use}
1825
{--D|dry : Perform a dry run of the sync}
19-
";
26+
';
2027

2128
$this->signature .= $baseSignature;
2229

2330
parent::__construct();
2431
}
2532

26-
protected function commandGenerator(): \Aerni\Sync\CommandGenerator
33+
protected function promptForMissingArgumentsUsing(): array
2734
{
28-
return CommandGenerator::operation($this->operation())
29-
->remote($this->remote())
30-
->recipe($this->recipe())
31-
->options($this->rsyncOptions());
35+
return [
36+
'operation' => fn () => select(
37+
label: 'Choose if you want to push or pull',
38+
options: ['push', 'pull'],
39+
),
40+
'remote' => fn () => select(
41+
label: 'Choose the remote you want to sync with',
42+
options: array_keys($this->remotes()),
43+
),
44+
'recipe' => fn () => select(
45+
label: 'Choose the recipe defining the paths to sync',
46+
options: array_keys($this->recipes()),
47+
),
48+
];
49+
}
50+
51+
protected function validate(): void
52+
{
53+
Validator::validate($this->arguments(), [
54+
'operation' => 'required|in:push,pull',
55+
'remote' => ['required', Rule::in(array_keys($this->remotes()))],
56+
'recipe' => ['required', Rule::in(array_keys($this->recipes()))],
57+
], [
58+
'operation.in' => "The :attribute [:input] does not exists. Valid values are [push] or [pull].",
59+
'remote.in' => "The :attribute [:input] does not exists. Please choose a valid remote.",
60+
'recipe.in' => "The :attribute [:input] does not exists. Please choose a valid recipe.",
61+
]);
62+
63+
if ($this->localPathEqualsRemotePath()) {
64+
throw new \RuntimeException("The origin and target path are one and the same. You can't sync a path with itself.");
65+
}
66+
67+
if ($this->remoteIsReadOnly() && $this->operation() === 'push') {
68+
throw new \RuntimeException("You can't push to the selected target as it is configured to be read-only.");
69+
}
70+
}
71+
72+
protected function localPathEqualsRemotePath(): bool
73+
{
74+
return PathGenerator::localPath($this->recipe()[0])
75+
=== PathGenerator::remotePath($this->remote(), $this->recipe()[0]);
76+
}
77+
78+
protected function remoteIsReadOnly(): bool
79+
{
80+
return Arr::get($this->remote(), 'read_only', false);
81+
}
82+
83+
protected function commands(): Collection
84+
{
85+
return collect($this->recipe())
86+
->map(fn ($path) => new SyncCommand(
87+
path: $path,
88+
operation: $this->operation(),
89+
remote: $this->remote(),
90+
options: $this->rsyncOptions(),
91+
));
3292
}
3393

3494
protected function operation(): string
3595
{
36-
return Config::operation($this->argument('operation'));
96+
return $this->argument('operation');
3797
}
3898

3999
protected function remote(): array
40100
{
41-
return Config::remote($this->argument('remote'));
101+
return Arr::get($this->remotes(), $this->argument('remote'));
42102
}
43103

44104
protected function recipe(): array
45105
{
46-
return Config::recipe($this->argument('recipe'));
106+
return Arr::get($this->recipes(), $this->argument('recipe'));
47107
}
48108

49-
protected function rsyncOptions(): array
109+
protected function remotes(): array
50110
{
51-
$options = Config::options($this->option('option'));
111+
$remotes = config('sync.remotes');
52112

53-
return collect($options)
54-
->push($this->dry())
55-
->filter()
56-
->unique()
57-
->toArray();
113+
if (empty($remotes)) {
114+
throw new \RuntimeException('You need to define at least one remote in your config file.');
115+
}
116+
117+
return $remotes;
118+
}
119+
120+
protected function recipes(): array
121+
{
122+
$recipes = config('sync.recipes');
123+
124+
if (empty($recipes)) {
125+
throw new \RuntimeException('You need to define at least one recipe in your config file.');
126+
}
127+
128+
return $recipes;
58129
}
59130

60-
protected function dry(): string
131+
protected function rsyncOptions(): string
61132
{
62-
return $this->option('dry') ? '--dry-run' : '';
133+
$options = $this->option('option');
134+
135+
if (empty($options)) {
136+
$options = config('sync.options');
137+
}
138+
139+
return collect($options)
140+
->when(
141+
$this->option('dry'),
142+
fn ($collection) => $collection->merge(['--dry-run', '--human-readable', '--progress', '--stats', '--verbose'])
143+
)
144+
->when(
145+
$this->output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE,
146+
fn ($collection) => $collection->merge(['--human-readable', '--progress', '--stats', '--verbose'])
147+
)
148+
->filter()
149+
->unique()
150+
->implode(' ');
63151
}
64152
}

‎src/Commands/Sync.php

+21-16
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
namespace Aerni\Sync\Commands;
44

5-
use Facades\Aerni\Sync\SyncProcessor;
6-
use Illuminate\Support\Arr;
5+
use function Laravel\Prompts\confirm;
6+
use Illuminate\Support\Facades\Process;
7+
use Symfony\Component\Console\Output\OutputInterface;
78

89
class Sync extends BaseCommand
910
{
@@ -26,25 +27,29 @@ class Sync extends BaseCommand
2627
*/
2728
public function handle(): void
2829
{
29-
if ($this->operation() === 'push' && Arr::get($this->remote(), 'read_only') === true) {
30-
$this->error("You can't push to the selected target as it is configured to be read only.");
30+
$this->validate();
3131

32+
/* Only show the confirmation if we're not performing a dry run */
33+
if (! $this->option('dry') && ! confirm($this->confirmText())) {
3234
return;
3335
}
3436

35-
if (! $this->confirm($this->confirmText(), true)) {
36-
return;
37-
}
37+
$this->option('dry')
38+
? $this->info('Starting a dry run ...')
39+
: $this->info('Syncing files ...');
3840

39-
$commands = $this->commandGenerator()->run();
41+
$this->commands()->each(function ($command) {
42+
Process::forever()->run($command, function (string $type, string $output) {
43+
/* Only show the output if we're performing a dry run or the verbosity is set to verbose */
44+
if ($this->option('dry') || $this->output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
45+
echo $output;
46+
}
47+
});
48+
});
4049

41-
$sync = SyncProcessor::commands($commands)
42-
->artisanCommand($this)
43-
->run();
44-
45-
if ($sync->successful()) {
46-
$this->info('The sync was successful');
47-
}
50+
$this->option('dry')
51+
? $this->info("The dry run of the <comment>{$this->argument('recipe')}</comment> recipe was successfull.")
52+
: $this->info("The sync of the <comment>{$this->argument('recipe')}</comment> recipe was successfull.");
4853
}
4954

5055
protected function confirmText(): string
@@ -54,6 +59,6 @@ protected function confirmText(): string
5459
$remote = $this->argument('remote');
5560
$preposition = $operation === 'pull' ? 'from' : 'to';
5661

57-
return "Please confirm that you want to <comment>$operation</comment> the <comment>$recipe</comment> $preposition <comment>$remote</comment>";
62+
return "You are about to $operation the $recipe $preposition $remote. Are you sure?";
5863
}
5964
}

‎src/Commands/SyncCommands.php

+3-4
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,8 @@ class SyncCommands extends BaseCommand
2323
*/
2424
public function handle(): void
2525
{
26-
$this->commandGenerator()->commandsString()->each(function ($command) {
27-
$this->info($command);
28-
$this->newLine();
29-
});
26+
$this->validate();
27+
28+
$this->commands()->each(fn ($command) => $this->info($command));
3029
}
3130
}

‎src/Commands/SyncList.php

+7-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace Aerni\Sync\Commands;
44

5+
use function Laravel\Prompts\table;
6+
57
class SyncList extends BaseCommand
68
{
79
/**
@@ -23,9 +25,11 @@ class SyncList extends BaseCommand
2325
*/
2426
public function handle(): void
2527
{
26-
$headers = ['Origin', 'Target', 'Options', 'Port'];
27-
$commands = $this->commandGenerator()->commandsArray();
28+
$this->validate();
2829

29-
$this->table($headers, $commands);
30+
table(
31+
['Origin', 'Target', 'Options', 'Port'],
32+
$this->commands()->map->toArray()
33+
);
3034
}
3135
}

‎src/Config.php

-49
This file was deleted.

‎src/Exceptions/ConfigException.php

-23
This file was deleted.

‎src/Exceptions/SyncException.php

-18
This file was deleted.

‎src/PathGenerator.php

+7-9
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,23 @@
66

77
class PathGenerator
88
{
9-
public function localPath(string $path): string
9+
public static function localPath(string $path): string
1010
{
1111
return base_path($path);
1212
}
1313

14-
public function remotePath(array $remote, string $path): string
14+
public static function remotePath(array $remote, string $path): string
1515
{
16-
$fullPath = $this->joinPaths($remote['root'], $path);
16+
$fullPath = self::joinPaths($remote['root'], $path);
1717

18-
if ($this->remoteHostEqualsLocalHost($remote['host'])) {
18+
if (self::remoteHostEqualsLocalHost($remote['host'])) {
1919
return $fullPath;
2020
}
2121

2222
return "{$remote['user']}@{$remote['host']}:$fullPath";
2323
}
2424

25-
protected function joinPaths(): string
25+
protected static function joinPaths(): string
2626
{
2727
$paths = [];
2828

@@ -35,10 +35,8 @@ protected function joinPaths(): string
3535
return preg_replace('#/+#', '/', implode('/', $paths));
3636
}
3737

38-
protected function remoteHostEqualsLocalHost(string $remoteHost): bool
38+
protected static function remoteHostEqualsLocalHost(string $host): bool
3939
{
40-
$publicIp = Http::get('https://api.ipify.org/?format=json')->json('ip');
41-
42-
return $publicIp === $remoteHost;
40+
return once(fn () => $host === Http::get('https://api.ipify.org/?format=json')->json('ip'));
4341
}
4442
}

‎src/SyncCommand.php

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
namespace Aerni\Sync;
4+
5+
use Stringable;
6+
use Aerni\Sync\PathGenerator;
7+
use Illuminate\Contracts\Support\Arrayable;
8+
9+
class SyncCommand implements Arrayable, Stringable
10+
{
11+
public function __construct(
12+
protected string $path,
13+
protected string $operation,
14+
protected array $remote,
15+
protected string $options,
16+
) {
17+
}
18+
19+
public function toArray(): array
20+
{
21+
return [
22+
'origin' => $this->origin(),
23+
'target' => $this->target(),
24+
'options' => $this->options,
25+
'port' => $this->port(),
26+
];
27+
}
28+
29+
public function __toString(): string
30+
{
31+
return "rsync -e 'ssh -p {$this->port()}' {$this->options} {$this->origin()} {$this->target()}";
32+
}
33+
34+
protected function origin(): string
35+
{
36+
return $this->operation === 'pull'
37+
? PathGenerator::remotePath($this->remote, $this->path)
38+
: PathGenerator::localPath($this->path);
39+
}
40+
41+
protected function target(): string
42+
{
43+
return $this->operation === 'pull'
44+
? PathGenerator::localPath($this->path)
45+
: PathGenerator::remotePath($this->remote, $this->path);
46+
}
47+
48+
protected function port(): string
49+
{
50+
return $this->remote['port'] ?? '22';
51+
}
52+
}

‎src/SyncProcessor.php

-47
This file was deleted.

0 commit comments

Comments
 (0)
Please sign in to comment.