From ef26244aff69ea9394f69142a99fa1816a510319 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Sun, 30 Dec 2018 16:39:21 +0000 Subject: [PATCH] feat: search working again License: MIT Signed-off-by: Henrique Dias --- http/http.go | 2 + http/websockets.go | 196 +++++++++++++++++++++++++++++++++++++++++++ search/conditions.go | 102 ++++++++++++++++++++++ search/search.go | 57 +++++++++++++ 4 files changed, 357 insertions(+) create mode 100644 http/websockets.go create mode 100644 search/conditions.go create mode 100644 search/search.go diff --git a/http/http.go b/http/http.go index e8f95a70..71de70e3 100644 --- a/http/http.go +++ b/http/http.go @@ -63,6 +63,8 @@ func Handler(e *Env) http.Handler { api.HandleFunc("/settings", e.auth(e.settingsPutHandler)).Methods("PUT") api.PathPrefix("/raw").HandlerFunc(e.auth(e.rawHandler)).Methods("GET") + api.PathPrefix("/commands").HandlerFunc(e.auth(e.commandsHandler)) + api.PathPrefix("/search").HandlerFunc(e.auth(e.searchHandler)) return r } diff --git a/http/websockets.go b/http/websockets.go new file mode 100644 index 00000000..d7a54e78 --- /dev/null +++ b/http/websockets.go @@ -0,0 +1,196 @@ +package http + +import ( + "encoding/json" + "net/http" + "os" + "strings" + + "github.com/filebrowser/filebrowser/search" + + "github.com/gorilla/websocket" +) + +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, +} + +var ( + cmdNotImplemented = []byte("Command not implemented.") + cmdNotAllowed = []byte("Command not allowed.") +) + +func (e *Env) commandsHandler(w http.ResponseWriter, r *http.Request) { + /* user, ok := e.getUser(w, r) + if !ok { + return + } + + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + httpErr(w, r, http.StatusInternalServerError, err) + return + } + defer conn.Close() + + var ( + message []byte + command []string + ) + + // Starts an infinite loop until a valid command is captured. + for { + _, message, err = conn.ReadMessage() + if err != nil { + httpErr(w, r, http.StatusInternalServerError, err) + return + } + + command = strings.Split(string(message), " ") + if len(command) != 0 { + break + } + } + + allowed := false + + for _, cmd := range user.Commands { + if regexp.MustCompile(cmd).MatchString(command[0]) { + allowed = true + break + } + } + + if !allowed { + err = conn.WriteMessage(websocket.TextMessage, cmdNotAllowed) + if err != nil { + httpErr(w, r, http.StatusInternalServerError, err) + return + } + + return + } + + // Check if the program is installed on the computer. + if _, err = exec.LookPath(command[0]); err != nil { + err = conn.WriteMessage(websocket.TextMessage, cmdNotImplemented) + if err != nil { + httpErr(w, r, http.StatusInternalServerError, err) + return + } else { + httpErr + } + + return http.StatusNotImplemented, nil + } + + // Gets the path and initializes a buffer. + path := c.User.Scope + "/" + r.URL.Path + path = filepath.Clean(path) + buff := new(bytes.Buffer) + + // Sets up the command executation. + cmd := exec.Command(command[0], command[1:]...) + cmd.Dir = path + cmd.Stderr = buff + cmd.Stdout = buff + + // Starts the command and checks for fb.Errors. + err = cmd.Start() + if err != nil { + return http.StatusInternalServerError, err + } + + // Set a 'done' variable to check whetever the command has already finished + // running or not. This verification is done using a goroutine that uses the + // method .Wait() from the command. + done := false + go func() { + err = cmd.Wait() + done = true + }() + + // Function to print the current information on the buffer to the connection. + print := func() error { + by := buff.Bytes() + if len(by) > 0 { + err = conn.WriteMessage(websocket.TextMessage, by) + if err != nil { + return err + } + } + + return nil + } + + // While the command hasn't finished running, continue sending the output + // to the client in intervals of 100 milliseconds. + for !done { + if err = print(); err != nil { + return http.StatusInternalServerError, err + } + + time.Sleep(100 * time.Millisecond) + } + + // After the command is done executing, send the output one more time to the + // browser to make sure it gets the latest information. + if err = print(); err != nil { + return http.StatusInternalServerError, err + } + + return 0, nil */ + +} + +func (e *Env) searchHandler(w http.ResponseWriter, r *http.Request) { + user, ok := e.getUser(w, r) + if !ok { + return + } + + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + httpErr(w, r, http.StatusInternalServerError, err) + return + } + defer conn.Close() + + var ( + value string + message []byte + ) + + for { + _, message, err = conn.ReadMessage() + if err != nil { + httpErr(w, r, http.StatusInternalServerError, err) + return + } + + if len(message) != 0 { + value = string(message) + break + } + } + + scope := strings.TrimPrefix(r.URL.Path, "/api/search") + err = search.Search(user.Fs, scope, value, func(path string, f os.FileInfo) error { + if !user.IsAllowed(path) { + return nil + } + + response, _ := json.Marshal(map[string]interface{}{ + "dir": f.IsDir(), + "path": path, + }) + + return conn.WriteMessage(websocket.TextMessage, response) + }) + + if err != nil { + httpErr(w, r, http.StatusInternalServerError, err) + return + } +} diff --git a/search/conditions.go b/search/conditions.go new file mode 100644 index 00000000..d4d98fed --- /dev/null +++ b/search/conditions.go @@ -0,0 +1,102 @@ +package search + +import ( + "mime" + "path/filepath" + "regexp" + "strings" +) + +var ( + typeRegexp = regexp.MustCompile(`type:(\w+)`) +) + +type condition func(path string) bool + +func extensionCondition(extension string) condition { + return func(path string) bool { + return filepath.Ext(path) == "."+extension + } +} + +func imageCondition(path string) bool { + extension := filepath.Ext(path) + mimetype := mime.TypeByExtension(extension) + + return strings.HasPrefix(mimetype, "image") +} + +func audioCondition(path string) bool { + extension := filepath.Ext(path) + mimetype := mime.TypeByExtension(extension) + + return strings.HasPrefix(mimetype, "audio") +} + +func videoCondition(path string) bool { + extension := filepath.Ext(path) + mimetype := mime.TypeByExtension(extension) + + return strings.HasPrefix(mimetype, "video") +} + +func parseSearch(value string) *searchOptions { + opts := &searchOptions{ + CaseSensitive: strings.Contains(value, "case:sensitive"), + Conditions: []condition{}, + Terms: []string{}, + } + + // removes the options from the value + value = strings.Replace(value, "case:insensitive", "", -1) + value = strings.Replace(value, "case:sensitive", "", -1) + value = strings.TrimSpace(value) + + types := typeRegexp.FindAllStringSubmatch(value, -1) + for _, t := range types { + if len(t) == 1 { + continue + } + + switch t[1] { + case "image": + opts.Conditions = append(opts.Conditions, imageCondition) + case "audio", "music": + opts.Conditions = append(opts.Conditions, audioCondition) + case "video": + opts.Conditions = append(opts.Conditions, videoCondition) + default: + opts.Conditions = append(opts.Conditions, extensionCondition(t[1])) + } + } + + if len(types) > 0 { + // Remove the fields from the search value. + value = typeRegexp.ReplaceAllString(value, "") + } + + // If it's canse insensitive, put everything in lowercase. + if !opts.CaseSensitive { + value = strings.ToLower(value) + } + + // Remove the spaces from the search value. + value = strings.TrimSpace(value) + + if value == "" { + return opts + } + + // if the value starts with " and finishes what that character, we will + // only search for that term + if value[0] == '"' && value[len(value)-1] == '"' { + unique := strings.TrimPrefix(value, "\"") + unique = strings.TrimSuffix(unique, "\"") + + opts.Terms = []string{unique} + return opts + } + + opts.Terms = strings.Split(value, " ") + return opts +} diff --git a/search/search.go b/search/search.go new file mode 100644 index 00000000..0f4fa712 --- /dev/null +++ b/search/search.go @@ -0,0 +1,57 @@ +package search + +import ( + "os" + "strings" + + "github.com/spf13/afero" +) + +type searchOptions struct { + CaseSensitive bool + Conditions []condition + Terms []string +} + +// Search searches for a query in a fs. +func Search(fs afero.Fs, scope string, query string, found func(path string, f os.FileInfo) error) error { + search := parseSearch(query) + + return afero.Walk(fs, scope, func(path string, f os.FileInfo, err error) error { + path = strings.TrimPrefix(path, "/") + path = strings.Replace(path, "\\", "/", -1) + + if !search.CaseSensitive { + path = strings.ToLower(path) + } + + if !search.CaseSensitive { + path = strings.ToLower(path) + } + + if len(search.Conditions) > 0 { + match := false + + for _, t := range search.Conditions { + if t(path) { + match = true + break + } + } + + if !match { + return nil + } + } + + if len(search.Terms) > 0 { + for _, term := range search.Terms { + if strings.Contains(path, term) { + return found(strings.TrimPrefix(path, scope), f) + } + } + } + + return nil + }) +}