<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>文件备份系统 - 同步版本</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }
.container { max-width: 1200px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
h1 { color: #333; border-bottom: 2px solid #0073aa; padding-bottom: 10px; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input[type="text"], input[type="url"], textarea {
width: 100%;
padding: 8px;
box-sizing: border-box;
border: 1px solid #ddd;
border-radius: 4px;
}
.btn {
background: #0073aa;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
margin-right: 10px;
}
.btn:hover { background: #005a87; }
.btn-danger { background: #dc3545; }
.btn-danger:hover { background: #c82333; }
.btn-success { background: #28a745; }
.btn-success:hover { background: #218838; }
.status-panel {
margin-top: 20px;
padding: 15px;
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
}
.progress-container { margin: 20px 0; }
.progress-bar {
width: 100%;
height: 30px;
background: #e9ecef;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: #28a745;
width: 0%;
transition: width 0.3s;
text-align: center;
line-height: 30px;
color: white;
font-weight: bold;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px;
margin-top: 15px;
}
.stat-box {
padding: 10px;
background: white;
border: 1px solid #ddd;
border-radius: 4px;
text-align: center;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #0073aa;
}
.stat-label {
font-size: 12px;
color: #666;
margin-top: 5px;
}
.log-panel {
margin-top: 20px;
padding: 15px;
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
max-height: 300px;
overflow-y: auto;
font-family: monospace;
font-size: 12px;
}
.log-entry { margin-bottom: 5px; padding: 3px; }
.log-time { color: #666; }
.log-info { color: #0073aa; }
.log-success { color: #28a745; }
.log-error { color: #dc3545; }
.log-warning { color: #ffc107; }
.tab-container { margin-top: 20px; }
.tab-header { display: flex; border-bottom: 1px solid #ddd; }
.tab { padding: 10px 20px; cursor: pointer; }
.tab.active { background: #0073aa; color: white; border-bottom: 2px solid #005a87; }
.tab-content { display: none; padding: 20px 0; }
.tab-content.active { display: block; }
.config-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.file-list { max-height: 200px; overflow-y: auto; }
.current-file {
padding: 10px;
background: #e7f3ff;
border-left: 4px solid #0073aa;
margin: 10px 0;
}
</style>
</head>
<body>
<div class="container">
<h1>📁 文件备份系统 - 同步版本</h1>
<div class="tab-container">
<div class="tab-header">
<div class="tab active" onclick="switchTab('backup')">备份管理</div>
<div class="tab" onclick="switchTab('config')">排除配置</div>
<div class="tab" onclick="switchTab('history')">备份历史</div>
</div>
<!-- 备份管理标签页 -->
<div id="backup-tab" class="tab-content active">
<div class="form-group">
<label for="api_url">备份API URL:</label>
<input type="url" id="api_url" name="api_url"
value="http://your-domain.com/backup_receiver.php"
placeholder="http://your-domain.com/backup_receiver.php" required>
</div>
<div class="form-group">
<label for="backup_dir">备份目录 (可选,默认当前目录):</label>
<input type="text" id="backup_dir" name="backup_dir"
value="/mnt/sda1/www/TVbox"
placeholder="/mnt/sda1/www/TVbox">
</div>
<button class="btn btn-success" onclick="initBackup()">初始化备份任务</button>
<button class="btn" onclick="startBackup()" id="startBtn">开始备份</button>
<button class="btn btn-danger" onclick="resetBackup()">重置任务</button>
<button class="btn" onclick="pauseBackup()" id="pauseBtn">暂停</button>
<div class="status-panel">
<h3>备份状态</h3>
<div id="backupStatus">
<p>等待开始备份...</p>
</div>
<div class="progress-container">
<div class="progress-bar">
<div class="progress-fill" id="progressFill">0%</div>
</div>
</div>
<div id="currentFile" class="current-file"></div>
<div class="stats-grid" id="statsGrid">
<div class="stat-box">
<div class="stat-value" id="statTotal">0</div>
<div class="stat-label">总文件数</div>
</div>
<div class="stat-box">
<div class="stat-value" id="statPending">0</div>
<div class="stat-label">待备份</div>
</div>
<div class="stat-box">
<div class="stat-value" id="statCompleted">0</div>
<div class="stat-label">已备份</div>
</div>
<div class="stat-box">
<div class="stat-value" id="statFailed">0</div>
<div class="stat-label">失败数</div>
</div>
<div class="stat-box">
<div class="stat-value" id="statExcluded">0</div>
<div class="stat-label">已排除</div>
</div>
<div class="stat-box">
<div class="stat-value" id="statSize">0 B</div>
<div class="stat-label">总大小</div>
</div>
</div>
<div class="log-panel" id="logPanel"></div>
</div>
</div>
<!-- 排除配置标签页 -->
<div id="config-tab" class="tab-content">
<div class="config-grid">
<div>
<h3>排除目录</h3>
<textarea id="exclude_dirs" rows="10" placeholder="每行一个目录 例如: .git node_modules vendor"></textarea>
</div>
<div>
<h3>排除文件</h3>
<textarea id="exclude_files" rows="5" placeholder="每行一个文件名 例如: config.php backup.php"></textarea>
</div>
<div>
<h3>排除扩展名</h3>
<textarea id="exclude_exts" rows="5" placeholder="每行一个扩展名 例如: .log .tmp .cache"></textarea>
</div>
<div>
<h3>说明</h3>
<p>排除规则说明:</p>
<ul>
<li>排除目录:不扫描指定目录下的任何文件</li>
<li>排除文件:完全匹配文件名(包含路径)</li>
<li>排除扩展名:匹配文件扩展名</li>
</ul>
<button class="btn btn-success" onclick="saveConfig()">保存配置</button>
</div>
</div>
</div>
<!-- 备份历史标签页 -->
<div id="history-tab" class="tab-content">
<h3>备份历史记录</h3>
<div id="historyList"></div>
</div>
</div>
</div>
<script>
let backupInterval = null;
let isBackupRunning = false;
let isPaused = false;
function switchTab(tabName) {
// 隐藏所有标签页
document.querySelectorAll('.tab-content').forEach(tab => {
tab.classList.remove('active');
});
document.querySelectorAll('.tab').forEach(tab => {
tab.classList.remove('active');
});
// 显示选中的标签页
document.getElementById(tabName + '-tab').classList.add('active');
event.target.classList.add('active');
// 如果是历史标签页,加载历史记录
if (tabName === 'history') {
loadHistory();
}
}
function initBackup() {
const apiUrl = document.getElementById('api_url').value;
const backupDir = document.getElementById('backup_dir').value || '.';
if (!apiUrl) {
alert('请输入API URL');
return;
}
addLog('正在初始化备份任务...', 'info');
fetch('', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest'
},
body: `action=init&api_url=${encodeURIComponent(apiUrl)}&backup_dir=${encodeURIComponent(backupDir)}`
})
.then(response => response.json())
.then(data => {
if (data.status) {
updateStatus(data.status);
addLog('备份任务初始化完成', 'success');
addLog(`检测到 ${data.status.stats.excluded} 个被排除文件`, 'warning');
addLog(`检测到 ${data.status.stats.total} 个待备份文件`, 'info');
addLog('系统就绪,可以开始备份', 'success');
} else {
addLog('初始化失败', 'error');
}
})
.catch(error => {
addLog('初始化失败: ' + error, 'error');
});
}
function startBackup() {
if (isBackupRunning) return;
const apiUrl = document.getElementById('api_url').value;
if (!apiUrl) {
alert('请先设置API URL');
return;
}
isBackupRunning = true;
document.getElementById('startBtn').disabled = true;
document.getElementById('pauseBtn').textContent = '暂停';
addLog('开始同步备份...', 'info');
backupInterval = setInterval(processNextFile, 1000); // 每秒处理一个文件
}
function pauseBackup() {
if (!isBackupRunning) return;
if (isPaused) {
// 恢复备份
isPaused = false;
document.getElementById('pauseBtn').textContent = '暂停';
addLog('恢复备份', 'info');
} else {
// 暂停备份
isPaused = true;
document.getElementById('pauseBtn').textContent = '继续';
addLog('备份已暂停', 'warning');
}
}
function resetBackup() {
if (isBackupRunning) {
clearInterval(backupInterval);
isBackupRunning = false;
}
const apiUrl = document.getElementById('api_url').value;
fetch('', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest'
},
body: `action=reset&api_url=${encodeURIComponent(apiUrl)}`
})
.then(response => response.json())
.then(data => {
addLog('备份任务已重置', 'info');
document.getElementById('backupStatus').innerHTML = '<p>等待开始备份...</p>';
document.getElementById('progressFill').style.width = '0%';
document.getElementById('progressFill').textContent = '0%';
resetStats();
});
}
function processNextFile() {
if (isPaused) return;
const apiUrl = document.getElementById('api_url').value;
fetch('', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest'
},
body: `action=next&api_url=${encodeURIComponent(apiUrl)}`
})
.then(response => response.json())
.then(data => {
if (data.error) {
addLog('错误: ' + data.error, 'error');
return;
}
if (data.completed) {
clearInterval(backupInterval);
isBackupRunning = false;
document.getElementById('startBtn').disabled = false;
addLog(data.message, 'success');
return;
}
// 更新当前文件
const currentFileDiv = document.getElementById('currentFile');
currentFileDiv.innerHTML = `
<strong>正在备份:</strong> ${data.current_file}<br>
<small>大小: ${data.current_size}</small>
`;
// 更新进度
document.getElementById('progressFill').style.width = data.progress + '%';
document.getElementById('progressFill').textContent = data.progress.toFixed(1) + '%';
// 更新统计
if (data.stats) {
updateStats(data.stats);
}
// 记录日志
if (data.result && data.result.success) {
addLog(`已备份: ${data.current_file}`, 'success');
} else if (data.result) {
addLog(`备份失败: ${data.current_file} (${data.result.error || '未知错误'})`, 'error');
}
})
.catch(error => {
addLog('请求失败: ' + error, 'error');
});
}
function updateStatus(status) {
const statusDiv = document.getElementById('backupStatus');
statusDiv.innerHTML = `
<p><strong>备份目录:</strong> ${status.scan_dir}</p>
<p><strong>备份ID:</strong> ${status.backup_id}</p>
<p><strong>开始时间:</strong> ${status.start_time}</p>
<p><strong>最后更新:</strong> ${status.last_update}</p>
`;
// 更新进度
document.getElementById('progressFill').style.width = status.progress + '%';
document.getElementById('progressFill').textContent = status.progress.toFixed(1) + '%';
// 更新统计
updateStats(status.stats);
// 显示排除文件
if (status.excluded_files && status.excluded_files.length > 0) {
addLog('被排除的文件:', 'warning');
status.excluded_files.forEach(file => {
addLog(` - ${file}`, 'info');
});
}
}
function updateStats(stats) {
document.getElementById('statTotal').textContent = stats.total;
document.getElementById('statPending').textContent = stats.pending;
document.getElementById('statCompleted').textContent = stats.completed;
document.getElementById('statFailed').textContent = stats.failed;
document.getElementById('statExcluded').textContent = stats.excluded;
document.getElementById('statSize').textContent = stats.total_size;
}
function resetStats() {
document.getElementById('statTotal').textContent = '0';
document.getElementById('statPending').textContent = '0';
document.getElementById('statCompleted').textContent = '0';
document.getElementById('statFailed').textContent = '0';
document.getElementById('statExcluded').textContent = '0';
document.getElementById('statSize').textContent = '0 B';
}
function addLog(message, type = 'info') {
const logPanel = document.getElementById('logPanel');
const now = new Date();
const timeStr = now.toTimeString().split(' ')[0];
const logEntry = document.createElement('div');
logEntry.className = 'log-entry';
logEntry.innerHTML = `
<span class="log-time">[${timeStr}]</span>
<span class="log-${type}"> ${message}</span>
`;
logPanel.appendChild(logEntry);
logPanel.scrollTop = logPanel.scrollHeight;
}
function saveConfig() {
const directories = document.getElementById('exclude_dirs').value;
const files = document.getElementById('exclude_files').value;
const extensions = document.getElementById('exclude_exts').value;
const apiUrl = document.getElementById('api_url').value;
fetch('', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest'
},
body: `action=save_config&api_url=${encodeURIComponent(apiUrl)}&exclude_dirs=${encodeURIComponent(directories)}&exclude_files=${encodeURIComponent(files)}&exclude_exts=${encodeURIComponent(extensions)}`
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('配置保存成功');
} else {
alert('配置保存失败');
}
});
}
function loadHistory() {
const apiUrl = document.getElementById('api_url').value;
if (!apiUrl) return;
// 这里可以添加加载历史记录的AJAX调用
// 由于时间关系,这里只是显示一个提示
document.getElementById('historyList').innerHTML = `
<p>备份历史记录功能需要从服务器加载数据。</p>
<p>请确保 backup_history.json 文件存在且可读。</p>
`;
}
// 页面加载时检查现有任务
window.onload = function() {
const apiUrl = document.getElementById('api_url').value;
if (apiUrl) {
fetch('', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest'
},
body: `action=status&api_url=${encodeURIComponent(apiUrl)}`
})
.then(response => response.json())
.then(data => {
if (data.status) {
updateStatus(data.status);
addLog('检测到现有备份任务', 'info');
}
});
}
};
</script>
</body>
</html>