fix: 解决访问没有埋点的问题

This commit is contained in:
2025-11-10 17:28:52 +08:00
parent e440631275
commit 57dea2ca87
38 changed files with 4172 additions and 1624 deletions

533
scripts/deploy.php Normal file
View File

@@ -0,0 +1,533 @@
<?php
// deploy.php
#!/usr/bin/env php
/**
* 文件自动部署脚本
* 功能比较ZIP包与目标目录的差异自动部署文件
* PHP 7.4 兼容版本
*/
declare(strict_types=1);
class AutoDeployer
{
private string $zipPath;
private string $targetDir;
private array $log = [];
private bool $dryRun = false;
private bool $verbose = false;
private bool $backup = false;
public function __construct(string $zipPath, string $targetDir, array $options = [])
{
$this->zipPath = $zipPath;
$this->targetDir = rtrim($targetDir, DIRECTORY_SEPARATOR);
$this->dryRun = $options['dryRun'] ?? false;
$this->verbose = $options['verbose'] ?? false;
$this->backup = $options['backup'] ?? false;
$this->validatePaths();
}
/**
* 验证路径有效性
*/
private function validatePaths(): void
{
if (!file_exists($this->zipPath)) {
throw new InvalidArgumentException("ZIP文件不存在: {$this->zipPath}");
}
if (!is_readable($this->zipPath)) {
throw new InvalidArgumentException("ZIP文件不可读: {$this->zipPath}");
}
if (!is_dir($this->targetDir)) {
throw new InvalidArgumentException("目标目录不存在: {$this->targetDir}");
}
if (!is_writable($this->targetDir)) {
throw new InvalidArgumentException("目标目录不可写: {$this->targetDir}");
}
}
/**
* 执行部署
*/
public function deploy(): array
{
$this->logMessage('INFO', '开始自动部署流程');
$this->logMessage('INFO', "ZIP文件: {$this->zipPath}");
$this->logMessage('INFO', "目标目录: {$this->targetDir}");
try {
// 打开ZIP文件
$zip = new ZipArchive();
$openResult = $zip->open($this->zipPath);
if ($openResult !== true) {
throw new RuntimeException("无法打开ZIP文件错误代码: {$openResult}");
}
$this->logMessage('INFO', 'ZIP文件打开成功');
// 分析ZIP文件结构
$zipFiles = $this->analyzeZipStructure($zip);
$this->logMessage('INFO', "ZIP包中包含 {$zipFiles['fileCount']} 个文件");
// 分析目标目录结构
$targetFiles = $this->analyzeTargetDirectory();
$this->logMessage('INFO', "目标目录中包含 {$targetFiles['fileCount']} 个文件");
// 比较差异并部署
$result = $this->compareAndDeploy($zip, $zipFiles, $targetFiles);
// 关闭ZIP文件
$zip->close();
$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 compareAndDeploy(ZipArchive $zip, array $zipFiles, array $targetFiles): array
{
$results = [
'added' => 0,
'updated' => 0,
'skipped' => 0,
'errors' => 0,
'details' => []
];
foreach ($zipFiles['files'] as $filePath => $zipFileInfo) {
$targetFilePath = $this->targetDir . DIRECTORY_SEPARATOR . $filePath;
$targetFileExists = isset($targetFiles['files'][$filePath]);
try {
if (!$targetFileExists) {
// 新文件 - 需要创建目录并复制文件
$result = $this->deployNewFile($zip, $filePath, $targetFilePath);
$results['added']++;
$results['details'][] = [
'file' => $filePath,
'action' => 'added',
'success' => true
];
} else {
// 文件已存在 - 检查是否需要更新
$needsUpdate = $this->needsUpdate($zip, $filePath, $targetFiles['files'][$filePath]);
if ($needsUpdate) {
$result = $this->deployUpdatedFile($zip, $filePath, $targetFilePath);
$results['updated']++;
$results['details'][] = [
'file' => $filePath,
'action' => 'updated',
'success' => true
];
} else {
$results['skipped']++;
$results['details'][] = [
'file' => $filePath,
'action' => 'skipped',
'success' => true
];
$this->logMessage('INFO', "文件无需更新: {$filePath}");
}
}
} catch (Exception $e) {
$results['errors']++;
$results['details'][] = [
'file' => $filePath,
'action' => 'error',
'success' => false,
'error' => $e->getMessage()
];
$this->logMessage('ERROR', "处理文件失败 {$filePath}: " . $e->getMessage());
}
}
return $results;
}
/**
* 部署新文件
*/
private function deployNewFile(ZipArchive $zip, string $filePath, string $targetFilePath): bool
{
$this->logMessage('INFO', "添加新文件: {$filePath}");
if ($this->dryRun) {
$this->logMessage('DRY_RUN', "将创建文件: {$targetFilePath}");
return true;
}
// 创建目录(如果需要)
$targetDir = dirname($targetFilePath);
if (!is_dir($targetDir)) {
if (!mkdir($targetDir, 0755, true)) {
throw new RuntimeException("无法创建目录: {$targetDir}");
}
$this->logMessage('INFO', "创建目录: {$targetDir}");
}
// 从ZIP中提取文件
$fileContent = $zip->getFromName($filePath);
if ($fileContent === false) {
throw new RuntimeException("无法从ZIP中读取文件: {$filePath}");
}
// 写入文件
if (file_put_contents($targetFilePath, $fileContent) === false) {
throw new RuntimeException("无法写入文件: {$targetFilePath}");
}
$this->logMessage('SUCCESS', "文件创建成功: {$filePath}");
return true;
}
/**
* 部署更新文件
*/
private function deployUpdatedFile(ZipArchive $zip, string $filePath, string $targetFilePath): bool
{
$this->logMessage('INFO', "更新文件: {$filePath}");
if ($this->dryRun) {
$this->logMessage('DRY_RUN', "将更新文件: {$targetFilePath}");
return true;
}
// 备份原文件(可选)
if ($this->backup && file_exists($targetFilePath)) {
$backupPath = $targetFilePath . '.backup.' . date('YmdHis');
if (copy($targetFilePath, $backupPath)) {
$this->logMessage('INFO', "文件已备份: {$backupPath}");
}
}
// 从ZIP中提取文件内容
$fileContent = $zip->getFromName($filePath);
if ($fileContent === false) {
throw new RuntimeException("无法从ZIP中读取文件: {$filePath}");
}
// 写入文件
if (file_put_contents($targetFilePath, $fileContent) === false) {
throw new RuntimeException("无法写入文件: {$targetFilePath}");
}
$this->logMessage('SUCCESS', "文件更新成功: {$filePath}");
return true;
}
/**
* 检查文件是否需要更新
*/
private function needsUpdate(ZipArchive $zip, string $filePath, array $targetFileInfo): bool
{
$zipFileInfo = $zip->statName($filePath);
if ($zipFileInfo === false) {
return false;
}
// 比较文件大小
if ($zipFileInfo['size'] != $targetFileInfo['size']) {
$this->logMessage('DEBUG', "文件大小不同: {$filePath} (ZIP: {$zipFileInfo['size']}, 目标: {$targetFileInfo['size']})");
return true;
}
// 比较修改时间ZIP时间可能不准确作为次要判断
if ($zipFileInfo['mtime'] > $targetFileInfo['modified']) {
$this->logMessage('DEBUG', "ZIP文件较新: {$filePath}");
return true;
}
// 比较CRC32校验最可靠的方法
$targetCrc = hash_file('crc32b', $this->targetDir . DIRECTORY_SEPARATOR . $filePath);
$zipCrc = sprintf('%08x', $zipFileInfo['crc']);
if (strtolower($targetCrc) !== strtolower($zipCrc)) {
$this->logMessage('DEBUG', "文件内容不同: {$filePath}");
return true;
}
return false;
}
/**
* 获取相对路径
*/
private function getRelativePath(string $path, string $basePath): string
{
$basePath = rtrim($basePath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
return str_replace($basePath, '', $path);
}
/**
* 记录日志
*/
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'])) {
echo $logEntry . PHP_EOL;
}
}
/**
* 获取部署日志
*/
public function getLog(): array
{
return $this->log;
}
}
// 命令行接口
class CommandLineInterface
{
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);
}
// 验证参数
$zipPath = $this->options['zip'] ?? null;
$targetDir = $this->options['target'] ?? null;
if (!$zipPath || !$targetDir) {
echo "错误: 必须指定 --zip 和 --target 参数\n";
$this->showHelp();
exit(1);
}
// 创建部署器实例
$deployer = new AutoDeployer($zipPath, $targetDir, [
'dryRun' => $this->options['dry-run'] ?? false,
'verbose' => $this->options['verbose'] ?? false,
'backup' => $this->options['backup'] ?? false
]);
// 执行部署
$result = $deployer->deploy();
// 显示结果摘要
$this->showSummary($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 '--backup':
case '-b':
$options['backup'] = true;
break;
case '--zip':
$options['zip'] = $argv[++$i] ?? null;
break;
case '--target':
$options['target'] = $argv[++$i] ?? null;
break;
default:
if (substr($arg, 0, 1) === '-') {
echo "警告: 未知选项 {$arg}\n";
}
break;
}
}
return $options;
}
private function showBanner(): void
{
echo "========================================\n";
echo " 文件自动部署工具 v1.0\n";
echo " PHP 7.4 兼容版本\n";
echo "========================================\n\n";
}
private function showHelp(): void
{
echo "用法: php deploy.php [选项]\n\n";
echo "选项:\n";
echo " --zip <path> ZIP文件路径 (必需)\n";
echo " --target <path> 目标目录路径 (必需)\n";
echo " --dry-run, -d 试运行,不实际修改文件\n";
echo " --backup, -b 部署前备份目标文件或目录\n";
echo " --verbose, -v 显示详细输出\n";
echo " --help, -h 显示此帮助信息\n\n";
echo "示例:\n";
echo " php deploy.php --zip update.zip --target /var/www/html\n";
echo " php deploy.php --zip patch.zip --target ./app --dry-run\n";
echo " php deploy.php --zip release.zip --target /var/www --verbose\n";
}
private function showSummary(array $result): void
{
echo "\n部署摘要:\n";
echo "========================================\n";
echo "新增文件: {$result['added']}\n";
echo "更新文件: {$result['updated']}\n";
echo "跳过文件: {$result['skipped']}\n";
echo "错误文件: {$result['errors']}\n";
echo "========================================\n";
if ($result['errors'] > 0) {
echo "\n错误详情:\n";
foreach ($result['details'] as $detail) {
if (!$detail['success']) {
echo " - {$detail['file']}: {$detail['error']}\n";
}
}
}
}
}
// 主程序入口
if (PHP_SAPI === 'cli') {
$cli = new CommandLineInterface($argv);
$cli->run();
} else {
echo "此脚本只能在命令行模式下运行\n";
exit(1);
}
?>

679
scripts/enhanced_deploy.php Normal file
View File

@@ -0,0 +1,679 @@
<?php
// enhanced_deploy.php
#!/usr/bin/env php
/**
* 增强版自动部署脚本
* 支持根据补丁ZIP文件删除不需要的文件
* PHP 7.4 兼容版本
*/
declare(strict_types=1);
class EnhancedAutoDeployer
{
private string $zipPath;
private string $targetDir;
private array $config;
private array $log = [];
private bool $dryRun;
private bool $verbose;
private array $deleteList = [];
public function __construct(string $zipPath, string $targetDir, array $config = [])
{
$this->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 <path> ZIP文件路径 (必需)\n";
echo " --target <path> 目标目录路径 (必需)\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);
}
?>

View File

@@ -0,0 +1,801 @@
<?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);
}
?>

View File

@@ -0,0 +1,72 @@
#!/usr/bin/env php
// security_config.php
<?php
/**
* 安全策略配置文件
* 用于控制删除操作的安全性
*/
return [
// 启用/禁用删除功能
'delete_enabled' => true,
// 安全删除模式
'safe_delete' => true,
// 受保护的文件模式(不会被删除)
'protected_patterns' => [
'/\.env$/',
'/\.env\./',
'/config\.php$/',
'/config\/.*\.php$/',
'/database\.php$/',
'/composer\.json$/',
'/composer\.lock$/',
'/package\.json$/',
'/yarn\.lock$/',
'/\.htaccess$/',
'/web\.config$/',
'/index\.php$/',
'/app\.php$/',
'/bootstrap\.php$/',
'/\.gitignore$/',
'/README\.md$/',
'/LICENSE$/',
'/CHANGELOG$/',
'/robots\.txt$/',
'/sitemap\.xml$/',
'/favicon\.ico$/'
],
// 受保护的目录(不会被删除)
'protected_directories' => [
'/\.git/',
'/vendor/',
'/node_modules/',
'/storage/',
'/logs/',
'/uploads/',
'/backups/',
'/cache/',
'/temp/',
'/tmp/'
],
// 需要确认的删除操作
'confirmation_required' => [
'\.php$' => true, // PHP文件需要确认
'\.js$' => true, // JS文件需要确认
'\.css$' => false, // CSS文件不需要确认
'config/' => true, // 配置目录需要确认
],
// 备份设置
'backup_enabled' => true,
'backup_dir' => '/var/backups/deploy',
'max_backups' => 10,
// 日志设置
'log_deletions' => true,
'log_file' => '/var/log/deploy_deletions.log'
];
?>