zipPath = $zipPath; $this->targetDir = rtrim($targetDir, DIRECTORY_SEPARATOR); $this->config = array_merge([ 'dry_run' => false, 'verbose' => false, 'backup_enabled' => true, 'backup_dir' => $this->targetDir . '/backups/deploy', 'delete_enabled' => true, 'delete_list_file' => '_DELETE_LIST.txt', 'delete_manifest_file' => '_DELETE_MANIFEST.json', 'safe_delete' => true, 'max_backups' => 5, 'confirmation_required' => false ], $config); $this->dryRun = $this->config['dry_run']; $this->verbose = $this->config['verbose']; $this->validatePaths(); } /** * 获取相对路径 */ private function getRelativePath(string $path, string $basePath): string { $basePath = rtrim($basePath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; return str_replace($basePath, '', $path); } /** * 执行部署(包含删除功能) */ public function deploy(): array { $this->logMessage('INFO', '开始增强版自动部署流程'); $this->logMessage('INFO', "ZIP文件: {$this->zipPath}"); $this->logMessage('INFO', "目标目录: {$this->targetDir}"); try { // 打开ZIP文件 $zip = new ZipArchive(); if ($zip->open($this->zipPath) !== true) { throw new RuntimeException("无法打开ZIP文件: {$this->zipPath}"); } $this->logMessage('INFO', 'ZIP文件打开成功'); // 步骤1:读取删除清单 $this->loadDeleteList($zip); // 步骤2:分析ZIP文件结构 $zipFiles = $this->analyzeZipStructure($zip); $this->logMessage('INFO', "ZIP包中包含 {$zipFiles['file_count']} 个文件"); // 步骤3:分析目标目录结构 $targetFiles = $this->analyzeTargetDirectory(); $this->logMessage('INFO', "目标目录中包含 {$targetFiles['file_count']} 个文件"); // 步骤4:确认删除操作(如果需要) if ($this->config['confirmation_required'] && !empty($this->deleteList)) { if (!$this->confirmDeletion()) { $zip->close(); throw new RuntimeException("用户取消部署操作"); } } // 步骤5:执行删除操作 $deleteResult = $this->executeDeletion(); // 步骤6:部署新文件和更新文件 $deployResult = $this->deployFiles($zip, $zipFiles, $targetFiles); // 关闭ZIP文件 $zip->close(); // 合并结果 $result = array_merge($deployResult, [ 'deletion' => $deleteResult ]); $this->logMessage('SUCCESS', '部署完成'); return $result; } catch (Exception $e) { $this->logMessage('ERROR', "部署失败: " . $e->getMessage()); throw $e; } } /** * 分析ZIP文件结构 */ private function analyzeZipStructure(ZipArchive $zip): array { $files = []; $fileCount = 0; $totalSize = 0; for ($i = 0; $i < $zip->numFiles; $i++) { $fileInfo = $zip->statIndex($i); if ($fileInfo === false) { continue; } $fileName = $fileInfo['name']; // 跳过目录条目 if (substr($fileName, -1) === '/') { continue; } $files[$fileName] = [ 'size' => $fileInfo['size'], 'compressed_size' => $fileInfo['comp_size'], 'modified' => $fileInfo['mtime'], 'crc32' => $fileInfo['crc'] ]; $fileCount++; $totalSize += $fileInfo['size']; } return [ 'files' => $files, 'fileCount' => $fileCount, 'totalSize' => $totalSize ]; } /** * 分析目标目录结构 */ private function analyzeTargetDirectory(): array { $files = []; $fileCount = 0; $totalSize = 0; $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $this->targetDir, RecursiveDirectoryIterator::SKIP_DOTS ), RecursiveIteratorIterator::SELF_FIRST ); foreach ($iterator as $file) { if ($file->isDir()) { continue; } $relativePath = $this->getRelativePath($file->getPathname(), $this->targetDir); $files[$relativePath] = [ 'size' => $file->getSize(), 'modified' => $file->getMTime(), 'permissions' => $file->getPerms() ]; $fileCount++; $totalSize += $file->getSize(); } return [ 'files' => $files, 'fileCount' => $fileCount, 'totalSize' => $totalSize ]; } /** * 加载删除清单 */ private function loadDeleteList(ZipArchive $zip): void { $this->deleteList = []; // 尝试从多个可能的文件中读取删除清单 $possibleFiles = [ $this->config['delete_list_file'], 'DELETE_LIST.txt', 'delete.list', 'manifest.json', $this->config['delete_manifest_file'] ]; foreach ($possibleFiles as $listFile) { $content = $zip->getFromName($listFile); if ($content !== false) { $this->parseDeleteList($content, $listFile); break; } } if (empty($this->deleteList)) { $this->logMessage('INFO', '未找到删除清单文件,跳过删除操作'); } else { $this->logMessage('INFO', "找到删除清单,包含 " . count($this->deleteList) . " 个待删除文件"); } } /** * 解析删除清单 */ private function parseDeleteList(string $content, string $sourceFile): void { $this->logMessage('DEBUG', "从文件 {$sourceFile} 解析删除清单"); // 尝试解析JSON格式 if (strpos($sourceFile, '.json') !== false) { $data = json_decode($content, true); if ($data && isset($data['files_to_delete'])) { $this->deleteList = $data['files_to_delete']; return; } } // 解析文本格式(每行一个文件路径) $lines = explode("\n", $content); foreach ($lines as $line) { $line = trim($line); // 跳过空行和注释 if (empty($line) || $line[0] === '#' || $line[0] === ';') { continue; } $this->deleteList[] = $line; } // 去重 $this->deleteList = array_unique($this->deleteList); } /** * 执行删除操作 */ private function executeDeletion(): array { if (!$this->config['delete_enabled'] || empty($this->deleteList)) { return [ 'total' => 0, 'deleted' => 0, 'skipped' => 0, 'errors' => 0, 'details' => [] ]; } $result = [ 'total' => count($this->deleteList), 'deleted' => 0, 'skipped' => 0, 'errors' => 0, 'details' => [] ]; $this->logMessage('INFO', "开始执行删除操作,共 {$result['total']} 个文件"); foreach ($this->deleteList as $fileToDelete) { try { $fullPath = $this->targetDir . DIRECTORY_SEPARATOR . $fileToDelete; if (!$this->shouldDeleteFile($fullPath, $fileToDelete)) { $result['skipped']++; $result['details'][] = [ 'file' => $fileToDelete, 'action' => 'skipped', 'reason' => '安全检查未通过', 'success' => true ]; continue; } $deleteResult = $this->deleteFile($fullPath, $fileToDelete); if ($deleteResult) { $result['deleted']++; $result['details'][] = [ 'file' => $fileToDelete, 'action' => 'deleted', 'success' => true ]; } else { $result['skipped']++; $result['details'][] = [ 'file' => $fileToDelete, 'action' => 'skipped', 'reason' => '文件不存在或无法删除', 'success' => true ]; } } catch (Exception $e) { $result['errors']++; $result['details'][] = [ 'file' => $fileToDelete, 'action' => 'error', 'error' => $e->getMessage(), 'success' => false ]; $this->logMessage('ERROR', "删除文件失败 {$fileToDelete}: " . $e->getMessage()); } } $this->logMessage('SUCCESS', "删除操作完成: 成功 {$result['deleted']} 个, 跳过 {$result['skipped']} 个, 错误 {$result['errors']} 个"); return $result; } /** * 检查是否应该删除文件 */ private function shouldDeleteFile(string $fullPath, string $relativePath): bool { // 安全检查:确保路径在目标目录内 $realTarget = realpath($this->targetDir); $realFile = realpath($fullPath); if ($realFile === false || strpos($realFile, $realTarget) !== 0) { $this->logMessage('WARNING', "安全警告: 跳过删除外部文件 {$relativePath}"); return false; } // 检查重要文件保护 $protectedPatterns = [ '/\.env$/', '/config\.php$/', '/database\.php$/', '/\.htaccess$/', '/web\.config$/', '/index\.php$/', '/composer\.json$/', '/composer\.lock$/', '/package\.json$/', '/yarn\.lock$/', '/\.gitignore$/' ]; foreach ($protectedPatterns as $pattern) { if (preg_match($pattern, $relativePath)) { $this->logMessage('WARNING', "保护文件: 跳过删除 {$relativePath}"); return false; } } // 检查文件是否存在 if (!file_exists($fullPath)) { $this->logMessage('DEBUG', "文件不存在,跳过删除: {$relativePath}"); return false; } // 检查是否为目录 if (is_dir($fullPath)) { $this->logMessage('WARNING', "跳过删除目录: {$relativePath}"); return false; } return true; } /** * 删除单个文件 */ private function deleteFile(string $fullPath, string $relativePath): bool { if ($this->dryRun) { $this->logMessage('DRY_RUN', "将删除文件: {$relativePath}"); return true; } // 备份文件(如果启用) if ($this->config['backup_enabled']) { $this->backupFile($fullPath, $relativePath); } // 删除文件 if (unlink($fullPath)) { $this->logMessage('INFO', "文件已删除: {$relativePath}"); // 尝试删除空目录 $this->cleanupEmptyDirectories(dirname($fullPath)); return true; } else { $this->logMessage('ERROR', "无法删除文件: {$relativePath}"); return false; } } /** * 备份文件 */ private function backupFile(string $filePath, string $relativePath): void { if (!file_exists($filePath)) { return; } $backupDir = $this->config['backup_dir']; if (!is_dir($backupDir)) { mkdir($backupDir, 0755, true); } $backupPath = $backupDir . DIRECTORY_SEPARATOR . date('Ymd_His') . '_' . str_replace('/', '_', $relativePath); $backupDir = dirname($backupPath); if (!is_dir($backupDir)) { mkdir($backupDir, 0755, true); } if (copy($filePath, $backupPath)) { $this->logMessage('INFO', "文件已备份: {$backupPath}"); } else { $this->logMessage('WARNING', "文件备份失败: {$relativePath}"); } } /** * 清理空目录 */ private function cleanupEmptyDirectories(string $directory): void { $currentDir = $directory; $baseDir = realpath($this->targetDir); while ($currentDir !== $baseDir && is_dir($currentDir)) { // 检查目录是否为空 $files = scandir($currentDir); if (count($files) === 2) { // 只有 . 和 .. if ($this->dryRun) { $this->logMessage('DRY_RUN', "将删除空目录: {$currentDir}"); } else { if (rmdir($currentDir)) { $this->logMessage('INFO', "空目录已删除: {$currentDir}"); } } $currentDir = dirname($currentDir); } else { break; } } } /** * 确认删除操作 */ private function confirmDeletion(): bool { echo "\n⚠️ 警告: 以下文件将被删除:\n"; echo "========================================\n"; foreach ($this->deleteList as $index => $file) { echo sprintf("%3d. %s\n", $index + 1, $file); } echo "========================================\n"; echo "总共 " . count($this->deleteList) . " 个文件将被删除\n\n"; echo "是否继续?(y/N): "; $handle = fopen('php://stdin', 'r'); $input = trim(fgets($handle)); fclose($handle); return strtolower($input) === 'y'; } /** * 部署文件(从父类继承或实现) */ private function deployFiles(ZipArchive $zip, array $zipFiles, array $targetFiles): array { // 这里实现文件部署逻辑,与之前的AutoDeployer类似 // 包括添加新文件和更新现有文件 $result = [ 'added' => 0, 'updated' => 0, 'skipped' => 0, 'errors' => 0, 'details' => [] ]; // 实现文件部署逻辑... return $result; } /** * 记录日志 */ private function logMessage(string $level, string $message): void { $timestamp = date('Y-m-d H:i:s'); $logEntry = "[{$timestamp}] [{$level}] {$message}"; $this->log[] = $logEntry; if ($this->verbose || in_array($level, ['ERROR', 'SUCCESS', 'WARNING'])) { echo $logEntry . PHP_EOL; } } /** * 验证路径 */ private function validatePaths(): void { if (!file_exists($this->zipPath)) { throw new InvalidArgumentException("ZIP文件不存在: {$this->zipPath}"); } if (!is_dir($this->targetDir)) { throw new InvalidArgumentException("目标目录不存在: {$this->targetDir}"); } } /** * 获取日志 */ public function getLog(): array { return $this->log; } } // 命令行接口 class EnhancedDeployerCLI { public function run(array $argv): void { try { $options = $this->parseOptions($argv); if ($options['help']) { $this->showHelp(); exit(0); } if (!$options['zip'] || !$options['target']) { echo "错误: 必须指定 --zip 和 --target 参数\n"; $this->showHelp(); exit(1); } $deployer = new EnhancedAutoDeployer($options['zip'], $options['target'], [ 'dry_run' => $options['dry-run'], 'verbose' => $options['verbose'], 'confirmation_required' => $options['confirm'] ]); $result = $deployer->deploy(); $this->showResults($result); } catch (Exception $e) { echo "错误: " . $e->getMessage() . PHP_EOL; exit(1); } } private function parseOptions(array $argv): array { $options = [ 'help' => false, 'dry-run' => false, 'verbose' => false, 'confirm' => false, 'zip' => null, 'target' => null ]; for ($i = 1; $i < count($argv); $i++) { switch ($argv[$i]) { case '--help': case '-h': $options['help'] = true; break; case '--dry-run': case '-d': $options['dry-run'] = true; break; case '--verbose': case '-v': $options['verbose'] = true; break; case '--confirm': case '-c': $options['confirm'] = true; break; case '--zip': $options['zip'] = $argv[++$i] ?? null; break; case '--target': $options['target'] = $argv[++$i] ?? null; break; } } return $options; } private function showHelp(): void { echo "增强版自动部署工具\n\n"; echo "用法: php enhanced_deploy.php [选项]\n\n"; echo "选项:\n"; echo " --zip ZIP文件路径 (必需)\n"; echo " --target 目标目录路径 (必需)\n"; echo " --dry-run, -d 试运行,不实际修改文件\n"; echo " --verbose, -v 显示详细输出\n"; echo " --confirm, -c 确认删除操作\n"; echo " --help, -h 显示此帮助信息\n\n"; echo "示例:\n"; echo " php enhanced_deploy.php --zip update.zip --target /var/www/html\n"; echo " php enhanced_deploy.php --zip patch.zip --target ./app --dry-run\n"; echo " php enhanced_deploy.php --zip deploy.zip --target /var/www --confirm\n"; } private function showResults(array $result): void { echo "\n部署结果:\n"; echo "========================================\n"; echo "文件部署:\n"; echo " 新增: {$result['added']} 个文件\n"; echo " 更新: {$result['updated']} 个文件\n"; echo " 跳过: {$result['skipped']} 个文件\n"; echo " 错误: {$result['errors']} 个文件\n\n"; echo "文件删除:\n"; echo " 总计: {$result['deletion']['total']} 个文件\n"; echo " 删除: {$result['deletion']['deleted']} 个文件\n"; echo " 跳过: {$result['deletion']['skipped']} 个文件\n"; echo " 错误: {$result['deletion']['errors']} 个文件\n"; echo "========================================\n"; } } // 主程序入口 if (PHP_SAPI === 'cli') { $cli = new EnhancedDeployerCLI(); $cli->run($argv); } else { echo "此脚本只能在命令行模式下运行\n"; exit(1); } ?>