From 28091cb98e164ba5cd43a4a50e56480c3e88bcbb Mon Sep 17 00:00:00 2001 From: LTLA Date: Sat, 19 Oct 2024 22:13:56 -0700 Subject: [PATCH] Added more options for filtering the list of registered directories. This primarily involves filtering on the paths - namely, whether the registered directory contains a particular path or vice versa. --- README.md | 6 ++++- database.go | 52 ++++++++++++++++++++++++++++++-------- database_test.go | 51 +++++++++++++++++++++++++++++++++++++ handlers.go | 33 +++++++++++++++++++----- handlers_test.go | 66 ++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 189 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 6a94543..f57d702 100644 --- a/README.md +++ b/README.md @@ -398,7 +398,11 @@ On success, this returns an array of objects containing: - `time`, the Unix time of the registration. - `names`, the base names of the metadata files to be indexed in this directory. -This can be filtered on user by passing the `user=` query parameter in the request. +This can be filtered by passing additional query parameters: + +- `user=`, which filters on the `user`. +- `contains_path=`, which filters for `path` that contain (i.e., are parents of) the specified path. +- `path_prefix=`, which filters for `path` that start with the specified prefix. On error, the response may either be `text/plain` content containing the error message directly, or `application/json` content encoding a JSON object with the `reason` for the error. diff --git a/database.go b/database.go index 72839cf..02eb42e 100644 --- a/database.go +++ b/database.go @@ -756,6 +756,8 @@ func retrievePath(db * sql.DB, path string, include_metadata bool) (*queryResult type listRegisteredDirectoriesQuery struct { User *string `json:"user"` + ContainsPath *string `json:"contains_path"` + PathPrefix *string `json:"path_prefix"` } type listRegisteredDirectoriesResult struct { @@ -775,6 +777,24 @@ func listRegisteredDirectories(db * sql.DB, query *listRegisteredDirectoriesQuer parameters = append(parameters, *(query.User)) } + if query.ContainsPath != nil { + collected, err := stripPaths(*(query.ContainsPath)) + if err != nil { + return nil, err + } + query_clause := "?" + for i := 1; i < len(collected); i++ { + query_clause += ", ?" + } + filters = append(filters, "path IN (" + query_clause + ")") + parameters = append(parameters, collected...) + } + + if query.PathPrefix != nil { + filters = append(filters, "path LIKE ?") + parameters = append(parameters, *(query.PathPrefix) + "%") + } + if len(filters) > 0 { q = q + " WHERE " + strings.Join(filters, " AND ") } @@ -800,18 +820,17 @@ func listRegisteredDirectories(db * sql.DB, query *listRegisteredDirectoriesQuer return output, nil } -func isDirectoryRegistered(db * sql.DB, path string) (bool, error) { +func stripPaths(path string) ([]interface{}, error) { collected := []interface{}{} for { info, err := os.Lstat(path) // Lstat() is deliberate as we need to distinguish symlinks, see below. + if errors.Is(err, os.ErrNotExist) { + return nil, newHttpError(http.StatusNotFound, errors.New("path at does not exist")) + } else if err != nil { + return nil, fmt.Errorf("inaccessible path at %q; %v", path, err) + } - if err != nil { - if errors.Is(err, os.ErrNotExist) { - return false, newHttpError(http.StatusNotFound, errors.New("path does not exist")) - } else { - return false, fmt.Errorf("inaccessible path; %v", err) - } - } else if info.Mode() & fs.ModeSymlink != 0 { + if info.Mode() & fs.ModeSymlink != 0 { // Symlinks to directories within a registered directory are not // followed during registration or updates. This allows us to quit // the loop and search on the current 'collected'; if any of these @@ -820,8 +839,10 @@ func isDirectoryRegistered(db * sql.DB, path string) (bool, error) { // fine as the symlink would have been inside the registered // directory of subsequent additions to 'collected'. break - } else if !info.IsDir() { - return false, newHttpError(http.StatusBadRequest, errors.New("path should refer to a directory")) + } + + if !info.IsDir() { + return nil, newHttpError(http.StatusBadRequest, errors.New("path should refer to a directory")) } // Incidentally, note that there's no need to defend against '..', as @@ -835,6 +856,15 @@ func isDirectoryRegistered(db * sql.DB, path string) (bool, error) { path = newpath } + return collected, nil +} + +func isDirectoryRegistered(db * sql.DB, path string) (bool, error) { + collected, err := stripPaths(path) + if err != nil { + return false, err + } + if len(collected) == 0 { return false, nil } @@ -846,7 +876,7 @@ func isDirectoryRegistered(db * sql.DB, path string) (bool, error) { q := fmt.Sprintf("SELECT COUNT(1) FROM dirs WHERE path IN (%s)", query) row := db.QueryRow(q, collected...) var num int - err := row.Scan(&num) + err = row.Scan(&num) if err != nil { return false, err diff --git a/database_test.go b/database_test.go index 138726e..62a9c3f 100644 --- a/database_test.go +++ b/database_test.go @@ -1769,6 +1769,57 @@ func TestListRegisteredDirectories(t *testing.T) { t.Fatal("should have found no matching paths") } }) + + t.Run("filtered on contains_path", func(t *testing.T) { + query := listRegisteredDirectoriesQuery{} + + desired := filepath.Join(tmp, "bar") + query.ContainsPath = &desired + + out, err := listRegisteredDirectories(dbconn, &query) + if err != nil { + t.Fatal(err) + } + if len(out) != 1 { + t.Fatal("should have found one matching path") + } + if out[0].Path != desired { + t.Fatalf("unexpected entry %v", out[0]) + } + + desired = filepath.Join(filepath.Dir(tmp)) + query.ContainsPath = &desired + out, err = listRegisteredDirectories(dbconn, &query) + if err != nil { + t.Fatal(err) + } + if len(out) != 0 { + t.Fatal("should have found no matching paths") + } + }) + + t.Run("filtered on has_prefix", func(t *testing.T) { + query := listRegisteredDirectoriesQuery{} + query.PathPrefix = &tmp + + out, err := listRegisteredDirectories(dbconn, &query) + if err != nil { + t.Fatal(err) + } + if len(out) != 2 { + t.Fatal("should have found two matching paths") + } + + absent := tmp + "_asdasd" + query.PathPrefix = &absent + out, err = listRegisteredDirectories(dbconn, &query) + if err != nil { + t.Fatal(err) + } + if len(out) != 0 { + t.Fatal("should have found no matching paths") + } + }) } func TestIsDirectoryRegistered(t *testing.T) { diff --git a/handlers.go b/handlers.go index 25c1171..cc98ba2 100644 --- a/handlers.go +++ b/handlers.go @@ -436,19 +436,22 @@ func newQueryHandler(db *sql.DB, tokenizer *unicodeTokenizer, wild_tokenizer *un /**********************************************************************/ -func getRetrievePath(params url.Values) (string, error) { - if !params.Has("path") { - return "", errors.New("expected a 'path' query parameter") - } - - path, err := url.QueryUnescape(params.Get("path")) +func sanitizePath(path string) (string, error) { + path, err := url.QueryUnescape(path) if err != nil { return "", fmt.Errorf("path is not properly URL-encoded; %w", err) } - return filepath.Clean(path), nil } +func getRetrievePath(params url.Values) (string, error) { + if !params.Has("path") { + return "", errors.New("expected a 'path' query parameter") + } + path, err := sanitizePath(params.Get("path")) + return path, err +} + func newRetrieveMetadataHandler(db *sql.DB) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { if configureCors(w, r) { @@ -622,6 +625,22 @@ func newListRegisteredDirectoriesHandler(db *sql.DB) func(http.ResponseWriter, * user := params.Get("user") query.User = &user } + if params.Has("contains_path") { + path, err := sanitizePath(params.Get("contains_path")) + if err != nil { + dumpHttpErrorResponse(w, newHttpError(http.StatusBadRequest, err)) + return + } + query.ContainsPath = &path + } + if params.Has("path_prefix") { + path, err := sanitizePath(params.Get("path_prefix")) + if err != nil { + dumpHttpErrorResponse(w, newHttpError(http.StatusBadRequest, err)) + return + } + query.PathPrefix = &path + } output, err := listRegisteredDirectories(db, &query) if err != nil { diff --git a/handlers_test.go b/handlers_test.go index 1ef34d9..d80b3cb 100644 --- a/handlers_test.go +++ b/handlers_test.go @@ -1304,4 +1304,70 @@ func TestListRegisteredDirectoriesHandler(t *testing.T) { t.Fatalf("unexpected listing results %q", r) } }) + + t.Run("filtered by contains_path", func (t *testing.T) { + inside := filepath.Join(tmp, "to_add_akari", "stuff") + encoded := url.QueryEscape(inside) + req, err := http.NewRequest("GET", "/registered?contains_path=" + encoded, nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + if rr.Code != http.StatusOK { + t.Fatalf("should have succeeded (got %d)", rr.Code) + } + + type lrdResult struct { + Path string + User string + Time int64 + Names []string + } + + r := []lrdResult{} + dec := json.NewDecoder(rr.Body) + err = dec.Decode(&r) + if err != nil { + t.Fatal(err) + } + + if len(r) != 1 || r[0].User != "akari" { + t.Fatalf("unexpected listing results %q", r) + } + }) + + t.Run("filtered by has_prefix", func (t *testing.T) { + encoded := url.QueryEscape(tmp) + req, err := http.NewRequest("GET", "/registered?has_prefix=" + encoded, nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + if rr.Code != http.StatusOK { + t.Fatalf("should have succeeded (got %d)", rr.Code) + } + + type lrdResult struct { + Path string + User string + Time int64 + Names []string + } + + r := []lrdResult{} + dec := json.NewDecoder(rr.Body) + err = dec.Decode(&r) + if err != nil { + t.Fatal(err) + } + + if len(r) != 3 { + t.Fatalf("unexpected listing results %q", r) + } + }) + }