Added UpDb
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
<?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 . '"');
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -14,7 +14,8 @@
|
||||
"laravel/framework": "^13.0",
|
||||
"laravel/sanctum": "^4.0",
|
||||
"laravel/tinker": "^3.0",
|
||||
"tightenco/ziggy": "^2.0"
|
||||
"tightenco/ziggy": "^2.0",
|
||||
"ext-pdo": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.23",
|
||||
|
||||
@@ -99,6 +99,18 @@ return [
|
||||
'sslmode' => env('DB_SSLMODE', 'prefer'),
|
||||
],
|
||||
|
||||
'pgsql_production' => [
|
||||
'driver' => 'pgsql',
|
||||
'host' => env('PRODUCTION_DB_HOST', '127.0.0.1'),
|
||||
'port' => env('PRODUCTION_DB_PORT', '5432'),
|
||||
'database' => env('PRODUCTION_DB_DATABASE', ''),
|
||||
'username' => env('PRODUCTION_DB_USERNAME', ''),
|
||||
'password' => env('PRODUCTION_DB_PASSWORD', ''),
|
||||
'charset' => 'utf8',
|
||||
'prefix' => '',
|
||||
'schema' => 'public',
|
||||
],
|
||||
|
||||
'sqlsrv' => [
|
||||
'driver' => 'sqlsrv',
|
||||
'url' => env('DB_URL'),
|
||||
|
||||
Reference in New Issue
Block a user