diff --git a/app/http/controllers/plugins/constants.go b/app/http/controllers/plugins/constants.go
index 0a7a321a4c..9b0e215962 100644
--- a/app/http/controllers/plugins/constants.go
+++ b/app/http/controllers/plugins/constants.go
@@ -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"`
+}
diff --git a/app/http/controllers/plugins/rsync_controller.go b/app/http/controllers/plugins/rsync_controller.go
new file mode 100644
index 0000000000..4b8c529231
--- /dev/null
+++ b/app/http/controllers/plugins/rsync_controller.go
@@ -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)
+}
diff --git a/app/http/requests/plugins/rsync/add.go b/app/http/requests/plugins/rsync/add.go
new file mode 100644
index 0000000000..70f3a8d3a8
--- /dev/null
+++ b/app/http/requests/plugins/rsync/add.go
@@ -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
+}
diff --git a/app/http/requests/plugins/rsync/update.go b/app/http/requests/plugins/rsync/update.go
new file mode 100644
index 0000000000..d1d5534655
--- /dev/null
+++ b/app/http/requests/plugins/rsync/update.go
@@ -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
+}
diff --git a/app/plugins/rsync/rsync.go b/app/plugins/rsync/rsync.go
new file mode 100644
index 0000000000..5590b462bc
--- /dev/null
+++ b/app/plugins/rsync/rsync.go
@@ -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`
+)
diff --git a/app/services/plugin.go b/app/services/plugin.go
index 3127342558..15e0fc20e4 100644
--- a/app/services/plugin.go
+++ b/app/services/plugin.go
@@ -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,
diff --git a/docs/docs.go b/docs/docs.go
index 7b815634a8..d88baeb78f 100644
--- a/docs/docs.go
+++ b/docs/docs.go
@@ -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": {
diff --git a/docs/swagger.json b/docs/swagger.json
index 69e7637bc6..0339956b8a 100644
--- a/docs/swagger.json
+++ b/docs/swagger.json
@@ -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": {
diff --git a/docs/swagger.yaml b/docs/swagger.yaml
index ecbb248e16..35ed250f39 100644
--- a/docs/swagger.yaml
+++ b/docs/swagger.yaml
@@ -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
diff --git a/pkg/tools/system.go b/pkg/tools/system.go
index 6b9dcc7a57..b1c008affc 100644
--- a/pkg/tools/system.go
+++ b/pkg/tools/system.go
@@ -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)
diff --git a/pkg/tools/system_test.go b/pkg/tools/system_test.go
index 7295df27b2..bf6dd55f5c 100644
--- a/pkg/tools/system_test.go
+++ b/pkg/tools/system_test.go
@@ -18,11 +18,23 @@ 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)
+ s.Nil(filePath.Close())
+ s.Nil(Remove(filePath.Name()))
+}
+
+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)
@@ -30,27 +42,96 @@ func (s *SystemHelperTestSuite) TestWrite() {
s.Nil(Remove(filePath.Name()))
}
-func (s *SystemHelperTestSuite) TestRead() {
+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)
- data, err := Read(filePath.Name())
+ err = WriteAppend(filePath.Name(), " appended data")
s.Nil(err)
- s.Equal("test data", data)
+
+ content, _ := Read(filePath.Name())
+ s.Equal("initial data appended data", content)
s.Nil(filePath.Close())
s.Nil(Remove(filePath.Name()))
}
-func (s *SystemHelperTestSuite) TestRemove() {
- file, _ := TempFile("testfile")
- file.Close()
+func (s *SystemHelperTestSuite) WriteAppendCreatesFileIfNotExists() {
+ filePath, _ := TempFile("testfile")
+ s.Nil(filePath.Close())
+ s.Nil(Remove(filePath.Name()))
- err := Write(file.Name(), "test data", 0644)
+ err := WriteAppend(filePath.Name(), "test data")
s.Nil(err)
- s.Nil(Remove(file.Name()))
+ 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)
+
+ content, err := Read(filePath.Name())
+ s.Nil(err)
+ s.Equal("test data", content)
+
+ s.Nil(filePath.Close())
+ s.Nil(Remove(filePath.Name()))
+}
+
+func (s *SystemHelperTestSuite) ReadReturnsErrorForNonExistentFile() {
+ _, err := Read("/nonexistent/testfile")
+ s.NotNil(err)
+}
+
+func (s *SystemHelperTestSuite) RemoveSuccessfullyRemovesFile() {
+ filePath, _ := TempFile("testfile")
+
+ err := Write(filePath.Name(), "test data", 0644)
+ s.Nil(err)
+
+ 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() {
diff --git a/routes/plugin.go b/routes/plugin.go
index 79a0344ae8..2c88b543d2 100644
--- a/routes/plugin.go
+++ b/routes/plugin.go
@@ -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)
diff --git a/scripts/rsync/install.sh b/scripts/rsync/install.sh
new file mode 100644
index 0000000000..2369d7a118
--- /dev/null
+++ b/scripts/rsync/install.sh
@@ -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 .
+'
+
+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
diff --git a/scripts/rsync/uninstall.sh b/scripts/rsync/uninstall.sh
new file mode 100644
index 0000000000..82dad01211
--- /dev/null
+++ b/scripts/rsync/uninstall.sh
@@ -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 .
+'
+
+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
diff --git a/scripts/rsync/update.sh b/scripts/rsync/update.sh
new file mode 100644
index 0000000000..b1ffe37dff
--- /dev/null
+++ b/scripts/rsync/update.sh
@@ -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 .
+'
+
+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