mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-09 08:00:27 +00:00
Compare commits
88
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d548c7df9 | ||
|
|
8ec5a5bd4b | ||
|
|
2e8bcc09e6 | ||
|
|
29a304e2c8 | ||
|
|
b1adc37920 | ||
|
|
03fac3ec06 | ||
|
|
6e465ac215 | ||
|
|
21bbadc37b | ||
|
|
3061d640ce | ||
|
|
874717cbcd | ||
|
|
6850acabae | ||
|
|
890ea42749 | ||
|
|
6aaf61cde4 | ||
|
|
73930c5e29 | ||
|
|
8f8d280e90 | ||
|
|
b46192024f | ||
|
|
8f939ad5be | ||
|
|
a6363ce4e5 | ||
|
|
fd490b2642 | ||
|
|
7624670ee0 | ||
|
|
8c3145e1c6 | ||
|
|
bec7b6989f | ||
|
|
326f2654e6 | ||
|
|
708499a335 | ||
|
|
bdeb842939 | ||
|
|
edefdce3ff | ||
|
|
b348c36571 | ||
|
|
6ae150b64e | ||
|
|
3687b9bea2 | ||
|
|
711431da82 | ||
|
|
e93afc33aa | ||
|
|
82dd993751 | ||
|
|
9ba293e154 | ||
|
|
8fa9f128d8 | ||
|
|
ed9f1599e6 | ||
|
|
787a29769d | ||
|
|
6b379389d3 | ||
|
|
5bcbf32823 | ||
|
|
0aa90e1b2d | ||
|
|
a7b87211b4 | ||
|
|
2c666933a5 | ||
|
|
f280b533c6 | ||
|
|
47a16bc9a9 | ||
|
|
713202b6eb | ||
|
|
79020abb1c | ||
|
|
83ef41cf1a | ||
|
|
91675c69e9 | ||
|
|
0e68065b40 | ||
|
|
600b769083 | ||
|
|
e7670f96de | ||
|
|
342c04b30a | ||
|
|
5ab8377ac1 | ||
|
|
74eb2c8dd6 | ||
|
|
f136d97941 | ||
|
|
a5629e3f42 | ||
|
|
515d6ca500 | ||
|
|
c0ccc0318d | ||
|
|
8196c11010 | ||
|
|
170bd22062 | ||
|
|
097a33c68f | ||
|
|
3be3bf9926 | ||
|
|
038f2b4f54 | ||
|
|
f84e284c20 | ||
|
|
48e75b5916 | ||
|
|
a12ceaee6e | ||
|
|
9050680115 | ||
|
|
0022a9b508 | ||
|
|
3437d62f31 | ||
|
|
dc880835d0 | ||
|
|
1663bf8677 | ||
|
|
aed236cbc9 | ||
|
|
90be74504e | ||
|
|
bba8aab18c | ||
|
|
47d135ecca | ||
|
|
d96fe49993 | ||
|
|
ac396b61ce | ||
|
|
6798d0c7ee | ||
|
|
1490ed7541 | ||
|
|
48b3032de6 | ||
|
|
ed12bec460 | ||
|
|
3c4a1ea614 | ||
|
|
79eb0288b4 | ||
|
|
99775e1eb4 | ||
|
|
db848b41d2 | ||
|
|
b5b11dff86 | ||
|
|
9cf4bfc606 | ||
|
|
221162d44f | ||
|
|
617970a1f8 |
+5
-3
@@ -10,7 +10,7 @@ before:
|
||||
- ./ci/script.sh
|
||||
- sed -i 's@ORIGINAL_VERSION=.*@ORIGINAL_VERSION=v{{ .Version }}@g' 1pctl
|
||||
- go mod tidy
|
||||
|
||||
|
||||
builds:
|
||||
- main: ./cmd/server/main.go
|
||||
binary: 1panel
|
||||
@@ -31,6 +31,7 @@ builds:
|
||||
- arm
|
||||
- ppc64le
|
||||
- s390x
|
||||
- riscv64
|
||||
|
||||
archives:
|
||||
- formats: [ 'tar.gz' ]
|
||||
@@ -38,10 +39,11 @@ archives:
|
||||
wrap_in_directory: true
|
||||
files:
|
||||
- 1pctl
|
||||
- 1panel.service
|
||||
- install.sh
|
||||
- GeoIP.mmdb
|
||||
- 1panel.service
|
||||
- initscript/*
|
||||
- lang/*
|
||||
- GeoIP.mmdb
|
||||
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
|
||||
@@ -23,7 +23,7 @@ build_frontend:
|
||||
|
||||
build_backend_on_linux:
|
||||
cd $(SERVER_PATH) \
|
||||
&& GOOS=$(GOOS) GOARCH=$(GOARCH) $(GOBUILD) -trimpath -ldflags '-s -w' -o $(BUILD_PATH)/$(APP_NAME) $(MAIN)
|
||||
&& CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) $(GOBUILD) -trimpath -ldflags '-s -w' -o $(BUILD_PATH)/$(APP_NAME) $(MAIN)
|
||||
|
||||
build_backend_on_darwin:
|
||||
cd $(SERVER_PATH) \
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<p align="center"><a href="https://1panel.pro"><img src="https://resource.1panel.pro/img/1panel-logo.png" alt="1Panel" width="300" /></a></p>
|
||||
<p align="center"><b>Top-Rated Web-based Linux Server Management Tool</b><br>Best VPS control panel<br>新一代的 Linux 服务器运维管理面板</p>
|
||||
<p align="center"><b>Simplify Linux Server Management</b></p>
|
||||
<p align="center">
|
||||
<a href="https://trendshift.io/repositories/2462" target="_blank"><img src="https://trendshift.io/api/badge/repositories/2462" alt="1Panel-dev%2F1Panel | Trendshift" style="width: 240px; height: auto;" /></a>
|
||||
</p>
|
||||
@@ -31,13 +31,14 @@
|
||||
|
||||
------------------------------
|
||||
|
||||
1Panel is an open-source, modern web-based control panel for Linux server management.
|
||||
1Panel provides an intuitive web interface and MCP Server to manage websites, files, containers, databases, and LLMs on a Linux server.
|
||||
|
||||
- **Efficient Management**: Through a user-friendly web graphical interface, 1Panel enables users to effortlessly manage their Linux servers. Key features include host monitoring, file management, database administration, container management, LLMs management.
|
||||
- **Rapid Website Deployment**: With deep integration of the popular open-source website building software WordPress, 1Panel streamlines the process of domain binding and SSL certificate configuration, all achievable with just one click.
|
||||
- **Application Store**: 1Panel curates a wide range of high-quality open-source tools and applications, facilitating easy installation and updates for its users.
|
||||
- **Security and Reliability**: By leveraging containerization and secure application deployment practices, 1Panel minimizes vulnerability exposure. It further enhances security through integrated firewall management and log auditing capabilities.
|
||||
- **One-Click Backup & Restore**: Data protection is made simple with 1Panel's one-click backup and restore functionality, supporting various cloud storage solutions to ensure data integrity and availability.
|
||||
- **MCP Server**: [1Panel MCP Server](https://github.com/1Panel-dev/mcp-1panel) allow user to execute server operations via natural language.
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -49,7 +50,7 @@ curl -sSL https://resource.1panel.pro/quick_start.sh -o quick_start.sh && bash q
|
||||
|
||||
Please refer to our [documentation](https://docs.1panel.pro/quick_start/) for more details.
|
||||
|
||||
中国用户请使用这个 [安装脚本](https://1panel.cn/docs/installation/online_installation/),其应用数量比国际版本更丰富。
|
||||
中国用户请使用此 [安装脚本](https://1panel.cn/docs/v2/installation/online_installation/),默认安装 V2 版本,应用数量相较国际版本更丰富。
|
||||
|
||||
## Screenshot
|
||||
|
||||
@@ -69,6 +70,10 @@ Compared to the OSS Edition, 1Panel Pro Edition provides users with a wealth of
|
||||
|
||||
If you discover any security issues, please refer to [SECURITY.md](/SECURITY.md).
|
||||
|
||||
## GitAds Sponsored
|
||||
|
||||
[](https://gitads.dev/v1/ad-track?source=1panel-dev/1panel@github)
|
||||
|
||||
## License
|
||||
|
||||
Licensed under The GNU General Public License version 3 (GPLv3) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
|
||||
@@ -76,3 +81,7 @@ Licensed under The GNU General Public License version 3 (GPLv3) (the "License")
|
||||
<https://www.gnu.org/licenses/gpl-3.0.html>
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
<!-- GitAds-Verify: 9M8AMBB11GYJ3RUNXGB5R1MR71KPATU9 -->
|
||||
|
||||
|
||||
|
||||
@@ -68,4 +68,6 @@ var (
|
||||
favoriteService = service.NewIFavoriteService()
|
||||
|
||||
websiteCAService = service.NewIWebsiteCAService()
|
||||
|
||||
mcpServerService = service.NewIMcpServerService()
|
||||
)
|
||||
|
||||
@@ -410,11 +410,20 @@ func (b *BaseApi) CheckFile(c *gin.Context) {
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if _, err := os.Stat(req.Path); err != nil {
|
||||
helper.SuccessWithData(c, false)
|
||||
fileOp := files.NewFileOp()
|
||||
if fileOp.Stat(req.Path) {
|
||||
helper.SuccessWithData(c, true)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, true)
|
||||
if req.WithInit {
|
||||
if err := fileOp.CreateDir(req.Path, 0644); err != nil {
|
||||
helper.SuccessWithData(c, false)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, true)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, false)
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
// @Success 200 {object} response.HostToolRes
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /host/tool [post]
|
||||
// @Router /hosts/tool [post]
|
||||
func (b *BaseApi) GetToolStatus(c *gin.Context) {
|
||||
var req request.HostToolReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
@@ -36,7 +36,7 @@ func (b *BaseApi) GetToolStatus(c *gin.Context) {
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /host/tool/create [post]
|
||||
// @Router /hosts/tool/create [post]
|
||||
// @x-panel-log {"bodyKeys":["type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建 [type] 配置","formatEN":"create [type] config"}
|
||||
func (b *BaseApi) InitToolConfig(c *gin.Context) {
|
||||
var req request.HostToolCreate
|
||||
@@ -58,7 +58,7 @@ func (b *BaseApi) InitToolConfig(c *gin.Context) {
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /host/tool/operate [post]
|
||||
// @Router /hosts/tool/operate [post]
|
||||
// @x-panel-log {"bodyKeys":["operate","type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] [type] ","formatEN":"[operate] [type]"}
|
||||
func (b *BaseApi) OperateTool(c *gin.Context) {
|
||||
var req request.HostToolReq
|
||||
@@ -80,7 +80,7 @@ func (b *BaseApi) OperateTool(c *gin.Context) {
|
||||
// @Success 200 {object} response.HostToolConfig
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /host/tool/config [post]
|
||||
// @Router /hosts/tool/config [post]
|
||||
// @x-panel-log {"bodyKeys":["operate"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] 主机工具配置文件 ","formatEN":"[operate] tool config"}
|
||||
func (b *BaseApi) OperateToolConfig(c *gin.Context) {
|
||||
var req request.HostToolConfig
|
||||
@@ -103,7 +103,7 @@ func (b *BaseApi) OperateToolConfig(c *gin.Context) {
|
||||
// @Success 200 {string} logContent
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /host/tool/log [post]
|
||||
// @Router /hosts/tool/log [post]
|
||||
func (b *BaseApi) GetToolLog(c *gin.Context) {
|
||||
var req request.HostToolLogReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
@@ -125,7 +125,7 @@ func (b *BaseApi) GetToolLog(c *gin.Context) {
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /host/tool/supervisor/process [post]
|
||||
// @Router /hosts/tool/supervisor/process [post]
|
||||
// @x-panel-log {"bodyKeys":["operate"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] 守护进程 ","formatEN":"[operate] process"}
|
||||
func (b *BaseApi) OperateProcess(c *gin.Context) {
|
||||
var req request.SupervisorProcessConfig
|
||||
@@ -147,7 +147,7 @@ func (b *BaseApi) OperateProcess(c *gin.Context) {
|
||||
// @Success 200 {object} response.SupervisorProcessConfig
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /host/tool/supervisor/process [get]
|
||||
// @Router /hosts/tool/supervisor/process [get]
|
||||
func (b *BaseApi) GetProcess(c *gin.Context) {
|
||||
configs, err := hostToolService.GetSupervisorProcessConfig()
|
||||
if err != nil {
|
||||
@@ -164,7 +164,7 @@ func (b *BaseApi) GetProcess(c *gin.Context) {
|
||||
// @Success 200 {string} content
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /host/tool/supervisor/process/file [post]
|
||||
// @Router /hosts/tool/supervisor/process/file [post]
|
||||
// @x-panel-log {"bodyKeys":["operate"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] Supervisor 进程文件 ","formatEN":"[operate] Supervisor Process Config file"}
|
||||
func (b *BaseApi) GetProcessFile(c *gin.Context) {
|
||||
var req request.SupervisorProcessFileReq
|
||||
|
||||
@@ -76,7 +76,7 @@ func (b *BaseApi) CheckRepoStatus(c *gin.Context) {
|
||||
// @Tags Container Image-repo
|
||||
// @Summary Create image repo
|
||||
// @Accept json
|
||||
// @Param request body dto.ImageRepoDelete true "request"
|
||||
// @Param request body dto.ImageRepoCreate true "request"
|
||||
// @Produce json
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
@@ -99,20 +99,20 @@ func (b *BaseApi) CreateRepo(c *gin.Context) {
|
||||
// @Tags Container Image-repo
|
||||
// @Summary Delete image repo
|
||||
// @Accept json
|
||||
// @Param request body dto.ImageRepoDelete true "request"
|
||||
// @Param request body dto.OperateByID true "request"
|
||||
// @Produce json
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /containers/repo/del [post]
|
||||
// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"image_repos","output_column":"name","output_value":"names"}],"formatZH":"删除镜像仓库 [names]","formatEN":"delete image repo [names]"}
|
||||
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"image_repos","output_column":"name","output_value":"name"}],"formatZH":"删除镜像仓库 [name]","formatEN":"delete image repo [name]"}
|
||||
func (b *BaseApi) DeleteRepo(c *gin.Context) {
|
||||
var req dto.ImageRepoDelete
|
||||
var req dto.OperateByID
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := imageRepoService.BatchDelete(req); err != nil {
|
||||
if err := imageRepoService.Delete(req); err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
|
||||
"github.com/1Panel-dev/1Panel/backend/app/dto/request"
|
||||
"github.com/1Panel-dev/1Panel/backend/constant"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// @Tags McpServer
|
||||
// @Summary List mcp servers
|
||||
// @Accept json
|
||||
// @Param request body request.McpServerSearch true "request"
|
||||
// @Success 200 {object} response.McpServersRes
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /mcp/search [post]
|
||||
func (b *BaseApi) PageMcpServers(c *gin.Context) {
|
||||
var req request.McpServerSearch
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
list := mcpServerService.Page(req)
|
||||
helper.SuccessWithData(c, list)
|
||||
}
|
||||
|
||||
// @Tags McpServer
|
||||
// @Summary Create mcp server
|
||||
// @Accept json
|
||||
// @Param request body request.McpServerCreate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /mcp/server [post]
|
||||
func (b *BaseApi) CreateMcpServer(c *gin.Context) {
|
||||
var req request.McpServerCreate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
err := mcpServerService.Create(req)
|
||||
if err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithOutData(c)
|
||||
}
|
||||
|
||||
// @Tags McpServer
|
||||
// @Summary Update mcp server
|
||||
// @Accept json
|
||||
// @Param request body request.McpServerUpdate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /mcp/server/update [post]
|
||||
func (b *BaseApi) UpdateMcpServer(c *gin.Context) {
|
||||
var req request.McpServerUpdate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
err := mcpServerService.Update(req)
|
||||
if err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithOutData(c)
|
||||
}
|
||||
|
||||
// @Tags McpServer
|
||||
// @Summary Delete mcp server
|
||||
// @Accept json
|
||||
// @Param request body request.McpServerDelete true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /mcp/server/del [post]
|
||||
func (b *BaseApi) DeleteMcpServer(c *gin.Context) {
|
||||
var req request.McpServerDelete
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
err := mcpServerService.Delete(req.ID)
|
||||
if err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithOutData(c)
|
||||
}
|
||||
|
||||
// @Tags McpServer
|
||||
// @Summary Operate mcp server
|
||||
// @Accept json
|
||||
// @Param request body request.McpServerOperate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /mcp/server/op [post]
|
||||
func (b *BaseApi) OperateMcpServer(c *gin.Context) {
|
||||
var req request.McpServerOperate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
err := mcpServerService.Operate(req)
|
||||
if err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithOutData(c)
|
||||
}
|
||||
|
||||
// @Tags McpServer
|
||||
// @Summary Bind Domain for mcp server
|
||||
// @Accept json
|
||||
// @Param request body request.McpBindDomain true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /mcp/domain/bind [post]
|
||||
func (b *BaseApi) BindMcpDomain(c *gin.Context) {
|
||||
var req request.McpBindDomain
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
err := mcpServerService.BindDomain(req)
|
||||
if err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithOutData(c)
|
||||
}
|
||||
|
||||
// @Tags McpServer
|
||||
// @Summary Update bind Domain for mcp server
|
||||
// @Accept json
|
||||
// @Param request body request.McpBindDomainUpdate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /mcp/domain/update [post]
|
||||
func (b *BaseApi) UpdateMcpBindDomain(c *gin.Context) {
|
||||
var req request.McpBindDomainUpdate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
err := mcpServerService.UpdateBindDomain(req)
|
||||
if err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithOutData(c)
|
||||
}
|
||||
|
||||
// @Tags McpServer
|
||||
// @Summary Get bin Domain for mcp server
|
||||
// @Accept json
|
||||
// @Success 200 {object} response.McpBindDomainRes
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /mcp/domain/get [get]
|
||||
func (b *BaseApi) GetMcpBindDomain(c *gin.Context) {
|
||||
res, err := mcpServerService.GetBindDomain()
|
||||
if err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, res)
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
// @Success 200 {object} dto.SSHInfo
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /host/ssh/search [post]
|
||||
// @Router /hosts/ssh/search [post]
|
||||
func (b *BaseApi) GetSSHInfo(c *gin.Context) {
|
||||
info, err := sshService.GetSSHInfo()
|
||||
if err != nil {
|
||||
@@ -29,7 +29,7 @@ func (b *BaseApi) GetSSHInfo(c *gin.Context) {
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /host/ssh/operate [post]
|
||||
// @Router /hosts/ssh/operate [post]
|
||||
// @x-panel-log {"bodyKeys":["operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] SSH ","formatEN":"[operation] SSH"}
|
||||
func (b *BaseApi) OperateSSH(c *gin.Context) {
|
||||
var req dto.Operate
|
||||
@@ -51,7 +51,7 @@ func (b *BaseApi) OperateSSH(c *gin.Context) {
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /host/ssh/update [post]
|
||||
// @Router /hosts/ssh/update [post]
|
||||
// @x-panel-log {"bodyKeys":["key","value"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改 SSH 配置 [key] => [value]","formatEN":"update SSH setting [key] => [value]"}
|
||||
func (b *BaseApi) UpdateSSH(c *gin.Context) {
|
||||
var req dto.SSHUpdate
|
||||
@@ -73,7 +73,7 @@ func (b *BaseApi) UpdateSSH(c *gin.Context) {
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /host/conffile/update [post]
|
||||
// @Router /hosts/conffile/update [post]
|
||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改 SSH 配置文件","formatEN":"update SSH conf"}
|
||||
func (b *BaseApi) UpdateSSHByfile(c *gin.Context) {
|
||||
var req dto.SSHConf
|
||||
@@ -95,7 +95,7 @@ func (b *BaseApi) UpdateSSHByfile(c *gin.Context) {
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /host/ssh/generate [post]
|
||||
// @Router /hosts/ssh/generate [post]
|
||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"生成 SSH 密钥 ","formatEN":"generate SSH secret"}
|
||||
func (b *BaseApi) GenerateSSH(c *gin.Context) {
|
||||
var req dto.GenerateSSH
|
||||
@@ -117,7 +117,7 @@ func (b *BaseApi) GenerateSSH(c *gin.Context) {
|
||||
// @Success 200 {string} data
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /host/ssh/secret [post]
|
||||
// @Router /hosts/ssh/secret [post]
|
||||
func (b *BaseApi) LoadSSHSecret(c *gin.Context) {
|
||||
var req dto.GenerateLoad
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
@@ -139,7 +139,7 @@ func (b *BaseApi) LoadSSHSecret(c *gin.Context) {
|
||||
// @Success 200 {object} dto.SSHLog
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /host/ssh/log [post]
|
||||
// @Router /hosts/ssh/log [post]
|
||||
func (b *BaseApi) LoadSSHLogs(c *gin.Context) {
|
||||
var req dto.SearchSSHLog
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
@@ -159,7 +159,7 @@ func (b *BaseApi) LoadSSHLogs(c *gin.Context) {
|
||||
// @Success 200 {string} data
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /host/ssh/conf [get]
|
||||
// @Router /hosts/ssh/conf [get]
|
||||
func (b *BaseApi) LoadSSHConf(c *gin.Context) {
|
||||
data, err := sshService.LoadSSHConf()
|
||||
if err != nil {
|
||||
|
||||
@@ -68,9 +68,8 @@ func (b *BaseApi) WsSsh(c *gin.Context) {
|
||||
|
||||
<-quitChan
|
||||
|
||||
if wshandleError(wsConn, err) {
|
||||
return
|
||||
}
|
||||
dt := time.Now().Add(time.Second)
|
||||
_ = wsConn.WriteControl(websocket.CloseMessage, nil, dt)
|
||||
}
|
||||
|
||||
func (b *BaseApi) ContainerWsSSH(c *gin.Context) {
|
||||
@@ -97,7 +96,7 @@ func (b *BaseApi) ContainerWsSSH(c *gin.Context) {
|
||||
}
|
||||
source := c.Query("source")
|
||||
var containerID string
|
||||
var initCmd string
|
||||
var initCmd []string
|
||||
switch source {
|
||||
case "redis":
|
||||
containerID, initCmd, err = loadRedisInitCmd(c)
|
||||
@@ -114,11 +113,11 @@ func (b *BaseApi) ContainerWsSSH(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
pidMap := loadMapFromDockerTop(containerID)
|
||||
slave, err := terminal.NewCommand("clear && " + initCmd)
|
||||
slave, err := terminal.NewCommand(initCmd)
|
||||
if wshandleError(wsConn, err) {
|
||||
return
|
||||
}
|
||||
defer killBash(containerID, strings.ReplaceAll(initCmd, fmt.Sprintf("docker exec -it %s ", containerID), ""), pidMap)
|
||||
defer killBash(containerID, strings.ReplaceAll(strings.Join(initCmd, " "), fmt.Sprintf("exec -it %s ", containerID), ""), pidMap)
|
||||
defer slave.Close()
|
||||
|
||||
tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave, false)
|
||||
@@ -133,67 +132,68 @@ func (b *BaseApi) ContainerWsSSH(c *gin.Context) {
|
||||
<-quitChan
|
||||
|
||||
global.LOG.Info("websocket finished")
|
||||
if wshandleError(wsConn, err) {
|
||||
return
|
||||
}
|
||||
dt := time.Now().Add(time.Second)
|
||||
_ = wsConn.WriteControl(websocket.CloseMessage, nil, dt)
|
||||
|
||||
}
|
||||
|
||||
func loadRedisInitCmd(c *gin.Context) (string, string, error) {
|
||||
func loadRedisInitCmd(c *gin.Context) (string, []string, error) {
|
||||
name := c.Query("name")
|
||||
from := c.Query("from")
|
||||
commands := "redis-cli"
|
||||
commands := []string{"exec", "-it"}
|
||||
database, err := databaseService.Get(name)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("no such database in db, err: %v", err)
|
||||
return "", nil, fmt.Errorf("no such database in db, err: %v", err)
|
||||
}
|
||||
if from == "local" {
|
||||
redisInfo, err := appInstallService.LoadConnInfo(dto.OperationWithNameAndType{Name: name, Type: "redis"})
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("no such app in db, err: %v", err)
|
||||
return "", nil, fmt.Errorf("no such app in db, err: %v", err)
|
||||
}
|
||||
name = redisInfo.ContainerName
|
||||
commands = append(commands, []string{name, "redis-cli"}...)
|
||||
if len(database.Password) != 0 {
|
||||
commands = "redis-cli -a " + database.Password + " --no-auth-warning"
|
||||
commands = append(commands, []string{"-a", database.Password, "--no-auth-warning"}...)
|
||||
}
|
||||
} else {
|
||||
commands = fmt.Sprintf("redis-cli -h %s -p %v", database.Address, database.Port)
|
||||
if len(database.Password) != 0 {
|
||||
commands = fmt.Sprintf("redis-cli -h %s -p %v -a %s --no-auth-warning", database.Address, database.Port, database.Password)
|
||||
}
|
||||
name = "1Panel-redis-cli-tools"
|
||||
commands = append(commands, []string{name, "redis-cli", "-h", database.Address, "-p", fmt.Sprintf("%v", database.Port)}...)
|
||||
if len(database.Password) != 0 {
|
||||
commands = append(commands, []string{"-a", database.Password, "--no-auth-warning"}...)
|
||||
}
|
||||
}
|
||||
return name, fmt.Sprintf("docker exec -it %s %s", name, commands), nil
|
||||
return name, commands, nil
|
||||
}
|
||||
|
||||
func loadOllamaInitCmd(c *gin.Context) (string, string, error) {
|
||||
func loadOllamaInitCmd(c *gin.Context) (string, []string, error) {
|
||||
name := c.Query("name")
|
||||
if cmd.CheckIllegal(name) {
|
||||
return "", "", fmt.Errorf("ollama model %s contains illegal characters", name)
|
||||
return "", nil, fmt.Errorf("ollama model %s contains illegal characters", name)
|
||||
}
|
||||
ollamaInfo, err := appInstallService.LoadConnInfo(dto.OperationWithNameAndType{Name: "", Type: "ollama"})
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("no such app in db, err: %v", err)
|
||||
return "", nil, fmt.Errorf("no such app in db, err: %v", err)
|
||||
}
|
||||
containerName := ollamaInfo.ContainerName
|
||||
return containerName, fmt.Sprintf("docker exec -it %s ollama run %s", containerName, name), nil
|
||||
return containerName, []string{"exec", "-it", containerName, "ollama", "run", name}, nil
|
||||
}
|
||||
|
||||
func loadContainerInitCmd(c *gin.Context) (string, string, error) {
|
||||
func loadContainerInitCmd(c *gin.Context) (string, []string, error) {
|
||||
containerID := c.Query("containerid")
|
||||
command := c.Query("command")
|
||||
user := c.Query("user")
|
||||
if cmd.CheckIllegal(user, containerID, command) {
|
||||
return "", "", fmt.Errorf("the command contains illegal characters. command: %s, user: %s, containerID: %s", command, user, containerID)
|
||||
return "", nil, fmt.Errorf("the command contains illegal characters. command: %s, user: %s, containerID: %s", command, user, containerID)
|
||||
}
|
||||
if len(command) == 0 || len(containerID) == 0 {
|
||||
return "", "", fmt.Errorf("error param of command: %s or containerID: %s", command, containerID)
|
||||
return "", nil, fmt.Errorf("error param of command: %s or containerID: %s", command, containerID)
|
||||
}
|
||||
command = fmt.Sprintf("docker exec -it %s %s", containerID, command)
|
||||
commands := []string{"exec", "-it", containerID, command}
|
||||
if len(user) != 0 {
|
||||
command = fmt.Sprintf("docker exec -it -u %s %s %s", user, containerID, command)
|
||||
commands = []string{"exec", "-it", "-u", user, containerID, command}
|
||||
}
|
||||
|
||||
return containerID, command, nil
|
||||
return containerID, commands, nil
|
||||
}
|
||||
|
||||
func wshandleError(ws *websocket.Conn, err error) bool {
|
||||
|
||||
@@ -71,14 +71,16 @@ type CronjobDownload struct {
|
||||
}
|
||||
|
||||
type CronjobClean struct {
|
||||
IsDelete bool `json:"isDelete"`
|
||||
CleanData bool `json:"cleanData"`
|
||||
CronjobID uint `json:"cronjobID" validate:"required"`
|
||||
IsDelete bool `json:"isDelete"`
|
||||
CleanData bool `json:"cleanData"`
|
||||
CleanRemoteData bool `json:"cleanRemoteData"`
|
||||
CronjobID uint `json:"cronjobID" validate:"required"`
|
||||
}
|
||||
|
||||
type CronjobBatchDelete struct {
|
||||
CleanData bool `json:"cleanData"`
|
||||
IDs []uint `json:"ids" validate:"required"`
|
||||
CleanData bool `json:"cleanData"`
|
||||
CleanRemoteData bool `json:"cleanRemoteData"`
|
||||
IDs []uint `json:"ids" validate:"required"`
|
||||
}
|
||||
|
||||
type CronjobInfo struct {
|
||||
|
||||
@@ -6,8 +6,8 @@ type ImageRepoCreate struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
DownloadUrl string `json:"downloadUrl"`
|
||||
Protocol string `json:"protocol"`
|
||||
Username string `json:"username" validate:"max=256"`
|
||||
Password string `json:"password" validate:"max=256"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Auth bool `json:"auth"`
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ type ImageRepoUpdate struct {
|
||||
ID uint `json:"id"`
|
||||
DownloadUrl string `json:"downloadUrl"`
|
||||
Protocol string `json:"protocol"`
|
||||
Username string `json:"username" validate:"max=256"`
|
||||
Password string `json:"password" validate:"max=256"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Auth bool `json:"auth"`
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package dto
|
||||
|
||||
type DockerComposeService struct {
|
||||
Image string `yaml:"image"`
|
||||
ContainerName string `yaml:"container_name"`
|
||||
Restart string `yaml:"restart"`
|
||||
Ports []string `yaml:"ports"`
|
||||
Environment []string `yaml:"environment"`
|
||||
Command []string `yaml:"command"`
|
||||
}
|
||||
@@ -76,7 +76,8 @@ type FileRename struct {
|
||||
}
|
||||
|
||||
type FilePathCheck struct {
|
||||
Path string `json:"path" validate:"required"`
|
||||
Path string `json:"path" validate:"required"`
|
||||
WithInit bool `json:"withInit"`
|
||||
}
|
||||
|
||||
type FilePathsCheck struct {
|
||||
@@ -91,11 +92,12 @@ type FileWget struct {
|
||||
}
|
||||
|
||||
type FileMove struct {
|
||||
Type string `json:"type" validate:"required"`
|
||||
OldPaths []string `json:"oldPaths" validate:"required"`
|
||||
NewPath string `json:"newPath" validate:"required"`
|
||||
Name string `json:"name"`
|
||||
Cover bool `json:"cover"`
|
||||
Type string `json:"type" validate:"required"`
|
||||
OldPaths []string `json:"oldPaths" validate:"required"`
|
||||
NewPath string `json:"newPath" validate:"required"`
|
||||
Name string `json:"name"`
|
||||
Cover bool `json:"cover"`
|
||||
CoverPaths []string `json:"coverPaths"`
|
||||
}
|
||||
|
||||
type FileDownload struct {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package request
|
||||
|
||||
import "github.com/1Panel-dev/1Panel/backend/app/dto"
|
||||
|
||||
type McpServerSearch struct {
|
||||
dto.PageInfo
|
||||
Name string `json:"name"`
|
||||
Sync bool `json:"sync"`
|
||||
}
|
||||
|
||||
type McpServerCreate struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Command string `json:"command" validate:"required"`
|
||||
Environments []Environment `json:"environments"`
|
||||
Volumes []Volume `json:"volumes"`
|
||||
Port int `json:"port" validate:"required"`
|
||||
ContainerName string `json:"containerName"`
|
||||
BaseURL string `json:"baseUrl"`
|
||||
SsePath string `json:"ssePath"`
|
||||
HostIP string `json:"hostIP"`
|
||||
}
|
||||
|
||||
type McpServerUpdate struct {
|
||||
ID uint `json:"id" validate:"required"`
|
||||
McpServerCreate
|
||||
}
|
||||
|
||||
type Environment struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type Volume struct {
|
||||
Source string `json:"source"`
|
||||
Target string `json:"target"`
|
||||
}
|
||||
|
||||
type McpServerDelete struct {
|
||||
ID uint `json:"id" validate:"required"`
|
||||
}
|
||||
|
||||
type McpServerOperate struct {
|
||||
ID uint `json:"id" validate:"required"`
|
||||
Operate string `json:"operate" validate:"required"`
|
||||
}
|
||||
|
||||
type McpBindDomain struct {
|
||||
Domain string `json:"domain" validate:"required"`
|
||||
SSLID uint `json:"sslID"`
|
||||
IPList string `json:"ipList"`
|
||||
}
|
||||
|
||||
type McpBindDomainUpdate struct {
|
||||
WebsiteID uint `json:"websiteID" validate:"required"`
|
||||
SSLID uint `json:"sslID"`
|
||||
IPList string `json:"ipList"`
|
||||
}
|
||||
@@ -53,4 +53,5 @@ type ExistFileInfo struct {
|
||||
Path string `json:"path"`
|
||||
Size float64 `json:"size"`
|
||||
ModTime time.Time `json:"modTime"`
|
||||
IsDir bool `json:"isDir"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package response
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/backend/app/dto/request"
|
||||
"github.com/1Panel-dev/1Panel/backend/app/model"
|
||||
)
|
||||
|
||||
type McpServersRes struct {
|
||||
Items []McpServerDTO `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
type McpServerDTO struct {
|
||||
model.McpServer
|
||||
Environments []request.Environment `json:"environments"`
|
||||
Volumes []request.Volume `json:"volumes"`
|
||||
}
|
||||
|
||||
type McpBindDomainRes struct {
|
||||
Domain string `json:"domain"`
|
||||
SSLID uint `json:"sslID"`
|
||||
AcmeAccountID uint `json:"acmeAccountID"`
|
||||
AllowIPs []string `json:"allowIPs"`
|
||||
WebsiteID uint `json:"websiteID"`
|
||||
ConnUrl string `json:"connUrl"`
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package model
|
||||
|
||||
type McpServer struct {
|
||||
BaseModel
|
||||
Name string `json:"name"`
|
||||
DockerCompose string `json:"dockerCompose"`
|
||||
Command string `json:"command"`
|
||||
ContainerName string `json:"containerName"`
|
||||
Message string `json:"message"`
|
||||
Port int `json:"port"`
|
||||
Status string `json:"status"`
|
||||
Env string `json:"env"`
|
||||
BaseURL string `json:"baseUrl"`
|
||||
SsePath string `json:"ssePath"`
|
||||
WebsiteID int `json:"websiteID"`
|
||||
Dir string `json:"dir"`
|
||||
HostIP string `json:"hostIP"`
|
||||
}
|
||||
@@ -16,6 +16,7 @@ type IGroupRepo interface {
|
||||
Delete(opts ...DBOption) error
|
||||
CancelDefault(groupType string) error
|
||||
WithByHostDefault() DBOption
|
||||
WithByWebsiteDefault() DBOption
|
||||
}
|
||||
|
||||
func NewIGroupRepo() IGroupRepo {
|
||||
@@ -56,6 +57,12 @@ func (u *GroupRepo) WithByHostDefault() DBOption {
|
||||
}
|
||||
}
|
||||
|
||||
func (u *GroupRepo) WithByWebsiteDefault() DBOption {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
return g.Where("is_default = ? AND type = ?", 1, "website")
|
||||
}
|
||||
}
|
||||
|
||||
func (u *GroupRepo) Delete(opts ...DBOption) error {
|
||||
db := global.DB
|
||||
for _, opt := range opts {
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package repo
|
||||
|
||||
import "github.com/1Panel-dev/1Panel/backend/app/model"
|
||||
|
||||
type McpServerRepo struct {
|
||||
}
|
||||
|
||||
type IMcpServerRepo interface {
|
||||
Page(page, size int, opts ...DBOption) (int64, []model.McpServer, error)
|
||||
GetFirst(opts ...DBOption) (*model.McpServer, error)
|
||||
Create(mcpServer *model.McpServer) error
|
||||
Save(mcpServer *model.McpServer) error
|
||||
DeleteBy(opts ...DBOption) error
|
||||
List(opts ...DBOption) ([]model.McpServer, error)
|
||||
}
|
||||
|
||||
func NewIMcpServerRepo() IMcpServerRepo {
|
||||
return &McpServerRepo{}
|
||||
}
|
||||
|
||||
func (m McpServerRepo) Page(page, size int, opts ...DBOption) (int64, []model.McpServer, error) {
|
||||
var servers []model.McpServer
|
||||
db := getDb(opts...).Model(&model.McpServer{})
|
||||
count := int64(0)
|
||||
db = db.Count(&count)
|
||||
err := db.Limit(size).Offset(size * (page - 1)).Find(&servers).Error
|
||||
return count, servers, err
|
||||
}
|
||||
|
||||
func (m McpServerRepo) GetFirst(opts ...DBOption) (*model.McpServer, error) {
|
||||
var mcpServer model.McpServer
|
||||
if err := getDb(opts...).First(&mcpServer).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &mcpServer, nil
|
||||
}
|
||||
|
||||
func (m McpServerRepo) List(opts ...DBOption) ([]model.McpServer, error) {
|
||||
var mcpServers []model.McpServer
|
||||
if err := getDb(opts...).Find(&mcpServers).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return mcpServers, nil
|
||||
}
|
||||
|
||||
func (m McpServerRepo) Create(mcpServer *model.McpServer) error {
|
||||
return getDb().Create(mcpServer).Error
|
||||
}
|
||||
|
||||
func (m McpServerRepo) Save(mcpServer *model.McpServer) error {
|
||||
return getDb().Save(mcpServer).Error
|
||||
}
|
||||
|
||||
func (m McpServerRepo) DeleteBy(opts ...DBOption) error {
|
||||
return getDb(opts...).Delete(&model.McpServer{}).Error
|
||||
}
|
||||
@@ -839,6 +839,9 @@ func updateInstallInfoInDB(appKey, appName, param string, value interface{}) err
|
||||
"param": strings.ReplaceAll(appInstall.Param, oldVal, newVal),
|
||||
"env": strings.ReplaceAll(appInstall.Env, oldVal, newVal),
|
||||
}, commonRepo.WithByID(appInstall.ID))
|
||||
if appKey == "mysql" || appKey == "postgresql" {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if param == "user-password" {
|
||||
oldVal = fmt.Sprintf("\"PANEL_DB_USER_PASSWORD\":\"%v\"", appInstall.UserPassword)
|
||||
|
||||
@@ -442,17 +442,14 @@ func deleteLink(ctx context.Context, install *model.AppInstall, deleteDB bool, f
|
||||
return appInstallResourceRepo.DeleteBy(ctx, appInstallResourceRepo.WithAppInstallId(install.ID))
|
||||
}
|
||||
|
||||
func getUpgradeCompose(install model.AppInstall, detail model.AppDetail) (string, error) {
|
||||
if detail.DockerCompose == "" {
|
||||
return "", nil
|
||||
}
|
||||
func handleUpgradeCompose(install model.AppInstall, detail model.AppDetail) (map[string]interface{}, error) {
|
||||
composeMap := make(map[string]interface{})
|
||||
if err := yaml.Unmarshal([]byte(detail.DockerCompose), &composeMap); err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
value, ok := composeMap["services"]
|
||||
if !ok || value == nil {
|
||||
return "", buserr.New(constant.ErrFileParse)
|
||||
return nil, buserr.New("ErrFileParse")
|
||||
}
|
||||
servicesMap := value.(map[string]interface{})
|
||||
if len(servicesMap) == 1 {
|
||||
@@ -470,6 +467,34 @@ func getUpgradeCompose(install model.AppInstall, detail model.AppDetail) (string
|
||||
delete(servicesMap, oldServiceName)
|
||||
}
|
||||
}
|
||||
serviceValue := servicesMap[install.ServiceName].(map[string]interface{})
|
||||
|
||||
oldComposeMap := make(map[string]interface{})
|
||||
if err := yaml.Unmarshal([]byte(install.DockerCompose), &oldComposeMap); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
oldValue, ok := oldComposeMap["services"]
|
||||
if !ok || oldValue == nil {
|
||||
return nil, buserr.New("ErrFileParse")
|
||||
}
|
||||
oldValueMap := oldValue.(map[string]interface{})
|
||||
oldServiceValue := oldValueMap[install.ServiceName].(map[string]interface{})
|
||||
if oldServiceValue["deploy"] != nil {
|
||||
serviceValue["deploy"] = oldServiceValue["deploy"]
|
||||
}
|
||||
servicesMap[install.ServiceName] = serviceValue
|
||||
composeMap["services"] = servicesMap
|
||||
return composeMap, nil
|
||||
}
|
||||
|
||||
func getUpgradeCompose(install model.AppInstall, detail model.AppDetail) (string, error) {
|
||||
if detail.DockerCompose == "" {
|
||||
return "", nil
|
||||
}
|
||||
composeMap, err := handleUpgradeCompose(install, detail)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
envs := make(map[string]interface{})
|
||||
if err := json.Unmarshal([]byte(install.Env), &envs); err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -518,47 +518,40 @@ func (u *BackupService) loadAccessToken(backup *model.BackupAccount) error {
|
||||
}
|
||||
|
||||
func (u *BackupService) loadRecordSize(records []model.BackupRecord) ([]dto.BackupFile, error) {
|
||||
var datas []dto.BackupFile
|
||||
data := make([]dto.BackupFile, len(records))
|
||||
clientMap := make(map[string]loadSizeHelper)
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < len(records); i++ {
|
||||
var item dto.BackupFile
|
||||
item.ID = records[i].ID
|
||||
item.Name = records[i].FileName
|
||||
data[i].ID = records[i].ID
|
||||
data[i].Name = records[i].FileName
|
||||
itemPath := path.Join(records[i].FileDir, records[i].FileName)
|
||||
if _, ok := clientMap[records[i].Source]; !ok {
|
||||
backup, err := backupRepo.Get(commonRepo.WithByType(records[i].Source))
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load backup model %s from db failed, err: %v", records[i].Source, err)
|
||||
clientMap[records[i].Source] = loadSizeHelper{}
|
||||
datas = append(datas, item)
|
||||
continue
|
||||
}
|
||||
client, err := u.NewClient(&backup)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load backup client %s from db failed, err: %v", records[i].Source, err)
|
||||
clientMap[records[i].Source] = loadSizeHelper{}
|
||||
datas = append(datas, item)
|
||||
continue
|
||||
}
|
||||
item.Size, _ = client.Size(path.Join(strings.TrimLeft(backup.BackupPath, "/"), itemPath))
|
||||
datas = append(datas, item)
|
||||
data[i].Size, _ = client.Size(path.Join(strings.TrimLeft(backup.BackupPath, "/"), itemPath))
|
||||
clientMap[records[i].Source] = loadSizeHelper{backupPath: strings.TrimLeft(backup.BackupPath, "/"), client: client, isOk: true}
|
||||
continue
|
||||
}
|
||||
if clientMap[records[i].Source].isOk {
|
||||
wg.Add(1)
|
||||
go func(index int) {
|
||||
item.Size, _ = clientMap[records[index].Source].client.Size(path.Join(clientMap[records[index].Source].backupPath, itemPath))
|
||||
datas = append(datas, item)
|
||||
data[i].Size, _ = clientMap[records[index].Source].client.Size(path.Join(clientMap[records[index].Source].backupPath, itemPath))
|
||||
wg.Done()
|
||||
}(i)
|
||||
} else {
|
||||
datas = append(datas, item)
|
||||
}
|
||||
}
|
||||
wg.Wait()
|
||||
return datas, nil
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func loadLocalDir() (string, error) {
|
||||
|
||||
+103
-129
@@ -1,7 +1,6 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -28,14 +27,14 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
clamServiceNameCentOs = "clamd@scan.service"
|
||||
clamServiceNameUbuntu = "clamav-daemon.service"
|
||||
freshClamService = "clamav-freshclam.service"
|
||||
resultDir = "clamav"
|
||||
clamServiceKey = "clam"
|
||||
freshClamServiceKey = "freshclam"
|
||||
resultDir = "clamav"
|
||||
)
|
||||
|
||||
type ClamService struct {
|
||||
serviceName string
|
||||
serviceName string
|
||||
freshClamService string
|
||||
}
|
||||
|
||||
type IClamService interface {
|
||||
@@ -63,23 +62,32 @@ func (c *ClamService) LoadBaseInfo() (dto.ClamBaseInfo, error) {
|
||||
var baseInfo dto.ClamBaseInfo
|
||||
baseInfo.Version = "-"
|
||||
baseInfo.FreshVersion = "-"
|
||||
exist1, _ := systemctl.IsExist(clamServiceNameCentOs)
|
||||
if exist1 {
|
||||
c.serviceName = clamServiceNameCentOs
|
||||
baseInfo.IsExist = true
|
||||
baseInfo.IsActive, _ = systemctl.IsActive(clamServiceNameCentOs)
|
||||
clamSvc, err := systemctl.GetServiceName(clamServiceKey)
|
||||
if err != nil {
|
||||
baseInfo.IsExist = false
|
||||
return baseInfo, nil
|
||||
}
|
||||
exist2, _ := systemctl.IsExist(clamServiceNameUbuntu)
|
||||
if exist2 {
|
||||
c.serviceName = clamServiceNameUbuntu
|
||||
baseInfo.IsExist = true
|
||||
baseInfo.IsActive, _ = systemctl.IsActive(clamServiceNameUbuntu)
|
||||
c.serviceName = clamSvc
|
||||
isExist, err := systemctl.IsExist(c.serviceName)
|
||||
if err != nil {
|
||||
baseInfo.IsExist = false
|
||||
}
|
||||
freshExist, _ := systemctl.IsExist(freshClamService)
|
||||
if freshExist {
|
||||
baseInfo.FreshIsExist = true
|
||||
baseInfo.FreshIsActive, _ = systemctl.IsActive(freshClamService)
|
||||
baseInfo.IsExist = isExist
|
||||
baseInfo.IsActive, _ = systemctl.IsActive(clamSvc)
|
||||
|
||||
freshSvc, err := systemctl.GetServiceName(freshClamServiceKey)
|
||||
if err != nil {
|
||||
baseInfo.FreshIsExist = false
|
||||
return baseInfo, nil
|
||||
}
|
||||
c.freshClamService = freshSvc
|
||||
freshisExist, err := systemctl.IsExist(c.freshClamService)
|
||||
if err != nil {
|
||||
baseInfo.FreshIsExist = false
|
||||
}
|
||||
baseInfo.FreshIsExist = freshisExist
|
||||
baseInfo.FreshIsActive, _ = systemctl.IsActive(freshSvc)
|
||||
|
||||
if !cmd.Which("clamdscan") {
|
||||
baseInfo.IsActive = false
|
||||
}
|
||||
@@ -110,22 +118,27 @@ func (c *ClamService) LoadBaseInfo() (dto.ClamBaseInfo, error) {
|
||||
}
|
||||
|
||||
func (c *ClamService) Operate(operate string) error {
|
||||
var err error
|
||||
switch operate {
|
||||
case "start", "restart", "stop":
|
||||
stdout, err := cmd.Execf("systemctl %s %s", operate, c.serviceName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s the %s failed, err: %s", operate, c.serviceName, stdout)
|
||||
}
|
||||
return nil
|
||||
case "fresh-start", "fresh-restart", "fresh-stop":
|
||||
stdout, err := cmd.Execf("systemctl %s %s", strings.TrimPrefix(operate, "fresh-"), freshClamService)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s the %s failed, err: %s", operate, c.serviceName, stdout)
|
||||
}
|
||||
return nil
|
||||
case "start":
|
||||
err = systemctl.Start(c.serviceName)
|
||||
case "stop":
|
||||
err = systemctl.Stop(c.serviceName)
|
||||
case "restart":
|
||||
err = systemctl.Restart(c.serviceName)
|
||||
case "fresh-start":
|
||||
err = systemctl.Start(c.freshClamService)
|
||||
case "fresh-stop":
|
||||
err = systemctl.Stop(c.freshClamService)
|
||||
case "fresh-restart":
|
||||
err = systemctl.Restart(c.freshClamService)
|
||||
default:
|
||||
return fmt.Errorf("not support such operation: %v", operate)
|
||||
return fmt.Errorf("unsupported operation: %s", operate)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s %s failed: %v", operate, c.serviceName, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ClamService) SearchWithPage(req dto.SearchClamWithPage) (int64, interface{}, error) {
|
||||
@@ -432,102 +445,80 @@ func (c *ClamService) LoadFile(req dto.ClamFileReq) (string, error) {
|
||||
filePath := ""
|
||||
switch req.Name {
|
||||
case "clamd":
|
||||
if c.serviceName == clamServiceNameUbuntu {
|
||||
filePath = "/etc/clamav/clamd.conf"
|
||||
} else {
|
||||
filePath = "/etc/clamd.d/scan.conf"
|
||||
}
|
||||
filePath = c.getConfigPath("clamd")
|
||||
case "clamd-log":
|
||||
filePath = c.loadLogPath("clamd-log")
|
||||
if len(filePath) != 0 {
|
||||
break
|
||||
}
|
||||
if c.serviceName == clamServiceNameUbuntu {
|
||||
filePath = "/var/log/clamav/clamav.log"
|
||||
} else {
|
||||
filePath = "/var/log/clamd.scan"
|
||||
}
|
||||
case "freshclam":
|
||||
if c.serviceName == clamServiceNameUbuntu {
|
||||
filePath = "/etc/clamav/freshclam.conf"
|
||||
} else {
|
||||
filePath = "/etc/freshclam.conf"
|
||||
}
|
||||
filePath = c.getConfigPath("freshclam")
|
||||
case "freshclam-log":
|
||||
filePath = c.loadLogPath("freshclam-log")
|
||||
if len(filePath) != 0 {
|
||||
break
|
||||
}
|
||||
if c.serviceName == clamServiceNameUbuntu {
|
||||
filePath = "/var/log/clamav/freshclam.log"
|
||||
} else {
|
||||
filePath = "/var/log/freshclam.log"
|
||||
}
|
||||
default:
|
||||
return "", fmt.Errorf("not support such type")
|
||||
return "", fmt.Errorf("unsupported file type")
|
||||
}
|
||||
if _, err := os.Stat(filePath); err != nil {
|
||||
|
||||
content, err := systemctl.ViewConfig(filePath, systemctl.ConfigOption{TailLines: req.Tail})
|
||||
if err != nil {
|
||||
return "", buserr.New("ErrHttpReqNotFound")
|
||||
}
|
||||
var tail string
|
||||
if req.Tail != "0" {
|
||||
tail = req.Tail
|
||||
} else {
|
||||
tail = "+1"
|
||||
}
|
||||
cmd := exec.Command("tail", "-n", tail, filePath)
|
||||
stdout, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("tail -n %v failed, err: %v", req.Tail, err)
|
||||
}
|
||||
return string(stdout), nil
|
||||
return content, nil
|
||||
}
|
||||
|
||||
func (c *ClamService) UpdateFile(req dto.UpdateByNameAndFile) error {
|
||||
filePath := ""
|
||||
service := ""
|
||||
var (
|
||||
filePath string
|
||||
service string
|
||||
)
|
||||
|
||||
switch req.Name {
|
||||
case "clamd":
|
||||
if c.serviceName == clamServiceNameUbuntu {
|
||||
service = clamServiceNameUbuntu
|
||||
filePath = "/etc/clamav/clamd.conf"
|
||||
} else {
|
||||
service = clamServiceNameCentOs
|
||||
filePath = "/etc/clamd.d/scan.conf"
|
||||
}
|
||||
filePath = c.getConfigPath("clamd")
|
||||
service = c.serviceName
|
||||
case "freshclam":
|
||||
if c.serviceName == clamServiceNameUbuntu {
|
||||
filePath = "/etc/clamav/freshclam.conf"
|
||||
} else {
|
||||
filePath = "/etc/freshclam.conf"
|
||||
}
|
||||
service = "clamav-freshclam.service"
|
||||
filePath = c.getConfigPath("freshclam")
|
||||
service = c.freshClamService
|
||||
default:
|
||||
return fmt.Errorf("not support such type")
|
||||
return fmt.Errorf("unsupported file type")
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_TRUNC, 0640)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
write := bufio.NewWriter(file)
|
||||
_, _ = write.WriteString(req.File)
|
||||
write.Flush()
|
||||
|
||||
_ = systemctl.Restart(service)
|
||||
if _, err := file.WriteString(req.File); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := systemctl.Restart(service); err != nil {
|
||||
return fmt.Errorf("restart %s failed: %v", service, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ClamService) getConfigPath(confType string) string {
|
||||
switch confType {
|
||||
case "clamd":
|
||||
if _, err := os.Stat("/etc/clamav/clamd.conf"); err == nil {
|
||||
return "/etc/clamav/clamd.conf"
|
||||
}
|
||||
return "/etc/clamd.d/scan.conf"
|
||||
case "freshclam":
|
||||
if _, err := os.Stat("/etc/clamav/freshclam.conf"); err == nil {
|
||||
return "/etc/clamav/freshclam.conf"
|
||||
}
|
||||
return "/etc/freshclam.conf"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func StopAllCronJob(withCheck bool) bool {
|
||||
if withCheck {
|
||||
isActive := false
|
||||
exist1, _ := systemctl.IsExist(clamServiceNameCentOs)
|
||||
if exist1 {
|
||||
isActive, _ = systemctl.IsActive(clamServiceNameCentOs)
|
||||
}
|
||||
exist2, _ := systemctl.IsExist(clamServiceNameUbuntu)
|
||||
if exist2 {
|
||||
isActive, _ = systemctl.IsActive(clamServiceNameUbuntu)
|
||||
isexist, _ := systemctl.IsExist(clamServiceKey)
|
||||
if isexist {
|
||||
isActive, _ = systemctl.IsActive(clamServiceKey)
|
||||
}
|
||||
if isActive {
|
||||
return false
|
||||
@@ -590,42 +581,25 @@ func loadResultFromLog(pathItem string) dto.ClamLog {
|
||||
return data
|
||||
}
|
||||
func (c *ClamService) loadLogPath(name string) string {
|
||||
confPath := ""
|
||||
if name == "clamd-log" {
|
||||
if c.serviceName == clamServiceNameUbuntu {
|
||||
confPath = "/etc/clamav/clamd.conf"
|
||||
} else {
|
||||
confPath = "/etc/clamd.d/scan.conf"
|
||||
}
|
||||
} else {
|
||||
if c.serviceName == clamServiceNameUbuntu {
|
||||
confPath = "/etc/clamav/freshclam.conf"
|
||||
} else {
|
||||
confPath = "/etc/freshclam.conf"
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(confPath); err != nil {
|
||||
return ""
|
||||
configKey := "clamd"
|
||||
searchPrefix := "LogFile "
|
||||
if name != "clamd-log" {
|
||||
configKey = "freshclam"
|
||||
searchPrefix = "UpdateLogFile "
|
||||
}
|
||||
confPath := c.getConfigPath(configKey)
|
||||
|
||||
content, err := os.ReadFile(confPath)
|
||||
if err != nil {
|
||||
global.LOG.Debugf("Failed to read %s config: %v", configKey, err)
|
||||
return ""
|
||||
}
|
||||
lines := strings.Split(string(content), "\n")
|
||||
if name == "clamd-log" {
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "LogFile ") {
|
||||
return strings.Trim(strings.ReplaceAll(line, "LogFile ", ""), " ")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "UpdateLogFile ") {
|
||||
return strings.Trim(strings.ReplaceAll(line, "UpdateLogFile ", ""), " ")
|
||||
}
|
||||
|
||||
for _, line := range strings.Split(string(content), "\n") {
|
||||
if strings.HasPrefix(line, searchPrefix) {
|
||||
return strings.TrimSpace(strings.TrimPrefix(line, searchPrefix))
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
|
||||
@@ -450,12 +450,14 @@ func (u *ContainerService) ContainerInfo(req dto.OperationWithName) (*dto.Contai
|
||||
networkSettings := oldContainer.NetworkSettings
|
||||
bridgeNetworkSettings := networkSettings.Networks[data.Network]
|
||||
if bridgeNetworkSettings.IPAMConfig != nil {
|
||||
ipv4Address := bridgeNetworkSettings.IPAMConfig.IPv4Address
|
||||
data.Ipv4 = ipv4Address
|
||||
ipv6Address := bridgeNetworkSettings.IPAMConfig.IPv6Address
|
||||
data.Ipv6 = ipv6Address
|
||||
if data.Network != "bridge" {
|
||||
data.Ipv4 = bridgeNetworkSettings.IPAMConfig.IPv4Address
|
||||
data.Ipv6 = bridgeNetworkSettings.IPAMConfig.IPv6Address
|
||||
}
|
||||
} else {
|
||||
data.Ipv4 = bridgeNetworkSettings.IPAddress
|
||||
if data.Network != "bridge" {
|
||||
data.Ipv4 = bridgeNetworkSettings.IPAddress
|
||||
}
|
||||
}
|
||||
|
||||
data.Cmd = oldContainer.Config.Cmd
|
||||
@@ -556,10 +558,7 @@ func (u *ContainerService) ContainerUpgrade(req dto.ContainerUpgrade) error {
|
||||
hostConf := oldContainer.HostConfig
|
||||
var networkConf network.NetworkingConfig
|
||||
if oldContainer.NetworkSettings != nil {
|
||||
for networkKey := range oldContainer.NetworkSettings.Networks {
|
||||
networkConf.EndpointsConfig = map[string]*network.EndpointSettings{networkKey: {}}
|
||||
break
|
||||
}
|
||||
networkConf.EndpointsConfig = oldContainer.NetworkSettings.Networks
|
||||
}
|
||||
if err := client.ContainerRemove(ctx, req.Name, container.RemoveOptions{Force: true}); err != nil {
|
||||
return err
|
||||
@@ -1213,10 +1212,7 @@ func reCreateAfterUpdate(name string, client *client.Client, config *container.C
|
||||
|
||||
var oldNetworkConf network.NetworkingConfig
|
||||
if networkConf != nil {
|
||||
for networkKey := range networkConf.Networks {
|
||||
oldNetworkConf.EndpointsConfig = map[string]*network.EndpointSettings{networkKey: {}}
|
||||
break
|
||||
}
|
||||
oldNetworkConf.EndpointsConfig = networkConf.Networks
|
||||
}
|
||||
|
||||
oldContainer, err := client.ContainerCreate(ctx, config, hostConf, &oldNetworkConf, &v1.Platform{}, name)
|
||||
|
||||
@@ -114,8 +114,17 @@ func (u *CronjobService) CleanRecord(req dto.CronjobClean) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !req.CleanRemoteData {
|
||||
for key := range accountMap {
|
||||
if key != constant.Local {
|
||||
delete(accountMap, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
cronjob.RetainCopies = 0
|
||||
u.removeExpiredBackup(cronjob, accountMap, model.BackupRecord{})
|
||||
if len(accountMap) != 0 {
|
||||
u.removeExpiredBackup(cronjob, accountMap, model.BackupRecord{})
|
||||
}
|
||||
} else {
|
||||
u.removeExpiredLog(cronjob)
|
||||
}
|
||||
@@ -249,7 +258,7 @@ func (u *CronjobService) Delete(req dto.CronjobBatchDelete) error {
|
||||
global.Cron.Remove(cron.EntryID(idItem))
|
||||
}
|
||||
global.LOG.Infof("stop cronjob entryID: %s", cronjob.EntryIDs)
|
||||
if err := u.CleanRecord(dto.CronjobClean{CronjobID: id, CleanData: req.CleanData, IsDelete: true}); err != nil {
|
||||
if err := u.CleanRecord(dto.CronjobClean{CronjobID: id, CleanData: req.CleanData, CleanRemoteData: req.CleanRemoteData, IsDelete: true}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cronjobRepo.Delete(commonRepo.WithByID(id)); err != nil {
|
||||
|
||||
@@ -322,7 +322,8 @@ func (u *CronjobService) uploadCronjobBackFile(cronjob model.Cronjob, accountMap
|
||||
if len(account) != 0 {
|
||||
global.LOG.Debugf("start upload file to %s, dir: %s", account, path.Join(accountMap[account].backupPath, cloudSrc))
|
||||
if _, err := accountMap[account].client.Upload(file, path.Join(accountMap[account].backupPath, cloudSrc)); err != nil {
|
||||
return "", err
|
||||
global.LOG.Errorf("upload file to %s failed, err: %v", account, err)
|
||||
continue
|
||||
}
|
||||
global.LOG.Debugf("upload successful!")
|
||||
}
|
||||
@@ -349,12 +350,18 @@ func (u *CronjobService) removeExpiredBackup(cronjob model.Cronjob, accountMap m
|
||||
if cronjob.Type == "snapshot" {
|
||||
for _, account := range accounts {
|
||||
if len(account) != 0 {
|
||||
if _, ok := accountMap[account]; !ok {
|
||||
continue
|
||||
}
|
||||
_, _ = accountMap[account].client.Delete(path.Join(accountMap[account].backupPath, "system_snapshot", records[i].FileName))
|
||||
}
|
||||
}
|
||||
_ = snapshotRepo.Delete(commonRepo.WithByName(strings.TrimSuffix(records[i].FileName, ".tar.gz")))
|
||||
} else {
|
||||
for _, account := range accounts {
|
||||
if _, ok := accountMap[account]; !ok {
|
||||
continue
|
||||
}
|
||||
if len(account) != 0 {
|
||||
_, _ = accountMap[account].client.Delete(path.Join(accountMap[account].backupPath, records[i].FileDir, records[i].FileName))
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/ntp"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/systemctl"
|
||||
"github.com/shirou/gopsutil/v3/mem"
|
||||
)
|
||||
|
||||
@@ -121,7 +122,7 @@ func (u *DeviceService) Update(key, value string) error {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
_, err := cmd.Exec("systemctl restart 1panel.service")
|
||||
err := systemctl.Restart("1panel")
|
||||
if err != nil {
|
||||
global.LOG.Errorf("restart system for new time zone failed, err: %v", err)
|
||||
}
|
||||
|
||||
@@ -11,12 +11,12 @@ import (
|
||||
|
||||
"github.com/1Panel-dev/1Panel/backend/constant"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/docker"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/systemctl"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/backend/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/backend/global"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/common"
|
||||
fileUtils "github.com/1Panel-dev/1Panel/backend/utils/files"
|
||||
"github.com/google/uuid"
|
||||
@@ -285,7 +285,7 @@ func (u *DeviceService) Clean(req []dto.Clean) {
|
||||
|
||||
if restart {
|
||||
go func() {
|
||||
_, err := cmd.Exec("systemctl restart 1panel.service")
|
||||
err := systemctl.Restart("1panel")
|
||||
if err != nil {
|
||||
global.LOG.Errorf("restart system port failed, err: %v", err)
|
||||
}
|
||||
|
||||
@@ -365,17 +365,18 @@ func (u *DockerService) UpdateConfByFile(req dto.DaemonJsonUpdateByFile) error {
|
||||
|
||||
func (u *DockerService) OperateDocker(req dto.DockerOperation) error {
|
||||
service := "docker"
|
||||
sudo := cmd.SudoHandleCmd()
|
||||
dockerCmd, err := getDockerRestartCommand()
|
||||
h, err := systemctl.DefaultHandler(service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if req.Operation == "stop" {
|
||||
isSocketActive, _ := systemctl.IsActive("docker.socket")
|
||||
if isSocketActive {
|
||||
std, err := cmd.Execf("%s systemctl stop docker.socket", sudo)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("handle systemctl stop docker.socket failed, err: %v", std)
|
||||
socketHandle, err := systemctl.DefaultHandler("docker.socket")
|
||||
if err == nil {
|
||||
status, err := socketHandle.IsActive()
|
||||
if err == nil && status.IsActive {
|
||||
if std, err := socketHandle.ExecuteAction("stop"); err != nil {
|
||||
global.LOG.Errorf("handle stop docker.socket failed, err: %v", std)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -386,13 +387,20 @@ func (u *DockerService) OperateDocker(req dto.DockerOperation) error {
|
||||
}
|
||||
}
|
||||
|
||||
stdout, err := cmd.Execf("%s %s %s", dockerCmd, req.Operation, service)
|
||||
if isDockerSnapInstalled() {
|
||||
command := fmt.Sprintf("snap %s docker", req.Operation)
|
||||
stdout, err := cmd.Exec(command)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to restart docker: %v", stdout)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
result, err := h.ExecuteAction(req.Operation)
|
||||
if err != nil {
|
||||
return errors.New(stdout)
|
||||
return errors.New(result.Output)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func changeLogOption(daemonMap map[string]interface{}, logMaxFile, logMaxSize string) {
|
||||
if opts, ok := daemonMap["log-opts"]; ok {
|
||||
if len(logMaxFile) != 0 || len(logMaxSize) != 0 {
|
||||
@@ -451,31 +459,27 @@ func validateDockerConfig() error {
|
||||
return nil
|
||||
}
|
||||
if err != nil || (stdout != "" && strings.TrimSpace(stdout) != "configuration OK") {
|
||||
return fmt.Errorf("Docker configuration validation failed, err: %v", stdout)
|
||||
return fmt.Errorf("docker configuration validation failed, err: %v", stdout)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getDockerRestartCommand() (string, error) {
|
||||
func isDockerSnapInstalled() bool {
|
||||
stdout, err := cmd.Exec("which docker")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to find docker: %v", err)
|
||||
return false
|
||||
}
|
||||
dockerPath := stdout
|
||||
if strings.Contains(dockerPath, "snap") {
|
||||
return "snap", nil
|
||||
}
|
||||
return "systemctl", nil
|
||||
stdout = strings.TrimSpace(stdout)
|
||||
return strings.Contains(stdout, "snap")
|
||||
}
|
||||
|
||||
func restartDocker() error {
|
||||
restartCmd, err := getDockerRestartCommand()
|
||||
if err != nil {
|
||||
return err
|
||||
if isDockerSnapInstalled() {
|
||||
stdout, err := cmd.Exec("snap restart docker")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to restart docker: %v", stdout)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
stdout, err := cmd.Execf("%s restart docker", restartCmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to restart Docker: %s", stdout)
|
||||
}
|
||||
return nil
|
||||
return systemctl.Restart("docker")
|
||||
}
|
||||
|
||||
@@ -46,4 +46,6 @@ var (
|
||||
phpExtensionsRepo = repo.NewIPHPExtensionsRepo()
|
||||
|
||||
favoriteRepo = repo.NewIFavoriteRepo()
|
||||
|
||||
mcpServerRepo = repo.NewIMcpServerRepo()
|
||||
)
|
||||
|
||||
@@ -366,10 +366,18 @@ func (f *FileService) MvFile(m request.FileMove) error {
|
||||
return buserr.New(constant.ErrMovePathFailed)
|
||||
}
|
||||
}
|
||||
var errs []error
|
||||
if m.Type == "cut" {
|
||||
if len(m.CoverPaths) > 0 {
|
||||
for _, src := range m.CoverPaths {
|
||||
if err := fo.CopyAndReName(src, m.NewPath, "", true); err != nil {
|
||||
errs = append(errs, err)
|
||||
global.LOG.Errorf("cut copy file [%s] to [%s] failed, err: %s", src, m.NewPath, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
return fo.Cut(m.OldPaths, m.NewPath, m.Name, m.Cover)
|
||||
}
|
||||
var errs []error
|
||||
if m.Type == "copy" {
|
||||
for _, src := range m.OldPaths {
|
||||
if err := fo.CopyAndReName(src, m.NewPath, m.Name, m.Cover); err != nil {
|
||||
@@ -518,6 +526,7 @@ func (f *FileService) BatchCheckFiles(req request.FilePathsCheck) []response.Exi
|
||||
Name: info.Name(),
|
||||
Path: filePath,
|
||||
ModTime: info.ModTime(),
|
||||
IsDir: info.IsDir(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,12 @@ package service
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/backend/app/dto/request"
|
||||
"github.com/1Panel-dev/1Panel/backend/app/dto/response"
|
||||
"github.com/1Panel-dev/1Panel/backend/buserr"
|
||||
@@ -14,11 +20,6 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/systemctl"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/ini.v1"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type HostToolService struct{}
|
||||
@@ -39,93 +40,108 @@ func NewIHostToolService() IHostToolService {
|
||||
}
|
||||
|
||||
func (h *HostToolService) GetToolStatus(req request.HostToolReq) (*response.HostToolRes, error) {
|
||||
res := &response.HostToolRes{}
|
||||
res.Type = req.Type
|
||||
res := &response.HostToolRes{Type: req.Type}
|
||||
switch req.Type {
|
||||
case constant.Supervisord:
|
||||
supervisorConfig := &response.Supervisor{}
|
||||
if !cmd.Which(constant.Supervisord) {
|
||||
supervisorConfig.IsExist = false
|
||||
res.Config = supervisorConfig
|
||||
return res, nil
|
||||
}
|
||||
supervisorConfig.IsExist = true
|
||||
serviceExist, _ := systemctl.IsExist(constant.Supervisord)
|
||||
if !serviceExist {
|
||||
serviceExist, _ = systemctl.IsExist(constant.Supervisor)
|
||||
if !serviceExist {
|
||||
supervisorConfig.IsExist = false
|
||||
res.Config = supervisorConfig
|
||||
return res, nil
|
||||
} else {
|
||||
supervisorConfig.ServiceName = constant.Supervisor
|
||||
}
|
||||
} else {
|
||||
supervisorConfig.ServiceName = constant.Supervisord
|
||||
}
|
||||
|
||||
serviceNameSet, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorServiceName))
|
||||
if serviceNameSet.ID != 0 || serviceNameSet.Value != "" {
|
||||
supervisorConfig.ServiceName = serviceNameSet.Value
|
||||
}
|
||||
|
||||
versionRes, _ := cmd.Exec("supervisord -v")
|
||||
supervisorConfig.Version = strings.TrimSuffix(versionRes, "\n")
|
||||
_, ctlRrr := exec.LookPath("supervisorctl")
|
||||
supervisorConfig.CtlExist = ctlRrr == nil
|
||||
|
||||
active, _ := systemctl.IsActive(supervisorConfig.ServiceName)
|
||||
if active {
|
||||
supervisorConfig.Status = "running"
|
||||
} else {
|
||||
supervisorConfig.Status = "stopped"
|
||||
}
|
||||
|
||||
pathSet, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorConfigPath))
|
||||
if pathSet.ID != 0 || pathSet.Value != "" {
|
||||
supervisorConfig.ConfigPath = pathSet.Value
|
||||
res.Config = supervisorConfig
|
||||
return res, nil
|
||||
} else {
|
||||
supervisorConfig.Init = true
|
||||
}
|
||||
|
||||
servicePath := "/usr/lib/systemd/system/supervisor.service"
|
||||
fileOp := files.NewFileOp()
|
||||
if !fileOp.Stat(servicePath) {
|
||||
servicePath = "/usr/lib/systemd/system/supervisord.service"
|
||||
}
|
||||
if fileOp.Stat(servicePath) {
|
||||
startCmd, _ := ini_conf.GetIniValue(servicePath, "Service", "ExecStart")
|
||||
if startCmd != "" {
|
||||
args := strings.Fields(startCmd)
|
||||
cIndex := -1
|
||||
for i, arg := range args {
|
||||
if arg == "-c" {
|
||||
cIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if cIndex != -1 && cIndex+1 < len(args) {
|
||||
supervisorConfig.ConfigPath = args[cIndex+1]
|
||||
}
|
||||
}
|
||||
}
|
||||
if supervisorConfig.ConfigPath == "" {
|
||||
configPath := "/etc/supervisord.conf"
|
||||
if !fileOp.Stat(configPath) {
|
||||
configPath = "/etc/supervisor/supervisord.conf"
|
||||
if fileOp.Stat(configPath) {
|
||||
supervisorConfig.ConfigPath = configPath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.Config = supervisorConfig
|
||||
return h.getSupervisorStatus(res)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (h *HostToolService) getSupervisorStatus(res *response.HostToolRes) (*response.HostToolRes, error) {
|
||||
supervisorConfig := &response.Supervisor{}
|
||||
if !cmd.Which(constant.Supervisord) {
|
||||
supervisorConfig.IsExist = false
|
||||
res.Config = supervisorConfig
|
||||
return res, nil
|
||||
}
|
||||
supervisorConfig.IsExist = true
|
||||
|
||||
serviceName, err := h.determineServiceName()
|
||||
if err != nil || serviceName == "" {
|
||||
supervisorConfig.IsExist = false
|
||||
res.Config = supervisorConfig
|
||||
return res, nil
|
||||
}
|
||||
supervisorConfig.ServiceName = serviceName
|
||||
|
||||
if nameSetting, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorServiceName)); nameSetting.Value != "" {
|
||||
supervisorConfig.ServiceName = nameSetting.Value
|
||||
}
|
||||
|
||||
if version, err := cmd.Exec("supervisord -v"); err == nil {
|
||||
supervisorConfig.Version = strings.TrimSpace(version)
|
||||
}
|
||||
|
||||
_, errCtl := exec.LookPath("supervisorctl")
|
||||
supervisorConfig.CtlExist = errCtl == nil
|
||||
|
||||
if active, err := systemctl.IsActive(supervisorConfig.ServiceName); err == nil && active {
|
||||
supervisorConfig.Status = "running"
|
||||
} else {
|
||||
supervisorConfig.Status = "stopped"
|
||||
}
|
||||
|
||||
h.resolveConfigPath(supervisorConfig)
|
||||
|
||||
res.Config = supervisorConfig
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (h *HostToolService) determineServiceName() (string, error) {
|
||||
if setting, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorServiceName)); setting.Value != "" {
|
||||
serviceName, err := systemctl.GetServiceName(setting.Value)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get service name error: %v", err)
|
||||
}
|
||||
return serviceName, nil
|
||||
}
|
||||
if serviceName, err := systemctl.GetServiceName(constant.Supervisord); err == nil {
|
||||
return serviceName, nil
|
||||
}
|
||||
return systemctl.GetServiceName(constant.Supervisor)
|
||||
}
|
||||
|
||||
func (h *HostToolService) resolveConfigPath(config *response.Supervisor) {
|
||||
if pathSetting, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorConfigPath)); pathSetting.Value != "" {
|
||||
config.ConfigPath = pathSetting.Value
|
||||
return
|
||||
}
|
||||
|
||||
config.Init = true
|
||||
|
||||
if servicePath, err := systemctl.GetServicePath(config.ServiceName); err == nil {
|
||||
if startCmd, _ := ini_conf.GetIniValue(servicePath, "Service", "ExecStart"); startCmd != "" {
|
||||
if path := parseConfigPathFromCommand(startCmd); path != "" {
|
||||
config.ConfigPath = path
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defaultPaths := []string{
|
||||
"/etc/supervisord.conf",
|
||||
"/etc/supervisor/supervisord.conf",
|
||||
"/usr/local/etc/supervisord.conf",
|
||||
}
|
||||
for _, path := range defaultPaths {
|
||||
if systemctl.FileExist(path) {
|
||||
config.ConfigPath = path
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseConfigPathFromCommand(cmd string) string {
|
||||
parts := strings.Fields(cmd)
|
||||
for i, part := range parts {
|
||||
if part == "-c" && i+1 < len(parts) {
|
||||
return parts[i+1]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (h *HostToolService) CreateToolConfig(req request.HostToolCreate) error {
|
||||
switch req.Type {
|
||||
case constant.Supervisord:
|
||||
@@ -200,14 +216,28 @@ func (h *HostToolService) CreateToolConfig(req request.HostToolCreate) error {
|
||||
}
|
||||
|
||||
func (h *HostToolService) OperateTool(req request.HostToolReq) error {
|
||||
serviceName := req.Type
|
||||
if req.Type == constant.Supervisord {
|
||||
var serviceName string
|
||||
var err error
|
||||
switch req.Type {
|
||||
case constant.Supervisord:
|
||||
serviceNameSet, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorServiceName))
|
||||
if serviceNameSet.ID != 0 || serviceNameSet.Value != "" {
|
||||
serviceName = serviceNameSet.Value
|
||||
} else {
|
||||
serviceName = req.Type
|
||||
}
|
||||
default:
|
||||
serviceName = req.Type
|
||||
}
|
||||
return systemctl.Operate(req.Operate, serviceName)
|
||||
serviceName, err = systemctl.GetServiceName(serviceName)
|
||||
if err != nil {
|
||||
if errors.Is(err, systemctl.ErrServiceNotFound) {
|
||||
return fmt.Errorf("%s service unavailable. Please ensure supervisor server is installed and configured", serviceName)
|
||||
}
|
||||
return err
|
||||
}
|
||||
_, err = systemctl.CustomAction(req.Operate, serviceName)
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *HostToolService) OperateToolConfig(req request.HostToolConfig) (*response.HostToolConfig, error) {
|
||||
|
||||
@@ -26,7 +26,7 @@ type IImageRepoService interface {
|
||||
Login(req dto.OperateByID) error
|
||||
Create(req dto.ImageRepoCreate) error
|
||||
Update(req dto.ImageRepoUpdate) error
|
||||
BatchDelete(req dto.ImageRepoDelete) error
|
||||
Delete(req dto.OperateByID) error
|
||||
}
|
||||
|
||||
func NewIImageRepoService() IImageRepoService {
|
||||
@@ -89,31 +89,7 @@ func (u *ImageRepoService) Create(req dto.ImageRepoCreate) error {
|
||||
if err := u.handleRegistries(req.DownloadUrl, "", "create"); err != nil {
|
||||
return fmt.Errorf("create registry %s failed, err: %v", req.DownloadUrl, err)
|
||||
}
|
||||
if err := validateDockerConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := restartDocker(); err != nil {
|
||||
return err
|
||||
}
|
||||
ticker := time.NewTicker(3 * time.Second)
|
||||
defer ticker.Stop()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
|
||||
if err := func() error {
|
||||
for range ticker.C {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
cancel()
|
||||
return errors.New("the docker service cannot be restarted")
|
||||
default:
|
||||
stdout, err := cmd.Exec("systemctl is-active docker")
|
||||
if string(stdout) == "active\n" && err == nil {
|
||||
global.LOG.Info("docker restart with new conf successful!")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
if err := stopBeforeUpdateRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -131,15 +107,32 @@ func (u *ImageRepoService) Create(req dto.ImageRepoCreate) error {
|
||||
return imageRepoRepo.Create(&imageRepo)
|
||||
}
|
||||
|
||||
func (u *ImageRepoService) BatchDelete(req dto.ImageRepoDelete) error {
|
||||
for _, id := range req.Ids {
|
||||
if id == 1 {
|
||||
return errors.New("The default value cannot be edit !")
|
||||
}
|
||||
func (u *ImageRepoService) Delete(req dto.OperateByID) error {
|
||||
if req.ID == 1 {
|
||||
return errors.New("The default value cannot be edit !")
|
||||
}
|
||||
if err := imageRepoRepo.Delete(commonRepo.WithIdsIn(req.Ids)); err != nil {
|
||||
itemRepo, _ := imageRepoRepo.Get(commonRepo.WithByID(req.ID))
|
||||
if itemRepo.ID == 0 {
|
||||
return buserr.New("ErrRecordNotFound")
|
||||
}
|
||||
if itemRepo.Auth {
|
||||
_, _ = cmd.Execf("docker logout -i %s", itemRepo.DownloadUrl)
|
||||
}
|
||||
if itemRepo.Protocol == "https" {
|
||||
return imageRepoRepo.Delete(commonRepo.WithByID(req.ID))
|
||||
}
|
||||
if err := u.handleRegistries("", itemRepo.DownloadUrl, "delete"); err != nil {
|
||||
return fmt.Errorf("delete registry %s failed, err: %v", itemRepo.DownloadUrl, err)
|
||||
}
|
||||
if err := validateDockerConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := imageRepoRepo.Delete(commonRepo.WithByID(req.ID)); err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
_ = restartDocker()
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -154,37 +147,39 @@ func (u *ImageRepoService) Update(req dto.ImageRepoUpdate) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
needRestart := false
|
||||
if repo.Protocol == "http" && req.Protocol == "https" {
|
||||
if err := u.handleRegistries("", repo.DownloadUrl, "delete"); err != nil {
|
||||
return fmt.Errorf("delete registry %s failed, err: %v", repo.DownloadUrl, err)
|
||||
}
|
||||
needRestart = true
|
||||
}
|
||||
if repo.Protocol == "http" && req.Protocol == "http" {
|
||||
if err := u.handleRegistries(req.DownloadUrl, repo.DownloadUrl, "update"); err != nil {
|
||||
return fmt.Errorf("update registry %s => %s failed, err: %v", repo.DownloadUrl, req.DownloadUrl, err)
|
||||
if repo.DownloadUrl != req.DownloadUrl {
|
||||
if err := u.handleRegistries(req.DownloadUrl, repo.DownloadUrl, "update"); err != nil {
|
||||
return fmt.Errorf("update registry %s => %s failed, err: %v", repo.DownloadUrl, req.DownloadUrl, err)
|
||||
}
|
||||
needRestart = true
|
||||
}
|
||||
}
|
||||
if repo.Protocol == "https" && req.Protocol == "http" {
|
||||
if err := u.handleRegistries(req.DownloadUrl, "", "create"); err != nil {
|
||||
return fmt.Errorf("create registry %s failed, err: %v", req.DownloadUrl, err)
|
||||
}
|
||||
needRestart = true
|
||||
}
|
||||
if repo.Auth != req.Auth || repo.DownloadUrl != req.DownloadUrl {
|
||||
if repo.Auth {
|
||||
_, _ = cmd.ExecWithCheck("docker", "logout", repo.DownloadUrl)
|
||||
}
|
||||
if req.Auth {
|
||||
if err := u.CheckConn(req.DownloadUrl, req.Username, req.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
if needRestart {
|
||||
if err := stopBeforeUpdateRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := validateDockerConfig(); err != nil {
|
||||
return err
|
||||
if repo.Auth {
|
||||
_, _ = cmd.Execf("docker logout -i %s", repo.DownloadUrl)
|
||||
}
|
||||
if err := restartDocker(); err != nil {
|
||||
return err
|
||||
if req.Auth {
|
||||
if err := u.CheckConn(req.DownloadUrl, req.Username, req.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
upMap := make(map[string]interface{})
|
||||
@@ -256,3 +251,34 @@ func (u *ImageRepoService) handleRegistries(newHost, delHost, handle string) err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func stopBeforeUpdateRepo() error {
|
||||
if err := validateDockerConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := restartDocker(); err != nil {
|
||||
return err
|
||||
}
|
||||
ticker := time.NewTicker(3 * time.Second)
|
||||
defer ticker.Stop()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
|
||||
if err := func() error {
|
||||
for range ticker.C {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
cancel()
|
||||
return errors.New("the docker service cannot be restarted")
|
||||
default:
|
||||
stdout, err := cmd.Exec("systemctl is-active docker")
|
||||
if string(stdout) == "active\n" && err == nil {
|
||||
global.LOG.Info("docker restart with new conf successful!")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,643 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/1Panel-dev/1Panel/backend/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/backend/app/dto/request"
|
||||
"github.com/1Panel-dev/1Panel/backend/app/dto/response"
|
||||
"github.com/1Panel-dev/1Panel/backend/app/model"
|
||||
"github.com/1Panel-dev/1Panel/backend/buserr"
|
||||
"github.com/1Panel-dev/1Panel/backend/constant"
|
||||
"github.com/1Panel-dev/1Panel/backend/global"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/compose"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/docker"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/files"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/nginx"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/nginx/components"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/nginx/parser"
|
||||
"github.com/1Panel-dev/1Panel/cmd/server/mcp"
|
||||
"github.com/1Panel-dev/1Panel/cmd/server/nginx_conf"
|
||||
"github.com/subosito/gotenv"
|
||||
"gopkg.in/yaml.v3"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type McpServerService struct{}
|
||||
|
||||
type IMcpServerService interface {
|
||||
Page(req request.McpServerSearch) response.McpServersRes
|
||||
Create(create request.McpServerCreate) error
|
||||
Update(req request.McpServerUpdate) error
|
||||
Delete(id uint) error
|
||||
Operate(req request.McpServerOperate) error
|
||||
GetBindDomain() (response.McpBindDomainRes, error)
|
||||
BindDomain(req request.McpBindDomain) error
|
||||
UpdateBindDomain(req request.McpBindDomainUpdate) error
|
||||
}
|
||||
|
||||
func NewIMcpServerService() IMcpServerService {
|
||||
return &McpServerService{}
|
||||
}
|
||||
|
||||
func (m McpServerService) Page(req request.McpServerSearch) response.McpServersRes {
|
||||
var (
|
||||
res response.McpServersRes
|
||||
items []response.McpServerDTO
|
||||
)
|
||||
|
||||
total, data, _ := mcpServerRepo.Page(req.PageInfo.Page, req.PageInfo.PageSize)
|
||||
for _, item := range data {
|
||||
_ = syncMcpServerContainerStatus(&item)
|
||||
serverDTO := response.McpServerDTO{
|
||||
McpServer: item,
|
||||
Environments: make([]request.Environment, 0),
|
||||
Volumes: make([]request.Volume, 0),
|
||||
}
|
||||
project, _ := docker.GetComposeProject(item.Name, path.Join(constant.McpDir, item.Name), []byte(item.DockerCompose), []byte(item.Env), true)
|
||||
for _, service := range project.Services {
|
||||
if service.Environment != nil {
|
||||
for key, value := range service.Environment {
|
||||
serverDTO.Environments = append(serverDTO.Environments, request.Environment{
|
||||
Key: key,
|
||||
Value: *value,
|
||||
})
|
||||
}
|
||||
}
|
||||
if service.Volumes != nil {
|
||||
for _, volume := range service.Volumes {
|
||||
serverDTO.Volumes = append(serverDTO.Volumes, request.Volume{
|
||||
Source: volume.Source,
|
||||
Target: volume.Target,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
items = append(items, serverDTO)
|
||||
}
|
||||
res.Total = total
|
||||
res.Items = items
|
||||
return res
|
||||
}
|
||||
|
||||
func (m McpServerService) Update(req request.McpServerUpdate) error {
|
||||
mcpServer, err := mcpServerRepo.GetFirst(commonRepo.WithByID(req.ID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if mcpServer.Port != req.Port {
|
||||
if err := checkPortExist(req.Port); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if mcpServer.ContainerName != req.ContainerName {
|
||||
if err := checkContainerName(req.ContainerName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
mcpServer.Name = req.Name
|
||||
mcpServer.ContainerName = req.ContainerName
|
||||
mcpServer.Port = req.Port
|
||||
mcpServer.Command = req.Command
|
||||
mcpServer.BaseURL = req.BaseURL
|
||||
mcpServer.SsePath = req.SsePath
|
||||
mcpServer.HostIP = req.HostIP
|
||||
if err := handleCreateParams(mcpServer, req.Environments, req.Volumes); err != nil {
|
||||
return err
|
||||
}
|
||||
env := handleEnv(mcpServer)
|
||||
mcpDir := path.Join(constant.McpDir, mcpServer.Name)
|
||||
envPath := path.Join(mcpDir, ".env")
|
||||
if err := gotenv.Write(env, envPath); err != nil {
|
||||
return err
|
||||
}
|
||||
dockerComposePath := path.Join(mcpDir, "docker-compose.yml")
|
||||
if err := files.NewFileOp().SaveFile(dockerComposePath, mcpServer.DockerCompose, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
mcpServer.Status = constant.RuntimeStarting
|
||||
if err := mcpServerRepo.Save(mcpServer); err != nil {
|
||||
return err
|
||||
}
|
||||
go startMcp(mcpServer)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m McpServerService) Create(create request.McpServerCreate) error {
|
||||
servers, _ := mcpServerRepo.List()
|
||||
for _, server := range servers {
|
||||
if server.Port == create.Port {
|
||||
return buserr.New("ErrPortInUsed")
|
||||
}
|
||||
if server.ContainerName == create.ContainerName {
|
||||
return buserr.New("ErrContainerName")
|
||||
}
|
||||
if server.Name == create.Name {
|
||||
return buserr.New(constant.ErrNameIsExist)
|
||||
}
|
||||
if server.SsePath == create.SsePath {
|
||||
return buserr.New("ErrSsePath")
|
||||
}
|
||||
}
|
||||
if err := checkPortExist(create.Port); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkContainerName(create.ContainerName); err != nil {
|
||||
return err
|
||||
}
|
||||
mcpDir := path.Join(constant.McpDir, create.Name)
|
||||
mcpServer := &model.McpServer{
|
||||
Name: create.Name,
|
||||
ContainerName: create.ContainerName,
|
||||
Port: create.Port,
|
||||
Command: create.Command,
|
||||
Status: constant.RuntimeStarting,
|
||||
BaseURL: create.BaseURL,
|
||||
SsePath: create.SsePath,
|
||||
Dir: mcpDir,
|
||||
HostIP: create.HostIP,
|
||||
}
|
||||
if err := handleCreateParams(mcpServer, create.Environments, create.Volumes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
env := handleEnv(mcpServer)
|
||||
filesOP := files.NewFileOp()
|
||||
if !filesOP.Stat(mcpDir) {
|
||||
_ = filesOP.CreateDir(mcpDir, 0644)
|
||||
}
|
||||
envPath := path.Join(mcpDir, ".env")
|
||||
if err := gotenv.Write(env, envPath); err != nil {
|
||||
return err
|
||||
}
|
||||
dockerComposePath := path.Join(mcpDir, "docker-compose.yml")
|
||||
if err := filesOP.SaveFile(dockerComposePath, mcpServer.DockerCompose, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := mcpServerRepo.Create(mcpServer); err != nil {
|
||||
return err
|
||||
}
|
||||
addProxy(mcpServer)
|
||||
go startMcp(mcpServer)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m McpServerService) Delete(id uint) error {
|
||||
mcpServer, err := mcpServerRepo.GetFirst(commonRepo.WithByID(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
composePath := path.Join(constant.McpDir, mcpServer.Name, "docker-compose.yml")
|
||||
_, _ = compose.Down(composePath)
|
||||
_ = files.NewFileOp().DeleteDir(path.Join(constant.McpDir, mcpServer.Name))
|
||||
|
||||
websiteID := GetWebsiteID()
|
||||
if websiteID > 0 {
|
||||
websiteService := NewIWebsiteService()
|
||||
delProxyReq := request.WebsiteProxyDel{
|
||||
ID: websiteID,
|
||||
Name: mcpServer.Name,
|
||||
}
|
||||
_ = websiteService.DeleteProxy(delProxyReq)
|
||||
}
|
||||
return mcpServerRepo.DeleteBy(commonRepo.WithByID(id))
|
||||
}
|
||||
|
||||
func (m McpServerService) Operate(req request.McpServerOperate) error {
|
||||
mcpServer, err := mcpServerRepo.GetFirst(commonRepo.WithByID(req.ID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
composePath := path.Join(mcpServer.Dir, "docker-compose.yml")
|
||||
var out string
|
||||
switch req.Operate {
|
||||
case "start":
|
||||
out, err = compose.Up(composePath)
|
||||
mcpServer.Status = constant.RuntimeRunning
|
||||
case "stop":
|
||||
out, err = compose.Down(composePath)
|
||||
mcpServer.Status = constant.RuntimeStopped
|
||||
case "restart":
|
||||
out, err = compose.Restart(composePath)
|
||||
mcpServer.Status = constant.RuntimeRunning
|
||||
}
|
||||
if err != nil {
|
||||
mcpServer.Status = constant.RuntimeError
|
||||
mcpServer.Message = out
|
||||
}
|
||||
return mcpServerRepo.Save(mcpServer)
|
||||
}
|
||||
|
||||
func (m McpServerService) GetBindDomain() (response.McpBindDomainRes, error) {
|
||||
var res response.McpBindDomainRes
|
||||
websiteID := GetWebsiteID()
|
||||
if websiteID == 0 {
|
||||
return res, nil
|
||||
}
|
||||
website, err := websiteRepo.GetFirst(commonRepo.WithByID(websiteID))
|
||||
if err != nil {
|
||||
return res, nil
|
||||
}
|
||||
res.WebsiteID = website.ID
|
||||
res.Domain = website.PrimaryDomain
|
||||
if website.WebsiteSSLID > 0 {
|
||||
res.SSLID = website.WebsiteSSLID
|
||||
ssl, _ := websiteSSLRepo.GetFirst(commonRepo.WithByID(website.WebsiteSSLID))
|
||||
res.AcmeAccountID = ssl.AcmeAccountID
|
||||
}
|
||||
res.ConnUrl = fmt.Sprintf("%s://%s", strings.ToLower(website.Protocol), website.PrimaryDomain)
|
||||
res.AllowIPs = GetAllowIps(website)
|
||||
return res, nil
|
||||
|
||||
}
|
||||
|
||||
func (m McpServerService) BindDomain(req request.McpBindDomain) error {
|
||||
nginxInstall, _ := getAppInstallByKey(constant.AppOpenresty)
|
||||
if nginxInstall.ID == 0 {
|
||||
return buserr.New("ErrOpenrestyInstall")
|
||||
}
|
||||
var (
|
||||
ipList []string
|
||||
err error
|
||||
)
|
||||
if len(req.IPList) > 0 {
|
||||
ipList, err = common.HandleIPList(req.IPList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if req.SSLID > 0 {
|
||||
ssl, err := websiteSSLRepo.GetFirst(commonRepo.WithByID(req.SSLID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ssl.Pem == "" {
|
||||
return buserr.New("ErrSSL")
|
||||
}
|
||||
}
|
||||
group, _ := groupRepo.Get(groupRepo.WithByWebsiteDefault())
|
||||
createWebsiteReq := request.WebsiteCreate{
|
||||
PrimaryDomain: req.Domain,
|
||||
Alias: strings.ToLower(req.Domain),
|
||||
Type: constant.Static,
|
||||
WebsiteGroupID: group.ID,
|
||||
}
|
||||
websiteService := NewIWebsiteService()
|
||||
if err := websiteService.CreateWebsite(createWebsiteReq); err != nil {
|
||||
return err
|
||||
}
|
||||
website, err := websiteRepo.GetFirst(websiteRepo.WithAlias(strings.ToLower(req.Domain)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = settingRepo.UpdateOrCreate("MCP_WEBSITE_ID", fmt.Sprintf("%d", website.ID))
|
||||
if len(ipList) > 0 {
|
||||
if err = ConfigAllowIPs(ipList, website); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if req.SSLID > 0 {
|
||||
sslReq := request.WebsiteHTTPSOp{
|
||||
WebsiteID: website.ID,
|
||||
Enable: true,
|
||||
Type: "existed",
|
||||
WebsiteSSLID: req.SSLID,
|
||||
HttpConfig: "HTTPSOnly",
|
||||
}
|
||||
if _, err = websiteService.OpWebsiteHTTPS(context.Background(), sslReq); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = addMCPProxy(website.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m McpServerService) UpdateBindDomain(req request.McpBindDomainUpdate) error {
|
||||
nginxInstall, _ := getAppInstallByKey(constant.AppOpenresty)
|
||||
if nginxInstall.ID == 0 {
|
||||
return buserr.New("ErrOpenrestyInstall")
|
||||
}
|
||||
var (
|
||||
ipList []string
|
||||
err error
|
||||
)
|
||||
if len(req.IPList) > 0 {
|
||||
ipList, err = common.HandleIPList(req.IPList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if req.SSLID > 0 {
|
||||
ssl, err := websiteSSLRepo.GetFirst(commonRepo.WithByID(req.SSLID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ssl.Pem == "" {
|
||||
return buserr.New("ErrSSL")
|
||||
}
|
||||
}
|
||||
websiteService := NewIWebsiteService()
|
||||
website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = ConfigAllowIPs(ipList, website); err != nil {
|
||||
return err
|
||||
}
|
||||
if req.SSLID > 0 {
|
||||
sslReq := request.WebsiteHTTPSOp{
|
||||
WebsiteID: website.ID,
|
||||
Enable: true,
|
||||
Type: "existed",
|
||||
WebsiteSSLID: req.SSLID,
|
||||
HttpConfig: "HTTPSOnly",
|
||||
}
|
||||
if _, err = websiteService.OpWebsiteHTTPS(context.Background(), sslReq); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if website.WebsiteSSLID > 0 && req.SSLID == 0 {
|
||||
sslReq := request.WebsiteHTTPSOp{
|
||||
WebsiteID: website.ID,
|
||||
Enable: false,
|
||||
}
|
||||
if _, err = websiteService.OpWebsiteHTTPS(context.Background(), sslReq); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
go updateMcpConfig(website.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateMcpConfig(websiteID uint) {
|
||||
servers, _ := mcpServerRepo.List()
|
||||
if len(servers) == 0 {
|
||||
return
|
||||
}
|
||||
website, _ := websiteRepo.GetFirst(commonRepo.WithByID(websiteID))
|
||||
websiteDomain := website.Domains[0]
|
||||
var baseUrl string
|
||||
if website.Protocol == constant.ProtocolHTTP {
|
||||
baseUrl = fmt.Sprintf("http://%s", websiteDomain.Domain)
|
||||
} else {
|
||||
baseUrl = fmt.Sprintf("https://%s", websiteDomain.Domain)
|
||||
}
|
||||
|
||||
go func() {
|
||||
for _, server := range servers {
|
||||
if server.BaseURL != baseUrl {
|
||||
server.BaseURL = baseUrl
|
||||
server.HostIP = "127.0.0.1"
|
||||
_ = updateMcpServer(&server)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func addProxy(server *model.McpServer) {
|
||||
websiteID := GetWebsiteID()
|
||||
website, err := websiteRepo.GetFirst(commonRepo.WithByID(websiteID))
|
||||
if err != nil {
|
||||
global.LOG.Errorf("[mcp] add proxy failed, err: %v", err)
|
||||
return
|
||||
}
|
||||
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("[mcp] add proxy failed, err: %v", err)
|
||||
return
|
||||
}
|
||||
fileOp := files.NewFileOp()
|
||||
includeDir := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "proxy")
|
||||
if !fileOp.Stat(includeDir) {
|
||||
if err = fileOp.CreateDir(includeDir, 0644); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
config, err := parser.NewStringParser(string(nginx_conf.SSE)).Parse()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
includePath := path.Join(includeDir, server.Name+".conf")
|
||||
config.FilePath = includePath
|
||||
directives := config.Directives
|
||||
location, ok := directives[0].(*components.Location)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
location.UpdateDirective("proxy_pass", []string{fmt.Sprintf("http://127.0.0.1:%d%s", server.Port, server.SsePath)})
|
||||
location.ChangePath("^~", server.SsePath)
|
||||
if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
|
||||
global.LOG.Errorf("write config failed, err: %v", buserr.WithErr(constant.ErrUpdateBuWebsite, err))
|
||||
return
|
||||
}
|
||||
nginxInclude := fmt.Sprintf("/www/sites/%s/proxy/*.conf", website.Alias)
|
||||
if err = updateNginxConfig(constant.NginxScopeServer, []dto.NginxParam{{Name: "include", Params: []string{nginxInclude}}}, &website); err != nil {
|
||||
global.LOG.Errorf("update nginx config failed, err: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func addMCPProxy(websiteID uint) error {
|
||||
servers, _ := mcpServerRepo.List()
|
||||
if len(servers) == 0 {
|
||||
return nil
|
||||
}
|
||||
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
website, err := websiteRepo.GetFirst(commonRepo.WithByID(websiteID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fileOp := files.NewFileOp()
|
||||
includeDir := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "proxy")
|
||||
if !fileOp.Stat(includeDir) {
|
||||
if err = fileOp.CreateDir(includeDir, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
config, err := parser.NewStringParser(string(nginx_conf.SSE)).Parse()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
websiteDomain := website.Domains[0]
|
||||
var baseUrl string
|
||||
if website.Protocol == constant.ProtocolHTTP {
|
||||
baseUrl = fmt.Sprintf("http://%s", websiteDomain.Domain)
|
||||
} else {
|
||||
baseUrl = fmt.Sprintf("https://%s", websiteDomain.Domain)
|
||||
}
|
||||
if websiteDomain.Port != 80 && websiteDomain.Port != 443 {
|
||||
baseUrl = fmt.Sprintf("%s:%d", baseUrl, websiteDomain.Port)
|
||||
}
|
||||
for _, server := range servers {
|
||||
includePath := path.Join(includeDir, server.Name+".conf")
|
||||
config.FilePath = includePath
|
||||
directives := config.Directives
|
||||
location, ok := directives[0].(*components.Location)
|
||||
if !ok {
|
||||
err = errors.New("error")
|
||||
return err
|
||||
}
|
||||
location.UpdateDirective("proxy_pass", []string{fmt.Sprintf("http://127.0.0.1:%d%s", server.Port, server.SsePath)})
|
||||
location.ChangePath("^~", server.SsePath)
|
||||
if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
|
||||
return buserr.WithErr(constant.ErrUpdateBuWebsite, err)
|
||||
}
|
||||
server.BaseURL = baseUrl
|
||||
server.HostIP = "127.0.0.1"
|
||||
go updateMcpServer(&server)
|
||||
}
|
||||
nginxInclude := fmt.Sprintf("/www/sites/%s/proxy/*.conf", website.Alias)
|
||||
if err = updateNginxConfig(constant.NginxScopeServer, []dto.NginxParam{{Name: "include", Params: []string{nginxInclude}}}, &website); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateMcpServer(mcpServer *model.McpServer) error {
|
||||
env := handleEnv(mcpServer)
|
||||
if err := gotenv.Write(env, path.Join(mcpServer.Dir, ".env")); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = mcpServerRepo.Save(mcpServer)
|
||||
composePath := path.Join(constant.McpDir, mcpServer.Name, "docker-compose.yml")
|
||||
_, _ = compose.Down(composePath)
|
||||
if _, err := compose.Up(composePath); err != nil {
|
||||
mcpServer.Status = constant.RuntimeError
|
||||
mcpServer.Message = err.Error()
|
||||
}
|
||||
return mcpServerRepo.Save(mcpServer)
|
||||
}
|
||||
|
||||
func handleEnv(mcpServer *model.McpServer) gotenv.Env {
|
||||
env := make(gotenv.Env)
|
||||
env["CONTAINER_NAME"] = mcpServer.ContainerName
|
||||
env["COMMAND"] = mcpServer.Command
|
||||
env["PANEL_APP_PORT_HTTP"] = strconv.Itoa(mcpServer.Port)
|
||||
env["BASE_URL"] = mcpServer.BaseURL
|
||||
env["SSE_PATH"] = mcpServer.SsePath
|
||||
env["HOST_IP"] = mcpServer.HostIP
|
||||
envStr, _ := gotenv.Marshal(env)
|
||||
mcpServer.Env = envStr
|
||||
return env
|
||||
}
|
||||
|
||||
func handleCreateParams(mcpServer *model.McpServer, environments []request.Environment, volumes []request.Volume) error {
|
||||
var composeContent []byte
|
||||
if mcpServer.ID == 0 {
|
||||
composeContent = mcp.DefaultMcpCompose
|
||||
} else {
|
||||
composeContent = []byte(mcpServer.DockerCompose)
|
||||
}
|
||||
composeMap := make(map[string]interface{})
|
||||
if err := yaml.Unmarshal(composeContent, &composeMap); err != nil {
|
||||
return err
|
||||
}
|
||||
services, serviceValid := composeMap["services"].(map[string]interface{})
|
||||
if !serviceValid {
|
||||
return buserr.New(constant.ErrFileParse)
|
||||
}
|
||||
serviceName := ""
|
||||
serviceValue := make(map[string]interface{})
|
||||
|
||||
if mcpServer.ID > 0 {
|
||||
serviceName = mcpServer.Name
|
||||
serviceValue = services[serviceName].(map[string]interface{})
|
||||
} else {
|
||||
for name, service := range services {
|
||||
serviceName = name
|
||||
serviceValue = service.(map[string]interface{})
|
||||
break
|
||||
}
|
||||
delete(services, serviceName)
|
||||
}
|
||||
delete(serviceValue, "environment")
|
||||
if len(environments) > 0 {
|
||||
envMap := make(map[string]string)
|
||||
for _, env := range environments {
|
||||
envMap[env.Key] = env.Value
|
||||
}
|
||||
serviceValue["environment"] = envMap
|
||||
}
|
||||
delete(serviceValue, "volumes")
|
||||
if len(volumes) > 0 {
|
||||
volumeList := make([]string, 0)
|
||||
for _, volume := range volumes {
|
||||
volumeList = append(volumeList, fmt.Sprintf("%s:%s", volume.Source, volume.Target))
|
||||
}
|
||||
serviceValue["volumes"] = volumeList
|
||||
}
|
||||
|
||||
services[mcpServer.Name] = serviceValue
|
||||
composeByte, err := yaml.Marshal(composeMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mcpServer.DockerCompose = string(composeByte)
|
||||
return nil
|
||||
}
|
||||
|
||||
func startMcp(mcpServer *model.McpServer) {
|
||||
composePath := path.Join(constant.McpDir, mcpServer.Name, "docker-compose.yml")
|
||||
if mcpServer.Status != constant.RuntimeNormal {
|
||||
_, _ = compose.Down(composePath)
|
||||
}
|
||||
if out, err := compose.Up(composePath); err != nil {
|
||||
mcpServer.Status = constant.RuntimeError
|
||||
mcpServer.Message = out
|
||||
} else {
|
||||
mcpServer.Status = constant.RuntimeRunning
|
||||
mcpServer.Message = ""
|
||||
}
|
||||
_ = syncMcpServerContainerStatus(mcpServer)
|
||||
}
|
||||
|
||||
func syncMcpServerContainerStatus(mcpServer *model.McpServer) error {
|
||||
containerNames := []string{mcpServer.ContainerName}
|
||||
cli, err := docker.NewClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cli.Close()
|
||||
containers, err := cli.ListContainersByName(containerNames)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(containers) == 0 {
|
||||
mcpServer.Status = constant.RuntimeStopped
|
||||
return mcpServerRepo.Save(mcpServer)
|
||||
}
|
||||
container := containers[0]
|
||||
switch container.State {
|
||||
case "exited":
|
||||
mcpServer.Status = constant.RuntimeError
|
||||
case "running":
|
||||
mcpServer.Status = constant.RuntimeRunning
|
||||
case "paused":
|
||||
mcpServer.Status = constant.RuntimeStopped
|
||||
case "restarting":
|
||||
mcpServer.Status = constant.RuntimeReStarting
|
||||
default:
|
||||
if mcpServer.Status != constant.RuntimeBuildIng {
|
||||
mcpServer.Status = constant.RuntimeStopped
|
||||
}
|
||||
}
|
||||
return mcpServerRepo.Save(mcpServer)
|
||||
}
|
||||
|
||||
func GetWebsiteID() uint {
|
||||
websiteID, _ := settingRepo.Get(settingRepo.WithByKey("MCP_WEBSITE_ID"))
|
||||
if websiteID.Value == "" {
|
||||
return 0
|
||||
}
|
||||
websiteIDUint, _ := strconv.ParseUint(websiteID.Value, 10, 64)
|
||||
return uint(websiteIDUint)
|
||||
}
|
||||
@@ -21,10 +21,10 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/backend/buserr"
|
||||
"github.com/1Panel-dev/1Panel/backend/constant"
|
||||
"github.com/1Panel-dev/1Panel/backend/global"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/encrypt"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/files"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/systemctl"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
@@ -166,7 +166,7 @@ func (u *SettingService) UpdateBindInfo(req dto.BindInfo) error {
|
||||
}
|
||||
go func() {
|
||||
time.Sleep(1 * time.Second)
|
||||
_, err := cmd.Exec("systemctl restart 1panel.service")
|
||||
err := systemctl.Restart("1panel")
|
||||
if err != nil {
|
||||
global.LOG.Errorf("restart system with new bind info failed, err: %v", err)
|
||||
}
|
||||
@@ -214,7 +214,7 @@ func (u *SettingService) UpdatePort(port uint) error {
|
||||
}
|
||||
go func() {
|
||||
time.Sleep(1 * time.Second)
|
||||
_, err := cmd.Exec("systemctl restart 1panel.service")
|
||||
err := systemctl.Restart("1panel")
|
||||
if err != nil {
|
||||
global.LOG.Errorf("restart system port failed, err: %v", err)
|
||||
}
|
||||
@@ -237,7 +237,7 @@ func (u *SettingService) UpdateSSL(c *gin.Context, req dto.SSLUpdate) error {
|
||||
c.SetCookie(constant.SessionName, sID, 0, "", "", false, true)
|
||||
|
||||
go func() {
|
||||
_, err := cmd.Exec("systemctl restart 1panel.service")
|
||||
err := systemctl.Restart("1panel")
|
||||
if err != nil {
|
||||
global.LOG.Errorf("restart system failed, err: %v", err)
|
||||
}
|
||||
@@ -335,7 +335,7 @@ func (u *SettingService) UpdateSSL(c *gin.Context, req dto.SSLUpdate) error {
|
||||
c.SetCookie(constant.SessionName, sID, 0, "", "", true, true)
|
||||
go func() {
|
||||
time.Sleep(1 * time.Second)
|
||||
_, err := cmd.Exec("systemctl restart 1panel.service")
|
||||
err := systemctl.Restart("1panel")
|
||||
if err != nil {
|
||||
global.LOG.Errorf("restart system failed, err: %v", err)
|
||||
}
|
||||
@@ -552,7 +552,7 @@ func (u *SettingService) GenerateRSAKey() error {
|
||||
return err
|
||||
}
|
||||
privateKeyPEM := exportPrivateKeyToPEM(privateKey)
|
||||
publicKeyPEM, err := exportPublicKeyToPEM(&privateKey.PublicKey)
|
||||
publicKeyPEM, _ := exportPublicKeyToPEM(&privateKey.PublicKey)
|
||||
err = settingRepo.UpdateOrCreate("PASSWORD_PRIVATE_KEY", privateKeyPEM)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -503,7 +503,8 @@ func hasOs(name string) bool {
|
||||
strings.Contains(name, "arm64") ||
|
||||
strings.Contains(name, "armv7") ||
|
||||
strings.Contains(name, "ppc64le") ||
|
||||
strings.Contains(name, "s390x")
|
||||
strings.Contains(name, "s390x") ||
|
||||
strings.Contains(name, "riscv64")
|
||||
}
|
||||
|
||||
func loadOs() string {
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/files"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/systemctl"
|
||||
)
|
||||
|
||||
type snapHelper struct {
|
||||
@@ -43,16 +44,39 @@ func snapPanel(snap snapHelper, targetDir string) {
|
||||
defer snap.Wg.Done()
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel": constant.Running})
|
||||
status := constant.StatusDone
|
||||
if err := common.CopyFile("/usr/local/bin/1panel", path.Join(targetDir, "1panel")); err != nil {
|
||||
status = err.Error()
|
||||
h, err := systemctl.DefaultHandler("1panel")
|
||||
|
||||
if err != nil {
|
||||
status = fmt.Sprintf("initialize service handle failed: %v", err)
|
||||
snap.Status.Panel = status
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel": status})
|
||||
return
|
||||
}
|
||||
|
||||
if err := common.CopyFile("/usr/local/bin/1pctl", targetDir); err != nil {
|
||||
binDir := systemctl.BinaryPath
|
||||
servicePath, err := h.GetServicePath()
|
||||
if err != nil {
|
||||
status = fmt.Sprintf("get service path failed: %v", err)
|
||||
snap.Status.Panel = status
|
||||
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel": status})
|
||||
return
|
||||
}
|
||||
|
||||
if err := common.CopyFile(path.Join(binDir, "1panel"), path.Join(targetDir, "1panel")); err != nil {
|
||||
status = err.Error()
|
||||
}
|
||||
_, _ = cmd.Execf("cp -r /usr/local/bin/lang %s", targetDir)
|
||||
|
||||
if err := common.CopyFile("/etc/systemd/system/1panel.service", targetDir); err != nil {
|
||||
if err := common.CopyFile(path.Join(binDir, "1pctl"), targetDir); err != nil {
|
||||
status = err.Error()
|
||||
}
|
||||
if _, err := cmd.Execf("cp -r %s/lang %s", binDir, targetDir); err != nil {
|
||||
status = err.Error()
|
||||
}
|
||||
initScriptDir := path.Join(constant.DataDir, "initscript")
|
||||
if err := common.CopyFile(servicePath, initScriptDir); err != nil {
|
||||
status = err.Error()
|
||||
}
|
||||
global.LOG.Debugf("from %s copy init script to %s", initScriptDir, path.Join(targetDir, "initscript"))
|
||||
if err := common.CopyDirs(initScriptDir, path.Join(targetDir, "initscript")); err != nil { // copy init script to targetDir
|
||||
status = err.Error()
|
||||
}
|
||||
snap.Status.Panel = status
|
||||
@@ -260,7 +284,7 @@ func handleSnapTar(sourceDir, targetDir, name, exclusionRules string, secret str
|
||||
commands = fmt.Sprintf("tar --warning=no-file-changed --ignore-failed-read --exclude-from=<(find %s -type s -print) -zcf %s %s %s %s", sourceDir, " -"+exStr, path, extraCmd, targetDir+"/"+name)
|
||||
global.LOG.Debug(strings.ReplaceAll(commands, fmt.Sprintf(" %s ", secret), "******"))
|
||||
} else {
|
||||
commands = fmt.Sprintf("tar --warning=no-file-changed --ignore-failed-read --exclude-from=<(find %s -type s -printf '%s' | sed 's|^|./|') -zcf %s %s -C %s .", sourceDir, "%P\n", targetDir+"/"+name, exStr, sourceDir)
|
||||
commands = fmt.Sprintf("tar --warning=no-file-changed --ignore-failed-read --exclude-from=<(find %s -type s -printf '%%P\n' | sed 's|^|./|') -zcf %s %s -C %s .", sourceDir, targetDir+"/"+name, exStr, sourceDir)
|
||||
global.LOG.Debug(commands)
|
||||
}
|
||||
stdout, err := cmd.ExecWithTimeOut(commands, 30*time.Minute)
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/files"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/systemctl"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@@ -91,8 +92,15 @@ func (u *SnapshotService) HandleSnapshotRecover(snap model.Snapshot, isRecover b
|
||||
req.IsNew = true
|
||||
}
|
||||
|
||||
h, err := systemctl.DefaultHandler("1panel")
|
||||
if err != nil {
|
||||
updateRecoverStatus(snap.ID, isRecover, "ServiceHandle", constant.StatusFailed, fmt.Sprintf("initialize service handle failed: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if req.IsNew || snap.InterruptStep == "1PanelBinary" {
|
||||
if err := recoverPanel(path.Join(snapFileDir, "1panel/1panel"), "/usr/local/bin"); err != nil {
|
||||
binDir := systemctl.BinaryPath
|
||||
if err := recoverPanel(path.Join(snapFileDir, "1panel/1panel"), binDir); err != nil {
|
||||
updateRecoverStatus(snap.ID, isRecover, "1PanelBinary", constant.StatusFailed, err.Error())
|
||||
return
|
||||
}
|
||||
@@ -100,16 +108,32 @@ func (u *SnapshotService) HandleSnapshotRecover(snap model.Snapshot, isRecover b
|
||||
req.IsNew = true
|
||||
}
|
||||
if req.IsNew || snap.InterruptStep == "1PctlBinary" {
|
||||
if err := recoverPanel(path.Join(snapFileDir, "1panel/1pctl"), "/usr/local/bin"); err != nil {
|
||||
binDir := systemctl.BinaryPath
|
||||
if err := recoverPanel(path.Join(snapFileDir, "1panel/1pctl"), binDir); err != nil {
|
||||
updateRecoverStatus(snap.ID, isRecover, "1PctlBinary", constant.StatusFailed, err.Error())
|
||||
return
|
||||
}
|
||||
_, _ = cmd.Execf("cp -r %s %s", path.Join(snapFileDir, "1panel/lang"), "/usr/local/bin/")
|
||||
langDir := path.Join(binDir, "lang")
|
||||
if err := os.RemoveAll(langDir); err != nil {
|
||||
updateRecoverStatus(snap.ID, isRecover, "RemoveLang", constant.StatusFailed, fmt.Sprintf("remove lang dir failed: %v", err))
|
||||
return
|
||||
}
|
||||
if err := common.CopyDirs(path.Join(snapFileDir, "1panel/lang"), langDir); err != nil {
|
||||
updateRecoverStatus(snap.ID, isRecover, "CopyLang", constant.StatusFailed, fmt.Sprintf("copy lang files failed: %v", err))
|
||||
return
|
||||
}
|
||||
global.LOG.Debug("recover 1pctl from snapshot file successful!")
|
||||
req.IsNew = true
|
||||
}
|
||||
if req.IsNew || snap.InterruptStep == "1PanelService" {
|
||||
if err := recoverPanel(path.Join(snapFileDir, "1panel/1panel.service"), "/etc/systemd/system"); err != nil {
|
||||
servicePath, err := h.GetServicePath()
|
||||
currentServiceName := h.GetServiceName()
|
||||
if err != nil {
|
||||
updateRecoverStatus(snap.ID, isRecover, "GetServicePath", constant.StatusFailed, fmt.Sprintf("get service path failed: %v", err))
|
||||
return
|
||||
}
|
||||
global.LOG.Debugf("current service path: %s", servicePath)
|
||||
if err := common.CopyFile(selectInitScript(path.Join(snapFileDir, "1panel/initscript"), currentServiceName), servicePath); err != nil {
|
||||
updateRecoverStatus(snap.ID, isRecover, "1PanelService", constant.StatusFailed, err.Error())
|
||||
return
|
||||
}
|
||||
@@ -147,7 +171,12 @@ func (u *SnapshotService) HandleSnapshotRecover(snap model.Snapshot, isRecover b
|
||||
global.LOG.Debugf("remove the file %s after the operation is successful", path.Dir(snapFileDir))
|
||||
_ = os.RemoveAll(path.Dir(snapFileDir))
|
||||
}
|
||||
_, _ = cmd.Exec("systemctl daemon-reload && systemctl restart 1panel.service")
|
||||
if h.ManagerName() == "systemd" {
|
||||
_, _ = cmd.Exec("systemctl daemon-reload")
|
||||
}
|
||||
if err := systemctl.Restart("1panel"); err != nil {
|
||||
global.LOG.Errorf("restart 1panel service failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func backupBeforeRecover(snap model.Snapshot) error {
|
||||
|
||||
+214
-162
@@ -2,8 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/geo"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net"
|
||||
"os"
|
||||
"os/user"
|
||||
"path"
|
||||
@@ -12,6 +11,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/geo"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/backend/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/backend/buserr"
|
||||
"github.com/1Panel-dev/1Panel/backend/constant"
|
||||
@@ -59,26 +61,16 @@ func (u *SSHService) GetSSHInfo() (*dto.SSHInfo, error) {
|
||||
if err != nil {
|
||||
data.Status = constant.StatusDisable
|
||||
data.Message = err.Error()
|
||||
} else {
|
||||
active, err := systemctl.IsActive(serviceName)
|
||||
if !active {
|
||||
data.Status = constant.StatusDisable
|
||||
if err != nil {
|
||||
data.Message = err.Error()
|
||||
}
|
||||
} else {
|
||||
data.Status = constant.StatusEnable
|
||||
}
|
||||
}
|
||||
|
||||
out, err := systemctl.RunSystemCtl("is-enabled", serviceName)
|
||||
if err != nil {
|
||||
data.AutoStart = false
|
||||
isEnabled, _ := systemctl.IsEnable(serviceName)
|
||||
data.AutoStart = isEnabled
|
||||
isActive, err := systemctl.IsActive(serviceName)
|
||||
if isActive {
|
||||
data.Status = constant.StatusEnable
|
||||
} else {
|
||||
if out == "alias\n" {
|
||||
data.AutoStart, _ = systemctl.IsEnable("ssh")
|
||||
} else {
|
||||
data.AutoStart = out == "enabled\n"
|
||||
data.Status = constant.StatusDisable
|
||||
if err != nil {
|
||||
data.Message = err.Error()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,29 +113,26 @@ func (u *SSHService) OperateSSH(operation string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sudo := cmd.SudoHandleCmd()
|
||||
if operation == "enable" || operation == "disable" {
|
||||
serviceName += ".service"
|
||||
}
|
||||
if operation == "stop" {
|
||||
switch operation {
|
||||
case "enable":
|
||||
return systemctl.Enable(serviceName)
|
||||
case "disable":
|
||||
return systemctl.Disable(serviceName)
|
||||
case "start":
|
||||
return systemctl.Start(serviceName)
|
||||
case "stop":
|
||||
isSocketActive, _ := systemctl.IsActive(serviceName + ".socket")
|
||||
if isSocketActive {
|
||||
std, err := cmd.Execf("%s systemctl stop %s", sudo, serviceName+".socket")
|
||||
if err != nil {
|
||||
global.LOG.Errorf("handle systemctl stop %s.socket failed, err: %v", serviceName, std)
|
||||
if err := systemctl.Stop(serviceName + ".socket"); err != nil {
|
||||
global.LOG.Errorf("Failed to stop %s.socket: %v", serviceName, err)
|
||||
}
|
||||
}
|
||||
return systemctl.Stop(serviceName)
|
||||
case "restart":
|
||||
return systemctl.Restart(serviceName)
|
||||
}
|
||||
|
||||
stdout, err := cmd.Execf("%s systemctl %s %s", sudo, operation, serviceName)
|
||||
if err != nil {
|
||||
if strings.Contains(stdout, "alias name or linked unit file") {
|
||||
stdout, err := cmd.Execf("%s systemctl %s ssh", sudo, operation)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s ssh(alias name or linked unit file) failed, stdout: %s, err: %v", operation, stdout, err)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("%s %s failed, stdout: %s, err: %v", operation, serviceName, stdout, err)
|
||||
if result, err := systemctl.CustomAction(operation, serviceName); err != nil {
|
||||
return fmt.Errorf("failed to execute custom action: %v", result.Output)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -198,7 +187,10 @@ func (u *SSHService) Update(req dto.SSHUpdate) error {
|
||||
}
|
||||
}
|
||||
|
||||
_, _ = cmd.Execf("%s systemctl restart %s", sudo, serviceName)
|
||||
err = systemctl.Restart(serviceName)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("handle restart %s failed, err: %v", serviceName, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -216,8 +208,10 @@ func (u *SSHService) UpdateByFile(value string) error {
|
||||
if _, err = file.WriteString(value); err != nil {
|
||||
return err
|
||||
}
|
||||
sudo := cmd.SudoHandleCmd()
|
||||
_, _ = cmd.Execf("%s systemctl restart %s", sudo, serviceName)
|
||||
err = systemctl.Restart(serviceName)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("handle restart %s failed, err: %v", serviceName, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -298,7 +292,7 @@ func (u *SSHService) LoadLog(c *gin.Context, req dto.SearchSSHLog) (*dto.SSHLog,
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() && (strings.HasPrefix(info.Name(), "secure") || strings.HasPrefix(info.Name(), "auth")) {
|
||||
if !info.IsDir() && (strings.HasPrefix(info.Name(), "secure") || strings.HasPrefix(info.Name(), "auth") || strings.HasPrefix(info.Name(), "messages")) {
|
||||
if !strings.HasSuffix(info.Name(), ".gz") {
|
||||
fileList = append(fileList, sshFileItem{Name: pathItem, Year: info.ModTime().Year()})
|
||||
return nil
|
||||
@@ -326,7 +320,8 @@ func (u *SSHService) LoadLog(c *gin.Context, req dto.SearchSSHLog) (*dto.SSHLog,
|
||||
nyc, _ := time.LoadLocation(common.LoadTimeZoneByCmd())
|
||||
for _, file := range fileList {
|
||||
commandItem := ""
|
||||
if strings.HasPrefix(path.Base(file.Name), "secure") {
|
||||
switch {
|
||||
case strings.HasPrefix(path.Base(file.Name), "secure"):
|
||||
switch req.Status {
|
||||
case constant.StatusSuccess:
|
||||
commandItem = fmt.Sprintf("cat %s | grep -a Accepted %s", file.Name, command)
|
||||
@@ -335,8 +330,16 @@ func (u *SSHService) LoadLog(c *gin.Context, req dto.SearchSSHLog) (*dto.SSHLog,
|
||||
default:
|
||||
commandItem = fmt.Sprintf("cat %s | grep -aE '(Failed password for|Accepted)' %s", file.Name, command)
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(path.Base(file.Name), "auth.log") {
|
||||
case strings.HasPrefix(path.Base(file.Name), "messages"):
|
||||
switch req.Status {
|
||||
case constant.StatusSuccess:
|
||||
commandItem = fmt.Sprintf("cat %s | grep -aE 'sshd.*Accepted (password|publickey)' %s", file.Name, command)
|
||||
case constant.StatusFailed:
|
||||
commandItem = fmt.Sprintf("cat %s | grep -aE 'sshd.*(Failed password for|Connection closed by authenticating user)' %s", file.Name, command)
|
||||
default:
|
||||
commandItem = fmt.Sprintf("cat %s | grep -aE 'sshd.*(Accepted|Failed password for|Connection closed)' %s", file.Name, command)
|
||||
}
|
||||
case strings.HasPrefix(path.Base(file.Name), "auth.log"):
|
||||
switch req.Status {
|
||||
case constant.StatusSuccess:
|
||||
commandItem = fmt.Sprintf("cat %s | grep -a Accepted %s", file.Name, command)
|
||||
@@ -432,107 +435,160 @@ func loadSSHData(c *gin.Context, command string, showCountFrom, showCountTo, cur
|
||||
lines := strings.Split(string(stdout2), "\n")
|
||||
for i := len(lines) - 1; i >= 0; i-- {
|
||||
var itemData dto.SSHHistory
|
||||
line := strings.TrimSpace(lines[i])
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) < 12 {
|
||||
continue
|
||||
}
|
||||
|
||||
// 统一时间解析逻辑
|
||||
var dateStr string
|
||||
var timeIndex int
|
||||
if strings.Contains(parts[0], "-") { // 处理RFC3339时间格式
|
||||
t, err := time.Parse(time.RFC3339Nano, parts[0])
|
||||
if err == nil {
|
||||
dateStr = t.Format("2006 Jan 2 15:04:05")
|
||||
timeIndex = 0
|
||||
}
|
||||
} else { // 处理系统日志格式
|
||||
dateParts := parts[:3]
|
||||
if len(dateParts) < 3 {
|
||||
continue
|
||||
}
|
||||
dateStr = strings.Join(dateParts, " ")
|
||||
timeIndex = 3
|
||||
}
|
||||
|
||||
// 根据日志类型解析内容
|
||||
switch {
|
||||
case strings.Contains(lines[i], "Failed password for"):
|
||||
itemData = loadFailedSecureDatas(lines[i])
|
||||
if len(itemData.Address) != 0 {
|
||||
if successCount+failedCount >= showCountFrom && successCount+failedCount < showCountTo {
|
||||
itemData.Area, _ = geo.GetIPLocation(itemData.Address, common.GetLang(c))
|
||||
itemData.Date = loadDate(currentYear, itemData.DateStr, nyc)
|
||||
datas = append(datas, itemData)
|
||||
}
|
||||
failedCount++
|
||||
}
|
||||
case strings.Contains(lines[i], "Connection closed by authenticating user"):
|
||||
itemData = loadFailedAuthDatas(lines[i])
|
||||
if len(itemData.Address) != 0 {
|
||||
if successCount+failedCount >= showCountFrom && successCount+failedCount < showCountTo {
|
||||
itemData.Area, _ = geo.GetIPLocation(itemData.Address, common.GetLang(c))
|
||||
itemData.Date = loadDate(currentYear, itemData.DateStr, nyc)
|
||||
datas = append(datas, itemData)
|
||||
}
|
||||
failedCount++
|
||||
}
|
||||
case strings.Contains(lines[i], "Accepted "):
|
||||
itemData = loadSuccessDatas(lines[i])
|
||||
if len(itemData.Address) != 0 {
|
||||
if successCount+failedCount >= showCountFrom && successCount+failedCount < showCountTo {
|
||||
itemData.Area, _ = geo.GetIPLocation(itemData.Address, common.GetLang(c))
|
||||
itemData.Date = loadDate(currentYear, itemData.DateStr, nyc)
|
||||
datas = append(datas, itemData)
|
||||
}
|
||||
successCount++
|
||||
}
|
||||
case strings.Contains(line, "Failed password for"):
|
||||
itemData = parseFailedPasswordLog(parts, timeIndex, dateStr)
|
||||
case strings.Contains(line, "Connection closed by authenticating user"):
|
||||
itemData = parseConnectionClosedLog(parts, timeIndex, dateStr)
|
||||
case strings.Contains(line, "Accepted"):
|
||||
itemData = parseAcceptedLog(parts, timeIndex, dateStr)
|
||||
default:
|
||||
continue
|
||||
}
|
||||
if itemData.Address == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
total := successCount + failedCount
|
||||
if total >= showCountFrom && total < showCountTo {
|
||||
itemData.Area, _ = geo.GetIPLocation(itemData.Address, common.GetLang(c))
|
||||
itemData.Date = parseLogDate(currentYear, dateStr, nyc)
|
||||
datas = append(datas, itemData)
|
||||
}
|
||||
|
||||
if itemData.Status == constant.StatusSuccess {
|
||||
successCount++
|
||||
} else {
|
||||
failedCount++
|
||||
}
|
||||
|
||||
if total >= showCountTo {
|
||||
break
|
||||
}
|
||||
}
|
||||
return datas, successCount, failedCount
|
||||
}
|
||||
|
||||
func loadSuccessDatas(line string) dto.SSHHistory {
|
||||
var data dto.SSHHistory
|
||||
parts := strings.Fields(line)
|
||||
index, dataStr := analyzeDateStr(parts)
|
||||
if dataStr == "" {
|
||||
return data
|
||||
func parseFailedPasswordLog(parts []string, timeIndex int, dateStr string) dto.SSHHistory {
|
||||
data := dto.SSHHistory{
|
||||
DateStr: dateStr,
|
||||
Status: constant.StatusFailed,
|
||||
AuthMode: "publickey",
|
||||
}
|
||||
data.DateStr = dataStr
|
||||
data.AuthMode = parts[4+index]
|
||||
data.User = parts[6+index]
|
||||
data.Address = parts[8+index]
|
||||
data.Port = parts[10+index]
|
||||
data.Status = constant.StatusSuccess
|
||||
return data
|
||||
}
|
||||
|
||||
func loadFailedAuthDatas(line string) dto.SSHHistory {
|
||||
var data dto.SSHHistory
|
||||
parts := strings.Fields(line)
|
||||
index, dataStr := analyzeDateStr(parts)
|
||||
if dataStr == "" {
|
||||
return data
|
||||
// 查找关键字段位置
|
||||
for i := timeIndex; i < len(parts); i++ {
|
||||
switch parts[i] {
|
||||
case "for":
|
||||
if i+1 < len(parts) {
|
||||
data.User = parts[i+1]
|
||||
}
|
||||
case "from":
|
||||
if i+1 < len(parts) {
|
||||
data.Address = parts[i+1]
|
||||
}
|
||||
case "port":
|
||||
if i+1 < len(parts) {
|
||||
data.Port = parts[i+1]
|
||||
}
|
||||
case "password":
|
||||
data.AuthMode = "password"
|
||||
}
|
||||
}
|
||||
data.DateStr = dataStr
|
||||
switch index {
|
||||
case 1:
|
||||
data.User = parts[9]
|
||||
case 2:
|
||||
data.User = parts[10]
|
||||
default:
|
||||
data.User = parts[7]
|
||||
}
|
||||
data.AuthMode = parts[6+index]
|
||||
data.Address = parts[9+index]
|
||||
data.Port = parts[11+index]
|
||||
data.Status = constant.StatusFailed
|
||||
if strings.Contains(line, ": ") {
|
||||
data.Message = strings.Split(line, ": ")[1]
|
||||
}
|
||||
return data
|
||||
}
|
||||
func loadFailedSecureDatas(line string) dto.SSHHistory {
|
||||
var data dto.SSHHistory
|
||||
parts := strings.Fields(line)
|
||||
index, dataStr := analyzeDateStr(parts)
|
||||
if dataStr == "" {
|
||||
return data
|
||||
}
|
||||
data.DateStr = dataStr
|
||||
if strings.Contains(line, " invalid ") {
|
||||
data.AuthMode = parts[4+index]
|
||||
index += 2
|
||||
} else {
|
||||
data.AuthMode = parts[4+index]
|
||||
}
|
||||
data.User = parts[6+index]
|
||||
data.Address = parts[8+index]
|
||||
data.Port = parts[10+index]
|
||||
data.Status = constant.StatusFailed
|
||||
if strings.Contains(line, ": ") {
|
||||
data.Message = strings.Split(line, ": ")[1]
|
||||
|
||||
if strings.Contains(strings.Join(parts, " "), "invalid user") {
|
||||
data.Message = "invalid user attempt"
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func parseConnectionClosedLog(parts []string, timeIndex int, dateStr string) dto.SSHHistory {
|
||||
data := dto.SSHHistory{
|
||||
DateStr: dateStr,
|
||||
Status: constant.StatusFailed,
|
||||
}
|
||||
|
||||
// 解析Alpine格式的特殊字段
|
||||
fieldStart := timeIndex + 5 // 跳过时间、主机、进程字段
|
||||
for i := fieldStart; i < len(parts); i++ {
|
||||
switch {
|
||||
case parts[i] == "user":
|
||||
if i+1 < len(parts) {
|
||||
data.User = parts[i+1]
|
||||
}
|
||||
case parts[i] == "port":
|
||||
if i+1 < len(parts) {
|
||||
data.Port = parts[i+1]
|
||||
}
|
||||
case isIPAddress(parts[i]):
|
||||
data.Address = parts[i]
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func parseAcceptedLog(parts []string, timeIndex int, dateStr string) dto.SSHHistory {
|
||||
data := dto.SSHHistory{
|
||||
DateStr: dateStr,
|
||||
Status: constant.StatusSuccess,
|
||||
AuthMode: "password", // 默认值
|
||||
}
|
||||
|
||||
// 处理不同日志格式
|
||||
fieldStart := timeIndex + 5 // 基础字段偏移
|
||||
for i := fieldStart; i < len(parts); i++ {
|
||||
switch {
|
||||
case parts[i] == "for":
|
||||
if i+1 < len(parts) {
|
||||
data.User = parts[i+1]
|
||||
}
|
||||
case parts[i] == "from":
|
||||
if i+1 < len(parts) {
|
||||
data.Address = parts[i+1]
|
||||
}
|
||||
case parts[i] == "port":
|
||||
if i+1 < len(parts) {
|
||||
data.Port = parts[i+1]
|
||||
}
|
||||
case strings.Contains(parts[i], "ssh2:"):
|
||||
data.AuthMode = "publickey"
|
||||
case parts[i] == "publickey":
|
||||
data.AuthMode = "publickey"
|
||||
case parts[i] == "password":
|
||||
data.AuthMode = "password"
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
func handleGunzip(path string) error {
|
||||
if _, err := cmd.Execf("gunzip %s", path); err != nil {
|
||||
return err
|
||||
@@ -541,40 +597,36 @@ func handleGunzip(path string) error {
|
||||
}
|
||||
|
||||
func loadServiceName() (string, error) {
|
||||
if exist, _ := systemctl.IsExist("sshd"); exist {
|
||||
return "sshd", nil
|
||||
} else if exist, _ := systemctl.IsExist("ssh"); exist {
|
||||
return "ssh", nil
|
||||
}
|
||||
return "", errors.New("The ssh or sshd service is unavailable")
|
||||
}
|
||||
|
||||
func loadDate(currentYear int, DateStr string, nyc *time.Location) time.Time {
|
||||
itemDate, err := time.ParseInLocation("2006 Jan 2 15:04:05", fmt.Sprintf("%d %s", currentYear, DateStr), nyc)
|
||||
serviceName, err := systemctl.GetServiceName("ssh")
|
||||
if err != nil {
|
||||
itemDate, _ = time.ParseInLocation("2006 Jan 2 15:04:05", DateStr, nyc)
|
||||
if errors.Is(err, systemctl.ErrServiceNotFound) {
|
||||
return "", fmt.Errorf("SSH service unavailable. Please ensure OpenSSH server is installed and configured")
|
||||
}
|
||||
return "", fmt.Errorf("failed to load SSH service name: %w", err)
|
||||
}
|
||||
return itemDate
|
||||
return serviceName, nil
|
||||
}
|
||||
|
||||
func analyzeDateStr(parts []string) (int, string) {
|
||||
t, err := time.Parse(time.RFC3339Nano, parts[0])
|
||||
if err == nil {
|
||||
if len(parts) < 12 {
|
||||
return 0, ""
|
||||
}
|
||||
return 0, t.Format("2006 Jan 2 15:04:05")
|
||||
}
|
||||
t, err = time.Parse(constant.DateTimeLayout, fmt.Sprintf("%s %s", parts[0], parts[1]))
|
||||
if err == nil {
|
||||
if len(parts) < 14 {
|
||||
return 0, ""
|
||||
}
|
||||
return 1, t.Format("2006 Jan 2 15:04:05")
|
||||
func parseLogDate(currentYear int, dateStr string, loc *time.Location) time.Time {
|
||||
// 处理带年份和不带年份的情况
|
||||
formats := []string{
|
||||
"2006 Jan 2 15:04:05",
|
||||
"Jan 2 15:04:05",
|
||||
}
|
||||
|
||||
if len(parts) < 14 {
|
||||
return 0, ""
|
||||
for _, format := range formats {
|
||||
t, err := time.ParseInLocation(format, dateStr, loc)
|
||||
if err == nil {
|
||||
if t.Year() == 0 {
|
||||
return time.Date(currentYear, t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), 0, loc)
|
||||
}
|
||||
return t
|
||||
}
|
||||
}
|
||||
return 2, fmt.Sprintf("%s %s %s", parts[0], parts[1], parts[2])
|
||||
return time.Now().In(loc)
|
||||
}
|
||||
|
||||
func isIPAddress(s string) bool {
|
||||
return net.ParseIP(s) != nil
|
||||
|
||||
}
|
||||
|
||||
+154
-80
@@ -6,6 +6,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -17,6 +18,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/files"
|
||||
httpUtil "github.com/1Panel-dev/1Panel/backend/utils/http"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/systemctl"
|
||||
)
|
||||
|
||||
type UpgradeService struct{}
|
||||
@@ -103,123 +105,159 @@ func (u *UpgradeService) Upgrade(req dto.Upgrade) error {
|
||||
}
|
||||
downloadPath := fmt.Sprintf("%s/%s/%s/release", global.CONF.System.RepoUrl, mode, req.Version)
|
||||
fileName := fmt.Sprintf("1panel-%s-%s-%s.tar.gz", req.Version, "linux", itemArch)
|
||||
_ = settingRepo.Update("SystemStatus", "Upgrading")
|
||||
serviceHandle, _ := systemctl.DefaultHandler("1panel")
|
||||
currentServiceName := serviceHandle.GetServiceName()
|
||||
if err := settingRepo.Update("SystemStatus", "Upgrading"); err != nil {
|
||||
return fmt.Errorf("update system status failed: %w", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if err := settingRepo.Update("SystemStatus", "Free"); err != nil {
|
||||
global.LOG.Errorf("Reset system status failed: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
_ = global.Cron.Stop()
|
||||
defer func() {
|
||||
global.Cron.Start()
|
||||
}()
|
||||
if err := fileOp.DownloadFileWithProxy(downloadPath+"/"+fileName, rootDir+"/"+fileName); err != nil {
|
||||
global.LOG.Errorf("download service file failed, err: %v", err)
|
||||
_ = settingRepo.Update("SystemStatus", "Free")
|
||||
defer global.Cron.Start()
|
||||
|
||||
if err := fileOp.DownloadFileWithProxy(
|
||||
fmt.Sprintf("%s/%s", downloadPath, fileName),
|
||||
path.Join(rootDir, fileName),
|
||||
); err != nil {
|
||||
global.LOG.Errorf("Failed to download upgrade package: %v", err)
|
||||
return
|
||||
}
|
||||
global.LOG.Info("download all file successful!")
|
||||
defer func() {
|
||||
_ = os.Remove(rootDir)
|
||||
}()
|
||||
if err := handleUnTar(rootDir+"/"+fileName, rootDir, ""); err != nil {
|
||||
global.LOG.Errorf("decompress file failed, err: %v", err)
|
||||
_ = settingRepo.Update("SystemStatus", "Free")
|
||||
defer os.RemoveAll(rootDir)
|
||||
|
||||
if err := handleUnTar(path.Join(rootDir, fileName), rootDir, ""); err != nil {
|
||||
global.LOG.Errorf("Failed to extract package: %v", err)
|
||||
return
|
||||
}
|
||||
tmpDir := rootDir + "/" + strings.ReplaceAll(fileName, ".tar.gz", "")
|
||||
|
||||
tmpDir := path.Join(rootDir, strings.TrimSuffix(fileName, ".tar.gz"))
|
||||
|
||||
if err := u.handleBackup(fileOp, originalDir); err != nil {
|
||||
global.LOG.Errorf("handle backup original file failed, err: %v", err)
|
||||
_ = settingRepo.Update("SystemStatus", "Free")
|
||||
return
|
||||
}
|
||||
global.LOG.Info("backup original data successful, now start to upgrade!")
|
||||
|
||||
if err := common.CopyFile(path.Join(tmpDir, "1panel"), "/usr/local/bin"); err != nil {
|
||||
global.LOG.Errorf("upgrade 1panel failed, err: %v", err)
|
||||
u.handleRollback(originalDir, 1)
|
||||
global.LOG.Errorf("Backup failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := common.CopyFile(path.Join(tmpDir, "1pctl"), "/usr/local/bin"); err != nil {
|
||||
global.LOG.Errorf("upgrade 1pctl failed, err: %v", err)
|
||||
u.handleRollback(originalDir, 2)
|
||||
return
|
||||
}
|
||||
_, _ = cmd.Execf("cp -r %s /usr/local/bin", path.Join(tmpDir, "lang"))
|
||||
geoPath := path.Join(global.CONF.System.BaseDir, "1panel/geo")
|
||||
_, _ = cmd.Execf("mkdir %s && cp %s %s/", geoPath, path.Join(tmpDir, "GeoIP.mmdb"), geoPath)
|
||||
binDir := systemctl.BinaryPath
|
||||
servicePath, _ := serviceHandle.GetServicePath()
|
||||
geoPath := path.Join(global.CONF.System.BaseDir, "1panel/geo/GeoIP.mmdb")
|
||||
|
||||
if _, err := cmd.Execf("sed -i -e 's#BASE_DIR=.*#BASE_DIR=%s#g' /usr/local/bin/1pctl", global.CONF.System.BaseDir); err != nil {
|
||||
global.LOG.Errorf("upgrade basedir in 1pctl failed, err: %v", err)
|
||||
criticalUpdates := []struct {
|
||||
src string
|
||||
dest string
|
||||
step int
|
||||
}{
|
||||
{path.Join(tmpDir, "1panel"), path.Join(binDir, "1panel"), 1},
|
||||
{path.Join(tmpDir, "1pctl"), path.Join(binDir, "1pctl"), 2},
|
||||
{selectInitScript(path.Join(tmpDir, "initscript"), currentServiceName), servicePath, 3},
|
||||
}
|
||||
|
||||
for _, update := range criticalUpdates {
|
||||
if err := common.Copy(update.src, update.dest); err != nil {
|
||||
global.LOG.Errorf("Update %s failed: %v", path.Base(update.dest), err)
|
||||
u.handleRollback(originalDir, update.step)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := cmd.Execf("sed -i -e 's#BASE_DIR=.*#BASE_DIR=%s#g' /usr/local/bin/1pctl",
|
||||
global.CONF.System.BaseDir); err != nil {
|
||||
global.LOG.Errorf("Update base directory failed: %v", err)
|
||||
u.handleRollback(originalDir, 2)
|
||||
return
|
||||
}
|
||||
|
||||
if err := common.CopyFile(path.Join(tmpDir, "1panel.service"), "/etc/systemd/system"); err != nil {
|
||||
global.LOG.Errorf("upgrade 1panel.service failed, err: %v", err)
|
||||
u.handleRollback(originalDir, 3)
|
||||
return
|
||||
langDir := path.Join(binDir, "lang")
|
||||
if err := common.Copy(path.Join(tmpDir, "lang"), langDir); err != nil {
|
||||
global.LOG.Errorf("Update language files failed: %v", err)
|
||||
}
|
||||
if err := common.Copy(path.Join(tmpDir, "GeoIP.mmdb"), geoPath); err != nil {
|
||||
global.LOG.Warnf("Update GeoIP database failed: %v", err)
|
||||
}
|
||||
|
||||
global.LOG.Info("upgrade successful!")
|
||||
go writeLogs(req.Version)
|
||||
_ = settingRepo.Update("SystemVersion", req.Version)
|
||||
_ = settingRepo.Update("SystemStatus", "Free")
|
||||
checkPointOfWal()
|
||||
_, _ = cmd.ExecWithTimeOut("systemctl daemon-reload && systemctl restart 1panel.service", 1*time.Minute)
|
||||
if err := settingRepo.Update("SystemVersion", req.Version); err != nil {
|
||||
global.LOG.Errorf("Update system version failed: %v", err)
|
||||
}
|
||||
if serviceHandle.ManagerName() == "systemd" {
|
||||
_, _ = cmd.Exec("systemctl daemon-reload")
|
||||
}
|
||||
if err := systemctl.Restart("1panel"); err != nil {
|
||||
global.LOG.Errorf("Service restart failed: %v", err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UpgradeService) handleBackup(fileOp files.FileOp, originalDir string) error {
|
||||
if err := fileOp.Copy("/usr/local/bin/1panel", originalDir); err != nil {
|
||||
return err
|
||||
global.LOG.Info("Initiating backup procedure...")
|
||||
h, _ := systemctl.DefaultHandler("1panel")
|
||||
binDir := systemctl.BinaryPath
|
||||
servicePath, _ := h.GetServicePath()
|
||||
geoPath := path.Join(global.CONF.System.BaseDir, "1panel/geo/GeoIP.mmdb")
|
||||
|
||||
backupItems := []struct {
|
||||
src string
|
||||
dest string
|
||||
}{
|
||||
{path.Join(binDir, "1panel"), originalDir},
|
||||
{path.Join(binDir, "1pctl"), originalDir},
|
||||
{servicePath, originalDir},
|
||||
{path.Join(binDir, "lang"), originalDir},
|
||||
{geoPath, originalDir},
|
||||
}
|
||||
if err := fileOp.Copy("/usr/local/bin/1pctl", originalDir); err != nil {
|
||||
return err
|
||||
|
||||
for _, item := range backupItems {
|
||||
if err := fileOp.Copy(item.src, item.dest); err != nil {
|
||||
return fmt.Errorf("backup %s failed: %w", path.Base(item.src), err)
|
||||
}
|
||||
}
|
||||
if err := fileOp.Copy("/etc/systemd/system/1panel.service", originalDir); err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = cmd.Execf("cp -r /usr/local/bin/lang %s", originalDir)
|
||||
_, _ = cmd.Execf("cp %s %s", path.Join(global.CONF.System.BaseDir, "1panel/geo/GeoIP.mmdb"), originalDir)
|
||||
checkPointOfWal()
|
||||
if err := handleTar(path.Join(global.CONF.System.BaseDir, "1panel/db"), originalDir, "db.tar.gz", "db/1Panel.db-*", ""); err != nil {
|
||||
return err
|
||||
|
||||
if err := handleTar(
|
||||
path.Join(global.CONF.System.BaseDir, "1panel/db"),
|
||||
originalDir,
|
||||
"db.tar.gz",
|
||||
"db/1Panel.db-*",
|
||||
"",
|
||||
); err != nil {
|
||||
return fmt.Errorf("database backup failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UpgradeService) handleRollback(originalDir string, errStep int) {
|
||||
_ = settingRepo.Update("SystemStatus", "Free")
|
||||
global.LOG.Info("Initiating rollback procedure...")
|
||||
h, _ := systemctl.DefaultHandler("1panel")
|
||||
binDir := systemctl.BinaryPath
|
||||
servicePath, _ := h.GetServicePath()
|
||||
geoPath := path.Join(global.CONF.System.BaseDir, "1panel/geo/GeoIP.mmdb")
|
||||
|
||||
checkPointOfWal()
|
||||
if _, err := os.Stat(path.Join(originalDir, "1Panel.db")); err == nil {
|
||||
if err := common.CopyFile(path.Join(originalDir, "1Panel.db"), global.CONF.System.DbPath); err != nil {
|
||||
global.LOG.Errorf("rollback 1panel db failed, err: %v", err)
|
||||
rollbackSteps := []struct {
|
||||
src string
|
||||
dest string
|
||||
}{
|
||||
{path.Join(originalDir, "1panel"), path.Join(binDir, "1panel")},
|
||||
{path.Join(originalDir, "1pctl"), path.Join(binDir, "1pctl")},
|
||||
{path.Join(originalDir, filepath.Base(servicePath)), servicePath},
|
||||
{path.Join(originalDir, "lang"), path.Join(binDir, "lang")},
|
||||
{path.Join(originalDir, "GeoIP.mmdb"), geoPath},
|
||||
}
|
||||
|
||||
for _, step := range rollbackSteps[:errStep] {
|
||||
if err := common.CopyFile(step.src, step.dest); err != nil {
|
||||
global.LOG.Errorf("Rollback %s failed: %v", path.Base(step.src), err)
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(path.Join(originalDir, "db.tar.gz")); err == nil {
|
||||
if err := handleUnTar(path.Join(originalDir, "db.tar.gz"), global.CONF.System.DbPath, ""); err != nil {
|
||||
global.LOG.Errorf("rollback 1panel db failed, err: %v", err)
|
||||
}
|
||||
}
|
||||
if err := common.CopyFile(path.Join(originalDir, "1panel"), "/usr/local/bin"); err != nil {
|
||||
global.LOG.Errorf("rollback 1pctl failed, err: %v", err)
|
||||
}
|
||||
if errStep == 1 {
|
||||
return
|
||||
}
|
||||
if err := common.CopyFile(path.Join(originalDir, "1pctl"), "/usr/local/bin"); err != nil {
|
||||
global.LOG.Errorf("rollback 1panel failed, err: %v", err)
|
||||
}
|
||||
_, _ = cmd.Execf("cp -r %s /usr/local/bin", path.Join(originalDir, "lang"))
|
||||
geoPath := path.Join(global.CONF.System.BaseDir, "1panel/geo")
|
||||
_, _ = cmd.Execf("mkdir %s && cp %s %s/", geoPath, path.Join(originalDir, "GeoIP.mmdb"), geoPath)
|
||||
|
||||
if errStep == 2 {
|
||||
return
|
||||
}
|
||||
if err := common.CopyFile(path.Join(originalDir, "1panel.service"), "/etc/systemd/system"); err != nil {
|
||||
global.LOG.Errorf("rollback 1panel failed, err: %v", err)
|
||||
if err := systemctl.Restart("1panel"); err != nil {
|
||||
global.LOG.Errorf("Service restart during rollback failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,5 +395,41 @@ func loadArch() (string, error) {
|
||||
if strings.Contains(std, "s390x") {
|
||||
return "s390x", nil
|
||||
}
|
||||
if strings.Contains(std, "riscv64") {
|
||||
return "riscv64", nil
|
||||
}
|
||||
return "", fmt.Errorf("unsupported such arch: %s", std)
|
||||
}
|
||||
|
||||
func selectInitScript(path string, serviceName string) string {
|
||||
path = strings.TrimSuffix(path, "/")
|
||||
mgr := systemctl.GetGlobalManager().Name()
|
||||
var serviceFileName string
|
||||
switch mgr {
|
||||
case "systemd":
|
||||
serviceFileName = "1panel.service"
|
||||
case "openrc":
|
||||
serviceFileName = "1paneld.openrc"
|
||||
case "sysvinit":
|
||||
isWrt := systemctl.FileExist("/etc/rc.common")
|
||||
if isWrt {
|
||||
serviceFileName = "1paneld.procd"
|
||||
} else {
|
||||
serviceFileName = "1paneld.init"
|
||||
}
|
||||
default:
|
||||
serviceFileName = serviceName
|
||||
global.LOG.Warnf("[%s]unselect InitScript, used default: %s", mgr, serviceName)
|
||||
}
|
||||
sourcePath := filepath.Join(path, serviceFileName)
|
||||
targetPath := filepath.Join(path, serviceName)
|
||||
|
||||
if serviceFileName != serviceName {
|
||||
if _, err := cmd.Execf("cp %s %s", sourcePath, targetPath); err != nil {
|
||||
global.LOG.Errorf("Failed to copy init script from %s to %s: %v",
|
||||
serviceFileName, serviceName, err)
|
||||
}
|
||||
}
|
||||
|
||||
return targetPath
|
||||
}
|
||||
|
||||
@@ -427,7 +427,9 @@ func (w WebsiteService) UpdateWebsite(req request.WebsiteUpdate) error {
|
||||
}
|
||||
}
|
||||
website.PrimaryDomain = req.PrimaryDomain
|
||||
website.WebsiteGroupID = req.WebsiteGroupID
|
||||
if req.WebsiteGroupID > 0 {
|
||||
website.WebsiteGroupID = req.WebsiteGroupID
|
||||
}
|
||||
website.Remark = req.Remark
|
||||
website.IPV6 = req.IPV6
|
||||
|
||||
@@ -496,6 +498,10 @@ func (w WebsiteService) DeleteWebsite(req request.WebsiteDelete) error {
|
||||
if err := websiteDomainRepo.DeleteBy(ctx, websiteDomainRepo.WithWebsiteId(req.ID)); err != nil {
|
||||
return err
|
||||
}
|
||||
websiteID := GetWebsiteID()
|
||||
if req.ID == websiteID {
|
||||
_ = settingRepo.Update("MCP_WEBSITE_ID", "0")
|
||||
}
|
||||
tx.Commit()
|
||||
|
||||
uploadDir := path.Join(global.CONF.System.BaseDir, fmt.Sprintf("1panel/uploads/website/%s", website.Alias))
|
||||
@@ -1764,11 +1770,22 @@ func (w WebsiteService) GetProxies(id uint) (res []request.WebsiteProxyConfig, e
|
||||
}
|
||||
directives := config.GetDirectives()
|
||||
|
||||
location, ok := directives[0].(*components.Location)
|
||||
if !ok {
|
||||
err = errors.New("error")
|
||||
return
|
||||
var (
|
||||
location *components.Location
|
||||
ok bool
|
||||
)
|
||||
for _, directive := range directives {
|
||||
if directive.GetName() == "location" {
|
||||
location, ok = directive.(*components.Location)
|
||||
if ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if location == nil {
|
||||
return nil, buserr.New("ErrConfigParse")
|
||||
}
|
||||
|
||||
proxyConfig.ProxyPass = location.ProxyPass
|
||||
proxyConfig.Cache = location.Cache
|
||||
if location.CacheTime > 0 {
|
||||
|
||||
@@ -35,6 +35,7 @@ var WebUrlMap = map[string]struct{}{
|
||||
"/ai": {},
|
||||
"/ai/model": {},
|
||||
"/ai/gpu": {},
|
||||
"/ai/mcp": {},
|
||||
|
||||
"/containers": {},
|
||||
"/containers/container": {},
|
||||
|
||||
@@ -17,4 +17,5 @@ var (
|
||||
RuntimeDir = path.Join(DataDir, "runtime")
|
||||
RecycleBinDir = "/.1panel_clash"
|
||||
SSLLogDir = path.Join(global.CONF.System.DataDir, "log", "ssl")
|
||||
McpDir = path.Join(DataDir, "mcp")
|
||||
)
|
||||
|
||||
@@ -13,6 +13,7 @@ const (
|
||||
RuntimeStopped = "stopped"
|
||||
RuntimeUnhealthy = "unhealthy"
|
||||
RuntimeCreating = "creating"
|
||||
RuntimeReStarting = "restarting"
|
||||
|
||||
RuntimePHP = "php"
|
||||
RuntimeNode = "node"
|
||||
|
||||
@@ -288,6 +288,7 @@ SystemMode: "mode: "
|
||||
#ai-tool
|
||||
ErrOpenrestyInstall: 'Please install Openresty first'
|
||||
ErrSSL: "Certificate content is empty, please check the certificate!"
|
||||
ErrSsePath: "SSE path is duplicated"
|
||||
|
||||
#mobile app
|
||||
ErrVerifyToken: 'Token verification error, please reset and scan again.'
|
||||
|
||||
@@ -285,6 +285,7 @@ SystemMode: "モード: "
|
||||
#ai-tool
|
||||
ErrOpenrestyInstall: 'まず Openresty をインストールしてください'
|
||||
ErrSSL: "証明書の内容が空です。証明書を確認してください!"
|
||||
ErrSsePath: "SSE パスが重複しています"
|
||||
|
||||
|
||||
#mobile app
|
||||
|
||||
@@ -288,6 +288,7 @@ SystemMode: "모드: "
|
||||
#ai-tool
|
||||
ErrOpenrestyInstall: '먼저 Openresty를 설치하세요'
|
||||
ErrSSL: "인증서 내용이 비어 있습니다. 인증서를 확인하세요!"
|
||||
ErrSsePath: "SSE 경로가 중복되었습니다"
|
||||
|
||||
|
||||
#mobile app
|
||||
|
||||
@@ -287,7 +287,7 @@ SystemMode: "Mod: "
|
||||
#ai-tool
|
||||
ErrOpenrestyInstall: 'Sila pasang Openresty terlebih dahulu'
|
||||
ErrSSL: "Kandungan sijil kosong, sila periksa sijil!"
|
||||
|
||||
ErrSsePath: "Laluan SSE bertindan"
|
||||
|
||||
#mobile app
|
||||
ErrVerifyToken: 'Ralat pengesahan token, sila tetapkan semula dan imbas semula.'
|
||||
|
||||
@@ -285,6 +285,7 @@ SystemMode: "modo: "
|
||||
#ai-tool
|
||||
ErrOpenrestyInstall: 'Por favor, instale o Openresty primeiro'
|
||||
ErrSSL: "O conteúdo do certificado está vazio, por favor, verifique o certificado!"
|
||||
ErrSsePath: "Caminho SSE duplicado"
|
||||
|
||||
#mobile app
|
||||
ErrVerifyToken: 'Erro de verificação do token, por favor, reinicie e escaneie novamente.'
|
||||
|
||||
@@ -288,6 +288,7 @@ SystemMode: "режим: "
|
||||
#ai-tool
|
||||
ErrOpenrestyInstall: "Пожалуйста, установите Openresty сначала"
|
||||
ErrSSL: "Содержимое сертификата пустое, пожалуйста, проверьте сертификат!"
|
||||
ErrSsePath: "Путь SSE дублируется"
|
||||
|
||||
#mobile app
|
||||
ErrVerifyToken: 'шибка проверки токена, пожалуйста, сбросьте и отсканируйте снова.'
|
||||
|
||||
@@ -288,6 +288,7 @@ SystemMode: "模式: "
|
||||
#ai-tool
|
||||
ErrOpenrestyInstall: "請先安裝 Openresty"
|
||||
ErrSSL: "證書內容為空,請檢查證書!"
|
||||
ErrSsePath: "SSE 路徑重複"
|
||||
|
||||
#mobile app
|
||||
ErrVerifyToken: '令牌驗證錯誤,請重置後再次掃碼'
|
||||
|
||||
@@ -288,6 +288,7 @@ SystemMode: "模式: "
|
||||
#ai-tool
|
||||
ErrOpenrestyInstall: '请先安装 Openresty'
|
||||
ErrSSL: "证书内容为空,请检查证书!"
|
||||
ErrSsePath: "SSE 路径重复"
|
||||
|
||||
#mobile app
|
||||
ErrVerifyToken: '令牌验证错误,请重置后再次扫码'
|
||||
|
||||
@@ -23,8 +23,11 @@ func Init() {
|
||||
|
||||
constant.SSLLogDir = path.Join(global.CONF.System.DataDir, "log", "ssl")
|
||||
|
||||
constant.McpDir = path.Join(constant.DataDir, "mcp")
|
||||
|
||||
dirs := []string{constant.DataDir, constant.ResourceDir, constant.AppResourceDir, constant.AppInstallDir,
|
||||
global.CONF.System.Backup, constant.RuntimeDir, constant.LocalAppResourceDir, constant.RemoteAppResourceDir, constant.SSLLogDir}
|
||||
global.CONF.System.Backup, constant.RuntimeDir, constant.LocalAppResourceDir, constant.RemoteAppResourceDir,
|
||||
constant.SSLLogDir, constant.McpDir}
|
||||
|
||||
fileOp := files.NewFileOp()
|
||||
for _, dir := range dirs {
|
||||
|
||||
@@ -106,6 +106,11 @@ func Init() {
|
||||
migrations.AddAppMenu,
|
||||
migrations.AddAppPanelName,
|
||||
migrations.AddLicenseVerify,
|
||||
|
||||
migrations.AddMcpServer,
|
||||
migrations.AddPbootCMSPHPExtensions,
|
||||
migrations.DeleteV2Openresty,
|
||||
migrations.UpdateOnedrive,
|
||||
})
|
||||
if err := m.Migrate(); err != nil {
|
||||
global.LOG.Error(err)
|
||||
|
||||
@@ -459,3 +459,53 @@ var AddLicenseVerify = &gormigrate.Migration{
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var AddMcpServer = &gormigrate.Migration{
|
||||
ID: "20250401-add-mcp-server",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
if err := tx.AutoMigrate(&model.McpServer{}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var AddPbootCMSPHPExtensions = &gormigrate.Migration{
|
||||
ID: "20250625-add-php-extensions",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
item := &model.PHPExtensions{
|
||||
Name: "PbootCMS",
|
||||
}
|
||||
if err := tx.Where("name = ?", "PbootCMS").FirstOrCreate(item, &model.PHPExtensions{
|
||||
Name: "PbootCMS",
|
||||
Extensions: "curl,gd,mbstring,mysqli,openssl,pdo_mysql,pdo_sqlite,sqlite3",
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var DeleteV2Openresty = &gormigrate.Migration{
|
||||
ID: "20250701-delete-v2-openresty",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.Where("version = '1.27.1.2-0-1-focal'").Delete(&model.AppDetail{}).Error
|
||||
},
|
||||
}
|
||||
|
||||
var UpdateOnedrive = &gormigrate.Migration{
|
||||
ID: "20250704-update-onedrive",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
if err := tx.Model(&model.Setting{}).
|
||||
Where("key = ?", "OneDriveID").
|
||||
Update("value", "NTQ0NmNmZTMtNGM3OS00N2EwLWFlMjUtZmM2NDU0NzhlMmQ5").Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Model(&model.Setting{}).
|
||||
Where("key = ?", "OneDriveSc").
|
||||
Update("value", "bGRlOFF+WEVrR1M0b25Vb1VsRWpMYzE2MW9rTXZEM25KdnZ1MGN6MA==").Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
@@ -24,5 +24,6 @@ func commonGroups() []CommonRouter {
|
||||
&ProcessRouter{},
|
||||
&WebsiteCARouter{},
|
||||
&AIToolsRouter{},
|
||||
&McpServerRouter{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,5 +26,14 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
aiToolsRouter.POST("/domain/bind", baseApi.BindDomain)
|
||||
aiToolsRouter.POST("/domain/get", baseApi.GetBindDomain)
|
||||
aiToolsRouter.POST("/domain/update", baseApi.UpdateBindDomain)
|
||||
|
||||
aiToolsRouter.POST("/mcp/search", baseApi.PageMcpServers)
|
||||
aiToolsRouter.POST("/mcp/server", baseApi.CreateMcpServer)
|
||||
aiToolsRouter.POST("/mcp/server/update", baseApi.UpdateMcpServer)
|
||||
aiToolsRouter.POST("/mcp/server/del", baseApi.DeleteMcpServer)
|
||||
aiToolsRouter.POST("/mcp/server/op", baseApi.OperateMcpServer)
|
||||
aiToolsRouter.POST("/mcp/domain/bind", baseApi.BindMcpDomain)
|
||||
aiToolsRouter.GET("/mcp/domain/get", baseApi.GetMcpBindDomain)
|
||||
aiToolsRouter.POST("/mcp/domain/update", baseApi.UpdateMcpBindDomain)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
v1 "github.com/1Panel-dev/1Panel/backend/app/api/v1"
|
||||
"github.com/1Panel-dev/1Panel/backend/middleware"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type McpServerRouter struct {
|
||||
}
|
||||
|
||||
func (m *McpServerRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
mcpRouter := Router.Group("mcp")
|
||||
mcpRouter.Use(middleware.JwtAuth()).Use(middleware.SessionAuth()).Use(middleware.PasswordExpired())
|
||||
|
||||
baseApi := v1.ApiGroupApp.BaseApi
|
||||
{
|
||||
mcpRouter.POST("/search", baseApi.PageMcpServers)
|
||||
mcpRouter.POST("/server", baseApi.CreateMcpServer)
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,12 @@ package common
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"github.com/1Panel-dev/1Panel/backend/buserr"
|
||||
"github.com/gin-gonic/gin"
|
||||
"io"
|
||||
mathRand "math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
@@ -18,6 +17,9 @@ import (
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/backend/buserr"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"golang.org/x/net/idna"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
|
||||
@@ -124,6 +126,9 @@ func GetSortedVersions(versions []string) []string {
|
||||
}
|
||||
|
||||
func CopyFile(src, dst string) error {
|
||||
if fi, err := os.Stat(src); err == nil && fi.IsDir() {
|
||||
return fmt.Errorf("src is a directory, use CopyDirs instead")
|
||||
}
|
||||
source, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -152,7 +157,57 @@ func CopyFile(src, dst string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func CopyDirs(srcDir, dstDir string) error {
|
||||
srcInfo, err := os.Stat(srcDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !srcInfo.IsDir() {
|
||||
return fmt.Errorf("%s is not a directory", srcDir)
|
||||
}
|
||||
if err := os.MkdirAll(dstDir, srcInfo.Mode()); err != nil {
|
||||
return err
|
||||
}
|
||||
return filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
relPath, err := filepath.Rel(srcDir, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dstPath := filepath.Join(dstDir, relPath)
|
||||
if info.IsDir() {
|
||||
return os.MkdirAll(dstPath, info.Mode())
|
||||
}
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
return copySymlink(path, dstPath)
|
||||
}
|
||||
return CopyFile(path, dstPath)
|
||||
})
|
||||
}
|
||||
|
||||
func copySymlink(src, dst string) error {
|
||||
link, err := os.Readlink(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Symlink(link, dst)
|
||||
}
|
||||
func Copy(src, dst string) error {
|
||||
fi, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch mode := fi.Mode(); {
|
||||
case mode.IsDir():
|
||||
return CopyDirs(src, dst)
|
||||
case mode.IsRegular():
|
||||
return CopyFile(src, dst)
|
||||
default:
|
||||
return fmt.Errorf("unsupported file type: %s", mode)
|
||||
}
|
||||
}
|
||||
func IsCrossVersion(version1, version2 string) bool {
|
||||
version1s := strings.Split(version1, ".")
|
||||
version2s := strings.Split(version2, ".")
|
||||
|
||||
@@ -349,26 +349,30 @@ func (f FileOp) DownloadFileWithProxy(url, dst string) error {
|
||||
}
|
||||
|
||||
func (f FileOp) Cut(oldPaths []string, dst, name string, cover bool) error {
|
||||
for _, p := range oldPaths {
|
||||
var dstPath string
|
||||
if name != "" {
|
||||
dstPath = filepath.Join(dst, name)
|
||||
if f.Stat(dstPath) {
|
||||
dstPath = dst
|
||||
}
|
||||
} else {
|
||||
base := filepath.Base(p)
|
||||
dstPath = filepath.Join(dst, base)
|
||||
if len(oldPaths) == 0 {
|
||||
return nil
|
||||
}
|
||||
var dstPath string
|
||||
coverFlag := ""
|
||||
if name != "" {
|
||||
dstPath = filepath.Join(dst, name)
|
||||
if f.Stat(dstPath) {
|
||||
dstPath = dst
|
||||
}
|
||||
coverFlag := ""
|
||||
if cover {
|
||||
coverFlag = "-f"
|
||||
}
|
||||
|
||||
cmdStr := fmt.Sprintf(`mv %s '%s' '%s'`, coverFlag, p, dstPath)
|
||||
if err := cmd.ExecCmd(cmdStr); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
dstPath = dst
|
||||
coverFlag = "-f"
|
||||
}
|
||||
var quotedPaths []string
|
||||
for _, p := range oldPaths {
|
||||
quotedPaths = append(quotedPaths, fmt.Sprintf("'%s'", p))
|
||||
}
|
||||
mvCommand := fmt.Sprintf("mv %s %s '%s'", coverFlag, strings.Join(quotedPaths, " "), dstPath)
|
||||
if err := cmd.ExecCmd(mvCommand); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/backend/constant"
|
||||
"github.com/1Panel-dev/1Panel/backend/global"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/systemctl"
|
||||
)
|
||||
|
||||
var ForwardListRegex = regexp.MustCompile(`^port=(\d{1,5}):proto=(.+?):toport=(\d{1,5}):toaddr=(.*)$`)
|
||||
@@ -41,27 +42,15 @@ func (f *Firewall) Version() (string, error) {
|
||||
}
|
||||
|
||||
func (f *Firewall) Start() error {
|
||||
stdout, err := cmd.Exec("systemctl start firewalld")
|
||||
if err != nil {
|
||||
return fmt.Errorf("enable the firewall failed, err: %s", stdout)
|
||||
}
|
||||
return nil
|
||||
return systemctl.Start("firewalld")
|
||||
}
|
||||
|
||||
func (f *Firewall) Stop() error {
|
||||
stdout, err := cmd.Exec("systemctl stop firewalld")
|
||||
if err != nil {
|
||||
return fmt.Errorf("stop the firewall failed, err: %s", stdout)
|
||||
}
|
||||
return nil
|
||||
return systemctl.Stop("firewalld")
|
||||
}
|
||||
|
||||
func (f *Firewall) Restart() error {
|
||||
stdout, err := cmd.Exec("systemctl restart firewalld")
|
||||
if err != nil {
|
||||
return fmt.Errorf("restart the firewall failed, err: %s", stdout)
|
||||
}
|
||||
return nil
|
||||
return systemctl.Restart("firewalld")
|
||||
}
|
||||
|
||||
func (f *Firewall) Reload() error {
|
||||
|
||||
@@ -46,7 +46,9 @@ func NewLocation(directive IDirective) *Location {
|
||||
location.Host = params[1]
|
||||
}
|
||||
case "proxy_cache":
|
||||
location.Cache = true
|
||||
if params[0] != "off" {
|
||||
location.Cache = true
|
||||
}
|
||||
case "if":
|
||||
if params[0] == "(" && params[1] == "$uri" && params[2] == "~*" {
|
||||
dirs := dir.GetBlock().GetDirectives()
|
||||
|
||||
+37
-12
@@ -3,6 +3,7 @@ package ssl
|
||||
import (
|
||||
"crypto"
|
||||
"encoding/json"
|
||||
"github.com/go-acme/lego/v4/providers/dns/westcn"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -20,6 +21,7 @@ import (
|
||||
"github.com/go-acme/lego/v4/providers/dns/alidns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/clouddns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/cloudflare"
|
||||
"github.com/go-acme/lego/v4/providers/dns/cloudns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/dnspod"
|
||||
"github.com/go-acme/lego/v4/providers/dns/godaddy"
|
||||
"github.com/go-acme/lego/v4/providers/dns/huaweicloud"
|
||||
@@ -75,6 +77,7 @@ const (
|
||||
Volcengine DnsType = "Volcengine"
|
||||
CloudFlare DnsType = "CloudFlare"
|
||||
CloudDns DnsType = "CloudDns"
|
||||
ClouDNS DnsType = "ClouDNS"
|
||||
FreeMyIP DnsType = "FreeMyIP"
|
||||
NameSilo DnsType = "NameSilo"
|
||||
NameCheap DnsType = "NameCheap"
|
||||
@@ -83,21 +86,26 @@ const (
|
||||
TencentCloud DnsType = "TencentCloud"
|
||||
HuaweiCloud DnsType = "HuaweiCloud"
|
||||
RainYun DnsType = "RainYun"
|
||||
WestCN DnsType = "WestCN"
|
||||
)
|
||||
|
||||
type DNSParam struct {
|
||||
ID string `json:"id"`
|
||||
Token string `json:"token"`
|
||||
AccessKey string `json:"accessKey"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
Email string `json:"email"`
|
||||
APIkey string `json:"apiKey"`
|
||||
APIUser string `json:"apiUser"`
|
||||
APISecret string `json:"apiSecret"`
|
||||
SecretID string `json:"secretID"`
|
||||
Region string `json:"region"`
|
||||
ClientID string `json:"clientID"`
|
||||
Password string `json:"password"`
|
||||
ID string `json:"id"`
|
||||
Token string `json:"token"`
|
||||
AccessKey string `json:"accessKey"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
Email string `json:"email"`
|
||||
APIkey string `json:"apiKey"`
|
||||
APIUser string `json:"apiUser"`
|
||||
APISecret string `json:"apiSecret"`
|
||||
SecretID string `json:"secretID"`
|
||||
Region string `json:"region"`
|
||||
ClientID string `json:"clientID"`
|
||||
Password string `json:"password"`
|
||||
AuthID string `json:"authID"`
|
||||
SubAuthID string `json:"subAuthID"`
|
||||
AuthPassword string `json:"authPassword"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -158,6 +166,15 @@ func (c *AcmeClient) UseDns(dnsType DnsType, params string, websiteSSL model.Web
|
||||
clouddnsConfig.PollingInterval = pollingInterval
|
||||
clouddnsConfig.TTL = ttl
|
||||
p, err = clouddns.NewDNSProviderConfig(clouddnsConfig)
|
||||
case ClouDNS:
|
||||
cloudnsConfig := cloudns.NewDefaultConfig()
|
||||
cloudnsConfig.AuthID = param.AuthID
|
||||
cloudnsConfig.SubAuthID = param.SubAuthID
|
||||
cloudnsConfig.AuthPassword = param.AuthPassword
|
||||
cloudnsConfig.PropagationTimeout = propagationTimeout
|
||||
cloudnsConfig.PollingInterval = pollingInterval
|
||||
cloudnsConfig.TTL = ttl
|
||||
p, err = cloudns.NewDNSProviderConfig(cloudnsConfig)
|
||||
case FreeMyIP:
|
||||
freeMyIpConfig := freemyip.NewDefaultConfig()
|
||||
freeMyIpConfig.Token = param.Token
|
||||
@@ -219,6 +236,14 @@ func (c *AcmeClient) UseDns(dnsType DnsType, params string, websiteSSL model.Web
|
||||
rainyunConfig.PollingInterval = pollingInterval
|
||||
rainyunConfig.TTL = ttl
|
||||
p, err = rainyun.NewDNSProviderConfig(rainyunConfig)
|
||||
case WestCN:
|
||||
westcnConfig := westcn.NewDefaultConfig()
|
||||
westcnConfig.Username = param.Username
|
||||
westcnConfig.Password = param.Password
|
||||
westcnConfig.PropagationTimeout = propagationTimeout
|
||||
westcnConfig.PollingInterval = pollingInterval
|
||||
westcnConfig.TTL = ttl
|
||||
p, err = westcn.NewDNSProviderConfig(westcnConfig)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -0,0 +1,393 @@
|
||||
package systemctl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/backend/constant"
|
||||
"github.com/1Panel-dev/1Panel/backend/global"
|
||||
"go4.org/syncutil/singleflight"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
var (
|
||||
aliasFile string
|
||||
serviceAliases sync.Map
|
||||
saveTimer *time.Timer
|
||||
saveMutex sync.Mutex
|
||||
afterSaveTime = 20 * time.Second
|
||||
)
|
||||
var (
|
||||
ErrServiceNotFound = errors.New("service not found")
|
||||
ErrDiscoveryTimeout = errors.New("service discovery timeout for: %w")
|
||||
ErrServiceDiscovery = errors.New("service discovery failed for: %w")
|
||||
ErrNoValidService = errors.New("no valid service found for: %w")
|
||||
)
|
||||
|
||||
func loadPredefinedAliases() map[string][]string {
|
||||
return map[string][]string{
|
||||
"clam": {"clamav-daemon.service", "clamd@scan.service", "clamd"},
|
||||
"freshclam": {"clamav-freshclam.service", "freshclam.service"},
|
||||
"fail2ban": {"fail2ban.service", "fail2ban"},
|
||||
"supervisor": {"supervisord.service", "supervisor.service", "supervisord", "supervisor"},
|
||||
"ssh": {"sshd.service", "ssh.service", "sshd", "ssh"},
|
||||
"1panel": {"1panel.service", "1paneld"},
|
||||
"docker": {"docker.service", "dockerd"},
|
||||
}
|
||||
}
|
||||
|
||||
func InitializeServiceDiscovery() {
|
||||
svcName := loadAliasesFromConfig()
|
||||
if len(svcName) > 0 {
|
||||
RegisterServiceAliases(svcName)
|
||||
}
|
||||
}
|
||||
|
||||
func RegisterServiceAliases(aliases map[string][]string) {
|
||||
for key, values := range aliases {
|
||||
existing, loaded := serviceAliases.LoadOrStore(key, values)
|
||||
if loaded {
|
||||
merged := append(existing.([]string), values...)
|
||||
serviceAliases.Store(key, merged)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadAliasesFromConfig() map[string][]string {
|
||||
data, err := os.ReadFile(aliasFile)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var rawAliases map[string][]string
|
||||
json.Unmarshal(data, &rawAliases)
|
||||
|
||||
validAliases := make(map[string][]string)
|
||||
for key, aliases := range rawAliases {
|
||||
valid := []string{}
|
||||
for _, alias := range aliases {
|
||||
confirmed, _ := confirmServiceExists(alias)
|
||||
if confirmed {
|
||||
valid = append(valid, alias)
|
||||
}
|
||||
}
|
||||
if len(valid) > 0 {
|
||||
validAliases[key] = valid
|
||||
}
|
||||
}
|
||||
return validAliases
|
||||
}
|
||||
|
||||
func cleanupKeywordAliases(keyword string) {
|
||||
serviceAliases.Range(func(k, v interface{}) bool {
|
||||
if k.(string) != keyword {
|
||||
return true
|
||||
}
|
||||
aliases := v.([]string)
|
||||
valid := make([]string, 0)
|
||||
for _, alias := range aliases {
|
||||
confirmed, _ := confirmServiceExists(alias)
|
||||
if confirmed {
|
||||
valid = append(valid, alias)
|
||||
}
|
||||
}
|
||||
if len(valid) == 0 {
|
||||
serviceAliases.Delete(k)
|
||||
serviceExistenceCache.Delete(k)
|
||||
} else {
|
||||
serviceAliases.Store(k, valid)
|
||||
}
|
||||
return true
|
||||
})
|
||||
go scheduleSave()
|
||||
}
|
||||
|
||||
func smartServiceName(keyword string) (string, error) {
|
||||
mgr := GetGlobalManager()
|
||||
processedName := handleServiceNaming(mgr, keyword)
|
||||
|
||||
confirmed, _ := confirmServiceExists(processedName)
|
||||
if confirmed {
|
||||
updateAliases(keyword, processedName)
|
||||
return processedName, nil
|
||||
}
|
||||
|
||||
candidates := append([]string{processedName}, getAliases(keyword)...)
|
||||
if name, err := validateCandidatesConcurrently(candidates); err == nil {
|
||||
updateAliases(keyword, name)
|
||||
return name, nil
|
||||
}
|
||||
|
||||
discoveredName, err := discoverAndSelectService(keyword)
|
||||
if err != nil {
|
||||
cleanupKeywordAliases(keyword)
|
||||
return "", ErrServiceNotFound
|
||||
}
|
||||
updateAliases(keyword, discoveredName)
|
||||
return discoveredName, nil
|
||||
}
|
||||
func handleServiceNaming(mgr ServiceManager, keyword string) string {
|
||||
keyword = strings.ToLower(keyword)
|
||||
if strings.HasSuffix(keyword, ".service.socket") {
|
||||
keyword = strings.TrimSuffix(keyword, ".service.socket") + ".socket"
|
||||
}
|
||||
if mgr.Name() != "systemd" {
|
||||
keyword = strings.TrimSuffix(keyword, ".service")
|
||||
return keyword
|
||||
}
|
||||
if !strings.HasSuffix(keyword, ".service") &&
|
||||
!strings.HasSuffix(keyword, ".socket") {
|
||||
keyword += ".service"
|
||||
}
|
||||
return keyword
|
||||
}
|
||||
func validateCandidatesConcurrently(candidates []string) (string, error) {
|
||||
var (
|
||||
g errgroup.Group
|
||||
found = make(chan string, 1)
|
||||
)
|
||||
|
||||
for _, candidate := range candidates {
|
||||
cand := candidate
|
||||
g.Go(func() error {
|
||||
confirmed, _ := confirmServiceExists(cand)
|
||||
if confirmed {
|
||||
select {
|
||||
case found <- cand:
|
||||
default:
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return ErrServiceNotFound
|
||||
})
|
||||
}
|
||||
|
||||
resultErr := make(chan error, 1)
|
||||
go func() {
|
||||
defer close(found)
|
||||
resultErr <- g.Wait()
|
||||
}()
|
||||
|
||||
select {
|
||||
case name := <-found:
|
||||
return name, nil
|
||||
case <-time.After(1000 * time.Millisecond):
|
||||
return "", fmt.Errorf(ErrDiscoveryTimeout.Error(), candidates[0])
|
||||
case err := <-resultErr:
|
||||
if err != nil {
|
||||
return "", ErrServiceNotFound
|
||||
}
|
||||
return "", ErrServiceNotFound
|
||||
}
|
||||
}
|
||||
func discoverAndSelectService(keyword string) (string, error) {
|
||||
discovered, err := discoverServices(keyword)
|
||||
if err != nil {
|
||||
return "", ErrServiceNotFound
|
||||
}
|
||||
|
||||
if len(discovered) == 0 {
|
||||
return "", ErrServiceNotFound
|
||||
}
|
||||
selected, err := selectBestMatch(keyword, discovered)
|
||||
if err != nil {
|
||||
return "", ErrServiceNotFound
|
||||
}
|
||||
|
||||
confirmed, err := confirmServiceExists(selected)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("service existence check failed: %w", err)
|
||||
}
|
||||
if confirmed {
|
||||
return selected, nil
|
||||
}
|
||||
return "", ErrServiceNotFound
|
||||
}
|
||||
func selectBestMatch(keyword string, candidates []string) (string, error) {
|
||||
if len(candidates) == 0 {
|
||||
return "", ErrServiceNotFound
|
||||
}
|
||||
lowerKeyword := strings.ToLower(keyword)
|
||||
var exactMatch string
|
||||
var firstContainMatch string
|
||||
for _, name := range candidates {
|
||||
if strings.EqualFold(name, keyword) {
|
||||
exactMatch = name
|
||||
break
|
||||
}
|
||||
}
|
||||
if exactMatch != "" {
|
||||
return exactMatch, nil
|
||||
}
|
||||
for _, name := range candidates {
|
||||
if strings.Contains(strings.ToLower(name), lowerKeyword) {
|
||||
firstContainMatch = name
|
||||
global.LOG.Debugf("[%s] [keyword: %s] Found first contain match: %s", getManagerName(), keyword, firstContainMatch)
|
||||
break
|
||||
}
|
||||
}
|
||||
if firstContainMatch != "" {
|
||||
return firstContainMatch, nil
|
||||
}
|
||||
return "", fmt.Errorf("%w: %q (no exact or partial match)", ErrNoValidService, keyword)
|
||||
}
|
||||
|
||||
type cacheItem struct {
|
||||
services []string
|
||||
expires time.Time
|
||||
exists bool
|
||||
}
|
||||
|
||||
var (
|
||||
discoveryCache sync.Map
|
||||
discoveryGroup singleflight.Group
|
||||
)
|
||||
|
||||
func discoverServices(keyword string) ([]string, error) {
|
||||
result, err := discoveryGroup.Do(keyword, func() (interface{}, error) {
|
||||
if cached, ok := discoveryCache.Load(keyword); ok {
|
||||
item := cached.(cacheItem)
|
||||
if time.Now().Before(item.expires) {
|
||||
return item.services, nil
|
||||
}
|
||||
discoveryCache.Delete(keyword)
|
||||
}
|
||||
manager := GetGlobalManager()
|
||||
results, err := manager.FindServices(keyword)
|
||||
|
||||
if err != nil {
|
||||
global.LOG.Errorf("Find services failed for %s: %v", keyword, err)
|
||||
return nil, fmt.Errorf("%w: %q (%v)", ErrServiceDiscovery, keyword, err)
|
||||
} else {
|
||||
discoveryCache.Store(keyword, cacheItem{
|
||||
services: results,
|
||||
expires: time.Now().Add(5 * time.Minute),
|
||||
})
|
||||
}
|
||||
return results, err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.([]string), nil
|
||||
}
|
||||
|
||||
func updateAliases(keyword, alias string) {
|
||||
if keyword == alias {
|
||||
return
|
||||
}
|
||||
existing, _ := serviceAliases.LoadOrStore(keyword, []string{})
|
||||
aliases := existing.([]string)
|
||||
if contains(aliases, alias) {
|
||||
return
|
||||
}
|
||||
serviceAliases.Store(keyword, append(aliases, alias))
|
||||
go scheduleSave()
|
||||
}
|
||||
|
||||
func scheduleSave() {
|
||||
saveMutex.Lock()
|
||||
defer saveMutex.Unlock()
|
||||
|
||||
if saveTimer != nil {
|
||||
saveTimer.Stop()
|
||||
}
|
||||
|
||||
dataSnapshot := make(map[string][]string)
|
||||
serviceAliases.Range(func(k, v interface{}) bool {
|
||||
dataSnapshot[k.(string)] = append([]string{}, v.([]string)...)
|
||||
return true
|
||||
})
|
||||
aliasFile = filepath.Join(constant.ResourceDir, "svcaliases.json")
|
||||
saveTimer = time.AfterFunc(afterSaveTime, func() {
|
||||
tmpFile := aliasFile + ".tmp"
|
||||
if err := saveAliasesToFile(dataSnapshot, tmpFile); err == nil {
|
||||
os.Rename(tmpFile, aliasFile)
|
||||
}
|
||||
})
|
||||
}
|
||||
func saveAliasesToFile(data map[string][]string, path string) error {
|
||||
fileData, err := json.MarshalIndent(data, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("serialization failed: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, fileData, 0644); err != nil {
|
||||
return fmt.Errorf("file write failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func contains(slice []string, item string) bool {
|
||||
for _, s := range slice {
|
||||
if s == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var serviceExistenceCache sync.Map
|
||||
|
||||
func confirmServiceExists(serviceName string) (bool, error) {
|
||||
if val, ok := serviceExistenceCache.Load(serviceName); ok {
|
||||
if item, ok := val.(cacheItem); ok && time.Now().Before(item.expires) {
|
||||
return item.exists, nil
|
||||
}
|
||||
serviceExistenceCache.Delete(serviceName)
|
||||
}
|
||||
handler := NewServiceHandler(defaultServiceConfig(serviceName))
|
||||
isExist, err := handler.IsExists()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("check service existence failed: %w", err)
|
||||
}
|
||||
serviceExistenceCache.Store(serviceName, cacheItem{
|
||||
exists: isExist.IsExists,
|
||||
expires: time.Now().Add(30 * time.Second),
|
||||
})
|
||||
return isExist.IsExists, nil
|
||||
}
|
||||
|
||||
func getAliases(keyword string) []string {
|
||||
predefined := loadPredefinedAliases()[keyword]
|
||||
runtimeAliases, _ := serviceAliases.LoadOrStore(keyword, []string{})
|
||||
merged := make(map[string]struct{})
|
||||
for _, alias := range predefined {
|
||||
merged[alias] = struct{}{}
|
||||
}
|
||||
for _, alias := range runtimeAliases.([]string) {
|
||||
merged[alias] = struct{}{}
|
||||
}
|
||||
result := make([]string, 0, len(merged))
|
||||
for k := range merged {
|
||||
result = append(result, k)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
type ConfigOption struct {
|
||||
TailLines string
|
||||
}
|
||||
|
||||
func ViewConfig(path string, opt ConfigOption) (string, error) {
|
||||
var cmd []string
|
||||
if opt.TailLines != "" && opt.TailLines != "0" {
|
||||
cmd = []string{"tail", "-n", opt.TailLines, path}
|
||||
} else {
|
||||
cmd = []string{"cat", path}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
output, err := executeCommand(ctx, cmd[0], cmd[1:]...)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("view config failed: %w", err)
|
||||
}
|
||||
return string(output), nil
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
package systemctl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/backend/global"
|
||||
)
|
||||
|
||||
type ServiceConfig struct {
|
||||
ServiceName map[string]string
|
||||
}
|
||||
|
||||
type ServiceHandler struct {
|
||||
config *ServiceConfig
|
||||
manager ServiceManager
|
||||
}
|
||||
|
||||
func NewServiceHandler(serviceNames map[string]string) *ServiceHandler {
|
||||
mgr := GetGlobalManager()
|
||||
if mgr == nil {
|
||||
global.LOG.Error("failed to get global service manager when creating ServiceHandler")
|
||||
return nil
|
||||
}
|
||||
return &ServiceHandler{
|
||||
config: &ServiceConfig{
|
||||
ServiceName: serviceNames,
|
||||
},
|
||||
manager: mgr,
|
||||
}
|
||||
}
|
||||
|
||||
type ServiceStatus struct {
|
||||
IsActive bool `json:"isActive"`
|
||||
IsEnabled bool `json:"isEnabled"`
|
||||
IsExists bool `json:"isExists"`
|
||||
Output string `json:"output"`
|
||||
}
|
||||
type ServiceIsActive struct {
|
||||
IsActive bool `json:"isActive"`
|
||||
Output string `json:"output"`
|
||||
}
|
||||
|
||||
type ServiceIsEnabled struct {
|
||||
IsEnabled bool `json:"isEnabled"`
|
||||
Output string `json:"output"`
|
||||
}
|
||||
|
||||
type ServiceResult struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Output string `json:"output"`
|
||||
}
|
||||
|
||||
var (
|
||||
BinaryPath = "/usr/local/bin" // 1panl service default path
|
||||
ErrServiceNotExist = errors.New("service does not exist")
|
||||
)
|
||||
|
||||
func defaultServiceConfig(serviceName string) map[string]string {
|
||||
mgr := getManagerName()
|
||||
if mgr == "" {
|
||||
global.LOG.Error("failed to get manager name for default service config")
|
||||
return nil
|
||||
}
|
||||
return map[string]string{
|
||||
mgr: serviceName,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ServiceHandler) ManagerName() string { return h.manager.Name() }
|
||||
func getManagerName() string {
|
||||
if mgr := GetGlobalManager(); mgr != nil {
|
||||
return mgr.Name()
|
||||
}
|
||||
global.LOG.Error("failed to get global service manager")
|
||||
return ""
|
||||
}
|
||||
|
||||
func (h *ServiceHandler) GetServiceName() string {
|
||||
manager := h.ManagerName()
|
||||
if manager == "" {
|
||||
global.LOG.Error("manager name is empty when getting service name")
|
||||
return ""
|
||||
}
|
||||
return h.config.ServiceName[manager]
|
||||
}
|
||||
|
||||
func (h *ServiceHandler) GetServicePath() (string, error) {
|
||||
manager := h.ManagerName()
|
||||
serviceName := h.config.ServiceName[manager]
|
||||
|
||||
if serviceName == "" {
|
||||
err := fmt.Errorf("service name not found for %s", manager)
|
||||
global.LOG.Errorf("GetServicePath error: %v", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
cleanPath := filepath.Clean(serviceName)
|
||||
if strings.Contains(cleanPath, "..") {
|
||||
err := fmt.Errorf("invalid path: %q", cleanPath)
|
||||
global.LOG.Errorf("GetServicePath security check failed: %v", err)
|
||||
return "", err
|
||||
}
|
||||
switch manager {
|
||||
case "systemd":
|
||||
return findSystemdPath(cleanPath)
|
||||
case "openrc", "sysvinit":
|
||||
return checkInitDPath(cleanPath)
|
||||
default:
|
||||
err := fmt.Errorf("unsupported init system: %s", manager)
|
||||
global.LOG.Errorf("GetServicePath error: %v", err)
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
func findSystemdPath(name string) (string, error) {
|
||||
paths := []string{"/etc/systemd/system", "/usr/lib/systemd/system",
|
||||
"/usr/share/systemd/system", "/usr/local/lib/systemd/system"}
|
||||
|
||||
for _, p := range paths {
|
||||
if path := filepath.Join(p, name); FileExist(path) {
|
||||
return path, nil
|
||||
}
|
||||
}
|
||||
err := fmt.Errorf("service path not found for %s", name)
|
||||
global.LOG.Errorf("findSystemdPath error: %v", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
func checkInitDPath(name string) (string, error) {
|
||||
path := filepath.Join("/etc/init.d", name)
|
||||
if !FileExist(path) {
|
||||
err := fmt.Errorf("service path not found for %s", name)
|
||||
global.LOG.Errorf("checkInitDPath error: %v", err)
|
||||
return "", err
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func (h *ServiceHandler) ExecuteAction(action string) (ServiceResult, error) {
|
||||
successMsg := fmt.Sprintf("%s : %s completed", action, h.GetServiceName())
|
||||
return h.executeAction(action, successMsg)
|
||||
}
|
||||
|
||||
func (h *ServiceHandler) IsExists() (ServiceStatus, error) {
|
||||
manager := GetGlobalManager()
|
||||
isExist, _ := manager.ServiceExists(h.config)
|
||||
return ServiceStatus{
|
||||
IsExists: isExist,
|
||||
}, nil
|
||||
}
|
||||
func (h *ServiceHandler) IsActive() (ServiceStatus, error) {
|
||||
manager := GetGlobalManager()
|
||||
if manager == nil {
|
||||
global.LOG.Error("service manager not initialized during active check")
|
||||
return ServiceStatus{}, fmt.Errorf("service manager not initialized")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
activeCmd, err := manager.BuildCommand("status", h.config)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("Build status command failed: %v", err)
|
||||
return ServiceStatus{}, fmt.Errorf("build status command failed: %w", err)
|
||||
}
|
||||
|
||||
output, err := executeCommand(ctx, activeCmd[0], activeCmd[1:]...)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "inactive") {
|
||||
return ServiceStatus{
|
||||
IsExists: false,
|
||||
}, nil
|
||||
}
|
||||
global.LOG.Errorf("Active check execution failed: %v", err)
|
||||
return ServiceStatus{}, fmt.Errorf("status check failed: %w", err)
|
||||
}
|
||||
|
||||
isActive, err := manager.ParseStatus(string(output), h.config, "active")
|
||||
if err != nil {
|
||||
global.LOG.Warnf("Status parse error: %v", err)
|
||||
}
|
||||
return ServiceStatus{
|
||||
IsActive: isActive,
|
||||
Output: string(output),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *ServiceHandler) IsEnabled() (ServiceStatus, error) {
|
||||
manager := GetGlobalManager()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
enabledCmd, err := manager.BuildCommand("is-enabled", h.config)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
return ServiceStatus{
|
||||
IsEnabled: false,
|
||||
}, nil
|
||||
}
|
||||
global.LOG.Errorf("Build is-enabled command failed: %v", err)
|
||||
return ServiceStatus{}, fmt.Errorf("build enabled check command failed: %w", err)
|
||||
}
|
||||
|
||||
output, err := executeCommand(ctx, enabledCmd[0], enabledCmd[1:]...)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "disabled") {
|
||||
return ServiceStatus{
|
||||
IsEnabled: false,
|
||||
}, nil
|
||||
}
|
||||
// // isEnabled, err := h.ParseStatus(string(output), h.config, "enabled")
|
||||
// global.LOG.Errorf("Enabled check execution failed: %v", err)
|
||||
// return ServiceStatus{}, fmt.Errorf("enabled check failed: %w", err)
|
||||
}
|
||||
|
||||
isEnabled, err := manager.ParseStatus(string(output), h.config, "enabled")
|
||||
if err != nil {
|
||||
global.LOG.Warnf("Enabled status parse error: %v", err)
|
||||
}
|
||||
return ServiceStatus{
|
||||
IsEnabled: isEnabled,
|
||||
Output: string(output),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *ServiceHandler) StartService() (ServiceResult, error) {
|
||||
return h.ExecuteAction("start")
|
||||
}
|
||||
|
||||
func (h *ServiceHandler) StopService() (ServiceResult, error) {
|
||||
return h.ExecuteAction("stop")
|
||||
}
|
||||
|
||||
func (h *ServiceHandler) RestartService() (ServiceResult, error) {
|
||||
return h.ExecuteAction("restart")
|
||||
}
|
||||
|
||||
func (h *ServiceHandler) EnableService() (ServiceResult, error) {
|
||||
return h.ExecuteAction("enable")
|
||||
}
|
||||
|
||||
func (h *ServiceHandler) DisableService() (ServiceResult, error) {
|
||||
return h.ExecuteAction("disable")
|
||||
}
|
||||
|
||||
func (h *ServiceHandler) executeAction(action, successMsg string) (ServiceResult, error) {
|
||||
manager := GetGlobalManager()
|
||||
if manager == nil {
|
||||
global.LOG.Error("service manager not initialized during action execution")
|
||||
return ServiceResult{}, fmt.Errorf("service manager not initialized")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultCommandTimeout)
|
||||
defer cancel()
|
||||
|
||||
cmdArgs, err := manager.BuildCommand(action, h.config)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("Build command failed for action %s: %v", action, err)
|
||||
return ServiceResult{}, fmt.Errorf("build command failed: %w", err)
|
||||
}
|
||||
|
||||
output, err := executeCommand(ctx, cmdArgs[0], cmdArgs[1:]...)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("%s operation failed: %v", action, err)
|
||||
return ServiceResult{
|
||||
Success: false,
|
||||
Message: fmt.Sprintf("%s failed", action),
|
||||
Output: string(output),
|
||||
}, fmt.Errorf("%s operation failed: %w", action, err)
|
||||
}
|
||||
|
||||
global.LOG.Infof("[%s]: %s", manager.Name(), successMsg)
|
||||
return ServiceResult{
|
||||
Success: true,
|
||||
Message: successMsg,
|
||||
Output: string(output),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
package systemctl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/backend/global"
|
||||
)
|
||||
|
||||
var (
|
||||
managers = make(map[string]ServiceManager)
|
||||
mu sync.RWMutex
|
||||
globalManager ServiceManager
|
||||
managerPriority = []string{"systemd", "openrc", "sysvinit"}
|
||||
)
|
||||
|
||||
const (
|
||||
defaultCommandTimeout = 60 * time.Second
|
||||
serviceCheckTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
type ServiceManager interface {
|
||||
Name() string
|
||||
IsAvailable() bool
|
||||
ServiceExists(*ServiceConfig) (bool, error)
|
||||
BuildCommand(string, *ServiceConfig) ([]string, error)
|
||||
ParseStatus(string, *ServiceConfig, string) (bool, error)
|
||||
FindServices(string) ([]string, error)
|
||||
}
|
||||
|
||||
type baseManager struct {
|
||||
name string
|
||||
cmdTool string
|
||||
activeRegex *regexp.Regexp
|
||||
enabledRegex *regexp.Regexp
|
||||
}
|
||||
|
||||
func (b *baseManager) Name() string { return b.name }
|
||||
|
||||
func isRootUser() bool {
|
||||
return os.Geteuid() == 0
|
||||
}
|
||||
func (b *baseManager) buildBaseCommand() []string {
|
||||
var cmdArgs []string
|
||||
if !isRootUser() {
|
||||
cmdArgs = append(cmdArgs, "sudo")
|
||||
}
|
||||
cmdArgs = append(cmdArgs, b.cmdTool)
|
||||
return cmdArgs
|
||||
}
|
||||
func (b *baseManager) commonServiceExists(config *ServiceConfig, checkFn func(string) (bool, error)) (bool, error) {
|
||||
if name := config.ServiceName[b.name]; name != "" {
|
||||
exists, checkErr := checkFn(name)
|
||||
if checkErr != nil {
|
||||
return false, nil
|
||||
}
|
||||
return exists, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
func (b *baseManager) ParseStatus(output string, _ *ServiceConfig, statusType string) (bool, error) {
|
||||
if output == "" {
|
||||
return false, nil
|
||||
}
|
||||
switch statusType {
|
||||
case "active":
|
||||
if b.activeRegex == nil {
|
||||
return false, nil
|
||||
}
|
||||
return b.activeRegex.MatchString(output), nil
|
||||
case "enabled":
|
||||
if b.enabledRegex == nil {
|
||||
return false, nil
|
||||
}
|
||||
return b.enabledRegex.MatchString(output), nil
|
||||
default:
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
func registerManager(m ServiceManager) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
managers[m.Name()] = m
|
||||
}
|
||||
func init() {
|
||||
for _, mgr := range []ServiceManager{
|
||||
newSystemdManager(),
|
||||
newOpenrcManager(),
|
||||
newSysvinitManager(),
|
||||
} {
|
||||
registerManager(mgr)
|
||||
}
|
||||
}
|
||||
func InitializeGlobalManager() (err error) {
|
||||
|
||||
for _, name := range managerPriority {
|
||||
if mgr, ok := managers[name]; ok && mgr.IsAvailable() {
|
||||
if testManager(mgr) {
|
||||
globalManager = mgr
|
||||
global.LOG.Infof("Initialized service manager: %s", name)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
err = fmt.Errorf("no available service manager found (tried: %v)", managerPriority)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func testManager(mgr ServiceManager) bool {
|
||||
_, err := mgr.BuildCommand("status", &ServiceConfig{
|
||||
ServiceName: map[string]string{mgr.Name(): "test-service"},
|
||||
})
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func GetGlobalManager() ServiceManager {
|
||||
if globalManager == nil {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if globalManager == nil {
|
||||
return initializeWithRetry()
|
||||
}
|
||||
}
|
||||
return globalManager
|
||||
}
|
||||
func initializeWithRetry() ServiceManager {
|
||||
const (
|
||||
maxRetries = 5
|
||||
initialWait = 1 * time.Second
|
||||
)
|
||||
backoff := initialWait
|
||||
for attempt := 1; attempt <= maxRetries; attempt++ {
|
||||
if err := InitializeGlobalManager(); err == nil {
|
||||
return globalManager
|
||||
}
|
||||
|
||||
logMessage := fmt.Sprintf("Manager init attempt %d/%d failed", attempt, maxRetries)
|
||||
if global.LOG != nil {
|
||||
global.LOG.Warn(logMessage)
|
||||
} else {
|
||||
fmt.Printf("[WARN] %s\n", logMessage)
|
||||
}
|
||||
|
||||
if attempt < maxRetries {
|
||||
time.Sleep(backoff)
|
||||
backoff *= 2
|
||||
}
|
||||
}
|
||||
|
||||
if global.LOG != nil {
|
||||
global.LOG.Error("All manager initialization attempts failed")
|
||||
} else {
|
||||
fmt.Println("[FATAL] All manager initialization attempts failed")
|
||||
}
|
||||
panic("unable to initialize service manager")
|
||||
}
|
||||
func executeCommand(ctx context.Context, command string, args ...string) ([]byte, error) {
|
||||
if _, ok := ctx.Deadline(); !ok {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(ctx, defaultCommandTimeout)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, command, args...)
|
||||
var buf bytes.Buffer
|
||||
cmd.Stdout = &buf
|
||||
cmd.Stderr = &buf
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return buf.Bytes(), &CommandError{
|
||||
Cmd: cmd.String(),
|
||||
Output: buf.String(),
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
type systemdManager struct{ baseManager }
|
||||
|
||||
func newSystemdManager() ServiceManager {
|
||||
return &systemdManager{baseManager{
|
||||
name: "systemd",
|
||||
cmdTool: "systemctl",
|
||||
activeRegex: regexp.MustCompile(`(?i)Active:\s+active\b`),
|
||||
enabledRegex: regexp.MustCompile(`(?i)^\s*enabled\s*$`),
|
||||
}}
|
||||
}
|
||||
|
||||
func (m *systemdManager) IsAvailable() bool {
|
||||
_, err := exec.LookPath(m.cmdTool)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (m *systemdManager) ServiceExists(config *ServiceConfig) (bool, error) {
|
||||
return m.commonServiceExists(config, func(name string) (bool, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), serviceCheckTimeout)
|
||||
defer cancel()
|
||||
out, cmdErr := executeCommand(ctx, m.cmdTool, "is-enabled", name)
|
||||
if cmdErr == nil || strings.Contains(string(out), "disabled") {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (m *systemdManager) BuildCommand(action string, config *ServiceConfig) ([]string, error) {
|
||||
cmdArgs := m.buildBaseCommand()
|
||||
service := config.ServiceName[m.name]
|
||||
switch action {
|
||||
case "is-enabled":
|
||||
cmdArgs = append(cmdArgs, "is-enabled", service)
|
||||
default:
|
||||
cmdArgs = append(cmdArgs, action, service)
|
||||
}
|
||||
return cmdArgs, nil
|
||||
}
|
||||
|
||||
func (m *systemdManager) ParseStatus(output string, config *ServiceConfig, statusType string) (bool, error) {
|
||||
if strings.Contains(output, "could not be found") {
|
||||
return false, nil
|
||||
}
|
||||
result, err := m.baseManager.ParseStatus(output, config, statusType)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
func (m *systemdManager) FindServices(keyword string) ([]string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
out, err := executeCommand(ctx, m.cmdTool, "list-unit-files", "--type=service", "--no-legend")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list systemd services: %w", err)
|
||||
}
|
||||
|
||||
var services []string
|
||||
lines := strings.Split(string(out), "\n")
|
||||
for _, line := range lines {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 1 {
|
||||
continue
|
||||
}
|
||||
serviceName := fields[0]
|
||||
if strings.Contains(serviceName, keyword) {
|
||||
services = append(services, serviceName)
|
||||
}
|
||||
}
|
||||
return services, nil
|
||||
}
|
||||
|
||||
type sysvinitManager struct{ baseManager }
|
||||
|
||||
func newSysvinitManager() ServiceManager {
|
||||
return &sysvinitManager{baseManager{
|
||||
name: "sysvinit",
|
||||
cmdTool: "service",
|
||||
activeRegex: regexp.MustCompile(`(?i)(?:^|\s)\b(running|active)\b(?:$|\s)`),
|
||||
enabledRegex: regexp.MustCompile(`(?i)(?:^|\s)\b(enabled)\b(?:$|\s)`),
|
||||
}}
|
||||
}
|
||||
|
||||
func (m *sysvinitManager) IsAvailable() bool {
|
||||
_, err := exec.LookPath(m.cmdTool)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (m *sysvinitManager) ServiceExists(config *ServiceConfig) (bool, error) {
|
||||
return m.commonServiceExists(config, func(name string) (bool, error) {
|
||||
_, err := os.Stat(filepath.Join("/etc/init.d", name))
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
} else if err != nil {
|
||||
return false, fmt.Errorf("stat /etc/init.d/%s failed: %w", name, err)
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (m *sysvinitManager) BuildCommand(action string, config *ServiceConfig) ([]string, error) {
|
||||
service := config.ServiceName[m.name]
|
||||
switch action {
|
||||
case "is-enabled":
|
||||
return []string{
|
||||
"sh",
|
||||
"-c",
|
||||
fmt.Sprintf("if ls /etc/rc*.d/S*%s >/dev/null 2>&1; then echo 'enabled'; else echo 'disabled'; fi", service)}, nil
|
||||
case "is-active", "status":
|
||||
return []string{
|
||||
"sh",
|
||||
"-c",
|
||||
fmt.Sprintf("if service %s status >/dev/null 2>&1; then echo 'active'; else echo 'inactive'; fi", service),
|
||||
}, nil
|
||||
default:
|
||||
cmdArgs := m.buildBaseCommand()
|
||||
cmdArgs = append(cmdArgs, service, action)
|
||||
return cmdArgs, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m *sysvinitManager) ParseStatus(output string, config *ServiceConfig, statusType string) (bool, error) {
|
||||
serviceName := config.ServiceName[m.name]
|
||||
switch statusType {
|
||||
case "enabled":
|
||||
if strings.Contains(output, "no such file or directory") {
|
||||
return false, nil
|
||||
}
|
||||
return strings.TrimSpace(output) != "", nil
|
||||
case "active":
|
||||
if strings.Contains(output, "not found") {
|
||||
return false, nil
|
||||
}
|
||||
if strings.Contains(output, "running") || strings.Contains(output, "active") {
|
||||
return true, nil
|
||||
}
|
||||
default:
|
||||
result, err := m.baseManager.ParseStatus(output, config, statusType)
|
||||
if err != nil {
|
||||
global.LOG.Debugf("[sysvinit] Status parse failed. [ServiceName:%s] Type: %s, Output: %q", serviceName, statusType, output)
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
return false, fmt.Errorf("unsupported status type: %s", statusType)
|
||||
}
|
||||
|
||||
func (m *sysvinitManager) FindServices(keyword string) ([]string, error) {
|
||||
files, err := os.ReadDir("/etc/init.d/")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read init.d directory: %w", err)
|
||||
}
|
||||
|
||||
var services []string
|
||||
for _, file := range files {
|
||||
if strings.Contains(file.Name(), keyword) {
|
||||
services = append(services, file.Name())
|
||||
}
|
||||
}
|
||||
return services, nil
|
||||
}
|
||||
|
||||
type openrcManager struct{ baseManager }
|
||||
|
||||
func newOpenrcManager() ServiceManager {
|
||||
return &openrcManager{baseManager{
|
||||
name: "openrc",
|
||||
cmdTool: "rc-service",
|
||||
// activeRegex: regexp.MustCompile(`(?i)^\s*status:\s+(started|running|active)\s*$`),
|
||||
// enabledRegex: regexp.MustCompile(`(?i)^[^\|]+\|\s*(default|enabled)\b.*$`),
|
||||
activeRegex: regexp.MustCompile(`(?i)(?:^|\s)\b(running|active)\b(?:$|\s)`),
|
||||
enabledRegex: regexp.MustCompile(`(?i)(?:^|\s)\b(enabled)\b(?:$|\s)`),
|
||||
}}
|
||||
}
|
||||
func (m *openrcManager) IsAvailable() bool {
|
||||
_, err := exec.LookPath(m.cmdTool)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (m *openrcManager) ServiceExists(config *ServiceConfig) (bool, error) {
|
||||
return m.commonServiceExists(config, func(name string) (bool, error) {
|
||||
_, err := os.Stat(filepath.Join("/etc/init.d", name))
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
} else if err != nil {
|
||||
return false, fmt.Errorf("stat /etc/init.d/%s failed: %w", name, err)
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (m *openrcManager) BuildCommand(action string, config *ServiceConfig) ([]string, error) {
|
||||
cmdArgs := m.buildBaseCommand()
|
||||
service := config.ServiceName[m.name]
|
||||
switch action {
|
||||
case "is-enabled":
|
||||
return []string{
|
||||
"sh",
|
||||
"-c",
|
||||
fmt.Sprintf("if ls /etc/runlevels/default/%s >/dev/null 2>&1; then echo 'enabled'; else echo 'disabled'; fi", service)}, nil
|
||||
case "is-active", "status":
|
||||
return []string{
|
||||
"sh",
|
||||
"-c",
|
||||
fmt.Sprintf("if service %s status >/dev/null 2>&1; then echo 'active'; else echo 'inactive'; fi", service),
|
||||
}, nil
|
||||
case "enable":
|
||||
return []string{"rc-update", "add", service, "default"}, nil
|
||||
case "disable":
|
||||
return []string{"rc-update", "del", service, "default"}, nil
|
||||
default:
|
||||
cmdArgs = append(cmdArgs, service, action)
|
||||
return cmdArgs, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m *openrcManager) ParseStatus(output string, config *ServiceConfig, statusType string) (bool, error) {
|
||||
if strings.Contains(output, "does not exist") {
|
||||
return false, nil
|
||||
}
|
||||
result, err := m.baseManager.ParseStatus(output, config, statusType)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
func (m *openrcManager) FindServices(keyword string) ([]string, error) {
|
||||
files, err := os.ReadDir("/etc/init.d/")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read init.d directory: %w", err)
|
||||
}
|
||||
keyword = strings.ToLower(keyword)
|
||||
var services []string
|
||||
for _, file := range files {
|
||||
if strings.Contains(file.Name(), keyword) {
|
||||
services = append(services, file.Name())
|
||||
}
|
||||
}
|
||||
return services, nil
|
||||
}
|
||||
|
||||
type CommandError struct {
|
||||
Cmd string
|
||||
Err error
|
||||
Output string
|
||||
}
|
||||
|
||||
func (e CommandError) Error() string {
|
||||
return fmt.Sprintf("command %q failed: %v \nOutput: %s",
|
||||
e.Cmd, e.Err, e.Output)
|
||||
}
|
||||
@@ -2,63 +2,153 @@ package systemctl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/pkg/errors"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"os"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/backend/global"
|
||||
)
|
||||
|
||||
func RunSystemCtl(args ...string) (string, error) {
|
||||
cmd := exec.Command("systemctl", args...)
|
||||
output, err := cmd.CombinedOutput()
|
||||
func DefaultHandler(serviceName string) (*ServiceHandler, error) {
|
||||
svcName, err := smartServiceName(serviceName)
|
||||
if err != nil {
|
||||
return string(output), fmt.Errorf("failed to run command: %w", err)
|
||||
return nil, ErrServiceNotFound
|
||||
}
|
||||
return string(output), nil
|
||||
return NewServiceHandler(defaultServiceConfig(svcName)), nil
|
||||
}
|
||||
|
||||
func IsActive(serviceName string) (bool, error) {
|
||||
out, err := RunSystemCtl("is-active", serviceName)
|
||||
func GetServiceName(serviceName string) (string, error) {
|
||||
serviceName, err := smartServiceName(serviceName)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return "", ErrServiceNotFound
|
||||
}
|
||||
return out == "active\n", nil
|
||||
return serviceName, nil
|
||||
}
|
||||
|
||||
func IsEnable(serviceName string) (bool, error) {
|
||||
out, err := RunSystemCtl("is-enabled", serviceName)
|
||||
func GetServicePath(serviceName string) (string, error) {
|
||||
handler, err := DefaultHandler(serviceName)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return "", ErrServiceNotFound
|
||||
}
|
||||
return out == "enabled\n", nil
|
||||
return handler.GetServicePath()
|
||||
}
|
||||
|
||||
func CustomAction(action string, serviceName string) (ServiceResult, error) {
|
||||
handler, err := DefaultHandler(serviceName)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("CustomAction handler init failed: %v", err)
|
||||
return ServiceResult{}, ErrServiceNotFound
|
||||
}
|
||||
result, err := handler.ExecuteAction(action)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("CustomAction %s failed: %v", action, err)
|
||||
return result, fmt.Errorf("%s operation failed: %w | Output: %s", action, err, result.Output)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func IsExist(serviceName string) (bool, error) {
|
||||
out, err := RunSystemCtl("is-enabled", serviceName)
|
||||
handler, err := DefaultHandler(serviceName)
|
||||
if err != nil {
|
||||
if strings.Contains(out, "disabled") {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
result, _ := handler.IsExists()
|
||||
if result.IsExists {
|
||||
return true, nil
|
||||
} else {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
func handlerErr(out string, err error) error {
|
||||
func Start(serviceName string) error {
|
||||
handler, _ := DefaultHandler(serviceName)
|
||||
result, err := handler.StartService()
|
||||
if err != nil {
|
||||
if out != "" {
|
||||
return errors.New(out)
|
||||
}
|
||||
return err
|
||||
global.LOG.Errorf("Service start failed: %v | Output: %s", err, result.Output)
|
||||
return fmt.Errorf("start failed: %v | Output: %s", err, result.Output)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Stop(serviceName string) error {
|
||||
handler, err := DefaultHandler(serviceName)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("Stop handler init failed: %v", err)
|
||||
return fmt.Errorf("%s is not exist", serviceName)
|
||||
}
|
||||
result, err := handler.StopService()
|
||||
if err != nil {
|
||||
global.LOG.Errorf("Service stop failed: %v", err)
|
||||
return fmt.Errorf("stop failed: %v | Output: %s", err, result.Output)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Restart(serviceName string) error {
|
||||
out, err := RunSystemCtl("restart", serviceName)
|
||||
return handlerErr(out, err)
|
||||
handler, err := DefaultHandler(serviceName)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("Restart handler init failed: %v", err)
|
||||
return fmt.Errorf("%s is not exist", serviceName)
|
||||
}
|
||||
result, err := handler.RestartService()
|
||||
if err != nil {
|
||||
global.LOG.Errorf("Service restart failed: %v", err)
|
||||
return fmt.Errorf("restart failed: %v | Output: %s", err, result.Output)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Operate(operate, serviceName string) error {
|
||||
out, err := RunSystemCtl(operate, serviceName)
|
||||
return handlerErr(out, err)
|
||||
func Enable(serviceName string) error {
|
||||
handler, err := DefaultHandler(serviceName)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("Enable handler init failed: %v", err)
|
||||
return fmt.Errorf("%s is not exist", serviceName)
|
||||
}
|
||||
result, err := handler.EnableService()
|
||||
if err != nil {
|
||||
global.LOG.Errorf("Service enable failed: %v | Output: %s", err, result.Output)
|
||||
return fmt.Errorf("%s enable failed: %v ", serviceName, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Disable(serviceName string) error {
|
||||
handler, _ := DefaultHandler(serviceName)
|
||||
result, err := handler.DisableService()
|
||||
if err != nil {
|
||||
global.LOG.Errorf("Service disable failed: %v", err)
|
||||
return fmt.Errorf("disable failed: %v | Output: %s", err, result.Output)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func IsActive(serviceName string) (bool, error) {
|
||||
handler, err := DefaultHandler(serviceName)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
status, err := handler.IsActive()
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
return status.IsActive, nil
|
||||
}
|
||||
|
||||
func IsEnable(serviceName string) (bool, error) {
|
||||
handler, err := DefaultHandler(serviceName)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
status, err := handler.IsEnabled()
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
return status.IsEnabled, nil
|
||||
}
|
||||
|
||||
type LogOption struct {
|
||||
TailLines string
|
||||
}
|
||||
|
||||
func FileExist(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@ type LocalCommand struct {
|
||||
pty *os.File
|
||||
}
|
||||
|
||||
func NewCommand(initCmd string) (*LocalCommand, error) {
|
||||
cmd := exec.Command("bash")
|
||||
func NewCommand(initCmd []string) (*LocalCommand, error) {
|
||||
cmd := exec.Command("docker", initCmd...)
|
||||
if term := os.Getenv("TERM"); term != "" {
|
||||
cmd.Env = append(os.Environ(), "TERM="+term)
|
||||
} else {
|
||||
@@ -38,11 +38,6 @@ func NewCommand(initCmd string) (*LocalCommand, error) {
|
||||
return nil, errors.Wrapf(err, "failed to start command")
|
||||
}
|
||||
|
||||
if len(initCmd) != 0 {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
_, _ = pty.Write([]byte(initCmd + "\n"))
|
||||
}
|
||||
|
||||
lcmd := &LocalCommand{
|
||||
closeSignal: DefaultCloseSignal,
|
||||
closeTimeout: DefaultCloseTimeout,
|
||||
@@ -100,4 +95,5 @@ func (lcmd *LocalCommand) Wait(quitChan chan bool) {
|
||||
global.LOG.Errorf("ssh session wait failed, err: %v", err)
|
||||
setQuit(quitChan)
|
||||
}
|
||||
setQuit(quitChan)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package toolbox
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/backend/global"
|
||||
@@ -22,15 +23,15 @@ type FirewallClient interface {
|
||||
}
|
||||
|
||||
func NewFail2Ban() (*Fail2ban, error) {
|
||||
isExist, _ := systemctl.IsExist("fail2ban.service")
|
||||
isExist, _ := systemctl.IsExist("fail2ban")
|
||||
if isExist {
|
||||
if _, err := os.Stat(defaultPath); err != nil {
|
||||
if err := initLocalFile(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stdout, err := cmd.Exec("systemctl restart fail2ban.service")
|
||||
err := systemctl.Restart("fail2ban")
|
||||
if err != nil {
|
||||
global.LOG.Errorf("restart fail2ban failed, err: %s", stdout)
|
||||
global.LOG.Errorf("restart fail2ban failed, err: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -47,20 +48,25 @@ func (f *Fail2ban) Status() (bool, bool, bool) {
|
||||
}
|
||||
|
||||
func (f *Fail2ban) Version() string {
|
||||
stdout, err := cmd.Exec("fail2ban-client version")
|
||||
stdout, err := cmd.Exec("fail2ban-client --version")
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load the fail2ban version failed, err: %s", stdout)
|
||||
return "-"
|
||||
}
|
||||
return strings.ReplaceAll(stdout, "\n", "")
|
||||
versionRe := regexp.MustCompile(`(?i)fail2ban[:\s-]*v?(\d+\.\d+\.\d+)`)
|
||||
matches := versionRe.FindStringSubmatch(stdout)
|
||||
if len(matches) > 1 {
|
||||
return matches[1]
|
||||
}
|
||||
global.LOG.Errorf("Version regex failed to match output: %s", stdout)
|
||||
return "-"
|
||||
}
|
||||
|
||||
func (f *Fail2ban) Operate(operate string) error {
|
||||
switch operate {
|
||||
case "start", "restart", "stop", "enable", "disable":
|
||||
stdout, err := cmd.Execf("systemctl %s fail2ban.service", operate)
|
||||
stdout, err := systemctl.CustomAction(operate, "fail2ban")
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s the fail2ban.service failed, err: %s", operate, stdout)
|
||||
return fmt.Errorf("%s the fail2ban failed, err: %s", operate, stdout.Output)
|
||||
}
|
||||
return nil
|
||||
case "reload":
|
||||
@@ -136,7 +142,32 @@ func initLocalFile() error {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
initFile := `#DEFAULT-START
|
||||
var initFile string
|
||||
if systemctl.GetGlobalManager().Name() == "openrc" {
|
||||
initFile = `[sshd]
|
||||
enabled = true
|
||||
filter = alpine-sshd
|
||||
port = $ssh_port
|
||||
logpath = $logpath
|
||||
maxretry = 2
|
||||
banaction = $banaction
|
||||
|
||||
[sshd-ddos]
|
||||
enabled = true
|
||||
filter = alpine-sshd-ddos
|
||||
port = $ssh_port
|
||||
logpath = /var/log/messages
|
||||
maxretry = 2
|
||||
|
||||
[sshd-key]
|
||||
enabled = true
|
||||
filter = alpine-sshd-key
|
||||
port = $ssh_port
|
||||
logpath = /var/log/messages
|
||||
maxretry = 2
|
||||
`
|
||||
} else {
|
||||
initFile = `# DEFAULT-START
|
||||
[DEFAULT]
|
||||
bantime = 600
|
||||
findtime = 300
|
||||
@@ -146,36 +177,93 @@ action = %(action_mwl)s
|
||||
#DEFAULT-END
|
||||
|
||||
[sshd]
|
||||
ignoreip = 127.0.0.1/8
|
||||
ignoreip = 127.0.0.1/8 ::1
|
||||
enabled = true
|
||||
filter = sshd
|
||||
port = 22
|
||||
port = $ssh_port
|
||||
maxretry = 5
|
||||
findtime = 300
|
||||
bantime = 600
|
||||
banaction = $banaction
|
||||
action = %(action_mwl)s
|
||||
logpath = $logpath`
|
||||
|
||||
banaction := ""
|
||||
if active, _ := systemctl.IsActive("firewalld"); active {
|
||||
banaction = "firewallcmd-ipset"
|
||||
} else if active, _ := systemctl.IsActive("ufw"); active {
|
||||
banaction = "ufw"
|
||||
} else {
|
||||
banaction = "iptables-allports"
|
||||
logpath = $logpath
|
||||
maxmatches = 3
|
||||
`
|
||||
}
|
||||
// 检测防火墙类型
|
||||
banaction := detectFirewall()
|
||||
|
||||
// 检测SSH端口(支持Alpine的sshd_config位置)
|
||||
sshPort := detectSSHPort()
|
||||
|
||||
// 检测日志路径(兼容Alpine的多种日志位置)
|
||||
logPath := detectAuthLogPath()
|
||||
|
||||
// 执行变量替换
|
||||
initFile = strings.ReplaceAll(initFile, "$banaction", banaction)
|
||||
|
||||
logPath := ""
|
||||
if _, err := os.Stat("/var/log/secure"); err == nil {
|
||||
logPath = "/var/log/secure"
|
||||
} else {
|
||||
logPath = "/var/log/auth.log"
|
||||
}
|
||||
initFile = strings.ReplaceAll(initFile, "$logpath", logPath)
|
||||
initFile = strings.ReplaceAll(initFile, "$ssh_port", sshPort)
|
||||
|
||||
if err := os.WriteFile(defaultPath, []byte(initFile), 0640); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func detectFirewall() string {
|
||||
if active, _ := systemctl.IsActive("firewalld"); active {
|
||||
return "firewallcmd-ipset"
|
||||
}
|
||||
if active, _ := systemctl.IsActive("ufw"); active {
|
||||
return "ufw"
|
||||
}
|
||||
if active, _ := systemctl.IsActive("iptables"); active {
|
||||
return "iptables-allports"
|
||||
}
|
||||
if active, _ := systemctl.IsActive("nftables"); active {
|
||||
return "nftables-allports"
|
||||
}
|
||||
return "iptables-allports"
|
||||
}
|
||||
|
||||
func detectAuthLogPath() string {
|
||||
paths := []string{
|
||||
"/var/log/auth.log", // 常见默认路径
|
||||
"/var/log/messages", // Alpine系统日志
|
||||
"/var/log/secure", // RHEL风格路径
|
||||
"/var/log/syslog", // Debian风格路径
|
||||
}
|
||||
|
||||
for _, path := range paths {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return path
|
||||
}
|
||||
}
|
||||
return "/var/log/auth.log"
|
||||
}
|
||||
|
||||
func detectSSHPort() string {
|
||||
// 检查标准sshd_config路径
|
||||
configPaths := []string{
|
||||
"/etc/ssh/sshd_config",
|
||||
"/etc/sshd_config",
|
||||
}
|
||||
|
||||
for _, path := range configPaths {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(strings.TrimSpace(line), "Port") {
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) >= 2 {
|
||||
return parts[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "22" // 默认SSH端口
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"os"
|
||||
"os/user"
|
||||
"path"
|
||||
@@ -12,6 +11,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/backend/constant"
|
||||
"github.com/1Panel-dev/1Panel/backend/global"
|
||||
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
|
||||
@@ -94,9 +95,9 @@ func (f *Ftp) Status() (bool, bool) {
|
||||
func (f *Ftp) Operate(operate string) error {
|
||||
switch operate {
|
||||
case "start", "restart", "stop":
|
||||
stdout, err := cmd.Execf("systemctl %s pure-ftpd.service", operate)
|
||||
stdout, err := systemctl.CustomAction(operate, "pure-ftpd")
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s the pure-ftpd.service failed, err: %s", operate, stdout)
|
||||
return fmt.Errorf("%s the pure-ftpd service failed, err: %s", operate, stdout.Output)
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
|
||||
+17
-10
@@ -9,22 +9,29 @@ if [ ! -f "1pctl" ]; then
|
||||
wget https://github.com/1Panel-dev/installer/raw/main/1pctl
|
||||
fi
|
||||
|
||||
if [ ! -f "1panel.service" ]; then
|
||||
wget https://github.com/1Panel-dev/installer/raw/main/1panel.service
|
||||
fi
|
||||
|
||||
if [ ! -f "install.sh" ]; then
|
||||
wget https://github.com/1Panel-dev/installer/raw/main/install.sh
|
||||
fi
|
||||
|
||||
if [ ! -f "GeoIP.mmdb" ]; then
|
||||
wget https://resource.1panel.pro/geo/GeoIP.mmdb
|
||||
if [ ! -d "initscript" ]; then
|
||||
wget https://github.com/1Panel-dev/installer/raw/main/initscript/1panel.service
|
||||
mkdir -p initscript && cd initscript
|
||||
for file in 1panel.service 1paneld.init 1paneld.openrc 1paneld.procd; do
|
||||
wget -q https://github.com/1Panel-dev/installer/raw/main/initscript/$file
|
||||
done
|
||||
cd ..
|
||||
fi
|
||||
|
||||
if [ ! -f "lang.tar.gz" ]; then
|
||||
wget https://resource.1panel.pro/language/lang.tar.gz
|
||||
tar zxvf lang.tar.gz
|
||||
rm -rf lang.tar.gz
|
||||
if [ ! -d "lang" ]; then
|
||||
mkdir -p lang && cd lang
|
||||
for lang in en fa pt-BR ru zh; do
|
||||
wget -q https://github.com/1Panel-dev/installer/raw/main/lang/$lang.sh
|
||||
done
|
||||
cd ..
|
||||
fi
|
||||
|
||||
if [ ! -f "GeoIP.mmdb" ]; then
|
||||
wget https://resource.fit2cloud.com/1panel/package/geo/GeoIP.mmdb
|
||||
fi
|
||||
|
||||
chmod 755 1pctl install.sh
|
||||
|
||||
@@ -58,8 +58,15 @@ var restoreCmd = &cobra.Command{
|
||||
geoPath := path.Join(global.CONF.System.BaseDir, "1panel/geo")
|
||||
_, _ = cmdUtils.Execf("mkdir %s && cp %s %s/", geoPath, path.Join(tmpPath, "GeoIP.mmdb"), geoPath)
|
||||
fmt.Println(i18n.GetMsgByKeyForCmd("RestoreStep3"))
|
||||
if err := common.CopyFile(path.Join(tmpPath, "1panel.service"), "/etc/systemd/system"); err != nil {
|
||||
return err
|
||||
if isSystemd() {
|
||||
if err := common.CopyFile(path.Join(tmpPath, "1panel.service"), "/etc/systemd/system"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
} else {
|
||||
if err := common.CopyFile(path.Join(tmpPath, "1paneld"), "/etc/init.d"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
fmt.Println(i18n.GetMsgByKeyForCmd("RestoreStep4"))
|
||||
checkPointOfWal()
|
||||
@@ -124,3 +131,10 @@ func handleUnTar(sourceFile, targetDir string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isSystemd() bool {
|
||||
if _, err := os.Stat("/etc/systemd/system"); err == nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
+629
-626
File diff suppressed because it is too large
Load Diff
+629
-626
File diff suppressed because it is too large
Load Diff
+378
-384
@@ -3342,6 +3342,8 @@ definitions:
|
||||
type: string
|
||||
lastCleanTime:
|
||||
type: string
|
||||
licenseVerify:
|
||||
type: string
|
||||
localTime:
|
||||
type: string
|
||||
menuTabs:
|
||||
@@ -11237,381 +11239,6 @@ paths:
|
||||
formatEN: update group [name][type]
|
||||
formatZH: 更新组 [name][type]
|
||||
paramKeys: []
|
||||
/host/conffile/update:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/dto.SSHConf'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
- Timestamp: []
|
||||
summary: Update host SSH setting by file
|
||||
tags:
|
||||
- SSH
|
||||
x-panel-log:
|
||||
BeforeFunctions: []
|
||||
bodyKeys: []
|
||||
formatEN: update SSH conf
|
||||
formatZH: 修改 SSH 配置文件
|
||||
paramKeys: []
|
||||
/host/ssh/conf:
|
||||
get:
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
type: string
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
- Timestamp: []
|
||||
summary: Load host SSH conf
|
||||
tags:
|
||||
- SSH
|
||||
/host/ssh/generate:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/dto.GenerateSSH'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
- Timestamp: []
|
||||
summary: Generate host SSH secret
|
||||
tags:
|
||||
- SSH
|
||||
x-panel-log:
|
||||
BeforeFunctions: []
|
||||
bodyKeys: []
|
||||
formatEN: generate SSH secret
|
||||
formatZH: '生成 SSH 密钥 '
|
||||
paramKeys: []
|
||||
/host/ssh/log:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/dto.SearchSSHLog'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/dto.SSHLog'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
- Timestamp: []
|
||||
summary: Load host SSH logs
|
||||
tags:
|
||||
- SSH
|
||||
/host/ssh/operate:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/dto.Operate'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
- Timestamp: []
|
||||
summary: Operate SSH
|
||||
tags:
|
||||
- SSH
|
||||
x-panel-log:
|
||||
BeforeFunctions: []
|
||||
bodyKeys:
|
||||
- operation
|
||||
formatEN: '[operation] SSH'
|
||||
formatZH: '[operation] SSH '
|
||||
paramKeys: []
|
||||
/host/ssh/search:
|
||||
post:
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/dto.SSHInfo'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
- Timestamp: []
|
||||
summary: Load host SSH setting info
|
||||
tags:
|
||||
- SSH
|
||||
/host/ssh/secret:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/dto.GenerateLoad'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
type: string
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
- Timestamp: []
|
||||
summary: Load host SSH secret
|
||||
tags:
|
||||
- SSH
|
||||
/host/ssh/update:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/dto.SSHUpdate'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
- Timestamp: []
|
||||
summary: Update host SSH setting
|
||||
tags:
|
||||
- SSH
|
||||
x-panel-log:
|
||||
BeforeFunctions: []
|
||||
bodyKeys:
|
||||
- key
|
||||
- value
|
||||
formatEN: update SSH setting [key] => [value]
|
||||
formatZH: 修改 SSH 配置 [key] => [value]
|
||||
paramKeys: []
|
||||
/host/tool:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/request.HostToolReq'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/response.HostToolRes'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
- Timestamp: []
|
||||
summary: Get tool status
|
||||
tags:
|
||||
- Host tool
|
||||
/host/tool/config:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/request.HostToolConfig'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/response.HostToolConfig'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
- Timestamp: []
|
||||
summary: Get tool config
|
||||
tags:
|
||||
- Host tool
|
||||
x-panel-log:
|
||||
BeforeFunctions: []
|
||||
bodyKeys:
|
||||
- operate
|
||||
formatEN: '[operate] tool config'
|
||||
formatZH: '[operate] 主机工具配置文件 '
|
||||
paramKeys: []
|
||||
/host/tool/create:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/request.HostToolCreate'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
- Timestamp: []
|
||||
summary: Create Host tool Config
|
||||
tags:
|
||||
- Host tool
|
||||
x-panel-log:
|
||||
BeforeFunctions: []
|
||||
bodyKeys:
|
||||
- type
|
||||
formatEN: create [type] config
|
||||
formatZH: 创建 [type] 配置
|
||||
paramKeys: []
|
||||
/host/tool/log:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/request.HostToolLogReq'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
type: string
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
- Timestamp: []
|
||||
summary: Get tool logs
|
||||
tags:
|
||||
- Host tool
|
||||
/host/tool/operate:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/request.HostToolReq'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
- Timestamp: []
|
||||
summary: Operate tool
|
||||
tags:
|
||||
- Host tool
|
||||
x-panel-log:
|
||||
BeforeFunctions: []
|
||||
bodyKeys:
|
||||
- operate
|
||||
- type
|
||||
formatEN: '[operate] [type]'
|
||||
formatZH: '[operate] [type] '
|
||||
paramKeys: []
|
||||
/host/tool/supervisor/process:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/response.SupervisorProcessConfig'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
- Timestamp: []
|
||||
summary: Get Supervisor process config
|
||||
tags:
|
||||
- Host tool
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/request.SupervisorProcessConfig'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
- Timestamp: []
|
||||
summary: Create Supervisor process
|
||||
tags:
|
||||
- Host tool
|
||||
x-panel-log:
|
||||
BeforeFunctions: []
|
||||
bodyKeys:
|
||||
- operate
|
||||
formatEN: '[operate] process'
|
||||
formatZH: '[operate] 守护进程 '
|
||||
paramKeys: []
|
||||
/host/tool/supervisor/process/file:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/request.SupervisorProcessFileReq'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
type: string
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
- Timestamp: []
|
||||
summary: Get Supervisor process config
|
||||
tags:
|
||||
- Host tool
|
||||
x-panel-log:
|
||||
BeforeFunctions: []
|
||||
bodyKeys:
|
||||
- operate
|
||||
formatEN: '[operate] Supervisor Process Config file'
|
||||
formatZH: '[operate] Supervisor 进程文件 '
|
||||
paramKeys: []
|
||||
/hosts:
|
||||
post:
|
||||
consumes:
|
||||
@@ -11878,6 +11505,32 @@ paths:
|
||||
formatEN: update quick command [name]
|
||||
formatZH: 更新快捷命令 [name]
|
||||
paramKeys: []
|
||||
/hosts/conffile/update:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/dto.SSHConf'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
- Timestamp: []
|
||||
summary: Update host SSH setting by file
|
||||
tags:
|
||||
- SSH
|
||||
x-panel-log:
|
||||
BeforeFunctions: []
|
||||
bodyKeys: []
|
||||
formatEN: update SSH conf
|
||||
formatZH: 修改 SSH 配置文件
|
||||
paramKeys: []
|
||||
/hosts/del:
|
||||
post:
|
||||
consumes:
|
||||
@@ -12197,6 +11850,157 @@ paths:
|
||||
summary: Page host
|
||||
tags:
|
||||
- Host
|
||||
/hosts/ssh/conf:
|
||||
get:
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
type: string
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
- Timestamp: []
|
||||
summary: Load host SSH conf
|
||||
tags:
|
||||
- SSH
|
||||
/hosts/ssh/generate:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/dto.GenerateSSH'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
- Timestamp: []
|
||||
summary: Generate host SSH secret
|
||||
tags:
|
||||
- SSH
|
||||
x-panel-log:
|
||||
BeforeFunctions: []
|
||||
bodyKeys: []
|
||||
formatEN: generate SSH secret
|
||||
formatZH: '生成 SSH 密钥 '
|
||||
paramKeys: []
|
||||
/hosts/ssh/log:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/dto.SearchSSHLog'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/dto.SSHLog'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
- Timestamp: []
|
||||
summary: Load host SSH logs
|
||||
tags:
|
||||
- SSH
|
||||
/hosts/ssh/operate:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/dto.Operate'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
- Timestamp: []
|
||||
summary: Operate SSH
|
||||
tags:
|
||||
- SSH
|
||||
x-panel-log:
|
||||
BeforeFunctions: []
|
||||
bodyKeys:
|
||||
- operation
|
||||
formatEN: '[operation] SSH'
|
||||
formatZH: '[operation] SSH '
|
||||
paramKeys: []
|
||||
/hosts/ssh/search:
|
||||
post:
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/dto.SSHInfo'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
- Timestamp: []
|
||||
summary: Load host SSH setting info
|
||||
tags:
|
||||
- SSH
|
||||
/hosts/ssh/secret:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/dto.GenerateLoad'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
type: string
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
- Timestamp: []
|
||||
summary: Load host SSH secret
|
||||
tags:
|
||||
- SSH
|
||||
/hosts/ssh/update:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/dto.SSHUpdate'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
- Timestamp: []
|
||||
summary: Update host SSH setting
|
||||
tags:
|
||||
- SSH
|
||||
x-panel-log:
|
||||
BeforeFunctions: []
|
||||
bodyKeys:
|
||||
- key
|
||||
- value
|
||||
formatEN: update SSH setting [key] => [value]
|
||||
formatZH: 修改 SSH 配置 [key] => [value]
|
||||
paramKeys: []
|
||||
/hosts/test/byid/{id}:
|
||||
post:
|
||||
consumes:
|
||||
@@ -12240,6 +12044,204 @@ paths:
|
||||
summary: Test host conn by info
|
||||
tags:
|
||||
- Host
|
||||
/hosts/tool:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/request.HostToolReq'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/response.HostToolRes'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
- Timestamp: []
|
||||
summary: Get tool status
|
||||
tags:
|
||||
- Host tool
|
||||
/hosts/tool/config:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/request.HostToolConfig'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/response.HostToolConfig'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
- Timestamp: []
|
||||
summary: Get tool config
|
||||
tags:
|
||||
- Host tool
|
||||
x-panel-log:
|
||||
BeforeFunctions: []
|
||||
bodyKeys:
|
||||
- operate
|
||||
formatEN: '[operate] tool config'
|
||||
formatZH: '[operate] 主机工具配置文件 '
|
||||
paramKeys: []
|
||||
/hosts/tool/create:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/request.HostToolCreate'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
- Timestamp: []
|
||||
summary: Create Host tool Config
|
||||
tags:
|
||||
- Host tool
|
||||
x-panel-log:
|
||||
BeforeFunctions: []
|
||||
bodyKeys:
|
||||
- type
|
||||
formatEN: create [type] config
|
||||
formatZH: 创建 [type] 配置
|
||||
paramKeys: []
|
||||
/hosts/tool/log:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/request.HostToolLogReq'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
type: string
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
- Timestamp: []
|
||||
summary: Get tool logs
|
||||
tags:
|
||||
- Host tool
|
||||
/hosts/tool/operate:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/request.HostToolReq'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
- Timestamp: []
|
||||
summary: Operate tool
|
||||
tags:
|
||||
- Host tool
|
||||
x-panel-log:
|
||||
BeforeFunctions: []
|
||||
bodyKeys:
|
||||
- operate
|
||||
- type
|
||||
formatEN: '[operate] [type]'
|
||||
formatZH: '[operate] [type] '
|
||||
paramKeys: []
|
||||
/hosts/tool/supervisor/process:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/response.SupervisorProcessConfig'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
- Timestamp: []
|
||||
summary: Get Supervisor process config
|
||||
tags:
|
||||
- Host tool
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/request.SupervisorProcessConfig'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
- Timestamp: []
|
||||
summary: Create Supervisor process
|
||||
tags:
|
||||
- Host tool
|
||||
x-panel-log:
|
||||
BeforeFunctions: []
|
||||
bodyKeys:
|
||||
- operate
|
||||
formatEN: '[operate] process'
|
||||
formatZH: '[operate] 守护进程 '
|
||||
paramKeys: []
|
||||
/hosts/tool/supervisor/process/file:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/request.SupervisorProcessFileReq'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
type: string
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
- Timestamp: []
|
||||
summary: Get Supervisor process config
|
||||
tags:
|
||||
- Host tool
|
||||
x-panel-log:
|
||||
BeforeFunctions: []
|
||||
bodyKeys:
|
||||
- operate
|
||||
formatEN: '[operate] Supervisor Process Config file'
|
||||
formatZH: '[operate] Supervisor 进程文件 '
|
||||
paramKeys: []
|
||||
/hosts/tree:
|
||||
post:
|
||||
consumes:
|
||||
@@ -16766,15 +16768,7 @@ schemes:
|
||||
- https
|
||||
securityDefinitions:
|
||||
ApiKeyAuth:
|
||||
description: |-
|
||||
Custom Token Format, Format: md5('1panel' + API-Key + UnixTimestamp).
|
||||
```
|
||||
eg:
|
||||
curl -X GET "http://localhost:4004/api/v1/dashboard/current" \
|
||||
-H "1Panel-Token: <1panel_token>" \
|
||||
-H "1Panel-Timestamp: <current_unix_timestamp>"
|
||||
```
|
||||
- `1Panel-Token` is the key for the panel API Key.
|
||||
description: '- `1Panel-Token` is the key for the panel API Key.'
|
||||
in: header
|
||||
name: 1Panel-Token
|
||||
type: apiKey
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
services:
|
||||
mcp-server:
|
||||
image: supercorp/supergateway:latest
|
||||
container_name: ${CONTAINER_NAME}
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${HOST_IP}:${PANEL_APP_PORT_HTTP}:${PANEL_APP_PORT_HTTP}"
|
||||
command: [
|
||||
"--stdio", "${COMMAND}",
|
||||
"--port", "${PANEL_APP_PORT_HTTP}",
|
||||
"--baseUrl", "${BASE_URL}",
|
||||
"--ssePath", "${SSE_PATH}",
|
||||
"--messagePath", "${SSE_PATH}/messages"
|
||||
]
|
||||
networks:
|
||||
- 1panel-network
|
||||
networks:
|
||||
1panel-network:
|
||||
external: true
|
||||
@@ -0,0 +1,8 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
//go:embed compose.yml
|
||||
var DefaultMcpCompose []byte
|
||||
@@ -37,3 +37,6 @@ var DomainNotFoundHTML []byte
|
||||
|
||||
//go:embed stop.html
|
||||
var StopHTML []byte
|
||||
|
||||
//go:embed sse.conf
|
||||
var SSE []byte
|
||||
|
||||
@@ -7,6 +7,7 @@ location ^~ /test {
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $http_connection;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
proxy_http_version 1.1;
|
||||
add_header X-Cache $upstream_cache_status;
|
||||
add_header Cache-Control no-cache;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
location ^~ /github {
|
||||
proxy_pass http://127.0.0.1:8001/github;
|
||||
proxy_buffering off;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection '';
|
||||
chunked_transfer_encoding off;
|
||||
}
|
||||
@@ -28,3 +28,4 @@ stats.html
|
||||
*.sln
|
||||
*.sw?
|
||||
.vscode
|
||||
*.code-workspace
|
||||
@@ -109,4 +109,75 @@ export namespace AI {
|
||||
websiteID?: number;
|
||||
connUrl: string;
|
||||
}
|
||||
|
||||
export interface Environment {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface Volume {
|
||||
source: string;
|
||||
target: string;
|
||||
}
|
||||
|
||||
export interface McpServer {
|
||||
id: number;
|
||||
name: string;
|
||||
status: string;
|
||||
baseUrl: string;
|
||||
ssePath: string;
|
||||
command: string;
|
||||
port: number;
|
||||
message: string;
|
||||
createdAt?: string;
|
||||
containerName: string;
|
||||
environments: Environment[];
|
||||
volumes: Volume[];
|
||||
dir?: string;
|
||||
hostIP: string;
|
||||
protocol: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface McpServerSearch extends ReqPage {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface McpServerDelete {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface McpServerOperate {
|
||||
id: number;
|
||||
operate: string;
|
||||
}
|
||||
|
||||
export interface McpBindDomain {
|
||||
domain: string;
|
||||
sslID: number;
|
||||
ipList: string;
|
||||
}
|
||||
|
||||
export interface McpDomainRes {
|
||||
domain: string;
|
||||
sslID: number;
|
||||
acmeAccountID: number;
|
||||
allowIPs: string[];
|
||||
websiteID?: number;
|
||||
connUrl: string;
|
||||
}
|
||||
|
||||
export interface McpBindDomainUpdate {
|
||||
websiteID: number;
|
||||
sslID: number;
|
||||
ipList: string;
|
||||
}
|
||||
|
||||
export interface ImportMcpServer {
|
||||
name: string;
|
||||
command: string;
|
||||
ssePath: string;
|
||||
containerName: string;
|
||||
environments: Environment[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,9 +212,6 @@ export namespace Container {
|
||||
password: string;
|
||||
auth: boolean;
|
||||
}
|
||||
export interface RepoDelete {
|
||||
ids: Array<number>;
|
||||
}
|
||||
export interface RepoInfo {
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
|
||||
@@ -81,6 +81,7 @@ export namespace Cronjob {
|
||||
export interface CronjobDelete {
|
||||
ids: Array<number>;
|
||||
cleanData: boolean;
|
||||
cleanRemoteData: boolean;
|
||||
}
|
||||
export interface UpdateStatus {
|
||||
id: number;
|
||||
|
||||
@@ -39,3 +39,35 @@ export const getBindDomain = (req: AI.BindDomainReq) => {
|
||||
export const updateBindDomain = (req: AI.BindDomain) => {
|
||||
return http.post(`/ai/domain/update`, req);
|
||||
};
|
||||
|
||||
export const pageMcpServer = (req: AI.McpServerSearch) => {
|
||||
return http.post<ResPage<AI.McpServer>>(`/ai/mcp/search`, req);
|
||||
};
|
||||
|
||||
export const createMcpServer = (req: AI.McpServer) => {
|
||||
return http.post(`/ai/mcp/server`, req);
|
||||
};
|
||||
|
||||
export const updateMcpServer = (req: AI.McpServer) => {
|
||||
return http.post(`/ai/mcp/server/update`, req);
|
||||
};
|
||||
|
||||
export const deleteMcpServer = (req: AI.McpServerDelete) => {
|
||||
return http.post(`/ai/mcp/server/del`, req);
|
||||
};
|
||||
|
||||
export const operateMcpServer = (req: AI.McpServerOperate) => {
|
||||
return http.post(`/ai/mcp/server/op`, req);
|
||||
};
|
||||
|
||||
export const bindMcpDomain = (req: AI.McpBindDomain) => {
|
||||
return http.post(`/ai/mcp/domain/bind`, req);
|
||||
};
|
||||
|
||||
export const getMcpDomain = () => {
|
||||
return http.get<AI.McpDomainRes>(`/ai/mcp/domain/get`);
|
||||
};
|
||||
|
||||
export const updateMcpDomain = (req: AI.McpBindDomainUpdate) => {
|
||||
return http.post(`/ai/mcp/domain/update`, req);
|
||||
};
|
||||
|
||||
@@ -135,8 +135,8 @@ export const createImageRepo = (params: Container.RepoCreate) => {
|
||||
export const updateImageRepo = (params: Container.RepoUpdate) => {
|
||||
return http.post(`/containers/repo/update`, params, TimeoutEnum.T_40S);
|
||||
};
|
||||
export const deleteImageRepo = (params: Container.RepoDelete) => {
|
||||
return http.post(`/containers/repo/del`, params, TimeoutEnum.T_40S);
|
||||
export const deleteImageRepo = (id: Number) => {
|
||||
return http.post(`/containers/repo/del`, { id: id }, TimeoutEnum.T_40S);
|
||||
};
|
||||
|
||||
// composeTemplate
|
||||
|
||||
@@ -27,8 +27,12 @@ export const searchRecords = (params: Cronjob.SearchRecord) => {
|
||||
return http.post<ResPage<Cronjob.Record>>(`cronjobs/search/records`, params);
|
||||
};
|
||||
|
||||
export const cleanRecords = (id: number, cleanData: boolean) => {
|
||||
return http.post(`cronjobs/records/clean`, { cronjobID: id, cleanData: cleanData });
|
||||
export const cleanRecords = (id: number, cleanData: boolean, cleanRemoteData: boolean) => {
|
||||
return http.post(`cronjobs/records/clean`, {
|
||||
cronjobID: id,
|
||||
cleanData: cleanData,
|
||||
cleanRemoteData: cleanRemoteData,
|
||||
});
|
||||
};
|
||||
|
||||
export const getRecordDetail = (params: string) => {
|
||||
|
||||
@@ -49,8 +49,8 @@ export const SaveFileContent = (params: File.FileEdit) => {
|
||||
return http.post<File.File>('files/save', params);
|
||||
};
|
||||
|
||||
export const CheckFile = (path: string) => {
|
||||
return http.post<boolean>('files/check', { path: path });
|
||||
export const CheckFile = (path: string, withInit: boolean) => {
|
||||
return http.post<boolean>('files/check', { path: path, withInit: withInit });
|
||||
};
|
||||
|
||||
export const BatchCheckFiles = (paths: string[]) => {
|
||||
|
||||
@@ -85,7 +85,9 @@ function onClose(isKeepShow: boolean = false) {
|
||||
term.value.dispose();
|
||||
} catch {}
|
||||
}
|
||||
terminalElement.value.innerHTML = '';
|
||||
if (terminalElement.value) {
|
||||
terminalElement.value.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
// terminal 相关代码 start
|
||||
@@ -202,10 +204,10 @@ const errorRealTerminal = (ex: any) => {
|
||||
|
||||
const closeRealTerminal = (ev: CloseEvent) => {
|
||||
if (heartbeatTimer.value) {
|
||||
clearInterval(heartbeatTimer.value);
|
||||
clearInterval(Number(heartbeatTimer.value));
|
||||
}
|
||||
term.value.write('The connection has been disconnected.');
|
||||
term.value.write(ev.reason);
|
||||
term.value?.write('The connection has been disconnected.');
|
||||
term.value?.write(ev.reason);
|
||||
};
|
||||
|
||||
const isWsOpen = () => {
|
||||
|
||||
@@ -308,7 +308,7 @@ const onSubmit = async () => {
|
||||
MsgError(i18n.global.t('commons.msg.fileNameErr'));
|
||||
return;
|
||||
}
|
||||
const res = await CheckFile(baseDir.value + file.raw.name);
|
||||
const res = await CheckFile(baseDir.value + file.raw.name, false);
|
||||
if (res.data) {
|
||||
MsgError(i18n.global.t('commons.msg.fileExist'));
|
||||
return;
|
||||
|
||||
@@ -121,7 +121,8 @@ const checkIllegal = (rule: any, value: any, callback: any) => {
|
||||
value.indexOf('`') !== -1 ||
|
||||
value.indexOf('(') !== -1 ||
|
||||
value.indexOf(')') !== -1 ||
|
||||
value.indexOf("'") !== -1
|
||||
value.indexOf('>') !== -1 ||
|
||||
value.indexOf('<') !== -1
|
||||
) {
|
||||
callback(new Error(i18n.global.t('commons.rule.illegalInput')));
|
||||
} else {
|
||||
|
||||
@@ -95,6 +95,10 @@ export const Languages = [
|
||||
label: 'css',
|
||||
value: ['css'],
|
||||
},
|
||||
{
|
||||
label: 'ini',
|
||||
value: ['ini'],
|
||||
},
|
||||
];
|
||||
|
||||
export const Rewrites = [
|
||||
@@ -169,21 +173,13 @@ export const DNSTypes = [
|
||||
value: 'HuaweiCloud',
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('website.volcengine'),
|
||||
value: 'Volcengine',
|
||||
},
|
||||
{
|
||||
label: 'DNSPod (' + i18n.global.t('ssl.deprecated') + ')',
|
||||
value: 'DnsPod',
|
||||
label: 'GoDaddy',
|
||||
value: 'Godaddy',
|
||||
},
|
||||
{
|
||||
label: 'Cloudflare',
|
||||
value: 'CloudFlare',
|
||||
},
|
||||
{
|
||||
label: 'CloudDNS',
|
||||
value: 'CloudDns',
|
||||
},
|
||||
{
|
||||
label: 'NameSilo',
|
||||
value: 'NameSilo',
|
||||
@@ -193,12 +189,16 @@ export const DNSTypes = [
|
||||
value: 'NameCheap',
|
||||
},
|
||||
{
|
||||
label: 'Name.com',
|
||||
value: 'NameCom',
|
||||
label: 'CloudDNS',
|
||||
value: 'CloudDns',
|
||||
},
|
||||
{
|
||||
label: 'GoDaddy',
|
||||
value: 'Godaddy',
|
||||
label: 'ClouDNS',
|
||||
value: 'ClouDNS',
|
||||
},
|
||||
{
|
||||
label: 'Name.com',
|
||||
value: 'NameCom',
|
||||
},
|
||||
{
|
||||
label: 'FreeMyIP',
|
||||
@@ -208,6 +208,18 @@ export const DNSTypes = [
|
||||
label: i18n.global.t('website.rainyun'),
|
||||
value: 'RainYun',
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('website.westCN'),
|
||||
value: 'WestCN',
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('website.volcengine'),
|
||||
value: 'Volcengine',
|
||||
},
|
||||
{
|
||||
label: 'DNSPod (' + i18n.global.t('ssl.deprecated') + ')',
|
||||
value: 'DnsPod',
|
||||
},
|
||||
];
|
||||
|
||||
export const Fields = [
|
||||
|
||||
@@ -191,6 +191,7 @@ const message = {
|
||||
rePassword: 'Confirm password is inconsistent with the password.',
|
||||
requiredInput: 'This field is required.',
|
||||
requiredSelect: 'Select an item in the list',
|
||||
illegalChar: 'Injection of characters & ; $ \' ` ( ) " > < | is currently not supported',
|
||||
illegalInput: `This field mustn't contains illegal characters.`,
|
||||
commonName:
|
||||
'This field must start with non-special characters and must consist of English, Chinese, numbers, ".", "-", and "_" characters with a length of 1-128.',
|
||||
@@ -332,6 +333,7 @@ const message = {
|
||||
ssl: 'Certificate | Certificates',
|
||||
database: 'Database | Databases',
|
||||
ai_tools: 'AI',
|
||||
mcp: 'MCP',
|
||||
container: 'Container | Containers',
|
||||
cronjob: 'Cron Job | Cron Jobs',
|
||||
host: 'Host | Hosts',
|
||||
@@ -427,6 +429,8 @@ const message = {
|
||||
permission: 'Permissions',
|
||||
permissionForIP: 'IP',
|
||||
permissionAll: 'All of them(%)',
|
||||
localhostHelper:
|
||||
'Configuring database permissions as "localhost" for container deployment will prevent external access to the container. Please choose carefully!',
|
||||
databaseConnInfo: 'Connection info',
|
||||
rootPassword: 'Root password',
|
||||
serviceName: 'Service Name',
|
||||
@@ -438,6 +442,7 @@ const message = {
|
||||
remoteConnHelper:
|
||||
'Remote connection to MySQL as user root may have security risks. Therefore, perform this operation with caution.',
|
||||
changePassword: 'Password',
|
||||
changeConnHelper: 'This operation will modify the current database {0}. Do you want to continue?',
|
||||
changePasswordHelper:
|
||||
'The database has been associated with an application. Changing the password will change the database password of the application at the same time. The change takes effect after the application restarts.',
|
||||
|
||||
@@ -829,6 +834,7 @@ const message = {
|
||||
|
||||
repo: 'Registries',
|
||||
createRepo: 'Add',
|
||||
httpRepoHelper: 'Operating an HTTP-type repository requires restarting the Docker service.',
|
||||
httpRepo: 'Choosing HTTP protocol requires restarting the Docker service to add it into insecure registries.',
|
||||
delInsecure: 'Deletion of credit',
|
||||
delInsecureHelper:
|
||||
@@ -1009,6 +1015,7 @@ const message = {
|
||||
errHandle: 'Cronjob execution failure',
|
||||
noRecord: 'Trigger the Cron Job, and you will see the records here.',
|
||||
cleanData: 'Clean data',
|
||||
cleanRemoteData: 'Delete remote data',
|
||||
cleanDataHelper: 'Delete the backup file generated during this task.',
|
||||
noLogs: 'No task output yet...',
|
||||
errPath: 'Backup path [{0}] error, cannot download!',
|
||||
@@ -1379,7 +1386,9 @@ const message = {
|
||||
existFileTitle: 'Same name file prompt',
|
||||
existFileHelper: 'The uploaded file contains a file with the same name, do you want to overwrite it?',
|
||||
existFileSize: 'File size (new -> old)',
|
||||
existFileDirHelper: 'The selected file/folder has a duplicate name. Please proceed with caution!',
|
||||
existFileDirHelper: 'The selected file/folder has a duplicate name. Please proceed with caution! \n',
|
||||
coverDirHelper: 'The selected folders to be replaced will be copied to the destination path!',
|
||||
noSuchFile: 'The file or directory was not found. Please check and try again.',
|
||||
},
|
||||
ssh: {
|
||||
setting: 'Setting',
|
||||
@@ -2256,6 +2265,7 @@ const message = {
|
||||
"When the reverse proxy backend is HTTPS, you might need to set the origin SNI. Please refer to the CDN service provider's documentation for details.",
|
||||
huaweicloud: 'Huawei Cloud',
|
||||
rainyun: 'Rain Yun',
|
||||
westCN: 'West CN',
|
||||
},
|
||||
php: {
|
||||
short_open_tag: 'Short tag support',
|
||||
@@ -2537,8 +2547,8 @@ const message = {
|
||||
env: 'Environments',
|
||||
noenv: 'None',
|
||||
net: 'Network connections',
|
||||
laddr: 'Source address/port',
|
||||
raddr: 'Destination address/port',
|
||||
laddr: 'Local address/port',
|
||||
raddr: 'Remote address/port',
|
||||
stopProcess: 'End',
|
||||
viewDetails: 'Details',
|
||||
stopProcessWarn: 'Are you sure you want to end this process (PID:{0})?',
|
||||
@@ -2597,6 +2607,29 @@ const message = {
|
||||
proxyHelper6: 'To disable proxy configuration, you can delete it from the website list.',
|
||||
whiteListHelper: 'Restrict access to only IPs in the whitelist',
|
||||
},
|
||||
mcp: {
|
||||
server: 'MCP Server',
|
||||
create: 'Add MCP Server',
|
||||
edit: 'Edit MCP Server',
|
||||
commandHelper: 'For example: npx -y {0}',
|
||||
baseUrl: 'External Access Path',
|
||||
baseUrlHelper: 'For example: http://192.168.1.2:8000',
|
||||
ssePath: 'SSE Path',
|
||||
ssePathHelper: 'For example: /sse, note not to duplicate with other servers',
|
||||
environment: 'Environment Variables',
|
||||
envKey: 'Variable Name',
|
||||
envValue: 'Variable Value',
|
||||
externalUrl: 'External Connection Address',
|
||||
operatorHelper: 'Will perform {1} operation on {0}, continue?',
|
||||
domain: 'Default Access Address',
|
||||
domainHelper: 'For example: 192.168.1.1 or example.com',
|
||||
bindDomain: 'Bind Website',
|
||||
commandPlaceHolder: 'Currently only supports npx and binary startup commands',
|
||||
importMcpJson: 'Import MCP Server Configuration',
|
||||
importMcpJsonError: 'mcpServers structure is incorrect',
|
||||
bindDomainHelper:
|
||||
'After binding the website, it will modify the access address of all installed MCP Servers and close external access to the ports',
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@@ -74,7 +74,7 @@ const message = {
|
||||
createNewFile: '新しいファイルを作成します',
|
||||
helpDoc: '文書をヘルプします',
|
||||
unbind: 'バインド',
|
||||
cover: '上書き',
|
||||
cover: 'に覆いを',
|
||||
skip: 'スキップ',
|
||||
},
|
||||
search: {
|
||||
@@ -190,6 +190,7 @@ const message = {
|
||||
rePassword: 'パスワードがパスワードと矛盾することを確認してください。',
|
||||
requiredInput: 'この項目は必須です。',
|
||||
requiredSelect: 'リスト内のアイテムを選択します',
|
||||
illegalChar: '現在、文字 & ; $ \' ` ( ) " > < | の注入はサポートされていません',
|
||||
illegalInput: `このフィールドには違法なキャラクターが含まれてはなりません。`,
|
||||
commonName:
|
||||
'このフィールドは、特別なキャラクターではなく、英語、中国語、数字で構成されている必要があります。「。」、「」、および「_」文字が1〜128の文字で構成されている必要があります。',
|
||||
@@ -332,6 +333,7 @@ const message = {
|
||||
ssl: '証明書|証明書',
|
||||
database: 'データベース|データベース',
|
||||
ai_tools: 'AI',
|
||||
mcp: 'MCP',
|
||||
container: 'コンテナ|コンテナ',
|
||||
cronjob: 'クロンジョブ|クロンの仕事',
|
||||
host: 'ホスト|ホスト',
|
||||
@@ -427,6 +429,8 @@ const message = {
|
||||
permission: '権限',
|
||||
permissionForIP: 'ip',
|
||||
permissionAll: 'それらすべて(%)',
|
||||
localhostHelper:
|
||||
'コンテナ展開でデータベース権限を"localhost"に設定すると、コンテナ外部からのアクセスができなくなります。慎重に選択してください!',
|
||||
databaseConnInfo: '接続情報',
|
||||
rootPassword: 'ルートパスワード',
|
||||
serviceName: 'サービス名',
|
||||
@@ -438,6 +442,7 @@ const message = {
|
||||
remoteConnHelper:
|
||||
'ユーザールートとしてのMySQLへのリモート接続には、セキュリティリスクがある場合があります。したがって、この操作を慎重に実行します。',
|
||||
changePassword: 'パスワード',
|
||||
changeConnHelper: 'この操作は現在のデータベース {0} を変更します。続行しますか?',
|
||||
changePasswordHelper:
|
||||
'データベースはアプリケーションに関連付けられています。パスワードを変更すると、アプリケーションのデータベースパスワードが同時に変更されます。アプリケーションが再起動した後、変更は有効になります。',
|
||||
|
||||
@@ -825,6 +830,7 @@ const message = {
|
||||
|
||||
repo: 'レジストリ',
|
||||
createRepo: '追加',
|
||||
httpRepoHelper: 'HTTPタイプのリポジトリを操作するにはDockerサービスの再起動が必要です。',
|
||||
httpRepo:
|
||||
'HTTPプロトコルを選択するには、Dockerサービスを再起動して不安定なレジストリに追加する必要があります。',
|
||||
delInsecure: 'クレジットの削除',
|
||||
@@ -994,6 +1000,7 @@ const message = {
|
||||
errHandle: 'cronjob実行障害',
|
||||
noRecord: 'Cronジョブをトリガーすると、ここにレコードが表示されます。',
|
||||
cleanData: 'クリーンデータ',
|
||||
cleanRemoteData: 'リモートデータを削除',
|
||||
cleanDataHelper: 'このタスク中に生成されたバックアップファイルを削除します。',
|
||||
noLogs: 'タスク出力はまだありません...',
|
||||
errPath: 'バックアップパス[{0}]エラー、ダウンロードできません!',
|
||||
@@ -1358,6 +1365,8 @@ const message = {
|
||||
existFileHelper: 'アップロードしたファイルに同じ名前のファイルが含まれています。上書きしますか?',
|
||||
existFileSize: 'ファイルサイズ(新しい -> 古い)',
|
||||
existFileDirHelper: '選択したファイル/フォルダーには同じ名前のものが既に存在します。慎重に操作してください!',
|
||||
coverDirHelper: '上書きするフォルダを選択すると、対象パスにコピーされます!',
|
||||
noSuchFile: 'ファイルまたはディレクトリが見つかりませんでした。確認して再試行してください。',
|
||||
},
|
||||
ssh: {
|
||||
setting: '設定',
|
||||
@@ -2228,6 +2237,8 @@ const message = {
|
||||
sniHelper:
|
||||
'逆プロキシバックエンドがHTTPSの場合、Origin SNIを設定する必要がある場合があります。詳細については、CDNサービスプロバイダーのドキュメントを参照してください。',
|
||||
huaweicloud: 'huaweiCloud',
|
||||
rainyun: 'Rain Yun',
|
||||
westCN: 'West CN',
|
||||
},
|
||||
php: {
|
||||
short_open_tag: '短いタグサポート',
|
||||
@@ -2566,6 +2577,29 @@ const message = {
|
||||
proxyHelper6: 'プロキシ設定を無効にするには、ウェブサイトリストから削除できます。',
|
||||
whiteListHelper: 'ホワイトリスト内のIPのみアクセスを許可する',
|
||||
},
|
||||
mcp: {
|
||||
server: 'MCP サーバー',
|
||||
create: 'サーバーを追加',
|
||||
edit: 'サーバーを編集',
|
||||
commandHelper: '例: npx -y {0}',
|
||||
baseUrl: '外部アクセスパス',
|
||||
baseUrlHelper: '例: http://192.168.1.2:8000',
|
||||
ssePath: 'SSE パス',
|
||||
ssePathHelper: '例: /sse, 他のサーバーと重複しないように注意してください',
|
||||
environment: '環境変数',
|
||||
envKey: '変数名',
|
||||
envValue: '変数値',
|
||||
externalUrl: '外部接続アドレス',
|
||||
operatorHelper: '{0} に {1} 操作を実行します、続行しますか?',
|
||||
domain: 'デフォルトアクセスアドレス',
|
||||
domainHelper: '例: 192.168.1.1 または example.com',
|
||||
bindDomain: 'ウェブサイトをバインド',
|
||||
commandPlaceHolder: '現在、npx およびバイナリスタートアップコマンドのみをサポートしています',
|
||||
importMcpJson: 'MCP サーバー設定をインポート',
|
||||
importMcpJsonError: 'mcpServers 構造が正しくありません',
|
||||
bindDomainHelper:
|
||||
'ウェブサイトをバインドした後、インストールされたすべての MCP サーバーのアクセスアドレスを変更し、ポートへの外部アクセスを閉じます',
|
||||
},
|
||||
};
|
||||
export default {
|
||||
...fit2cloudJaLocale,
|
||||
|
||||
@@ -190,6 +190,7 @@ const message = {
|
||||
rePassword: '확인 비밀번호가 비밀번호와 일치하지 않습니다.',
|
||||
requiredInput: '이 필드는 필수 항목입니다.',
|
||||
requiredSelect: '목록에서 항목을 선택하세요',
|
||||
illegalChar: '현재 & ; $ \' ` ( ) " > < | 문자 주입은 지원되지 않습니다',
|
||||
illegalInput: '이 필드에는 유효하지 않은 문자가 포함될 수 없습니다.',
|
||||
commonName:
|
||||
'이 필드는 특수 문자로 시작할 수 없으며, 영어, 한자, 숫자, ".", "-", "_" 문자로 구성되어야 하며 길이는 1-128자여야 합니다.',
|
||||
@@ -333,6 +334,7 @@ const message = {
|
||||
ssl: '인증서 | 인증서들',
|
||||
database: '데이터베이스 | 데이터베이스들',
|
||||
ai_tools: 'AI',
|
||||
mcp: 'MCP',
|
||||
container: '컨테이너 | 컨테이너들',
|
||||
cronjob: '크론 작업 | 크론 작업들',
|
||||
host: '호스트 | 호스트들',
|
||||
@@ -429,6 +431,8 @@ const message = {
|
||||
permission: '권한',
|
||||
permissionForIP: 'IP',
|
||||
permissionAll: '모두(%)',
|
||||
localhostHelper:
|
||||
'컨테이너 배포 시 데이터베이스 권한을 "localhost"로 설정하면 컨테이너 외부에서 접근할 수 없게 됩니다. 신중하게 선택하세요!',
|
||||
databaseConnInfo: '연결 정보',
|
||||
rootPassword: '루트 비밀번호',
|
||||
serviceName: '서비스 이름',
|
||||
@@ -440,6 +444,7 @@ const message = {
|
||||
remoteConnHelper:
|
||||
'MySQL 의 root 사용자로 원격 접속은 보안 위험을 초래할 수 있습니다. 따라서 이 작업은 신중히 수행해야 합니다.',
|
||||
changePassword: '비밀번호',
|
||||
changeConnHelper: '이 작업은 현재 데이터베이스 {0}을(를) 수정합니다. 계속하시겠습니까?',
|
||||
changePasswordHelper:
|
||||
'데이터베이스가 애플리케이션과 연결되어 있습니다. 비밀번호를 변경하면 애플리케이션의 데이터베이스 비밀번호도 변경됩니다. 변경 사항은 애플리케이션이 재시작된 후에 적용됩니다.',
|
||||
confChange: '설정',
|
||||
@@ -815,6 +820,7 @@ const message = {
|
||||
|
||||
repo: '레지스트리',
|
||||
createRepo: '추가',
|
||||
httpRepoHelper: 'HTTP 타입 저장소 작업 시 Docker 서비스 재시작이 필요합니다.',
|
||||
httpRepo: 'HTTP 프로토콜을 선택하면 Docker 서비스를 재시작하여 불안정한 레지스트리에 추가해야 합니다.',
|
||||
delInsecure: '신뢰할 수 없는 항목 삭제',
|
||||
delInsecureHelper:
|
||||
@@ -988,6 +994,7 @@ const message = {
|
||||
errHandle: '크론 작업 실행 실패',
|
||||
noRecord: '크론 작업을 트리거하고 나면 여기에 레코드가 표시됩니다.',
|
||||
cleanData: '데이터 정리',
|
||||
cleanRemoteData: '원격 데이터 삭제',
|
||||
cleanDataHelper: '이 작업에서 생성된 백업 파일을 삭제합니다.',
|
||||
noLogs: '작업 출력이 아직 없습니다...',
|
||||
errPath: '백업 경로 [{0}] 오류, 다운로드할 수 없습니다!',
|
||||
@@ -1344,6 +1351,8 @@ const message = {
|
||||
existFileHelper: '업로드한 파일에 동일한 이름의 파일이 포함되어 있습니다. 덮어쓰시겠습니까?',
|
||||
existFileSize: '파일 크기 (새로운 -> 오래된)',
|
||||
existFileDirHelper: '선택한 파일/폴더에 동일한 이름이 이미 존재합니다. 신중하게 작업하세요!',
|
||||
coverDirHelper: '덮어쓸 폴더를 선택하면 대상 경로로 복사됩니다!',
|
||||
noSuchFile: '파일 또는 디렉터리를 찾을 수 없습니다. 확인 후 다시 시도하세요.',
|
||||
},
|
||||
ssh: {
|
||||
setting: '설정',
|
||||
@@ -2190,6 +2199,7 @@ const message = {
|
||||
'역방향 프록시 백엔드가 HTTPS 인 경우 원본 SNI 를 설정해야 할 수 있습니다. 자세한 내용은 CDN 서비스 제공자의 문서를 참조하세요.',
|
||||
huaweicloud: '화웨이 클라우드',
|
||||
rainyun: '레인윤',
|
||||
westCN: 'West CN',
|
||||
},
|
||||
php: {
|
||||
short_open_tag: '짧은 태그 지원',
|
||||
@@ -2526,6 +2536,29 @@ const message = {
|
||||
proxyHelper6: '프록시 구성을 비활성화하려면 웹사이트 목록에서 삭제할 수 있습니다.',
|
||||
whiteListHelper: '화이트리스트에 있는 IP만 접근 허용',
|
||||
},
|
||||
mcp: {
|
||||
server: 'MCP サーバー',
|
||||
create: 'サーバーを追加',
|
||||
edit: 'サーバーを編集',
|
||||
commandHelper: '例: npx -y {0}',
|
||||
baseUrl: '外部アクセスパス',
|
||||
baseUrlHelper: '例: http://192.168.1.2:8000',
|
||||
ssePath: 'SSE パス',
|
||||
ssePathHelper: '例: /sse, 他のサーバーと重複しないように注意してください',
|
||||
environment: '環境変数',
|
||||
envKey: '変数名',
|
||||
envValue: '変数値',
|
||||
externalUrl: '外部接続アドレス',
|
||||
operatorHelper: '{0} に {1} 操作を実行します、続行しますか?',
|
||||
domain: 'デフォルトアクセスアドレス',
|
||||
domainHelper: '例: 192.168.1.1 または example.com',
|
||||
bindDomain: 'ウェブサイトをバインド',
|
||||
commandPlaceHolder: '현재 npx 및 바이너리 시작 명령만 지원합니다',
|
||||
importMcpJson: 'MCP サーバー設定をインポート',
|
||||
importMcpJsonError: 'mcpServers 構造が正しくありません',
|
||||
bindDomainHelper:
|
||||
'웹사이트를 바인딩한 후, 설치된 모든 MCP 서버의 접근 주소를 수정하고 포트의 외부 접근을 닫습니다',
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@@ -193,6 +193,7 @@ const message = {
|
||||
rePassword: 'Pengesahan kata laluan tidak sepadan dengan kata laluan.',
|
||||
requiredInput: 'Ruangan ini wajib diisi.',
|
||||
requiredSelect: 'Pilih satu item dalam senarai',
|
||||
illegalChar: 'Suntikan aksara & ; $ \' ` ( ) " > < | tidak disokong buat masa ini',
|
||||
illegalInput: 'Ruangan ini tidak boleh mengandungi aksara tidak sah.',
|
||||
commonName:
|
||||
'Ruangan ini mesti bermula dengan aksara bukan khas dan mesti terdiri daripada aksara rumi, Cina, nombor, ".", "-", dan "_" dengan panjang 1-128 aksara.',
|
||||
@@ -339,6 +340,7 @@ const message = {
|
||||
ssl: 'Certificate | Certificates',
|
||||
database: 'Database | Databases',
|
||||
ai_tools: 'AI',
|
||||
mcp: 'MCP',
|
||||
container: 'Container | Containers',
|
||||
cronjob: 'Cron Job | Cron Jobs',
|
||||
host: 'Host | Hosts',
|
||||
@@ -435,6 +437,8 @@ const message = {
|
||||
permission: 'Kebenaran',
|
||||
permissionForIP: 'IP',
|
||||
permissionAll: 'Kesemuanya(%)',
|
||||
localhostHelper:
|
||||
'Mengkonfigurasi kebenaran pangkalan data sebagai "localhost" untuk penyebaran kontena akan menghalang akses luar ke kontena. Sila pilih dengan teliti!',
|
||||
databaseConnInfo: 'Maklumat sambungan',
|
||||
rootPassword: 'Kata laluan root',
|
||||
serviceName: 'Nama Perkhidmatan',
|
||||
@@ -446,6 +450,7 @@ const message = {
|
||||
remoteConnHelper:
|
||||
'Sambungan jauh ke MySQL sebagai pengguna root mungkin mempunyai risiko keselamatan. Oleh itu, lakukan operasi ini dengan berhati-hati.',
|
||||
changePassword: 'Kata laluan',
|
||||
changeConnHelper: 'Operasi ini akan mengubah pangkalan data semasa {0}. Adakah anda ingin meneruskan?',
|
||||
changePasswordHelper:
|
||||
'Pangkalan data telah dikaitkan dengan aplikasi. Menukar kata laluan akan menukar kata laluan pangkalan data aplikasi pada masa yang sama. Perubahan ini akan berkuat kuasa selepas aplikasi dimulakan semula.',
|
||||
|
||||
@@ -841,6 +846,7 @@ const message = {
|
||||
|
||||
repo: 'Pendaftaran',
|
||||
createRepo: 'Tambah',
|
||||
httpRepoHelper: 'Mengoperasikan repositori jenis HTTP memerlukan mulakan semula perkhidmatan Docker.',
|
||||
httpRepo:
|
||||
'Memilih protokol HTTP memerlukan memulakan semula perkhidmatan Docker untuk menambahkannya ke pendaftaran tidak selamat.',
|
||||
delInsecure: 'Padamkan pendaftaran tidak selamat',
|
||||
@@ -1023,6 +1029,7 @@ const message = {
|
||||
errHandle: 'Kegagalan pelaksanaan tugas cron',
|
||||
noRecord: 'Picu Tugas Cron, dan anda akan melihat rekod di sini.',
|
||||
cleanData: 'Bersihkan data',
|
||||
cleanRemoteData: 'Padam data jarak jauh',
|
||||
cleanDataHelper: 'Padam fail sandaran yang dijana semasa tugas ini.',
|
||||
noLogs: 'Tiada keluaran tugas lagi...',
|
||||
errPath: 'Laluan sandaran [{0}] salah, tidak boleh dimuat turun!',
|
||||
@@ -1400,7 +1407,9 @@ const message = {
|
||||
existFileTitle: 'Amaran fail dengan nama yang sama',
|
||||
existFileHelper: 'Fail yang dimuat naik mengandungi fail dengan nama yang sama. Adakah anda mahu menimpanya?',
|
||||
existFileSize: 'Saiz fail (baru -> lama)',
|
||||
existFileDirHelper: 'Fail/folder yang dipilih mempunyai nama yang sama. Sila berhati-hati!',
|
||||
existFileDirHelper: 'Fail/folder yang dipilih mempunyai nama yang sama. Sila berhati-hati!\n',
|
||||
coverDirHelper: 'Folder yang dipilih untuk ditimpa akan disalin ke laluan destinasi!',
|
||||
noSuchFile: 'Fail atau direktori tidak ditemui. Sila periksa dan cuba lagi.',
|
||||
},
|
||||
ssh: {
|
||||
setting: 'tetapan',
|
||||
@@ -2279,6 +2288,7 @@ const message = {
|
||||
'Apabila backend proksi terbalik adalah HTTPS, anda mungkin perlu menetapkan sumber SNI. Sila rujuk dokumentasi penyedia perkhidmatan CDN untuk butiran.',
|
||||
huaweicloud: 'Huawei Cloud',
|
||||
rainyun: 'Rain Yun',
|
||||
westCN: 'West CN',
|
||||
},
|
||||
php: {
|
||||
short_open_tag: 'Sokongan tag pendek',
|
||||
@@ -2625,6 +2635,29 @@ const message = {
|
||||
proxyHelper6: 'Untuk melumpuhkan konfigurasi proksi, anda boleh memadamnya dari senarai laman web.',
|
||||
whiteListHelper: 'Hadkan akses kepada hanya IP dalam senarai putih',
|
||||
},
|
||||
mcp: {
|
||||
server: 'Pelayan MCP',
|
||||
create: 'Tambah Pelayan',
|
||||
edit: 'Edit Pelayan',
|
||||
commandHelper: 'Contoh: npx -y {0}',
|
||||
baseUrl: 'Laluan Akses Luar',
|
||||
baseUrlHelper: 'Contoh: http://192.168.1.2:8000',
|
||||
ssePath: 'Laluan SSE',
|
||||
ssePathHelper: 'Contoh: /sse, berhati-hati jangan bertindan dengan pelayan lain',
|
||||
environment: 'Pemboleh Ubah Persekitaran',
|
||||
envKey: 'Nama Pemboleh Ubah',
|
||||
envValue: 'Nilai Pemboleh Ubah',
|
||||
externalUrl: 'Alamat Sambungan Luar',
|
||||
operatorHelper: 'Akan melakukan operasi {1} pada {0}, teruskan?',
|
||||
domain: 'Alamat Akses Lalai',
|
||||
domainHelper: 'Contoh: 192.168.1.1 atau example.com',
|
||||
bindDomain: 'Sematkan Laman Web',
|
||||
commandPlaceHolder: 'Kini hanya menyokong perintah pelancaran npx dan binari',
|
||||
importMcpJson: 'Import Konfigurasi Pelayan MCP',
|
||||
importMcpJsonError: 'Struktur mcpServers tidak betul',
|
||||
bindDomainHelper:
|
||||
'Setelah mengikat laman web, ia akan mengubah alamat akses semua Pelayan MCP yang dipasang dan menutup akses luaran ke pelabuhan',
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@@ -192,6 +192,7 @@ const message = {
|
||||
rePassword: 'A confirmação da senha não corresponde à senha.',
|
||||
requiredInput: 'Este campo é obrigatório.',
|
||||
requiredSelect: 'Selecione um item na lista',
|
||||
illegalChar: 'Atualmente não há suporte para injeção dos caracteres & ; $ \' ` ( ) " > < |',
|
||||
illegalInput: 'Este campo não deve conter caracteres ilegais.',
|
||||
commonName:
|
||||
'Este campo deve começar com caracteres não especiais e consistir em letras, números, ".", "-", e "_" com comprimento de 1-128.',
|
||||
@@ -337,6 +338,7 @@ const message = {
|
||||
ssl: 'Certificado | Certificados',
|
||||
database: 'Banco de Dados | Bancos de Dados',
|
||||
ai_tools: 'AI',
|
||||
mcp: 'MCP',
|
||||
container: 'Container | Containers',
|
||||
cronjob: 'Tarefa Cron | Tarefas Cron',
|
||||
host: 'Host | Hosts',
|
||||
@@ -433,6 +435,8 @@ const message = {
|
||||
permission: 'Permissões',
|
||||
permissionForIP: 'IP',
|
||||
permissionAll: 'Todos (% de)',
|
||||
localhostHelper:
|
||||
'Configurar permissões de banco de dados como "localhost" para implantação em contêiner impedirá o acesso externo ao contêiner. Por favor, escolha com cuidado!',
|
||||
databaseConnInfo: 'Informações de conexão',
|
||||
rootPassword: 'Senha root',
|
||||
serviceName: 'Nome do serviço',
|
||||
@@ -444,6 +448,7 @@ const message = {
|
||||
remoteConnHelper:
|
||||
'Conectar-se ao MySQL como usuário root pode representar riscos de segurança. Realize esta operação com cautela.',
|
||||
changePassword: 'Senha',
|
||||
changeConnHelper: 'Esta operação modificará o banco de dados atual {0}. Deseja continuar?',
|
||||
changePasswordHelper:
|
||||
'O banco de dados está associado a um aplicativo. Alterar a senha alterará a senha do banco de dados do aplicativo ao mesmo tempo. A mudança surtirá efeito após a reinicialização do aplicativo.',
|
||||
|
||||
@@ -837,6 +842,7 @@ const message = {
|
||||
|
||||
repo: 'Registries',
|
||||
createRepo: 'Adicionar',
|
||||
httpRepoHelper: 'Operar um repositório do tipo HTTP requer reinicialização do serviço Docker.',
|
||||
httpRepo:
|
||||
'Escolher o protocolo HTTP requer reiniciar o serviço Docker para adicioná-lo a registries inseguros.',
|
||||
delInsecure: 'Remover da lista de segurança',
|
||||
@@ -1014,6 +1020,7 @@ const message = {
|
||||
errHandle: 'Falha na execução do Cronjob',
|
||||
noRecord: 'Acione a tarefa Cron e você verá os registros aqui.',
|
||||
cleanData: 'Limpar dados',
|
||||
cleanRemoteData: 'Excluir dados remotos',
|
||||
cleanDataHelper: 'Excluir o arquivo de backup gerado durante esta tarefa.',
|
||||
noLogs: 'Ainda não há saída de tarefa...',
|
||||
errPath: 'Caminho de backup [{0}] com erro, não é possível fazer o download!',
|
||||
@@ -1387,7 +1394,9 @@ const message = {
|
||||
existFileTitle: 'Aviso de arquivo com o mesmo nome',
|
||||
existFileHelper: 'O arquivo enviado contém um arquivo com o mesmo nome. Deseja substituí-lo?',
|
||||
existFileSize: 'Tamanho do arquivo (novo -> antigo)',
|
||||
existFileDirHelper: 'O arquivo/pasta selecionado tem um nome duplicado. Por favor, prossiga com cautela!',
|
||||
existFileDirHelper: 'O arquivo/pasta selecionado tem um nome duplicado. Por favor, prossiga com cautela!\n',
|
||||
coverDirHelper: 'As pastas selecionadas para substituição serão copiadas para o caminho de destino!',
|
||||
noSuchFile: 'O arquivo ou diretório não foi encontrado. Por favor, verifique e tente novamente.',
|
||||
},
|
||||
ssh: {
|
||||
setting: 'configuração',
|
||||
@@ -2275,6 +2284,8 @@ const message = {
|
||||
sniHelper:
|
||||
'Quando o proxy reverso de backend for HTTPS, você pode precisar configurar o SNI de origem. Consulte a documentação do provedor de serviços CDN para mais detalhes.',
|
||||
huaweicloud: 'Huawei Cloud',
|
||||
rainyun: 'Rain Yun',
|
||||
westCN: 'West CN',
|
||||
},
|
||||
php: {
|
||||
short_open_tag: 'Suporte para short tags',
|
||||
@@ -2622,6 +2633,29 @@ const message = {
|
||||
proxyHelper6: 'Para desativar a configuração de proxy, você pode excluí-la da lista de sites.',
|
||||
whiteListHelper: 'Restringir o acesso apenas aos IPs na lista branca',
|
||||
},
|
||||
mcp: {
|
||||
server: 'Servidor MCP',
|
||||
create: 'Adicionar Servidor',
|
||||
edit: 'Editar Servidor',
|
||||
commandHelper: 'Por exemplo: npx -y {0}',
|
||||
baseUrl: 'Caminho de Acesso Externo',
|
||||
baseUrlHelper: 'Por exemplo: http://192.168.1.2:8000',
|
||||
ssePath: 'Caminho SSE',
|
||||
ssePathHelper: 'Por exemplo: /sse, tome cuidado para não duplicar com outros servidores',
|
||||
environment: 'Variáveis de Ambiente',
|
||||
envKey: 'Nome da Variável',
|
||||
envValue: 'Valor da Variável',
|
||||
externalUrl: 'Endereço de Conexão Externo',
|
||||
operatorHelper: 'Será realizada a operação {1} no {0}, continuar?',
|
||||
domain: 'Endereço de Acesso Padrão',
|
||||
domainHelper: 'Por exemplo: 192.168.1.1 ou example.com',
|
||||
bindDomain: 'Vincular Site',
|
||||
commandPlaceHolder: 'Atualmente, apenas comandos de inicialização npx e binários são suportados',
|
||||
importMcpJson: 'Importar Configuração do Servidor MCP',
|
||||
importMcpJsonError: 'A estrutura mcpServers está incorreta',
|
||||
bindDomainHelper:
|
||||
'Após vincular o site, ele modificará o endereço de acesso de todos os servidores MCP instalados e fechará o acesso externo às portas',
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@@ -113,8 +113,7 @@ const message = {
|
||||
refreshRate: 'Частота обновления',
|
||||
refreshRateUnit: 'Без обновления | {n} секунда/раз | {n} секунд/раз',
|
||||
selectColumn: 'Выбрать столбец',
|
||||
cover: 'Заменить',
|
||||
skip: 'Пропустить',
|
||||
serialNumber: 'Серийный номер',
|
||||
},
|
||||
loadingText: {
|
||||
Upgrading: 'Обновление системы, пожалуйста, подождите...',
|
||||
@@ -192,6 +191,7 @@ const message = {
|
||||
rePassword: 'Подтверждение пароля не совпадает с паролем.',
|
||||
requiredInput: 'Это поле обязательно для заполнения.',
|
||||
requiredSelect: 'Выберите элемент из списка',
|
||||
illegalChar: 'В настоящее время не поддерживается вставка символов & ; $ \' ` ( ) " > < |',
|
||||
illegalInput: 'Это поле не должно содержать недопустимых символов.',
|
||||
commonName:
|
||||
'Это поле должно начинаться с неспециальных символов и должно состоять из английских букв, китайских иероглифов, цифр, ".", "-" и "_" длиной 1-128.',
|
||||
@@ -334,6 +334,7 @@ const message = {
|
||||
ssl: 'Сертификат | Сертификаты',
|
||||
database: 'База данных | Базы данных',
|
||||
ai_tools: 'AI',
|
||||
mcp: 'MCP',
|
||||
container: 'Контейнер | Контейнеры',
|
||||
cronjob: 'Cron | Задачи Cron',
|
||||
host: 'Хост | Хосты',
|
||||
@@ -430,6 +431,8 @@ const message = {
|
||||
permission: 'Разрешения',
|
||||
permissionForIP: 'IP',
|
||||
permissionAll: 'Все (%)',
|
||||
localhostHelper:
|
||||
'Настройка разрешений базы данных как "localhost" для развертывания контейнера предотвратит внешний доступ к контейнеру. Пожалуйста, выбирайте внимательно!',
|
||||
databaseConnInfo: 'Информация о подключении',
|
||||
rootPassword: 'Пароль root',
|
||||
serviceName: 'Имя сервиса',
|
||||
@@ -441,6 +444,7 @@ const message = {
|
||||
remoteConnHelper:
|
||||
'Удаленное подключение к MySQL как пользователь root может иметь риски безопасности. Поэтому выполняйте эту операцию с осторожностью.',
|
||||
changePassword: 'Пароль',
|
||||
changeConnHelper: 'Эта операция изменит текущую базу данных {0}. Продолжить?',
|
||||
changePasswordHelper:
|
||||
'База данных была связана с приложением. Изменение пароля изменит пароль базы данных приложения одновременно. Изменение вступит в силу после перезапуска приложения.',
|
||||
|
||||
@@ -840,6 +844,7 @@ const message = {
|
||||
|
||||
repo: 'Реестры',
|
||||
createRepo: 'Добавить',
|
||||
httpRepoHelper: 'Работа с репозиторием HTTP-типа требует перезапуска службы Docker.',
|
||||
httpRepo: 'Выбор HTTP протокола требует перезапуска службы Docker для добавления в небезопасные реестры.',
|
||||
delInsecure: 'Удаление учетных данных',
|
||||
delInsecureHelper: 'Это перезапустит службу Docker для удаления из небезопасных реестров. Хотите продолжить?',
|
||||
@@ -1018,6 +1023,7 @@ const message = {
|
||||
errHandle: 'Сбой выполнения задачи Cron',
|
||||
noRecord: 'Запустите задачу Cron, и вы увидите записи здесь.',
|
||||
cleanData: 'Очистить данные',
|
||||
cleanRemoteData: 'Удалить удалённые данные',
|
||||
cleanDataHelper: 'Удалить файл резервной копии, созданный во время этой задачи.',
|
||||
noLogs: 'Пока нет вывода задачи...',
|
||||
errPath: 'Ошибка пути резервной копии [{0}], невозможно скачать!',
|
||||
@@ -1390,7 +1396,9 @@ const message = {
|
||||
existFileTitle: 'Предупреждение о файле с тем же именем',
|
||||
existFileHelper: 'Загруженный файл содержит файл с таким же именем. Заменить его?',
|
||||
existFileSize: 'Размер файла (новый -> старый)',
|
||||
existFileDirHelper: 'Выбранный файл/папка имеет дублирующееся имя. Пожалуйста, действуйте осторожно!',
|
||||
existFileDirHelper: 'Выбранный файл/папка имеет дублирующееся имя. Пожалуйста, действуйте осторожно!\n',
|
||||
coverDirHelper: 'При выборе перезаписываемой папки она будет скопирована в целевой путь!',
|
||||
noSuchFile: 'Файл или каталог не найдены. Пожалуйста, проверьте и повторите попытку.',
|
||||
},
|
||||
ssh: {
|
||||
setting: 'настройка',
|
||||
@@ -2277,6 +2285,7 @@ const message = {
|
||||
'Когда бэкенд обратного прокси использует HTTPS, может потребоваться установить origin SNI. Подробности см. в документации провайдера CDN.',
|
||||
huaweicloud: 'Huawei Cloud',
|
||||
rainyun: 'Rain Yun',
|
||||
westCN: 'West CN',
|
||||
},
|
||||
php: {
|
||||
short_open_tag: 'Поддержка коротких тегов',
|
||||
@@ -2620,6 +2629,29 @@ const message = {
|
||||
proxyHelper6: 'Чтобы отключить настройку прокси, вы можете удалить её из списка сайтов.',
|
||||
whiteListHelper: 'Ограничить доступ только для IP-адресов из белого списка',
|
||||
},
|
||||
mcp: {
|
||||
server: 'Сервер MCP',
|
||||
create: 'Добавить сервер',
|
||||
edit: 'Редактировать сервер',
|
||||
commandHelper: 'Например: npx -y {0}',
|
||||
baseUrl: 'Внешний путь доступа',
|
||||
baseUrlHelper: 'Например: http://192.168.1.2:8000',
|
||||
ssePath: 'Путь SSE',
|
||||
ssePathHelper: 'Например: /sse, будьте осторожны, чтобы не дублировать с другими серверами',
|
||||
environment: 'Переменные среды',
|
||||
envKey: 'Имя переменной',
|
||||
envValue: 'Значение переменной',
|
||||
externalUrl: 'Внешний адрес подключения',
|
||||
operatorHelper: 'Будет выполнена операция {1} на {0}, продолжить?',
|
||||
domain: 'Адрес доступа по умолчанию',
|
||||
domainHelper: 'Например: 192.168.1.1 или example.com',
|
||||
bindDomain: 'Привязать сайт',
|
||||
commandPlaceHolder: 'В настоящее время поддерживаются только команды запуска npx и двоичных файлов',
|
||||
importMcpJson: 'Импортировать конфигурацию сервера MCP',
|
||||
importMcpJsonError: 'Структура mcpServers некорректна',
|
||||
bindDomainHelper:
|
||||
'После привязки веб-сайта он изменит адрес доступа для всех установленных серверов MCP и закроет внешний доступ к портам',
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@@ -192,6 +192,7 @@ const message = {
|
||||
rePassword: '密碼不一致,請檢查後重新輸入',
|
||||
requiredInput: '請填寫必填項',
|
||||
requiredSelect: '請選擇必選項',
|
||||
illegalChar: '暫不支援注入字元 & ; $ \' ` ( ) " > < |',
|
||||
illegalInput: '輸入框中存在不合法字元',
|
||||
commonName: '支援非特殊字元開頭,英文、中文、數字、.-和_,長度1-128',
|
||||
userName: '支援英文、中文、數字和_,長度3-30',
|
||||
@@ -326,6 +327,7 @@ const message = {
|
||||
ssl: '證書',
|
||||
database: '資料庫',
|
||||
ai_tools: 'AI',
|
||||
mcp: 'MCP',
|
||||
container: '容器',
|
||||
cronjob: '計劃任務',
|
||||
host: '主機',
|
||||
@@ -374,7 +376,7 @@ const message = {
|
||||
|
||||
core: '物理核心',
|
||||
logicCore: '邏輯核心',
|
||||
loadAverage: '最近 {0} 分鐘平均負載',
|
||||
loadAverage: '最近 1 分鐘平均負載 | 最近 {n} 分鐘平均負載',
|
||||
load: '負載',
|
||||
mount: '掛載點',
|
||||
fileSystem: '檔案系統',
|
||||
@@ -418,6 +420,7 @@ const message = {
|
||||
permission: '權限',
|
||||
permissionForIP: '指定 IP',
|
||||
permissionAll: '所有人(%)',
|
||||
localhostHelper: '將容器部署的資料庫權限配置為"localhost"會導致容器外部無法存取,請謹慎選擇!',
|
||||
databaseConnInfo: '連接資訊',
|
||||
rootPassword: 'root 密碼',
|
||||
serviceName: '服務名稱',
|
||||
@@ -428,6 +431,7 @@ const message = {
|
||||
remoteHelper: '多個 ip 以逗號分隔,例:172.16.10.111,172.16.10.112',
|
||||
remoteConnHelper: 'root 帳號遠端連接 MySQL 有安全風險,開啟需謹慎!',
|
||||
changePassword: '改密',
|
||||
changeConnHelper: '此操作將修改當前資料庫 {0},是否繼續?',
|
||||
changePasswordHelper: '目前資料庫已經關聯應用,修改密碼將同步修改應用中資料庫密碼,修改後重啟生效。',
|
||||
|
||||
portHelper: '該埠為容器對外暴露埠,修改需要單獨儲存並且重啟容器!',
|
||||
@@ -801,6 +805,7 @@ const message = {
|
||||
|
||||
repo: '倉庫',
|
||||
createRepo: '新增倉庫',
|
||||
httpRepoHelper: '操作 HTTP 類型倉庫需要重啟 Docker 服務。',
|
||||
downloadUrl: '下載網址',
|
||||
imageRepo: '鏡像倉庫',
|
||||
repoHelper: '是否包含鏡像倉庫/組織/項目?',
|
||||
@@ -962,6 +967,7 @@ const message = {
|
||||
errHandle: '任務執行失敗',
|
||||
noRecord: '目前計劃任務暫未產生記錄',
|
||||
cleanData: '刪除備份檔案',
|
||||
cleanRemoteData: '刪除遠端備份檔案',
|
||||
cleanDataHelper: '刪除該任務執行過程中產生的備份檔案',
|
||||
noLogs: '暫無任務輸出...',
|
||||
errPath: '備份路徑 [{0}] 錯誤,無法下載!',
|
||||
@@ -1315,6 +1321,8 @@ const message = {
|
||||
existFileHelper: '上傳的檔案存在同名檔案,是否覆蓋?',
|
||||
existFileSize: '文件大小(新->舊)',
|
||||
existFileDirHelper: '選擇的檔案/資料夾存在同名,請謹慎操作!',
|
||||
coverDirHelper: '選取要覆蓋的資料夾,將複製到目標路徑!',
|
||||
noSuchFile: '找不到該檔案或目錄,請檢查後重試。',
|
||||
},
|
||||
ssh: {
|
||||
setting: '設定',
|
||||
@@ -2112,6 +2120,7 @@ const message = {
|
||||
sniHelper: '反代後端為 https 的時候可能需要設定回源 SNI,具體需要看 CDN 服務商文件',
|
||||
huaweicloud: '華為雲',
|
||||
rainyun: '雨雲',
|
||||
westCN: '西部數據',
|
||||
},
|
||||
php: {
|
||||
short_open_tag: '短標籤支援',
|
||||
@@ -2375,8 +2384,8 @@ const message = {
|
||||
env: '環境變數',
|
||||
noenv: '無',
|
||||
net: '網路連線',
|
||||
laddr: '源地址/埠',
|
||||
raddr: '目標地址/埠',
|
||||
laddr: '本地地址/埠',
|
||||
raddr: '远程地址/埠',
|
||||
stopProcess: '結束',
|
||||
viewDetails: '查看詳情',
|
||||
stopProcessWarn: '是否確定結束此行程 (PID:{0})?',
|
||||
@@ -2433,6 +2442,28 @@ const message = {
|
||||
proxyHelper6: '如需關閉代理配置,可以在網站列表中刪除',
|
||||
whiteListHelper: '限制僅白名單中的 IP 可瀏覽',
|
||||
},
|
||||
mcp: {
|
||||
server: 'MCP Server',
|
||||
create: '创建 MCP Server',
|
||||
edit: '編輯 MCP Server',
|
||||
commandHelper: '例如:npx -y {0}',
|
||||
baseUrl: '外部訪問路徑',
|
||||
baseUrlHelper: '例如:http://192.168.1.2:8000',
|
||||
ssePath: 'SSE 路徑',
|
||||
ssePathHelper: '例如:/sse,注意不要與其他 Server 重複',
|
||||
environment: '環境變數',
|
||||
envKey: '變數名',
|
||||
envValue: '變數值',
|
||||
externalUrl: '外部連接地址',
|
||||
operatorHelper: '將對 {0} 進行 {1} 操作,是否繼續?',
|
||||
domain: '默認訪問地址',
|
||||
domainHelper: '例如:192.168.1.1 或者 example.com',
|
||||
bindDomain: '綁定網站',
|
||||
commandPlaceHolder: '當前僅支持 npx 和 二進制啟動的命令',
|
||||
importMcpJson: '導入 MCP Server配置',
|
||||
importMcpJsonError: 'mcpServers 結構不正確',
|
||||
bindDomainHelper: '綁定網站之後會修改所有已安裝 MCP Server 的訪問地址,並關閉端口的外部訪問',
|
||||
},
|
||||
};
|
||||
export default {
|
||||
...fit2cloudTwLocale,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user