<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文件备份系统</title>
<style>
/* 保留原有样式,新增样式在下面 */
.exclude-panel {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
border-left: 4px solid #ff9800;
}
.exclude-panel h3 {
color: #ff9800;
margin-bottom: 15px;
display: flex;
align-items: center;
justify-content: space-between;
}
.exclude-rules {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 15px;
margin-bottom: 15px;
}
.rule-category {
background: white;
padding: 15px;
border-radius: 6px;
border: 1px solid #ddd;
}
.rule-category h4 {
margin-bottom: 10px;
color: #333;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 1px;
}
.rule-items {
font-size: 13px;
line-height: 1.4;
color: #666;
}
.rule-item {
display: inline-block;
background: #e3f2fd;
padding: 3px 8px;
margin: 2px;
border-radius: 3px;
font-family: monospace;
}
.excluded-files {
max-height: 200px;
overflow-y: auto;
background: white;
padding: 10px;
border-radius: 6px;
border: 1px solid #ddd;
font-size: 12px;
}
.excluded-file {
padding: 3px 5px;
border-bottom: 1px solid #eee;
font-family: monospace;
}
.excluded-file:last-child {
border-bottom: none;
}
.toggle-btn {
background: #ff9800;
color: white;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.toggle-btn:hover {
background: #f57c00;
}
.config-form {
background: white;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
border: 1px solid #ddd;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #333;
}
.form-control {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.form-control:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
}
.form-text {
font-size: 12px;
color: #666;
margin-top: 5px;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
justify-content: center;
align-items: center;
}
.modal-content {
background: white;
padding: 30px;
border-radius: 10px;
max-width: 500px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.modal-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
}
.modal-close:hover {
color: #333;
}
.modal-buttons {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>📁 文件备份系统</h1>
<p>安全、可靠的自动化文件备份解决方案</p>
</header>
<div class="content">
<div class="stats-grid" id="stats">
<!-- 统计数据将通过JavaScript动态加载 -->
</div>
<div class="exclude-panel" id="excludePanel">
<!-- 排除规则信息将通过JavaScript动态加载 -->
</div>
<div class="backup-controls">
<button class="btn" onclick="startBackup()" id="startBtn">开始备份</button>
<button class="btn btn-stop" onclick="stopBackup()" id="stopBtn" style="display:none">停止备份</button>
<button class="btn" onclick="resetBackup()" id="resetBtn">重置备份</button>
<button class="btn" onclick="showConfig()" id="configBtn">配置</button>
<button class="btn" onclick="location.reload()">刷新状态</button>
<div class="auto-backup">
<label>
<input type="checkbox" id="autoBackup" onchange="toggleAutoBackup()">
自动备份
</label>
<input type="number" id="interval" value="2" min="1" max="60" style="display:none">
<span style="display:none" id="intervalText">秒间隔</span>
</div>
</div>
<div class="progress-section">
<h2>备份进度</h2>
<div class="progress-container">
<div class="progress-bar" id="progressBar">
<div class="progress-text" id="progressText">0%</div>
</div>
</div>
<div id="progressInfo">等待开始备份...</div>
</div>
<div class="file-info" id="currentFile">
<!-- 当前文件信息将通过JavaScript动态加载 -->
</div>
<div class="log-container" id="log">
<!-- 日志将通过JavaScript动态加载 -->
</div>
<div class="spinner" id="spinner"></div>
</div>
<footer>
<p>© 2023 文件备份系统 | 兼容 PHP 5.3 - PHP 8.x</p>
<p>当前备份目录: /mnt/sda1/www/2026</p>
<p>API地址: https://zgyzty.com/assets/sapi.php</p>
<p>默认排除: config.php, backup.php, api.php 等系统文件</p>
</footer>
</div>
<!-- 配置模态框 -->
<div class="modal" id="configModal">
<div class="modal-content">
<div class="modal-header">
<h2>备份配置</h2>
<button class="modal-close" onclick="hideConfig()">×</button>
</div>
<div class="config-form">
<div class="form-group">
<label>扫描目录:</label>
<input type="text" class="form-control" id="configScanDir" value=".">
<div class="form-text">要备份的目录路径</div>
</div>
<div class="form-group">
<label>排除目录 (用逗号分隔):</label>
<textarea class="form-control" id="configExcludeDirs" rows="3">.git, vendor, node_modules, backups</textarea>
<div class="form-text">排除的目录名,如 .git, vendor 等</div>
</div>
<div class="form-group">
<label>排除文件 (用逗号分隔):</label>
<textarea class="form-control" id="configExcludeFiles" rows="3">config.php, backup.php, api.php, backup_status.json, backup_errors.json, curlog.txt</textarea>
<div class="form-text">排除的文件名,支持通配符如 *.php</div>
</div>
<div class="form-group">
<label>排除扩展名 (用逗号分隔):</label>
<textarea class="form-control" id="configExcludeExtensions" rows="3">tmp, log, cache</textarea>
<div class="form-text">排除的文件扩展名,如 tmp, log 等</div>
</div>
<div class="modal-buttons">
<button class="btn" onclick="saveConfig()">保存配置</button>
<button class="btn btn-stop" onclick="hideConfig()">取消</button>
</div>
</div>
</div>
</div>
<script>
let isBackupRunning = false;
let autoBackupEnabled = false;
let backupInterval;
let logEntries = [];
// 加载统计信息
function loadStats() {
fetch('backup.php?action=stats')
.then(response => response.json())
.then(data => {
const statsHtml = `
<div class="stat-card">
<h3>📊 总文件数</h3>
<div class="number">${data.total_files}</div>
</div>
<div class="stat-card">
<h3>✅ 已备份</h3>
<div class="number">${data.backed_up}</div>
</div>
<div class="stat-card">
<h3>⏳ 待备份</h3>
<div class="number">${data.pending}</div>
</div>
<div class="stat-card">
<h3>❌ 失败数</h3>
<div class="number">${data.errors}</div>
</div>
<div class="stat-card">
<h3>🚫 已排除</h3>
<div class="number">${data.excluded_count}</div>
</div>
`;
document.getElementById('stats').innerHTML = statsHtml;
// 加载排除规则
loadExcludeRules(data);
});
}
// 加载排除规则
function loadExcludeRules(data) {
fetch('backup.php?action=exclude_rules')
.then(response => response.json())
.then(rules => {
let excludedFilesHtml = '';
if (data.excluded_files && data.excluded_files.length > 0) {
data.excluded_files.slice(0, 20).forEach(file => {
excludedFilesHtml += `<div class="excluded-file">${file}</div>`;
});
if (data.excluded_files.length > 20) {
excludedFilesHtml += `<div class="excluded-file">... 还有 ${data.excluded_files.length - 20} 个文件</div>`;
}
} else {
excludedFilesHtml = '<div class="excluded-file">无被排除文件</div>';
}
const excludeHtml = `
<h3>
🛡️ 排除规则
<button class="toggle-btn" onclick="toggleExcludeDetails()">查看详情</button>
</h3>
<div class="exclude-rules">
<div class="rule-category">
<h4>排除目录</h4>
<div class="rule-items">
${rules['排除目录'].map(dir => `<span class="rule-item">${dir}</span>`).join('')}
</div>
</div>
<div class="rule-category">
<h4>排除文件</h4>
<div class="rule-items">
${rules['排除文件'].map(file => `<span class="rule-item">${file}</span>`).join('')}
</div>
</div>
<div class="rule-category">
<h4>排除扩展名</h4>
<div class="rule-items">
${rules['排除扩展名'].map(ext => `<span class="rule-item">.${ext}</span>`).join('')}
</div>
</div>
</div>
<div id="excludeDetails" style="display:none">
<h4>被排除的文件 (${data.excluded_count}个):</h4>
<div class="excluded-files">
${excludedFilesHtml}
</div>
</div>
`;
document.getElementById('excludePanel').innerHTML = excludeHtml;
});
}
// 切换排除详情显示
function toggleExcludeDetails() {
const details = document.getElementById('excludeDetails');
const button = document.querySelector('.toggle-btn');
if (details.style.display === 'none') {
details.style.display = 'block';
button.textContent = '隐藏详情';
} else {
details.style.display = 'none';
button.textContent = '查看详情';
}
}
// 显示配置模态框
function showConfig() {
document.getElementById('configModal').style.display = 'flex';
}
// 隐藏配置模态框
function hideConfig() {
document.getElementById('configModal').style.display = 'none';
}
// 保存配置
function saveConfig() {
const config = {
scan_dir: document.getElementById('configScanDir').value,
exclude_dirs: document.getElementById('configExcludeDirs').value.split(',').map(s => s.trim()).filter(s => s),
exclude_files: document.getElementById('configExcludeFiles').value.split(',').map(s => s.trim()).filter(s => s),
exclude_extensions: document.getElementById('configExcludeExtensions').value.split(',').map(s => s.trim()).filter(s => s)
};
// 这里应该将配置保存到服务器
// 由于是单文件系统,我们可以通过URL参数传递
alert('配置已更新,请刷新页面应用新配置。\n注意:需要重新扫描文件列表。');
hideConfig();
// 重新加载页面并重置扫描
setTimeout(() => {
window.location.href = 'backup.php?reset=1';
}, 1000);
}
// 添加日志条目
function addLogEntry(message, type = 'info') {
const timestamp = new Date().toLocaleTimeString();
const entry = {
time: timestamp,
message: message,
type: type
};
logEntries.unshift(entry); // 添加到开头
// 限制日志数量
if (logEntries.length > 50) {
logEntries = logEntries.slice(0, 50);
}
// 更新日志显示
updateLogDisplay();
}
// 更新日志显示
function updateLogDisplay() {
const logContainer = document.getElementById('log');
let logHtml = '';
logEntries.forEach(entry => {
logHtml += `
<div class="log-entry ${entry.type}">
<strong>[${entry.time}]</strong> ${entry.message}
</div>
`;
});
logContainer.innerHTML = logHtml;
}
// 开始备份
function startBackup() {
if (isBackupRunning) return;
isBackupRunning = true;
document.getElementById('startBtn').style.display = 'none';
document.getElementById('stopBtn').style.display = 'inline-block';
document.getElementById('spinner').style.display = 'block';
addLogEntry('开始备份进程...', 'info');
addLogEntry('注意:config.php, backup.php, api.php 等系统文件已被排除', 'info');
// 执行备份
performBackup();
}
// 执行备份
function performBackup() {
if (!isBackupRunning) return;
fetch('backup.php?action=backup')
.then(response => response.json())
.then(data => {
if (data.completed) {
// 备份完成
backupCompleted(data);
return;
}
// 更新进度
updateProgress(data);
// 显示当前文件信息
updateCurrentFile(data.current_file);
// 添加日志
if (data.result.success) {
addLogEntry(`✅ ${data.result.file} - ${data.result.message} (${data.result.size})`, 'success');
} else {
addLogEntry(`❌ ${data.result.file} - ${data.result.message}`, 'error');
}
// 如果启用了自动备份,继续下一个文件
if (autoBackupEnabled) {
setTimeout(performBackup, document.getElementById('interval').value * 1000);
}
})
.catch(error => {
addLogEntry(`请求错误: ${error.message}`, 'error');
if (autoBackupEnabled) {
setTimeout(performBackup, 5000); // 5秒后重试
}
});
}
// 更新进度
function updateProgress(data) {
const progress = data.progress;
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
const progressInfo = document.getElementById('progressInfo');
progressBar.style.width = progress.percent + '%';
progressText.textContent = progress.percent + '%';
const stats = data.stats;
progressInfo.innerHTML = `
进度: ${progress.current}/${progress.total} 文件 |
成功: ${stats.success} |
失败: ${stats.failed} |
耗时: ${Math.round((Date.now()/1000 - stats.start_time))}秒
`;
}
// 更新当前文件信息
function updateCurrentFile(file) {
if (!file) return;
const fileSize = formatBytes(file.size);
const fileInfo = document.getElementById('currentFile');
fileInfo.innerHTML = `
<h3>📄 当前备份文件</h3>
<p><strong>文件路径:</strong> ${file.path}</p>
<p><strong>文件大小:</strong> ${fileSize}</p>
<p><strong>修改时间:</strong> ${new Date(file.mtime * 1000).toLocaleString()}</p>
<p><strong>MD5:</strong> ${file.md5.substring(0, 16)}...</p>
`;
}
// 备份完成
function backupCompleted(data) {
isBackupRunning = false;
document.getElementById('startBtn').style.display = 'inline-block';
document.getElementById('stopBtn').style.display = 'none';
document.getElementById('spinner').style.display = 'none';
addLogEntry(`🎉 备份完成!总计: ${data.stats.success} 成功, ${data.stats.failed} 失败`, 'success');
addLogEntry(`🚫 系统文件 (config.php, backup.php, api.php) 已被排除,不会备份`, 'info');
// 更新统计信息
loadStats();
// 显示完成信息
document.getElementById('currentFile').innerHTML = `
<h3>✅ 备份完成</h3>
<p><strong>总文件数:</strong> ${data.stats.total}</p>
<p><strong>成功:</strong> ${data.stats.success}</p>
<p><strong>失败:</strong> ${data.stats.failed}</p>
<p><strong>开始时间:</strong> ${new Date(data.stats.start_time * 1000).toLocaleString()}</p>
<p><strong>完成时间:</strong> ${new Date().toLocaleString()}</p>
<p><strong>备注:</strong> 系统文件已被排除,不会备份</p>
`;
// 重置进度条
document.getElementById('progressBar').style.width = '100%';
document.getElementById('progressText').textContent = '100%';
// 如果启用了自动备份,提示用户
if (autoBackupEnabled) {
addLogEntry('自动备份已完成一轮,等待下次计划执行', 'info');
}
}
// 停止备份
function stopBackup() {
isBackupRunning = false;
document.getElementById('startBtn').style.display = 'inline-block';
document.getElementById('stopBtn').style.display = 'none';
document.getElementById('spinner').style.display = 'none';
addLogEntry('备份已停止', 'info');
}
// 重置备份
function resetBackup() {
if (isBackupRunning) {
if (!confirm('备份正在进行中,确定要重置吗?')) {
return;
}
stopBackup();
}
fetch('backup.php?action=reset')
.then(() => {
location.reload();
});
}
// 切换自动备份
function toggleAutoBackup() {
autoBackupEnabled = document.getElementById('autoBackup').checked;
const intervalInput = document.getElementById('interval');
const intervalText = document.getElementById('intervalText');
if (autoBackupEnabled) {
intervalInput.style.display = 'inline-block';
intervalText.style.display = 'inline-block';
// 如果备份没有运行,自动开始
if (!isBackupRunning) {
startBackup();
}
} else {
intervalInput.style.display = 'none';
intervalText.style.display = 'none';
}
}
// 格式化字节大小
function formatBytes(bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
// 页面加载时初始化
window.onload = function() {
loadStats();
addLogEntry('系统就绪,可以开始备份', 'info');
addLogEntry('注意:config.php, backup.php, api.php 等系统文件默认被排除', 'info');
// 检查是否有待备份文件
fetch('backup.php?action=stats')
.then(response => response.json())
.then(data => {
if (data.pending > 0) {
addLogEntry(`检测到 ${data.pending} 个待备份文件`, 'info');
addLogEntry(`检测到 ${data.excluded_count} 个被排除文件`, 'info');
}
});
};
</script>
</body>
</html>