From 2bb646d1500910714d79145450c479394c7587ec Mon Sep 17 00:00:00 2001 From: kuaifan Date: Thu, 15 May 2025 12:02:08 +0800 Subject: [PATCH] no message --- app/Http/Controllers/Api/AppsController.php | 58 +++- app/Module/Apps/Apps.php | 317 ++++++++++++++++++ app/Module/Base.php | 23 +- docker/appstore/temp/.gitignore | 2 + language/original-api.txt | 18 + .../assets/js/components/MicroApps/index.vue | 14 +- 6 files changed, 422 insertions(+), 10 deletions(-) create mode 100644 docker/appstore/temp/.gitignore diff --git a/app/Http/Controllers/Api/AppsController.php b/app/Http/Controllers/Api/AppsController.php index 17928e672..daa65e08d 100755 --- a/app/Http/Controllers/Api/AppsController.php +++ b/app/Http/Controllers/Api/AppsController.php @@ -34,7 +34,25 @@ class AppsController extends AbstractController } /** - * @api {get} api/apps/info 02. 获取应用详情(限管理员) + * @api {get} api/apps/list/update 02. 更新应用列表(限管理员) + * + * @apiVersion 1.0.0 + * @apiGroup apps + * @apiName list_update + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Array} data 应用列表数据 + */ + public function list__update() + { + User::auth('admin'); + // + return Apps::appListUpdate(); + } + + /** + * @api {get} api/apps/info 03. 获取应用详情(限管理员) * * @apiVersion 1.0.0 * @apiGroup apps @@ -62,7 +80,7 @@ class AppsController extends AbstractController } /** - * @api {post} api/apps/install 03. 安装应用(限管理员) + * @api {post} api/apps/install 04. 安装应用(限管理员) * * @apiVersion 1.0.0 * @apiGroup apps @@ -122,6 +140,38 @@ class AppsController extends AbstractController return Apps::dockerComposeUp($appName, $version); } + /** + * @api {get} api/apps/install/url 05. 通过url安装应用(限管理员) + * + * @apiVersion 1.0.0 + * @apiGroup apps + * @apiName install_url + * + * @apiParam {String} url 应用url + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 安装结果信息 + */ + public function install__url() + { + User::auth('admin'); + // + $url = Request::input('url'); + if (empty($url)) { + return Base::retError('应用url不能为空'); + } + + // 下载应用 + $res = Apps::downloadApp($url); + if (Base::isError($res)) { + return $res; + } + + // 安装应用 + return Apps::dockerComposeUp($res['app_name']); + } + /** * 更新应用状态(用于安装结束之后回调) * @@ -161,7 +211,7 @@ class AppsController extends AbstractController } /** - * @api {post} api/apps/uninstall 04. 卸载应用(限管理员) + * @api {post} api/apps/uninstall 06. 卸载应用(限管理员) * * @apiVersion 1.0.0 * @apiGroup apps @@ -204,7 +254,7 @@ class AppsController extends AbstractController } /** - * @api {get} api/apps/logs 05. 获取应用日志(限管理员) + * @api {get} api/apps/logs 07. 获取应用日志(限管理员) * * @apiVersion 1.0.0 * @apiGroup apps diff --git a/app/Module/Apps/Apps.php b/app/Module/Apps/Apps.php index 229979022..35b3fe2dd 100644 --- a/app/Module/Apps/Apps.php +++ b/app/Module/Apps/Apps.php @@ -4,6 +4,7 @@ namespace App\Module\Apps; use App\Module\Base; use App\Module\Ihttp; +use Cache; use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Exception\ParseException; @@ -27,6 +28,9 @@ class Apps 'appstore', ]; + // 软件源列表URL + protected static string $sourcesUrl = 'https://appstore.dootask.com/sources.list'; + /** * 获取应用列表 * @return array @@ -527,6 +531,260 @@ class Apps return $appConfig['status'] === 'installed'; } + /** + * 更新应用列表 + * @return array + */ + public static function appListUpdate(): array + { + // 检查是否正在更新 + $cacheKey = 'appstore_update_running'; + if (Cache::has($cacheKey)) { + return Base::retError('应用列表正在更新中,请稍后再试'); + } + $onFailure = function (string $message) use ($cacheKey) { + Cache::forget($cacheKey); + return Base::retError($message); + }; + $onSuccess = function (string $message, array $data = []) use ($cacheKey) { + Cache::forget($cacheKey); + return Base::retSuccess($message, $data); + }; + + // 设置更新状态 + Cache::put($cacheKey, true, 180); // 3分钟有效期 + + // 临时目录 + $tempDir = base_path('docker/appstore/temp/sources'); + $zipFile = $tempDir . '/sources.zip'; + + // 清空临时目录 + if (is_dir($tempDir)) { + Base::deleteDirAndFile($tempDir, true); + } else { + Base::makeDir($tempDir); + } + + try { + // 下载源列表 + $res = Ihttp::ihttp_request(self::$sourcesUrl); + if (Base::isError($res)) { + return $onFailure('下载源列表失败'); + } + file_put_contents($zipFile, $res['data']); + + // 解压文件 + $zip = new \ZipArchive(); + if ($zip->open($zipFile) !== true) { + return $onFailure('文件打开失败'); + } + $zip->extractTo($tempDir); + $zip->close(); + unlink($zipFile); + + // 遍历目录 + $dirs = scandir($tempDir); + $results = [ + 'success' => [], + 'failed' => [] + ]; + + foreach ($dirs as $appName) { + // 跳过当前目录、父目录和隐藏文件 + if ($appName === '.' || $appName === '..' || str_starts_with($appName, '.')) { + continue; + } + + $sourceDir = $tempDir . '/' . $appName; + if (!is_dir($sourceDir)) { + continue; + } + + // 检查config.yml文件 + $configFile = $sourceDir . '/config.yml'; + if (!file_exists($configFile)) { + $results['failed'][] = [ + 'app_name' => $appName, + 'reason' => '未找到config.yml配置文件' + ]; + continue; + } + + // 解析配置文件 + try { + $configData = Yaml::parseFile($configFile); + } catch (ParseException $e) { + $results['failed'][] = [ + 'app_name' => $appName, + 'reason' => 'YAML解析失败:' . $e->getMessage() + ]; + continue; + } + + // 检查name字段 + if (empty($configData['name'])) { + $results['failed'][] = [ + 'app_name' => $appName, + 'reason' => '配置文件不正确' + ]; + continue; + } + + // 使用目录名作为应用名称 + $targetDir = base_path('docker/appstore/apps/' . $appName); + + // 复制目录 + if (!self::copyDirAndFile($sourceDir, $targetDir, true)) { + $results['failed'][] = [ + 'app_name' => $appName, + 'reason' => '复制文件失败' + ]; + continue; + } + + $results['success'][] = [ + 'app_name' => $appName + ]; + } + + // 清理临时目录 + Base::deleteDirAndFile($tempDir, true); + return $onSuccess('更新成功', $results); + } catch (\Exception $e) { + // 清理临时目录 + Base::deleteDirAndFile($tempDir, true); + return $onFailure('更新失败:' . $e->getMessage()); + } + } + + /** + * 下载应用 + * @param string $url 应用url + * @return array 下载结果 + */ + public static function downloadApp(string $url): array + { + // 检查是否正在下载 + $cacheKey = 'appstore_download_running'; + if (Cache::has($cacheKey)) { + return Base::retError('应用正在下载中,请稍后再试'); + } + $onFailure = function (string $message) use ($cacheKey) { + Cache::forget($cacheKey); + return Base::retError($message); + }; + $onSuccess = function (string $message, array $data = []) use ($cacheKey) { + Cache::forget($cacheKey); + return Base::retSuccess($message, $data); + }; + + // 设置下载状态 + Cache::put($cacheKey, true, 180); // 3分钟有效期 + + // 验证URL格式 + if (!filter_var($url, FILTER_VALIDATE_URL)) { + return $onFailure('URL格式不正确'); + } + + // 验证URL协议 + $scheme = parse_url($url, PHP_URL_SCHEME); + if (!in_array($scheme, ['http', 'https', 'git'])) { + return $onFailure('不支持的URL协议,仅支持http、https和git协议'); + } + + // 临时目录 + $tempDir = base_path('docker/appstore/temp/' . md5($url)); + + // 清空临时目录 + if (is_dir($tempDir)) { + Base::deleteDirAndFile($tempDir, true); + } else { + Base::makeDir($tempDir); + } + + // 判断URL类型 + $isGit = str_ends_with($url, '.git') || str_contains($url, 'github.com') || str_contains($url, 'gitlab.com'); + + try { + if ($isGit) { + // 克隆Git仓库 + $cmd = sprintf('cd %s && git clone --depth=1 %s .', escapeshellarg($tempDir), escapeshellarg($url)); + exec($cmd, $output, $returnVar); + if ($returnVar !== 0) { + return $onFailure('Git克隆失败'); + } + } else { + // 下载ZIP文件 + $zipFile = $tempDir . '/app.zip'; + $res = Ihttp::ihttp_request($url); + if (Base::isError($res)) { + return $onFailure('下载失败'); + } + file_put_contents($zipFile, $res['data']); + + // 解压ZIP文件 + $zip = new \ZipArchive(); + if ($zip->open($zipFile) !== true) { + return $onFailure('文件打开失败'); + } + $zip->extractTo($tempDir); + $zip->close(); + unlink($zipFile); + } + + // 检查config.yml文件 + $configFile = $tempDir . '/config.yml'; + if (!file_exists($configFile)) { + return $onFailure('未找到config.yml配置文件'); + } + + // 解析配置文件 + try { + $configData = Yaml::parseFile($configFile); + } catch (ParseException $e) { + return $onFailure('YAML解析失败:' . $e->getMessage()); + } + + // 检查name字段 + if (empty($configData['name'])) { + return $onFailure('配置文件不正确'); + } + + // 处理应用名称 + $appName = Base::camel2snake(Base::cn2pinyin($configData['name'], '_')); + $targetDir = base_path('docker/appstore/apps/' . $appName); + $targetConfigFile = $targetDir . '/config.json'; + + // 检查目标是否存在 + if (file_exists($targetConfigFile)) { + $targetConfigData = json_decode(file_get_contents($targetConfigFile), true); + if (is_array($targetConfigData)) { + $status = $targetConfigData['status']; + $errorMessages = [ + 'installed' => '应用已存在,请先卸载后再安装', + 'installing' => '应用正在安装中,请稍后再试', + 'uninstalling' => '应用正在卸载中,请稍后再试' + ]; + if (isset($errorMessages[$status])) { + return $onFailure($errorMessages[$status]); + } + } + Base::deleteDirAndFile($targetDir); + } + + // 移动文件到目标目录 + if (!rename($tempDir, $targetDir)) { + return $onFailure('移动文件失败'); + } + + return $onSuccess('下载成功', [ 'app_name' => $appName ]); + } catch (\Exception $e) { + // 清理临时目录 + Base::deleteDirAndFile($tempDir, true); + return $onFailure('下载失败:' . $e->getMessage()); + } + } + /** * 获取应用的文档(README) * @@ -825,6 +1083,65 @@ class Apps return $versions; } + /** + * 复制目录和文件 + * @param string $sourceDir 源目录 + * @param string $targetDir 目标目录 + * @param bool $recursive 是否递归复制 + * @return bool + */ + private static function copyDirAndFile(string $sourceDir, string $targetDir, bool $recursive = false): bool + { + if (!is_dir($sourceDir)) { + return false; + } + + // 创建目标目录 + if (!is_dir($targetDir)) { + if (!mkdir($targetDir, 0755, true)) { + return false; + } + } + + // 打开源目录 + $dir = opendir($sourceDir); + if (!$dir) { + return false; + } + + // 遍历源目录 + while (($file = readdir($dir)) !== false) { + // 跳过当前目录和父目录 + if ($file === '.' || $file === '..') { + continue; + } + + $sourceFile = $sourceDir . '/' . $file; + $targetFile = $targetDir . '/' . $file; + + if (is_dir($sourceFile)) { + // 如果是目录且需要递归复制 + if ($recursive) { + if (!self::copyDirAndFile($sourceFile, $targetFile, true)) { + closedir($dir); + return false; + } + } + } else { + // 复制文件 + if (!copy($sourceFile, $targetFile)) { + closedir($dir); + return false; + } + // 保持文件权限 + chmod($targetFile, fileperms($sourceFile)); + } + } + + closedir($dir); + return true; + } + /** * 执行curl请求 * @param $path diff --git a/app/Module/Base.php b/app/Module/Base.php index 3d5e8e078..958bc0b0d 100755 --- a/app/Module/Base.php +++ b/app/Module/Base.php @@ -351,7 +351,7 @@ class Base /** * 删除文件夹及文件夹下所有的文件 * @param $dirName - * @param bool $undeleteDir 不删除文件夹(只删除文件) + * @param bool $undeleteDir 不删除文件夹本身(只删除文件夹里面的内容) */ public static function deleteDirAndFile($dirName, $undeleteDir = false) { @@ -2581,22 +2581,37 @@ class Base /** * 中文转拼音 * @param $str + * @param $delim * @return string */ - public static function cn2pinyin($str) + public static function cn2pinyin($str, $delim = '') { if (empty($str)) { return ''; } if (!preg_match("/^[a-zA-Z0-9_.]+$/", $str)) { - $str = Cache::rememberForever("cn2pinyin:" . md5($str), function() use ($str) { + $str = Cache::rememberForever("cn2pinyin:" . md5($str . '_' . $delim), function () use ($delim, $str) { $pinyin = new Pinyin(); - return $pinyin->permalink($str, ''); + return $pinyin->permalink($str, $delim); }); } return $str; } + /** + * 驼峰转下划线 + * @param $str + * @return string + */ + public static function camel2snake($str) + { + if (empty($str)) { + return ''; + } + $str = preg_replace('/([a-z])([A-Z])/', '$1_$2', $str); + return strtolower($str); + } + /** * 缓存数据 * @param $name diff --git a/docker/appstore/temp/.gitignore b/docker/appstore/temp/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/docker/appstore/temp/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/language/original-api.txt b/language/original-api.txt index 1bf736316..ac40025ce 100644 --- a/language/original-api.txt +++ b/language/original-api.txt @@ -856,3 +856,21 @@ Docker compose文件缺少services配置 请求失败 没有可用的版本 没有找到版本(*) + +Git克隆失败 +下载失败 +文件打开失败 +未找到config.yml配置文件 +YAML解析失败:(*) +配置文件不正确 +应用已存在,请先卸载后再安装 +应用正在安装中,请稍后再试 +应用正在卸载中,请稍后再试 +URL格式不正确 +不支持的URL协议,仅支持http、https和git协议 +下载源列表失败 +保存文件失败 +更新成功 +更新失败:(*) +应用列表正在更新中,请稍后再试 +应用正在下载中,请稍后再试 diff --git a/resources/assets/js/components/MicroApps/index.vue b/resources/assets/js/components/MicroApps/index.vue index cf4e9eb7e..8eebdffc9 100644 --- a/resources/assets/js/components/MicroApps/index.vue +++ b/resources/assets/js/components/MicroApps/index.vue @@ -143,8 +143,10 @@ export default { if (token) { return } - this.apps = []; - microApp.unmountAllApps({destroy: true}) + this.closeAllMicroApp() + }, + themeName() { + this.closeAllMicroApp() }, }, @@ -404,6 +406,14 @@ export default { } }, + /** + * 关闭所有微应用 + */ + closeAllMicroApp() { + this.apps = []; + microApp.unmountAllApps({destroy: true}) + }, + /** * 关闭之前判断 * @param name