Files
Investidor-V2/tools/desk-activation-backup.php
nala 6ddafc95ba
tests / ci (push) Canceled after 0s
oportunidades 4
2026-10-10 07:03:59 +02:00

103 lines
5.9 KiB
PHP

<?php
use Illuminate\Foundation\Bootstrap\LoadConfiguration;
use Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables;
// Active connection is read-only; restore/compatibility writes are restricted to a new QA database.
require dirname(__DIR__).'/vendor/autoload.php';
$app = require dirname(__DIR__).'/bootstrap/app.php';
(new LoadEnvironmentVariables)->bootstrap($app);
(new LoadConfiguration)->bootstrap($app);
$c = config('database.connections.pgsql');
if (($c['url'] ?? null) || $c['database'] !== 'investemoz' || (int) $c['port'] !== 5432) {
throw new RuntimeException('Active backup identity configuration rejected.');
}
$active = new PDO('pgsql:host='.$c['host'].';port=5432;dbname=investemoz', $c['username'], $c['password'], [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$active->exec('BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY');
$target = $active->query('select current_database() as database, inet_server_port() as port')->fetch(PDO::FETCH_ASSOC);
if ($target['database'] !== 'investemoz' || (int) $target['port'] !== 5432) {
throw new RuntimeException('Active backup actual identity rejected.');
}
$stamp = date('Ymd_His');
$directory = storage_path('app/private/virtual-desk/activation-backup-'.$stamp);
mkdir($directory, 0700, true);
$dump = $directory.'/database.dump';
$restoreName = 'desk_restore_'.$stamp.'_test';
$bin = 'C:/Program Files/PostgreSQL/17/bin/';
$run = function (array $args, string $password): void {
$process = proc_open($args, [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes, null, [...getenv(), 'PGPASSWORD' => $password], ['bypass_shell' => true]);
if (! is_resource($process)) {
throw new RuntimeException('Backup process unavailable.');
}
fclose($pipes[0]);
stream_get_contents($pipes[1]);
fclose($pipes[1]);
$error = stream_get_contents($pipes[2]);
fclose($pipes[2]);
if (proc_close($process) !== 0) {
throw new RuntimeException('Backup/restore failed: '.$error);
}
};
$snapshot = $active->query('select pg_export_snapshot()')->fetchColumn();
$run([$bin.'pg_dump.exe', '-h', $c['host'], '-p', '5432', '-U', $c['username'], '-d', 'investemoz', '-Fc', '--snapshot='.$snapshot, '-f', $dump], $c['password']);
$maintenance = new PDO('pgsql:host=127.0.0.1;port=55441;dbname=postgres', 'postgres', '', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
if ((int) $maintenance->query('select inet_server_port()')->fetchColumn() !== 55441 || ! preg_match('/^desk_restore_\d{8}_\d{6}_test$/', $restoreName)) {
throw new RuntimeException('Restore target rejected.');
}
$maintenance->exec('CREATE DATABASE "'.$restoreName.'"');
$run([$bin.'pg_restore.exe', '-h', '127.0.0.1', '-p', '55441', '-U', 'postgres', '-d', $restoreName, '--exit-on-error', '--no-owner', '--no-privileges', $dump], '');
$qa = new PDO('pgsql:host=127.0.0.1;port=55441;dbname='.$restoreName, 'postgres', '', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$restored = $qa->query('select current_database() as database, inet_server_port() as port')->fetch(PDO::FETCH_ASSOC);
if ($restored['database'] !== $restoreName || (int) $restored['port'] !== 55441) {
throw new RuntimeException('Actual restore target rejected.');
}
$tables = $active->query("select schemaname,tablename from pg_tables where schemaname not in ('pg_catalog','information_schema') order by schemaname,tablename")->fetchAll(PDO::FETCH_ASSOC);
$counts = [];
$hashes = [];
foreach ($tables as $t) {
$name = '"'.str_replace('"', '""', $t['schemaname']).'"."'.str_replace('"', '""', $t['tablename']).'"';
$sql = 'select row_to_json(t)::text from '.$name.' t order by row_to_json(t)::text';
$a = $active->query($sql)->fetchAll(PDO::FETCH_COLUMN);
$b = $qa->query($sql)->fetchAll(PDO::FETCH_COLUMN);
if ($a !== $b) {
throw new RuntimeException('Restored contents differ: '.$name);
}
$counts[$name] = count($a);
$hashes[$name] = hash('sha256', implode("\n", $a));
}
$auditEventIds = $active->query('SELECT id FROM audit_events ORDER BY id')->fetchAll(PDO::FETCH_COLUMN);
$active->exec('ROLLBACK');
// Preserve exactly the private files referenced by restored document records, plus the runtime key.
$files = $directory.'/private-files.zip';
$zip = new ZipArchive;
$zip->open($files, ZipArchive::CREATE | ZipArchive::EXCL);
$manifest = [];
foreach ($qa->query('select storage_key from documents where storage_key is not null')->fetchAll(PDO::FETCH_COLUMN) as $relative) {
$path = realpath(storage_path('app/private/'.$relative));
$root = realpath(storage_path('app/private'));
if (! $path || ! str_starts_with(strtolower($path), strtolower($root.DIRECTORY_SEPARATOR))) {
throw new RuntimeException('Referenced private document missing or outside storage.');
}
$name = 'private/'.str_replace('\\', '/', $relative);
$zip->addFile($path, $name);
$manifest[$name] = hash_file('sha256', $path);
}
$zip->addFile(base_path('.env'), 'runtime/.env');
$manifest['runtime/.env'] = hash_file('sha256', base_path('.env'));
$zip->close();
$read = new ZipArchive;
$read->open($files);
foreach ($manifest as $name => $hash) {
$bytes = $read->getFromName($name);
if (! is_string($bytes) || hash('sha256', $bytes) !== $hash) {
throw new RuntimeException('Private file restore verification failed.');
}
}
$read->close();
$proof = ['restore_verified' => true, 'verified_at' => gmdate('c'), 'target' => $target, 'restored_target' => $restored,
'dump_path' => $dump, 'sha256' => hash_file('sha256', $dump), 'table_counts' => $counts, 'table_hashes' => $hashes,
'files_verified' => true, 'files_path' => $files, 'files_sha256' => hash_file('sha256', $files), 'file_hashes' => $manifest,
'audit_event_ids' => $auditEventIds];
file_put_contents(storage_path('app/private/virtual-desk/backup-proof.json'), json_encode($proof, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
echo json_encode(['restore_verified' => true, 'tables' => count($tables), 'files' => count($manifest), 'restore_target' => $restored], JSON_THROW_ON_ERROR);