Skip to content

Commit

Permalink
Added endpoint to list currently registered directories.
Browse files Browse the repository at this point in the history
This is useful to get some quick statistics on who has registered what,
whether a directory needs to be re-/de-registered, etc.
  • Loading branch information
LTLA committed Sep 14, 2024
1 parent 091a043 commit ae399f7
Show file tree
Hide file tree
Showing 6 changed files with 286 additions and 0 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,26 @@ On error, the exact response may either be `text/plain` content containing the e
or `application/json` content encoding a JSON object with the `reason` for the error.
If the path does not exist in the index, a standard 404 error is returned.

### Identifying registered directories

We can determine which directories are actually registered by making a GET request to the `/registered` endpoint of the SewerRat API.

```shell
curl -L ${SEWER_RAT_URL}/registered | jq
```

On success, this returns an array of objects containing:

- `path`, a string containing the path to the registered directory.
- `user`, the identity of the user who registered this directory.
- `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.

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.

## Administration

Clone this repository and build the binary.
Expand Down
47 changes: 47 additions & 0 deletions database.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"time"
"sync"
"errors"
"strings"
"encoding/json"
"path/filepath"
"io/fs"
Expand Down Expand Up @@ -743,6 +744,52 @@ func retrievePath(db * sql.DB, path string, include_metadata bool) (*queryResult

/**********************************************************************/

type listRegisteredDirectoriesQuery struct {
User *string `json:"user"`
}

type listRegisteredDirectoriesResult struct {
Path string `json:"path"`
User string `json:"user"`
Time int64 `json:"time"`
Names json.RawMessage `json:"names"`
}

func listRegisteredDirectories(db * sql.DB, query *listRegisteredDirectoriesQuery) ([]listRegisteredDirectoriesResult, error) {
q := "SELECT path, user, time, json_extract(names, '$') FROM dirs"

filters := []string{}
parameters := []interface{}{}
if query.User != nil {
filters = append(filters, "user == ?")
parameters = append(parameters, *(query.User))
}

if len(filters) > 0 {
q = q + " WHERE " + strings.Join(filters, " AND ")
}

rows, err := db.Query(q, parameters...)
if err != nil {
return nil, fmt.Errorf("failed to list registered directories; %w", err)
}
defer rows.Close()

output := []listRegisteredDirectoriesResult{}
for rows.Next() {
current := listRegisteredDirectoriesResult{}
var names string
err := rows.Scan(&(current.Path), &(current.User), &(current.Time), &names)
current.Names = []byte(names)
if err != nil {
return nil, fmt.Errorf("failed to traverse rows of the 'dir' table; %w", err)
}
output = append(output, current)
}

return output, nil
}

func isDirectoryRegistered(db * sql.DB, path string) (bool, error) {
collected := []interface{}{}
for {
Expand Down
86 changes: 86 additions & 0 deletions database_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"strings"
"errors"
"database/sql"
"encoding/json"
)

func TestInitializeDatabase(t *testing.T) {
Expand Down Expand Up @@ -1675,6 +1676,91 @@ func TestRetrievePath(t *testing.T) {
})
}

func TestListRegisteredDirectories(t *testing.T) {
tmp, err := os.MkdirTemp("", "")
if err != nil {
t.Fatalf(err.Error())
}
defer os.RemoveAll(tmp)

dbpath := filepath.Join(tmp, "db.sqlite3")
dbconn, err := initializeDatabase(dbpath)
if err != nil {
t.Fatalf(err.Error())
}
defer dbconn.Close()

tokr, err := newUnicodeTokenizer(false)
if err != nil {
t.Fatalf(err.Error())
}

// Mocking up some contents.
for _, name := range []string{ "foo", "bar" } {
to_add := filepath.Join(tmp, name)
err = mockDirectory(to_add)
if err != nil {
t.Fatalf(err.Error())
}

comments, err := addNewDirectory(dbconn, to_add, []string{ "metadata.json", "other.json" }, name + "_user", tokr)
if err != nil {
t.Fatalf(err.Error())
}
if len(comments) > 0 {
t.Fatalf("unexpected comments from the directory addition %v", comments)
}
}

t.Run("basic", func(t *testing.T) {
query := listRegisteredDirectoriesQuery{}
out, err := listRegisteredDirectories(dbconn, &query)
if err != nil {
t.Fatal(err)
}
if len(out) != 2 {
t.Fatal("should have found two matching paths")
}

for i := 0; i < 2; i++ {
name := "foo"
if i == 1 {
name = "bar"
}

if out[i].User != name + "_user" || filepath.Base(out[i].Path) != name || out[i].Time == 0 {
t.Fatalf("unexpected entry for path %d; %v", i, out[i])
}

var payload []string
err = json.Unmarshal(out[0].Names, &payload)
if err != nil {
t.Fatalf("failed to unmashal names; %v", string(out[0].Names))
}

if len(payload) != 2 || payload[0] != "metadata.json" || payload[1] != "other.json" {
t.Fatalf("unexpected value for names; %v", payload)
}
}
})

t.Run("filtered on user", func(t *testing.T) {
query := listRegisteredDirectoriesQuery{}
desired := "bar_user"
query.User = &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].User != desired {
t.Fatalf("unexpected entry %v", out[0])
}
})
}

func TestIsDirectoryRegistered(t *testing.T) {
tmp, err := os.MkdirTemp("", "")
if err != nil {
Expand Down
28 changes: 28 additions & 0 deletions handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,34 @@ func newListFilesHandler(db *sql.DB) func(http.ResponseWriter, *http.Request) {
}
}

func newListRegisteredDirectoriesHandler(db *sql.DB) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
if configureCors(w, r) {
return
}
if r.Method != "GET" {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}

query := listRegisteredDirectoriesQuery{}

params := r.URL.Query()
if params.Has("user") {
user := params.Get("user")
query.User = &user
}

output, err := listRegisteredDirectories(db, &query)
if err != nil {
dumpHttpErrorResponse(w, fmt.Errorf("failed to check registered directories; %w", err))
return
}

dumpJsonResponse(w, http.StatusOK, output)
}
}

/**********************************************************************/

func newDefaultHandler() func(http.ResponseWriter, *http.Request) {
Expand Down
104 changes: 104 additions & 0 deletions handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1201,3 +1201,107 @@ func TestListFilesHandler(t *testing.T) {
}
})
}

func TestListRegisteredDirectoriesHandler(t *testing.T) {
tmp, err := os.MkdirTemp("", "")
if err != nil {
t.Fatalf(err.Error())
}
defer os.RemoveAll(tmp)

dbpath := filepath.Join(tmp, "db.sqlite3")
dbconn, err := initializeDatabase(dbpath)
if err != nil {
t.Fatalf(err.Error())
}
defer dbconn.Close()

tokr, err := newUnicodeTokenizer(false)
if err != nil {
t.Fatalf(err.Error())
}

for _, name := range []string{ "akari", "ai", "alice" } {
to_add := filepath.Join(tmp, "to_add_" + name)
err = mockDirectory(to_add)
if err != nil {
t.Fatalf(err.Error())
}

comments, err := addNewDirectory(dbconn, to_add, []string{"metadata.json"}, name, tokr)
if err != nil {
t.Fatal(err)
}
if len(comments) != 0 {
t.Fatal("no comments should be present")
}
}

handler := http.HandlerFunc(newListRegisteredDirectoriesHandler(dbconn))

t.Run("basic", func (t *testing.T) {
req, err := http.NewRequest("GET", "/registered", 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 || r[0].User != "akari" || r[1].User != "ai" || r[2].User != "alice" {
t.Fatalf("unexpected listing results for the users %q", r)
}

if filepath.Base(r[0].Path) != "to_add_akari" || r[0].Time == 0 || r[0].Names[0] != "metadata.json" {
t.Fatalf("unexpected listing results for the first entry %q", r)
}
})

t.Run("filtered by user", func (t *testing.T) {
req, err := http.NewRequest("GET", "/registered?user=alice", 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 != "alice" || filepath.Base(r[0].Path) != "to_add_alice" || r[0].Time == 0 || r[0].Names[0] != "metadata.json" {
t.Fatalf("unexpected listing results %q", r)
}
})
}
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ func main() {
http.HandleFunc("POST " + prefix + "/deregister/start", newDeregisterStartHandler(db, verifier))
http.HandleFunc("POST " + prefix + "/deregister/finish", newDeregisterFinishHandler(db, verifier, timeout))

http.HandleFunc(prefix + "/registered", newListRegisteredDirectoriesHandler(db))
http.HandleFunc(prefix + "/query", newQueryHandler(db, tokenizer, wild_tokenizer, "/query"))
http.HandleFunc(prefix + "/retrieve/metadata", newRetrieveMetadataHandler(db))
http.HandleFunc(prefix + "/retrieve/file", newRetrieveFileHandler(db))
Expand Down

0 comments on commit ae399f7

Please sign in to comment.