backupBaseDir = $baseDir; } // 创建必要的目录 $this->createDirectory($this->backupBaseDir); $this->createDirectory($this->backupBaseDir . '/' . $this->chunkDir); } /** * 创建目录 */ private function createDirectory($dir) { if (!file_exists($dir)) { mkdir($dir, 0755, true); } } /** * 处理API请求 */ public function handleRequest() { $action = isset($_POST['action']) ? $_POST['action'] : (isset($_GET['action']) ? $_GET['action'] : ''); $backupId = isset($_POST['backup_id']) ? $_POST['backup_id'] : (isset($_GET['backup_id']) ? $_GET['backup_id'] : ''); // 验证备份ID if (empty($backupId) && $action != 'test') { return $this->jsonResponse(['success' => false, 'error' => '无效的备份ID']); } switch ($action) { case 'upload': return $this->uploadFile($backupId); case 'upload_chunk': return $this->uploadChunk($backupId); case 'merge_chunks': return $this->mergeChunks($backupId); case 'check_md5': return $this->checkFileMd5($backupId); case 'test': return $this->testConnection(); default: return $this->jsonResponse(['success' => false, 'error' => '无效的操作']); } } /** * 检查文件MD5,决定是否需要覆盖 */ private function checkFileMd5($backupId) { $filePath = isset($_POST['file_path']) ? $_POST['file_path'] : ''; $fileMd5 = isset($_POST['file_md5']) ? $_POST['file_md5'] : ''; if (empty($filePath) || empty($fileMd5)) { return $this->jsonResponse(['success' => false, 'error' => '缺少参数']); } $savePath = $this->getSavePath($backupId, $filePath); // 检查文件是否存在 if (!file_exists($savePath)) { return $this->jsonResponse([ 'success' => true, 'need_upload' => true, 'reason' => '文件不存在' ]); } // 检查MD5是否匹配 $existingMd5 = md5_file($savePath); if ($existingMd5 !== $fileMd5) { return $this->jsonResponse([ 'success' => true, 'need_upload' => true, 'reason' => 'MD5不匹配', 'existing_md5' => $existingMd5, 'new_md5' => $fileMd5 ]); } // 文件已存在且MD5匹配,无需上传 return $this->jsonResponse([ 'success' => true, 'need_upload' => false, 'reason' => '文件已存在且MD5相同', 'file_size' => filesize($savePath) ]); } /** * 上传单个文件(支持覆盖) */ private function uploadFile($backupId) { $filePath = isset($_POST['file_path']) ? $_POST['file_path'] : ''; $fileContent = isset($_POST['file_content']) ? $_POST['file_content'] : ''; $fileMd5 = isset($_POST['file_md5']) ? $_POST['file_md5'] : ''; $fileSize = isset($_POST['file_size']) ? intval($_POST['file_size']) : 0; if (empty($filePath) || empty($fileContent)) { return $this->jsonResponse(['success' => false, 'error' => '缺少文件数据']); } // 验证文件大小 if ($fileSize > $this->maxFileSize) { return $this->jsonResponse(['success' => false, 'error' => '文件大小超过限制']); } // 解码文件内容 $content = base64_decode($fileContent); if ($content === false) { return $this->jsonResponse(['success' => false, 'error' => '文件解码失败']); } // 验证MD5 $receivedMd5 = md5($content); if ($fileMd5 && $receivedMd5 !== $fileMd5) { return $this->jsonResponse(['success' => false, 'error' => '文件校验失败']); } // 检查是否需要覆盖 $savePath = $this->getSavePath($backupId, $filePath); $needOverwrite = false; if (file_exists($savePath)) { $existingMd5 = md5_file($savePath); if ($existingMd5 === $fileMd5) { // MD5相同,跳过上传 return $this->jsonResponse([ 'success' => true, 'message' => '文件已存在且MD5相同,跳过上传', 'skipped' => true, 'path' => $savePath ]); } $needOverwrite = true; } // 保存文件 $this->createDirectory(dirname($savePath)); if (file_put_contents($savePath, $content) !== false) { // 记录备份信息 $this->logBackup($backupId, $filePath, $fileSize, $fileMd5, $needOverwrite); return $this->jsonResponse([ 'success' => true, 'message' => $needOverwrite ? '文件覆盖成功' : '文件上传成功', 'path' => $savePath, 'overwritten' => $needOverwrite ]); } else { return $this->jsonResponse(['success' => false, 'error' => '文件保存失败']); } } /** * 上传分块 */ private function uploadChunk($backupId) { $filePath = isset($_POST['file_path']) ? $_POST['file_path'] : ''; $chunkIndex = isset($_POST['chunk_index']) ? intval($_POST['chunk_index']) : 0; $totalChunks = isset($_POST['total_chunks']) ? intval($_POST['total_chunks']) : 0; $chunkData = isset($_POST['chunk_data']) ? $_POST['chunk_data'] : ''; $chunkSize = isset($_POST['chunk_size']) ? intval($_POST['chunk_size']) : 0; if (empty($filePath) || empty($chunkData) || $totalChunks == 0) { return $this->jsonResponse(['success' => false, 'error' => '缺少分块数据']); } // 解码分块数据 $content = base64_decode($chunkData); if ($content === false) { return $this->jsonResponse(['success' => false, 'error' => '分块解码失败']); } // 验证分块大小 if (strlen($content) != $chunkSize) { return $this->jsonResponse(['success' => false, 'error' => '分块大小不匹配']); } // 保存分块 $chunkFileName = $this->getChunkFileName($backupId, $filePath, $chunkIndex); if (file_put_contents($chunkFileName, $content) !== false) { return $this->jsonResponse([ 'success' => true, 'message' => '分块上传成功', 'chunk_index' => $chunkIndex ]); } else { return $this->jsonResponse(['success' => false, 'error' => '分块保存失败']); } } /** * 合并分块(支持覆盖) */ private function mergeChunks($backupId) { $filePath = isset($_POST['file_path']) ? $_POST['file_path'] : ''; $totalChunks = isset($_POST['total_chunks']) ? intval($_POST['total_chunks']) : 0; $fileMd5 = isset($_POST['file_md5']) ? $_POST['file_md5'] : ''; $fileSize = isset($_POST['file_size']) ? intval($_POST['file_size']) : 0; if (empty($filePath) || $totalChunks == 0) { return $this->jsonResponse(['success' => false, 'error' => '缺少合并参数']); } // 验证文件大小 if ($fileSize > $this->maxFileSize) { return $this->jsonResponse(['success' => false, 'error' => '文件大小超过限制']); } // 检查是否需要覆盖 $savePath = $this->getSavePath($backupId, $filePath); $needOverwrite = false; if (file_exists($savePath)) { $existingMd5 = md5_file($savePath); if ($existingMd5 === $fileMd5) { // MD5相同,跳过合并,删除分块文件 $this->cleanupChunks($backupId, $filePath, $totalChunks); return $this->jsonResponse([ 'success' => true, 'message' => '文件已存在且MD5相同,跳过合并', 'skipped' => true, 'path' => $savePath ]); } $needOverwrite = true; } // 合并所有分块 $this->createDirectory(dirname($savePath)); $fp = fopen($savePath, 'wb'); if (!$fp) { return $this->jsonResponse(['success' => false, 'error' => '创建文件失败']); } $success = true; for ($i = 0; $i < $totalChunks; $i++) { $chunkFileName = $this->getChunkFileName($backupId, $filePath, $i); if (!file_exists($chunkFileName)) { $success = false; break; } $chunkData = file_get_contents($chunkFileName); if ($chunkData === false) { $success = false; break; } fwrite($fp, $chunkData); // 删除分块文件 unlink($chunkFileName); } fclose($fp); if (!$success) { @unlink($savePath); return $this->jsonResponse(['success' => false, 'error' => '合并分块失败']); } // 验证合并后的文件 if ($fileMd5) { $actualMd5 = md5_file($savePath); if ($actualMd5 !== $fileMd5) { @unlink($savePath); return $this->jsonResponse(['success' => false, 'error' => '文件校验失败']); } } // 验证文件大小 $actualSize = filesize($savePath); if ($actualSize != $fileSize) { @unlink($savePath); return $this->jsonResponse(['success' => false, 'error' => '文件大小不匹配']); } // 记录备份信息 $this->logBackup($backupId, $filePath, $fileSize, $fileMd5, $needOverwrite); return $this->jsonResponse([ 'success' => true, 'message' => $needOverwrite ? '文件覆盖合并成功' : '文件合并成功', 'path' => $savePath, 'overwritten' => $needOverwrite ]); } /** * 清理分块文件 */ private function cleanupChunks($backupId, $filePath, $totalChunks) { for ($i = 0; $i < $totalChunks; $i++) { $chunkFileName = $this->getChunkFileName($backupId, $filePath, $i); if (file_exists($chunkFileName)) { @unlink($chunkFileName); } } } /** * 获取分块文件名 */ private function getChunkFileName($backupId, $filePath, $chunkIndex) { $safePath = md5($filePath); return $this->backupBaseDir . '/' . $this->chunkDir . '/' . $backupId . '_' . $safePath . '_' . $chunkIndex . '.chunk'; } /** * 获取保存路径 */ private function getSavePath($backupId, $filePath) { // 创建备份ID目录(基于目录的固定ID) $backupDir = $this->backupBaseDir . '/' . $backupId; $this->createDirectory($backupDir); return $backupDir . '/' . $filePath; } /** * 记录备份信息 */ private function logBackup($backupId, $filePath, $fileSize, $fileMd5, $overwritten = false) { $logFile = $this->backupBaseDir . '/' . $backupId . '/backup_log.json'; $logData = []; if (file_exists($logFile)) { $content = file_get_contents($logFile); $logData = json_decode($content, true); if (!is_array($logData)) { $logData = []; } } $logData[$filePath] = [ 'size' => $fileSize, 'md5' => $fileMd5, 'backup_time' => date('Y-m-d H:i:s'), 'timestamp' => time(), 'overwritten' => $overwritten ]; file_put_contents($logFile, json_encode($logData, JSON_PRETTY_PRINT)); // 同时更新主备份历史 $this->updateBackupHistory($backupId, $filePath, $fileSize, $overwritten); } /** * 更新备份历史 */ private function updateBackupHistory($backupId, $filePath, $fileSize, $overwritten = false) { $history = []; if (file_exists($this->historyFile)) { $content = file_get_contents($this->historyFile); $history = json_decode($content, true); if (!is_array($history)) { $history = []; } } if (!isset($history[$backupId])) { $history[$backupId] = [ 'first_backup' => date('Y-m-d H:i:s'), 'last_backup' => date('Y-m-d H:i:s'), 'files' => [], 'total_size' => 0, 'total_files' => 0, 'overwritten_count' => 0 ]; } // 如果是覆盖操作且文件已存在,更新统计 $existingSize = 0; if ($overwritten && isset($history[$backupId]['files'][$filePath])) { $existingSize = $history[$backupId]['files'][$filePath]['size']; $history[$backupId]['overwritten_count']++; } $history[$backupId]['files'][$filePath] = [ 'size' => $fileSize, 'backup_time' => date('Y-m-d H:i:s'), 'overwritten' => $overwritten ]; // 更新总大小(减去被覆盖文件的大小) $history[$backupId]['total_size'] = $history[$backupId]['total_size'] - $existingSize + $fileSize; $history[$backupId]['total_files'] = count($history[$backupId]['files']); $history[$backupId]['last_backup'] = date('Y-m-d H:i:s'); file_put_contents($this->historyFile, json_encode($history, JSON_PRETTY_PRINT)); } /** * 测试连接 */ private function testConnection() { return $this->jsonResponse([ 'success' => true, 'message' => 'API连接正常', 'server_time' => date('Y-m-d H:i:s'), 'php_version' => PHP_VERSION, 'storage_path' => realpath($this->backupBaseDir), 'max_file_size' => $this->formatSize($this->maxFileSize) ]); } /** * 格式化文件大小 */ 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]; } /** * JSON响应 */ private function jsonResponse($data) { header('Content-Type: application/json; charset=utf-8'); echo json_encode($data); exit; } } // 处理请求 $receiver = new FileBackupReceiver(); $receiver->handleRequest(); ?>