no message

This commit is contained in:
kuaifan 2025-05-12 09:49:31 +08:00
parent 2cfcb081a2
commit 467f2368dd
9 changed files with 116 additions and 101 deletions

View File

@ -42,7 +42,7 @@ class AppsController extends AbstractController
* @apiSuccess {String} msg 返回信息(错误描述) * @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 应用详细信息 * @apiSuccess {Object} data 应用详细信息
* @apiSuccess {Object} data.info 应用基本信息 * @apiSuccess {Object} data.info 应用基本信息
* @apiSuccess {Object} data.local 应用本地安装信息 * @apiSuccess {Object} data.config 应用配置信息
* @apiSuccess {Array} data.versions 可用版本列表 * @apiSuccess {Array} data.versions 可用版本列表
*/ */
public function info() public function info()
@ -85,21 +85,21 @@ class AppsController extends AbstractController
} }
// 保存用户设置的参数 // 保存用户设置的参数
$localData = []; $configData = [];
// 设置参数 // 设置参数
if (!empty($params) && is_array($params)) { if (!empty($params) && is_array($params)) {
$localData['params'] = $params; $configData['params'] = $params;
} }
// 设置资源限制 // 设置资源限制
if (!empty($resources) && is_array($resources)) { if (!empty($resources) && is_array($resources)) {
$localData['resources'] = $resources; $configData['resources'] = $resources;
} }
// 保存配置 // 保存配置
if (!empty($localData)) { if (!empty($configData)) {
Apps::saveAppLocalInfo($appName, $localData); Apps::saveAppConfig($appName, $configData);
} }
// 执行安装 // 执行安装
@ -176,7 +176,7 @@ class AppsController extends AbstractController
* @apiSuccess {String} msg 返回信息(错误描述) * @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据 * @apiSuccess {Object} data 返回数据
* @apiSuccess {String} data.name 应用名称 * @apiSuccess {String} data.name 应用名称
* @apiSuccess {Object} data.local 应用本地安装信息 * @apiSuccess {Object} data.config 应用配置信息
* @apiSuccess {String} data.log 日志内容 * @apiSuccess {String} data.log 日志内容
*/ */
public function logs() public function logs()
@ -184,35 +184,11 @@ class AppsController extends AbstractController
$appName = Request::input('app_name'); $appName = Request::input('app_name');
$lines = intval(Request::input('lines', 50)); $lines = intval(Request::input('lines', 50));
if (empty($appName)) { $logContent = implode("\n", Apps::getAppLog($appName, $lines));
return Base::retError('应用名称不能为空');
}
// 限制获取行数
if ($lines <= 0) {
$lines = 50;
} else if ($lines > 2000) {
$lines = 2000;
}
// 日志文件路径
$logFile = base_path('docker/logs/apps/' . $appName . '.log');
if (!file_exists($logFile)) {
return Base::retSuccess('success', [
'log' => ''
]);
}
// 读取日志文件最后几行
$output = [];
$cmd = 'tail -n ' . $lines . ' ' . escapeshellarg($logFile);
exec($cmd, $output);
$logContent = implode("\n", $output);
return Base::retSuccess('success', [ return Base::retSuccess('success', [
'name' => $appName, 'name' => $appName,
'local' => Apps::getAppLocalInfo($appName), 'config' => Apps::getAppConfig($appName),
'log' => trim($logContent) 'log' => trim($logContent)
]); ]);
} }

View File

@ -34,7 +34,7 @@ class Apps
public static function appList(): array public static function appList(): array
{ {
$apps = []; $apps = [];
$baseDir = base_path('docker/apps'); $baseDir = base_path('docker/appstore/apps');
$dirs = scandir($baseDir); $dirs = scandir($baseDir);
foreach ($dirs as $dir) { foreach ($dirs as $dir) {
// 跳过当前目录、父目录和隐藏文件 // 跳过当前目录、父目录和隐藏文件
@ -44,7 +44,7 @@ class Apps
$apps[] = [ $apps[] = [
'name' => $dir, 'name' => $dir,
'info' => self::getAppInfo($dir), 'info' => self::getAppInfo($dir),
'local' => self::getAppLocalInfo($dir), 'config' => self::getAppConfig($dir),
'versions' => self::getAvailableVersions($dir), 'versions' => self::getAvailableVersions($dir),
]; ];
} }
@ -52,7 +52,7 @@ class Apps
} }
/** /**
* 获取应用信息 * 获取应用信息(比列表多返回了 document
* @param string $appName 应用名称 * @param string $appName 应用名称
* @return array * @return array
*/ */
@ -61,7 +61,7 @@ class Apps
return Base::retSuccess("success", [ return Base::retSuccess("success", [
'name' => $appName, 'name' => $appName,
'info' => self::getAppInfo($appName), 'info' => self::getAppInfo($appName),
'local' => self::getAppLocalInfo($appName), 'config' => self::getAppConfig($appName),
'versions' => self::getAvailableVersions($appName), 'versions' => self::getAvailableVersions($appName),
'document' => self::getAppDocument($appName), 'document' => self::getAppDocument($appName),
]); ]);
@ -94,12 +94,12 @@ class Apps
return Base::retError("没有找到版本 {$version}"); return Base::retError("没有找到版本 {$version}");
} }
// 获取本地安装信息 // 获取安装配置信息
$localInfo = self::getAppLocalInfo($appName); $appConfig = self::getAppConfig($appName);
// 检查是否需要卸载旧版本 // 检查是否需要卸载旧版本
if ($command === 'up' && $localInfo['status'] === 'installed' && $localInfo['require_uninstalls']) { if ($command === 'up' && $appConfig['status'] === 'installed' && $appConfig['require_uninstalls']) {
foreach ($localInfo['require_uninstalls'] as $requireUninstall) { foreach ($appConfig['require_uninstalls'] as $requireUninstall) {
if (version_compare($versionInfo['version'], $requireUninstall['version'], $requireUninstall['operator'])) { if (version_compare($versionInfo['version'], $requireUninstall['version'], $requireUninstall['operator'])) {
$op = $requireUninstall['operator'] ?: '='; $op = $requireUninstall['operator'] ?: '=';
$reason = !empty($requireUninstall['reason']) ? "(原因:{$requireUninstall['reason']}" : ''; $reason = !empty($requireUninstall['reason']) ? "(原因:{$requireUninstall['reason']}" : '';
@ -109,28 +109,28 @@ class Apps
} }
// 生成docker-compose.yml文件 // 生成docker-compose.yml文件
$res = self::generateDockerComposeYml($versionInfo['compose_file'], $localInfo['params']); $res = self::generateDockerComposeYml($versionInfo['compose_file'], $appConfig['params']);
if (Base::isError($res)) { if (Base::isError($res)) {
return $res; return $res;
} }
$RESULTS['generate'] = $res['data']; $RESULTS['generate'] = $res['data'];
// 保存信息到.applocal文件 // 保存信息到配置信息
$prefix = $command === 'up' ? 'install' : 'uninstall'; $prefix = $command === 'up' ? 'install' : 'uninstall';
$localUpdate = ['status' => $prefix . 'ing']; $updateConfig = ['status' => $prefix . 'ing'];
$localUpdate[$prefix . '_num'] = intval($localInfo[$prefix . '_num']) + 1; $updateConfig[$prefix . '_num'] = intval($appConfig[$prefix . '_num']) + 1;
$localUpdate[$prefix . '_version'] = $versionInfo['version']; $updateConfig[$prefix . '_version'] = $versionInfo['version'];
$localUpdate[$prefix . '_at'] = date('Y-m-d H:i:s'); $updateConfig[$prefix . '_at'] = date('Y-m-d H:i:s');
self::saveAppLocalInfo($appName, $localUpdate); self::saveAppConfig($appName, $updateConfig);
// 执行docker-compose命令 // 执行docker-compose命令
$curlPath = "apps/{$command}/{$appName}"; $curlPath = "apps/{$command}/{$appName}";
if ($command === 'up') { if ($command === 'up') {
$curlPath .= "?callback_url=" . urlencode("http://host.docker.internal:" . env("APP_PORT") . "/api/apps/install/callback?install_num=" . $localUpdate[$prefix . '_num']); $curlPath .= "?callback_url=" . urlencode("http://host.docker.internal:" . env("APP_PORT") . "/api/apps/install/callback?install_num=" . $updateConfig[$prefix . '_num']);
} }
$res = self::curl($curlPath); $res = self::curl($curlPath);
if (Base::isError($res)) { if (Base::isError($res)) {
self::saveAppLocalInfo($appName, ['status' => 'error']); self::saveAppConfig($appName, ['status' => 'error']);
return $res; return $res;
} }
$RESULTS['compose'] = $res['data']; $RESULTS['compose'] = $res['data'];
@ -165,7 +165,7 @@ class Apps
public static function dockerComposeFinalize(string $appName, string $status): array public static function dockerComposeFinalize(string $appName, string $status): array
{ {
// 获取当前应用信息 // 获取当前应用信息
$appInfo = self::getAppLocalInfo($appName); $appInfo = self::getAppConfig($appName);
// 只有在安装中的状态才能更新 // 只有在安装中的状态才能更新
if (!in_array($appInfo['status'], ['installing', 'uninstalling'])) { if (!in_array($appInfo['status'], ['installing', 'uninstalling'])) {
@ -173,14 +173,13 @@ class Apps
} }
// 保存配置 // 保存配置
if (!self::saveAppLocalInfo($appName, ['status' => $status])) { if (!self::saveAppConfig($appName, ['status' => $status])) {
return Base::retError('更新状态失败'); return Base::retError('更新状态失败');
} }
// 更新nginx配置
$res = self::nginxUpdate($appName); $res = self::nginxUpdate($appName);
if (Base::isError($res)) { if (Base::isError($res)) {
self::saveAppLocalInfo($appName, ['status' => 'error']); self::saveAppConfig($appName, ['status' => 'error']);
return $res; return $res;
} }
@ -205,18 +204,18 @@ class Apps
*/ */
public static function nginxUpdate(string $appName): array public static function nginxUpdate(string $appName): array
{ {
// 获取本地安装信息 // 获取安装配置信息
$localInfo = self::getAppLocalInfo($appName); $appConfig = self::getAppConfig($appName);
// nginx配置文件处理 // nginx配置文件处理
$nginxFile = base_path('docker/apps/' . $appName . '/' . $localInfo['install_version'] . '/nginx.conf'); $nginxFile = base_path('docker/appstore/apps/' . $appName . '/' . $appConfig['install_version'] . '/nginx.conf');
$nginxTarget = base_path('docker/nginx/apps/' . $appName . '.conf'); $nginxTarget = base_path('docker/appstore/configs/' . $appName . '/nginx.conf');
$needReload = false; $needReload = false;
if (file_exists($nginxTarget)) { if (file_exists($nginxTarget)) {
unlink($nginxTarget); unlink($nginxTarget);
$needReload = true; $needReload = true;
} }
if (file_exists($nginxFile) && $localInfo['status'] === 'installed') { if (file_exists($nginxFile) && $appConfig['status'] === 'installed') {
copy($nginxFile, $nginxTarget); copy($nginxFile, $nginxTarget);
$res = self::curl("nginx/test"); $res = self::curl("nginx/test");
if (Base::isError($res)) { if (Base::isError($res)) {
@ -245,7 +244,7 @@ class Apps
*/ */
public static function getAppInfo(string $appName): array public static function getAppInfo(string $appName): array
{ {
$baseDir = base_path('docker/apps/' . $appName); $baseDir = base_path('docker/appstore/apps/' . $appName);
$info = [ $info = [
'name' => $appName, 'name' => $appName,
'description' => '', 'description' => '',
@ -379,7 +378,7 @@ class Apps
*/ */
public static function getAppEntryPoints(string $appName): array public static function getAppEntryPoints(string $appName): array
{ {
$baseDir = base_path('docker/apps/' . $appName); $baseDir = base_path('docker/appstore/apps/' . $appName);
$entryPoints = []; $entryPoints = [];
if (!file_exists($baseDir . '/config.yml')) { if (!file_exists($baseDir . '/config.yml')) {
@ -417,15 +416,15 @@ class Apps
} }
/** /**
* 获取应用的本地安装信息 * 获取应用配置信息
* *
* @param string $appName 应用名称 * @param string $appName 应用名称
* @return array 应用的本地安装信息 * @return array 应用配置信息
*/ */
public static function getAppLocalInfo(string $appName): array public static function getAppConfig(string $appName): array
{ {
$baseDir = base_path('docker/apps/' . $appName); $baseDir = base_path('docker/appstore/configs/' . $appName);
$appLocalFile = $baseDir . '/.applocal'; $configFile = $baseDir . '/config.json';
$defaultInfo = [ $defaultInfo = [
'install_at' => '', // 最后一次安装的时间 'install_at' => '', // 最后一次安装的时间
@ -439,14 +438,14 @@ class Apps
], ],
]; ];
if (file_exists($appLocalFile)) { if (file_exists($configFile)) {
$localInfo = json_decode(file_get_contents($appLocalFile), true); $appConfig = json_decode(file_get_contents($configFile), true);
if (json_last_error() === JSON_ERROR_NONE && is_array($localInfo)) { if (json_last_error() === JSON_ERROR_NONE && is_array($appConfig)) {
$defaultInfo = array_merge($defaultInfo, $localInfo); $defaultInfo = array_merge($defaultInfo, $appConfig);
} }
} else { } else {
Base::makeDir($baseDir); Base::makeDir($baseDir);
file_put_contents($appLocalFile, json_encode($defaultInfo, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); file_put_contents($configFile, json_encode($defaultInfo, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
} }
// 确保 status 状态 // 确保 status 状态
@ -466,37 +465,37 @@ class Apps
} }
/** /**
* 保存应用的本地配置信息 * 保存应用配置信息
* *
* @param string $appName 应用名称 * @param string $appName 应用名称
* @param array $data 要更新的数据 * @param array $data 要更新的数据
* @param bool $merge 是否与现有配置合并默认true * @param bool $merge 是否与现有配置合并默认true
* @return bool 保存是否成功 * @return bool 保存是否成功
*/ */
public static function saveAppLocalInfo(string $appName, array $data, bool $merge = true): bool public static function saveAppConfig(string $appName, array $data, bool $merge = true): bool
{ {
$baseDir = base_path('docker/apps/' . $appName); $baseDir = base_path('docker/appstore/configs/' . $appName);
$appLocalFile = $baseDir . '/.applocal'; $configFile = $baseDir . '/config.json';
// 初始化数据 // 初始化数据
$localInfo = []; $appConfig = [];
// 如果需要合并,先读取现有配置 // 如果需要合并,先读取现有配置
if ($merge && file_exists($appLocalFile)) { if ($merge && file_exists($configFile)) {
$existingData = json_decode(file_get_contents($appLocalFile), true); $existingData = json_decode(file_get_contents($configFile), true);
if (json_last_error() === JSON_ERROR_NONE && is_array($existingData)) { if (json_last_error() === JSON_ERROR_NONE && is_array($existingData)) {
$localInfo = $existingData; $appConfig = $existingData;
} }
} }
// 更新数据 // 更新数据
foreach ($data as $key => $value) { foreach ($data as $key => $value) {
if (is_array($value) && isset($localInfo[$key]) && is_array($localInfo[$key])) { if (is_array($value) && isset($appConfig[$key]) && is_array($appConfig[$key])) {
// 如果是嵌套数组,进行深度合并 // 如果是嵌套数组,进行深度合并
$localInfo[$key] = array_replace_recursive($localInfo[$key], $value); $appConfig[$key] = array_replace_recursive($appConfig[$key], $value);
} else { } else {
// 普通值直接覆盖 // 普通值直接覆盖
$localInfo[$key] = $value; $appConfig[$key] = $value;
} }
} }
@ -507,11 +506,43 @@ class Apps
// 写入文件 // 写入文件
return (bool)file_put_contents( return (bool)file_put_contents(
$appLocalFile, $configFile,
json_encode($localInfo, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) json_encode($appConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
); );
} }
/**
* 获取应用的日志
* @param string $appName
* @param int $lines 获取的行数默认50行, 最大2000行
* @return array
*/
public static function getAppLog(string $appName, int $lines = 50): array
{
// 日志文件路径
$logFile = base_path('docker/appstore/logs/' . $appName . '.log');
// 检查日志文件是否存在
if (!file_exists($logFile)) {
return [];
}
// 限制获取行数
if ($lines <= 0) {
$lines = 50;
} else if ($lines > 2000) {
$lines = 2000;
}
// 读取日志文件最后几行
$output = [];
$cmd = 'tail -n ' . $lines . ' ' . escapeshellarg($logFile);
exec($cmd, $output);
// 返回日志内容
return $output;
}
/** /**
* 获取应用的文档README * 获取应用的文档README
* *
@ -519,7 +550,7 @@ class Apps
* @return string 文档内容,如果未找到则返回空字符串 * @return string 文档内容,如果未找到则返回空字符串
*/ */
private static function getAppDocument(string $appName): string { private static function getAppDocument(string $appName): string {
$baseDir = base_path('docker/apps/' . $appName); $baseDir = base_path('docker/appstore/apps/' . $appName);
$lang = Base::headerOrInput('language'); $lang = Base::headerOrInput('language');
// 使用 glob 遍历目录 // 使用 glob 遍历目录
@ -578,10 +609,10 @@ class Apps
$networkName = 'dootask-networks-' . env('APP_ID'); $networkName = 'dootask-networks-' . env('APP_ID');
// 主机路径 // 主机路径
$hostPwd = '${HOST_PWD}/docker/apps/' . $appName . '/' . basename(dirname($filePath)); $hostPwd = '${HOST_PWD}/docker/appstore/apps/' . $appName . '/' . basename(dirname($filePath));
// 保存路径 // 保存路径
$savePath = dirname($filePath) . '/.docker-compose.local.yml'; $savePath = base_path('docker/appstore/configs/' . $appName . '/docker-compose.yml');
try { try {
// 读取文件内容 // 读取文件内容
@ -681,8 +712,8 @@ class Apps
*/ */
private static function processAppIcon(string $appName, array $iconFiles): string private static function processAppIcon(string $appName, array $iconFiles): string
{ {
$baseDir = base_path('docker/apps/' . $appName); $baseDir = base_path('docker/appstore/apps/' . $appName);
$iconDir = public_path('uploads/file/apps/' . $appName); $iconDir = public_path('uploads/file/appstore/' . $appName);
foreach ($iconFiles as $iconFile) { foreach ($iconFiles as $iconFile) {
// 如果图标为空,则跳过 // 如果图标为空,则跳过
@ -711,7 +742,7 @@ class Apps
} }
// 返回图标URL // 返回图标URL
return Base::fillUrl('uploads/file/apps/' . $appName . '/' . $targetName); return Base::fillUrl('uploads/file/appstore/' . $appName . '/' . $targetName);
} }
} }
@ -726,7 +757,7 @@ class Apps
*/ */
private static function getAvailableVersions(string $appName): array private static function getAvailableVersions(string $appName): array
{ {
$baseDir = base_path('docker/apps/' . $appName); $baseDir = base_path('docker/appstore/apps/' . $appName);
$versions = []; $versions = [];
// 检查应用目录是否存在 // 检查应用目录是否存在

View File

@ -97,8 +97,7 @@ class Volumes
// 处理./或../开头的显式相对路径 // 处理./或../开头的显式相对路径
if (str_starts_with($path, './') || str_starts_with($path, '../')) { if (str_starts_with($path, './') || str_starts_with($path, '../')) {
$cleanPath = ltrim($path, './'); return $hostPwd . '/' . ltrim($path, './');
return $hostPwd . '/' . $cleanPath;
} }
// 处理不以/开头的路径,且要么包含/(明确是路径而非命名卷) // 处理不以/开头的路径,且要么包含/(明确是路径而非命名卷)

View File

@ -39,8 +39,7 @@ services:
- "${APP_PORT}:80" - "${APP_PORT}:80"
- "${APP_SSL_PORT:-}:443" - "${APP_SSL_PORT:-}:443"
volumes: volumes:
- ./docker/nginx:/etc/nginx/conf.d - ./:/var/www
- ./public:/var/www/public
networks: networks:
extnetwork: extnetwork:
ipv4_address: "${APP_IPPR}.3" ipv4_address: "${APP_IPPR}.3"
@ -223,8 +222,8 @@ services:
privileged: true privileged: true
image: "kuaifan/dootask-appstore:0.0.1" image: "kuaifan/dootask-appstore:0.0.1"
volumes: volumes:
- ./:/var/www
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- ./:/var/www
environment: environment:
DOO_ENV: "/var/www" DOO_ENV: "/var/www"
HOST_PWD: "${PWD}" HOST_PWD: "${PWD}"

View File

@ -0,0 +1,8 @@
Directory structure
```
appstore/
├── apps/ # Application Directory
├── config/ # Configuration file directory
└── logs/ # Log file directory
```

2
docker/appstore/logs/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

View File

@ -21,7 +21,7 @@ upstream service {
server { server {
listen 80; listen 80;
include /etc/nginx/conf.d/site/*.conf; include /var/www/docker/nginx/site/*.conf;
root /var/www/public; root /var/www/public;
@ -78,8 +78,8 @@ server {
proxy_pass http://service; proxy_pass http://service;
} }
include /etc/nginx/conf.d/location/*.conf; include /var/www/docker/nginx/location/*.conf;
include /etc/nginx/conf.d/apps/*.conf; include /var/www/docker/appstore/configs/*/nginx.conf;
} }
include /etc/nginx/conf.d/conf.d/*.conf; include /var/www/docker/nginx/conf.d/*.conf;