Update the backup code to be a full backup zip file download of uploads and db dump along with version meta data file. Also allow to restore a single file in setup currently hidden

This commit is contained in:
johnnyq 2025-05-07 15:37:57 -04:00
parent 069772f27d
commit 2ffb2be083
3 changed files with 142 additions and 126 deletions

View File

@ -8,8 +8,7 @@ require_once "includes/inc_all_admin.php";
</div> </div>
<div class="card-body" style="text-align: center;"> <div class="card-body" style="text-align: center;">
<div class="alert alert-secondary">If you are unable to back up the entire VM, you'll need to back up the files & database individually. There is no built-in restore. See the <a href="https://docs.itflow.org/backups" target="_blank">docs here</a>.</div> <div class="alert alert-secondary">If you are unable to back up the entire VM, you'll need to back up the files & database individually. There is no built-in restore. See the <a href="https://docs.itflow.org/backups" target="_blank">docs here</a>.</div>
<a class="btn btn-primary btn-lg p-3" href="post.php?download_database&csrf_token=<?php echo $_SESSION['csrf_token'] ?>"><i class="fas fa-fw fa-4x fa-download"></i><br><br>Download database</a> <a class="btn btn-primary btn-lg p-3" href="post.php?download_backup&csrf_token=<?php echo $_SESSION['csrf_token'] ?>"><i class="fas fa-fw fa-4x fa-download"></i><br><br>Download Backup</a>
<a class="btn btn-primary btn-lg p-3" href="post.php?download_uploads&csrf_token=<?php echo $_SESSION['csrf_token'] ?>"><i class="fas fa-fw fa-4x fa-download"></i><br><br>Download Uploads</a>
</div> </div>
</div> </div>

View File

@ -6,28 +6,24 @@
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed"); defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_GET['download_database'])) { require_once "includes/app_version.php";
validateCSRFToken($_GET['csrf_token']);
if (isset($_GET['download_backup'])) {
validateCSRFToken($_GET['csrf_token']);
global $mysqli, $database; global $mysqli, $database;
$backupFileName = date('Y-m-d_H-i-s') . '_backup.sql'; $timestamp = date('YmdHis');
$baseName = "itflow_$timestamp";
$sqlFile = "$baseName.sql";
$uploadsZip = "$baseName_uploads.zip";
$finalZip = "$baseName.zip";
$versionFile = "version.txt";
header('Content-Type: application/sql'); // === 1. Generate SQL Dump ===
header('Content-Disposition: attachment; filename="' . $backupFileName . '"'); $sqlContent = "-- UTF-8 + Foreign Key Safe Dump\n";
header('Cache-Control: no-store, no-cache, must-revalidate'); $sqlContent .= "SET NAMES 'utf8mb4';\n";
header('Pragma: no-cache'); $sqlContent .= "SET foreign_key_checks = 0;\n\n";
header('Expires: 0');
if (ob_get_level()) ob_end_clean();
flush();
// Start of dump file — charset declaration
echo "-- UTF-8 + Foreign Key Safe Dump\n";
echo "SET NAMES 'utf8mb4';\n";
echo "SET foreign_key_checks = 0;\n\n";
// Get all tables
$tables = []; $tables = [];
$res = $mysqli->query("SHOW TABLES"); $res = $mysqli->query("SHOW TABLES");
while ($row = $res->fetch_row()) { while ($row = $res->fetch_row()) {
@ -35,59 +31,47 @@ if (isset($_GET['download_database'])) {
} }
foreach ($tables as $table) { foreach ($tables as $table) {
// Table structure
$createRes = $mysqli->query("SHOW CREATE TABLE `$table`"); $createRes = $mysqli->query("SHOW CREATE TABLE `$table`");
$createRow = $createRes->fetch_assoc(); $createRow = $createRes->fetch_assoc();
$createSQL = array_values($createRow)[1]; $createSQL = array_values($createRow)[1];
echo "\n-- ----------------------------\n"; $sqlContent .= "\n-- ----------------------------\n";
echo "-- Table structure for `$table`\n"; $sqlContent .= "-- Table structure for `$table`\n";
echo "-- ----------------------------\n"; $sqlContent .= "-- ----------------------------\n";
echo "DROP TABLE IF EXISTS `$table`;\n"; $sqlContent .= "DROP TABLE IF EXISTS `$table`;\n";
echo $createSQL . ";\n\n"; $sqlContent .= $createSQL . ";\n\n";
// Table data
$dataRes = $mysqli->query("SELECT * FROM `$table`"); $dataRes = $mysqli->query("SELECT * FROM `$table`");
if ($dataRes->num_rows > 0) { if ($dataRes->num_rows > 0) {
echo "-- Dumping data for table `$table`\n"; $sqlContent .= "-- Dumping data for table `$table`\n";
while ($row = $dataRes->fetch_assoc()) { while ($row = $dataRes->fetch_assoc()) {
$columns = array_map(fn($col) => '`' . $mysqli->real_escape_string($col) . '`', array_keys($row)); $columns = array_map(fn($col) => '`' . $mysqli->real_escape_string($col) . '`', array_keys($row));
$values = array_map(function ($val) use ($mysqli) { $values = array_map(function ($val) use ($mysqli) {
if (is_null($val)) return "NULL"; return is_null($val) ? "NULL" : "'" . $mysqli->real_escape_string($val) . "'";
return "'" . $mysqli->real_escape_string($val) . "'";
}, array_values($row)); }, array_values($row));
$sqlContent .= "INSERT INTO `$table` (" . implode(", ", $columns) . ") VALUES (" . implode(", ", $values) . ");\n";
echo "INSERT INTO `$table` (" . implode(", ", $columns) . ") VALUES (" . implode(", ", $values) . ");\n";
} }
echo "\n"; $sqlContent .= "\n";
} }
} }
//FINAL STEP: Re-enable foreign key checks $sqlContent .= "SET foreign_key_checks = 1;\n";
echo "\nSET foreign_key_checks = 1;\n"; file_put_contents($sqlFile, $sqlContent);
logAction("Database", "Download", "$session_name downloaded the database.");
$_SESSION['alert_message'] = "Database downloaded";
exit;
}
if (isset($_GET['download_uploads'])) {
validateCSRFToken($_GET['csrf_token']);
// === 2. Create uploads.zip ===
function zipFolder($folderPath, $zipFilePath) { function zipFolder($folderPath, $zipFilePath) {
$zip = new ZipArchive(); $zip = new ZipArchive();
if ($zip->open($zipFilePath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== TRUE) { if ($zip->open($zipFilePath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== TRUE) {
die("Cannot open <$zipFilePath>\n"); die("Cannot open <$zipFilePath>");
} }
$folderPath = realpath($folderPath); $folderPath = realpath($folderPath);
$files = new RecursiveIteratorIterator( $files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($folderPath), new RecursiveDirectoryIterator($folderPath),
RecursiveIteratorIterator::LEAVES_ONLY RecursiveIteratorIterator::LEAVES_ONLY
); );
foreach ($files as $name => $file) { foreach ($files as $file) {
if (!$file->isDir()) { if (!$file->isDir()) {
$filePath = $file->getRealPath(); $filePath = $file->getRealPath();
$relativePath = substr($filePath, strlen($folderPath) + 1); $relativePath = substr($filePath, strlen($folderPath) + 1);
@ -98,27 +82,54 @@ if (isset($_GET['download_uploads'])) {
$zip->close(); $zip->close();
} }
$uploadDir = 'uploads'; zipFolder("uploads", $uploadsZip);
$zipFile = 'uploads.zip';
zipFolder($uploadDir, $zipFile); // === 3. Generate version.txt ===
$commitHash = trim(shell_exec('git log -1 --format=%H'));
$gitBranch = trim(shell_exec('git rev-parse --abbrev-ref HEAD'));
// Trigger file download $versionContent = "ITFlow Backup Metadata\n";
if (file_exists($zipFile)) { $versionContent .= "-----------------------------\n";
header('Content-Type: application/zip'); $versionContent .= "Generated: " . date('Y-m-d H:i:s') . "\n";
header('Content-Disposition: attachment; filename="' . basename($zipFile) . '"'); $versionContent .= "Backup File: $baseName.zip\n";
header('Content-Length: ' . filesize($zipFile)); $versionContent .= "Generated By: $session_name\n";
flush(); $versionContent .= "Host: " . gethostname() . "\n";
readfile($zipFile); $versionContent .= "Git Branch: $gitBranch\n";
unlink($zipFile); // Optional: delete after download $versionContent .= "Git Commit: $commitHash\n";
exit; $versionContent .= "ITFlow Version: " . (defined('APP_VERSION') ? APP_VERSION : 'Unknown') . "\n";
$versionContent .= "Database Version: " . (defined('CURRENT_DATABASE_VERSION') ? CURRENT_DATABASE_VERSION : 'Unknown') . "\n";
file_put_contents($versionFile, $versionContent);
// === 4. Combine into final .zip file ===
$final = new ZipArchive();
if ($final->open($finalZip, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== TRUE) {
die("Cannot create final backup zip.");
} }
logAction("Uploads", "Download", "$session_name downloaded the uploads folder."); $final->addFile($sqlFile, "db.sql");
$final->addFile($uploadsZip, "uploads.zip");
$final->addFile($versionFile, "version.txt");
$final->close();
// Cleanup temp files before download
unlink($sqlFile);
unlink($uploadsZip);
unlink($versionFile);
// === 5. Serve the zip for download ===
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="' . $finalZip . '"');
header('Content-Length: ' . filesize($finalZip));
flush();
readfile($finalZip);
unlink($finalZip); // remove final zip after serving
logAction("System", "Backup Download", "$session_name downloaded full backup.");
$_SESSION['alert_message'] = "Full backup downloaded.";
exit;
} }
if (isset($_POST['backup_master_key'])) { if (isset($_POST['backup_master_key'])) {
validateCSRFToken($_POST['csrf_token']); validateCSRFToken($_POST['csrf_token']);

140
setup.php
View File

@ -110,10 +110,43 @@ if (isset($_POST['add_database'])) {
if (isset($_POST['restore'])) { if (isset($_POST['restore'])) {
// === 1. Restore SQL Dump === if (!isset($_FILES['backup_zip']) || $_FILES['backup_zip']['error'] !== UPLOAD_ERR_OK) {
if (isset($_FILES["sql_file"])) { die("No backup file uploaded or upload failed.");
}
// Drop all existing tables $file = $_FILES['backup_zip'];
$fileExt = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if ($fileExt !== "zip") {
die("Only .zip files are allowed.");
}
// Save uploaded file temporarily
$backupZip = "restore_" . time() . ".zip";
if (!move_uploaded_file($file["tmp_name"], $backupZip)) {
die("Failed to save uploaded backup file.");
}
$zip = new ZipArchive;
if ($zip->open($backupZip) !== TRUE) {
unlink($backupZip);
die("Failed to open backup zip file.");
}
// Extract to a temp directory
$tempDir = "restore_temp_" . time();
mkdir($tempDir);
if (!$zip->extractTo($tempDir)) {
$zip->close();
unlink($backupZip);
die("Failed to extract backup contents.");
}
$zip->close();
unlink($backupZip);
// === 1. Restore SQL Dump ===
$sqlPath = "$tempDir/db.sql";
if (file_exists($sqlPath)) {
mysqli_query($mysqli, "SET foreign_key_checks = 0"); mysqli_query($mysqli, "SET foreign_key_checks = 0");
$tables = mysqli_query($mysqli, "SHOW TABLES"); $tables = mysqli_query($mysqli, "SHOW TABLES");
while ($row = mysqli_fetch_array($tables)) { while ($row = mysqli_fetch_array($tables)) {
@ -121,88 +154,68 @@ if (isset($_POST['restore'])) {
} }
mysqli_query($mysqli, "SET foreign_key_checks = 1"); mysqli_query($mysqli, "SET foreign_key_checks = 1");
$file = $_FILES["sql_file"];
$filename = $file["name"];
$tempPath = $file["tmp_name"];
$fileExt = pathinfo($filename, PATHINFO_EXTENSION);
if (strtolower($fileExt) !== "sql") {
die("Only .sql files are allowed.");
}
// Save uploaded file temporarily
$destination = "temp_" . time() . ".sql";
if (!move_uploaded_file($tempPath, $destination)) {
die("Failed to upload the SQL file.");
}
$command = sprintf( $command = sprintf(
'mysql -h%s -u%s -p%s %s < %s', 'mysql -h%s -u%s -p%s %s < %s',
escapeshellarg($dbhost), escapeshellarg($dbhost),
escapeshellarg($dbusername), escapeshellarg($dbusername),
escapeshellarg($dbpassword), escapeshellarg($dbpassword),
escapeshellarg($database), escapeshellarg($database),
escapeshellarg($destination) escapeshellarg($sqlPath)
); );
exec($command, $output, $returnCode); exec($command, $output, $returnCode);
unlink($destination); // cleanup
if ($returnCode !== 0) { if ($returnCode !== 0) {
die("SQL import failed. Error code: $returnCode"); die("SQL import failed. Error code: $returnCode");
} }
} else {
die("Missing db.sql in the backup archive.");
} }
// === 2. Restore Upload Folder from ZIP === // === 2. Restore Upload Folder ===
if (isset($_FILES["upload_zip"])) { $uploadDir = __DIR__ . "/uploads/";
$uploadDir = __DIR__ . "/uploads/"; $uploadsZip = "$tempDir/uploads.zip";
$zipFile = $_FILES["upload_zip"]; if (file_exists($uploadsZip)) {
$zipName = basename($zipFile["name"]); $uploads = new ZipArchive;
$zipExt = strtolower(pathinfo($zipName, PATHINFO_EXTENSION)); if ($uploads->open($uploadsZip) === TRUE) {
// Clean existing uploads
if ($zipExt !== "zip") { foreach (glob($uploadDir . '*') as $item) {
die("Only .zip files are allowed for upload restore."); if (is_dir($item)) {
} array_map('unlink', glob("$item/*"));
rmdir($item);
$tempZip = "upload_restore_" . time() . ".zip";
if (!move_uploaded_file($zipFile["tmp_name"], $tempZip)) {
die("Failed to upload the zip file.");
}
$zip = new ZipArchive;
if ($zip->open($tempZip) === TRUE) {
// Clear existing upload folder
foreach (glob($uploadDir . '*') as $file) {
if (is_dir($file)) {
$files = array_diff(scandir($file), array('.', '..'));
foreach ($files as $subfile) {
unlink("$file/$subfile");
}
rmdir($file);
} else { } else {
unlink($file); unlink($item);
} }
} }
// Extract new files $uploads->extractTo($uploadDir);
$zip->extractTo($uploadDir); $uploads->close();
$zip->close();
unlink($tempZip); // cleanup
} else { } else {
unlink($tempZip); die("Failed to open uploads.zip in backup.");
die("Failed to open zip file.");
} }
} else {
die("Missing uploads.zip in the backup archive.");
} }
// === 3. Final Setup Stages === // === 3. Read version.txt (optional log/display)
$versionTxt = "$tempDir/version.txt";
if (file_exists($versionTxt)) {
$versionInfo = file_get_contents($versionTxt);
// You could log it, show it, or ignore it
// e.g. logAction("Backup Restore", "Version Info", $versionInfo);
}
// Cleanup temp restore directory
array_map('unlink', glob("$tempDir/*"));
rmdir($tempDir);
// === 4. Final Setup Stages ===
$myfile = fopen("config.php", "a"); $myfile = fopen("config.php", "a");
$txt = "\$config_enable_setup = 0;\n\n"; $txt = "\$config_enable_setup = 0;\n\n";
fwrite($myfile, $txt); fwrite($myfile, $txt);
fclose($myfile); fclose($myfile);
$_SESSION['alert_message'] = "Database and uploads restored successfully"; $_SESSION['alert_message'] = "Full backup restored successfully.";
// header("Location: login.php"); // header("Location: login.php");
exit; exit;
} }
@ -1029,18 +1042,11 @@ if (isset($_POST['add_telemetry'])) {
</div> </div>
<div class="card-body"> <div class="card-body">
<form method="post" enctype="multipart/form-data"> <form method="post" enctype="multipart/form-data">
<h5>Upload SQL File to Import into DB</h5> <label>Restore ITFlow Backup (.zip)</label>
<input type="file" name="sql_file" accept=".sql" required> <input type="file" name="backup_zip" accept=".zip" required>
<hr> <hr>
<h5>Upload Folder Backup (.zip)</h5>
<input type="file" name="upload_zip" accept=".zip" required>
<hr>
<button type="submit" name="restore" class="btn btn-primary text-bold"> <button type="submit" name="restore" class="btn btn-primary text-bold">
Restore then login<i class="fas fa-fw fa-arrow-circle-right ml-2"></i> Restore Backup<i class="fas fa-fw fa-upload ml-2"></i>
</button> </button>
</form> </form>
</div> </div>