Files
shop-platform/scripts/generate_deploy_package.php

801 lines
28 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// enhanced_package_generator.php
#!/usr/bin/env php
/**
* 部署包生成器
* 生成部署包, 只包含新增、修改的文件
*/
class DeployPackageGenerator
{
private string $sourceDir; // 目录A新版本
private string $targetDir; // 目录B旧版本
private string $outputPath; // 输出ZIP文件路径
private array $config; // 配置选项
private array $log = []; // 日志记录
private array $comparisonResult = []; // 比较结果
public function __construct(string $sourceDir, string $targetDir, string $outputPath, array $config = [])
{
$this->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 <path> 源目录(新版本)\n";
echo " --target, -t <path> 目标目录(旧版本)\n";
echo " --output, -o <path> 输出ZIP文件路径\n";
echo " --config, -c <path> 配置文件路径\n";
echo " --compare <method> 比较方法 (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);
}
?>