Skip to content

Commit

Permalink
add File search endpoint (#44)
Browse files Browse the repository at this point in the history
* lets start with this

* Be able to get files and identify directory

* log the full path, needed later for the recursion

* we need to trim this to look good

* Return matched files in the same way list does it

* simplify some logic

* fix search in the middle of the file name

* we have recursion!

* move search in to its own file and add some config values

* use Contains for blacklist

* remove debug print & lowercase the blacklist

* make folders show first in the filesystem

* handle relative config path

* fix: symlinks detecting

* fix: config loading with default values

* Revert: e8ef74a as it does not work
  • Loading branch information
QuintenQVD0 authored Dec 6, 2024
1 parent 5858856 commit c2d1b27
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 20 deletions.
30 changes: 15 additions & 15 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,20 +91,20 @@ func init() {
}

func isDockerSnap() bool {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
log.Fatalf("Unable to initialize Docker client: %s", err)
}
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
log.Fatalf("Unable to initialize Docker client: %s", err)
}

defer cli.Close() // Close the client when the function returns (should not be needed, but just to be safe)
defer cli.Close() // Close the client when the function returns (should not be needed, but just to be safe)

info, err := cli.Info(context.Background())
if err != nil {
log.Fatalf("Unable to get Docker info: %s", err)
}
info, err := cli.Info(context.Background())
if err != nil {
log.Fatalf("Unable to get Docker info: %s", err)
}

// Check if Docker root directory contains '/var/snap/docker'
return strings.Contains(info.DockerRootDir, "/var/snap/docker")
// Check if Docker root directory contains '/var/snap/docker'
return strings.Contains(info.DockerRootDir, "/var/snap/docker")
}

func rootCmdRun(cmd *cobra.Command, _ []string) {
Expand Down Expand Up @@ -401,12 +401,12 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
// Reads the configuration from the disk and then sets up the global singleton
// with all the configuration values.
func initConfig() {
if !strings.HasPrefix(configPath, "/") {
d, err := os.Getwd()
if !filepath.IsAbs(configPath) {
d, err := filepath.Abs(configPath)
if err != nil {
log2.Fatalf("cmd/root: could not determine directory: %s", err)
log2.Fatalf("cmd/root: failed to get path to config file: %s", err)
}
configPath = path.Clean(path.Join(d, configPath))
configPath = d
}
err := config.FromFile(configPath)
if err != nil {
Expand Down
13 changes: 12 additions & 1 deletion config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"crypto/tls"
"fmt"
"github.com/gbrlsnchs/jwt/v3"
"os"
"os/exec"
"os/user"
Expand All @@ -17,6 +16,8 @@ import (
"text/template"
"time"

"github.com/gbrlsnchs/jwt/v3"

"emperror.dev/errors"
"github.com/acobaugh/osrelease"
"github.com/apex/log"
Expand Down Expand Up @@ -321,6 +322,7 @@ type Configuration struct {
// This is required to have the "Server Mounts" feature work properly.
AllowedMounts []string `json:"-" yaml:"allowed_mounts"`

SearchRecursion SearchRecursion `yaml:"Search"`
// BlockBaseDirMount indicates whether mounting to /home/container is blocked.
// If true, mounting to /home/container is blocked.
// If false, mounting to /home/container is allowed.
Expand All @@ -341,6 +343,15 @@ type Configuration struct {
IgnorePanelConfigUpdates bool `json:"ignore_panel_config_updates" yaml:"ignore_panel_config_updates"`
}

// SearchRecursion holds the configuration for directory search recursion settings.
type SearchRecursion struct {
// BlacklistedDirs is a list of directory names that should be excluded from the recursion.
BlacklistedDirs []string `default:"[\"node_modules\", \".wine\", \"appcache\", \"depotcache\", \"vendor\"]" yaml:"blacklisted_dirs" json:"blacklisted_dirs"`

// MaxRecursionDepth specifies the maximum depth for directory recursion.
MaxRecursionDepth int `default:"8" yaml:"max_recursion_depth" json:"max_recursion_depth"`
}

// NewAtPath creates a new struct and set the path where it should be stored.
// This function does not modify the currently stored global configuration.
func NewAtPath(path string) (*Configuration, error) {
Expand Down
1 change: 1 addition & 0 deletions router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ func Configure(m *wserver.Manager, client remote.Client) *gin.Engine {
files.POST("/compress", postServerCompressFiles)
files.POST("/decompress", postServerDecompressFiles)
files.POST("/chmod", postServerChmodFile)
files.GET("/search", getFilesBySearch)

files.GET("/pull", middleware.RemoteDownloadEnabled(), getServerPullingFiles)
files.POST("/pull", middleware.RemoteDownloadEnabled(), postServerPullRemoteFile)
Expand Down
3 changes: 2 additions & 1 deletion router/router_server_files.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func getServerFileContents(c *gin.Context) {

// Returns the contents of a directory for a server.
func getServerListDirectory(c *gin.Context) {
s := ExtractServer(c)
s := middleware.ExtractServer(c)
dir := c.Query("directory")
if stats, err := s.Filesystem().ListDirectory(dir); err != nil {
middleware.CaptureAndAbort(c, err)
Expand All @@ -89,6 +89,7 @@ func getServerListDirectory(c *gin.Context) {
}
}


type renameFile struct {
To string `json:"to"`
From string `json:"from"`
Expand Down
135 changes: 135 additions & 0 deletions router/router_server_files_search.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package router

import (
"net/http"
"path/filepath"
"strings"

"github.com/gin-gonic/gin"

"github.com/pelican-dev/wings/config"
"github.com/pelican-dev/wings/internal/ufs"
"github.com/pelican-dev/wings/router/middleware"
"github.com/pelican-dev/wings/server"
"github.com/pelican-dev/wings/server/filesystem"
)

// Structs needed to respond with the matched files and all their info
type customFileInfo struct {
ufs.FileInfo
newName string
}

func (cfi customFileInfo) Name() string {
return cfi.newName // Return the custom name (i.e., with the directory prefix)
}

// Helper function to append matched entries
func appendMatchedEntry(matchedEntries *[]filesystem.Stat, fileInfo ufs.FileInfo, fullPath string, fileType string) {
*matchedEntries = append(*matchedEntries, filesystem.Stat{
FileInfo: customFileInfo{
FileInfo: fileInfo,
newName: fullPath,
},
Mimetype: fileType,
})
}

// todo make this config value work as now it cause a panic
//var blacklist = config.Get().SearchRecursion.BlacklistedDirs

var blacklist = []string{"node_modules", ".wine", "appcache", "depotcache", "vendor"}

// Helper function to check if a directory name is in the blacklist
func isBlacklisted(dirName string) bool {
for _, blacklisted := range blacklist {
if strings.Contains(dirName, strings.ToLower(blacklisted)) {
return true
}
}
return false
}

// Recursive function to search through directories
func searchDirectory(s *server.Server, dir string, patternLower string, depth int, matchedEntries *[]filesystem.Stat, matchedDirectories *[]string, c *gin.Context) {
if depth > config.Get().SearchRecursion.MaxRecursionDepth {
return // Stop recursion if depth exceeds
}

stats, err := s.Filesystem().ListDirectory(dir)
if err != nil {
c.JSON(http.StatusOK, gin.H{"message": "Directory not found"})
return
}

for _, fileInfo := range stats {
fileName := fileInfo.Name()
fileType := fileInfo.Mimetype
fileNameLower := strings.ToLower(fileName)
fullPath := filepath.Join(dir, fileName)

// Store directories separately
if fileType == "inode/directory" {
if isBlacklisted(fileNameLower) {
continue // Skip blacklisted directories
}
*matchedDirectories = append(*matchedDirectories, fullPath)

// Recursive search in the matched directory
searchDirectory(s, fullPath, patternLower, depth+1, matchedEntries, matchedDirectories, c)
}

// Wildcard or exact matching logic
if strings.ContainsAny(patternLower, "*?") {
if match, _ := filepath.Match(patternLower, fileNameLower); match {
appendMatchedEntry(matchedEntries, fileInfo, fullPath, fileType)
}
} else {
// Check for substring matches (case-insensitive)
if strings.Contains(fileNameLower, patternLower) {
appendMatchedEntry(matchedEntries, fileInfo, fullPath, fileType)
} else {
// Extension matching logic
ext := filepath.Ext(fileNameLower)
if strings.HasPrefix(patternLower, ".") || !strings.Contains(patternLower, ".") {
// Match extension without dot
if strings.TrimPrefix(ext, ".") == strings.TrimPrefix(patternLower, ".") {
appendMatchedEntry(matchedEntries, fileInfo, fullPath, fileType)
}
} else if fileNameLower == patternLower { // Full name match
appendMatchedEntry(matchedEntries, fileInfo, fullPath, fileType)
}
}
}
}
}

func getFilesBySearch(c *gin.Context) {
s := middleware.ExtractServer(c)
dir := strings.TrimSuffix(c.Query("directory"), "/")
pattern := c.Query("pattern")

// Convert the pattern to lowercase for case-insensitive comparison
patternLower := strings.ToLower(pattern)

// Check if the pattern length is at least 3 characters
if len(pattern) < 3 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Pattern must be at least 3 characters long"})
return
}

// Prepare slices to store matched stats and directories
matchedEntries := []filesystem.Stat{}
matchedDirectories := []string{}

// Start the search from the initial directory
searchDirectory(s, dir, patternLower, 0, &matchedEntries, &matchedDirectories, c)

// Return the matched stats (only those that matched the pattern) and directories separately
if len(matchedEntries) == 0 && len(matchedDirectories) != 0 {
c.JSON(http.StatusOK, gin.H{"message": "No matches found."})
} else {
// Return all matched files with their stats and the name now included the directory
c.JSON(http.StatusOK, matchedEntries)
}
}
4 changes: 2 additions & 2 deletions server/filesystem/filesystem.go
Original file line number Diff line number Diff line change
Expand Up @@ -480,9 +480,9 @@ func (fs *Filesystem) ListDirectory(p string) ([]Stat, error) {
case a.IsDir() && b.IsDir():
return 0
case a.IsDir():
return 1
default:
return -1
default:
return 1
}
})

Expand Down
2 changes: 1 addition & 1 deletion server/filesystem/stat.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func (s *Stat) MarshalJSON() ([]byte, error) {
Size: s.Size(),
Directory: s.IsDir(),
File: !s.IsDir(),
Symlink: s.Mode().Perm()&ufs.ModeSymlink != 0,
Symlink: s.Mode().Type()&ufs.ModeSymlink != 0,
Mime: s.Mimetype,
})
}
Expand Down

0 comments on commit c2d1b27

Please sign in to comment.