mirror of
https://github.com/Noooste/garage-ui.git
synced 2026-08-18 00:57:53 +00:00
3098e474f2
* feat(search): implement recursive substring search for bucket objects * fix: update key formatting to remove trailing slashes in ObjectsTable * feat(search): implement debounced search functionality and improve UI text clarity * test(objects): add test for handling search error response
122 lines
3.9 KiB
Go
122 lines
3.9 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// s3SearchHandler serves a two-page recursive ListObjectsV2 response and counts
|
|
// HEAD (StatObject) requests. Page selection is driven by the
|
|
// `continuation-token` query parameter, mirroring how the MinIO SDK paginates.
|
|
func s3SearchHandler(bucket string, page1, page2 []struct {
|
|
Key string
|
|
Size int64
|
|
LastModified string
|
|
ETag string
|
|
}) (http.Handler, *int, *string) {
|
|
var heads int
|
|
var lastDelimiter string
|
|
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == http.MethodHead {
|
|
heads++
|
|
w.Header().Set("Content-Length", "0")
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
lastDelimiter = r.URL.Query().Get("delimiter")
|
|
w.Header().Set("Content-Type", "application/xml")
|
|
w.WriteHeader(http.StatusOK)
|
|
if r.URL.Query().Get("continuation-token") == "PAGE2" {
|
|
_, _ = io.WriteString(w, listBucketResultXML(bucket, false, "", page2, nil))
|
|
return
|
|
}
|
|
_, _ = io.WriteString(w, listBucketResultXML(bucket, true, "PAGE2", page1, nil))
|
|
})
|
|
return h, &heads, &lastDelimiter
|
|
}
|
|
|
|
// TestS3_SearchObjects_FindsMatchOnLaterPage reproduces issue #87: an object
|
|
// that lives on the second page of a listing must still be found by search.
|
|
// SearchObjects should page through the whole listing recursively.
|
|
func TestS3_SearchObjects_FindsMatchOnLaterPage(t *testing.T) {
|
|
bucket := "b-TestS3_SearchObjects_FindsMatchOnLaterPage"
|
|
page1 := []struct {
|
|
Key string
|
|
Size int64
|
|
LastModified string
|
|
ETag string
|
|
}{
|
|
{Key: "docs/alpha.txt", Size: 10},
|
|
{Key: "docs/beta.txt", Size: 10},
|
|
}
|
|
page2 := []struct {
|
|
Key string
|
|
Size int64
|
|
LastModified string
|
|
ETag string
|
|
}{
|
|
{Key: "docs/target-report.pdf", Size: 20},
|
|
{Key: "docs/gamma.txt", Size: 10},
|
|
}
|
|
h, heads, lastDelimiter := s3SearchHandler(bucket, page1, page2)
|
|
s3 := newS3TestService(t, h)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
got, err := s3.SearchObjects(ctx, bucket, "", "target")
|
|
if err != nil {
|
|
t.Fatalf("SearchObjects: %v", err)
|
|
}
|
|
if got.Count != 1 || len(got.Objects) != 1 || got.Objects[0].Key != "docs/target-report.pdf" {
|
|
t.Fatalf("expected to find docs/target-report.pdf on page 2, got %+v", got.Objects)
|
|
}
|
|
// Search must be recursive (no delimiter) so it descends into folders.
|
|
if *lastDelimiter != "" {
|
|
t.Errorf("delimiter = %q, want empty (recursive listing)", *lastDelimiter)
|
|
}
|
|
// Search must not fetch ContentType per object — that would be N stat calls.
|
|
if *heads != 0 {
|
|
t.Errorf("StatObject (HEAD) calls = %d, want 0 during search", *heads)
|
|
}
|
|
}
|
|
|
|
// TestS3_SearchObjects_ExcludesDirectoryMarkersAndIsCaseInsensitive verifies
|
|
// that zero-byte directory markers never appear as matches and that matching
|
|
// is a case-insensitive substring test on the full key.
|
|
func TestS3_SearchObjects_ExcludesDirectoryMarkersAndIsCaseInsensitive(t *testing.T) {
|
|
bucket := "b-TestS3_SearchObjects_ExcludesDirectoryMarkers"
|
|
page1 := []struct {
|
|
Key string
|
|
Size int64
|
|
LastModified string
|
|
ETag string
|
|
}{
|
|
{Key: "Reports/", Size: 0}, // directory marker — must be excluded
|
|
{Key: "Reports/Q1-REPORT.csv", Size: 5}, // matches "report" case-insensitively
|
|
{Key: "images/logo.png", Size: 5}, // no match
|
|
}
|
|
h, _, _ := s3SearchHandler(bucket, page1, nil)
|
|
s3 := newS3TestService(t, h)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
got, err := s3.SearchObjects(ctx, bucket, "", "report")
|
|
if err != nil {
|
|
t.Fatalf("SearchObjects: %v", err)
|
|
}
|
|
if got.Count != 1 || got.Objects[0].Key != "Reports/Q1-REPORT.csv" {
|
|
t.Fatalf("expected only Reports/Q1-REPORT.csv, got %+v", got.Objects)
|
|
}
|
|
for _, o := range got.Objects {
|
|
if strings.HasSuffix(o.Key, "/") {
|
|
t.Errorf("directory marker leaked into results: %q", o.Key)
|
|
}
|
|
}
|
|
}
|