[#131] client: Re-implement Object.Head method

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
This commit is contained in:
Leonard Lyubich 2022-02-11 19:13:53 +03:00 committed by LeL
parent 8d734d1020
commit caf31a928c
5 changed files with 216 additions and 251 deletions

View file

@ -271,7 +271,7 @@ func (x *contextCall) readResponse() bool {
return x.processResponse() return x.processResponse()
} }
// closes the message stream (if closer is set) and writes the results (if resuls is set). // closes the message stream (if closer is set) and writes the results (if result is set).
// Return means success. If failed, contextCall.err contains the reason. // Return means success. If failed, contextCall.err contains the reason.
func (x *contextCall) close() bool { func (x *contextCall) close() bool {
if x.closer != nil { if x.closer != nil {

View file

@ -32,14 +32,6 @@ type DeleteObjectParams struct {
tombTgt ObjectAddressWriter tombTgt ObjectAddressWriter
} }
type ObjectHeaderParams struct {
addr *address.Address
raw bool
short bool
}
type RangeDataParams struct { type RangeDataParams struct {
addr *address.Address addr *address.Address
@ -251,190 +243,6 @@ func writeUnexpectedMessageTypeErr(res resCommon, val interface{}) {
res.setStatus(st) res.setStatus(st)
} }
func (p *ObjectHeaderParams) WithAddress(v *address.Address) *ObjectHeaderParams {
if p != nil {
p.addr = v
}
return p
}
func (p *ObjectHeaderParams) Address() *address.Address {
if p != nil {
return p.addr
}
return nil
}
func (p *ObjectHeaderParams) WithAllFields() *ObjectHeaderParams {
if p != nil {
p.short = false
}
return p
}
// AllFields return true if parameter set to return all header fields, returns
// false if parameter set to return only main fields of header.
func (p *ObjectHeaderParams) AllFields() bool {
if p != nil {
return !p.short
}
return false
}
func (p *ObjectHeaderParams) WithMainFields() *ObjectHeaderParams {
if p != nil {
p.short = true
}
return p
}
func (p *ObjectHeaderParams) WithRawFlag(v bool) *ObjectHeaderParams {
if p != nil {
p.raw = v
}
return p
}
func (p *ObjectHeaderParams) RawFlag() bool {
if p != nil {
return p.raw
}
return false
}
type ObjectHeadRes struct {
statusRes
objectRes
}
// HeadObject receives object's header through NeoFS API call.
//
// Any client's internal or transport errors are returned as `error`.
// If WithNeoFSErrorParsing option has been provided, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure.
func (c *Client) HeadObject(ctx context.Context, p *ObjectHeaderParams, opts ...CallOption) (*ObjectHeadRes, error) {
callOpts := c.defaultCallOptions()
for i := range opts {
if opts[i] != nil {
opts[i](callOpts)
}
}
// create request
req := new(v2object.HeadRequest)
// initialize request body
body := new(v2object.HeadRequestBody)
req.SetBody(body)
// set meta header
meta := v2MetaHeaderFromOpts(callOpts)
if err := c.attachV2SessionToken(callOpts, meta, v2SessionReqInfo{
addr: p.addr.ToV2(),
verb: v2session.ObjectVerbHead,
}); err != nil {
return nil, fmt.Errorf("could not attach session token: %w", err)
}
req.SetMetaHeader(meta)
// fill body fields
body.SetAddress(p.addr.ToV2())
body.SetMainOnly(p.short)
body.SetRaw(p.raw)
// sign the request
if err := signature.SignServiceMessage(callOpts.key, req); err != nil {
return nil, fmt.Errorf("signing the request failed: %w", err)
}
// send Head request
resp, err := rpcapi.HeadObject(c.Raw(), req, client.WithContext(ctx))
if err != nil {
return nil, fmt.Errorf("sending the request failed: %w", err)
}
var (
res = new(ObjectHeadRes)
procPrm processResponseV2Prm
procRes processResponseV2Res
)
procPrm.callOpts = callOpts
procPrm.resp = resp
procRes.statusRes = res
// process response in general
if c.processResponseV2(&procRes, procPrm) {
if procRes.cliErr != nil {
return nil, procRes.cliErr
}
return res, nil
}
var (
hdr *v2object.Header
idSig *v2refs.Signature
)
switch v := resp.GetBody().GetHeaderPart().(type) {
case nil:
writeUnexpectedMessageTypeErr(res, v)
return res, nil
case *v2object.ShortHeader:
if !p.short {
writeUnexpectedMessageTypeErr(res, v)
return res, nil
}
h := v
hdr = new(v2object.Header)
hdr.SetPayloadLength(h.GetPayloadLength())
hdr.SetVersion(h.GetVersion())
hdr.SetOwnerID(h.GetOwnerID())
hdr.SetObjectType(h.GetObjectType())
hdr.SetCreationEpoch(h.GetCreationEpoch())
hdr.SetPayloadHash(h.GetPayloadHash())
hdr.SetHomomorphicHash(h.GetHomomorphicHash())
case *v2object.HeaderWithSignature:
if p.short {
writeUnexpectedMessageTypeErr(res, v)
return res, nil
}
hdr = v.GetHeader()
idSig = v.GetSignature()
case *v2object.SplitInfo:
si := object.NewSplitInfoFromV2(v)
return nil, object.NewSplitInfoError(si)
}
obj := new(v2object.Object)
obj.SetHeader(hdr)
obj.SetSignature(idSig)
raw := object.NewRawFromV2(obj)
raw.SetID(p.addr.ObjectID())
res.setObject(raw.Object())
return res, nil
}
func (p *RangeDataParams) WithAddress(v *address.Address) *RangeDataParams { func (p *RangeDataParams) WithAddress(v *address.Address) *RangeDataParams {
if p != nil { if p != nil {
p.addr = v p.addr = v

View file

@ -20,8 +20,8 @@ import (
"github.com/nspcc-dev/neofs-sdk-go/token" "github.com/nspcc-dev/neofs-sdk-go/token"
) )
// PrmObjectGet groups parameters of ObjectGetInit operation. // shared parameters of GET/HEAD/RANGE.
type PrmObjectGet struct { type prmObjectRead struct {
raw bool raw bool
local bool local bool
@ -40,41 +40,55 @@ type PrmObjectGet struct {
} }
// MarkRaw marks an intent to read physically stored object. // MarkRaw marks an intent to read physically stored object.
func (x *PrmObjectGet) MarkRaw() { func (x *prmObjectRead) MarkRaw() {
x.raw = true x.raw = true
} }
// MarkLocal tells the server to execute the operation locally. // MarkLocal tells the server to execute the operation locally.
func (x *PrmObjectGet) MarkLocal() { func (x *prmObjectRead) MarkLocal() {
x.local = true x.local = true
} }
// WithinSession specifies session within which object should be read. // WithinSession specifies session within which object should be read.
func (x *PrmObjectGet) WithinSession(t session.Token) { //
// Creator of the session acquires the authorship of the request.
// This may affect the execution of an operation (e.g. access control).
//
// Must be signed.
func (x *prmObjectRead) WithinSession(t session.Token) {
x.session = t x.session = t
x.sessionSet = true x.sessionSet = true
} }
// WithBearerToken attaches bearer token to be used for the operation. // WithBearerToken attaches bearer token to be used for the operation.
func (x *PrmObjectGet) WithBearerToken(t token.BearerToken) { //
// If set, underlying eACL rules will be used in access control.
//
// Must be signed.
func (x *prmObjectRead) WithBearerToken(t token.BearerToken) {
x.bearer = t x.bearer = t
x.bearerSet = true x.bearerSet = true
} }
// FromContainer specifies NeoFS container of the object. // FromContainer specifies NeoFS container of the object.
// Required parameter. // Required parameter.
func (x *PrmObjectGet) FromContainer(id cid.ID) { func (x *prmObjectRead) FromContainer(id cid.ID) {
x.cnr = id x.cnr = id
x.cnrSet = true x.cnrSet = true
} }
// ByID specifies identifier of the requested object. // ByID specifies identifier of the requested object.
// Required parameter. // Required parameter.
func (x *PrmObjectGet) ByID(id oid.ID) { func (x *prmObjectRead) ByID(id oid.ID) {
x.obj = id x.obj = id
x.objSet = true x.objSet = true
} }
// PrmObjectGet groups parameters of ObjectGetInit operation.
type PrmObjectGet struct {
prmObjectRead
}
// ResObjectGet groups the final result values of ObjectGetInit operation. // ResObjectGet groups the final result values of ObjectGetInit operation.
type ResObjectGet struct { type ResObjectGet struct {
statusRes statusRes
@ -101,6 +115,10 @@ func (x *ObjectReader) UseKey(key ecdsa.PrivateKey) {
x.ctxCall.key = key x.ctxCall.key = key
} }
func handleSplitInfo(ctx *contextCall, i *v2object.SplitInfo) {
ctx.err = object.NewSplitInfoError(object.NewSplitInfoFromV2(i))
}
// ReadHeader reads header of the object. Result means success. // ReadHeader reads header of the object. Result means success.
// Failure reason can be received via Close. // Failure reason can be received via Close.
func (x *ObjectReader) ReadHeader(dst *object.Object) bool { func (x *ObjectReader) ReadHeader(dst *object.Object) bool {
@ -117,7 +135,7 @@ func (x *ObjectReader) ReadHeader(dst *object.Object) bool {
x.ctxCall.err = fmt.Errorf("unexpected message instead of heading part: %T", v) x.ctxCall.err = fmt.Errorf("unexpected message instead of heading part: %T", v)
return false return false
case *v2object.SplitInfo: case *v2object.SplitInfo:
x.ctxCall.err = object.NewSplitInfoError(object.NewSplitInfoFromV2(v)) handleSplitInfo(&x.ctxCall, v)
return false return false
case *v2object.GetObjectPartInit: case *v2object.GetObjectPartInit:
partInit = v partInit = v
@ -231,7 +249,7 @@ func (x *ObjectReader) Read(p []byte) (int, error) {
// ObjectGetInit initiates reading an object through a remote server using NeoFS API protocol. // ObjectGetInit initiates reading an object through a remote server using NeoFS API protocol.
// //
// The call only opens the transmission channel, explicit fetching is done using the ObjectWriter. // The call only opens the transmission channel, explicit fetching is done using the ObjectReader.
// Exactly one return value is non-nil. Resulting reader must be finally closed. // Exactly one return value is non-nil. Resulting reader must be finally closed.
// //
// Immediately panics if parameters are set incorrectly (see PrmObjectGet docs). // Immediately panics if parameters are set incorrectly (see PrmObjectGet docs).
@ -311,3 +329,130 @@ func (c *Client) ObjectGetInit(ctx context.Context, prm PrmObjectGet) (*ObjectRe
return &r, nil return &r, nil
} }
// PrmObjectHead groups parameters of ObjectHead operation.
type PrmObjectHead struct {
prmObjectRead
}
// ResObjectHead groups resulting values of ObjectHead operation.
type ResObjectHead struct {
statusRes
// requested object (response doesn't carry the ID)
idObj oid.ID
hdr *v2object.HeaderWithSignature
}
// ReadHeader reads header of the requested object.
// Returns false if header is missing in the response (not read).
func (x *ResObjectHead) ReadHeader(dst *object.Object) bool {
if x.hdr == nil {
return false
}
var objv2 v2object.Object
objv2.SetHeader(x.hdr.GetHeader())
objv2.SetSignature(x.hdr.GetSignature())
raw := object.NewRawFromV2(&objv2)
raw.SetID(&x.idObj)
*dst = *raw.Object()
return true
}
// ObjectHead reads object header through a remote server using NeoFS API protocol.
//
// Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as `error`,
// If WithNeoFSErrorParsing option has been provided, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure.
//
// Immediately panics if parameters are set incorrectly (see PrmObjectHead docs).
// Context is required and must not be nil. It is used for network communication.
//
// Return errors:
// *object.SplitInfoError (returned on virtual objects with PrmObjectHead.MakeRaw).
//
// Return statuses:
// - global (see Client docs).
func (c *Client) ObjectHead(ctx context.Context, prm PrmObjectHead) (*ResObjectHead, error) {
switch {
case ctx == nil:
panic(panicMsgMissingContext)
case !prm.cnrSet:
panic(panicMsgMissingContainer)
case !prm.objSet:
panic("missing object")
}
var addr v2refs.Address
addr.SetContainerID(prm.cnr.ToV2())
addr.SetObjectID(prm.obj.ToV2())
// form request body
var body v2object.HeadRequestBody
body.SetRaw(prm.raw)
body.SetAddress(&addr)
// form meta header
var meta v2session.RequestMetaHeader
if prm.local {
meta.SetTTL(1)
}
if prm.bearerSet {
meta.SetBearerToken(prm.bearer.ToV2())
}
if prm.sessionSet {
meta.SetSessionToken(prm.session.ToV2())
}
// form request
var req v2object.HeadRequest
req.SetBody(&body)
req.SetMetaHeader(&meta)
// init call context
var (
cc contextCall
res ResObjectHead
)
res.idObj = prm.obj
c.initCallContext(&cc)
cc.req = &req
cc.statusRes = &res
cc.call = func() (responseV2, error) {
return rpcapi.HeadObject(c.Raw(), &req, client.WithContext(ctx))
}
cc.result = func(r responseV2) {
switch v := r.(*v2object.HeadResponse).GetBody().GetHeaderPart().(type) {
default:
cc.err = fmt.Errorf("unexpected header type %T", v)
case *v2object.SplitInfo:
handleSplitInfo(&cc, v)
case *v2object.HeaderWithSignature:
res.hdr = v
}
}
// process call
if !cc.processCall() {
return nil, cc.err
}
return &res, nil
}

View file

@ -260,15 +260,11 @@ func (mr *MockClientMockRecorder) HashObjectPayloadRanges(arg0, arg1 interface{}
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HashObjectPayloadRanges", reflect.TypeOf((*MockClient)(nil).HashObjectPayloadRanges), varargs...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HashObjectPayloadRanges", reflect.TypeOf((*MockClient)(nil).HashObjectPayloadRanges), varargs...)
} }
// HeadObject mocks base method. // ObjectHead mocks base method.
func (m *MockClient) HeadObject(arg0 context.Context, arg1 *client0.ObjectHeaderParams, arg2 ...client0.CallOption) (*client0.ObjectHeadRes, error) { func (m *MockClient) ObjectHead(arg0 context.Context, arg1 client0.PrmObjectHead) (*client0.ResObjectHead, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
varargs := []interface{}{arg0, arg1} ret := m.ctrl.Call(m, "HeadObject", arg0, arg1)
for _, a := range arg2 { ret0, _ := ret[0].(*client0.ResObjectHead)
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "HeadObject", varargs...)
ret0, _ := ret[0].(*client0.ObjectHeadRes)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
@ -277,7 +273,7 @@ func (m *MockClient) HeadObject(arg0 context.Context, arg1 *client0.ObjectHeader
func (mr *MockClientMockRecorder) HeadObject(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { func (mr *MockClientMockRecorder) HeadObject(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0, arg1}, arg2...) varargs := append([]interface{}{arg0, arg1}, arg2...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HeadObject", reflect.TypeOf((*MockClient)(nil).HeadObject), varargs...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HeadObject", reflect.TypeOf((*MockClient)(nil).ObjectHead), varargs...)
} }
// ListContainers mocks base method. // ListContainers mocks base method.

View file

@ -46,7 +46,7 @@ type Client interface {
ObjectPutInit(context.Context, client.PrmObjectPutInit) (*client.ObjectWriter, error) ObjectPutInit(context.Context, client.PrmObjectPutInit) (*client.ObjectWriter, error)
DeleteObject(context.Context, *client.DeleteObjectParams, ...client.CallOption) (*client.ObjectDeleteRes, error) DeleteObject(context.Context, *client.DeleteObjectParams, ...client.CallOption) (*client.ObjectDeleteRes, error)
ObjectGetInit(context.Context, client.PrmObjectGet) (*client.ObjectReader, error) ObjectGetInit(context.Context, client.PrmObjectGet) (*client.ObjectReader, error)
HeadObject(context.Context, *client.ObjectHeaderParams, ...client.CallOption) (*client.ObjectHeadRes, error) ObjectHead(context.Context, client.PrmObjectHead) (*client.ResObjectHead, error)
ObjectPayloadRangeData(context.Context, *client.RangeDataParams, ...client.CallOption) (*client.ObjectRangeRes, error) ObjectPayloadRangeData(context.Context, *client.RangeDataParams, ...client.CallOption) (*client.ObjectRangeRes, error)
HashObjectPayloadRanges(context.Context, *client.RangeChecksumParams, ...client.CallOption) (*client.ObjectRangeHashRes, error) HashObjectPayloadRanges(context.Context, *client.RangeChecksumParams, ...client.CallOption) (*client.ObjectRangeHashRes, error)
SearchObjects(context.Context, *client.SearchObjectParams, ...client.CallOption) (*client.ObjectSearchRes, error) SearchObjects(context.Context, *client.SearchObjectParams, ...client.CallOption) (*client.ObjectSearchRes, error)
@ -164,7 +164,7 @@ type Object interface {
PutObject(ctx context.Context, hdr object.Object, payload io.Reader, opts ...CallOption) (*oid.ID, error) PutObject(ctx context.Context, hdr object.Object, payload io.Reader, opts ...CallOption) (*oid.ID, error)
DeleteObject(ctx context.Context, params *client.DeleteObjectParams, opts ...CallOption) error DeleteObject(ctx context.Context, params *client.DeleteObjectParams, opts ...CallOption) error
GetObject(ctx context.Context, addr address.Address, opts ...CallOption) (*ResGetObject, error) GetObject(ctx context.Context, addr address.Address, opts ...CallOption) (*ResGetObject, error)
GetObjectHeader(ctx context.Context, params *client.ObjectHeaderParams, opts ...CallOption) (*object.Object, error) HeadObject(context.Context, address.Address, ...CallOption) (*object.Object, error)
ObjectPayloadRangeData(ctx context.Context, params *client.RangeDataParams, opts ...CallOption) ([]byte, error) ObjectPayloadRangeData(ctx context.Context, params *client.RangeDataParams, opts ...CallOption) ([]byte, error)
ObjectPayloadRangeSHA256(ctx context.Context, params *client.RangeChecksumParams, opts ...CallOption) ([][32]byte, error) ObjectPayloadRangeSHA256(ctx context.Context, params *client.RangeChecksumParams, opts ...CallOption) ([][32]byte, error)
ObjectPayloadRangeTZ(ctx context.Context, params *client.RangeChecksumParams, opts ...CallOption) ([][64]byte, error) ObjectPayloadRangeTZ(ctx context.Context, params *client.RangeChecksumParams, opts ...CallOption) ([][64]byte, error)
@ -648,6 +648,8 @@ func (p *pool) initCallContextWithRetry(ctx *callContextWithRetry, cfg *callConf
return nil return nil
} }
// opens new session or uses cached one.
// Must be called only on initialized callContext with set sessionTarget.
func (p *pool) openDefaultSession(ctx *callContext) error { func (p *pool) openDefaultSession(ctx *callContext) error {
cacheKey := formCacheKey(ctx.endpoint, ctx.key) cacheKey := formCacheKey(ctx.endpoint, ctx.key)
@ -684,14 +686,29 @@ func (p *pool) openDefaultSession(ctx *callContext) error {
return nil return nil
} }
func (p *pool) handleAttemptError(ctx *callContextWithRetry, err error) bool { // opens default session (if sessionDefault is set), and calls f. If f returns
isTokenErr := p.checkSessionTokenErr(err, ctx.endpoint) // session-related error (*), and retrying is enabled, then f is called once more.
// note that checkSessionTokenErr must be called //
res := isTokenErr && !ctx.noRetry // (*) in this case cached token is removed.
func (p *pool) callWithRetry(ctx *callContextWithRetry, f func() error) error {
var err error
ctx.noRetry = true if ctx.sessionDefault {
err = p.openDefaultSession(&ctx.callContext)
if err != nil {
return fmt.Errorf("open default session: %w", err)
}
}
return res err = f()
if p.checkSessionTokenErr(err, ctx.endpoint) && !ctx.noRetry {
// don't retry anymore
ctx.noRetry = true
return p.callWithRetry(ctx, f)
}
return err
} }
func (p *pool) PutObject(ctx context.Context, hdr object.Object, payload io.Reader, opts ...CallOption) (*oid.ID, error) { func (p *pool) PutObject(ctx context.Context, hdr object.Object, payload io.Reader, opts ...CallOption) (*oid.ID, error) {
@ -836,27 +853,6 @@ type ResGetObject struct {
Payload io.ReadCloser Payload io.ReadCloser
} }
func (p *pool) callWithRetry(ctx *callContextWithRetry, f func() error) error {
var err error
if ctx.sessionDefault {
err = p.openDefaultSession(&ctx.callContext)
if err != nil {
return fmt.Errorf("open default session: %w", err)
}
}
err = f()
if p.checkSessionTokenErr(err, ctx.endpoint) && !ctx.noRetry {
// don't retry anymore
ctx.noRetry = true
return p.callWithRetry(ctx, f)
}
return err
}
func (p *pool) GetObject(ctx context.Context, addr address.Address, opts ...CallOption) (*ResGetObject, error) { func (p *pool) GetObject(ctx context.Context, addr address.Address, opts ...CallOption) (*ResGetObject, error) {
cfg := cfgFromOpts(append(opts, useDefaultSession())...) cfg := cfgFromOpts(append(opts, useDefaultSession())...)
@ -903,25 +899,45 @@ func (p *pool) GetObject(ctx context.Context, addr address.Address, opts ...Call
return &res, nil return &res, nil
} }
func (p *pool) GetObjectHeader(ctx context.Context, params *client.ObjectHeaderParams, opts ...CallOption) (*object.Object, error) { func (p *pool) HeadObject(ctx context.Context, addr address.Address, opts ...CallOption) (*object.Object, error) {
cfg := cfgFromOpts(append(opts, useDefaultSession())...) cfg := cfgFromOpts(append(opts, useDefaultSession())...)
cp, options, err := p.conn(ctx, cfg)
var prm client.PrmObjectHead
var cc callContextWithRetry
cc.Context = ctx
cc.sessionTarget = prm.WithinSession
err := p.initCallContextWithRetry(&cc, cfg)
if err != nil { if err != nil {
return nil, err return nil, err
} }
res, err := cp.client.HeadObject(ctx, params, options...) if cnr := addr.ContainerID(); cnr != nil {
prm.FromContainer(*cnr)
if p.checkSessionTokenErr(err, cp.address) && !cfg.isRetry {
opts = append(opts, retry())
return p.GetObjectHeader(ctx, params, opts...)
} }
if err != nil { // here err already carries both status and client errors if obj := addr.ObjectID(); obj != nil {
return nil, err prm.ByID(*obj)
} }
return res.Object(), nil var obj object.Object
err = p.callWithRetry(&cc, func() error {
res, err := cc.client.ObjectHead(ctx, prm)
if err != nil {
return fmt.Errorf("read object header via client: %w", err)
}
if !res.ReadHeader(&obj) {
return errors.New("missing object header in response")
}
return nil
})
return &obj, nil
} }
func (p *pool) ObjectPayloadRangeData(ctx context.Context, params *client.RangeDataParams, opts ...CallOption) ([]byte, error) { func (p *pool) ObjectPayloadRangeData(ctx context.Context, params *client.RangeDataParams, opts ...CallOption) ([]byte, error) {