diff --git a/router/router.go b/router/router.go index d748e27b8..6d39d77c8 100644 --- a/router/router.go +++ b/router/router.go @@ -87,6 +87,7 @@ func Configure(m *wserver.Manager, client remote.Client) *gin.Engine { files := server.Group("/files") { files.GET("/contents", getServerFileContents) + files.POST("/search", postServerSearchFiles ) files.GET("/list-directory", getServerListDirectory) files.PUT("/rename", putServerRenameFiles) files.POST("/copy", postServerCopyFile) diff --git a/router/router_server_files.go b/router/router_server_files.go index 09ad8cd1f..0beaef065 100644 --- a/router/router_server_files.go +++ b/router/router_server_files.go @@ -74,6 +74,86 @@ func getServerFileContents(c *gin.Context) { } } +type SearchResult struct { + File string `json:"file"` + Line int `json:"line"` + Snippet string `json:"snippet"` +} + + +// postServerSearchFiles handles fuzzy file content search +func postServerSearchFiles(c *gin.Context) { + s := middleware.ExtractServer(c) + maxResults := 100 + + var data struct { + Query string `json:"query"` + } + + if err := c.BindJSON(&data); err != nil { + return + } + + if data.Query == "" { + c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{ + "error": "No search query was provided.", + }) + return + } + + var results []SearchResult + + err := filesystem.WalkDirectory(s.Filesystem(), "/", func(p string, info filesystem.Stat, err error) error { + if err != nil { + return err + } + + if info.IsDir() { + return nil + } + + // Skip large files (> 5MB) for performance + if info.Size() > 5*1024*1024 { + return nil + } + + f, _, err := s.Filesystem().File(p) + if err != nil { + return nil // Skip files we can't read + } + defer f.Close() + + scanner := bufio.NewScanner(f) + lineNum := 1 + for scanner.Scan() { + line := scanner.Text() + haystack := line + needle := data.Query + + + if strings.Contains(haystack, needle) { + results = append(results, SearchResult{ + File: p, + Line: lineNum, + Snippet: line, + }) + if len(results) >= maxResults { + return io.EOF // Stop early when limit reached + } + } + lineNum++ + } + return nil + }) + + if err != nil && err != io.EOF { + middleware.CaptureAndAbort(c, err) + return + } + + c.JSON(http.StatusOK, results) +} + // Returns the contents of a directory for a server. func getServerListDirectory(c *gin.Context) { s := ExtractServer(c) diff --git a/server/filesystem/filesystem.go b/server/filesystem/filesystem.go index 42c56f2de..0a405cefa 100644 --- a/server/filesystem/filesystem.go +++ b/server/filesystem/filesystem.go @@ -19,6 +19,9 @@ import ( "github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/internal/ufs" + + "path" + ) type Filesystem struct { @@ -495,3 +498,27 @@ func (fs *Filesystem) Chtimes(path string, atime, mtime time.Time) error { } return fs.unixFS.Chtimes(path, atime, mtime) } + +// WalkDirectory recursively walks a directory and applies the callback function to each file. +func WalkDirectory(fs *Filesystem, dir string, fn func(string, Stat, error) error) error { + files, err := fs.ListDirectory(dir) + if err != nil { + return err + } + + for _, file := range files { + fullPath := path.Join(dir, file.Name()) + + if err := fn(fullPath, file, nil); err != nil { + return err + } + + if file.IsDir() { + if err := WalkDirectory(fs, fullPath, fn); err != nil { + return err + } + } + } + + return nil +}