367 lines
11 KiB
PHP
367 lines
11 KiB
PHP
<?php
|
||
/**
|
||
*+------------------
|
||
* madong
|
||
*+------------------
|
||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||
*+------------------
|
||
* Author: Mr. April (405784684@qq.com)
|
||
*+------------------
|
||
* Official Website: http://www.madong.tech
|
||
*/
|
||
|
||
namespace app\service\core\plugin;
|
||
|
||
use core\exception\handler\PluginException;
|
||
use core\tool\Util;
|
||
use ZipArchive;
|
||
use GuzzleHttp\Client;
|
||
|
||
/**
|
||
* 插件下载服务
|
||
* 职责:下载插件ZIP包到插件目录并解压
|
||
*/
|
||
final class PluginDownloadService extends PluginBaseService
|
||
{
|
||
|
||
public function __construct()
|
||
{
|
||
parent::__construct();
|
||
}
|
||
|
||
/**
|
||
* 下载插件到插件目录并解压
|
||
*
|
||
* @param string $pluginCode 插件编码(name)
|
||
* @param string $downloadUrl 下载链接
|
||
*
|
||
* @return array 返回插件信息
|
||
* @throws PluginException
|
||
*/
|
||
public function downloadAndExtract(string $pluginCode, string $downloadUrl): array
|
||
{
|
||
// 1. 准备下载路径
|
||
$pluginDir = $this->plugin_path . DIRECTORY_SEPARATOR . $pluginCode;
|
||
$zipFile = $this->plugin_path . DIRECTORY_SEPARATOR . $pluginCode . '.zip';
|
||
|
||
// 2. 创建插件目录(如果不存在)
|
||
if (!is_dir($this->plugin_path) && !mkdir($this->plugin_path, 0755, true)) {
|
||
throw new PluginException("无法创建插件目录: {$this->plugin_path}");
|
||
}
|
||
|
||
// 如果插件目录已存在,先删除
|
||
if (is_dir($pluginDir)) {
|
||
$this->deleteDirectory($pluginDir);
|
||
}
|
||
|
||
// 3. 下载ZIP文件
|
||
$this->downloadZipFile($downloadUrl, $zipFile);
|
||
|
||
// 4. 解压ZIP到插件目录
|
||
$this->extractZipFile($zipFile, $this->plugin_path);
|
||
|
||
// 5. 读取插件配置信息
|
||
$pluginInfo = $this->readPluginInfo($pluginCode);
|
||
|
||
return [
|
||
'plugin_code' => $pluginCode,
|
||
'plugin_dir' => $pluginDir,
|
||
'plugin_info' => $pluginInfo,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 递归删除目录
|
||
*
|
||
* @param string $dir 目录路径
|
||
* @return void
|
||
*/
|
||
private function deleteDirectory(string $dir): void
|
||
{
|
||
if (!is_dir($dir)) {
|
||
return;
|
||
}
|
||
|
||
$files = array_diff(scandir($dir), ['.', '..']);
|
||
foreach ($files as $file) {
|
||
$path = $dir . DIRECTORY_SEPARATOR . $file;
|
||
if (is_dir($path)) {
|
||
$this->deleteDirectory($path);
|
||
} else {
|
||
unlink($path);
|
||
}
|
||
}
|
||
rmdir($dir);
|
||
}
|
||
|
||
/**
|
||
* 移动目录内容
|
||
*
|
||
* @param string $source 源目录
|
||
* @param string $target 目标目录
|
||
* @return void
|
||
*/
|
||
private function moveDirectoryContents(string $source, string $target): void
|
||
{
|
||
// 确保目标目录存在
|
||
if (!is_dir($target)) {
|
||
mkdir($target, 0755, true);
|
||
}
|
||
|
||
$files = array_diff(scandir($source), ['.', '..']);
|
||
foreach ($files as $file) {
|
||
$sourcePath = $source . DIRECTORY_SEPARATOR . $file;
|
||
$targetPath = $target . DIRECTORY_SEPARATOR . $file;
|
||
|
||
if (is_dir($sourcePath)) {
|
||
// 递归移动子目录
|
||
$this->moveDirectoryContents($sourcePath, $targetPath);
|
||
} else {
|
||
// 移动文件
|
||
rename($sourcePath, $targetPath);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 下载ZIP文件
|
||
*
|
||
* @param string $url 下载URL
|
||
* @param string $file 保存路径
|
||
*
|
||
* @return void
|
||
* @throws PluginException
|
||
*/
|
||
private function downloadZipFile(string $url, string $file): void
|
||
{
|
||
$client = new Client([
|
||
'timeout' => 300, // 下载大文件设置更长的超时时间
|
||
'verify' => false,
|
||
]);
|
||
|
||
$response = $client->get($url, ['sink' => $file]);
|
||
$status = $response->getStatusCode();
|
||
|
||
if ($status !== 200) {
|
||
throw new PluginException("下载失败,HTTP状态码: {$status}");
|
||
}
|
||
|
||
// 检查文件是否下载成功
|
||
if (!file_exists($file) || filesize($file) === 0) {
|
||
throw new PluginException("下载的文件为空或不存在: {$file}");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 解压ZIP文件
|
||
*
|
||
* @param string $zipFile ZIP文件路径
|
||
* @param string $extractTo 解压目标路径(插件目录)
|
||
*
|
||
* @return void
|
||
* @throws PluginException
|
||
*/
|
||
private function extractZipFile(string $zipFile, string $extractTo): void
|
||
{
|
||
$hasZipArchive = class_exists(ZipArchive::class, false);
|
||
|
||
if (!$hasZipArchive) {
|
||
$cmd = $this->getUnzipCmd($zipFile, $extractTo);
|
||
if (!$cmd) {
|
||
throw new PluginException('请给php安装zip模块或者给系统安装unzip命令');
|
||
}
|
||
if (!function_exists('proc_open')) {
|
||
throw new PluginException('请解除proc_open函数的禁用或者给php安装zip模块');
|
||
}
|
||
}
|
||
|
||
Util::pauseFileMonitor();
|
||
try {
|
||
// 创建临时解压目录
|
||
$tempDir = $extractTo . '_temp';
|
||
if (is_dir($tempDir)) {
|
||
$this->deleteDirectory($tempDir);
|
||
}
|
||
mkdir($tempDir, 0755, true);
|
||
|
||
// 解压zip到临时目录
|
||
if ($hasZipArchive) {
|
||
$zip = new ZipArchive();
|
||
if ($zip->open($zipFile) === true) {
|
||
if (!$zip->extractTo($tempDir)) {
|
||
throw new PluginException("解压失败: {$zipFile}");
|
||
}
|
||
$zip->close();
|
||
} else {
|
||
throw new PluginException("无法打开压缩包: {$zipFile}");
|
||
}
|
||
} else {
|
||
$this->unzipWithCmd($cmd);
|
||
}
|
||
|
||
// 处理解压后的目录结构
|
||
$extractedFiles = scandir($tempDir);
|
||
$extractedFiles = array_diff($extractedFiles, ['.', '..']);
|
||
|
||
// 如果只有一个目录,说明ZIP包含了一个根目录(如 nexus/),需要移动内容
|
||
if (count($extractedFiles) === 1) {
|
||
$onlyDir = $tempDir . DIRECTORY_SEPARATOR . $extractedFiles[0];
|
||
if (is_dir($onlyDir)) {
|
||
// 移动目录内容到目标位置
|
||
$this->moveDirectoryContents($onlyDir, $extractTo);
|
||
$this->deleteDirectory($onlyDir);
|
||
} else {
|
||
// 是单个文件,直接移动
|
||
rename($onlyDir, $extractTo . DIRECTORY_SEPARATOR . $extractedFiles[0]);
|
||
}
|
||
} else {
|
||
// 多个文件,直接移动所有内容
|
||
foreach ($extractedFiles as $file) {
|
||
rename($tempDir . DIRECTORY_SEPARATOR . $file, $extractTo . DIRECTORY_SEPARATOR . $file);
|
||
}
|
||
}
|
||
|
||
// 删除临时目录
|
||
$this->deleteDirectory($tempDir);
|
||
|
||
// 删除临时zip文件
|
||
if (file_exists($zipFile)) {
|
||
unlink($zipFile);
|
||
}
|
||
} finally {
|
||
Util::resumeFileMonitor();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 读取插件配置信息
|
||
*
|
||
* @param string $pluginCode 插件编码
|
||
*
|
||
* @return array 插件信息
|
||
* @throws PluginException
|
||
*/
|
||
private function readPluginInfo(string $pluginCode): array
|
||
{
|
||
$infoFile = $this->plugin_path . DIRECTORY_SEPARATOR . $pluginCode . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'info.php';
|
||
|
||
if (!file_exists($infoFile)) {
|
||
throw new PluginException("插件配置文件不存在: {$infoFile}");
|
||
}
|
||
|
||
$info = include $infoFile;
|
||
|
||
if (!is_array($info)) {
|
||
throw new PluginException("插件配置文件格式错误: {$infoFile}");
|
||
}
|
||
|
||
return $info;
|
||
}
|
||
|
||
/**
|
||
* 获取系统支持的解压命令
|
||
*
|
||
* @param string $zipFile ZIP文件路径
|
||
* @param string $extractTo 解压目标路径
|
||
*
|
||
* @return string|null 解压命令
|
||
*/
|
||
private function getUnzipCmd(string $zipFile, string $extractTo): ?string
|
||
{
|
||
if ($cmd = $this->findCmd('unzip')) {
|
||
return "{$cmd} -o -qq {$zipFile} -d {$extractTo}";
|
||
}
|
||
|
||
if ($cmd = $this->findCmd('7z')) {
|
||
return "{$cmd} x -bb0 -y {$zipFile} -o{$extractTo}";
|
||
}
|
||
|
||
if ($cmd = $this->findCmd('7zz')) {
|
||
return "{$cmd} x -bb0 -y {$zipFile} -o{$extractTo}";
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 使用命令行解压ZIP文件
|
||
*
|
||
* @param string $cmd 解压命令
|
||
*
|
||
* @return void
|
||
* @throws PluginException
|
||
*/
|
||
private function unzipWithCmd(string $cmd): void
|
||
{
|
||
$desc = [
|
||
0 => ["pipe", "r"],
|
||
1 => ["pipe", "w"],
|
||
2 => ["pipe", "w"],
|
||
];
|
||
|
||
$handler = proc_open($cmd, $desc, $pipes);
|
||
if (!is_resource($handler)) {
|
||
throw new PluginException("解压zip时出错: proc_open调用失败");
|
||
}
|
||
|
||
$err = fread($pipes[2], 1024);
|
||
fclose($pipes[2]);
|
||
proc_close($handler);
|
||
|
||
if (!empty($err)) {
|
||
throw new PluginException("解压zip时出错: {$err}");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 查找系统命令
|
||
*
|
||
* @param string $name 命令名称
|
||
* @param string|null $default 默认值
|
||
* @param array $extraDirs 额外搜索目录
|
||
*
|
||
* @return string|null 命令路径
|
||
*/
|
||
private function findCmd(string $name, ?string $default = null, array $extraDirs = []): ?string
|
||
{
|
||
if (ini_get('open_basedir')) {
|
||
$searchPath = array_merge(explode(PATH_SEPARATOR, ini_get('open_basedir')), $extraDirs);
|
||
$dirs = [];
|
||
|
||
foreach ($searchPath as $path) {
|
||
if (@is_dir($path)) {
|
||
$dirs[] = $path;
|
||
} else {
|
||
if (basename($path) === $name && @is_executable($path)) {
|
||
return $path;
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
$dirs = array_merge(
|
||
explode(PATH_SEPARATOR, getenv('PATH') ?: getenv('Path')),
|
||
$extraDirs
|
||
);
|
||
}
|
||
|
||
$suffixes = [''];
|
||
if ('\\' === DIRECTORY_SEPARATOR) {
|
||
$pathExt = getenv('PATHEXT');
|
||
$suffixes = array_merge(
|
||
$pathExt ? explode(PATH_SEPARATOR, $pathExt) : ['.exe', '.bat', '.cmd', '.com'],
|
||
$suffixes
|
||
);
|
||
}
|
||
|
||
foreach ($suffixes as $suffix) {
|
||
foreach ($dirs as $dir) {
|
||
$file = $dir . DIRECTORY_SEPARATOR . $name . $suffix;
|
||
if (@is_file($file) && ('\\' === DIRECTORY_SEPARATOR || @is_executable($file))) {
|
||
return $file;
|
||
}
|
||
}
|
||
}
|
||
|
||
return $default;
|
||
}
|
||
} |