ver. 0.300: Manifest-based update system with checksum verification and file backup

Replaces the manual ZIP packaging workflow with an automated build script.
UpdateRepository now supports both manifest JSON format (new) and legacy
_sql.txt/_files.txt format (fallback), enabling a smooth transition for
existing client instances.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-21 23:30:58 +01:00
parent d2e85e94df
commit b409806f02
19 changed files with 932 additions and 218 deletions

View File

@@ -61,10 +61,284 @@ class UpdateRepository
return [ 'success' => true, 'log' => $log, 'no_updates' => true ];
}
/**
* Dispatcher — próbuje pobrać manifest, jeśli jest → nowa ścieżka, jeśli brak → legacy.
*/
private function downloadAndApply( string $ver, string $dir, array $log ): array
{
$baseUrl = 'https://shoppro.project-dc.pl/updates/' . $dir;
$manifest = $this->downloadManifest( $baseUrl, $ver );
if ( $manifest !== null ) {
$log[] = '[INFO] Znaleziono manifest dla wersji ' . $ver;
return $this->downloadAndApplyWithManifest( $ver, $dir, $manifest, $log );
}
$log[] = '[INFO] Brak manifestu, używam trybu legacy';
return $this->downloadAndApplyLegacy( $ver, $dir, $log );
}
/**
* Pobiera manifest JSON dla danej wersji.
*
* @return array|null Zdekodowany manifest lub null jeśli brak
*/
private function downloadManifest( string $baseUrl, string $ver )
{
$manifestUrl = $baseUrl . '/ver_' . $ver . '_manifest.json';
$ch = curl_init( $manifestUrl );
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
curl_setopt( $ch, CURLOPT_HEADER, false );
curl_setopt( $ch, CURLOPT_TIMEOUT, 15 );
$response = curl_exec( $ch );
$httpCode = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
curl_close( $ch );
if ( !$response || $httpCode !== 200 ) {
return null;
}
$manifest = json_decode( $response, true );
if ( !is_array( $manifest ) || !isset( $manifest['version'] ) ) {
return null;
}
return $manifest;
}
/**
* Aktualizacja z użyciem manifestu — checksum, backup, SQL z manifestu, usuwanie z manifestu.
*/
private function downloadAndApplyWithManifest( string $ver, string $dir, array $manifest, array $log ): array
{
$baseUrl = 'https://shoppro.project-dc.pl/updates/' . $dir;
$log[] = '[INFO] Tryb aktualizacji: manifest';
// 1. Pobieranie ZIP
$zipUrl = $baseUrl . '/ver_' . $ver . '.zip';
$log[] = '[INFO] Pobieranie pliku ZIP: ' . $zipUrl;
$file = @file_get_contents( $zipUrl );
if ( $file === false ) {
$log[] = '[ERROR] Nie udało się pobrać pliku ZIP';
return [ 'success' => false, 'log' => $log ];
}
$fileSize = strlen( $file );
$log[] = '[OK] Pobrano plik ZIP, rozmiar: ' . $fileSize . ' bajtów';
if ( $fileSize < 100 ) {
$log[] = '[ERROR] Plik ZIP jest za mały (prawdopodobnie błąd pobierania)';
return [ 'success' => false, 'log' => $log ];
}
$dlHandler = @fopen( 'update.zip', 'w' );
if ( !$dlHandler ) {
$log[] = '[ERROR] Nie udało się otworzyć pliku update.zip do zapisu';
return [ 'success' => false, 'log' => $log ];
}
$written = fwrite( $dlHandler, $file );
fclose( $dlHandler );
if ( $written === false || $written === 0 ) {
$log[] = '[ERROR] Nie udało się zapisać pliku ZIP';
return [ 'success' => false, 'log' => $log ];
}
$log[] = '[OK] Zapisano plik ZIP (' . $written . ' bajtów)';
// 2. Weryfikacja checksum
if ( isset( $manifest['checksum_zip'] ) ) {
$checksumResult = $this->verifyChecksum( 'update.zip', $manifest['checksum_zip'], $log );
$log = $checksumResult['log'];
if ( !$checksumResult['valid'] ) {
@unlink( 'update.zip' );
return [ 'success' => false, 'log' => $log ];
}
}
// 3. Backup plików przed nadpisaniem
$log = $this->createBackup( $manifest, $log );
// 4. SQL z manifestu
if ( !empty( $manifest['sql'] ) ) {
$log[] = '[INFO] Wykonywanie zapytań SQL z manifestu (' . count( $manifest['sql'] ) . ')';
$success = 0;
$errors = 0;
foreach ( $manifest['sql'] as $query ) {
$query = trim( $query );
if ( $query !== '' ) {
if ( $this->db->query( $query ) ) {
$success++;
} else {
$errors++;
$log[] = '[WARNING] Błąd SQL: ' . $query;
}
}
}
$log[] = '[INFO] Wykonano zapytania SQL - sukces: ' . $success . ', błędy: ' . $errors;
}
// 5. Usuwanie plików z manifestu
if ( !empty( $manifest['files']['deleted'] ) ) {
$deletedCount = 0;
foreach ( $manifest['files']['deleted'] as $relativePath ) {
$fullPath = '../' . $relativePath;
if ( file_exists( $fullPath ) ) {
if ( @unlink( $fullPath ) ) {
$deletedCount++;
} else {
$log[] = '[WARNING] Nie udało się usunąć pliku: ' . $fullPath;
}
}
}
$log[] = '[INFO] Usunięto plików: ' . $deletedCount;
}
// 6. Usuwanie katalogów z manifestu
if ( !empty( $manifest['directories_deleted'] ) ) {
$deletedDirs = 0;
foreach ( $manifest['directories_deleted'] as $dirPath ) {
$fullPath = '../' . $dirPath;
if ( is_dir( $fullPath ) ) {
\Shared\Helpers\Helpers::delete_dir( $fullPath );
$deletedDirs++;
}
}
$log[] = '[INFO] Usunięto katalogów: ' . $deletedDirs;
}
// 7. Rozpakowywanie ZIP
$log = $this->extractZip( 'update.zip', $log );
// 8. Aktualizacja wersji
$versionFile = '../libraries/version.ini';
$handle = @fopen( $versionFile, 'w' );
if ( !$handle ) {
$log[] = '[ERROR] Nie udało się otworzyć pliku version.ini do zapisu';
return [ 'success' => false, 'log' => $log ];
}
fwrite( $handle, $ver );
fclose( $handle );
$log[] = '[OK] Zaktualizowano plik version.ini do wersji: ' . $ver;
$log[] = '[SUCCESS] Aktualizacja do wersji ' . $ver . ' zakończona pomyślnie';
return [ 'success' => true, 'log' => $log ];
}
/**
* Weryfikuje sumę kontrolną pliku.
*
* @param string $filePath Ścieżka do pliku
* @param string $expectedChecksum Suma w formacie "sha256:abc123..."
* @param array $log Tablica logów
* @return array{valid: bool, log: array}
*/
private function verifyChecksum( string $filePath, string $expectedChecksum, array $log ): array
{
$parts = explode( ':', $expectedChecksum, 2 );
if ( count( $parts ) !== 2 ) {
$log[] = '[ERROR] Nieprawidłowy format sumy kontrolnej: ' . $expectedChecksum;
return [ 'valid' => false, 'log' => $log ];
}
$algorithm = $parts[0];
$expected = $parts[1];
$actual = @hash_file( $algorithm, $filePath );
if ( $actual === false ) {
$log[] = '[ERROR] Nie udało się obliczyć sumy kontrolnej pliku';
return [ 'valid' => false, 'log' => $log ];
}
if ( $actual !== $expected ) {
$log[] = '[ERROR] Suma kontrolna nie zgadza się! Oczekiwano: ' . $expected . ', otrzymano: ' . $actual;
return [ 'valid' => false, 'log' => $log ];
}
$log[] = '[OK] Suma kontrolna ZIP zgodna';
return [ 'valid' => true, 'log' => $log ];
}
/**
* Tworzy kopię zapasową plików przed aktualizacją.
*
* @param array $manifest Dane z manifestu
* @param array $log Tablica logów
* @return array Zaktualizowana tablica logów
*/
private function createBackup( array $manifest, array $log ): array
{
$version = isset( $manifest['version'] ) ? $manifest['version'] : 'unknown';
$backupDir = '../backups/' . str_replace( '.', '_', $version ) . '_' . date( 'Ymd_His' );
$log[] = '[INFO] Tworzenie kopii zapasowej w: ' . $backupDir;
$projectRoot = realpath( '../' );
if ( !$projectRoot ) {
$log[] = '[WARNING] Nie udało się określić katalogu projektu, pomijam backup';
return $log;
}
$filesToBackup = [];
if ( isset( $manifest['files']['modified'] ) && is_array( $manifest['files']['modified'] ) ) {
$filesToBackup = array_merge( $filesToBackup, $manifest['files']['modified'] );
}
if ( isset( $manifest['files']['deleted'] ) && is_array( $manifest['files']['deleted'] ) ) {
$filesToBackup = array_merge( $filesToBackup, $manifest['files']['deleted'] );
}
if ( empty( $filesToBackup ) ) {
$log[] = '[INFO] Brak plików do backupu';
return $log;
}
$backedUp = 0;
foreach ( $filesToBackup as $relativePath ) {
$sourcePath = $projectRoot . '/' . $relativePath;
if ( !file_exists( $sourcePath ) ) {
continue;
}
$targetPath = $backupDir . '/' . $relativePath;
$targetDir = dirname( $targetPath );
if ( !is_dir( $targetDir ) ) {
@mkdir( $targetDir, 0755, true );
}
if ( @copy( $sourcePath, $targetPath ) ) {
$backedUp++;
} else {
$log[] = '[WARNING] Nie udało się skopiować do backupu: ' . $relativePath;
}
}
$log[] = '[OK] Backup: skopiowano ' . $backedUp . ' plików';
@file_put_contents(
$backupDir . '/manifest.json',
json_encode( $manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE )
);
return $log;
}
/**
* Legacy — stary format aktualizacji (ZIP + _sql.txt + _files.txt).
*/
private function downloadAndApplyLegacy( string $ver, string $dir, array $log ): array
{
$baseUrl = 'https://shoppro.project-dc.pl/updates/' . $dir;
// Pobieranie ZIP
$zipUrl = $baseUrl . '/ver_' . $ver . '.zip';
$log[] = '[INFO] Pobieranie pliku ZIP: ' . $zipUrl;