mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 07:57:21 +08:00
feat: 优化文件搜索体验,close #997
This commit is contained in:
4
go.mod
4
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
|
||||
|
||||
18
go.sum
18
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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
100
pkg/io/search.go
Normal file
100
pkg/io/search.go
Normal file
@@ -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
|
||||
}
|
||||
78
pkg/io/search_test.go
Normal file
78
pkg/io/search_test.go
Normal file
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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 } })
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -24,7 +24,11 @@ const permission = ref(false)
|
||||
<template>
|
||||
<common-page show-footer>
|
||||
<n-flex vertical :size="20">
|
||||
<path-input v-model:path="fileStore.path" />
|
||||
<path-input
|
||||
v-model:path="fileStore.path"
|
||||
v-model:keyword="fileStore.keyword"
|
||||
v-model:sub="fileStore.sub"
|
||||
/>
|
||||
<tool-bar
|
||||
v-model:path="fileStore.path"
|
||||
v-model:selected="selected"
|
||||
@@ -35,6 +39,8 @@ const permission = ref(false)
|
||||
/>
|
||||
<list-table
|
||||
v-model:path="fileStore.path"
|
||||
v-model:keyword="fileStore.keyword"
|
||||
v-model:sub="fileStore.sub"
|
||||
v-model:selected="selected"
|
||||
v-model:marked="marked"
|
||||
v-model:markedType="markedType"
|
||||
|
||||
@@ -31,7 +31,9 @@ import type { Marked } from '@/views/file/types'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
const sort = ref<string>('')
|
||||
const path = defineModel<string>('path', { type: String, required: true })
|
||||
const path = defineModel<string>('path', { type: String, required: true }) // 当前路径
|
||||
const keyword = defineModel<string>('keyword', { type: String, default: '' }) // 搜索关键词
|
||||
const sub = defineModel<boolean>('sub', { type: Boolean, default: false }) // 搜索是否包括子目录
|
||||
const selected = defineModel<any[]>('selected', { type: Array, default: () => [] })
|
||||
const marked = defineModel<Marked[]>('marked', { type: Array, default: () => [] })
|
||||
const markedType = defineModel<string>('markedType', { type: String, required: true })
|
||||
@@ -389,7 +391,7 @@ const rowProps = (row: any) => {
|
||||
}
|
||||
|
||||
const { loading, data, page, total, pageSize, pageCount, refresh } = usePagination(
|
||||
(page, pageSize) => file.list(path.value, page, pageSize, sort.value),
|
||||
(page, pageSize) => file.list(path.value, keyword.value, sub.value, sort.value, page, pageSize),
|
||||
{
|
||||
initialData: { total: 0, list: [] },
|
||||
initialPageSize: 100,
|
||||
@@ -633,15 +635,21 @@ const handleSorterChange = (sorter: {
|
||||
switch (sorter.order) {
|
||||
case 'ascend':
|
||||
sort.value = 'asc'
|
||||
refresh()
|
||||
nextTick(() => {
|
||||
refresh()
|
||||
})
|
||||
break
|
||||
case 'descend':
|
||||
sort.value = 'desc'
|
||||
refresh()
|
||||
nextTick(() => {
|
||||
refresh()
|
||||
})
|
||||
break
|
||||
default:
|
||||
sort.value = ''
|
||||
refresh()
|
||||
nextTick(() => {
|
||||
refresh()
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -649,15 +657,29 @@ const handleSorterChange = (sorter: {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 监听路径变化并刷新列表
|
||||
watch(
|
||||
path,
|
||||
() => {
|
||||
selected.value = []
|
||||
refresh()
|
||||
window.$bus.emit('push-history', path.value)
|
||||
keyword.value = ''
|
||||
sub.value = false
|
||||
nextTick(() => {
|
||||
refresh()
|
||||
})
|
||||
window.$bus.emit('file:push-history', path.value)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
// 监听搜索事件
|
||||
window.$bus.on('file:search', () => {
|
||||
selected.value = []
|
||||
nextTick(() => {
|
||||
refresh()
|
||||
})
|
||||
window.$bus.emit('file:push-history', path.value)
|
||||
})
|
||||
// 监听刷新事件
|
||||
window.$bus.on('file:refresh', refresh)
|
||||
})
|
||||
|
||||
|
||||
@@ -3,10 +3,11 @@ import type { InputInst } from 'naive-ui'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
|
||||
import { checkPath } from '@/utils/file'
|
||||
import SearchModal from '@/views/file/SearchModal.vue'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
const path = defineModel<string>('path', { type: String, required: true })
|
||||
const path = defineModel<string>('path', { type: String, required: true }) // 当前路径
|
||||
const keyword = defineModel<string>('keyword', { type: String, default: '' }) // 搜索关键词
|
||||
const sub = defineModel<boolean>('sub', { type: Boolean, default: false }) // 搜索是否包括子目录
|
||||
const isInput = ref(false)
|
||||
const pathInput = ref<InputInst | null>(null)
|
||||
const input = ref('www')
|
||||
@@ -14,12 +15,6 @@ const input = ref('www')
|
||||
const history: string[] = []
|
||||
let current = -1
|
||||
|
||||
const searchShow = ref(false)
|
||||
const search = ref({
|
||||
keyword: '',
|
||||
sub: false
|
||||
})
|
||||
|
||||
const handleInput = () => {
|
||||
isInput.value = true
|
||||
nextTick(() => {
|
||||
@@ -92,7 +87,7 @@ const handlePushHistory = (path: string) => {
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
searchShow.value = true
|
||||
window.$bus.emit('file:search')
|
||||
}
|
||||
|
||||
watch(
|
||||
@@ -104,11 +99,11 @@ watch(
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
window.$bus.on('push-history', handlePushHistory)
|
||||
window.$bus.on('file:push-history', handlePushHistory)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.$bus.off('push-history')
|
||||
window.$bus.off('file:push-history')
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -151,9 +146,9 @@ onUnmounted(() => {
|
||||
/>
|
||||
</n-input-group>
|
||||
<n-input-group w-400>
|
||||
<n-input v-model:value="search.keyword" :placeholder="$gettext('Enter search content')">
|
||||
<n-input v-model:value="keyword" :placeholder="$gettext('Enter search content')">
|
||||
<template #suffix>
|
||||
<n-checkbox v-model:checked="search.sub">
|
||||
<n-checkbox v-model:checked="sub">
|
||||
{{ $gettext('Include subdirectories') }}
|
||||
</n-checkbox>
|
||||
</template>
|
||||
@@ -163,12 +158,6 @@ onUnmounted(() => {
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
</n-flex>
|
||||
<search-modal
|
||||
v-model:show="searchShow"
|
||||
v-model:path="path"
|
||||
v-model:keyword="search.keyword"
|
||||
v-model:sub="search.sub"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import file from '@/api/panel/file'
|
||||
import { NButton, NPopconfirm, NSpace, NTag } from 'naive-ui'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
|
||||
import copy2clipboard from '@vavt/copy2clipboard'
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
import type { RowData } from 'naive-ui/es/data-table/src/interface'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
const show = defineModel<boolean>('show', { type: Boolean, required: true })
|
||||
const path = defineModel<string>('path', { type: String, required: true })
|
||||
const keyword = defineModel<string>('keyword', { type: String, required: true })
|
||||
const sub = defineModel<boolean>('sub', { type: Boolean, required: true })
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const columns: DataTableColumns<RowData> = [
|
||||
{
|
||||
title: $gettext('Name'),
|
||||
key: 'full',
|
||||
minWidth: 300,
|
||||
ellipsis: {
|
||||
tooltip: true
|
||||
}
|
||||
},
|
||||
{
|
||||
title: $gettext('Size'),
|
||||
key: 'size',
|
||||
width: 80,
|
||||
render(row: any): any {
|
||||
return h(NTag, { type: 'info', size: 'small', bordered: false }, { default: () => row.size })
|
||||
}
|
||||
},
|
||||
{
|
||||
title: $gettext('Modification Time'),
|
||||
key: 'modify',
|
||||
width: 200,
|
||||
render(row: any): any {
|
||||
return h(
|
||||
NTag,
|
||||
{ type: 'warning', size: 'small', bordered: false },
|
||||
{ default: () => row.modify }
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: $gettext('Actions'),
|
||||
key: 'action',
|
||||
width: 200,
|
||||
render(row) {
|
||||
return h(
|
||||
NSpace,
|
||||
{},
|
||||
{
|
||||
default: () => [
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'success',
|
||||
tertiary: true,
|
||||
onClick: () => {
|
||||
copy2clipboard(row.full).then(() => {
|
||||
window.$message.success($gettext('Copied successfully'))
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
return $gettext('Copy Path')
|
||||
}
|
||||
}
|
||||
),
|
||||
h(
|
||||
NPopconfirm,
|
||||
{
|
||||
onPositiveClick: () => {
|
||||
useRequest(file.delete(row.full)).onSuccess(() => {
|
||||
window.$bus.emit('file:refresh')
|
||||
window.$message.success($gettext('Deleted successfully'))
|
||||
})
|
||||
},
|
||||
onNegativeClick: () => {}
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
return $gettext('Are you sure you want to delete %{ name }?', { name: row.name })
|
||||
},
|
||||
trigger: () => {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'error',
|
||||
tertiary: true
|
||||
},
|
||||
{ default: () => $gettext('Delete') }
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const data = ref<RowData[]>([])
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageCount: 1,
|
||||
pageSize: 100,
|
||||
itemCount: 0,
|
||||
showQuickJumper: true,
|
||||
showSizePicker: true,
|
||||
pageSizes: [100, 200, 500, 1000, 1500, 2000, 5000]
|
||||
})
|
||||
|
||||
const handlePageSizeChange = (pageSize: number) => {
|
||||
pagination.pageSize = pageSize
|
||||
handlePageChange(1)
|
||||
}
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
search(page)
|
||||
}
|
||||
|
||||
const search = async (page: number) => {
|
||||
loading.value = true
|
||||
useRequest(
|
||||
file.search(path.value, keyword.value, sub.value, page, pagination.pageSize!)
|
||||
).onSuccess(({ data }) => {
|
||||
data.value = data.items
|
||||
pagination.itemCount = data.total
|
||||
pagination.pageCount = data.total / pagination.pageSize! + 1
|
||||
})
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
watch(show, (value) => {
|
||||
if (value) {
|
||||
search(1)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="show"
|
||||
preset="card"
|
||||
:title="$gettext('%{ keyword } - Search Results', { keyword })"
|
||||
style="width: 60vw"
|
||||
size="huge"
|
||||
:bordered="false"
|
||||
:segmented="false"
|
||||
>
|
||||
<n-data-table
|
||||
remote
|
||||
striped
|
||||
virtual-scroll
|
||||
size="small"
|
||||
:scroll-x="800"
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:row-key="(row: any) => row.full"
|
||||
max-height="60vh"
|
||||
@update:page="handlePageChange"
|
||||
@update:page-size="handlePageSizeChange"
|
||||
/>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
Reference in New Issue
Block a user