Skip to content

Commit

Permalink
feat: Rsync 插件
Browse files Browse the repository at this point in the history
  • Loading branch information
devhaozi committed Nov 22, 2023
1 parent 94ff9e2 commit d55bb5f
Show file tree
Hide file tree
Showing 15 changed files with 1,539 additions and 97 deletions.
10 changes: 10 additions & 0 deletions app/http/controllers/plugins/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,13 @@ type S3fsMount struct {
Bucket string `json:"bucket"`
Url string `json:"url"`
}

type RsyncModule struct {
Name string `json:"name"`
Path string `json:"path"`
Comment string `json:"comment"`
ReadOnly bool `json:"read_only"`
AuthUser string `json:"auth_user"`
Secret string `json:"secret"`
HostsAllow string `json:"hosts_allow"`
}
331 changes: 331 additions & 0 deletions app/http/controllers/plugins/rsync_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,331 @@
package plugins

import (
"strings"

"github.com/goravel/framework/contracts/http"

"panel/app/http/controllers"
commonrequests "panel/app/http/requests/common"
requests "panel/app/http/requests/plugins/rsync"
"panel/app/services"
"panel/pkg/tools"
)

type RsyncController struct {
setting services.Setting
}

func NewRsyncController() *RsyncController {
return &RsyncController{
setting: services.NewSettingImpl(),
}
}

// Status
//
// @Summary 服务状态
// @Description 获取 Rsync 服务状态
// @Tags 插件-Rsync
// @Produce json
// @Security BearerToken
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/rsync/status [get]
func (r *RsyncController) Status(ctx http.Context) http.Response {
status, err := tools.ServiceStatus("rsyncd")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取服务运行状态失败")
}

return controllers.Success(ctx, status)
}

// Restart
//
// @Summary 重启服务
// @Description 重启 Rsync 服务
// @Tags 插件-Rsync
// @Produce json
// @Security BearerToken
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/rsync/restart [post]
func (r *RsyncController) Restart(ctx http.Context) http.Response {
if err := tools.ServiceRestart("rsyncd"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重启服务失败")
}

return controllers.Success(ctx, nil)
}

// Start
//
// @Summary 启动服务
// @Description 启动 Rsync 服务
// @Tags 插件-Rsync
// @Produce json
// @Security BearerToken
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/rsync/start [post]
func (r *RsyncController) Start(ctx http.Context) http.Response {
if err := tools.ServiceStart("rsyncd"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "启动服务失败")
}

return controllers.Success(ctx, nil)
}

// Stop
//
// @Summary 停止服务
// @Description 停止 Rsync 服务
// @Tags 插件-Rsync
// @Produce json
// @Security BearerToken
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/rsync/stop [post]
func (r *RsyncController) Stop(ctx http.Context) http.Response {
if err := tools.ServiceStop("rsyncd"); err != nil {
return nil
}
status, err := tools.ServiceStatus("rsyncd")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取服务运行状态失败")
}

return controllers.Success(ctx, !status)
}

// List
//
// @Summary 列出模块
// @Description 列出所有 Rsync 模块
// @Tags 插件-Rsync
// @Produce json
// @Security BearerToken
// @Param data body commonrequests.Paginate true "request"
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/rsync/modules [get]
func (r *RsyncController) List(ctx http.Context) http.Response {
var paginateRequest commonrequests.Paginate
sanitize := controllers.Sanitize(ctx, &paginateRequest)
if sanitize != nil {
return sanitize
}

config, err := tools.Read("/etc/rsyncd.conf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}

var modules []RsyncModule
lines := strings.Split(config, "\n")
var currentModule *RsyncModule

for _, line := range lines {
line = strings.TrimSpace(line)

if line == "" || strings.HasPrefix(line, "#") {
continue
}

if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
if currentModule != nil {
modules = append(modules, *currentModule)
}
moduleName := line[1 : len(line)-1]
secret, err := tools.Exec("grep -E '^" + moduleName + ":.*$' /etc/rsyncd.secrets | awk '{print $2}'")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取模块"+moduleName+"的密钥失败")
}
currentModule = &RsyncModule{
Name: moduleName,
Secret: secret,
}
} else if currentModule != nil {
parts := strings.SplitN(line, "=", 2)
if len(parts) == 2 {
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])

switch key {
case "path":
currentModule.Path = value
case "comment":
currentModule.Comment = value
case "read only":
currentModule.ReadOnly = value == "yes" || value == "true"
case "auth users":
currentModule.AuthUser = value
case "hosts allow":
currentModule.HostsAllow = value
}
}
}
}

if currentModule != nil {
modules = append(modules, *currentModule)
}

startIndex := (paginateRequest.Page - 1) * paginateRequest.Limit
endIndex := paginateRequest.Page * paginateRequest.Limit
if startIndex > len(modules) {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []RsyncModule{},
})
}
if endIndex > len(modules) {
endIndex = len(modules)
}
pagedModules := modules[startIndex:endIndex]
if pagedModules == nil {
pagedModules = []RsyncModule{}
}

return controllers.Success(ctx, http.Json{
"total": len(modules),
"items": pagedModules,
})
}

// Add
//
// @Summary 添加模块
// @Description 添加 Rsync 模块
// @Tags 插件-Rsync
// @Produce json
// @Security BearerToken
// @Param data body requests.Add true "request"
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/rsync/modules [post]
func (r *RsyncController) Add(ctx http.Context) http.Response {
var addRequest requests.Add
sanitize := controllers.Sanitize(ctx, &addRequest)
if sanitize != nil {
return sanitize
}

conf := `
# ` + addRequest.Name + `-START
[` + addRequest.Name + `]
path = ` + addRequest.Path + `
comment = ` + addRequest.Comment + `
read only = no
auth users = ` + addRequest.AuthUser + `
hosts allow = ` + addRequest.HostsAllow + `
secrets file = /etc/rsyncd.secrets
# ` + addRequest.Name + `-END
`

if err := tools.WriteAppend("/etc/rsyncd.conf", conf); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if err := tools.WriteAppend("/etc/rsyncd.secrets", addRequest.Name+":"+addRequest.Secret); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}

if err := tools.ServiceRestart("rsyncd"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}

return controllers.Success(ctx, nil)
}

// Delete
//
// @Summary 删除模块
// @Description 删除 Rsync 模块
// @Tags 插件-Rsync
// @Produce json
// @Security BearerToken
// @Param name query string true "模块名称"
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/rsync/modules [delete]
func (r *RsyncController) Delete(ctx http.Context) http.Response {
name := ctx.Request().Input("name")
if len(name) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "name 不能为空")
}

config, err := tools.Read("/etc/rsyncd.conf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if !strings.Contains(config, "["+name+"]") {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "模块 "+name+" 不存在")
}

module := tools.Cut(config, "# "+name+"-START", "# "+name+"-END")
config = strings.Replace(config, "\n# "+name+"-START"+module+"# "+name+"-END", "", -1)

if err = tools.Write("/etc/rsyncd.conf", config, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if out, err := tools.Exec("sed -i '/^" + name + ":.*$/d' /etc/rsyncd.secrets"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}

if err = tools.ServiceRestart("rsyncd"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}

return controllers.Success(ctx, nil)
}

// Update
//
// @Summary 更新模块
// @Description 更新 Rsync 模块
// @Tags 插件-Rsync
// @Produce json
// @Security BearerToken
// @Param data body requests.Update true "request"
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/rsync/modules [put]
func (r *RsyncController) Update(ctx http.Context) http.Response {
var updateRequest requests.Update
sanitize := controllers.Sanitize(ctx, &updateRequest)
if sanitize != nil {
return sanitize
}

config, err := tools.Read("/etc/rsyncd.conf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}

if !strings.Contains(config, "["+updateRequest.Name+"]") {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "模块 "+updateRequest.Name+" 不存在")
}

newConf := `# ` + updateRequest.Name + `-START
[` + updateRequest.Name + `]
path = ` + updateRequest.Path + `
comment = ` + updateRequest.Comment + `
read only = no
auth users = ` + updateRequest.AuthUser + `
hosts allow = ` + updateRequest.HostsAllow + `
secrets file = /etc/rsyncd.secrets
# ` + updateRequest.Name + `-END
`

module := tools.Cut(config, "# "+updateRequest.Name+"-START", "# "+updateRequest.Name+"-END")
config = strings.Replace(config, "# "+updateRequest.Name+"-START"+module+"# "+updateRequest.Name+"-END", newConf, -1)

if err = tools.Write("/etc/rsyncd.conf", config, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if out, err := tools.Exec("sed -i '/^" + updateRequest.Name + ":.*$/d' /etc/rsyncd.secrets"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if err := tools.WriteAppend("/etc/rsyncd.secrets", updateRequest.Name+":"+updateRequest.Secret); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}

if err = tools.ServiceRestart("rsyncd"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}

return controllers.Success(ctx, nil)
}
42 changes: 42 additions & 0 deletions app/http/requests/plugins/rsync/add.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package requests

import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/validation"
)

type Add struct {
Name string `form:"name" json:"name"`
Path string `form:"path" json:"path"`
Comment string `form:"comment" json:"comment"`
AuthUser string `form:"auth_user" json:"auth_user"`
Secret string `form:"secret" json:"secret"`
HostsAllow string `form:"hosts_allow" json:"hosts_allow"`
}

func (r *Add) Authorize(ctx http.Context) error {
return nil
}

func (r *Add) Rules(ctx http.Context) map[string]string {
return map[string]string{
"name": "required|regex:^[a-zA-Z0-9-_]+$",
"path": "regex:^/[a-zA-Z0-9_-]+(\\/[a-zA-Z0-9_-]+)*$",
"comment": "string",
"auth_user": "required|regex:^[a-zA-Z0-9-_]+$",
"secret": "required|min_len:8",
"hosts_allow": "string",
}
}

func (r *Add) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}

func (r *Add) Attributes(ctx http.Context) map[string]string {
return map[string]string{}
}

func (r *Add) PrepareForValidation(ctx http.Context, data validation.Data) error {
return nil
}
Loading

0 comments on commit d55bb5f

Please sign in to comment.