[#83] client: Implement status library
Define base `Status` interface. Provide the functionality to distinguish success and failure returns. Provide functionality to transport statuses over NeoFS API V2 protocol. Support success `OK` and failure `INTERNAL` returns. Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
This commit is contained in:
parent
fc18ca2cb3
commit
9dcff95a29
20 changed files with 465 additions and 11 deletions
|
@ -4,9 +4,9 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/nspcc-dev/neofs-api-go/rpc/client"
|
||||
v2accounting "github.com/nspcc-dev/neofs-api-go/v2/accounting"
|
||||
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
|
||||
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
|
||||
v2signature "github.com/nspcc-dev/neofs-api-go/v2/signature"
|
||||
"github.com/nspcc-dev/neofs-sdk-go/accounting"
|
||||
"github.com/nspcc-dev/neofs-sdk-go/owner"
|
||||
|
|
|
@ -3,7 +3,7 @@ package client
|
|||
import (
|
||||
"io"
|
||||
|
||||
"github.com/nspcc-dev/neofs-api-go/rpc/client"
|
||||
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
|
||||
)
|
||||
|
||||
// Client represents NeoFS client.
|
||||
|
|
|
@ -5,10 +5,10 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/nspcc-dev/neofs-api-go/rpc/client"
|
||||
v2container "github.com/nspcc-dev/neofs-api-go/v2/container"
|
||||
"github.com/nspcc-dev/neofs-api-go/v2/refs"
|
||||
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
|
||||
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
|
||||
v2signature "github.com/nspcc-dev/neofs-api-go/v2/signature"
|
||||
"github.com/nspcc-dev/neofs-sdk-go/container"
|
||||
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
|
||||
|
|
|
@ -4,9 +4,9 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/nspcc-dev/neofs-api-go/rpc/client"
|
||||
v2netmap "github.com/nspcc-dev/neofs-api-go/v2/netmap"
|
||||
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
|
||||
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
|
||||
v2signature "github.com/nspcc-dev/neofs-api-go/v2/signature"
|
||||
"github.com/nspcc-dev/neofs-sdk-go/netmap"
|
||||
"github.com/nspcc-dev/neofs-sdk-go/version"
|
||||
|
|
|
@ -9,10 +9,10 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/nspcc-dev/neofs-api-go/rpc/client"
|
||||
v2object "github.com/nspcc-dev/neofs-api-go/v2/object"
|
||||
v2refs "github.com/nspcc-dev/neofs-api-go/v2/refs"
|
||||
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
|
||||
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
|
||||
v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
|
||||
"github.com/nspcc-dev/neofs-api-go/v2/signature"
|
||||
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
|
||||
|
|
|
@ -5,8 +5,8 @@ import (
|
|||
"crypto/tls"
|
||||
"time"
|
||||
|
||||
"github.com/nspcc-dev/neofs-api-go/rpc/client"
|
||||
"github.com/nspcc-dev/neofs-api-go/v2/refs"
|
||||
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
|
||||
v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
|
||||
"github.com/nspcc-dev/neofs-sdk-go/session"
|
||||
"github.com/nspcc-dev/neofs-sdk-go/token"
|
||||
|
|
|
@ -3,7 +3,7 @@ package client
|
|||
import (
|
||||
"io"
|
||||
|
||||
"github.com/nspcc-dev/neofs-api-go/rpc/client"
|
||||
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
|
||||
)
|
||||
|
||||
// Raw returns underlying raw protobuf client.
|
||||
|
|
|
@ -4,9 +4,9 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/nspcc-dev/neofs-api-go/rpc/client"
|
||||
v2reputation "github.com/nspcc-dev/neofs-api-go/v2/reputation"
|
||||
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
|
||||
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
|
||||
v2signature "github.com/nspcc-dev/neofs-api-go/v2/signature"
|
||||
"github.com/nspcc-dev/neofs-sdk-go/reputation"
|
||||
)
|
||||
|
|
|
@ -5,8 +5,8 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/nspcc-dev/neofs-api-go/rpc/client"
|
||||
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
|
||||
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
|
||||
v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
|
||||
v2signature "github.com/nspcc-dev/neofs-api-go/v2/signature"
|
||||
"github.com/nspcc-dev/neofs-sdk-go/owner"
|
||||
|
|
55
client/status/common.go
Normal file
55
client/status/common.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
package apistatus
|
||||
|
||||
import (
|
||||
"github.com/nspcc-dev/neofs-api-go/v2/status"
|
||||
)
|
||||
|
||||
// ServerInternal describes failure statuses related to internal server errors.
|
||||
// Instances provide Status and StatusV2 interfaces.
|
||||
//
|
||||
// The status is purely informative, the client should not go into details of the error except for debugging needs.
|
||||
type ServerInternal struct {
|
||||
v2 status.Status
|
||||
}
|
||||
|
||||
func (x ServerInternal) Error() string {
|
||||
return errMessageStatusV2(
|
||||
globalizeCodeV2(status.Internal, status.GlobalizeCommonFail),
|
||||
x.v2.Message(),
|
||||
)
|
||||
}
|
||||
|
||||
// implements method of the FromStatusV2 local interface.
|
||||
func (x *ServerInternal) fromStatusV2(st *status.Status) {
|
||||
x.v2 = *st
|
||||
}
|
||||
|
||||
// ToStatusV2 implements StatusV2 interface method.
|
||||
// If the value was returned by FromStatusV2, returns the source message.
|
||||
// Otherwise, returns message with
|
||||
// * code: INTERNAL;
|
||||
// * string message: empty;
|
||||
// * details: empty.
|
||||
func (x ServerInternal) ToStatusV2() *status.Status {
|
||||
x.v2.SetCode(globalizeCodeV2(status.Internal, status.GlobalizeCommonFail))
|
||||
return &x.v2
|
||||
}
|
||||
|
||||
// SetMessage sets message describing internal error.
|
||||
//
|
||||
// Message should be used for debug purposes only.
|
||||
func (x *ServerInternal) SetMessage(msg string) {
|
||||
x.v2.SetMessage(msg)
|
||||
}
|
||||
|
||||
// Message returns message describing internal server error.
|
||||
//
|
||||
// Message should be used for debug purposes only. By default, it is empty.
|
||||
func (x ServerInternal) Message() string {
|
||||
return x.v2.Message()
|
||||
}
|
||||
|
||||
// WriteInternalServerErr writes err message to ServerInternal instance.
|
||||
func WriteInternalServerErr(x *ServerInternal, err error) {
|
||||
x.SetMessage(err.Error())
|
||||
}
|
26
client/status/common_test.go
Normal file
26
client/status/common_test.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
package apistatus_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestServerInternal_Message(t *testing.T) {
|
||||
const msg = "some message"
|
||||
|
||||
var st apistatus.ServerInternal
|
||||
|
||||
res := st.Message()
|
||||
resv2 := apistatus.ToStatusV2(st).Message()
|
||||
require.Empty(t, res)
|
||||
require.Empty(t, resv2)
|
||||
|
||||
st.SetMessage(msg)
|
||||
|
||||
res = st.Message()
|
||||
resv2 = apistatus.ToStatusV2(st).Message()
|
||||
require.Equal(t, msg, res)
|
||||
require.Equal(t, msg, resv2)
|
||||
}
|
44
client/status/status.go
Normal file
44
client/status/status.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
package apistatus
|
||||
|
||||
// Status defines a variety of NeoFS API status returns.
|
||||
//
|
||||
// All statuses are split into two disjoint subsets: successful and failed, and:
|
||||
// * statuses that implement the build-in error interface are considered failed statuses;
|
||||
// * all other value types are considered successes (nil is a default success).
|
||||
//
|
||||
// In Go code type of success can be determined by a type switch, failure - by a switch with errors.As calls.
|
||||
// Nil should be considered as a success, and default switch section - as an unrecognized Status.
|
||||
//
|
||||
// To convert statuses into errors and vice versa, use functions ErrToStatus and ErrFromStatus, respectively.
|
||||
// ErrFromStatus function returns nil for successful statuses. However, to simplify the check of statuses for success,
|
||||
// IsSuccessful function should be used (try to avoid nil comparison).
|
||||
// It should be noted that using direct typecasting is not a compatible approach.
|
||||
//
|
||||
// To transport statuses using the NeoFS API V2 protocol, see StatusV2 interface and FromStatusV2 and ToStatusV2 functions.
|
||||
type Status interface{}
|
||||
|
||||
// ErrFromStatus converts Status instance to error if it is failed. Returns nil on successful Status.
|
||||
//
|
||||
// Note: direct assignment may not be compatibility-safe.
|
||||
func ErrFromStatus(st Status) error {
|
||||
if err, ok := st.(error); ok {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ErrToStatus converts the error instance to Status instance.
|
||||
//
|
||||
// Note: direct assignment may not be compatibility-safe.
|
||||
func ErrToStatus(err error) Status {
|
||||
return err
|
||||
}
|
||||
|
||||
// IsSuccessful checks if status is successful.
|
||||
//
|
||||
// Note: direct cast may not be compatibility-safe.
|
||||
func IsSuccessful(st Status) bool {
|
||||
_, ok := st.(error)
|
||||
return !ok
|
||||
}
|
35
client/status/status_test.go
Normal file
35
client/status/status_test.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
package apistatus_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestErrors(t *testing.T) {
|
||||
t.Run("error source", func(t *testing.T) {
|
||||
err := errors.New("some error")
|
||||
|
||||
st := apistatus.ErrToStatus(err)
|
||||
|
||||
success := apistatus.IsSuccessful(st)
|
||||
require.False(t, success)
|
||||
|
||||
res := apistatus.ErrFromStatus(st)
|
||||
|
||||
require.Equal(t, err, res)
|
||||
})
|
||||
|
||||
t.Run("non-error source", func(t *testing.T) {
|
||||
var st apistatus.Status = "any non-error type"
|
||||
|
||||
success := apistatus.IsSuccessful(st)
|
||||
require.True(t, success)
|
||||
|
||||
res := apistatus.ErrFromStatus(st)
|
||||
|
||||
require.Nil(t, res)
|
||||
})
|
||||
}
|
32
client/status/success.go
Normal file
32
client/status/success.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
package apistatus
|
||||
|
||||
import (
|
||||
"github.com/nspcc-dev/neofs-api-go/v2/status"
|
||||
)
|
||||
|
||||
// SuccessDefaultV2 represents Status instance of default success. Implements StatusV2.
|
||||
type SuccessDefaultV2 struct {
|
||||
isNil bool
|
||||
|
||||
v2 *status.Status
|
||||
}
|
||||
|
||||
// implements method of the FromStatusV2 local interface.
|
||||
func (x *SuccessDefaultV2) fromStatusV2(st *status.Status) {
|
||||
x.isNil = st == nil
|
||||
x.v2 = st
|
||||
}
|
||||
|
||||
// ToStatusV2 implements StatusV2 interface method.
|
||||
// If the value was returned by FromStatusV2, returns the source message.
|
||||
// Otherwise, returns message with
|
||||
// * code: OK;
|
||||
// * string message: empty;
|
||||
// * details: empty.
|
||||
func (x SuccessDefaultV2) ToStatusV2() *status.Status {
|
||||
if x.isNil || x.v2 != nil {
|
||||
return x.v2
|
||||
}
|
||||
|
||||
return newStatusV2WithLocalCode(status.OK, status.GlobalizeSuccess)
|
||||
}
|
18
client/status/unrecognized.go
Normal file
18
client/status/unrecognized.go
Normal file
|
@ -0,0 +1,18 @@
|
|||
package apistatus
|
||||
|
||||
import (
|
||||
"github.com/nspcc-dev/neofs-api-go/v2/status"
|
||||
)
|
||||
|
||||
type unrecognizedStatusV2 struct {
|
||||
v2 status.Status
|
||||
}
|
||||
|
||||
func (x unrecognizedStatusV2) Error() string {
|
||||
return errMessageStatusV2("unrecognized", x.v2.Message())
|
||||
}
|
||||
|
||||
// implements method of the FromStatusV2 local interface.
|
||||
func (x *unrecognizedStatusV2) fromStatusV2(st *status.Status) {
|
||||
x.v2 = *st
|
||||
}
|
99
client/status/v2.go
Normal file
99
client/status/v2.go
Normal file
|
@ -0,0 +1,99 @@
|
|||
package apistatus
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/nspcc-dev/neofs-api-go/v2/status"
|
||||
)
|
||||
|
||||
// StatusV2 defines a variety of Status instances compatible with NeoFS API V2 protocol.
|
||||
//
|
||||
// Note: it is not recommended to use this type directly, it is intended for documentation of the library functionality.
|
||||
type StatusV2 interface {
|
||||
Status
|
||||
|
||||
// ToStatusV2 returns the status as github.com/nspcc-dev/neofs-api-go/v2/status.Status message structure.
|
||||
ToStatusV2() *status.Status
|
||||
}
|
||||
|
||||
// FromStatusV2 converts status.Status message structure to Status instance. Inverse to ToStatusV2 operation.
|
||||
//
|
||||
// If result is not nil, it implements StatusV2. This fact should be taken into account only when passing
|
||||
// the result to the inverse function ToStatusV2, casts are not compatibility-safe.
|
||||
//
|
||||
// Below is the mapping of return codes to Status instance types (with a description of parsing details).
|
||||
// Note: notice if the return type is a pointer.
|
||||
//
|
||||
// Successes:
|
||||
// * status.OK: *SuccessDefaultV2 (this also includes nil argument).
|
||||
//
|
||||
// Common failures:
|
||||
// * status.Internal: *ServerInternal.
|
||||
func FromStatusV2(st *status.Status) Status {
|
||||
var decoder interface {
|
||||
fromStatusV2(*status.Status)
|
||||
}
|
||||
|
||||
switch code := st.Code(); {
|
||||
case status.IsSuccess(code):
|
||||
switch status.LocalizeSuccess(&code); code {
|
||||
case status.OK:
|
||||
decoder = new(SuccessDefaultV2)
|
||||
}
|
||||
case status.IsCommonFail(code):
|
||||
switch status.LocalizeCommonFail(&code); code {
|
||||
case status.Internal:
|
||||
decoder = new(ServerInternal)
|
||||
}
|
||||
}
|
||||
|
||||
if decoder == nil {
|
||||
decoder = new(unrecognizedStatusV2)
|
||||
}
|
||||
|
||||
decoder.fromStatusV2(st)
|
||||
|
||||
return decoder
|
||||
}
|
||||
|
||||
// ToStatusV2 converts Status instance to status.Status message structure. Inverse to FromStatusV2 operation.
|
||||
//
|
||||
// If argument is the StatusV2 instance, it is converted directly.
|
||||
// Otherwise, successes are converted with status.OK code w/o details and message, failures - with status.Internal.
|
||||
func ToStatusV2(st Status) *status.Status {
|
||||
if v, ok := st.(StatusV2); ok {
|
||||
return v.ToStatusV2()
|
||||
}
|
||||
|
||||
if IsSuccessful(st) {
|
||||
return newStatusV2WithLocalCode(status.OK, status.GlobalizeSuccess)
|
||||
}
|
||||
|
||||
return newStatusV2WithLocalCode(status.Internal, status.GlobalizeCommonFail)
|
||||
}
|
||||
|
||||
func errMessageStatusV2(code interface{}, msg string) string {
|
||||
const (
|
||||
noMsgFmt = "status: code = %v"
|
||||
msgFmt = noMsgFmt + " message = %s"
|
||||
)
|
||||
|
||||
if msg != "" {
|
||||
return fmt.Sprintf(msgFmt, code, msg)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(noMsgFmt, code)
|
||||
}
|
||||
|
||||
func newStatusV2WithLocalCode(code status.Code, globalizer func(*status.Code)) *status.Status {
|
||||
var st status.Status
|
||||
|
||||
st.SetCode(globalizeCodeV2(code, globalizer))
|
||||
|
||||
return &st
|
||||
}
|
||||
|
||||
func globalizeCodeV2(code status.Code, globalizer func(*status.Code)) status.Code {
|
||||
globalizer(&code)
|
||||
return code
|
||||
}
|
145
client/status/v2_test.go
Normal file
145
client/status/v2_test.go
Normal file
|
@ -0,0 +1,145 @@
|
|||
package apistatus_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestToStatusV2(t *testing.T) {
|
||||
type statusConstructor func() apistatus.Status
|
||||
|
||||
for _, testItem := range [...]struct {
|
||||
status interface{} // Status or statusConstructor
|
||||
codeV2 uint64
|
||||
}{
|
||||
{
|
||||
status: errors.New("some error"),
|
||||
codeV2: 1024,
|
||||
},
|
||||
{
|
||||
status: 1,
|
||||
codeV2: 0,
|
||||
},
|
||||
{
|
||||
status: "text",
|
||||
codeV2: 0,
|
||||
},
|
||||
{
|
||||
status: true,
|
||||
codeV2: 0,
|
||||
},
|
||||
{
|
||||
status: true,
|
||||
codeV2: 0,
|
||||
},
|
||||
{
|
||||
status: nil,
|
||||
codeV2: 0,
|
||||
},
|
||||
{
|
||||
status: (statusConstructor)(func() apistatus.Status {
|
||||
var st apistatus.ServerInternal
|
||||
|
||||
st.SetMessage("internal error message")
|
||||
|
||||
return st
|
||||
}),
|
||||
codeV2: 1024,
|
||||
},
|
||||
} {
|
||||
var st apistatus.Status
|
||||
|
||||
if cons, ok := testItem.status.(statusConstructor); ok {
|
||||
st = cons()
|
||||
} else {
|
||||
st = testItem.status
|
||||
}
|
||||
|
||||
stv2 := apistatus.ToStatusV2(st)
|
||||
|
||||
// must generate the same status.Status message
|
||||
require.EqualValues(t, testItem.codeV2, stv2.Code())
|
||||
|
||||
_, ok := st.(apistatus.StatusV2)
|
||||
if ok {
|
||||
// restore and convert again
|
||||
restored := apistatus.FromStatusV2(stv2)
|
||||
|
||||
res := apistatus.ToStatusV2(restored)
|
||||
|
||||
// must generate the same status.Status message
|
||||
require.Equal(t, stv2, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromStatusV2(t *testing.T) {
|
||||
type statusConstructor func() apistatus.Status
|
||||
|
||||
for _, testItem := range [...]struct {
|
||||
status interface{} // Status or statusConstructor
|
||||
codeV2 uint64
|
||||
}{
|
||||
{
|
||||
status: errors.New("some error"),
|
||||
codeV2: 1024,
|
||||
},
|
||||
{
|
||||
status: 1,
|
||||
codeV2: 0,
|
||||
},
|
||||
{
|
||||
status: "text",
|
||||
codeV2: 0,
|
||||
},
|
||||
{
|
||||
status: true,
|
||||
codeV2: 0,
|
||||
},
|
||||
{
|
||||
status: true,
|
||||
codeV2: 0,
|
||||
},
|
||||
{
|
||||
status: nil,
|
||||
codeV2: 0,
|
||||
},
|
||||
{
|
||||
status: (statusConstructor)(func() apistatus.Status {
|
||||
var st apistatus.ServerInternal
|
||||
|
||||
st.SetMessage("internal error message")
|
||||
|
||||
return st
|
||||
}),
|
||||
codeV2: 1024,
|
||||
},
|
||||
} {
|
||||
var st apistatus.Status
|
||||
|
||||
if cons, ok := testItem.status.(statusConstructor); ok {
|
||||
st = cons()
|
||||
} else {
|
||||
st = testItem.status
|
||||
}
|
||||
|
||||
stv2 := apistatus.ToStatusV2(st)
|
||||
|
||||
// must generate the same status.Status message
|
||||
require.EqualValues(t, testItem.codeV2, stv2.Code())
|
||||
|
||||
_, ok := st.(apistatus.StatusV2)
|
||||
if ok {
|
||||
// restore and convert again
|
||||
restored := apistatus.FromStatusV2(stv2)
|
||||
|
||||
res := apistatus.ToStatusV2(restored)
|
||||
|
||||
// must generate the same status.Status message
|
||||
require.Equal(t, stv2, res)
|
||||
}
|
||||
}
|
||||
}
|
2
go.mod
2
go.mod
|
@ -9,7 +9,7 @@ require (
|
|||
github.com/hashicorp/golang-lru v0.5.4
|
||||
github.com/mr-tron/base58 v1.2.0
|
||||
github.com/nspcc-dev/hrw v1.0.9
|
||||
github.com/nspcc-dev/neofs-api-go v1.30.0
|
||||
github.com/nspcc-dev/neofs-api-go/v2 v2.11.0-pre.0.20211118144033-580f6c5554ff
|
||||
github.com/nspcc-dev/neofs-crypto v0.3.0
|
||||
github.com/nspcc-dev/rfc6979 v0.2.0
|
||||
github.com/stretchr/testify v1.7.0
|
||||
|
|
BIN
go.sum
BIN
go.sum
Binary file not shown.
|
@ -10,7 +10,7 @@ import (
|
|||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
client "github.com/nspcc-dev/neofs-api-go/rpc/client"
|
||||
client "github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
|
||||
accounting "github.com/nspcc-dev/neofs-sdk-go/accounting"
|
||||
client0 "github.com/nspcc-dev/neofs-sdk-go/client"
|
||||
container "github.com/nspcc-dev/neofs-sdk-go/container"
|
||||
|
|
Loading…
Reference in a new issue