Skip to content

Commit 6144658

Browse files
committed
Added support for multiple remote server definition
1 parent d0edc7b commit 6144658

File tree

5 files changed

+143
-31
lines changed

5 files changed

+143
-31
lines changed

README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,18 @@ php artisan sync:pull --delete
9797
```
9898

9999
### Server Configuration Options
100+
By default, it'll take host, user, path etc. from the production server definition in config file.
101+
102+
Also we can specify which remote server definition to use (see config file), if we have defined multiple servers. If any config is specified as inline option, it'll get precidence over config values.
100103

101104
```bash
105+
php artisan sync:pull --remote=staging
106+
102107
# Override server details inline
103108
php artisan sync:pull --host=prod.example.com --user=deploy --path=/var/www/app
104109

105-
# All options can be combined
106-
php artisan sync:pull --host=prod.example.com --user=deploy --exclude-tables=logs --delete
110+
# All options can be specified inline to overwrite config values
111+
php artisan sync:pull --host=prod.example.com --user=deploy --exclude-tables=logs,migrations
107112
```
108113

109114
### Safety Features
@@ -146,9 +151,6 @@ php artisan sync:pull --force
146151

147152
Please see [CONTRIBUTING](CONTRIBUTING.md) for details.
148153

149-
## Security Vulnerabilities
150-
151-
Please review [our security policy](../../security/policy) on how to report security vulnerabilities.
152154

153155
## Credits
154156

config/server-sync.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,21 @@
1010
| Configure your production server details here or in your .env file.
1111
|
1212
*/
13+
// Default production server configuration
1314
'production' => [
1415
'host' => env('PROD_SSH_HOST', ''),
1516
'user' => env('PROD_SSH_USER', ''),
1617
'path' => env('PROD_SSH_PATH', ''),
1718
],
1819

20+
// Staging server configuration. Uncomment to use.
21+
// Or add any other environments you need.
22+
// 'staging' => [
23+
// 'host' => env('STAGING_SSH_HOST', ''),
24+
// 'user' => env('STAGING_SSH_USER', ''),
25+
// 'path' => env('STAGING_SSH_PATH', ''),
26+
// ],
27+
1928
/*
2029
|--------------------------------------------------------------------------
2130
| Database Sync Settings

src/Commands/SyncPullCommand.php

Lines changed: 33 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
namespace Ajaxray\ServerSync\Commands;
44

55
use Illuminate\Console\Command;
6-
use Illuminate\Support\Facades\DB;
7-
use Illuminate\Support\Facades\Storage;
86
use Illuminate\Support\Facades\Config;
97
use Illuminate\Support\Facades\App;
108
use Symfony\Component\Process\Process;
@@ -15,6 +13,7 @@ class SyncPullCommand extends Command
1513
{--host= : Production server hostname or IP}
1614
{--user= : SSH username for production server}
1715
{--path= : Path to production installation}
16+
{--remote=production : Remote server configuration to use (e.g., production, staging)}
1817
{--skip-db : Skip database sync}
1918
{--skip-files : Skip files sync}
2019
{--delete : Remove files that don\'t exist in production}
@@ -24,22 +23,28 @@ class SyncPullCommand extends Command
2423

2524
protected $description = 'Pull and sync database and files from production to local environment';
2625

27-
protected string $prodHost;
28-
protected string $prodUser;
29-
protected string $prodPath;
26+
protected string $remoteHost;
27+
protected string $remoteUser;
28+
protected string $remotePath;
3029

3130
public function handle()
32-
{
31+
{
3332
if (App::environment('production') && !$this->option('force')) {
3433
$this->error('This command cannot be run in production environment. Use --force to override.');
3534
return 1;
3635
}
3736

38-
$this->prodHost = $this->option('host') ?: Config::get('server-sync.production.host');
39-
$this->prodUser = $this->option('user') ?: Config::get('server-sync.production.user');
40-
$this->prodPath = $this->option('path') ?: Config::get('server-sync.production.path');
37+
$remote = $this->option('remote');
38+
39+
$this->remoteHost = $this->option('host') ?: Config::get("server-sync.$remote.host");
40+
$this->remoteUser = $this->option('user') ?: Config::get("server-sync.$remote.user");
41+
$this->remotePath = $this->option('path') ?: Config::get("server-sync.$remote.path");
42+
43+
if ($remote) {
44+
$this->info("Pulling from {$this->remoteHost} as {$this->remoteUser}");
45+
}
4146

42-
if (!$this->validateRequirements()) {
47+
if (!$this->validateRequirements()) {
4348
return 1;
4449
}
4550

@@ -57,7 +62,7 @@ public function handle()
5762

5863
protected function validateRequirements(): bool
5964
{
60-
if (!$this->prodHost || !$this->prodUser || !$this->prodPath) {
65+
if (!$this->remoteHost || !$this->remoteUser || !$this->remotePath) {
6166
$this->error('Production server details are required. Please provide --host, --user and --path options or configure them in config/server-sync.php');
6267
return false;
6368
}
@@ -75,12 +80,14 @@ protected function validateRequirements(): bool
7580
}
7681

7782
// Test SSH connection
78-
$testConnection = Process::fromShellCommandline("ssh -q {$this->prodUser}@{$this->prodHost} exit");
79-
$testConnection->run();
80-
81-
if (!$testConnection->isSuccessful()) {
82-
$this->error('Failed to connect to production server. Please check your SSH configuration.');
83-
return false;
83+
if (!$this->option('skip-db') || !$this->option('skip-files')) {
84+
$testConnection = Process::fromShellCommandline("ssh -q {$this->remoteUser}@{$this->remoteHost} exit");
85+
$testConnection->run();
86+
87+
if (!$testConnection->isSuccessful()) {
88+
$this->error('Failed to connect to production server. Please check your SSH configuration.');
89+
return false;
90+
}
8491
}
8592

8693
return true;
@@ -165,9 +172,9 @@ protected function syncFiles()
165172
'rsync -avz --compress %s %s --progress %s@%s:%s/%s/ %s/',
166173
$deleteFlag,
167174
$excludeFlags,
168-
$this->prodUser,
169-
$this->prodHost,
170-
$this->prodPath,
175+
$this->remoteUser,
176+
$this->remoteHost,
177+
$this->remotePath,
171178
$relativePath,
172179
$path
173180
);
@@ -193,9 +200,9 @@ protected function getProductionDatabaseConfig(): ?array
193200
$this->info('Retrieving production database credentials...');
194201
$sshCommand = sprintf(
195202
'ssh %s@%s "cd %s && grep -E \'^DB_(HOST|DATABASE|USERNAME|PASSWORD)=\' .env"',
196-
$this->prodUser,
197-
$this->prodHost,
198-
$this->prodPath
203+
$this->remoteUser,
204+
$this->remoteHost,
205+
$this->remotePath
199206
);
200207

201208
$process = Process::fromShellCommandline($sshCommand);
@@ -228,8 +235,8 @@ protected function buildMysqlDumpCommand(array $dbConfig, string $tempFile): str
228235
{
229236
$command = sprintf(
230237
'ssh %s@%s "mysqldump -h%s -u%s -p\'%s\' %s',
231-
$this->prodUser,
232-
$this->prodHost,
238+
$this->remoteUser,
239+
$this->remoteHost,
233240
$dbConfig['host'],
234241
$dbConfig['username'],
235242
$dbConfig['password'],
@@ -254,7 +261,7 @@ protected function buildMysqlDumpCommand(array $dbConfig, string $tempFile): str
254261

255262
// Create a temporary file on the remote server, then download it
256263
$remoteTempFile = '/tmp/temp_dump.sql';
257-
$command .= " > {$remoteTempFile}\" && scp {$this->prodUser}@{$this->prodHost}:{$remoteTempFile} {$tempFile} && ssh {$this->prodUser}@{$this->prodHost} \"rm {$remoteTempFile}\"";
264+
$command .= " > {$remoteTempFile}\" && scp {$this->remoteUser}@{$this->remoteHost}:{$remoteTempFile} {$tempFile} && ssh {$this->remoteUser}@{$this->remoteHost} \"rm {$remoteTempFile}\"";
258265

259266
return $command;
260267
}

tests/TestCase.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
namespace Ajaxray\ServerSync\Tests;
4+
5+
use Ajaxray\ServerSync\ServerSyncServiceProvider;
6+
use Orchestra\Testbench\TestCase as BaseTestCase;
7+
8+
class TestCase extends BaseTestCase
9+
{
10+
11+
public function setUp(): void
12+
{
13+
parent::setUp();
14+
// additional setup
15+
}
16+
17+
protected function getPackageProviders($app)
18+
{
19+
return [
20+
ServerSyncServiceProvider::class,
21+
];
22+
}
23+
24+
protected function getEnvironmentSetUp($app)
25+
{
26+
$app['config']->set('server-sync.production', [
27+
'host' => 'localhost',
28+
'user' => 'root',
29+
'path' => '/var/www/html/laravel',
30+
]);
31+
}
32+
}

tests/Unit/SyncPullCommandTest.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
namespace Tests\Unit;
4+
5+
use Ajaxray\ServerSync\Commands\SyncPullCommand;
6+
use Illuminate\Support\Facades\Config;
7+
use Ajaxray\ServerSync\Tests\TestCase;
8+
9+
class SyncPullCommandTest extends TestCase
10+
{
11+
public function testProductionHostIsFetchedByDefault()
12+
{
13+
$host = Config::get('server-sync.production.host');
14+
$user = Config::get('server-sync.production.user');
15+
// Mock the command
16+
$this->artisan(SyncPullCommand::class, [
17+
'--skip-db' => true,
18+
'--skip-files' => true,
19+
])
20+
->expectsOutputToContain("Pulling from {$host} as {$user}")
21+
->assertExitCode(0)
22+
;
23+
}
24+
25+
public function testRemoteOptionAreFetchedCorrectly()
26+
{
27+
// Set up the configuration for the test
28+
Config::set('server-sync.staging.host', 'staging.example.com');
29+
Config::set('server-sync.staging.user', 'staging_user');
30+
Config::set('server-sync.staging.path', '/path/to/staging');
31+
32+
// Mock the command
33+
$this->artisan(SyncPullCommand::class, [
34+
'--remote' => 'staging',
35+
'--skip-db' => true,
36+
'--skip-files' => true,
37+
])
38+
->expectsOutputToContain('Pulling from staging.example.com as staging_user')
39+
->assertExitCode(0)
40+
;
41+
}
42+
43+
public function testCommandOptionsOverwritesConfigValues()
44+
{
45+
// Set up default config values
46+
Config::set('server-sync.production.host', 'default.example.com');
47+
Config::set('server-sync.production.user', 'default_user');
48+
Config::set('server-sync.production.path', '/default/path');
49+
50+
// Mock the command with overriding options
51+
$this->artisan(SyncPullCommand::class, [
52+
'--host' => 'custom.example.com',
53+
'--user' => 'custom_user',
54+
'--path' => '/custom/path',
55+
'--skip-db' => true,
56+
'--skip-files' => true,
57+
])
58+
->expectsOutputToContain('Pulling from custom.example.com as custom_user')
59+
->assertExitCode(0)
60+
;
61+
}
62+
}

0 commit comments

Comments
 (0)