forked from TrueCloudLab/frostfs-sdk-go
[#131] client: Change interface of object PUT and GET ops
Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
This commit is contained in:
parent
40aaaafc73
commit
2624347d9b
8 changed files with 822 additions and 578 deletions
|
@ -151,15 +151,26 @@ type contextCall struct {
|
||||||
req interface {
|
req interface {
|
||||||
GetMetaHeader() *v2session.RequestMetaHeader
|
GetMetaHeader() *v2session.RequestMetaHeader
|
||||||
SetMetaHeader(*v2session.RequestMetaHeader)
|
SetMetaHeader(*v2session.RequestMetaHeader)
|
||||||
|
SetVerificationHeader(*v2session.RequestVerificationHeader)
|
||||||
}
|
}
|
||||||
|
|
||||||
// function to send a request (unary) and receive a response
|
// function to send a request (unary) and receive a response
|
||||||
call func() (responseV2, error)
|
call func() (responseV2, error)
|
||||||
|
|
||||||
|
// function to send the request (req field)
|
||||||
|
wReq func() error
|
||||||
|
|
||||||
|
// function to recv the response (resp field)
|
||||||
|
rResp func() error
|
||||||
|
|
||||||
|
// function to close the message stream
|
||||||
|
closer func() error
|
||||||
|
|
||||||
// function of writing response fields to the resulting structure (optional)
|
// function of writing response fields to the resulting structure (optional)
|
||||||
result func(v2 responseV2)
|
result func(v2 responseV2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sets needed fields of the request meta header.
|
||||||
func (x contextCall) prepareRequest() {
|
func (x contextCall) prepareRequest() {
|
||||||
meta := x.req.GetMetaHeader()
|
meta := x.req.GetMetaHeader()
|
||||||
if meta == nil {
|
if meta == nil {
|
||||||
|
@ -178,6 +189,29 @@ func (x contextCall) prepareRequest() {
|
||||||
meta.SetNetworkMagic(x.netMagic)
|
meta.SetNetworkMagic(x.netMagic)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// prepares, signs and writes the request. Result means success.
|
||||||
|
// If failed, contextCall.err contains the reason.
|
||||||
|
func (x *contextCall) writeRequest() bool {
|
||||||
|
x.prepareRequest()
|
||||||
|
|
||||||
|
x.req.SetVerificationHeader(nil)
|
||||||
|
|
||||||
|
// sign the request
|
||||||
|
x.err = signature.SignServiceMessage(&x.key, x.req)
|
||||||
|
if x.err != nil {
|
||||||
|
x.err = fmt.Errorf("sign request: %w", x.err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
x.err = x.wReq()
|
||||||
|
if x.err != nil {
|
||||||
|
x.err = fmt.Errorf("write request: %w", x.err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// performs common actions of response processing and writes any problem as a result status or client error
|
// performs common actions of response processing and writes any problem as a result status or client error
|
||||||
// (in both cases returns false).
|
// (in both cases returns false).
|
||||||
//
|
//
|
||||||
|
@ -220,33 +254,33 @@ func (x *contextCall) processResponse() bool {
|
||||||
|
|
||||||
x.statusRes.setStatus(st)
|
x.statusRes.setStatus(st)
|
||||||
|
|
||||||
return successfulStatus
|
return successfulStatus || !x.resolveAPIFailures
|
||||||
}
|
}
|
||||||
|
|
||||||
// goes through all stages of sending a request and processing a response. Returns true if successful.
|
// reads response (if rResp is set) and processes it. Result means success.
|
||||||
func (x *contextCall) processCall() bool {
|
// If failed, contextCall.err contains the reason.
|
||||||
// prepare the request
|
func (x *contextCall) readResponse() bool {
|
||||||
x.prepareRequest()
|
if x.rResp != nil {
|
||||||
|
x.err = x.rResp()
|
||||||
// sign the request
|
|
||||||
x.err = signature.SignServiceMessage(&x.key, x.req)
|
|
||||||
if x.err != nil {
|
if x.err != nil {
|
||||||
x.err = fmt.Errorf("sign request: %w", x.err)
|
x.err = fmt.Errorf("read response: %w", x.err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// perform RPC
|
|
||||||
x.resp, x.err = x.call()
|
|
||||||
if x.err != nil {
|
|
||||||
x.err = fmt.Errorf("transport error: %w", x.err)
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// process the response
|
return x.processResponse()
|
||||||
ok := x.processResponse()
|
}
|
||||||
if !ok {
|
|
||||||
|
// closes the message stream (if closer is set) and writes the results (if resuls is set).
|
||||||
|
// Return means success. If failed, contextCall.err contains the reason.
|
||||||
|
func (x *contextCall) close() bool {
|
||||||
|
if x.closer != nil {
|
||||||
|
x.err = x.closer()
|
||||||
|
if x.err != nil {
|
||||||
|
x.err = fmt.Errorf("close RPC: %w", x.err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// write response to resulting structure
|
// write response to resulting structure
|
||||||
if x.result != nil {
|
if x.result != nil {
|
||||||
|
@ -256,6 +290,34 @@ func (x *contextCall) processCall() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// goes through all stages of sending a request and processing a response. Returns true if successful.
|
||||||
|
// If failed, contextCall.err contains the reason.
|
||||||
|
func (x *contextCall) processCall() bool {
|
||||||
|
// set request writer
|
||||||
|
x.wReq = func() error {
|
||||||
|
var err error
|
||||||
|
x.resp, err = x.call()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// write request
|
||||||
|
ok := x.writeRequest()
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// read response
|
||||||
|
ok = x.readResponse()
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// close and write response to resulting structure
|
||||||
|
ok = x.close()
|
||||||
|
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
// initializes static cross-call parameters inherited from client.
|
// initializes static cross-call parameters inherited from client.
|
||||||
func (c *Client) initCallContext(ctx *contextCall) {
|
func (c *Client) initCallContext(ctx *contextCall) {
|
||||||
ctx.key = *c.opts.key
|
ctx.key = *c.opts.key
|
||||||
|
|
504
client/object.go
504
client/object.go
|
@ -1,9 +1,7 @@
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"crypto/ecdsa"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
@ -22,12 +20,6 @@ import (
|
||||||
signer "github.com/nspcc-dev/neofs-sdk-go/util/signature"
|
signer "github.com/nspcc-dev/neofs-sdk-go/util/signature"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PutObjectParams struct {
|
|
||||||
obj *object.Object
|
|
||||||
|
|
||||||
r io.Reader
|
|
||||||
}
|
|
||||||
|
|
||||||
// ObjectAddressWriter is an interface of the
|
// ObjectAddressWriter is an interface of the
|
||||||
// component that writes the object address.
|
// component that writes the object address.
|
||||||
type ObjectAddressWriter interface {
|
type ObjectAddressWriter interface {
|
||||||
|
@ -40,16 +32,6 @@ type DeleteObjectParams struct {
|
||||||
tombTgt ObjectAddressWriter
|
tombTgt ObjectAddressWriter
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetObjectParams struct {
|
|
||||||
addr *address.Address
|
|
||||||
|
|
||||||
raw bool
|
|
||||||
|
|
||||||
w io.Writer
|
|
||||||
|
|
||||||
readerHandler ReaderHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
type ObjectHeaderParams struct {
|
type ObjectHeaderParams struct {
|
||||||
addr *address.Address
|
addr *address.Address
|
||||||
|
|
||||||
|
@ -84,20 +66,6 @@ type SearchObjectParams struct {
|
||||||
filters object.SearchFilters
|
filters object.SearchFilters
|
||||||
}
|
}
|
||||||
|
|
||||||
type putObjectV2Reader struct {
|
|
||||||
r io.Reader
|
|
||||||
}
|
|
||||||
|
|
||||||
type putObjectV2Writer struct {
|
|
||||||
key *ecdsa.PrivateKey
|
|
||||||
|
|
||||||
chunkPart *v2object.PutObjectPartChunk
|
|
||||||
|
|
||||||
req *v2object.PutRequest
|
|
||||||
|
|
||||||
stream *rpcapi.PutRequestWriter
|
|
||||||
}
|
|
||||||
|
|
||||||
type checksumType int
|
type checksumType int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -106,8 +74,6 @@ const (
|
||||||
checksumTZ
|
checksumTZ
|
||||||
)
|
)
|
||||||
|
|
||||||
const chunkSize = 3 * (1 << 20)
|
|
||||||
|
|
||||||
const TZSize = 64
|
const TZSize = 64
|
||||||
|
|
||||||
const searchQueryVersion uint32 = 1
|
const searchQueryVersion uint32 = 1
|
||||||
|
@ -133,199 +99,6 @@ func (t checksumType) toV2() v2refs.ChecksumType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *putObjectV2Reader) Read(p []byte) (int, error) {
|
|
||||||
return w.r.Read(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *putObjectV2Writer) Write(p []byte) (int, error) {
|
|
||||||
w.chunkPart.SetChunk(p)
|
|
||||||
|
|
||||||
w.req.SetVerificationHeader(nil)
|
|
||||||
|
|
||||||
if err := signature.SignServiceMessage(w.key, w.req); err != nil {
|
|
||||||
return 0, fmt.Errorf("could not sign chunk request message: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := w.stream.Write(w.req); err != nil {
|
|
||||||
return 0, fmt.Errorf("could not send chunk request message: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return len(p), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PutObjectParams) WithObject(v *object.Object) *PutObjectParams {
|
|
||||||
if p != nil {
|
|
||||||
p.obj = v
|
|
||||||
}
|
|
||||||
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PutObjectParams) Object() *object.Object {
|
|
||||||
if p != nil {
|
|
||||||
return p.obj
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PutObjectParams) WithPayloadReader(v io.Reader) *PutObjectParams {
|
|
||||||
if p != nil {
|
|
||||||
p.r = v
|
|
||||||
}
|
|
||||||
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PutObjectParams) PayloadReader() io.Reader {
|
|
||||||
if p != nil {
|
|
||||||
return p.r
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type ObjectPutRes struct {
|
|
||||||
statusRes
|
|
||||||
|
|
||||||
id *oid.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *ObjectPutRes) setID(id *oid.ID) {
|
|
||||||
x.id = id
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x ObjectPutRes) ID() *oid.ID {
|
|
||||||
return x.id
|
|
||||||
}
|
|
||||||
|
|
||||||
// PutObject puts object 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) PutObject(ctx context.Context, p *PutObjectParams, opts ...CallOption) (*ObjectPutRes, error) {
|
|
||||||
callOpts := c.defaultCallOptions()
|
|
||||||
|
|
||||||
for i := range opts {
|
|
||||||
if opts[i] != nil {
|
|
||||||
opts[i](callOpts)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// create request
|
|
||||||
req := new(v2object.PutRequest)
|
|
||||||
|
|
||||||
// initialize request body
|
|
||||||
body := new(v2object.PutRequestBody)
|
|
||||||
req.SetBody(body)
|
|
||||||
|
|
||||||
v2Addr := new(v2refs.Address)
|
|
||||||
v2Addr.SetObjectID(p.obj.ID().ToV2())
|
|
||||||
v2Addr.SetContainerID(p.obj.ContainerID().ToV2())
|
|
||||||
|
|
||||||
// set meta header
|
|
||||||
meta := v2MetaHeaderFromOpts(callOpts)
|
|
||||||
|
|
||||||
if err := c.attachV2SessionToken(callOpts, meta, v2SessionReqInfo{
|
|
||||||
addr: v2Addr,
|
|
||||||
verb: v2session.ObjectVerbPut,
|
|
||||||
}); err != nil {
|
|
||||||
return nil, fmt.Errorf("could not attach session token: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
req.SetMetaHeader(meta)
|
|
||||||
|
|
||||||
// initialize init part
|
|
||||||
initPart := new(v2object.PutObjectPartInit)
|
|
||||||
body.SetObjectPart(initPart)
|
|
||||||
|
|
||||||
obj := p.obj.ToV2()
|
|
||||||
|
|
||||||
// set init part fields
|
|
||||||
initPart.SetObjectID(obj.GetObjectID())
|
|
||||||
initPart.SetSignature(obj.GetSignature())
|
|
||||||
initPart.SetHeader(obj.GetHeader())
|
|
||||||
|
|
||||||
// sign the request
|
|
||||||
if err := signature.SignServiceMessage(callOpts.key, req); err != nil {
|
|
||||||
return nil, fmt.Errorf("signing the request failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// open stream
|
|
||||||
resp := new(v2object.PutResponse)
|
|
||||||
|
|
||||||
stream, err := rpcapi.PutObject(c.Raw(), resp, client.WithContext(ctx))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("stream opening failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// send init part
|
|
||||||
err = stream.Write(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("sending the initial message to stream failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// create payload bytes reader
|
|
||||||
var rPayload io.Reader = bytes.NewReader(obj.GetPayload())
|
|
||||||
if p.r != nil {
|
|
||||||
rPayload = io.MultiReader(rPayload, p.r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// create v2 payload stream writer
|
|
||||||
chunkPart := new(v2object.PutObjectPartChunk)
|
|
||||||
body.SetObjectPart(chunkPart)
|
|
||||||
|
|
||||||
w := &putObjectV2Writer{
|
|
||||||
key: callOpts.key,
|
|
||||||
chunkPart: chunkPart,
|
|
||||||
req: req,
|
|
||||||
stream: stream,
|
|
||||||
}
|
|
||||||
|
|
||||||
r := &putObjectV2Reader{r: rPayload}
|
|
||||||
|
|
||||||
// copy payload from reader to stream writer
|
|
||||||
_, err = io.CopyBuffer(w, r, make([]byte, chunkSize))
|
|
||||||
if err != nil && !errors.Is(err, io.EOF) {
|
|
||||||
return nil, fmt.Errorf("payload streaming failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// close object stream and receive response from remote node
|
|
||||||
err = stream.Close()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("closing the stream failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
res = new(ObjectPutRes)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// convert object identifier
|
|
||||||
id := oid.NewIDFromV2(resp.GetBody().GetObjectID())
|
|
||||||
|
|
||||||
res.setID(id)
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *DeleteObjectParams) WithAddress(v *address.Address) *DeleteObjectParams {
|
func (p *DeleteObjectParams) WithAddress(v *address.Address) *DeleteObjectParams {
|
||||||
if p != nil {
|
if p != nil {
|
||||||
p.addr = v
|
p.addr = v
|
||||||
|
@ -451,132 +224,6 @@ func (c *Client) DeleteObject(ctx context.Context, p *DeleteObjectParams, opts .
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *GetObjectParams) WithAddress(v *address.Address) *GetObjectParams {
|
|
||||||
if p != nil {
|
|
||||||
p.addr = v
|
|
||||||
}
|
|
||||||
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *GetObjectParams) Address() *address.Address {
|
|
||||||
if p != nil {
|
|
||||||
return p.addr
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *GetObjectParams) WithPayloadWriter(w io.Writer) *GetObjectParams {
|
|
||||||
if p != nil {
|
|
||||||
p.w = w
|
|
||||||
}
|
|
||||||
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *GetObjectParams) PayloadWriter() io.Writer {
|
|
||||||
if p != nil {
|
|
||||||
return p.w
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *GetObjectParams) WithRawFlag(v bool) *GetObjectParams {
|
|
||||||
if p != nil {
|
|
||||||
p.raw = v
|
|
||||||
}
|
|
||||||
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *GetObjectParams) RawFlag() bool {
|
|
||||||
if p != nil {
|
|
||||||
return p.raw
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReaderHandler is a function over io.Reader.
|
|
||||||
type ReaderHandler func(io.Reader)
|
|
||||||
|
|
||||||
// WithPayloadReaderHandler sets handler of the payload reader.
|
|
||||||
//
|
|
||||||
// If provided, payload reader is composed after receiving the header.
|
|
||||||
// In this case payload writer set via WithPayloadWriter is ignored.
|
|
||||||
//
|
|
||||||
// Handler should not be nil.
|
|
||||||
func (p *GetObjectParams) WithPayloadReaderHandler(f ReaderHandler) *GetObjectParams {
|
|
||||||
if p != nil {
|
|
||||||
p.readerHandler = f
|
|
||||||
}
|
|
||||||
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
// wrapper over the Object Get stream that provides io.Reader.
|
|
||||||
type objectPayloadReader struct {
|
|
||||||
stream interface {
|
|
||||||
Read(*v2object.GetResponse) error
|
|
||||||
}
|
|
||||||
|
|
||||||
resp v2object.GetResponse
|
|
||||||
|
|
||||||
tail []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *objectPayloadReader) Read(p []byte) (read int, err error) {
|
|
||||||
// read remaining tail
|
|
||||||
read = copy(p, x.tail)
|
|
||||||
|
|
||||||
x.tail = x.tail[read:]
|
|
||||||
|
|
||||||
if len(p)-read == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// receive message from server stream
|
|
||||||
err = x.stream.Read(&x.resp)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, io.EOF) {
|
|
||||||
err = io.EOF
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = fmt.Errorf("reading the response failed: %w", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// get chunk part message
|
|
||||||
part := x.resp.GetBody().GetObjectPart()
|
|
||||||
|
|
||||||
chunkPart, ok := part.(*v2object.GetObjectPartChunk)
|
|
||||||
if !ok {
|
|
||||||
err = errWrongMessageSeq
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// verify response structure
|
|
||||||
if err = signature.VerifyServiceMessage(&x.resp); err != nil {
|
|
||||||
err = fmt.Errorf("response verification failed: %w", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// read new chunk
|
|
||||||
chunk := chunkPart.GetChunk()
|
|
||||||
|
|
||||||
tailOffset := copy(p[read:], chunk)
|
|
||||||
|
|
||||||
read += tailOffset
|
|
||||||
|
|
||||||
// save the tail
|
|
||||||
x.tail = append(x.tail, chunk[tailOffset:]...)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var errWrongMessageSeq = errors.New("incorrect message sequence")
|
var errWrongMessageSeq = errors.New("incorrect message sequence")
|
||||||
|
|
||||||
type ObjectGetRes struct {
|
type ObjectGetRes struct {
|
||||||
|
@ -604,157 +251,6 @@ func writeUnexpectedMessageTypeErr(res resCommon, val interface{}) {
|
||||||
res.setStatus(st)
|
res.setStatus(st)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetObject receives object 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) GetObject(ctx context.Context, p *GetObjectParams, opts ...CallOption) (*ObjectGetRes, error) {
|
|
||||||
callOpts := c.defaultCallOptions()
|
|
||||||
|
|
||||||
for i := range opts {
|
|
||||||
if opts[i] != nil {
|
|
||||||
opts[i](callOpts)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// create request
|
|
||||||
req := new(v2object.GetRequest)
|
|
||||||
|
|
||||||
// initialize request body
|
|
||||||
body := new(v2object.GetRequestBody)
|
|
||||||
req.SetBody(body)
|
|
||||||
|
|
||||||
// set meta header
|
|
||||||
meta := v2MetaHeaderFromOpts(callOpts)
|
|
||||||
|
|
||||||
if err := c.attachV2SessionToken(callOpts, meta, v2SessionReqInfo{
|
|
||||||
addr: p.addr.ToV2(),
|
|
||||||
verb: v2session.ObjectVerbGet,
|
|
||||||
}); 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.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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// open stream
|
|
||||||
stream, err := rpcapi.GetObject(c.Raw(), req, client.WithContext(ctx))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("stream opening failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
headWas bool
|
|
||||||
payload []byte
|
|
||||||
obj = new(v2object.Object)
|
|
||||||
resp = new(v2object.GetResponse)
|
|
||||||
|
|
||||||
messageWas bool
|
|
||||||
|
|
||||||
res = new(ObjectGetRes)
|
|
||||||
procPrm processResponseV2Prm
|
|
||||||
procRes processResponseV2Res
|
|
||||||
)
|
|
||||||
|
|
||||||
procPrm.callOpts = callOpts
|
|
||||||
procPrm.resp = resp
|
|
||||||
|
|
||||||
procRes.statusRes = res
|
|
||||||
|
|
||||||
loop:
|
|
||||||
for {
|
|
||||||
// receive message from server stream
|
|
||||||
err := stream.Read(resp)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, io.EOF) {
|
|
||||||
if !messageWas {
|
|
||||||
return nil, errWrongMessageSeq
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("reading the response failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
messageWas = true
|
|
||||||
|
|
||||||
// process response in general
|
|
||||||
if c.processResponseV2(&procRes, procPrm) {
|
|
||||||
if procRes.cliErr != nil {
|
|
||||||
return nil, procRes.cliErr
|
|
||||||
}
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
switch v := resp.GetBody().GetObjectPart().(type) {
|
|
||||||
default:
|
|
||||||
return nil, errWrongMessageSeq
|
|
||||||
case *v2object.GetObjectPartInit:
|
|
||||||
if headWas {
|
|
||||||
return nil, errWrongMessageSeq
|
|
||||||
}
|
|
||||||
|
|
||||||
headWas = true
|
|
||||||
|
|
||||||
obj.SetObjectID(v.GetObjectID())
|
|
||||||
obj.SetSignature(v.GetSignature())
|
|
||||||
|
|
||||||
hdr := v.GetHeader()
|
|
||||||
obj.SetHeader(hdr)
|
|
||||||
|
|
||||||
if p.readerHandler != nil {
|
|
||||||
p.readerHandler(&objectPayloadReader{
|
|
||||||
stream: stream,
|
|
||||||
})
|
|
||||||
|
|
||||||
break loop
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.w == nil {
|
|
||||||
payload = make([]byte, 0, hdr.GetPayloadLength())
|
|
||||||
}
|
|
||||||
case *v2object.GetObjectPartChunk:
|
|
||||||
if !headWas {
|
|
||||||
return nil, errWrongMessageSeq
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.w != nil {
|
|
||||||
if _, err := p.w.Write(v.GetChunk()); err != nil {
|
|
||||||
return nil, fmt.Errorf("could not write payload chunk: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
payload = append(payload, v.GetChunk()...)
|
|
||||||
}
|
|
||||||
case *v2object.SplitInfo:
|
|
||||||
if headWas {
|
|
||||||
return nil, errWrongMessageSeq
|
|
||||||
}
|
|
||||||
|
|
||||||
si := object.NewSplitInfoFromV2(v)
|
|
||||||
return nil, object.NewSplitInfoError(si)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
obj.SetPayload(payload)
|
|
||||||
|
|
||||||
// convert the object
|
|
||||||
res.setObject(object.NewFromV2(obj))
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ObjectHeaderParams) WithAddress(v *address.Address) *ObjectHeaderParams {
|
func (p *ObjectHeaderParams) WithAddress(v *address.Address) *ObjectHeaderParams {
|
||||||
if p != nil {
|
if p != nil {
|
||||||
p.addr = v
|
p.addr = v
|
||||||
|
|
313
client/object_get.go
Normal file
313
client/object_get.go
Normal file
|
@ -0,0 +1,313 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
v2object "github.com/nspcc-dev/neofs-api-go/v2/object"
|
||||||
|
v2refs "github.com/nspcc-dev/neofs-api-go/v2/refs"
|
||||||
|
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
|
||||||
|
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
|
||||||
|
v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
|
||||||
|
apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
|
||||||
|
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
|
||||||
|
"github.com/nspcc-dev/neofs-sdk-go/object"
|
||||||
|
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
|
||||||
|
"github.com/nspcc-dev/neofs-sdk-go/session"
|
||||||
|
"github.com/nspcc-dev/neofs-sdk-go/token"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PrmObjectGet groups parameters of ObjectGetInit operation.
|
||||||
|
type PrmObjectGet struct {
|
||||||
|
raw bool
|
||||||
|
|
||||||
|
local bool
|
||||||
|
|
||||||
|
sessionSet bool
|
||||||
|
session session.Token
|
||||||
|
|
||||||
|
bearerSet bool
|
||||||
|
bearer token.BearerToken
|
||||||
|
|
||||||
|
cnrSet bool
|
||||||
|
cnr cid.ID
|
||||||
|
|
||||||
|
objSet bool
|
||||||
|
obj oid.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkRaw marks an intent to read physically stored object.
|
||||||
|
func (x *PrmObjectGet) MarkRaw() {
|
||||||
|
x.raw = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkLocal tells the server to execute the operation locally.
|
||||||
|
func (x *PrmObjectGet) MarkLocal() {
|
||||||
|
x.local = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithinSession specifies session within which object should be read.
|
||||||
|
func (x *PrmObjectGet) WithinSession(t session.Token) {
|
||||||
|
x.session = t
|
||||||
|
x.sessionSet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithBearerToken attaches bearer token to be used for the operation.
|
||||||
|
func (x *PrmObjectGet) WithBearerToken(t token.BearerToken) {
|
||||||
|
x.bearer = t
|
||||||
|
x.bearerSet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromContainer specifies NeoFS container of the object.
|
||||||
|
// Required parameter.
|
||||||
|
func (x *PrmObjectGet) FromContainer(id cid.ID) {
|
||||||
|
x.cnr = id
|
||||||
|
x.cnrSet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByID specifies identifier of the requested object.
|
||||||
|
// Required parameter.
|
||||||
|
func (x *PrmObjectGet) ByID(id oid.ID) {
|
||||||
|
x.obj = id
|
||||||
|
x.objSet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResObjectGet groups the final result values of ObjectGetInit operation.
|
||||||
|
type ResObjectGet struct {
|
||||||
|
statusRes
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObjectReader is designed to read one object from NeoFS system.
|
||||||
|
//
|
||||||
|
// Must be initialized using Client.ObjectGetInit, any other
|
||||||
|
// usage is unsafe.
|
||||||
|
type ObjectReader struct {
|
||||||
|
cancelCtxStream context.CancelFunc
|
||||||
|
|
||||||
|
ctxCall contextCall
|
||||||
|
|
||||||
|
// initially bound to contextCall
|
||||||
|
bodyResp v2object.GetResponseBody
|
||||||
|
|
||||||
|
tailPayload []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseKey specifies private key to sign the requests.
|
||||||
|
// If key is not provided, then Client default key is used.
|
||||||
|
func (x *ObjectReader) UseKey(key ecdsa.PrivateKey) {
|
||||||
|
x.ctxCall.key = key
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadHeader reads header of the object. Result means success.
|
||||||
|
// Failure reason can be received via Close.
|
||||||
|
func (x *ObjectReader) ReadHeader(dst *object.Object) bool {
|
||||||
|
if !x.ctxCall.writeRequest() {
|
||||||
|
return false
|
||||||
|
} else if !x.ctxCall.readResponse() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var partInit *v2object.GetObjectPartInit
|
||||||
|
|
||||||
|
switch v := x.bodyResp.GetObjectPart().(type) {
|
||||||
|
default:
|
||||||
|
x.ctxCall.err = fmt.Errorf("unexpected message instead of heading part: %T", v)
|
||||||
|
return false
|
||||||
|
case *v2object.SplitInfo:
|
||||||
|
x.ctxCall.err = object.NewSplitInfoError(object.NewSplitInfoFromV2(v))
|
||||||
|
return false
|
||||||
|
case *v2object.GetObjectPartInit:
|
||||||
|
partInit = v
|
||||||
|
}
|
||||||
|
|
||||||
|
var objv2 v2object.Object
|
||||||
|
|
||||||
|
objv2.SetObjectID(partInit.GetObjectID())
|
||||||
|
objv2.SetHeader(partInit.GetHeader())
|
||||||
|
objv2.SetSignature(partInit.GetSignature())
|
||||||
|
|
||||||
|
*dst = *object.NewFromV2(&objv2) // need smth better
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ObjectReader) readChunk(buf []byte) (int, bool) {
|
||||||
|
var read int
|
||||||
|
|
||||||
|
// read remaining tail
|
||||||
|
read = copy(buf, x.tailPayload)
|
||||||
|
|
||||||
|
x.tailPayload = x.tailPayload[read:]
|
||||||
|
|
||||||
|
if len(buf) == read {
|
||||||
|
return read, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// receive next message
|
||||||
|
ok := x.ctxCall.readResponse()
|
||||||
|
if !ok {
|
||||||
|
return read, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// get chunk part message
|
||||||
|
part := x.bodyResp.GetObjectPart()
|
||||||
|
|
||||||
|
var partChunk *v2object.GetObjectPartChunk
|
||||||
|
|
||||||
|
partChunk, ok = part.(*v2object.GetObjectPartChunk)
|
||||||
|
if !ok {
|
||||||
|
x.ctxCall.err = fmt.Errorf("unexpected message instead of chunk part: %T", part)
|
||||||
|
return read, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// read new chunk
|
||||||
|
chunk := partChunk.GetChunk()
|
||||||
|
|
||||||
|
tailOffset := copy(buf[read:], chunk)
|
||||||
|
|
||||||
|
read += tailOffset
|
||||||
|
|
||||||
|
// save the tail
|
||||||
|
x.tailPayload = append(x.tailPayload, chunk[tailOffset:]...)
|
||||||
|
|
||||||
|
return read, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadChunk reads another chunk of the object payload. Works similar to
|
||||||
|
// io.Reader.Read but returns success flag instead of error.
|
||||||
|
//
|
||||||
|
// Failure reason can be received via Close.
|
||||||
|
func (x *ObjectReader) ReadChunk(buf []byte) (int, bool) {
|
||||||
|
return x.readChunk(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ObjectReader) close(ignoreEOF bool) (*ResObjectGet, error) {
|
||||||
|
defer x.cancelCtxStream()
|
||||||
|
|
||||||
|
if x.ctxCall.err != nil {
|
||||||
|
if !errors.Is(x.ctxCall.err, io.EOF) {
|
||||||
|
return nil, x.ctxCall.err
|
||||||
|
} else if !ignoreEOF {
|
||||||
|
return nil, io.EOF
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return x.ctxCall.statusRes.(*ResObjectGet), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close ends reading the object and returns the result of the operation
|
||||||
|
// along with the final results. Must be called after using the ObjectReader.
|
||||||
|
//
|
||||||
|
// 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 Go built-in error.
|
||||||
|
// If Client is tuned to resolve NeoFS API statuses, then NeoFS failures
|
||||||
|
// codes are returned as error.
|
||||||
|
//
|
||||||
|
// Return errors:
|
||||||
|
// *object.SplitInfoError (returned on virtual objects with PrmObjectGet.MakeRaw).
|
||||||
|
//
|
||||||
|
// Return statuses:
|
||||||
|
// global (see Client docs).
|
||||||
|
func (x *ObjectReader) Close() (*ResObjectGet, error) {
|
||||||
|
return x.close(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ObjectReader) Read(p []byte) (int, error) {
|
||||||
|
n, ok := x.readChunk(p)
|
||||||
|
if !ok {
|
||||||
|
res, err := x.close(false)
|
||||||
|
if err != nil {
|
||||||
|
return n, err
|
||||||
|
} else if !x.ctxCall.resolveAPIFailures {
|
||||||
|
return n, apistatus.ErrFromStatus(res.Status())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
// Exactly one return value is non-nil. Resulting reader must be finally closed.
|
||||||
|
//
|
||||||
|
// Immediately panics if parameters are set incorrectly (see PrmObjectGet docs).
|
||||||
|
// Context is required and must not be nil. It is used for network communication.
|
||||||
|
func (c *Client) ObjectGetInit(ctx context.Context, prm PrmObjectGet) (*ObjectReader, error) {
|
||||||
|
// check parameters
|
||||||
|
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.GetRequestBody
|
||||||
|
|
||||||
|
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.GetRequest
|
||||||
|
|
||||||
|
req.SetBody(&body)
|
||||||
|
req.SetMetaHeader(&meta)
|
||||||
|
|
||||||
|
// init reader
|
||||||
|
var (
|
||||||
|
r ObjectReader
|
||||||
|
resp v2object.GetResponse
|
||||||
|
stream *rpcapi.GetResponseReader
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx, r.cancelCtxStream = context.WithCancel(ctx)
|
||||||
|
|
||||||
|
resp.SetBody(&r.bodyResp)
|
||||||
|
|
||||||
|
// init call context
|
||||||
|
c.initCallContext(&r.ctxCall)
|
||||||
|
r.ctxCall.req = &req
|
||||||
|
r.ctxCall.statusRes = new(ResObjectGet)
|
||||||
|
r.ctxCall.resp = &resp
|
||||||
|
r.ctxCall.wReq = func() error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
stream, err = rpcapi.GetObject(c.Raw(), &req, client.WithContext(ctx))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("open stream: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
r.ctxCall.rResp = func() error {
|
||||||
|
return stream.Read(&resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &r, nil
|
||||||
|
}
|
199
client/object_put.go
Normal file
199
client/object_put.go
Normal file
|
@ -0,0 +1,199 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
v2object "github.com/nspcc-dev/neofs-api-go/v2/object"
|
||||||
|
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
|
||||||
|
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
|
||||||
|
v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
|
||||||
|
"github.com/nspcc-dev/neofs-sdk-go/object"
|
||||||
|
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
|
||||||
|
"github.com/nspcc-dev/neofs-sdk-go/session"
|
||||||
|
"github.com/nspcc-dev/neofs-sdk-go/token"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PrmObjectPutInit groups parameters of ObjectPutInit operation.
|
||||||
|
//
|
||||||
|
// At the moment the operation is not parameterized, however,
|
||||||
|
// the structure is still declared for backward compatibility.
|
||||||
|
type PrmObjectPutInit struct{}
|
||||||
|
|
||||||
|
// ResObjectPut groups the final result values of ObjectPutInit operation.
|
||||||
|
type ResObjectPut struct {
|
||||||
|
statusRes
|
||||||
|
|
||||||
|
resp v2object.PutResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadStoredObject reads identifier of the saved object.
|
||||||
|
// Returns false if ID is missing (not read).
|
||||||
|
func (x *ResObjectPut) ReadStoredObject(id *oid.ID) bool {
|
||||||
|
idv2 := x.resp.GetBody().GetObjectID()
|
||||||
|
if idv2 == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
*id = *oid.NewIDFromV2(idv2) // need smth better
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObjectWriter is designed to write one object to NeoFS system.
|
||||||
|
//
|
||||||
|
// Must be initialized using Client.ObjectPutInit, any other
|
||||||
|
// usage is unsafe.
|
||||||
|
type ObjectWriter struct {
|
||||||
|
cancelCtxStream context.CancelFunc
|
||||||
|
|
||||||
|
ctxCall contextCall
|
||||||
|
|
||||||
|
// initially bound tp contextCall
|
||||||
|
metaHdr v2session.RequestMetaHeader
|
||||||
|
|
||||||
|
// initially bound to contextCall
|
||||||
|
partInit v2object.PutObjectPartInit
|
||||||
|
|
||||||
|
chunkCalled bool
|
||||||
|
|
||||||
|
partChunk v2object.PutObjectPartChunk
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseKey specifies private key to sign the requests.
|
||||||
|
// If key is not provided, then Client default key is used.
|
||||||
|
func (x *ObjectWriter) UseKey(key ecdsa.PrivateKey) {
|
||||||
|
x.ctxCall.key = key
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithBearerToken attaches bearer token to be used for the operation.
|
||||||
|
// Should be called once before any writing steps.
|
||||||
|
func (x *ObjectWriter) WithBearerToken(t token.BearerToken) {
|
||||||
|
x.metaHdr.SetBearerToken(t.ToV2())
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithinSession specifies session within which object should be stored.
|
||||||
|
// Should be called once before any writing steps.
|
||||||
|
func (x *ObjectWriter) WithinSession(t session.Token) {
|
||||||
|
x.metaHdr.SetSessionToken(t.ToV2())
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkLocal tells the server to execute the operation locally.
|
||||||
|
func (x *ObjectWriter) MarkLocal() {
|
||||||
|
x.metaHdr.SetTTL(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteHeader writes header of the object. Result means success.
|
||||||
|
// Failure reason can be received via Close.
|
||||||
|
func (x *ObjectWriter) WriteHeader(hdr object.Object) bool {
|
||||||
|
v2Hdr := hdr.ToV2()
|
||||||
|
|
||||||
|
x.partInit.SetObjectID(v2Hdr.GetObjectID())
|
||||||
|
x.partInit.SetHeader(v2Hdr.GetHeader())
|
||||||
|
x.partInit.SetSignature(v2Hdr.GetSignature())
|
||||||
|
|
||||||
|
return x.ctxCall.writeRequest()
|
||||||
|
}
|
||||||
|
|
||||||
|
// WritePayloadChunk writes chunk of the object payload. Result means success.
|
||||||
|
// Failure reason can be received via Close.
|
||||||
|
func (x *ObjectWriter) WritePayloadChunk(chunk []byte) bool {
|
||||||
|
if !x.chunkCalled {
|
||||||
|
x.chunkCalled = true
|
||||||
|
x.ctxCall.req.(*v2object.PutRequest).GetBody().SetObjectPart(&x.partChunk)
|
||||||
|
}
|
||||||
|
|
||||||
|
for ln := len(chunk); ln > 0; ln = len(chunk) {
|
||||||
|
if ln > 512 {
|
||||||
|
ln = 512
|
||||||
|
}
|
||||||
|
|
||||||
|
x.partChunk.SetChunk(chunk[:ln])
|
||||||
|
|
||||||
|
if !x.ctxCall.writeRequest() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
chunk = chunk[ln:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close ends writing the object and returns the result of the operation
|
||||||
|
// along with the final results. Must be called after using the ObjectWriter.
|
||||||
|
//
|
||||||
|
// 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 Go built-in error.
|
||||||
|
// If Client is tuned to resolve NeoFS API statuses, then NeoFS failures
|
||||||
|
// codes are returned as error.
|
||||||
|
//
|
||||||
|
// Return statuses:
|
||||||
|
// global (see Client docs).
|
||||||
|
func (x *ObjectWriter) Close() (*ResObjectPut, error) {
|
||||||
|
defer x.cancelCtxStream()
|
||||||
|
|
||||||
|
if x.ctxCall.err != nil {
|
||||||
|
return nil, x.ctxCall.err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !x.ctxCall.close() {
|
||||||
|
return nil, x.ctxCall.err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !x.ctxCall.processResponse() {
|
||||||
|
return nil, x.ctxCall.err
|
||||||
|
}
|
||||||
|
|
||||||
|
return x.ctxCall.statusRes.(*ResObjectPut), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObjectPutInit initiates writing an object through a remote server using NeoFS API protocol.
|
||||||
|
//
|
||||||
|
// The call only opens the transmission channel, explicit recording is done using the ObjectWriter.
|
||||||
|
// Exactly one return value is non-nil. Resulting writer must be finally closed.
|
||||||
|
//
|
||||||
|
// Context is required and must not be nil. It is used for network communication.
|
||||||
|
func (c *Client) ObjectPutInit(ctx context.Context, _ PrmObjectPutInit) (*ObjectWriter, error) {
|
||||||
|
// check parameters
|
||||||
|
if ctx == nil {
|
||||||
|
panic(panicMsgMissingContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
// open stream
|
||||||
|
var (
|
||||||
|
res ResObjectPut
|
||||||
|
w ObjectWriter
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx, w.cancelCtxStream = context.WithCancel(ctx)
|
||||||
|
|
||||||
|
stream, err := rpcapi.PutObject(c.Raw(), &res.resp, client.WithContext(ctx))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("open stream: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// form request body
|
||||||
|
var body v2object.PutRequestBody
|
||||||
|
|
||||||
|
// form request
|
||||||
|
var req v2object.PutRequest
|
||||||
|
|
||||||
|
req.SetBody(&body)
|
||||||
|
|
||||||
|
req.SetMetaHeader(&w.metaHdr)
|
||||||
|
body.SetObjectPart(&w.partInit)
|
||||||
|
|
||||||
|
// init call context
|
||||||
|
c.initCallContext(&w.ctxCall)
|
||||||
|
w.ctxCall.req = &req
|
||||||
|
w.ctxCall.statusRes = &res
|
||||||
|
w.ctxCall.resp = &res.resp
|
||||||
|
w.ctxCall.wReq = func() error {
|
||||||
|
return stream.Write(&req)
|
||||||
|
}
|
||||||
|
w.ctxCall.closer = stream.Close
|
||||||
|
|
||||||
|
return &w, nil
|
||||||
|
}
|
1
go.mod
1
go.mod
|
@ -11,7 +11,6 @@ require (
|
||||||
github.com/nspcc-dev/hrw v1.0.9
|
github.com/nspcc-dev/hrw v1.0.9
|
||||||
github.com/nspcc-dev/neo-go v0.98.0
|
github.com/nspcc-dev/neo-go v0.98.0
|
||||||
github.com/nspcc-dev/neofs-api-go/v2 v2.11.2-0.20220127135316-32dd0bb3f9c5
|
github.com/nspcc-dev/neofs-api-go/v2 v2.11.2-0.20220127135316-32dd0bb3f9c5
|
||||||
github.com/nspcc-dev/neofs-crypto v0.3.0
|
|
||||||
github.com/stretchr/testify v1.7.0
|
github.com/stretchr/testify v1.7.0
|
||||||
go.uber.org/zap v1.18.1
|
go.uber.org/zap v1.18.1
|
||||||
google.golang.org/grpc v1.41.0
|
google.golang.org/grpc v1.41.0
|
||||||
|
|
|
@ -224,15 +224,11 @@ func (mr *MockClientMockRecorder) GetContainer(arg0, arg1 interface{}) *gomock.C
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetContainer", reflect.TypeOf((*MockClient)(nil).GetContainer), varargs...)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetContainer", reflect.TypeOf((*MockClient)(nil).GetContainer), varargs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetObject mocks base method.
|
// ObjectGetInitmocks base method.
|
||||||
func (m *MockClient) GetObject(arg0 context.Context, arg1 *client0.GetObjectParams, arg2 ...client0.CallOption) (*client0.ObjectGetRes, error) {
|
func (m *MockClient) ObjectGetInit(arg0 context.Context, arg1 client0.PrmObjectGet) (*client0.ObjectReader, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
varargs := []interface{}{arg0, arg1}
|
ret := m.ctrl.Call(m, "GetObject", arg0, arg1)
|
||||||
for _, a := range arg2 {
|
ret0, _ := ret[0].(*client0.ObjectReader)
|
||||||
varargs = append(varargs, a)
|
|
||||||
}
|
|
||||||
ret := m.ctrl.Call(m, "GetObject", varargs...)
|
|
||||||
ret0, _ := ret[0].(*client0.ObjectGetRes)
|
|
||||||
ret1, _ := ret[1].(error)
|
ret1, _ := ret[1].(error)
|
||||||
return ret0, ret1
|
return ret0, ret1
|
||||||
}
|
}
|
||||||
|
@ -241,7 +237,7 @@ func (m *MockClient) GetObject(arg0 context.Context, arg1 *client0.GetObjectPara
|
||||||
func (mr *MockClientMockRecorder) GetObject(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
|
func (mr *MockClientMockRecorder) GetObject(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, "GetObject", reflect.TypeOf((*MockClient)(nil).GetObject), varargs...)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ObjectGetInit", reflect.TypeOf((*MockClient)(nil).ObjectGetInit), varargs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HashObjectPayloadRanges mocks base method.
|
// HashObjectPayloadRanges mocks base method.
|
||||||
|
@ -355,24 +351,20 @@ func (mr *MockClientMockRecorder) PutContainer(arg0, arg1 interface{}) *gomock.C
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutContainer", reflect.TypeOf((*MockClient)(nil).PutContainer), varargs...)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutContainer", reflect.TypeOf((*MockClient)(nil).PutContainer), varargs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PutObject mocks base method.
|
// ObjectPutInitmocks base method.
|
||||||
func (m *MockClient) PutObject(arg0 context.Context, arg1 *client0.PutObjectParams, arg2 ...client0.CallOption) (*client0.ObjectPutRes, error) {
|
func (m *MockClient) ObjectPutInit(arg0 context.Context, arg1 client0.PrmObjectPutInit) (*client0.ObjectWriter, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
varargs := []interface{}{arg0, arg1}
|
ret := m.ctrl.Call(m, "PutObject", arg0)
|
||||||
for _, a := range arg2 {
|
ret0, _ := ret[0].(*client0.ObjectWriter)
|
||||||
varargs = append(varargs, a)
|
|
||||||
}
|
|
||||||
ret := m.ctrl.Call(m, "PutObject", varargs...)
|
|
||||||
ret0, _ := ret[0].(*client0.ObjectPutRes)
|
|
||||||
ret1, _ := ret[1].(error)
|
ret1, _ := ret[1].(error)
|
||||||
return ret0, ret1
|
return ret0, ret1
|
||||||
}
|
}
|
||||||
|
|
||||||
// PutObject indicates an expected call of PutObject.
|
// PutObject indicates an expected call of PutObject.
|
||||||
func (mr *MockClientMockRecorder) PutObject(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
|
func (mr *MockClientMockRecorder) PutObject(arg0, arg1 interface{}) *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
varargs := append([]interface{}{arg0, arg1}, arg2...)
|
varargs := append([]interface{}{arg0, arg1})
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutObject", reflect.TypeOf((*MockClient)(nil).PutObject), varargs...)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ObjectPutInit", reflect.TypeOf((*MockClient)(nil).ObjectPutInit), varargs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Raw mocks base method.
|
// Raw mocks base method.
|
||||||
|
|
237
pool/pool.go
237
pool/pool.go
|
@ -1,6 +1,7 @@
|
||||||
package pool
|
package pool
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
@ -22,6 +23,7 @@ import (
|
||||||
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
|
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
|
||||||
"github.com/nspcc-dev/neofs-sdk-go/eacl"
|
"github.com/nspcc-dev/neofs-sdk-go/eacl"
|
||||||
"github.com/nspcc-dev/neofs-sdk-go/object"
|
"github.com/nspcc-dev/neofs-sdk-go/object"
|
||||||
|
"github.com/nspcc-dev/neofs-sdk-go/object/address"
|
||||||
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
|
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
|
||||||
"github.com/nspcc-dev/neofs-sdk-go/owner"
|
"github.com/nspcc-dev/neofs-sdk-go/owner"
|
||||||
"github.com/nspcc-dev/neofs-sdk-go/session"
|
"github.com/nspcc-dev/neofs-sdk-go/session"
|
||||||
|
@ -41,9 +43,9 @@ type Client interface {
|
||||||
AnnounceContainerUsedSpace(context.Context, client.AnnounceSpacePrm) (*client.AnnounceSpaceRes, error)
|
AnnounceContainerUsedSpace(context.Context, client.AnnounceSpacePrm) (*client.AnnounceSpaceRes, error)
|
||||||
EndpointInfo(context.Context, client.EndpointInfoPrm) (*client.EndpointInfoRes, error)
|
EndpointInfo(context.Context, client.EndpointInfoPrm) (*client.EndpointInfoRes, error)
|
||||||
NetworkInfo(context.Context, client.NetworkInfoPrm) (*client.NetworkInfoRes, error)
|
NetworkInfo(context.Context, client.NetworkInfoPrm) (*client.NetworkInfoRes, error)
|
||||||
PutObject(context.Context, *client.PutObjectParams, ...client.CallOption) (*client.ObjectPutRes, 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)
|
||||||
GetObject(context.Context, *client.GetObjectParams, ...client.CallOption) (*client.ObjectGetRes, error)
|
ObjectGetInit(context.Context, client.PrmObjectGet) (*client.ObjectReader, error)
|
||||||
HeadObject(context.Context, *client.ObjectHeaderParams, ...client.CallOption) (*client.ObjectHeadRes, error)
|
HeadObject(context.Context, *client.ObjectHeaderParams, ...client.CallOption) (*client.ObjectHeadRes, 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)
|
||||||
|
@ -159,9 +161,9 @@ type Pool interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Object interface {
|
type Object interface {
|
||||||
PutObject(ctx context.Context, params *client.PutObjectParams, 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, params *client.GetObjectParams, opts ...CallOption) (*object.Object, error)
|
GetObject(ctx context.Context, addr address.Address, opts ...CallOption) (*ResGetObject, error)
|
||||||
GetObjectHeader(ctx context.Context, params *client.ObjectHeaderParams, opts ...CallOption) (*object.Object, error)
|
GetObjectHeader(ctx context.Context, params *client.ObjectHeaderParams, opts ...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)
|
||||||
|
@ -282,9 +284,9 @@ func newPool(ctx context.Context, options *BuilderOptions) (Pool, error) {
|
||||||
|
|
||||||
for i, params := range options.nodesParams {
|
for i, params := range options.nodesParams {
|
||||||
clientPacks := make([]*clientPack, len(params.weights))
|
clientPacks := make([]*clientPack, len(params.weights))
|
||||||
for j, address := range params.addresses {
|
for j, addr := range params.addresses {
|
||||||
c, err := options.clientBuilder(client.WithDefaultPrivateKey(options.Key),
|
c, err := options.clientBuilder(client.WithDefaultPrivateKey(options.Key),
|
||||||
client.WithURIAddress(address, nil),
|
client.WithURIAddress(addr, nil),
|
||||||
client.WithDialTimeout(options.NodeConnectionTimeout),
|
client.WithDialTimeout(options.NodeConnectionTimeout),
|
||||||
client.WithNeoFSErrorParsing())
|
client.WithNeoFSErrorParsing())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -294,14 +296,14 @@ func newPool(ctx context.Context, options *BuilderOptions) (Pool, error) {
|
||||||
cliRes, err := createSessionTokenForDuration(ctx, c, options.SessionExpirationDuration)
|
cliRes, err := createSessionTokenForDuration(ctx, c, options.SessionExpirationDuration)
|
||||||
if err != nil && options.Logger != nil {
|
if err != nil && options.Logger != nil {
|
||||||
options.Logger.Warn("failed to create neofs session token for client",
|
options.Logger.Warn("failed to create neofs session token for client",
|
||||||
zap.String("address", address),
|
zap.String("address", addr),
|
||||||
zap.Error(err))
|
zap.Error(err))
|
||||||
} else if err == nil {
|
} else if err == nil {
|
||||||
healthy, atLeastOneHealthy = true, true
|
healthy, atLeastOneHealthy = true, true
|
||||||
st := sessionTokenForOwner(ownerID, cliRes)
|
st := sessionTokenForOwner(ownerID, cliRes)
|
||||||
_ = cache.Put(formCacheKey(address, options.Key), st)
|
_ = cache.Put(formCacheKey(addr, options.Key), st)
|
||||||
}
|
}
|
||||||
clientPacks[j] = &clientPack{client: c, healthy: healthy, address: address}
|
clientPacks[j] = &clientPack{client: c, healthy: healthy, address: addr}
|
||||||
}
|
}
|
||||||
source := rand.NewSource(time.Now().UnixNano())
|
source := rand.NewSource(time.Now().UnixNano())
|
||||||
sampler := NewSampler(params.weights, source)
|
sampler := NewSampler(params.weights, source)
|
||||||
|
@ -448,8 +450,8 @@ func (p *pool) Connection() (Client, *session.Token, error) {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
token := p.cache.Get(formCacheKey(cp.address, p.key))
|
tok := p.cache.Get(formCacheKey(cp.address, p.key))
|
||||||
return cp.client, token, nil
|
return cp.client, tok, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *pool) connection() (*clientPack, error) {
|
func (p *pool) connection() (*clientPack, error) {
|
||||||
|
@ -585,7 +587,68 @@ func (p *pool) removeSessionTokenAfterThreshold(cfg *callConfig) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *pool) PutObject(ctx context.Context, params *client.PutObjectParams, opts ...CallOption) (*oid.ID, error) {
|
type callContext struct {
|
||||||
|
// context for RPC
|
||||||
|
ctxBase context.Context
|
||||||
|
|
||||||
|
client Client
|
||||||
|
|
||||||
|
// client endpoint
|
||||||
|
endpoint string
|
||||||
|
|
||||||
|
// request signer
|
||||||
|
key *ecdsa.PrivateKey
|
||||||
|
|
||||||
|
// flag to open default session if session token is missing
|
||||||
|
sessionDefault bool
|
||||||
|
sessionToken *session.Token
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pool) prepareCallContext(ctx *callContext, cfg *callConfig) error {
|
||||||
|
cp, err := p.connection()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.endpoint = cp.address
|
||||||
|
ctx.client = cp.client
|
||||||
|
|
||||||
|
ctx.key = cfg.key
|
||||||
|
if ctx.key == nil {
|
||||||
|
ctx.key = p.key
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.sessionDefault = cfg.useDefaultSession
|
||||||
|
ctx.sessionToken = cfg.stoken
|
||||||
|
|
||||||
|
if ctx.sessionToken == nil && ctx.sessionDefault {
|
||||||
|
cacheKey := formCacheKey(ctx.endpoint, ctx.key)
|
||||||
|
|
||||||
|
ctx.sessionToken = p.cache.Get(cacheKey)
|
||||||
|
if ctx.sessionToken == nil {
|
||||||
|
var cliPrm client.CreateSessionPrm
|
||||||
|
|
||||||
|
cliPrm.SetExp(math.MaxUint32)
|
||||||
|
|
||||||
|
cliRes, err := ctx.client.CreateSession(ctx.ctxBase, cliPrm)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("default session: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.sessionToken = sessionTokenForOwner(owner.NewIDFromPublicKey(&ctx.key.PublicKey), cliRes)
|
||||||
|
|
||||||
|
_ = p.cache.Put(cacheKey, ctx.sessionToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.sessionToken != nil && ctx.sessionToken.Signature() == nil {
|
||||||
|
err = ctx.sessionToken.Sign(ctx.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pool) PutObject(ctx context.Context, hdr object.Object, payload io.Reader, opts ...CallOption) (*oid.ID, error) {
|
||||||
cfg := cfgFromOpts(append(opts, useDefaultSession())...)
|
cfg := cfgFromOpts(append(opts, useDefaultSession())...)
|
||||||
|
|
||||||
// Put object is different from other object service methods. Put request
|
// Put object is different from other object service methods. Put request
|
||||||
|
@ -603,21 +666,90 @@ func (p *pool) PutObject(ctx context.Context, params *client.PutObjectParams, op
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
cp, options, err := p.conn(ctx, cfg)
|
var ctxCall callContext
|
||||||
|
|
||||||
|
ctxCall.ctxBase = ctx
|
||||||
|
|
||||||
|
err = p.prepareCallContext(&ctxCall, cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := cp.client.PutObject(ctx, params, options...)
|
var prm client.PrmObjectPutInit
|
||||||
|
|
||||||
// removes session token from cache in case of token error
|
wObj, err := ctxCall.client.ObjectPutInit(ctx, prm)
|
||||||
_ = p.checkSessionTokenErr(err, cp.address)
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("init writing on API client: %w", err)
|
||||||
if err != nil { // here err already carries both status and client errors
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.ID(), nil
|
wObj.UseKey(*ctxCall.key)
|
||||||
|
|
||||||
|
if ctxCall.sessionToken != nil {
|
||||||
|
wObj.WithinSession(*ctxCall.sessionToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.btoken != nil {
|
||||||
|
wObj.WithBearerToken(*cfg.btoken)
|
||||||
|
}
|
||||||
|
|
||||||
|
if wObj.WriteHeader(hdr) {
|
||||||
|
sz := hdr.PayloadSize()
|
||||||
|
|
||||||
|
if data := hdr.Payload(); len(data) > 0 {
|
||||||
|
if payload != nil {
|
||||||
|
payload = io.MultiReader(bytes.NewReader(data), payload)
|
||||||
|
} else {
|
||||||
|
payload = bytes.NewReader(data)
|
||||||
|
sz = uint64(len(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload != nil {
|
||||||
|
const defaultBufferSizePut = 4096 // configure?
|
||||||
|
|
||||||
|
if sz == 0 || sz > defaultBufferSizePut {
|
||||||
|
sz = defaultBufferSizePut
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, sz)
|
||||||
|
|
||||||
|
var n int
|
||||||
|
var ok bool
|
||||||
|
|
||||||
|
for {
|
||||||
|
n, err = payload.Read(buf)
|
||||||
|
if n > 0 {
|
||||||
|
ok = wObj.WritePayloadChunk(buf[:n])
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("read payload: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := wObj.Close()
|
||||||
|
if err != nil { // here err already carries both status and client errors
|
||||||
|
// removes session token from cache in case of token error
|
||||||
|
p.checkSessionTokenErr(err, ctxCall.endpoint)
|
||||||
|
return nil, fmt.Errorf("client failure: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var id oid.ID
|
||||||
|
|
||||||
|
if !res.ReadStoredObject(&id) {
|
||||||
|
return nil, errors.New("missing ID of the stored object")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *pool) DeleteObject(ctx context.Context, params *client.DeleteObjectParams, opts ...CallOption) error {
|
func (p *pool) DeleteObject(ctx context.Context, params *client.DeleteObjectParams, opts ...CallOption) error {
|
||||||
|
@ -639,25 +771,74 @@ func (p *pool) DeleteObject(ctx context.Context, params *client.DeleteObjectPara
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *pool) GetObject(ctx context.Context, params *client.GetObjectParams, opts ...CallOption) (*object.Object, error) {
|
type objectReadCloser client.ObjectReader
|
||||||
|
|
||||||
|
func (x *objectReadCloser) Read(p []byte) (int, error) {
|
||||||
|
return (*client.ObjectReader)(x).Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *objectReadCloser) Close() error {
|
||||||
|
_, err := (*client.ObjectReader)(x).Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResGetObject struct {
|
||||||
|
Header object.Object
|
||||||
|
|
||||||
|
Payload io.ReadCloser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pool) GetObject(ctx context.Context, addr address.Address, opts ...CallOption) (*ResGetObject, error) {
|
||||||
cfg := cfgFromOpts(append(opts, useDefaultSession())...)
|
cfg := cfgFromOpts(append(opts, useDefaultSession())...)
|
||||||
cp, options, err := p.conn(ctx, cfg)
|
|
||||||
|
var ctxCall callContext
|
||||||
|
|
||||||
|
ctxCall.ctxBase = ctx
|
||||||
|
|
||||||
|
err := p.prepareCallContext(&ctxCall, cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := cp.client.GetObject(ctx, params, options...)
|
var prm client.PrmObjectGet
|
||||||
|
|
||||||
if p.checkSessionTokenErr(err, cp.address) && !cfg.isRetry {
|
if cnr := addr.ContainerID(); cnr != nil {
|
||||||
opts = append(opts, retry())
|
prm.FromContainer(*cnr)
|
||||||
return p.GetObject(ctx, params, opts...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil { // here err already carries both status and client errors
|
if obj := addr.ObjectID(); obj != nil {
|
||||||
|
prm.ByID(*obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctxCall.sessionToken != nil {
|
||||||
|
prm.WithinSession(*ctxCall.sessionToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.btoken != nil {
|
||||||
|
prm.WithBearerToken(*cfg.btoken)
|
||||||
|
}
|
||||||
|
|
||||||
|
rObj, err := ctxCall.client.ObjectGetInit(ctx, prm)
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.Object(), nil
|
rObj.UseKey(*ctxCall.key)
|
||||||
|
|
||||||
|
var res ResGetObject
|
||||||
|
|
||||||
|
if !rObj.ReadHeader(&res.Header) {
|
||||||
|
_, err = rObj.Close()
|
||||||
|
if p.checkSessionTokenErr(err, ctxCall.endpoint) && !cfg.isRetry {
|
||||||
|
return p.GetObject(ctx, addr, append(opts, retry())...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("read header: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Payload = (*objectReadCloser)(rObj)
|
||||||
|
|
||||||
|
return &res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *pool) GetObjectHeader(ctx context.Context, params *client.ObjectHeaderParams, opts ...CallOption) (*object.Object, error) {
|
func (p *pool) GetObjectHeader(ctx context.Context, params *client.ObjectHeaderParams, opts ...CallOption) (*object.Object, error) {
|
||||||
|
|
|
@ -15,6 +15,8 @@ import (
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||||
"github.com/nspcc-dev/neofs-sdk-go/client"
|
"github.com/nspcc-dev/neofs-sdk-go/client"
|
||||||
"github.com/nspcc-dev/neofs-sdk-go/netmap"
|
"github.com/nspcc-dev/neofs-sdk-go/netmap"
|
||||||
|
"github.com/nspcc-dev/neofs-sdk-go/object"
|
||||||
|
"github.com/nspcc-dev/neofs-sdk-go/object/address"
|
||||||
"github.com/nspcc-dev/neofs-sdk-go/owner"
|
"github.com/nspcc-dev/neofs-sdk-go/owner"
|
||||||
"github.com/nspcc-dev/neofs-sdk-go/session"
|
"github.com/nspcc-dev/neofs-sdk-go/session"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
@ -329,7 +331,7 @@ func TestSessionCache(t *testing.T) {
|
||||||
}).MaxTimes(3)
|
}).MaxTimes(3)
|
||||||
|
|
||||||
mockClient.EXPECT().GetObject(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("session token does not exist"))
|
mockClient.EXPECT().GetObject(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("session token does not exist"))
|
||||||
mockClient.EXPECT().PutObject(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil)
|
mockClient.EXPECT().PutObject(gomock.Any(), gomock.Any()).Return(nil, nil)
|
||||||
|
|
||||||
return mockClient, nil
|
return mockClient, nil
|
||||||
}
|
}
|
||||||
|
@ -355,7 +357,7 @@ func TestSessionCache(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Contains(t, tokens, st)
|
require.Contains(t, tokens, st)
|
||||||
|
|
||||||
_, err = pool.GetObject(ctx, nil, retry())
|
_, err = pool.GetObject(ctx, address.Address{}, retry())
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
// cache must not contain session token
|
// cache must not contain session token
|
||||||
|
@ -363,7 +365,7 @@ func TestSessionCache(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Nil(t, st)
|
require.Nil(t, st)
|
||||||
|
|
||||||
_, err = pool.PutObject(ctx, nil)
|
_, err = pool.PutObject(ctx, object.Object{}, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// cache must contain session token
|
// cache must contain session token
|
||||||
|
@ -481,7 +483,7 @@ func TestSessionCacheWithKey(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Contains(t, tokens, st)
|
require.Contains(t, tokens, st)
|
||||||
|
|
||||||
_, err = pool.GetObject(ctx, nil, WithKey(newPrivateKey(t)))
|
_, err = pool.GetObject(ctx, address.Address{}, WithKey(newPrivateKey(t)))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, tokens, 2)
|
require.Len(t, tokens, 2)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue