Files
shop-platform/scripts/enhanced_deploy.php

679 lines
21 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_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);
}
?>