diff --git a/app/Http/Controllers/Api/AppsController.php b/app/Http/Controllers/Api/AppsController.php index 3441024db..3170b409f 100755 --- a/app/Http/Controllers/Api/AppsController.php +++ b/app/Http/Controllers/Api/AppsController.php @@ -304,15 +304,18 @@ class AppsController extends AbstractController * @apiGroup apps * @apiName logs * - * @apiParam {String} app_name 应用名称 - * @apiParam {Number} [lines=50] 获取日志行数,默认50行 + * @apiParam {String} app_name 应用名称 + * @apiParam {Number} [lines=50] 获取日志行数,默认50行 + * @apiParam {Boolean} [simple=false] 是否只返回日志,默认false * * @apiSuccess {Number} ret 返回状态码(1正确、0错误) * @apiSuccess {String} msg 返回信息(错误描述) * @apiSuccess {Object} data 返回数据 - * @apiSuccess {String} data.name 应用名称 - * @apiSuccess {Object} data.config 应用配置信息 - * @apiSuccess {String} data.log 日志内容 + * @apiSuccess {String} data.name 应用名称 + * @apiSuccess {Object} data.config 应用配置信息 + * @apiSuccess {Object} data.versions 可用版本列表 + * @apiSuccess {Boolean} data.upgradeable 是否可升级 + * @apiSuccess {String} data.log 日志内容 */ public function logs() { @@ -320,13 +323,22 @@ class AppsController extends AbstractController // $appName = Request::input('app_name'); $lines = intval(Request::input('lines', 50)); + $simple = Request::input('simple', false); $logContent = implode("\n", Apps::getAppLog($appName, $lines)); - return Base::retSuccess('success', [ + $data = [ 'name' => $appName, - 'config' => Apps::getAppConfig($appName), 'log' => trim($logContent) - ]); + ]; + if (!$simple) { + $config = Apps::getAppConfig($appName); + $versions = Apps::getAvailableVersions($appName); + $data['config'] = $config; + $data['versions'] = $versions; + $data['upgradeable'] = Apps::isUpgradeable($config, $versions); + } + + return Base::retSuccess('success', $data); } } diff --git a/app/Module/Apps.php b/app/Module/Apps.php index 395093f60..9fafc6f86 100644 --- a/app/Module/Apps.php +++ b/app/Module/Apps.php @@ -8,8 +8,11 @@ use Symfony\Component\Yaml\Yaml; class Apps { + // 软件源列表URL + const SOURCES_URL = 'https://appstore.dootask.com/'; + // 受保护的服务名称列表 - protected static array $protectedServiceNames = [ + const PROTECTED_NAMES = [ 'php', 'nginx', 'redis', @@ -18,13 +21,11 @@ class Apps 'appstore', ]; - // 软件源列表URL - protected static string $sourcesUrl = 'https://appstore.dootask.com/sources.list'; - // 缓存键集合 - protected static array $cacheKeys = [ - 'app_list' => 'appstore_app_list', - 'menu_items' => 'appstore_menu_items', + const CACHE_KEYS = [ + 'list' => 'appstore_app_list', // 应用列表缓存 + 'menu' => 'appstore_menu_items', // 应用菜单缓存 + 'installed' => 'appstore_installed', // 已安装应用缓存 ]; /** @@ -36,22 +37,25 @@ class Apps public static function appList(bool $cache = true): array { if ($cache === false) { - Cache::forget(self::$cacheKeys['app_list']); + Cache::forget(self::CACHE_KEYS['list']); } - $apps = Cache::remember(self::$cacheKeys['app_list'], now()->addHour(), function () { + $apps = Cache::remember(self::CACHE_KEYS['list'], now()->addHour(), function () { $apps = []; $baseDir = base_path('docker/appstore/apps'); $dirs = scandir($baseDir); - foreach ($dirs as $dir) { - // 跳过当前目录、父目录和隐藏文件 - if ($dir === '.' || $dir === '..' || str_starts_with($dir, '.')) { + foreach ($dirs as $appName) { + if ($appName === '.' || $appName === '..' || str_starts_with($appName, '.')) { continue; } + $info = self::getAppInfo($appName); + $config = self::getAppConfig($appName); + $versions = self::getAvailableVersions($appName); $apps[] = [ - 'name' => $dir, - 'info' => self::getAppInfo($dir), - 'config' => self::getAppConfig($dir), - 'versions' => self::getAvailableVersions($dir), + 'name' => $appName, + 'info' => $info, + 'config' => $config, + 'versions' => $versions, + 'upgradeable' => self::isUpgradeable($config, $versions), ]; } return $apps; @@ -67,11 +71,15 @@ class Apps */ public static function appInfo(string $appName): array { + $info = self::getAppInfo($appName); + $config = self::getAppConfig($appName); + $versions = self::getAvailableVersions($appName); return Base::retSuccess("success", [ 'name' => $appName, - 'info' => self::getAppInfo($appName), - 'config' => self::getAppConfig($appName), - 'versions' => self::getAvailableVersions($appName), + 'info' => $info, + 'config' => $config, + 'versions' => $versions, + 'upgradeable' => self::isUpgradeable($config, $versions), 'document' => self::getAppDocument($appName), ]); } @@ -185,11 +193,6 @@ class Apps */ public static function dockerComposeFinalize(string $appName, string $status): array { - // 清理缓存 - foreach (self::$cacheKeys as $key) { - Cache::forget($key); - } - // 获取当前应用信息 $appInfo = self::getAppConfig($appName); @@ -362,13 +365,13 @@ class Apps public static function getAppMenuItems(bool $cache = true): array { if ($cache === false) { - Cache::forget(self::$cacheKeys['menu_items']); + Cache::forget(self::CACHE_KEYS['menu']); } - $res = Cache::remember(self::$cacheKeys['menu_items'], now()->addHour(), function () { + $res = Cache::remember(self::CACHE_KEYS['menu'], now()->addHour(), function () { return self::menuGetAll(); }); if (Base::isError($res)) { - Cache::forget(self::$cacheKeys['menu_items']); + Cache::forget(self::CACHE_KEYS['menu']); } return $res; } @@ -419,17 +422,30 @@ class Apps return Base::retSuccess("success", $allMenuItems); } + // 获取所有已安装的应用配置 + $installedApps = []; $dirs = scandir($baseDir); - foreach ($dirs as $dir) { - if ($dir === '.' || $dir === '..' || str_starts_with($dir, '.')) { + foreach ($dirs as $appName) { + if ($appName === '.' || $appName === '..' || str_starts_with($appName, '.')) { continue; } - if (!self::isInstalled($dir)) { + $appConfig = self::getAppConfig($appName); + if ($appConfig['status'] !== 'installed') { continue; } - $appMenuItems = self::menuGetSingle($dir); + $installedApps[$appName] = strtotime($appConfig['install_at'] ?? ''); + } + + // 按安装时间排序应用 + uasort($installedApps, function ($timeA, $timeB) { + return $timeB <=> $timeA; // 降序排列 + }); + + // 按排序后的顺序获取菜单 + foreach ($installedApps as $appName => $installAt) { + $appMenuItems = self::menuGetSingle($appName); if (Base::isSuccess($appMenuItems)) { $allMenuItems = array_merge($allMenuItems, $appMenuItems['data']); } @@ -570,6 +586,9 @@ class Apps mkdir($baseDir, 0755, true); } + // 清理缓存 + self::clearCache(); + // 写入文件 return (bool)file_put_contents( $configFile, @@ -618,8 +637,16 @@ class Apps */ public static function isInstalled(string $appName): bool { + $array = Base::json2array(Cache::get(self::CACHE_KEYS['installed'], [])); + if (isset($array[$appName])) { + return $array[$appName]; + } + $appConfig = self::getAppConfig($appName); - return $appConfig['status'] === 'installed'; + $array[$appName] = $appConfig['status'] === 'installed'; + Cache::put(self::CACHE_KEYS['installed'], Base::array2json($array)); + + return $array[$appName]; } /** @@ -630,21 +657,22 @@ class Apps public static function appListUpdate(): array { // 检查是否正在更新 - $cacheKey = 'appstore_update_running'; - if (Cache::has($cacheKey)) { + $cacheTmp = 'appstore_update_running'; + if (Cache::has($cacheTmp)) { return Base::retError('应用列表正在更新中,请稍后再试'); } - $onFailure = function (string $message) use ($cacheKey) { - Cache::forget($cacheKey); + $onFailure = function (string $message) use ($cacheTmp) { + Cache::forget($cacheTmp); return Base::retError($message); }; - $onSuccess = function (string $message, array $data = []) use ($cacheKey) { - Cache::forget($cacheKey); + $onSuccess = function (string $message, array $data = []) use ($cacheTmp) { + self::clearCache(); + Cache::forget($cacheTmp); return Base::retSuccess($message, $data); }; // 设置更新状态 - Cache::put($cacheKey, true, 180); // 3分钟有效期 + Cache::put($cacheTmp, true, 180); // 3分钟有效期 // 临时目录 $tempDir = base_path('docker/appstore/temp/sources'); @@ -659,7 +687,7 @@ class Apps try { // 下载源列表 - $res = Ihttp::ihttp_request(self::$sourcesUrl); + $res = Ihttp::ihttp_request(self::SOURCES_URL); if (Base::isError($res)) { return $onFailure('下载源列表失败'); } @@ -758,21 +786,22 @@ class Apps public static function downloadApp(string $url): array { // 检查是否正在下载 - $cacheKey = 'appstore_download_running'; - if (Cache::has($cacheKey)) { + $cacheTmp = 'appstore_download_running'; + if (Cache::has($cacheTmp)) { return Base::retError('应用正在下载中,请稍后再试'); } - $onFailure = function (string $message) use ($cacheKey) { - Cache::forget($cacheKey); + $onFailure = function (string $message) use ($cacheTmp) { + Cache::forget($cacheTmp); return Base::retError($message); }; - $onSuccess = function (string $message, array $data = []) use ($cacheKey) { - Cache::forget($cacheKey); + $onSuccess = function (string $message, array $data = []) use ($cacheTmp) { + self::clearCache(); + Cache::forget($cacheTmp); return Base::retSuccess($message, $data); }; // 设置下载状态 - Cache::put($cacheKey, true, 180); // 3分钟有效期 + Cache::put($cacheTmp, true, 180); // 3分钟有效期 // 验证URL格式 if (!filter_var($url, FILTER_VALIDATE_URL)) { @@ -845,7 +874,7 @@ class Apps // 处理应用名称 $appName = Base::camel2snake(Base::cn2pinyin($configData['name'], '_')); - if (in_array($appName, self::$protectedServiceNames)) { + if (in_array($appName, self::PROTECTED_NAMES)) { return Base::retError('服务名称 "' . $appName . '" 被保护,不能使用'); } $targetDir = base_path('docker/appstore/apps/' . $appName); @@ -920,23 +949,6 @@ class Apps return ''; } - /** - * 检查文件名是否匹配 README 模式 - * - * @param string $fileName 文件名 - * @param array $patterns 正则模式数组 - * @return bool 是否匹配 - */ - private static function matchReadmePattern(string $fileName, array $patterns): bool - { - foreach ($patterns as $pattern) { - if (preg_match($pattern, $fileName)) { - return true; - } - } - return false; - } - /** * 生成docker-compose.yml文件配置 * @@ -989,7 +1001,7 @@ class Apps // 检查服务名称是否被保护 foreach ($content['services'] as $name => $service) { - if (in_array($name, self::$protectedServiceNames)) { + if (in_array($name, self::PROTECTED_NAMES)) { return Base::retError('服务名称 "' . $name . '" 被保护,不能使用'); } } @@ -1164,6 +1176,27 @@ class Apps return $field[$lang] ?? $field[array_key_first($field)]; } + /** + * 检查应用是否可升级 + * + * @param array $config 应用配置 + * @param array $versions 可用版本列表 + * @return bool 如果可升级返回 true,否则返回 false + */ + public static function isUpgradeable(array $config, array $versions): bool + { + $upgradeable = false; + if ($config['status'] === 'installed' && !empty($versions)) { + foreach ($versions as $version) { + if (version_compare($config['install_version'], $version['version'], '<')) { + $upgradeable = true; + break; + } + } + } + return $upgradeable; + } + /** * 处理应用图标 * @@ -1216,7 +1249,7 @@ class Apps * @param string $appName 应用名称 * @return array 按语义化版本排序(从新到旧)的版本号数组 */ - private static function getAvailableVersions(string $appName): array + public static function getAvailableVersions(string $appName): array { $baseDir = base_path('docker/appstore/apps/' . $appName); $versions = []; @@ -1347,6 +1380,18 @@ class Apps return true; } + /** + * 清除缓存 + * + * @return void + */ + private static function clearCache(): void + { + foreach (self::CACHE_KEYS as $key) { + Cache::forget($key); + } + } + /** * 执行curl请求 * diff --git a/docker-compose.yml b/docker-compose.yml index 44f26094e..7273695f6 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,7 +36,7 @@ services: image: "nginx:alpine" ports: - "${APP_PORT}:80" - - "${APP_SSL_PORT:-}:443" + - "${APP_SSL_PORT:-0}:443" volumes: - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf - ./:/var/www @@ -100,7 +100,7 @@ services: appstore: container_name: "dootask-appstore-${APP_ID}" privileged: true - image: "kuaifan/dootask-appstore:0.0.2" + image: "kuaifan/dootask-appstore:0.0.3" volumes: - shared_data:/usr/share/dootask - /var/run/docker.sock:/var/run/docker.sock