forked from TrueCloudLab/frostfs-sdk-go
[#170] storagegroup: Refactor and document package functionality
Signed-off-by: Pavel Karpy <carpawell@nspcc.ru>
This commit is contained in:
parent
1186f2f703
commit
9b63c07c59
5 changed files with 244 additions and 55 deletions
36
storagegroup/doc.go
Normal file
36
storagegroup/doc.go
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
Package storagegroup provides features to work with information that is
|
||||||
|
used for proof of storage in NeoFS system.
|
||||||
|
|
||||||
|
StorageGroup type groups verification values for Data Audit sessions:
|
||||||
|
// receive sg info
|
||||||
|
|
||||||
|
sg.ExpirationEpoch() // expiration of the storage group
|
||||||
|
sg.Members() // objects in the group
|
||||||
|
sg.ValidationDataHash() // hash for objects validation
|
||||||
|
sg.ValidationDataSize() // total objects' payload size
|
||||||
|
|
||||||
|
Instances can be also used to process NeoFS API V2 protocol messages
|
||||||
|
(see neo.fs.v2.storagegroup package in https://github.com/nspcc-dev/neofs-api).
|
||||||
|
|
||||||
|
On client side:
|
||||||
|
import "github.com/nspcc-dev/neofs-api-go/v2/storagegroup"
|
||||||
|
|
||||||
|
var msg storagegroup.StorageGroup
|
||||||
|
sg.WriteToV2(&msg)
|
||||||
|
|
||||||
|
// send msg
|
||||||
|
|
||||||
|
On server side:
|
||||||
|
// recv msg
|
||||||
|
|
||||||
|
var sg StorageGroupDecimal
|
||||||
|
sg.ReadFromV2(msg)
|
||||||
|
|
||||||
|
// process sg
|
||||||
|
|
||||||
|
Using package types in an application is recommended to potentially work with
|
||||||
|
different protocol versions with which these types are compatible.
|
||||||
|
|
||||||
|
*/
|
||||||
|
package storagegroup
|
|
@ -7,35 +7,47 @@ import (
|
||||||
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
|
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
// StorageGroup represents v2-compatible storage group.
|
// StorageGroup represents storage group of the NeoFS objects.
|
||||||
|
//
|
||||||
|
// StorageGroup is mutually compatible with github.com/nspcc-dev/neofs-api-go/v2/storagegroup.StorageGroup
|
||||||
|
// message. See ReadFromMessageV2 / WriteToMessageV2 methods.
|
||||||
|
//
|
||||||
|
// Instances can be created using built-in var declaration.
|
||||||
|
//
|
||||||
|
// Note that direct typecast is not safe and may result in loss of compatibility:
|
||||||
|
// _ = StorageGroup(storagegroup.StorageGroup) // not recommended
|
||||||
type StorageGroup storagegroup.StorageGroup
|
type StorageGroup storagegroup.StorageGroup
|
||||||
|
|
||||||
// NewFromV2 wraps v2 StorageGroup message to StorageGroup.
|
// ReadFromV2 reads StorageGroup from the storagegroup.StorageGroup message.
|
||||||
//
|
//
|
||||||
// Nil storagegroup.StorageGroup converts to nil.
|
// See also WriteToV2.
|
||||||
func NewFromV2(aV2 *storagegroup.StorageGroup) *StorageGroup {
|
func (sg *StorageGroup) ReadFromV2(m storagegroup.StorageGroup) {
|
||||||
return (*StorageGroup)(aV2)
|
*sg = StorageGroup(m)
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates and initializes blank StorageGroup.
|
// WriteToV2 writes StorageGroup to the storagegroup.StorageGroup message.
|
||||||
|
// The message must not be nil.
|
||||||
//
|
//
|
||||||
// Defaults:
|
// See also ReadFromV2.
|
||||||
// - size: 0;
|
func (sg StorageGroup) WriteToV2(m *storagegroup.StorageGroup) {
|
||||||
// - exp: 0;
|
*m = (storagegroup.StorageGroup)(sg)
|
||||||
// - members: nil;
|
|
||||||
// - hash: nil.
|
|
||||||
func New() *StorageGroup {
|
|
||||||
return NewFromV2(new(storagegroup.StorageGroup))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidationDataSize returns total size of the payloads
|
// ValidationDataSize returns total size of the payloads
|
||||||
// of objects in the storage group.
|
// of objects in the storage group.
|
||||||
func (sg *StorageGroup) ValidationDataSize() uint64 {
|
//
|
||||||
return (*storagegroup.StorageGroup)(sg).GetValidationDataSize()
|
// Zero StorageGroup has 0 data size.
|
||||||
|
//
|
||||||
|
// See also SetValidationDataSize.
|
||||||
|
func (sg StorageGroup) ValidationDataSize() uint64 {
|
||||||
|
v2 := (storagegroup.StorageGroup)(sg)
|
||||||
|
return v2.GetValidationDataSize()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetValidationDataSize sets total size of the payloads
|
// SetValidationDataSize sets total size of the payloads
|
||||||
// of objects in the storage group.
|
// of objects in the storage group.
|
||||||
|
//
|
||||||
|
// See also ValidationDataSize.
|
||||||
func (sg *StorageGroup) SetValidationDataSize(epoch uint64) {
|
func (sg *StorageGroup) SetValidationDataSize(epoch uint64) {
|
||||||
(*storagegroup.StorageGroup)(sg).SetValidationDataSize(epoch)
|
(*storagegroup.StorageGroup)(sg).SetValidationDataSize(epoch)
|
||||||
}
|
}
|
||||||
|
@ -71,20 +83,32 @@ func (sg *StorageGroup) SetValidationDataHash(hash checksum.Checksum) {
|
||||||
|
|
||||||
// ExpirationEpoch returns last NeoFS epoch number
|
// ExpirationEpoch returns last NeoFS epoch number
|
||||||
// of the storage group lifetime.
|
// of the storage group lifetime.
|
||||||
func (sg *StorageGroup) ExpirationEpoch() uint64 {
|
//
|
||||||
return (*storagegroup.StorageGroup)(sg).GetExpirationEpoch()
|
// Zero StorageGroup has 0 expiration epoch.
|
||||||
|
//
|
||||||
|
// See also SetExpirationEpoch.
|
||||||
|
func (sg StorageGroup) ExpirationEpoch() uint64 {
|
||||||
|
v2 := (storagegroup.StorageGroup)(sg)
|
||||||
|
return v2.GetExpirationEpoch()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetExpirationEpoch sets last NeoFS epoch number
|
// SetExpirationEpoch sets last NeoFS epoch number
|
||||||
// of the storage group lifetime.
|
// of the storage group lifetime.
|
||||||
|
//
|
||||||
|
// See also ExpirationEpoch.
|
||||||
func (sg *StorageGroup) SetExpirationEpoch(epoch uint64) {
|
func (sg *StorageGroup) SetExpirationEpoch(epoch uint64) {
|
||||||
(*storagegroup.StorageGroup)(sg).SetExpirationEpoch(epoch)
|
(*storagegroup.StorageGroup)(sg).SetExpirationEpoch(epoch)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Members returns strictly ordered list of
|
// Members returns strictly ordered list of
|
||||||
// storage group member objects.
|
// storage group member objects.
|
||||||
func (sg *StorageGroup) Members() []oid.ID {
|
//
|
||||||
mV2 := (*storagegroup.StorageGroup)(sg).GetMembers()
|
// Zero StorageGroup has nil members value.
|
||||||
|
//
|
||||||
|
// See also SetMembers.
|
||||||
|
func (sg StorageGroup) Members() []oid.ID {
|
||||||
|
v2 := (storagegroup.StorageGroup)(sg)
|
||||||
|
mV2 := v2.GetMembers()
|
||||||
|
|
||||||
if mV2 == nil {
|
if mV2 == nil {
|
||||||
return nil
|
return nil
|
||||||
|
@ -101,6 +125,8 @@ func (sg *StorageGroup) Members() []oid.ID {
|
||||||
|
|
||||||
// SetMembers sets strictly ordered list of
|
// SetMembers sets strictly ordered list of
|
||||||
// storage group member objects.
|
// storage group member objects.
|
||||||
|
//
|
||||||
|
// See also Members.
|
||||||
func (sg *StorageGroup) SetMembers(members []oid.ID) {
|
func (sg *StorageGroup) SetMembers(members []oid.ID) {
|
||||||
mV2 := (*storagegroup.StorageGroup)(sg).GetMembers()
|
mV2 := (*storagegroup.StorageGroup)(sg).GetMembers()
|
||||||
|
|
||||||
|
@ -126,29 +152,57 @@ func (sg *StorageGroup) SetMembers(members []oid.ID) {
|
||||||
(*storagegroup.StorageGroup)(sg).SetMembers(mV2)
|
(*storagegroup.StorageGroup)(sg).SetMembers(mV2)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToV2 converts StorageGroup to v2 StorageGroup message.
|
|
||||||
//
|
|
||||||
// Nil StorageGroup converts to nil.
|
|
||||||
func (sg *StorageGroup) ToV2() *storagegroup.StorageGroup {
|
|
||||||
return (*storagegroup.StorageGroup)(sg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Marshal marshals StorageGroup into a protobuf binary form.
|
// Marshal marshals StorageGroup into a protobuf binary form.
|
||||||
func (sg *StorageGroup) Marshal() ([]byte, error) {
|
//
|
||||||
return (*storagegroup.StorageGroup)(sg).StableMarshal(nil)
|
// See also Unmarshal.
|
||||||
|
func (sg StorageGroup) Marshal() ([]byte, error) {
|
||||||
|
v2 := (storagegroup.StorageGroup)(sg)
|
||||||
|
return v2.StableMarshal(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unmarshal unmarshals protobuf binary representation of StorageGroup.
|
// Unmarshal unmarshals protobuf binary representation of StorageGroup.
|
||||||
|
//
|
||||||
|
// See also Marshal.
|
||||||
func (sg *StorageGroup) Unmarshal(data []byte) error {
|
func (sg *StorageGroup) Unmarshal(data []byte) error {
|
||||||
return (*storagegroup.StorageGroup)(sg).Unmarshal(data)
|
v2 := (*storagegroup.StorageGroup)(sg)
|
||||||
|
err := v2.Unmarshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatCheck(v2)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalJSON encodes StorageGroup to protobuf JSON format.
|
// MarshalJSON encodes StorageGroup to protobuf JSON format.
|
||||||
func (sg *StorageGroup) MarshalJSON() ([]byte, error) {
|
//
|
||||||
return (*storagegroup.StorageGroup)(sg).MarshalJSON()
|
// See also UnmarshalJSON.
|
||||||
|
func (sg StorageGroup) MarshalJSON() ([]byte, error) {
|
||||||
|
v2 := (storagegroup.StorageGroup)(sg)
|
||||||
|
return v2.MarshalJSON()
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalJSON decodes StorageGroup from protobuf JSON format.
|
// UnmarshalJSON decodes StorageGroup from protobuf JSON format.
|
||||||
|
//
|
||||||
|
// See also MarshalJSON.
|
||||||
func (sg *StorageGroup) UnmarshalJSON(data []byte) error {
|
func (sg *StorageGroup) UnmarshalJSON(data []byte) error {
|
||||||
return (*storagegroup.StorageGroup)(sg).UnmarshalJSON(data)
|
v2 := (*storagegroup.StorageGroup)(sg)
|
||||||
|
err := v2.UnmarshalJSON(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatCheck(v2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatCheck(v2 *storagegroup.StorageGroup) error {
|
||||||
|
var oID oid.ID
|
||||||
|
|
||||||
|
for _, m := range v2.GetMembers() {
|
||||||
|
err := oID.ReadFromV2(m)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
package storagegroup_test
|
package storagegroup_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nspcc-dev/neofs-api-go/v2/refs"
|
||||||
storagegroupV2 "github.com/nspcc-dev/neofs-api-go/v2/storagegroup"
|
storagegroupV2 "github.com/nspcc-dev/neofs-api-go/v2/storagegroup"
|
||||||
|
storagegroupV2test "github.com/nspcc-dev/neofs-api-go/v2/storagegroup/test"
|
||||||
|
"github.com/nspcc-dev/neofs-sdk-go/checksum"
|
||||||
checksumtest "github.com/nspcc-dev/neofs-sdk-go/checksum/test"
|
checksumtest "github.com/nspcc-dev/neofs-sdk-go/checksum/test"
|
||||||
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
|
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
|
||||||
oidtest "github.com/nspcc-dev/neofs-sdk-go/object/id/test"
|
oidtest "github.com/nspcc-dev/neofs-sdk-go/object/id/test"
|
||||||
|
@ -13,7 +17,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestStorageGroup(t *testing.T) {
|
func TestStorageGroup(t *testing.T) {
|
||||||
sg := storagegroup.New()
|
var sg storagegroup.StorageGroup
|
||||||
|
|
||||||
sz := uint64(13)
|
sz := uint64(13)
|
||||||
sg.SetValidationDataSize(sz)
|
sg.SetValidationDataSize(sz)
|
||||||
|
@ -35,6 +39,56 @@ func TestStorageGroup(t *testing.T) {
|
||||||
require.Equal(t, members, sg.Members())
|
require.Equal(t, members, sg.Members())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStorageGroup_ReadFromV2(t *testing.T) {
|
||||||
|
t.Run("from zero", func(t *testing.T) {
|
||||||
|
var (
|
||||||
|
x storagegroup.StorageGroup
|
||||||
|
v2 storagegroupV2.StorageGroup
|
||||||
|
)
|
||||||
|
|
||||||
|
x.ReadFromV2(v2)
|
||||||
|
|
||||||
|
require.Zero(t, x.ExpirationEpoch())
|
||||||
|
require.Zero(t, x.ValidationDataSize())
|
||||||
|
_, set := x.ValidationDataHash()
|
||||||
|
require.False(t, set)
|
||||||
|
require.Zero(t, x.Members())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("from non-zero", func(t *testing.T) {
|
||||||
|
var (
|
||||||
|
x storagegroup.StorageGroup
|
||||||
|
v2 = storagegroupV2test.GenerateStorageGroup(false)
|
||||||
|
)
|
||||||
|
|
||||||
|
// https://github.com/nspcc-dev/neofs-api-go/issues/394
|
||||||
|
v2.SetMembers(generateOIDList())
|
||||||
|
|
||||||
|
size := v2.GetValidationDataSize()
|
||||||
|
epoch := v2.GetExpirationEpoch()
|
||||||
|
mm := v2.GetMembers()
|
||||||
|
hashV2 := v2.GetValidationHash()
|
||||||
|
|
||||||
|
x.ReadFromV2(*v2)
|
||||||
|
|
||||||
|
require.Equal(t, epoch, x.ExpirationEpoch())
|
||||||
|
require.Equal(t, size, x.ValidationDataSize())
|
||||||
|
|
||||||
|
var hash checksum.Checksum
|
||||||
|
hash.ReadFromV2(*hashV2)
|
||||||
|
h, set := x.ValidationDataHash()
|
||||||
|
require.True(t, set)
|
||||||
|
require.Equal(t, hash, h)
|
||||||
|
|
||||||
|
var oidV2 refs.ObjectID
|
||||||
|
|
||||||
|
for i, m := range mm {
|
||||||
|
x.Members()[i].WriteToV2(&oidV2)
|
||||||
|
require.Equal(t, m, oidV2)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestStorageGroupEncoding(t *testing.T) {
|
func TestStorageGroupEncoding(t *testing.T) {
|
||||||
sg := storagegrouptest.StorageGroup()
|
sg := storagegrouptest.StorageGroup()
|
||||||
|
|
||||||
|
@ -42,7 +96,7 @@ func TestStorageGroupEncoding(t *testing.T) {
|
||||||
data, err := sg.Marshal()
|
data, err := sg.Marshal()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
sg2 := storagegroup.New()
|
var sg2 storagegroup.StorageGroup
|
||||||
require.NoError(t, sg2.Unmarshal(data))
|
require.NoError(t, sg2.Unmarshal(data))
|
||||||
|
|
||||||
require.Equal(t, sg, sg2)
|
require.Equal(t, sg, sg2)
|
||||||
|
@ -52,32 +106,58 @@ func TestStorageGroupEncoding(t *testing.T) {
|
||||||
data, err := sg.MarshalJSON()
|
data, err := sg.MarshalJSON()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
sg2 := storagegroup.New()
|
var sg2 storagegroup.StorageGroup
|
||||||
require.NoError(t, sg2.UnmarshalJSON(data))
|
require.NoError(t, sg2.UnmarshalJSON(data))
|
||||||
|
|
||||||
require.Equal(t, sg, sg2)
|
require.Equal(t, sg, sg2)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewFromV2(t *testing.T) {
|
func TestStorageGroup_WriteToV2(t *testing.T) {
|
||||||
t.Run("from nil", func(t *testing.T) {
|
t.Run("zero to v2", func(t *testing.T) {
|
||||||
var x *storagegroupV2.StorageGroup
|
var (
|
||||||
|
x storagegroup.StorageGroup
|
||||||
|
v2 storagegroupV2.StorageGroup
|
||||||
|
)
|
||||||
|
|
||||||
require.Nil(t, storagegroup.NewFromV2(x))
|
x.WriteToV2(&v2)
|
||||||
|
|
||||||
|
require.Nil(t, v2.GetValidationHash())
|
||||||
|
require.Nil(t, v2.GetMembers())
|
||||||
|
require.Zero(t, v2.GetValidationDataSize())
|
||||||
|
require.Zero(t, v2.GetExpirationEpoch())
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
func TestStorageGroup_ToV2(t *testing.T) {
|
t.Run("non-zero to v2", func(t *testing.T) {
|
||||||
t.Run("nil", func(t *testing.T) {
|
var (
|
||||||
var x *storagegroup.StorageGroup
|
x = storagegrouptest.StorageGroup()
|
||||||
|
v2 storagegroupV2.StorageGroup
|
||||||
|
)
|
||||||
|
|
||||||
require.Nil(t, x.ToV2())
|
x.WriteToV2(&v2)
|
||||||
|
|
||||||
|
require.Equal(t, x.ExpirationEpoch(), v2.GetExpirationEpoch())
|
||||||
|
require.Equal(t, x.ValidationDataSize(), v2.GetValidationDataSize())
|
||||||
|
|
||||||
|
var hash checksum.Checksum
|
||||||
|
hash.ReadFromV2(*v2.GetValidationHash())
|
||||||
|
|
||||||
|
h, set := x.ValidationDataHash()
|
||||||
|
require.True(t, set)
|
||||||
|
require.Equal(t, h, hash)
|
||||||
|
|
||||||
|
var oidV2 refs.ObjectID
|
||||||
|
|
||||||
|
for i, m := range x.Members() {
|
||||||
|
m.WriteToV2(&oidV2)
|
||||||
|
require.Equal(t, oidV2, v2.GetMembers()[i])
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNew(t *testing.T) {
|
func TestNew(t *testing.T) {
|
||||||
t.Run("default values", func(t *testing.T) {
|
t.Run("default values", func(t *testing.T) {
|
||||||
sg := storagegroup.New()
|
var sg storagegroup.StorageGroup
|
||||||
|
|
||||||
// check initial values
|
// check initial values
|
||||||
require.Nil(t, sg.Members())
|
require.Nil(t, sg.Members())
|
||||||
|
@ -85,13 +165,19 @@ func TestNew(t *testing.T) {
|
||||||
require.False(t, set)
|
require.False(t, set)
|
||||||
require.Zero(t, sg.ExpirationEpoch())
|
require.Zero(t, sg.ExpirationEpoch())
|
||||||
require.Zero(t, sg.ValidationDataSize())
|
require.Zero(t, sg.ValidationDataSize())
|
||||||
|
|
||||||
// convert to v2 message
|
|
||||||
sgV2 := sg.ToV2()
|
|
||||||
|
|
||||||
require.Nil(t, sgV2.GetMembers())
|
|
||||||
require.Nil(t, sgV2.GetValidationHash())
|
|
||||||
require.Zero(t, sgV2.GetExpirationEpoch())
|
|
||||||
require.Zero(t, sgV2.GetValidationDataSize())
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func generateOIDList() []refs.ObjectID {
|
||||||
|
const size = 3
|
||||||
|
|
||||||
|
mmV2 := make([]refs.ObjectID, size)
|
||||||
|
for i := 0; i < size; i++ {
|
||||||
|
oidV2 := make([]byte, sha256.Size)
|
||||||
|
oidV2[i] = byte(i)
|
||||||
|
|
||||||
|
mmV2[i].SetValue(oidV2)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mmV2
|
||||||
|
}
|
||||||
|
|
13
storagegroup/test/doc.go
Normal file
13
storagegroup/test/doc.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
/*
|
||||||
|
Package storagegrouptest provides functions for convenient testing of storagegroup package API.
|
||||||
|
|
||||||
|
Note that importing the package into source files is highly discouraged.
|
||||||
|
|
||||||
|
Random instance generation functions can be useful when testing expects any value, e.g.:
|
||||||
|
import storagegrouptest "github.com/nspcc-dev/neofs-sdk-go/storagegroup/test"
|
||||||
|
|
||||||
|
val := storagegrouptest.StorageGroup()
|
||||||
|
// test the value
|
||||||
|
|
||||||
|
*/
|
||||||
|
package storagegrouptest
|
|
@ -8,8 +8,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// StorageGroup returns random storagegroup.StorageGroup.
|
// StorageGroup returns random storagegroup.StorageGroup.
|
||||||
func StorageGroup() *storagegroup.StorageGroup {
|
func StorageGroup() storagegroup.StorageGroup {
|
||||||
x := storagegroup.New()
|
var x storagegroup.StorageGroup
|
||||||
|
|
||||||
x.SetExpirationEpoch(66)
|
x.SetExpirationEpoch(66)
|
||||||
x.SetValidationDataSize(322)
|
x.SetValidationDataSize(322)
|
||||||
|
|
Loading…
Reference in a new issue