Compare commits
3 commits
master
...
feat/patch
Author | SHA1 | Date | |
---|---|---|---|
1566f74d8c | |||
25fbae1582 | |||
ff563851fc |
7 changed files with 1240 additions and 3 deletions
165
client/object_patch.go
Normal file
165
client/object_patch.go
Normal file
|
@ -0,0 +1,165 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl"
|
||||
v2object "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
|
||||
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
||||
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
|
||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
||||
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
|
||||
)
|
||||
|
||||
type ResObjectPatch struct {
|
||||
statusRes
|
||||
|
||||
obj oid.ID
|
||||
}
|
||||
|
||||
func (r ResObjectPatch) ObjectID() oid.ID {
|
||||
return r.obj
|
||||
}
|
||||
|
||||
// PrmObjectPatch groups parameters of ObjectPatch operation.
|
||||
type PrmObjectPatch struct {
|
||||
XHeaders []string
|
||||
|
||||
BearerToken *bearer.Token
|
||||
|
||||
Session *session.Object
|
||||
|
||||
Local bool
|
||||
|
||||
Address oid.Address
|
||||
|
||||
Key *ecdsa.PrivateKey
|
||||
|
||||
Offset uint64
|
||||
|
||||
Length uint64
|
||||
|
||||
Chunk []byte
|
||||
}
|
||||
|
||||
func (c *Client) ObjectPatchInit(ctx context.Context, prm PrmObjectPatch) (*objectPatcher, error) {
|
||||
if len(prm.XHeaders)%2 != 0 {
|
||||
return nil, errorInvalidXHeaders
|
||||
}
|
||||
|
||||
var p objectPatcher
|
||||
stream, err := rpcapi.Patch(&c.c, &p.respV2, client.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open stream: %w", err)
|
||||
}
|
||||
|
||||
p.key = &c.prm.Key
|
||||
if prm.Key != nil {
|
||||
p.key = prm.Key
|
||||
}
|
||||
p.client = c
|
||||
p.stream = stream
|
||||
p.addr = prm.Address
|
||||
|
||||
p.req.SetBody(&v2object.PatchRequestBody{})
|
||||
|
||||
meta := new(v2session.RequestMetaHeader)
|
||||
writeXHeadersToMeta(prm.XHeaders, meta)
|
||||
|
||||
if prm.BearerToken != nil {
|
||||
v2BearerToken := new(acl.BearerToken)
|
||||
prm.BearerToken.WriteToV2(v2BearerToken)
|
||||
meta.SetBearerToken(v2BearerToken)
|
||||
}
|
||||
|
||||
if prm.Session != nil {
|
||||
v2SessionToken := new(v2session.Token)
|
||||
prm.Session.WriteToV2(v2SessionToken)
|
||||
meta.SetSessionToken(v2SessionToken)
|
||||
}
|
||||
|
||||
if prm.Local {
|
||||
meta.SetTTL(1)
|
||||
}
|
||||
|
||||
c.prepareRequest(&p.req, meta)
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
type objectPatcher struct {
|
||||
client *Client
|
||||
|
||||
stream interface {
|
||||
Write(*v2object.PatchRequest) error
|
||||
Close() error
|
||||
}
|
||||
|
||||
key *ecdsa.PrivateKey
|
||||
res ResObjectPatch
|
||||
err error
|
||||
|
||||
addr oid.Address
|
||||
|
||||
req v2object.PatchRequest
|
||||
respV2 v2object.PatchResponse
|
||||
}
|
||||
|
||||
func (x *objectPatcher) Patch(_ context.Context, patch *object.Patch) bool {
|
||||
x.req.SetBody(patch.ToV2())
|
||||
x.req.SetVerificationHeader(nil)
|
||||
|
||||
x.err = signature.SignServiceMessage(x.key, &x.req)
|
||||
if x.err != nil {
|
||||
x.err = fmt.Errorf("sign message: %w", x.err)
|
||||
return false
|
||||
}
|
||||
|
||||
x.err = x.stream.Write(&x.req)
|
||||
|
||||
return x.err == nil
|
||||
}
|
||||
|
||||
func (x *objectPatcher) Close(_ context.Context) (*ResObjectPatch, error) {
|
||||
// Ignore io.EOF error, because it is expected error for client-side
|
||||
// stream termination by the server. E.g. when stream contains invalid
|
||||
// message. Server returns an error in response message (in status).
|
||||
if x.err != nil && !errors.Is(x.err, io.EOF) {
|
||||
return nil, x.err
|
||||
}
|
||||
|
||||
if x.err = x.stream.Close(); x.err != nil {
|
||||
return nil, x.err
|
||||
}
|
||||
|
||||
x.res.st, x.err = x.client.processResponse(&x.respV2)
|
||||
if x.err != nil {
|
||||
return nil, x.err
|
||||
}
|
||||
|
||||
if !apistatus.IsSuccessful(x.res.st) {
|
||||
return &x.res, nil
|
||||
}
|
||||
|
||||
const fieldID = "ID"
|
||||
|
||||
idV2 := x.respV2.Body.ObjectID
|
||||
if idV2 == nil {
|
||||
return nil, newErrMissingResponseField(fieldID)
|
||||
}
|
||||
|
||||
x.err = x.res.obj.ReadFromV2(*idV2)
|
||||
if x.err != nil {
|
||||
x.err = newErrInvalidResponseField(fieldID, x.err)
|
||||
}
|
||||
|
||||
return &x.res, nil
|
||||
}
|
2
go.mod
2
go.mod
|
@ -3,7 +3,7 @@ module git.frostfs.info/TrueCloudLab/frostfs-sdk-go
|
|||
go 1.21
|
||||
|
||||
require (
|
||||
git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.16.1-0.20240726072425-3dfa2f4fd65e
|
||||
git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.16.1-0.20240730145254-c27b978770a3
|
||||
git.frostfs.info/TrueCloudLab/frostfs-contract v0.19.3-0.20240621131249-49e5270f673e
|
||||
git.frostfs.info/TrueCloudLab/hrw v1.2.1
|
||||
git.frostfs.info/TrueCloudLab/tzhash v1.8.0
|
||||
|
|
4
go.sum
4
go.sum
|
@ -1,5 +1,5 @@
|
|||
git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.16.1-0.20240726072425-3dfa2f4fd65e h1:gEWT+70E/RvGkxtSv+PlyUN2vtJVymhQa1mypvrXukM=
|
||||
git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.16.1-0.20240726072425-3dfa2f4fd65e/go.mod h1:OBDSr+DqV1z4VDouoX3YMleNc4DPBVBWTG3WDT2PK1o=
|
||||
git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.16.1-0.20240730145254-c27b978770a3 h1:BbtF/98HU0nBl4szdDYAV3XadNE5sJ92uSFmNePQmfA=
|
||||
git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.16.1-0.20240730145254-c27b978770a3/go.mod h1:OBDSr+DqV1z4VDouoX3YMleNc4DPBVBWTG3WDT2PK1o=
|
||||
git.frostfs.info/TrueCloudLab/frostfs-contract v0.19.3-0.20240621131249-49e5270f673e h1:kcBqZBiFIUBATUqEuvVigtkJJWQ2Gug/eYXn967o3M4=
|
||||
git.frostfs.info/TrueCloudLab/frostfs-contract v0.19.3-0.20240621131249-49e5270f673e/go.mod h1:F/fe1OoIDKr5Bz99q4sriuHDuf3aZefZy9ZsCqEtgxc=
|
||||
git.frostfs.info/TrueCloudLab/frostfs-crypto v0.6.0 h1:FxqFDhQYYgpe41qsIHVOcdzSVCB8JNSfPG7Uk4r2oSk=
|
||||
|
|
103
object/patch.go
Normal file
103
object/patch.go
Normal file
|
@ -0,0 +1,103 @@
|
|||
package object
|
||||
|
||||
import (
|
||||
v2object "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
||||
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
)
|
||||
|
||||
// Patch is a patch that's applied for an object.
|
||||
type Patch struct {
|
||||
// The address of the object for which the patch is being applied.
|
||||
Address oid.Address
|
||||
|
||||
// The list of new attributes to set in the object's header.
|
||||
NewAttributes []Attribute
|
||||
|
||||
// If ReplaceAttributes flag is true, then the header's attributes are reset and
|
||||
// filled with NewAttributes. Otherwise, the attributes are just merged.
|
||||
ReplaceAttributes bool
|
||||
|
||||
// Payload patch. If this field is not set, then it assumed such Patch patches only
|
||||
// header (see NewAttributes, ReplaceAttributes).
|
||||
PayloadPatch *PayloadPatch
|
||||
}
|
||||
|
||||
func (p *Patch) ToV2() *v2object.PatchRequestBody {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
v2 := new(v2object.PatchRequestBody)
|
||||
|
||||
addrV2 := new(refs.Address)
|
||||
p.Address.WriteToV2(addrV2)
|
||||
v2.SetAddress(addrV2)
|
||||
|
||||
attrs := make([]v2object.Attribute, len(p.NewAttributes))
|
||||
for i := range p.NewAttributes {
|
||||
attrs[i] = *p.NewAttributes[i].ToV2()
|
||||
}
|
||||
v2.SetNewAttributes(attrs)
|
||||
v2.SetReplaceAttributes(p.ReplaceAttributes)
|
||||
|
||||
v2.SetPatch(p.PayloadPatch.ToV2())
|
||||
|
||||
return v2
|
||||
}
|
||||
|
||||
func (p *Patch) FromV2(patch *v2object.PatchRequestBody) {
|
||||
if patch == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if addr := patch.GetAddress(); addr != nil {
|
||||
_ = p.Address.ReadFromV2(*addr)
|
||||
}
|
||||
|
||||
newAttrs := patch.GetNewAttributes()
|
||||
p.NewAttributes = make([]Attribute, len(newAttrs))
|
||||
for i := range newAttrs {
|
||||
p.NewAttributes[i] = *NewAttributeFromV2(&newAttrs[i])
|
||||
}
|
||||
|
||||
p.ReplaceAttributes = patch.GetReplaceAttributes()
|
||||
|
||||
if v2patch := patch.GetPatch(); v2patch != nil {
|
||||
p.PayloadPatch = new(PayloadPatch)
|
||||
p.PayloadPatch.FromV2(v2patch)
|
||||
}
|
||||
}
|
||||
|
||||
// Patch is a patch that's applied for an object's payload.
|
||||
type PayloadPatch struct {
|
||||
// Range of the patch application.
|
||||
Range *Range
|
||||
|
||||
// Chunk is the payload that replaces (or is appended to) the original object payload.
|
||||
Chunk []byte
|
||||
}
|
||||
|
||||
func (p *PayloadPatch) ToV2() *v2object.PatchRequestBodyPatch {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
v2 := new(v2object.PatchRequestBodyPatch)
|
||||
|
||||
v2.Chunk = p.Chunk
|
||||
v2.Range = p.Range.ToV2()
|
||||
|
||||
return v2
|
||||
}
|
||||
|
||||
func (p *PayloadPatch) FromV2(patch *v2object.PatchRequestBodyPatch) {
|
||||
if patch == nil {
|
||||
return
|
||||
}
|
||||
|
||||
p.Chunk = patch.Chunk
|
||||
if patch.Range != nil {
|
||||
p.Range = NewRangeFromV2(patch.Range)
|
||||
}
|
||||
}
|
161
object/patch_test.go
Normal file
161
object/patch_test.go
Normal file
|
@ -0,0 +1,161 @@
|
|||
package object
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
v2object "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
||||
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPatch(t *testing.T) {
|
||||
t.Run("to v2", func(t *testing.T) {
|
||||
t.Run("only attributes", func(t *testing.T) {
|
||||
var p Patch
|
||||
|
||||
var attr1, attr2 Attribute
|
||||
attr1.SetKey("key1")
|
||||
attr1.SetValue("val1")
|
||||
attr2.SetKey("key2")
|
||||
attr2.SetValue("val2")
|
||||
|
||||
p.Address = oidtest.Address()
|
||||
p.NewAttributes = []Attribute{attr1, attr2}
|
||||
p.ReplaceAttributes = true
|
||||
|
||||
v2patch := p.ToV2()
|
||||
|
||||
addr := new(oid.Address)
|
||||
require.NotNil(t, v2patch.GetAddress())
|
||||
addr.ReadFromV2(*v2patch.GetAddress())
|
||||
require.True(t, p.Address.Equals(*addr))
|
||||
|
||||
require.Equal(t, p.ReplaceAttributes, v2patch.GetReplaceAttributes())
|
||||
|
||||
require.Nil(t, v2patch.GetPatch())
|
||||
|
||||
require.Len(t, v2patch.GetNewAttributes(), 2)
|
||||
require.Equal(t, attr1.Key(), v2patch.GetNewAttributes()[0].GetKey())
|
||||
require.Equal(t, attr2.Key(), v2patch.GetNewAttributes()[1].GetKey())
|
||||
require.Equal(t, attr1.Value(), v2patch.GetNewAttributes()[0].GetValue())
|
||||
require.Equal(t, attr2.Value(), v2patch.GetNewAttributes()[1].GetValue())
|
||||
})
|
||||
|
||||
t.Run("with payload patch", func(t *testing.T) {
|
||||
var p Patch
|
||||
|
||||
var attr1, attr2 Attribute
|
||||
attr1.SetKey("key1")
|
||||
attr1.SetValue("val1")
|
||||
attr1.SetKey("key2")
|
||||
attr1.SetValue("val2")
|
||||
|
||||
p.Address = oidtest.Address()
|
||||
p.NewAttributes = []Attribute{attr1, attr2}
|
||||
p.ReplaceAttributes = true
|
||||
|
||||
rng := &Range{}
|
||||
rng.SetOffset(100)
|
||||
rng.SetLength(10)
|
||||
p.PayloadPatch = &PayloadPatch{
|
||||
Range: rng,
|
||||
Chunk: []byte("payload_patch_chunk"),
|
||||
}
|
||||
|
||||
v2patch := p.ToV2()
|
||||
|
||||
addr := new(oid.Address)
|
||||
require.NotNil(t, v2patch.GetAddress())
|
||||
addr.ReadFromV2(*v2patch.GetAddress())
|
||||
require.True(t, p.Address.Equals(*addr))
|
||||
|
||||
require.Equal(t, p.ReplaceAttributes, v2patch.GetReplaceAttributes())
|
||||
|
||||
require.Len(t, v2patch.GetNewAttributes(), 2)
|
||||
require.Equal(t, attr1.Key(), v2patch.GetNewAttributes()[0].GetKey())
|
||||
require.Equal(t, attr2.Key(), v2patch.GetNewAttributes()[1].GetKey())
|
||||
require.Equal(t, attr1.Value(), v2patch.GetNewAttributes()[0].GetValue())
|
||||
require.Equal(t, attr2.Value(), v2patch.GetNewAttributes()[1].GetValue())
|
||||
|
||||
require.NotNil(t, v2patch.GetPatch())
|
||||
require.NotNil(t, v2patch.GetPatch().Range)
|
||||
require.Equal(t, uint64(100), v2patch.GetPatch().Range.GetOffset())
|
||||
require.Equal(t, uint64(10), v2patch.GetPatch().Range.GetLength())
|
||||
require.Equal(t, []byte("payload_patch_chunk"), v2patch.GetPatch().Chunk)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
t.Run("from v2", func(t *testing.T) {
|
||||
t.Run("only attributes", func(t *testing.T) {
|
||||
v2patch := new(v2object.PatchRequestBody)
|
||||
|
||||
address := oidtest.Address()
|
||||
v2addr := new(refs.Address)
|
||||
address.WriteToV2(v2addr)
|
||||
v2patch.SetAddress(v2addr)
|
||||
|
||||
var attr1, attr2 Attribute
|
||||
attr1.SetKey("key1")
|
||||
attr1.SetValue("val1")
|
||||
attr2.SetKey("key2")
|
||||
attr2.SetValue("val2")
|
||||
|
||||
v2patch.SetNewAttributes([]v2object.Attribute{
|
||||
*attr1.ToV2(), *attr2.ToV2(),
|
||||
})
|
||||
v2patch.SetReplaceAttributes(true)
|
||||
|
||||
var p Patch
|
||||
p.FromV2(v2patch)
|
||||
|
||||
require.Equal(t, address, p.Address)
|
||||
require.Equal(t, []Attribute{attr1, attr2}, p.NewAttributes)
|
||||
require.Equal(t, true, p.ReplaceAttributes)
|
||||
require.Nil(t, p.PayloadPatch)
|
||||
})
|
||||
|
||||
t.Run("with payload patch", func(t *testing.T) {
|
||||
v2patchReqBody := new(v2object.PatchRequestBody)
|
||||
|
||||
address := oidtest.Address()
|
||||
v2addr := new(refs.Address)
|
||||
address.WriteToV2(v2addr)
|
||||
v2patchReqBody.SetAddress(v2addr)
|
||||
|
||||
var attr1, attr2 Attribute
|
||||
attr1.SetKey("key1")
|
||||
attr1.SetValue("val1")
|
||||
attr2.SetKey("key2")
|
||||
attr2.SetValue("val2")
|
||||
|
||||
v2patchReqBody.SetNewAttributes([]v2object.Attribute{
|
||||
*attr1.ToV2(), *attr2.ToV2(),
|
||||
})
|
||||
v2patchReqBody.SetReplaceAttributes(true)
|
||||
|
||||
v2Rng := &v2object.Range{}
|
||||
v2Rng.SetOffset(13)
|
||||
v2Rng.SetLength(10)
|
||||
v2Patch := &v2object.PatchRequestBodyPatch{
|
||||
Range: v2Rng,
|
||||
Chunk: []byte("payload_patch_chunk"),
|
||||
}
|
||||
v2patchReqBody.SetPatch(v2Patch)
|
||||
|
||||
var p Patch
|
||||
p.FromV2(v2patchReqBody)
|
||||
|
||||
require.Equal(t, address, p.Address)
|
||||
require.Equal(t, []Attribute{attr1, attr2}, p.NewAttributes)
|
||||
require.Equal(t, true, p.ReplaceAttributes)
|
||||
require.NotNil(t, p.PayloadPatch)
|
||||
require.NotNil(t, p.PayloadPatch.Range)
|
||||
require.Equal(t, uint64(13), p.PayloadPatch.Range.GetOffset())
|
||||
require.Equal(t, uint64(10), p.PayloadPatch.Range.GetLength())
|
||||
require.Equal(t, []byte("payload_patch_chunk"), p.PayloadPatch.Chunk)
|
||||
})
|
||||
})
|
||||
}
|
244
object/patcher/patcher.go
Normal file
244
object/patcher/patcher.go
Normal file
|
@ -0,0 +1,244 @@
|
|||
package patcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/transformer"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrOffsetExceedsSize = errors.New("patch offset exceeds object size")
|
||||
ErrInvalidPatchOffsetOrder = errors.New("invalid patch offset order")
|
||||
ErrPayloadPatchIsNil = errors.New("nil payload patch")
|
||||
ErrAttrPatchAlreadyApplied = errors.New("attribute patch already applied")
|
||||
)
|
||||
|
||||
// PatchRes is the result of patch application.
|
||||
type PatchRes struct {
|
||||
AccessIdentifiers *transformer.AccessIdentifiers
|
||||
}
|
||||
|
||||
// PatchApplier is the interface that provides method to apply header and payload patches.
|
||||
type PatchApplier interface {
|
||||
// ApplyAttributesPatch applies the patch only for the object's attributes.
|
||||
// This method must be ALWAYS invoked first as attribute patch must income ONLY with the first patch
|
||||
// request message.
|
||||
//
|
||||
// ApplyAttributesPatch can't be invoked few times, otherwise it returns `ErrAttrPatchAlreadyApplied` error.
|
||||
//
|
||||
// ApplyAttributesPatch is idempotent for the original header if it's invoked with empty `newAttrs` and
|
||||
// `replaceAttrs = false`.
|
||||
ApplyAttributesPatch(ctx context.Context, newAttrs []objectSDK.Attribute, replaceAttrs bool) error
|
||||
|
||||
// ApplyPayloadPatch applies the patch for the object's payload.
|
||||
//
|
||||
// ApplyPayloadPatch returns `ErrPayloadPatchIsNil` error if patch is nil.
|
||||
ApplyPayloadPatch(ctx context.Context, payloadPatch *objectSDK.PayloadPatch) error
|
||||
|
||||
// Close closes PatchApplier when the patch stream is over.
|
||||
Close(context.Context) (PatchRes, error)
|
||||
}
|
||||
|
||||
// RangeProvider is the interface that provides a method to get original object payload
|
||||
// by a given range.
|
||||
type RangeProvider interface {
|
||||
// ReadRange reads an original object payload by the given range.
|
||||
// The method returns io.Reader over the data range only. This means if the data is read out,
|
||||
// then ReadRange has to be invoked to provide reader over the next range.
|
||||
ReadRange(ctx context.Context, rng *objectSDK.Range) io.Reader
|
||||
}
|
||||
|
||||
type patcher struct {
|
||||
rangeProvider RangeProvider
|
||||
|
||||
objectWriter transformer.ChunkedObjectWriter
|
||||
|
||||
currOffset uint64
|
||||
|
||||
originalPayloadSize uint64
|
||||
|
||||
hdr *objectSDK.Object
|
||||
|
||||
attrPatchAlreadyApplied bool
|
||||
|
||||
readerBuffSize int
|
||||
}
|
||||
|
||||
const (
|
||||
DefaultReaderBufferSize = 64 * 1024
|
||||
)
|
||||
|
||||
// Params is parameters to initialize patcher.
|
||||
type Params struct {
|
||||
// Original object header.
|
||||
Header *objectSDK.Object
|
||||
|
||||
// Range provider.
|
||||
RangeProvider RangeProvider
|
||||
|
||||
// ObjectWriter is the writer that writes the patched object.
|
||||
ObjectWriter transformer.ChunkedObjectWriter
|
||||
|
||||
// The size of the buffer used by the original payload range reader.
|
||||
// If it's set to <=0, then `DefaultReaderBufferSize` is used.
|
||||
ReaderBufferSize int
|
||||
}
|
||||
|
||||
func New(prm Params) PatchApplier {
|
||||
readerBufferSize := prm.ReaderBufferSize
|
||||
if readerBufferSize <= 0 {
|
||||
readerBufferSize = DefaultReaderBufferSize
|
||||
}
|
||||
|
||||
return &patcher{
|
||||
rangeProvider: prm.RangeProvider,
|
||||
|
||||
objectWriter: prm.ObjectWriter,
|
||||
|
||||
hdr: prm.Header,
|
||||
|
||||
originalPayloadSize: prm.Header.PayloadSize(),
|
||||
|
||||
readerBuffSize: readerBufferSize,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *patcher) ApplyAttributesPatch(ctx context.Context, newAttrs []objectSDK.Attribute, replaceAttrs bool) error {
|
||||
defer func() {
|
||||
p.attrPatchAlreadyApplied = true
|
||||
}()
|
||||
|
||||
if p.attrPatchAlreadyApplied {
|
||||
return ErrAttrPatchAlreadyApplied
|
||||
}
|
||||
|
||||
if replaceAttrs {
|
||||
p.hdr.SetAttributes(newAttrs...)
|
||||
} else if len(newAttrs) > 0 {
|
||||
mergedAttrs := mergeAttributes(newAttrs, p.hdr.Attributes())
|
||||
p.hdr.SetAttributes(mergedAttrs...)
|
||||
}
|
||||
|
||||
if err := p.objectWriter.WriteHeader(ctx, p.hdr); err != nil {
|
||||
return fmt.Errorf("writer header error: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *patcher) ApplyPayloadPatch(ctx context.Context, payloadPatch *objectSDK.PayloadPatch) error {
|
||||
if payloadPatch == nil {
|
||||
return ErrPayloadPatchIsNil
|
||||
}
|
||||
|
||||
if payloadPatch.Range.GetOffset() < p.currOffset {
|
||||
return fmt.Errorf("%w: current = %d, previous = %d", ErrInvalidPatchOffsetOrder, payloadPatch.Range.GetOffset(), p.currOffset)
|
||||
}
|
||||
|
||||
if payloadPatch.Range.GetOffset() > p.originalPayloadSize {
|
||||
return fmt.Errorf("%w: offset = %d, object size = %d", ErrOffsetExceedsSize, payloadPatch.Range.GetOffset(), p.originalPayloadSize)
|
||||
}
|
||||
|
||||
var err error
|
||||
if p.currOffset, err = p.applyPatch(ctx, payloadPatch, p.currOffset); err != nil {
|
||||
return fmt.Errorf("apply patch error: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *patcher) Close(ctx context.Context) (PatchRes, error) {
|
||||
rng := new(objectSDK.Range)
|
||||
rng.SetOffset(p.currOffset)
|
||||
rng.SetLength(p.originalPayloadSize - p.currOffset)
|
||||
|
||||
// copy remaining originial payload
|
||||
if err := p.copyRange(ctx, rng); err != nil {
|
||||
return PatchRes{}, fmt.Errorf("copy payload error: %w", err)
|
||||
}
|
||||
|
||||
aid, err := p.objectWriter.Close(ctx)
|
||||
if err != nil {
|
||||
return PatchRes{}, fmt.Errorf("close object writer error: %w", err)
|
||||
}
|
||||
|
||||
return PatchRes{
|
||||
AccessIdentifiers: aid,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *patcher) copyRange(ctx context.Context, rng *objectSDK.Range) error {
|
||||
rdr := p.rangeProvider.ReadRange(ctx, rng)
|
||||
for {
|
||||
buffOrigPayload := make([]byte, p.readerBuffSize)
|
||||
n, readErr := rdr.Read(buffOrigPayload)
|
||||
if readErr != nil {
|
||||
if readErr != io.EOF {
|
||||
return fmt.Errorf("read error: %w", readErr)
|
||||
}
|
||||
}
|
||||
_, wrErr := p.objectWriter.Write(ctx, buffOrigPayload[:n])
|
||||
if wrErr != nil {
|
||||
return fmt.Errorf("write error: %w", wrErr)
|
||||
}
|
||||
if readErr == io.EOF {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *patcher) applyPatch(ctx context.Context, payloadPatch *objectSDK.PayloadPatch, offset uint64) (newOffset uint64, err error) {
|
||||
// write the original payload chunk before the start of the patch
|
||||
if payloadPatch.Range.GetOffset() > offset {
|
||||
rng := new(objectSDK.Range)
|
||||
rng.SetOffset(offset)
|
||||
rng.SetLength(payloadPatch.Range.GetOffset() - offset)
|
||||
|
||||
if err = p.copyRange(ctx, rng); err != nil {
|
||||
err = fmt.Errorf("copy payload error: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
newOffset = payloadPatch.Range.GetOffset()
|
||||
}
|
||||
|
||||
// apply patch
|
||||
if _, err = p.objectWriter.Write(ctx, payloadPatch.Chunk); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if payloadPatch.Range.GetLength() > 0 {
|
||||
newOffset += payloadPatch.Range.GetLength()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func mergeAttributes(newAttrs, oldAttrs []objectSDK.Attribute) []objectSDK.Attribute {
|
||||
attrMap := make(map[string]string)
|
||||
|
||||
for _, attr := range newAttrs {
|
||||
attrMap[attr.Key()] = attr.Value()
|
||||
}
|
||||
|
||||
for _, oldAttr := range oldAttrs {
|
||||
newVal, ok := attrMap[oldAttr.Key()]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
oldAttr.SetValue(newVal)
|
||||
delete(attrMap, oldAttr.Key())
|
||||
}
|
||||
|
||||
for _, newAttr := range newAttrs {
|
||||
if _, ok := attrMap[newAttr.Key()]; ok {
|
||||
oldAttrs = append(oldAttrs, newAttr)
|
||||
}
|
||||
}
|
||||
|
||||
return oldAttrs
|
||||
}
|
564
object/patcher/patcher_test.go
Normal file
564
object/patcher/patcher_test.go
Normal file
|
@ -0,0 +1,564 @@
|
|||
package patcher
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
||||
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/transformer"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type mockPatchedObjectWriter struct {
|
||||
obj *objectSDK.Object
|
||||
}
|
||||
|
||||
func (m *mockPatchedObjectWriter) Write(_ context.Context, chunk []byte) (int, error) {
|
||||
res := append(m.obj.Payload(), chunk...)
|
||||
|
||||
m.obj.SetPayload(res)
|
||||
m.obj.SetPayloadSize(uint64(len(res)))
|
||||
|
||||
return len(chunk), nil
|
||||
}
|
||||
|
||||
func (m *mockPatchedObjectWriter) WriteHeader(_ context.Context, hdr *objectSDK.Object) error {
|
||||
m.obj.ToV2().SetHeader(hdr.ToV2().GetHeader())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockPatchedObjectWriter) Close(context.Context) (*transformer.AccessIdentifiers, error) {
|
||||
return &transformer.AccessIdentifiers{}, nil
|
||||
}
|
||||
|
||||
type mockRangeProvider struct {
|
||||
originalObjectPayload []byte
|
||||
}
|
||||
|
||||
var _ RangeProvider = (*mockRangeProvider)(nil)
|
||||
|
||||
func (m *mockRangeProvider) ReadRange(_ context.Context, rng *objectSDK.Range) io.Reader {
|
||||
offset := rng.GetOffset()
|
||||
length := rng.GetLength()
|
||||
|
||||
if length == 0 {
|
||||
return bytes.NewReader(m.originalObjectPayload[offset:])
|
||||
}
|
||||
return bytes.NewReader(m.originalObjectPayload[offset : offset+length])
|
||||
}
|
||||
|
||||
func newTestObject() (*objectSDK.Object, oid.Address) {
|
||||
obj := objectSDK.New()
|
||||
|
||||
addr := oidtest.Address()
|
||||
obj.SetContainerID(addr.Container())
|
||||
obj.SetID(addr.Object())
|
||||
|
||||
return obj, addr
|
||||
}
|
||||
|
||||
func rangeWithOffestWithLength(offset, length uint64) *objectSDK.Range {
|
||||
rng := new(objectSDK.Range)
|
||||
rng.SetOffset(offset)
|
||||
rng.SetLength(length)
|
||||
return rng
|
||||
}
|
||||
|
||||
func TestPatchRevert(t *testing.T) {
|
||||
obj, _ := newTestObject()
|
||||
|
||||
modifPatch := &objectSDK.Patch{
|
||||
PayloadPatch: &objectSDK.PayloadPatch{
|
||||
Range: rangeWithOffestWithLength(0, 0),
|
||||
|
||||
Chunk: []byte("inserted"),
|
||||
},
|
||||
}
|
||||
|
||||
originalObjectPayload := []byte("*******************")
|
||||
|
||||
obj.SetPayload(originalObjectPayload)
|
||||
obj.SetPayloadSize(uint64(len(originalObjectPayload)))
|
||||
|
||||
exp := []byte("inserted*******************")
|
||||
|
||||
rangeProvider := &mockRangeProvider{
|
||||
originalObjectPayload: originalObjectPayload,
|
||||
}
|
||||
|
||||
patchedObj, _ := newTestObject()
|
||||
|
||||
wr := &mockPatchedObjectWriter{
|
||||
obj: patchedObj,
|
||||
}
|
||||
|
||||
prm := Params{
|
||||
Header: obj.CutPayload(),
|
||||
|
||||
RangeProvider: rangeProvider,
|
||||
|
||||
ObjectWriter: wr,
|
||||
}
|
||||
|
||||
patcher := New(prm)
|
||||
|
||||
err := patcher.ApplyAttributesPatch(context.Background(), modifPatch.NewAttributes, modifPatch.ReplaceAttributes)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = patcher.ApplyPayloadPatch(context.Background(), modifPatch.PayloadPatch)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = patcher.Close(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, exp, patchedObj.Payload())
|
||||
|
||||
revertPatch := &objectSDK.Patch{
|
||||
PayloadPatch: &objectSDK.PayloadPatch{
|
||||
Range: rangeWithOffestWithLength(0, uint64(len("inserted"))),
|
||||
|
||||
Chunk: []byte{},
|
||||
},
|
||||
}
|
||||
|
||||
rangeProvider = &mockRangeProvider{
|
||||
originalObjectPayload: exp,
|
||||
}
|
||||
|
||||
patchedPatchedObj, _ := newTestObject()
|
||||
|
||||
wr = &mockPatchedObjectWriter{
|
||||
obj: patchedPatchedObj,
|
||||
}
|
||||
|
||||
prm = Params{
|
||||
Header: patchedObj.CutPayload(),
|
||||
|
||||
RangeProvider: rangeProvider,
|
||||
|
||||
ObjectWriter: wr,
|
||||
}
|
||||
|
||||
patcher = New(prm)
|
||||
|
||||
err = patcher.ApplyAttributesPatch(context.Background(), revertPatch.NewAttributes, revertPatch.ReplaceAttributes)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = patcher.ApplyPayloadPatch(context.Background(), revertPatch.PayloadPatch)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = patcher.Close(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, originalObjectPayload, patchedPatchedObj.Payload())
|
||||
}
|
||||
|
||||
func TestPatchRepeatAttributePatch(t *testing.T) {
|
||||
obj, _ := newTestObject()
|
||||
|
||||
modifPatch := &objectSDK.Patch{}
|
||||
|
||||
originalObjectPayload := []byte("*******************")
|
||||
|
||||
obj.SetPayload(originalObjectPayload)
|
||||
obj.SetPayloadSize(uint64(len(originalObjectPayload)))
|
||||
|
||||
rangeProvider := &mockRangeProvider{
|
||||
originalObjectPayload: originalObjectPayload,
|
||||
}
|
||||
|
||||
patchedObj, _ := newTestObject()
|
||||
|
||||
wr := &mockPatchedObjectWriter{
|
||||
obj: patchedObj,
|
||||
}
|
||||
|
||||
prm := Params{
|
||||
Header: obj.CutPayload(),
|
||||
|
||||
RangeProvider: rangeProvider,
|
||||
|
||||
ObjectWriter: wr,
|
||||
}
|
||||
|
||||
patcher := New(prm)
|
||||
|
||||
err := patcher.ApplyAttributesPatch(context.Background(), modifPatch.NewAttributes, modifPatch.ReplaceAttributes)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = patcher.ApplyAttributesPatch(context.Background(), modifPatch.NewAttributes, modifPatch.ReplaceAttributes)
|
||||
require.ErrorIs(t, err, ErrAttrPatchAlreadyApplied)
|
||||
}
|
||||
|
||||
func TestPatchEmptyPayloadPatch(t *testing.T) {
|
||||
obj, _ := newTestObject()
|
||||
|
||||
modifPatch := &objectSDK.Patch{}
|
||||
|
||||
originalObjectPayload := []byte("*******************")
|
||||
|
||||
obj.SetPayload(originalObjectPayload)
|
||||
obj.SetPayloadSize(uint64(len(originalObjectPayload)))
|
||||
|
||||
rangeProvider := &mockRangeProvider{
|
||||
originalObjectPayload: originalObjectPayload,
|
||||
}
|
||||
|
||||
patchedObj, _ := newTestObject()
|
||||
|
||||
wr := &mockPatchedObjectWriter{
|
||||
obj: patchedObj,
|
||||
}
|
||||
|
||||
prm := Params{
|
||||
Header: obj.CutPayload(),
|
||||
|
||||
RangeProvider: rangeProvider,
|
||||
|
||||
ObjectWriter: wr,
|
||||
}
|
||||
|
||||
patcher := New(prm)
|
||||
|
||||
err := patcher.ApplyAttributesPatch(context.Background(), modifPatch.NewAttributes, modifPatch.ReplaceAttributes)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = patcher.ApplyPayloadPatch(context.Background(), nil)
|
||||
require.ErrorIs(t, err, ErrPayloadPatchIsNil)
|
||||
}
|
||||
|
||||
func newTestAttribute(key, val string) objectSDK.Attribute {
|
||||
var attr objectSDK.Attribute
|
||||
attr.SetKey(key)
|
||||
attr.SetValue(val)
|
||||
return attr
|
||||
}
|
||||
|
||||
func TestPatch(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
patches []objectSDK.Patch
|
||||
originalObjectPayload []byte
|
||||
patched []byte
|
||||
expectedPayloadPatchErr error
|
||||
}{
|
||||
{
|
||||
name: "invalid offset",
|
||||
patches: []objectSDK.Patch{
|
||||
{
|
||||
PayloadPatch: &objectSDK.PayloadPatch{
|
||||
Range: rangeWithOffestWithLength(100, 0),
|
||||
Chunk: []byte(""),
|
||||
},
|
||||
},
|
||||
},
|
||||
originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"),
|
||||
expectedPayloadPatchErr: ErrOffsetExceedsSize,
|
||||
},
|
||||
{
|
||||
name: "invalid following patch offset",
|
||||
patches: []objectSDK.Patch{
|
||||
{
|
||||
PayloadPatch: &objectSDK.PayloadPatch{
|
||||
Range: rangeWithOffestWithLength(10, 0),
|
||||
Chunk: []byte(""),
|
||||
},
|
||||
},
|
||||
{
|
||||
PayloadPatch: &objectSDK.PayloadPatch{
|
||||
Range: rangeWithOffestWithLength(7, 0),
|
||||
Chunk: []byte(""),
|
||||
},
|
||||
},
|
||||
},
|
||||
originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"),
|
||||
expectedPayloadPatchErr: ErrInvalidPatchOffsetOrder,
|
||||
},
|
||||
{
|
||||
name: "only header patch",
|
||||
patches: []objectSDK.Patch{
|
||||
{
|
||||
NewAttributes: []objectSDK.Attribute{
|
||||
newTestAttribute("key1", "val2"),
|
||||
newTestAttribute("key2", "val2"),
|
||||
},
|
||||
},
|
||||
},
|
||||
originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"),
|
||||
patched: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"),
|
||||
},
|
||||
{
|
||||
name: "header and payload",
|
||||
patches: []objectSDK.Patch{
|
||||
{
|
||||
NewAttributes: []objectSDK.Attribute{
|
||||
newTestAttribute("key1", "val2"),
|
||||
newTestAttribute("key2", "val2"),
|
||||
},
|
||||
PayloadPatch: &objectSDK.PayloadPatch{
|
||||
Range: rangeWithOffestWithLength(0, 0),
|
||||
Chunk: []byte("inserted at the beginning"),
|
||||
},
|
||||
},
|
||||
},
|
||||
originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"),
|
||||
patched: []byte("inserted at the beginning0123456789qwertyuiopasdfghjklzxcvbnm"),
|
||||
},
|
||||
{
|
||||
name: "header only then payload",
|
||||
patches: []objectSDK.Patch{
|
||||
{
|
||||
NewAttributes: []objectSDK.Attribute{
|
||||
newTestAttribute("key1", "val2"),
|
||||
newTestAttribute("key2", "val2"),
|
||||
},
|
||||
},
|
||||
{
|
||||
PayloadPatch: &objectSDK.PayloadPatch{
|
||||
Range: rangeWithOffestWithLength(0, 0),
|
||||
Chunk: []byte("inserted at the beginning"),
|
||||
},
|
||||
},
|
||||
},
|
||||
originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"),
|
||||
patched: []byte("inserted at the beginning0123456789qwertyuiopasdfghjklzxcvbnm"),
|
||||
expectedPayloadPatchErr: ErrPayloadPatchIsNil,
|
||||
},
|
||||
{
|
||||
name: "no effect",
|
||||
patches: []objectSDK.Patch{
|
||||
{
|
||||
PayloadPatch: &objectSDK.PayloadPatch{
|
||||
Range: rangeWithOffestWithLength(0, 0),
|
||||
Chunk: []byte(""),
|
||||
},
|
||||
},
|
||||
{
|
||||
PayloadPatch: &objectSDK.PayloadPatch{
|
||||
Range: rangeWithOffestWithLength(12, 0),
|
||||
Chunk: []byte(""),
|
||||
},
|
||||
},
|
||||
{
|
||||
PayloadPatch: &objectSDK.PayloadPatch{
|
||||
Range: rangeWithOffestWithLength(20, 0),
|
||||
Chunk: []byte(""),
|
||||
},
|
||||
},
|
||||
},
|
||||
originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"),
|
||||
patched: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"),
|
||||
},
|
||||
{
|
||||
name: "insert prefix",
|
||||
patches: []objectSDK.Patch{
|
||||
{
|
||||
PayloadPatch: &objectSDK.PayloadPatch{
|
||||
Range: rangeWithOffestWithLength(0, 0),
|
||||
Chunk: []byte("inserted at the beginning"),
|
||||
},
|
||||
},
|
||||
},
|
||||
originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"),
|
||||
patched: []byte("inserted at the beginning0123456789qwertyuiopasdfghjklzxcvbnm"),
|
||||
},
|
||||
{
|
||||
name: "insert in the middle",
|
||||
patches: []objectSDK.Patch{
|
||||
{
|
||||
PayloadPatch: &objectSDK.PayloadPatch{
|
||||
Range: rangeWithOffestWithLength(5, 0),
|
||||
Chunk: []byte("inserted somewhere in the middle"),
|
||||
},
|
||||
},
|
||||
},
|
||||
originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"),
|
||||
patched: []byte("01234inserted somewhere in the middle56789qwertyuiopasdfghjklzxcvbnm"),
|
||||
},
|
||||
{
|
||||
name: "insert at the end",
|
||||
patches: []objectSDK.Patch{
|
||||
{
|
||||
PayloadPatch: &objectSDK.PayloadPatch{
|
||||
Range: rangeWithOffestWithLength(36, 0),
|
||||
Chunk: []byte("inserted somewhere at the end"),
|
||||
},
|
||||
},
|
||||
},
|
||||
originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"),
|
||||
patched: []byte("0123456789qwertyuiopasdfghjklzxcvbnminserted somewhere at the end"),
|
||||
},
|
||||
{
|
||||
name: "replace by range",
|
||||
patches: []objectSDK.Patch{
|
||||
{
|
||||
PayloadPatch: &objectSDK.PayloadPatch{
|
||||
Range: rangeWithOffestWithLength(0, 12),
|
||||
Chunk: []byte("just replace"),
|
||||
},
|
||||
},
|
||||
},
|
||||
originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"),
|
||||
patched: []byte("just replaceertyuiopasdfghjklzxcvbnm"),
|
||||
},
|
||||
{
|
||||
name: "replace and insert some bytes",
|
||||
patches: []objectSDK.Patch{
|
||||
{
|
||||
PayloadPatch: &objectSDK.PayloadPatch{
|
||||
Range: rangeWithOffestWithLength(0, 11),
|
||||
Chunk: []byte("replace and append in the middle"),
|
||||
},
|
||||
},
|
||||
},
|
||||
originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"),
|
||||
patched: []byte("replace and append in the middlewertyuiopasdfghjklzxcvbnm"),
|
||||
},
|
||||
{
|
||||
name: "replace and insert some bytes in the middle",
|
||||
patches: []objectSDK.Patch{
|
||||
{
|
||||
PayloadPatch: &objectSDK.PayloadPatch{
|
||||
Range: rangeWithOffestWithLength(5, 3),
|
||||
Chunk: []byte("@@@@@"),
|
||||
},
|
||||
},
|
||||
},
|
||||
originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"),
|
||||
patched: []byte("01234@@@@@89qwertyuiopasdfghjklzxcvbnm"),
|
||||
},
|
||||
{
|
||||
name: "a few patches: prefix, suffix",
|
||||
patches: []objectSDK.Patch{
|
||||
{
|
||||
PayloadPatch: &objectSDK.PayloadPatch{
|
||||
Range: rangeWithOffestWithLength(0, 0),
|
||||
Chunk: []byte("this_will_be_prefix"),
|
||||
},
|
||||
},
|
||||
{
|
||||
PayloadPatch: &objectSDK.PayloadPatch{
|
||||
Range: rangeWithOffestWithLength(36, 0),
|
||||
Chunk: []byte("this_will_be_suffix"),
|
||||
},
|
||||
},
|
||||
},
|
||||
originalObjectPayload: []byte("0123456789qwertyuiopasdfghjklzxcvbnm"),
|
||||
patched: []byte("this_will_be_prefix0123456789qwertyuiopasdfghjklzxcvbnmthis_will_be_suffix"),
|
||||
},
|
||||
{
|
||||
name: "a few patches: replace and insert some bytes",
|
||||
patches: []objectSDK.Patch{
|
||||
{
|
||||
PayloadPatch: &objectSDK.PayloadPatch{
|
||||
Range: rangeWithOffestWithLength(10, 3),
|
||||
Chunk: []byte("aaaaa"),
|
||||
},
|
||||
},
|
||||
{
|
||||
PayloadPatch: &objectSDK.PayloadPatch{
|
||||
Range: rangeWithOffestWithLength(16, 0),
|
||||
Chunk: []byte("bbbbb"),
|
||||
},
|
||||
},
|
||||
},
|
||||
originalObjectPayload: []byte("0123456789ABCDEF"),
|
||||
patched: []byte("0123456789aaaaaDEFbbbbb"),
|
||||
},
|
||||
{
|
||||
name: "a few patches: various modifiactions",
|
||||
patches: []objectSDK.Patch{
|
||||
{
|
||||
PayloadPatch: &objectSDK.PayloadPatch{
|
||||
Range: rangeWithOffestWithLength(4, 8),
|
||||
Chunk: []byte("earliest"),
|
||||
},
|
||||
},
|
||||
{
|
||||
PayloadPatch: &objectSDK.PayloadPatch{
|
||||
Range: rangeWithOffestWithLength(13, 0),
|
||||
Chunk: []byte("known "),
|
||||
},
|
||||
},
|
||||
{
|
||||
PayloadPatch: &objectSDK.PayloadPatch{
|
||||
Range: rangeWithOffestWithLength(35, 8),
|
||||
Chunk: []byte("a small town"),
|
||||
},
|
||||
},
|
||||
{
|
||||
PayloadPatch: &objectSDK.PayloadPatch{
|
||||
Range: rangeWithOffestWithLength(62, 6),
|
||||
Chunk: []byte("tablet"),
|
||||
},
|
||||
},
|
||||
{
|
||||
PayloadPatch: &objectSDK.PayloadPatch{
|
||||
Range: rangeWithOffestWithLength(87, 0),
|
||||
Chunk: []byte("Shar-Kali-Sharri"),
|
||||
},
|
||||
},
|
||||
},
|
||||
originalObjectPayload: []byte("The ******** mention of Babylon as [insert] appears on a clay ****** from the reign of "),
|
||||
patched: []byte("The earliest known mention of Babylon as a small town appears on a clay tablet from the reign of Shar-Kali-Sharri"),
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
rangeProvider := &mockRangeProvider{
|
||||
originalObjectPayload: test.originalObjectPayload,
|
||||
}
|
||||
|
||||
originalObject, _ := newTestObject()
|
||||
originalObject.SetPayload(test.originalObjectPayload)
|
||||
originalObject.SetPayloadSize(uint64(len(test.originalObjectPayload)))
|
||||
|
||||
patchedObject, _ := newTestObject()
|
||||
|
||||
wr := &mockPatchedObjectWriter{
|
||||
obj: patchedObject,
|
||||
}
|
||||
|
||||
prm := Params{
|
||||
Header: originalObject.CutPayload(),
|
||||
|
||||
RangeProvider: rangeProvider,
|
||||
|
||||
ObjectWriter: wr,
|
||||
}
|
||||
|
||||
patcher := New(prm)
|
||||
|
||||
for i, patch := range test.patches {
|
||||
if i == 0 {
|
||||
_ = patcher.ApplyAttributesPatch(context.Background(), patch.NewAttributes, patch.ReplaceAttributes)
|
||||
}
|
||||
|
||||
if patch.PayloadPatch == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
err := patcher.ApplyPayloadPatch(context.Background(), patch.PayloadPatch)
|
||||
if err != nil {
|
||||
if test.expectedPayloadPatchErr != nil {
|
||||
require.ErrorIs(t, err, test.expectedPayloadPatchErr)
|
||||
return
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_, err := patcher.Close(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.patched, patchedObject.Payload())
|
||||
|
||||
newAttrs := append([]objectSDK.Attribute{}, test.patches[0].NewAttributes...)
|
||||
require.Equal(t, newAttrs, patchedObject.Attributes())
|
||||
})
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue