Files
shop-platform/scripts/deploy.php

533 lines
17 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
// 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);
}
?>