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/framework": "^13.0",
|
||||||
"laravel/sanctum": "^4.0",
|
"laravel/sanctum": "^4.0",
|
||||||
"laravel/tinker": "^3.0",
|
"laravel/tinker": "^3.0",
|
||||||
"tightenco/ziggy": "^2.0"
|
"tightenco/ziggy": "^2.0",
|
||||||
|
"ext-pdo": "*"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"fakerphp/faker": "^1.23",
|
"fakerphp/faker": "^1.23",
|
||||||
|
|||||||
@@ -99,6 +99,18 @@ return [
|
|||||||
'sslmode' => env('DB_SSLMODE', 'prefer'),
|
'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' => [
|
'sqlsrv' => [
|
||||||
'driver' => 'sqlsrv',
|
'driver' => 'sqlsrv',
|
||||||
'url' => env('DB_URL'),
|
'url' => env('DB_URL'),
|
||||||
|
|||||||
Reference in New Issue
Block a user