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 }