Files
FlightsAPI/app/Console/Commands/UpDb.php
T
2026-04-03 14:53:07 +10:00

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 . '"');
}
}