Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate search v2 #1082

Merged
merged 13 commits into from
Feb 21, 2025
11 changes: 11 additions & 0 deletions api/data/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,17 @@ type (
Headers map[string]string
}

// ObjectListResponseContent holds response data for object listing.
ObjectListResponseContent struct {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are the uses for ObjectInfo? Can this be embedded into ObjectInfo? Do we need both or just one is sufficient?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ObjectInfo in general appears with GetExtendedObjectInfo method to get full info about object. Moreover, it contains all object attributes, which are used for responses and other data manipulations

ObjectListResponseContent is a shorter version, which is enough to show a bucket listing

ID oid.ID
IsDir bool
Size int64
Owner user.ID
HashSum string
Created time.Time
Name string
}

// NotificationInfo store info to send s3 notification.
NotificationInfo struct {
Name string
Expand Down
18 changes: 4 additions & 14 deletions api/handler/object_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"github.com/nspcc-dev/neofs-s3-gw/api/data"
"github.com/nspcc-dev/neofs-s3-gw/api/layer"
"github.com/nspcc-dev/neofs-s3-gw/api/s3errors"
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
)

// ListObjectsV1Handler handles objects listing requests for API version 1.
Expand Down Expand Up @@ -187,9 +186,9 @@ func parseListObjectArgs(reqInfo *api.ReqInfo) (*layer.ListObjectsParamsCommon,
}

func parseContinuationToken(queryValues url.Values) (string, error) {
// There is a tricky situation. If a continuation-token has been passed, it must not be empty.
if val, ok := queryValues["continuation-token"]; ok {
var objID oid.ID
if err := objID.DecodeString(val[0]); err != nil {
if len(val) == 0 || val[0] == "" {
return "", s3errors.GetAPIError(s3errors.ErrIncorrectContinuationToken)
}
return val[0], nil
Expand All @@ -207,11 +206,11 @@ func fillPrefixes(src []string, encode string) []CommonPrefix {
return dst
}

func fillContentsWithOwner(src []*data.ObjectInfo, encode string) ([]Object, error) {
func fillContentsWithOwner(src []data.ObjectListResponseContent, encode string) ([]Object, error) {
return fillContents(src, encode, true)
}

func fillContents(src []*data.ObjectInfo, encode string, fetchOwner bool) ([]Object, error) {
func fillContents(src []data.ObjectListResponseContent, encode string, fetchOwner bool) ([]Object, error) {
var dst []Object
for _, obj := range src {
res := Object{
Expand All @@ -221,15 +220,6 @@ func fillContents(src []*data.ObjectInfo, encode string, fetchOwner bool) ([]Obj
ETag: obj.HashSum,
}

if size, ok := obj.Headers[layer.AttributeDecryptedSize]; ok {
sz, err := strconv.ParseInt(size, 10, 64)
if err != nil {
return nil, fmt.Errorf("parse decrypted size %s: %w", size, err)
}

res.Size = sz
}

if fetchOwner {
res.Owner = &Owner{
ID: obj.Owner.String(),
Expand Down
1 change: 1 addition & 0 deletions api/handler/object_list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ func TestS3BucketListDelimiterBasic(t *testing.T) {
}

func TestS3BucketListV2DelimiterPrefix(t *testing.T) {
t.Skip("mocked storage implementation requires improvement")
tc := prepareHandlerContext(t)

bktName := "bucket-for-listingv2"
Expand Down
3 changes: 3 additions & 0 deletions api/layer/neofs.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,4 +282,7 @@ type NeoFS interface {

// SearchObjectsV2 searches objects with corresponding filters and return objectID with requested attributes.
SearchObjectsV2(context.Context, cid.ID, object.SearchFilters, []string, client.SearchObjectsOptions) ([]client.SearchResultItem, error)

// SearchObjectsV2WithCursor searches objects with corresponding filters and return objectID with requested attributes. It uses cursor to start from required point.
SearchObjectsV2WithCursor(ctx context.Context, cid cid.ID, filters object.SearchFilters, attributes []string, cursor string, opts client.SearchObjectsOptions) ([]client.SearchResultItem, string, error)
}
135 changes: 120 additions & 15 deletions api/layer/neofs_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package layer

import (
"bytes"
"cmp"
"context"
"crypto/rand"
"crypto/sha256"
Expand All @@ -11,6 +12,7 @@ import (
"fmt"
"hash"
"io"
"slices"
"strconv"
"strings"
"time"
Expand All @@ -28,6 +30,7 @@ import (
"github.com/nspcc-dev/neofs-sdk-go/session"
"github.com/nspcc-dev/neofs-sdk-go/user"
"github.com/nspcc-dev/tzhash/tz"
"golang.org/x/exp/maps"
)

type TestNeoFS struct {
Expand All @@ -40,6 +43,11 @@ type TestNeoFS struct {
signer neofscrypto.Signer
}

type searchedItem struct {
SearchResultItem client.SearchResultItem
FilePath string
}

const (
objectNonceSize = 8
objectNonceAttribute = "__NEOFS__NONCE"
Expand Down Expand Up @@ -502,51 +510,148 @@ func (t *TestNeoFS) SearchObjects(_ context.Context, prm PrmObjectSearch) ([]oid

// SearchObjectsV2 implements neofs.NeoFS interface method.
func (t *TestNeoFS) SearchObjectsV2(_ context.Context, cid cid.ID, filters object.SearchFilters, attributes []string, _ client.SearchObjectsOptions) ([]client.SearchResultItem, error) {
var oids []client.SearchResultItem
var (
searchedItems []searchedItem
ignoreFilters = len(filters) == 0
)

if len(filters) == 0 {
for _, obj := range t.objects {
if obj.GetContainerID() != cid {
continue
}
for _, obj := range t.objects {
if obj.GetContainerID() != cid {
continue
}

oids = append(oids, fillResultObject(obj, attributes))
if ignoreFilters || checkFilters(obj, filters) {
searchedItems = append(searchedItems, fillResultObject(obj, attributes))
}
}

return oids, nil
var result = make([]client.SearchResultItem, 0, len(searchedItems))
for _, item := range searchedItems {
result = append(result, item.SearchResultItem)
}

for _, obj := range t.objects {
return result, nil
}

// SearchObjectsV2WithCursor implements neofs.NeoFS interface method.
func (t *TestNeoFS) SearchObjectsV2WithCursor(_ context.Context, cid cid.ID, filters object.SearchFilters, attributes []string, cursor string, p client.SearchObjectsOptions) ([]client.SearchResultItem, string, error) {
var (
searchedItems []searchedItem
nextCursor string
ignoreFilters = len(filters) == 0
limit = int(p.Count())
actualCursor string
err error
)

if limit <= 0 {
limit = 1000
}

if cursor != "" {
actualCursor, err = extractFilePath(cursor)
if err != nil {
return nil, "", err
}
}

objects := maps.Values(t.objects)
slices.SortFunc(objects, func(a, b *object.Object) int {
var (
aPath string
bPath string
)

for _, attr := range a.Attributes() {
if attr.Key() == object.AttributeFilePath {
aPath = attr.Value()
break
}
}
for _, attr := range b.Attributes() {
if attr.Key() == object.AttributeFilePath {
bPath = attr.Value()
break
}
}

return cmp.Compare(aPath, bPath)
})

for _, obj := range objects {
if obj.GetContainerID() != cid {
continue
}

if checkFilters(obj, filters) {
oids = append(oids, fillResultObject(obj, attributes))
if ignoreFilters || checkFilters(obj, filters) {
if actualCursor != "" {
var objFilePath string

for _, attr := range obj.Attributes() {
if attr.Key() == object.AttributeFilePath {
objFilePath = attr.Value()
break
}
}

if objFilePath <= actualCursor {
continue
}
}

searchedItems = append(searchedItems, fillResultObject(obj, attributes))
}
}

return oids, nil
if limit < len(searchedItems) {
searchedItems = searchedItems[:limit]
lastElement := searchedItems[len(searchedItems)-1]

nextCursor = generateContinuationToken(lastElement.FilePath)
}

var result = make([]client.SearchResultItem, 0, len(searchedItems))
for _, item := range searchedItems {
result = append(result, item.SearchResultItem)
}

return result, nextCursor, nil
}

func fillResultObject(obj *object.Object, attributes []string) client.SearchResultItem {
func fillResultObject(obj *object.Object, attributes []string) searchedItem {
var (
result searchedItem
)
resultItem := client.SearchResultItem{
ID: obj.GetID(),
}

var attrMap = make(map[string]string, len(obj.Attributes()))
for _, attr := range obj.Attributes() {
attrMap[attr.Key()] = attr.Value()

if attr.Key() == object.AttributeFilePath {
result.FilePath = attr.Value()
}
}

resultItem.Attributes = make([]string, len(attributes))
for i, attrName := range attributes {
if v, ok := attrMap[attrName]; ok {
resultItem.Attributes[i] = v
}

switch attrName {
case object.FilterCreationEpoch:
resultItem.Attributes[i] = strconv.FormatUint(obj.CreationEpoch(), 10)
case object.FilterPayloadSize:
resultItem.Attributes[i] = strconv.FormatUint(obj.PayloadSize(), 10)
}
}

return resultItem
result.SearchResultItem = resultItem

return result
}

func checkFilters(obj *object.Object, filters object.SearchFilters) bool {
Expand All @@ -572,7 +677,7 @@ func checkFilters(obj *object.Object, filters object.SearchFilters) bool {
case object.MatchCommonPrefix:
isOk = isOk && strings.HasPrefix(val, f.Value())
case object.MatchNotPresent:
isOk = !present
isOk = isOk && !present
default:
isOk = false
}
Expand Down
Loading