From be5fb5eb94b16a68c410e524bdb109d97e1bdc65 Mon Sep 17 00:00:00 2001 From: TsMask <340112800@qq.com> Date: Fri, 25 Apr 2025 15:38:07 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=AF=B9=E6=9C=AC?= =?UTF-8?q?=E5=9C=B0=E6=96=87=E4=BB=B6=E7=9A=84=E6=93=8D=E4=BD=9C=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/framework/ssh/files.go | 12 +- src/framework/utils/file/files.go | 78 ++++++++++ src/framework/utils/file/files_unix.go | 36 +++++ src/framework/utils/file/files_windows.go | 13 ++ src/modules/common/common.go | 3 + src/modules/common/controller/file.go | 172 ++++++++++++++++++++++ 6 files changed, 308 insertions(+), 6 deletions(-) create mode 100644 src/framework/utils/file/files.go create mode 100644 src/framework/utils/file/files_unix.go create mode 100644 src/framework/utils/file/files_windows.go diff --git a/src/framework/ssh/files.go b/src/framework/ssh/files.go index e681ee99..8081877a 100644 --- a/src/framework/ssh/files.go +++ b/src/framework/ssh/files.go @@ -16,7 +16,7 @@ type FileListRow struct { LinkCount int64 `json:"linkCount"` // 硬链接数目 Owner string `json:"owner"` // 所属用户 Group string `json:"group"` // 所属组 - Size string `json:"size"` // 文件的大小 + Size int64 `json:"size"` // 文件的大小 ModifiedTime int64 `json:"modifiedTime"` // 最后修改时间,单位为秒 FileName string `json:"fileName"` // 文件的名称 } @@ -34,10 +34,10 @@ func FileList(sshClient *ConnSSH, path, search string) ([]FileListRow, error) { if search != "" { searchStr = search + searchStr } - // cd /var/log && find. -maxdepth 1 -name'mme*' -exec ls -lthd --time-style=+%s {} + - cmdStr := fmt.Sprintf("cd %s && find . -maxdepth 1 -name '%s' -exec ls -lthd --time-style=+%%s {} +", path, searchStr) - // cd /var/log && ls -lthd --time-style=+%s mme* - // cmdStr := fmt.Sprintf("cd %s && ls -lthd --time-style=+%%s %s", path, searchStr) + // cd /var/log && find. -maxdepth 1 -name'mme*' -exec ls -ltd --time-style=+%s {} + + cmdStr := fmt.Sprintf("cd %s && find . -maxdepth 1 -name '%s' -exec ls -ltd --time-style=+%%s {} +", path, searchStr) + // cd /var/log && ls -ltd --time-style=+%s mme* + // cmdStr := fmt.Sprintf("cd %s && ls -ltd --time-style=+%%s %s", path, searchStr) // 是否远程客户端读取 if sshClient == nil { @@ -94,7 +94,7 @@ func FileList(sshClient *ConnSSH, path, search string) ([]FileListRow, error) { LinkCount: parse.Number(fields[1]), Owner: fields[2], Group: fields[3], - Size: fields[4], + Size: parse.Number(fields[4]), ModifiedTime: parse.Number(fields[5]), FileName: fileName, }) diff --git a/src/framework/utils/file/files.go b/src/framework/utils/file/files.go new file mode 100644 index 00000000..9f45e47a --- /dev/null +++ b/src/framework/utils/file/files.go @@ -0,0 +1,78 @@ +package file + +import ( + "os" + "path/filepath" +) + +// FileListRow 文件列表行数据 +type FileListRow struct { + FileType string `json:"fileType"` // 文件类型 dir, file, symlink + FileMode string `json:"fileMode"` // 文件的权限 + LinkCount int64 `json:"linkCount"` // 硬链接数目 + Owner string `json:"owner"` // 所属用户 + Group string `json:"group"` // 所属组 + Size int64 `json:"size"` // 文件的大小 + ModifiedTime int64 `json:"modifiedTime"` // 最后修改时间,单位为秒 + FileName string `json:"fileName"` // 文件的名称 +} + +// 文件列表 +// search 文件名后模糊* +// +// return 行记录,异常 +func FileList(path, search string) ([]FileListRow, error) { + var rows []FileListRow + + // 构建搜索模式 + pattern := "*" + if search != "" { + pattern = search + pattern + } + + // 读取目录内容 + entries, err := os.ReadDir(path) + if err != nil { + return nil, err + } + + // 遍历目录项 + for _, entry := range entries { + // 匹配文件名 + matched, err := filepath.Match(pattern, entry.Name()) + if err != nil || !matched { + continue + } + + // 获取文件详细信息 + info, err := entry.Info() + if err != nil { + continue + } + + // 确定文件类型 + fileType := "file" + if info.IsDir() { + fileType = "dir" + } else if info.Mode()&os.ModeSymlink != 0 { + fileType = "symlink" + } + + // 获取系统特定的文件信息 + linkCount, owner, group := getFileInfo(info) + + // 组装文件信息 + rows = append(rows, FileListRow{ + FileMode: info.Mode().String(), + FileType: fileType, + LinkCount: linkCount, + Owner: owner, + Group: group, + Size: info.Size(), + ModifiedTime: info.ModTime().UnixMilli(), + FileName: entry.Name(), + }) + } + + return rows, nil +} diff --git a/src/framework/utils/file/files_unix.go b/src/framework/utils/file/files_unix.go new file mode 100644 index 00000000..e7fd8c79 --- /dev/null +++ b/src/framework/utils/file/files_unix.go @@ -0,0 +1,36 @@ +//go:build !windows +// +build !windows + +package file + +import ( + "fmt" + "os" + "os/user" + "syscall" +) + +// getFileInfo 获取系统特定的文件信息s +func getFileInfo(info os.FileInfo) (linkCount int64, owner, group string) { + // Unix-like 系统 (Linux, macOS) + if stat, ok := info.Sys().(*syscall.Stat_t); ok { + // 获取用户名 + ownerName := "root" + if stat.Uid != 0 { + if u, err := user.LookupId(fmt.Sprint(stat.Uid)); err == nil { + ownerName = u.Username + } + } + + // 获取组名 + groupName := "root" + if stat.Gid != 0 { + if g, err := user.LookupGroupId(fmt.Sprint(stat.Gid)); err == nil { + groupName = g.Name + } + } + + return int64(stat.Nlink), ownerName, groupName + } + return 1, "", "" +} diff --git a/src/framework/utils/file/files_windows.go b/src/framework/utils/file/files_windows.go new file mode 100644 index 00000000..efaf414b --- /dev/null +++ b/src/framework/utils/file/files_windows.go @@ -0,0 +1,13 @@ +//go:build windows +// +build windows + +package file + +import ( + "os" +) + +// getFileInfo 获取系统特定的文件信息 +func getFileInfo(_ os.FileInfo) (linkCount int64, owner, group string) { + return 1, "Administrator", "Administrators" +} diff --git a/src/modules/common/common.go b/src/modules/common/common.go index 9c497c4e..a22d2057 100644 --- a/src/modules/common/common.go +++ b/src/modules/common/common.go @@ -37,6 +37,9 @@ func Setup(router *gin.Engine) { fileGroup.POST("/chunk-upload", middleware.PreAuthorize(nil), controller.NewFile.ChunkUpload) fileGroup.POST("/chunk-merge", middleware.PreAuthorize(nil), controller.NewFile.ChunkMerge) fileGroup.GET("/download/:filePath", middleware.PreAuthorize(nil), controller.NewFile.Download) + fileGroup.GET("/list", middleware.PreAuthorize(nil), controller.NewFile.List) + fileGroup.GET("", middleware.PreAuthorize(nil), controller.NewFile.File) + fileGroup.DELETE("", middleware.PreAuthorize(nil), controller.NewFile.Remove) fileGroup.POST("/transfer-static-file", middleware.PreAuthorize(nil), controller.NewFile.TransferStaticFile) } } diff --git a/src/modules/common/controller/file.go b/src/modules/common/controller/file.go index e7e46746..1b50a3f6 100644 --- a/src/modules/common/controller/file.go +++ b/src/modules/common/controller/file.go @@ -4,7 +4,9 @@ import ( "encoding/base64" "fmt" "net/url" + "os" "path/filepath" + "runtime" "strings" "be.ems/src/framework/config" @@ -235,6 +237,176 @@ func (s *FileController) ChunkUpload(c *gin.Context) { c.JSON(206, resp.OkData(chunkFilePath)) } +// 本地文件列表 +// +// GET /list +// +// @Tags common/file +// @Accept json +// @Produce json +// @Param path query string true "file path" default(/var/log) +// @Param pageNum query number true "pageNum" default(1) +// @Param pageSize query number true "pageSize" default(10) +// @Param search query string false "search prefix" default(upf) +// @Success 200 {object} object "Response Results" +// @Security TokenAuth +// @Summary Local file list +// @Description Local file list +// @Router /file/list [get] +func (s *FileController) List(c *gin.Context) { + var querys struct { + Path string `form:"path" binding:"required"` + PageNum int64 `form:"pageNum" binding:"required"` + PageSize int64 `form:"pageSize" binding:"required"` + Search string `form:"search"` + } + if err := c.ShouldBindQuery(&querys); err != nil { + errMsgs := fmt.Sprintf("bind err: %s", resp.FormatBindError(err)) + c.JSON(422, resp.CodeMsg(40422, errMsgs)) + return + } + + // 获取文件列表 + localFilePath := querys.Path + if runtime.GOOS == "windows" { + localFilePath = fmt.Sprintf("C:%s", localFilePath) + } + rows, err := file.FileList(localFilePath, querys.Search) + if err != nil { + c.JSON(200, resp.OkData(map[string]any{ + "path": querys.Path, + "total": len(rows), + "rows": []file.FileListRow{}, + })) + return + } + + // 对数组进行切片分页 + lenNum := int64(len(rows)) + start := (querys.PageNum - 1) * querys.PageSize + end := start + querys.PageSize + var splitRows []file.FileListRow + if start >= lenNum { + splitRows = []file.FileListRow{} + } else if end >= lenNum { + splitRows = rows[start:] + } else { + splitRows = rows[start:end] + } + + c.JSON(200, resp.OkData(map[string]any{ + "path": querys.Path, + "total": lenNum, + "rows": splitRows, + })) +} + +// 本地文件获取下载 +// +// DELETE / +// +// @Tags common/file +// @Accept json +// @Produce json +// @Param path query string true "file path" default(/var/log) +// @Param fileName query string true "file name" default(omc.log) +// @Success 200 {object} object "Response Results" +// @Security TokenAuth +// @Summary Local files for download +// @Description Local files for download +// @Router /file [get] +func (s *FileController) File(c *gin.Context) { + var querys struct { + Path string `form:"path" binding:"required"` + Filename string `form:"fileName" binding:"required"` + } + if err := c.ShouldBindQuery(&querys); err != nil { + errMsgs := fmt.Sprintf("bind err: %s", resp.FormatBindError(err)) + c.JSON(422, resp.CodeMsg(40422, errMsgs)) + return + } + + // 检查路径是否在允许的目录范围内 + allowedPaths := []string{"/var/log", "/tmp", "/usr/local/omc/backup"} + allowed := false + for _, p := range allowedPaths { + if strings.HasPrefix(querys.Path, p) { + allowed = true + break + } + } + if !allowed { + c.JSON(200, resp.ErrMsg("operation path is not within the allowed range")) + return + } + + // 获取文件路径并下载 + localFilePath := filepath.Join(querys.Path, querys.Filename) + if runtime.GOOS == "windows" { + localFilePath = fmt.Sprintf("C:%s", localFilePath) + } + if _, err := os.Stat(localFilePath); os.IsNotExist(err) { + c.JSON(200, resp.ErrMsg("file does not exist")) + return + } + c.FileAttachment(localFilePath, querys.Filename) +} + +// 本地文件删除 +// +// DELETE / +// +// @Tags common/file +// @Accept json +// @Produce json +// @Param path query string true "file path" default(/var/log) +// @Param fileName query string true "file name" default(omc.log) +// @Success 200 {object} object "Response Results" +// @Security TokenAuth +// @Summary Local file deletion +// @Description Local file deletion +// @Router /file [delete] +func (s *FileController) Remove(c *gin.Context) { + var querys struct { + Path string `form:"path" binding:"required"` + Filename string `form:"fileName" binding:"required"` + } + if err := c.ShouldBindQuery(&querys); err != nil { + errMsgs := fmt.Sprintf("bind err: %s", resp.FormatBindError(err)) + c.JSON(422, resp.CodeMsg(40422, errMsgs)) + return + } + + // 检查路径是否在允许的目录范围内 + allowedPaths := []string{"/tmp", "/usr/local/omc/backup"} + allowed := false + for _, p := range allowedPaths { + if strings.HasPrefix(querys.Path, p) { + allowed = true + break + } + } + if !allowed { + c.JSON(200, resp.ErrMsg("operation path is not within the allowed range")) + return + } + + // 获取文件路径并删除 + localFilePath := filepath.Join(querys.Path, querys.Filename) + if runtime.GOOS == "windows" { + localFilePath = fmt.Sprintf("C:%s", localFilePath) + } + if _, err := os.Stat(localFilePath); os.IsNotExist(err) { + c.JSON(200, resp.ErrMsg("file does not exist")) + return + } + if err := os.Remove(localFilePath); err != nil { + c.JSON(200, resp.ErrMsg(err.Error())) + return + } + c.JSON(200, resp.Ok(nil)) +} + // 转存指定对应文件到静态目录 // // POST /transfer-static-file