mirror of
https://github.com/kuaifan/dootask.git
synced 2025-12-11 18:42:54 +00:00
no message
This commit is contained in:
parent
e7749b2dff
commit
2bb646d150
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
2
docker/appstore/temp/.gitignore
vendored
Normal file
2
docker/appstore/temp/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@ -856,3 +856,21 @@ Docker compose文件缺少services配置
|
||||
请求失败
|
||||
没有可用的版本
|
||||
没有找到版本(*)
|
||||
|
||||
Git克隆失败
|
||||
下载失败
|
||||
文件打开失败
|
||||
未找到config.yml配置文件
|
||||
YAML解析失败:(*)
|
||||
配置文件不正确
|
||||
应用已存在,请先卸载后再安装
|
||||
应用正在安装中,请稍后再试
|
||||
应用正在卸载中,请稍后再试
|
||||
URL格式不正确
|
||||
不支持的URL协议,仅支持http、https和git协议
|
||||
下载源列表失败
|
||||
保存文件失败
|
||||
更新成功
|
||||
更新失败:(*)
|
||||
应用列表正在更新中,请稍后再试
|
||||
应用正在下载中,请稍后再试
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user