diff --git a/app/admin/apis/devops/explorer.go b/app/admin/apis/devops/explorer.go new file mode 100644 index 0000000000000000000000000000000000000000..24a95710ddede9c7bd389d045bfba29bb57a27ad --- /dev/null +++ b/app/admin/apis/devops/explorer.go @@ -0,0 +1,170 @@ +package devops + +import ( + "errors" + "github.com/MarchGe/go-admin-server/app/admin/service/dvservice" + "github.com/MarchGe/go-admin-server/app/admin/service/dvservice/dto/res" + "github.com/MarchGe/go-admin-server/app/common/E" + "github.com/MarchGe/go-admin-server/app/common/R" + ginUtils "github.com/MarchGe/go-admin-server/app/common/utils/gin_utils" + "github.com/gin-gonic/gin" + "net/http" + "os" + "path" + "sort" + "strings" +) + +var _explorerApi = &ExplorerApi{ + explorerService: dvservice.GetExplorerService(), +} + +type ExplorerApi struct { + explorerService *dvservice.ExplorerService +} + +func GetExplorerApi() *ExplorerApi { + return _explorerApi +} + +// GetEntries godoc +// +// @Summary 查询entry列表 +// @Tags 资源管理器 +// @Produce application/json +// @Param dir query string true "目录路径" +// @Param keyword query string false "按照名称模糊搜索" +// @Success 200 {object} R.Result{value=[]res.ExplorerEntry} +// @Router /devops/explorer/entries [get] +func (a *ExplorerApi) GetEntries(c *gin.Context) { + dir := ginUtils.GetStringQuery(c, "dir", "") + if dir == "" { + R.Fail(c, "目录路径不能为空", http.StatusBadRequest) + return + } + keyword := ginUtils.GetStringQuery(c, "keyword", "") + entries, err := a.explorerService.ListEntries(dir, keyword) + if err != nil { + E.PanicErr(err) + } + a.sortEntries(entries) + R.Success(c, entries) +} + +// DeleteEntry godoc +// +// @Summary 删除文件或文件夹 +// @Tags 资源管理器 +// @Produce application/json +// @Param path query string true "文件或文件夹的路径" +// @Success 200 {object} R.Result +// @Router /devops/explorer/entry [delete] +func (a *ExplorerApi) DeleteEntry(c *gin.Context) { + deletePath := ginUtils.GetStringQuery(c, "path", "") + if deletePath == "" { + R.Fail(c, "操作的资源路径不能为空", http.StatusBadRequest) + return + } + if err := os.RemoveAll(deletePath); err != nil { + if errors.Is(err, os.ErrPermission) { + R.Fail(c, "文件系统:permission denied", http.StatusBadRequest) + return + } + E.PanicErr(err) + } + R.Success(c, nil) +} + +// Upload godoc +// +// @Summary 上传文件 +// @Tags 资源管理器 +// @Accept multipart/form-data +// @Produce application/json +// @Param dir formData string true "文件目录" +// @Param file formData file true "文件信息" +// @Success 200 {object} R.Result +// @Router /devops/explorer/upload [post] +func (a *ExplorerApi) Upload(c *gin.Context) { + dir := c.PostForm("dir") + if dir == "" { + R.Fail(c, "目录参数不能为空", http.StatusBadRequest) + return + } + file, err := c.FormFile("file") + if err != nil { + E.PanicErr(err) + } + filePath := path.Clean(dir) + "/" + file.Filename + if err = c.SaveUploadedFile(file, filePath); err != nil { + if errors.Is(err, os.ErrPermission) { + R.Fail(c, "文件系统:permission denied", http.StatusBadRequest) + return + } + E.PanicErr(err) + } + R.Success(c, nil) +} + +// Download godoc +// +// @Summary 下载文件 +// @Tags 资源管理器 +// @Accept multipart/form-data +// @Produce application/json +// @Param path query string true "文件完整路径" +// @Success 200 {object} R.Result +// @Router /devops/explorer/download [get] +func (a *ExplorerApi) Download(c *gin.Context) { + filePath := c.Query("path") + if filePath == "" { + R.Fail(c, "文件路径不能为空", http.StatusBadRequest) + return + } + info, err := os.Stat(filePath) + if err != nil { + R.Fail(c, "获取文件信息失败", http.StatusBadRequest) + return + } + if info.IsDir() { + R.Fail(c, "不支持下载文件夹", http.StatusBadRequest) + return + } + parts := strings.Split(filePath, "/") + fileName := parts[len(parts)-1] + c.Writer.Header().Add("Content-Type", "application/octet-stream") + c.Writer.Header().Set("Content-Disposition", "attachment; filename="+fileName) + c.File(filePath) +} + +// sortEntries 排序规则:文件夹在前,然后按字母自然顺序排序(忽略大小写) +func (a *ExplorerApi) sortEntries(entries []*res.ExplorerEntry) { + length := len(entries) + sort.Slice(entries, func(i, j int) bool { + if entries[i].Type == res.EntryTypeDir { + return true + } + return false + }) + dirNum := 0 + for i := 0; i < length; i++ { + if entries[i].Type != res.EntryTypeDir { + dirNum = i + break + } + } + dirEntries := entries[0:dirNum] + sort.Slice(dirEntries, func(i, j int) bool { + if strings.Compare(strings.ToLower(dirEntries[i].Name), strings.ToLower(dirEntries[j].Name)) <= 0 { + return true + } + return false + }) + nonDirEntries := entries[dirNum:] + sort.Slice(nonDirEntries, func(i, j int) bool { + if strings.Compare(strings.ToLower(nonDirEntries[i].Name), strings.ToLower(nonDirEntries[j].Name)) <= 0 { + return true + } + return false + }) +} diff --git a/app/admin/apis/routes/dvroutes/explorer_routes.go b/app/admin/apis/routes/dvroutes/explorer_routes.go new file mode 100644 index 0000000000000000000000000000000000000000..a3841a87a09b0bc50b037c10f5ce2a416dc8979a --- /dev/null +++ b/app/admin/apis/routes/dvroutes/explorer_routes.go @@ -0,0 +1,23 @@ +package dvroutes + +import ( + "github.com/MarchGe/go-admin-server/app" + "github.com/MarchGe/go-admin-server/app/admin/apis/devops" + "github.com/MarchGe/go-admin-server/app/common/middleware/authz" + "github.com/MarchGe/go-admin-server/app/common/middleware/recorder" + "github.com/gin-gonic/gin" +) + +func init() { + app.RouterRegister(registerExplorerRoutes) +} + +func registerExplorerRoutes(g *gin.RouterGroup) { + a := devops.GetExplorerApi() + rg := g.Group("/devops/explorer") + + rg.DELETE("/entry", authz.RequiresPermissions("explorer:delete"), recorder.RecordOpLog("资源管理器资源"), a.DeleteEntry) + rg.GET("/entries", authz.RequiresPermissions("explorer:entries"), a.GetEntries) + rg.POST("/upload", authz.RequiresPermissions("explorer:upload"), a.Upload) + rg.GET("/download", authz.RequiresPermissions("explorer:download"), a.Download) +} diff --git a/app/admin/service/dvservice/dto/res/explorer_entry.go b/app/admin/service/dvservice/dto/res/explorer_entry.go new file mode 100644 index 0000000000000000000000000000000000000000..48ee16e9a300bf68cbf20c2a1253a56cff211bb2 --- /dev/null +++ b/app/admin/service/dvservice/dto/res/explorer_entry.go @@ -0,0 +1,18 @@ +package res + +type EntryType string + +const ( + EntryTypeDefault EntryType = "" // 其他未具体处理的类型,统一归到这个类型下 + EntryTypeDir EntryType = "d" + EntryTypeLink EntryType = "l" + EntryTypeCharDevice EntryType = "c" + EntryTypeBlockDevice EntryType = "b" + EntryTypeSocket EntryType = "s" + EntryTypeNamedPipe EntryType = "p" +) + +type ExplorerEntry struct { + Name string `json:"name"` + Type EntryType `json:"type"` +} diff --git a/app/admin/service/dvservice/explorer.go b/app/admin/service/dvservice/explorer.go new file mode 100644 index 0000000000000000000000000000000000000000..0c29ddf7b12d371db5bb365ed9c0b7917e39f132 --- /dev/null +++ b/app/admin/service/dvservice/explorer.go @@ -0,0 +1,76 @@ +package dvservice + +import ( + "errors" + dvRes "github.com/MarchGe/go-admin-server/app/admin/service/dvservice/dto/res" + "github.com/MarchGe/go-admin-server/app/common/E" + "os" + "strings" +) + +var _explorerService = &ExplorerService{} + +type ExplorerService struct { +} + +func GetExplorerService() *ExplorerService { + return _explorerService +} + +func (s *ExplorerService) ListEntries(parentDir, keyword string) ([]*dvRes.ExplorerEntry, error) { + info, err := os.Stat(parentDir) + if err != nil { + return nil, err + } + if !info.IsDir() { + return nil, E.Message("父目录参数有误") + } + dirEntries, err := os.ReadDir(parentDir) + if err != nil { + if errors.Is(err, os.ErrPermission) { + return nil, E.Message("没有权限访问该目录") + } + return nil, err + } + var length = 0 + if keyword == "" { + length = len(dirEntries) + } + for _, item := range dirEntries { + if keyword != "" && strings.Contains(item.Name(), keyword) { + length++ + } + } + entries := make([]*dvRes.ExplorerEntry, length) + for i, item := range dirEntries { + if keyword == "" || keyword != "" && strings.Contains(item.Name(), keyword) { + entry := &dvRes.ExplorerEntry{ + Name: item.Name(), + Type: s.parseType(item.Type()), + } + entries[i] = entry + } + } + return entries, nil +} + +func (s *ExplorerService) parseType(mode os.FileMode) dvRes.EntryType { + fileType := mode.Type() + switch { + case fileType.IsDir(): + return dvRes.EntryTypeDir + case fileType&os.ModeSymlink == os.ModeSymlink: + return dvRes.EntryTypeLink + case fileType&os.ModeSocket == os.ModeSocket: + return dvRes.EntryTypeSocket + case fileType&os.ModeNamedPipe == os.ModeNamedPipe: + return dvRes.EntryTypeNamedPipe + case fileType&os.ModeDevice == os.ModeDevice: + if fileType&os.ModeCharDevice == os.ModeCharDevice { + return dvRes.EntryTypeCharDevice + } else { + return dvRes.EntryTypeBlockDevice + } + } + return dvRes.EntryTypeDefault +} diff --git a/app/common/middleware/api_debug_logger.go b/app/common/middleware/api_debug_logger.go index 4244a32f0c44f11a724e81589e29748174b40a60..d677ed7b58dd60be9ec0645407f474d690723af1 100644 --- a/app/common/middleware/api_debug_logger.go +++ b/app/common/middleware/api_debug_logger.go @@ -20,6 +20,7 @@ func initDebugPatterns(contextPath string) { contextPath + "/terminal/ws", contextPath + "/terminal/ws/ssh/*", contextPath + "/devops/app/upload", + contextPath + "/devops/explorer/upload", } } diff --git a/docs/docs.go b/docs/docs.go index e9716ceedcf9e7a871e3344c6066cc3758b03e19..c69ce763c98864ce77ece33f3c833eb9806ba019 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -580,6 +580,152 @@ const docTemplate = `{ } } }, + "/devops/explorer/download": { + "get": { + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "资源管理器" + ], + "summary": "下载文件", + "parameters": [ + { + "type": "string", + "description": "文件完整路径", + "name": "path", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/R.Result" + } + } + } + } + }, + "/devops/explorer/entries": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "资源管理器" + ], + "summary": "查询entry列表", + "parameters": [ + { + "type": "string", + "description": "目录路径", + "name": "dir", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "按照名称模糊搜索", + "name": "keyword", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/R.Result" + }, + { + "type": "object", + "properties": { + "value": { + "type": "array", + "items": { + "$ref": "#/definitions/res.ExplorerEntry" + } + } + } + } + ] + } + } + } + } + }, + "/devops/explorer/entry": { + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "资源管理器" + ], + "summary": "删除文件或文件夹", + "parameters": [ + { + "type": "string", + "description": "文件或文件夹的路径", + "name": "path", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/R.Result" + } + } + } + } + }, + "/devops/explorer/upload": { + "post": { + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "资源管理器" + ], + "summary": "上传文件", + "parameters": [ + { + "type": "string", + "description": "文件目录", + "name": "dir", + "in": "formData", + "required": true + }, + { + "type": "file", + "description": "文件信息", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/R.Result" + } + } + } + } + }, "/devops/group": { "post": { "consumes": [ @@ -4002,6 +4148,41 @@ const docTemplate = `{ } } }, + "res.EntryType": { + "type": "string", + "enum": [ + "", + "d", + "l", + "c", + "b", + "s", + "p" + ], + "x-enum-comments": { + "EntryTypeDefault": "其他未具体处理的类型,统一归到这个类型下" + }, + "x-enum-varnames": [ + "EntryTypeDefault", + "EntryTypeDir", + "EntryTypeLink", + "EntryTypeCharDevice", + "EntryTypeBlockDevice", + "EntryTypeSocket", + "EntryTypeNamedPipe" + ] + }, + "res.ExplorerEntry": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/res.EntryType" + } + } + }, "res.HostBasicRes": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 00c4934af80f337f3ab44b34a8dfb3279de72466..bd2db045eb8222ce8857196f69ef4915898465f3 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -572,6 +572,152 @@ } } }, + "/devops/explorer/download": { + "get": { + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "资源管理器" + ], + "summary": "下载文件", + "parameters": [ + { + "type": "string", + "description": "文件完整路径", + "name": "path", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/R.Result" + } + } + } + } + }, + "/devops/explorer/entries": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "资源管理器" + ], + "summary": "查询entry列表", + "parameters": [ + { + "type": "string", + "description": "目录路径", + "name": "dir", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "按照名称模糊搜索", + "name": "keyword", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/R.Result" + }, + { + "type": "object", + "properties": { + "value": { + "type": "array", + "items": { + "$ref": "#/definitions/res.ExplorerEntry" + } + } + } + } + ] + } + } + } + } + }, + "/devops/explorer/entry": { + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "资源管理器" + ], + "summary": "删除文件或文件夹", + "parameters": [ + { + "type": "string", + "description": "文件或文件夹的路径", + "name": "path", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/R.Result" + } + } + } + } + }, + "/devops/explorer/upload": { + "post": { + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "资源管理器" + ], + "summary": "上传文件", + "parameters": [ + { + "type": "string", + "description": "文件目录", + "name": "dir", + "in": "formData", + "required": true + }, + { + "type": "file", + "description": "文件信息", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/R.Result" + } + } + } + } + }, "/devops/group": { "post": { "consumes": [ @@ -3994,6 +4140,41 @@ } } }, + "res.EntryType": { + "type": "string", + "enum": [ + "", + "d", + "l", + "c", + "b", + "s", + "p" + ], + "x-enum-comments": { + "EntryTypeDefault": "其他未具体处理的类型,统一归到这个类型下" + }, + "x-enum-varnames": [ + "EntryTypeDefault", + "EntryTypeDir", + "EntryTypeLink", + "EntryTypeCharDevice", + "EntryTypeBlockDevice", + "EntryTypeSocket", + "EntryTypeNamedPipe" + ] + }, + "res.ExplorerEntry": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/res.EntryType" + } + } + }, "res.HostBasicRes": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 88093c30214ccc50606f55eec7ac2ddda7f2888d..71ad0ef9e9b756d473481e1f36cc6013bd91a68c 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -720,6 +720,33 @@ definitions: updateTime: type: string type: object + res.EntryType: + enum: + - "" + - d + - l + - c + - b + - s + - p + type: string + x-enum-comments: + EntryTypeDefault: 其他未具体处理的类型,统一归到这个类型下 + x-enum-varnames: + - EntryTypeDefault + - EntryTypeDir + - EntryTypeLink + - EntryTypeCharDevice + - EntryTypeBlockDevice + - EntryTypeSocket + - EntryTypeNamedPipe + res.ExplorerEntry: + properties: + name: + type: string + type: + $ref: '#/definitions/res.EntryType' + type: object res.HostBasicRes: properties: id: @@ -1239,6 +1266,98 @@ paths: summary: 上传部署包 tags: - 应用管理 + /devops/explorer/download: + get: + consumes: + - multipart/form-data + parameters: + - description: 文件完整路径 + in: query + name: path + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/R.Result' + summary: 下载文件 + tags: + - 资源管理器 + /devops/explorer/entries: + get: + parameters: + - description: 目录路径 + in: query + name: dir + required: true + type: string + - description: 按照名称模糊搜索 + in: query + name: keyword + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/R.Result' + - properties: + value: + items: + $ref: '#/definitions/res.ExplorerEntry' + type: array + type: object + summary: 查询entry列表 + tags: + - 资源管理器 + /devops/explorer/entry: + delete: + parameters: + - description: 文件或文件夹的路径 + in: query + name: path + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/R.Result' + summary: 删除文件或文件夹 + tags: + - 资源管理器 + /devops/explorer/upload: + post: + consumes: + - multipart/form-data + parameters: + - description: 文件目录 + in: formData + name: dir + required: true + type: string + - description: 文件信息 + in: formData + name: file + required: true + type: file + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/R.Result' + summary: 上传文件 + tags: + - 资源管理器 /devops/group: post: consumes: diff --git a/proto/grpc/service/sys_stats_service.proto b/proto/grpc/service/sys_stats_service.proto index 25b0adc0e45ea5074fac0fae85466b5888a1aea0..79b63330b29b025638bc2b6a48a00a7a7356793b 100644 --- a/proto/grpc/service/sys_stats_service.proto +++ b/proto/grpc/service/sys_stats_service.proto @@ -11,6 +11,6 @@ service SysStatsService { // report performance statistics frequently rpc reportSystemStats(model.SysStats) returns (google.protobuf.Empty); - // report host information, only report once after agent started + // report host information frequently rpc reportHostInformation(model.HostInfo) returns (google.protobuf.Empty); } \ No newline at end of file