git.frostfs.info/TrueCloudLab/frostfs-sdk-go@v0.0.0-20241022124111-5361f0ecebd3/client/object_search.go (about)

     1  package client
     2  
     3  import (
     4  	"context"
     5  	"crypto/ecdsa"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  
    10  	"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl"
    11  	v2object "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
    12  	v2refs "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
    13  	rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
    14  	"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
    15  	v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
    16  	"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
    17  	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
    18  	apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
    19  	cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
    20  	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
    21  	oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
    22  	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
    23  )
    24  
    25  // PrmObjectSearch groups parameters of ObjectSearch operation.
    26  type PrmObjectSearch struct {
    27  	XHeaders []string
    28  
    29  	Local bool
    30  
    31  	BearerToken *bearer.Token
    32  
    33  	Session *session.Object
    34  
    35  	ContainerID *cid.ID
    36  
    37  	Key *ecdsa.PrivateKey
    38  
    39  	Filters object.SearchFilters
    40  }
    41  
    42  // MarkLocal tells the server to execute the operation locally.
    43  //
    44  // Deprecated: Use PrmObjectSearch.Local instead.
    45  func (x *PrmObjectSearch) MarkLocal() {
    46  	x.Local = true
    47  }
    48  
    49  // WithinSession specifies session within which the search query must be executed.
    50  //
    51  // Creator of the session acquires the authorship of the request.
    52  // This may affect the execution of an operation (e.g. access control).
    53  //
    54  // Must be signed.
    55  //
    56  // Deprecated: Use PrmObjectSearch.Session instead.
    57  func (x *PrmObjectSearch) WithinSession(t session.Object) {
    58  	x.Session = &t
    59  }
    60  
    61  // WithBearerToken attaches bearer token to be used for the operation.
    62  //
    63  // If set, underlying eACL rules will be used in access control.
    64  //
    65  // Must be signed.
    66  //
    67  // Deprecated: Use PrmObjectSearch.BearerToken instead.
    68  func (x *PrmObjectSearch) WithBearerToken(t bearer.Token) {
    69  	x.BearerToken = &t
    70  }
    71  
    72  // WithXHeaders specifies list of extended headers (string key-value pairs)
    73  // to be attached to the request. Must have an even length.
    74  //
    75  // Slice must not be mutated until the operation completes.
    76  //
    77  // Deprecated: Use PrmObjectSearch.XHeaders instead.
    78  func (x *PrmObjectSearch) WithXHeaders(hs ...string) {
    79  	x.XHeaders = hs
    80  }
    81  
    82  // UseKey specifies private key to sign the requests.
    83  // If key is not provided, then Client default key is used.
    84  //
    85  // Deprecated: Use PrmObjectSearch.Key instead.
    86  func (x *PrmObjectSearch) UseKey(key ecdsa.PrivateKey) {
    87  	x.Key = &key
    88  }
    89  
    90  // InContainer specifies the container in which to look for objects.
    91  // Required parameter.
    92  //
    93  // Deprecated: Use PrmObjectSearch.ContainerID instead.
    94  func (x *PrmObjectSearch) InContainer(id cid.ID) {
    95  	x.ContainerID = &id
    96  }
    97  
    98  // SetFilters sets filters by which to select objects. All container objects
    99  // match unset/empty filters.
   100  //
   101  // Deprecated: Use PrmObjectSearch.Filters instead.
   102  func (x *PrmObjectSearch) SetFilters(filters object.SearchFilters) {
   103  	x.Filters = filters
   104  }
   105  
   106  // ResObjectSearch groups the final result values of ObjectSearch operation.
   107  type ResObjectSearch struct {
   108  	statusRes
   109  }
   110  
   111  // ObjectListReader is designed to read list of object identifiers from FrostFS system.
   112  //
   113  // Must be initialized using Client.ObjectSearch, any other usage is unsafe.
   114  type ObjectListReader struct {
   115  	client          *Client
   116  	cancelCtxStream context.CancelFunc
   117  	err             error
   118  	res             ResObjectSearch
   119  	stream          interface {
   120  		Read(resp *v2object.SearchResponse) error
   121  	}
   122  	tail []v2refs.ObjectID
   123  }
   124  
   125  // Read reads another list of the object identifiers. Works similar to
   126  // io.Reader.Read but copies oid.ID and returns success flag instead of error.
   127  //
   128  // Failure reason can be received via Close.
   129  //
   130  // Panics if buf has zero length.
   131  func (x *ObjectListReader) Read(buf []oid.ID) (int, bool) {
   132  	if len(buf) == 0 {
   133  		panic("empty buffer in ObjectListReader.ReadList")
   134  	}
   135  
   136  	read := copyIDBuffers(buf, x.tail)
   137  	x.tail = x.tail[read:]
   138  
   139  	if len(buf) == read {
   140  		return read, true
   141  	}
   142  
   143  	for {
   144  		var resp v2object.SearchResponse
   145  		x.err = x.stream.Read(&resp)
   146  		if x.err != nil {
   147  			return read, false
   148  		}
   149  
   150  		x.res.st, x.err = x.client.processResponse(&resp)
   151  		if x.err != nil || !apistatus.IsSuccessful(x.res.st) {
   152  			return read, false
   153  		}
   154  
   155  		// read new chunk of objects
   156  		ids := resp.GetBody().GetIDList()
   157  		if len(ids) == 0 {
   158  			// just skip empty lists since they are not prohibited by protocol
   159  			continue
   160  		}
   161  
   162  		ln := copyIDBuffers(buf[read:], ids)
   163  		read += ln
   164  
   165  		if read == len(buf) {
   166  			// save the tail
   167  			x.tail = append(x.tail, ids[ln:]...)
   168  
   169  			return read, true
   170  		}
   171  	}
   172  }
   173  
   174  func copyIDBuffers(dst []oid.ID, src []v2refs.ObjectID) int {
   175  	var i int
   176  	for ; i < len(dst) && i < len(src); i++ {
   177  		_ = dst[i].ReadFromV2(src[i])
   178  	}
   179  	return i
   180  }
   181  
   182  // Iterate iterates over the list of found object identifiers.
   183  // f can return true to stop iteration earlier.
   184  //
   185  // Returns an error if object can't be read.
   186  func (x *ObjectListReader) Iterate(f func(oid.ID) bool) error {
   187  	buf := make([]oid.ID, 1)
   188  
   189  	for {
   190  		// Do not check first return value because `len(buf) == 1`,
   191  		// so false means nothing was read.
   192  		_, ok := x.Read(buf)
   193  		if !ok {
   194  			res, err := x.Close()
   195  			if err != nil {
   196  				return err
   197  			}
   198  			return apistatus.ErrFromStatus(res.Status())
   199  		}
   200  		if f(buf[0]) {
   201  			return nil
   202  		}
   203  	}
   204  }
   205  
   206  // Close ends reading list of the matched objects and returns the result of the operation
   207  // along with the final results. Must be called after using the ObjectListReader.
   208  //
   209  // Exactly one return value is non-nil. By default, server status is returned in res structure.
   210  // Any client's internal or transport errors are returned as Go built-in error.
   211  // If Client is tuned to resolve FrostFS API statuses, then FrostFS failures
   212  // codes are returned as error.
   213  //
   214  // Return statuses:
   215  //   - global (see Client docs);
   216  //   - *apistatus.ContainerNotFound;
   217  //   - *apistatus.ObjectAccessDenied;
   218  //   - *apistatus.SessionTokenExpired.
   219  func (x *ObjectListReader) Close() (*ResObjectSearch, error) {
   220  	defer x.cancelCtxStream()
   221  
   222  	if x.err != nil && !errors.Is(x.err, io.EOF) {
   223  		return &x.res, x.err
   224  	}
   225  
   226  	return &x.res, nil
   227  }
   228  
   229  func (x *PrmObjectSearch) buildRequest(c *Client) (*v2object.SearchRequest, error) {
   230  	if x.ContainerID == nil {
   231  		return nil, errorMissingContainer
   232  	}
   233  
   234  	if len(x.XHeaders)%2 != 0 {
   235  		return nil, errorInvalidXHeaders
   236  	}
   237  
   238  	meta := new(v2session.RequestMetaHeader)
   239  	writeXHeadersToMeta(x.XHeaders, meta)
   240  
   241  	if x.BearerToken != nil {
   242  		v2BearerToken := new(acl.BearerToken)
   243  		x.BearerToken.WriteToV2(v2BearerToken)
   244  		meta.SetBearerToken(v2BearerToken)
   245  	}
   246  
   247  	if x.Session != nil {
   248  		v2SessionToken := new(v2session.Token)
   249  		x.Session.WriteToV2(v2SessionToken)
   250  		meta.SetSessionToken(v2SessionToken)
   251  	}
   252  
   253  	if x.Local {
   254  		meta.SetTTL(1)
   255  	}
   256  	cnrV2 := new(v2refs.ContainerID)
   257  	x.ContainerID.WriteToV2(cnrV2)
   258  
   259  	body := new(v2object.SearchRequestBody)
   260  	body.SetVersion(1)
   261  	body.SetContainerID(cnrV2)
   262  	body.SetFilters(x.Filters.ToV2())
   263  
   264  	req := new(v2object.SearchRequest)
   265  	req.SetBody(body)
   266  	c.prepareRequest(req, meta)
   267  
   268  	return req, nil
   269  }
   270  
   271  // ObjectSearchInit initiates object selection through a remote server using FrostFS API protocol.
   272  //
   273  // The call only opens the transmission channel, explicit fetching of matched objects
   274  // is done using the ObjectListReader. Exactly one return value is non-nil.
   275  // Resulting reader must be finally closed.
   276  //
   277  // Returns an error if parameters are set incorrectly (see PrmObjectSearch docs).
   278  // Context is required and must not be nil. It is used for network communication.
   279  func (c *Client) ObjectSearchInit(ctx context.Context, prm PrmObjectSearch) (*ObjectListReader, error) {
   280  	req, err := prm.buildRequest(c)
   281  	if err != nil {
   282  		return nil, err
   283  	}
   284  
   285  	key := prm.Key
   286  	if key == nil {
   287  		key = &c.prm.Key
   288  	}
   289  
   290  	err = signature.SignServiceMessage(key, req)
   291  	if err != nil {
   292  		return nil, fmt.Errorf("sign request: %w", err)
   293  	}
   294  
   295  	var r ObjectListReader
   296  	ctx, r.cancelCtxStream = context.WithCancel(ctx)
   297  
   298  	r.stream, err = rpcapi.SearchObjects(&c.c, req, client.WithContext(ctx))
   299  	if err != nil {
   300  		return nil, fmt.Errorf("open stream: %w", err)
   301  	}
   302  	r.client = c
   303  
   304  	return &r, nil
   305  }