2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-05 08:07:17 +08:00

feat: update

This commit is contained in:
耗子
2023-07-21 02:26:12 +08:00
parent ea59149175
commit 0d4f8bbea1
15 changed files with 388 additions and 98 deletions

View File

@@ -73,7 +73,12 @@ func (receiver *Panel) Handle(ctx console.Context) error {
color.Greenln("初始化成功")
case "update":
err := tools.UpdatePanel()
input := arg1
proxy := false
if input == "y" || input == "Y" || input == "yes" || input == "Yes" {
proxy = true
}
err := tools.UpdatePanel(cast.ToBool(proxy))
if err != nil {
color.Redln("更新失败: " + err.Error())
return nil
@@ -279,7 +284,7 @@ func (receiver *Panel) Handle(ctx console.Context) error {
default:
color.Yellowln(facades.Config().GetString("panel.name") + "命令行工具 - " + facades.Config().GetString("panel.version"))
color.Greenln("请使用以下命令:")
color.Greenln("panel update 更新/修复面板到最新版本")
color.Greenln("panel update {proxy} 更新/修复面板到最新版本")
color.Greenln("panel getInfo 重新初始化面板账号信息")
color.Greenln("panel getPort 获取面板访问端口")
color.Greenln("panel getEntrance 获取面板访问入口")
@@ -287,7 +292,7 @@ func (receiver *Panel) Handle(ctx console.Context) error {
color.Greenln("panel backup {website/mysql/postgresql} {name} {path} 备份网站/MySQL数据库/PostgreSQL数据库到指定目录")
color.Redln("以下命令请在开发者指导下使用:")
color.Yellowln("panel init 初始化面板")
color.Yellowln("panel writePlugin {slug} 写入插件安装状态")
color.Yellowln("panel writePlugin {slug} {version} 写入插件安装状态")
color.Yellowln("panel deletePlugin {slug} 移除插件安装状态")
color.Yellowln("panel writeMysqlPassword {password} 写入MySQL root密码")
color.Yellowln("panel writeSite {name} {status} {path} {php} {ssl} 写入网站数据到面板")

View File

@@ -122,3 +122,42 @@ func (r *InfoController) InstalledDbAndPhp(ctx http.Context) {
"postgresql": postgresqlInstalled,
})
}
func (r *InfoController) CheckUpdate(ctx http.Context) {
version := facades.Config().GetString("panel.version")
remote, err := tools.GetLatestPanelVersion()
if err != nil {
Error(ctx, http.StatusInternalServerError, "获取最新版本失败")
return
}
if version == remote.Version {
Success(ctx, http.Json{
"update": false,
"version": remote.Version,
"name": remote.Name,
"body": remote.Body,
"date": remote.Date,
})
return
}
Success(ctx, http.Json{
"update": true,
"version": remote.Version,
"name": remote.Name,
"body": remote.Body,
"date": remote.Date,
})
}
func (r *InfoController) Update(ctx http.Context) {
proxy := ctx.Request().InputBool("proxy")
err := tools.UpdatePanel(proxy)
if err != nil {
Error(ctx, http.StatusInternalServerError, "更新失败")
return
}
Success(ctx, nil)
}

View File

@@ -170,6 +170,11 @@ func (r *OpenRestyController) ErrorLog(ctx http.Context) {
return
}
if !tools.Exists("/www/wwwlogs/nginx_error.log") {
controllers.Success(ctx, "")
return
}
out := tools.ExecShell("tail -n 100 /www/wwwlogs/nginx_error.log")
controllers.Success(ctx, out)
}
@@ -180,7 +185,7 @@ func (r *OpenRestyController) ClearErrorLog(ctx http.Context) {
return
}
_ = tools.ExecShell("echo '' > /www/wwwlogs/nginx_error.log")
tools.ExecShell("echo '' > /www/wwwlogs/nginx_error.log")
controllers.Success(ctx, "清空OpenResty错误日志成功")
}
@@ -199,42 +204,64 @@ func (r *OpenRestyController) Load(ctx http.Context) {
}
raw := resp.String()
var data map[int]map[string]any
type nginxStatus struct {
Name string `json:"name"`
Value string `json:"value"`
}
var data []nginxStatus
out := tools.ExecShell("ps aux | grep nginx | grep 'worker process' | wc -l")
workers := strings.TrimSpace(out)
data[0]["name"] = "工作进程"
data[0]["value"] = workers
data = append(data, nginxStatus{
Name: "工作进程",
Value: workers,
})
out = tools.ExecShell("ps aux | grep nginx | grep 'worker process' | awk '{memsum+=$6};END {print memsum}'")
mem := tools.FormatBytes(cast.ToFloat64(strings.TrimSpace(out)))
data[1]["name"] = "内存占用"
data[1]["value"] = mem
data = append(data, nginxStatus{
Name: "内存占用",
Value: mem,
})
match := regexp.MustCompile(`Active connections:\s+(\d+)`).FindStringSubmatch(raw)
if len(match) == 2 {
data[2]["name"] = "活跃连接数"
data[2]["value"] = match[1]
data = append(data, nginxStatus{
Name: "活跃连接数",
Value: match[1],
})
}
match = regexp.MustCompile(`server accepts handled requests\s+(\d+)\s+(\d+)\s+(\d+)`).FindStringSubmatch(raw)
if len(match) == 4 {
data[3]["name"] = "总连接次数"
data[3]["value"] = match[1]
data[4]["name"] = "总握手次数"
data[4]["value"] = match[2]
data[5]["name"] = "总请求次数"
data[5]["value"] = match[3]
data = append(data, nginxStatus{
Name: "总连接次数",
Value: match[1],
})
data = append(data, nginxStatus{
Name: "总握手次数",
Value: match[2],
})
data = append(data, nginxStatus{
Name: "总请求次数",
Value: match[3],
})
}
match = regexp.MustCompile(`Reading:\s+(\d+)\s+Writing:\s+(\d+)\s+Waiting:\s+(\d+)`).FindStringSubmatch(raw)
if len(match) == 4 {
data[6]["name"] = "请求数"
data[6]["value"] = match[1]
data[7]["name"] = "响应数"
data[7]["value"] = match[2]
data[8]["name"] = "驻留进程"
data[8]["value"] = match[3]
data = append(data, nginxStatus{
Name: "请求数",
Value: match[1],
})
data = append(data, nginxStatus{
Name: "响应数",
Value: match[2],
})
data = append(data, nginxStatus{
Name: "驻留进程",
Value: match[3],
})
}
controllers.Success(ctx, data)

View File

@@ -69,7 +69,7 @@ func (r *TaskController) Log(ctx http.Context) {
func (r *TaskController) Delete(ctx http.Context) {
var task models.Task
_, err := facades.Orm().Query().Where("id", ctx.Request().QueryInt("id")).Delete(&task)
_, err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).Delete(&task)
if err != nil {
facades.Log().Error("[面板][TaskController] 删除任务失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")

View File

@@ -8,6 +8,6 @@ func init() {
config := facades.Config()
config.Add("panel", map[string]any{
"name": "耗子面板",
"version": "2.0.0",
"version": "v2.0.0",
})
}

View File

@@ -51,7 +51,7 @@ func ExecShell(shell string) string {
output, err := cmd.CombinedOutput()
if err != nil {
facades.Log().Errorf("[面板][Helpers] 执行命令 $s 失败: %s", shell, err.Error())
facades.Log().Errorf("[面板][Helpers] 执行命令 %s 失败: %s", shell, err.Error())
return ""
}
@@ -64,13 +64,13 @@ func ExecShellAsync(shell string) {
err := cmd.Start()
if err != nil {
facades.Log().Errorf("[面板][Helpers] 执行命令 $s 失败: %s", shell, err.Error())
facades.Log().Errorf("[面板][Helpers] 执行命令 %s 失败: %s", shell, err.Error())
}
go func() {
err := cmd.Wait()
if err != nil {
facades.Log().Errorf("[面板][Helpers] 执行命令 $s 失败: %s", shell, err.Error())
facades.Log().Errorf("[面板][Helpers] 执行命令 %s 失败: %s", shell, err.Error())
}
}()
}

View File

@@ -5,9 +5,11 @@ import (
"errors"
"os"
"os/exec"
"strconv"
"strings"
"time"
"github.com/gookit/color"
"github.com/shirou/gopsutil/cpu"
"github.com/shirou/gopsutil/disk"
"github.com/shirou/gopsutil/host"
@@ -114,17 +116,52 @@ func GetLatestPanelVersion() (PanelInfo, error) {
}
// UpdatePanel 更新面板
func UpdatePanel() error {
func UpdatePanel(proxy bool) error {
panelInfo, err := GetLatestPanelVersion()
if err != nil {
return err
}
cmd := exec.Command("/bin/bash", "-c", "wget -O panel.tar.gz "+panelInfo.DownloadUrl+" && tar -zxvf panel.tar.gz && rm -rf panel.tar.gz && chmod +x panel && ./panel artisan migrate")
_, err = cmd.Output()
if err != nil {
return errors.New("更新面板失败")
color.Greenln("最新版本: " + panelInfo.Version)
color.Greenln("下载链接: " + panelInfo.DownloadUrl)
color.Greenln("使用代理: " + strconv.FormatBool(proxy))
color.Greenln("备份面板配置...")
ExecShell("cp -f /www/panel/database/panel.db /tmp/panel.db.bak")
ExecShell("cp -f /www/panel/panel.conf /tmp/panel.conf.bak")
if !Exists("/tmp/panel.db.bak") || !Exists("/tmp/panel.conf.bak") {
return errors.New("备份面板配置失败")
}
color.Greenln("备份完成")
color.Greenln("清理旧版本...")
ExecShell("rm -rf /www/panel/*")
color.Greenln("清理完成")
color.Greenln("正在下载...")
if proxy {
ExecShell("wget -O /www/panel/panel.zip https://ghproxy.com/" + panelInfo.DownloadUrl)
} else {
ExecShell("wget -O /www/panel/panel.zip " + panelInfo.DownloadUrl)
}
color.Greenln("下载完成")
color.Greenln("更新新版本...")
ExecShell("cd /www/panel && unzip -o panel.zip && rm -rf panel.zip && chmod 700 panel")
color.Greenln("更新完成")
color.Greenln("恢复面板配置...")
ExecShell("cp -f /tmp/panel.db.bak /www/panel/database/panel.db")
ExecShell("cp -f /tmp/panel.conf.bak /www/panel/panel.conf")
if !Exists("/www/panel/database/panel.db") || !Exists("/www/panel/panel.conf") {
return errors.New("恢复面板配置失败")
}
ExecShell("/www/panel/panel --env=panel.conf artisan migrate")
color.Greenln("恢复完成")
color.Greenln("重启面板...")
ExecShell("systemctl restart panel")
color.Greenln("重启完成")
return nil
}

View File

@@ -63,7 +63,7 @@ layui.define(['laytpl', 'layer'], function (exports) {
delete options.success
delete options.error
if (options.type === 'post' || options.type === 'put' || options.type === 'delete' || options.type === 'patch' || options.type === 'POST' || options.type === 'PUT' || options.type === 'DELETE' || options.type === 'PATCH') {
if (options.method === 'post' || options.method === 'put' || options.method === 'delete' || options.method === 'patch' || options.method === 'POST' || options.method === 'PUT' || options.method === 'DELETE' || options.method === 'PATCH') {
options.contentType = 'application/json'
options.data = JSON.stringify(options.data)
}

View File

@@ -320,22 +320,50 @@ Date: 2023-06-22
layer.msg('获取版本信息失败,请刷新重试!')
return false
}
if (result.data.version) {
admin.popup({
title: '提示'
,
shade: 0
,
anim: -1
,
area: ['400px', '200px']
,
id: 'layadmin-layer-skin-update-panel'
,
skin: 'layui-anim layui-anim-upbit'
,
content: '最新版本:' + result.data.version + '<br><br>更新日志:' + result.data.describe + '<br><br>请在SSH执行<span class="layui-badge-rim">panel update</span>以更新面板!'
})
if (result.data.update) {
layer.confirm('更新日期: <br>'+new Date(result.data.date).toLocaleString()+'<br>更新日志: <pre>'+result.data.body+'</pre>', {
title: '最新版本: '+result.data.version+' ,是否更新?',
btn: ['更新', '取消']
}, function () {
let proxy = false
layer.confirm('对于大陆服务器,建议使用代理进行更新', {
title: '是否使用代理更新?',
btn: ['', '']
}, function () {
proxy = true
index = layer.msg('正在更新...', { icon: 16, time: 0, shade: 0.3 })
admin.req({
url: '/api/panel/info/update'
, method: 'post'
, data: { proxy: proxy }
, success: function (result) {
layer.close(index)
if (result.code !== 0) {
return false
}
layer.alert('更新成功!')
location.href = '/';
}
})
}, function(){
proxy = false
index = layer.msg('正在更新...', { icon: 16, time: 0, shade: 0.3 })
admin.req({
url: '/api/panel/info/update'
, method: 'post'
, data: { proxy: proxy }
, success: function (result) {
layer.close(index)
if (result.code !== 0) {
return false
}
layer.alert('更新成功!')
location.href = '/';
}
})
})
})
} else {
layer.msg('当前已是最新版本!')
}

View File

@@ -43,7 +43,7 @@ Date: 2023-06-24
</div>
<div class="layui-tab-item">
<div class="layui-btn-container">
<button id="openresty-clean-error-log" class="layui-btn">清空日志</button>
<button id="openresty-clear-error-log" class="layui-btn">清空日志</button>
</div>
<pre id="openresty-error-log" class="layui-code">
获取中...
@@ -141,54 +141,50 @@ Date: 2023-06-24
// 事件监听
$('#openresty-start').click(function () {
admin.popup({
title: '<span style="color: red;">警告</span>'
,
shade: 0
,
anim: -1
,
area: ['300px', '200px']
,
id: 'layadmin-layer-skin-openresty-start'
,
skin: 'layui-anim layui-anim-upbit'
,
content: '面板的正常访问依赖OpenResty因此不支持在面板启动OpenResty如您确需操作请在SSH执行<span class="layui-badge-rim">systemctl start nginx</span>以启动OpenResty'
})
index = layer.msg('正在启动OpenResty...', { icon: 16, time: 0, shade: 0.3 })
admin.req({
url: '/api/plugins/openresty/start'
, method: 'post'
, success: function (result) {
layer.close(index)
if (result.code !== 0) {
return false
}
admin.events.refresh()
layer.alert('OpenResty启动成功')
}
})
})
$('#openresty-stop').click(function () {
admin.popup({
title: '<span style="color: red;">警告</span>'
,
shade: 0
,
anim: -1
,
area: ['300px', '200px']
,
id: 'layadmin-layer-skin-openresty-stop'
,
skin: 'layui-anim layui-anim-upbit'
,
content: '面板的正常访问依赖OpenResty因此不支持在面板停止OpenResty如您确需操作请在SSH执行<span class="layui-badge-rim">systemctl stop nginx</span>以停止OpenResty'
})
layer.confirm('停止OpenResty将导致网站无法访问是否继续停止', {
btn: ['停止', '取消']
}, function () {
index = layer.msg('正在停止OpenResty...', { icon: 16, time: 0, shade: 0.3 })
admin.req({
url: '/api/plugins/openresty/stop'
, method: 'post'
, success: function (result) {
layer.close(index)
if (result.code !== 0) {
return false
}
admin.events.refresh()
layer.alert('OpenResty停止成功')
}
})
})
})
$('#openresty-restart').click(function () {
layer.confirm('重启OpenResty有可能导致面板短时间无法访问,是否继续重启?', {
layer.confirm('重启OpenResty将导致网站短时间无法访问,是否继续重启?', {
btn: ['重启', '取消']
}, function () {
index = layer.msg('正在重启OpenResty...', { icon: 16, time: 0 })
index = layer.msg('正在重启OpenResty...', { icon: 16, time: 0, shade: 0.3 })
admin.req({
url: '/api/plugins/openresty/restart'
, method: 'post'
, beforeSend: function () {
layer.msg('已发送重启请求,请稍后刷新确认重启状态。')
}
, success: function (result) {
layer.close(index)
if (result.code !== 0) {
console.log('耗子Linux面板OpenResty重启失败接口返回' + result)
return false
}
admin.events.refresh()
@@ -198,22 +194,22 @@ Date: 2023-06-24
})
})
$('#openresty-reload').click(function () {
index = layer.msg('正在重载OpenResty...', { icon: 16, time: 0 })
index = layer.msg('正在重载OpenResty...', { icon: 16, time: 0, shade: 0.3 })
admin.req({
url: '/api/plugins/openresty/reload'
, method: 'post'
, success: function (result) {
layer.close(index)
if (result.code !== 0) {
console.log('耗子Linux面板OpenResty重载失败接口返回' + result)
return false
}
admin.events.refresh()
layer.alert('OpenResty重载成功')
}
})
})
$('#openresty-config-save').click(function () {
index = layer.msg('正在保存OpenResty主配置...', { icon: 16, time: 0 })
index = layer.msg('正在保存OpenResty主配置...', { icon: 16, time: 0, shade: 0.3 })
admin.req({
url: '/api/plugins/openresty/config'
, method: 'post'
@@ -223,17 +219,16 @@ Date: 2023-06-24
, success: function (result) {
layer.close(index)
if (result.code !== 0) {
console.log('耗子Linux面板OpenResty配置保存失败接口返回' + result)
return false
}
layer.alert('OpenResty配置保存成功')
}
})
})
$('#openresty-clean-error-log').click(function () {
index = layer.msg('正在清空OpenResty错误日志...', { icon: 16, time: 0 })
$('#openresty-clear-error-log').click(function () {
index = layer.msg('正在清空OpenResty错误日志...', { icon: 16, time: 0, shade: 0.3 })
admin.req({
url: '/api/plugins/openresty/cleanErrorLog'
url: '/api/plugins/openresty/clearErrorLog'
, method: 'post'
, success: function (result) {
layer.close(index)

View File

@@ -0,0 +1,158 @@
<!--
Name: 任务中心
Author: 耗子
Date: 2023-07-21
-->
<title>任务中心</title>
<div class="layui-fluid" id="component-tabs">
<div class="layui-row">
<div class="layui-col-md12">
<div class="layui-card">
<div class="layui-card-header">任务列表</div>
<div class="layui-card-body">
<div class="layui-tab">
<ul class="layui-tab-title">
<li class="layui-this">进行中</li>
<li>等待中</li>
<li>已完成</li>
</ul>
<div class="layui-tab-content">
<div class="layui-tab-item layui-show">
<script type="text/html" template
lay-url="/api/panel/task/list?status=running&page=1&limit=10"
lay-done="layui.data.done(d);">
{{# if(d.data.total != 0){ }}
<blockquote class="layui-elem-quote">{{ d.data.items[0].name }}</blockquote>
<pre id="plugin-install-log" class="layui-code">
日志获取中...
</pre>
{{# } else { }}
<blockquote class="layui-elem-quote">暂无任务</blockquote>
{{# } }}
</script>
</div>
<div class="layui-tab-item">
<table id="panel-task-waiting" lay-filter="panel-task-waiting"></table>
</div>
<div class="layui-tab-item">
<table id="panel-task-finished" lay-filter="panel-task-finished"></table>
<script type="text/html" id="panel-task-finished-control-tpl">
<a class="layui-btn layui-btn-xs" lay-event="remove">移除</a>
</script>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
function render_plugin_install_log(d) {
layui.use(['admin', 'jquery'], function () {
let admin = layui.admin
, $ = layui.jquery;
admin.req({
url: "/api/panel/task/log?id=" + d.data.items[0].id
, method: 'get'
, success: function (result) {
if (result.code !== 0) {
$('#plugin-install-log').html('实时安装日志获取失败,请刷新重试!');
return false;
}
$('#plugin-install-log').html(result.data);
}
})
});
}
layui.data.done = function (d) {
if (d.data.items[0] !== undefined) {
render_plugin_install_log(d);
setInterval(function () {
render_plugin_install_log(d);
}, 1000);
}
};
layui.use(['admin', 'table', 'jquery'], function () {
var table = layui.table
, admin = layui.admin;
table.render({
elem: '#panel-task-waiting'
, url: '/api/panel/task/list?status=waiting'
, cols: [[
{field: 'id', hide: true, title: 'ID', sort: true}
, {field: 'name', width: '100%', title: '任务名'}
]]
, page: true
, text: '耗子Linux面板数据加载出现异常'
, parseData: function (res) {
return {
"code": res.code,
"msg": res.message,
"count": res.data.total,
"data": res.data.items
};
}
});
table.render({
elem: '#panel-task-finished'
, id: 'panel-task-finished-table'
, url: '/api/panel/task/list?status=finished'
, cols: [[
{field: 'id', hide: true, title: 'ID', sort: true}
, {field: 'name', width: '80%', title: '任务名'}
, {
field: 'control',
title: '操作',
templet: '#panel-task-finished-control-tpl',
fixed: 'right',
align: 'center'
}
]]
, page: true
, text: '耗子Linux面板数据加载出现异常'
, parseData: function (res) {
return {
"code": res.code,
"msg": res.message,
"count": res.data.total,
"data": res.data.items
};
}
});
// 工具条
table.on('tool(panel-task-finished)', function (obj) {
let data = obj.data;
if (obj.event === 'remove') {
layer.confirm('确定移除该记录吗?', function (index) {
layer.close(index);
admin.req({
url: '/api/panel/task/delete',
type: 'post',
data: {
id: data.id
}
, success: function (res) {
if (res.code == 0) {
layer.msg('移除任务:' + data.name + ' 成功!', {icon: 1, time: 1000}, function () {
// 重载表格
table.reload('panel-task-finished-table');
});
} else {
layer.msg(res.msg, {icon: 2, time: 1000});
}
}
});
});
}
});
});
</script>

View File

@@ -23,7 +23,7 @@ func Plugin() {
route.Get("config", openRestyController.GetConfig)
route.Post("config", openRestyController.SaveConfig)
route.Get("errorLog", openRestyController.ErrorLog)
route.Get("clearErrorLog", openRestyController.ClearErrorLog)
route.Post("clearErrorLog", openRestyController.ClearErrorLog)
})
facades.Route().Prefix("api/plugins/mysql80").Middleware(middleware.Jwt()).Group(func(route route.Route) {
mysql80Controller := mysql80.NewMysql80Controller()

View File

@@ -19,6 +19,8 @@ func Web() {
r.Middleware(middleware.Jwt()).Get("nowMonitor", infoController.NowMonitor)
r.Middleware(middleware.Jwt()).Get("systemInfo", infoController.SystemInfo)
r.Middleware(middleware.Jwt()).Get("installedDbAndPhp", infoController.InstalledDbAndPhp)
r.Middleware(middleware.Jwt()).Get("checkUpdate", infoController.CheckUpdate)
r.Middleware(middleware.Jwt()).Post("update", infoController.Update)
})
r.Prefix("user").Group(func(r route.Route) {
userController := controllers.NewUserController()

View File

@@ -58,8 +58,7 @@ Prepare_system() {
exit 1
fi
wwwUserCheck=$(cat /etc/passwd | grep www)
if [ "${wwwUserCheck}" == "" ]; then
if ! id -u "www" >/dev/null 2>&1; then
groupadd www
useradd -s /sbin/nologin -g www www
fi

View File

@@ -362,6 +362,6 @@ systemctl daemon-reload
systemctl enable openresty.service
systemctl start openresty.service
panel writePlugin openresty
panel writePlugin openresty ${openrestyVersion}
echo -e "${HR}\nOpenResty install completed.\n${HR}"