首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP MySQL 資料庫備份與還原系統完整實作 (含分卷、刪除管理)

     編輯:PHP 相關知識

PHP MySQL 資料庫備份還原管理系統完整教學

本系統提供完整的資料庫備份與還原管理介面,功能包含:自動備份 (支援分卷)、備份檔案列表、一鍵還原、備份檔案刪除、狀態回饋。採用類別封裝設計,前後端分離,適合整合至後台管理系統。

系統架構

backup_system/
├── config/
│   └── config.php           # 資料庫設定
├── includes/
│   ├── dbbackup.class.php   # 核心備份邏輯類別
│   └── msg.class.php        # 訊息提示類別
├── export.php               # 備份執行頁
├── import.php               # 還原/管理頁
└── style.css                # 樣式

1. 設定檔 (config/config.php)

2. 核心備份類別 (includes/dbbackup.class.php)

 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);
            }
        }
    }
}

3. 訊息提示類別 (includes/msg.class.php)

";
        echo "系統訊息";
        echo "";
        echo "
"; echo "
" . (strpos($message, '失敗') !== false ? '❌' : '✅') . "
"; echo "

" . htmlspecialchars($message) . "

"; if ($redirectUrl) { echo "

系統將在 $autoRedirect 秒後自動跳轉...

"; echo "

若未跳轉請點這裡

"; echo ""; } echo "
"; } }

4. 備份執行頁 (export.php)

backup($tables);
    
    if ($result) {
        $msg->show('備份完成!', 'import.php');
    } else {
        $msg->show('備份失敗,請檢查目錄權限', 'export.php');
    }
    exit;
}

// 取得資料表列表供選擇
$tables = $backup->getTables();
?>



    
    資料庫備份
    


📦 資料庫備份

備份設定

管理備份檔案

備份說明

  • 備份檔案儲存於:
  • 分卷大小: 0 ? BACKUP_PART_SIZE . ' KB' : '不分卷 (單一檔案)' ?>
  • 檔案命名格式: backup_YYYYmmdd_HHMMSS[_partN].sql
  • 備份包含: DROP TABLE、CREATE TABLE、INSERT DATA
  • 還原時會先 DROP 現有表,請務必確認

5. 還原管理頁 (import.php)

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;
}
?>



    
    備份管理與還原
    


📂 備份管理與還原

建立新備份

目前沒有備份檔案
備份時間 分卷數 總大小 操作
個檔案 還原 1): ?> 分卷備份

6. 樣式表 (style.css)

/* 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,僅管理員可存取

自動化備份 (Cron 排程)

# 每天凌晨 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";
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved