diff --git a/go.mod b/go.mod index a481fb2..faa6b6b 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index a23b405..b84f659 100644 Binary files a/go.sum and b/go.sum differ diff --git a/object/patch.go b/object/patch.go new file mode 100644 index 0000000..1cc30fc --- /dev/null +++ b/object/patch.go @@ -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) + } +} diff --git a/object/patch_test.go b/object/patch_test.go new file mode 100644 index 0000000..f66fd29 --- /dev/null +++ b/object/patch_test.go @@ -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") + attr2.SetKey("key2") + attr2.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) + }) + }) +}