本系統提供完整的資料庫備份與還原管理介面,功能包含:自動備份 (支援分卷)、備份檔案列表、一鍵還原、備份檔案刪除、狀態回饋。採用類別封裝設計,前後端分離,適合整合至後台管理系統。
backup_system/ ├── config/ │ └── config.php # 資料庫設定 ├── includes/ │ ├── dbbackup.class.php # 核心備份邏輯類別 │ └── msg.class.php # 訊息提示類別 ├── export.php # 備份執行頁 ├── import.php # 還原/管理頁 └── style.css # 樣式
PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES $charset", ]; $this->pdo = new PDO($dsn, $user, $pass, $options); $this->backupDir = $backupDir ?? BACKUP_DIR; $this->partSize = $partSize * 1024; // 轉為 bytes // 確保備份目錄存在 if (!is_dir($this->backupDir)) { mkdir($this->backupDir, 0755, true); } } /** * 取得所有資料表名稱 */ public function getTables() { $stmt = $this->pdo->query("SHOW TABLES"); return $stmt->fetchAll(PDO::FETCH_COLUMN); } /** * 產生單一資料表的 SQL */ private function getTableSql($table) { $sql = ""; // DROP TABLE IF EXISTS $sql .= "DROP TABLE IF EXISTS `$table`;\n"; // CREATE TABLE $stmt = $this->pdo->query("SHOW CREATE TABLE `$table`"); $row = $stmt->fetch(); $sql .= $row['Create Table'] . ";\n\n"; // INSERT DATA $stmt = $this->pdo->query("SELECT * FROM `$table`"); $rows = $stmt->fetchAll(); if (!empty($rows)) { $columns = array_keys($rows[0]); $colList = '`' . implode('`,`', $columns) . '`'; $batch = []; foreach ($rows as $row) { $values = []; foreach ($row as $val) { if ($val === null) { $values[] = 'NULL'; } elseif (is_int($val) || is_float($val)) { $values[] = $val; } else { $values[] = $this->pdo->quote($val); } } $batch[] = '(' . implode(',', $values) . ')'; // 分卷檢查 if ($this->partSize > 0 && strlen(implode('', $batch)) > $this->partSize) { $sql .= "INSERT INTO `$table` ($colList) VALUES\n" . implode(",\n", $batch) . ";\n\n"; $batch = []; } } if (!empty($batch)) { $sql .= "INSERT INTO `$table` ($colList) VALUES\n" . implode(",\n", $batch) . ";\n\n"; } } return $sql; } /** * 執行備份 * @param array|null $tables 指定資料表,null 為全部 * @return string|false 備份檔案路徑或陣列(分卷),失敗回傳 false */ public function backup($tables = null) { $tables = $tables ?? $this->getTables(); $timestamp = date('Ymd_His'); $baseName = "backup_{$timestamp}"; if ($this->partSize === 0) { // 單一檔案 $filePath = $this->backupDir . $baseName . '.sql'; $sql = ""; foreach ($tables as $table) { $sql .= $this->getTableSql($table); } if (file_put_contents($filePath, $sql) === false) { return false; } return $filePath; } else { // 分卷備份 $files = []; $partNum = 1; $currentSql = ""; $currentSize = 0; foreach ($tables as $table) { $tableSql = $this->getTableSql($table); $tableSize = strlen($tableSql); // 若單表超過分卷大小,強制單獨成檔 if ($tableSize > $this->partSize) { if ($currentSize > 0) { $filePath = $this->backupDir . $baseName . "_part{$partNum}.sql"; file_put_contents($filePath, $currentSql); $files[] = $filePath; $partNum++; $currentSql = ""; $currentSize = 0; } $filePath = $this->backupDir . $baseName . "_part{$partNum}.sql"; file_put_contents($filePath, $tableSql); $files[] = $filePath; $partNum++; continue; } // 累加至當前分卷 if ($currentSize + $tableSize > $this->partSize && $currentSize > 0) { $filePath = $this->backupDir . $baseName . "_part{$partNum}.sql"; file_put_contents($filePath, $currentSql); $files[] = $filePath; $partNum++; $currentSql = ""; $currentSize = 0; } $currentSql .= $tableSql; $currentSize += $tableSize; } // 寫入最後一卷 if ($currentSize > 0) { $filePath = $this->backupDir . $baseName . "_part{$partNum}.sql"; file_put_contents($filePath, $currentSql); $files[] = $filePath; } return $files; } } /** * 取得備份檔案列表 * @return array 檔案資訊陣列 [filename, size, mtime, parts] */ public function getBackupList() { $files = glob($this->backupDir . 'backup_*.sql'); $list = []; // 依時間排序 (新到舊) usort($files, function($a, $b) { return filemtime($b) - filemtime($a); }); $grouped = []; foreach ($files as $file) { $basename = basename($file); // 匹配 backup_YYYYmmdd_His[_partN].sql if (preg_match('/^backup_(\d{8}_\d{6})(?:_part(\d+))?\.sql$/', $basename, $m)) { $key = $m[1]; // 同一批次的時間戳 $part = $m[2] ?? 1; $grouped[$key]['timestamp'] = $key; $grouped[$key]['parts'][$part] = [ 'file' => $basename, 'path' => $file, 'size' => filesize($file), 'mtime' => filemtime($file), ]; $grouped[$key]['total_size'] = ($grouped[$key]['total_size'] ?? 0) + filesize($file); } } // 整理輸出格式 foreach ($grouped as $g) { ksort($g['parts']); $list[] = [ 'timestamp' => $g['timestamp'], 'parts' => array_values($g['parts']), 'total_size' => $g['total_size'], 'part_count' => count($g['parts']), ]; } return $list; } /** * 還原資料庫 (支援分卷) * @param string $filename 單一檔案或主檔案 (會自動尋找分卷) * @return bool */ public function restore($filename) { $filePath = $this->backupDir . $filename; if (!is_file($filePath)) return false; // 判斷是否為分卷備份 $isPart = preg_match('/_part(\d+)\.sql$/', $filename); $files = [$filePath]; if ($isPart) { // 尋找同批次所有分卷 $baseName = preg_replace('/_part\d+\.sql$/', '', $filename); $partFiles = glob($this->backupDir . $baseName . '_part*.sql'); // 依 part 號排序 usort($partFiles, function($a, $b) { preg_match('/_part(\d+)\.sql$/', $a, $ma); preg_match('/_part(\d+)\.sql$/', $b, $mb); return ($ma[1] ?? 0) - ($mb[1] ?? 0); }); $files = $partFiles; } else { // 檢查是否有分卷檔案 $partFiles = glob($this->backupDir . $filename . '_part*.sql'); if (!empty($partFiles)) { usort($partFiles, function($a, $b) { preg_match('/_part(\d+)\.sql$/', $a, $ma); preg_match('/_part(\d+)\.sql$/', $b, $mb); return ($ma[1] ?? 0) - ($mb[1] ?? 0); }); $files = array_merge([$filePath], $partFiles); } } // 執行還原 (使用交易確保一致性) try { $this->pdo->beginTransaction(); foreach ($files as $file) { $sql = file_get_contents($file); if ($sql === false) throw new Exception("無法讀取檔案: $file"); // 分割 SQL 語句 (簡易版,以 ;\n 為界) $statements = preg_split("/;\s*\n/", $sql); foreach ($statements as $stmt) { $stmt = trim($stmt); if ($stmt === '' || strpos($stmt, '--') === 0) continue; $this->pdo->exec($stmt); } } $this->pdo->commit(); return true; } catch (Exception $e) { $this->pdo->rollBack(); error_log("還原失敗: " . $e->getMessage()); return false; } } /** * 刪除備份 (支援分卷批次刪除) * @param string|array $filenames 檔案名稱或陣列 * @return bool */ public function delete($filenames) { $filenames = (array)$filenames; $success = true; foreach ($filenames as $filename) { $filePath = $this->backupDir . $filename; if (is_file($filePath)) { if (!@unlink($filePath)) $success = false; } // 若為主檔案,同時刪除分卷 if (!preg_match('/_part\d+\.sql$/', $filename)) { $partFiles = glob($this->backupDir . $filename . '_part*.sql'); foreach ($partFiles as $pf) @unlink($pf); } } return $success; } /** * 清理舊備份 (保留最新 N 份) */ public function cleanup($keepCount = BACKUP_KEEP_COUNT) { $list = $this->getBackupList(); if (count($list) > $keepCount) { $toDelete = array_slice($list, $keepCount); foreach ($toDelete as $backup) { $files = array_column($backup['parts'], 'file'); $this->delete($files); } } } }
"; echo "系統訊息 "; echo ""; echo ""; echo ""; } }" . (strpos($message, '失敗') !== false ? '❌' : '✅') . ""; echo "" . htmlspecialchars($message) . "
"; if ($redirectUrl) { echo "系統將在 $autoRedirect 秒後自動跳轉...
"; echo ""; echo ""; } echo "
backup($tables); if ($result) { $msg->show('備份完成!', 'import.php'); } else { $msg->show('備份失敗,請檢查目錄權限', 'export.php'); } exit; } // 取得資料表列表供選擇 $tables = $backup->getTables(); ?>資料庫備份 📦 資料庫備份
備份設定
備份說明
- 備份檔案儲存於:
= BACKUP_DIR ?>- 分卷大小: = BACKUP_PART_SIZE > 0 ? BACKUP_PART_SIZE . ' KB' : '不分卷 (單一檔案)' ?>
- 檔案命名格式:
backup_YYYYmmdd_HHMMSS[_partN].sql- 備份包含: DROP TABLE、CREATE TABLE、INSERT DATA
- 還原時會先 DROP 現有表,請務必確認
getBackupList(); // 還原動作 if (isset($_GET['restore'])) { $filename = basename($_GET['restore']); // 安全性:只取檔名 if ($backup->restore($filename)) { $msg->show("備份「$filename」還原成功!", 'import.php'); } else { $msg->show("還原失敗,請檢查檔案完整性", 'import.php'); } exit; } // 刪除動作 if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete'])) { $files = $_POST['files'] ?? []; if (!empty($files)) { if ($backup->delete($files)) { $msg->show('選取的備份檔案已刪除', 'import.php'); } else { $msg->show('刪除失敗', 'import.php'); } } exit; } ?>備份管理與還原
/* style.css */ * { box-sizing: border-box; } body { font-family: 'Microsoft JhengHei', sans-serif; background: #f0f2f5; margin: 0; padding: 20px; } .container { max-width: 1000px; margin: 0 auto; } h1 { color: #2c3e50; margin-bottom: 30px; } .card { background: white; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); padding: 25px; margin-bottom: 20px; } .card.empty { text-align: center; color: #999; padding: 60px; } h2 { margin-top: 0; color: #34495e; border-bottom: 1px solid #eee; padding-bottom: 10px; margin-bottom: 20px; } .form-group { margin-bottom: 20px; } label { display: block; font-weight: 500; margin-bottom: 8px; } .checkbox-group { display: flex; flex-wrap: wrap; gap: 15px; } .checkbox-item { display: flex; align-items: center; gap: 5px; cursor: pointer; } .btn { display: inline-block; padding: 10px 20px; border-radius: 4px; text-decoration: none; font-size: 14px; cursor: pointer; border: none; margin-right: 10px; transition: all 0.2s; } .btn.small { padding: 6px 12px; font-size: 12px; } .btn.primary { background: #3498db; color: white; } .btn.primary:hover { background: #2980b9; } .btn.danger { background: #e74c3c; color: white; } .btn.danger:hover { background: #c0392b; } .btn { background: #95a5a6; color: white; } .btn:hover { background: #7f8c8d; } .data-table { width: 100%; border-collapse: collapse; } .data-table th, .data-table td { padding: 12px 15px; text-align: left; border-bottom: 1px solid #eee; } .data-table th { background: #f8f9fa; font-weight: 600; } .data-table tr:hover td { background: #f8f9fa; } .badge { background: #f39c12; color: white; padding: 2px 8px; border-radius: 3px; font-size: 11px; } .batch-actions { margin-top: 20px; padding-top: 20px; border-top: 1px solid #eee; display: flex; gap: 10px; } code { background: #f8f9fa; padding: 2px 6px; border-radius: 3px; font-family: monospace; }
| 風險 | 防護措施 |
|---|---|
| 任意檔案讀取/刪除 | 使用 basename() 限制檔名、檢查檔案是否在備份目錄下 |
| SQL 注入 (還原時) | 備份檔案為系統產生,但執行前可驗證 SQL 語句結構 |
| 大檔案記憶體耗盡 | 分卷備份、還原時逐語句執行而非一次載入 |
| 還原覆蓋資料 | 介面強制確認、建議先自動備份當前狀態再還原 |
| 權限控管 | 整合後台 RBAC,僅管理員可存取 |
# 每天凌晨 2:00 自動備份 0 2 * * * /usr/bin/php /path/to/backup_system/auto_backup.php >> /var/log/db_backup.log 2>&1 # auto_backup.php 內容 backup(); $backup->cleanup(BACKUP_KEEP_COUNT); echo date('Y-m-d H:i:s') . " Backup " . ($result ? "OK" : "FAILED") . "\n";