2026-10-20 06:45:30 -04:00
<? php
/** Scheduler entry point. Credentials are never passed on the process command line. */
if ( PHP_SAPI !== 'cli' ){ http_response_code ( 404 ); exit ;}
require __DIR__ . '/../classes/Base.php' ;
$directory = realpath ( $argv [ 1 ] ?? '' ); $root = realpath ( __DIR__ . '/..' );
if ( ! $directory ||! is_dir ( $directory ) || strtolower ( $directory ) === strtolower ( $root ) || str_starts_with ( strtolower ( $directory ), strtolower ( $root ) . DIRECTORY_SEPARATOR )) throw new RuntimeException ( 'Choose an existing protected backup directory outside the application tree.' );
$base = new Base (); $settings = $base -> getSettings (); $db = $base -> connect ();
$engines = $db -> query ( "SELECT ENGINE FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_TYPE='BASE TABLE'" ) -> fetchAll ( PDO :: FETCH_COLUMN );
if ( array_filter ( $engines , static fn ( $v ) => $v !== 'InnoDB' )) throw new RuntimeException ( 'Nontransactional tables require a maintenance-window locked backup.' );
$file = $directory . DIRECTORY_SEPARATOR . 'hcr-' . gmdate ( 'Ymd-His' ) . '-' . bin2hex ( random_bytes ( 3 )) . '.sql' ;
2026-10-21 06:53:45 -04:00
$partial = $file . '.partial' ; $config = null ;
2026-10-20 06:45:30 -04:00
$quote = static fn ( $value ) => '"' . str_replace ([ " \\ " , '"' , " \r " , " \n " ],[ " \\\\ " , '\\"' , '\\r' , '\\n' ],( string ) $value ) . '"' ;
$options = "[client] \n host=" . $quote ( $settings -> DatabaseServer ) . " \n port=" . ( int )( $settings -> DatabasePort ?? 3306 ) . " \n user=" . $quote ( $settings -> DatabaseUser ) . " \n password=" . $quote ( $settings -> DatabasePassword ) . " \n " ;
$ca = getenv ( 'HCR_DB_TLS_CA' ); if ( $ca ) $options .= 'ssl-ca=' . $quote ( $ca ) . " \n ssl-verify-server-cert \n " ;
try {
2026-10-21 06:53:45 -04:00
$handle = fopen ( $partial , 'x' ); if ( ! $handle ) throw new RuntimeException ( 'Cannot create protected backup output.' ); fclose ( $handle ); chmod ( $partial , 0600 );
$config = tempnam ( $directory , 'hcr-connection-' ); if ( $config === false ) throw new RuntimeException ( 'Cannot create protected connection configuration.' ); chmod ( $config , 0600 );
2026-10-20 06:45:30 -04:00
file_put_contents ( $config , $options );
$command = [ getenv ( 'HCR_DUMP_BINARY' ) ?: 'mariadb-dump' , '--defaults-extra-file=' . $config , '--single-transaction' , '--quick' , '--routines' , '--events' , '--triggers' , '--hex-blob' , '--databases' ,( string ) $settings -> DatabaseName , '--result-file=' . $partial ];
$process = proc_open ( $command ,[ 0 => [ 'pipe' , 'r' ], 1 => [ 'pipe' , 'w' ], 2 => [ 'pipe' , 'w' ]], $pipes );
if ( ! is_resource ( $process )) throw new RuntimeException ( 'Backup process could not start.' );
fclose ( $pipes [ 0 ]); $out = stream_get_contents ( $pipes [ 1 ]); $error = stream_get_contents ( $pipes [ 2 ]); fclose ( $pipes [ 1 ]); fclose ( $pipes [ 2 ]);
if ( proc_close ( $process ) !== 0 ){ error_log ( 'Backup process failed: ' . $error ); throw new RuntimeException ( 'Backup failed; inspect the protected backup error log.' );}
2026-10-21 06:53:45 -04:00
if ( ! BackupValidation :: complete ( $partial )) throw new RuntimeException ( 'Backup completion could not be confirmed.' );
2026-10-20 06:45:30 -04:00
if ( ! rename ( $partial , $file )) throw new RuntimeException ( 'Cannot finalize completed backup.' );
chmod ( $file , 0600 );
$metadata = [ 'created_utc' => gmdate ( DATE_ATOM ), 'bytes' => filesize ( $file ), 'sha256' => hash_file ( 'sha256' , $file )]; file_put_contents ( $file . '.json' , json_encode ( $metadata , JSON_PRETTY_PRINT )); chmod ( $file . '.json' , 0600 );
echo json_encode ([ 'backup' => $file ] + $metadata ), " \n " ;
2026-10-21 06:53:45 -04:00
} finally { if ( is_string ( $config ) && is_file ( $config )) unlink ( $config ); if ( is_file ( $partial )) unlink ( $partial );}