feat: auto-verify backup integrity after creation (#65)
After archive is created and checksum computed, automatically verify: - Archive opens without error - Contains at least one entry - database.sql present when backup type includes database - First entry is readable (spot-check) Applied to both BackupEngine and SteppedBackupEngine. Throws RuntimeException on verification failure (backup marked as failed). Closes #65
This commit is contained in:
@@ -232,6 +232,11 @@ class BackupEngine
|
||||
$this->log('Archive created: ' . $sizeHuman);
|
||||
$this->log('SHA-256: ' . ($checksum ?: 'N/A'));
|
||||
|
||||
// Verify archive integrity
|
||||
$this->log('Verifying archive integrity...');
|
||||
$this->verifyArchive($archivePath, $profile->backup_type);
|
||||
$this->log('Archive integrity verified');
|
||||
|
||||
// Step 2.5: Wrap with MokoRestore script (if enabled)
|
||||
$includeMokoRestore = (bool) ($profile->include_mokorestore ?? false);
|
||||
|
||||
@@ -518,6 +523,90 @@ class BackupEngine
|
||||
$zip->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that a backup archive can be opened and contains expected entries.
|
||||
*
|
||||
* @param string $archivePath Absolute path to the archive file
|
||||
* @param string $backupType Backup type: full, database, files, differential
|
||||
*
|
||||
* @throws \RuntimeException If the archive fails verification
|
||||
*/
|
||||
private function verifyArchive(string $archivePath, string $backupType): void
|
||||
{
|
||||
if (!is_file($archivePath)) {
|
||||
throw new \RuntimeException('Archive file does not exist: ' . $archivePath);
|
||||
}
|
||||
|
||||
$extension = strtolower(pathinfo($archivePath, PATHINFO_EXTENSION));
|
||||
|
||||
// Detect tar.gz (pathinfo only returns 'gz')
|
||||
if ($extension === 'gz' && str_ends_with(strtolower($archivePath), '.tar.gz')) {
|
||||
$this->verifyTarGzArchive($archivePath);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// ZIP verification
|
||||
$zip = new \ZipArchive();
|
||||
|
||||
if ($zip->open($archivePath, \ZipArchive::RDONLY) !== true) {
|
||||
throw new \RuntimeException('Archive integrity check failed: cannot open ZIP file');
|
||||
}
|
||||
|
||||
if ($zip->numFiles < 1) {
|
||||
$zip->close();
|
||||
throw new \RuntimeException('Archive integrity check failed: archive contains no files');
|
||||
}
|
||||
|
||||
// Verify database.sql exists when backup includes database
|
||||
if ($backupType !== 'files') {
|
||||
if ($zip->locateName('database.sql') === false) {
|
||||
$zip->close();
|
||||
throw new \RuntimeException('Archive integrity check failed: database.sql missing from archive');
|
||||
}
|
||||
}
|
||||
|
||||
// Spot-check: verify the first entry is readable
|
||||
$firstName = $zip->getNameIndex(0);
|
||||
|
||||
if ($firstName === false) {
|
||||
$zip->close();
|
||||
throw new \RuntimeException('Archive integrity check failed: cannot read first entry');
|
||||
}
|
||||
|
||||
$zip->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a tar.gz archive can be opened and iterated.
|
||||
*
|
||||
* @param string $archivePath Absolute path to the .tar.gz file
|
||||
*
|
||||
* @throws \RuntimeException If the archive fails verification
|
||||
*/
|
||||
private function verifyTarGzArchive(string $archivePath): void
|
||||
{
|
||||
try {
|
||||
$phar = new \PharData($archivePath);
|
||||
$count = 0;
|
||||
|
||||
foreach ($phar as $entry) {
|
||||
// Spot-check: verify at least the first entry is accessible
|
||||
$entry->getFilename();
|
||||
$count++;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($count === 0) {
|
||||
throw new \RuntimeException('Archive integrity check failed: tar.gz archive contains no entries');
|
||||
}
|
||||
} catch (\RuntimeException $e) {
|
||||
throw $e;
|
||||
} catch (\Throwable $e) {
|
||||
throw new \RuntimeException('Archive integrity check failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the onMokoSuiteBackupAfterRun event so plugins (actionlog, etc.) can react.
|
||||
*/
|
||||
|
||||
@@ -347,6 +347,11 @@ class SteppedBackupEngine
|
||||
|
||||
$totalSize = file_exists($session->archivePath) ? filesize($session->archivePath) : 0;
|
||||
|
||||
// Verify archive integrity
|
||||
$session->log('Verifying archive integrity...');
|
||||
$this->verifyArchive($session->archivePath, $session->backupType);
|
||||
$session->log('Archive integrity verified');
|
||||
|
||||
// MokoRestore wrapper
|
||||
if ($session->includeMokoRestore) {
|
||||
$session->log('Wrapping with MokoRestore script...');
|
||||
@@ -449,6 +454,50 @@ class SteppedBackupEngine
|
||||
$this->completeRecord($session, $uploadFailed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that a backup archive can be opened and contains expected entries.
|
||||
*
|
||||
* @param string $archivePath Absolute path to the archive file
|
||||
* @param string $backupType Backup type: full, database, files, differential
|
||||
*
|
||||
* @throws \RuntimeException If the archive fails verification
|
||||
*/
|
||||
private function verifyArchive(string $archivePath, string $backupType): void
|
||||
{
|
||||
if (!is_file($archivePath)) {
|
||||
throw new \RuntimeException('Archive file does not exist: ' . $archivePath);
|
||||
}
|
||||
|
||||
$zip = new \ZipArchive();
|
||||
|
||||
if ($zip->open($archivePath, \ZipArchive::RDONLY) !== true) {
|
||||
throw new \RuntimeException('Archive integrity check failed: cannot open ZIP file');
|
||||
}
|
||||
|
||||
if ($zip->numFiles < 1) {
|
||||
$zip->close();
|
||||
throw new \RuntimeException('Archive integrity check failed: archive contains no files');
|
||||
}
|
||||
|
||||
// Verify database.sql exists when backup includes database
|
||||
if ($backupType !== 'files') {
|
||||
if ($zip->locateName('database.sql') === false) {
|
||||
$zip->close();
|
||||
throw new \RuntimeException('Archive integrity check failed: database.sql missing from archive');
|
||||
}
|
||||
}
|
||||
|
||||
// Spot-check: verify the first entry is readable
|
||||
$firstName = $zip->getNameIndex(0);
|
||||
|
||||
if ($firstName === false) {
|
||||
$zip->close();
|
||||
throw new \RuntimeException('Archive integrity check failed: cannot read first entry');
|
||||
}
|
||||
|
||||
$zip->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the backup record as complete.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user