diff --git a/go.mod b/go.mod index 137d4711..bb78b9a9 100644 --- a/go.mod +++ b/go.mod @@ -60,11 +60,11 @@ require ( require ( filippo.io/edwards25519 v1.1.0 // indirect - github.com/G-Core/gcore-dns-sdk-go v0.3.2 // indirect + github.com/G-Core/gcore-dns-sdk-go v0.3.3 // indirect github.com/boombuler/barcode v1.1.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.9 // indirect + github.com/gabriel-vasile/mimetype v1.4.10 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/gofiber/schema v1.6.0 // indirect diff --git a/go.sum b/go.sum index 051f633e..1f2ded76 100644 --- a/go.sum +++ b/go.sum @@ -15,8 +15,8 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/G-Core/gcore-dns-sdk-go v0.3.2 h1:WCTKmoJ5uND69UFE318yxWuHy/h3AA28Z1OnqSYUxRk= -github.com/G-Core/gcore-dns-sdk-go v0.3.2/go.mod h1:35t795gOfzfVanhzkFyUXEzaBuMXwETmJldPpP28MN4= +github.com/G-Core/gcore-dns-sdk-go v0.3.3 h1:McILJSbJ5nOcT0MI0aBYhEuufCF329YbqKwFIN0RjCI= +github.com/G-Core/gcore-dns-sdk-go v0.3.3/go.mod h1:35t795gOfzfVanhzkFyUXEzaBuMXwETmJldPpP28MN4= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -57,8 +57,8 @@ github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7z github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= -github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= +github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= +github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= @@ -106,8 +106,6 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= -github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4= github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -218,14 +216,10 @@ github.com/libtnb/acmez/v3 v3.0.0-20250707093727-dc5aedd96413 h1:q6Qttk+i7r8JWw1 github.com/libtnb/acmez/v3 v3.0.0-20250707093727-dc5aedd96413/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= github.com/libtnb/chix v1.3.0 h1:/U+CyuxI41ooeB6M/762PHOjQlfHRg6BQjHKLZarlrM= github.com/libtnb/chix v1.3.0/go.mod h1:o8nQLEp/UrUojBKYzw8K8sltU/h0XxI2VLZ/z7AQCQg= -github.com/libtnb/gormstore v1.1.0 h1:VbX8u0hhyl2YFSGfkSlF/xSfFG/LE29DCLrY8MJ+uxM= -github.com/libtnb/gormstore v1.1.0/go.mod h1:8A5QzeZxi1MpSmjUVsHTDAL6KnU84feIXMutFLPawwA= github.com/libtnb/gormstore v1.1.1 h1:FG/3P4PuWM6/vB4weVJ31meiSaoeXns1NQlP66quKeg= github.com/libtnb/gormstore v1.1.1/go.mod h1:8A5QzeZxi1MpSmjUVsHTDAL6KnU84feIXMutFLPawwA= github.com/libtnb/securecookie v1.2.0 h1:2uc0PBDm0foeSTrcZ9QTX1IEjf6kFEwfgEYSIXQSKrA= github.com/libtnb/securecookie v1.2.0/go.mod h1:ja+wNGnQzYqcqXQnJWu6icsaWi5JEBwNEMJ2ReTVDxA= -github.com/libtnb/sessions v1.2.0 h1:g/RMcfGTC5P2BQE1IqrgppPEQ++x/QjOuiwbN9Frke8= -github.com/libtnb/sessions v1.2.0/go.mod h1:45Bn9d6PseDINLIM1QaJrlCMbzSZ0NWpDbWkdrKJKw0= github.com/libtnb/sessions v1.2.1 h1:O9gkEIeZuqyaxopXrUJcGxlNxmNfRBI8BOK43yLJXDI= github.com/libtnb/sessions v1.2.1/go.mod h1:45Bn9d6PseDINLIM1QaJrlCMbzSZ0NWpDbWkdrKJKw0= github.com/libtnb/utils v1.2.0 h1:6bTZrWn2OkNrODpCY4dhuHwbhsVRV7HICIgmZ31we98= @@ -380,8 +374,6 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= -golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -454,8 +446,6 @@ golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= -golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= diff --git a/internal/http/request/file.go b/internal/http/request/file.go index b6159e21..f1ac4ce1 100644 --- a/internal/http/request/file.go +++ b/internal/http/request/file.go @@ -7,8 +7,15 @@ import ( ) type FileList struct { - Path string `json:"path" form:"path" validate:"required|isUnixPath"` - Sort string `json:"sort" form:"sort"` + Path string `json:"path" form:"path" validate:"required|isUnixPath"` + Sort string `json:"sort" form:"sort"` + Keyword string `form:"keyword" json:"keyword"` + Sub bool `form:"sub" json:"sub"` +} + +func (r *FileList) Prepare(req *http.Request) error { + r.Sub = cast.ToBool(req.FormValue("sub")) + return nil } type FilePath struct { @@ -53,14 +60,3 @@ type FileUnCompress struct { File string `form:"file" json:"file" validate:"required|isUnixPath"` Path string `form:"path" json:"path" validate:"required|isUnixPath"` } - -type FileSearch struct { - Path string `form:"path" json:"path" validate:"required|isUnixPath"` - 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 ddd70a15..70b704c6 100644 --- a/internal/route/http.go +++ b/internal/route/http.go @@ -360,7 +360,6 @@ func (route *Http) Register(r *chi.Mux) { r.Post("/permission", route.file.Permission) r.Post("/compress", route.file.Compress) r.Post("/un_compress", route.file.UnCompress) - r.Get("/search", route.file.Search) r.Get("/list", route.file.List) }) diff --git a/internal/service/file.go b/internal/service/file.go index 6c78e8cb..cb57ed90 100644 --- a/internal/service/file.go +++ b/internal/service/file.go @@ -395,27 +395,6 @@ func (s *FileService) UnCompress(w http.ResponseWriter, r *http.Request) { Success(w, nil) } -func (s *FileService) Search(w http.ResponseWriter, r *http.Request) { - req, err := Bind[request.FileSearch](r) - if err != nil { - Error(w, http.StatusInternalServerError, "%v", err) - return - } - - results, err := io.SearchX(req.Path, req.Keyword, req.Sub) - if err != nil { - Error(w, http.StatusInternalServerError, "%v", err) - return - } - - 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) { req, err := Bind[request.FileList](r) if err != nil { @@ -423,10 +402,19 @@ func (s *FileService) List(w http.ResponseWriter, r *http.Request) { return } - list, err := stdos.ReadDir(req.Path) - if err != nil { - Error(w, http.StatusInternalServerError, "%v", err) - return + var list []stdos.DirEntry + if req.Keyword != "" { + list, err = io.SearchX(req.Path, req.Keyword, req.Sub) + if err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } + } else { + list, err = stdos.ReadDir(req.Path) + if err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } } switch req.Sort { @@ -464,7 +452,10 @@ func (s *FileService) formatDir(base string, entries []stdos.DirEntry) []any { for item := range slices.Values(entries) { info, err := item.Info() if err != nil { - continue + continue // 直接跳过,不返回错误,不然很烦人的 + } + if de, ok := item.(*io.SearchEntry); ok { + base = filepath.Dir(de.Path()) } stat := info.Sys().(*syscall.Stat_t) diff --git a/internal/service/file_windows.go b/internal/service/file_windows.go index 19f4254b..f91c149d 100644 --- a/internal/service/file_windows.go +++ b/internal/service/file_windows.go @@ -53,6 +53,4 @@ func (s *FileService) Compress(w http.ResponseWriter, r *http.Request) {} func (s *FileService) UnCompress(w http.ResponseWriter, r *http.Request) {} -func (s *FileService) Search(w http.ResponseWriter, r *http.Request) {} - func (s *FileService) List(w http.ResponseWriter, r *http.Request) {} diff --git a/pkg/io/path.go b/pkg/io/path.go index 0ac8c9c4..cb3e874b 100644 --- a/pkg/io/path.go +++ b/pkg/io/path.go @@ -104,55 +104,3 @@ func CountX(path string) (int64, error) { count := len(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 out string - var err error - if sub { - out, err = shell.Execf("find '%s' -name '*%s*'", path, keyword) - } else { - out, err = shell.Execf("find '%s' -maxdepth 1 -name '*%s*'", path, keyword) - } - if err != nil { - return nil, err - } - - lines := strings.Split(out, "\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/pkg/io/search.go b/pkg/io/search.go new file mode 100644 index 00000000..7c5fe2bc --- /dev/null +++ b/pkg/io/search.go @@ -0,0 +1,100 @@ +package io + +import ( + "os" + "path/filepath" + "strings" + + "github.com/tnborg/panel/pkg/shell" +) + +// 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) ([]os.DirEntry, error) { + var out string + var err error + if sub { + out, err = shell.Execf("find '%s' -name '*%s*'", path, keyword) + } else { + out, err = shell.Execf("find '%s' -maxdepth 1 -name '*%s*'", path, keyword) + } + if err != nil { + return nil, err + } + + var entries []os.DirEntry + lines := strings.Split(out, "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" || line == path { + continue + } + entry, err := newDirEntryFromPath(line) + if err != nil { + continue // 直接跳过,不返回错误,不然很烦人的 + } + entries = append(entries, entry) + } + + return entries, nil +} + +// SearchEntry 实现 os.DirEntry 接口 +type SearchEntry struct { + path string + info os.FileInfo +} + +// newDirEntryFromPath 根据文件路径创建 SearchEntry +func newDirEntryFromPath(path string) (*SearchEntry, error) { + info, err := os.Stat(path) + if err != nil { + return nil, err + } + return &SearchEntry{path: path, info: info}, nil +} + +// Name 返回文件基本名称 +func (d *SearchEntry) Name() string { + return filepath.Base(d.path) +} + +// IsDir 判断是否为目录 +func (d *SearchEntry) IsDir() bool { + return d.info.IsDir() +} + +// Type 返回文件模式类型 +func (d *SearchEntry) Type() os.FileMode { + return d.info.Mode().Type() +} + +// Info 返回文件信息 +func (d *SearchEntry) Info() (os.FileInfo, error) { + return d.info, nil +} + +// Path 返回文件完整路径 +func (d *SearchEntry) Path() string { + return d.path +} diff --git a/pkg/io/search_test.go b/pkg/io/search_test.go new file mode 100644 index 00000000..20ba104c --- /dev/null +++ b/pkg/io/search_test.go @@ -0,0 +1,78 @@ +package io + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/suite" +) + +type SearchTestSuite struct { + suite.Suite +} + +func TestSearchTestSuite(t *testing.T) { + suite.Run(t, &SearchTestSuite{}) +} + +func (s *SearchTestSuite) SetupTest() { + if _, err := os.Stat("testdata"); os.IsNotExist(err) { + s.NoError(os.MkdirAll("testdata", 0755)) + } +} + +func (s *SearchTestSuite) TearDownTest() { + s.NoError(os.RemoveAll("testdata")) +} + +func (s *SearchTestSuite) TestSearchX() { + testDir := "testdata/search_test" + s.NoError(os.MkdirAll(testDir, 0755)) + s.NoError(os.MkdirAll(filepath.Join(testDir, "subdir"), 0755)) + + testFiles := map[string]string{ + "test_file1.txt": "内容1", + "test_file2.log": "内容2", + "another_test.txt": "内容3", + "subdir/nested_test.txt": "嵌套内容", + "unrelated.dat": "无关内容", + } + + for path, content := range testFiles { + s.NoError(Write(filepath.Join(testDir, path), content, 0644)) + } + + s.Run("正常搜索", func() { + entries, err := SearchX(testDir, "test", false) + s.NoError(err) + + names := make(map[string]bool) + for _, entry := range entries { + names[entry.Name()] = true + s.NotEmpty(entry.Name()) + info, err := entry.Info() + s.NoError(err) + s.NotNil(info) + s.Equal(entry.Type(), info.Mode().Type()) + s.Equal(entry.IsDir(), info.IsDir()) + } + + s.True(names["test_file1.txt"]) + s.True(names["test_file2.log"]) + s.True(names["another_test.txt"]) + s.False(names["nested_test.txt"]) // 不应该找到子目录中的文件 + s.False(names["unrelated.dat"]) // 不应该找到不匹配的文件 + }) + + s.Run("无匹配结果", func() { + entries, err := SearchX(testDir, "nonexistent", false) + s.NoError(err) + s.Empty(entries) + }) + + s.Run("路径不存在", func() { + _, err := SearchX("/path/does/not/exist", "test", false) + s.Error(err) + }) +} diff --git a/web/src/api/panel/file/index.ts b/web/src/api/panel/file/index.ts index f4e5dc47..edafe824 100644 --- a/web/src/api/panel/file/index.ts +++ b/web/src/api/panel/file/index.ts @@ -34,6 +34,12 @@ export default { search: (path: string, keyword: string, sub: boolean, page: number, limit: number): any => http.Get('/file/search', { params: { path, keyword, sub, page, limit } }), // 获取文件列表 - list: (path: string, page: number, limit: number, sort: string): any => - http.Get('/file/list', { params: { path, page, limit, sort } }) + list: ( + path: string, + keyword: string, + sub: boolean, + sort: string, + page: number, + limit: number + ): any => http.Get('/file/list', { params: { path, keyword, sub, sort, page, limit } }) } diff --git a/web/src/store/modules/file/index.ts b/web/src/store/modules/file/index.ts index ca701673..908c40f5 100644 --- a/web/src/store/modules/file/index.ts +++ b/web/src/store/modules/file/index.ts @@ -1,16 +1,22 @@ export interface File { path: string + keyword: string + sub: boolean } export const useFileStore = defineStore('file', { state: (): File => { return { - path: '/' + path: '/opt', + keyword: '', + sub: false } }, actions: { set(info: File) { this.path = info.path + this.keyword = info.keyword + this.sub = info.sub } }, persist: true diff --git a/web/src/views/file/IndexView.vue b/web/src/views/file/IndexView.vue index 38f604f9..d18996f0 100644 --- a/web/src/views/file/IndexView.vue +++ b/web/src/views/file/IndexView.vue @@ -24,7 +24,11 @@ const permission = ref(false) diff --git a/web/src/views/file/SearchModal.vue b/web/src/views/file/SearchModal.vue deleted file mode 100644 index 0beedeb9..00000000 --- a/web/src/views/file/SearchModal.vue +++ /dev/null @@ -1,178 +0,0 @@ - - - - -