171 lines
5.9 KiB
PHP
171 lines
5.9 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use Illuminate\Console\Command;
|
|
|
|
class UpDb extends Command
|
|
{
|
|
protected $signature = 'db:updb
|
|
{--no-backup : Skip saving a local backup before wiping}
|
|
{--force : Skip confirmation prompt}';
|
|
|
|
protected $description = 'Download the production database and replace the local database with it';
|
|
|
|
public function handle(): int
|
|
{
|
|
if (! $this->option('force') && ! $this->confirm('This will wipe your local database and replace it with production. Are you sure?')) {
|
|
$this->info('Aborted.');
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
$local = $this->connectionConfig('local');
|
|
$production = $this->connectionConfig('production');
|
|
|
|
$timestamp = now()->format('Y_m_d_His');
|
|
$dumpPath = storage_path("app/private/db_dumps/production_{$timestamp}.dump");
|
|
$backupPath = storage_path("app/private/db_dumps/local_backup_{$timestamp}.dump");
|
|
|
|
if (! is_dir(storage_path('app/private/db_dumps'))) {
|
|
mkdir(storage_path('app/private/db_dumps'), 0755, true);
|
|
}
|
|
|
|
// Step 1: Dump production
|
|
$this->info('Dumping production database...');
|
|
$dumpResult = $this->pgDump($production, $dumpPath);
|
|
|
|
if ($dumpResult !== 0) {
|
|
$this->error('Failed to dump production database.');
|
|
return self::FAILURE;
|
|
}
|
|
|
|
$this->info("Production dump saved to: {$dumpPath}");
|
|
|
|
// Step 2: Back up local (unless skipped)
|
|
if (! $this->option('no-backup')) {
|
|
$this->info('Backing up local database...');
|
|
$backupResult = $this->pgDump($local, $backupPath);
|
|
|
|
if ($backupResult !== 0) {
|
|
$this->error('Failed to back up local database. Aborting to be safe. Use --no-backup to skip.');
|
|
return self::FAILURE;
|
|
}
|
|
|
|
$this->info("Local backup saved to: {$backupPath}");
|
|
}
|
|
|
|
// Step 3: Drop and recreate the local database
|
|
$this->info('Wiping local database...');
|
|
$this->dropAndRecreateLocalDatabase($local);
|
|
|
|
// Step 4: Restore production dump into local
|
|
$this->info('Restoring production dump to local database...');
|
|
$restoreResult = $this->pgRestore($local, $dumpPath);
|
|
|
|
if ($restoreResult !== 0) {
|
|
$this->error('Restore failed. Your local backup is at: ' . ($this->option('no-backup') ? 'N/A (backup was skipped)' : $backupPath));
|
|
return self::FAILURE;
|
|
}
|
|
|
|
$this->info('Done. Local database now mirrors production.');
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
private function connectionConfig(string $type): array
|
|
{
|
|
$connection = $type === 'production' ? 'pgsql_production' : config('database.default');
|
|
|
|
return [
|
|
'host' => config("database.connections.{$connection}.host"),
|
|
'port' => config("database.connections.{$connection}.port"),
|
|
'database' => config("database.connections.{$connection}.database"),
|
|
'username' => config("database.connections.{$connection}.username"),
|
|
'password' => config("database.connections.{$connection}.password"),
|
|
];
|
|
}
|
|
|
|
private function pgDump(array $config, string $outputPath): int
|
|
{
|
|
$pgpass = $this->writePgPass($config);
|
|
|
|
$command = sprintf(
|
|
'pg_dump -Fc -h %s -p %s -U %s %s -f %s',
|
|
escapeshellarg($config['host']),
|
|
escapeshellarg($config['port']),
|
|
escapeshellarg($config['username']),
|
|
escapeshellarg($config['database']),
|
|
escapeshellarg($outputPath),
|
|
);
|
|
|
|
$resultCode = $this->runWithPgPass($pgpass, $command);
|
|
|
|
unlink($pgpass);
|
|
|
|
return $resultCode;
|
|
}
|
|
|
|
private function pgRestore(array $config, string $dumpPath): int
|
|
{
|
|
$pgpass = $this->writePgPass($config);
|
|
|
|
$command = sprintf(
|
|
'pg_restore -h %s -p %s -U %s -d %s --no-owner --no-privileges %s',
|
|
escapeshellarg($config['host']),
|
|
escapeshellarg($config['port']),
|
|
escapeshellarg($config['username']),
|
|
escapeshellarg($config['database']),
|
|
escapeshellarg($dumpPath),
|
|
);
|
|
|
|
$resultCode = $this->runWithPgPass($pgpass, $command);
|
|
|
|
unlink($pgpass);
|
|
|
|
return $resultCode;
|
|
}
|
|
|
|
/**
|
|
* Write a temporary pgpass file and return its path.
|
|
* This is the cross-platform alternative to PGPASSWORD which doesn't work on Windows.
|
|
*/
|
|
private function writePgPass(array $config): string
|
|
{
|
|
// Escape any colons or backslashes in the password per pgpass format rules
|
|
$password = str_replace(['\\', ':'], ['\\\\', '\\:'], $config['password']);
|
|
$content = "{$config['host']}:{$config['port']}:*:{$config['username']}:{$password}";
|
|
$path = tempnam(sys_get_temp_dir(), 'pgpass_');
|
|
|
|
file_put_contents($path, $content);
|
|
chmod($path, 0600);
|
|
|
|
return $path;
|
|
}
|
|
|
|
private function runWithPgPass(string $pgpassFile, string $command): int
|
|
{
|
|
// PGPASSFILE is supported on all platforms including Windows
|
|
putenv("PGPASSFILE={$pgpassFile}");
|
|
passthru($command, $resultCode);
|
|
putenv('PGPASSFILE');
|
|
|
|
return $resultCode;
|
|
}
|
|
|
|
private function dropAndRecreateLocalDatabase(array $config): void
|
|
{
|
|
$database = $config['database'];
|
|
|
|
// Connect to the postgres maintenance database to drop/recreate
|
|
$pdo = new \PDO(
|
|
"pgsql:host={$config['host']};port={$config['port']};dbname=postgres",
|
|
$config['username'],
|
|
$config['password'],
|
|
);
|
|
|
|
$pdo->exec("SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = " . $pdo->quote($database));
|
|
$pdo->exec("DROP DATABASE IF EXISTS " . '"' . $database . '"');
|
|
$pdo->exec("CREATE DATABASE " . '"' . $database . '"');
|
|
}
|
|
}
|