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