40 lines
4.5 KiB
PHP
40 lines
4.5 KiB
PHP
<?php
|
|
// Snapshot the already authorized full QA, preserving it. Never connect to port 5432.
|
|
use Symfony\Component\Process\Process;
|
|
$root=dirname(__DIR__);require $root.'/vendor/autoload.php';ini_set('zend.exception_ignore_args','1');
|
|
$private=$root.'/storage/app/private/investmoz-v2/portal-audit';
|
|
$plan=json_decode(file_get_contents($private.'/portal-stability-latest.json'),true,512,JSON_THROW_ON_ERROR);
|
|
$old=json_decode(file_get_contents($root.'/storage/app/private/investmoz-v2/opportunity-resources-backup-proof.json'),true,512,JSON_THROW_ON_ERROR);
|
|
$db=$old['restored_target']['database'];throw_unless((int)$old['restored_target']['port']===55441 && preg_match('/^desk_restore_\d{8}_\d{6}_test$/D',$db),RuntimeException::class,'Existing full QA required.');
|
|
$pdo=new PDO('pgsql:host=127.0.0.1;port=55441;dbname='.$db,'postgres','',[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION]);
|
|
$pdo->exec('BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY');
|
|
throw_unless((int)$pdo->query('SELECT inet_server_port()')->fetchColumn()===55441,RuntimeException::class,'QA server required.');
|
|
$stamp=gmdate('Ymd_His');$clone='stability_consumers_'.$stamp.'_test';$dir=$private.'/'.$clone;mkdir($dir,0700,true);
|
|
$snapshot=$pdo->query('SELECT pg_export_snapshot()')->fetchColumn();$bin='C:/Program Files/PostgreSQL/17/bin/';
|
|
$run=static function(array $args) use($dir):void {$p=new Process($args,null,['PGPASSWORD'=>'']);$p->setTimeout(300);$p->run();if(!$p->isSuccessful()){file_put_contents($dir.'/command-error.txt',$p->getErrorOutput());throw new RuntimeException('QA clone failed; diagnostic private.');}};
|
|
$run([$bin.'pg_dump.exe','-h','127.0.0.1','-p','55441','-U','postgres','-d',$db,'-Fc','--snapshot='.$snapshot,'-f',$dir.'/database.dump']);
|
|
$maintenance=new PDO('pgsql:host=127.0.0.1;port=55441;dbname=postgres','postgres','',[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION]);
|
|
$maintenance->exec('CREATE DATABASE "'.$clone.'"');
|
|
$run([$bin.'pg_restore.exe','-h','127.0.0.1','-p','55441','-U','postgres','-d',$clone,'--exit-on-error','--no-owner','--no-privileges',$dir.'/database.dump']);
|
|
$restored=new PDO('pgsql:host=127.0.0.1;port=55441;dbname='.$clone,'postgres','',[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION]);$restored->exec('BEGIN READ ONLY');
|
|
$tables=[];
|
|
foreach($pdo->query("SELECT schemaname,tablename FROM pg_tables WHERE schemaname NOT IN ('pg_catalog','information_schema') ORDER BY schemaname,tablename")->fetchAll(PDO::FETCH_ASSOC) 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=$pdo->query($sql)->fetchAll(PDO::FETCH_COLUMN);throw_unless($a===$restored->query($sql)->fetchAll(PDO::FETCH_COLUMN),RuntimeException::class,'QA clone differs.');
|
|
$tables[$name]=['count'=>count($a),'sha256'=>hash('sha256',implode("\n",$a))];
|
|
}
|
|
$files=[];$missing=[];
|
|
foreach($pdo->query('SELECT DISTINCT storage_key FROM documents WHERE storage_key IS NOT NULL')->fetchAll(PDO::FETCH_COLUMN) as $relative){
|
|
$src=realpath($root.'/storage/app/private/'.$relative);$base=realpath($root.'/storage/app/private');
|
|
if(!$src || !str_starts_with(strtolower($src),strtolower($base.DIRECTORY_SEPARATOR))){$missing[]=$relative;continue;}
|
|
throw_unless(!str_contains($relative,'..')&&!str_contains($relative,':'),RuntimeException::class,'Private member path refused.');
|
|
$dest=$plan['source'].'/storage/app/private/'.$relative;if(!is_dir(dirname($dest)))mkdir(dirname($dest),0700,true);copy($src,$dest);
|
|
throw_unless(hash_file('sha256',$src)===hash_file('sha256',$dest),RuntimeException::class,'Private copy differs.');$files[$relative]=hash_file('sha256',$dest);
|
|
}
|
|
$pair=$pdo->query('SELECT first_user_id,second_user_id FROM portal_superuser_pair WHERE id=1')->fetch(PDO::FETCH_ASSOC);
|
|
$pdo->exec('ROLLBACK');$restored->exec('ROLLBACK');
|
|
$proof=['result'=>'restored','at'=>gmdate('c'),'target'=>['database'=>$clone,'port'=>55441],'source_QA_database'=>$db,'tables'=>$tables,'files'=>$files,'missing_files'=>$missing,'pair'=>$pair,'active_writes'=>0,'original_QA_writes'=>0];
|
|
file_put_contents($dir.'/proof.json',json_encode($proof,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
|
|
file_put_contents($private.'/portal-stability-consumers-latest.json',json_encode(['path'=>$dir.'/proof.json','target'=>$proof['target'],'directory'=>$dir],JSON_THROW_ON_ERROR));
|
|
echo json_encode(['result'=>'full_QA_fork_verified','tables'=>count($tables),'private_files'=>count($files),'missing_files'=>count($missing),'target'=>$proof['target'],'active_writes'=>0,'original_QA_writes'=>0]).PHP_EOL;
|