mirror of
https://gitee.com/goploy/goploy.git
synced 2024-11-29 18:57:59 +08:00
add nginx manage
This commit is contained in:
parent
e987fad091
commit
50b8f75971
@ -21,6 +21,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@ -59,6 +60,11 @@ func (s Server) Handler() []server.Route {
|
||||
server.NewRoute("/server/deleteProcess", http.MethodDelete, s.DeleteProcess).Permissions(config.DeleteServerProcess).LogFunc(middleware.AddOPLog),
|
||||
server.NewRoute("/server/execProcess", http.MethodPost, s.ExecProcess).Permissions(config.ShowServerProcessPage).LogFunc(middleware.AddOPLog),
|
||||
server.NewRoute("/server/execScript", http.MethodPost, s.ExecScript).Permissions(config.ShowServerScriptPage).LogFunc(middleware.AddOPLog),
|
||||
server.NewRoute("/server/getNginxConfigList", http.MethodGet, s.GetNginxConfigList).Permissions(config.ShowServerNginxPage),
|
||||
server.NewRoute("/server/manageNginx", http.MethodPost, s.ManageNginx).Permissions(config.ManageServerNginx).LogFunc(middleware.AddOPLog),
|
||||
server.NewRoute("/server/getNginxConfigContent", http.MethodPost, s.GetNginxConfContent).Permissions(config.ShowServerNginxPage),
|
||||
server.NewRoute("/server/editNginxConfig", http.MethodPut, s.EditNginxConfig).Permissions(config.EditNginxConfig).LogFunc(middleware.AddOPLog),
|
||||
server.NewRoute("/server/copyNginxConfig", http.MethodPut, s.CopyNginxConfig).Permissions(config.CopyNginxConfig).LogFunc(middleware.AddOPLog),
|
||||
}
|
||||
}
|
||||
|
||||
@ -1242,3 +1248,300 @@ func (Server) ExecScript(gp *server.Goploy) server.Response {
|
||||
}
|
||||
return response.JSON{Data: respData}
|
||||
}
|
||||
|
||||
func (Server) GetNginxConfigList(gp *server.Goploy) server.Response {
|
||||
serverID, err := strconv.ParseInt(gp.URLQuery.Get("serverId"), 10, 64)
|
||||
srv, err := (model.Server{ID: serverID}).GetData()
|
||||
if err != nil {
|
||||
return response.JSON{Code: response.Error, Message: err.Error()}
|
||||
}
|
||||
|
||||
client, err := srv.ToSSHConfig().Dial()
|
||||
if err != nil {
|
||||
return response.JSON{Code: response.Error, Message: err.Error()}
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
sftpClient, err := sftp.NewClient(client)
|
||||
if err != nil {
|
||||
return response.JSON{Code: response.Error, Message: err.Error()}
|
||||
}
|
||||
defer sftpClient.Close()
|
||||
|
||||
session, err := client.NewSession()
|
||||
if err != nil {
|
||||
return response.JSON{Code: response.Error, Message: err.Error()}
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
nginxPath := gp.URLQuery.Get("dir")
|
||||
|
||||
output, err := session.CombinedOutput(path.Join(nginxPath, "sbin", "nginx") + " -t")
|
||||
|
||||
if err != nil {
|
||||
return response.JSON{Code: response.Error, Message: fmt.Sprintf("output: %s", output)}
|
||||
}
|
||||
|
||||
configPath := ""
|
||||
lines := strings.Split(string(output), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "nginx: the configuration file ") && strings.Contains(line, "syntax is ok") {
|
||||
configPath = strings.TrimPrefix(line, "nginx: the configuration file ")
|
||||
configPath = strings.TrimSuffix(configPath, " syntax is ok")
|
||||
}
|
||||
}
|
||||
|
||||
if configPath == "" {
|
||||
return response.JSON{Code: response.Error, Message: "can not find nginx config path or config error"}
|
||||
}
|
||||
|
||||
configFile, err := sftpClient.Open(configPath)
|
||||
if err != nil {
|
||||
return response.JSON{Code: response.Error, Message: err.Error()}
|
||||
}
|
||||
defer configFile.Close()
|
||||
|
||||
configContent, err := io.ReadAll(configFile)
|
||||
if err != nil {
|
||||
return response.JSON{Code: response.Error, Message: err.Error()}
|
||||
}
|
||||
|
||||
configFileDir := path.Dir(configPath)
|
||||
var includeConfigPaths []string
|
||||
|
||||
// match the file path in the include directive
|
||||
re := regexp.MustCompile("(?i)include\\s+([\"']?)(.*?)([\"']?);")
|
||||
matches := re.FindAllSubmatch(configContent, -1)
|
||||
|
||||
for _, match := range matches {
|
||||
tmpPath := match[2]
|
||||
includeConfigPaths = append(includeConfigPaths, path.Join(configFileDir, string(tmpPath)))
|
||||
}
|
||||
|
||||
type fileInfo struct {
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
Mode string `json:"mode"`
|
||||
ModTime string `json:"modTime"`
|
||||
Dir string `json:"dir"`
|
||||
}
|
||||
|
||||
var fileList []fileInfo
|
||||
for _, includeConfigPath := range includeConfigPaths {
|
||||
fileInfos, err := sftpClient.Glob(includeConfigPath)
|
||||
if err != nil {
|
||||
return response.JSON{Code: response.Error, Message: err.Error()}
|
||||
} else {
|
||||
for _, f := range fileInfos {
|
||||
fileStat, err := sftpClient.Stat(f)
|
||||
if err != nil {
|
||||
return response.JSON{Code: response.Error, Message: err.Error()}
|
||||
}
|
||||
|
||||
if fileStat.Mode()&os.ModeSymlink != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if !fileStat.IsDir() {
|
||||
fileList = append(fileList, fileInfo{
|
||||
Name: fileStat.Name(),
|
||||
Size: fileStat.Size(),
|
||||
Mode: fileStat.Mode().String(),
|
||||
ModTime: fileStat.ModTime().Format("2006-01-02 15:04:05"),
|
||||
Dir: path.Dir(includeConfigPath),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response.JSON{
|
||||
Data: struct {
|
||||
List []fileInfo `json:"list"`
|
||||
}{List: fileList},
|
||||
}
|
||||
}
|
||||
|
||||
func (Server) ManageNginx(gp *server.Goploy) server.Response {
|
||||
type ReqData struct {
|
||||
ServerID int64 `json:"serverId" validate:"gt=0"`
|
||||
Path string `json:"path" validate:"required"`
|
||||
Command string `json:"command" validate:"required"`
|
||||
}
|
||||
var reqData ReqData
|
||||
if err := decodeJson(gp.Body, &reqData); err != nil {
|
||||
return response.JSON{Code: response.Error, Message: err.Error()}
|
||||
}
|
||||
|
||||
srv, err := (model.Server{ID: reqData.ServerID}).GetData()
|
||||
if err != nil {
|
||||
return response.JSON{Code: response.Error, Message: err.Error()}
|
||||
}
|
||||
|
||||
nginxPath := path.Join(reqData.Path, "sbin", "nginx")
|
||||
|
||||
script := ""
|
||||
switch reqData.Command {
|
||||
case "reload":
|
||||
script = nginxPath + " -s reload"
|
||||
case "stop":
|
||||
script = nginxPath + " -s stop"
|
||||
case "check":
|
||||
script = nginxPath + " -t"
|
||||
default:
|
||||
script = reqData.Command
|
||||
}
|
||||
|
||||
client, err := srv.ToSSHConfig().Dial()
|
||||
if err != nil {
|
||||
return response.JSON{Code: response.Error, Message: err.Error()}
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
session, err := client.NewSession()
|
||||
if err != nil {
|
||||
return response.JSON{Code: response.Error, Message: err.Error()}
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
output, err := session.CombinedOutput(script)
|
||||
if err != nil {
|
||||
return response.JSON{Code: response.Error, Message: err.Error()}
|
||||
}
|
||||
|
||||
log.Trace(fmt.Sprintf("%s exec nginx cmd %s, result %t, output: %s", gp.UserInfo.Name, script, err == nil, string(output)))
|
||||
return response.JSON{
|
||||
Data: struct {
|
||||
ExecRes bool `json:"execRes"`
|
||||
Output string `json:"output"`
|
||||
}{ExecRes: err == nil, Output: string(output)},
|
||||
}
|
||||
}
|
||||
|
||||
func (Server) GetNginxConfContent(gp *server.Goploy) server.Response {
|
||||
type ReqData struct {
|
||||
ServerID int64 `json:"serverId" validate:"gt=0"`
|
||||
Dir string `json:"dir" validate:"required"`
|
||||
Filename string `json:"filename" validate:"required"`
|
||||
}
|
||||
var reqData ReqData
|
||||
if err := decodeJson(gp.Body, &reqData); err != nil {
|
||||
return response.JSON{Code: response.Error, Message: err.Error()}
|
||||
}
|
||||
|
||||
srv, err := (model.Server{ID: reqData.ServerID}).GetData()
|
||||
if err != nil {
|
||||
return response.JSON{Code: response.Error, Message: err.Error()}
|
||||
}
|
||||
|
||||
client, err := srv.ToSSHConfig().Dial()
|
||||
if err != nil {
|
||||
return response.JSON{Code: response.Error, Message: err.Error()}
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
sftpClient, err := sftp.NewClient(client)
|
||||
if err != nil {
|
||||
return response.JSON{Code: response.Error, Message: err.Error()}
|
||||
}
|
||||
defer sftpClient.Close()
|
||||
|
||||
configFile, err := sftpClient.Open(path.Join(reqData.Dir, reqData.Filename))
|
||||
if err != nil {
|
||||
return response.JSON{Code: response.Error, Message: err.Error()}
|
||||
}
|
||||
defer configFile.Close()
|
||||
|
||||
configContent, err := io.ReadAll(configFile)
|
||||
if err != nil {
|
||||
return response.JSON{Code: response.Error, Message: err.Error()}
|
||||
}
|
||||
|
||||
return response.JSON{
|
||||
Data: struct {
|
||||
Content string `json:"content"`
|
||||
}{Content: string(configContent)},
|
||||
}
|
||||
}
|
||||
|
||||
func (Server) EditNginxConfig(gp *server.Goploy) server.Response {
|
||||
type ReqData struct {
|
||||
ServerID int64 `json:"serverId" validate:"gt=0"`
|
||||
Dir string `json:"dir" validate:"required"`
|
||||
Filename string `json:"filename" validate:"required"`
|
||||
Content string `json:"content" validate:"required"`
|
||||
}
|
||||
var reqData ReqData
|
||||
if err := decodeJson(gp.Body, &reqData); err != nil {
|
||||
return response.JSON{Code: response.Error, Message: err.Error()}
|
||||
}
|
||||
|
||||
srv, err := (model.Server{ID: reqData.ServerID}).GetData()
|
||||
if err != nil {
|
||||
return response.JSON{Code: response.Error, Message: err.Error()}
|
||||
}
|
||||
|
||||
client, err := srv.ToSSHConfig().Dial()
|
||||
if err != nil {
|
||||
return response.JSON{Code: response.Error, Message: err.Error()}
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
sftpClient, err := sftp.NewClient(client)
|
||||
if err != nil {
|
||||
return response.JSON{Code: response.Error, Message: err.Error()}
|
||||
}
|
||||
defer sftpClient.Close()
|
||||
|
||||
file, err := sftpClient.Create(path.Join(reqData.Dir, reqData.Filename))
|
||||
if err != nil {
|
||||
return response.JSON{Code: response.Error, Message: err.Error()}
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, err = file.Write([]byte(reqData.Content))
|
||||
if err != nil {
|
||||
return response.JSON{Code: response.Error, Message: err.Error()}
|
||||
}
|
||||
|
||||
return response.JSON{}
|
||||
}
|
||||
|
||||
func (Server) CopyNginxConfig(gp *server.Goploy) server.Response {
|
||||
type ReqData struct {
|
||||
ServerID int64 `json:"serverId" validate:"gt=0"`
|
||||
Dir string `json:"dir" validate:"required"`
|
||||
SrcName string `json:"srcName" validate:"required"`
|
||||
DstName string `json:"dstName" validate:"required"`
|
||||
}
|
||||
var reqData ReqData
|
||||
if err := decodeJson(gp.Body, &reqData); err != nil {
|
||||
return response.JSON{Code: response.Error, Message: err.Error()}
|
||||
}
|
||||
|
||||
srv, err := (model.Server{ID: reqData.ServerID}).GetData()
|
||||
if err != nil {
|
||||
return response.JSON{Code: response.Error, Message: err.Error()}
|
||||
}
|
||||
|
||||
client, err := srv.ToSSHConfig().Dial()
|
||||
if err != nil {
|
||||
return response.JSON{Code: response.Error, Message: err.Error()}
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
session, err := client.NewSession()
|
||||
if err != nil {
|
||||
return response.JSON{Code: response.Error, Message: err.Error()}
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
var sshOutbuf, sshErrbuf bytes.Buffer
|
||||
session.Stdout = &sshOutbuf
|
||||
session.Stderr = &sshErrbuf
|
||||
|
||||
if err = session.Run(fmt.Sprintf("cp %s %s", path.Join(reqData.Dir, reqData.SrcName), path.Join(reqData.Dir, reqData.DstName))); err != nil {
|
||||
return response.JSON{Code: response.Error, Message: "err: " + err.Error() + ", detail: " + sshErrbuf.String()}
|
||||
}
|
||||
return response.JSON{}
|
||||
}
|
||||
|
@ -84,4 +84,8 @@ const (
|
||||
ShowServerScriptPage = 77
|
||||
SFTPRenameFile = 78
|
||||
SFTPEditFile = 79
|
||||
ShowServerNginxPage = 80
|
||||
ManageServerNginx = 81
|
||||
EditNginxConfig = 82
|
||||
CopyNginxConfig = 83
|
||||
)
|
||||
|
@ -475,6 +475,10 @@ INSERT IGNORE INTO `permission`(`id`, `pid`, `name`, `sort`, `description`) VALU
|
||||
INSERT IGNORE INTO `permission`(`id`, `pid`, `name`, `sort`, `description`) VALUES (77, 23, 'ShowServerScriptPage', 0, '');
|
||||
INSERT IGNORE INTO `permission`(`id`, `pid`, `name`, `sort`, `description`) VALUES (78, 23, 'SFTPRenameFile', 0, '');
|
||||
INSERT IGNORE INTO `permission`(`id`, `pid`, `name`, `sort`, `description`) VALUES (79, 23, 'SFTPEditFile', 0, '');
|
||||
INSERT IGNORE INTO `permission`(`id`, `pid`, `name`, `sort`, `description`) VALUES (80, 23, 'ShowServerNginxPage', 0, '');
|
||||
INSERT IGNORE INTO `permission`(`id`, `pid`, `name`, `sort`, `description`) VALUES (81, 23, 'ManageServerNginx', 0, '');
|
||||
INSERT IGNORE INTO `permission`(`id`, `pid`, `name`, `sort`, `description`) VALUES (82, 23, 'EditNginxConfig', 0, '');
|
||||
INSERT IGNORE INTO `permission`(`id`, `pid`, `name`, `sort`, `description`) VALUES (83, 23, 'CopyNginxConfig', 0, '');
|
||||
INSERT IGNORE INTO `role_permission`(`role_id`, `permission_id`) VALUES (1, 13);
|
||||
INSERT IGNORE INTO `role_permission`(`role_id`, `permission_id`) VALUES (1, 14);
|
||||
INSERT IGNORE INTO `role_permission`(`role_id`, `permission_id`) VALUES (1, 15);
|
||||
|
@ -505,3 +505,98 @@ export class ServerRemoteCrontabList extends Request {
|
||||
this.param = param
|
||||
}
|
||||
}
|
||||
|
||||
export class ManageNginx extends Request {
|
||||
readonly url = '/server/manageNginx'
|
||||
readonly method = 'post'
|
||||
readonly timeout = 0
|
||||
public param: {
|
||||
serverId: number
|
||||
path: string
|
||||
command: string
|
||||
}
|
||||
public declare datagram: {
|
||||
execRes: boolean
|
||||
output: string
|
||||
}
|
||||
constructor(param: ManageNginx['param']) {
|
||||
super()
|
||||
this.param = param
|
||||
}
|
||||
}
|
||||
|
||||
export interface ServerNginxData {
|
||||
[key: string]: any
|
||||
modTime: string
|
||||
mode: string
|
||||
name: string
|
||||
size: number
|
||||
dir: string
|
||||
}
|
||||
|
||||
export class ServerNginxConfigList extends Request {
|
||||
readonly url = '/server/getNginxConfigList'
|
||||
readonly method = 'get'
|
||||
|
||||
public declare datagram: {
|
||||
list: ServerNginxData[]
|
||||
}
|
||||
|
||||
public param: {
|
||||
serverId: number
|
||||
dir: string
|
||||
}
|
||||
|
||||
constructor(param: ServerNginxConfigList['param']) {
|
||||
super()
|
||||
this.param = param
|
||||
}
|
||||
}
|
||||
|
||||
export class NginxConfigContent extends Request {
|
||||
readonly url = '/server/getNginxConfigContent'
|
||||
readonly method = 'post'
|
||||
public param: {
|
||||
serverId: number
|
||||
dir: string
|
||||
filename: string
|
||||
}
|
||||
public declare datagram: {
|
||||
content: string
|
||||
}
|
||||
constructor(param: NginxConfigContent['param']) {
|
||||
super()
|
||||
this.param = param
|
||||
}
|
||||
}
|
||||
|
||||
export class NginxConfigEdit extends Request {
|
||||
readonly url = '/server/editNginxConfig'
|
||||
readonly method = 'put'
|
||||
public param: {
|
||||
serverId: number
|
||||
dir: string
|
||||
filename: string
|
||||
content: string
|
||||
}
|
||||
constructor(param: NginxConfigEdit['param']) {
|
||||
super()
|
||||
this.param = param
|
||||
}
|
||||
}
|
||||
|
||||
export class NginxConfigCopy extends Request {
|
||||
readonly url = '/server/copyNginxConfig'
|
||||
readonly method = 'put'
|
||||
readonly timeout = 0
|
||||
public param: {
|
||||
serverId: number
|
||||
dir: string
|
||||
srcName: string
|
||||
dstName: string
|
||||
}
|
||||
constructor(param: NginxConfigCopy['param']) {
|
||||
super()
|
||||
this.param = param
|
||||
}
|
||||
}
|
||||
|
1
web/src/icons/svg/nginx.svg
Normal file
1
web/src/icons/svg/nginx.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1684729012224" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="983" width="360" height="360" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M512 0L68.48 256v512L512 1024l443.52-256V256L512 0z m256 707.84c0 30.08-27.562667 55.04-65.237333 55.04-26.922667 0-57.642667-10.88-76.842667-34.56l-256-304.682667v284.16c0 30.762667-24.32 55.04-54.357333 55.04H312.32c-30.762667 0-55.04-25.6-55.04-55.04V316.16c0-30.08 26.88-55.04 64-55.04 27.562667 0 58.88 10.88 78.08 34.56l254.72 304.682667V316.16c0-30.762667 25.6-55.04 55.04-55.04h3.2c30.72 0 55.04 25.6 55.04 55.04v391.68H768z" fill="" p-id="984"></path></svg>
|
After Width: | Height: | Size: 798 B |
@ -166,6 +166,7 @@
|
||||
"serverProcess": "Process",
|
||||
"serverCrontab": "Crontab UI",
|
||||
"serverCron": "Cron",
|
||||
"serverNginx": "Nginx",
|
||||
"namespace": "Namespace",
|
||||
"namespaceSetting": "NS setting",
|
||||
"roleSetting": "Role setting",
|
||||
@ -253,7 +254,11 @@
|
||||
"DeleteServerProcess": "Delete the server process",
|
||||
"SFTPTransferFile": "Can transfer file via SFTP",
|
||||
"SFTPDeleteFile": "Can delete file via SFTP",
|
||||
"ShowServerScriptPage": "Can execute script"
|
||||
"ShowServerScriptPage": "Can execute script",
|
||||
"ShowServerNginxPage": "See the server nginx page",
|
||||
"ManageServerNginx": "Manage nginx process",
|
||||
"EditNginxConfig": "Edit the nginx config",
|
||||
"CopyNginxConfig": "Copy the nginx config"
|
||||
},
|
||||
"tagsView": {
|
||||
"refresh": "Refresh",
|
||||
@ -306,7 +311,9 @@
|
||||
"advance": "Advanced setting",
|
||||
"transferFile": "Transfer file",
|
||||
"sftpFileCount": "items",
|
||||
"saveTemplate": "Save template"
|
||||
"saveTemplate": "Save template",
|
||||
"execTips": "Exec command {command}?",
|
||||
"nginxStartTips": "Input your nginx start command"
|
||||
},
|
||||
"monitorPage": {
|
||||
"defaultServer": "Follow Host",
|
||||
|
@ -148,6 +148,7 @@
|
||||
"serverProcess": "进程管理",
|
||||
"serverCrontab": "Crontab UI",
|
||||
"serverCron": "定时任务",
|
||||
"serverNginx": "Nginx管理",
|
||||
"namespace": "空间",
|
||||
"namespaceSetting": "空间设置",
|
||||
"roleSetting": "角色设置",
|
||||
@ -236,7 +237,11 @@
|
||||
"ShowOperationLogPage": "查看操作日志",
|
||||
"SFTPTransferFile": "SFTP传输文件",
|
||||
"SFTPDeleteFile": "SFTP删除文件",
|
||||
"ShowServerScriptPage": "执行脚本"
|
||||
"ShowServerScriptPage": "执行脚本",
|
||||
"ShowServerNginxPage": "查看Nginx管理",
|
||||
"ManageServerNginx": "Nginx进程管理",
|
||||
"EditNginxConfig": "编辑Nginx配置文件",
|
||||
"CopyNginxConfig": "复制Nginx配置文件"
|
||||
},
|
||||
"tagsView": {
|
||||
"refresh": "刷新",
|
||||
@ -289,7 +294,9 @@
|
||||
"advance": "高级选项",
|
||||
"transferFile": "传输文件",
|
||||
"sftpFileCount": "个项目",
|
||||
"saveTemplate": "保存模板"
|
||||
"saveTemplate": "保存模板",
|
||||
"execTips": "执行{command}命令?",
|
||||
"nginxStartTips": "请输入nginx的启动命令"
|
||||
},
|
||||
"monitorPage": {
|
||||
"scriptMode": "脚本类型",
|
||||
|
@ -78,4 +78,8 @@ export default Object.freeze({
|
||||
ShowServerScriptPage: 77,
|
||||
SFTPRenameFile: 78,
|
||||
SFTPEditFile: 79,
|
||||
ShowServerNginxPage: 80,
|
||||
ManageServerNginx: 81,
|
||||
EditNginxConfig: 82,
|
||||
CopyNginxConfig: 83,
|
||||
})
|
||||
|
@ -156,6 +156,16 @@ export default <RouteRecordRaw[]>[
|
||||
permissions: [permission.ShowServerMonitorPage],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'nginx',
|
||||
name: 'ServerNginx',
|
||||
component: () => import('@/views/server/nginx/index.vue'),
|
||||
meta: {
|
||||
title: 'serverNginx',
|
||||
icon: 'nginx',
|
||||
permissions: [permission.ShowServerNginxPage],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
479
web/src/views/server/nginx/explorer.vue
Normal file
479
web/src/views/server/nginx/explorer.vue
Normal file
@ -0,0 +1,479 @@
|
||||
<template>
|
||||
<el-row class="nginx-container">
|
||||
<el-row class="nav" align="middle">
|
||||
<el-row class="nav-path" style="flex: 1">
|
||||
<el-input
|
||||
v-model="dir"
|
||||
placeholder="Please input nginx absolute path"
|
||||
class="input-with-select"
|
||||
@keyup.enter="dirOpen(dir)"
|
||||
>
|
||||
<template #append>
|
||||
<el-button :icon="RefreshRight" @click="dirOpen(dir)" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-row>
|
||||
<el-row style="margin-left: 10px">
|
||||
<el-button
|
||||
:loading="commandLoading"
|
||||
type="warning"
|
||||
@click="handleNginxCmd(dir, 'check')"
|
||||
>
|
||||
check
|
||||
</el-button>
|
||||
<el-button
|
||||
:loading="commandLoading"
|
||||
type="primary"
|
||||
@click="handleNginxCmd(dir, 'reload')"
|
||||
>
|
||||
reload
|
||||
</el-button>
|
||||
<el-button
|
||||
:loading="commandLoading"
|
||||
type="success"
|
||||
@click="handleNginxCmd(dir, 'start')"
|
||||
>
|
||||
start
|
||||
</el-button>
|
||||
<el-button
|
||||
:loading="commandLoading"
|
||||
type="danger"
|
||||
@click="handleNginxCmd(dir, 'stop')"
|
||||
>
|
||||
stop
|
||||
</el-button>
|
||||
</el-row>
|
||||
</el-row>
|
||||
<el-row class="operator">
|
||||
<el-row class="nav-search" align="middle">
|
||||
<el-dropdown
|
||||
:disabled="fileFilteredList.length === 0"
|
||||
@command="handleSort"
|
||||
>
|
||||
<el-button text :icon="Sort">
|
||||
{{ $t('sort') }}
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="default">
|
||||
{{ $t('default') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="nameAsc">
|
||||
{{ $t('name') }} {{ $t('asc') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="nameDesc">
|
||||
{{ $t('name') }} {{ $t('desc') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="sizeAsc">
|
||||
{{ $t('size') }} {{ $t('asc') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="sizeDesc">
|
||||
{{ $t('size') }} {{ $t('desc') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="modTimeAsc">
|
||||
{{ $t('modifiedTime') }} {{ $t('asc') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="modTimeDesc">
|
||||
{{ $t('modifiedTime') }} {{ $t('desc') }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-input
|
||||
v-model="input"
|
||||
placeholder="Filter file"
|
||||
style="flex: 1"
|
||||
@input="filterFile"
|
||||
/>
|
||||
</el-row>
|
||||
</el-row>
|
||||
<el-row v-loading="fileListLoading" class="files">
|
||||
<div style="width: 100%">
|
||||
<el-empty
|
||||
v-show="fileFilteredList.length === 0"
|
||||
description="No result"
|
||||
></el-empty>
|
||||
<el-table
|
||||
v-show="fileFilteredList.length !== 0"
|
||||
v-loading="fileListLoading"
|
||||
height="100%"
|
||||
highlight-current-row
|
||||
:data="fileFilteredList"
|
||||
>
|
||||
<el-table-column prop="name" :label="$t('name')" min-width="120" />
|
||||
<el-table-column
|
||||
prop="modTime"
|
||||
:label="$t('updateTime')"
|
||||
min-width="100"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="operation"
|
||||
:label="$t('op')"
|
||||
width="180"
|
||||
align="center"
|
||||
>
|
||||
<template #default="scope">
|
||||
<Button
|
||||
type="primary"
|
||||
:loading="EditProps.editButtonLoading"
|
||||
:permissions="[permission.EditNginxConfig]"
|
||||
@click="handleEdit(scope.row)"
|
||||
>
|
||||
{{ $t('edit') }}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
:permissions="[permission.CopyNginxConfig]"
|
||||
@click="handleCopy(scope.row)"
|
||||
>
|
||||
{{ $t('copy') }}
|
||||
</Button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-row>
|
||||
<el-row class="footer" justify="space-between">
|
||||
<div>
|
||||
{{ fileFilteredList.length }} {{ $t('serverPage.sftpFileCount') }}
|
||||
</div>
|
||||
</el-row>
|
||||
<el-dialog
|
||||
v-model="editDialogVisible"
|
||||
:title="$t('edit')"
|
||||
:close-on-click-modal="false"
|
||||
:fullscreen="$store.state.app.device === 'mobile'"
|
||||
@close="editDialogVisible = false"
|
||||
>
|
||||
<v-ace-editor
|
||||
v-model:value="fileContent"
|
||||
lang="nginx"
|
||||
:theme="isDark ? 'one_dark' : 'github'"
|
||||
style="height: 500px; width: 100%"
|
||||
/>
|
||||
<template #footer>
|
||||
<el-button
|
||||
:disabled="EditProps.disabled"
|
||||
@click="editDialogVisible = false"
|
||||
>
|
||||
{{ $t('cancel') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
:loading="EditProps.disabled"
|
||||
type="primary"
|
||||
@click="editConfig"
|
||||
>
|
||||
{{ $t('confirm') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'NginxExplorer' }
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import { RefreshRight, Sort } from '@element-plus/icons-vue'
|
||||
import {
|
||||
ServerData,
|
||||
ServerNginxConfigList,
|
||||
ServerNginxData,
|
||||
NginxConfigContent,
|
||||
NginxConfigEdit,
|
||||
NginxConfigCopy,
|
||||
} from '@/api/server'
|
||||
import permission from '@/permission'
|
||||
import { Button } from '@/components/Permission'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ref, PropType } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ManageNginx } from '@/api/server'
|
||||
import { useDark } from '@vueuse/core'
|
||||
import { VAceEditor } from 'vue3-ace-editor'
|
||||
import * as ace from 'ace-builds/src-noconflict/ace'
|
||||
ace.config.set(
|
||||
'basePath',
|
||||
'https://cdn.jsdelivr.net/npm/ace-builds@' + ace.version + '/src-noconflict/'
|
||||
)
|
||||
ace.config.set(
|
||||
'themePath',
|
||||
'https://cdn.jsdelivr.net/npm/ace-builds@' + ace.version + '/src-noconflict/'
|
||||
)
|
||||
const { t } = useI18n()
|
||||
|
||||
const emit = defineEmits(['dir-change'])
|
||||
const props = defineProps({
|
||||
uuid: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
server: {
|
||||
type: Object as PropType<ServerData>,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const serverId = ref(props.server.id)
|
||||
const dir = ref('')
|
||||
const lastDir = ref('')
|
||||
const fileListLoading = ref(false)
|
||||
const fileList = ref<ServerNginxData[]>([])
|
||||
const fileFilteredList = ref<ServerNginxData[]>([])
|
||||
const input = ref('')
|
||||
const fileContent = ref('')
|
||||
const editDialogVisible = ref(false)
|
||||
const isDark = useDark()
|
||||
const EditProps = ref({
|
||||
editButtonLoading: false,
|
||||
disabled: false,
|
||||
selectedConfig: {} as ServerNginxData,
|
||||
})
|
||||
|
||||
const commandLoading = ref(false)
|
||||
|
||||
function handleSort(command: string) {
|
||||
let compareFunc = (
|
||||
fileA: ServerNginxData,
|
||||
fileB: ServerNginxData
|
||||
): number => {
|
||||
return 0
|
||||
}
|
||||
switch (command) {
|
||||
case 'nameAsc':
|
||||
compareFunc = (
|
||||
fileA: ServerNginxData,
|
||||
fileB: ServerNginxData
|
||||
): number => {
|
||||
return fileA.name.localeCompare(fileB.name)
|
||||
}
|
||||
break
|
||||
case 'nameDesc':
|
||||
compareFunc = (
|
||||
fileA: ServerNginxData,
|
||||
fileB: ServerNginxData
|
||||
): number => {
|
||||
return fileB.name.localeCompare(fileA.name)
|
||||
}
|
||||
break
|
||||
case 'sizeAsc':
|
||||
compareFunc = (
|
||||
fileA: ServerNginxData,
|
||||
fileB: ServerNginxData
|
||||
): number => {
|
||||
return fileA.size - fileB.size
|
||||
}
|
||||
break
|
||||
case 'sizeDesc':
|
||||
compareFunc = (
|
||||
fileA: ServerNginxData,
|
||||
fileB: ServerNginxData
|
||||
): number => {
|
||||
return fileB.size - fileA.size
|
||||
}
|
||||
break
|
||||
case 'modTimeAsc':
|
||||
compareFunc = (
|
||||
fileA: ServerNginxData,
|
||||
fileB: ServerNginxData
|
||||
): number => {
|
||||
return (
|
||||
new Date(fileA.modTime).getTime() - new Date(fileB.modTime).getTime()
|
||||
)
|
||||
}
|
||||
break
|
||||
case 'modTimeDesc':
|
||||
compareFunc = (
|
||||
fileA: ServerNginxData,
|
||||
fileB: ServerNginxData
|
||||
): number => {
|
||||
return (
|
||||
new Date(fileB.modTime).getTime() - new Date(fileA.modTime).getTime()
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
fileFilteredList.value.sort(compareFunc)
|
||||
}
|
||||
|
||||
function dirOpen(dir: string) {
|
||||
lastDir.value = dir
|
||||
emit('dir-change', dir)
|
||||
getNginxConfigList(dir)
|
||||
}
|
||||
|
||||
function filterFile(value: string) {
|
||||
fileFilteredList.value = fileList.value.filter((file) =>
|
||||
file.name.includes(value)
|
||||
)
|
||||
}
|
||||
|
||||
function handleCopy(data: ServerNginxData) {
|
||||
ElMessageBox.prompt('', t('copy') + ' ' + data.name, {
|
||||
confirmButtonText: t('confirm'),
|
||||
cancelButtonText: t('cancel'),
|
||||
inputPattern: /.+/,
|
||||
inputErrorMessage: 'Name required',
|
||||
})
|
||||
.then(({ value }) => {
|
||||
fileListLoading.value = true
|
||||
new NginxConfigCopy({
|
||||
serverId: serverId.value,
|
||||
dir: data.dir,
|
||||
srcName: data.name,
|
||||
dstName: value,
|
||||
})
|
||||
.request()
|
||||
.then(() => {
|
||||
getNginxConfigList(lastDir.value)
|
||||
})
|
||||
.finally(() => {
|
||||
fileListLoading.value = false
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('Cancel')
|
||||
})
|
||||
}
|
||||
|
||||
function handleEdit(data: any) {
|
||||
EditProps.value.editButtonLoading = true
|
||||
EditProps.value.selectedConfig = data
|
||||
new NginxConfigContent({
|
||||
serverId: serverId.value,
|
||||
dir: data.dir,
|
||||
filename: data.name,
|
||||
})
|
||||
.request()
|
||||
.then((response) => {
|
||||
editDialogVisible.value = true
|
||||
fileContent.value = response.data.content
|
||||
})
|
||||
.finally(() => {
|
||||
EditProps.value.editButtonLoading = false
|
||||
})
|
||||
}
|
||||
|
||||
function getNginxConfigList(dir: string) {
|
||||
fileListLoading.value = true
|
||||
fileList.value = []
|
||||
new ServerNginxConfigList({ serverId: serverId.value, dir: dir })
|
||||
.request()
|
||||
.then((response) => {
|
||||
fileFilteredList.value = fileList.value = response.data.list
|
||||
})
|
||||
.finally(() => {
|
||||
fileListLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
async function handleNginxCmd(dir: string, command: string) {
|
||||
if (command === 'start') {
|
||||
await ElMessageBox.prompt(t('serverPage.nginxStartTips'), 'start', {
|
||||
confirmButtonText: t('confirm'),
|
||||
cancelButtonText: t('cancel'),
|
||||
inputPattern: /.+/,
|
||||
inputErrorMessage: 'command required',
|
||||
})
|
||||
.then(({ value }) => {
|
||||
command = value
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('Cancel')
|
||||
command = ''
|
||||
})
|
||||
}
|
||||
if (command !== '') {
|
||||
ElMessageBox.confirm(t('serverPage.execTips', { command }), t('tips'), {
|
||||
confirmButtonText: t('confirm'),
|
||||
cancelButtonText: t('cancel'),
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => {
|
||||
commandLoading.value = true
|
||||
new ManageNginx({
|
||||
serverId: serverId.value,
|
||||
path: dir,
|
||||
command,
|
||||
})
|
||||
.request()
|
||||
.then((response) => {
|
||||
ElMessage.success(
|
||||
response.data.output === '' ? 'Success' : response.data.output
|
||||
)
|
||||
})
|
||||
.finally(() => {
|
||||
commandLoading.value = false
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('Cancel')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function editConfig() {
|
||||
const file = EditProps.value.selectedConfig
|
||||
EditProps.value.disabled = true
|
||||
new NginxConfigEdit({
|
||||
serverId: serverId.value,
|
||||
dir: file.dir,
|
||||
content: fileContent.value,
|
||||
filename: file.name,
|
||||
})
|
||||
.request()
|
||||
.then(() => {
|
||||
editDialogVisible.value = false
|
||||
ElMessage.success('Success')
|
||||
})
|
||||
.finally(() => {
|
||||
EditProps.value.disabled = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/mixin.scss';
|
||||
.nginx-container {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
min-height: 1px;
|
||||
.nav {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
}
|
||||
.operator {
|
||||
padding: 5px 10px;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
.nav-search {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
.files {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
@include scrollBar();
|
||||
}
|
||||
.footer {
|
||||
padding: 8px 15px;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
@media only screen and (max-device-width: 400px) {
|
||||
.nginx-container {
|
||||
.nav {
|
||||
flex-direction: column;
|
||||
.el-row {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="scss">
|
||||
.file-detail .el-dialog__body {
|
||||
padding: 5px 20px;
|
||||
}
|
||||
</style>
|
191
web/src/views/server/nginx/index.vue
Normal file
191
web/src/views/server/nginx/index.vue
Normal file
@ -0,0 +1,191 @@
|
||||
<template>
|
||||
<el-row class="app-container">
|
||||
<el-row class="tabs-container">
|
||||
<el-scrollbar style="width: 100%">
|
||||
<el-row class="tabs" justify="start" align="middle">
|
||||
<div
|
||||
v-for="(item, index) in serverList"
|
||||
:key="item.uuid"
|
||||
class="tabs-item"
|
||||
:class="item.uuid === currentUUID ? 'tabs-item-selected' : ''"
|
||||
>
|
||||
<el-row>
|
||||
<div class="tabs-item-serial" @click="selectTab(item)">
|
||||
{{ index + 1 }}
|
||||
</div>
|
||||
<div
|
||||
class="tabs-item-name"
|
||||
:title="`${item.server.name} : ${item.dir}`"
|
||||
@click="selectTab(item)"
|
||||
>
|
||||
{{ item.server.name }} : {{ item.dir }}
|
||||
</div>
|
||||
<el-button
|
||||
link
|
||||
style="font-size: 14px; padding-left: 8px"
|
||||
@click="deleteTab(item, index)"
|
||||
>
|
||||
x
|
||||
</el-button>
|
||||
</el-row>
|
||||
{{ item }}
|
||||
</div>
|
||||
|
||||
<div class="tabs-plus">
|
||||
<el-select
|
||||
v-model="serverId"
|
||||
style="width: 200px"
|
||||
filterable
|
||||
clearable
|
||||
@change="selectServer"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in serverOption"
|
||||
:key="item.id"
|
||||
:label="item.label"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="tabs-placeholder"></div>
|
||||
</el-row>
|
||||
</el-scrollbar>
|
||||
</el-row>
|
||||
<explorer
|
||||
v-for="item in serverList"
|
||||
v-show="item.uuid === currentUUID"
|
||||
:key="item.uuid"
|
||||
:server="item.server"
|
||||
@dir-change="handleDirChange"
|
||||
></explorer>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'ServerNginx' }
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import explorer from './explorer.vue'
|
||||
import { ServerOption, ServerData } from '@/api/server'
|
||||
import { ref } from 'vue'
|
||||
interface nginx {
|
||||
uuid: number
|
||||
server: ServerData
|
||||
dir: string
|
||||
}
|
||||
const currentUUID = ref(0)
|
||||
const serverOption = ref<ServerOption['datagram']['list']>([])
|
||||
const serverId = ref('')
|
||||
const serverList = ref<nginx[]>([])
|
||||
const selectedNginx = ref<nginx>({} as nginx)
|
||||
|
||||
function handleDirChange(dir: string) {
|
||||
selectedNginx.value.dir = dir
|
||||
}
|
||||
|
||||
getServerOption()
|
||||
|
||||
function getServerOption() {
|
||||
new ServerOption().request().then((response) => {
|
||||
serverOption.value = response.data.list
|
||||
})
|
||||
}
|
||||
|
||||
function selectTab(nginx: nginx) {
|
||||
currentUUID.value = nginx.uuid
|
||||
selectedNginx.value = nginx
|
||||
}
|
||||
|
||||
function deleteTab(nginx: nginx, index: number) {
|
||||
serverList.value.splice(index, 1)
|
||||
if (currentUUID.value === nginx.uuid) {
|
||||
currentUUID.value =
|
||||
serverList.value.length === 0
|
||||
? 0
|
||||
: serverList.value[serverList.value.length - 1].uuid
|
||||
}
|
||||
}
|
||||
|
||||
function selectServer(value: number) {
|
||||
const server =
|
||||
serverOption.value.find((_) => _.id === value) || ({} as ServerData)
|
||||
if (serverList.value.length === 0) {
|
||||
currentUUID.value = 0
|
||||
} else {
|
||||
currentUUID.value = serverList.value[serverList.value.length - 1].uuid + 1
|
||||
}
|
||||
const serverTab = { uuid: currentUUID.value, server, dir: '' }
|
||||
serverList.value.push(serverTab)
|
||||
selectTab(serverTab)
|
||||
serverId.value = ''
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/mixin.scss';
|
||||
.app-container {
|
||||
background-color: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color);
|
||||
}
|
||||
.tabs-container {
|
||||
width: 100%;
|
||||
height: 46px;
|
||||
.tabs {
|
||||
height: auto;
|
||||
flex-wrap: nowrap;
|
||||
&-item {
|
||||
font-size: 14px;
|
||||
flex-shrink: 0;
|
||||
width: 195px;
|
||||
height: 46px;
|
||||
line-height: 46px;
|
||||
padding: 0 10px;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
background-color: var(--el-disabled-bg-color);
|
||||
&:not(:last-child) {
|
||||
border-right: 1px solid var(--el-border-color);
|
||||
}
|
||||
&-serial {
|
||||
cursor: pointer;
|
||||
color: var(--el-text-color-secondary);
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
width: 20px;
|
||||
}
|
||||
&-name {
|
||||
flex: 1;
|
||||
cursor: pointer;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
padding-left: 5px;
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
}
|
||||
&-selected {
|
||||
border-bottom: none;
|
||||
background-color: var(--el-bg-color);
|
||||
}
|
||||
}
|
||||
&-placeholder {
|
||||
height: 46px;
|
||||
flex: 1;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
}
|
||||
&-plus {
|
||||
text-align: center;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
border-right: 1px solid var(--el-border-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="scss">
|
||||
.tabs-plus {
|
||||
.el-input__wrapper {
|
||||
border-radius: 0px !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
.el-input__inner {
|
||||
height: 43px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
Loading…
Reference in New Issue
Block a user