| Server IP : 188.114.96.3 / Your IP : 216.73.216.143 Web Server : Apache System : Linux sv058.home.net.pl 6.8.2 husak_dirty_pages_fix #1 SMP PREEMPT_DYNAMIC Tue Apr 2 22:06:51 CEST 2024 x86_64 User : ( 3194769) PHP Version : 8.5.12 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : ON | Sudo : ON | Pkexec : OFF Directory : /polana/includes/ |
Upload File : |
<?php
declare(strict_types=1);
/** POLANA V18.11 — additive quarterly + pre-change full backups and Drive verification. */
require_once __DIR__ . '/polana_backup_v182.php';
function p1811_state_path(string $name): string {
if (!in_array($name, ['manual','quarter','cron'], true)) throw new RuntimeException('Nieprawidłowy stan.');
return pbk_private() . '/v1811-' . $name . '.json';
}
function p1811_read(string $name): array {
$file = p1811_state_path($name);
if (!is_file($file)) return [];
$data = json_decode((string)file_get_contents($file), true);
if (!is_array($data)) throw new RuntimeException('Niepoprawny plik stanu V18.11: ' . $name);
return $data;
}
function p1811_write(string $name, array $data): void {
$file = p1811_state_path($name);
$tmp = $file . '.tmp-' . bin2hex(random_bytes(5));
try {
if (file_put_contents($tmp, json_encode($data, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES|JSON_THROW_ON_ERROR), LOCK_EX) === false)
throw new RuntimeException('Nie można zapisać stanu backupu.');
@chmod($tmp, 0600);
if (!rename($tmp, $file)) throw new RuntimeException('Nie można zatwierdzić stanu backupu.');
} finally { if (is_file($tmp)) @unlink($tmp); }
}
function p1811_fullsql_dir(): string {
$dir = pbk_private() . '/fullsql';
if (!is_dir($dir) && !mkdir($dir, 0700)) throw new RuntimeException('Nie można utworzyć katalogu kopii pełnych SQL.');
@chmod($dir,0700);
return $dir;
}
function p1811_remote_meta(string $id, array $cfg): array {
if (!preg_match('/^[A-Za-z0-9_-]{8,200}$/', $id)) throw new RuntimeException('Brak poprawnego ID kopii w Google Drive.');
$token = pbk_drive_access_token($cfg);
[$code,$body] = pbk_http('https://www.googleapis.com/drive/v3/files/' . rawurlencode($id) . '?fields=id,name,size,md5Checksum,trashed,parents',
'GET', ['Authorization: Bearer ' . $token]);
if ($code !== 200) throw new RuntimeException('Google Drive nie potwierdził obecności pliku (HTTP ' . $code . ').');
$data = json_decode($body, true);
if (!is_array($data) || empty($data['id']) || !empty($data['trashed'])) throw new RuntimeException('Nieprawidłowa odpowiedź Google Drive.');
return $data;
}
function p1811_verify_remote(string $path, string $id, array $cfg): array {
if (!is_file($path) || !pbk_verify_encrypted($path)) throw new RuntimeException('Kopia lokalna jest nieobecna lub ma uszkodzone szyfrowanie.');
$remote = p1811_remote_meta($id,$cfg);
$expectedSize = filesize($path);
if (($remote['name'] ?? '') !== basename($path) || (string)($remote['size'] ?? '') !== (string)$expectedSize) {
throw new RuntimeException('Google Drive: nazwa lub wielkość pliku nie zgadza się z kopią lokalną.');
}
$md5 = (string)($remote['md5Checksum'] ?? '');
if ($md5 === '' || !hash_equals(strtolower($md5), strtolower(hash_file('md5',$path)))) {
throw new RuntimeException('Google Drive: suma kontrolna przesłanego archiwum jest niezgodna lub niedostępna.');
}
return ['size'=>$expectedSize,'md5'=>$md5,'id'=>$id,'verified_at'=>date(DATE_ATOM)];
}
function p1811_verify_existing(string $path, array &$cfg): array {
$state = pbk_status($path);
$id = (string)($state['drive_file_id'] ?? '');
if ($id === '') throw new RuntimeException('Ta kopia nie ma potwierdzonego identyfikatora Google Drive.');
$result = p1811_verify_remote($path,$id,$cfg);
$state['drive'] = true;
$state['drive_verified_at'] = $result['verified_at'];
$state['drive_verified_size'] = $result['size'];
pbk_save_status($path,$state);
return $result;
}
function p1811_send_verified(string $path, array &$cfg): array {
if (empty($cfg['drive_enabled'])) throw new RuntimeException('Włącz wysyłkę na Google Drive w ustawieniach backupów.');
if (empty($cfg['drive_refresh_token'])) throw new RuntimeException('Połącz konto Google Drive.');
$state = pbk_status($path);
if (!empty($state['drive_file_id'])) {
try { return p1811_verify_existing($path,$cfg); }
catch (Throwable $e) {
// Do not silently replace existing remote file on transient API failures.
throw new RuntimeException('Istniejący plik Drive wymaga wyjaśnienia: ' . $e->getMessage());
}
}
if (!pbk_verify_encrypted($path)) throw new RuntimeException('Archiwum nie przeszło sprawdzenia szyfrowania.');
$id = pbk_drive_upload($path,$cfg);
// Save ID immediately: after a temporary verification failure the next run checks the same remote file, not a duplicate.
$state['drive_file_id'] = $id;
$state['drive'] = false;
pbk_save_status($path,$state);
$verified = p1811_verify_remote($path,$id,$cfg);
$state['drive'] = true;
$state['drive_verified_at'] = $verified['verified_at'];
$state['drive_verified_size'] = $verified['size'];
pbk_save_status($path,$state);
pbk_log('drive-verify','OK',basename($path));
return $verified;
}
/** A tiny encrypted probe is uploaded, verified using Drive md5/size and deleted afterwards. */
function p1811_drive_probe(array &$cfg): string {
if (empty($cfg['drive_refresh_token'])) throw new RuntimeException('Najpierw połącz konto Google Drive.');
$path = pbk_private() . '/tmp/polana-test-drive-' . date('YmdHis') . '-' . bin2hex(random_bytes(4)) . '.pbackup';
$source = fopen('php://temp','w+b');
if ($source === false) throw new RuntimeException('Brak pamięci na test.');
fwrite($source, 'POLANA DRIVE TEST ' . date(DATE_ATOM) . ' ' . bin2hex(random_bytes(16)));
rewind($source);
$id = '';
try {
pbk_encrypt_stream($source,$path);
$id = pbk_drive_upload($path,$cfg);
$verified = p1811_verify_remote($path,$id,$cfg);
pbk_log('drive-probe','OK','Wysłano i sprawdzono plik testowy na Drive: ' . $verified['size'] . ' B.');
$message = 'Wysłano i zweryfikowano plik testowy na Google Drive (rozmiar i MD5).';
} finally {
fclose($source);
@unlink($path);
if ($id !== '') {
try {
$token = pbk_drive_access_token($cfg);
[$http] = pbk_http('https://www.googleapis.com/drive/v3/files/'.rawurlencode($id),'DELETE',['Authorization: Bear '.$token]);
if (!in_array($http,[200,204],true)) pbk_log('drive-probe','WARN','Nie udało się usunąć testowego pliku z Drive.');
} catch (Throwable $ignored) { pbk_log('drive-probe','WARN','Testowy plik może pozostać na Drive.'); }
}
}
return $message;
}
function p1811_estimate_site_bytes(): int {
$root = pbk_root(); $bytes = 0; $files = 0;
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS),RecursiveIteratorIterator::LEAVES_ONLY);
foreach ($it as $entry) {
if (!$entry->isFile() || $entry->isLink()) continue;
$rel = str_replace('\\','/',substr($entry->getPathname(),strlen($root)+1));
if (preg_match('~(^|/)(\.git|node_modules|vendor-cache|cache|tmp|sessions|\.polana_v174_backups|polana-v15-backups|polana-i18n-v96-backup)(/|$)~i', $rel)) continue;
if (preg_match('~(\.log$|\.bak$|\.tmp$|\.zip$|\.tar$|\.sql$|\.pbackup$|^admin/database_z_zip\.sql$)~i', $rel)) continue;
$bytes += max(0,(int)$entry->getSize()); $files++;
if ($files > 150000) throw new RuntimeException('Za dużo plików do archiwizacji.');
}
if ($files < 15) throw new RuntimeException('Znaleziono za mało plików strony.');
$free = disk_free_space(pbk_private());
if ($free !== false && $free < ($bytes * 2.2 + 64 * 1048576)) {
throw new RuntimeException('Za mało wolnego miejsca na tymczasowe archiwum strony i kopię szyfrowaną.');
}
return $bytes;
}
function p1811_current_quarter(): string {
return date('Y') . '-Q' . (int)ceil((int)date('n') / 3);
}
function p1811_enqueue_manual(): array {
$cfg = pbk_cfg();
if (empty($cfg['enabled']) || empty($cfg['key_exported'])) throw new RuntimeException('Najpierw włącz backupy i zachowaj klucz szyfrowania.');
$job = p1811_read('manual');
if (in_array(($job['status'] ?? ''), ['queued','working','waiting_drive'],true)) throw new RuntimeException('Pełna kopia jest już zlecona. Zaczekaj na zakończenie.');
$job = ['id'=>bin2hex(random_bytes(8)), 'status'=>'queued','requested_at'=>date(DATE_ATOM),'source'=>'manual'];
p1811_write('manual',$job);
return $job;
}
function p1811_site_job(string $name, array $job, array &$cfg): array {
$lock = fopen(pbk_private() . '/backup.lock','c+');
if (!$lock || !flock($lock, LOCK_EX|LOCK_NB)) throw new RuntimeException('Trwa już inne zadanie backupu.');
try {
$job['status']='working';
p1811_write($name,$job);
// Stamp is kept stable across retries, preventing multiple new full archives.
$stamp = (string)($job['stamp'] ?? '');
if ($stamp === '') { $stamp = date('Y-m-d-His'); $job['stamp']=$stamp; p1811_write($name,$job); }
if (empty($job['site'])) {
p1811_estimate_site_bytes();
$site = pbk_site_weekly($stamp);
$job['site'] = basename($site);
p1811_write($name,$job);
pbk_log('full-site','OK','Utworzono kopię plików: '.$job['site']);
}
if (empty($job['sql'])) {
$dest = p1811_fullsql_dir().'/polana-sql-'.$stamp.'.sql.gz.pbackup';
if (!is_file($dest)) {
$src = pbk_sql_daily($stamp);
if (!rename($src,$dest)) throw new RuntimeException('Nie można przenieść SQL do katalogu pełnych kopii.');
@chmod($dest,0600);
}
$job['sql']=basename($dest);
p1811_write($name,$job);
pbk_log('full-sql','OK','Utworzono punktowy SQL: '.$job['sql']);
}
$sitePath = pbk_private().'/weekly/'.basename((string)$job['site']);
$sqlPath = p1811_fullsql_dir().'/'.basename((string)$job['sql']);
if (!is_file($sitePath) || !is_file($sqlPath)) throw new RuntimeException('Brakuje jednej z dwóch części kompletnej kopii.');
if (empty($cfg['drive_enabled']) || empty($cfg['drive_refresh_token'])) {
$job['status']='waiting_drive';
$job['last_error']='Obie kopie powstały lokalnie, ale wysyłka Drive nie jest włączona.';
p1811_write($name,$job);
pbk_log('full-site','WARN',$job['last_error']);
return $job;
}
$siteVerified = p1811_send_verified($sitePath,$cfg);
$sqlVerified = p1811_send_verified($sqlPath,$cfg);
$job['status']='done'; $job['completed_at']=date(DATE_ATOM);
$job['site_drive_id']=$siteVerified['id']; $job['sql_drive_id']=$sqlVerified['id'];
unset($job['last_error']);
p1811_write($name,$job);
pbk_log('full-backup','OK','Pełna kopia plików + SQL potwierdzona na Google Drive: '.$job['site'].' / '.$job['sql']);
return $job;
} catch (Throwable $e) {
$job['status']='waiting_drive';
$job['last_error']=substr($e->getMessage(),0,300);
p1811_write($name,$job);
pbk_log('full-backup','ERROR',$job['last_error']);
throw $e;
} finally { flock($lock,LOCK_UN); fclose($lock); }
}
function p1811_prune_fullsql(array $cfg): void {
$list=glob(p1811_fullsql_dir().'/*.pbackup')?:[];
rsort($list,SORT_STRING);
foreach (array_slice($list,8) as $file) {
$state=pbk_status($file);
if (empty($state['drive_verified_at'])) continue;
@unlink($file); @unlink(pbk_status_path($file));
}
}
function p1811_sql_needs_retry(array $cfg): bool {
$path=pbk_private().'/daily/polana-sql-'.date('Y-m-d').'.sql.gz.pbackup';
if (!is_file($path)) return true;
$s=pbk_status($path);
return (!empty($cfg['email_enabled']) && empty($s['mail'])) || (!empty($cfg['drive_enabled']) && empty($s['drive']));
}
/** 1 cron hourly: daily SQL after 03:00, quarterly full once per quarter, queued full from panel. */
function p1811_tick(bool $forceFull=false): array {
date_default_timezone_set('Europe/Warsaw');
$cfg=pbk_cfg();
if (empty($cfg['enabled']) || empty($cfg['key_exported'])) throw new RuntimeException('Backup wyłączony lub nie pobrano klucza.');
$result=[]; $failed=[];
if ((int)date('G')>=3 && p1811_sql_needs_retry($cfg)) {
try { $result=array_merge($result,pbk_run('sql')); }
catch (Throwable $e) { $failed[]='SQL: '.$e->getMessage(); }
}
$quarter=p1811_current_quarter();
$qjob=p1811_read('quarter');
if (($qjob['quarter']??'')!==$quarter) {
$qjob=['source'=>'quarter','quarter'=>$quarter,'status'=>'queued','requested_at'=>date(DATE_ATOM)];
p1811_write('quarter',$qjob);
}
if ($forceFull) p1811_enqueue_manual();
// Manual backup takes priority; the same archive can satisfy the quarterly point.
foreach (['manual','quarter'] as $name) {
$job=p1811_read($name);
if (!in_array(($job['status']??''),['queued','working','waiting_drive'],true)) continue;
try {
$updated=p1811_site_job($name,$job,$cfg);
$result[]=$name.': '.$updated['status'];
if ($name==='manual' && !empty($updated['site']) && !empty($updated['sql'])) {
$quarterJob=p1811_read('quarter');
if (($quarterJob['quarter']??'')===$quarter && ($quarterJob['status']??'')==='queued') {
foreach (['stamp','site','sql','status','completed_at','site_drive_id','sql_drive_id','last_error'] as $field) {
if (array_key_exists($field,$updated)) $quarterJob[$field]=$updated[$field];
}
p1811_write('quarter',$quarterJob);
}
}
if (($updated['status'] ?? '') !== 'done') $failed[]=$name.': wymaga potwierdzenia wysyłki na Drive';
} catch(Throwable $e) { $failed[]=$name.': '.$e->getMessage(); }
}
p1811_prune_fullsql($cfg);
p1811_write('cron',['last_run'=>date(DATE_ATOM),'last_result'=>$failed?'WARN':'OK','details'=>array_slice($result,-6),'errors'=>array_slice($failed,-4)]);
if ($failed) throw new RuntimeException(implode('; ',$failed));
return $result?:['Harmonogram sprawdzony; nowe kopie nie są obecnie wymagane.'];
}