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; } }