sourceDir = rtrim($sourceDir, DIRECTORY_SEPARATOR); $this->targetDir = rtrim($targetDir, DIRECTORY_SEPARATOR); $this->outputPath = $outputPath; $this->config = array_merge([ 'exclude_patterns' => [ '/\.git/', // Git目录 '/\.svn/', // SVN目录 '/node_modules/', // Node模块 '/vendor/', // Composer依赖 '/cache/', // 缓存目录 '/logs/', // 日志目录 '/temp/', // 临时目录 '/tmp/', // 临时目录 '/backup/', // 备份目录 '/runtime/', // 运行时目录 '/upload/', // 上传目录 '/\.DS_Store$/', // Mac系统文件 '/Thumbs\.db$/', // Windows缩略图 '/\.log$/', // 日志文件 '/\.tmp$/', // 临时文件 '/\.env$/', // 环境变量 '/composer\.lock$/', // Composer依赖锁文件 ], 'include_patterns' => [], // 包含模式(优先于排除) 'compare_method' => 'content', // content, size, mtime, hash 'hash_algorithm' => 'md5', // md5, sha1, crc32 'min_file_size' => 0, // 最小文件大小(字节) 'max_file_size' => 10485760, // 最大文件大小(10MB) 'dry_run' => false, // 试运行 'verbose' => false, // 详细输出 'backup_old' => true, // 备份旧版本 'compression_level' => 9, // ZIP压缩级别 0-9 ], $config); $this->validateDirectories(); } /** * 验证目录有效性 */ private function validateDirectories(): void { if (!is_dir($this->sourceDir)) { throw new InvalidArgumentException("源目录不存在: {$this->sourceDir}"); } if (!is_readable($this->sourceDir)) { throw new InvalidArgumentException("源目录不可读: {$this->sourceDir}"); } if (!is_dir($this->targetDir)) { $this->logMessage('WARNING', "目标目录不存在,将创建完整部署包: {$this->targetDir}"); } elseif (!is_readable($this->targetDir)) { throw new InvalidArgumentException("目标目录不可读: {$this->targetDir}"); } $outputDir = dirname($this->outputPath); if (!is_dir($outputDir)) { throw new InvalidArgumentException("输出目录不存在: {$outputDir}"); } if (!is_writable($outputDir)) { throw new InvalidArgumentException("输出目录不可写: {$outputDir}"); } } /** * 生成部署包 */ public function generate(): array { $this->logMessage('INFO', '开始生成部署包'); $this->logMessage('INFO', "源目录: {$this->sourceDir}"); $this->logMessage('INFO', "目标目录: {$this->targetDir}"); $this->logMessage('INFO', "输出文件: {$this->outputPath}"); try { // 步骤1:分析目录结构 $this->logMessage('INFO', '分析目录结构...'); $sourceFiles = $this->analyzeDirectory($this->sourceDir); $targetFiles = is_dir($this->targetDir) ? $this->analyzeDirectory($this->targetDir) : []; $this->logMessage('INFO', "源目录文件数: " . count($sourceFiles)); $this->logMessage('INFO', "目标目录文件数: " . count($targetFiles)); // 步骤2:比较文件差异 $this->logMessage('INFO', '比较文件差异...'); $diffFiles = $this->compareFiles($sourceFiles, $targetFiles); // 步骤3:生成部署包 $this->logMessage('INFO', '生成部署ZIP包...'); $result = $this->createDeployPackage($diffFiles); $this->logMessage('SUCCESS', '部署包生成完成'); return array_merge($result, [ 'comparison' => $this->comparisonResult ]); } catch (Exception $e) { $this->logMessage('ERROR', "生成失败: " . $e->getMessage()); throw $e; } } /** * 分析目录结构 */ private function analyzeDirectory(string $directory): array { $files = []; $basePath = rtrim($directory, DIRECTORY_SEPARATOR); $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $directory, FilesystemIterator::SKIP_DOTS | FilesystemIterator::FOLLOW_SYMLINKS ), RecursiveIteratorIterator::SELF_FIRST ); foreach ($iterator as $file) { if ($file->isDir()) { continue; } $filePath = $file->getPathname(); $relativePath = $this->getRelativePath($filePath, $basePath); // 检查是否应该排除 if ($this->shouldExclude($relativePath)) { continue; } // 检查文件大小限制 $fileSize = $file->getSize(); if ($fileSize < $this->config['min_file_size'] || $fileSize > $this->config['max_file_size']) { $this->logMessage('DEBUG', "跳过文件(大小限制): {$relativePath} ({$fileSize} bytes)"); continue; } $files[$relativePath] = [ 'full_path' => $filePath, 'size' => $fileSize, 'modified' => $file->getMTime(), 'permissions' => $file->getPerms(), 'hash' => $this->calculateFileHash($filePath) ]; } return $files; } /** * 比较文件差异 */ private function compareFiles(array $sourceFiles, array $targetFiles): array { $diffFiles = [ 'added' => [], // 新增文件 'modified' => [], // 修改文件 'deleted' => [], // 删除文件(记录以便清理) 'unchanged' => [] // 未改变文件 ]; $this->comparisonResult = [ 'total_source_files' => count($sourceFiles), 'total_target_files' => count($targetFiles), 'files_analyzed' => 0 ]; // 检查新增和修改的文件 foreach ($sourceFiles as $relativePath => $sourceFileInfo) { $this->comparisonResult['files_analyzed']++; if (!isset($targetFiles[$relativePath])) { // 新增文件 $diffFiles['added'][$relativePath] = $sourceFileInfo; $this->logMessage('INFO', "新增文件: {$relativePath}"); } else { // 文件存在,检查是否需要更新 $targetFileInfo = $targetFiles[$relativePath]; if ($this->needsUpdate($sourceFileInfo, $targetFileInfo)) { $diffFiles['modified'][$relativePath] = $sourceFileInfo; $this->logMessage('INFO', "修改文件: {$relativePath}"); } else { $diffFiles['unchanged'][$relativePath] = $sourceFileInfo; $this->logMessage('DEBUG', "未改变: {$relativePath}"); } } } // 检查需要删除的文件 foreach ($targetFiles as $relativePath => $targetFileInfo) { if (!isset($sourceFiles[$relativePath])) { $diffFiles['deleted'][$relativePath] = $targetFileInfo; $this->logMessage('INFO', "待删除文件: {$relativePath}"); } } $this->comparisonResult = array_merge($this->comparisonResult, [ 'added_files' => count($diffFiles['added']), 'modified_files' => count($diffFiles['modified']), 'deleted_files' => count($diffFiles['deleted']), 'unchanged_files' => count($diffFiles['unchanged']) ]); $this->logMessage('INFO', "比较完成: 新增{$this->comparisonResult['added_files']}个, 修改{$this->comparisonResult['modified_files']}个, 删除{$this->comparisonResult['deleted_files']}个"); return $diffFiles; } /** * 检查文件是否需要更新 */ private function needsUpdate(array $sourceFile, array $targetFile): bool { $method = $this->config['compare_method']; switch ($method) { case 'content': // 内容比较(最准确) return $sourceFile['hash'] !== $targetFile['hash']; case 'size': // 大小比较 return $sourceFile['size'] !== $targetFile['size']; case 'mtime': // 修改时间比较 return $sourceFile['modified'] > $targetFile['modified']; case 'hash': default: // 哈希比较(默认) return $sourceFile['hash'] !== $targetFile['hash']; } } /** * 创建部署包 */ protected function createDeployPackage(array $diffFiles): array { if ($this->config['dry_run']) { $this->logMessage('DRY_RUN', "试运行模式,将创建包含 " . (count($diffFiles['added']) + count($diffFiles['modified'])) . " 个文件的ZIP包"); return $this->simulatePackageCreation($diffFiles); } $zip = new ZipArchive(); $openResult = $zip->open($this->outputPath, ZipArchive::CREATE | ZipArchive::OVERWRITE); if ($openResult !== true) { throw new RuntimeException("无法创建ZIP文件,错误代码: {$openResult}"); } $addedCount = 0; $modifiedCount = 0; // 添加新增和修改的文件 $filesToAdd = array_merge($diffFiles['added'], $diffFiles['modified']); $fileIndex = 0; foreach ($filesToAdd as $relativePath => $fileInfo) { try { // 确保目录结构被正确保留 // 首先尝试直接添加文件 if ($zip->addFile($fileInfo['full_path'], $relativePath)) { $zip->setCompressionName($relativePath, ZipArchive::CM_DEFLATE); $zip->setCompressionIndex($fileIndex, ZipArchive::CM_DEFLATE, $this->config['compression_level']); $fileIndex++; if (isset($diffFiles['added'][$relativePath])) { $addedCount++; } else { $modifiedCount++; } $this->logMessage('DEBUG', "添加到ZIP: {$relativePath}"); } else { // 如果直接添加失败,尝试先创建目录结构 $dir = dirname($relativePath); if ($dir !== '.' && $dir !== '') { // 确保所有父目录都被创建 $parts = explode(DIRECTORY_SEPARATOR, $dir); $currentPath = ''; foreach ($parts as $part) { $currentPath .= $part . DIRECTORY_SEPARATOR; $currentPath = rtrim($currentPath, DIRECTORY_SEPARATOR); if ($currentPath !== '' && !$zip->addEmptyDir($currentPath)) { // 添加空目录失败可能是因为目录已存在,继续尝试 continue; } } } // 再次尝试添加文件 if ($zip->addFile($fileInfo['full_path'], $relativePath)) { $zip->setCompressionName($relativePath, ZipArchive::CM_DEFLATE); $zip->setCompressionIndex($fileIndex, ZipArchive::CM_DEFLATE, $this->config['compression_level']); $fileIndex++; if (isset($diffFiles['added'][$relativePath])) { $addedCount++; } else { $modifiedCount++; } $this->logMessage('DEBUG', "添加到ZIP (创建目录后): {$relativePath}"); } else { $this->logMessage('WARNING', "添加文件失败: {$relativePath}"); } } } catch (Exception $e) { $this->logMessage('ERROR', "处理文件失败 {$relativePath}: " . $e->getMessage()); } } // 创建删除列表文件 if (!empty($diffFiles['deleted'])) { $deleteListContent = "# 需要删除的文件列表\n# 生成时间: " . date('Y-m-d H:i:s') . "\n\n"; foreach (array_keys($diffFiles['deleted']) as $fileToDelete) { $deleteListContent .= $fileToDelete . "\n"; } $zip->addFromString('_DELETE_LIST.txt', $deleteListContent); $this->logMessage('INFO', "创建删除列表,包含 " . count($diffFiles['deleted']) . " 个文件"); } // 创建部署清单 $manifest = $this->createManifest($diffFiles); $zip->addFromString('_DEPLOY_MANIFEST.json', json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); // 关闭ZIP文件 if (!$zip->close()) { throw new RuntimeException("ZIP文件保存失败"); } // 验证ZIP文件 $zipSize = filesize($this->outputPath); $fileCount = $zip->numFiles; $result = [ 'output_path' => $this->outputPath, 'file_size' => $zipSize, 'file_size_human' => $this->formatBytes($zipSize), 'files_added' => $addedCount, 'files_modified' => $modifiedCount, 'files_deleted' => count($diffFiles['deleted']), 'total_files' => $addedCount + $modifiedCount, 'compression_level' => $this->config['compression_level'], 'created_at' => date('Y-m-d H:i:s') ]; $this->logMessage('SUCCESS', "部署包创建成功: {$result['file_size_human']}, 包含 {$result['total_files']} 个文件"); return $result; } /** * 创建部署清单 */ private function createManifest(array $diffFiles): array { return [ 'metadata' => [ 'generator' => 'DeployPackageGenerator', 'version' => '1.0', 'created_at' => date('c'), 'source_dir' => $this->sourceDir, 'target_dir' => $this->targetDir, 'compare_method' => $this->config['compare_method'] ], 'statistics' => $this->comparisonResult, 'files' => [ 'added' => array_keys($diffFiles['added']), 'modified' => array_keys($diffFiles['modified']), 'deleted' => array_keys($diffFiles['deleted']) ], 'file_details' => [ 'added' => $diffFiles['added'], 'modified' => $diffFiles['modified'] ] ]; } /** * 试运行模式 */ private function simulatePackageCreation(array $diffFiles): array { $totalSize = 0; foreach (array_merge($diffFiles['added'], $diffFiles['modified']) as $fileInfo) { $totalSize += $fileInfo['size']; } return [ 'output_path' => $this->outputPath, 'file_size' => $totalSize, 'file_size_human' => $this->formatBytes($totalSize), 'files_added' => count($diffFiles['added']), 'files_modified' => count($diffFiles['modified']), 'files_deleted' => count($diffFiles['deleted']), 'total_files' => count($diffFiles['added']) + count($diffFiles['modified']), 'dry_run' => true, 'message' => '试运行模式 - 未实际创建文件' ]; } /** * 检查是否应该排除文件 */ private function shouldExclude(string $relativePath): bool { // 首先检查包含模式 foreach ($this->config['include_patterns'] as $pattern) { if (preg_match($pattern, $relativePath)) { return false; } } // 然后检查排除模式 foreach ($this->config['exclude_patterns'] as $pattern) { if (preg_match($pattern, $relativePath)) { $this->logMessage('DEBUG', "排除文件: {$relativePath} (匹配模式: {$pattern})"); return true; } } return false; } /** * 计算文件哈希 */ private function calculateFileHash(string $filePath): string { $algorithm = $this->config['hash_algorithm']; switch ($algorithm) { case 'md5': return md5_file($filePath); case 'sha1': return sha1_file($filePath); case 'crc32': return hash_file('crc32b', $filePath); default: return md5_file($filePath); } } /** * 获取相对路径 */ private function getRelativePath(string $path, string $basePath): string { $basePath = rtrim($basePath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; return str_replace($basePath, '', $path); } /** * 格式化字节大小 */ private function formatBytes(int $bytes, int $precision = 2): string { $units = ['B', 'KB', 'MB', 'GB', 'TB']; $bytes = max($bytes, 0); $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); $pow = min($pow, count($units) - 1); $bytes /= pow(1024, $pow); return round($bytes, $precision) . ' ' . $units[$pow]; } /** * 记录日志 */ 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->config['verbose'] || in_array($level, ['ERROR', 'SUCCESS', 'WARNING'])) { echo $logEntry . PHP_EOL; } } /** * 获取日志 */ public function getLog(): array { return $this->log; } } /** * 增强版部署包生成器 * 生成包含删除清单的部署包 */ class EnhancedPackageGenerator extends DeployPackageGenerator { /** * 创建部署包(增强版) */ protected function createDeployPackage(array $diffFiles): array { $result = parent::createDeployPackage($diffFiles); // 添加删除清单 if (!empty($diffFiles['deleted'])) { $this->addDeleteManifest($diffFiles['deleted']); } return $result; } /** * 添加删除清单文件 */ private function addDeleteManifest(array $deletedFiles): void { if ($this->config['dry_run']) { $this->logMessage('DRY_RUN', "将创建删除清单,包含 " . count($deletedFiles) . " 个文件"); return; } $zip = new ZipArchive(); if ($zip->open($this->outputPath) !== true) { throw new RuntimeException("无法打开ZIP文件添加删除清单"); } // 文本格式删除清单 $textContent = $this->createTextDeleteList($deletedFiles); $zip->addFromString('DELETE_LIST.txt', $textContent); // JSON格式删除清单 $jsonContent = $this->createJsonDeleteList($deletedFiles); $zip->addFromString('delete_manifest.json', $jsonContent); $zip->close(); $this->logMessage('INFO', "删除清单已添加: " . count($deletedFiles) . " 个待删除文件"); } /** * 创建文本格式删除清单 */ private function createTextDeleteList(array $deletedFiles): string { $content = "# 自动部署删除清单\n"; $content .= "# 生成时间: " . date('Y-m-d H:i:s') . "\n"; $content .= "# 此文件列出了需要删除的文件列表\n\n"; foreach (array_keys($deletedFiles) as $filePath) { $content .= $filePath . "\n"; } $content .= "\n# 注释以 # 开头\n"; $content .= "# 空行和注释行将被忽略\n"; return $content; } /** * 创建JSON格式删除清单 */ private function createJsonDeleteList(array $deletedFiles): string { $manifest = [ 'version' => '1.0', 'generated_at' => date('c'), 'description' => '自动部署删除清单', 'files_to_delete' => array_keys($deletedFiles), 'total_files' => count($deletedFiles), 'metadata' => [ 'safe_deletion' => true, 'backup_recommended' => true, 'generator' => 'EnhancedPackageGenerator' ] ]; return json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); } } // 命令行接口 class PackageGeneratorCLI { private array $options; public function __construct(array $argv) { $this->options = $this->parseOptions($argv); } public function run(): void { try { $this->showBanner(); if ($this->options['help'] ?? false) { $this->showHelp(); exit(0); } // 验证参数 $sourceDir = $this->options['source'] ?? null; $targetDir = $this->options['target'] ?? null; $outputFile = $this->options['output'] ?? null; if (!$sourceDir || !$targetDir || !$outputFile) { echo "错误: 必须指定 --source, --target 和 --output 参数\n"; $this->showHelp(); exit(1); } // 构建配置 $config = [ 'dry_run' => $this->options['dry-run'] ?? false, 'verbose' => $this->options['verbose'] ?? false, 'compare_method' => $this->options['compare'] ?? 'content' ]; // 加载配置文件 if (isset($this->options['config'])) { $config = array_merge($config, $this->loadConfigFile($this->options['config'])); } // 创建生成器实例 $generator = new DeployPackageGenerator($sourceDir, $targetDir, $outputFile, $config); // 生成部署包 $result = $generator->generate(); // 显示结果 $this->showResults($result); } catch (Exception $e) { echo "错误: " . $e->getMessage() . PHP_EOL; exit(1); } } private function parseOptions(array $argv): array { $options = [ 'help' => false, 'verbose' => false, 'dry-run' => false ]; for ($i = 1; $i < count($argv); $i++) { $arg = $argv[$i]; switch ($arg) { case '--help': case '-h': $options['help'] = true; break; case '--verbose': case '-v': $options['verbose'] = true; break; case '--dry-run': case '-d': $options['dry-run'] = true; break; case '--source': case '-s': $options['source'] = $argv[++$i] ?? null; break; case '--target': case '-t': $options['target'] = $argv[++$i] ?? null; break; case '--output': case '-o': $options['output'] = $argv[++$i] ?? null; break; case '--config': case '-c': $options['config'] = $argv[++$i] ?? null; break; case '--compare': $options['compare'] = $argv[++$i] ?? 'content'; break; default: if (substr($arg, 0, 1) === '-') { echo "警告: 未知选项 {$arg}\n"; } break; } } return $options; } private function loadConfigFile(string $configPath): array { if (!file_exists($configPath)) { throw new InvalidArgumentException("配置文件不存在: {$configPath}"); } $config = include $configPath; if (!is_array($config)) { throw new InvalidArgumentException("无效的配置文件: {$configPath}"); } return $config; } private function showBanner(): void { echo "========================================\n"; echo " 部署包生成工具 v1.0\n"; echo " PHP 7.4 兼容版本\n"; echo "========================================\n\n"; } private function showHelp(): void { echo "用法: php generate_deploy_package.php [选项]\n\n"; echo "选项:\n"; echo " --source, -s 源目录(新版本)\n"; echo " --target, -t 目标目录(旧版本)\n"; echo " --output, -o 输出ZIP文件路径\n"; echo " --config, -c 配置文件路径\n"; echo " --compare 比较方法 (content, size, mtime, hash)\n"; echo " --dry-run, -d 试运行,不实际创建文件\n"; echo " --verbose, -v 显示详细输出\n"; echo " --help, -h 显示此帮助信息\n\n"; echo "示例:\n"; echo " php generate_deploy_package.php --source ./new_version --target ./old_version --output update.zip\n"; echo " php generate_deploy_package.php -s /var/www/new -t /var/www/old -o patch.zip --dry-run\n"; echo " php generate_deploy_package.php --source ./v2.0 --target ./v1.0 --output deploy.zip --config deploy.conf.php\n"; } private function showResults(array $result): void { echo "\n生成结果:\n"; echo "========================================\n"; echo "输出文件: {$result['output_path']}\n"; echo "文件大小: {$result['file_size_human']}\n"; echo "包含文件: {$result['total_files']} 个\n"; echo " - 新增: {$result['files_added']} 个\n"; echo " - 修改: {$result['files_modified']} 个\n"; echo " - 删除: {$result['files_deleted']} 个\n"; if (isset($result['dry_run'])) { echo "模式: 试运行(未实际创建文件)\n"; } echo "========================================\n"; } } // 主程序入口 if (PHP_SAPI === 'cli') { $cli = new PackageGeneratorCLI($argv); $cli->run(); } else { echo "此脚本只能在命令行模式下运行\n"; exit(1); } ?>