git.frostfs.info/TrueCloudLab/frostfs-sdk-go@v0.0.0-20241022124111-5361f0ecebd3/client/object_put_raw.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  	rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
    13  	"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
    14  	v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
    15  	"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
    16  	apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
    17  	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
    18  )
    19  
    20  func (c *Client) objectPutInitRaw(ctx context.Context, prm PrmObjectPutInit) (*objectWriterRaw, error) {
    21  	if len(prm.XHeaders)%2 != 0 {
    22  		return nil, errorInvalidXHeaders
    23  	}
    24  
    25  	var w objectWriterRaw
    26  	stream, err := rpcapi.PutObject(&c.c, &w.respV2, client.WithContext(ctx))
    27  	if err != nil {
    28  		return nil, fmt.Errorf("open stream: %w", err)
    29  	}
    30  
    31  	w.key = &c.prm.Key
    32  	if prm.Key != nil {
    33  		w.key = prm.Key
    34  	}
    35  	w.client = c
    36  	w.stream = stream
    37  	w.partInit.SetCopiesNumber(prm.CopiesNumber)
    38  	w.req.SetBody(new(v2object.PutRequestBody))
    39  	if prm.MaxChunkLength > 0 {
    40  		w.maxChunkLen = prm.MaxChunkLength
    41  	} else {
    42  		w.maxChunkLen = defaultGRPCPayloadChunkLen
    43  	}
    44  
    45  	meta := new(v2session.RequestMetaHeader)
    46  	writeXHeadersToMeta(prm.XHeaders, meta)
    47  
    48  	if prm.BearerToken != nil {
    49  		v2BearerToken := new(acl.BearerToken)
    50  		prm.BearerToken.WriteToV2(v2BearerToken)
    51  		meta.SetBearerToken(v2BearerToken)
    52  	}
    53  
    54  	if prm.Session != nil {
    55  		v2SessionToken := new(v2session.Token)
    56  		prm.Session.WriteToV2(v2SessionToken)
    57  		meta.SetSessionToken(v2SessionToken)
    58  	}
    59  
    60  	if prm.Local {
    61  		meta.SetTTL(1)
    62  	}
    63  
    64  	c.prepareRequest(&w.req, meta)
    65  	return &w, nil
    66  }
    67  
    68  type objectWriterRaw struct {
    69  	client *Client
    70  	stream interface {
    71  		Write(*v2object.PutRequest) error
    72  		Close() error
    73  	}
    74  
    75  	key         *ecdsa.PrivateKey
    76  	res         ResObjectPut
    77  	err         error
    78  	chunkCalled bool
    79  	respV2      v2object.PutResponse
    80  	req         v2object.PutRequest
    81  	partInit    v2object.PutObjectPartInit
    82  	partChunk   v2object.PutObjectPartChunk
    83  	maxChunkLen int
    84  }
    85  
    86  func (x *objectWriterRaw) WriteHeader(_ context.Context, hdr object.Object) bool {
    87  	v2Hdr := hdr.ToV2()
    88  
    89  	x.partInit.SetObjectID(v2Hdr.GetObjectID())
    90  	x.partInit.SetHeader(v2Hdr.GetHeader())
    91  	x.partInit.SetSignature(v2Hdr.GetSignature())
    92  
    93  	x.req.GetBody().SetObjectPart(&x.partInit)
    94  	x.req.SetVerificationHeader(nil)
    95  
    96  	x.err = signature.SignServiceMessage(x.key, &x.req)
    97  	if x.err != nil {
    98  		x.err = fmt.Errorf("sign message: %w", x.err)
    99  		return false
   100  	}
   101  
   102  	x.err = x.stream.Write(&x.req)
   103  	return x.err == nil
   104  }
   105  
   106  func (x *objectWriterRaw) WritePayloadChunk(_ context.Context, chunk []byte) bool {
   107  	if !x.chunkCalled {
   108  		x.chunkCalled = true
   109  		x.req.GetBody().SetObjectPart(&x.partChunk)
   110  	}
   111  
   112  	for ln := len(chunk); ln > 0; ln = len(chunk) {
   113  		if ln > x.maxChunkLen {
   114  			ln = x.maxChunkLen
   115  		}
   116  
   117  		// we deal with size limit overflow above, but there is another case:
   118  		// what if method is called with "small" chunk many times? We write
   119  		// a message to the stream on each call. Alternatively, we could use buffering.
   120  		// In most cases, the chunk length does not vary between calls. Given this
   121  		// assumption, as well as the length of the payload from the header, it is
   122  		// possible to buffer the data of intermediate chunks, and send a message when
   123  		// the allocated buffer is filled, or when the last chunk is received.
   124  		// It is mentally assumed that allocating and filling the buffer is better than
   125  		// synchronous sending, but this needs to be tested.
   126  		x.partChunk.SetChunk(chunk[:ln])
   127  		x.req.SetVerificationHeader(nil)
   128  
   129  		x.err = signature.SignServiceMessage(x.key, &x.req)
   130  		if x.err != nil {
   131  			x.err = fmt.Errorf("sign message: %w", x.err)
   132  			return false
   133  		}
   134  
   135  		x.err = x.stream.Write(&x.req)
   136  		if x.err != nil {
   137  			return false
   138  		}
   139  
   140  		chunk = chunk[ln:]
   141  	}
   142  
   143  	return true
   144  }
   145  
   146  func (x *objectWriterRaw) Close(_ context.Context) (*ResObjectPut, error) {
   147  	// Ignore io.EOF error, because it is expected error for client-side
   148  	// stream termination by the server. E.g. when stream contains invalid
   149  	// message. Server returns an error in response message (in status).
   150  	if x.err != nil && !errors.Is(x.err, io.EOF) {
   151  		return nil, x.err
   152  	}
   153  
   154  	if x.err = x.stream.Close(); x.err != nil {
   155  		return nil, x.err
   156  	}
   157  
   158  	x.res.st, x.err = x.client.processResponse(&x.respV2)
   159  	if x.err != nil || !apistatus.IsSuccessful(x.res.st) {
   160  		return &x.res, x.err
   161  	}
   162  
   163  	const fieldID = "ID"
   164  
   165  	idV2 := x.respV2.GetBody().GetObjectID()
   166  	if idV2 == nil {
   167  		return nil, newErrMissingResponseField(fieldID)
   168  	}
   169  
   170  	x.err = x.res.obj.ReadFromV2(*idV2)
   171  	if x.err != nil {
   172  		x.err = newErrInvalidResponseField(fieldID, x.err)
   173  	}
   174  	x.res.epoch = x.respV2.GetMetaHeader().GetEpoch()
   175  
   176  	return &x.res, nil
   177  }