Skip to content

Commit

Permalink
Added more options for filtering the list of registered directories.
Browse files Browse the repository at this point in the history
This primarily involves filtering on the paths - namely, whether the registered
directory contains a particular path or vice versa.
  • Loading branch information
LTLA committed Oct 20, 2024
1 parent f2ba84d commit 28091cb
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 19 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
52 changes: 41 additions & 11 deletions database.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 ")
}
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
}
Expand All @@ -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
Expand Down
51 changes: 51 additions & 0 deletions database_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
33 changes: 26 additions & 7 deletions handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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 {
Expand Down
66 changes: 66 additions & 0 deletions handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})

}

0 comments on commit 28091cb

Please sign in to comment.