mirror of
https://github.com/tnb-labs/panel.git
synced 2026-08-30 17:05:19 +08:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 577fb370a7 | |||
| 7ef8fc6787 | |||
| e5ba1375b1 | |||
| e8a01e2d04 | |||
| bdee27541c | |||
| f807da175d | |||
| ae29541d02 | |||
| 0f844ac3ea | |||
| 744d0b9617 | |||
| 1bea44146f | |||
| a7a68f6b34 | |||
| 80bfe50335 | |||
| f235492f8b | |||
| e1bcabec5d | |||
| 8a03006772 | |||
| e497f5f7d7 | |||
| 5668bc1afd | |||
| bf9c0bc12e | |||
| 7677c3b50e | |||
| 82a74a538b | |||
| d84e3f75e3 | |||
| ce155af56b | |||
| ebc5fb9796 | |||
| 5e237a6bce | |||
| 567663a6f5 |
@@ -36,21 +36,29 @@
|
||||
|
||||
## 运行环境
|
||||
|
||||
耗子面板支持 `amd64` | `arm64` 架构下的主流系统。
|
||||
耗子面板支持 `amd64` | `arm64` 架构下的主流系统,下表中的系统均已测试 LNMP 环境安装。
|
||||
|
||||
优先建议使用 `AlmaLinux` | `RockyLinux`。
|
||||
优先建议使用标注**推荐**的系统,无特殊情况不建议使用标注**不推荐**的系统。
|
||||
|
||||
不在下表中的其他系统,可自行尝试安装,但不保证能够正常运行,且不提供技术支持。
|
||||
不在下表中的其他系统,可自行尝试安装,但不提供技术支持(接受相关 PR 提交)。
|
||||
|
||||
| 系统 | 版本 |
|
||||
|------------|----|
|
||||
| RHEL | 9 |
|
||||
| AlmaLinux | 9 |
|
||||
| RockyLinux | 9 |
|
||||
| Ubuntu | 22 |
|
||||
| Ubuntu | 24 |
|
||||
| Debian | 11 |
|
||||
| Debian | 12 |
|
||||
| 系统 | 版本 | 备注 |
|
||||
|---------------------|-----|-----|
|
||||
| AlmaLinux | 9 | 推荐 |
|
||||
| AlmaLinux | 8 | 不推荐 |
|
||||
| RockyLinux | 9 | |
|
||||
| RockyLinux | 8 | 不推荐 |
|
||||
| CentOS Stream | 9 | 不推荐 |
|
||||
| CentOS Stream | 8 | 不推荐 |
|
||||
| Ubuntu | 24 | 推荐 |
|
||||
| Ubuntu | 22 | |
|
||||
| Debian | 12 | 推荐 |
|
||||
| Debian | 11 | |
|
||||
| OpenCloudOS | 9 | |
|
||||
| TencentOS Server | 4 | |
|
||||
| TencentOS Server | 3.1 | 不推荐 |
|
||||
| Alibaba Cloud Linux | 3.2 | 不推荐 |
|
||||
| Anolis | 8 | 不推荐 |
|
||||
|
||||
随着系统版本的不断更新,我们亦可能会终止部分过于老旧的系统的支持,以保证面板的健壮性。
|
||||
|
||||
|
||||
+20
-12
@@ -36,21 +36,29 @@ Communication QQ group: [12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | For
|
||||
|
||||
## Operating Environment
|
||||
|
||||
Rat Panel supports mainstream systems under the `amd64` | `arm64` architecture.
|
||||
Rat Panel supports mainstream systems under the `amd64` | `arm64` architecture, and the systems in the table below have been tested for LNMP environment installation.
|
||||
|
||||
It is recommended to use `AlmaLinux` | `RockyLinux` first.
|
||||
It is recommended to use the systems marked **Recommended** first, and not recommended to use the systems marked **Not recommended** without special circumstances.
|
||||
|
||||
For other systems not listed in the table below, you can try to install them by yourself, but there is no guarantee that they will run normally, and no technical support will be provided.
|
||||
For other systems not listed in the table below, you can try to install them by yourself, but there is no technical support will be provided (accept related PR submissions).
|
||||
|
||||
| OS | Version |
|
||||
|------------|---------|
|
||||
| RHEL | 9 |
|
||||
| RockyLinux | 9 |
|
||||
| AlmaLinux | 9 |
|
||||
| Ubuntu | 22 |
|
||||
| Ubuntu | 24 |
|
||||
| Debian | 11 |
|
||||
| Debian | 12 |
|
||||
| OS | Version | Note |
|
||||
|---------------------|---------|-----------------|
|
||||
| AlmaLinux | 9 | Recommended |
|
||||
| AlmaLinux | 8 | Not recommended |
|
||||
| RockyLinux | 9 | |
|
||||
| RockyLinux | 8 | Not recommended |
|
||||
| CentOS Stream | 9 | Not recommended |
|
||||
| CentOS Stream | 8 | Not recommended |
|
||||
| Ubuntu | 24 | Recommended |
|
||||
| Ubuntu | 22 | |
|
||||
| Debian | 12 | Recommended |
|
||||
| Debian | 11 | |
|
||||
| OpenCloudOS | 9 | |
|
||||
| TencentOS Server | 4 | |
|
||||
| TencentOS Server | 3.1 | Not recommended |
|
||||
| Alibaba Cloud Linux | 3.2 | Not recommended |
|
||||
| Anolis | 8 | Not recommended |
|
||||
|
||||
As system versions are constantly updated, we may also terminate support for some older systems to ensure the robustness of the panel.
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ require (
|
||||
github.com/go-playground/locales v0.14.1
|
||||
github.com/go-playground/universal-translator v0.18.1
|
||||
github.com/go-playground/validator/v10 v10.22.1
|
||||
github.com/go-rat/chix v1.1.3
|
||||
github.com/go-rat/chix v1.1.4
|
||||
github.com/go-rat/gormstore v1.0.6
|
||||
github.com/go-rat/sessions v1.0.11
|
||||
github.com/go-rat/utils v1.0.3
|
||||
@@ -39,7 +39,8 @@ require (
|
||||
github.com/shirou/gopsutil v2.21.11+incompatible
|
||||
github.com/spf13/cast v1.7.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/urfave/cli/v3 v3.0.0-alpha9
|
||||
github.com/tufanbarisyildirim/gonginx v0.0.0-20241013191809-e73b7dd454e8
|
||||
github.com/urfave/cli/v3 v3.0.0-alpha9.1
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.28.0
|
||||
golang.org/x/net v0.30.0
|
||||
@@ -72,6 +73,7 @@ require (
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-rat/securecookie v1.0.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.0.0 // indirect
|
||||
github.com/gofiber/schema v1.2.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
@@ -101,13 +103,13 @@ require (
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/samber/lo v1.47.0 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.597 // indirect
|
||||
github.com/therootcompany/xz v1.0.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.14 // indirect
|
||||
github.com/tklauser/numcpus v0.8.0 // indirect
|
||||
github.com/ulikunitz/xz v0.5.12 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect
|
||||
go.opentelemetry.io/otel v1.30.0 // indirect
|
||||
@@ -124,7 +126,6 @@ require (
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gotest.tools/v3 v3.5.1 // indirect
|
||||
modernc.org/libc v1.60.1 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.8.0 // indirect
|
||||
|
||||
@@ -102,8 +102,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
|
||||
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-rat/chix v1.1.3 h1:NeDbmA3OTfhEeizn528dz8z8/lBcsziftgtSarYn7Vo=
|
||||
github.com/go-rat/chix v1.1.3/go.mod h1:XeC0hmaAXlPB/V8g0QZiIAgnAJO70ZeT+k1Q4Zh9qeI=
|
||||
github.com/go-rat/chix v1.1.4 h1:YVDwF4vrlkm5hmlDGWKoiWPgP98pJooBvhsto8UWq+o=
|
||||
github.com/go-rat/chix v1.1.4/go.mod h1:eKUTG7GXqMS278PH0QZi8yMWZddvMfxqWu7mverM6kQ=
|
||||
github.com/go-rat/gormstore v1.0.6 h1:YxmaY6n3JoTk6o9RXoZ1+cQ6HQL+ayf/6d8D76ydCAc=
|
||||
github.com/go-rat/gormstore v1.0.6/go.mod h1:xG/n+Du8baWf7ptiafiHLWBZWVXrYIPC42CsoEbfr4M=
|
||||
github.com/go-rat/securecookie v1.0.1 h1:HW0fpKmB+FjJzXTw8ABOwBJ+XrPmRBSZqHhmrv86lBo=
|
||||
@@ -120,6 +120,8 @@ github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAp
|
||||
github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/goccy/go-yaml v1.12.0 h1:/1WHjnMsI1dlIBQutrvSMGZRQufVO3asrHfTwfACoPM=
|
||||
github.com/goccy/go-yaml v1.12.0/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU=
|
||||
github.com/gofiber/schema v1.2.0 h1:j+ZRrNnUa/0ZuWrn/6kAtAufEr4jCJ+JuTURAMxNSZg=
|
||||
github.com/gofiber/schema v1.2.0/go.mod h1:YYwj01w3hVfaNjhtJzaqetymL56VW642YS3qZPhuE6c=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
@@ -269,6 +271,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
|
||||
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
|
||||
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
|
||||
github.com/sethvargo/go-limiter v1.0.0 h1:JqW13eWEMn0VFv86OKn8wiYJY/m250WoXdrjRV0kLe4=
|
||||
github.com/sethvargo/go-limiter v1.0.0/go.mod h1:01b6tW25Ap+MeLYBuD4aHunMrJoNO5PVUFdS9rac3II=
|
||||
github.com/shirou/gopsutil v2.21.11+incompatible h1:lOGOyCG67a5dv2hq5Z1BLDUqqKp3HkbjPcz5j6XMS0U=
|
||||
@@ -295,15 +299,17 @@ github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZ
|
||||
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
|
||||
github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
|
||||
github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
|
||||
github.com/tufanbarisyildirim/gonginx v0.0.0-20240907135031-d38eb71142ac h1:IXccMEFcB+UqGWae8OF9EoA0/8GCLlDj6s84LCU7y58=
|
||||
github.com/tufanbarisyildirim/gonginx v0.0.0-20240907135031-d38eb71142ac/go.mod h1:itu4KWRgrfEwGcfNka+rV4houuirUau53i0diN4lG5g=
|
||||
github.com/tufanbarisyildirim/gonginx v0.0.0-20241013191809-e73b7dd454e8 h1:7yw1yHkylDcu/CwY4hEEe8MycDLo7B9LjcqwhoL8NrM=
|
||||
github.com/tufanbarisyildirim/gonginx v0.0.0-20241013191809-e73b7dd454e8/go.mod h1:itu4KWRgrfEwGcfNka+rV4houuirUau53i0diN4lG5g=
|
||||
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
|
||||
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/urfave/cli/v3 v3.0.0-alpha9 h1:P0RMy5fQm1AslQS+XCmy9UknDXctOmG/q/FZkUFnJSo=
|
||||
github.com/urfave/cli/v3 v3.0.0-alpha9/go.mod h1:0kK/RUFHyh+yIKSfWxwheGndfnrvYSmYFVeKCh03ZUc=
|
||||
github.com/urfave/cli/v3 v3.0.0-alpha9.1 h1:1fJU+bltkwN8lF4Sni/X0i1d8XwPIrS82ivZ8qsp/q4=
|
||||
github.com/urfave/cli/v3 v3.0.0-alpha9.1/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y=
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
|
||||
@@ -13,7 +13,7 @@ func init() {
|
||||
Route: func(r chi.Router) {
|
||||
service := NewService()
|
||||
r.Get("/jails", service.List)
|
||||
r.Post("/jails", service.Add)
|
||||
r.Post("/jails", service.Create)
|
||||
r.Delete("/jails", service.Delete)
|
||||
r.Get("/jails/{name}", service.BanList)
|
||||
r.Post("/unban", service.Unban)
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
package fail2ban
|
||||
|
||||
type Add struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
MaxRetry string `json:"maxretry"`
|
||||
FindTime string `json:"findtime"`
|
||||
BanTime string `json:"bantime"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Type string `json:"type" validate:"required"`
|
||||
MaxRetry int `json:"maxretry" validate:"required"`
|
||||
FindTime int `json:"findtime" validate:"required"`
|
||||
BanTime int `json:"bantime" validate:"required"`
|
||||
WebsiteName string `json:"website_name"`
|
||||
WebsiteMode string `json:"website_mode"`
|
||||
WebsitePath string `json:"website_path"`
|
||||
}
|
||||
|
||||
type Delete struct {
|
||||
Name string `json:"name"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
}
|
||||
|
||||
type BanList struct {
|
||||
Name string `json:"name"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
}
|
||||
|
||||
type Unban struct {
|
||||
Name string `json:"name"`
|
||||
IP string `json:"ip"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
IP string `json:"ip" validate:"required"`
|
||||
}
|
||||
|
||||
type SetWhiteList struct {
|
||||
IP string `json:"ip"`
|
||||
IP string `json:"ip" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package fail2ban
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
@@ -28,7 +29,7 @@ func NewService() *Service {
|
||||
}
|
||||
}
|
||||
|
||||
// List 所有 Fail2ban 规则
|
||||
// List 所有规则
|
||||
func (s *Service) List(w http.ResponseWriter, r *http.Request) {
|
||||
raw, err := io.Read("/etc/fail2ban/jail.local")
|
||||
if err != nil {
|
||||
@@ -77,8 +78,8 @@ func (s *Service) List(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// Add 添加 Fail2ban 规则
|
||||
func (s *Service) Add(w http.ResponseWriter, r *http.Request) {
|
||||
// Create 添加规则
|
||||
func (s *Service) Create(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Add](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
@@ -86,9 +87,9 @@ func (s *Service) Add(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
jailName := req.Name
|
||||
jailType := req.Type
|
||||
jailMaxRetry := req.MaxRetry
|
||||
jailFindTime := req.FindTime
|
||||
jailBanTime := req.BanTime
|
||||
jailMaxRetry := cast.ToString(req.MaxRetry)
|
||||
jailFindTime := cast.ToString(req.FindTime)
|
||||
jailBanTime := cast.ToString(req.BanTime)
|
||||
jailWebsiteName := req.WebsiteName
|
||||
jailWebsiteMode := req.WebsiteMode
|
||||
jailWebsitePath := req.WebsitePath
|
||||
@@ -111,10 +112,12 @@ func (s *Service) Add(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
var ports string
|
||||
for _, port := range website.Ports {
|
||||
fields := strings.Fields(cast.ToString(port))
|
||||
ports += fields[0] + ","
|
||||
for _, listen := range website.Listens {
|
||||
if port, err := cast.ToIntE(listen.Address); err == nil {
|
||||
ports += fmt.Sprintf("%d", port) + ","
|
||||
}
|
||||
}
|
||||
ports = strings.TrimSuffix(ports, ",")
|
||||
|
||||
rule := `
|
||||
# ` + jailWebsiteName + `-` + jailWebsiteMode + `-START
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package frp
|
||||
|
||||
type Name struct {
|
||||
Name string `form:"name" json:"name"`
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
}
|
||||
|
||||
type UpdateConfig struct {
|
||||
Name string `form:"name" json:"name"`
|
||||
Config string `form:"config" json:"config"`
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
package gitea
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config"`
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -17,12 +17,7 @@ func NewService() *Service {
|
||||
}
|
||||
|
||||
func (s *Service) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/gitea/app.ini", app.Root))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
config, _ := io.Read(fmt.Sprintf("%s/server/gitea/app.ini", app.Root))
|
||||
service.Success(w, config)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package mysql
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config"`
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
type SetRootPassword struct {
|
||||
Password string `form:"password" json:"password"`
|
||||
Password string `form:"password" json:"password" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -49,13 +49,13 @@ func (s *Service) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := io.Write(app.Root+"/server/mysql/conf/my.cnf", req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "写入配置失败")
|
||||
if err = io.Write(app.Root+"/server/mysql/conf/my.cnf", req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "写入配置失败:%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := systemctl.Reload("mysqld"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "重载失败")
|
||||
if err = systemctl.Restart("mysqld"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "重启失败:%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ func (s *Service) Load(w http.ResponseWriter, r *http.Request) {
|
||||
{`Max_used_connections\s+\|\s+(\d+)\s+\|`, "峰值连接数"},
|
||||
{`Key_read_requests\s+\|\s+(\d+)\s+\|`, "索引命中率"},
|
||||
{`Innodb_buffer_pool_reads\s+\|\s+(\d+)\s+\|`, "Innodb索引命中率"},
|
||||
{`Created_tmp_disk_tables\s+\|\s+(\d+)\s+\|`, "创建临时表到磁盘"},
|
||||
{`Created_tmp_disk_tables\s+\|\s+(\d+)\s+\|`, "创建临时表到硬盘"},
|
||||
{`Open_tables\s+\|\s+(\d+)\s+\|`, "已打开的表"},
|
||||
{`Select_full_join\s+\|\s+(\d+)\s+\|`, "没有使用索引的量"},
|
||||
{`Select_full_range_join\s+\|\s+(\d+)\s+\|`, "没有索引的JOIN量"},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
package nginx
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config"`
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package php
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config"`
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
type ExtensionSlug struct {
|
||||
Slug string `form:"slug" json:"slug"`
|
||||
Slug string `form:"slug" json:"slug" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package php
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -183,7 +184,7 @@ func (s *Service) InstallExtension(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
cmd := fmt.Sprintf(`curl -fsLm 10 --retry 3 "https://dl.cdn.haozi.net/panel/php_exts/%s" | bash -s -- "install" "%d" >> /tmp/%s.log 2>&1`, req.Slug, s.version, req.Slug)
|
||||
cmd := fmt.Sprintf(`curl -fsLm 10 --retry 3 "https://dl.cdn.haozi.net/panel/php_exts/%s.sh" | bash -s -- "install" "%d" >> /tmp/%s.log 2>&1`, url.QueryEscape(req.Slug), s.version, req.Slug)
|
||||
officials := []string{"fileinfo", "exif", "imap", "pdo_pgsql", "zip", "bz2", "readline", "snmp", "ldap", "enchant", "pspell", "calendar", "gmp", "sysvmsg", "sysvsem", "sysvshm", "xsl", "intl", "gettext"}
|
||||
if slices.Contains(officials, req.Slug) {
|
||||
cmd = fmt.Sprintf(`curl -fsLm 10 --retry 3 "https://dl.cdn.haozi.net/panel/php_exts/official.sh" | bash -s -- "install" "%d" "%s" >> /tmp/%s.log 2>&1`, s.version, req.Slug, req.Slug)
|
||||
@@ -214,7 +215,7 @@ func (s *Service) UninstallExtension(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
cmd := fmt.Sprintf(`curl -fsLm 10 --retry 3 "https://dl.cdn.haozi.net/panel/php_exts/%s" | bash -s -- "uninstall" "%d" >> /tmp/%s.log 2>&1`, req.Slug, s.version, req.Slug)
|
||||
cmd := fmt.Sprintf(`curl -fsLm 10 --retry 3 "https://dl.cdn.haozi.net/panel/php_exts/%s.sh" | bash -s -- "uninstall" "%d" >> /tmp/%s.log 2>&1`, url.QueryEscape(req.Slug), s.version, req.Slug)
|
||||
officials := []string{"fileinfo", "exif", "imap", "pdo_pgsql", "zip", "bz2", "readline", "snmp", "ldap", "enchant", "pspell", "calendar", "gmp", "sysvmsg", "sysvsem", "sysvshm", "xsl", "intl", "gettext"}
|
||||
if slices.Contains(officials, req.Slug) {
|
||||
cmd = fmt.Sprintf(`curl -fsLm 10 --retry 3 "https://dl.cdn.haozi.net/panel/php_exts/official.sh" | bash -s -- "uninstall" "%d" "%s" >> /tmp/%s.log 2>&1`, s.version, req.Slug, req.Slug)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package phpmyadmin
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config"`
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
type UpdatePort struct {
|
||||
Port uint `form:"port" json:"port"`
|
||||
Port uint `form:"port" json:"port" validate:"required,number,gte=1,lte=65535"`
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
package podman
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config"`
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
package postgresql
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config"`
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ func init() {
|
||||
service := NewService()
|
||||
r.Get("/users", service.List)
|
||||
r.Post("/users", service.Create)
|
||||
r.Delete("/users/{name}", service.Delete)
|
||||
r.Post("/users/{name}/password", service.ChangePassword)
|
||||
r.Delete("/users/{username}", service.Delete)
|
||||
r.Post("/users/{username}/password", service.ChangePassword)
|
||||
r.Get("/port", service.GetPort)
|
||||
r.Post("/port", service.UpdatePort)
|
||||
},
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
package pureftpd
|
||||
|
||||
type Create struct {
|
||||
Username string `form:"username" json:"username"`
|
||||
Password string `form:"password" json:"password"`
|
||||
Path string `form:"path" json:"path"`
|
||||
Username string `form:"username" json:"username" validate:"required"`
|
||||
Password string `form:"password" json:"password" validate:"required"`
|
||||
Path string `form:"path" json:"path" validate:"required"`
|
||||
}
|
||||
|
||||
type Delete struct {
|
||||
Username string `form:"username" json:"username"`
|
||||
Username string `form:"username" json:"username" validate:"required"`
|
||||
}
|
||||
|
||||
type ChangePassword struct {
|
||||
Username string `form:"username" json:"username"`
|
||||
Password string `form:"password" json:"password"`
|
||||
Username string `form:"username" json:"username" validate:"required"`
|
||||
Password string `form:"password" json:"password" validate:"required"`
|
||||
}
|
||||
|
||||
type UpdatePort struct {
|
||||
Port uint `form:"port" json:"port"`
|
||||
Port uint `form:"port" json:"port" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
package redis
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config"`
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
package rsync
|
||||
|
||||
type Create struct {
|
||||
Name string `form:"name" json:"name"`
|
||||
Path string `form:"path" json:"path"`
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
Path string `form:"path" json:"path" validate:"required"`
|
||||
Comment string `form:"comment" json:"comment"`
|
||||
AuthUser string `form:"auth_user" json:"auth_user"`
|
||||
Secret string `form:"secret" json:"secret"`
|
||||
AuthUser string `form:"auth_user" json:"auth_user" validate:"required"`
|
||||
Secret string `form:"secret" json:"secret" validate:"required"`
|
||||
HostsAllow string `form:"hosts_allow" json:"hosts_allow"`
|
||||
}
|
||||
|
||||
type Delete struct {
|
||||
Name string `form:"name" json:"name"`
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
}
|
||||
|
||||
type Update struct {
|
||||
Name string `form:"name" json:"name"`
|
||||
Path string `form:"path" json:"path"`
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
Path string `form:"path" json:"path" validate:"required"`
|
||||
Comment string `form:"comment" json:"comment"`
|
||||
AuthUser string `form:"auth_user" json:"auth_user"`
|
||||
Secret string `form:"secret" json:"secret"`
|
||||
AuthUser string `form:"auth_user" json:"auth_user" validate:"required"`
|
||||
Secret string `form:"secret" json:"secret" validate:"required"`
|
||||
HostsAllow string `form:"hosts_allow" json:"hosts_allow"`
|
||||
}
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config"`
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package s3fs
|
||||
|
||||
type Create struct {
|
||||
Ak string `form:"ak" json:"ak"`
|
||||
Sk string `form:"sk" json:"sk"`
|
||||
Bucket string `form:"bucket" json:"bucket"`
|
||||
URL string `form:"url" json:"url"`
|
||||
Path string `form:"path" json:"path"`
|
||||
Ak string `form:"ak" json:"ak" validate:"required"`
|
||||
Sk string `form:"sk" json:"sk" validate:"required"`
|
||||
Bucket string `form:"bucket" json:"bucket" validate:"required"`
|
||||
URL string `form:"url" json:"url" validate:"required"`
|
||||
Path string `form:"path" json:"path" validate:"required"`
|
||||
}
|
||||
|
||||
type Delete struct {
|
||||
ID int64 `form:"id" json:"id"`
|
||||
ID int64 `form:"id" json:"id" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -18,14 +18,14 @@ func init() {
|
||||
r.Get("/config", service.GetConfig)
|
||||
r.Post("/config", service.UpdateConfig)
|
||||
r.Get("/processes", service.Processes)
|
||||
r.Post("/processes/{name}/start", service.StartProcess)
|
||||
r.Post("/processes/{name}/stop", service.StopProcess)
|
||||
r.Post("/processes/{name}/restart", service.RestartProcess)
|
||||
r.Get("/processes/{name}/log", service.ProcessLog)
|
||||
r.Post("/processes/{name}/clearLog", service.ClearProcessLog)
|
||||
r.Get("/processes/{name}", service.ProcessConfig)
|
||||
r.Post("/processes/{name}", service.UpdateProcessConfig)
|
||||
r.Delete("/processes/{name}", service.DeleteProcess)
|
||||
r.Post("/processes/{process}/start", service.StartProcess)
|
||||
r.Post("/processes/{process}/stop", service.StopProcess)
|
||||
r.Post("/processes/{process}/restart", service.RestartProcess)
|
||||
r.Get("/processes/{process}/log", service.ProcessLog)
|
||||
r.Post("/processes/{process}/clearLog", service.ClearProcessLog)
|
||||
r.Get("/processes/{process}", service.ProcessConfig)
|
||||
r.Post("/processes/{process}", service.UpdateProcessConfig)
|
||||
r.Delete("/processes/{process}", service.DeleteProcess)
|
||||
r.Post("/processes", service.CreateProcess)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
package supervisor
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config"`
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
type UpdateProcessConfig struct {
|
||||
Process string `form:"config" json:"process"`
|
||||
Config string `form:"config" json:"config"`
|
||||
Process string `form:"process" json:"process" validate:"required"`
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
type ProcessName struct {
|
||||
Process string `form:"config" json:"process"`
|
||||
Process string `form:"process" json:"process" validate:"required"`
|
||||
}
|
||||
|
||||
type CreateProcess struct {
|
||||
Name string `form:"name" json:"name"`
|
||||
User string `form:"user" json:"user"`
|
||||
Path string `form:"path" json:"path"`
|
||||
Command string `form:"command" json:"command"`
|
||||
Num int `form:"num" json:"num"`
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
User string `form:"user" json:"user" validate:"required"`
|
||||
Path string `form:"path" json:"path" validate:"required"`
|
||||
Command string `form:"command" json:"command" validate:"required"`
|
||||
Num int `form:"num" json:"num" validate:"required,gte=1"`
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
package toolbox
|
||||
|
||||
type DNS struct {
|
||||
DNS1 string `form:"dns1" json:"dns1"`
|
||||
DNS2 string `form:"dns2" json:"dns2"`
|
||||
DNS1 string `form:"dns1" json:"dns1" validate:"required"`
|
||||
DNS2 string `form:"dns2" json:"dns2" validate:"required"`
|
||||
}
|
||||
|
||||
type SWAP struct {
|
||||
Size int64 `form:"size" json:"size"`
|
||||
Size int64 `form:"size" json:"size" validate:"gte=0"`
|
||||
}
|
||||
|
||||
type Timezone struct {
|
||||
Timezone string `form:"timezone" json:"timezone"`
|
||||
Timezone string `form:"timezone" json:"timezone" validate:"required"`
|
||||
}
|
||||
|
||||
type Hosts struct {
|
||||
@@ -18,5 +18,5 @@ type Hosts struct {
|
||||
}
|
||||
|
||||
type Password struct {
|
||||
Password string `form:"password" json:"password"`
|
||||
Password string `form:"password" json:"password" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -52,11 +52,6 @@ func (s *Service) UpdateDNS(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.DNS1) == 0 || len(req.DNS2) == 0 {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "DNS 信息不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
var dns string
|
||||
dns += "nameserver " + req.DNS1 + "\n"
|
||||
dns += "nameserver " + req.DNS2 + "\n"
|
||||
@@ -76,7 +71,7 @@ func (s *Service) GetSWAP(w http.ResponseWriter, r *http.Request) {
|
||||
if io.Exists(filepath.Join(app.Root, "swap")) {
|
||||
file, err := io.FileInfo(filepath.Join(app.Root, "swap"))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "获取 SWAP 信息失败")
|
||||
service.Error(w, http.StatusInternalServerError, "获取 SWAP 信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -89,12 +84,12 @@ func (s *Service) GetSWAP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
raw, err := shell.Execf("free | grep Swap")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "获取 SWAP 信息失败")
|
||||
service.Error(w, http.StatusInternalServerError, "获取 SWAP 信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
match := regexp.MustCompile(`Swap:\s+(\d+)\s+(\d+)\s+(\d+)`).FindStringSubmatch(raw)
|
||||
if len(match) > 0 {
|
||||
if len(match) >= 4 {
|
||||
used = str.FormatBytes(cast.ToFloat64(match[2]) * 1024)
|
||||
free = str.FormatBytes(cast.ToFloat64(match[3]) * 1024)
|
||||
}
|
||||
@@ -117,15 +112,15 @@ func (s *Service) UpdateSWAP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if io.Exists(filepath.Join(app.Root, "swap")) {
|
||||
if _, err = shell.Execf("swapoff '%s'", filepath.Join(app.Root, "swap")); err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if _, err = shell.Execf("rm -f '%s'", filepath.Join(app.Root, "swap")); err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if _, err = shell.Execf(`sed -i '%s/d' /etc/fstab`, filepath.Join(app.Root, "swap")); err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
if _, err = shell.Execf(`sed -i "\|^%s|d" /etc/fstab`, filepath.Join(app.Root, "swap")); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -134,40 +129,40 @@ func (s *Service) UpdateSWAP(w http.ResponseWriter, r *http.Request) {
|
||||
var free string
|
||||
free, err = shell.Execf("df -k %s | awk '{print $4}' | tail -n 1", app.Root)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "获取磁盘空间失败")
|
||||
service.Error(w, http.StatusInternalServerError, "获取硬盘空间失败")
|
||||
return
|
||||
}
|
||||
if cast.ToInt64(free)*1024 < req.Size*1024*1024 {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "磁盘空间不足,当前剩余%s", str.FormatBytes(cast.ToFloat64(free)))
|
||||
service.Error(w, http.StatusInternalServerError, "硬盘空间不足,当前剩余%s", str.FormatBytes(cast.ToFloat64(free)))
|
||||
return
|
||||
}
|
||||
|
||||
btrfsCheck, _ := shell.Execf("df -T %s | awk '{print $2}' | tail -n 1", app.Root)
|
||||
if strings.Contains(btrfsCheck, "btrfs") {
|
||||
if _, err = shell.Execf("btrfs filesystem mkswapfile --size %dM --uuid clear %s", req.Size, filepath.Join(app.Root, "swap")); err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if _, err = shell.Execf("dd if=/dev/zero of=%s bs=1M count=%d", filepath.Join(app.Root, "swap"), req.Size); err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if _, err = shell.Execf("mkswap -f '%s'", filepath.Join(app.Root, "swap")); err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if err = io.Chmod(filepath.Join(app.Root, "swap"), 0600); err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "设置 SWAP 权限失败")
|
||||
service.Error(w, http.StatusInternalServerError, "设置 SWAP 权限失败")
|
||||
return
|
||||
}
|
||||
}
|
||||
if _, err = shell.Execf("swapon '%s'", filepath.Join(app.Root, "swap")); err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if _, err = shell.Execf("echo '%s swap swap defaults 0 0' >> /etc/fstab", filepath.Join(app.Root, "swap")); err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -179,19 +174,19 @@ func (s *Service) UpdateSWAP(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *Service) GetTimezone(w http.ResponseWriter, r *http.Request) {
|
||||
raw, err := shell.Execf("timedatectl | grep zone")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "获取时区信息失败")
|
||||
service.Error(w, http.StatusInternalServerError, "获取时区信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
match := regexp.MustCompile(`zone:\s+(\S+)`).FindStringSubmatch(raw)
|
||||
if len(match) == 0 {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "找不到时区信息")
|
||||
service.Error(w, http.StatusInternalServerError, "找不到时区信息")
|
||||
return
|
||||
}
|
||||
|
||||
zonesRaw, err := shell.Execf("timedatectl list-timezones")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "获取时区列表失败")
|
||||
service.Error(w, http.StatusInternalServerError, "获取时区列表失败")
|
||||
return
|
||||
}
|
||||
zones := strings.Split(zonesRaw, "\n")
|
||||
@@ -219,7 +214,7 @@ func (s *Service) UpdateTimezone(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if _, err = shell.Execf("timedatectl set-timezone '%s'", req.Timezone); err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -228,12 +223,7 @@ func (s *Service) UpdateTimezone(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// GetHosts 获取 hosts 信息
|
||||
func (s *Service) GetHosts(w http.ResponseWriter, r *http.Request) {
|
||||
hosts, err := io.Read("/etc/hosts")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
hosts, _ := io.Read("/etc/hosts")
|
||||
service.Success(w, hosts)
|
||||
}
|
||||
|
||||
@@ -246,7 +236,7 @@ func (s *Service) UpdateHosts(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if err = io.Write("/etc/hosts", req.Hosts, 0644); err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "写入 hosts 信息失败")
|
||||
service.Error(w, http.StatusInternalServerError, "写入 hosts 信息失败")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -268,7 +258,7 @@ func (s *Service) UpdateRootPassword(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
req.Password = strings.ReplaceAll(req.Password, `'`, `\'`)
|
||||
if _, err = shell.Execf(`yes '%s' | passwd root`, req.Password); err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package biz
|
||||
|
||||
import "github.com/TheTNB/panel/pkg/types"
|
||||
|
||||
type BackupType string
|
||||
|
||||
const (
|
||||
BackupTypePath BackupType = "path"
|
||||
BackupTypeWebsite BackupType = "website"
|
||||
BackupTypeMySQL BackupType = "mysql"
|
||||
BackupTypePostgres BackupType = "postgres"
|
||||
BackupTypeRedis BackupType = "redis"
|
||||
BackupTypePanel BackupType = "panel"
|
||||
)
|
||||
|
||||
type BackupRepo interface {
|
||||
List(typ BackupType) ([]*types.BackupFile, error)
|
||||
Create(typ BackupType, target string, path ...string) error
|
||||
Delete(typ BackupType, name string) error
|
||||
Restore(typ BackupType, backup, target string) error
|
||||
ClearExpired(path, prefix string, save int) error
|
||||
CutoffLog(path, target string) error
|
||||
GetPath(typ BackupType) (string, error)
|
||||
}
|
||||
@@ -12,8 +12,7 @@ type Website struct {
|
||||
Name string `gorm:"not null;unique" json:"name"`
|
||||
Status bool `gorm:"not null;default:true" json:"status"`
|
||||
Path string `gorm:"not null" json:"path"`
|
||||
PHP int `gorm:"not null" json:"php"`
|
||||
SSL bool `gorm:"not null" json:"ssl"`
|
||||
Https bool `gorm:"not null" json:"https"`
|
||||
Remark string `gorm:"not null" json:"remark"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
@@ -26,7 +26,7 @@ func initConf() {
|
||||
|
||||
func initGlobal() {
|
||||
app.Root = app.Conf.MustString("app.root")
|
||||
app.Version = "2.3.2"
|
||||
app.Version = "2.3.4"
|
||||
app.Locale = app.Conf.MustString("app.locale")
|
||||
|
||||
// 初始化时区
|
||||
|
||||
@@ -0,0 +1,527 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gookit/color"
|
||||
"github.com/shirou/gopsutil/disk"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/pkg/db"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
type backupRepo struct {
|
||||
setting biz.SettingRepo
|
||||
website biz.WebsiteRepo
|
||||
}
|
||||
|
||||
func NewBackupRepo() biz.BackupRepo {
|
||||
return &backupRepo{
|
||||
setting: NewSettingRepo(),
|
||||
website: NewWebsiteRepo(),
|
||||
}
|
||||
}
|
||||
|
||||
// List 备份列表
|
||||
func (r *backupRepo) List(typ biz.BackupType) ([]*types.BackupFile, error) {
|
||||
path, err := r.GetPath(typ)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
files, err := io.ReadDir(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list := make([]*types.BackupFile, 0)
|
||||
for _, file := range files {
|
||||
info, err := file.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
list = append(list, &types.BackupFile{
|
||||
Name: file.Name(),
|
||||
Path: filepath.Join(path, file.Name()),
|
||||
Size: str.FormatBytes(float64(info.Size())),
|
||||
})
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// Create 创建备份
|
||||
// typ 备份类型
|
||||
// target 目标名称
|
||||
// path 可选备份保存路径
|
||||
func (r *backupRepo) Create(typ biz.BackupType, target string, path ...string) error {
|
||||
defPath, err := r.GetPath(typ)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(path) > 0 && path[0] != "" {
|
||||
defPath = path[0]
|
||||
}
|
||||
|
||||
switch typ {
|
||||
case biz.BackupTypeWebsite:
|
||||
return r.createWebsite(defPath, target)
|
||||
case biz.BackupTypeMySQL:
|
||||
return r.createMySQL(defPath, target)
|
||||
case biz.BackupTypePostgres:
|
||||
return r.createPostgres(defPath, target)
|
||||
case biz.BackupTypePanel:
|
||||
return r.createPanel(defPath)
|
||||
|
||||
}
|
||||
|
||||
return errors.New("未知备份类型")
|
||||
}
|
||||
|
||||
// Delete 删除备份
|
||||
func (r *backupRepo) Delete(typ biz.BackupType, name string) error {
|
||||
path, err := r.GetPath(typ)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
file := filepath.Join(path, name)
|
||||
return io.Remove(file)
|
||||
}
|
||||
|
||||
// Restore 恢复备份
|
||||
// typ 备份类型
|
||||
// backup 备份压缩包,可以是绝对路径或者相对路径
|
||||
// target 目标名称
|
||||
func (r *backupRepo) Restore(typ biz.BackupType, backup, target string) error {
|
||||
if !io.Exists(backup) {
|
||||
path, err := r.GetPath(typ)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
backup = filepath.Join(path, backup)
|
||||
}
|
||||
|
||||
switch typ {
|
||||
case biz.BackupTypeWebsite:
|
||||
return r.restoreWebsite(backup, target)
|
||||
case biz.BackupTypeMySQL:
|
||||
return r.restoreMySQL(backup, target)
|
||||
case biz.BackupTypePostgres:
|
||||
return r.restorePostgres(backup, target)
|
||||
}
|
||||
|
||||
return errors.New("未知备份类型")
|
||||
}
|
||||
|
||||
// CutoffLog 切割日志
|
||||
// path 保存目录绝对路径
|
||||
// target 待切割日志文件绝对路径
|
||||
func (r *backupRepo) CutoffLog(path, target string) error {
|
||||
if !io.Exists(target) {
|
||||
return errors.New("日志文件不存在")
|
||||
}
|
||||
|
||||
to := filepath.Join(path, fmt.Sprintf("%s_%s.zip", time.Now().Format("20060102150405"), filepath.Base(target)))
|
||||
if err := io.Compress([]string{target}, to, io.Zip); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return io.Remove(target)
|
||||
}
|
||||
|
||||
// ClearExpired 清理过期备份
|
||||
// path 备份目录绝对路径
|
||||
// prefix 目标文件前缀
|
||||
// save 保存份数
|
||||
func (r *backupRepo) ClearExpired(path, prefix string, save int) error {
|
||||
files, err := io.ReadDir(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var filtered []os.FileInfo
|
||||
for _, file := range files {
|
||||
if strings.HasPrefix(file.Name(), prefix) && strings.HasSuffix(file.Name(), ".zip") {
|
||||
info, err := os.Stat(filepath.Join(path, file.Name()))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, info)
|
||||
}
|
||||
}
|
||||
|
||||
// 排序所有备份文件,从新到旧
|
||||
slices.SortFunc(filtered, func(a, b os.FileInfo) int {
|
||||
if a.ModTime().After(b.ModTime()) {
|
||||
return -1
|
||||
}
|
||||
if a.ModTime().Before(b.ModTime()) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
if len(filtered) <= save {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 切片保留 save 份,删除剩余
|
||||
toDelete := filtered[save:]
|
||||
for _, file := range toDelete {
|
||||
filePath := filepath.Join(path, file.Name())
|
||||
color.Greenln(fmt.Sprintf("|-清理过期文件:%s", filePath))
|
||||
if err = os.Remove(filePath); err != nil {
|
||||
color.Redln(fmt.Sprintf("|-清理失败:%v", err))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPath 获取备份路径
|
||||
func (r *backupRepo) GetPath(typ biz.BackupType) (string, error) {
|
||||
backupPath, err := r.setting.Get(biz.SettingKeyBackupPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !slices.Contains([]biz.BackupType{biz.BackupTypePath, biz.BackupTypeWebsite, biz.BackupTypeMySQL, biz.BackupTypePostgres, biz.BackupTypeRedis, biz.BackupTypePanel}, typ) {
|
||||
return "", errors.New("未知备份类型")
|
||||
}
|
||||
|
||||
backupPath = filepath.Join(backupPath, string(typ))
|
||||
if !io.Exists(backupPath) {
|
||||
if err = io.Mkdir(backupPath, 0644); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return backupPath, nil
|
||||
}
|
||||
|
||||
// createWebsite 创建网站备份
|
||||
func (r *backupRepo) createWebsite(to string, name string) error {
|
||||
website, err := r.website.GetByName(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = r.preCheckPath(to, website.Path); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var paths []string
|
||||
dirs, err := io.ReadDir(website.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range dirs {
|
||||
paths = append(paths, filepath.Join(website.Path, item.Name()))
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
backup := filepath.Join(to, fmt.Sprintf("%s_%s.zip", website.Name, time.Now().Format("20060102150405")))
|
||||
if err = io.Compress(paths, backup, io.Zip); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
|
||||
color.Greenln(fmt.Sprintf("|-已备份至文件:%s", backup))
|
||||
return nil
|
||||
}
|
||||
|
||||
// createMySQL 创建 MySQL 备份
|
||||
func (r *backupRepo) createMySQL(to string, name string) error {
|
||||
rootPassword, err := r.setting.Get(biz.SettingKeyMySQLRootPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exist, _ := mysql.DatabaseExists(name); !exist {
|
||||
return fmt.Errorf("数据库不存在:%s", name)
|
||||
}
|
||||
size, err := mysql.DatabaseSize(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = r.preCheckDB(to, size); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = os.Setenv("MYSQL_PWD", rootPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
start := time.Now()
|
||||
backup := filepath.Join(to, fmt.Sprintf("%s_%s.sql", name, time.Now().Format("20060102150405")))
|
||||
if _, err = shell.Execf(`mysqldump -u root '%s' > '%s'`, name, backup); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = os.Unsetenv("MYSQL_PWD"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = io.Compress([]string{backup}, backup+".zip", io.Zip); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = io.Remove(backup); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
|
||||
color.Greenln(fmt.Sprintf("|-已备份至文件:%s", backup+".zip"))
|
||||
return nil
|
||||
}
|
||||
|
||||
// createPostgres 创建 PostgreSQL 备份
|
||||
func (r *backupRepo) createPostgres(to string, name string) error {
|
||||
postgres, err := db.NewPostgres("postgres", "", "127.0.0.1", fmt.Sprintf("%s/server/postgresql/data/pg_hba.conf", app.Root), 5432)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exist, _ := postgres.DatabaseExist(name); !exist {
|
||||
return fmt.Errorf("数据库不存在:%s", name)
|
||||
}
|
||||
size, err := postgres.DatabaseSize(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = r.preCheckDB(to, size); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
backup := filepath.Join(to, fmt.Sprintf("%s_%s.sql", name, time.Now().Format("20060102150405")))
|
||||
if _, err = shell.Execf(`su - postgres -c "pg_dump '%s'" > '%s'`, name, backup); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = io.Compress([]string{backup}, backup+".zip", io.Zip); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = io.Remove(backup); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
|
||||
color.Greenln(fmt.Sprintf("|-已备份至文件:%s", backup+".zip"))
|
||||
return nil
|
||||
}
|
||||
|
||||
// createPanel 创建面板备份
|
||||
func (r *backupRepo) createPanel(to string) error {
|
||||
backup := filepath.Join(to, fmt.Sprintf("panel_%s.zip", time.Now().Format("20060102150405")))
|
||||
|
||||
if err := r.preCheckPath(to, filepath.Join(app.Root, "panel")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
if err := io.Compress([]string{
|
||||
filepath.Join(app.Root, "panel"),
|
||||
"/usr/local/sbin/panel-cli",
|
||||
"/usr/local/etc/panel/config.yml",
|
||||
}, backup, io.Zip); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
|
||||
color.Greenln(fmt.Sprintf("|-已备份至文件:%s", backup))
|
||||
return nil
|
||||
}
|
||||
|
||||
// restoreWebsite 恢复网站备份
|
||||
func (r *backupRepo) restoreWebsite(backup, target string) error {
|
||||
if !io.Exists(backup) {
|
||||
return errors.New("备份文件不存在")
|
||||
}
|
||||
|
||||
website, err := r.website.GetByName(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
format, err := io.FormatArchiveByPath(backup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = io.Remove(website.Path); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = io.UnCompress(backup, website.Path, format); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = io.Chmod(website.Path, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = io.Chown(website.Path, "www", "www"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// restoreMySQL 恢复 MySQL 备份
|
||||
func (r *backupRepo) restoreMySQL(backup, target string) error {
|
||||
if !io.Exists(backup) {
|
||||
return errors.New("备份文件不存在")
|
||||
}
|
||||
|
||||
rootPassword, err := r.setting.Get(biz.SettingKeyMySQLRootPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exist, _ := mysql.DatabaseExists(target); !exist {
|
||||
return fmt.Errorf("数据库不存在:%s", target)
|
||||
}
|
||||
if err = os.Setenv("MYSQL_PWD", rootPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(backup, ".sql") {
|
||||
backup, err = r.autoUnCompressSQL(backup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err = shell.Execf(`mysql -u root '%s' < '%s'`, target, backup); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = os.Unsetenv("MYSQL_PWD"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = io.Remove(filepath.Dir(backup))
|
||||
return nil
|
||||
}
|
||||
|
||||
// restorePostgres 恢复 PostgreSQL 备份
|
||||
func (r *backupRepo) restorePostgres(backup, target string) error {
|
||||
if !io.Exists(backup) {
|
||||
return errors.New("备份文件不存在")
|
||||
}
|
||||
|
||||
postgres, err := db.NewPostgres("postgres", "", "127.0.0.1", fmt.Sprintf("%s/server/postgresql/data/pg_hba.conf", app.Root), 5432)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exist, _ := postgres.DatabaseExist(target); !exist {
|
||||
return fmt.Errorf("数据库不存在:%s", target)
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(backup, ".sql") {
|
||||
backup, err = r.autoUnCompressSQL(backup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err = shell.Execf(`su - postgres -c "psql '%s'" < '%s'`, target, backup); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = io.Remove(filepath.Dir(backup))
|
||||
return nil
|
||||
}
|
||||
|
||||
// preCheckPath 预检空间和 inode 是否足够
|
||||
// to 备份保存目录
|
||||
// path 待备份目录
|
||||
func (r *backupRepo) preCheckPath(to, path string) error {
|
||||
size, err := io.SizeX(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
files, err := io.CountX(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
usage, err := disk.Usage(to)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln(fmt.Sprintf("|-目标大小:%s", str.FormatBytes(float64(size))))
|
||||
color.Greenln(fmt.Sprintf("|-目标文件数:%d", files))
|
||||
color.Greenln(fmt.Sprintf("|-备份目录可用空间:%s", str.FormatBytes(float64(usage.Free))))
|
||||
color.Greenln(fmt.Sprintf("|-备份目录可用Inode:%d", usage.InodesFree))
|
||||
|
||||
if uint64(size) > usage.Free {
|
||||
return errors.New("备份目录空间不足")
|
||||
}
|
||||
if uint64(files) > usage.InodesFree {
|
||||
return errors.New("备份目录Inode不足")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// preCheckDB 预检空间和 inode 是否足够
|
||||
// to 备份保存目录
|
||||
// size 数据库大小
|
||||
func (r *backupRepo) preCheckDB(to string, size int64) error {
|
||||
usage, err := disk.Usage(to)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln(fmt.Sprintf("|-目标大小:%s", str.FormatBytes(float64(size))))
|
||||
color.Greenln(fmt.Sprintf("|-备份目录可用空间:%s", str.FormatBytes(float64(usage.Free))))
|
||||
color.Greenln(fmt.Sprintf("|-备份目录可用Inode:%d", usage.InodesFree))
|
||||
|
||||
if uint64(size) > usage.Free {
|
||||
return errors.New("备份目录空间不足")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// autoUnCompressSQL 自动处理压缩文件
|
||||
func (r *backupRepo) autoUnCompressSQL(backup string) (string, error) {
|
||||
temp, err := io.TempDir(backup)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
format, err := io.FormatArchiveByPath(backup)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err = io.UnCompress(backup, temp, format); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
backup = "" // 置空,防止干扰后续判断
|
||||
if files, err := os.ReadDir(temp); err == nil {
|
||||
if len(files) != 1 {
|
||||
return "", fmt.Errorf("压缩文件中包含的文件数量不为1,实际为%d", len(files))
|
||||
}
|
||||
if strings.HasSuffix(files[0].Name(), ".sql") {
|
||||
backup = filepath.Join(temp, files[0].Name())
|
||||
}
|
||||
}
|
||||
|
||||
if backup == "" {
|
||||
return "", errors.New("无法找到.sql备份文件")
|
||||
}
|
||||
|
||||
return backup, nil
|
||||
}
|
||||
+19
-21
@@ -60,26 +60,26 @@ func (r *cronRepo) Create(req *request.CronCreate) error {
|
||||
|
||||
var script string
|
||||
if req.Type == "backup" {
|
||||
if len(req.BackupPath) == 0 {
|
||||
req.BackupPath, _ = r.settingRepo.Get(biz.SettingKeyBackupPath)
|
||||
if len(req.BackupPath) == 0 {
|
||||
return errors.New("备份路径不能为空")
|
||||
}
|
||||
req.BackupPath = filepath.Join(req.BackupPath, req.BackupType)
|
||||
}
|
||||
script = fmt.Sprintf(`#!/bin/bash
|
||||
if req.BackupType == "website" {
|
||||
script = fmt.Sprintf(`#!/bin/bash
|
||||
export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
|
||||
|
||||
# 耗子面板 - 数据备份脚本
|
||||
# 耗子面板 - 网站备份脚本
|
||||
|
||||
type=%s
|
||||
path=%s
|
||||
name=%s
|
||||
save=%d
|
||||
panel-cli backup website -n '%s' -p '%s'
|
||||
panel-cli backup clear -t website -f '%s' -s '%d' -p '%s'
|
||||
`, req.Target, req.BackupPath, req.Target, req.Save, req.BackupPath)
|
||||
}
|
||||
if req.BackupType == "mysql" || req.BackupType == "postgres" {
|
||||
script = fmt.Sprintf(`#!/bin/bash
|
||||
export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
|
||||
|
||||
# 执行备份
|
||||
panel backup ${type} ${name} ${path} ${save}
|
||||
`, req.BackupType, req.BackupPath, req.Target, req.Save)
|
||||
# 耗子面板 - 数据库备份脚本
|
||||
|
||||
panel-cli backup database -t '%s' -n '%s' -p '%s'
|
||||
panel-cli backup clear -t '%s' -f '%s' -s '%d' -p '%s'
|
||||
`, req.BackupType, req.Target, req.BackupPath, req.BackupType, req.Target, req.Save, req.BackupPath)
|
||||
}
|
||||
}
|
||||
if req.Type == "cutoff" {
|
||||
script = fmt.Sprintf(`#!/bin/bash
|
||||
@@ -87,12 +87,10 @@ export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
|
||||
|
||||
# 耗子面板 - 日志切割脚本
|
||||
|
||||
name=%s
|
||||
save=%d
|
||||
|
||||
# 执行切割
|
||||
panel cutoff ${name} ${save}
|
||||
`, req.Target, req.Save)
|
||||
panel-cli cutoff website -n %s -p %s
|
||||
panel-cli cutoff clear -t website -f %s -s %d -p %s
|
||||
`, req.Target, req.BackupPath, req.Target, req.Save, req.BackupPath)
|
||||
}
|
||||
|
||||
shellDir := fmt.Sprintf("%s/server/cron/", app.Root)
|
||||
|
||||
@@ -177,10 +177,18 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P
|
||||
}
|
||||
|
||||
func (r *settingRepo) UpdatePanel(version, url, checksum string) error {
|
||||
// 预先优化数据库
|
||||
if err := app.Orm.Exec("VACUUM").Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := app.Orm.Exec("PRAGMA wal_checkpoint(TRUNCATE);").Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
name := filepath.Base(url)
|
||||
color.Greenln("目标版本: ", version)
|
||||
color.Greenln("下载链接: ", url)
|
||||
color.Greenln("文件名: ", name)
|
||||
color.Greenln(fmt.Sprintf("目标版本:%s", version))
|
||||
color.Greenln(fmt.Sprintf("下载链接:%s", url))
|
||||
color.Greenln(fmt.Sprintf("文件名:%s", name))
|
||||
|
||||
color.Greenln("前置检查...")
|
||||
if io.Exists("/tmp/panel-storage.zip") {
|
||||
|
||||
+321
-404
@@ -4,12 +4,9 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
@@ -18,8 +15,9 @@ import (
|
||||
"github.com/TheTNB/panel/pkg/cert"
|
||||
"github.com/TheTNB/panel/pkg/db"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/nginx"
|
||||
"github.com/TheTNB/panel/pkg/punycode"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
@@ -59,78 +57,84 @@ func (r *websiteRepo) Get(id uint) (*types.WebsiteSetting, error) {
|
||||
if err := app.Orm.Where("id", id).First(website).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 解析nginx配置
|
||||
config, err := io.Read(filepath.Join(app.Root, "server/vhost", website.Name+".conf"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p, err := nginx.NewParser(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
setting := new(types.WebsiteSetting)
|
||||
setting.ID = website.ID
|
||||
setting.Name = website.Name
|
||||
setting.Path = website.Path
|
||||
setting.SSL = website.SSL
|
||||
setting.PHP = website.PHP
|
||||
setting.HTTPS = website.Https
|
||||
setting.PHP = p.GetPHP()
|
||||
setting.Raw = config
|
||||
|
||||
portStr := str.Cut(config, "# port标记位开始", "# port标记位结束")
|
||||
matches := regexp.MustCompile(`listen\s+([^;]*);?`).FindAllStringSubmatch(portStr, -1)
|
||||
for _, match := range matches {
|
||||
if len(match) < 2 {
|
||||
continue
|
||||
}
|
||||
// 跳过 ipv6
|
||||
if strings.Contains(match[1], "[::]") {
|
||||
continue
|
||||
}
|
||||
|
||||
// 处理 443 ssl 之类的情况
|
||||
ports := strings.Fields(match[1])
|
||||
if len(ports) == 0 {
|
||||
continue
|
||||
}
|
||||
if !slices.Contains(setting.Ports, cast.ToUint(ports[0])) {
|
||||
setting.Ports = append(setting.Ports, cast.ToUint(ports[0]))
|
||||
}
|
||||
if len(ports) > 1 && ports[1] == "ssl" {
|
||||
setting.SSLPorts = append(setting.SSLPorts, cast.ToUint(ports[0]))
|
||||
} else if len(ports) > 1 && ports[1] == "quic" {
|
||||
setting.QUICPorts = append(setting.QUICPorts, cast.ToUint(ports[0]))
|
||||
}
|
||||
// 监听地址
|
||||
listens, err := p.GetListen()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
serverName := str.Cut(config, "# server_name标记位开始", "# server_name标记位结束")
|
||||
match := regexp.MustCompile(`server_name\s+([^;]*);?`).FindStringSubmatch(serverName)
|
||||
if len(match) > 1 {
|
||||
setting.Domains = strings.Split(match[1], " ")
|
||||
setting.Listens = lo.Map(
|
||||
lo.UniqBy(listens, func(listen []string) string {
|
||||
if len(listen) == 0 {
|
||||
return ""
|
||||
}
|
||||
return listen[0]
|
||||
}),
|
||||
func(listen []string, _ int) types.WebsiteListen {
|
||||
addr := listen[0]
|
||||
grouped := lo.GroupBy(listens, func(listen []string) string {
|
||||
if len(listen) == 0 {
|
||||
return ""
|
||||
}
|
||||
return listen[0]
|
||||
})[addr]
|
||||
return types.WebsiteListen{
|
||||
Address: addr,
|
||||
HTTPS: lo.SomeBy(grouped, func(listen []string) bool { return lo.Contains(listen, "ssl") }),
|
||||
QUIC: lo.SomeBy(grouped, func(listen []string) bool { return lo.Contains(listen, "quic") }),
|
||||
}
|
||||
},
|
||||
)
|
||||
// 域名
|
||||
domains, err := p.GetServerName()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
root := str.Cut(config, "# root标记位开始", "# root标记位结束")
|
||||
match = regexp.MustCompile(`root\s+([^;]*);?`).FindStringSubmatch(root)
|
||||
if len(match) > 1 {
|
||||
setting.Root = match[1]
|
||||
domains, err = punycode.DecodeDomains(domains)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
index := str.Cut(config, "# index标记位开始", "# index标记位结束")
|
||||
match = regexp.MustCompile(`index\s+([^;]*);?`).FindStringSubmatch(index)
|
||||
if len(match) > 1 {
|
||||
setting.Index = match[1]
|
||||
}
|
||||
|
||||
setting.Domains = domains
|
||||
// 运行目录
|
||||
root, _ := p.GetRoot()
|
||||
setting.Root = root
|
||||
// 默认文档
|
||||
index, _ := p.GetIndex()
|
||||
setting.Index = index
|
||||
// 防跨站
|
||||
if io.Exists(filepath.Join(setting.Root, ".user.ini")) {
|
||||
userIni, _ := io.Read(filepath.Join(setting.Root, ".user.ini"))
|
||||
if strings.Contains(userIni, "open_basedir") {
|
||||
setting.OpenBasedir = true
|
||||
}
|
||||
}
|
||||
|
||||
// HTTPS
|
||||
if setting.HTTPS {
|
||||
setting.HTTPRedirect = p.GetHTTPSRedirect()
|
||||
setting.HSTS = p.GetHSTS()
|
||||
setting.OCSP = p.GetOCSP()
|
||||
}
|
||||
// 证书
|
||||
crt, _ := io.Read(filepath.Join(app.Root, "server/vhost/ssl", website.Name+".pem"))
|
||||
setting.SSLCertificate = crt
|
||||
key, _ := io.Read(filepath.Join(app.Root, "server/vhost/ssl", website.Name+".key"))
|
||||
setting.SSLCertificateKey = key
|
||||
if setting.SSL {
|
||||
ssl := str.Cut(config, "# ssl标记位开始", "# ssl标记位结束")
|
||||
setting.HTTPRedirect = strings.Contains(ssl, "# http重定向标记位")
|
||||
setting.HSTS = strings.Contains(ssl, "# hsts标记位")
|
||||
setting.OCSP = strings.Contains(ssl, "# ocsp标记位")
|
||||
}
|
||||
|
||||
// 解析证书信息
|
||||
if decode, err := cert.ParseCert(crt); err == nil {
|
||||
setting.SSLNotBefore = decode.NotBefore.Format("2006-01-02 15:04:05")
|
||||
@@ -139,9 +143,10 @@ func (r *websiteRepo) Get(id uint) (*types.WebsiteSetting, error) {
|
||||
setting.SSLOCSPServer = decode.OCSPServer
|
||||
setting.SSLDNSNames = decode.DNSNames
|
||||
}
|
||||
|
||||
// 伪静态
|
||||
rewrite, _ := io.Read(filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf"))
|
||||
setting.Rewrite = rewrite
|
||||
// 访问日志
|
||||
log, _ := shell.Execf(`tail -n 100 '%s/wwwlogs/%s.log'`, app.Root, website.Name)
|
||||
setting.Log = log
|
||||
|
||||
@@ -174,21 +179,57 @@ func (r *websiteRepo) List(page, limit uint) ([]*biz.Website, int64, error) {
|
||||
}
|
||||
|
||||
func (r *websiteRepo) Create(req *request.WebsiteCreate) (*biz.Website, error) {
|
||||
w := &biz.Website{
|
||||
Name: req.Name,
|
||||
Status: true,
|
||||
Path: req.Path,
|
||||
PHP: req.PHP,
|
||||
SSL: false,
|
||||
// 初始化nginx配置
|
||||
p, err := nginx.NewParser()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := app.Orm.Create(w).Error; err != nil {
|
||||
// 监听地址
|
||||
var listens [][]string
|
||||
for _, listen := range req.Listens {
|
||||
listens = append(listens, []string{listen})
|
||||
}
|
||||
if err = p.SetListen(listens); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 域名
|
||||
domains, err := punycode.EncodeDomains(req.Domains)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = p.SetServerName(domains); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 运行目录
|
||||
if err = p.SetRoot(req.Path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// PHP
|
||||
if err = p.SetPHP(req.PHP); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 伪静态
|
||||
includes, comments, err := p.GetIncludes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
includes = append(includes, filepath.Join(app.Root, "server/vhost/rewrite", req.Name+".conf"))
|
||||
comments = append(comments, []string{"# 伪静态规则"})
|
||||
if err = p.SetIncludes(includes, comments); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 日志
|
||||
if err = p.SetAccessLog(filepath.Join(app.Root, "wwwlogs", req.Name+".log")); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = p.SetErrorLog(filepath.Join(app.Root, "wwwlogs", req.Name+".error.log")); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := io.Mkdir(req.Path, 0755); err != nil {
|
||||
// 初始化网站目录
|
||||
if err = io.Mkdir(req.Path, 0755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
index, err := embed.WebsiteFS.ReadFile(filepath.Join("website", "index.html"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取index模板文件失败: %w", err)
|
||||
@@ -196,100 +237,21 @@ func (r *websiteRepo) Create(req *request.WebsiteCreate) (*biz.Website, error) {
|
||||
if err = io.Write(filepath.Join(req.Path, "index.html"), string(index), 0644); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
notFound, err := embed.WebsiteFS.ReadFile(filepath.Join("website", "404.html"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取404模板文件失败: %w", err)
|
||||
}
|
||||
if err = io.Write(req.Path+"/404.html", string(notFound), 0644); err != nil {
|
||||
if err = io.Write(filepath.Join(req.Path, "404.html"), string(notFound), 0644); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
portList := ""
|
||||
domainList := ""
|
||||
portUsed := make(map[uint]bool)
|
||||
domainUsed := make(map[string]bool)
|
||||
|
||||
for i, port := range req.Ports {
|
||||
if _, ok := portUsed[port]; !ok {
|
||||
if i == len(req.Ports)-1 {
|
||||
portList += " listen " + cast.ToString(port) + ";\n"
|
||||
portList += " listen [::]:" + cast.ToString(port) + ";"
|
||||
} else {
|
||||
portList += " listen " + cast.ToString(port) + ";\n"
|
||||
portList += " listen [::]:" + cast.ToString(port) + ";\n"
|
||||
}
|
||||
portUsed[port] = true
|
||||
}
|
||||
}
|
||||
for _, domain := range req.Domains {
|
||||
if _, ok := domainUsed[domain]; !ok {
|
||||
domainList += " " + domain
|
||||
domainUsed[domain] = true
|
||||
}
|
||||
}
|
||||
|
||||
nginxConf := fmt.Sprintf(`# 配置文件中的标记位请勿随意修改,改错将导致面板无法识别!
|
||||
# 有自定义配置需求的,请将自定义的配置写在各标记位下方。
|
||||
server
|
||||
{
|
||||
# port标记位开始
|
||||
%s
|
||||
# port标记位结束
|
||||
# server_name标记位开始
|
||||
server_name%s;
|
||||
# server_name标记位结束
|
||||
# index标记位开始
|
||||
index index.php index.html;
|
||||
# index标记位结束
|
||||
# root标记位开始
|
||||
root %s;
|
||||
# root标记位结束
|
||||
|
||||
# ssl标记位开始
|
||||
# ssl标记位结束
|
||||
|
||||
# php标记位开始
|
||||
include enable-php-%d.conf;
|
||||
# php标记位结束
|
||||
|
||||
# 错误页配置,可自行设置
|
||||
error_page 404 /404.html;
|
||||
#error_page 502 /502.html;
|
||||
|
||||
# acme证书签发配置,不可修改
|
||||
include %s/server/vhost/acme/%s.conf;
|
||||
|
||||
# 伪静态规则引入,修改后将导致面板设置的伪静态规则失效
|
||||
include %s/server/vhost/rewrite/%s.conf;
|
||||
|
||||
# 面板默认禁止访问部分敏感目录,可自行修改
|
||||
location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn)
|
||||
{
|
||||
return 404;
|
||||
}
|
||||
# 面板默认不记录静态资源的访问日志并开启1小时浏览器缓存,可自行修改
|
||||
location ~ .*\.(js|css)$
|
||||
{
|
||||
expires 1h;
|
||||
error_log /dev/null;
|
||||
access_log /dev/null;
|
||||
}
|
||||
|
||||
access_log %s/wwwlogs/%s.log;
|
||||
error_log %s/wwwlogs/%s.log;
|
||||
}
|
||||
`, portList, domainList, req.Path, req.PHP, app.Root, req.Name, app.Root, req.Name, app.Root, req.Name, app.Root, req.Name)
|
||||
|
||||
if err = io.Write(filepath.Join(app.Root, "server/vhost", req.Name+".conf"), nginxConf, 0644); err != nil {
|
||||
// 写nginx配置
|
||||
if err = io.Write(filepath.Join(app.Root, "server/vhost", req.Name+".conf"), p.Dump(), 0644); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = io.Write(filepath.Join(app.Root, "server/vhost/rewrite", req.Name+".conf"), "", 0644); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = io.Write(filepath.Join(app.Root, "server/vhost/acme", req.Name+".conf"), "", 0644); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = io.Write(filepath.Join(app.Root, "server/vhost/ssl", req.Name+".pem"), "", 0644); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -297,6 +259,7 @@ server
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 设置目录权限
|
||||
if err = io.Chmod(req.Path, 0755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -304,11 +267,23 @@ server
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建面板网站
|
||||
w := &biz.Website{
|
||||
Name: req.Name,
|
||||
Status: true,
|
||||
Path: req.Path,
|
||||
Https: false,
|
||||
}
|
||||
if err = app.Orm.Create(w).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = systemctl.Reload("nginx"); err != nil {
|
||||
_, err = shell.Execf("nginx -t")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建数据库
|
||||
rootPassword, err := r.settingRepo.Get(biz.SettingKeyMySQLRootPassword)
|
||||
if err == nil && req.DB && req.DBType == "mysql" {
|
||||
mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock", "unix")
|
||||
@@ -343,17 +318,17 @@ func (r *websiteRepo) Update(req *request.WebsiteUpdate) error {
|
||||
if err := app.Orm.Where("id", req.ID).First(website).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !website.Status {
|
||||
return errors.New("网站已停用,请先启用")
|
||||
}
|
||||
|
||||
// 原文
|
||||
raw, err := io.Read(filepath.Join(app.Root, "server/vhost", website.Name+".conf"))
|
||||
// 解析nginx配置
|
||||
config, err := io.Read(filepath.Join(app.Root, "server/vhost", website.Name+".conf"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(raw) != strings.TrimSpace(req.Raw) {
|
||||
// 如果修改了原文,直接写入返回
|
||||
if strings.TrimSpace(config) != strings.TrimSpace(req.Raw) {
|
||||
if err = io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), req.Raw, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -361,188 +336,133 @@ func (r *websiteRepo) Update(req *request.WebsiteUpdate) error {
|
||||
_, err = shell.Execf("nginx -t")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 目录
|
||||
path := req.Path
|
||||
if !io.Exists(path) {
|
||||
// 初始化nginx配置
|
||||
p, err := nginx.NewParser(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 监听地址
|
||||
var listens [][]string
|
||||
for _, listen := range req.Listens {
|
||||
if !listen.HTTPS && !listen.QUIC {
|
||||
listens = append(listens, []string{listen.Address})
|
||||
}
|
||||
if listen.HTTPS {
|
||||
listens = append(listens, []string{listen.Address, "ssl"})
|
||||
}
|
||||
if listen.QUIC {
|
||||
listens = append(listens, []string{listen.Address, "quic"})
|
||||
}
|
||||
}
|
||||
if err = p.SetListen(listens); err != nil {
|
||||
return err
|
||||
}
|
||||
// 域名
|
||||
domains, err := punycode.EncodeDomains(req.Domains)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = p.SetServerName(domains); err != nil {
|
||||
return err
|
||||
}
|
||||
// 首页文件
|
||||
if err = p.SetIndex(req.Index); err != nil {
|
||||
return err
|
||||
}
|
||||
// 运行目录
|
||||
if !io.Exists(req.Root) {
|
||||
return errors.New("运行目录不存在")
|
||||
}
|
||||
if err = p.SetRoot(req.Root); err != nil {
|
||||
return err
|
||||
}
|
||||
// 运行目录
|
||||
if !io.Exists(req.Path) {
|
||||
return errors.New("网站目录不存在")
|
||||
}
|
||||
website.Path = path
|
||||
|
||||
// 域名
|
||||
domain := "server_name"
|
||||
domains := req.Domains
|
||||
for _, v := range domains {
|
||||
if v == "" {
|
||||
continue
|
||||
website.Path = req.Path
|
||||
// PHP
|
||||
if err = p.SetPHP(req.PHP); err != nil {
|
||||
return err
|
||||
}
|
||||
// HTTPS
|
||||
certPath := filepath.Join(app.Root, "server/vhost/ssl", website.Name+".pem")
|
||||
keyPath := filepath.Join(app.Root, "server/vhost/ssl", website.Name+".key")
|
||||
if err = io.Write(certPath, req.SSLCertificate, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = io.Write(keyPath, req.SSLCertificateKey, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
website.Https = req.HTTPS
|
||||
if req.HTTPS {
|
||||
if err = p.SetHTTPS(certPath, keyPath); err != nil {
|
||||
return err
|
||||
}
|
||||
domain += " " + v
|
||||
}
|
||||
domain += ";"
|
||||
domainConfigOld := str.Cut(raw, "# server_name标记位开始", "# server_name标记位结束")
|
||||
if len(strings.TrimSpace(domainConfigOld)) == 0 {
|
||||
return errors.New("配置文件中缺少server_name标记位")
|
||||
}
|
||||
raw = strings.Replace(raw, domainConfigOld, "\n "+domain+"\n ", -1)
|
||||
|
||||
// 端口
|
||||
var portConf strings.Builder
|
||||
ports := req.Ports
|
||||
for _, port := range ports {
|
||||
https := ""
|
||||
quic := false
|
||||
if slices.Contains(req.SSLPorts, port) {
|
||||
https = " ssl"
|
||||
if slices.Contains(req.QUICPorts, port) {
|
||||
quic = true
|
||||
}
|
||||
if err = p.SetHTTPRedirect(req.HTTPRedirect); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = p.SetHSTS(req.HSTS); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = p.SetOCSP(req.OCSP); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
portConf.WriteString(fmt.Sprintf(" listen %d%s;\n", port, https))
|
||||
portConf.WriteString(fmt.Sprintf(" listen [::]:%d%s;\n", port, https))
|
||||
if quic {
|
||||
portConf.WriteString(fmt.Sprintf(" listen %d%s;\n", port, " quic"))
|
||||
portConf.WriteString(fmt.Sprintf(" listen [::]:%d%s;\n", port, " quic"))
|
||||
} else {
|
||||
if err = p.ClearSetHTTPS(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = p.SetHTTPRedirect(false); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = p.SetHSTS(false); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = p.SetOCSP(false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
portConf.WriteString(" ")
|
||||
portConfNew := portConf.String()
|
||||
portConfOld := str.Cut(raw, "# port标记位开始", "# port标记位结束")
|
||||
if len(strings.TrimSpace(portConfOld)) == 0 {
|
||||
return errors.New("配置文件中缺少port标记位")
|
||||
}
|
||||
raw = strings.Replace(raw, portConfOld, "\n"+portConfNew, -1)
|
||||
|
||||
// 运行目录
|
||||
root := str.Cut(raw, "# root标记位开始", "# root标记位结束")
|
||||
if len(strings.TrimSpace(root)) == 0 {
|
||||
return errors.New("配置文件中缺少root标记位")
|
||||
}
|
||||
match := regexp.MustCompile(`root\s+(.+);`).FindStringSubmatch(root)
|
||||
if len(match) != 2 {
|
||||
return errors.New("配置文件中root标记位格式错误")
|
||||
}
|
||||
rootNew := strings.Replace(root, match[1], req.Root, -1)
|
||||
raw = strings.Replace(raw, root, rootNew, -1)
|
||||
|
||||
// 默认文件
|
||||
index := str.Cut(raw, "# index标记位开始", "# index标记位结束")
|
||||
if len(strings.TrimSpace(index)) == 0 {
|
||||
return errors.New("配置文件中缺少index标记位")
|
||||
}
|
||||
match = regexp.MustCompile(`index\s+(.+);`).FindStringSubmatch(index)
|
||||
if len(match) != 2 {
|
||||
return errors.New("配置文件中index标记位格式错误")
|
||||
}
|
||||
indexNew := strings.Replace(index, match[1], req.Index, -1)
|
||||
raw = strings.Replace(raw, index, indexNew, -1)
|
||||
|
||||
// 防跨站
|
||||
if !strings.HasSuffix(req.Root, "/") {
|
||||
req.Root += "/"
|
||||
}
|
||||
userIni := filepath.Join(req.Root, ".user.ini")
|
||||
if req.OpenBasedir {
|
||||
if err = io.Write(req.Root+".user.ini", "open_basedir="+path+":/tmp/", 0644); err != nil {
|
||||
_, _ = shell.Execf(`chattr -i '%s'`, userIni)
|
||||
if err = io.Write(userIni, fmt.Sprintf("open_basedir=%s:/tmp/", req.Root), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = shell.Execf(`chattr +i '%s'`, userIni)
|
||||
} else {
|
||||
if io.Exists(req.Root + ".user.ini") {
|
||||
if err = io.Remove(req.Root + ".user.ini"); err != nil {
|
||||
if io.Exists(userIni) {
|
||||
_, _ = shell.Execf(`chattr -i '%s'`, userIni)
|
||||
if err = io.Remove(userIni); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SSL
|
||||
if err = io.Write(filepath.Join(app.Root, "server/vhost/ssl", website.Name+".pem"), req.SSLCertificate, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = io.Write(filepath.Join(app.Root, "server/vhost/ssl", website.Name+".key"), req.SSLCertificateKey, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
website.SSL = req.SSL
|
||||
if req.SSL {
|
||||
if _, err = cert.ParseCert(req.SSLCertificate); err != nil {
|
||||
return errors.New("TLS证书格式错误")
|
||||
}
|
||||
if _, err = cert.ParseKey(req.SSLCertificateKey); err != nil {
|
||||
return errors.New("TLS私钥格式错误")
|
||||
}
|
||||
sslConfig := fmt.Sprintf(`# ssl标记位开始
|
||||
ssl_certificate %s/server/vhost/ssl/%s.pem;
|
||||
ssl_certificate_key %s/server/vhost/ssl/%s.key;
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_prefer_server_ciphers off;
|
||||
ssl_early_data on;
|
||||
`, app.Root, website.Name, app.Root, website.Name)
|
||||
if req.HTTPRedirect {
|
||||
sslConfig += `# http重定向标记位开始
|
||||
if ($server_port !~ 443){
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
error_page 497 https://$host$request_uri;
|
||||
# http重定向标记位结束
|
||||
`
|
||||
}
|
||||
if req.HSTS {
|
||||
sslConfig += `# hsts标记位开始
|
||||
add_header Strict-Transport-Security "max-age=63072000" always;
|
||||
# hsts标记位结束
|
||||
`
|
||||
}
|
||||
if req.OCSP {
|
||||
sslConfig += `# ocsp标记位开始
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
# ocsp标记位结束
|
||||
`
|
||||
}
|
||||
sslConfigOld := str.Cut(raw, "# ssl标记位开始", "# ssl标记位结束")
|
||||
if len(strings.TrimSpace(sslConfigOld)) != 0 {
|
||||
raw = strings.Replace(raw, sslConfigOld, "", -1)
|
||||
}
|
||||
raw = strings.Replace(raw, "# ssl标记位开始", sslConfig, -1)
|
||||
} else {
|
||||
sslConfigOld := str.Cut(raw, "# ssl标记位开始", "# ssl标记位结束")
|
||||
if len(strings.TrimSpace(sslConfigOld)) != 0 {
|
||||
raw = strings.Replace(raw, sslConfigOld, "\n ", -1)
|
||||
}
|
||||
}
|
||||
|
||||
if website.PHP != req.PHP {
|
||||
website.PHP = req.PHP
|
||||
phpConfigOld := str.Cut(raw, "# php标记位开始", "# php标记位结束")
|
||||
phpConfig := `
|
||||
include enable-php-` + strconv.Itoa(website.PHP) + `.conf;
|
||||
`
|
||||
if len(strings.TrimSpace(phpConfigOld)) != 0 {
|
||||
raw = strings.Replace(raw, phpConfigOld, phpConfig, -1)
|
||||
}
|
||||
}
|
||||
|
||||
if err = app.Orm.Save(website).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), raw, 0644); err != nil {
|
||||
if err = io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), p.Dump(), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = io.Write(filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf"), req.Rewrite, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = systemctl.Reload("nginx")
|
||||
if err != nil {
|
||||
_, err = shell.Execf("nginx -t")
|
||||
if err = app.Orm.Save(website).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
if err = systemctl.Reload("nginx"); err != nil {
|
||||
_, err = shell.Execf("nginx -t")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *websiteRepo) Delete(req *request.WebsiteDelete) error {
|
||||
@@ -550,15 +470,10 @@ func (r *websiteRepo) Delete(req *request.WebsiteDelete) error {
|
||||
if err := app.Orm.Preload("Cert").Where("id", req.ID).First(website).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if website.Cert != nil {
|
||||
return errors.New("网站" + website.Name + "已绑定SSL证书,请先删除证书")
|
||||
}
|
||||
|
||||
if err := app.Orm.Delete(website).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = io.Remove(filepath.Join(app.Root, "server/vhost", website.Name+".conf"))
|
||||
_ = io.Remove(filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf"))
|
||||
_ = io.Remove(filepath.Join(app.Root, "server/vhost/acme", website.Name+".conf"))
|
||||
@@ -583,12 +498,16 @@ func (r *websiteRepo) Delete(req *request.WebsiteDelete) error {
|
||||
_, _ = shell.Execf(`echo "DROP USER IF EXISTS '%s';" | su - postgres -c "psql"`, website.Name)
|
||||
}
|
||||
|
||||
err := systemctl.Reload("nginx")
|
||||
if err != nil {
|
||||
_, err = shell.Execf("nginx -t")
|
||||
if err := app.Orm.Delete(website).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
if err := systemctl.Reload("nginx"); err != nil {
|
||||
_, err = shell.Execf("nginx -t")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *websiteRepo) ClearLog(id uint) error {
|
||||
@@ -617,75 +536,47 @@ func (r *websiteRepo) ResetConfig(id uint) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 初始化nginx配置
|
||||
p, err := nginx.NewParser()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 运行目录
|
||||
if err = p.SetRoot(website.Path); err != nil {
|
||||
return err
|
||||
}
|
||||
// 伪静态
|
||||
includes, comments, err := p.GetIncludes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
includes = append(includes, filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf"))
|
||||
comments = append(comments, []string{"# 伪静态规则"})
|
||||
if err = p.SetIncludes(includes, comments); err != nil {
|
||||
return err
|
||||
}
|
||||
// 日志
|
||||
if err = p.SetAccessLog(filepath.Join(app.Root, "wwwlogs", website.Name+".log")); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = p.SetErrorLog(filepath.Join(app.Root, "wwwlogs", website.Name+".error.log")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), p.Dump(), 0644); err != nil {
|
||||
return nil
|
||||
}
|
||||
if err = io.Write(filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf"), "", 0644); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
website.Status = true
|
||||
website.SSL = false
|
||||
website.Https = false
|
||||
if err := app.Orm.Save(website).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
raw := fmt.Sprintf(`
|
||||
# 配置文件中的标记位请勿随意修改,改错将导致面板无法识别!
|
||||
# 有自定义配置需求的,请将自定义的配置写在各标记位下方。
|
||||
server
|
||||
{
|
||||
# port标记位开始
|
||||
listen 80;
|
||||
# port标记位结束
|
||||
# server_name标记位开始
|
||||
server_name localhost;
|
||||
# server_name标记位结束
|
||||
# index标记位开始
|
||||
index index.php index.html;
|
||||
# index标记位结束
|
||||
# root标记位开始
|
||||
root %s;
|
||||
# root标记位结束
|
||||
|
||||
# ssl标记位开始
|
||||
# ssl标记位结束
|
||||
|
||||
# php标记位开始
|
||||
include enable-php-%d.conf;
|
||||
# php标记位结束
|
||||
|
||||
# 错误页配置,可自行设置
|
||||
error_page 404 /404.html;
|
||||
#error_page 502 /502.html;
|
||||
|
||||
# acme证书签发配置,不可修改
|
||||
include %s/server/vhost/acme/%s.conf;
|
||||
|
||||
# 伪静态规则引入,修改后将导致面板设置的伪静态规则失效
|
||||
include %s/server/vhost/rewrite/%s.conf;
|
||||
|
||||
# 面板默认禁止访问部分敏感目录,可自行修改
|
||||
location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn)
|
||||
{
|
||||
return 404;
|
||||
}
|
||||
# 面板默认不记录静态资源的访问日志并开启1小时浏览器缓存,可自行修改
|
||||
location ~ .*\.(js|css)$
|
||||
{
|
||||
expires 1h;
|
||||
error_log /dev/null;
|
||||
access_log /dev/null;
|
||||
}
|
||||
|
||||
access_log %s/wwwlogs/%s.log;
|
||||
error_log %s/wwwlogs/%s.log;
|
||||
}
|
||||
|
||||
`, website.Path, website.PHP, app.Root, website.Name, app.Root, website.Name, app.Root, website.Name, app.Root, website.Name)
|
||||
if err := io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), raw, 0644); err != nil {
|
||||
return nil
|
||||
}
|
||||
if err := io.Write(filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf"), "", 0644); err != nil {
|
||||
return nil
|
||||
}
|
||||
if err := io.Write(filepath.Join(app.Root, "server/vhost/acme", website.Name+".conf"), "", 0644); err != nil {
|
||||
return nil
|
||||
}
|
||||
if err := systemctl.Reload("nginx"); err != nil {
|
||||
if err = systemctl.Reload("nginx"); err != nil {
|
||||
_, err = shell.Execf("nginx -t")
|
||||
return err
|
||||
}
|
||||
@@ -699,43 +590,69 @@ func (r *websiteRepo) UpdateStatus(id uint, status bool) error {
|
||||
return err
|
||||
}
|
||||
|
||||
website.Status = status
|
||||
if err := app.Orm.Save(website).Error; err != nil {
|
||||
// 解析nginx配置
|
||||
config, err := io.Read(filepath.Join(app.Root, "server/vhost", website.Name+".conf"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
raw, err := io.Read(filepath.Join(app.Root, "server/vhost", website.Name+".conf"))
|
||||
p, err := nginx.NewParser(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 运行目录
|
||||
rootConfig := str.Cut(raw, "# root标记位开始\n", "# root标记位结束")
|
||||
match := regexp.MustCompile(`root\s+(.+);`).FindStringSubmatch(rootConfig)
|
||||
if len(match) == 2 {
|
||||
if website.Status {
|
||||
root := regexp.MustCompile(`# root\s+(.+);`).FindStringSubmatch(rootConfig)
|
||||
raw = strings.ReplaceAll(raw, rootConfig, fmt.Sprintf(" root %s;\n ", root[1]))
|
||||
} else {
|
||||
raw = strings.ReplaceAll(raw, rootConfig, fmt.Sprintf(" root %s/server/nginx/html;\n # root %s;\n ", app.Root, match[1]))
|
||||
}
|
||||
}
|
||||
|
||||
// 默认文件
|
||||
indexConfig := str.Cut(raw, "# index标记位开始\n", "# index标记位结束")
|
||||
match = regexp.MustCompile(`index\s+(.+);`).FindStringSubmatch(indexConfig)
|
||||
if len(match) == 2 {
|
||||
if website.Status {
|
||||
index := regexp.MustCompile(`# index\s+(.+);`).FindStringSubmatch(indexConfig)
|
||||
raw = strings.ReplaceAll(raw, indexConfig, " index "+index[1]+";\n ")
|
||||
} else {
|
||||
raw = strings.ReplaceAll(raw, indexConfig, " index stop.html;\n # index "+match[1]+";\n ")
|
||||
}
|
||||
}
|
||||
|
||||
if err = io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), raw, 0644); err != nil {
|
||||
// 取运行目录和默认文档
|
||||
root, rootComment, err := p.GetRootWithComment()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
index, indexComment, err := p.GetIndexWithComment()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
indexStr := strings.Join(index, " ")
|
||||
|
||||
if status {
|
||||
if len(rootComment) == 0 {
|
||||
return fmt.Errorf("未找到运行目录注释")
|
||||
}
|
||||
if len(rootComment) != 1 {
|
||||
return fmt.Errorf("运行目录注释数量不正确,预期1个,实际%d个", len(rootComment))
|
||||
}
|
||||
rootComment[0] = strings.TrimPrefix(rootComment[0], "# ")
|
||||
if !io.Exists(rootComment[0]) {
|
||||
return fmt.Errorf("运行目录不存在")
|
||||
}
|
||||
if err = p.SetRoot(rootComment[0]); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(indexComment) == 0 {
|
||||
return fmt.Errorf("未找到默认文档注释")
|
||||
}
|
||||
if len(indexComment) != 1 {
|
||||
return fmt.Errorf("默认文档注释数量不正确,预期1个,实际%d个", len(indexComment))
|
||||
}
|
||||
indexComment[0] = strings.TrimPrefix(indexComment[0], "# ")
|
||||
if err = p.SetIndex(strings.Fields(indexComment[0])); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err = p.SetRootWithComment(filepath.Join(app.Root, "server/nginx/html"), []string{"# " + root}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = p.SetIndexWithComment([]string{"stop.html"}, []string{"# " + indexStr}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err = io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), p.Dump(), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
website.Status = status
|
||||
if err = app.Orm.Save(website).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = systemctl.Reload("nginx"); err != nil {
|
||||
_, err = shell.Execf("nginx -t")
|
||||
return err
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
package request
|
||||
|
||||
type App struct {
|
||||
Slug string `json:"slug" form:"slug"`
|
||||
Channel string `json:"channel" form:"channel"`
|
||||
Slug string `json:"slug" form:"slug" validate:"required"`
|
||||
Channel string `json:"channel" form:"channel" validate:"required"`
|
||||
}
|
||||
|
||||
type AppSlug struct {
|
||||
Slug string `json:"slug" form:"slug"`
|
||||
Slug string `json:"slug" form:"slug" validate:"required"`
|
||||
}
|
||||
|
||||
type AppUpdateShow struct {
|
||||
Slug string `json:"slug" form:"slug"`
|
||||
Show bool `json:"show" form:"show"`
|
||||
Slug string `json:"slug" form:"slug" validate:"required"`
|
||||
Show bool `json:"show" form:"show" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package request
|
||||
|
||||
type BackupList struct {
|
||||
Type string `json:"type" form:"type" validate:"required,oneof=path website mysql postgres redis panel"`
|
||||
}
|
||||
|
||||
type BackupCreate struct {
|
||||
Type string `json:"type" form:"type" validate:"required,oneof=website mysql postgres redis panel"`
|
||||
Target string `json:"target" form:"target" validate:"required"`
|
||||
Path string `json:"path" form:"path"`
|
||||
}
|
||||
|
||||
type BackupFile struct {
|
||||
Type string `json:"type" form:"type" validate:"required,oneof=website mysql postgres redis panel"`
|
||||
File string `json:"file" form:"file" validate:"required"`
|
||||
}
|
||||
|
||||
type BackupRestore struct {
|
||||
Type string `json:"type" form:"type" validate:"required,oneof=website mysql postgres redis panel"`
|
||||
File string `json:"file" form:"file" validate:"required"`
|
||||
Target string `json:"target" form:"target" validate:"required"`
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package request
|
||||
|
||||
type CertCreate struct {
|
||||
Type string `form:"type" json:"type"`
|
||||
Domains []string `form:"domains" json:"domains"`
|
||||
Type string `form:"type" json:"type" validate:"required"`
|
||||
Domains []string `form:"domains" json:"domains" validate:"min=1,dive,required"`
|
||||
AutoRenew bool `form:"auto_renew" json:"auto_renew"`
|
||||
AccountID uint `form:"account_id" json:"account_id"`
|
||||
DNSID uint `form:"dns_id" json:"dns_id"`
|
||||
@@ -10,9 +10,9 @@ type CertCreate struct {
|
||||
}
|
||||
|
||||
type CertUpdate struct {
|
||||
ID uint `form:"id" json:"id"`
|
||||
Type string `form:"type" json:"type"`
|
||||
Domains []string `form:"domains" json:"domains"`
|
||||
ID uint `form:"id" json:"id" validate:"required"`
|
||||
Type string `form:"type" json:"type" validate:"required"`
|
||||
Domains []string `form:"domains" json:"domains" validate:"min=1,dive,required"`
|
||||
AutoRenew bool `form:"auto_renew" json:"auto_renew"`
|
||||
AccountID uint `form:"account_id" json:"account_id"`
|
||||
DNSID uint `form:"dns_id" json:"dns_id"`
|
||||
@@ -20,6 +20,6 @@ type CertUpdate struct {
|
||||
}
|
||||
|
||||
type CertDeploy struct {
|
||||
ID uint `form:"id" json:"id"`
|
||||
WebsiteID uint `form:"website_id" json:"website_id"`
|
||||
ID uint `form:"id" json:"id" validate:"required"`
|
||||
WebsiteID uint `form:"website_id" json:"website_id" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
package request
|
||||
|
||||
type CertAccountCreate struct {
|
||||
CA string `form:"ca" json:"ca"`
|
||||
Email string `form:"email" json:"email"`
|
||||
CA string `form:"ca" json:"ca" validate:"required"`
|
||||
Email string `form:"email" json:"email" validate:"required"`
|
||||
Kid string `form:"kid" json:"kid"`
|
||||
HmacEncoded string `form:"hmac_encoded" json:"hmac_encoded"`
|
||||
KeyType string `form:"key_type" json:"key_type"`
|
||||
KeyType string `form:"key_type" json:"key_type" validate:"required"`
|
||||
}
|
||||
|
||||
type CertAccountUpdate struct {
|
||||
ID uint `form:"id" json:"id"`
|
||||
CA string `form:"ca" json:"ca"`
|
||||
Email string `form:"email" json:"email"`
|
||||
ID uint `form:"id" json:"id" validate:"required"`
|
||||
CA string `form:"ca" json:"ca" validate:"required"`
|
||||
Email string `form:"email" json:"email" validate:"required"`
|
||||
Kid string `form:"kid" json:"kid"`
|
||||
HmacEncoded string `form:"hmac_encoded" json:"hmac_encoded"`
|
||||
KeyType string `form:"key_type" json:"key_type"`
|
||||
KeyType string `form:"key_type" json:"key_type" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -3,14 +3,14 @@ package request
|
||||
import "github.com/TheTNB/panel/pkg/acme"
|
||||
|
||||
type CertDNSCreate struct {
|
||||
Type string `form:"type" json:"type"`
|
||||
Name string `form:"name" json:"name"`
|
||||
Type string `form:"type" json:"type" validate:"required"`
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
Data acme.DNSParam `form:"data" json:"data"`
|
||||
}
|
||||
|
||||
type CertDNSUpdate struct {
|
||||
ID uint `form:"id" json:"id"`
|
||||
Type string `form:"type" json:"type"`
|
||||
Name string `form:"name" json:"name"`
|
||||
ID uint `form:"id" json:"id" validate:"required"`
|
||||
Type string `form:"type" json:"type" validate:"required"`
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
Data acme.DNSParam `form:"data" json:"data"`
|
||||
}
|
||||
|
||||
@@ -3,17 +3,17 @@ package request
|
||||
import "github.com/TheTNB/panel/pkg/types"
|
||||
|
||||
type ContainerID struct {
|
||||
ID string `json:"id" form:"id"`
|
||||
ID string `json:"id" form:"id" validate:"required"`
|
||||
}
|
||||
|
||||
type ContainerRename struct {
|
||||
ID string `form:"id" json:"id"`
|
||||
Name string `form:"name" json:"name"`
|
||||
ID string `form:"id" json:"id" validate:"required"`
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
}
|
||||
|
||||
type ContainerCreate struct {
|
||||
Name string `form:"name" json:"name"`
|
||||
Image string `form:"image" json:"image"`
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
Image string `form:"image" json:"image" validate:"required"`
|
||||
Ports []types.ContainerPort `form:"ports" json:"ports"`
|
||||
Network string `form:"network" json:"network"`
|
||||
Volumes []types.ContainerVolume `form:"volumes" json:"volumes"`
|
||||
|
||||
@@ -5,7 +5,7 @@ type ContainerImageID struct {
|
||||
}
|
||||
|
||||
type ContainerImagePull struct {
|
||||
Name string `form:"name" json:"name"`
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
Auth bool `form:"auth" json:"auth"`
|
||||
Username string `form:"username" json:"username"`
|
||||
Password string `form:"password" json:"password"`
|
||||
|
||||
@@ -3,11 +3,11 @@ package request
|
||||
import "github.com/TheTNB/panel/pkg/types"
|
||||
|
||||
type ContainerNetworkID struct {
|
||||
ID string `json:"id" form:"id"`
|
||||
ID string `json:"id" form:"id" validate:"required"`
|
||||
}
|
||||
|
||||
type ContainerNetworkCreate struct {
|
||||
Name string `form:"name" json:"name"`
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
Driver string `form:"driver" json:"driver"`
|
||||
Ipv4 types.ContainerNetwork `form:"ipv4" json:"ipv4"`
|
||||
Ipv6 types.ContainerNetwork `form:"ipv6" json:"ipv6"`
|
||||
@@ -16,6 +16,6 @@ type ContainerNetworkCreate struct {
|
||||
}
|
||||
|
||||
type ContainerNetworkConnect struct {
|
||||
Network string `form:"network" json:"network"`
|
||||
Container string `form:"container" json:"container"`
|
||||
Network string `form:"network" json:"network" validate:"required"`
|
||||
Container string `form:"container" json:"container" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@ package request
|
||||
import "github.com/TheTNB/panel/pkg/types"
|
||||
|
||||
type ContainerVolumeID struct {
|
||||
ID string `json:"id" form:"id"`
|
||||
ID string `json:"id" form:"id" validate:"required"`
|
||||
}
|
||||
|
||||
type ContainerVolumeCreate struct {
|
||||
Name string `form:"name" json:"name"`
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
Driver string `form:"driver" json:"driver"`
|
||||
Labels []types.KV `form:"labels" json:"labels"`
|
||||
Options []types.KV `form:"options" json:"options"`
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
package request
|
||||
|
||||
type CronCreate struct {
|
||||
Name string `form:"name" json:"name"`
|
||||
Type string `form:"type" json:"type"`
|
||||
Time string `form:"time" json:"time"`
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
Type string `form:"type" json:"type" validate:"required"`
|
||||
Time string `form:"time" json:"time" validate:"required"`
|
||||
Script string `form:"script" json:"script"`
|
||||
BackupType string `form:"backup_type" json:"backup_type"`
|
||||
BackupType string `form:"backup_type" json:"backup_type" validate:"required"`
|
||||
BackupPath string `form:"backup_path" json:"backup_path"`
|
||||
Target string `form:"target" json:"target"`
|
||||
Save int `form:"save" json:"save"`
|
||||
Target string `form:"target" json:"target" validate:"required"`
|
||||
Save int `form:"save" json:"save" validate:"required"`
|
||||
}
|
||||
|
||||
type CronUpdate struct {
|
||||
ID uint `form:"id" json:"id"`
|
||||
Name string `form:"name" json:"name"`
|
||||
Time string `form:"time" json:"time"`
|
||||
Script string `form:"script" json:"script"`
|
||||
ID uint `form:"id" json:"id" validate:"required"`
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
Time string `form:"time" json:"time" validate:"required"`
|
||||
Script string `form:"script" json:"script" validate:"required"`
|
||||
}
|
||||
|
||||
type CronStatus struct {
|
||||
ID uint `form:"id" json:"id"`
|
||||
ID uint `form:"id" json:"id" validate:"required"`
|
||||
Status bool `form:"status" json:"status"`
|
||||
}
|
||||
|
||||
@@ -1,54 +1,49 @@
|
||||
package request
|
||||
|
||||
type FilePath struct {
|
||||
Path string `json:"path" form:"path"`
|
||||
Path string `json:"path" form:"path" validate:"required"`
|
||||
}
|
||||
|
||||
type FileCreate struct {
|
||||
Dir bool `json:"dir" form:"dir"`
|
||||
Path string `json:"path" form:"path"`
|
||||
Path string `json:"path" form:"path" validate:"required"`
|
||||
}
|
||||
|
||||
type FileSave struct {
|
||||
Path string `form:"path" json:"path"`
|
||||
Content string `form:"content" json:"content"`
|
||||
}
|
||||
|
||||
type FileUpload struct {
|
||||
Path string `json:"path" form:"path"`
|
||||
File []byte `json:"file" form:"file"`
|
||||
Path string `form:"path" json:"path" validate:"required"`
|
||||
Content string `form:"content" json:"content" validate:"required"`
|
||||
}
|
||||
|
||||
type FileMove struct {
|
||||
Source string `form:"source" json:"source"`
|
||||
Target string `form:"target" json:"target"`
|
||||
Source string `form:"source" json:"source" validate:"required"`
|
||||
Target string `form:"target" json:"target" validate:"required"`
|
||||
Force bool `form:"force" json:"force"`
|
||||
}
|
||||
|
||||
type FileCopy struct {
|
||||
Source string `form:"source" json:"source"`
|
||||
Target string `form:"target" json:"target"`
|
||||
Source string `form:"source" json:"source" validate:"required"`
|
||||
Target string `form:"target" json:"target" validate:"required"`
|
||||
Force bool `form:"force" json:"force"`
|
||||
}
|
||||
|
||||
type FilePermission struct {
|
||||
Path string `form:"path" json:"path"`
|
||||
Mode string `form:"mode" json:"mode"`
|
||||
Owner string `form:"owner" json:"owner"`
|
||||
Group string `form:"group" json:"group"`
|
||||
Path string `form:"path" json:"path" validate:"required"`
|
||||
Mode string `form:"mode" json:"mode" validate:"required"`
|
||||
Owner string `form:"owner" json:"owner" validate:"required"`
|
||||
Group string `form:"group" json:"group" validate:"required"`
|
||||
}
|
||||
|
||||
type FileCompress struct {
|
||||
Paths []string `form:"paths" json:"paths"`
|
||||
File string `form:"file" json:"file"`
|
||||
Paths []string `form:"paths" json:"paths" validate:"min=1,dive,required"`
|
||||
File string `form:"file" json:"file" validate:"required"`
|
||||
}
|
||||
|
||||
type FileUnCompress struct {
|
||||
File string `form:"file" json:"file"`
|
||||
Path string `form:"path" json:"path"`
|
||||
File string `form:"file" json:"file" validate:"required"`
|
||||
Path string `form:"path" json:"path" validate:"required"`
|
||||
}
|
||||
|
||||
type FileSearch struct {
|
||||
Path string `form:"path" json:"path"`
|
||||
KeyWord string `form:"keyword" json:"keyword"`
|
||||
Path string `form:"path" json:"path" validate:"required"`
|
||||
KeyWord string `form:"keyword" json:"keyword" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -5,6 +5,6 @@ type FirewallStatus struct {
|
||||
}
|
||||
|
||||
type FirewallCreateRule struct {
|
||||
Port uint `json:"port"`
|
||||
Protocol string `json:"protocol"`
|
||||
Port uint `json:"port" validate:"required"`
|
||||
Protocol string `json:"protocol" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package request
|
||||
|
||||
type SafeUpdateSSH struct {
|
||||
Port uint `json:"port" form:"port"`
|
||||
Port uint `json:"port" form:"port" validate:"required,number,gte=1,lte=65535"`
|
||||
Status bool `json:"status" form:"status"`
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
package request
|
||||
|
||||
type PanelSetting struct {
|
||||
Name string `json:"name"`
|
||||
Locale string `json:"locale"`
|
||||
Entrance string `json:"entrance"`
|
||||
WebsitePath string `json:"website_path"`
|
||||
BackupPath string `json:"backup_path"`
|
||||
Username string `json:"username"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Locale string `json:"locale" validate:"required"`
|
||||
Entrance string `json:"entrance" validate:"required"`
|
||||
WebsitePath string `json:"website_path" validate:"required"`
|
||||
BackupPath string `json:"backup_path" validate:"required"`
|
||||
Username string `json:"username" validate:"required"`
|
||||
Password string `json:"password"`
|
||||
Email string `json:"email"`
|
||||
Port int `json:"port"`
|
||||
Email string `json:"email" validate:"required"`
|
||||
Port int `json:"port" validate:"required,number,gte=1,lte=65535"`
|
||||
HTTPS bool `json:"https"`
|
||||
Cert string `json:"cert"`
|
||||
Key string `json:"key"`
|
||||
Cert string `json:"cert" validate:"required"`
|
||||
Key string `json:"key" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package request
|
||||
|
||||
type SSHUpdateInfo struct {
|
||||
Host string `json:"host" form:"host"`
|
||||
Port int `json:"port" form:"port"`
|
||||
User string `json:"user" form:"user"`
|
||||
Password string `json:"password" form:"password"`
|
||||
Host string `json:"host" form:"host" validate:"required"`
|
||||
Port int `json:"port" form:"port" validate:"required,number,gte=1,lte=65535"`
|
||||
User string `json:"user" form:"user" validate:"required"`
|
||||
Password string `json:"password" form:"password" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
package request
|
||||
|
||||
type SystemctlService struct {
|
||||
Service string `json:"service"`
|
||||
Service string `json:"service" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -4,11 +4,3 @@ type UserLogin struct {
|
||||
Username string `json:"username" form:"username" validate:"required,min=3,max=255"`
|
||||
Password string `json:"password" form:"password" validate:"required,min=6,max=255"`
|
||||
}
|
||||
|
||||
type UserID struct {
|
||||
ID uint `uri:"id" validate:"required,number"`
|
||||
}
|
||||
|
||||
type AddUser struct {
|
||||
Name string `json:"name" form:"name" validate:"required,min=3,max=255" comment:"用户名"`
|
||||
}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
package request
|
||||
|
||||
import "github.com/TheTNB/panel/pkg/types"
|
||||
|
||||
type WebsiteDefaultConfig struct {
|
||||
Index string `json:"index" form:"index"`
|
||||
Stop string `json:"stop" form:"stop"`
|
||||
Index string `json:"index" form:"index" validate:"required"`
|
||||
Stop string `json:"stop" form:"stop" validate:"required"`
|
||||
}
|
||||
|
||||
type WebsiteCreate struct {
|
||||
Name string `form:"name" json:"name"`
|
||||
Domains []string `form:"domains" json:"domains"`
|
||||
Ports []uint `form:"ports" json:"ports"`
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
Listens []string `form:"listens" json:"listens" validate:"min=1,dive,required"`
|
||||
Domains []string `form:"domains" json:"domains" validate:"min=1,dive,required"`
|
||||
Path string `form:"path" json:"path"`
|
||||
PHP int `form:"php" json:"php"`
|
||||
PHP int `form:"php" json:"php" validate:"number,gte=0"`
|
||||
DB bool `form:"db" json:"db"`
|
||||
DBType string `form:"db_type" json:"db_type"`
|
||||
DBName string `form:"db_name" json:"db_name"`
|
||||
@@ -19,38 +21,36 @@ type WebsiteCreate struct {
|
||||
}
|
||||
|
||||
type WebsiteDelete struct {
|
||||
ID uint `form:"id" json:"id"`
|
||||
ID uint `form:"id" json:"id" validate:"required"`
|
||||
Path bool `form:"path" json:"path"`
|
||||
DB bool `form:"db" json:"db"`
|
||||
}
|
||||
|
||||
type WebsiteUpdate struct {
|
||||
ID uint `form:"id" json:"id"`
|
||||
Domains []string `form:"domains" json:"domains"`
|
||||
Ports []uint `form:"ports" json:"ports"`
|
||||
SSLPorts []uint `form:"ssl_ports" json:"ssl_ports"`
|
||||
QUICPorts []uint `form:"quic_ports" json:"quic_ports"`
|
||||
OCSP bool `form:"ocsp" json:"ocsp"`
|
||||
HSTS bool `form:"hsts" json:"hsts"`
|
||||
SSL bool `form:"ssl" json:"ssl"`
|
||||
HTTPRedirect bool `form:"http_redirect" json:"http_redirect"`
|
||||
OpenBasedir bool `form:"open_basedir" json:"open_basedir"`
|
||||
Index string `form:"index" json:"index"`
|
||||
Path string `form:"path" json:"path"`
|
||||
Root string `form:"root" json:"root"`
|
||||
Raw string `form:"raw" json:"raw"`
|
||||
Rewrite string `form:"rewrite" json:"rewrite"`
|
||||
PHP int `form:"php" json:"php"`
|
||||
SSLCertificate string `form:"ssl_certificate" json:"ssl_certificate"`
|
||||
SSLCertificateKey string `form:"ssl_certificate_key" json:"ssl_certificate_key"`
|
||||
ID uint `form:"id" json:"id" validate:"required"`
|
||||
Listens []types.WebsiteListen `form:"listens" json:"listens" validate:"min=1"`
|
||||
Domains []string `form:"domains" json:"domains" validate:"min=1,dive,required"`
|
||||
HTTPS bool `form:"https" json:"https"`
|
||||
OCSP bool `form:"ocsp" json:"ocsp"`
|
||||
HSTS bool `form:"hsts" json:"hsts"`
|
||||
HTTPRedirect bool `form:"http_redirect" json:"http_redirect"`
|
||||
OpenBasedir bool `form:"open_basedir" json:"open_basedir"`
|
||||
Index []string `form:"index" json:"index" validate:"min=1,dive,required"`
|
||||
Path string `form:"path" json:"path" validate:"required"` // 网站目录
|
||||
Root string `form:"root" json:"root" validate:"required"` // 运行目录
|
||||
Raw string `form:"raw" json:"raw"`
|
||||
Rewrite string `form:"rewrite" json:"rewrite"`
|
||||
PHP int `form:"php" json:"php"`
|
||||
SSLCertificate string `form:"ssl_certificate" json:"ssl_certificate" validate:"required_if=HTTPS true"`
|
||||
SSLCertificateKey string `form:"ssl_certificate_key" json:"ssl_certificate_key" validate:"required_if=HTTPS true"`
|
||||
}
|
||||
|
||||
type WebsiteUpdateRemark struct {
|
||||
ID uint `form:"id" json:"id"`
|
||||
ID uint `form:"id" json:"id" validate:"required"`
|
||||
Remark string `form:"remark" json:"remark"`
|
||||
}
|
||||
|
||||
type WebsiteUpdateStatus struct {
|
||||
ID uint `json:"id" form:"id"`
|
||||
ID uint `json:"id" form:"id" validate:"required"`
|
||||
Status bool `json:"status" form:"status"`
|
||||
}
|
||||
|
||||
+16
-13
@@ -1,29 +1,27 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/internal/data"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
// PanelTask 面板每日任务
|
||||
type PanelTask struct {
|
||||
appRepo biz.AppRepo
|
||||
appRepo biz.AppRepo
|
||||
backupRepo biz.BackupRepo
|
||||
}
|
||||
|
||||
func NewPanelTask() *PanelTask {
|
||||
return &PanelTask{
|
||||
appRepo: data.NewAppRepo(),
|
||||
appRepo: data.NewAppRepo(),
|
||||
backupRepo: data.NewBackupRepo(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,21 +33,26 @@ func (receiver *PanelTask) Run() {
|
||||
types.Status = types.StatusFailed
|
||||
app.Logger.Error("优化面板数据库失败", zap.Error(err))
|
||||
}
|
||||
if err := app.Orm.Exec("PRAGMA wal_checkpoint(TRUNCATE);").Error; err != nil {
|
||||
types.Status = types.StatusFailed
|
||||
app.Logger.Error("优化面板数据库失败", zap.Error(err))
|
||||
}
|
||||
|
||||
// 备份面板
|
||||
if err := io.Compress([]string{"/www/panel"}, filepath.Join(app.Root, "backup", "panel", "panel-"+time.Now().Format(time.DateOnly)+".zip"), io.Zip); err != nil {
|
||||
types.Status = types.StatusFailed
|
||||
if err := receiver.backupRepo.Create(biz.BackupTypePanel, ""); err != nil {
|
||||
app.Logger.Error("备份面板失败", zap.Error(err))
|
||||
}
|
||||
|
||||
// 清理 7 天前的备份
|
||||
if _, err := shell.Execf(`find %s -mtime +7 -name "*.zip" -exec rm -rf {} \;`, filepath.Join(app.Root, "backup", "panel")); err != nil {
|
||||
types.Status = types.StatusFailed
|
||||
app.Logger.Error("清理面板备份失败", zap.Error(err))
|
||||
// 清理备份
|
||||
path, err := receiver.backupRepo.GetPath("panel")
|
||||
if err == nil {
|
||||
if err = receiver.backupRepo.ClearExpired(path, "panel_", 10); err != nil {
|
||||
app.Logger.Error("清理面板备份失败", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// 更新商店缓存
|
||||
if err := receiver.appRepo.UpdateCache(); err != nil {
|
||||
if err = receiver.appRepo.UpdateCache(); err != nil {
|
||||
app.Logger.Error("更新商店缓存失败", zap.Error(err))
|
||||
}
|
||||
|
||||
|
||||
+161
-4
@@ -98,18 +98,62 @@ func Cli() []*cli.Command {
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "create",
|
||||
Usage: "创建新站点",
|
||||
Usage: "创建新网站",
|
||||
Action: cliService.WebsiteCreate,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Usage: "网站名称",
|
||||
Aliases: []string{"n"},
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "domains",
|
||||
Usage: "与网站关联的域名列表",
|
||||
Aliases: []string{"d"},
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "listens",
|
||||
Usage: "与网站关联的监听地址列表",
|
||||
Aliases: []string{"l"},
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "path",
|
||||
Usage: "网站托管的路径(不填则默认路径)",
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "php",
|
||||
Usage: "网站使用的 PHP 版本(不填不使用)",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "remove",
|
||||
Usage: "移除站点",
|
||||
Usage: "移除网站",
|
||||
Action: cliService.WebsiteRemove,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Usage: "网站名称",
|
||||
Aliases: []string{"n"},
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "delete",
|
||||
Usage: "删除站点(包括站点目录、同名数据库)",
|
||||
Usage: "删除网站(包括网站目录、同名数据库)",
|
||||
Action: cliService.WebsiteDelete,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Usage: "网站名称",
|
||||
Aliases: []string{"n"},
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "write",
|
||||
@@ -121,22 +165,91 @@ func Cli() []*cli.Command {
|
||||
},
|
||||
{
|
||||
Name: "backup",
|
||||
Usage: "备份数据",
|
||||
Usage: "数据备份",
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "website",
|
||||
Usage: "备份网站",
|
||||
Action: cliService.BackupWebsite,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Aliases: []string{"n"},
|
||||
Usage: "网站名称",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "path",
|
||||
Aliases: []string{"p"},
|
||||
Usage: "保存目录(不填则默认路径)",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "database",
|
||||
Usage: "备份数据库",
|
||||
Action: cliService.BackupDatabase,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "type",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "数据库类型",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Aliases: []string{"n"},
|
||||
Usage: "数据库名称",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "path",
|
||||
Aliases: []string{"p"},
|
||||
Usage: "保存目录(不填则默认路径)",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "panel",
|
||||
Usage: "备份面板",
|
||||
Action: cliService.BackupPanel,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "path",
|
||||
Aliases: []string{"p"},
|
||||
Usage: "保存目录(不填则默认路径)",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "clear",
|
||||
Usage: "清理备份",
|
||||
Action: cliService.BackupClear,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "type",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "备份类型",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "file",
|
||||
Aliases: []string{"f"},
|
||||
Usage: "备份文件",
|
||||
Required: true,
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "save",
|
||||
Aliases: []string{"s"},
|
||||
Usage: "保存份数",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "path",
|
||||
Aliases: []string{"p"},
|
||||
Usage: "备份目录(不填则默认路径)",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -148,6 +261,50 @@ func Cli() []*cli.Command {
|
||||
Name: "website",
|
||||
Usage: "网站",
|
||||
Action: cliService.CutoffWebsite,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Aliases: []string{"n"},
|
||||
Usage: "网站名称",
|
||||
Required: true,
|
||||
},
|
||||
|
||||
&cli.StringFlag{
|
||||
Name: "path",
|
||||
Aliases: []string{"p"},
|
||||
Usage: "保存目录(不填则默认路径)",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "clear",
|
||||
Usage: "清理切割的日志",
|
||||
Action: cliService.CutoffClear,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "type",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "切割类型",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "file",
|
||||
Aliases: []string{"f"},
|
||||
Usage: "切割文件",
|
||||
Required: true,
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "save",
|
||||
Aliases: []string{"s"},
|
||||
Usage: "保存份数",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "path",
|
||||
Aliases: []string{"p"},
|
||||
Usage: "切割目录(不填则默认路径)",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -62,16 +62,14 @@ func Http(r chi.Router) {
|
||||
r.Post("/{id}/status", website.UpdateStatus)
|
||||
})
|
||||
|
||||
// TODO
|
||||
r.Route("/backup", func(r chi.Router) {
|
||||
r.Use(middleware.MustLogin)
|
||||
backup := service.NewBackupService()
|
||||
r.Get("/backup", backup.List)
|
||||
r.Post("/create", backup.Create)
|
||||
r.Post("/update", backup.Update)
|
||||
r.Get("/{id}", backup.Get)
|
||||
r.Delete("/{id}", backup.Delete)
|
||||
r.Delete("/{id}/restore", backup.Restore)
|
||||
r.Get("/{type}", backup.List)
|
||||
r.Post("/{type}", backup.Create)
|
||||
r.Post("/{type}/upload", backup.Upload)
|
||||
r.Delete("/{type}/delete", backup.Delete)
|
||||
r.Post("/{type}/restore", backup.Restore)
|
||||
})
|
||||
|
||||
r.Route("/cert", func(r chi.Router) {
|
||||
@@ -219,7 +217,7 @@ func Http(r chi.Router) {
|
||||
r.Get("/content", file.Content)
|
||||
r.Post("/save", file.Save)
|
||||
r.Post("/delete", file.Delete)
|
||||
r.Post("/upload", file.Upload) // TODO fix
|
||||
r.Post("/upload", file.Upload)
|
||||
r.Post("/move", file.Move)
|
||||
r.Post("/copy", file.Copy)
|
||||
r.Get("/download", file.Download)
|
||||
|
||||
@@ -1,34 +1,126 @@
|
||||
package service
|
||||
|
||||
import "net/http"
|
||||
import (
|
||||
stdio "io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/go-rat/chix"
|
||||
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/internal/data"
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
)
|
||||
|
||||
type BackupService struct {
|
||||
backupRepo biz.BackupRepo
|
||||
}
|
||||
|
||||
func NewBackupService() *BackupService {
|
||||
return &BackupService{}
|
||||
return &BackupService{
|
||||
backupRepo: data.NewBackupRepo(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *BackupService) List(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.BackupList](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
list, _ := s.backupRepo.List(biz.BackupType(req.Type))
|
||||
paged, total := Paginate(r, list)
|
||||
|
||||
Success(w, chix.M{
|
||||
"total": total,
|
||||
"items": paged,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *BackupService) Create(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.BackupCreate](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.backupRepo.Create(biz.BackupType(req.Type), req.Target, req.Path); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *BackupService) Update(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *BackupService) Upload(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseMultipartForm(2 << 30); err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
_, handler, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "上传文件失败:%v", err)
|
||||
return
|
||||
}
|
||||
path, err := s.backupRepo.GetPath(biz.BackupType(r.FormValue("type")))
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *BackupService) Get(w http.ResponseWriter, r *http.Request) {
|
||||
if !io.Exists(filepath.Dir(path)) {
|
||||
if err = io.Mkdir(filepath.Dir(path), 0755); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "创建文件夹失败:%v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
src, _ := handler.Open()
|
||||
out, err := os.OpenFile(filepath.Join(path, handler.Filename), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "打开文件失败:%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = stdio.Copy(out, src); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "写入文件失败:%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
_ = src.Close()
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *BackupService) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.BackupFile](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.backupRepo.Delete(biz.BackupType(req.Type), req.File); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *BackupService) Restore(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.BackupRestore](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.backupRepo.Restore(biz.BackupType(req.Type), req.File, req.Target); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
+201
-26
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/go-rat/utils/hash"
|
||||
"github.com/goccy/go-yaml"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/internal/data"
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"github.com/TheTNB/panel/pkg/api"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
@@ -25,33 +27,54 @@ import (
|
||||
)
|
||||
|
||||
type CliService struct {
|
||||
api *api.API
|
||||
app biz.AppRepo
|
||||
user biz.UserRepo
|
||||
setting biz.SettingRepo
|
||||
hash hash.Hasher
|
||||
hr string
|
||||
api *api.API
|
||||
appRepo biz.AppRepo
|
||||
userRepo biz.UserRepo
|
||||
settingRepo biz.SettingRepo
|
||||
backupRepo biz.BackupRepo
|
||||
websiteRepo biz.WebsiteRepo
|
||||
hash hash.Hasher
|
||||
}
|
||||
|
||||
func NewCliService() *CliService {
|
||||
return &CliService{
|
||||
api: api.NewAPI(app.Version),
|
||||
app: data.NewAppRepo(),
|
||||
user: data.NewUserRepo(),
|
||||
setting: data.NewSettingRepo(),
|
||||
hash: hash.NewArgon2id(),
|
||||
hr: `+----------------------------------------------------`,
|
||||
api: api.NewAPI(app.Version),
|
||||
appRepo: data.NewAppRepo(),
|
||||
userRepo: data.NewUserRepo(),
|
||||
settingRepo: data.NewSettingRepo(),
|
||||
backupRepo: data.NewBackupRepo(),
|
||||
websiteRepo: data.NewWebsiteRepo(),
|
||||
hash: hash.NewArgon2id(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CliService) Restart(ctx context.Context, cmd *cli.Command) error {
|
||||
return systemctl.Restart("panel")
|
||||
if err := systemctl.Restart("panel"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln("面板服务已重启")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CliService) Stop(ctx context.Context, cmd *cli.Command) error {
|
||||
return systemctl.Stop("panel")
|
||||
if err := systemctl.Stop("panel"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln("面板服务已停止")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CliService) Start(ctx context.Context, cmd *cli.Command) error {
|
||||
return systemctl.Start("panel")
|
||||
if err := systemctl.Start("panel"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln("面板服务已启动")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CliService) Update(ctx context.Context, cmd *cli.Command) error {
|
||||
@@ -66,7 +89,7 @@ func (s *CliService) Update(ctx context.Context, cmd *cli.Command) error {
|
||||
}
|
||||
ver, url, checksum := panel.Version, download.URL, download.Checksum
|
||||
|
||||
return s.setting.UpdatePanel(ver, url, checksum)
|
||||
return s.settingRepo.UpdatePanel(ver, url, checksum)
|
||||
}
|
||||
|
||||
func (s *CliService) Info(ctx context.Context, cmd *cli.Command) error {
|
||||
@@ -153,6 +176,7 @@ func (s *CliService) UserName(ctx context.Context, cmd *cli.Command) error {
|
||||
return fmt.Errorf("用户名修改失败:%v", err)
|
||||
}
|
||||
|
||||
color.Greenln(fmt.Sprintf("用户 %s 修改为 %s 成功", oldUsername, newUsername))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -184,6 +208,7 @@ func (s *CliService) UserPassword(ctx context.Context, cmd *cli.Command) error {
|
||||
return fmt.Errorf("密码修改失败:%v", err)
|
||||
}
|
||||
|
||||
color.Greenln(fmt.Sprintf("用户 %s 密码修改成功", username))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -209,6 +234,7 @@ func (s *CliService) HTTPSOn(ctx context.Context, cmd *cli.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln("已开启HTTPS")
|
||||
return s.Restart(ctx, cmd)
|
||||
}
|
||||
|
||||
@@ -234,6 +260,7 @@ func (s *CliService) HTTPSOff(ctx context.Context, cmd *cli.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln("已关闭HTTPS")
|
||||
return s.Restart(ctx, cmd)
|
||||
}
|
||||
|
||||
@@ -259,6 +286,8 @@ func (s *CliService) EntranceOn(ctx context.Context, cmd *cli.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln("已开启访问入口")
|
||||
color.Greenln(fmt.Sprintf("访问入口:%s", config.HTTP.Entrance))
|
||||
return s.Restart(ctx, cmd)
|
||||
}
|
||||
|
||||
@@ -284,6 +313,7 @@ func (s *CliService) EntranceOff(ctx context.Context, cmd *cli.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln("已关闭访问入口")
|
||||
return s.Restart(ctx, cmd)
|
||||
}
|
||||
|
||||
@@ -314,46 +344,184 @@ func (s *CliService) Port(ctx context.Context, cmd *cli.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln(fmt.Sprintf("已修改端口为 %d", port))
|
||||
return s.Restart(ctx, cmd)
|
||||
}
|
||||
|
||||
func (s *CliService) WebsiteCreate(ctx context.Context, cmd *cli.Command) error {
|
||||
println("Hello, World!")
|
||||
req := &request.WebsiteCreate{
|
||||
Name: cmd.String("name"),
|
||||
Domains: cmd.StringSlice("domains"),
|
||||
Listens: cmd.StringSlice("listens"),
|
||||
Path: cmd.String("path"),
|
||||
PHP: int(cmd.Int("php")),
|
||||
DB: false,
|
||||
}
|
||||
|
||||
website, err := s.websiteRepo.Create(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln(fmt.Sprintf("网站 %s 创建成功", website.Name))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CliService) WebsiteRemove(ctx context.Context, cmd *cli.Command) error {
|
||||
println("Hello, World!")
|
||||
website, err := s.websiteRepo.GetByName(cmd.String("name"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req := &request.WebsiteDelete{
|
||||
ID: website.ID,
|
||||
}
|
||||
|
||||
if err = s.websiteRepo.Delete(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln(fmt.Sprintf("网站 %s 移除成功", website.Name))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CliService) WebsiteDelete(ctx context.Context, cmd *cli.Command) error {
|
||||
println("Hello, World!")
|
||||
website, err := s.websiteRepo.GetByName(cmd.String("name"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req := &request.WebsiteDelete{
|
||||
ID: website.ID,
|
||||
Path: true,
|
||||
DB: true,
|
||||
}
|
||||
|
||||
if err = s.websiteRepo.Delete(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln(fmt.Sprintf("网站 %s 删除成功", website.Name))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CliService) WebsiteWrite(ctx context.Context, cmd *cli.Command) error {
|
||||
println("Hello, World!")
|
||||
println("not support")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CliService) BackupWebsite(ctx context.Context, cmd *cli.Command) error {
|
||||
println("Hello, World!")
|
||||
color.Greenln(s.hr)
|
||||
color.Greenln(fmt.Sprintf("★ 开始备份 [%s]", time.Now().Format(time.DateTime)))
|
||||
color.Greenln(s.hr)
|
||||
color.Greenln("|-备份类型:网站")
|
||||
color.Greenln(fmt.Sprintf("|-备份目标:%s", cmd.String("name")))
|
||||
if err := s.backupRepo.Create(biz.BackupTypeWebsite, cmd.String("name"), cmd.String("path")); err != nil {
|
||||
return fmt.Errorf("|-备份失败:%v", err)
|
||||
}
|
||||
color.Greenln(s.hr)
|
||||
color.Greenln(fmt.Sprintf("☆ 备份成功 [%s]", time.Now().Format(time.DateTime)))
|
||||
color.Greenln(s.hr)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CliService) BackupDatabase(ctx context.Context, cmd *cli.Command) error {
|
||||
println("Hello, World!")
|
||||
color.Greenln(s.hr)
|
||||
color.Greenln(fmt.Sprintf("★ 开始备份 [%s]", time.Now().Format(time.DateTime)))
|
||||
color.Greenln(s.hr)
|
||||
color.Greenln("|-备份类型:数据库")
|
||||
color.Greenln(fmt.Sprintf("|-数据库:%s", cmd.String("type")))
|
||||
color.Greenln(fmt.Sprintf("|-备份目标:%s", cmd.String("name")))
|
||||
if err := s.backupRepo.Create(biz.BackupType(cmd.String("type")), cmd.String("name"), cmd.String("path")); err != nil {
|
||||
return fmt.Errorf("|-备份失败:%v", err)
|
||||
}
|
||||
color.Greenln(s.hr)
|
||||
color.Greenln(fmt.Sprintf("☆ 备份成功 [%s]", time.Now().Format(time.DateTime)))
|
||||
color.Greenln(s.hr)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CliService) BackupPanel(ctx context.Context, cmd *cli.Command) error {
|
||||
println("Hello, World!")
|
||||
color.Greenln(s.hr)
|
||||
color.Greenln(fmt.Sprintf("★ 开始备份 [%s]", time.Now().Format(time.DateTime)))
|
||||
color.Greenln(s.hr)
|
||||
color.Greenln("|-备份类型:面板")
|
||||
if err := s.backupRepo.Create(biz.BackupTypePanel, "", cmd.String("path")); err != nil {
|
||||
return fmt.Errorf("|-备份失败:%v", err)
|
||||
}
|
||||
color.Greenln(s.hr)
|
||||
color.Greenln(fmt.Sprintf("☆ 备份成功 [%s]", time.Now().Format(time.DateTime)))
|
||||
color.Greenln(s.hr)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CliService) BackupClear(ctx context.Context, cmd *cli.Command) error {
|
||||
path, err := s.backupRepo.GetPath(biz.BackupType(cmd.String("type")))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cmd.String("path") != "" {
|
||||
path = cmd.String("path")
|
||||
}
|
||||
|
||||
color.Greenln(s.hr)
|
||||
color.Greenln(fmt.Sprintf("★ 开始清理 [%s]", time.Now().Format(time.DateTime)))
|
||||
color.Greenln(s.hr)
|
||||
color.Greenln(fmt.Sprintf("|-清理类型:%s", cmd.String("type")))
|
||||
color.Greenln(fmt.Sprintf("|-清理目标:%s", cmd.String("file")))
|
||||
color.Greenln(fmt.Sprintf("|-保留份数:%d", cmd.Int("save")))
|
||||
if err = s.backupRepo.ClearExpired(path, cmd.String("file"), int(cmd.Int("save"))); err != nil {
|
||||
return fmt.Errorf("|-清理失败:%v", err)
|
||||
}
|
||||
color.Greenln(s.hr)
|
||||
color.Greenln(fmt.Sprintf("☆ 清理成功 [%s]", time.Now().Format(time.DateTime)))
|
||||
color.Greenln(s.hr)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CliService) CutoffWebsite(ctx context.Context, cmd *cli.Command) error {
|
||||
println("Hello, World!")
|
||||
website, err := s.websiteRepo.GetByName(cmd.String("name"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path := filepath.Join(app.Root, "wwwlogs")
|
||||
if cmd.String("path") != "" {
|
||||
path = cmd.String("path")
|
||||
}
|
||||
|
||||
color.Greenln(s.hr)
|
||||
color.Greenln(fmt.Sprintf("★ 开始切割日志 [%s]", time.Now().Format(time.DateTime)))
|
||||
color.Greenln(s.hr)
|
||||
color.Greenln("|-切割类型:网站")
|
||||
color.Greenln(fmt.Sprintf("|-切割目标:%s", website.Name))
|
||||
if err = s.backupRepo.CutoffLog(path, filepath.Join(app.Root, "wwwlogs", website.Name+".log")); err != nil {
|
||||
return err
|
||||
}
|
||||
color.Greenln(s.hr)
|
||||
color.Greenln(fmt.Sprintf("☆ 切割成功 [%s]", time.Now().Format(time.DateTime)))
|
||||
color.Greenln(s.hr)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CliService) CutoffClear(ctx context.Context, cmd *cli.Command) error {
|
||||
if cmd.String("type") != "website" {
|
||||
return errors.New("当前仅支持网站日志切割")
|
||||
}
|
||||
path := filepath.Join(app.Root, "wwwlogs")
|
||||
if cmd.String("path") != "" {
|
||||
path = cmd.String("path")
|
||||
}
|
||||
|
||||
color.Greenln(s.hr)
|
||||
color.Greenln(fmt.Sprintf("★ 开始清理切割日志 [%s]", time.Now().Format(time.DateTime)))
|
||||
color.Greenln(s.hr)
|
||||
color.Greenln(fmt.Sprintf("|-清理类型:%s", cmd.String("type")))
|
||||
color.Greenln(fmt.Sprintf("|-清理目标:%s", cmd.String("file")))
|
||||
color.Greenln(fmt.Sprintf("|-保留份数:%d", cmd.Int("save")))
|
||||
if err := s.backupRepo.ClearExpired(path, cmd.String("file"), int(cmd.Int("save"))); err != nil {
|
||||
return err
|
||||
}
|
||||
color.Greenln(s.hr)
|
||||
color.Greenln(fmt.Sprintf("☆ 清理成功 [%s]", time.Now().Format(time.DateTime)))
|
||||
color.Greenln(s.hr)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -364,7 +532,7 @@ func (s *CliService) AppInstall(ctx context.Context, cmd *cli.Command) error {
|
||||
return fmt.Errorf("参数不能为空")
|
||||
}
|
||||
|
||||
if err := s.app.Install(channel, slug); err != nil {
|
||||
if err := s.appRepo.Install(channel, slug); err != nil {
|
||||
return fmt.Errorf("应用安装失败:%v", err)
|
||||
}
|
||||
|
||||
@@ -379,7 +547,7 @@ func (s *CliService) AppUnInstall(ctx context.Context, cmd *cli.Command) error {
|
||||
return fmt.Errorf("参数不能为空")
|
||||
}
|
||||
|
||||
if err := s.app.UnInstall(slug); err != nil {
|
||||
if err := s.appRepo.UnInstall(slug); err != nil {
|
||||
return fmt.Errorf("应用卸载失败:%v", err)
|
||||
}
|
||||
|
||||
@@ -496,7 +664,14 @@ func (s *CliService) Init(ctx context.Context, cmd *cli.Command) error {
|
||||
return fmt.Errorf("已经初始化过了")
|
||||
}
|
||||
|
||||
settings := []biz.Setting{{Key: biz.SettingKeyName, Value: "耗子面板"}, {Key: biz.SettingKeyMonitor, Value: "1"}, {Key: biz.SettingKeyMonitorDays, Value: "30"}, {Key: biz.SettingKeyBackupPath, Value: filepath.Join(app.Root, "backup")}, {Key: biz.SettingKeyWebsitePath, Value: filepath.Join(app.Root, "wwwroot")}, {Key: biz.SettingKeyVersion, Value: app.Version}}
|
||||
settings := []biz.Setting{
|
||||
{Key: biz.SettingKeyName, Value: "耗子面板"},
|
||||
{Key: biz.SettingKeyMonitor, Value: "1"},
|
||||
{Key: biz.SettingKeyMonitorDays, Value: "30"},
|
||||
{Key: biz.SettingKeyBackupPath, Value: filepath.Join(app.Root, "backup")},
|
||||
{Key: biz.SettingKeyWebsitePath, Value: filepath.Join(app.Root, "wwwroot")},
|
||||
{Key: biz.SettingKeyVersion, Value: app.Version},
|
||||
}
|
||||
if err := app.Orm.Create(&settings).Error; err != nil {
|
||||
return fmt.Errorf("初始化失败:%v", err)
|
||||
}
|
||||
@@ -534,5 +709,5 @@ func (s *CliService) Init(ctx context.Context, cmd *cli.Command) error {
|
||||
}
|
||||
|
||||
// 初始化应用中心缓存
|
||||
return s.app.UpdateCache()
|
||||
return s.appRepo.UpdateCache()
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
stdio "io"
|
||||
"net/http"
|
||||
stdos "os"
|
||||
"path/filepath"
|
||||
@@ -119,18 +120,43 @@ func (s *FileService) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *FileService) Upload(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.FileUpload](r)
|
||||
if err := r.ParseMultipartForm(2 << 30); err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
path := r.FormValue("path")
|
||||
_, handler, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
Error(w, http.StatusInternalServerError, "上传文件失败:%v", err)
|
||||
return
|
||||
}
|
||||
if io.Exists(path) {
|
||||
Error(w, http.StatusForbidden, "目标路径%s已存在", path)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(req.Path, string(req.File), 0755); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
if !io.Exists(filepath.Dir(path)) {
|
||||
if err = io.Mkdir(filepath.Dir(path), 0755); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "创建文件夹失败:%v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
src, _ := handler.Open()
|
||||
out, err := stdos.OpenFile(path, stdos.O_CREATE|stdos.O_RDWR|stdos.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "打开文件失败:%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
s.setPermission(req.Path, 0755, "www", "www")
|
||||
if _, err = stdio.Copy(out, src); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "写入文件失败:%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
_ = src.Close()
|
||||
s.setPermission(path, 0755, "www", "www")
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
stdio "io"
|
||||
"net/http"
|
||||
stdos "os"
|
||||
"path/filepath"
|
||||
@@ -119,18 +120,43 @@ func (s *FileService) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *FileService) Upload(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.FileUpload](r)
|
||||
if err := r.ParseMultipartForm(2 << 30); err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
path := r.FormValue("path")
|
||||
_, handler, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
Error(w, http.StatusInternalServerError, "上传文件失败:%v", err)
|
||||
return
|
||||
}
|
||||
if io.Exists(path) {
|
||||
Error(w, http.StatusForbidden, "目标路径%s已存在", path)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(req.Path, string(req.File), 0755); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
if !io.Exists(filepath.Dir(path)) {
|
||||
if err = io.Mkdir(filepath.Dir(path), 0755); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "创建文件夹失败:%v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
src, _ := handler.Open()
|
||||
out, err := stdos.OpenFile(path, stdos.O_CREATE|stdos.O_RDWR|stdos.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "打开文件失败:%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
s.setPermission(req.Path, 0755, "www", "www")
|
||||
if _, err = stdio.Copy(out, src); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "写入文件失败:%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
_ = src.Close()
|
||||
s.setPermission(path, 0755, "www", "www")
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
|
||||
@@ -300,8 +300,8 @@ func (s *InfoService) Update(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
types.Status = types.StatusNormal
|
||||
tools.RestartPanel()
|
||||
Success(w, nil)
|
||||
tools.RestartPanel()
|
||||
}
|
||||
|
||||
func (s *InfoService) Restart(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
+2
-2
@@ -45,11 +45,11 @@ func (s httpSolver) Present(_ context.Context, challenge acme.Challenge) error {
|
||||
}
|
||||
`, challenge.Token, challenge.KeyAuthorization)
|
||||
if err = os.WriteFile(s.conf, []byte(conf), 0644); err != nil {
|
||||
return fmt.Errorf("无法写入OpenResty配置文件: %w", err)
|
||||
return fmt.Errorf("无法写入Nginx配置文件: %w", err)
|
||||
}
|
||||
if err = systemctl.Reload("nginx"); err != nil {
|
||||
_, err = shell.Execf("nginx -t")
|
||||
return fmt.Errorf("无法重载OpenResty: %w", err)
|
||||
return fmt.Errorf("无法重载Nginx: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -72,6 +72,31 @@ func (m *MySQL) DatabaseDrop(name string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *MySQL) DatabaseExists(name string) (bool, error) {
|
||||
rows, err := m.Query("SHOW DATABASES")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var database string
|
||||
if err := rows.Scan(&database); err != nil {
|
||||
continue
|
||||
}
|
||||
if database == name {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (m *MySQL) DatabaseSize(name string) (int64, error) {
|
||||
var size int64
|
||||
err := m.QueryRow(fmt.Sprintf("SELECT SUM(data_length + index_length) FROM information_schema.tables WHERE table_schema = '%s'", name)).Scan(&size)
|
||||
return size, err
|
||||
}
|
||||
|
||||
func (m *MySQL) UserCreate(user, password string) error {
|
||||
_, err := m.Exec(fmt.Sprintf("CREATE USER IF NOT EXISTS '%s'@'localhost' IDENTIFIED BY '%s'", user, password))
|
||||
m.flushPrivileges()
|
||||
|
||||
@@ -77,6 +77,23 @@ func (m *Postgres) DatabaseDrop(name string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *Postgres) DatabaseExist(name string) (bool, error) {
|
||||
var count int
|
||||
if err := m.QueryRow("SELECT COUNT(*) FROM pg_database WHERE datname = $1", name).Scan(&count); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (m *Postgres) DatabaseSize(name string) (int64, error) {
|
||||
query := fmt.Sprintf("SELECT pg_database_size('%s')", name)
|
||||
var size int64
|
||||
if err := m.QueryRow(query).Scan(&size); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return size, nil
|
||||
}
|
||||
|
||||
func (m *Postgres) UserCreate(user, password string) error {
|
||||
_, err := m.Exec(fmt.Sprintf("CREATE USER %s WITH PASSWORD '%s'", user, password))
|
||||
if err != nil {
|
||||
|
||||
@@ -191,11 +191,6 @@ func FormatArchiveByPath(path string) (FormatArchive, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// TempFile 创建临时文件
|
||||
func TempFile(prefix string) (*os.File, error) {
|
||||
return os.CreateTemp("", prefix)
|
||||
}
|
||||
|
||||
// IsSymlink 判读是否为软链接
|
||||
func IsSymlink(mode os.FileMode) bool {
|
||||
return mode&os.ModeSymlink != 0
|
||||
|
||||
+39
-7
@@ -6,6 +6,10 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
)
|
||||
|
||||
// Remove 删除文件/目录
|
||||
@@ -47,17 +51,14 @@ func Empty(path string) bool {
|
||||
}
|
||||
|
||||
func Mv(src, dst string) error {
|
||||
err := os.Rename(src, dst)
|
||||
if err != nil {
|
||||
// 如果在不同的文件系统中移动文件,os.Rename 可能会失败
|
||||
err = Cp(src, dst)
|
||||
if err != nil {
|
||||
if err := os.Rename(src, dst); err != nil {
|
||||
// 在不同的文件系统中无法使用os.Rename
|
||||
if _, err = shell.Execf(`mv -f '%s' '%s'`, src, dst); err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.RemoveAll(src)
|
||||
}
|
||||
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cp 复制文件或目录
|
||||
@@ -145,6 +146,11 @@ func TempDir(prefix string) (string, error) {
|
||||
return os.MkdirTemp("", prefix)
|
||||
}
|
||||
|
||||
// TempFile 创建临时文件
|
||||
func TempFile(dir, prefix string) (*os.File, error) {
|
||||
return os.CreateTemp(dir, prefix)
|
||||
}
|
||||
|
||||
// ReadDir 读取目录
|
||||
func ReadDir(path string) ([]os.DirEntry, error) {
|
||||
return os.ReadDir(path)
|
||||
@@ -158,3 +164,29 @@ func IsDir(path string) bool {
|
||||
}
|
||||
return info.IsDir()
|
||||
}
|
||||
|
||||
// SizeX 获取路径大小(du命令)
|
||||
func SizeX(path string) (int64, error) {
|
||||
out, err := exec.Command("du", "-sb", path).Output()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
parts := strings.Fields(string(out))
|
||||
if len(parts) == 0 {
|
||||
return 0, fmt.Errorf("无法解析 du 输出")
|
||||
}
|
||||
|
||||
return strconv.ParseInt(parts[0], 10, 64)
|
||||
}
|
||||
|
||||
// CountX 统计目录下文件数
|
||||
func CountX(path string) (int64, error) {
|
||||
out, err := exec.Command("find", path, "-printf", ".").Output()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
count := len(string(out))
|
||||
return int64(count), nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package nginx
|
||||
|
||||
var order = []string{"listen", "server_name", "index", "root",
|
||||
"ssl_certificate", "ssl_certificate_key", "ssl_session_timeout", "ssl_session_cache", "ssl_protocols", "ssl_ciphers", "ssl_prefer_server_ciphers", "ssl_early_data", "ssl_stapling", "ssl_stapling_verify", "ssl_trusted_certificate",
|
||||
"resolver", "error_page", "include", "if", "location", "add_header", "access_log", "error_log"}
|
||||
|
||||
const defaultConf = `server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
index index.php index.html index.htm;
|
||||
root /www/wwwroot/default;
|
||||
# 错误页配置
|
||||
error_page 404 /404.html;
|
||||
include enable-php-0.conf;
|
||||
# 不记录静态文件日志
|
||||
location ~ .*\.(bmp|jpg|jpeg|png|gif|svg|ico|tiff|webp|avif|heif|heic|jxl)$ {
|
||||
expires 30d;
|
||||
access_log /dev/null;
|
||||
error_log /dev/null;
|
||||
}
|
||||
location ~ .*\.(js|css|ttf|otf|woff|woff2|eot)$ {
|
||||
expires 6h;
|
||||
access_log /dev/null;
|
||||
error_log /dev/null;
|
||||
}
|
||||
# 禁止部分敏感目录
|
||||
location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn|\.env) {
|
||||
return 404;
|
||||
}
|
||||
access_log /www/wwwlogs/default.log;
|
||||
error_log /www/wwwlogs/default.log;
|
||||
}
|
||||
`
|
||||
@@ -0,0 +1,208 @@
|
||||
package nginx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (p *Parser) GetListen() ([][]string, error) {
|
||||
directives, err := p.Find("server.listen")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result [][]string
|
||||
for _, dir := range directives {
|
||||
result = append(result, dir.GetParameters())
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (p *Parser) GetServerName() ([]string, error) {
|
||||
directive, err := p.FindOne("server.server_name")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return directive.GetParameters(), nil
|
||||
}
|
||||
|
||||
func (p *Parser) GetIndex() ([]string, error) {
|
||||
directive, err := p.FindOne("server.index")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return directive.GetParameters(), nil
|
||||
}
|
||||
|
||||
func (p *Parser) GetIndexWithComment() ([]string, []string, error) {
|
||||
directive, err := p.FindOne("server.index")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return directive.GetParameters(), directive.GetComment(), nil
|
||||
}
|
||||
|
||||
func (p *Parser) GetRoot() (string, error) {
|
||||
directive, err := p.FindOne("server.root")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(directive.GetParameters()) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return directive.GetParameters()[0], nil
|
||||
}
|
||||
|
||||
func (p *Parser) GetRootWithComment() (string, []string, error) {
|
||||
directive, err := p.FindOne("server.root")
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
if len(directive.GetParameters()) == 0 {
|
||||
return "", directive.GetComment(), nil
|
||||
}
|
||||
|
||||
return directive.GetParameters()[0], directive.GetComment(), nil
|
||||
}
|
||||
|
||||
func (p *Parser) GetIncludes() (includes []string, comments [][]string, err error) {
|
||||
directives, err := p.Find("server.include")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
for _, dir := range directives {
|
||||
if len(dir.GetParameters()) != 1 {
|
||||
return nil, nil, fmt.Errorf("invalid include directive, expected 1 parameter but got %d", len(dir.GetParameters()))
|
||||
}
|
||||
includes = append(includes, dir.GetParameters()[0])
|
||||
comments = append(comments, dir.GetComment())
|
||||
}
|
||||
|
||||
return includes, comments, nil
|
||||
}
|
||||
|
||||
func (p *Parser) GetPHP() int {
|
||||
directives, err := p.Find("server.include")
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
var result int
|
||||
for _, dir := range directives {
|
||||
if slices.ContainsFunc(dir.GetParameters(), func(s string) bool {
|
||||
return strings.HasPrefix(s, "enable-php-") && strings.HasSuffix(s, ".conf")
|
||||
}) {
|
||||
_, _ = fmt.Sscanf(dir.GetParameters()[0], "enable-php-%d.conf", &result)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (p *Parser) GetHTTPS() bool {
|
||||
directive, err := p.FindOne("server.ssl_certificate")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if len(directive.GetParameters()) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *Parser) GetHTTPSProtocols() []string {
|
||||
directive, err := p.FindOne("server.ssl_protocols")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return directive.GetParameters()
|
||||
}
|
||||
|
||||
func (p *Parser) GetHTTPSCiphers() string {
|
||||
directive, err := p.FindOne("server.ssl_ciphers")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if len(directive.GetParameters()) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return directive.GetParameters()[0]
|
||||
}
|
||||
|
||||
func (p *Parser) GetOCSP() bool {
|
||||
directive, err := p.FindOne("server.ssl_stapling")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if len(directive.GetParameters()) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return directive.GetParameters()[0] == "on"
|
||||
}
|
||||
|
||||
func (p *Parser) GetHSTS() bool {
|
||||
directives, err := p.Find("server.add_header")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, dir := range directives {
|
||||
if slices.Contains(dir.GetParameters(), "Strict-Transport-Security") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Parser) GetHTTPSRedirect() bool {
|
||||
directives, err := p.Find("server.if")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, dir := range directives {
|
||||
for _, dir2 := range dir.GetBlock().GetDirectives() {
|
||||
if dir2.GetName() == "return" && slices.Contains(dir2.GetParameters(), "https://$host$request_uri") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Parser) GetAccessLog() (string, error) {
|
||||
directive, err := p.FindOne("server.access_log")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(directive.GetParameters()) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return directive.GetParameters()[0], nil
|
||||
}
|
||||
|
||||
func (p *Parser) GetErrorLog() (string, error) {
|
||||
directive, err := p.FindOne("server.error_log")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(directive.GetParameters()) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return directive.GetParameters()[0], nil
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package nginx
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/tufanbarisyildirim/gonginx/config"
|
||||
"github.com/tufanbarisyildirim/gonginx/dumper"
|
||||
"github.com/tufanbarisyildirim/gonginx/parser"
|
||||
)
|
||||
|
||||
// Parser Nginx vhost 配置解析器
|
||||
type Parser struct {
|
||||
c *config.Config
|
||||
orderIndex map[string]int
|
||||
}
|
||||
|
||||
func NewParser(str ...string) (*Parser, error) {
|
||||
if len(str) == 0 {
|
||||
str = append(str, defaultConf)
|
||||
}
|
||||
p := parser.NewStringParser(str[0], parser.WithSkipIncludeParsingErr(), parser.WithSkipValidDirectivesErr())
|
||||
c, err := p.Parse()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
orderIndex := make(map[string]int)
|
||||
for i, name := range order {
|
||||
orderIndex[name] = i
|
||||
}
|
||||
|
||||
return &Parser{c: c, orderIndex: orderIndex}, nil
|
||||
}
|
||||
|
||||
func (p *Parser) Config() *config.Config {
|
||||
return p.c
|
||||
}
|
||||
|
||||
// Find 通过表达式查找配置
|
||||
// e.g. Find("server.listen")
|
||||
func (p *Parser) Find(key string) ([]config.IDirective, error) {
|
||||
parts := strings.Split(key, ".")
|
||||
var block *config.Block
|
||||
var ok bool
|
||||
block = p.c.Block
|
||||
for i := 0; i < len(parts)-1; i++ {
|
||||
key = parts[i]
|
||||
directives := block.FindDirectives(key)
|
||||
if len(directives) == 0 {
|
||||
return nil, errors.New("given key not found")
|
||||
}
|
||||
if len(directives) > 1 {
|
||||
return nil, errors.New("multiple directives found")
|
||||
}
|
||||
block, ok = directives[0].GetBlock().(*config.Block)
|
||||
if !ok {
|
||||
return nil, errors.New("block is not *config.Block")
|
||||
}
|
||||
}
|
||||
|
||||
var result []config.IDirective
|
||||
for _, dir := range block.GetDirectives() {
|
||||
if dir.GetName() == parts[len(parts)-1] {
|
||||
result = append(result, dir)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// FindOne 通过表达式查找一个配置
|
||||
// e.g. FindOne("server.server_name")
|
||||
func (p *Parser) FindOne(key string) (config.IDirective, error) {
|
||||
directives, err := p.Find(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(directives) == 0 {
|
||||
return nil, errors.New("given key not found")
|
||||
}
|
||||
|
||||
return directives[0], nil
|
||||
}
|
||||
|
||||
// Clear 通过表达式移除配置
|
||||
// e.g. Clear("server.server_name")
|
||||
func (p *Parser) Clear(key string) error {
|
||||
parts := strings.Split(key, ".")
|
||||
last := parts[len(parts)-1]
|
||||
parts = parts[:len(parts)-1]
|
||||
|
||||
var block *config.Block
|
||||
var ok bool
|
||||
block = p.c.Block
|
||||
for i := 0; i < len(parts); i++ {
|
||||
directives := block.FindDirectives(parts[i])
|
||||
if len(directives) == 0 {
|
||||
return errors.New("given key not found")
|
||||
}
|
||||
if len(directives) > 1 {
|
||||
return errors.New("multiple directives found")
|
||||
}
|
||||
block, ok = directives[0].GetBlock().(*config.Block)
|
||||
if !ok {
|
||||
return errors.New("block is not *config.Block")
|
||||
}
|
||||
}
|
||||
|
||||
var newDirectives []config.IDirective
|
||||
for _, directive := range block.GetDirectives() {
|
||||
if directive.GetName() != last {
|
||||
newDirectives = append(newDirectives, directive)
|
||||
}
|
||||
}
|
||||
block.Directives = newDirectives
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set 通过表达式设置配置
|
||||
// e.g. Set("server.server_name", []directive)
|
||||
func (p *Parser) Set(key string, directives []*config.Directive) error {
|
||||
parts := strings.Split(key, ".")
|
||||
|
||||
var block *config.Block
|
||||
var ok bool
|
||||
block = p.c.Block
|
||||
for i := 0; i < len(parts); i++ {
|
||||
sub := block.FindDirectives(parts[i])
|
||||
if len(sub) == 0 {
|
||||
return errors.New("given key not found")
|
||||
}
|
||||
if len(sub) > 1 {
|
||||
return errors.New("multiple directives found")
|
||||
}
|
||||
block, ok = sub[0].GetBlock().(*config.Block)
|
||||
if !ok {
|
||||
return errors.New("block is not *config.Block")
|
||||
}
|
||||
}
|
||||
|
||||
for _, directive := range directives {
|
||||
directive.SetParent(block)
|
||||
block.Directives = append(block.Directives, directive)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Parser) Sort() {
|
||||
p.sortDirectives(p.c.Directives, p.orderIndex)
|
||||
}
|
||||
|
||||
func (p *Parser) Dump() string {
|
||||
p.Sort()
|
||||
return dumper.DumpConfig(p.c, dumper.IndentedStyle)
|
||||
}
|
||||
|
||||
func (p *Parser) sortDirectives(directives []config.IDirective, orderIndex map[string]int) {
|
||||
slices.SortFunc(directives, func(a config.IDirective, b config.IDirective) int {
|
||||
if orderIndex[a.GetName()] != orderIndex[b.GetName()] {
|
||||
return orderIndex[a.GetName()] - orderIndex[b.GetName()]
|
||||
}
|
||||
return slices.Compare(a.GetParameters(), b.GetParameters())
|
||||
})
|
||||
|
||||
for _, directive := range directives {
|
||||
if block, ok := directive.GetBlock().(*config.Block); ok {
|
||||
p.sortDirectives(block.Directives, orderIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
package nginx
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
)
|
||||
|
||||
type NginxTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func TestNginxTestSuite(t *testing.T) {
|
||||
suite.Run(t, &NginxTestSuite{})
|
||||
}
|
||||
|
||||
func (s *NginxTestSuite) TestListen() {
|
||||
parser, err := NewParser()
|
||||
s.NoError(err)
|
||||
listen, err := parser.GetListen()
|
||||
s.NoError(err)
|
||||
s.Equal([][]string{{"80"}}, listen)
|
||||
s.NoError(parser.SetListen([][]string{{"80"}, {"443"}}))
|
||||
listen, err = parser.GetListen()
|
||||
s.NoError(err)
|
||||
s.Equal([][]string{{"80"}, {"443"}}, listen)
|
||||
}
|
||||
|
||||
func (s *NginxTestSuite) TestServerName() {
|
||||
parser, err := NewParser()
|
||||
s.NoError(err)
|
||||
serverName, err := parser.GetServerName()
|
||||
s.NoError(err)
|
||||
s.Equal([]string{"localhost"}, serverName)
|
||||
s.NoError(parser.SetServerName([]string{"example.com"}))
|
||||
serverName, err = parser.GetServerName()
|
||||
s.NoError(err)
|
||||
s.Equal([]string{"example.com"}, serverName)
|
||||
}
|
||||
|
||||
func (s *NginxTestSuite) TestIndex() {
|
||||
parser, err := NewParser()
|
||||
s.NoError(err)
|
||||
index, err := parser.GetIndex()
|
||||
s.NoError(err)
|
||||
s.Equal([]string{"index.php", "index.html", "index.htm"}, index)
|
||||
s.NoError(parser.SetIndex([]string{"index.html", "index.htm"}))
|
||||
index, err = parser.GetIndex()
|
||||
s.NoError(err)
|
||||
s.Equal([]string{"index.html", "index.htm"}, index)
|
||||
}
|
||||
|
||||
func (s *NginxTestSuite) TestIndexWithComment() {
|
||||
parser, err := NewParser()
|
||||
s.NoError(err)
|
||||
index, comment, err := parser.GetIndexWithComment()
|
||||
s.NoError(err)
|
||||
s.Equal([]string{"index.php", "index.html", "index.htm"}, index)
|
||||
s.Equal([]string(nil), comment)
|
||||
s.NoError(parser.SetIndexWithComment([]string{"index.html", "index.htm"}, []string{"# 测试"}))
|
||||
index, comment, err = parser.GetIndexWithComment()
|
||||
s.NoError(err)
|
||||
s.Equal([]string{"index.html", "index.htm"}, index)
|
||||
s.Equal([]string{"# 测试"}, comment)
|
||||
}
|
||||
|
||||
func (s *NginxTestSuite) TestRoot() {
|
||||
parser, err := NewParser()
|
||||
s.NoError(err)
|
||||
root, err := parser.GetRoot()
|
||||
s.NoError(err)
|
||||
s.Equal("/www/wwwroot/default", root)
|
||||
s.NoError(parser.SetRoot("/www/wwwroot/test"))
|
||||
root, err = parser.GetRoot()
|
||||
s.NoError(err)
|
||||
s.Equal("/www/wwwroot/test", root)
|
||||
}
|
||||
|
||||
func (s *NginxTestSuite) TestRootWithComment() {
|
||||
parser, err := NewParser()
|
||||
s.NoError(err)
|
||||
root, comment, err := parser.GetRootWithComment()
|
||||
s.NoError(err)
|
||||
s.Equal("/www/wwwroot/default", root)
|
||||
s.Equal([]string(nil), comment)
|
||||
s.NoError(parser.SetRootWithComment("/www/wwwroot/test", []string{"# 测试"}))
|
||||
root, comment, err = parser.GetRootWithComment()
|
||||
s.NoError(err)
|
||||
s.Equal("/www/wwwroot/test", root)
|
||||
s.Equal([]string{"# 测试"}, comment)
|
||||
}
|
||||
|
||||
func (s *NginxTestSuite) TestIncludes() {
|
||||
parser, err := NewParser()
|
||||
s.NoError(err)
|
||||
includes, comments, err := parser.GetIncludes()
|
||||
s.NoError(err)
|
||||
s.Equal([]string{"enable-php-0.conf"}, includes)
|
||||
s.Equal([][]string{[]string(nil)}, comments)
|
||||
s.NoError(parser.SetIncludes([]string{"/www/server/vhost/rewrite/default.conf"}, nil))
|
||||
includes, comments, err = parser.GetIncludes()
|
||||
s.NoError(err)
|
||||
s.Equal([]string{"/www/server/vhost/rewrite/default.conf"}, includes)
|
||||
s.Equal([][]string{[]string(nil)}, comments)
|
||||
s.NoError(parser.SetIncludes([]string{"/www/server/vhost/rewrite/test.conf"}, [][]string{{"# 伪静态规则测试"}}))
|
||||
includes, comments, err = parser.GetIncludes()
|
||||
s.NoError(err)
|
||||
s.Equal([]string{"/www/server/vhost/rewrite/test.conf"}, includes)
|
||||
s.Equal([][]string{{"# 伪静态规则测试"}}, comments)
|
||||
}
|
||||
|
||||
func (s *NginxTestSuite) TestPHP() {
|
||||
parser, err := NewParser()
|
||||
s.NoError(err)
|
||||
s.Equal(0, parser.GetPHP())
|
||||
s.NoError(parser.SetPHP(80))
|
||||
s.Equal(80, parser.GetPHP())
|
||||
s.NoError(parser.SetPHP(0))
|
||||
s.Equal(0, parser.GetPHP())
|
||||
}
|
||||
|
||||
func (s *NginxTestSuite) TestHTTP() {
|
||||
parser, err := NewParser()
|
||||
s.NoError(err)
|
||||
expect, err := io.Read("testdata/http.conf")
|
||||
s.NoError(err)
|
||||
s.Equal(expect, parser.Dump())
|
||||
}
|
||||
|
||||
func (s *NginxTestSuite) TestHTTPS() {
|
||||
parser, err := NewParser()
|
||||
s.NoError(err)
|
||||
s.False(parser.GetHTTPS())
|
||||
s.NoError(parser.SetHTTPS("/www/server/vhost/ssl/default.pem", "/www/server/vhost/ssl/default.key"))
|
||||
s.True(parser.GetHTTPS())
|
||||
expect, err := io.Read("testdata/https.conf")
|
||||
s.NoError(err)
|
||||
s.Equal(expect, parser.Dump())
|
||||
}
|
||||
|
||||
func (s *NginxTestSuite) TestHTTPSProtocols() {
|
||||
parser, err := NewParser()
|
||||
s.NoError(err)
|
||||
s.NoError(parser.SetHTTPS("/www/server/vhost/ssl/default.pem", "/www/server/vhost/ssl/default.key"))
|
||||
s.Equal([]string{"TLSv1.2", "TLSv1.3"}, parser.GetHTTPSProtocols())
|
||||
s.NoError(parser.SetHTTPSProtocols([]string{"TLSv1.3"}))
|
||||
s.Equal([]string{"TLSv1.3"}, parser.GetHTTPSProtocols())
|
||||
}
|
||||
|
||||
func (s *NginxTestSuite) TestHTTPSCiphers() {
|
||||
parser, err := NewParser()
|
||||
s.NoError(err)
|
||||
s.NoError(parser.SetHTTPS("/www/server/vhost/ssl/default.pem", "/www/server/vhost/ssl/default.key"))
|
||||
s.Equal("ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305", parser.GetHTTPSCiphers())
|
||||
s.NoError(parser.SetHTTPSCiphers("TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384"))
|
||||
s.Equal("TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384", parser.GetHTTPSCiphers())
|
||||
}
|
||||
|
||||
func (s *NginxTestSuite) TestOCSP() {
|
||||
parser, err := NewParser()
|
||||
s.NoError(err)
|
||||
s.NoError(err)
|
||||
s.NoError(parser.SetHTTPS("/www/server/vhost/ssl/default.pem", "/www/server/vhost/ssl/default.key"))
|
||||
s.False(parser.GetOCSP())
|
||||
s.NoError(parser.SetOCSP(false))
|
||||
s.False(parser.GetOCSP())
|
||||
s.NoError(parser.SetOCSP(true))
|
||||
s.True(parser.GetOCSP())
|
||||
s.NoError(parser.SetOCSP(false))
|
||||
s.False(parser.GetOCSP())
|
||||
}
|
||||
|
||||
func (s *NginxTestSuite) TestHSTS() {
|
||||
parser, err := NewParser()
|
||||
s.NoError(err)
|
||||
s.NoError(parser.SetHTTPS("/www/server/vhost/ssl/default.pem", "/www/server/vhost/ssl/default.key"))
|
||||
s.False(parser.GetHSTS())
|
||||
s.NoError(parser.SetHSTS(false))
|
||||
s.False(parser.GetHSTS())
|
||||
s.NoError(parser.SetHSTS(true))
|
||||
s.True(parser.GetHSTS())
|
||||
s.NoError(parser.SetHSTS(false))
|
||||
s.False(parser.GetHSTS())
|
||||
}
|
||||
|
||||
func (s *NginxTestSuite) TestHTTPSRedirect() {
|
||||
parser, err := NewParser()
|
||||
s.NoError(err)
|
||||
s.NoError(parser.SetHTTPS("/www/server/vhost/ssl/default.pem", "/www/server/vhost/ssl/default.key"))
|
||||
s.False(parser.GetHTTPSRedirect())
|
||||
s.NoError(parser.SetHTTPRedirect(false))
|
||||
s.False(parser.GetHTTPSRedirect())
|
||||
s.NoError(parser.SetHTTPRedirect(true))
|
||||
s.True(parser.GetHTTPSRedirect())
|
||||
s.NoError(parser.SetHTTPRedirect(false))
|
||||
s.False(parser.GetHTTPSRedirect())
|
||||
}
|
||||
|
||||
func (s *NginxTestSuite) TestAccessLog() {
|
||||
parser, err := NewParser()
|
||||
s.NoError(err)
|
||||
log, err := parser.GetAccessLog()
|
||||
s.NoError(err)
|
||||
s.Equal("/www/wwwlogs/default.log", log)
|
||||
s.NoError(parser.SetAccessLog("/www/wwwlogs/access.log"))
|
||||
log, err = parser.GetAccessLog()
|
||||
s.NoError(err)
|
||||
s.Equal("/www/wwwlogs/access.log", log)
|
||||
}
|
||||
|
||||
func (s *NginxTestSuite) TestErrorLog() {
|
||||
parser, err := NewParser()
|
||||
s.NoError(err)
|
||||
log, err := parser.GetErrorLog()
|
||||
s.NoError(err)
|
||||
s.Equal("/www/wwwlogs/default.log", log)
|
||||
s.NoError(parser.SetErrorLog("/www/wwwlogs/error.log"))
|
||||
log, err = parser.GetErrorLog()
|
||||
s.NoError(err)
|
||||
s.Equal("/www/wwwlogs/error.log", log)
|
||||
}
|
||||
@@ -0,0 +1,452 @@
|
||||
package nginx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/tufanbarisyildirim/gonginx/config"
|
||||
)
|
||||
|
||||
func (p *Parser) SetListen(listen [][]string) error {
|
||||
var directives []*config.Directive
|
||||
for _, l := range listen {
|
||||
directives = append(directives, &config.Directive{
|
||||
Name: "listen",
|
||||
Parameters: l,
|
||||
})
|
||||
}
|
||||
|
||||
if err := p.Clear("server.listen"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return p.Set("server", directives)
|
||||
}
|
||||
|
||||
func (p *Parser) SetServerName(serverName []string) error {
|
||||
if err := p.Clear("server.server_name"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return p.Set("server", []*config.Directive{
|
||||
{
|
||||
Name: "server_name",
|
||||
Parameters: serverName,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Parser) SetIndex(index []string) error {
|
||||
if err := p.Clear("server.index"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return p.Set("server", []*config.Directive{
|
||||
{
|
||||
Name: "index",
|
||||
Parameters: index,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Parser) SetIndexWithComment(index []string, comment []string) error {
|
||||
if err := p.Clear("server.index"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return p.Set("server", []*config.Directive{
|
||||
{
|
||||
Name: "index",
|
||||
Parameters: index,
|
||||
Comment: comment,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Parser) SetRoot(root string) error {
|
||||
if err := p.Clear("server.root"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return p.Set("server", []*config.Directive{
|
||||
{
|
||||
Name: "root",
|
||||
Parameters: []string{root},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Parser) SetRootWithComment(root string, comment []string) error {
|
||||
if err := p.Clear("server.root"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return p.Set("server", []*config.Directive{
|
||||
{
|
||||
Name: "root",
|
||||
Parameters: []string{root},
|
||||
Comment: comment,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Parser) SetIncludes(includes []string, comments [][]string) error {
|
||||
if err := p.Clear("server.include"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var directives []*config.Directive
|
||||
for i, item := range includes {
|
||||
var comment []string
|
||||
if i < len(comments) {
|
||||
comment = comments[i]
|
||||
}
|
||||
directives = append(directives, &config.Directive{
|
||||
Name: "include",
|
||||
Parameters: []string{item},
|
||||
Comment: comment,
|
||||
})
|
||||
}
|
||||
|
||||
return p.Set("server", directives)
|
||||
}
|
||||
|
||||
func (p *Parser) SetPHP(php int) error {
|
||||
old, err := p.Find("server.include")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = p.Clear("server.include"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var directives []*config.Directive
|
||||
var foundFlag bool
|
||||
for _, item := range old {
|
||||
// 查找enable-php的配置
|
||||
if slices.ContainsFunc(item.GetParameters(), func(s string) bool {
|
||||
return strings.HasPrefix(s, "enable-php-") && strings.HasSuffix(s, ".conf")
|
||||
}) {
|
||||
foundFlag = true
|
||||
directives = append(directives, &config.Directive{
|
||||
Name: item.GetName(),
|
||||
Parameters: []string{fmt.Sprintf("enable-php-%d.conf", php)},
|
||||
Comment: item.GetComment(),
|
||||
})
|
||||
} else {
|
||||
// 其余的原样保留
|
||||
directives = append(directives, &config.Directive{
|
||||
Name: item.GetName(),
|
||||
Parameters: item.GetParameters(),
|
||||
Comment: item.GetComment(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到enable-php的配置,直接添加一个
|
||||
if !foundFlag {
|
||||
directives = append(directives, &config.Directive{
|
||||
Name: "include",
|
||||
Parameters: []string{fmt.Sprintf("enable-php-%d.conf", php)},
|
||||
})
|
||||
}
|
||||
|
||||
return p.Set("server", directives)
|
||||
}
|
||||
|
||||
func (p *Parser) ClearSetHTTPS() error {
|
||||
if err := p.Clear("server.ssl_certificate"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := p.Clear("server.ssl_certificate_key"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := p.Clear("server.ssl_session_timeout"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := p.Clear("server.ssl_session_cache"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := p.Clear("server.ssl_protocols"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := p.Clear("server.ssl_ciphers"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := p.Clear("server.ssl_prefer_server_ciphers"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := p.Clear("server.ssl_early_data"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Parser) SetHTTPS(cert, key string) error {
|
||||
if err := p.ClearSetHTTPS(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return p.Set("server", []*config.Directive{
|
||||
{
|
||||
Name: "ssl_certificate",
|
||||
Parameters: []string{cert},
|
||||
},
|
||||
{
|
||||
Name: "ssl_certificate_key",
|
||||
Parameters: []string{key},
|
||||
},
|
||||
{
|
||||
Name: "ssl_session_timeout",
|
||||
Parameters: []string{"1d"},
|
||||
},
|
||||
{
|
||||
Name: "ssl_session_cache",
|
||||
Parameters: []string{"shared:SSL:10m"},
|
||||
},
|
||||
{
|
||||
Name: "ssl_protocols",
|
||||
Parameters: []string{"TLSv1.2", "TLSv1.3"},
|
||||
},
|
||||
{
|
||||
Name: "ssl_ciphers",
|
||||
Parameters: []string{"ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305"},
|
||||
},
|
||||
{
|
||||
Name: "ssl_prefer_server_ciphers",
|
||||
Parameters: []string{"off"},
|
||||
},
|
||||
{
|
||||
Name: "ssl_early_data",
|
||||
Parameters: []string{"on"},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Parser) SetHTTPSProtocols(protocols []string) error {
|
||||
if err := p.Clear("server.ssl_protocols"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return p.Set("server", []*config.Directive{
|
||||
{
|
||||
Name: "ssl_protocols",
|
||||
Parameters: protocols,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Parser) SetHTTPSCiphers(ciphers string) error {
|
||||
if err := p.Clear("server.ssl_ciphers"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return p.Set("server", []*config.Directive{
|
||||
{
|
||||
Name: "ssl_ciphers",
|
||||
Parameters: []string{ciphers},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Parser) SetOCSP(ocsp bool) error {
|
||||
if err := p.Clear("server.ssl_stapling"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := p.Clear("server.ssl_stapling_verify"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ocsp {
|
||||
return p.Set("server", []*config.Directive{
|
||||
{
|
||||
Name: "ssl_stapling",
|
||||
Parameters: []string{"on"},
|
||||
},
|
||||
{
|
||||
Name: "ssl_stapling_verify",
|
||||
Parameters: []string{"on"},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Parser) SetHSTS(hsts bool) error {
|
||||
old, err := p.Find("server.add_header")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = p.Clear("server.add_header"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var directives []*config.Directive
|
||||
var foundFlag bool
|
||||
for _, dir := range old {
|
||||
if slices.Contains(dir.GetParameters(), "Strict-Transport-Security") {
|
||||
foundFlag = true
|
||||
if hsts {
|
||||
directives = append(directives, &config.Directive{
|
||||
Name: dir.GetName(),
|
||||
Parameters: []string{"Strict-Transport-Security", "max-age=31536000"},
|
||||
Comment: dir.GetComment(),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
directives = append(directives, &config.Directive{
|
||||
Name: dir.GetName(),
|
||||
Parameters: dir.GetParameters(),
|
||||
Comment: dir.GetComment(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if !foundFlag && hsts {
|
||||
directives = append(directives, &config.Directive{
|
||||
Name: "add_header",
|
||||
Parameters: []string{"Strict-Transport-Security", "max-age=31536000"},
|
||||
})
|
||||
}
|
||||
|
||||
return p.Set("server", directives)
|
||||
}
|
||||
|
||||
func (p *Parser) SetHTTPRedirect(httpRedirect bool) error {
|
||||
// if 重定向
|
||||
ifs, err := p.Find("server.if")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = p.Clear("server.if"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var directives []*config.Directive
|
||||
var foundFlag bool
|
||||
for _, dir := range ifs { // 所有 if
|
||||
if !httpRedirect {
|
||||
if len(dir.GetParameters()) == 3 && dir.GetParameters()[0] == "($scheme" && dir.GetParameters()[1] == "=" && dir.GetParameters()[2] == "http)" {
|
||||
continue
|
||||
}
|
||||
}
|
||||
var ifDirectives []config.IDirective
|
||||
for _, dir2 := range dir.GetBlock().GetDirectives() { // 每个 if 中所有指令
|
||||
if !httpRedirect {
|
||||
// 不启用http重定向,则判断并移除特定的return指令
|
||||
if dir2.GetName() != "return" && !slices.Contains(dir2.GetParameters(), "https://$host$request_uri") {
|
||||
ifDirectives = append(ifDirectives, dir2)
|
||||
}
|
||||
} else {
|
||||
// 启用http重定向,需要检查防止重复添加
|
||||
if dir2.GetName() == "return" && slices.Contains(dir2.GetParameters(), "https://$host$request_uri") {
|
||||
foundFlag = true
|
||||
}
|
||||
ifDirectives = append(ifDirectives, dir2)
|
||||
}
|
||||
}
|
||||
// 写回 if 指令
|
||||
if block, ok := dir.GetBlock().(*config.Block); ok {
|
||||
block.Directives = ifDirectives
|
||||
}
|
||||
directives = append(directives, &config.Directive{
|
||||
Block: dir.GetBlock(),
|
||||
Name: dir.GetName(),
|
||||
Parameters: dir.GetParameters(),
|
||||
Comment: dir.GetComment(),
|
||||
})
|
||||
}
|
||||
|
||||
if !foundFlag && httpRedirect {
|
||||
ifDir := &config.Directive{
|
||||
Name: "if",
|
||||
Block: &config.Block{},
|
||||
Parameters: []string{"($scheme", "=", "http)"},
|
||||
}
|
||||
redirectDir := &config.Directive{
|
||||
Name: "return",
|
||||
Parameters: []string{"308", "https://$host$request_uri"},
|
||||
}
|
||||
redirectDir.SetParent(ifDir.GetBlock())
|
||||
ifBlock := ifDir.GetBlock().(*config.Block)
|
||||
ifBlock.Directives = append(ifBlock.Directives, redirectDir)
|
||||
directives = append(directives, ifDir)
|
||||
}
|
||||
|
||||
if err = p.Set("server", directives); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// error_page 497 重定向
|
||||
directives = nil
|
||||
errorPages, err := p.Find("server.error_page")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = p.Clear("server.error_page"); err != nil {
|
||||
return err
|
||||
}
|
||||
var found497 bool
|
||||
for _, dir := range errorPages {
|
||||
if !httpRedirect {
|
||||
// 不启用https重定向,则判断并移除特定的return指令
|
||||
if !slices.Contains(dir.GetParameters(), "497") && !slices.Contains(dir.GetParameters(), "https://$host:$server_port$request_uri") {
|
||||
directives = append(directives, &config.Directive{
|
||||
Block: dir.GetBlock(),
|
||||
Name: dir.GetName(),
|
||||
Parameters: dir.GetParameters(),
|
||||
Comment: dir.GetComment(),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// 启用https重定向,需要检查防止重复添加
|
||||
if slices.Contains(dir.GetParameters(), "497") && slices.Contains(dir.GetParameters(), "https://$host:$server_port$request_uri") {
|
||||
found497 = true
|
||||
}
|
||||
directives = append(directives, &config.Directive{
|
||||
Block: dir.GetBlock(),
|
||||
Name: dir.GetName(),
|
||||
Parameters: dir.GetParameters(),
|
||||
Comment: dir.GetComment(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if !found497 && httpRedirect {
|
||||
directives = append(directives, &config.Directive{
|
||||
Name: "error_page",
|
||||
Parameters: []string{"497", "=307", "https://$host:$server_port$request_uri"},
|
||||
})
|
||||
}
|
||||
|
||||
return p.Set("server", directives)
|
||||
}
|
||||
|
||||
func (p *Parser) SetAccessLog(accessLog string) error {
|
||||
if err := p.Clear("server.access_log"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return p.Set("server", []*config.Directive{
|
||||
{
|
||||
Name: "access_log",
|
||||
Parameters: []string{accessLog},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Parser) SetErrorLog(errorLog string) error {
|
||||
if err := p.Clear("server.error_log"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return p.Set("server", []*config.Directive{
|
||||
{
|
||||
Name: "error_log",
|
||||
Parameters: []string{errorLog},
|
||||
},
|
||||
})
|
||||
}
|
||||
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
index index.php index.html index.htm;
|
||||
root /www/wwwroot/default;
|
||||
# 错误页配置
|
||||
error_page 404 /404.html;
|
||||
include enable-php-0.conf;
|
||||
# 不记录静态文件日志
|
||||
location ~ .*\.(bmp|jpg|jpeg|png|gif|svg|ico|tiff|webp|avif|heif|heic|jxl)$ {
|
||||
expires 30d;
|
||||
access_log /dev/null;
|
||||
error_log /dev/null;
|
||||
}
|
||||
location ~ .*\.(js|css|ttf|otf|woff|woff2|eot)$ {
|
||||
expires 6h;
|
||||
access_log /dev/null;
|
||||
error_log /dev/null;
|
||||
}
|
||||
# 禁止部分敏感目录
|
||||
location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn|\.env) {
|
||||
return 404;
|
||||
}
|
||||
access_log /www/wwwlogs/default.log;
|
||||
error_log /www/wwwlogs/default.log;
|
||||
}
|
||||
Vendored
+34
@@ -0,0 +1,34 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
index index.php index.html index.htm;
|
||||
root /www/wwwroot/default;
|
||||
ssl_certificate /www/server/vhost/ssl/default.pem;
|
||||
ssl_certificate_key /www/server/vhost/ssl/default.key;
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305;
|
||||
ssl_prefer_server_ciphers off;
|
||||
ssl_early_data on;
|
||||
# 错误页配置
|
||||
error_page 404 /404.html;
|
||||
include enable-php-0.conf;
|
||||
# 不记录静态文件日志
|
||||
location ~ .*\.(bmp|jpg|jpeg|png|gif|svg|ico|tiff|webp|avif|heif|heic|jxl)$ {
|
||||
expires 30d;
|
||||
access_log /dev/null;
|
||||
error_log /dev/null;
|
||||
}
|
||||
location ~ .*\.(js|css|ttf|otf|woff|woff2|eot)$ {
|
||||
expires 6h;
|
||||
access_log /dev/null;
|
||||
error_log /dev/null;
|
||||
}
|
||||
# 禁止部分敏感目录
|
||||
location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn|\.env) {
|
||||
return 404;
|
||||
}
|
||||
access_log /www/wwwlogs/default.log;
|
||||
error_log /www/wwwlogs/default.log;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package punycode
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"golang.org/x/net/idna"
|
||||
)
|
||||
|
||||
// EncodeDomain 将 Unicode 域名编码为 Punycode
|
||||
func EncodeDomain(domain string) (string, error) {
|
||||
ascii, err := idna.ToASCII(domain)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("domain encode failed: %w", err)
|
||||
}
|
||||
return ascii, nil
|
||||
}
|
||||
|
||||
// EncodeDomains 将 Unicode 域名列表编码为 Punycode
|
||||
func EncodeDomains(domain []string) (encoded []string, err error) {
|
||||
var punycode string
|
||||
for item := range slices.Values(domain) {
|
||||
punycode, err = EncodeDomain(item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encoded = append(encoded, punycode)
|
||||
}
|
||||
return encoded, nil
|
||||
}
|
||||
|
||||
// DecodeDomain 将 Punycode 域名解码为 Unicode 域名
|
||||
func DecodeDomain(punycodeDomain string) (string, error) {
|
||||
unicode, err := idna.ToUnicode(punycodeDomain)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("domain decode failed: %w", err)
|
||||
}
|
||||
return unicode, nil
|
||||
}
|
||||
|
||||
// DecodeDomains 将 Punycode 域名列表解码为 Unicode 域名
|
||||
func DecodeDomains(punycode []string) (decoded []string, err error) {
|
||||
var unicode string
|
||||
for item := range slices.Values(punycode) {
|
||||
unicode, err = DecodeDomain(item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decoded = append(decoded, unicode)
|
||||
}
|
||||
return decoded, nil
|
||||
}
|
||||
@@ -2,5 +2,6 @@ package types
|
||||
|
||||
type BackupFile struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Size string `json:"size"`
|
||||
}
|
||||
|
||||
+30
-24
@@ -1,29 +1,35 @@
|
||||
package types
|
||||
|
||||
// WebsiteListen 网站监听配置
|
||||
type WebsiteListen struct {
|
||||
Address string `form:"address" json:"address" validate:"required"` // 监听地址 e.g. 80 0.0.0.0:80 [::]:80
|
||||
HTTPS bool `form:"https" json:"https" validate:"required"` // 是否启用HTTPS
|
||||
QUIC bool `form:"quic" json:"quic"` // 是否启用QUIC
|
||||
}
|
||||
|
||||
// WebsiteSetting 网站设置
|
||||
type WebsiteSetting struct {
|
||||
Name string `json:"name"`
|
||||
Domains []string `json:"domains"`
|
||||
Ports []uint `json:"ports"`
|
||||
SSLPorts []uint `json:"ssl_ports"`
|
||||
QUICPorts []uint `json:"quic_ports"`
|
||||
Root string `json:"root"`
|
||||
Path string `json:"path"`
|
||||
Index string `json:"index"`
|
||||
PHP int `json:"php"`
|
||||
OpenBasedir bool `json:"open_basedir"`
|
||||
SSL bool `json:"ssl"`
|
||||
SSLCertificate string `json:"ssl_certificate"`
|
||||
SSLCertificateKey string `json:"ssl_certificate_key"`
|
||||
SSLNotBefore string `json:"ssl_not_before"`
|
||||
SSLNotAfter string `json:"ssl_not_after"`
|
||||
SSLDNSNames []string `json:"ssl_dns_names"`
|
||||
SSLIssuer string `json:"ssl_issuer"`
|
||||
SSLOCSPServer []string `json:"ssl_ocsp_server"`
|
||||
HTTPRedirect bool `json:"http_redirect"`
|
||||
HSTS bool `json:"hsts"`
|
||||
OCSP bool `json:"ocsp"`
|
||||
Rewrite string `json:"rewrite"`
|
||||
Raw string `json:"raw"`
|
||||
Log string `json:"log"`
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Listens []WebsiteListen `form:"listens" json:"listens" validate:"required"`
|
||||
Domains []string `json:"domains"`
|
||||
Path string `json:"path"` // 网站目录
|
||||
Root string `json:"root"` // 运行目录
|
||||
Index []string `json:"index"`
|
||||
PHP int `json:"php"`
|
||||
OpenBasedir bool `json:"open_basedir"`
|
||||
HTTPS bool `json:"https"`
|
||||
SSLCertificate string `json:"ssl_certificate"`
|
||||
SSLCertificateKey string `json:"ssl_certificate_key"`
|
||||
SSLNotBefore string `json:"ssl_not_before"`
|
||||
SSLNotAfter string `json:"ssl_not_after"`
|
||||
SSLDNSNames []string `json:"ssl_dns_names"`
|
||||
SSLIssuer string `json:"ssl_issuer"`
|
||||
SSLOCSPServer []string `json:"ssl_ocsp_server"`
|
||||
HTTPRedirect bool `json:"http_redirect"`
|
||||
HSTS bool `json:"hsts"`
|
||||
OCSP bool `json:"ocsp"`
|
||||
Rewrite string `json:"rewrite"`
|
||||
Raw string `json:"raw"`
|
||||
Log string `json:"log"`
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ export function createViteProxy(isUseProxy = true, proxyType: ProxyType) {
|
||||
const proxy: Record<string, string | ProxyOptions> = {
|
||||
[proxyConfig.prefix]: {
|
||||
target: proxyConfig.target,
|
||||
secure: proxyConfig.secure,
|
||||
changeOrigin: true,
|
||||
rewrite: (path: string) => path.replace(new RegExp(`^${proxyConfig.prefix}`), '')
|
||||
}
|
||||
|
||||
Generated
+4
-4
@@ -59,7 +59,7 @@ importers:
|
||||
devDependencies:
|
||||
'@iconify/json':
|
||||
specifier: ^2.2.258
|
||||
version: 2.2.259
|
||||
version: 2.2.260
|
||||
'@iconify/vue':
|
||||
specifier: ^4.1.2
|
||||
version: 4.1.2(vue@3.5.12(typescript@5.6.3))
|
||||
@@ -516,8 +516,8 @@ packages:
|
||||
resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==}
|
||||
deprecated: Use @eslint/object-schema instead
|
||||
|
||||
'@iconify/json@2.2.259':
|
||||
resolution: {integrity: sha512-QA5chdx3Jw/jpPv6NrABJIbG6++572KocO/hyebEpSx0KYPc1f5dWkGG1tFu6v9NB7Xls8bXZHXak011Gvoiiw==}
|
||||
'@iconify/json@2.2.260':
|
||||
resolution: {integrity: sha512-0vVJBAJ8GOoS07b8yC6c8NHfFx1GJnM8xtebtyynQYZ0/60dxKi86imdNftq5oY/cFfvVUGlmBi/2HS+sxWsWA==}
|
||||
|
||||
'@iconify/types@2.0.0':
|
||||
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
|
||||
@@ -2862,7 +2862,7 @@ snapshots:
|
||||
|
||||
'@humanwhocodes/object-schema@2.0.3': {}
|
||||
|
||||
'@iconify/json@2.2.259':
|
||||
'@iconify/json@2.2.260':
|
||||
dependencies:
|
||||
'@iconify/types': 2.0.0
|
||||
pathe: 1.1.2
|
||||
|
||||
@@ -4,9 +4,9 @@ import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
// 获取配置
|
||||
config: (service: string): Promise<AxiosResponse<any>> =>
|
||||
request.get('/apps/frp/config', { params: { service } }),
|
||||
config: (name: string): Promise<AxiosResponse<any>> =>
|
||||
request.get('/apps/frp/config', { params: { name } }),
|
||||
// 保存配置
|
||||
saveConfig: (service: string, config: string): Promise<AxiosResponse<any>> =>
|
||||
request.post('/apps/frp/config', { service, config })
|
||||
saveConfig: (name: string, config: string): Promise<AxiosResponse<any>> =>
|
||||
request.post('/apps/frp/config', { name, config })
|
||||
}
|
||||
|
||||
@@ -5,16 +5,16 @@ import { request } from '@/utils'
|
||||
export default {
|
||||
// 列表
|
||||
list: (page: number, limit: number): Promise<AxiosResponse<any>> =>
|
||||
request.get('/apps/pureftpd/list', { params: { page, limit } }),
|
||||
request.get('/apps/pureftpd/users', { params: { page, limit } }),
|
||||
// 添加
|
||||
add: (username: string, password: string, path: string): Promise<AxiosResponse<any>> =>
|
||||
request.post('/apps/pureftpd/add', { username, password, path }),
|
||||
request.post('/apps/pureftpd/users', { username, password, path }),
|
||||
// 删除
|
||||
delete: (username: string): Promise<AxiosResponse<any>> =>
|
||||
request.delete('/apps/pureftpd/delete', { params: { username } }),
|
||||
request.delete(`/apps/pureftpd/users/${username}`),
|
||||
// 修改密码
|
||||
changePassword: (username: string, password: string): Promise<AxiosResponse<any>> =>
|
||||
request.post('/apps/pureftpd/changePassword', { username, password }),
|
||||
request.post(`/apps/pureftpd/users/${username}/password`, { password }),
|
||||
// 获取端口
|
||||
port: (): Promise<AxiosResponse<any>> => request.get('/apps/pureftpd/port'),
|
||||
// 修改端口
|
||||
|
||||
@@ -5,9 +5,9 @@ import { request } from '@/utils'
|
||||
export default {
|
||||
// 列表
|
||||
list: (page: number, limit: number): Promise<AxiosResponse<any>> =>
|
||||
request.get('/apps/s3fs/list', { params: { page, limit } }),
|
||||
request.get('/apps/s3fs/mounts', { params: { page, limit } }),
|
||||
// 添加
|
||||
add: (data: any): Promise<AxiosResponse<any>> => request.post('/apps/s3fs/add', data),
|
||||
add: (data: any): Promise<AxiosResponse<any>> => request.post('/apps/s3fs/mounts', data),
|
||||
// 删除
|
||||
delete: (id: number): Promise<AxiosResponse<any>> => request.post('/apps/s3fs/delete', { id })
|
||||
delete: (id: number): Promise<AxiosResponse<any>> => request.post('/apps/s3fs/mounts', { id })
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@ import { request } from '@/utils'
|
||||
export default {
|
||||
// 服务名称
|
||||
service: (): Promise<AxiosResponse<any>> => request.get('/apps/supervisor/service'),
|
||||
// 负载状态
|
||||
load: (): Promise<AxiosResponse<any>> => request.get('/apps/supervisor/load'),
|
||||
// 获取错误日志
|
||||
log: (): Promise<AxiosResponse<any>> => request.get('/apps/supervisor/log'),
|
||||
// 清空错误日志
|
||||
@@ -21,29 +19,29 @@ export default {
|
||||
request.get('/apps/supervisor/processes', { params: { page, limit } }),
|
||||
// 进程启动
|
||||
startProcess: (process: string): Promise<AxiosResponse<any>> =>
|
||||
request.post('/apps/supervisor/startProcess', { process }),
|
||||
request.post(`/apps/supervisor/processes/${process}/start`, {}),
|
||||
// 进程停止
|
||||
stopProcess: (process: string): Promise<AxiosResponse<any>> =>
|
||||
request.post('/apps/supervisor/stopProcess', { process }),
|
||||
request.post(`/apps/supervisor/processes/${process}/stop`, {}),
|
||||
// 进程重启
|
||||
restartProcess: (process: string): Promise<AxiosResponse<any>> =>
|
||||
request.post('/apps/supervisor/restartProcess', { process }),
|
||||
request.post(`/apps/supervisor/processes/${process}/restart`, {}),
|
||||
// 进程日志
|
||||
processLog: (process: string): Promise<AxiosResponse<any>> =>
|
||||
request.get('/apps/supervisor/processLog', { params: { process } }),
|
||||
request.get(`/apps/supervisor/processes/${process}/log`),
|
||||
// 清空进程日志
|
||||
clearProcessLog: (process: string): Promise<AxiosResponse<any>> =>
|
||||
request.post('/apps/supervisor/clearProcessLog', { process }),
|
||||
request.post(`/apps/supervisor/processes/${process}/clearLog`, {}),
|
||||
// 进程配置
|
||||
processConfig: (process: string): Promise<AxiosResponse<any>> =>
|
||||
request.get('/apps/supervisor/processConfig', { params: { process } }),
|
||||
request.get(`/apps/supervisor/processes/${process}`),
|
||||
// 保存进程配置
|
||||
saveProcessConfig: (process: string, config: string): Promise<AxiosResponse<any>> =>
|
||||
request.post('/apps/supervisor/processConfig', { process, config }),
|
||||
// 添加进程
|
||||
addProcess: (process: any): Promise<AxiosResponse<any>> =>
|
||||
request.post('/apps/supervisor/addProcess', process),
|
||||
request.post(`/apps/supervisor/processes/${process}`, { config }),
|
||||
// 创建进程
|
||||
createProcess: (process: any): Promise<AxiosResponse<any>> =>
|
||||
request.post('/apps/supervisor/processes', process),
|
||||
// 删除进程
|
||||
deleteProcess: (process: string): Promise<AxiosResponse<any>> =>
|
||||
request.post('/apps/supervisor/deleteProcess', { process })
|
||||
request.delete(`/apps/supervisor/processes/${process}`)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { AxiosResponse } from 'axios'
|
||||
|
||||
import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
// 获取备份列表
|
||||
list: (type: string, page: number, limit: number): Promise<AxiosResponse<any>> =>
|
||||
request.get(`/backup/${type}`, { params: { page, limit } }),
|
||||
// 创建备份
|
||||
create: (type: string, target: string, path: string): Promise<AxiosResponse<any>> =>
|
||||
request.post(`/backup/${type}`, { target, path }),
|
||||
// 上传备份
|
||||
upload: (type: string, formData: FormData): Promise<AxiosResponse<any>> => {
|
||||
return request.post(`/backup/${type}/upload`, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
},
|
||||
// 删除备份
|
||||
delete: (type: string, file: string): Promise<AxiosResponse<any>> =>
|
||||
request.delete(`/backup/${type}/delete`, { params: { file } }),
|
||||
// 恢复备份
|
||||
restore: (type: string, file: string, target: string): Promise<AxiosResponse<any>> =>
|
||||
request.post(`/backup/${type}/restore`, { file, target })
|
||||
}
|
||||
@@ -9,7 +9,8 @@ export default {
|
||||
// 创建
|
||||
create: (data: any): Promise<AxiosResponse<any>> => request.post('/website', data),
|
||||
// 删除
|
||||
delete: (data: any): Promise<AxiosResponse<any>> => request.post('/website/' + data.id, data),
|
||||
delete: (id: number, path: boolean, db: boolean): Promise<AxiosResponse<any>> =>
|
||||
request.delete(`/website/${id}`, { params: { path, db } }),
|
||||
// 获取默认配置
|
||||
defaultConfig: (): Promise<AxiosResponse<any>> => request.get('/website/defaultConfig'),
|
||||
// 保存默认配置
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
"total": "累计上行 { sent } / 累计下行 { received }"
|
||||
},
|
||||
"disk": {
|
||||
"title": "磁盘",
|
||||
"title": "硬盘",
|
||||
"current": "实时读取 { read }/s / 实时写入 { write }/s",
|
||||
"total": "累计读取 { read } / 累计写入 { write }"
|
||||
}
|
||||
@@ -111,14 +111,14 @@
|
||||
"title": "资源监控"
|
||||
},
|
||||
"appIndex": {
|
||||
"title": "应用市场",
|
||||
"title": "应用中心",
|
||||
"alerts": {
|
||||
"cache": "缓存更新成功",
|
||||
"warning": "升级应用前强烈建议先备份/快照,以免出现问题时无法回滚!",
|
||||
"setup": "设置成功",
|
||||
"install": "任务已提交,请稍后查看任务进度",
|
||||
"update": "任务已提交,请前往任务中心查看任务进度",
|
||||
"uninstall": "任务已提交,请前往任务中心查看任务进度"
|
||||
"update": "任务已提交,请前往后台任务查看任务进度",
|
||||
"uninstall": "任务已提交,请前往后台任务查看任务进度"
|
||||
},
|
||||
"buttons": {
|
||||
"updateCache": "更新缓存",
|
||||
@@ -286,7 +286,7 @@
|
||||
}
|
||||
},
|
||||
"taskIndex": {
|
||||
"title": "任务中心",
|
||||
"title": "后台任务",
|
||||
"alerts": {
|
||||
"delete": "任务已删除"
|
||||
},
|
||||
|
||||
@@ -82,7 +82,13 @@ const handleClose = () => {
|
||||
</n-form>
|
||||
<n-row :gutter="[0, 24]">
|
||||
<n-col :span="24">
|
||||
<n-button type="info" block :loading="doSubmit" :disabled="doSubmit" @click="handleSubmit">
|
||||
<n-button
|
||||
type="info"
|
||||
block
|
||||
:loading="doSubmit"
|
||||
:disabled="model.channel == null || doSubmit"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
提交
|
||||
</n-button>
|
||||
</n-col>
|
||||
|
||||
@@ -7,7 +7,7 @@ export default {
|
||||
path: '/app',
|
||||
component: Layout,
|
||||
meta: {
|
||||
order: 8
|
||||
order: 90
|
||||
},
|
||||
children: [
|
||||
{
|
||||
@@ -16,7 +16,7 @@ export default {
|
||||
component: () => import('./IndexView.vue'),
|
||||
meta: {
|
||||
title: 'appIndex.title',
|
||||
icon: 'mdi:puzzle-outline',
|
||||
icon: 'mdi:apps',
|
||||
role: ['admin'],
|
||||
requireAuth: true
|
||||
}
|
||||
|
||||
@@ -300,8 +300,8 @@ onMounted(() => {
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</common-page>
|
||||
<n-modal v-model:show="addUserModal" title="新建用户">
|
||||
<n-card closable @close="() => (addUserModal = false)" title="新建用户" style="width: 60vw">
|
||||
<n-modal v-model:show="addUserModal" title="创建用户">
|
||||
<n-card closable @close="() => (addUserModal = false)" title="创建用户" style="width: 60vw">
|
||||
<n-form :model="addUserModel">
|
||||
<n-form-item path="username" label="用户名">
|
||||
<n-input
|
||||
|
||||
@@ -291,7 +291,7 @@ const handleReload = async () => {
|
||||
}
|
||||
|
||||
const handleAddProcess = async () => {
|
||||
await supervisor.addProcess(addProcessModel.value)
|
||||
await supervisor.createProcess(addProcessModel.value)
|
||||
window.$message.success('添加成功')
|
||||
addProcessModal.value = false
|
||||
onPageChange(1)
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
<script setup lang="ts">
|
||||
import backup from '@/api/panel/backup'
|
||||
import ListView from '@/views/backup/ListView.vue'
|
||||
import { NButton, NInput } from 'naive-ui'
|
||||
|
||||
const currentTab = ref('website')
|
||||
const createModal = ref(false)
|
||||
const createModel = ref({
|
||||
target: '',
|
||||
path: ''
|
||||
})
|
||||
const oldTab = ref('')
|
||||
|
||||
const handleCreate = () => {
|
||||
backup.create(currentTab.value, createModel.value.target, createModel.value.path).then(() => {
|
||||
createModal.value = false
|
||||
window.$message.success('创建成功')
|
||||
// 有点low,但是没找到更好的办法
|
||||
oldTab.value = currentTab.value
|
||||
currentTab.value = ''
|
||||
setTimeout(() => {
|
||||
currentTab.value = oldTab.value
|
||||
}, 0)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<common-page show-footer>
|
||||
<template #action>
|
||||
<div flex items-center>
|
||||
<n-button class="ml-16" type="primary" @click="createModal = true">
|
||||
<TheIcon :size="18" class="mr-5" icon="material-symbols:add" />
|
||||
创建备份
|
||||
</n-button>
|
||||
</div>
|
||||
</template>
|
||||
<n-flex vertical>
|
||||
<n-alert type="info">此处仅显示面板默认备份目录的文件。</n-alert>
|
||||
<n-tabs v-model:value="currentTab" type="line" animated>
|
||||
<n-tab-pane name="website" tab="网站">
|
||||
<list-view v-model:type="currentTab" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="mysql" tab="MySQL">
|
||||
<list-view v-model:type="currentTab" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="postgres" tab="PostgreSQL">
|
||||
<list-view v-model:type="currentTab" />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-flex>
|
||||
</common-page>
|
||||
<n-modal
|
||||
v-model:show="createModal"
|
||||
preset="card"
|
||||
title="创建备份"
|
||||
style="width: 60vw"
|
||||
size="huge"
|
||||
:bordered="false"
|
||||
:segmented="false"
|
||||
@close="createModal = false"
|
||||
>
|
||||
<n-form :model="createModel">
|
||||
<n-form-item path="name" label="名称">
|
||||
<n-input
|
||||
v-model:value="createModel.target"
|
||||
type="text"
|
||||
@keydown.enter.prevent
|
||||
placeholder="输入网站/数据库名称"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item path="path" label="目录">
|
||||
<n-input
|
||||
v-model:value="createModel.path"
|
||||
type="text"
|
||||
@keydown.enter.prevent
|
||||
placeholder="留空使用默认路径"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-row :gutter="[0, 24]">
|
||||
<n-col :span="24">
|
||||
<n-button type="info" block @click="handleCreate">提交</n-button>
|
||||
</n-col>
|
||||
</n-row>
|
||||
</n-modal>
|
||||
</template>
|
||||
@@ -0,0 +1,165 @@
|
||||
<script setup lang="ts">
|
||||
import backup from '@/api/panel/backup'
|
||||
import { renderIcon } from '@/utils'
|
||||
import type { MessageReactive } from 'naive-ui'
|
||||
import { NButton, NInput, NPopconfirm } from 'naive-ui'
|
||||
|
||||
import type { Backup } from './types'
|
||||
|
||||
const type = defineModel<string>('type', { type: String, required: true })
|
||||
|
||||
let messageReactive: MessageReactive | null = null
|
||||
|
||||
const restoreModal = ref(false)
|
||||
const restoreModel = ref({
|
||||
file: '',
|
||||
target: ''
|
||||
})
|
||||
|
||||
const columns: any = [
|
||||
{ title: '文件名', key: 'name', fixed: 'left', resizable: true, ellipsis: { tooltip: true } },
|
||||
{ title: '大小', key: 'size', width: 200, ellipsis: { tooltip: true } },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 200,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
hideInExcel: true,
|
||||
render(row: any) {
|
||||
return [
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'warning',
|
||||
secondary: true,
|
||||
onClick: () => {
|
||||
restoreModel.value.file = row.path
|
||||
restoreModal.value = true
|
||||
}
|
||||
},
|
||||
{
|
||||
default: () => '恢复',
|
||||
icon: renderIcon('material-symbols:settings-backup-restore-rounded', { size: 14 })
|
||||
}
|
||||
),
|
||||
h(
|
||||
NPopconfirm,
|
||||
{
|
||||
onPositiveClick: () => handleDelete(row.name)
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
return '确定删除备份吗?'
|
||||
},
|
||||
trigger: () => {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'error',
|
||||
style: 'margin-left: 15px;'
|
||||
},
|
||||
{
|
||||
default: () => '删除',
|
||||
icon: renderIcon('material-symbols:delete-outline', { size: 14 })
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const data = ref<Backup[]>([])
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageCount: 1,
|
||||
pageSize: 10,
|
||||
itemCount: 0,
|
||||
showQuickJumper: true,
|
||||
showSizePicker: true,
|
||||
pageSizes: [10, 20, 50, 100]
|
||||
})
|
||||
|
||||
const getList = async (page: number, limit: number) => {
|
||||
const { data } = await backup.list(type.value, page, limit)
|
||||
return data
|
||||
}
|
||||
|
||||
const onPageChange = (page: number) => {
|
||||
pagination.page = page
|
||||
getList(page, pagination.pageSize).then((res) => {
|
||||
data.value = res.items
|
||||
pagination.itemCount = res.total
|
||||
pagination.pageCount = res.total / pagination.pageSize + 1
|
||||
})
|
||||
}
|
||||
|
||||
const onPageSizeChange = (pageSize: number) => {
|
||||
pagination.pageSize = pageSize
|
||||
onPageChange(1)
|
||||
}
|
||||
|
||||
const handleRestore = async () => {
|
||||
messageReactive = window.$message.loading('恢复中...', {
|
||||
duration: 0
|
||||
})
|
||||
await backup.restore(type.value, restoreModel.value.file, restoreModel.value.target).then(() => {
|
||||
messageReactive?.destroy()
|
||||
window.$message.success('恢复成功')
|
||||
onPageChange(pagination.page)
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = async (file: string) => {
|
||||
await backup.delete(type.value, file).then(() => {
|
||||
window.$message.success('删除成功')
|
||||
onPageChange(pagination.page)
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
onPageChange(pagination.page)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-data-table
|
||||
striped
|
||||
remote
|
||||
:loading="false"
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:row-key="(row: any) => row.name"
|
||||
@update:page="onPageChange"
|
||||
@update:page-size="onPageSizeChange"
|
||||
/>
|
||||
<n-modal
|
||||
v-model:show="restoreModal"
|
||||
preset="card"
|
||||
title="恢复备份"
|
||||
style="width: 60vw"
|
||||
size="huge"
|
||||
:bordered="false"
|
||||
:segmented="false"
|
||||
@close="restoreModal = false"
|
||||
>
|
||||
<n-form :model="restoreModel">
|
||||
<n-form-item path="name" label="恢复目标">
|
||||
<n-input v-model:value="restoreModel.target" type="text" @keydown.enter.prevent />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-row :gutter="[0, 24]">
|
||||
<n-col :span="24">
|
||||
<n-button type="info" block @click="handleRestore">提交</n-button>
|
||||
</n-col>
|
||||
</n-row>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { RouteType } from '~/types/router'
|
||||
|
||||
const Layout = () => import('@/layout/IndexView.vue')
|
||||
|
||||
export default {
|
||||
name: 'backup',
|
||||
path: '/backup',
|
||||
component: Layout,
|
||||
meta: {
|
||||
order: 60
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'backup-index',
|
||||
path: '',
|
||||
component: () => import('./IndexView.vue'),
|
||||
meta: {
|
||||
title: '数据备份',
|
||||
icon: 'mdi:backup-outline',
|
||||
role: ['admin'],
|
||||
requireAuth: true
|
||||
}
|
||||
}
|
||||
]
|
||||
} as RouteType
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface Backup {
|
||||
name: string
|
||||
path: string
|
||||
size: string
|
||||
}
|
||||
@@ -7,7 +7,7 @@ export default {
|
||||
path: '/cert',
|
||||
component: Layout,
|
||||
meta: {
|
||||
order: 2
|
||||
order: 10
|
||||
},
|
||||
children: [
|
||||
{
|
||||
|
||||
@@ -11,16 +11,16 @@ const currentTab = ref('container')
|
||||
<common-page show-footer>
|
||||
<n-tabs v-model:value="currentTab" type="line" animated size="large">
|
||||
<n-tab-pane name="container" tab="容器">
|
||||
<ContainerView />
|
||||
<container-view />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="image" tab="镜像">
|
||||
<ImageView />
|
||||
<image-view />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="network" tab="网络">
|
||||
<NetworkView />
|
||||
<network-view />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="volume" tab="卷">
|
||||
<VolumeView />
|
||||
<volume-view />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</common-page>
|
||||
|
||||
@@ -7,7 +7,7 @@ export default {
|
||||
path: '/container',
|
||||
component: Layout,
|
||||
meta: {
|
||||
order: 5
|
||||
order: 40
|
||||
},
|
||||
children: [
|
||||
{
|
||||
|
||||
@@ -12,10 +12,9 @@ import type { CronTask } from '@/views/cron/types'
|
||||
const addModel = ref({
|
||||
name: '',
|
||||
type: 'shell',
|
||||
backup_type: 'website',
|
||||
website: '',
|
||||
database: '',
|
||||
target: '',
|
||||
save: 1,
|
||||
backup_type: 'website',
|
||||
backup_path: '',
|
||||
script: '# 在此输入你要执行的脚本内容',
|
||||
time: '* * * * *'
|
||||
@@ -194,7 +193,7 @@ const getWebsiteList = async (page: number, limit: number) => {
|
||||
value: item.name
|
||||
})
|
||||
}
|
||||
addModel.value.website = websites.value[0]?.value
|
||||
addModel.value.target = websites.value[0]?.value
|
||||
}
|
||||
|
||||
const getPhpAndDb = async () => {
|
||||
@@ -273,6 +272,14 @@ const handleCronSelectUpdate = (value: string) => {
|
||||
addModel.value.time = value
|
||||
}
|
||||
|
||||
watch(addModel, (value) => {
|
||||
if (value.backup_type === 'website') {
|
||||
addModel.value.target = websites.value[0]?.value
|
||||
} else {
|
||||
addModel.value.target = ''
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getPhpAndDb()
|
||||
getWebsiteList(1, 10000)
|
||||
@@ -329,7 +336,7 @@ onMounted(() => {
|
||||
<n-radio-group v-model:value="addModel.backup_type">
|
||||
<n-radio value="website">网站目录</n-radio>
|
||||
<n-radio value="mysql" :disabled="!mySQLInstalled"> MySQL 数据库</n-radio>
|
||||
<n-radio value="postgresql" :disabled="!postgreSQLInstalled">
|
||||
<n-radio value="postgres" :disabled="!postgreSQLInstalled">
|
||||
PostgreSQL 数据库
|
||||
</n-radio>
|
||||
</n-radio-group>
|
||||
@@ -342,7 +349,7 @@ onMounted(() => {
|
||||
label="选择网站"
|
||||
>
|
||||
<n-select
|
||||
v-model:value="addModel.website"
|
||||
v-model:value="addModel.target"
|
||||
:options="websites"
|
||||
placeholder="选择网站"
|
||||
/>
|
||||
@@ -351,7 +358,7 @@ onMounted(() => {
|
||||
v-if="addModel.backup_type !== 'website' && addModel.type === 'backup'"
|
||||
label="数据库名"
|
||||
>
|
||||
<n-input v-model:value="addModel.database" placeholder="数据库名" />
|
||||
<n-input v-model:value="addModel.target" placeholder="数据库名" />
|
||||
</n-form-item>
|
||||
<n-form-item v-if="addModel.type === 'backup'" label="保存目录">
|
||||
<n-input v-model:value="addModel.backup_path" placeholder="保存目录" />
|
||||
|
||||
@@ -7,7 +7,7 @@ export default {
|
||||
path: '/cron',
|
||||
component: Layout,
|
||||
meta: {
|
||||
order: 5
|
||||
order: 70
|
||||
},
|
||||
children: [
|
||||
{
|
||||
|
||||
@@ -127,7 +127,7 @@ const columns: DataTableColumns<RowData> = [
|
||||
selected.value = [row.full]
|
||||
compress.value = true
|
||||
} else {
|
||||
window.open('/api/panel/file/download?path=' + encodeURIComponent(row.full))
|
||||
window.open('/api/file/download?path=' + encodeURIComponent(row.full))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@ export default {
|
||||
path: '/file',
|
||||
component: Layout,
|
||||
meta: {
|
||||
order: 6
|
||||
order: 50
|
||||
},
|
||||
children: [
|
||||
{
|
||||
|
||||
@@ -13,6 +13,7 @@ const appStore = useAppStore()
|
||||
const realtime = ref<Realtime | null>(null)
|
||||
const systemInfo = ref<SystemInfo | null>(null)
|
||||
const homeApps = ref<HomeApp[] | null>(null)
|
||||
const homeAppsLoading = ref(false)
|
||||
const countInfo = ref<CountInfo>({
|
||||
website: 0,
|
||||
database: 0,
|
||||
@@ -56,7 +57,7 @@ const getRealtime = async () => {
|
||||
netTotalRecv.value = netTotalRecvTemp
|
||||
netCurrentSent.value = (netTotalSent.value - netTotalSentOld) / 3
|
||||
netCurrentRecv.value = (netTotalRecv.value - netTotalRecvOld) / 3
|
||||
// 计算磁盘读写
|
||||
// 计算硬盘读写
|
||||
let diskTotalReadTemp = 0
|
||||
let diskTotalWriteTemp = 0
|
||||
let diskTotalReadOld = diskTotalRead.value
|
||||
@@ -86,8 +87,10 @@ const getCountInfo = async () => {
|
||||
}
|
||||
|
||||
const getHomeApps = async () => {
|
||||
homeAppsLoading.value = true
|
||||
info.homeApps().then((res) => {
|
||||
homeApps.value = res.data
|
||||
homeAppsLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
@@ -532,7 +535,7 @@ onUnmounted(() => {
|
||||
<div min-w-375 flex-1>
|
||||
<n-card :segmented="true" rounded-10 size="small" :title="$t('homeIndex.apps.title')">
|
||||
<n-grid
|
||||
v-if="homeApps"
|
||||
v-if="!homeAppsLoading"
|
||||
x-gap="12"
|
||||
y-gap="12"
|
||||
cols="3 s:1 m:2 l:3"
|
||||
@@ -568,7 +571,8 @@ onUnmounted(() => {
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
<n-skeleton v-else text :repeat="9" />
|
||||
<n-text v-if="!homeAppsLoading && !homeApps"> 您还没有设置任何应用在此显示! </n-text>
|
||||
<n-skeleton v-if="homeAppsLoading" text :repeat="9" />
|
||||
</n-card>
|
||||
</div>
|
||||
</n-space>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user