['.git', 'node_modules', 'vendor', '__pycache__', '.idea'], 'files' => ['config.php', 'backup.php', 'api.php', 'database.php', 'env.php', '.env'], 'extensions' => ['.log', '.tmp', '.cache', '.swp', '.swo'] ]; private $excludeCount = 0; private $backupHistory = 'backup_history.json'; public function __construct($apiUrl, $basePath = null) { $this->apiUrl = $apiUrl; $this->basePath = $basePath ? realpath($basePath) : realpath('.'); $this->backupId = $this->generateBackupId($this->basePath); // 加载自定义排除规则 $this->loadExcludeConfig(); } /** * 生成固定备份ID(基于目录路径) */ private function generateBackupId($path) { return md5($path); } /** * 加载排除配置 */ private function loadExcludeConfig() { $configFile = 'backup_config.json'; if (file_exists($configFile)) { $config = json_decode(file_get_contents($configFile), true); if ($config) { if (isset($config['exclude_directories'])) { $this->excludes['directories'] = array_merge( $this->excludes['directories'], $config['exclude_directories'] ); } if (isset($config['exclude_files'])) { $this->excludes['files'] = array_merge( $this->excludes['files'], $config['exclude_files'] ); } if (isset($config['exclude_extensions'])) { $this->excludes['extensions'] = array_merge( $this->excludes['extensions'], $config['exclude_extensions'] ); } } } } /** * 保存排除配置 */ public function saveExcludeConfig($directories = [], $files = [], $extensions = []) { $config = [ 'exclude_directories' => $directories, 'exclude_files' => $files, 'exclude_extensions' => $extensions ]; file_put_contents('backup_config.json', json_encode($config, JSON_PRETTY_PRINT)); } /** * 检查文件是否应该排除 */ private function shouldExcludeFile($relativePath, $fileName) { // 检查完整路径是否在排除列表中 foreach ($this->excludes['files'] as $excludeFile) { if (strpos($relativePath, $excludeFile) !== false) { return true; } } // 检查文件名是否在排除列表中 foreach ($this->excludes['files'] as $excludeFile) { if ($fileName === $excludeFile) { return true; } } // 检查扩展名 foreach ($this->excludes['extensions'] as $ext) { if (strtolower(substr($fileName, -strlen($ext))) === strtolower($ext)) { return true; } } return false; } /** * 检查目录是否应该排除 */ private function shouldExcludeDirectory($relativePath) { $parts = explode('/', $relativePath); foreach ($parts as $part) { if (in_array($part, $this->excludes['directories'])) { return true; } } return false; } /** * 扫描目录并保存任务 */ public function scanAndSaveTask($scanDir = null) { $scanDir = $scanDir ?: $this->basePath; $relativeScanDir = substr($scanDir, strlen($this->basePath)); if ($relativeScanDir === '') { $relativeScanDir = '.'; } $taskData = [ 'backup_id' => $this->backupId, 'base_path' => $this->basePath, 'scan_dir' => $scanDir, 'relative_scan_dir' => $relativeScanDir, 'files' => [], 'pending' => [], 'completed' => [], 'failed' => [], 'excluded' => [], 'start_time' => time(), 'last_update' => time(), 'total_files' => 0, 'total_size' => 0 ]; // 如果任务文件已存在,先加载已有数据 if (file_exists($this->taskFile)) { $existingData = json_decode(file_get_contents($this->taskFile), true); if ($existingData && $existingData['backup_id'] === $this->backupId) { $taskData = array_merge($taskData, $existingData); } } // 扫描新文件 $newFiles = $this->scanDirectory($scanDir); // 只添加未在任务中的文件 foreach ($newFiles as $fileInfo) { $filePath = $fileInfo['path']; if (!isset($taskData['files'][$filePath]) && !in_array($filePath, $taskData['completed']) && !in_array($filePath, $taskData['failed']) && !in_array($filePath, $taskData['pending']) && !in_array($filePath, $taskData['excluded'])) { // 检查是否需要排除 if ($this->shouldExcludeFile($fileInfo['path'], basename($fileInfo['path']))) { $taskData['excluded'][] = $fileInfo['path']; $this->excludeCount++; } else { $taskData['files'][$filePath] = $fileInfo; $taskData['pending'][] = $filePath; $taskData['total_files']++; $taskData['total_size'] += $fileInfo['size']; } } } // 保存任务 file_put_contents($this->taskFile, json_encode($taskData, JSON_PRETTY_PRINT)); return $taskData; } /** * 扫描目录 */ private function scanDirectory($dir) { $files = []; try { $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST ); foreach ($iterator as $item) { if ($item->isFile()) { $filePath = $item->getPathname(); $relativePath = substr($filePath, strlen($this->basePath) + 1); // 转换路径分隔符为Unix风格 $relativePath = str_replace('\\', '/', $relativePath); // 检查目录是否应该排除 if ($this->shouldExcludeDirectory(dirname($relativePath))) { continue; } $fileInfo = [ 'path' => $relativePath, 'full_path' => $filePath, 'size' => $item->getSize(), 'modified' => $item->getMTime(), 'md5' => $this->calculateFileMd5($filePath) ]; $files[] = $fileInfo; } } } catch (Exception $e) { $this->logError("扫描目录失败: " . $e->getMessage()); } return $files; } /** * 计算文件MD5 */ private function calculateFileMd5($filePath) { if (!file_exists($filePath) || !is_readable($filePath)) { return ''; } if (filesize($filePath) <= $this->chunkSize) { return md5_file($filePath); } // 大文件分块计算MD5 $hash = md5_init(); $handle = fopen($filePath, 'rb'); if (!$handle) { return ''; } while (!feof($handle)) { $chunk = fread($handle, $this->chunkSize); if (function_exists('md5_update')) { md5_update($hash, $chunk); } else { $hash = md5($hash . $chunk); } } fclose($handle); if (function_exists('md5_final')) { return md5_final($hash); } else { return md5($hash); } } /** * 同步处理下一个文件 */ public function processNextFile() { if (!file_exists($this->taskFile)) { return ['error' => '备份任务不存在']; } $taskData = json_decode(file_get_contents($this->taskFile), true); // 检查是否有待处理文件 if (empty($taskData['pending'])) { return [ 'completed' => true, 'message' => '所有文件已备份完成', 'stats' => [ 'total' => $taskData['total_files'], 'completed' => count($taskData['completed']), 'failed' => count($taskData['failed']), 'excluded' => count($taskData['excluded']), 'total_size' => $this->formatSize($taskData['total_size']) ] ]; } // 取出下一个文件 $filePath = array_shift($taskData['pending']); $fileInfo = $taskData['files'][$filePath]; // 发送文件 $result = $this->sendFile($fileInfo); // 更新任务状态 if ($result && isset($result['success']) && $result['success']) { $taskData['completed'][] = $filePath; $this->logHistory($fileInfo); } else { $taskData['failed'][] = $filePath; $taskData['pending'][] = $filePath; // 重新加入队列重试 } $taskData['last_update'] = time(); // 保存更新后的任务 file_put_contents($this->taskFile, json_encode($taskData, JSON_PRETTY_PRINT)); // 计算进度 $totalProcessed = count($taskData['completed']) + count($taskData['failed']); $progress = $taskData['total_files'] > 0 ? round(($totalProcessed / $taskData['total_files']) * 100, 2) : 0; return [ 'completed' => false, 'current_file' => $fileInfo['path'], 'current_size' => $this->formatSize($fileInfo['size']), 'progress' => $progress, 'stats' => [ 'total' => $taskData['total_files'], 'pending' => count($taskData['pending']), 'completed' => count($taskData['completed']), 'failed' => count($taskData['failed']), 'excluded' => count($taskData['excluded']), 'total_size' => $this->formatSize($taskData['total_size']) ], 'result' => $result ]; } /** * 发送文件到API */ private function sendFile($fileInfo) { $filePath = $fileInfo['full_path']; $fileSize = filesize($filePath); if ($fileSize <= $this->chunkSize) { return $this->sendSingleFile($fileInfo); } else { return $this->sendChunkedFile($fileInfo); } } /** * 发送单个文件 */ private function sendSingleFile($fileInfo) { $fileContent = file_get_contents($fileInfo['full_path']); if ($fileContent === false) { $this->logError("无法读取文件: " . $fileInfo['path']); return false; } $postData = [ 'backup_id' => $this->backupId, 'action' => 'upload', 'file_path' => $fileInfo['path'], 'file_content' => base64_encode($fileContent), 'file_md5' => $fileInfo['md5'], 'file_size' => $fileInfo['size'] ]; return $this->callApi($postData); } /** * 发送分块文件 */ private function sendChunkedFile($fileInfo) { $handle = fopen($fileInfo['full_path'], 'rb'); if (!$handle) { $this->logError("无法打开文件: " . $fileInfo['path']); return false; } $chunkIndex = 0; $totalChunks = ceil($fileInfo['size'] / $this->chunkSize); while (!feof($handle)) { $chunkData = fread($handle, $this->chunkSize); $postData = [ 'backup_id' => $this->backupId, 'action' => 'upload_chunk', 'file_path' => $fileInfo['path'], 'chunk_index' => $chunkIndex, 'total_chunks' => $totalChunks, 'chunk_data' => base64_encode($chunkData), 'chunk_size' => strlen($chunkData), 'file_md5' => $fileInfo['md5'], 'file_size' => $fileInfo['size'] ]; $result = $this->callApi($postData); if (!$result || !isset($result['success']) || !$result['success']) { fclose($handle); return $result; } $chunkIndex++; } fclose($handle); // 发送合并请求 $postData = [ 'backup_id' => $this->backupId, 'action' => 'merge_chunks', 'file_path' => $fileInfo['path'], 'total_chunks' => $totalChunks, 'file_md5' => $fileInfo['md5'], 'file_size' => $fileInfo['size'] ]; return $this->callApi($postData); } /** * API调用 */ private function callApi($data) { // 使用cURL if (function_exists('curl_init')) { $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $this->apiUrl, CURLOPT_POST => true, CURLOPT_POSTFIELDS => http_build_query($data), CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => false, CURLOPT_HTTPHEADER => [ 'Content-Type: application/x-www-form-urlencoded', 'User-Agent: FileBackupSender/1.0' ] ]); $result = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($result !== false && $httpCode == 200) { return json_decode($result, true); } } return false; } /** * 获取任务状态 */ public function getTaskStatus() { if (!file_exists($this->taskFile)) { return null; } $taskData = json_decode(file_get_contents($this->taskFile), true); $totalProcessed = count($taskData['completed']) + count($taskData['failed']); $progress = $taskData['total_files'] > 0 ? round(($totalProcessed / $taskData['total_files']) * 100, 2) : 0; return [ 'backup_id' => $taskData['backup_id'], 'base_path' => $taskData['base_path'], 'scan_dir' => $taskData['scan_dir'], 'progress' => $progress, 'stats' => [ 'total' => $taskData['total_files'], 'pending' => count($taskData['pending']), 'completed' => count($taskData['completed']), 'failed' => count($taskData['failed']), 'excluded' => count($taskData['excluded']), 'total_size' => $this->formatSize($taskData['total_size']) ], 'start_time' => date('Y-m-d H:i:s', $taskData['start_time']), 'last_update' => date('Y-m-d H:i:s', $taskData['last_update']), 'excluded_files' => array_slice($taskData['excluded'], 0, 10) // 只显示前10个排除文件 ]; } /** * 重置任务 */ public function resetTask() { if (file_exists($this->taskFile)) { unlink($this->taskFile); } return true; } /** * 记录历史 */ private function logHistory($fileInfo) { $history = []; if (file_exists($this->backupHistory)) { $history = json_decode(file_get_contents($this->backupHistory), true); } if (!isset($history[$this->backupId])) { $history[$this->backupId] = [ 'base_path' => $this->basePath, 'first_backup' => date('Y-m-d H:i:s'), 'last_backup' => date('Y-m-d H:i:s'), 'files' => [] ]; } $history[$this->backupId]['files'][$fileInfo['path']] = [ 'md5' => $fileInfo['md5'], 'size' => $fileInfo['size'], 'backup_time' => date('Y-m-d H:i:s') ]; $history[$this->backupId]['last_backup'] = date('Y-m-d H:i:s'); file_put_contents($this->backupHistory, json_encode($history, JSON_PRETTY_PRINT)); } /** * 获取备份历史 */ public function getBackupHistory() { if (file_exists($this->backupHistory)) { return json_decode(file_get_contents($this->backupHistory), true); } return []; } /** * 格式化文件大小 */ private function formatSize($bytes) { $units = ['B', 'KB', 'MB', 'GB', 'TB']; $i = 0; while ($bytes >= 1024 && $i < count($units) - 1) { $bytes /= 1024; $i++; } return round($bytes, 2) . ' ' . $units[$i]; } /** * 记录错误 */ private function logError($message) { error_log("[BackupSender] " . $message); } } /** * PHP5兼容函数 */ if (!function_exists('md5_init')) { function md5_init() { return md5(''); } } if (!function_exists('md5_update')) { function md5_update(&$context, $data) { $context = md5($context . $data); } } if (!function_exists('md5_final')) { function md5_final($context) { return md5($context); } } // 主程序 session_start(); // 处理AJAX请求 if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') { $apiUrl = isset($_POST['api_url']) ? $_POST['api_url'] : ''; $action = isset($_POST['action']) ? $_POST['action'] : ''; $backupDir = isset($_POST['backup_dir']) ? $_POST['backup_dir'] : null; if (empty($apiUrl)) { echo json_encode(['error' => '请提供API URL']); exit; } $sender = new FileBackupSender($apiUrl, $backupDir); switch ($action) { case 'init': // 初始化或重新扫描 $taskData = $sender->scanAndSaveTask($backupDir); $status = $sender->getTaskStatus(); echo json_encode(['status' => $status]); break; case 'next': // 处理下一个文件 $result = $sender->processNextFile(); echo json_encode($result); break; case 'status': // 获取状态 $status = $sender->getTaskStatus(); echo json_encode(['status' => $status]); break; case 'reset': // 重置任务 $result = $sender->resetTask(); echo json_encode(['success' => $result]); break; case 'save_config': // 保存排除配置 $directories = isset($_POST['exclude_dirs']) ? explode(',', $_POST['exclude_dirs']) : []; $files = isset($_POST['exclude_files']) ? explode(',', $_POST['exclude_files']) : []; $extensions = isset($_POST['exclude_exts']) ? explode(',', $_POST['exclude_exts']) : []; // 清理输入 $directories = array_map('trim', $directories); $files = array_map('trim', $files); $extensions = array_map('trim', $extensions); $sender->saveExcludeConfig($directories, $files, $extensions); echo json_encode(['success' => true]); break; default: echo json_encode(['error' => '无效的操作']); } exit; } // 显示界面 ?> 文件备份系统 - 同步版本

📁 文件备份系统 - 同步版本

备份管理
排除配置
备份历史

备份状态

等待开始备份...

0%
0
总文件数
0
待备份
0
已备份
0
失败数
0
已排除
0 B
总大小

排除目录

排除文件

排除扩展名

说明

排除规则说明:

  • 排除目录:不扫描指定目录下的任何文件
  • 排除文件:完全匹配文件名(包含路径)
  • 排除扩展名:匹配文件扩展名

备份历史记录