git.frostfs.info/TrueCloudLab/frostfs-sdk-go@v0.0.0-20241022124111-5361f0ecebd3/client/object_patch.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  	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
    17  	apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
    18  	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
    19  	oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
    20  	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
    21  )
    22  
    23  // ObjectPatcher is designed to patch an object.
    24  //
    25  // Must be initialized using Client.ObjectPatchInit, any other
    26  // usage is unsafe.
    27  type ObjectPatcher interface {
    28  	// PatchAttributes patches attributes. Attributes can be patched no more than once,
    29  	// otherwise, the server returns an error.
    30  	//
    31  	// Result means success. Failure reason can be received via Close.
    32  	PatchAttributes(ctx context.Context, newAttrs []object.Attribute, replace bool) bool
    33  
    34  	// PatchPayload patches the object's payload.
    35  	//
    36  	// PatchPayload receives `payloadReader` and thus the payload of the patch is read and sent by chunks of
    37  	// `MaxChunkLength` length.
    38  	//
    39  	// Result means success. Failure reason can be received via Close.
    40  	PatchPayload(ctx context.Context, rng *object.Range, payloadReader io.Reader) bool
    41  
    42  	// Close ends patching the object and returns the result of the operation
    43  	// along with the final results. Must be called after using the ObjectPatcher.
    44  	//
    45  	// Exactly one return value is non-nil. By default, server status is returned in res structure.
    46  	// Any client's internal or transport errors are returned as Go built-in error.
    47  	// If Client is tuned to resolve FrostFS API statuses, then FrostFS failures
    48  	// codes are returned as error.
    49  	//
    50  	// Return statuses:
    51  	//   - global (see Client docs);
    52  	//   - *apistatus.ContainerNotFound;
    53  	//   - *apistatus.ContainerAccessDenied;
    54  	//   - *apistatus.ObjectAccessDenied;
    55  	//   - *apistatus.ObjectAlreadyRemoved;
    56  	//   - *apistatus.ObjectLocked;
    57  	//   - *apistatus.ObjectOutOfRange;
    58  	//   - *apistatus.SessionTokenNotFound;
    59  	//   - *apistatus.SessionTokenExpired.
    60  	Close(_ context.Context) (*ResObjectPatch, error)
    61  }
    62  
    63  // ResObjectPatch groups resulting values of ObjectPatch operation.
    64  type ResObjectPatch struct {
    65  	statusRes
    66  
    67  	obj oid.ID
    68  }
    69  
    70  // ObjectID returns an object ID of the patched object.
    71  func (r ResObjectPatch) ObjectID() oid.ID {
    72  	return r.obj
    73  }
    74  
    75  // PrmObjectPatch groups parameters of ObjectPatch operation.
    76  type PrmObjectPatch struct {
    77  	XHeaders []string
    78  
    79  	Address oid.Address
    80  
    81  	BearerToken *bearer.Token
    82  
    83  	Session *session.Object
    84  
    85  	Key *ecdsa.PrivateKey
    86  
    87  	MaxChunkLength int
    88  }
    89  
    90  // ObjectPatchInit initializes object patcher.
    91  func (c *Client) ObjectPatchInit(ctx context.Context, prm PrmObjectPatch) (ObjectPatcher, error) {
    92  	if len(prm.XHeaders)%2 != 0 {
    93  		return nil, errorInvalidXHeaders
    94  	}
    95  
    96  	var objectPatcher objectPatcher
    97  	stream, err := rpcapi.Patch(&c.c, &objectPatcher.respV2, client.WithContext(ctx))
    98  	if err != nil {
    99  		return nil, fmt.Errorf("open stream: %w", err)
   100  	}
   101  
   102  	objectPatcher.addr = prm.Address
   103  	objectPatcher.key = &c.prm.Key
   104  	if prm.Key != nil {
   105  		objectPatcher.key = prm.Key
   106  	}
   107  	objectPatcher.client = c
   108  	objectPatcher.stream = stream
   109  
   110  	if prm.MaxChunkLength > 0 {
   111  		objectPatcher.maxChunkLen = prm.MaxChunkLength
   112  	} else {
   113  		objectPatcher.maxChunkLen = defaultGRPCPayloadChunkLen
   114  	}
   115  
   116  	objectPatcher.req.SetBody(&v2object.PatchRequestBody{})
   117  
   118  	meta := new(v2session.RequestMetaHeader)
   119  	writeXHeadersToMeta(prm.XHeaders, meta)
   120  
   121  	if prm.BearerToken != nil {
   122  		v2BearerToken := new(acl.BearerToken)
   123  		prm.BearerToken.WriteToV2(v2BearerToken)
   124  		meta.SetBearerToken(v2BearerToken)
   125  	}
   126  
   127  	if prm.Session != nil {
   128  		v2SessionToken := new(v2session.Token)
   129  		prm.Session.WriteToV2(v2SessionToken)
   130  		meta.SetSessionToken(v2SessionToken)
   131  	}
   132  
   133  	c.prepareRequest(&objectPatcher.req, meta)
   134  
   135  	return &objectPatcher, nil
   136  }
   137  
   138  type objectPatcher struct {
   139  	client *Client
   140  
   141  	stream interface {
   142  		Write(*v2object.PatchRequest) error
   143  		Close() error
   144  	}
   145  
   146  	key *ecdsa.PrivateKey
   147  	res ResObjectPatch
   148  	err error
   149  
   150  	addr oid.Address
   151  
   152  	req    v2object.PatchRequest
   153  	respV2 v2object.PatchResponse
   154  
   155  	maxChunkLen int
   156  }
   157  
   158  func (x *objectPatcher) PatchAttributes(_ context.Context, newAttrs []object.Attribute, replace bool) bool {
   159  	return x.patch(&object.Patch{
   160  		Address:           x.addr,
   161  		NewAttributes:     newAttrs,
   162  		ReplaceAttributes: replace,
   163  	})
   164  }
   165  
   166  func (x *objectPatcher) PatchPayload(_ context.Context, rng *object.Range, payloadReader io.Reader) bool {
   167  	offset := rng.GetOffset()
   168  
   169  	buf := make([]byte, x.maxChunkLen)
   170  
   171  	for patchIter := 0; ; patchIter++ {
   172  		n, err := payloadReader.Read(buf)
   173  		if err != nil && err != io.EOF {
   174  			x.err = fmt.Errorf("read payload: %w", err)
   175  			return false
   176  		}
   177  		if n == 0 {
   178  			if patchIter == 0 {
   179  				if rng.GetLength() == 0 {
   180  					x.err = errors.New("zero-length empty payload patch can't be applied")
   181  					return false
   182  				}
   183  				if !x.patch(&object.Patch{
   184  					Address: x.addr,
   185  					PayloadPatch: &object.PayloadPatch{
   186  						Range: rng,
   187  						Chunk: []byte{},
   188  					},
   189  				}) {
   190  					return false
   191  				}
   192  			}
   193  			break
   194  		}
   195  
   196  		rngPart := object.NewRange()
   197  		if patchIter == 0 {
   198  			rngPart.SetOffset(offset)
   199  			rngPart.SetLength(rng.GetLength())
   200  		} else {
   201  			rngPart.SetOffset(offset + rng.GetLength())
   202  		}
   203  
   204  		if !x.patch(&object.Patch{
   205  			Address: x.addr,
   206  			PayloadPatch: &object.PayloadPatch{
   207  				Range: rngPart,
   208  				Chunk: buf[:n],
   209  			},
   210  		}) {
   211  			return false
   212  		}
   213  
   214  		if err == io.EOF {
   215  			break
   216  		}
   217  	}
   218  
   219  	return true
   220  }
   221  
   222  func (x *objectPatcher) patch(patch *object.Patch) bool {
   223  	x.req.SetBody(patch.ToV2())
   224  	x.req.SetVerificationHeader(nil)
   225  
   226  	x.err = signature.SignServiceMessage(x.key, &x.req)
   227  	if x.err != nil {
   228  		x.err = fmt.Errorf("sign message: %w", x.err)
   229  		return false
   230  	}
   231  
   232  	x.err = x.stream.Write(&x.req)
   233  	return x.err == nil
   234  }
   235  
   236  func (x *objectPatcher) Close(_ context.Context) (*ResObjectPatch, error) {
   237  	// Ignore io.EOF error, because it is expected error for client-side
   238  	// stream termination by the server. E.g. when stream contains invalid
   239  	// message. Server returns an error in response message (in status).
   240  	if x.err != nil && !errors.Is(x.err, io.EOF) {
   241  		return nil, x.err
   242  	}
   243  
   244  	if x.err = x.stream.Close(); x.err != nil {
   245  		return nil, x.err
   246  	}
   247  
   248  	x.res.st, x.err = x.client.processResponse(&x.respV2)
   249  	if x.err != nil || !apistatus.IsSuccessful(x.res.st) {
   250  		return &x.res, x.err
   251  	}
   252  
   253  	const fieldID = "ID"
   254  
   255  	idV2 := x.respV2.Body.ObjectID
   256  	if idV2 == nil {
   257  		return nil, newErrMissingResponseField(fieldID)
   258  	}
   259  
   260  	x.err = x.res.obj.ReadFromV2(*idV2)
   261  	if x.err != nil {
   262  		x.err = newErrInvalidResponseField(fieldID, x.err)
   263  	}
   264  
   265  	return &x.res, nil
   266  }