| 層級 | 內容 | 頻率 | 保留 | 存儲 |
|---|---|---|---|---|
| L1: 代碼/配置 | 文件 (public_html, .env, config) | 每日 | 30 天 | 異地 FTP/S3 |
| L2: 數據庫 | 完整 SQL Dump | 每日/每 6 小時 | 30 天 | 異地 FTP/S3 |
| L3: 增量 | Git 提交 / rsync | 每小時 | 7 天 | 本地/異地 |
| L4: 全量鏡像 | 服務器快照/鏡像 | 每週 | 4 週 | 雲廠商 |
調用 cPanel UAPI 執行完整備份(含文件、數據庫、郵件、DNS),並通過 FTP/SFTP 推送到遠程存儲。
<?php
/**
* cPanel 完整備份自動化
* 需:cPanel 帳號、API Token(WHM 生成)或密碼
* 將腳本放在 public_html 外部,設置 600 權限
*/
class CpanelBackup
{
private string $host;
private string $user;
private string $token; // 或 password
private int $port = 2083; // SSL
private string $apiBase;
public function __construct(string $host, string $user, string $token, bool $useSsl = true)
{
$this->host = rtrim($host, '/');
$this->user = $user;
$this->token = $token;
$this->port = $useSsl ? 2083 : 2082;
$this->apiBase = ($useSsl ? 'https' : 'http') . "://{$this->host}:{$this->port}/execute";
}
/**
* 創建完整備份並可選推送到遠程 FTP
*/
public function createFullBackup(array $options = []): array
{
$params = [
'destination' => $options['destination'] ?? 'home', // home, ftp, scp, s3
'email' => $options['email'] ?? '', // 完成通知郵箱
];
// FTP 遠程存儲參數
if (($params['destination'] ?? '') === 'ftp') {
$params += [
'server' => $options['ftp_host'] ?? '',
'user' => $options['ftp_user'] ?? '',
'pass' => $options['ftp_pass'] ?? '',
'port' => $options['ftp_port'] ?? 21,
'dir' => $options['ftp_dir'] ?? '/',
'passive' => $options['ftp_passive'] ?? 1,
];
}
return $this->uapi('Backup', 'fullbackup', $params);
}
/**
* 下載已生成的備份文件
*/
public function listBackups(): array
{
return $this->uapi('Fileman', 'list_files', ['dir' => '/backup']);
}
private function uapi(string $module, string $function, array $params = []): array
{
$url = "{$this->apiBase}/{$module}/{$function}";
$query = http_build_query($params);
$ch = curl_init("{$url}?{$query}");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: cpanel {$this->user}:{$this->token}",
'Accept: application/json',
],
CURLOPT_SSL_VERIFYPEER => false, // 自簽證書時
CURLOPT_TIMEOUT => 300, // 備份可能耗時
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
throw new RuntimeException("cPanel API 失敗 [$httpCode]: $response");
}
$data = json_decode($response, true);
if (($data['metadata']['result'] ?? 0) !== 1) {
throw new RuntimeException("cPanel API 錯誤: " . ($data['metadata']['reason'] ?? '未知'));
}
return $data['data'] ?? [];
}
}
// === 使用示例 ===
$backup = new CpanelBackup('cpanel.yourdomain.com', 'cpanel_user', 'API_TOKEN_FROM_WHM');
try {
$result = $backup->createFullBackup([
'destination' => 'ftp',
'email' => 'admin@example.com',
'ftp_host' => 'backup.storage.com',
'ftp_user' => 'backup_user',
'ftp_pass' => 'strong_password',
'ftp_dir' => '/backups/cpanel/',
]);
echo "備份任務已提交:\n";
print_r($result);
} catch (Exception $e) {
error_log("cPanel 備份失敗: " . $e->getMessage());
// 發送告警...
}
<?php
/**
* 通用網站備份腳本
* 功能:打包文件、導出數據庫、加密、上傳 FTP/S3、清理舊備份
* 部署:放在 /home/user/backup/backup.php,chmod 600
* 定時:crontab -e → 0 2 * * * /usr/bin/php /home/user/backup/backup.php >> /home/user/backup/log.txt 2>&1
*/
class SiteBackup
{
private array $config;
public function __construct(array $config)
{
$this->config = array_merge([
'site_path' => $_SERVER['DOCUMENT_ROOT'] ?? '/var/www/html',
'exclude' => ['.git', 'node_modules', 'vendor', 'storage/logs', '*.log', 'cache/*'],
'db_host' => '127.0.0.1',
'db_port' => 3306,
'db_name' => '',
'db_user' => '',
'db_pass' => '',
'db_tables' => '*', // 或 ['users', 'orders']
'storage' => 'ftp', // ftp, s3, local
'ftp_host' => '',
'ftp_user' => '',
'ftp_pass' => '',
'ftp_dir' => '/backups/',
's3_key' => '',
's3_secret' => '',
's3_region' => 'us-east-1',
's3_bucket' => '',
'encrypt_key' => '', // OpenSSL AES-256-CBC 加密密鑰(32字節)
'keep_days' => 30,
'temp_dir' => sys_get_temp_dir() . '/site_backup_' . getmypid(),
], $config);
}
public function run(): void
{
$this->prepareTempDir();
$date = date('Y-m-d_H-i-s');
$baseName = "backup_{$date}";
$archive = "{$this->config['temp_dir']}/{$baseName}.tar.gz";
$encrypted = $archive . '.enc';
// 1. 打包文件
$this->createArchive($archive);
// 2. 導出數據庫
$sqlFile = "{$this->config['temp_dir']}/{$baseName}.sql";
$this->dumpDatabase($sqlFile);
// 將 SQL 加入壓縮包
$this->appendToArchive($archive, $sqlFile, $baseName . '.sql');
@unlink($sqlFile);
// 3. 加密(可選)
if ($this->config['encrypt_key']) {
$this->encryptFile($archive, $encrypted);
@unlink($archive);
$finalFile = $encrypted;
} else {
$finalFile = $archive;
}
// 4. 上傳
$remotePath = $this->config['ftp_dir'] . basename($finalFile);
$this->upload($finalFile, $remotePath);
// 5. 清理本地臨時文件
@unlink($finalFile);
$this->cleanOldBackups();
echo "備份完成:$remotePath\n";
}
private function prepareTempDir(): void
{
if (!is_dir($this->config['temp_dir'])) {
mkdir($this->config['temp_dir'], 0700, true);
}
}
private function createArchive(string $archive): void
{
$excludes = implode(' ', array_map(fn($p) => "--exclude='$p'", $this->config['exclude']));
$cmd = "tar -czf " . escapeshellarg($archive) . " $excludes -C " . escapeshellarg($this->config['site_path']) . " .";
$this->exec($cmd, "打包文件失敗");
}
private function appendToArchive(string $archive, string $file, string $nameInArchive): void
{
$cmd = "tar -rf " . escapeshellarg($archive) . " -C " . escapeshellarg(dirname($file)) . " " . escapeshellarg(basename($file));
$this->exec($cmd, "添加 SQL 到壓縮包失敗");
// 重新壓縮(tar -rf 不壓縮,簡化:直接重新打包含 SQL)
// 實際建議:先打包文件,再打包 SQL,最後合並或分開存儲
}
private function dumpDatabase(string $outputFile): void
{
$db = $this->config;
$tables = $db['db_tables'] === '*' ? '' : implode(' ', $db['db_tables']);
$cmd = "mysqldump -h{$db['db_host']} -P{$db['db_port']} -u{$db['db_user']} -p" . escapeshellarg($db['db_pass']) . " --single-transaction --quick --lock-tables=false $db[db_name] $tables > " . escapeshellarg($outputFile);
$this->exec($cmd, "數據庫導出失敗");
}
private function encryptFile(string $input, string $output): void
{
$key = $this->config['encrypt_key'];
if (strlen($key) !== 32) throw new Exception('加密密鑰必須 32 字節');
$iv = random_bytes(16);
$cmd = "openssl enc -aes-256-cbc -K " . bin2hex($key) . " -iv " . bin2hex($iv) . " -in " . escapeshellarg($input) . " -out " . escapeshellarg($output);
$this->exec($cmd, "加密失敗");
// 將 IV 存儲在文件名或單獨文件中,解密時需要
file_put_contents($output . '.iv', $iv);
}
private function upload(string $localFile, string $remotePath): void
{
switch ($this->config['storage']) {
case 'ftp':
$this->uploadFtp($localFile, $remotePath);
break;
case 's3':
$this->uploadS3($localFile, $remotePath);
break;
default:
copy($localFile, $this->config['temp_dir'] . '/' . basename($remotePath));
}
}
private function uploadFtp(string $local, string $remote): void
{
$conn = ftp_ssl_connect($this->config['ftp_host']) ?: ftp_connect($this->config['ftp_host']);
if (!$conn) throw new Exception("FTP 連接失敗");
if (!ftp_login($conn, $this->config['ftp_user'], $this->config['ftp_pass'])) {
throw new Exception("FTP 登錄失敗");
}
ftp_pasv($conn, true);
// 確保遠程目錄存在
$this->ftpMkdirRecursive($conn, dirname($remote));
if (!ftp_put($conn, $remote, $local, FTP_BINARY)) {
throw new Exception("FTP 上傳失敗");
}
ftp_close($conn);
}
private function ftpMkdirRecursive($conn, string $path): void
{
$parts = explode('/', trim($path, '/'));
$current = '';
foreach ($parts as $part) {
$current .= '/' . $part;
if (!@ftp_chdir($conn, $current)) {
ftp_mkdir($conn, $current);
ftp_chdir($conn, $current);
}
}
}
private function uploadS3(string $local, string $remote): void
{
// 需 aws/aws-sdk-php
// $s3 = new Aws\S3\S3Client([...]);
// $s3->putObject(['Bucket' => $this->config['s3_bucket'], 'Key' => $remote, 'SourceFile' => $local]);
}
private function cleanOldBackups(): void
{
// 列出遠程文件並刪除超過 keep_days 的
// 這裡簡化:實際需根據存儲類型實現
}
private function exec(string $cmd, string $errorMsg): void
{
$output = [];
$code = 0;
exec($cmd, $output, $code);
if ($code !== 0) {
throw new Exception("$errorMsg: $cmd\n" . implode("\n", $output));
}
}
}
// === 配置文件(單獨存放,如 /home/user/backup/config.php,chmod 600) ===
/*
return [
'site_path' => '/home/user/public_html',
'db_name' => 'wp_db',
'db_user' => 'wp_user',
'db_pass' => 'strong_db_pass',
'storage' => 'ftp',
'ftp_host' => 'backup.example.com',
'ftp_user' => 'backup_user',
'ftp_pass' => 'strong_ftp_pass',
'ftp_dir' => '/backups/site1/',
'encrypt_key' => '32_bytes_random_key_here_12345678', // openssl_random_pseudo_bytes(32)
'keep_days' => 30,
];
*/
// === 入口文件 backup.php ===
/*
require __DIR__ . '/config.php';
$backup = new SiteBackup($config);
try {
$backup->run();
} catch (Exception $e) {
error_log("備份失敗: " . $e->getMessage());
// 發送郵件/釘釘/企微告警
mail('admin@example.com', '網站備份失敗', $e->getMessage());
exit(1);
}
*/
#!/bin/bash
# backup_rsync.sh - 放在備份服務器上運行,拉取源服務器
# 原理:rsync --link-dest 指向上一次備份,未變文件創建硬鏈接,極大節省空間
SOURCE="user@source-server:/home/user/public_html/"
DEST="/backups/source-server"
DATE=$(date +%Y-%m-%d_%H-%M-%S)
LATEST="$DEST/latest"
CURRENT="$DEST/$DATE"
mkdir -p "$CURRENT"
rsync -avz --delete \
--link-dest="$LATEST" \
--exclude={'.git','node_modules','vendor','storage/logs','*.log','cache/*'} \
"$SOURCE" "$CURRENT/"
# 更新 latest 指向
rm -f "$LATEST"
ln -s "$CURRENT" "$LATEST"
# 清理 30 天前備份
find "$DEST" -maxdepth 1 -type d -mtime +30 -name '20*' -exec rm -rf {} \;
# 同步數據庫(單獨腳本 mysqldump + gzip)
ssh user@source-server "mysqldump -u db_user -p'pass' db_name | gzip" > "$CURRENT/db.sql.gz"
# crontab -e (以站點用戶身份運行,非 root)
# 方案二:每天凌晨 2 點備份
0 2 * * * /usr/bin/php /home/user/backup/backup.php >> /home/user/backup/backup.log 2>&1
# 方案三:備份服務器每天凌晨 3 點拉取
0 3 * * * /backups/scripts/backup_rsync.sh >> /backups/logs/rsync.log 2>&1
# 數據庫單獨高頻備份(每 6 小時)
0 */6 * * * /usr/bin/php /home/user/backup/dump_db.php >> /home/user/backup/db.log 2>&1
<?php
// restore.php - 緊急恢複用
// 1. 下載備份文件
// 2. 解密:openssl enc -d -aes-256-cbc -K $KEY -iv $IV -in backup.tar.gz.enc -out backup.tar.gz
// 3. 解壓:tar -xzf backup.tar.gz -C /target/path
// 4. 導入數據庫:mysql -u user -p db < db.sql
// 5. 修復權限、清理緩存、測試訪問