From f9233bd36b0829fafac3cc8c6dd2f5b769a325ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Sun, 20 Oct 2024 18:51:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=87=E4=BB=B6=E6=90=9C=E7=B4=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/http/request/file.go | 14 ++- internal/route/http.go | 2 +- internal/service/file.go | 89 +++++++++++---- internal/service/file_windows.go | 73 +++++++++--- pkg/io/file.go | 5 + pkg/io/path.go | 60 +++++++++- web/src/api/panel/file/index.ts | 10 +- web/src/views/file/ListTable.vue | 10 +- web/src/views/file/PathInput.vue | 19 ++- web/src/views/file/SearchModal.vue | 178 +++++++++++++++++++++++++++++ 10 files changed, 397 insertions(+), 63 deletions(-) create mode 100644 web/src/views/file/SearchModal.vue diff --git a/internal/http/request/file.go b/internal/http/request/file.go index 16e6e126..54df46e6 100644 --- a/internal/http/request/file.go +++ b/internal/http/request/file.go @@ -1,5 +1,11 @@ package request +import ( + "net/http" + + "github.com/spf13/cast" +) + type FileList struct { Path string `json:"path" form:"path" validate:"required"` Sort string `json:"sort" form:"sort"` @@ -50,5 +56,11 @@ type FileUnCompress struct { type FileSearch struct { Path string `form:"path" json:"path" validate:"required"` - KeyWord string `form:"keyword" json:"keyword" validate:"required"` + Keyword string `form:"keyword" json:"keyword" validate:"required"` + Sub bool `form:"sub" json:"sub"` +} + +func (r *FileSearch) Prepare(req *http.Request) error { + r.Sub = cast.ToBool(req.FormValue("sub")) + return nil } diff --git a/internal/route/http.go b/internal/route/http.go index f7666ec2..5e1f60e5 100644 --- a/internal/route/http.go +++ b/internal/route/http.go @@ -232,7 +232,7 @@ func Http(r chi.Router) { r.Post("/permission", file.Permission) r.Post("/compress", file.Compress) r.Post("/unCompress", file.UnCompress) - r.Post("/search", file.Search) + r.Get("/search", file.Search) r.Get("/list", file.List) }) diff --git a/internal/service/file.go b/internal/service/file.go index 11d4f8a1..24f87cb2 100644 --- a/internal/service/file.go +++ b/internal/service/file.go @@ -15,6 +15,7 @@ import ( "time" "github.com/go-rat/chix" + "github.com/spf13/cast" "github.com/TheTNB/panel/internal/http/request" "github.com/TheTNB/panel/pkg/io" @@ -327,22 +328,18 @@ func (s *FileService) Search(w http.ResponseWriter, r *http.Request) { return } - paths := make(map[string]stdos.FileInfo) - err = filepath.Walk(req.Path, func(path string, info stdos.FileInfo, err error) error { - if err != nil { - return err - } - if strings.Contains(info.Name(), req.KeyWord) { - paths[path] = info - } - return nil - }) + results, err := io.SearchX(req.Path, req.Keyword, req.Sub) if err != nil { Error(w, http.StatusInternalServerError, "%v", err) return } - Success(w, paths) + paged, total := Paginate(r, s.formatInfo(results)) + + Success(w, chix.M{ + "total": total, + "items": paged, + }) } func (s *FileService) List(w http.ResponseWriter, r *http.Request) { @@ -378,14 +375,27 @@ func (s *FileService) List(w http.ResponseWriter, r *http.Request) { }) } - var paths []any - for _, file := range list { - info, _ := file.Info() - stat := info.Sys().(*syscall.Stat_t) + paged, total := Paginate(r, s.formatDir(req.Path, list)) + Success(w, chix.M{ + "total": total, + "items": paged, + }) +} + +// formatDir 格式化目录信息 +func (s *FileService) formatDir(base string, entries []stdos.DirEntry) []any { + var paths []any + for _, file := range entries { + info, err := file.Info() + if err != nil { + continue + } + + stat := info.Sys().(*syscall.Stat_t) paths = append(paths, map[string]any{ "name": info.Name(), - "full": filepath.Join(req.Path, info.Name()), + "full": filepath.Join(base, info.Name()), "size": str.FormatBytes(float64(info.Size())), "mode_str": info.Mode().String(), "mode": fmt.Sprintf("%04o", info.Mode().Perm()), @@ -395,21 +405,52 @@ func (s *FileService) List(w http.ResponseWriter, r *http.Request) { "gid": stat.Gid, "hidden": io.IsHidden(info.Name()), "symlink": io.IsSymlink(info.Mode()), - "link": io.GetSymlink(filepath.Join(req.Path, info.Name())), + "link": io.GetSymlink(filepath.Join(base, info.Name())), "dir": info.IsDir(), "modify": info.ModTime().Format(time.DateTime), }) } - paged, total := Paginate(r, paths) - - Success(w, chix.M{ - "total": total, - "items": paged, - }) + return paths } -// setPermission +// formatInfo 格式化文件信息 +func (s *FileService) formatInfo(infos map[string]stdos.FileInfo) []map[string]any { + var paths []map[string]any + for path, info := range infos { + stat := info.Sys().(*syscall.Stat_t) + paths = append(paths, map[string]any{ + "name": info.Name(), + "full": path, + "size": str.FormatBytes(float64(info.Size())), + "mode_str": info.Mode().String(), + "mode": fmt.Sprintf("%04o", info.Mode().Perm()), + "owner": os.GetUser(stat.Uid), + "group": os.GetGroup(stat.Gid), + "uid": stat.Uid, + "gid": stat.Gid, + "hidden": io.IsHidden(info.Name()), + "symlink": io.IsSymlink(info.Mode()), + "link": io.GetSymlink(path), + "dir": info.IsDir(), + "modify": info.ModTime().Format(time.DateTime), + }) + } + + slices.SortFunc(paths, func(a, b map[string]any) int { + if cast.ToBool(a["dir"]) && !cast.ToBool(b["dir"]) { + return -1 + } + if !cast.ToBool(a["dir"]) && cast.ToBool(b["dir"]) { + return 1 + } + return strings.Compare(strings.ToLower(cast.ToString(a["name"])), strings.ToLower(cast.ToString(b["name"]))) + }) + + return paths +} + +// setPermission 设置权限 func (s *FileService) setPermission(path string, mode stdos.FileMode, owner, group string) { _ = io.Chmod(path, mode) _ = io.Chown(path, owner, group) diff --git a/internal/service/file_windows.go b/internal/service/file_windows.go index d6bafb70..8887869d 100644 --- a/internal/service/file_windows.go +++ b/internal/service/file_windows.go @@ -16,6 +16,7 @@ import ( "time" "github.com/go-rat/chix" + "github.com/spf13/cast" "github.com/TheTNB/panel/internal/http/request" "github.com/TheTNB/panel/pkg/io" @@ -314,22 +315,18 @@ func (s *FileService) Search(w http.ResponseWriter, r *http.Request) { return } - paths := make(map[string]stdos.FileInfo) - err = filepath.Walk(req.Path, func(path string, info stdos.FileInfo, err error) error { - if err != nil { - return err - } - if strings.Contains(info.Name(), req.KeyWord) { - paths[path] = info - } - return nil - }) + results, err := io.SearchX(req.Path, req.Keyword, req.Sub) if err != nil { Error(w, http.StatusInternalServerError, "%v", err) return } - Success(w, paths) + paged, total := Paginate(r, s.formatInfo(results)) + + Success(w, chix.M{ + "total": total, + "items": paged, + }) } func (s *FileService) List(w http.ResponseWriter, r *http.Request) { @@ -365,13 +362,23 @@ func (s *FileService) List(w http.ResponseWriter, r *http.Request) { }) } + paged, total := Paginate(r, s.formatDir(req.Path, list)) + + Success(w, chix.M{ + "total": total, + "items": paged, + }) +} + +// formatDir 格式化目录信息 +func (s *FileService) formatDir(base string, entries []stdos.DirEntry) []any { var paths []any - for _, file := range list { + for _, file := range entries { info, _ := file.Info() paths = append(paths, map[string]any{ "name": info.Name(), - "full": filepath.Join(req.Path, info.Name()), + "full": filepath.Join(base, info.Name()), "size": str.FormatBytes(float64(info.Size())), "mode_str": info.Mode().String(), "mode": fmt.Sprintf("%04o", info.Mode().Perm()), @@ -381,18 +388,48 @@ func (s *FileService) List(w http.ResponseWriter, r *http.Request) { "gid": 0, "hidden": io.IsHidden(info.Name()), "symlink": io.IsSymlink(info.Mode()), - "link": io.GetSymlink(filepath.Join(req.Path, info.Name())), + "link": io.GetSymlink(filepath.Join(base, info.Name())), "dir": info.IsDir(), "modify": info.ModTime().Format(time.DateTime), }) } - paged, total := Paginate(r, paths) + return paths +} - Success(w, chix.M{ - "total": total, - "items": paged, +// formatInfo 格式化文件信息 +func (s *FileService) formatInfo(infos map[string]stdos.FileInfo) []map[string]any { + var paths []map[string]any + for path, info := range infos { + paths = append(paths, map[string]any{ + "name": info.Name(), + "full": path, + "size": str.FormatBytes(float64(info.Size())), + "mode_str": info.Mode().String(), + "mode": fmt.Sprintf("%04o", info.Mode().Perm()), + "owner": "", + "group": "", + "uid": 0, + "gid": 0, + "hidden": io.IsHidden(info.Name()), + "symlink": io.IsSymlink(info.Mode()), + "link": io.GetSymlink(path), + "dir": info.IsDir(), + "modify": info.ModTime().Format(time.DateTime), + }) + } + + slices.SortFunc(paths, func(a, b map[string]any) int { + if cast.ToBool(a["dir"]) && !cast.ToBool(b["dir"]) { + return -1 + } + if !cast.ToBool(a["dir"]) && cast.ToBool(b["dir"]) { + return 1 + } + return strings.Compare(strings.ToLower(cast.ToString(a["name"])), strings.ToLower(cast.ToString(b["name"]))) }) + + return paths } // setPermission diff --git a/pkg/io/file.go b/pkg/io/file.go index 6a0af9d2..bd91e23d 100644 --- a/pkg/io/file.go +++ b/pkg/io/file.go @@ -211,6 +211,11 @@ func GetSymlink(path string) string { return linkPath } +// TempFile 创建临时文件 +func TempFile(dir, prefix string) (*os.File, error) { + return os.CreateTemp(dir, prefix) +} + func getFormat(f FormatArchive) archiver.CompressedArchive { format := archiver.CompressedArchive{} switch f { diff --git a/pkg/io/path.go b/pkg/io/path.go index be848ebe..9b2de3cc 100644 --- a/pkg/io/path.go +++ b/pkg/io/path.go @@ -1,6 +1,7 @@ package io import ( + "bytes" "fmt" "io" "os" @@ -146,11 +147,6 @@ func TempDir(prefix string) (string, error) { return os.MkdirTemp("", prefix) } -// TempFile 创建临时文件 -func TempFile(dir, prefix string) (*os.File, error) { - return os.CreateTemp(dir, prefix) -} - // ReadDir 读取目录 func ReadDir(path string) ([]os.DirEntry, error) { return os.ReadDir(path) @@ -190,3 +186,57 @@ func CountX(path string) (int64, error) { count := len(string(out)) return int64(count), nil } + +// Search 查找文件/文件夹 +func Search(path, keyword string, sub bool) (map[string]os.FileInfo, error) { + paths := make(map[string]os.FileInfo) + baseDepth := strings.Count(filepath.Clean(path), string(os.PathSeparator)) + + err := filepath.Walk(path, func(p string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !sub && strings.Count(p, string(os.PathSeparator)) > baseDepth+1 { + return filepath.SkipDir + } + if strings.Contains(info.Name(), keyword) { + paths[p] = info + } + return nil + }) + + return paths, err +} + +// SearchX 查找文件/文件夹(find命令) +func SearchX(path, keyword string, sub bool) (map[string]os.FileInfo, error) { + paths := make(map[string]os.FileInfo) + + var cmd *exec.Cmd + if sub { + cmd = exec.Command("find", path, "-name", "*"+keyword+"*") + } else { + cmd = exec.Command("find", path, "-maxdepth", "1", "-name", "*"+keyword+"*") + } + var out bytes.Buffer + cmd.Stdout = &out + + if err := cmd.Run(); err != nil { + return nil, err + } + + lines := strings.Split(out.String(), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + info, err := os.Stat(line) + if err != nil { + return nil, err + } + paths[line] = info + } + + return paths, nil +} diff --git a/web/src/api/panel/file/index.ts b/web/src/api/panel/file/index.ts index e2bd1673..78a5a705 100644 --- a/web/src/api/panel/file/index.ts +++ b/web/src/api/panel/file/index.ts @@ -50,8 +50,14 @@ export default { unCompress: (file: string, path: string): Promise> => request.post('/file/unCompress', { file, path }), // 搜索文件 - search: (keyword: string): Promise> => - request.post('/file/search', { keyword }), + search: ( + path: string, + keyword: string, + sub: boolean, + page: number, + limit: number + ): Promise> => + request.get('/file/search', { params: { path, keyword, sub, page, limit } }), // 获取文件列表 list: (path: string, page: number, limit: number, sort: string): Promise> => request.get('/file/list', { params: { path, page, limit, sort } }) diff --git a/web/src/views/file/ListTable.vue b/web/src/views/file/ListTable.vue index 7f677955..6cfb2ec6 100644 --- a/web/src/views/file/ListTable.vue +++ b/web/src/views/file/ListTable.vue @@ -71,13 +71,8 @@ const columns: DataTableColumns = [ title: '名称', key: 'name', minWidth: 180, - ellipsis: { - tooltip: true - }, defaultSortOrder: false, - sorter(row1, row2) { - return row1.name - row2.name - }, + sorter: 'default', render(row) { let icon = 'bi:file-earmark' if (row.dir) { @@ -388,7 +383,6 @@ const handleRename = () => { const target = path.value + '/' + renameModel.value.target if (!checkName(renameModel.value.source) || !checkName(renameModel.value.target)) { window.$message.error('名称不合法') - console.log(source, target) return } @@ -500,8 +494,6 @@ const handleSorterChange = (sorter: { }) => { if (!sorter || sorter.columnKey === 'name') { if (!loading.value) { - console.log(sorter) - console.log(sorter.order) switch (sorter.order) { case 'ascend': sort.value = 'asc' diff --git a/web/src/views/file/PathInput.vue b/web/src/views/file/PathInput.vue index 89503b1c..d9044285 100644 --- a/web/src/views/file/PathInput.vue +++ b/web/src/views/file/PathInput.vue @@ -4,6 +4,7 @@ import { onUnmounted } from 'vue' import EventBus from '@/utils/event' import { checkPath } from '@/utils/file' +import SearchModal from '@/views/file/SearchModal.vue' const path = defineModel('path', { type: String, required: true }) const isInput = ref(false) @@ -13,6 +14,12 @@ const input = ref('www') const history: string[] = [] let current = -1 +const searchModal = ref(false) +const search = ref({ + keyword: '', + sub: false +}) + const handleInput = () => { isInput.value = true nextTick(() => { @@ -84,7 +91,7 @@ const handlePushHistory = (path: string) => { } const handleSearch = () => { - window.$message.info('搜索功能暂未实现') + searchModal.value = true } watch( @@ -141,9 +148,9 @@ onUnmounted(() => { /> - + @@ -151,6 +158,12 @@ onUnmounted(() => { + diff --git a/web/src/views/file/SearchModal.vue b/web/src/views/file/SearchModal.vue new file mode 100644 index 00000000..574e80be --- /dev/null +++ b/web/src/views/file/SearchModal.vue @@ -0,0 +1,178 @@ + + + + +