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

feat: Rsync 插件

This commit is contained in:
耗子
2023-11-23 02:56:20 +08:00
parent 94ff9e2dbc
commit d55bb5ffe1
15 changed files with 1539 additions and 97 deletions

View File

@@ -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"`
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -0,0 +1,42 @@
package requests
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/validation"
)
type Update 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 *Update) Authorize(ctx http.Context) error {
return nil
}
func (r *Update) 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 *Update) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Update) Attributes(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Update) PrepareForValidation(ctx http.Context, data validation.Data) error {
return nil
}

View File

@@ -0,0 +1,13 @@
package rsync
var (
Name = "Rsync"
Description = "Rsync 是一款提供快速增量文件传输的开源工具。"
Slug = "rsync"
Version = "3.2.7"
Requires = []string{}
Excludes = []string{}
Install = `bash /www/panel/scripts/rsync/install.sh`
Uninstall = `bash /www/panel/scripts/rsync/uninstall.sh`
Update = `bash /www/panel/scripts/rsync/install.sh`
)

View File

@@ -18,6 +18,7 @@ import (
"panel/app/plugins/postgresql16"
"panel/app/plugins/pureftpd"
"panel/app/plugins/redis"
"panel/app/plugins/rsync"
"panel/app/plugins/s3fs"
"panel/app/plugins/supervisor"
"panel/app/plugins/toolbox"
@@ -229,6 +230,17 @@ func (r *PluginImpl) All() []PanelPlugin {
Uninstall: fail2ban.Uninstall,
Update: fail2ban.Update,
})
p = append(p, PanelPlugin{
Name: rsync.Name,
Description: rsync.Description,
Slug: rsync.Slug,
Version: rsync.Version,
Requires: rsync.Requires,
Excludes: rsync.Excludes,
Install: rsync.Install,
Uninstall: rsync.Uninstall,
Update: rsync.Update,
})
p = append(p, PanelPlugin{
Name: toolbox.Name,
Description: toolbox.Description,

View File

@@ -920,7 +920,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/requests.Update"
"$ref": "#/definitions/panel_app_http_requests_setting.Update"
}
}
],
@@ -1295,7 +1295,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/requests.Add"
"$ref": "#/definitions/panel_app_http_requests_website.Add"
}
}
],
@@ -1661,6 +1661,242 @@ const docTemplate = `{
}
}
},
"/plugins/rsync/modules": {
"get": {
"security": [
{
"BearerToken": []
}
],
"description": "列出所有 Rsync 模块",
"produces": [
"application/json"
],
"tags": [
"插件-Rsync"
],
"summary": "列出模块",
"parameters": [
{
"description": "request",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/commonrequests.Paginate"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controllers.SuccessResponse"
}
}
}
},
"put": {
"security": [
{
"BearerToken": []
}
],
"description": "更新 Rsync 模块",
"produces": [
"application/json"
],
"tags": [
"插件-Rsync"
],
"summary": "更新模块",
"parameters": [
{
"description": "request",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/panel_app_http_requests_plugins_rsync.Update"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controllers.SuccessResponse"
}
}
}
},
"post": {
"security": [
{
"BearerToken": []
}
],
"description": "添加 Rsync 模块",
"produces": [
"application/json"
],
"tags": [
"插件-Rsync"
],
"summary": "添加模块",
"parameters": [
{
"description": "request",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/panel_app_http_requests_plugins_rsync.Add"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controllers.SuccessResponse"
}
}
}
},
"delete": {
"security": [
{
"BearerToken": []
}
],
"description": "删除 Rsync 模块",
"produces": [
"application/json"
],
"tags": [
"插件-Rsync"
],
"summary": "删除模块",
"parameters": [
{
"type": "string",
"description": "模块名称",
"name": "name",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controllers.SuccessResponse"
}
}
}
}
},
"/plugins/rsync/restart": {
"post": {
"security": [
{
"BearerToken": []
}
],
"description": "重启 Rsync 服务",
"produces": [
"application/json"
],
"tags": [
"插件-Rsync"
],
"summary": "重启服务",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controllers.SuccessResponse"
}
}
}
}
},
"/plugins/rsync/start": {
"post": {
"security": [
{
"BearerToken": []
}
],
"description": "启动 Rsync 服务",
"produces": [
"application/json"
],
"tags": [
"插件-Rsync"
],
"summary": "启动服务",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controllers.SuccessResponse"
}
}
}
}
},
"/plugins/rsync/status": {
"get": {
"security": [
{
"BearerToken": []
}
],
"description": "获取 Rsync 服务状态",
"produces": [
"application/json"
],
"tags": [
"插件-Rsync"
],
"summary": "服务状态",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controllers.SuccessResponse"
}
}
}
}
},
"/plugins/rsync/stop": {
"post": {
"security": [
{
"BearerToken": []
}
],
"description": "停止 Rsync 服务",
"produces": [
"application/json"
],
"tags": [
"插件-Rsync"
],
"summary": "停止服务",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controllers.SuccessResponse"
}
}
}
}
},
"/swagger": {
"get": {
"description": "Swagger UI",
@@ -1905,7 +2141,82 @@ const docTemplate = `{
}
}
},
"requests.Add": {
"panel_app_http_requests_plugins_rsync.Add": {
"type": "object",
"properties": {
"auth_user": {
"type": "string"
},
"comment": {
"type": "string"
},
"hosts_allow": {
"type": "string"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
},
"secret": {
"type": "string"
}
}
},
"panel_app_http_requests_plugins_rsync.Update": {
"type": "object",
"properties": {
"auth_user": {
"type": "string"
},
"comment": {
"type": "string"
},
"hosts_allow": {
"type": "string"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
},
"secret": {
"type": "string"
}
}
},
"panel_app_http_requests_setting.Update": {
"type": "object",
"properties": {
"backup_path": {
"type": "string"
},
"email": {
"type": "string"
},
"entrance": {
"type": "string"
},
"name": {
"type": "string"
},
"password": {
"type": "string"
},
"port": {
"type": "integer"
},
"username": {
"type": "string"
},
"website_path": {
"type": "string"
}
}
},
"panel_app_http_requests_website.Add": {
"type": "object",
"properties": {
"db": {
@@ -2135,35 +2446,6 @@ const docTemplate = `{
}
}
},
"requests.Update": {
"type": "object",
"properties": {
"backup_path": {
"type": "string"
},
"email": {
"type": "string"
},
"entrance": {
"type": "string"
},
"name": {
"type": "string"
},
"password": {
"type": "string"
},
"port": {
"type": "integer"
},
"username": {
"type": "string"
},
"website_path": {
"type": "string"
}
}
},
"requests.UserStore": {
"type": "object",
"properties": {

View File

@@ -913,7 +913,7 @@
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/requests.Update"
"$ref": "#/definitions/panel_app_http_requests_setting.Update"
}
}
],
@@ -1288,7 +1288,7 @@
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/requests.Add"
"$ref": "#/definitions/panel_app_http_requests_website.Add"
}
}
],
@@ -1654,6 +1654,242 @@
}
}
},
"/plugins/rsync/modules": {
"get": {
"security": [
{
"BearerToken": []
}
],
"description": "列出所有 Rsync 模块",
"produces": [
"application/json"
],
"tags": [
"插件-Rsync"
],
"summary": "列出模块",
"parameters": [
{
"description": "request",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/commonrequests.Paginate"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controllers.SuccessResponse"
}
}
}
},
"put": {
"security": [
{
"BearerToken": []
}
],
"description": "更新 Rsync 模块",
"produces": [
"application/json"
],
"tags": [
"插件-Rsync"
],
"summary": "更新模块",
"parameters": [
{
"description": "request",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/panel_app_http_requests_plugins_rsync.Update"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controllers.SuccessResponse"
}
}
}
},
"post": {
"security": [
{
"BearerToken": []
}
],
"description": "添加 Rsync 模块",
"produces": [
"application/json"
],
"tags": [
"插件-Rsync"
],
"summary": "添加模块",
"parameters": [
{
"description": "request",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/panel_app_http_requests_plugins_rsync.Add"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controllers.SuccessResponse"
}
}
}
},
"delete": {
"security": [
{
"BearerToken": []
}
],
"description": "删除 Rsync 模块",
"produces": [
"application/json"
],
"tags": [
"插件-Rsync"
],
"summary": "删除模块",
"parameters": [
{
"type": "string",
"description": "模块名称",
"name": "name",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controllers.SuccessResponse"
}
}
}
}
},
"/plugins/rsync/restart": {
"post": {
"security": [
{
"BearerToken": []
}
],
"description": "重启 Rsync 服务",
"produces": [
"application/json"
],
"tags": [
"插件-Rsync"
],
"summary": "重启服务",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controllers.SuccessResponse"
}
}
}
}
},
"/plugins/rsync/start": {
"post": {
"security": [
{
"BearerToken": []
}
],
"description": "启动 Rsync 服务",
"produces": [
"application/json"
],
"tags": [
"插件-Rsync"
],
"summary": "启动服务",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controllers.SuccessResponse"
}
}
}
}
},
"/plugins/rsync/status": {
"get": {
"security": [
{
"BearerToken": []
}
],
"description": "获取 Rsync 服务状态",
"produces": [
"application/json"
],
"tags": [
"插件-Rsync"
],
"summary": "服务状态",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controllers.SuccessResponse"
}
}
}
}
},
"/plugins/rsync/stop": {
"post": {
"security": [
{
"BearerToken": []
}
],
"description": "停止 Rsync 服务",
"produces": [
"application/json"
],
"tags": [
"插件-Rsync"
],
"summary": "停止服务",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/controllers.SuccessResponse"
}
}
}
}
},
"/swagger": {
"get": {
"description": "Swagger UI",
@@ -1898,7 +2134,82 @@
}
}
},
"requests.Add": {
"panel_app_http_requests_plugins_rsync.Add": {
"type": "object",
"properties": {
"auth_user": {
"type": "string"
},
"comment": {
"type": "string"
},
"hosts_allow": {
"type": "string"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
},
"secret": {
"type": "string"
}
}
},
"panel_app_http_requests_plugins_rsync.Update": {
"type": "object",
"properties": {
"auth_user": {
"type": "string"
},
"comment": {
"type": "string"
},
"hosts_allow": {
"type": "string"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
},
"secret": {
"type": "string"
}
}
},
"panel_app_http_requests_setting.Update": {
"type": "object",
"properties": {
"backup_path": {
"type": "string"
},
"email": {
"type": "string"
},
"entrance": {
"type": "string"
},
"name": {
"type": "string"
},
"password": {
"type": "string"
},
"port": {
"type": "integer"
},
"username": {
"type": "string"
},
"website_path": {
"type": "string"
}
}
},
"panel_app_http_requests_website.Add": {
"type": "object",
"properties": {
"db": {
@@ -2128,35 +2439,6 @@
}
}
},
"requests.Update": {
"type": "object",
"properties": {
"backup_path": {
"type": "string"
},
"email": {
"type": "string"
},
"entrance": {
"type": "string"
},
"name": {
"type": "string"
},
"password": {
"type": "string"
},
"port": {
"type": "integer"
},
"username": {
"type": "string"
},
"website_path": {
"type": "string"
}
}
},
"requests.UserStore": {
"type": "object",
"properties": {

View File

@@ -151,7 +151,56 @@ definitions:
updated_at:
type: string
type: object
requests.Add:
panel_app_http_requests_plugins_rsync.Add:
properties:
auth_user:
type: string
comment:
type: string
hosts_allow:
type: string
name:
type: string
path:
type: string
secret:
type: string
type: object
panel_app_http_requests_plugins_rsync.Update:
properties:
auth_user:
type: string
comment:
type: string
hosts_allow:
type: string
name:
type: string
path:
type: string
secret:
type: string
type: object
panel_app_http_requests_setting.Update:
properties:
backup_path:
type: string
email:
type: string
entrance:
type: string
name:
type: string
password:
type: string
port:
type: integer
username:
type: string
website_path:
type: string
type: object
panel_app_http_requests_website.Add:
properties:
db:
type: boolean
@@ -301,25 +350,6 @@ definitions:
waf_mode:
type: string
type: object
requests.Update:
properties:
backup_path:
type: string
email:
type: string
entrance:
type: string
name:
type: string
password:
type: string
port:
type: integer
username:
type: string
website_path:
type: string
type: object
requests.UserStore:
properties:
ca:
@@ -990,7 +1020,7 @@ paths:
name: data
required: true
schema:
$ref: '#/definitions/requests.Update'
$ref: '#/definitions/panel_app_http_requests_setting.Update'
produces:
- application/json
responses:
@@ -1213,7 +1243,7 @@ paths:
name: data
required: true
schema:
$ref: '#/definitions/requests.Add'
$ref: '#/definitions/panel_app_http_requests_website.Add'
produces:
- application/json
responses:
@@ -1443,6 +1473,150 @@ paths:
summary: 更新备注
tags:
- 网站管理
/plugins/rsync/modules:
delete:
description: 删除 Rsync 模块
parameters:
- description: 模块名称
in: query
name: name
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/controllers.SuccessResponse'
security:
- BearerToken: []
summary: 删除模块
tags:
- 插件-Rsync
get:
description: 列出所有 Rsync 模块
parameters:
- description: request
in: body
name: data
required: true
schema:
$ref: '#/definitions/commonrequests.Paginate'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/controllers.SuccessResponse'
security:
- BearerToken: []
summary: 列出模块
tags:
- 插件-Rsync
post:
description: 添加 Rsync 模块
parameters:
- description: request
in: body
name: data
required: true
schema:
$ref: '#/definitions/panel_app_http_requests_plugins_rsync.Add'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/controllers.SuccessResponse'
security:
- BearerToken: []
summary: 添加模块
tags:
- 插件-Rsync
put:
description: 更新 Rsync 模块
parameters:
- description: request
in: body
name: data
required: true
schema:
$ref: '#/definitions/panel_app_http_requests_plugins_rsync.Update'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/controllers.SuccessResponse'
security:
- BearerToken: []
summary: 更新模块
tags:
- 插件-Rsync
/plugins/rsync/restart:
post:
description: 重启 Rsync 服务
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/controllers.SuccessResponse'
security:
- BearerToken: []
summary: 重启服务
tags:
- 插件-Rsync
/plugins/rsync/start:
post:
description: 启动 Rsync 服务
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/controllers.SuccessResponse'
security:
- BearerToken: []
summary: 启动服务
tags:
- 插件-Rsync
/plugins/rsync/status:
get:
description: 获取 Rsync 服务状态
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/controllers.SuccessResponse'
security:
- BearerToken: []
summary: 服务状态
tags:
- 插件-Rsync
/plugins/rsync/stop:
post:
description: 停止 Rsync 服务
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/controllers.SuccessResponse'
security:
- BearerToken: []
summary: 停止服务
tags:
- 插件-Rsync
/swagger:
get:
description: Swagger UI

View File

@@ -29,6 +29,22 @@ func Write(path string, data string, permission os.FileMode) error {
return nil
}
// WriteAppend 追加写入文件
func WriteAppend(path string, data string) error {
file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer file.Close()
_, err = file.WriteString(data)
if err != nil {
return err
}
return nil
}
// Read 读取文件
func Read(path string) (string, error) {
data, err := os.ReadFile(path)

View File

@@ -18,11 +18,11 @@ func TestSystemHelperTestSuite(t *testing.T) {
suite.Run(t, &SystemHelperTestSuite{})
}
func (s *SystemHelperTestSuite) TestWrite() {
func (s *SystemHelperTestSuite) WriteCreatesFileWithCorrectContent() {
filePath, _ := TempFile("testfile")
s.Nil(Write(filePath.Name(), "test data", 0644))
s.FileExists(filePath.Name())
err := Write(filePath.Name(), "test data", 0644)
s.Nil(err)
content, _ := Read(filePath.Name())
s.Equal("test data", content)
@@ -30,27 +30,108 @@ func (s *SystemHelperTestSuite) TestWrite() {
s.Nil(Remove(filePath.Name()))
}
func (s *SystemHelperTestSuite) TestRead() {
func (s *SystemHelperTestSuite) WriteCreatesDirectoriesIfNeeded() {
filePath, _ := TempFile("testdir/testfile")
err := Write(filePath.Name(), "test data", 0644)
s.Nil(err)
content, _ := Read(filePath.Name())
s.Equal("test data", content)
s.Nil(filePath.Close())
s.Nil(Remove(filePath.Name()))
}
func (s *SystemHelperTestSuite) WriteFailsIfDirectoryCannotBeCreated() {
filePath := "/nonexistent/testfile"
err := Write(filePath, "test data", 0644)
s.NotNil(err)
}
func (s *SystemHelperTestSuite) WriteFailsIfFileCannotBeWritten() {
filePath, _ := TempFile("testfile")
s.Nil(filePath.Close())
s.Nil(Chmod(filePath.Name(), 0400))
err := Write(filePath.Name(), "test data", 0644)
s.NotNil(err)
s.Nil(Chmod(filePath.Name(), 0644))
s.Nil(Remove(filePath.Name()))
}
func (s *SystemHelperTestSuite) WriteAppendSuccessfullyAppendsDataToFile() {
filePath, _ := TempFile("testfile")
err := Write(filePath.Name(), "initial data", 0644)
s.Nil(err)
err = WriteAppend(filePath.Name(), " appended data")
s.Nil(err)
content, _ := Read(filePath.Name())
s.Equal("initial data appended data", content)
s.Nil(filePath.Close())
s.Nil(Remove(filePath.Name()))
}
func (s *SystemHelperTestSuite) WriteAppendCreatesFileIfNotExists() {
filePath, _ := TempFile("testfile")
s.Nil(filePath.Close())
s.Nil(Remove(filePath.Name()))
err := WriteAppend(filePath.Name(), "test data")
s.Nil(err)
content, _ := Read(filePath.Name())
s.Equal("test data", content)
s.Nil(Remove(filePath.Name()))
}
func (s *SystemHelperTestSuite) WriteAppendReturnsErrorIfPathIsADirectory() {
dirPath, _ := TempDir("testdir")
err := WriteAppend(dirPath, "test data")
s.NotNil(err)
s.Nil(Remove(dirPath))
}
func (s *SystemHelperTestSuite) ReadSuccessfullyReadsFileContent() {
filePath, _ := TempFile("testfile")
err := Write(filePath.Name(), "test data", 0644)
s.Nil(err)
data, err := Read(filePath.Name())
content, err := Read(filePath.Name())
s.Nil(err)
s.Equal("test data", data)
s.Equal("test data", content)
s.Nil(filePath.Close())
s.Nil(Remove(filePath.Name()))
}
func (s *SystemHelperTestSuite) TestRemove() {
file, _ := TempFile("testfile")
file.Close()
func (s *SystemHelperTestSuite) ReadReturnsErrorForNonExistentFile() {
_, err := Read("/nonexistent/testfile")
s.NotNil(err)
}
err := Write(file.Name(), "test data", 0644)
func (s *SystemHelperTestSuite) RemoveSuccessfullyRemovesFile() {
filePath, _ := TempFile("testfile")
err := Write(filePath.Name(), "test data", 0644)
s.Nil(err)
s.Nil(Remove(file.Name()))
err = Remove(filePath.Name())
s.Nil(err)
s.False(Exists(filePath.Name()))
}
func (s *SystemHelperTestSuite) RemoveReturnsErrorForNonExistentFile() {
err := Remove("/nonexistent/testfile")
s.NotNil(err)
}
func (s *SystemHelperTestSuite) TestExec() {

View File

@@ -282,6 +282,16 @@ func Plugin() {
route.Post("whiteList", fail2banController.SetWhiteList)
route.Get("whiteList", fail2banController.GetWhiteList)
})
r.Prefix("rsync").Group(func(route route.Router) {
rsyncController := plugins.NewRsyncController()
route.Get("status", rsyncController.Status)
route.Post("start", rsyncController.Start)
route.Post("stop", rsyncController.Stop)
route.Post("restart", rsyncController.Restart)
route.Get("modules", rsyncController.List)
route.Post("modules", rsyncController.Add)
route.Delete("modules", rsyncController.Delete)
})
r.Prefix("toolbox").Group(func(route route.Router) {
toolboxController := plugins.NewToolBoxController()
route.Get("dns", toolboxController.GetDNS)

79
scripts/rsync/install.sh Normal file
View File

@@ -0,0 +1,79 @@
#!/bin/bash
export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
: '
Copyright (C) 2022 - now HaoZi Technology Co., Ltd.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
'
HR="+----------------------------------------------------"
OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown")
if [ "${OS}" == "centos" ]; then
dnf install -y rsync
elif [ "${OS}" == "debian" ]; then
apt-get install -y rsync
else
echo -e $HR
echo "错误:不支持的操作系统"
exit 1
fi
# 写入配置
cat > /etc/rsyncd.conf << EOF
uid = root
gid = root
port = 873
use chroot = no
read only = no
dont compress = *.jpg *.jpeg *.png *.gif *.webp *.avif *.mp4 *.avi *.mov *.mkv *.mp3 *.wav *.aac *.flac *.zip *.rar *.7z *.gz *.tgz *.tar *.pdf *.epub *.iso *.exe *.apk *.dmg *.rpm *.deb *.msi
hosts allow = 127.0.0.1/32 ::1/128
# hosts deny =
max connections = 100
timeout = 1800
lock file = /var/run/rsync.lock
pid file = /var/run/rsyncd.pid
log file = /var/log/rsyncd.log
EOF
touch /etc/rsyncd.secrets
chmod 600 /etc/rsyncd.conf
chmod 600 /etc/rsyncd.secrets
# 写入服务文件
cat > /etc/systemd/system/rsyncd.service << EOF
[Unit]
Description=fast remote file copy program daemon
After=network-online.target remote-fs.target nss-lookup.target
Wants=network-online.target
ConditionPathExists=/etc/rsyncd.conf
[Service]
ExecStart=/usr/bin/rsync --daemon --no-detach "\$OPTIONS"
ExecReload=/bin/kill -HUP \$MAINPID
KillMode=process
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable rsyncd.service
systemctl restart rsyncd.service
panel writePlugin rsync 3.2.7

View File

@@ -0,0 +1,34 @@
#!/bin/bash
export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
: '
Copyright (C) 2022 - now HaoZi Technology Co., Ltd.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
'
HR="+----------------------------------------------------"
OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown")
if [ "${OS}" == "centos" ]; then
dnf remove -y rsync
elif [ "${OS}" == "debian" ]; then
apt-get purge -y rsync
else
echo -e $HR
echo "错误:不支持的操作系统"
exit 1
fi
panel deletePlugin rsync

34
scripts/rsync/update.sh Normal file
View File

@@ -0,0 +1,34 @@
#!/bin/bash
export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
: '
Copyright (C) 2022 - now HaoZi Technology Co., Ltd.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
'
HR="+----------------------------------------------------"
OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown")
if [ "${OS}" == "centos" ]; then
dnf update -y rsync
elif [ "${OS}" == "debian" ]; then
apt-get install --only-upgrade -y rsync
else
echo -e $HR
echo "错误:不支持的操作系统"
exit 1
fi
panel writePlugin rsync 3.2.7