Compare commits

..

3 commits

Author SHA1 Message Date
Alex Vanin
a1e17ea1f2 [#193] pool: Return copy of session token in cache
To avoid side effects after token re-sign.

Signed-off-by: Alex Vanin <alexey@nspcc.ru>

(cherry picked from commit 2104945f9e)
2022-04-06 12:13:00 +03:00
Alex Vanin
129012748e [#193] pool: Add test to check side effects in token cache
Signed-off-by: Alex Vanin <alexey@nspcc.ru>
(cherry picked from commit f38a24e8b5)
2022-04-06 12:12:52 +03:00
Denis Kirillov
0367c83f5a [#191] pool: use bearer token
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-04-06 10:32:01 +03:00
311 changed files with 13665 additions and 20726 deletions

1
.github/CODEOWNERS vendored
View file

@ -1 +0,0 @@
* @TrueCloudLab/storage-core @TrueCloudLab/storage-services

View file

@ -1,45 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: community, triage, bug
assignees: ''
---
<!--- Provide a general summary of the issue in the Title above -->
## Expected Behavior
<!--- If you're describing a bug, tell us what should happen -->
<!--- If you're suggesting a change/improvement, tell us how it should work -->
## Current Behavior
<!--- If describing a bug, tell us what happens instead of the expected behavior -->
<!--- If suggesting a change/improvement, explain the difference from current behavior -->
## Possible Solution
<!--- Not obligatory -->
<!--- If no reason/fix/additions for the bug can be suggested, -->
<!--- uncomment the following phrase: -->
<!--- No fix can be suggested by a QA engineer. Further solutions shall be up to developers. -->
## Steps to Reproduce (for bugs)
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
<!--- reproduce this bug. -->
1.
## Context
<!--- How has this issue affected you? What are you trying to accomplish? -->
<!--- Providing context helps us come up with a solution that is most useful in the real world -->
## Regression
<!-- Is this issue a regression? (Yes / No) -->
<!-- If Yes, optionally please include version or commit id or PR# that caused this regression, if you have these details. -->
## Your Environment
<!--- Include as many relevant details about the environment you experienced the bug in -->
* Version used:
* Server setup and configuration:
* Operating System and version (`uname -a`):

View file

@ -1 +0,0 @@
blank_issues_enabled: false

View file

@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: community, triage
assignees: ''
---
## Is your feature request related to a problem? Please describe.
<!--- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
## Describe the solution you'd like
<!--- A clear and concise description of what you want to happen. -->
## Describe alternatives you've considered
<!--- A clear and concise description of any alternative solutions or features you've considered. -->
## Additional context
<!--- Add any other context or screenshots about the feature request here. -->

View file

@ -15,20 +15,20 @@ jobs:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
strategy: strategy:
matrix: matrix:
go_versions: [ '1.18.x', '1.19.x', '1.20.x' ] go_versions: [ '1.16.x', '1.17.x' ]
fail-fast: false fail-fast: false
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v2
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v3 uses: actions/setup-go@v2
with: with:
go-version: '${{ matrix.go_versions }}' go-version: '${{ matrix.go_versions }}'
- name: Restore Go modules from cache - name: Restore Go modules from cache
uses: actions/cache@v3 uses: actions/cache@v2
with: with:
path: /home/runner/go/pkg/mod path: /home/runner/go/pkg/mod
key: deps-${{ hashFiles('go.sum') }} key: deps-${{ hashFiles('go.sum') }}
@ -43,10 +43,10 @@ jobs:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@v3 uses: actions/checkout@v2
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v3 uses: golangci/golangci-lint-action@v2
with: with:
version: latest version: latest
only-new-issues: true only-new-issues: true

View file

@ -1,10 +0,0 @@
[general]
fail-without-commits=true
contrib=CC1
[title-match-regex]
regex=^\[\#[0-9Xx]+\]\s
[ignore-by-title]
regex=^Release(.*)
ignore=title-match-regex

View file

@ -24,9 +24,6 @@ linters-settings:
govet: govet:
# report about shadowed variables # report about shadowed variables
check-shadowing: false check-shadowing: false
staticcheck:
checks: ["all", "-SA1019"] # TODO Enable SA1019 after deprecated warning are fixed.
linters: linters:
enable: enable:
@ -35,12 +32,15 @@ linters:
- revive - revive
# some default golangci-lint linters # some default golangci-lint linters
- deadcode
- errcheck - errcheck
- gosimple - gosimple
- ineffassign - ineffassign
- staticcheck - staticcheck
- structcheck
- typecheck - typecheck
- unused - unused
- varcheck
# extra linters # extra linters
- exhaustive - exhaustive

View file

@ -1,30 +0,0 @@
ci:
autofix_prs: false
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: check-added-large-files
- id: check-case-conflict
- id: check-executables-have-shebangs
- id: check-shebang-scripts-are-executable
- id: check-merge-conflict
- id: check-json
- id: check-xml
- id: check-yaml
- id: trailing-whitespace
args: [--markdown-linebreak-ext=md]
- id: end-of-file-fixer
exclude: ".key$"
- repo: https://github.com/golangci/golangci-lint
rev: v1.51.2
hooks:
- id: golangci-lint
- repo: https://github.com/jorisroovers/gitlint
rev: v0.18.0
hooks:
- id: gitlint
stages: [commit-msg]

0
Makefile Executable file → Normal file
View file

View file

@ -1,6 +1,6 @@
# frostfs-sdk-go # neofs-sdk-go
Go implementation of FrostFS SDK. It contains high-level version-independent wrappers Go implementation of NeoFS SDK. It contains high-level version-independent wrappers
for structures from [frostfs-api-go](https://git.frostfs.info/TrueCloudLab/frostfs-api-go) as well as for structures from [neofs-api-go](https://github.com/nspcc-dev/neofs-api-go) as well as
helper functions for simplifying node/dApp implementations. helper functions for simplifying node/dApp implementations.
## Repository structure ## Repository structure
@ -10,75 +10,63 @@ Contains fixed-point `Decimal` type for performing balance calculations.
### eacl ### eacl
Contains Extended ACL types for fine-grained access control. Contains Extended ACL types for fine-grained access control.
There is also a reference implementation of checking algorithm which is used in FrostFS node. There is also a reference implementation of checking algorithm which is used in NeoFS node.
### checksum ### checksum
Contains `Checksum` type encapsulating checksum as well as it's kind. Contains `Checksum` type encapsulating checksum as well as it's kind.
Currently Sha256 and [Tillich-Zemor hashsum](https://git.frostfs.info/TrueCloudLab/tzhash) are in use. Currently Sha256 and [Tillich-Zemor hashsum](https://github.com/nspcc-dev/tzhash) are in use.
### owner ### owner
`owner.ID` type represents single account interacting with FrostFS. In v2 version of protocol `owner.ID` type represents single account interacting with NeoFS. In v2 version of protocol
it is just raw bytes behing [base58-encoded address](https://docs.neo.org/docs/en-us/basic/concept/wallets.html#address) it is just raw bytes behing [base58-encoded address](https://docs.neo.org/docs/en-us/basic/concept/wallets.html#address)
in Neo blockchain. Note that for historical reasons it contains in Neo blockchain. Note that for historical reasons it contains
version prefix and checksum in addition to script-hash. version prefix and checksum in addition to script-hash.
### token ### token
Contains Bearer token type with several FrostFS-specific methods. Contains Bearer token type with several NeoFS-specific methods.
### ns ### resolver
In FrostFS there are 2 types of name resolution: DNS and NNS. NNS stands for Neo Name Service In NeoFS there are 2 types of name resolution: DNS and NNS. NNS stands for Neo Name Service
is just a [contract](https://git.frostfs.info/TrueCloudLab/frostfs-contract) deployed on a Neo blockchain. is just a [contract](https://github.com/nspcc-dev/neofs-contract/) deployed on a Neo blockchain.
Basically, NNS is just a DNS-on-chain which can be used for resolving container nice-names as well Basically, NNS is just a DNS-on-chain which can be used for resolving container nice-names as well
as any other name in dApps. See our [CoreDNS plugin](https://github.com/nspcc-dev/coredns/tree/master/plugin/nns) as any other name in dApps. See our [CoreDNS plugin](https://github.com/nspcc-dev/coredns/tree/master/plugin/nns)
for the example of how NNS can be integrated in DNS. for the example of how NNS can be integrated in DNS.
### session ### session
To help lightweight clients interact with FrostFS without sacrificing trust, FrostFS has a concept To help lightweight clients interact with NeoFS without sacrificing trust, NeoFS has a concept
of session token. It is signed by client and allows any node with which a session is established of session token. It is signed by client and allows any node with which a session is established
to perform certain actions on behalf of the user. to perform certain actions on behalf of the user.
### client ### client
Contains client for working with FrostFS. Contains client for working with NeoFS.
```go ```go
var prmInit client.PrmInit c, _ := client.New(
prmInit.SetDefaultPrivateKey(key) // private key for request signing client.WithAddress("localhost:40005"), // endpoint address
prmInit.ResolveFrostFSFailures() // enable erroneous status parsing client.WithDefaultPrivateKey(key), // private key for request signing
client.WithNeoFSErrorHandling(), // enable erroneous status parsing
var c client.Client client.WithTLSConfig(&tls.Config{})) // custom TLS configuration
c.Init(prmInit)
var prmDial client.PrmDial
prmDial.SetServerURI("grpcs://localhost:40005") // endpoint address
err := c.Dial(prmDial)
if err != nil {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5 * time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5 * time.Second)
defer cancel() defer cancel()
var prm client.PrmBalanceGet res, err := c.BalanceGet(ctx, owner)
prm.SetAccount(acc)
res, err := c.BalanceGet(ctx, prm)
if err != nil { if err != nil {
return return
} }
fmt.Printf("Balance for %s: %v\n", acc, res.Amount()) fmt.Printf("Balance for %s: %s\n", owner, res.Amount())
``` ```
#### Response status #### Response status
In FrostFS every operation can fail on multiple levels, so a single `error` doesn't suffice, In NeoFS every operation can fail on multiple levels, so a single `error` doesn't suffice,
e.g. consider a case when object was put on 4 out of 5 replicas. Thus, all request execution e.g. consider a case when object was put on 4 out of 5 replicas. Thus, all request execution
details are contained in `Status` returned from every RPC call. dApp can inspect them details are contained in `Status` returned from every RPC call. dApp can inspect them
if needed and perform any desired action. In the case above we may want to report if needed and perform any desired action. In the case above we may want to report
these details to the user as well as retry an operation, possibly with different parameters. these details to the user as well as retry an operation, possibly with different parameters.
Status wire-format is extendable and each node can report any set of details it wants. Status wire-format is extendable and each node can report any set of details it wants.
The set of reserved status codes can be found in The set of reserved status codes can be found in
[FrostFS API](https://git.frostfs.info/TrueCloudLab/frostfs-api/src/branch/master/status/types.proto). There is also [NeoFS API](https://github.com/nspcc-dev/neofs-api/blob/master/status/types.proto). There is also
a `client.PrmInit.ResolveFrostFSFailures()` to seamlessly convert erroneous statuses into Go error type. a `client.WithNeoFSErrorHandling()` to seamlessly convert erroneous statuses into Go error type.
### policy ### policy
Contains helpers allowing conversion of placing policy from/to JSON representation Contains helpers allowing conversion of placing policy from/to JSON representation
@ -102,12 +90,12 @@ outdated in some details.
```go ```go
import ( import (
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap" "github.com/nspcc-dev/neofs-sdk-go/netmap"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object" "github.com/nspcc-dev/neofs-sdk-go/object"
) )
func placementNodes(addr *object.Address, p *netmap.PlacementPolicy, frostfsNodes []netmap.NodeInfo) { func placementNodes(addr *object.Address, p *netmap.PlacementPolicy, neofsNodes []netmap.NodeInfo) {
// Convert list of nodes in FrostFS API format to the intermediate representation. // Convert list of nodes in NeoFS API format to the intermediate representation.
nodes := netmap.NodesFromInfo(nodes) nodes := netmap.NodesFromInfo(nodes)
// Create new netmap (errors are skipped for the sake of clarity). // Create new netmap (errors are skipped for the sake of clarity).
@ -122,13 +110,13 @@ func placementNodes(addr *object.Address, p *netmap.PlacementPolicy, frostfsNode
``` ```
### pool ### pool
Simple pool for managing connections to FrostFS nodes. Simple pool for managing connections to NeoFS nodes.
### acl, checksum, version, signature ### acl, checksum, version, signature
Contain simple API wrappers. Contain simple API wrappers.
### logger ### logger
Wrapper over `zap.Logger` which is used across FrostFS codebase. Wrapper over `zap.Logger` which is used across NeoFS codebase.
### util ### util
Utilities for working with signature-related code. Utilities for working with signature-related code.

View file

@ -1,64 +1,69 @@
package accounting package accounting
import "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/accounting" import "github.com/nspcc-dev/neofs-api-go/v2/accounting"
// Decimal represents decimal number for accounting operations. // Decimal represents decimal number for accounting operations.
//
// Decimal is mutually compatible with git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/accounting.Decimal
// message. See ReadFromV2 / WriteToV2 methods.
//
// Instances can be created using built-in var declaration.
//
// Note that direct typecast is not safe and may result in loss of compatibility:
//
// _ = Decimal(accounting.Decimal{}) // not recommended
type Decimal accounting.Decimal type Decimal accounting.Decimal
// ReadFromV2 reads Decimal from the accounting.Decimal message. Checks if the // NewDecimal creates, initializes and returns empty Decimal instance.
// message conforms to FrostFS API V2 protocol.
// //
// See also WriteToV2. // Defaults:
func (d *Decimal) ReadFromV2(m accounting.Decimal) error { // - value: 0
*d = Decimal(m) // - precision: 0
return nil func NewDecimal() *Decimal {
return NewDecimalFromV2(new(accounting.Decimal))
} }
// WriteToV2 writes Decimal to the accounting.Decimal message. // NewDecimalFromV2 converts v2 Decimal to Decimal.
// The message must not be nil.
// //
// See also ReadFromV2. // Nil Decimal converts to nil.
func (d Decimal) WriteToV2(m *accounting.Decimal) { func NewDecimalFromV2(d *accounting.Decimal) *Decimal {
*m = (accounting.Decimal)(d) return (*Decimal)(d)
}
// ToV2 returns the v2 Decimal message.
//
// Nil Decimal converts to nil.
func (d *Decimal) ToV2() *accounting.Decimal {
return (*accounting.Decimal)(d)
} }
// Value returns value of the decimal number. // Value returns value of the decimal number.
// func (d *Decimal) Value() int64 {
// Zero Decimal has zero value. return (*accounting.Decimal)(d).GetValue()
//
// See also SetValue.
func (d Decimal) Value() int64 {
return (*accounting.Decimal)(&d).GetValue()
} }
// SetValue sets value of the decimal number. // SetValue sets value of the decimal number.
//
// See also Value.
func (d *Decimal) SetValue(v int64) { func (d *Decimal) SetValue(v int64) {
(*accounting.Decimal)(d).SetValue(v) (*accounting.Decimal)(d).SetValue(v)
} }
// Precision returns precision of the decimal number. // Precision returns precision of the decimal number.
// func (d *Decimal) Precision() uint32 {
// Zero Decimal has zero precision. return (*accounting.Decimal)(d).GetPrecision()
//
// See also SetPrecision.
func (d Decimal) Precision() uint32 {
return (*accounting.Decimal)(&d).GetPrecision()
} }
// SetPrecision sets precision of the decimal number. // SetPrecision sets precision of the decimal number.
//
// See also Precision.
func (d *Decimal) SetPrecision(p uint32) { func (d *Decimal) SetPrecision(p uint32) {
(*accounting.Decimal)(d).SetPrecision(p) (*accounting.Decimal)(d).SetPrecision(p)
} }
// Marshal marshals Decimal into a protobuf binary form.
func (d *Decimal) Marshal() ([]byte, error) {
return (*accounting.Decimal)(d).StableMarshal(nil)
}
// Unmarshal unmarshalls protobuf binary representation of Decimal.
func (d *Decimal) Unmarshal(data []byte) error {
return (*accounting.Decimal)(d).Unmarshal(data)
}
// MarshalJSON encodes Decimal to protobuf JSON format.
func (d *Decimal) MarshalJSON() ([]byte, error) {
return (*accounting.Decimal)(d).MarshalJSON()
}
// UnmarshalJSON decodes Decimal from protobuf JSON format.
func (d *Decimal) UnmarshalJSON(data []byte) error {
return (*accounting.Decimal)(d).UnmarshalJSON(data)
}

View file

@ -3,19 +3,15 @@ package accounting_test
import ( import (
"testing" "testing"
v2accounting "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/accounting" "github.com/nspcc-dev/neofs-sdk-go/accounting"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/accounting" accountingtest "github.com/nspcc-dev/neofs-sdk-go/accounting/test"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestDecimalData(t *testing.T) { func TestDecimal(t *testing.T) {
const v, p = 4, 2 const v, p = 4, 2
var d accounting.Decimal d := accounting.NewDecimal()
require.Zero(t, d.Value())
require.Zero(t, d.Precision())
d.SetValue(v) d.SetValue(v)
d.SetPrecision(p) d.SetPrecision(p)
@ -23,24 +19,26 @@ func TestDecimalData(t *testing.T) {
require.EqualValues(t, p, d.Precision()) require.EqualValues(t, p, d.Precision())
} }
func TestDecimalMessageV2(t *testing.T) { func TestDecimalEncoding(t *testing.T) {
var ( d := accountingtest.Decimal()
d accounting.Decimal
m v2accounting.Decimal
)
m.SetValue(7) t.Run("binary", func(t *testing.T) {
m.SetPrecision(8) data, err := d.Marshal()
require.NoError(t, err)
require.NoError(t, d.ReadFromV2(m)) d2 := accounting.NewDecimal()
require.NoError(t, d2.Unmarshal(data))
require.EqualValues(t, m.GetValue(), d.Value()) require.Equal(t, d, d2)
require.EqualValues(t, m.GetPrecision(), d.Precision()) })
var m2 v2accounting.Decimal t.Run("json", func(t *testing.T) {
data, err := d.MarshalJSON()
require.NoError(t, err)
d.WriteToV2(&m2) d2 := accounting.NewDecimal()
require.NoError(t, d2.UnmarshalJSON(data))
require.EqualValues(t, d.Value(), m2.GetValue()) require.Equal(t, d, d2)
require.EqualValues(t, d.Precision(), m2.GetPrecision()) })
} }

View file

@ -1,35 +0,0 @@
/*
Package accounting provides primitives to perform accounting operations in FrostFS.
Decimal type provides functionality to process user balances. For example, when
working with Fixed8 balance precision:
var dec accounting.Decimal
dec.SetValue(val)
dec.SetPrecision(8)
Instances can be also used to process FrostFS API V2 protocol messages
(see neo.fs.v2.accounting package in https://git.frostfs.info/TrueCloudLab/frostfs-api).
On client side:
import "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/accounting"
var msg accounting.Decimal
dec.WriteToV2(&msg)
// send msg
On server side:
// recv msg
var dec accounting.Decimal
dec.ReadFromV2(msg)
// process dec
Using package types in an application is recommended to potentially work with
different protocol versions with which these types are compatible.
*/
package accounting

View file

@ -3,14 +3,14 @@ package accountingtest
import ( import (
"math/rand" "math/rand"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/accounting" "github.com/nspcc-dev/neofs-sdk-go/accounting"
) )
// Decimal returns random accounting.Decimal. // Decimal returns random accounting.Decimal.
func Decimal() *accounting.Decimal { func Decimal() *accounting.Decimal {
var d accounting.Decimal d := accounting.NewDecimal()
d.SetValue(rand.Int63()) d.SetValue(rand.Int63())
d.SetPrecision(rand.Uint32()) d.SetPrecision(rand.Uint32())
return &d return d
} }

View file

@ -1,13 +0,0 @@
/*
Package accountingtest provides functions for convenient testing of accounting 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 accountingtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/accounting/test"
dec := accountingtest.Decimal()
// test the value
*/
package accountingtest

105
acl/types.go Normal file
View file

@ -0,0 +1,105 @@
package acl
import (
"fmt"
"strconv"
"strings"
)
// BasicACL is Access Control List that defines who can interact with containers and what exactly they can do.
type BasicACL uint32
func (a BasicACL) String() string {
return fmt.Sprintf("0x%08x", uint32(a))
}
const (
// PublicBasicRule is a basic ACL value for final public-read-write container for which extended ACL CANNOT be set.
PublicBasicRule BasicACL = 0x1FBFBFFF
// PrivateBasicRule is a basic ACL value for final private container for which extended ACL CANNOT be set.
PrivateBasicRule BasicACL = 0x1C8C8CCC
// ReadOnlyBasicRule is a basic ACL value for final public-read container for which extended ACL CANNOT be set.
ReadOnlyBasicRule BasicACL = 0x1FBF8CFF
// PublicAppendRule is a basic ACL value for final public-append container for which extended ACL CANNOT be set.
PublicAppendRule BasicACL = 0x1FBF9FFF
// EACLPublicBasicRule is a basic ACL value for non-final public-read-write container for which extended ACL CAN be set.
EACLPublicBasicRule BasicACL = 0x0FBFBFFF
// EACLPrivateBasicRule is a basic ACL value for non-final private container for which extended ACL CAN be set.
EACLPrivateBasicRule BasicACL = 0x0C8C8CCC
// EACLReadOnlyBasicRule is a basic ACL value for non-final public-read container for which extended ACL CAN be set.
EACLReadOnlyBasicRule BasicACL = 0x0FBF8CFF
// EACLPublicAppendRule is a basic ACL value for non-final public-append container for which extended ACL CAN be set.
EACLPublicAppendRule BasicACL = 0x0FBF9FFF
)
const (
// PublicBasicName is a well-known name for 0x1FBFBFFF basic ACL.
// It represents fully-public container without eACL.
PublicBasicName = "public-read-write"
// PrivateBasicName is a well-known name for 0x1C8C8CCC basic ACL.
// It represents fully-private container without eACL.
PrivateBasicName = "private"
// ReadOnlyBasicName is a well-known name for 0x1FBF8CFF basic ACL.
// It represents public read-only container without eACL.
ReadOnlyBasicName = "public-read"
// PublicAppendName is a well-known name for 0x1FBF9FFF basic ACL.
// It represents fully-public container without eACL except DELETE operation is only allowed on the owner.
PublicAppendName = "public-append"
// EACLPublicBasicName is a well-known name for 0x0FBFBFFF basic ACL.
// It represents fully-public container that allows eACL.
EACLPublicBasicName = "eacl-public-read-write"
// EACLPrivateBasicName is a well-known name for 0x0C8C8CCC basic ACL.
// It represents fully-private container that allows eACL.
EACLPrivateBasicName = "eacl-private"
// EACLReadOnlyBasicName is a well-known name for 0x0FBF8CFF basic ACL.
// It represents public read-only container that allows eACL.
EACLReadOnlyBasicName = "eacl-public-read"
// EACLPublicAppendName is a well-known name for 0x0FBF9FFF basic ACL.
// It represents fully-public container that allows eACL except DELETE operation is only allowed on the owner.
EACLPublicAppendName = "eacl-public-append"
)
// ParseBasicACL parse string ACL (well-known names or hex representation).
func ParseBasicACL(basicACL string) (BasicACL, error) {
switch basicACL {
case PublicBasicName:
return PublicBasicRule, nil
case PrivateBasicName:
return PrivateBasicRule, nil
case ReadOnlyBasicName:
return ReadOnlyBasicRule, nil
case PublicAppendName:
return PublicAppendRule, nil
case EACLPublicBasicName:
return EACLPublicBasicRule, nil
case EACLPrivateBasicName:
return EACLPrivateBasicRule, nil
case EACLReadOnlyBasicName:
return EACLReadOnlyBasicRule, nil
case EACLPublicAppendName:
return EACLPublicAppendRule, nil
default:
basicACL = strings.TrimPrefix(strings.ToLower(basicACL), "0x")
value, err := strconv.ParseUint(basicACL, 16, 32)
if err != nil {
return 0, fmt.Errorf("can't parse basic ACL: %s", basicACL)
}
return BasicACL(value), nil
}
}

82
acl/types_test.go Normal file
View file

@ -0,0 +1,82 @@
package acl
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestParser(t *testing.T) {
for _, tc := range []struct {
acl string
expected BasicACL
err bool
}{
{
acl: PublicBasicName,
expected: PublicBasicRule,
},
{
acl: PrivateBasicName,
expected: PrivateBasicRule,
},
{
acl: ReadOnlyBasicName,
expected: ReadOnlyBasicRule,
},
{
acl: PublicAppendName,
expected: PublicAppendRule,
},
{
acl: EACLPublicBasicName,
expected: EACLPublicBasicRule,
},
{
acl: EACLPrivateBasicName,
expected: EACLPrivateBasicRule,
},
{
acl: EACLReadOnlyBasicName,
expected: EACLReadOnlyBasicRule,
},
{
acl: EACLPublicAppendName,
expected: EACLPublicAppendRule,
},
{
acl: "0x1C8C8CCC",
expected: 0x1C8C8CCC,
},
{
acl: "1C8C8CCC",
expected: 0x1C8C8CCC,
},
{
acl: "123456789",
err: true,
},
{
acl: "0x1C8C8CCG",
err: true,
},
} {
actual, err := ParseBasicACL(tc.acl)
if tc.err {
require.Error(t, err)
continue
}
require.NoError(t, err)
require.Equal(t, tc.expected, actual)
}
}
func TestString(t *testing.T) {
acl := BasicACL(0x1fbfbfff)
require.Equal(t, "0x1fbfbfff", acl.String())
acl2, err := ParseBasicACL(PrivateBasicName)
require.NoError(t, err)
require.Equal(t, "0x1c8c8ccc", acl2.String())
}

View file

@ -1,26 +0,0 @@
/*
Package audit provides features to process data audit in FrostFS system.
Result type groups values which can be gathered during data audit process:
var res audit.Result
res.ForEpoch(32)
res.ForContainer(cnr)
// ...
res.Complete()
Result instances can be stored in a binary format. On reporter side:
data := res.Marshal()
// send data
On receiver side:
var res audit.Result
err := res.Unmarshal(data)
// ...
Using package types in an application is recommended to potentially work with
different protocol versions with which these types are compatible.
*/
package audit

View file

@ -1,377 +1,291 @@
package audit package audit
import ( import (
"errors" "github.com/nspcc-dev/neofs-api-go/v2/audit"
"fmt" "github.com/nspcc-dev/neofs-api-go/v2/refs"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/audit" oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs" "github.com/nspcc-dev/neofs-sdk-go/version"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/version"
) )
// Result represents report on the results of the data audit in FrostFS system. // Result represents v2-compatible data audit result.
// type Result audit.DataAuditResult
// Result is mutually binary-compatible with git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/audit.DataAuditResult
// message. See Marshal / Unmarshal methods.
//
// Instances can be created using built-in var declaration.
type Result struct {
versionEncoded bool
v2 audit.DataAuditResult // NewFromV2 wraps v2 DataAuditResult message to Result.
//
// Nil audit.DataAuditResult converts to nil.
func NewResultFromV2(aV2 *audit.DataAuditResult) *Result {
return (*Result)(aV2)
} }
// Marshal encodes Result into a canonical FrostFS binary format (Protocol Buffers // New creates and initializes blank Result.
// with direct field order).
// //
// Writes version.Current() protocol version into the resulting message if Result // Defaults:
// hasn't been already decoded from such a message using Unmarshal. // - version: version.Current();
// // - complete: false;
// See also Unmarshal. // - cid: nil;
func (r *Result) Marshal() []byte { // - pubKey: nil;
if !r.versionEncoded { // - passSG, failSG: nil;
var verV2 refs.Version // - failNodes, passNodes: nil;
version.Current().WriteToV2(&verV2) // - hit, miss, fail: 0;
r.v2.SetVersion(&verV2) // - requests, retries: 0;
r.versionEncoded = true // - auditEpoch: 0.
} func NewResult() *Result {
r := NewResultFromV2(new(audit.DataAuditResult))
r.SetVersion(version.Current())
return r.v2.StableMarshal(nil) return r
} }
var errCIDNotSet = errors.New("container ID is not set") // ToV2 converts Result to v2 DataAuditResult message.
// Unmarshal decodes Result from its canonical FrostFS binary format (Protocol Buffers
// with direct field order). Returns an error describing a format violation.
// //
// See also Marshal. // Nil Result converts to nil.
func (r *Result) ToV2() *audit.DataAuditResult {
return (*audit.DataAuditResult)(r)
}
// Marshal marshals Result into a protobuf binary form.
func (r *Result) Marshal() ([]byte, error) {
return (*audit.DataAuditResult)(r).StableMarshal(nil)
}
// Unmarshal unmarshals protobuf binary representation of Result.
func (r *Result) Unmarshal(data []byte) error { func (r *Result) Unmarshal(data []byte) error {
err := r.v2.Unmarshal(data) return (*audit.DataAuditResult)(r).Unmarshal(data)
if err != nil { }
return err
}
r.versionEncoded = true // MarshalJSON encodes Result to protobuf JSON format.
func (r *Result) MarshalJSON() ([]byte, error) {
return (*audit.DataAuditResult)(r).MarshalJSON()
}
// format checks // UnmarshalJSON decodes Result from protobuf JSON format.
func (r *Result) UnmarshalJSON(data []byte) error {
return (*audit.DataAuditResult)(r).UnmarshalJSON(data)
}
var cID cid.ID // Version returns Data Audit structure version.
func (r *Result) Version() *version.Version {
return version.NewFromV2(
(*audit.DataAuditResult)(r).GetVersion())
}
cidV2 := r.v2.GetContainerID() // SetVersion sets Data Audit structure version.
if cidV2 == nil { func (r *Result) SetVersion(v *version.Version) {
return errCIDNotSet (*audit.DataAuditResult)(r).SetVersion(v.ToV2())
} }
err = cID.ReadFromV2(*cidV2) // AuditEpoch returns epoch number when the Data Audit was conducted.
if err != nil { func (r *Result) AuditEpoch() uint64 {
return fmt.Errorf("could not convert V2 container ID: %w", err) return (*audit.DataAuditResult)(r).GetAuditEpoch()
} }
var ( // SetAuditEpoch sets epoch number when the Data Audit was conducted.
oID oid.ID func (r *Result) SetAuditEpoch(epoch uint64) {
oidV2 refs.ObjectID (*audit.DataAuditResult)(r).SetAuditEpoch(epoch)
) }
for _, oidV2 = range r.v2.GetPassSG() { // ContainerID returns container under audit.
err = oID.ReadFromV2(oidV2) func (r *Result) ContainerID() *cid.ID {
if err != nil { return cid.NewFromV2(
return fmt.Errorf("invalid passed storage group ID: %w", err) (*audit.DataAuditResult)(r).GetContainerID())
} }
}
for _, oidV2 = range r.v2.GetFailSG() { // SetContainerID sets container under audit.
err = oID.ReadFromV2(oidV2) func (r *Result) SetContainerID(id *cid.ID) {
if err != nil { (*audit.DataAuditResult)(r).SetContainerID(id.ToV2())
return fmt.Errorf("invalid failed storage group ID: %w", err) }
}
}
// PublicKey returns public key of the auditing InnerRing node in a binary format.
func (r *Result) PublicKey() []byte {
return (*audit.DataAuditResult)(r).GetPublicKey()
}
// SetPublicKey sets public key of the auditing InnerRing node in a binary format.
func (r *Result) SetPublicKey(key []byte) {
(*audit.DataAuditResult)(r).SetPublicKey(key)
}
// Complete returns completion state of audit result.
func (r *Result) Complete() bool {
return (*audit.DataAuditResult)(r).GetComplete()
}
// SetComplete sets completion state of audit result.
func (r *Result) SetComplete(v bool) {
(*audit.DataAuditResult)(r).SetComplete(v)
}
// Requests returns number of requests made by PoR audit check to get
// all headers of the objects inside storage groups.
func (r *Result) Requests() uint32 {
return (*audit.DataAuditResult)(r).GetRequests()
}
// SetRequests sets number of requests made by PoR audit check to get
// all headers of the objects inside storage groups.
func (r *Result) SetRequests(v uint32) {
(*audit.DataAuditResult)(r).SetRequests(v)
}
// Retries returns number of retries made by PoR audit check to get
// all headers of the objects inside storage groups.
func (r *Result) Retries() uint32 {
return (*audit.DataAuditResult)(r).GetRetries()
}
// SetRetries sets number of retries made by PoR audit check to get
// all headers of the objects inside storage groups.
func (r *Result) SetRetries(v uint32) {
(*audit.DataAuditResult)(r).SetRetries(v)
}
// PassSG returns list of Storage Groups that passed audit PoR stage.
func (r *Result) PassSG() []oid.ID {
mV2 := (*audit.DataAuditResult)(r).
GetPassSG()
if mV2 == nil {
return nil return nil
}
// Epoch returns FrostFS epoch when the data associated with the Result was audited.
//
// Zero Result has zero epoch.
//
// See also ForEpoch.
func (r Result) Epoch() uint64 {
return r.v2.GetAuditEpoch()
}
// ForEpoch specifies FrostFS epoch when the data associated with the Result was audited.
//
// See also Epoch.
func (r *Result) ForEpoch(epoch uint64) {
r.v2.SetAuditEpoch(epoch)
}
// Container returns identifier of the container with which the data audit Result
// is associated and a bool that indicates container ID field presence in the Result.
//
// Zero Result does not have container ID.
//
// See also ForContainer.
func (r Result) Container() (cid.ID, bool) {
var cID cid.ID
cidV2 := r.v2.GetContainerID()
if cidV2 != nil {
_ = cID.ReadFromV2(*cidV2)
return cID, true
} }
return cID, false m := make([]oid.ID, len(mV2))
for i := range mV2 {
m[i] = *oid.NewIDFromV2(&mV2[i])
}
return m
} }
// ForContainer sets identifier of the container with which the data audit Result // SetPassSG sets list of Storage Groups that passed audit PoR stage.
// is associated. func (r *Result) SetPassSG(list []oid.ID) {
// mV2 := (*audit.DataAuditResult)(r).
// See also Container. GetPassSG()
func (r *Result) ForContainer(cnr cid.ID) {
var cidV2 refs.ContainerID
cnr.WriteToV2(&cidV2)
r.v2.SetContainerID(&cidV2) if list == nil {
} mV2 = nil
} else {
ln := len(list)
// AuditorKey returns public key of the auditing FrostFS Inner Ring node in if cap(mV2) >= ln {
// a FrostFS binary key format. mV2 = mV2[:0]
// } else {
// Zero Result has nil key. Return value MUST NOT be mutated: to do this, mV2 = make([]refs.ObjectID, ln)
// first make a copy. }
//
// See also SetAuditorPublicKey.
func (r Result) AuditorKey() []byte {
return r.v2.GetPublicKey()
}
// SetAuditorKey specifies public key of the auditing FrostFS Inner Ring node in for i := 0; i < ln; i++ {
// a FrostFS binary key format. mV2[i] = *list[i].ToV2()
//
// Argument MUST NOT be mutated at least until the end of using the Result.
//
// See also AuditorKey.
func (r *Result) SetAuditorKey(key []byte) {
r.v2.SetPublicKey(key)
}
// Completed returns completion state of the data audit associated with the Result.
//
// Zero Result corresponds to incomplete data audit.
//
// See also Complete.
func (r Result) Completed() bool {
return r.v2.GetComplete()
}
// Complete marks the data audit associated with the Result as completed.
//
// See also Completed.
func (r *Result) Complete() {
r.v2.SetComplete(true)
}
// RequestsPoR returns number of requests made by Proof-of-Retrievability
// audit check to get all headers of the objects inside storage groups.
//
// Zero Result has zero requests.
//
// See also SetRequestsPoR.
func (r Result) RequestsPoR() uint32 {
return r.v2.GetRequests()
}
// SetRequestsPoR sets number of requests made by Proof-of-Retrievability
// audit check to get all headers of the objects inside storage groups.
//
// See also RequestsPoR.
func (r *Result) SetRequestsPoR(v uint32) {
r.v2.SetRequests(v)
}
// RetriesPoR returns number of retries made by Proof-of-Retrievability
// audit check to get all headers of the objects inside storage groups.
//
// Zero Result has zero retries.
//
// See also SetRetriesPoR.
func (r Result) RetriesPoR() uint32 {
return r.v2.GetRetries()
}
// SetRetriesPoR sets number of retries made by Proof-of-Retrievability
// audit check to get all headers of the objects inside storage groups.
//
// See also RetriesPoR.
func (r *Result) SetRetriesPoR(v uint32) {
r.v2.SetRetries(v)
}
// IteratePassedStorageGroups iterates over all storage groups that passed
// Proof-of-Retrievability audit check and passes them into f. Breaks on f's
// false return, f MUST NOT be nil.
//
// Zero Result has no passed storage groups and doesn't call f.
//
// See also SubmitPassedStorageGroup.
func (r Result) IteratePassedStorageGroups(f func(oid.ID) bool) {
r2 := r.v2.GetPassSG()
var id oid.ID
for i := range r2 {
_ = id.ReadFromV2(r2[i])
if !f(id) {
return
} }
} }
(*audit.DataAuditResult)(r).SetPassSG(mV2)
} }
// SubmitPassedStorageGroup marks storage group as passed Proof-of-Retrievability // FailSG returns list of Storage Groups that failed audit PoR stage.
// audit check. func (r *Result) FailSG() []oid.ID {
// mV2 := (*audit.DataAuditResult)(r).
// See also IteratePassedStorageGroups. GetFailSG()
func (r *Result) SubmitPassedStorageGroup(sg oid.ID) {
var idV2 refs.ObjectID
sg.WriteToV2(&idV2)
r.v2.SetPassSG(append(r.v2.GetPassSG(), idV2)) if mV2 == nil {
return nil
}
m := make([]oid.ID, len(mV2))
for i := range mV2 {
m[i] = *oid.NewIDFromV2(&mV2[i])
}
return m
} }
// IterateFailedStorageGroups is similar to IteratePassedStorageGroups but for failed groups. // SetFailSG sets list of Storage Groups that failed audit PoR stage.
// func (r *Result) SetFailSG(list []oid.ID) {
// See also SubmitFailedStorageGroup. mV2 := (*audit.DataAuditResult)(r).
func (r Result) IterateFailedStorageGroups(f func(oid.ID) bool) { GetFailSG()
v := r.v2.GetFailSG()
var id oid.ID
for i := range v { if list == nil {
_ = id.ReadFromV2(v[i]) mV2 = nil
if !f(id) { } else {
return ln := len(list)
if cap(mV2) >= ln {
mV2 = mV2[:0]
} else {
mV2 = make([]refs.ObjectID, ln)
}
for i := 0; i < ln; i++ {
mV2[i] = *list[i].ToV2()
} }
} }
(*audit.DataAuditResult)(r).SetFailSG(mV2)
} }
// SubmitFailedStorageGroup is similar to SubmitPassedStorageGroup but for failed groups. // Hit returns number of sampled objects under audit placed
//
// See also IterateFailedStorageGroups.
func (r *Result) SubmitFailedStorageGroup(sg oid.ID) {
var idV2 refs.ObjectID
sg.WriteToV2(&idV2)
r.v2.SetFailSG(append(r.v2.GetFailSG(), idV2))
}
// Hits returns number of sampled objects under audit placed
// in an optimal way according to the container's placement policy
// when checking Proof-of-Placement.
//
// Zero result has zero hits.
//
// See also SetHits.
func (r Result) Hits() uint32 {
return r.v2.GetHit()
}
// SetHits sets number of sampled objects under audit placed
// in an optimal way according to the containers placement policy // in an optimal way according to the containers placement policy
// when checking Proof-of-Placement. // when checking PoP.
// func (r *Result) Hit() uint32 {
// See also Hits. return (*audit.DataAuditResult)(r).GetHit()
func (r *Result) SetHits(hit uint32) {
r.v2.SetHit(hit)
} }
// Misses returns number of sampled objects under audit placed // SetHit sets number of sampled objects under audit placed
// in suboptimal way according to the container's placement policy, // in an optimal way according to the containers placement policy
// but still at a satisfactory level when checking Proof-of-Placement. // when checking PoP.
// func (r *Result) SetHit(hit uint32) {
// Zero Result has zero misses. (*audit.DataAuditResult)(r).SetHit(hit)
//
// See also SetMisses.
func (r Result) Misses() uint32 {
return r.v2.GetMiss()
} }
// SetMisses sets number of sampled objects under audit placed // Miss returns number of sampled objects under audit placed
// in suboptimal way according to the container's placement policy, // in suboptimal way according to the containers placement policy,
// but still at a satisfactory level when checking Proof-of-Placement. // but still at a satisfactory level when checking PoP.
// func (r *Result) Miss() uint32 {
// See also Misses. return (*audit.DataAuditResult)(r).GetMiss()
func (r *Result) SetMisses(miss uint32) {
r.v2.SetMiss(miss)
} }
// Failures returns number of sampled objects under audit stored // SetMiss sets number of sampled objects under audit placed
// in suboptimal way according to the containers placement policy,
// but still at a satisfactory level when checking PoP.
func (r *Result) SetMiss(miss uint32) {
(*audit.DataAuditResult)(r).SetMiss(miss)
}
// Fail returns number of sampled objects under audit stored
// in a way not confirming placement policy or not found at all // in a way not confirming placement policy or not found at all
// when checking Proof-of-Placement. // when checking PoP.
// func (r *Result) Fail() uint32 {
// Zero result has zero failures. return (*audit.DataAuditResult)(r).GetFail()
//
// See also SetFailures.
func (r Result) Failures() uint32 {
return r.v2.GetFail()
} }
// SetFailures sets number of sampled objects under audit stored // SetFail sets number of sampled objects under audit stored
// in a way not confirming placement policy or not found at all // in a way not confirming placement policy or not found at all
// when checking Proof-of-Placement. // when checking PoP.
// func (r *Result) SetFail(fail uint32) {
// See also Failures. (*audit.DataAuditResult)(r).SetFail(fail)
func (r *Result) SetFailures(fail uint32) {
r.v2.SetFail(fail)
} }
// IteratePassedStorageNodes iterates over all storage nodes that passed at least one // PassNodes returns list of storage node public keys that
// Proof-of-Data-Possession audit check and passes their public keys into f. Breaks on // passed at least one PDP.
// f's false return. func (r *Result) PassNodes() [][]byte {
// return (*audit.DataAuditResult)(r).GetPassNodes()
// f MUST NOT be nil and MUST NOT mutate parameter passed into it at least until
// the end of using the Result.
//
// Zero Result has no passed storage nodes and doesn't call f.
//
// See also SubmitPassedStorageNode.
func (r Result) IteratePassedStorageNodes(f func([]byte) bool) {
v := r.v2.GetPassNodes()
for i := range v {
if !f(v[i]) {
return
}
}
} }
// SubmitPassedStorageNodes marks storage node list as passed Proof-of-Data-Possession // SetPassNodes sets list of storage node public keys that
// audit check. The list contains public keys. // passed at least one PDP.
// func (r *Result) SetPassNodes(list [][]byte) {
// Argument and its elements MUST NOT be mutated at least until the end of using the Result. (*audit.DataAuditResult)(r).SetPassNodes(list)
//
// See also IteratePassedStorageNodes.
func (r *Result) SubmitPassedStorageNodes(list [][]byte) {
r.v2.SetPassNodes(list)
} }
// IterateFailedStorageNodes is similar to IteratePassedStorageNodes but for failed nodes. // FailNodes returns list of storage node public keys that
// // failed at least one PDP.
// See also SubmitPassedStorageNodes. func (r *Result) FailNodes() [][]byte {
func (r Result) IterateFailedStorageNodes(f func([]byte) bool) { return (*audit.DataAuditResult)(r).GetFailNodes()
v := r.v2.GetFailNodes()
for i := range v {
if !f(v[i]) {
return
}
}
} }
// SubmitFailedStorageNodes is similar to SubmitPassedStorageNodes but for failed nodes. // SetFailNodes sets list of storage node public keys that
// // failed at least one PDP.
// See also IterateFailedStorageNodes. func (r *Result) SetFailNodes(list [][]byte) {
func (r *Result) SubmitFailedStorageNodes(list [][]byte) { (*audit.DataAuditResult)(r).SetFailNodes(list)
r.v2.SetFailNodes(list)
} }

View file

@ -1,191 +1,154 @@
package audit_test package audit_test
import ( import (
"bytes"
"testing" "testing"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/audit" auditv2 "github.com/nspcc-dev/neofs-api-go/v2/audit"
audittest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/audit/test" "github.com/nspcc-dev/neofs-sdk-go/audit"
cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test" audittest "github.com/nspcc-dev/neofs-sdk-go/audit/test"
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test"
oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test" oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
oidtest "github.com/nspcc-dev/neofs-sdk-go/object/id/test"
"github.com/nspcc-dev/neofs-sdk-go/version"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestResultData(t *testing.T) { func TestResult(t *testing.T) {
var r audit.Result r := audit.NewResult()
require.Equal(t, version.Current(), r.Version())
countSG := func(passed bool, f func(oid.ID)) int {
called := 0
ff := func(arg oid.ID) bool {
called++
if f != nil {
f(arg)
}
return true
}
if passed {
r.IteratePassedStorageGroups(ff)
} else {
r.IterateFailedStorageGroups(ff)
}
return called
}
countPassSG := func(f func(oid.ID)) int { return countSG(true, f) }
countFailSG := func(f func(oid.ID)) int { return countSG(false, f) }
countNodes := func(passed bool, f func([]byte)) int {
called := 0
ff := func(arg []byte) bool {
called++
if f != nil {
f(arg)
}
return true
}
if passed {
r.IteratePassedStorageNodes(ff)
} else {
r.IterateFailedStorageNodes(ff)
}
return called
}
countPassNodes := func(f func([]byte)) int { return countNodes(true, f) }
countFailNodes := func(f func([]byte)) int { return countNodes(false, f) }
require.Zero(t, r.Epoch())
_, set := r.Container()
require.False(t, set)
require.Nil(t, r.AuditorKey())
require.False(t, r.Completed())
require.Zero(t, r.RequestsPoR())
require.Zero(t, r.RetriesPoR())
require.Zero(t, countPassSG(nil))
require.Zero(t, countFailSG(nil))
require.Zero(t, countPassNodes(nil))
require.Zero(t, countFailNodes(nil))
epoch := uint64(13) epoch := uint64(13)
r.ForEpoch(epoch) r.SetAuditEpoch(epoch)
require.Equal(t, epoch, r.Epoch()) require.Equal(t, epoch, r.AuditEpoch())
cnr := cidtest.ID() cid := cidtest.ID()
r.ForContainer(cnr) r.SetContainerID(cid)
cID, set := r.Container() require.Equal(t, cid, r.ContainerID())
require.True(t, set)
require.Equal(t, cnr, cID)
key := []byte{1, 2, 3} key := []byte{1, 2, 3}
r.SetAuditorKey(key) r.SetPublicKey(key)
require.Equal(t, key, r.AuditorKey()) require.Equal(t, key, r.PublicKey())
r.Complete() r.SetComplete(true)
require.True(t, r.Completed()) require.True(t, r.Complete())
requests := uint32(2) requests := uint32(2)
r.SetRequestsPoR(requests) r.SetRequests(requests)
require.Equal(t, requests, r.RequestsPoR()) require.Equal(t, requests, r.Requests())
retries := uint32(1) retries := uint32(1)
r.SetRetriesPoR(retries) r.SetRetries(retries)
require.Equal(t, retries, r.RetriesPoR()) require.Equal(t, retries, r.Retries())
passSG1, passSG2 := oidtest.ID(), oidtest.ID() passSG := []oid.ID{*oidtest.ID(), *oidtest.ID()}
r.SubmitPassedStorageGroup(passSG1) r.SetPassSG(passSG)
r.SubmitPassedStorageGroup(passSG2) require.Equal(t, passSG, r.PassSG())
called1, called2 := false, false failSG := []oid.ID{*oidtest.ID(), *oidtest.ID()}
r.SetFailSG(failSG)
require.EqualValues(t, 2, countPassSG(func(id oid.ID) { require.Equal(t, failSG, r.FailSG())
if id.Equals(passSG1) {
called1 = true
} else if id.Equals(passSG2) {
called2 = true
}
}))
require.True(t, called1)
require.True(t, called2)
failSG1, failSG2 := oidtest.ID(), oidtest.ID()
r.SubmitFailedStorageGroup(failSG1)
r.SubmitFailedStorageGroup(failSG2)
called1, called2 = false, false
require.EqualValues(t, 2, countFailSG(func(id oid.ID) {
if id.Equals(failSG1) {
called1 = true
} else if id.Equals(failSG2) {
called2 = true
}
}))
require.True(t, called1)
require.True(t, called2)
hit := uint32(1) hit := uint32(1)
r.SetHits(hit) r.SetHit(hit)
require.Equal(t, hit, r.Hits()) require.Equal(t, hit, r.Hit())
miss := uint32(2) miss := uint32(2)
r.SetMisses(miss) r.SetMiss(miss)
require.Equal(t, miss, r.Misses()) require.Equal(t, miss, r.Miss())
fail := uint32(3) fail := uint32(3)
r.SetFailures(fail) r.SetFail(fail)
require.Equal(t, fail, r.Failures()) require.Equal(t, fail, r.Fail())
passNodes := [][]byte{{1}, {2}} passNodes := [][]byte{{1}, {2}}
r.SubmitPassedStorageNodes(passNodes) r.SetPassNodes(passNodes)
require.Equal(t, passNodes, r.PassNodes())
called1, called2 = false, false
require.EqualValues(t, 2, countPassNodes(func(arg []byte) {
if bytes.Equal(arg, passNodes[0]) {
called1 = true
} else if bytes.Equal(arg, passNodes[1]) {
called2 = true
}
}))
require.True(t, called1)
require.True(t, called2)
failNodes := [][]byte{{3}, {4}} failNodes := [][]byte{{3}, {4}}
r.SubmitFailedStorageNodes(failNodes) r.SetFailNodes(failNodes)
require.Equal(t, failNodes, r.FailNodes())
called1, called2 = false, false
require.EqualValues(t, 2, countFailNodes(func(arg []byte) {
if bytes.Equal(arg, failNodes[0]) {
called1 = true
} else if bytes.Equal(arg, failNodes[1]) {
called2 = true
}
}))
require.True(t, called1)
require.True(t, called2)
} }
func TestResultEncoding(t *testing.T) { func TestStorageGroupEncoding(t *testing.T) {
r := *audittest.Result() r := audittest.Result()
t.Run("binary", func(t *testing.T) { t.Run("binary", func(t *testing.T) {
data := r.Marshal() data, err := r.Marshal()
require.NoError(t, err)
var r2 audit.Result r2 := audit.NewResult()
require.NoError(t, r2.Unmarshal(data)) require.NoError(t, r2.Unmarshal(data))
require.Equal(t, r, r2) require.Equal(t, r, r2)
}) })
t.Run("json", func(t *testing.T) {
data, err := r.MarshalJSON()
require.NoError(t, err)
r2 := audit.NewResult()
require.NoError(t, r2.UnmarshalJSON(data))
require.Equal(t, r, r2)
})
}
func TestResult_ToV2(t *testing.T) {
t.Run("nil", func(t *testing.T) {
var x *audit.Result
require.Nil(t, x.ToV2())
})
t.Run("default values", func(t *testing.T) {
result := audit.NewResult()
// check initial values
require.Equal(t, version.Current(), result.Version())
require.False(t, result.Complete())
require.Nil(t, result.ContainerID())
require.Nil(t, result.PublicKey())
require.Nil(t, result.PassSG())
require.Nil(t, result.FailSG())
require.Nil(t, result.PassNodes())
require.Nil(t, result.FailNodes())
require.Zero(t, result.Hit())
require.Zero(t, result.Miss())
require.Zero(t, result.Fail())
require.Zero(t, result.Requests())
require.Zero(t, result.Retries())
require.Zero(t, result.AuditEpoch())
// convert to v2 message
resultV2 := result.ToV2()
require.Equal(t, version.Current().ToV2(), resultV2.GetVersion())
require.False(t, resultV2.GetComplete())
require.Nil(t, resultV2.GetContainerID())
require.Nil(t, resultV2.GetPublicKey())
require.Nil(t, resultV2.GetPassSG())
require.Nil(t, resultV2.GetFailSG())
require.Nil(t, resultV2.GetPassNodes())
require.Nil(t, resultV2.GetFailNodes())
require.Zero(t, resultV2.GetHit())
require.Zero(t, resultV2.GetMiss())
require.Zero(t, resultV2.GetFail())
require.Zero(t, resultV2.GetRequests())
require.Zero(t, resultV2.GetRetries())
require.Zero(t, resultV2.GetAuditEpoch())
})
}
func TestNewResultFromV2(t *testing.T) {
t.Run("from nil", func(t *testing.T) {
var x *auditv2.DataAuditResult
require.Nil(t, audit.NewResultFromV2(x))
})
} }

View file

@ -1,13 +0,0 @@
/*
Package audittest provides functions for convenient testing of audit 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 audittest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/audit/test"
dec := audittest.Result()
// test the value
*/
package audittest

View file

@ -1,36 +1,37 @@
package audittest package audittest
import ( import (
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/audit" "github.com/nspcc-dev/neofs-sdk-go/audit"
cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test" cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test"
oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test" oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
oidtest "github.com/nspcc-dev/neofs-sdk-go/object/id/test"
versiontest "github.com/nspcc-dev/neofs-sdk-go/version/test"
) )
// Result returns random audit.Result. // Result returns random audit.Result.
func Result() *audit.Result { func Result() *audit.Result {
var x audit.Result x := audit.NewResult()
x.ForContainer(cidtest.ID()) x.SetVersion(versiontest.Version())
x.SetAuditorKey([]byte("key")) x.SetContainerID(cidtest.ID())
x.Complete() x.SetPublicKey([]byte("key"))
x.ForEpoch(44) x.SetComplete(true)
x.SetHits(55) x.SetAuditEpoch(44)
x.SetMisses(66) x.SetHit(55)
x.SetFailures(77) x.SetMiss(66)
x.SetRequestsPoR(88) x.SetFail(77)
x.SetRequestsPoR(99) x.SetRetries(88)
x.SubmitFailedStorageNodes([][]byte{ x.SetRequests(99)
x.SetFailNodes([][]byte{
[]byte("node1"), []byte("node1"),
[]byte("node2"), []byte("node2"),
}) })
x.SubmitPassedStorageNodes([][]byte{ x.SetPassNodes([][]byte{
[]byte("node3"), []byte("node3"),
[]byte("node4"), []byte("node4"),
}) })
x.SubmitPassedStorageGroup(oidtest.ID()) x.SetPassSG([]oid.ID{*oidtest.ID(), *oidtest.ID()})
x.SubmitPassedStorageGroup(oidtest.ID()) x.SetFailSG([]oid.ID{*oidtest.ID(), *oidtest.ID()})
x.SubmitFailedStorageGroup(oidtest.ID())
x.SubmitFailedStorageGroup(oidtest.ID())
return &x return x
} }

View file

@ -1,370 +0,0 @@
package bearer
import (
"crypto/ecdsa"
"errors"
"fmt"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto"
frostfsecdsa "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto/ecdsa"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
)
// Token represents bearer token for object service operations.
//
// Token is mutually compatible with git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl.BearerToken
// message. See ReadFromV2 / WriteToV2 methods.
//
// Instances can be created using built-in var declaration.
type Token struct {
targetUserSet bool
targetUser user.ID
eaclTableSet bool
eaclTable eacl.Table
lifetimeSet bool
iat, nbf, exp uint64
sigSet bool
sig refs.Signature
}
// reads Token from the acl.BearerToken message. If checkFieldPresence is set,
// returns an error on absence of any protocol-required field.
func (b *Token) readFromV2(m acl.BearerToken, checkFieldPresence bool) error {
var err error
body := m.GetBody()
if checkFieldPresence && body == nil {
return errors.New("missing token body")
}
eaclTable := body.GetEACL()
if b.eaclTableSet = eaclTable != nil; b.eaclTableSet {
b.eaclTable = *eacl.NewTableFromV2(eaclTable)
} else if checkFieldPresence {
return errors.New("missing eACL table")
}
targetUser := body.GetOwnerID()
if b.targetUserSet = targetUser != nil; b.targetUserSet {
err = b.targetUser.ReadFromV2(*targetUser)
if err != nil {
return fmt.Errorf("invalid target user: %w", err)
}
}
lifetime := body.GetLifetime()
if b.lifetimeSet = lifetime != nil; b.lifetimeSet {
b.iat = lifetime.GetIat()
b.nbf = lifetime.GetNbf()
b.exp = lifetime.GetExp()
} else if checkFieldPresence {
return errors.New("missing token lifetime")
}
sig := m.GetSignature()
if b.sigSet = sig != nil; sig != nil {
b.sig = *sig
} else if checkFieldPresence {
return errors.New("missing body signature")
}
return nil
}
// ReadFromV2 reads Token from the acl.BearerToken message.
//
// See also WriteToV2.
func (b *Token) ReadFromV2(m acl.BearerToken) error {
return b.readFromV2(m, true)
}
func (b Token) fillBody() *acl.BearerTokenBody {
if !b.eaclTableSet && !b.targetUserSet && !b.lifetimeSet {
return nil
}
var body acl.BearerTokenBody
if b.eaclTableSet {
body.SetEACL(b.eaclTable.ToV2())
}
if b.targetUserSet {
var targetUser refs.OwnerID
b.targetUser.WriteToV2(&targetUser)
body.SetOwnerID(&targetUser)
}
if b.lifetimeSet {
var lifetime acl.TokenLifetime
lifetime.SetIat(b.iat)
lifetime.SetNbf(b.nbf)
lifetime.SetExp(b.exp)
body.SetLifetime(&lifetime)
}
return &body
}
func (b Token) signedData() []byte {
return b.fillBody().StableMarshal(nil)
}
// WriteToV2 writes Token to the acl.BearerToken message.
// The message must not be nil.
//
// See also ReadFromV2.
func (b Token) WriteToV2(m *acl.BearerToken) {
m.SetBody(b.fillBody())
var sig *refs.Signature
if b.sigSet {
sig = &b.sig
}
m.SetSignature(sig)
}
// SetExp sets "exp" (expiration time) claim which identifies the
// expiration time (in FrostFS epochs) after which the Token MUST NOT be
// accepted for processing. The processing of the "exp" claim requires
// that the current epoch MUST be before or equal to the expiration epoch
// listed in the "exp" claim.
//
// Naming is inspired by https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4.
//
// See also InvalidAt.
func (b *Token) SetExp(exp uint64) {
b.exp = exp
b.lifetimeSet = true
}
// SetNbf sets "nbf" (not before) claim which identifies the time (in
// FrostFS epochs) before which the Token MUST NOT be accepted for processing. The
// processing of the "nbf" claim requires that the current epoch MUST be
// after or equal to the not-before epoch listed in the "nbf" claim.
//
// Naming is inspired by https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5.
//
// See also InvalidAt.
func (b *Token) SetNbf(nbf uint64) {
b.nbf = nbf
b.lifetimeSet = true
}
// SetIat sets "iat" (issued at) claim which identifies the time (in FrostFS
// epochs) at which the Token was issued. This claim can be used to determine
// the age of the Token.
//
// Naming is inspired by https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6.
//
// See also InvalidAt.
func (b *Token) SetIat(iat uint64) {
b.iat = iat
b.lifetimeSet = true
}
// InvalidAt asserts "exp", "nbf" and "iat" claims for the given epoch.
//
// Zero Container is invalid in any epoch.
//
// See also SetExp, SetNbf, SetIat.
func (b Token) InvalidAt(epoch uint64) bool {
return !b.lifetimeSet || b.nbf > epoch || b.iat > epoch || b.exp < epoch
}
// SetEACLTable sets eacl.Table that replaces the one from the issuer's
// container. If table has specified container, bearer token can be used only
// for operations within this specific container. Otherwise, Token can be used
// within any issuer's container.
//
// SetEACLTable MUST be called if Token is going to be transmitted over
// FrostFS API V2 protocol.
//
// See also EACLTable, AssertContainer.
func (b *Token) SetEACLTable(table eacl.Table) {
b.eaclTable = table
b.eaclTableSet = true
}
// EACLTable returns extended ACL table set by SetEACLTable.
//
// Zero Token has zero eacl.Table.
func (b Token) EACLTable() eacl.Table {
if b.eaclTableSet {
return b.eaclTable
}
return eacl.Table{}
}
// AssertContainer checks if the token is valid within the given container.
//
// Note: cnr is assumed to refer to the issuer's container, otherwise the check
// is meaningless.
//
// Zero Token is valid in any container.
//
// See also SetEACLTable.
func (b Token) AssertContainer(cnr cid.ID) bool {
if !b.eaclTableSet {
return true
}
cnrTable, set := b.eaclTable.CID()
return !set || cnrTable.Equals(cnr)
}
// ForUser specifies ID of the user who can use the Token for the operations
// within issuer's container(s).
//
// Optional: by default, any user has access to Token usage.
//
// See also AssertUser.
func (b *Token) ForUser(id user.ID) {
b.targetUser = id
b.targetUserSet = true
}
// AssertUser checks if the Token is issued to the given user.
//
// Zero Token is available to any user.
//
// See also ForUser.
func (b Token) AssertUser(id user.ID) bool {
return !b.targetUserSet || b.targetUser.Equals(id)
}
// Sign calculates and writes signature of the Token data using issuer's secret.
// Returns signature calculation errors.
//
// Sign MUST be called if Token is going to be transmitted over
// FrostFS API V2 protocol.
//
// Note that any Token mutation is likely to break the signature, so it is
// expected to be calculated as a final stage of Token formation.
//
// See also VerifySignature, Issuer.
func (b *Token) Sign(key ecdsa.PrivateKey) error {
var sig frostfscrypto.Signature
err := sig.Calculate(frostfsecdsa.Signer(key), b.signedData())
if err != nil {
return err
}
sig.WriteToV2(&b.sig)
b.sigSet = true
return nil
}
// VerifySignature checks if Token signature is presented and valid.
//
// Zero Token fails the check.
//
// See also Sign.
func (b Token) VerifySignature() bool {
if !b.sigSet {
return false
}
var sig frostfscrypto.Signature
// TODO: (#233) check owner<->key relation
return sig.ReadFromV2(b.sig) == nil && sig.Verify(b.signedData())
}
// Marshal encodes Token into a binary format of the FrostFS API protocol
// (Protocol Buffers V3 with direct field order).
//
// See also Unmarshal.
func (b Token) Marshal() []byte {
var m acl.BearerToken
b.WriteToV2(&m)
return m.StableMarshal(nil)
}
// Unmarshal decodes FrostFS API protocol binary data into the Token
// (Protocol Buffers V3 with direct field order). Returns an error describing
// a format violation.
//
// See also Marshal.
func (b *Token) Unmarshal(data []byte) error {
var m acl.BearerToken
err := m.Unmarshal(data)
if err != nil {
return err
}
return b.readFromV2(m, false)
}
// MarshalJSON encodes Token into a JSON format of the FrostFS API protocol
// (Protocol Buffers V3 JSON).
//
// See also UnmarshalJSON.
func (b Token) MarshalJSON() ([]byte, error) {
var m acl.BearerToken
b.WriteToV2(&m)
return m.MarshalJSON()
}
// UnmarshalJSON decodes FrostFS API protocol JSON data into the Token
// (Protocol Buffers V3 JSON). Returns an error describing a format violation.
//
// See also MarshalJSON.
func (b *Token) UnmarshalJSON(data []byte) error {
var m acl.BearerToken
err := m.UnmarshalJSON(data)
if err != nil {
return err
}
return b.readFromV2(m, false)
}
// SigningKeyBytes returns issuer's public key in a binary format of
// FrostFS API protocol.
//
// Unsigned Token has empty key.
//
// See also ResolveIssuer.
func (b Token) SigningKeyBytes() []byte {
if b.sigSet {
return b.sig.GetKey()
}
return nil
}
// ResolveIssuer resolves issuer's user.ID from the key used for Token signing.
// Returns zero user.ID if Token is unsigned or key has incorrect format.
//
// See also SigningKeyBytes.
func ResolveIssuer(b Token) (usr user.ID) {
binKey := b.SigningKeyBytes()
if len(binKey) != 0 {
var key frostfsecdsa.PublicKey
if key.Decode(binKey) == nil {
user.IDFromKey(&usr, ecdsa.PublicKey(key))
}
}
return
}

View file

@ -1,390 +0,0 @@
package bearer_test
import (
"bytes"
"math/rand"
"testing"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
bearertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer/test"
cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test"
frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto"
frostfsecdsa "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto/ecdsa"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl"
eacltest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl/test"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
usertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user/test"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/stretchr/testify/require"
)
// compares binary representations of two eacl.Table instances.
func isEqualEACLTables(t1, t2 eacl.Table) bool {
d1, err := t1.Marshal()
if err != nil {
panic(err)
}
d2, err := t2.Marshal()
if err != nil {
panic(err)
}
return bytes.Equal(d1, d2)
}
func TestToken_SetEACLTable(t *testing.T) {
var val bearer.Token
var m acl.BearerToken
filled := bearertest.Token()
val.WriteToV2(&m)
require.Zero(t, m.GetBody())
val2 := filled
require.NoError(t, val2.Unmarshal(val.Marshal()))
require.Zero(t, val2.EACLTable())
val2 = filled
jd, err := val.MarshalJSON()
require.NoError(t, err)
require.NoError(t, val2.UnmarshalJSON(jd))
require.Zero(t, val2.EACLTable())
// set value
eaclTable := *eacltest.Table()
val.SetEACLTable(eaclTable)
require.True(t, isEqualEACLTables(eaclTable, val.EACLTable()))
val.WriteToV2(&m)
eaclTableV2 := eaclTable.ToV2()
require.Equal(t, eaclTableV2, m.GetBody().GetEACL())
val2 = filled
require.NoError(t, val2.Unmarshal(val.Marshal()))
require.True(t, isEqualEACLTables(eaclTable, val.EACLTable()))
val2 = filled
jd, err = val.MarshalJSON()
require.NoError(t, err)
require.NoError(t, val2.UnmarshalJSON(jd))
require.True(t, isEqualEACLTables(eaclTable, val.EACLTable()))
}
func TestToken_ForUser(t *testing.T) {
var val bearer.Token
var m acl.BearerToken
filled := bearertest.Token()
val.WriteToV2(&m)
require.Zero(t, m.GetBody())
val2 := filled
require.NoError(t, val2.Unmarshal(val.Marshal()))
val2.WriteToV2(&m)
require.Zero(t, m.GetBody())
val2 = filled
jd, err := val.MarshalJSON()
require.NoError(t, err)
require.NoError(t, val2.UnmarshalJSON(jd))
val2.WriteToV2(&m)
require.Zero(t, m.GetBody())
// set value
usr := *usertest.ID()
var usrV2 refs.OwnerID
usr.WriteToV2(&usrV2)
val.ForUser(usr)
val.WriteToV2(&m)
require.Equal(t, usrV2, *m.GetBody().GetOwnerID())
val2 = filled
require.NoError(t, val2.Unmarshal(val.Marshal()))
val2.WriteToV2(&m)
require.Equal(t, usrV2, *m.GetBody().GetOwnerID())
val2 = filled
jd, err = val.MarshalJSON()
require.NoError(t, err)
require.NoError(t, val2.UnmarshalJSON(jd))
val2.WriteToV2(&m)
require.Equal(t, usrV2, *m.GetBody().GetOwnerID())
}
func testLifetimeClaim(t *testing.T, setter func(*bearer.Token, uint64), getter func(*acl.BearerToken) uint64) {
var val bearer.Token
var m acl.BearerToken
filled := bearertest.Token()
val.WriteToV2(&m)
require.Zero(t, m.GetBody())
val2 := filled
require.NoError(t, val2.Unmarshal(val.Marshal()))
val2.WriteToV2(&m)
require.Zero(t, m.GetBody())
val2 = filled
jd, err := val.MarshalJSON()
require.NoError(t, err)
require.NoError(t, val2.UnmarshalJSON(jd))
val2.WriteToV2(&m)
require.Zero(t, m.GetBody())
// set value
exp := rand.Uint64()
setter(&val, exp)
val.WriteToV2(&m)
require.Equal(t, exp, getter(&m))
val2 = filled
require.NoError(t, val2.Unmarshal(val.Marshal()))
val2.WriteToV2(&m)
require.Equal(t, exp, getter(&m))
val2 = filled
jd, err = val.MarshalJSON()
require.NoError(t, err)
require.NoError(t, val2.UnmarshalJSON(jd))
val2.WriteToV2(&m)
require.Equal(t, exp, getter(&m))
}
func TestToken_SetLifetime(t *testing.T) {
t.Run("iat", func(t *testing.T) {
testLifetimeClaim(t, (*bearer.Token).SetIat, func(token *acl.BearerToken) uint64 {
return token.GetBody().GetLifetime().GetIat()
})
})
t.Run("nbf", func(t *testing.T) {
testLifetimeClaim(t, (*bearer.Token).SetNbf, func(token *acl.BearerToken) uint64 {
return token.GetBody().GetLifetime().GetNbf()
})
})
t.Run("exp", func(t *testing.T) {
testLifetimeClaim(t, (*bearer.Token).SetExp, func(token *acl.BearerToken) uint64 {
return token.GetBody().GetLifetime().GetExp()
})
})
}
func TestToken_InvalidAt(t *testing.T) {
var val bearer.Token
require.True(t, val.InvalidAt(0))
require.True(t, val.InvalidAt(1))
val.SetIat(1)
val.SetNbf(2)
val.SetExp(4)
require.True(t, val.InvalidAt(0))
require.True(t, val.InvalidAt(1))
require.False(t, val.InvalidAt(2))
require.False(t, val.InvalidAt(3))
require.False(t, val.InvalidAt(4))
require.True(t, val.InvalidAt(5))
}
func TestToken_AssertContainer(t *testing.T) {
var val bearer.Token
cnr := cidtest.ID()
require.True(t, val.AssertContainer(cnr))
eaclTable := *eacltest.Table()
eaclTable.SetCID(cidtest.ID())
val.SetEACLTable(eaclTable)
require.False(t, val.AssertContainer(cnr))
eaclTable.SetCID(cnr)
val.SetEACLTable(eaclTable)
require.True(t, val.AssertContainer(cnr))
}
func TestToken_AssertUser(t *testing.T) {
var val bearer.Token
usr := *usertest.ID()
require.True(t, val.AssertUser(usr))
val.ForUser(*usertest.ID())
require.False(t, val.AssertUser(usr))
val.ForUser(usr)
require.True(t, val.AssertUser(usr))
}
func TestToken_Sign(t *testing.T) {
var val bearer.Token
require.False(t, val.VerifySignature())
k, err := keys.NewPrivateKey()
require.NoError(t, err)
key := k.PrivateKey
val = bearertest.Token()
require.NoError(t, val.Sign(key))
require.True(t, val.VerifySignature())
var m acl.BearerToken
val.WriteToV2(&m)
require.NotZero(t, m.GetSignature().GetKey())
require.NotZero(t, m.GetSignature().GetSign())
val2 := bearertest.Token()
require.NoError(t, val2.Unmarshal(val.Marshal()))
require.True(t, val2.VerifySignature())
jd, err := val.MarshalJSON()
require.NoError(t, err)
val2 = bearertest.Token()
require.NoError(t, val2.UnmarshalJSON(jd))
require.True(t, val2.VerifySignature())
}
func TestToken_ReadFromV2(t *testing.T) {
var val bearer.Token
var m acl.BearerToken
require.Error(t, val.ReadFromV2(m))
var body acl.BearerTokenBody
m.SetBody(&body)
require.Error(t, val.ReadFromV2(m))
eaclTable := eacltest.Table().ToV2()
body.SetEACL(eaclTable)
require.Error(t, val.ReadFromV2(m))
var lifetime acl.TokenLifetime
body.SetLifetime(&lifetime)
require.Error(t, val.ReadFromV2(m))
const iat, nbf, exp = 1, 2, 3
lifetime.SetIat(iat)
lifetime.SetNbf(nbf)
lifetime.SetExp(exp)
body.SetLifetime(&lifetime)
require.Error(t, val.ReadFromV2(m))
var sig refs.Signature
m.SetSignature(&sig)
require.NoError(t, val.ReadFromV2(m))
var m2 acl.BearerToken
val.WriteToV2(&m2)
require.Equal(t, m, m2)
usr, usr2 := *usertest.ID(), *usertest.ID()
require.True(t, val.AssertUser(usr))
require.True(t, val.AssertUser(usr2))
var usrV2 refs.OwnerID
usr.WriteToV2(&usrV2)
body.SetOwnerID(&usrV2)
require.NoError(t, val.ReadFromV2(m))
val.WriteToV2(&m2)
require.Equal(t, m, m2)
require.True(t, val.AssertUser(usr))
require.False(t, val.AssertUser(usr2))
k, err := keys.NewPrivateKey()
require.NoError(t, err)
signer := frostfsecdsa.Signer(k.PrivateKey)
var s frostfscrypto.Signature
require.NoError(t, s.Calculate(signer, body.StableMarshal(nil)))
s.WriteToV2(&sig)
require.NoError(t, val.ReadFromV2(m))
require.True(t, val.VerifySignature())
require.Equal(t, sig.GetKey(), val.SigningKeyBytes())
}
func TestResolveIssuer(t *testing.T) {
k, err := keys.NewPrivateKey()
require.NoError(t, err)
var val bearer.Token
require.Zero(t, bearer.ResolveIssuer(val))
var m acl.BearerToken
var sig refs.Signature
sig.SetKey([]byte("invalid key"))
m.SetSignature(&sig)
require.NoError(t, val.Unmarshal(m.StableMarshal(nil)))
require.Zero(t, bearer.ResolveIssuer(val))
require.NoError(t, val.Sign(k.PrivateKey))
var usr user.ID
user.IDFromKey(&usr, k.PrivateKey.PublicKey)
require.Equal(t, usr, bearer.ResolveIssuer(val))
}

View file

@ -1,31 +0,0 @@
/*
Package bearer provides bearer token definition.
Bearer token is attached to the object service requests, and it overwrites
extended ACL of the container. Mainly it is used to provide access of private
data for specific user. Therefore, it must be signed by owner of the container.
Define bearer token by setting correct lifetime, extended ACL and owner ID of
the user that will attach token to its requests.
var bearerToken bearer.Token
bearerToken.SetExpiration(500)
bearerToken.SetIssuedAt(10)
bearerToken.SetNotBefore(10)
bearerToken.SetEACL(eaclTable)
bearerToken.SetOwner(ownerID)
Bearer token must be signed by owner of the container.
err := bearerToken.Sign(privateKey)
Provide signed token in JSON or binary format to the request sender. Request
sender can attach this bearer token to the object service requests:
import sdkClient "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client"
var headParams sdkClient.PrmObjectHead
headParams.WithBearerToken(bearerToken)
response, err := client.ObjectHead(ctx, headParams)
*/
package bearer

View file

@ -1,6 +0,0 @@
/*
Package bearertest provides functions for testing bearer package.
Note that this package intended only for tests.
*/
package bearertest

View file

@ -1,20 +0,0 @@
package bearertest
import (
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
eacltest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl/test"
usertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user/test"
)
// Token returns random bearer.Token.
//
// Resulting token is unsigned.
func Token() (t bearer.Token) {
t.SetExp(3)
t.SetNbf(2)
t.SetIat(1)
t.ForUser(*usertest.ID())
t.SetEACLTable(*eacltest.Table())
return t
}

View file

@ -1,25 +1,15 @@
package checksum package checksum
import ( import (
"bytes"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"errors"
"fmt" "fmt"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs" "github.com/nspcc-dev/neofs-api-go/v2/refs"
"git.frostfs.info/TrueCloudLab/tzhash/tz"
) )
// Checksum represents checksum of some digital data. // Checksum represents v2-compatible checksum.
//
// Checksum is mutually compatible with git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs.Checksum
// message. See ReadFromV2 / WriteToV2 methods.
//
// Instances can be created using built-in var declaration.
//
// Note that direct typecast is not safe and may result in loss of compatibility:
//
// _ = Checksum(refs.Checksum{}) // not recommended
type Checksum refs.Checksum type Checksum refs.Checksum
// Type represents the enumeration // Type represents the enumeration
@ -33,46 +23,29 @@ const (
// SHA256 is a SHA256 checksum type. // SHA256 is a SHA256 checksum type.
SHA256 SHA256
// TZ is a Tillich-Zémor checksum type. // TZ is a Tillich-Zemor checksum type.
TZ TZ
) )
// ReadFromV2 reads Checksum from the refs.Checksum message. Checks if the // NewFromV2 wraps v2 Checksum message to Checksum.
// message conforms to FrostFS API V2 protocol.
// //
// See also WriteToV2. // Nil refs.Checksum converts to nil.
func (c *Checksum) ReadFromV2(m refs.Checksum) error { func NewFromV2(cV2 *refs.Checksum) *Checksum {
if len(m.GetSum()) == 0 { return (*Checksum)(cV2)
return errors.New("missing value")
}
switch m.GetType() {
default:
return fmt.Errorf("unsupported type %v", m.GetType())
case refs.SHA256, refs.TillichZemor:
}
*c = Checksum(m)
return nil
} }
// WriteToV2 writes Checksum to the refs.Checksum message. // New creates and initializes blank Checksum.
// The message must not be nil.
// //
// See also ReadFromV2. // Defaults:
func (c Checksum) WriteToV2(m *refs.Checksum) { // - sum: nil;
*m = (refs.Checksum)(c) // - type: Unknown.
func New() *Checksum {
return NewFromV2(new(refs.Checksum))
} }
// Type returns checksum type. // Type returns checksum type.
// func (c *Checksum) Type() Type {
// Zero Checksum has Unknown checksum type. switch (*refs.Checksum)(c).GetType() {
//
// See also SetTillichZemor and SetSHA256.
func (c Checksum) Type() Type {
v2 := (refs.Checksum)(c)
switch v2.GetType() {
case refs.SHA256: case refs.SHA256:
return SHA256 return SHA256
case refs.TillichZemor: case refs.TillichZemor:
@ -82,70 +55,93 @@ func (c Checksum) Type() Type {
} }
} }
// Value returns checksum bytes. Return value // Sum returns checksum bytes.
// MUST NOT be mutated. func (c *Checksum) Sum() []byte {
// return (*refs.Checksum)(c).GetSum()
// Zero Checksum has nil sum.
//
// See also SetTillichZemor and SetSHA256.
func (c Checksum) Value() []byte {
v2 := (refs.Checksum)(c)
return v2.GetSum()
} }
// SetSHA256 sets checksum to SHA256 hash. // SetSHA256 sets checksum to SHA256 hash.
//
// See also Calculate.
func (c *Checksum) SetSHA256(v [sha256.Size]byte) { func (c *Checksum) SetSHA256(v [sha256.Size]byte) {
v2 := (*refs.Checksum)(c) checksum := (*refs.Checksum)(c)
v2.SetType(refs.SHA256) checksum.SetType(refs.SHA256)
v2.SetSum(v[:]) checksum.SetSum(v[:])
} }
// Calculate calculates checksum and sets it // SetTillichZemor sets checksum to Tillich-Zemor hash.
// to the passed checksum. Checksum must not be nil. func (c *Checksum) SetTillichZemor(v [64]byte) {
checksum := (*refs.Checksum)(c)
checksum.SetType(refs.TillichZemor)
checksum.SetSum(v[:])
}
// ToV2 converts Checksum to v2 Checksum message.
// //
// Does nothing if the passed type is not one of the: // Nil Checksum converts to nil.
// - SHA256; func (c *Checksum) ToV2() *refs.Checksum {
// - TZ. return (*refs.Checksum)(c)
// }
// Does not mutate the passed value.
// func Equal(cs1, cs2 *Checksum) bool {
// See also SetSHA256, SetTillichZemor. return cs1.Type() == cs2.Type() && bytes.Equal(cs1.Sum(), cs2.Sum())
func Calculate(c *Checksum, t Type, v []byte) { }
switch t {
case SHA256: // Marshal marshals Checksum into a protobuf binary form.
c.SetSHA256(sha256.Sum256(v)) func (c *Checksum) Marshal() ([]byte, error) {
case TZ: return (*refs.Checksum)(c).StableMarshal(nil)
c.SetTillichZemor(tz.Sum(v)) }
default:
// Unmarshal unmarshals protobuf binary representation of Checksum.
func (c *Checksum) Unmarshal(data []byte) error {
return (*refs.Checksum)(c).Unmarshal(data)
}
// MarshalJSON encodes Checksum to protobuf JSON format.
func (c *Checksum) MarshalJSON() ([]byte, error) {
return (*refs.Checksum)(c).MarshalJSON()
}
// UnmarshalJSON decodes Checksum from protobuf JSON format.
func (c *Checksum) UnmarshalJSON(data []byte) error {
return (*refs.Checksum)(c).UnmarshalJSON(data)
}
func (c *Checksum) String() string {
return hex.EncodeToString((*refs.Checksum)(c).GetSum())
}
// Parse parses Checksum from its string representation.
func (c *Checksum) Parse(s string) error {
data, err := hex.DecodeString(s)
if err != nil {
return err
} }
var typ refs.ChecksumType
switch ln := len(data); ln {
default:
return fmt.Errorf("unsupported checksum length %d", ln)
case sha256.Size:
typ = refs.SHA256
case 64:
typ = refs.TillichZemor
}
cV2 := (*refs.Checksum)(c)
cV2.SetType(typ)
cV2.SetSum(data)
return nil
} }
// SetTillichZemor sets checksum to Tillich-Zémor hash. // String returns string representation of Type.
// //
// See also Calculate. // String mapping:
func (c *Checksum) SetTillichZemor(v [tz.Size]byte) { // * TZ: TZ;
v2 := (*refs.Checksum)(c) // * SHA256: SHA256;
// * Unknown, default: CHECKSUM_TYPE_UNSPECIFIED.
v2.SetType(refs.TillichZemor)
v2.SetSum(v[:])
}
// String implements fmt.Stringer.
//
// String is designed to be human-readable, and its format MAY differ between
// SDK versions.
func (c Checksum) String() string {
v2 := (refs.Checksum)(c)
return fmt.Sprintf("%s:%s", c.Type(), hex.EncodeToString(v2.GetSum()))
}
// String implements fmt.Stringer.
//
// String is designed to be human-readable, and its format MAY differ between
// SDK versions.
func (m Type) String() string { func (m Type) String() string {
var m2 refs.ChecksumType var m2 refs.ChecksumType
@ -160,3 +156,26 @@ func (m Type) String() string {
return m2.String() return m2.String()
} }
// FromString parses Type from a string representation.
// It is a reverse action to String().
//
// Returns true if s was parsed successfully.
func (m *Type) FromString(s string) bool {
var g refs.ChecksumType
ok := g.FromString(s)
if ok {
switch g {
default:
*m = Unknown
case refs.TillichZemor:
*m = TZ
case refs.SHA256:
*m = SHA256
}
}
return ok
}

View file

@ -5,13 +5,20 @@ import (
"crypto/sha256" "crypto/sha256"
"testing" "testing"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs" "github.com/nspcc-dev/neofs-api-go/v2/refs"
"git.frostfs.info/TrueCloudLab/tzhash/tz"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func randSHA256(t *testing.T) [sha256.Size]byte {
cSHA256 := [sha256.Size]byte{}
_, err := rand.Read(cSHA256[:])
require.NoError(t, err)
return cSHA256
}
func TestChecksum(t *testing.T) { func TestChecksum(t *testing.T) {
var c Checksum c := New()
cSHA256 := [sha256.Size]byte{} cSHA256 := [sha256.Size]byte{}
_, _ = rand.Read(cSHA256[:]) _, _ = rand.Read(cSHA256[:])
@ -19,62 +26,150 @@ func TestChecksum(t *testing.T) {
c.SetSHA256(cSHA256) c.SetSHA256(cSHA256)
require.Equal(t, SHA256, c.Type()) require.Equal(t, SHA256, c.Type())
require.Equal(t, cSHA256[:], c.Value()) require.Equal(t, cSHA256[:], c.Sum())
var cV2 refs.Checksum cV2 := c.ToV2()
c.WriteToV2(&cV2)
require.Equal(t, refs.SHA256, cV2.GetType()) require.Equal(t, refs.SHA256, cV2.GetType())
require.Equal(t, cSHA256[:], cV2.GetSum()) require.Equal(t, cSHA256[:], cV2.GetSum())
cTZ := [tz.Size]byte{} cTZ := [64]byte{}
_, _ = rand.Read(cSHA256[:]) _, _ = rand.Read(cSHA256[:])
c.SetTillichZemor(cTZ) c.SetTillichZemor(cTZ)
require.Equal(t, TZ, c.Type()) require.Equal(t, TZ, c.Type())
require.Equal(t, cTZ[:], c.Value()) require.Equal(t, cTZ[:], c.Sum())
c.WriteToV2(&cV2) cV2 = c.ToV2()
require.Equal(t, refs.TillichZemor, cV2.GetType()) require.Equal(t, refs.TillichZemor, cV2.GetType())
require.Equal(t, cTZ[:], cV2.GetSum()) require.Equal(t, cTZ[:], cV2.GetSum())
} }
func TestEqualChecksums(t *testing.T) {
require.True(t, Equal(nil, nil))
csSHA := [sha256.Size]byte{}
_, _ = rand.Read(csSHA[:])
cs1 := New()
cs1.SetSHA256(csSHA)
cs2 := New()
cs2.SetSHA256(csSHA)
require.True(t, Equal(cs1, cs2))
csSHA[0]++
cs2.SetSHA256(csSHA)
require.False(t, Equal(cs1, cs2))
}
func TestChecksumEncoding(t *testing.T) {
cs := New()
cs.SetSHA256(randSHA256(t))
t.Run("binary", func(t *testing.T) {
data, err := cs.Marshal()
require.NoError(t, err)
c2 := New()
require.NoError(t, c2.Unmarshal(data))
require.Equal(t, cs, c2)
})
t.Run("json", func(t *testing.T) {
data, err := cs.MarshalJSON()
require.NoError(t, err)
cs2 := New()
require.NoError(t, cs2.UnmarshalJSON(data))
require.Equal(t, cs, cs2)
})
t.Run("string", func(t *testing.T) {
cs2 := New()
require.NoError(t, cs2.Parse(cs.String()))
require.Equal(t, cs, cs2)
})
}
func TestNewChecksumFromV2(t *testing.T) {
t.Run("from nil", func(t *testing.T) {
var x *refs.Checksum
require.Nil(t, NewFromV2(x))
})
}
func TestChecksum_ToV2(t *testing.T) {
t.Run("nil", func(t *testing.T) {
var x *Checksum
require.Nil(t, x.ToV2())
})
}
func TestNewChecksum(t *testing.T) { func TestNewChecksum(t *testing.T) {
t.Run("default values", func(t *testing.T) { t.Run("default values", func(t *testing.T) {
var chs Checksum chs := New()
// check initial values // check initial values
require.Equal(t, Unknown, chs.Type()) require.Equal(t, Unknown, chs.Type())
require.Nil(t, chs.Value()) require.Nil(t, chs.Sum())
// convert to v2 message // convert to v2 message
var chsV2 refs.Checksum chsV2 := chs.ToV2()
chs.WriteToV2(&chsV2)
require.Equal(t, refs.UnknownChecksum, chsV2.GetType()) require.Equal(t, refs.UnknownChecksum, chsV2.GetType())
require.Nil(t, chsV2.GetSum()) require.Nil(t, chsV2.GetSum())
}) })
} }
func TestCalculation(t *testing.T) { type enumIface interface {
var c Checksum FromString(string) bool
payload := []byte{0, 1, 2, 3, 4, 5} String() string
}
t.Run("SHA256", func(t *testing.T) { type enumStringItem struct {
orig := sha256.Sum256(payload) val enumIface
str string
}
Calculate(&c, SHA256, payload) func testEnumStrings(t *testing.T, e enumIface, items []enumStringItem) {
for _, item := range items {
require.Equal(t, item.str, item.val.String())
require.Equal(t, orig[:], c.Value()) s := item.val.String()
})
t.Run("TZ", func(t *testing.T) { require.True(t, e.FromString(s), s)
orig := tz.Sum(payload)
Calculate(&c, TZ, payload) require.EqualValues(t, item.val, e, item.val)
}
require.Equal(t, orig[:], c.Value()) // incorrect strings
for _, str := range []string{
"some string",
"undefined",
} {
require.False(t, e.FromString(str))
}
}
func TestChecksumType_String(t *testing.T) {
toPtr := func(v Type) *Type {
return &v
}
testEnumStrings(t, new(Type), []enumStringItem{
{val: toPtr(TZ), str: "TZ"},
{val: toPtr(SHA256), str: "SHA256"},
{val: toPtr(Unknown), str: "CHECKSUM_TYPE_UNSPECIFIED"},
}) })
} }

View file

@ -1,18 +0,0 @@
/*
Package checksum provides primitives to work with checksums.
Checksum is a basic type of data checksums.
For example, calculating checksums:
// retrieving any payload for hashing
var sha256Sum Checksum
Calculate(&sha256Sum, SHA256, payload) // sha256Sum contains SHA256 hash of the payload
var tzSum Checksum
Calculate(&tzSum, TZ, payload) // tzSum contains TZ hash of the payload
Using package types in an application is recommended to potentially work with
different protocol versions with which these types are compatible.
*/
package checksum

View file

@ -1,34 +0,0 @@
package checksum
import (
"bytes"
"crypto/sha256"
"fmt"
"math/rand"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
)
func ExampleCalculate() {
payload := []byte{0, 1, 2, 3, 4, 5, 6}
var cs Checksum
Calculate(&cs, SHA256, payload)
Calculate(&cs, TZ, payload)
}
func ExampleChecksum_WriteToV2() {
var (
csRaw [sha256.Size]byte
csV2 refs.Checksum
cs Checksum
)
rand.Read(csRaw[:])
cs.SetSHA256(csRaw)
cs.WriteToV2(&csV2)
fmt.Println(bytes.Equal(cs.Value(), csV2.GetSum()))
// Output: true
}

View file

@ -1,13 +0,0 @@
/*
Package checksumtest provides functions for convenient testing of checksum 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 checksumtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/checksum/test"
cs := checksumtest.Checksum()
// test the value
*/
package checksumtest

View file

@ -4,16 +4,16 @@ import (
"crypto/sha256" "crypto/sha256"
"math/rand" "math/rand"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/checksum" "github.com/nspcc-dev/neofs-sdk-go/checksum"
) )
// Checksum returns random checksum.Checksum. // Checksum returns random checksum.Checksum.
func Checksum() checksum.Checksum { func Checksum() *checksum.Checksum {
var cs [sha256.Size]byte var cs [sha256.Size]byte
rand.Read(cs[:]) rand.Read(cs[:])
var x checksum.Checksum x := checksum.New()
x.SetSHA256(cs) x.SetSHA256(cs)

View file

@ -3,50 +3,55 @@ package client
import ( import (
"context" "context"
v2accounting "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/accounting" v2accounting "github.com/nspcc-dev/neofs-api-go/v2/accounting"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs" rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc" "github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client" "github.com/nspcc-dev/neofs-sdk-go/accounting"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/accounting" "github.com/nspcc-dev/neofs-sdk-go/owner"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
) )
// PrmBalanceGet groups parameters of BalanceGet operation. // PrmBalanceGet groups parameters of BalanceGet operation.
type PrmBalanceGet struct { type PrmBalanceGet struct {
prmCommonMeta prmCommonMeta
accountSet bool ownerSet bool
account user.ID ownerID owner.ID
} }
// SetAccount sets identifier of the FrostFS account for which the balance is requested. // SetAccount sets identifier of the NeoFS account for which the balance is requested.
// Required parameter. // Required parameter. Must be a valid ID according to NeoFS API protocol.
func (x *PrmBalanceGet) SetAccount(id user.ID) { func (x *PrmBalanceGet) SetAccount(id owner.ID) {
x.account = id x.ownerID = id
x.accountSet = true x.ownerSet = true
} }
// ResBalanceGet groups resulting values of BalanceGet operation. // ResBalanceGet groups resulting values of BalanceGet operation.
type ResBalanceGet struct { type ResBalanceGet struct {
statusRes statusRes
amount accounting.Decimal amount *accounting.Decimal
} }
// Amount returns current amount of funds on the FrostFS account as decimal number. func (x *ResBalanceGet) setAmount(v *accounting.Decimal) {
func (x ResBalanceGet) Amount() accounting.Decimal { x.amount = v
}
// Amount returns current amount of funds on the NeoFS account as decimal number.
//
// Client doesn't retain value so modification is safe.
func (x ResBalanceGet) Amount() *accounting.Decimal {
return x.amount return x.amount
} }
// BalanceGet requests current balance of the FrostFS account. // BalanceGet requests current balance of the NeoFS account.
// //
// Exactly one return value is non-nil. By default, server status is returned in res structure. // Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as `error`, // Any client's internal or transport errors are returned as `error`,
// If PrmInit.ResolveFrostFSFailures has been called, unsuccessful // If WithNeoFSErrorParsing option has been provided, unsuccessful
// FrostFS status codes are returned as `error`, otherwise, are included // NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure. // in the returned result structure.
// //
// Returns an error if parameters are set incorrectly (see PrmBalanceGet docs). // Immediately panics if parameters are set incorrectly (see PrmBalanceGet docs).
// Context is required and must not be nil. It is used for network communication. // Context is required and must not be nil. It is used for network communication.
// //
// Return statuses: // Return statuses:
@ -54,17 +59,17 @@ func (x ResBalanceGet) Amount() accounting.Decimal {
func (c *Client) BalanceGet(ctx context.Context, prm PrmBalanceGet) (*ResBalanceGet, error) { func (c *Client) BalanceGet(ctx context.Context, prm PrmBalanceGet) (*ResBalanceGet, error) {
switch { switch {
case ctx == nil: case ctx == nil:
return nil, errorMissingContext panic(panicMsgMissingContext)
case !prm.accountSet: case !prm.ownerSet:
return nil, errorAccountNotSet panic("account not set")
case !prm.ownerID.Valid():
panic("invalid account ID")
} }
// form request body // form request body
var accountV2 refs.OwnerID
prm.account.WriteToV2(&accountV2)
var body v2accounting.BalanceRequestBody var body v2accounting.BalanceRequestBody
body.SetOwnerID(&accountV2)
body.SetOwnerID(prm.ownerID.ToV2())
// form request // form request
var req v2accounting.BalanceRequest var req v2accounting.BalanceRequest
@ -87,19 +92,7 @@ func (c *Client) BalanceGet(ctx context.Context, prm PrmBalanceGet) (*ResBalance
} }
cc.result = func(r responseV2) { cc.result = func(r responseV2) {
resp := r.(*v2accounting.BalanceResponse) resp := r.(*v2accounting.BalanceResponse)
res.setAmount(accounting.NewDecimalFromV2(resp.GetBody().GetBalance()))
const fieldBalance = "balance"
bal := resp.GetBody().GetBalance()
if bal == nil {
cc.err = newErrMissingResponseField(fieldBalance)
return
}
cc.err = res.amount.ReadFromV2(*bal)
if cc.err != nil {
cc.err = newErrInvalidResponseField(fieldBalance, cc.err)
}
} }
// process call // process call

View file

@ -1,35 +0,0 @@
package client
import (
"context"
"fmt"
v2netmap "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/netmap"
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
)
// interface of FrostFS API server. Exists for test purposes only.
type frostFSAPIServer interface {
netMapSnapshot(context.Context, v2netmap.SnapshotRequest) (*v2netmap.SnapshotResponse, error)
}
// wrapper over real client connection which communicates over FrostFS API protocol.
// Provides frostFSAPIServer for Client instances used in real applications.
type coreServer client.Client
// unifies errors of all RPC.
func rpcErr(e error) error {
return fmt.Errorf("rpc failure: %w", e)
}
// executes NetmapService.NetmapSnapshot RPC declared in FrostFS API protocol
// using underlying client.Client.
func (x *coreServer) netMapSnapshot(ctx context.Context, req v2netmap.SnapshotRequest) (*v2netmap.SnapshotResponse, error) {
resp, err := rpcapi.NetMapSnapshot((*client.Client)(x), &req, client.WithContext(ctx))
if err != nil {
return nil, rpcErr(err)
}
return resp, nil
}

View file

@ -1,25 +1,23 @@
package client package client
import ( import (
"context"
"crypto/ecdsa" "crypto/ecdsa"
"crypto/tls" "crypto/tls"
"errors"
"time" "time"
v2accounting "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/accounting" v2accounting "github.com/nspcc-dev/neofs-api-go/v2/accounting"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc" "github.com/nspcc-dev/neofs-api-go/v2/rpc"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client" "github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
) )
// Client represents virtual connection to the FrostFS network to communicate // Client represents virtual connection to the NeoFS network to communicate
// with FrostFS server using FrostFS API protocol. It is designed to provide // with NeoFS server using NeoFS API protocol. It is designed to provide
// an abstraction interface from the protocol details of data transfer over // an abstraction interface from the protocol details of data transfer over
// a network in FrostFS. // a network in NeoFS.
// //
// Client can be created using simple Go variable declaration. Before starting // Client can be created using simple Go variable declaration. Before starting
// work with the Client, it SHOULD BE correctly initialized (see Init method). // work with the Client, it SHOULD BE correctly initialized (see Init method).
// Before executing the FrostFS operations using the Client, connection to the // Before executing the NeoFS operations using the Client, connection to the
// server MUST BE correctly established (see Dial method and pay attention // server MUST BE correctly established (see Dial method and pay attention
// to the mandatory parameters). Using the Client before connecting have // to the mandatory parameters). Using the Client before connecting have
// been established can lead to a panic. After the work, the Client SHOULD BE // been established can lead to a panic. After the work, the Client SHOULD BE
@ -28,7 +26,7 @@ import (
// during the communication process step strongly discouraged as it leads to // during the communication process step strongly discouraged as it leads to
// undefined behavior. // undefined behavior.
// //
// Each method which produces a FrostFS API call may return a server response. // Each method which produces a NeoFS API call may return a server response.
// Status responses are returned in the result structure, and can be cast // Status responses are returned in the result structure, and can be cast
// to built-in error instance (or in the returned error if the client is // to built-in error instance (or in the returned error if the client is
// configured accordingly). Certain statuses can be checked using `apistatus` // configured accordingly). Certain statuses can be checked using `apistatus`
@ -37,7 +35,6 @@ import (
// All possible responses are documented in methods, however, some may be // All possible responses are documented in methods, however, some may be
// returned from all of them (pay attention to the presence of the pointer sign): // returned from all of them (pay attention to the presence of the pointer sign):
// - *apistatus.ServerInternal on internal server error; // - *apistatus.ServerInternal on internal server error;
// - *apistatus.NodeUnderMaintenance if a server is under maintenance;
// - *apistatus.SuccessDefaultV2 on default success. // - *apistatus.SuccessDefaultV2 on default success.
// //
// Client MUST NOT be copied by value: use pointer to Client instead. // Client MUST NOT be copied by value: use pointer to Client instead.
@ -47,8 +44,6 @@ type Client struct {
prm PrmInit prm PrmInit
c client.Client c client.Client
server frostFSAPIServer
} }
// Init brings the Client instance to its initial state. // Init brings the Client instance to its initial state.
@ -61,15 +56,11 @@ func (c *Client) Init(prm PrmInit) {
c.prm = prm c.prm = prm
} }
// Dial establishes a connection to the server from the FrostFS network. // Dial establishes a connection to the server from the NeoFS network.
// Returns an error describing failure reason. If failed, the Client // Returns an error describing failure reason. If failed, the Client
// SHOULD NOT be used. // SHOULD NOT be used.
// //
// Uses the context specified by SetContext if it was called with non-nil // Panics if required parameters are set incorrectly, look carefully
// argument, otherwise context.Background() is used. Dial returns context
// errors, see context package docs for details.
//
// Returns an error if required parameters are set incorrectly, look carefully
// at the method documentation. // at the method documentation.
// //
// One-time method call during application start-up stage (after Init ) is expected. // One-time method call during application start-up stage (after Init ) is expected.
@ -78,58 +69,29 @@ func (c *Client) Init(prm PrmInit) {
// See also Init / Close. // See also Init / Close.
func (c *Client) Dial(prm PrmDial) error { func (c *Client) Dial(prm PrmDial) error {
if prm.endpoint == "" { if prm.endpoint == "" {
return errorServerAddrUnset panic("server address is unset or empty")
} }
if prm.timeoutDialSet { if prm.timeoutDialSet {
if prm.timeoutDial <= 0 { if prm.timeoutDial <= 0 {
return errorNonPositiveTimeout panic("non-positive timeout")
} }
} else { } else {
prm.timeoutDial = 5 * time.Second prm.timeoutDial = 5 * time.Second
} }
if prm.streamTimeoutSet {
if prm.streamTimeout <= 0 {
return errorNonPositiveTimeout
}
} else {
prm.streamTimeout = 10 * time.Second
}
c.c = *client.New(append( c.c = *client.New(append(
client.WithNetworkURIAddress(prm.endpoint, prm.tlsConfig), client.WithNetworkURIAddress(prm.endpoint, prm.tlsConfig),
client.WithDialTimeout(prm.timeoutDial), client.WithDialTimeout(prm.timeoutDial),
client.WithRWTimeout(prm.streamTimeout),
)...) )...)
c.setFrostFSAPIServer((*coreServer)(&c.c))
if prm.parentCtx == nil {
prm.parentCtx = context.Background()
}
// TODO: (neofs-api-go#382) perform generic dial stage of the client.Client // TODO: (neofs-api-go#382) perform generic dial stage of the client.Client
_, err := rpc.Balance(&c.c, new(v2accounting.BalanceRequest), _, _ = rpc.Balance(&c.c, new(v2accounting.BalanceRequest))
client.WithContext(prm.parentCtx),
)
// return context errors since they signal about dial problem
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return err
}
return nil return nil
} }
// sets underlying provider of frostFSAPIServer. The method is used for testing as an approach // Close closes underlying connection to the NeoFS server. Implements io.Closer.
// to skip Dial stage and override FrostFS API server. MUST NOT be used outside test code.
// In real applications wrapper over git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client
// is statically used.
func (c *Client) setFrostFSAPIServer(server frostFSAPIServer) {
c.server = server
}
// Close closes underlying connection to the FrostFS server. Implements io.Closer.
// MUST NOT be called before successful Dial. Can be called concurrently // MUST NOT be called before successful Dial. Can be called concurrently
// with server operations processing on running goroutines: in this case // with server operations processing on running goroutines: in this case
// they are likely to fail due to a connection error. // they are likely to fail due to a connection error.
@ -146,7 +108,7 @@ func (c *Client) Close() error {
// //
// See also Init. // See also Init.
type PrmInit struct { type PrmInit struct {
resolveFrostFSErrors bool resolveNeoFSErrors bool
key ecdsa.PrivateKey key ecdsa.PrivateKey
@ -163,16 +125,16 @@ func (x *PrmInit) SetDefaultPrivateKey(key ecdsa.PrivateKey) {
x.key = key x.key = key
} }
// ResolveFrostFSFailures makes the Client to resolve failure statuses of the // ResolveNeoFSFailures makes the Client to resolve failure statuses of the
// FrostFS protocol into Go built-in errors. These errors are returned from // NeoFS protocol into Go built-in errors. These errors are returned from
// each protocol operation. By default, statuses aren't resolved and written // each protocol operation. By default, statuses aren't resolved and written
// to the resulting structure (see corresponding Res* docs). // to the resulting structure (see corresponding Res* docs).
func (x *PrmInit) ResolveFrostFSFailures() { func (x *PrmInit) ResolveNeoFSFailures() {
x.resolveFrostFSErrors = true x.resolveNeoFSErrors = true
} }
// SetResponseInfoCallback makes the Client to pass ResponseMetaInfo from each // SetResponseInfoCallback makes the Client to pass ResponseMetaInfo from each
// FrostFS server response to f. Nil (default) means ignore response meta info. // NeoFS server response to f. Nil (default) means ignore response meta info.
func (x *PrmInit) SetResponseInfoCallback(f func(ResponseMetaInfo) error) { func (x *PrmInit) SetResponseInfoCallback(f func(ResponseMetaInfo) error) {
x.cbRespInfo = f x.cbRespInfo = f
} }
@ -187,22 +149,15 @@ type PrmDial struct {
timeoutDialSet bool timeoutDialSet bool
timeoutDial time.Duration timeoutDial time.Duration
streamTimeoutSet bool
streamTimeout time.Duration
parentCtx context.Context
} }
// SetServerURI sets server URI in the FrostFS network. // SetServerURI sets server URI in the NeoFS network.
// Required parameter. // Required parameter.
// //
// Format of the URI: // Format of the URI:
//
// [scheme://]host:port // [scheme://]host:port
// //
// Supported schemes: // Supported schemes:
//
// grpc // grpc
// grpcs // grpcs
// //
@ -212,7 +167,7 @@ func (x *PrmDial) SetServerURI(endpoint string) {
} }
// SetTLSConfig sets tls.Config to open TLS client connection // SetTLSConfig sets tls.Config to open TLS client connection
// to the FrostFS server. Nil (default) means insecure connection. // to the NeoFS server. Nil (default) means insecure connection.
// //
// See also SetServerURI. // See also SetServerURI.
func (x *PrmDial) SetTLSConfig(tlsConfig *tls.Config) { func (x *PrmDial) SetTLSConfig(tlsConfig *tls.Config) {
@ -225,18 +180,3 @@ func (x *PrmDial) SetTimeout(timeout time.Duration) {
x.timeoutDialSet = true x.timeoutDialSet = true
x.timeoutDial = timeout x.timeoutDial = timeout
} }
// SetStreamTimeout sets the timeout for individual operations in streaming RPC.
// MUST BE positive. If not called, 10s timeout will be used by default.
func (x *PrmDial) SetStreamTimeout(timeout time.Duration) {
x.streamTimeoutSet = true
x.streamTimeout = timeout
}
// SetContext allows to specify optional base context within which connection
// should be established.
//
// Context SHOULD NOT be nil.
func (x *PrmDial) SetContext(ctx context.Context) {
x.parentCtx = ctx
}

View file

@ -1,68 +0,0 @@
package client
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"testing"
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
"github.com/stretchr/testify/require"
)
/*
File contains common functionality used for client package testing.
*/
var key, _ = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
var statusErr apistatus.ServerInternal
func init() {
statusErr.SetMessage("test status error")
}
func assertStatusErr(tb testing.TB, res interface{ Status() apistatus.Status }) {
require.IsType(tb, &statusErr, res.Status())
require.Equal(tb, statusErr.Message(), res.Status().(*apistatus.ServerInternal).Message())
}
func newClient(server frostFSAPIServer) *Client {
var prm PrmInit
prm.SetDefaultPrivateKey(*key)
var c Client
c.Init(prm)
c.setFrostFSAPIServer(server)
return &c
}
func TestClient_DialContext(t *testing.T) {
var c Client
// try to connect to any host
var prm PrmDial
prm.SetServerURI("localhost:8080")
assert := func(ctx context.Context, errExpected error) {
// use the particular context
prm.SetContext(ctx)
// expect particular context error according to Dial docs
require.ErrorIs(t, c.Dial(prm), errExpected)
}
// create pre-abandoned context
ctx, cancel := context.WithCancel(context.Background())
cancel()
assert(ctx, context.Canceled)
// create "pre-deadlined" context
ctx, cancel = context.WithTimeout(context.Background(), 0)
defer cancel()
assert(ctx, context.DeadlineExceeded)
}

View file

@ -2,15 +2,14 @@ package client
import ( import (
"crypto/ecdsa" "crypto/ecdsa"
"errors"
"fmt" "fmt"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs" "github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client" v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session" "github.com/nspcc-dev/neofs-api-go/v2/signature"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature" apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status" "github.com/nspcc-dev/neofs-sdk-go/session"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/version" "github.com/nspcc-dev/neofs-sdk-go/version"
) )
// common interface of resulting structures with API status. // common interface of resulting structures with API status.
@ -35,9 +34,26 @@ func (x statusRes) Status() apistatus.Status {
return x.st return x.st
} }
type prmSession struct {
tokenSessionSet bool
tokenSession session.Token
}
// SetSessionToken sets token of the session within which request should be sent.
func (x *prmSession) SetSessionToken(tok session.Token) {
x.tokenSession = tok
x.tokenSessionSet = true
}
func (x prmSession) writeToMetaHeader(meta *v2session.RequestMetaHeader) {
if x.tokenSessionSet {
meta.SetSessionToken(x.tokenSession.ToV2())
}
}
// groups meta parameters shared between all Client operations. // groups meta parameters shared between all Client operations.
type prmCommonMeta struct { type prmCommonMeta struct {
// FrostFS request X-Headers // NeoFS request X-Headers
xHeaders []string xHeaders []string
} }
@ -53,39 +69,23 @@ func (x *prmCommonMeta) WithXHeaders(hs ...string) {
x.xHeaders = hs x.xHeaders = hs
} }
func writeXHeadersToMeta(xHeaders []string, h *v2session.RequestMetaHeader) { func (x prmCommonMeta) writeToMetaHeader(h *v2session.RequestMetaHeader) {
if len(xHeaders) == 0 { if len(x.xHeaders) > 0 {
return hs := make([]v2session.XHeader, len(x.xHeaders)/2)
}
if len(xHeaders)%2 != 0 { for i := 0; i < len(x.xHeaders); i += 2 {
panic("slice of X-Headers with odd length") hs[i].SetKey(x.xHeaders[i])
} hs[i].SetValue(x.xHeaders[i+1])
hs := make([]v2session.XHeader, len(xHeaders)/2)
for i := 0; i < len(xHeaders); i += 2 {
hs[i].SetKey(xHeaders[i])
hs[i].SetValue(xHeaders[i+1])
} }
h.SetXHeaders(hs) h.SetXHeaders(hs)
}
} }
// error messages. // panic messages.
var ( const (
errorMissingContext = errors.New("missing context") panicMsgMissingContext = "missing context"
errorMissingContainer = errors.New("missing container") panicMsgMissingContainer = "missing container"
errorMissingObject = errors.New("missing object")
errorAccountNotSet = errors.New("account not set")
errorServerAddrUnset = errors.New("server address is unset or empty")
errorNonPositiveTimeout = errors.New("non-positive timeout")
errorEACLTableNotSet = errors.New("eACL table not set")
errorMissingAnnouncements = errors.New("missing announcements")
errorZeroRangeLength = errors.New("zero range length")
errorMissingRanges = errors.New("missing ranges")
errorZeroEpoch = errors.New("zero epoch")
errorMissingTrusts = errors.New("missing trusts")
errorTrustNotSet = errors.New("current trust value not set")
) )
// groups all the details required to send a single request and process a response to it. // groups all the details required to send a single request and process a response to it.
@ -111,7 +111,7 @@ type contextCall struct {
// if set, protocol errors will be expanded into a final error // if set, protocol errors will be expanded into a final error
resolveAPIFailures bool resolveAPIFailures bool
// FrostFS network magic // NeoFS network magic
netMagic uint64 netMagic uint64
// Meta parameters // Meta parameters
@ -124,7 +124,11 @@ type contextCall struct {
statusRes resCommon statusRes resCommon
// request to be signed with a key and sent // request to be signed with a key and sent
req request req interface {
GetMetaHeader() *v2session.RequestMetaHeader
SetMetaHeader(*v2session.RequestMetaHeader)
SetVerificationHeader(*v2session.RequestVerificationHeader)
}
// function to send a request (unary) and receive a response // function to send a request (unary) and receive a response
call func() (responseV2, error) call func() (responseV2, error)
@ -142,12 +146,6 @@ type contextCall struct {
result func(v2 responseV2) result func(v2 responseV2)
} }
type request interface {
GetMetaHeader() *v2session.RequestMetaHeader
SetMetaHeader(*v2session.RequestMetaHeader)
SetVerificationHeader(*v2session.RequestVerificationHeader)
}
// sets needed fields of the request meta header. // sets needed fields of the request meta header.
func (x contextCall) prepareRequest() { func (x contextCall) prepareRequest() {
meta := x.req.GetMetaHeader() meta := x.req.GetMetaHeader()
@ -161,33 +159,12 @@ func (x contextCall) prepareRequest() {
} }
if meta.GetVersion() == nil { if meta.GetVersion() == nil {
var verV2 refs.Version meta.SetVersion(version.Current().ToV2())
version.Current().WriteToV2(&verV2)
meta.SetVersion(&verV2)
} }
meta.SetNetworkMagic(x.netMagic) meta.SetNetworkMagic(x.netMagic)
writeXHeadersToMeta(x.meta.xHeaders, meta) x.meta.writeToMetaHeader(meta)
}
func (c *Client) prepareRequest(req request, meta *v2session.RequestMetaHeader) {
ttl := meta.GetTTL()
if ttl == 0 {
ttl = 2
}
verV2 := meta.GetVersion()
if verV2 == nil {
verV2 = new(refs.Version)
version.Current().WriteToV2(verV2)
}
meta.SetTTL(ttl)
meta.SetVersion(verV2)
meta.SetNetworkMagic(c.prm.netMagic)
req.SetMetaHeader(meta)
} }
// prepares, signs and writes the request. Result means success. // prepares, signs and writes the request. Result means success.
@ -217,15 +194,14 @@ func (x *contextCall) writeRequest() bool {
// (in both cases returns false). // (in both cases returns false).
// //
// Actions: // Actions:
// - verify signature (internal); // * verify signature (internal);
// - call response callback (internal); // * call response callback (internal);
// - unwrap status error (optional). // * unwrap status error (optional).
func (x *contextCall) processResponse() bool { func (x *contextCall) processResponse() bool {
// call response callback if set // call response callback if set
if x.callbackResp != nil { if x.callbackResp != nil {
x.err = x.callbackResp(ResponseMetaInfo{ x.err = x.callbackResp(ResponseMetaInfo{
key: x.resp.GetVerificationHeader().GetBodySignature().GetKey(), key: x.resp.GetVerificationHeader().GetBodySignature().GetKey(),
epoch: x.resp.GetMetaHeader().GetEpoch(),
}) })
if x.err != nil { if x.err != nil {
x.err = fmt.Errorf("response callback error: %w", x.err) x.err = fmt.Errorf("response callback error: %w", x.err)
@ -249,32 +225,18 @@ func (x *contextCall) processResponse() bool {
// unwrap unsuccessful status and return it // unwrap unsuccessful status and return it
// as error if client has been configured so // as error if client has been configured so
successfulStatus := apistatus.IsSuccessful(st) successfulStatus := apistatus.IsSuccessful(st)
if !successfulStatus && x.resolveAPIFailures {
if x.resolveAPIFailures {
x.err = apistatus.ErrFromStatus(st) x.err = apistatus.ErrFromStatus(st)
} else { return false
}
x.statusRes.setStatus(st) x.statusRes.setStatus(st)
}
return successfulStatus return successfulStatus || !x.resolveAPIFailures
}
// processResponse verifies response signature and converts status to an error if needed.
func (c *Client) processResponse(resp responseV2) (apistatus.Status, error) {
err := signature.VerifyServiceMessage(resp)
if err != nil {
return nil, fmt.Errorf("invalid response signature: %w", err)
}
st := apistatus.FromStatusV2(resp.GetMetaHeader().GetStatus())
if c.prm.resolveFrostFSErrors {
return st, apistatus.ErrFromStatus(st)
}
return st, nil
} }
// reads response (if rResp is set) and processes it. Result means success. // reads response (if rResp is set) and processes it. Result means success.
// If failed, contextCall.err (or statusRes if resolveAPIFailures is set) contains the reason. // If failed, contextCall.err contains the reason.
func (x *contextCall) readResponse() bool { func (x *contextCall) readResponse() bool {
if x.rResp != nil { if x.rResp != nil {
x.err = x.rResp() x.err = x.rResp()
@ -303,7 +265,7 @@ func (x *contextCall) close() bool {
x.result(x.resp) x.result(x.resp)
} }
return x.err == nil return true
} }
// goes through all stages of sending a request and processing a response. Returns true if successful. // goes through all stages of sending a request and processing a response. Returns true if successful.
@ -325,7 +287,7 @@ func (x *contextCall) processCall() bool {
// read response // read response
ok = x.readResponse() ok = x.readResponse()
if !ok { if !ok {
return x.err == nil return false
} }
// close and write response to resulting structure // close and write response to resulting structure
@ -340,14 +302,19 @@ func (x *contextCall) processCall() bool {
// initializes static cross-call parameters inherited from client. // initializes static cross-call parameters inherited from client.
func (c *Client) initCallContext(ctx *contextCall) { func (c *Client) initCallContext(ctx *contextCall) {
ctx.key = c.prm.key ctx.key = c.prm.key
ctx.resolveAPIFailures = c.prm.resolveFrostFSErrors c.initCallContextWithoutKey(ctx)
}
// initializes static cross-call parameters inherited from client except private key.
func (c *Client) initCallContextWithoutKey(ctx *contextCall) {
ctx.resolveAPIFailures = c.prm.resolveNeoFSErrors
ctx.callbackResp = c.prm.cbRespInfo ctx.callbackResp = c.prm.cbRespInfo
ctx.netMagic = c.prm.netMagic ctx.netMagic = c.prm.netMagic
} }
// ExecRaw executes f with underlying git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client.Client // ExecRaw executes f with underlying github.com/nspcc-dev/neofs-api-go/v2/rpc/client.Client
// instance. Communicate over the Protocol Buffers protocol in a more flexible way: // instance. Communicate over the Protocol Buffers protocol in a more flexible way:
// most often used to transmit data over a fixed version of the FrostFS protocol, as well // most often used to transmit data over a fixed version of the NeoFS protocol, as well
// as to support custom services. // as to support custom services.
// //
// The f must not manipulate the client connection passed into it. // The f must not manipulate the client connection passed into it.
@ -356,7 +323,7 @@ func (c *Client) initCallContext(ctx *contextCall) {
// before closing the connection. // before closing the connection.
// //
// See also Dial and Close. // See also Dial and Close.
// See also git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client package docs. // See also github.com/nspcc-dev/neofs-api-go/v2/rpc/client package docs.
func (c *Client) ExecRaw(f func(client *client.Client) error) error { func (c *Client) ExecRaw(f func(client *client.Client) error) error {
return f(&c.c) return f(&c.c)
} }

View file

@ -2,21 +2,19 @@ package client
import ( import (
"context" "context"
"errors"
"fmt"
v2container "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container" v2container "github.com/nspcc-dev/neofs-api-go/v2/container"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs" rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc" "github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client" v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session" v2signature "github.com/nspcc-dev/neofs-api-go/v2/signature"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container" "github.com/nspcc-dev/neofs-sdk-go/container"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto" "github.com/nspcc-dev/neofs-sdk-go/eacl"
frostfsecdsa "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto/ecdsa" "github.com/nspcc-dev/neofs-sdk-go/owner"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl" "github.com/nspcc-dev/neofs-sdk-go/session"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session" "github.com/nspcc-dev/neofs-sdk-go/signature"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user" sigutil "github.com/nspcc-dev/neofs-sdk-go/util/signature"
) )
// PrmContainerPut groups parameters of ContainerPut operation. // PrmContainerPut groups parameters of ContainerPut operation.
@ -25,51 +23,41 @@ type PrmContainerPut struct {
cnrSet bool cnrSet bool
cnr container.Container cnr container.Container
sessionSet bool
session session.Container
} }
// SetContainer sets structured information about new FrostFS container. // SetContainer sets structured information about new NeoFS container.
// Required parameter. // Required parameter.
func (x *PrmContainerPut) SetContainer(cnr container.Container) { func (x *PrmContainerPut) SetContainer(cnr container.Container) {
x.cnr = cnr x.cnr = cnr
x.cnrSet = true x.cnrSet = true
} }
// WithinSession specifies session within which container should be saved.
//
// Creator of the session acquires the authorship of the request. This affects
// the execution of an operation (e.g. access control).
//
// Session is optional, if set the following requirements apply:
// - session operation MUST be session.VerbContainerPut (ForVerb)
// - token MUST be signed using private key of the owner of the container to be saved
func (x *PrmContainerPut) WithinSession(s session.Container) {
x.session = s
x.sessionSet = true
}
// ResContainerPut groups resulting values of ContainerPut operation. // ResContainerPut groups resulting values of ContainerPut operation.
type ResContainerPut struct { type ResContainerPut struct {
statusRes statusRes
id cid.ID id *cid.ID
} }
// ID returns identifier of the container declared to be stored in the system. // ID returns identifier of the container declared to be stored in the system.
// Used as a link to information about the container (in particular, you can // Used as a link to information about the container (in particular, you can
// asynchronously check if the save was successful). // asynchronously check if the save was successful).
func (x ResContainerPut) ID() cid.ID { //
// Client doesn't retain value so modification is safe.
func (x ResContainerPut) ID() *cid.ID {
return x.id return x.id
} }
// ContainerPut sends request to save container in FrostFS. func (x *ResContainerPut) setID(id *cid.ID) {
x.id = id
}
// ContainerPut sends request to save container in NeoFS.
// //
// Exactly one return value is non-nil. By default, server status is returned in res structure. // Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as `error`. // Any client's internal or transport errors are returned as `error`.
// If PrmInit.ResolveFrostFSFailures has been called, unsuccessful // If WithNeoFSErrorParsing option has been provided, unsuccessful
// FrostFS status codes are returned as `error`, otherwise, are included // NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure. // in the returned result structure.
// //
// Operation is asynchronous and no guaranteed even in the absence of errors. // Operation is asynchronous and no guaranteed even in the absence of errors.
@ -77,7 +65,7 @@ func (x ResContainerPut) ID() cid.ID {
// //
// Success can be verified by reading by identifier (see ResContainerPut.ID). // Success can be verified by reading by identifier (see ResContainerPut.ID).
// //
// Returns an error if parameters are set incorrectly (see PrmContainerPut docs). // Immediately panics if parameters are set incorrectly (see PrmContainerPut docs).
// Context is required and must not be nil. It is used for network communication. // Context is required and must not be nil. It is used for network communication.
// //
// Return statuses: // Return statuses:
@ -86,42 +74,31 @@ func (c *Client) ContainerPut(ctx context.Context, prm PrmContainerPut) (*ResCon
// check parameters // check parameters
switch { switch {
case ctx == nil: case ctx == nil:
return nil, errorMissingContext panic(panicMsgMissingContext)
case !prm.cnrSet: case !prm.cnrSet:
return nil, errorMissingContainer panic(panicMsgMissingContainer)
} }
// TODO: check private key is set before forming the request // TODO: check private key is set before forming the request
// sign container
var cnr v2container.Container
prm.cnr.WriteToV2(&cnr)
var sig frostfscrypto.Signature
err := container.CalculateSignature(&sig, prm.cnr, c.prm.key)
if err != nil {
return nil, fmt.Errorf("calculate container signature: %w", err)
}
var sigv2 refs.Signature
sig.WriteToV2(&sigv2)
// form request body // form request body
reqBody := new(v2container.PutRequestBody) reqBody := new(v2container.PutRequestBody)
reqBody.SetContainer(&cnr) reqBody.SetContainer(prm.cnr.ToV2())
reqBody.SetSignature(&sigv2)
// sign container
signWrapper := v2signature.StableMarshalerWrapper{SM: reqBody.GetContainer()}
sig, err := sigutil.SignData(&c.prm.key, signWrapper, sigutil.SignWithRFC6979())
if err != nil {
return nil, err
}
reqBody.SetSignature(sig.ToV2())
// form meta header // form meta header
var meta v2session.RequestMetaHeader var meta v2session.RequestMetaHeader
writeXHeadersToMeta(prm.prmCommonMeta.xHeaders, &meta) meta.SetSessionToken(prm.cnr.SessionToken().ToV2())
prm.prmCommonMeta.writeToMetaHeader(&meta)
if prm.sessionSet {
var tokv2 v2session.Token
prm.session.WriteToV2(&tokv2)
meta.SetSessionToken(&tokv2)
}
// form request // form request
var req v2container.PutRequest var req v2container.PutRequest
@ -144,19 +121,7 @@ func (c *Client) ContainerPut(ctx context.Context, prm PrmContainerPut) (*ResCon
} }
cc.result = func(r responseV2) { cc.result = func(r responseV2) {
resp := r.(*v2container.PutResponse) resp := r.(*v2container.PutResponse)
res.setID(cid.NewFromV2(resp.GetBody().GetContainerID()))
const fieldCnrID = "container ID"
cidV2 := resp.GetBody().GetContainerID()
if cidV2 == nil {
cc.err = newErrMissingResponseField(fieldCnrID)
return
}
cc.err = res.id.ReadFromV2(*cidV2)
if cc.err != nil {
cc.err = newErrInvalidResponseField(fieldCnrID, cc.err)
}
} }
// process call // process call
@ -186,25 +151,29 @@ func (x *PrmContainerGet) SetContainer(id cid.ID) {
type ResContainerGet struct { type ResContainerGet struct {
statusRes statusRes
cnr container.Container cnr *container.Container
} }
// Container returns structured information about the requested container. // Container returns structured information about the requested container.
// //
// Client doesn't retain value so modification is safe. // Client doesn't retain value so modification is safe.
func (x ResContainerGet) Container() container.Container { func (x ResContainerGet) Container() *container.Container {
return x.cnr return x.cnr
} }
// ContainerGet reads FrostFS container by ID. func (x *ResContainerGet) setContainer(cnr *container.Container) {
x.cnr = cnr
}
// ContainerGet reads NeoFS container by ID.
// //
// Exactly one return value is non-nil. By default, server status is returned in res structure. // Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as `error`. // Any client's internal or transport errors are returned as `error`.
// If PrmInit.ResolveFrostFSFailures has been called, unsuccessful // If WithNeoFSErrorParsing option has been provided, unsuccessful
// FrostFS status codes are returned as `error`, otherwise, are included // NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure. // in the returned result structure.
// //
// Returns an error if parameters are set incorrectly (see PrmContainerGet docs). // Immediately panics if parameters are set incorrectly (see PrmContainerGet docs).
// Context is required and must not be nil. It is used for network communication. // Context is required and must not be nil. It is used for network communication.
// //
// Return statuses: // Return statuses:
@ -213,17 +182,14 @@ func (x ResContainerGet) Container() container.Container {
func (c *Client) ContainerGet(ctx context.Context, prm PrmContainerGet) (*ResContainerGet, error) { func (c *Client) ContainerGet(ctx context.Context, prm PrmContainerGet) (*ResContainerGet, error) {
switch { switch {
case ctx == nil: case ctx == nil:
return nil, errorMissingContext panic(panicMsgMissingContext)
case !prm.idSet: case !prm.idSet:
return nil, errorMissingContainer panic(panicMsgMissingContainer)
} }
var cidV2 refs.ContainerID
prm.id.WriteToV2(&cidV2)
// form request body // form request body
reqBody := new(v2container.GetRequestBody) reqBody := new(v2container.GetRequestBody)
reqBody.SetContainerID(&cidV2) reqBody.SetContainerID(prm.id.ToV2())
// form request // form request
var req v2container.GetRequest var req v2container.GetRequest
@ -247,16 +213,19 @@ func (c *Client) ContainerGet(ctx context.Context, prm PrmContainerGet) (*ResCon
cc.result = func(r responseV2) { cc.result = func(r responseV2) {
resp := r.(*v2container.GetResponse) resp := r.(*v2container.GetResponse)
cnrV2 := resp.GetBody().GetContainer() body := resp.GetBody()
if cnrV2 == nil {
cc.err = errors.New("missing container in response")
return
}
cc.err = res.cnr.ReadFromV2(*cnrV2) cnr := container.NewContainerFromV2(body.GetContainer())
if cc.err != nil {
cc.err = fmt.Errorf("invalid container in response: %w", cc.err) cnr.SetSessionToken(
} session.NewTokenFromV2(body.GetSessionToken()),
)
cnr.SetSignature(
signature.NewFromV2(body.GetSignature()),
)
res.setContainer(cnr)
} }
// process call // process call
@ -272,12 +241,12 @@ type PrmContainerList struct {
prmCommonMeta prmCommonMeta
ownerSet bool ownerSet bool
ownerID user.ID ownerID owner.ID
} }
// SetAccount sets identifier of the FrostFS account to list the containers. // SetAccount sets identifier of the NeoFS account to list the containers.
// Required parameter. // Required parameter. Must be a valid ID according to NeoFS API protocol.
func (x *PrmContainerList) SetAccount(id user.ID) { func (x *PrmContainerList) SetAccount(id owner.ID) {
x.ownerID = id x.ownerID = id
x.ownerSet = true x.ownerSet = true
} }
@ -296,15 +265,19 @@ func (x ResContainerList) Containers() []cid.ID {
return x.ids return x.ids
} }
func (x *ResContainerList) setContainers(ids []cid.ID) {
x.ids = ids
}
// ContainerList requests identifiers of the account-owned containers. // ContainerList requests identifiers of the account-owned containers.
// //
// Exactly one return value is non-nil. By default, server status is returned in res structure. // Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as `error`. // Any client's internal or transport errors are returned as `error`.
// If PrmInit.ResolveFrostFSFailures has been called, unsuccessful // If WithNeoFSErrorParsing option has been provided, unsuccessful
// FrostFS status codes are returned as `error`, otherwise, are included // NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure. // in the returned result structure.
// //
// Returns an error if parameters are set incorrectly (see PrmContainerList docs). // Immediately panics if parameters are set incorrectly (see PrmContainerList docs).
// Context is required and must not be nil. It is used for network communication. // Context is required and must not be nil. It is used for network communication.
// //
// Return statuses: // Return statuses:
@ -313,17 +286,16 @@ func (c *Client) ContainerList(ctx context.Context, prm PrmContainerList) (*ResC
// check parameters // check parameters
switch { switch {
case ctx == nil: case ctx == nil:
return nil, errorMissingContext panic(panicMsgMissingContext)
case !prm.ownerSet: case !prm.ownerSet:
return nil, errorAccountNotSet panic("account not set")
case !prm.ownerID.Valid():
panic("invalid account")
} }
// form request body // form request body
var ownerV2 refs.OwnerID
prm.ownerID.WriteToV2(&ownerV2)
reqBody := new(v2container.ListRequestBody) reqBody := new(v2container.ListRequestBody)
reqBody.SetOwnerID(&ownerV2) reqBody.SetOwnerID(prm.ownerID.ToV2())
// form request // form request
var req v2container.ListRequest var req v2container.ListRequest
@ -347,15 +319,13 @@ func (c *Client) ContainerList(ctx context.Context, prm PrmContainerList) (*ResC
cc.result = func(r responseV2) { cc.result = func(r responseV2) {
resp := r.(*v2container.ListResponse) resp := r.(*v2container.ListResponse)
res.ids = make([]cid.ID, len(resp.GetBody().GetContainerIDs())) ids := make([]cid.ID, len(resp.GetBody().GetContainerIDs()))
for i, cidV2 := range resp.GetBody().GetContainerIDs() { for i, cidV2 := range resp.GetBody().GetContainerIDs() {
cc.err = res.ids[i].ReadFromV2(cidV2) ids[i] = *cid.NewFromV2(&cidV2)
if cc.err != nil {
cc.err = fmt.Errorf("invalid ID in the response: %w", cc.err)
return
}
} }
res.setContainers(ids)
} }
// process call // process call
@ -369,43 +339,43 @@ func (c *Client) ContainerList(ctx context.Context, prm PrmContainerList) (*ResC
// PrmContainerDelete groups parameters of ContainerDelete operation. // PrmContainerDelete groups parameters of ContainerDelete operation.
type PrmContainerDelete struct { type PrmContainerDelete struct {
prmCommonMeta prmCommonMeta
prmSession
idSet bool idSet bool
id cid.ID id cid.ID
tokSet bool
tok session.Container
} }
// SetContainer sets identifier of the FrostFS container to be removed. // SetContainer sets identifier of the NeoFS container to be removed.
// Required parameter. // Required parameter.
func (x *PrmContainerDelete) SetContainer(id cid.ID) { func (x *PrmContainerDelete) SetContainer(id cid.ID) {
x.id = id x.id = id
x.idSet = true x.idSet = true
} }
// WithinSession specifies session within which container should be removed.
//
// Creator of the session acquires the authorship of the request.
// This may affect the execution of an operation (e.g. access control).
//
// Must be signed.
func (x *PrmContainerDelete) WithinSession(tok session.Container) {
x.tok = tok
x.tokSet = true
}
// ResContainerDelete groups resulting values of ContainerDelete operation. // ResContainerDelete groups resulting values of ContainerDelete operation.
type ResContainerDelete struct { type ResContainerDelete struct {
statusRes statusRes
} }
// ContainerDelete sends request to remove the FrostFS container. // implements github.com/nspcc-dev/neofs-sdk-go/util/signature.DataSource.
type delContainerSignWrapper struct {
body *v2container.DeleteRequestBody
}
func (c delContainerSignWrapper) ReadSignedData([]byte) ([]byte, error) {
return c.body.GetContainerID().GetValue(), nil
}
func (c delContainerSignWrapper) SignedDataSize() int {
return len(c.body.GetContainerID().GetValue())
}
// ContainerDelete sends request to remove the NeoFS container.
// //
// Exactly one return value is non-nil. By default, server status is returned in res structure. // Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as `error`. // Any client's internal or transport errors are returned as `error`.
// If PrmInit.ResolveFrostFSFailures has been called, unsuccessful // If WithNeoFSErrorParsing option has been provided, unsuccessful
// FrostFS status codes are returned as `error`, otherwise, are included // NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure. // in the returned result structure.
// //
// Operation is asynchronous and no guaranteed even in the absence of errors. // Operation is asynchronous and no guaranteed even in the absence of errors.
@ -413,7 +383,7 @@ type ResContainerDelete struct {
// //
// Success can be verified by reading by identifier (see GetContainer). // Success can be verified by reading by identifier (see GetContainer).
// //
// Returns an error if parameters are set incorrectly (see PrmContainerDelete docs). // Immediately panics if parameters are set incorrectly (see PrmContainerDelete docs).
// Context is required and must not be nil. It is used for network communication. // Context is required and must not be nil. It is used for network communication.
// //
// Exactly one return value is non-nil. Server status return is returned in ResContainerDelete. // Exactly one return value is non-nil. Server status return is returned in ResContainerDelete.
@ -425,45 +395,30 @@ func (c *Client) ContainerDelete(ctx context.Context, prm PrmContainerDelete) (*
// check parameters // check parameters
switch { switch {
case ctx == nil: case ctx == nil:
return nil, errorMissingContext panic(panicMsgMissingContext)
case !prm.idSet: case !prm.idSet:
return nil, errorMissingContainer panic(panicMsgMissingContainer)
} }
// sign container ID
var cidV2 refs.ContainerID
prm.id.WriteToV2(&cidV2)
// container contract expects signature of container ID value
// don't get confused with stable marshaled protobuf container.ID structure
data := cidV2.GetValue()
var sig frostfscrypto.Signature
err := sig.Calculate(frostfsecdsa.SignerRFC6979(c.prm.key), data)
if err != nil {
return nil, fmt.Errorf("calculate signature: %w", err)
}
var sigv2 refs.Signature
sig.WriteToV2(&sigv2)
// form request body // form request body
reqBody := new(v2container.DeleteRequestBody) reqBody := new(v2container.DeleteRequestBody)
reqBody.SetContainerID(&cidV2) reqBody.SetContainerID(prm.id.ToV2())
reqBody.SetSignature(&sigv2)
signWrapper := delContainerSignWrapper{body: reqBody}
// sign container
sig, err := sigutil.SignData(&c.prm.key, signWrapper, sigutil.SignWithRFC6979())
if err != nil {
return nil, err
}
reqBody.SetSignature(sig.ToV2())
// form meta header // form meta header
var meta v2session.RequestMetaHeader var meta v2session.RequestMetaHeader
writeXHeadersToMeta(prm.prmCommonMeta.xHeaders, &meta)
if prm.tokSet { prm.prmSession.writeToMetaHeader(&meta)
var tokv2 v2session.Token prm.prmCommonMeta.writeToMetaHeader(&meta)
prm.tok.WriteToV2(&tokv2)
meta.SetSessionToken(&tokv2)
}
// form request // form request
var req v2container.DeleteRequest var req v2container.DeleteRequest
@ -501,7 +456,7 @@ type PrmContainerEACL struct {
id cid.ID id cid.ID
} }
// SetContainer sets identifier of the FrostFS container to read the eACL table. // SetContainer sets identifier of the NeoFS container to read the eACL table.
// Required parameter. // Required parameter.
func (x *PrmContainerEACL) SetContainer(id cid.ID) { func (x *PrmContainerEACL) SetContainer(id cid.ID) {
x.id = id x.id = id
@ -512,44 +467,46 @@ func (x *PrmContainerEACL) SetContainer(id cid.ID) {
type ResContainerEACL struct { type ResContainerEACL struct {
statusRes statusRes
table eacl.Table table *eacl.Table
} }
// Table returns eACL table of the requested container. // Table returns eACL table of the requested container.
func (x ResContainerEACL) Table() eacl.Table { //
// Client doesn't retain value so modification is safe.
func (x ResContainerEACL) Table() *eacl.Table {
return x.table return x.table
} }
// ContainerEACL reads eACL table of the FrostFS container. func (x *ResContainerEACL) setTable(table *eacl.Table) {
x.table = table
}
// ContainerEACL reads eACL table of the NeoFS container.
// //
// Exactly one return value is non-nil. By default, server status is returned in res structure. // Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as `error`. // Any client's internal or transport errors are returned as `error`.
// If PrmInit.ResolveFrostFSFailures has been called, unsuccessful // If WithNeoFSErrorParsing option has been provided, unsuccessful
// FrostFS status codes are returned as `error`, otherwise, are included // NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure. // in the returned result structure.
// //
// Returns an error if parameters are set incorrectly (see PrmContainerEACL docs). // Immediately panics if parameters are set incorrectly (see PrmContainerEACL docs).
// Context is required and must not be nil. It is used for network communication. // Context is required and must not be nil. It is used for network communication.
// //
// Return statuses: // Return statuses:
// - global (see Client docs); // - global (see Client docs);
// - *apistatus.ContainerNotFound; // - *apistatus.ContainerNotFound.
// - *apistatus.EACLNotFound.
func (c *Client) ContainerEACL(ctx context.Context, prm PrmContainerEACL) (*ResContainerEACL, error) { func (c *Client) ContainerEACL(ctx context.Context, prm PrmContainerEACL) (*ResContainerEACL, error) {
// check parameters // check parameters
switch { switch {
case ctx == nil: case ctx == nil:
return nil, errorMissingContext panic(panicMsgMissingContext)
case !prm.idSet: case !prm.idSet:
return nil, errorMissingContainer panic(panicMsgMissingContainer)
} }
var cidV2 refs.ContainerID
prm.id.WriteToV2(&cidV2)
// form request body // form request body
reqBody := new(v2container.GetExtendedACLRequestBody) reqBody := new(v2container.GetExtendedACLRequestBody)
reqBody.SetContainerID(&cidV2) reqBody.SetContainerID(prm.id.ToV2())
// form request // form request
var req v2container.GetExtendedACLRequest var req v2container.GetExtendedACLRequest
@ -573,13 +530,19 @@ func (c *Client) ContainerEACL(ctx context.Context, prm PrmContainerEACL) (*ResC
cc.result = func(r responseV2) { cc.result = func(r responseV2) {
resp := r.(*v2container.GetExtendedACLResponse) resp := r.(*v2container.GetExtendedACLResponse)
eACL := resp.GetBody().GetEACL() body := resp.GetBody()
if eACL == nil {
cc.err = newErrMissingResponseField("eACL")
return
}
res.table = *eacl.NewTableFromV2(eACL) table := eacl.NewTableFromV2(body.GetEACL())
table.SetSessionToken(
session.NewTokenFromV2(body.GetSessionToken()),
)
table.SetSignature(
signature.NewFromV2(body.GetSignature()),
)
res.setTable(table)
} }
// process call // process call
@ -596,9 +559,6 @@ type PrmContainerSetEACL struct {
tableSet bool tableSet bool
table eacl.Table table eacl.Table
sessionSet bool
session session.Container
} }
// SetTable sets eACL table structure to be set for the container. // SetTable sets eACL table structure to be set for the container.
@ -608,33 +568,17 @@ func (x *PrmContainerSetEACL) SetTable(table eacl.Table) {
x.tableSet = true x.tableSet = true
} }
// WithinSession specifies session within which extended ACL of the container
// should be saved.
//
// Creator of the session acquires the authorship of the request. This affects
// the execution of an operation (e.g. access control).
//
// Session is optional, if set the following requirements apply:
// - if particular container is specified (ApplyOnlyTo), it MUST equal the container
// for which extended ACL is going to be set
// - session operation MUST be session.VerbContainerSetEACL (ForVerb)
// - token MUST be signed using private key of the owner of the container to be saved
func (x *PrmContainerSetEACL) WithinSession(s session.Container) {
x.session = s
x.sessionSet = true
}
// ResContainerSetEACL groups resulting values of ContainerSetEACL operation. // ResContainerSetEACL groups resulting values of ContainerSetEACL operation.
type ResContainerSetEACL struct { type ResContainerSetEACL struct {
statusRes statusRes
} }
// ContainerSetEACL sends request to update eACL table of the FrostFS container. // ContainerSetEACL sends request to update eACL table of the NeoFS container.
// //
// Exactly one return value is non-nil. By default, server status is returned in res structure. // Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as `error`. // Any client's internal or transport errors are returned as `error`.
// If PrmInit.ResolveFrostFSFailures has been called, unsuccessful // If WithNeoFSErrorParsing option has been provided, unsuccessful
// FrostFS status codes are returned as `error`, otherwise, are included // NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure. // in the returned result structure.
// //
// Operation is asynchronous and no guaranteed even in the absence of errors. // Operation is asynchronous and no guaranteed even in the absence of errors.
@ -642,7 +586,7 @@ type ResContainerSetEACL struct {
// //
// Success can be verified by reading by identifier (see EACL). // Success can be verified by reading by identifier (see EACL).
// //
// Returns an error if parameters are set incorrectly (see PrmContainerSetEACL docs). // Immediately panics if parameters are set incorrectly (see PrmContainerSetEACL docs).
// Context is required and must not be nil. It is used for network communication. // Context is required and must not be nil. It is used for network communication.
// //
// Return statuses: // Return statuses:
@ -651,40 +595,29 @@ func (c *Client) ContainerSetEACL(ctx context.Context, prm PrmContainerSetEACL)
// check parameters // check parameters
switch { switch {
case ctx == nil: case ctx == nil:
return nil, errorMissingContext panic(panicMsgMissingContext)
case !prm.tableSet: case !prm.tableSet:
return nil, errorEACLTableNotSet panic("eACL table not set")
} }
// sign the eACL table
eaclV2 := prm.table.ToV2()
var sig frostfscrypto.Signature
err := sig.Calculate(frostfsecdsa.SignerRFC6979(c.prm.key), eaclV2.StableMarshal(nil))
if err != nil {
return nil, fmt.Errorf("calculate signature: %w", err)
}
var sigv2 refs.Signature
sig.WriteToV2(&sigv2)
// form request body // form request body
reqBody := new(v2container.SetExtendedACLRequestBody) reqBody := new(v2container.SetExtendedACLRequestBody)
reqBody.SetEACL(eaclV2) reqBody.SetEACL(prm.table.ToV2())
reqBody.SetSignature(&sigv2)
// sign the eACL table
signWrapper := v2signature.StableMarshalerWrapper{SM: reqBody.GetEACL()}
sig, err := sigutil.SignData(&c.prm.key, signWrapper, sigutil.SignWithRFC6979())
if err != nil {
return nil, err
}
reqBody.SetSignature(sig.ToV2())
// form meta header // form meta header
var meta v2session.RequestMetaHeader var meta v2session.RequestMetaHeader
writeXHeadersToMeta(prm.prmCommonMeta.xHeaders, &meta) meta.SetSessionToken(prm.table.SessionToken().ToV2())
prm.prmCommonMeta.writeToMetaHeader(&meta)
if prm.sessionSet {
var tokv2 v2session.Token
prm.session.WriteToV2(&tokv2)
meta.SetSessionToken(&tokv2)
}
// form request // form request
var req v2container.SetExtendedACLRequest var req v2container.SetExtendedACLRequest
@ -718,15 +651,15 @@ func (c *Client) ContainerSetEACL(ctx context.Context, prm PrmContainerSetEACL)
type PrmAnnounceSpace struct { type PrmAnnounceSpace struct {
prmCommonMeta prmCommonMeta
announcements []container.SizeEstimation announcements []container.UsedSpaceAnnouncement
} }
// SetValues sets values describing volume of space that is used for the container objects. // SetValues sets values describing volume of space that is used for the container objects.
// Required parameter. Must not be empty. // Required parameter. Must not be empty.
// //
// Must not be mutated before the end of the operation. // Must not be mutated before the end of the operation.
func (x *PrmAnnounceSpace) SetValues(vs []container.SizeEstimation) { func (x *PrmAnnounceSpace) SetValues(announcements []container.UsedSpaceAnnouncement) {
x.announcements = vs x.announcements = announcements
} }
// ResAnnounceSpace groups resulting values of ContainerAnnounceUsedSpace operation. // ResAnnounceSpace groups resulting values of ContainerAnnounceUsedSpace operation.
@ -738,8 +671,8 @@ type ResAnnounceSpace struct {
// //
// Exactly one return value is non-nil. By default, server status is returned in res structure. // Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as `error`. // Any client's internal or transport errors are returned as `error`.
// If PrmInit.ResolveFrostFSFailures has been called, unsuccessful // If WithNeoFSErrorParsing option has been provided, unsuccessful
// FrostFS status codes are returned as `error`, otherwise, are included // NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure. // in the returned result structure.
// //
// Operation is asynchronous and no guaranteed even in the absence of errors. // Operation is asynchronous and no guaranteed even in the absence of errors.
@ -747,7 +680,7 @@ type ResAnnounceSpace struct {
// //
// At this moment success can not be checked. // At this moment success can not be checked.
// //
// Returns an error if parameters are set incorrectly (see PrmAnnounceSpace docs). // Immediately panics if parameters are set incorrectly (see PrmAnnounceSpace docs).
// Context is required and must not be nil. It is used for network communication. // Context is required and must not be nil. It is used for network communication.
// //
// Return statuses: // Return statuses:
@ -756,18 +689,18 @@ func (c *Client) ContainerAnnounceUsedSpace(ctx context.Context, prm PrmAnnounce
// check parameters // check parameters
switch { switch {
case ctx == nil: case ctx == nil:
return nil, errorMissingContext panic(panicMsgMissingContext)
case len(prm.announcements) == 0: case len(prm.announcements) == 0:
return nil, errorMissingAnnouncements panic("missing announcements")
} }
// convert list of SDK announcement structures into FrostFS-API v2 list // convert list of SDK announcement structures into NeoFS-API v2 list
v2announce := make([]v2container.UsedSpaceAnnouncement, len(prm.announcements)) v2announce := make([]v2container.UsedSpaceAnnouncement, len(prm.announcements))
for i := range prm.announcements { for i := range prm.announcements {
prm.announcements[i].WriteToV2(&v2announce[i]) v2announce[i] = *prm.announcements[i].ToV2()
} }
// prepare body of the FrostFS-API v2 request and request itself // prepare body of the NeoFS-API v2 request and request itself
reqBody := new(v2container.AnnounceUsedSpaceRequestBody) reqBody := new(v2container.AnnounceUsedSpaceRequestBody)
reqBody.SetAnnouncements(v2announce) reqBody.SetAnnouncements(v2announce)
@ -798,23 +731,3 @@ func (c *Client) ContainerAnnounceUsedSpace(ctx context.Context, prm PrmAnnounce
return &res, nil return &res, nil
} }
// SyncContainerWithNetwork requests network configuration using passed client
// and applies it to the container. Container MUST not be nil.
//
// Note: if container does not match network configuration, SyncContainerWithNetwork
// changes it.
//
// Returns any network/parsing config errors.
//
// See also NetworkInfo, container.ApplyNetworkConfig.
func SyncContainerWithNetwork(ctx context.Context, cnr *container.Container, c *Client) error {
res, err := c.NetworkInfo(ctx, PrmNetworkInfo{})
if err != nil {
return fmt.Errorf("network info call: %w", err)
}
container.ApplyNetworkConfig(cnr, res.Info())
return nil
}

View file

@ -1,23 +1,20 @@
/* /*
Package client provides FrostFS API client implementation. Package client provides NeoFS API client implementation.
The main component is Client type. It is a virtual connection to the network The main component is Client type. It is a virtual connection to the network
and provides methods for executing operations on the server. and provides methods for executing operations on the server.
Create client instance: Create client instance:
var c client.Client var c client.Client
Initialize client state: Initialize client state:
var prm client.PrmInit var prm client.PrmInit
prm.SetDefaultPrivateKey(key) prm.SetDefaultPrivateKey(key)
// ... // ...
c.Init(prm) c.Init(prm)
Connect to the FrostFS server: Connect to the NeoFS server:
var prm client.PrmDial var prm client.PrmDial
prm.SetServerURI("localhost:8080") prm.SetServerURI("localhost:8080")
prm.SetDefaultPrivateKey(key) prm.SetDefaultPrivateKey(key)
@ -26,8 +23,7 @@ Connect to the FrostFS server:
err := c.Dial(prm) err := c.Dial(prm)
// ... // ...
Execute FrostFS operation on the server: Execute NeoFS operation on the server:
var prm client.PrmContainerPut var prm client.PrmContainerPut
prm.SetContainer(cnr) prm.SetContainer(cnr)
// ... // ...
@ -40,15 +36,14 @@ Execute FrostFS operation on the server:
// ... // ...
Consume custom service of the server: Consume custom service of the server:
syntax = "proto3"; syntax = "proto3";
service CustomService { service CustomService {
rpc CustomRPC(CustomRPCRequest) returns (CustomRPCResponse); rpc CustomRPC(CustomRPCRequest) returns (CustomRPCResponse);
} }
import "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client" import "github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
import "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/common" import "github.com/nspcc-dev/neofs-api-go/v2/rpc/common"
req := new(CustomRPCRequest) req := new(CustomRPCRequest)
// ... // ...
@ -63,7 +58,6 @@ Consume custom service of the server:
// ... // ...
Close the connection: Close the connection:
err := c.Close() err := c.Close()
// ... // ...
@ -71,10 +65,9 @@ Note that it's not allowed to override Client behaviour directly: the parameters
for the all operations are write-only and the results of the all operations are for the all operations are write-only and the results of the all operations are
read-only. To be able to override client behavior (e.g. for tests), abstract it read-only. To be able to override client behavior (e.g. for tests), abstract it
with an interface: with an interface:
import "github.com/nspcc-dev/neofs-sdk-go/client"
import "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client" type NeoFSClient interface {
type FrostFSClient interface {
// Operations according to the application needs // Operations according to the application needs
CreateContainer(context.Context, container.Container) error CreateContainer(context.Context, container.Container) error
// ... // ...
@ -87,5 +80,6 @@ with an interface:
func (x *client) CreateContainer(context.Context, container.Container) error { func (x *client) CreateContainer(context.Context, container.Container) error {
// ... // ...
} }
*/ */
package client package client

View file

@ -1,25 +1,11 @@
package client package client
import ( import apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
"errors"
"fmt"
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status" // IsErrContainerNotFound checks if err corresponds to NeoFS status
) // return corresponding to missing container.
// unwraps err using errors.Unwrap and returns the result.
func unwrapErr(err error) error {
for e := errors.Unwrap(err); e != nil; e = errors.Unwrap(err) {
err = e
}
return err
}
// IsErrContainerNotFound checks if err corresponds to FrostFS status
// return corresponding to missing container. Supports wrapped errors.
func IsErrContainerNotFound(err error) bool { func IsErrContainerNotFound(err error) bool {
switch unwrapErr(err).(type) { switch err.(type) {
default: default:
return false return false
case case
@ -29,23 +15,10 @@ func IsErrContainerNotFound(err error) bool {
} }
} }
// IsErrEACLNotFound checks if err corresponds to FrostFS status // IsErrObjectNotFound checks if err corresponds to NeoFS status
// return corresponding to missing eACL table. Supports wrapped errors. // return corresponding to missing object.
func IsErrEACLNotFound(err error) bool {
switch unwrapErr(err).(type) {
default:
return false
case
apistatus.EACLNotFound,
*apistatus.EACLNotFound:
return true
}
}
// IsErrObjectNotFound checks if err corresponds to FrostFS status
// return corresponding to missing object. Supports wrapped errors.
func IsErrObjectNotFound(err error) bool { func IsErrObjectNotFound(err error) bool {
switch unwrapErr(err).(type) { switch err.(type) {
default: default:
return false return false
case case
@ -55,10 +28,10 @@ func IsErrObjectNotFound(err error) bool {
} }
} }
// IsErrObjectAlreadyRemoved checks if err corresponds to FrostFS status // IsErrObjectAlreadyRemoved checks if err corresponds to NeoFS status
// return corresponding to already removed object. Supports wrapped errors. // return corresponding to already removed object.
func IsErrObjectAlreadyRemoved(err error) bool { func IsErrObjectAlreadyRemoved(err error) bool {
switch unwrapErr(err).(type) { switch err.(type) {
default: default:
return false return false
case case
@ -67,40 +40,3 @@ func IsErrObjectAlreadyRemoved(err error) bool {
return true return true
} }
} }
// IsErrSessionExpired checks if err corresponds to FrostFS status return
// corresponding to expired session. Supports wrapped errors.
func IsErrSessionExpired(err error) bool {
switch unwrapErr(err).(type) {
default:
return false
case
apistatus.SessionTokenExpired,
*apistatus.SessionTokenExpired:
return true
}
}
// IsErrSessionNotFound checks if err corresponds to FrostFS status return
// corresponding to missing session. Supports wrapped errors.
func IsErrSessionNotFound(err error) bool {
switch unwrapErr(err).(type) {
default:
return false
case
apistatus.SessionTokenNotFound,
*apistatus.SessionTokenNotFound:
return true
}
}
// returns error describing missing field with the given name.
func newErrMissingResponseField(name string) error {
return fmt.Errorf("missing %s field in the response", name)
}
// returns error describing invalid field (according to the FrostFS protocol)
// with the given name and format violation err.
func newErrInvalidResponseField(name string, err error) error {
return fmt.Errorf("invalid %s field in the response: %w", name, err)
}

View file

@ -1,68 +0,0 @@
package client_test
import (
"fmt"
"testing"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client"
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
"github.com/stretchr/testify/require"
)
func TestErrors(t *testing.T) {
for _, tc := range []struct {
check func(error) bool
errs []error
}{
{
check: client.IsErrContainerNotFound,
errs: []error{
apistatus.ContainerNotFound{},
new(apistatus.ContainerNotFound),
},
},
{
check: client.IsErrEACLNotFound,
errs: []error{
apistatus.EACLNotFound{},
new(apistatus.EACLNotFound),
},
},
{
check: client.IsErrObjectNotFound,
errs: []error{
apistatus.ObjectNotFound{},
new(apistatus.ObjectNotFound),
},
},
{
check: client.IsErrObjectAlreadyRemoved,
errs: []error{
apistatus.ObjectAlreadyRemoved{},
new(apistatus.ObjectAlreadyRemoved),
},
},
{
check: client.IsErrSessionExpired,
errs: []error{
apistatus.SessionTokenExpired{},
new(apistatus.SessionTokenExpired),
},
}, {
check: client.IsErrSessionNotFound,
errs: []error{
apistatus.SessionTokenNotFound{},
new(apistatus.SessionTokenNotFound),
},
},
} {
require.NotEmpty(t, tc.errs)
for i := range tc.errs {
require.True(t, tc.check(tc.errs[i]), tc.errs[i])
require.True(t, tc.check(fmt.Errorf("top-level context: :%w",
fmt.Errorf("inner context: %w", tc.errs[i])),
), tc.errs[i])
}
}
}

View file

@ -2,16 +2,12 @@ package client
import ( import (
"context" "context"
"fmt"
v2netmap "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/netmap" v2netmap "github.com/nspcc-dev/neofs-api-go/v2/netmap"
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc" rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client" "github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session" "github.com/nspcc-dev/neofs-sdk-go/netmap"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature" "github.com/nspcc-dev/neofs-sdk-go/version"
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/version"
) )
// PrmEndpointInfo groups parameters of EndpointInfo operation. // PrmEndpointInfo groups parameters of EndpointInfo operation.
@ -23,31 +19,43 @@ type PrmEndpointInfo struct {
type ResEndpointInfo struct { type ResEndpointInfo struct {
statusRes statusRes
version version.Version version *version.Version
ni netmap.NodeInfo ni *netmap.NodeInfo
} }
// LatestVersion returns latest FrostFS API protocol's version in use. // LatestVersion returns latest NeoFS API protocol's version in use.
func (x ResEndpointInfo) LatestVersion() version.Version { //
// Client doesn't retain value so modification is safe.
func (x ResEndpointInfo) LatestVersion() *version.Version {
return x.version return x.version
} }
// NodeInfo returns information about the FrostFS node served on the remote endpoint. func (x *ResEndpointInfo) setLatestVersion(ver *version.Version) {
func (x ResEndpointInfo) NodeInfo() netmap.NodeInfo { x.version = ver
}
// NodeInfo returns information about the NeoFS node served on the remote endpoint.
//
// Client doesn't retain value so modification is safe.
func (x ResEndpointInfo) NodeInfo() *netmap.NodeInfo {
return x.ni return x.ni
} }
func (x *ResEndpointInfo) setNodeInfo(info *netmap.NodeInfo) {
x.ni = info
}
// EndpointInfo requests information about the storage node served on the remote endpoint. // EndpointInfo requests information about the storage node served on the remote endpoint.
// //
// Method can be used as a health check to see if node is alive and responds to requests. // Method can be used as a health check to see if node is alive and responds to requests.
// //
// Any client's internal or transport errors are returned as `error`. // Any client's internal or transport errors are returned as `error`.
// If PrmInit.ResolveFrostFSFailures has been called, unsuccessful // If WithNeoFSErrorParsing option has been provided, unsuccessful
// FrostFS status codes are returned as `error`, otherwise, are included // NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure. // in the returned result structure.
// //
// Returns an error if parameters are set incorrectly (see PrmEndpointInfo docs). // Immediately panics if parameters are set incorrectly (see PrmEndpointInfo docs).
// Context is required and must not be nil. It is used for network communication. // Context is required and must not be nil. It is used for network communication.
// //
// Exactly one return value is non-nil. Server status return is returned in ResEndpointInfo. // Exactly one return value is non-nil. Server status return is returned in ResEndpointInfo.
@ -58,7 +66,7 @@ func (x ResEndpointInfo) NodeInfo() netmap.NodeInfo {
func (c *Client) EndpointInfo(ctx context.Context, prm PrmEndpointInfo) (*ResEndpointInfo, error) { func (c *Client) EndpointInfo(ctx context.Context, prm PrmEndpointInfo) (*ResEndpointInfo, error) {
// check context // check context
if ctx == nil { if ctx == nil {
return nil, errorMissingContext panic(panicMsgMissingContext)
} }
// form request // form request
@ -83,33 +91,8 @@ func (c *Client) EndpointInfo(ctx context.Context, prm PrmEndpointInfo) (*ResEnd
body := resp.GetBody() body := resp.GetBody()
const fieldVersion = "version" res.setLatestVersion(version.NewFromV2(body.GetVersion()))
res.setNodeInfo(netmap.NewNodeInfoFromV2(body.GetNodeInfo()))
verV2 := body.GetVersion()
if verV2 == nil {
cc.err = newErrMissingResponseField(fieldVersion)
return
}
cc.err = res.version.ReadFromV2(*verV2)
if cc.err != nil {
cc.err = newErrInvalidResponseField(fieldVersion, cc.err)
return
}
const fieldNodeInfo = "node info"
nodeInfoV2 := body.GetNodeInfo()
if nodeInfoV2 == nil {
cc.err = newErrMissingResponseField(fieldNodeInfo)
return
}
cc.err = res.ni.ReadFromV2(*nodeInfoV2)
if cc.err != nil {
cc.err = newErrInvalidResponseField(fieldNodeInfo, cc.err)
return
}
} }
// process call // process call
@ -129,22 +112,28 @@ type PrmNetworkInfo struct {
type ResNetworkInfo struct { type ResNetworkInfo struct {
statusRes statusRes
info netmap.NetworkInfo info *netmap.NetworkInfo
} }
// Info returns structured information about the FrostFS network. // Info returns structured information about the NeoFS network.
func (x ResNetworkInfo) Info() netmap.NetworkInfo { //
// Client doesn't retain value so modification is safe.
func (x ResNetworkInfo) Info() *netmap.NetworkInfo {
return x.info return x.info
} }
// NetworkInfo requests information about the FrostFS network of which the remote server is a part. func (x *ResNetworkInfo) setInfo(info *netmap.NetworkInfo) {
x.info = info
}
// NetworkInfo requests information about the NeoFS network of which the remote server is a part.
// //
// Any client's internal or transport errors are returned as `error`. // Any client's internal or transport errors are returned as `error`.
// If PrmInit.ResolveFrostFSFailures has been called, unsuccessful // If WithNeoFSErrorParsing option has been provided, unsuccessful
// FrostFS status codes are returned as `error`, otherwise, are included // NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure. // in the returned result structure.
// //
// Returns an error if parameters are set incorrectly (see PrmNetworkInfo docs). // Immediately panics if parameters are set incorrectly (see PrmNetworkInfo docs).
// Context is required and must not be nil. It is used for network communication. // Context is required and must not be nil. It is used for network communication.
// //
// Exactly one return value is non-nil. Server status return is returned in ResNetworkInfo. // Exactly one return value is non-nil. Server status return is returned in ResNetworkInfo.
@ -155,7 +144,7 @@ func (x ResNetworkInfo) Info() netmap.NetworkInfo {
func (c *Client) NetworkInfo(ctx context.Context, prm PrmNetworkInfo) (*ResNetworkInfo, error) { func (c *Client) NetworkInfo(ctx context.Context, prm PrmNetworkInfo) (*ResNetworkInfo, error) {
// check context // check context
if ctx == nil { if ctx == nil {
return nil, errorMissingContext panic(panicMsgMissingContext)
} }
// form request // form request
@ -178,19 +167,7 @@ func (c *Client) NetworkInfo(ctx context.Context, prm PrmNetworkInfo) (*ResNetwo
cc.result = func(r responseV2) { cc.result = func(r responseV2) {
resp := r.(*v2netmap.NetworkInfoResponse) resp := r.(*v2netmap.NetworkInfoResponse)
const fieldNetInfo = "network info" res.setInfo(netmap.NewNetworkInfoFromV2(resp.GetBody().GetNetworkInfo()))
netInfoV2 := resp.GetBody().GetNetworkInfo()
if netInfoV2 == nil {
cc.err = newErrMissingResponseField(fieldNetInfo)
return
}
cc.err = res.info.ReadFromV2(*netInfoV2)
if cc.err != nil {
cc.err = newErrInvalidResponseField(fieldNetInfo, cc.err)
return
}
} }
// process call // process call
@ -200,86 +177,3 @@ func (c *Client) NetworkInfo(ctx context.Context, prm PrmNetworkInfo) (*ResNetwo
return &res, nil return &res, nil
} }
// PrmNetMapSnapshot groups parameters of NetMapSnapshot operation.
type PrmNetMapSnapshot struct {
}
// ResNetMapSnapshot groups resulting values of NetMapSnapshot operation.
type ResNetMapSnapshot struct {
statusRes
netMap netmap.NetMap
}
// NetMap returns current server's local network map.
func (x ResNetMapSnapshot) NetMap() netmap.NetMap {
return x.netMap
}
// NetMapSnapshot requests current network view of the remote server.
//
// Any client's internal or transport errors are returned as `error`.
// If PrmInit.ResolveFrostFSFailures has been called, unsuccessful
// FrostFS status codes are returned as `error`, otherwise, are included
// in the returned result structure.
//
// Returns an error if parameters are set incorrectly.
// Context is required and MUST NOT be nil. It is used for network communication.
//
// Exactly one return value is non-nil. Server status return is returned in ResNetMapSnapshot.
// Reflects all internal errors in second return value (transport problems, response processing, etc.).
//
// Return statuses:
// - global (see Client docs).
func (c *Client) NetMapSnapshot(ctx context.Context, _ PrmNetMapSnapshot) (*ResNetMapSnapshot, error) {
// check context
if ctx == nil {
return nil, errorMissingContext
}
// form request body
var body v2netmap.SnapshotRequestBody
// form meta header
var meta v2session.RequestMetaHeader
// form request
var req v2netmap.SnapshotRequest
req.SetBody(&body)
c.prepareRequest(&req, &meta)
err := signature.SignServiceMessage(&c.prm.key, &req)
if err != nil {
return nil, fmt.Errorf("sign request: %w", err)
}
resp, err := c.server.netMapSnapshot(ctx, req)
if err != nil {
return nil, err
}
var res ResNetMapSnapshot
res.st, err = c.processResponse(resp)
if err != nil {
return nil, err
}
if !apistatus.IsSuccessful(res.st) {
return &res, nil
}
const fieldNetMap = "network map"
netMapV2 := resp.GetBody().NetMap()
if netMapV2 == nil {
return nil, newErrMissingResponseField(fieldNetMap)
}
err = res.netMap.ReadFromV2(*netMapV2)
if err != nil {
return nil, newErrInvalidResponseField(fieldNetMap, err)
}
return &res, nil
}

View file

@ -1,136 +0,0 @@
package client
import (
"context"
"errors"
"fmt"
"testing"
v2netmap "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/netmap"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
"github.com/stretchr/testify/require"
)
type serverNetMap struct {
errTransport error
signResponse bool
statusOK bool
setNetMap bool
netMap v2netmap.NetMap
}
func (x *serverNetMap) netMapSnapshot(ctx context.Context, req v2netmap.SnapshotRequest) (*v2netmap.SnapshotResponse, error) {
err := signature.VerifyServiceMessage(&req)
if err != nil {
return nil, err
}
if x.errTransport != nil {
return nil, x.errTransport
}
var body v2netmap.SnapshotResponseBody
if x.setNetMap {
body.SetNetMap(&x.netMap)
}
var meta session.ResponseMetaHeader
if !x.statusOK {
meta.SetStatus(statusErr.ToStatusV2())
}
var resp v2netmap.SnapshotResponse
resp.SetBody(&body)
resp.SetMetaHeader(&meta)
if x.signResponse {
err = signature.SignServiceMessage(key, &resp)
if err != nil {
panic(fmt.Sprintf("sign response: %v", err))
}
}
return &resp, nil
}
func TestClient_NetMapSnapshot(t *testing.T) {
var err error
var prm PrmNetMapSnapshot
var res *ResNetMapSnapshot
var srv serverNetMap
c := newClient(&srv)
ctx := context.Background()
// missing context
//nolint:staticcheck
_, err = c.NetMapSnapshot(nil, prm)
require.ErrorIs(t, err, errorMissingContext, "")
// request signature
srv.errTransport = errors.New("any error")
_, err = c.NetMapSnapshot(ctx, prm)
require.ErrorIs(t, err, srv.errTransport)
srv.errTransport = nil
// unsigned response
_, err = c.NetMapSnapshot(ctx, prm)
require.Error(t, err)
srv.signResponse = true
// status failure
res, err = c.NetMapSnapshot(ctx, prm)
require.NoError(t, err)
assertStatusErr(t, res)
srv.statusOK = true
// missing netmap field
_, err = c.NetMapSnapshot(ctx, prm)
require.Error(t, err)
srv.setNetMap = true
// invalid network map
var netMap netmap.NetMap
var node netmap.NodeInfo
// TODO: #260 use instance corrupter
var nodeV2 v2netmap.NodeInfo
node.WriteToV2(&nodeV2)
require.Error(t, new(netmap.NodeInfo).ReadFromV2(nodeV2))
netMap.SetNodes([]netmap.NodeInfo{node})
netMap.WriteToV2(&srv.netMap)
_, err = c.NetMapSnapshot(ctx, prm)
require.Error(t, err)
// correct network map
// TODO: #260 use instance normalizer
node.SetPublicKey([]byte{1, 2, 3})
node.SetNetworkEndpoints("1", "2", "3")
node.WriteToV2(&nodeV2)
require.NoError(t, new(netmap.NodeInfo).ReadFromV2(nodeV2))
netMap.SetNodes([]netmap.NodeInfo{node})
netMap.WriteToV2(&srv.netMap)
res, err = c.NetMapSnapshot(ctx, prm)
require.NoError(t, err)
require.True(t, apistatus.IsSuccessful(res.Status()))
require.Equal(t, netMap, res.NetMap())
}

View file

@ -3,20 +3,16 @@ package client
import ( import (
"context" "context"
"crypto/ecdsa" "crypto/ecdsa"
"fmt"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl" v2object "github.com/nspcc-dev/neofs-api-go/v2/object"
v2object "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object" v2refs "github.com/nspcc-dev/neofs-api-go/v2/refs"
v2refs "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs" rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc" "github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client" v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session" cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature" oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer" "github.com/nspcc-dev/neofs-sdk-go/session"
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status" "github.com/nspcc-dev/neofs-sdk-go/token"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
) )
// PrmObjectDelete groups parameters of ObjectDelete operation. // PrmObjectDelete groups parameters of ObjectDelete operation.
@ -37,11 +33,8 @@ type PrmObjectDelete struct {
// This may affect the execution of an operation (e.g. access control). // This may affect the execution of an operation (e.g. access control).
// //
// Must be signed. // Must be signed.
func (x *PrmObjectDelete) WithinSession(t session.Object) { func (x *PrmObjectDelete) WithinSession(t session.Token) {
var tv2 v2session.Token x.meta.SetSessionToken(t.ToV2())
t.WriteToV2(&tv2)
x.meta.SetSessionToken(&tv2)
} }
// WithBearerToken attaches bearer token to be used for the operation. // WithBearerToken attaches bearer token to be used for the operation.
@ -49,28 +42,20 @@ func (x *PrmObjectDelete) WithinSession(t session.Object) {
// If set, underlying eACL rules will be used in access control. // If set, underlying eACL rules will be used in access control.
// //
// Must be signed. // Must be signed.
func (x *PrmObjectDelete) WithBearerToken(t bearer.Token) { func (x *PrmObjectDelete) WithBearerToken(t token.BearerToken) {
var v2token acl.BearerToken x.meta.SetBearerToken(t.ToV2())
t.WriteToV2(&v2token)
x.meta.SetBearerToken(&v2token)
} }
// FromContainer specifies FrostFS container of the object. // FromContainer specifies NeoFS container of the object.
// Required parameter. // Required parameter.
func (x *PrmObjectDelete) FromContainer(id cid.ID) { func (x *PrmObjectDelete) FromContainer(id cid.ID) {
var cidV2 v2refs.ContainerID x.addr.SetContainerID(id.ToV2())
id.WriteToV2(&cidV2)
x.addr.SetContainerID(&cidV2)
} }
// ByID specifies identifier of the requested object. // ByID specifies identifier of the requested object.
// Required parameter. // Required parameter.
func (x *PrmObjectDelete) ByID(id oid.ID) { func (x *PrmObjectDelete) ByID(id oid.ID) {
var idV2 v2refs.ObjectID x.addr.SetObjectID(id.ToV2())
id.WriteToV2(&idV2)
x.addr.SetObjectID(&idV2)
} }
// UseKey specifies private key to sign the requests. // UseKey specifies private key to sign the requests.
@ -85,22 +70,32 @@ func (x *PrmObjectDelete) UseKey(key ecdsa.PrivateKey) {
// //
// Slice must not be mutated until the operation completes. // Slice must not be mutated until the operation completes.
func (x *PrmObjectDelete) WithXHeaders(hs ...string) { func (x *PrmObjectDelete) WithXHeaders(hs ...string) {
writeXHeadersToMeta(hs, &x.meta) if len(hs)%2 != 0 {
panic("slice of X-Headers with odd length")
}
prmCommonMeta{xHeaders: hs}.writeToMetaHeader(&x.meta)
} }
// ResObjectDelete groups resulting values of ObjectDelete operation. // ResObjectDelete groups resulting values of ObjectDelete operation.
type ResObjectDelete struct { type ResObjectDelete struct {
statusRes statusRes
tomb oid.ID idTomb *v2refs.ObjectID
} }
// Tombstone returns identifier of the created tombstone object. // ReadTombstoneID reads identifier of the created tombstone object.
func (x ResObjectDelete) Tombstone() oid.ID { // Returns false if ID is missing (not read).
return x.tomb func (x ResObjectDelete) ReadTombstoneID(dst *oid.ID) bool {
if x.idTomb != nil {
*dst = *oid.NewIDFromV2(x.idTomb) // need smth better
return true
}
return false
} }
// ObjectDelete marks an object for deletion from the container using FrostFS API protocol. // ObjectDelete marks an object for deletion from the container using NeoFS API protocol.
// As a marker, a special unit called a tombstone is placed in the container. // As a marker, a special unit called a tombstone is placed in the container.
// It confirms the user's intent to delete the object, and is itself a container object. // It confirms the user's intent to delete the object, and is itself a container object.
// Explicit deletion is done asynchronously, and is generally not guaranteed. // Explicit deletion is done asynchronously, and is generally not guaranteed.
@ -110,11 +105,11 @@ func (x ResObjectDelete) Tombstone() oid.ID {
// //
// Exactly one return value is non-nil. By default, server status is returned in res structure. // Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as `error`, // Any client's internal or transport errors are returned as `error`,
// If PrmInit.ResolveFrostFSFailures has been called, unsuccessful // If WithNeoFSErrorParsing option has been provided, unsuccessful
// FrostFS status codes are returned as `error`, otherwise, are included // NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure. // in the returned result structure.
// //
// Returns an error if parameters are set incorrectly (see PrmObjectDelete docs). // Immediately panics if parameters are set incorrectly (see PrmObjectDelete docs).
// Context is required and must not be nil. It is used for network communication. // Context is required and must not be nil. It is used for network communication.
// //
// Return statuses: // Return statuses:
@ -126,11 +121,11 @@ func (x ResObjectDelete) Tombstone() oid.ID {
func (c *Client) ObjectDelete(ctx context.Context, prm PrmObjectDelete) (*ResObjectDelete, error) { func (c *Client) ObjectDelete(ctx context.Context, prm PrmObjectDelete) (*ResObjectDelete, error) {
switch { switch {
case ctx == nil: case ctx == nil:
return nil, errorMissingContext panic(panicMsgMissingContext)
case prm.addr.GetContainerID() == nil: case prm.addr.GetContainerID() == nil:
return nil, errorMissingContainer panic(panicMsgMissingContainer)
case prm.addr.GetObjectID() == nil: case prm.addr.GetObjectID() == nil:
return nil, errorMissingObject panic("missing object")
} }
// form request body // form request body
@ -139,43 +134,33 @@ func (c *Client) ObjectDelete(ctx context.Context, prm PrmObjectDelete) (*ResObj
// form request // form request
var req v2object.DeleteRequest var req v2object.DeleteRequest
req.SetBody(&prm.body) req.SetBody(&prm.body)
c.prepareRequest(&req, &prm.meta) req.SetMetaHeader(&prm.meta)
// init call context
var (
cc contextCall
res ResObjectDelete
)
key := c.prm.key
if prm.keySet { if prm.keySet {
key = prm.key c.initCallContextWithoutKey(&cc)
cc.key = prm.key
} else {
c.initCallContext(&cc)
} }
err := signature.SignServiceMessage(&key, &req) cc.req = &req
if err != nil { cc.statusRes = &res
return nil, fmt.Errorf("sign request: %w", err) cc.call = func() (responseV2, error) {
return rpcapi.DeleteObject(&c.c, &req, client.WithContext(ctx))
}
cc.result = func(r responseV2) {
res.idTomb = r.(*v2object.DeleteResponse).GetBody().GetTombstone().GetObjectID()
} }
resp, err := rpcapi.DeleteObject(&c.c, &req, client.WithContext(ctx)) // process call
if err != nil { if !cc.processCall() {
return nil, err return nil, cc.err
}
var res ResObjectDelete
res.st, err = c.processResponse(resp)
if err != nil {
return nil, err
}
if !apistatus.IsSuccessful(res.st) {
return &res, nil
}
const fieldTombstone = "tombstone"
idTombV2 := resp.GetBody().GetTombstone().GetObjectID()
if idTombV2 == nil {
return nil, newErrMissingResponseField(fieldTombstone)
}
err = res.tomb.ReadFromV2(*idTombV2)
if err != nil {
return nil, newErrInvalidResponseField(fieldTombstone, err)
} }
return &res, nil return &res, nil

View file

@ -7,36 +7,54 @@ import (
"fmt" "fmt"
"io" "io"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl" v2object "github.com/nspcc-dev/neofs-api-go/v2/object"
v2object "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object" v2refs "github.com/nspcc-dev/neofs-api-go/v2/refs"
v2refs "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs" rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc" "github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client" v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session" apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature" cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer" "github.com/nspcc-dev/neofs-sdk-go/object"
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status" oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" "github.com/nspcc-dev/neofs-sdk-go/session"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object" "github.com/nspcc-dev/neofs-sdk-go/token"
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
) )
// shared parameters of GET/HEAD/RANGE. // shared parameters of GET/HEAD/RANGE.
type prmObjectRead struct { type prmObjectRead struct {
meta v2session.RequestMetaHeader prmCommonMeta
raw bool raw bool
addr v2refs.Address local bool
sessionSet bool
session session.Token
bearerSet bool
bearer token.BearerToken
cnrSet bool
cnr cid.ID
objSet bool
obj oid.ID
} }
// WithXHeaders specifies list of extended headers (string key-value pairs) func (x prmObjectRead) writeToMetaHeader(h *v2session.RequestMetaHeader) {
// to be attached to the request. Must have an even length. if x.local {
// h.SetTTL(1)
// Slice must not be mutated until the operation completes. }
func (x *prmObjectRead) WithXHeaders(hs ...string) {
writeXHeadersToMeta(hs, &x.meta) if x.bearerSet {
h.SetBearerToken(x.bearer.ToV2())
}
if x.sessionSet {
h.SetSessionToken(x.session.ToV2())
}
x.prmCommonMeta.writeToMetaHeader(h)
} }
// MarkRaw marks an intent to read physically stored object. // MarkRaw marks an intent to read physically stored object.
@ -46,7 +64,7 @@ func (x *prmObjectRead) MarkRaw() {
// MarkLocal tells the server to execute the operation locally. // MarkLocal tells the server to execute the operation locally.
func (x *prmObjectRead) MarkLocal() { func (x *prmObjectRead) MarkLocal() {
x.meta.SetTTL(1) x.local = true
} }
// WithinSession specifies session within which object should be read. // WithinSession specifies session within which object should be read.
@ -55,10 +73,9 @@ func (x *prmObjectRead) MarkLocal() {
// This may affect the execution of an operation (e.g. access control). // This may affect the execution of an operation (e.g. access control).
// //
// Must be signed. // Must be signed.
func (x *prmObjectRead) WithinSession(t session.Object) { func (x *prmObjectRead) WithinSession(t session.Token) {
var tokv2 v2session.Token x.session = t
t.WriteToV2(&tokv2) x.sessionSet = true
x.meta.SetSessionToken(&tokv2)
} }
// WithBearerToken attaches bearer token to be used for the operation. // WithBearerToken attaches bearer token to be used for the operation.
@ -66,33 +83,28 @@ func (x *prmObjectRead) WithinSession(t session.Object) {
// If set, underlying eACL rules will be used in access control. // If set, underlying eACL rules will be used in access control.
// //
// Must be signed. // Must be signed.
func (x *prmObjectRead) WithBearerToken(t bearer.Token) { func (x *prmObjectRead) WithBearerToken(t token.BearerToken) {
var v2token acl.BearerToken x.bearer = t
t.WriteToV2(&v2token) x.bearerSet = true
x.meta.SetBearerToken(&v2token)
} }
// FromContainer specifies FrostFS container of the object. // FromContainer specifies NeoFS container of the object.
// Required parameter. // Required parameter.
func (x *prmObjectRead) FromContainer(id cid.ID) { func (x *prmObjectRead) FromContainer(id cid.ID) {
var cnrV2 v2refs.ContainerID x.cnr = id
id.WriteToV2(&cnrV2) x.cnrSet = true
x.addr.SetContainerID(&cnrV2)
} }
// ByID specifies identifier of the requested object. // ByID specifies identifier of the requested object.
// Required parameter. // Required parameter.
func (x *prmObjectRead) ByID(id oid.ID) { func (x *prmObjectRead) ByID(id oid.ID) {
var objV2 v2refs.ObjectID x.obj = id
id.WriteToV2(&objV2) x.objSet = true
x.addr.SetObjectID(&objV2)
} }
// PrmObjectGet groups parameters of ObjectGetInit operation. // PrmObjectGet groups parameters of ObjectGetInit operation.
type PrmObjectGet struct { type PrmObjectGet struct {
prmObjectRead prmObjectRead
key *ecdsa.PrivateKey
} }
// ResObjectGet groups the final result values of ObjectGetInit operation. // ResObjectGet groups the final result values of ObjectGetInit operation.
@ -100,20 +112,17 @@ type ResObjectGet struct {
statusRes statusRes
} }
// ObjectReader is designed to read one object from FrostFS system. // ObjectReader is designed to read one object from NeoFS system.
// //
// Must be initialized using Client.ObjectGetInit, any other // Must be initialized using Client.ObjectGetInit, any other
// usage is unsafe. // usage is unsafe.
type ObjectReader struct { type ObjectReader struct {
cancelCtxStream context.CancelFunc cancelCtxStream context.CancelFunc
client *Client ctxCall contextCall
stream interface {
Read(resp *v2object.GetResponse) error
}
res ResObjectGet // initially bound to contextCall
err error bodyResp v2object.GetResponseBody
tailPayload []byte tailPayload []byte
@ -122,32 +131,29 @@ type ObjectReader struct {
// UseKey specifies private key to sign the requests. // UseKey specifies private key to sign the requests.
// If key is not provided, then Client default key is used. // If key is not provided, then Client default key is used.
func (x *PrmObjectGet) UseKey(key ecdsa.PrivateKey) { func (x *ObjectReader) UseKey(key ecdsa.PrivateKey) {
x.key = &key x.ctxCall.key = key
}
func handleSplitInfo(ctx *contextCall, i *v2object.SplitInfo) {
ctx.err = object.NewSplitInfoError(object.NewSplitInfoFromV2(i))
} }
// ReadHeader reads header of the object. Result means success. // ReadHeader reads header of the object. Result means success.
// Failure reason can be received via Close. // Failure reason can be received via Close.
func (x *ObjectReader) ReadHeader(dst *object.Object) bool { func (x *ObjectReader) ReadHeader(dst *object.Object) bool {
var resp v2object.GetResponse if !x.ctxCall.writeRequest() || !x.ctxCall.readResponse() {
x.err = x.stream.Read(&resp)
if x.err != nil {
return false
}
x.res.st, x.err = x.client.processResponse(&resp)
if x.err != nil || !apistatus.IsSuccessful(x.res.st) {
return false return false
} }
var partInit *v2object.GetObjectPartInit var partInit *v2object.GetObjectPartInit
switch v := resp.GetBody().GetObjectPart().(type) { switch v := x.bodyResp.GetObjectPart().(type) {
default: default:
x.err = fmt.Errorf("unexpected message instead of heading part: %T", v) x.ctxCall.err = fmt.Errorf("unexpected message instead of heading part: %T", v)
return false return false
case *v2object.SplitInfo: case *v2object.SplitInfo:
x.err = object.NewSplitInfoError(object.NewSplitInfoFromV2(v)) handleSplitInfo(&x.ctxCall, v)
return false return false
case *v2object.GetObjectPartInit: case *v2object.GetObjectPartInit:
partInit = v partInit = v
@ -178,25 +184,26 @@ func (x *ObjectReader) readChunk(buf []byte) (int, bool) {
return read, true return read, true
} }
var ok bool
var part v2object.GetObjectPart
var chunk []byte var chunk []byte
var lastRead int var lastRead int
for { for {
var resp v2object.GetResponse // receive next message
x.err = x.stream.Read(&resp) ok = x.ctxCall.readResponse()
if x.err != nil {
return read, false
}
x.res.st, x.err = x.client.processResponse(&resp)
if x.err != nil || !apistatus.IsSuccessful(x.res.st) {
return read, false
}
part := resp.GetBody().GetObjectPart()
partChunk, ok := part.(*v2object.GetObjectPartChunk)
if !ok { if !ok {
x.err = fmt.Errorf("unexpected message instead of chunk part: %T", part) return read, false
}
// get chunk part message
part = x.bodyResp.GetObjectPart()
var partChunk *v2object.GetObjectPartChunk
partChunk, ok = part.(*v2object.GetObjectPartChunk)
if !ok {
x.ctxCall.err = fmt.Errorf("unexpected message instead of chunk part: %T", part)
return read, false return read, false
} }
@ -231,9 +238,9 @@ func (x *ObjectReader) ReadChunk(buf []byte) (int, bool) {
func (x *ObjectReader) close(ignoreEOF bool) (*ResObjectGet, error) { func (x *ObjectReader) close(ignoreEOF bool) (*ResObjectGet, error) {
defer x.cancelCtxStream() defer x.cancelCtxStream()
if x.err != nil { if x.ctxCall.err != nil {
if !errors.Is(x.err, io.EOF) { if !errors.Is(x.ctxCall.err, io.EOF) {
return nil, x.err return nil, x.ctxCall.err
} else if !ignoreEOF { } else if !ignoreEOF {
if x.remainingPayloadLen > 0 { if x.remainingPayloadLen > 0 {
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
@ -243,7 +250,7 @@ func (x *ObjectReader) close(ignoreEOF bool) (*ResObjectGet, error) {
} }
} }
return &x.res, nil return x.ctxCall.statusRes.(*ResObjectGet), nil
} }
// Close ends reading the object and returns the result of the operation // Close ends reading the object and returns the result of the operation
@ -251,11 +258,10 @@ func (x *ObjectReader) close(ignoreEOF bool) (*ResObjectGet, error) {
// //
// Exactly one return value is non-nil. By default, server status is returned in res structure. // Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as Go built-in error. // Any client's internal or transport errors are returned as Go built-in error.
// If Client is tuned to resolve FrostFS API statuses, then FrostFS failures // If Client is tuned to resolve NeoFS API statuses, then NeoFS failures
// codes are returned as error. // codes are returned as error.
// //
// Return errors: // Return errors:
//
// *object.SplitInfoError (returned on virtual objects with PrmObjectGet.MakeRaw). // *object.SplitInfoError (returned on virtual objects with PrmObjectGet.MakeRaw).
// //
// Return statuses: // Return statuses:
@ -291,58 +297,75 @@ func (x *ObjectReader) Read(p []byte) (int, error) {
return n, nil return n, nil
} }
// ObjectGetInit initiates reading an object through a remote server using FrostFS API protocol. // ObjectGetInit initiates reading an object through a remote server using NeoFS API protocol.
// //
// The call only opens the transmission channel, explicit fetching is done using the ObjectReader. // The call only opens the transmission channel, explicit fetching is done using the ObjectReader.
// Exactly one return value is non-nil. Resulting reader must be finally closed. // Exactly one return value is non-nil. Resulting reader must be finally closed.
// //
// Returns an error if parameters are set incorrectly (see PrmObjectGet docs). // Immediately panics if parameters are set incorrectly (see PrmObjectGet docs).
// Context is required and must not be nil. It is used for network communication. // Context is required and must not be nil. It is used for network communication.
func (c *Client) ObjectGetInit(ctx context.Context, prm PrmObjectGet) (*ObjectReader, error) { func (c *Client) ObjectGetInit(ctx context.Context, prm PrmObjectGet) (*ObjectReader, error) {
// check parameters // check parameters
switch { switch {
case ctx == nil: case ctx == nil:
return nil, errorMissingContext panic(panicMsgMissingContext)
case prm.addr.GetContainerID() == nil: case !prm.cnrSet:
return nil, errorMissingContainer panic(panicMsgMissingContainer)
case prm.addr.GetObjectID() == nil: case !prm.objSet:
return nil, errorMissingObject panic("missing object")
} }
var addr v2refs.Address
addr.SetContainerID(prm.cnr.ToV2())
addr.SetObjectID(prm.obj.ToV2())
// form request body // form request body
var body v2object.GetRequestBody var body v2object.GetRequestBody
body.SetRaw(prm.raw) body.SetRaw(prm.raw)
body.SetAddress(&prm.addr) body.SetAddress(&addr)
// form meta header
var meta v2session.RequestMetaHeader
prm.prmObjectRead.writeToMetaHeader(&meta)
// form request // form request
var req v2object.GetRequest var req v2object.GetRequest
req.SetBody(&body) req.SetBody(&body)
c.prepareRequest(&req, &prm.meta) req.SetMetaHeader(&meta)
key := prm.key // init reader
if key == nil { var (
key = &c.prm.key r ObjectReader
} resp v2object.GetResponse
stream *rpcapi.GetResponseReader
)
err := signature.SignServiceMessage(key, &req) ctx, r.cancelCtxStream = context.WithCancel(ctx)
resp.SetBody(&r.bodyResp)
// init call context
c.initCallContext(&r.ctxCall)
r.ctxCall.req = &req
r.ctxCall.statusRes = new(ResObjectGet)
r.ctxCall.resp = &resp
r.ctxCall.wReq = func() error {
var err error
stream, err = rpcapi.GetObject(&c.c, &req, client.WithContext(ctx))
if err != nil { if err != nil {
return nil, fmt.Errorf("sign request: %w", err) return fmt.Errorf("open stream: %w", err)
} }
ctx, cancel := context.WithCancel(ctx) return nil
}
stream, err := rpcapi.GetObject(&c.c, &req, client.WithContext(ctx)) r.ctxCall.rResp = func() error {
if err != nil { return stream.Read(&resp)
cancel()
return nil, fmt.Errorf("open stream: %w", err)
} }
var r ObjectReader
r.cancelCtxStream = cancel
r.stream = stream
r.client = c
return &r, nil return &r, nil
} }
@ -385,26 +408,25 @@ func (x *ResObjectHead) ReadHeader(dst *object.Object) bool {
objv2.SetSignature(x.hdr.GetSignature()) objv2.SetSignature(x.hdr.GetSignature())
obj := object.NewFromV2(&objv2) obj := object.NewFromV2(&objv2)
obj.SetID(x.idObj) obj.SetID(&x.idObj)
*dst = *obj *dst = *obj
return true return true
} }
// ObjectHead reads object header through a remote server using FrostFS API protocol. // ObjectHead reads object header through a remote server using NeoFS API protocol.
// //
// Exactly one return value is non-nil. By default, server status is returned in res structure. // Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as `error`, // Any client's internal or transport errors are returned as `error`,
// If PrmInit.ResolveFrostFSFailures has been called, unsuccessful // If WithNeoFSErrorParsing option has been provided, unsuccessful
// FrostFS status codes are returned as `error`, otherwise, are included // NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure. // in the returned result structure.
// //
// Returns an error if parameters are set incorrectly (see PrmObjectHead docs). // Immediately panics if parameters are set incorrectly (see PrmObjectHead docs).
// Context is required and must not be nil. It is used for network communication. // Context is required and must not be nil. It is used for network communication.
// //
// Return errors: // Return errors:
//
// *object.SplitInfoError (returned on virtual objects with PrmObjectHead.MakeRaw). // *object.SplitInfoError (returned on virtual objects with PrmObjectHead.MakeRaw).
// //
// Return statuses: // Return statuses:
@ -417,57 +439,71 @@ func (x *ResObjectHead) ReadHeader(dst *object.Object) bool {
func (c *Client) ObjectHead(ctx context.Context, prm PrmObjectHead) (*ResObjectHead, error) { func (c *Client) ObjectHead(ctx context.Context, prm PrmObjectHead) (*ResObjectHead, error) {
switch { switch {
case ctx == nil: case ctx == nil:
return nil, errorMissingContext panic(panicMsgMissingContext)
case prm.addr.GetContainerID() == nil: case !prm.cnrSet:
return nil, errorMissingContainer panic(panicMsgMissingContainer)
case prm.addr.GetObjectID() == nil: case !prm.objSet:
return nil, errorMissingObject panic("missing object")
} }
var addr v2refs.Address
addr.SetContainerID(prm.cnr.ToV2())
addr.SetObjectID(prm.obj.ToV2())
// form request body
var body v2object.HeadRequestBody var body v2object.HeadRequestBody
body.SetRaw(prm.raw) body.SetRaw(prm.raw)
body.SetAddress(&prm.addr) body.SetAddress(&addr)
// form meta header
var meta v2session.RequestMetaHeader
prm.prmObjectRead.writeToMetaHeader(&meta)
// form request
var req v2object.HeadRequest var req v2object.HeadRequest
req.SetBody(&body) req.SetBody(&body)
c.prepareRequest(&req, &prm.meta) req.SetMetaHeader(&meta)
// init call context
var (
cc contextCall
res ResObjectHead
)
res.idObj = prm.obj
key := c.prm.key
if prm.keySet { if prm.keySet {
key = prm.key c.initCallContextWithoutKey(&cc)
cc.key = prm.key
} else {
c.initCallContext(&cc)
} }
// sign the request cc.req = &req
err := signature.SignServiceMessage(&key, &req) cc.statusRes = &res
if err != nil { cc.call = func() (responseV2, error) {
return nil, fmt.Errorf("sign request: %w", err) return rpcapi.HeadObject(&c.c, &req, client.WithContext(ctx))
} }
cc.result = func(r responseV2) {
resp, err := rpcapi.HeadObject(&c.c, &req, client.WithContext(ctx)) switch v := r.(*v2object.HeadResponse).GetBody().GetHeaderPart().(type) {
if err != nil {
return nil, fmt.Errorf("write request: %w", err)
}
var res ResObjectHead
res.st, err = c.processResponse(resp)
if err != nil {
return nil, err
}
if !apistatus.IsSuccessful(res.st) {
return &res, nil
}
_ = res.idObj.ReadFromV2(*prm.addr.GetObjectID())
switch v := resp.GetBody().GetHeaderPart().(type) {
default: default:
return nil, fmt.Errorf("unexpected header type %T", v) cc.err = fmt.Errorf("unexpected header type %T", v)
case *v2object.SplitInfo: case *v2object.SplitInfo:
return nil, object.NewSplitInfoError(object.NewSplitInfoFromV2(v)) handleSplitInfo(&cc, v)
case *v2object.HeaderWithSignature: case *v2object.HeaderWithSignature:
res.hdr = v res.hdr = v
} }
}
// process call
if !cc.processCall() {
return nil, cc.err
}
return &res, nil return &res, nil
} }
@ -476,27 +512,19 @@ func (c *Client) ObjectHead(ctx context.Context, prm PrmObjectHead) (*ResObjectH
type PrmObjectRange struct { type PrmObjectRange struct {
prmObjectRead prmObjectRead
key *ecdsa.PrivateKey off, ln uint64
rng v2object.Range
} }
// SetOffset sets offset of the payload range to be read. // SetOffset sets offset of the payload range to be read.
// Zero by default. // Zero by default.
func (x *PrmObjectRange) SetOffset(off uint64) { func (x *PrmObjectRange) SetOffset(off uint64) {
x.rng.SetOffset(off) x.off = off
} }
// SetLength sets length of the payload range to be read. // SetLength sets length of the payload range to be read.
// Must be positive. // Must be positive.
func (x *PrmObjectRange) SetLength(ln uint64) { func (x *PrmObjectRange) SetLength(ln uint64) {
x.rng.SetLength(ln) x.ln = ln
}
// UseKey specifies private key to sign the requests.
// If key is not provided, then Client default key is used.
func (x *PrmObjectRange) UseKey(key ecdsa.PrivateKey) {
x.key = &key
} }
// ResObjectRange groups the final result values of ObjectRange operation. // ResObjectRange groups the final result values of ObjectRange operation.
@ -505,28 +533,40 @@ type ResObjectRange struct {
} }
// ObjectRangeReader is designed to read payload range of one object // ObjectRangeReader is designed to read payload range of one object
// from FrostFS system. // from NeoFS system.
// //
// Must be initialized using Client.ObjectRangeInit, any other // Must be initialized using Client.ObjectRangeInit, any other
// usage is unsafe. // usage is unsafe.
type ObjectRangeReader struct { type ObjectRangeReader struct {
cancelCtxStream context.CancelFunc cancelCtxStream context.CancelFunc
client *Client ctxCall contextCall
res ResObjectRange reqWritten bool
err error
stream interface { // initially bound to contextCall
Read(resp *v2object.GetRangeResponse) error bodyResp v2object.GetRangeResponseBody
}
tailPayload []byte tailPayload []byte
remainingPayloadLen int remainingPayloadLen int
} }
// UseKey specifies private key to sign the requests.
// If key is not provided, then Client default key is used.
func (x *ObjectRangeReader) UseKey(key ecdsa.PrivateKey) {
x.ctxCall.key = key
}
func (x *ObjectRangeReader) readChunk(buf []byte) (int, bool) { func (x *ObjectRangeReader) readChunk(buf []byte) (int, bool) {
if !x.reqWritten {
if !x.ctxCall.writeRequest() {
return 0, false
}
x.reqWritten = true
}
var read int var read int
// read remaining tail // read remaining tail
@ -538,29 +578,25 @@ func (x *ObjectRangeReader) readChunk(buf []byte) (int, bool) {
return read, true return read, true
} }
var ok bool
var partChunk *v2object.GetRangePartChunk var partChunk *v2object.GetRangePartChunk
var chunk []byte var chunk []byte
var lastRead int var lastRead int
for { for {
var resp v2object.GetRangeResponse // receive next message
x.err = x.stream.Read(&resp) ok = x.ctxCall.readResponse()
if x.err != nil { if !ok {
return read, false
}
x.res.st, x.err = x.client.processResponse(&resp)
if x.err != nil || !apistatus.IsSuccessful(x.res.st) {
return read, false return read, false
} }
// get chunk message // get chunk message
switch v := resp.GetBody().GetRangePart().(type) { switch v := x.bodyResp.GetRangePart().(type) {
default: default:
x.err = fmt.Errorf("unexpected message received: %T", v) x.ctxCall.err = fmt.Errorf("unexpected message received: %T", v)
return read, false return read, false
case *v2object.SplitInfo: case *v2object.SplitInfo:
x.err = object.NewSplitInfoError(object.NewSplitInfoFromV2(v)) handleSplitInfo(&x.ctxCall, v)
return read, false return read, false
case *v2object.GetRangePartChunk: case *v2object.GetRangePartChunk:
partChunk = v partChunk = v
@ -596,9 +632,9 @@ func (x *ObjectRangeReader) ReadChunk(buf []byte) (int, bool) {
func (x *ObjectRangeReader) close(ignoreEOF bool) (*ResObjectRange, error) { func (x *ObjectRangeReader) close(ignoreEOF bool) (*ResObjectRange, error) {
defer x.cancelCtxStream() defer x.cancelCtxStream()
if x.err != nil { if x.ctxCall.err != nil {
if !errors.Is(x.err, io.EOF) { if !errors.Is(x.ctxCall.err, io.EOF) {
return nil, x.err return nil, x.ctxCall.err
} else if !ignoreEOF { } else if !ignoreEOF {
if x.remainingPayloadLen > 0 { if x.remainingPayloadLen > 0 {
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
@ -608,7 +644,7 @@ func (x *ObjectRangeReader) close(ignoreEOF bool) (*ResObjectRange, error) {
} }
} }
return &x.res, nil return x.ctxCall.statusRes.(*ResObjectRange), nil
} }
// Close ends reading the payload range and returns the result of the operation // Close ends reading the payload range and returns the result of the operation
@ -616,11 +652,10 @@ func (x *ObjectRangeReader) close(ignoreEOF bool) (*ResObjectRange, error) {
// //
// Exactly one return value is non-nil. By default, server status is returned in res structure. // Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as Go built-in error. // Any client's internal or transport errors are returned as Go built-in error.
// If Client is tuned to resolve FrostFS API statuses, then FrostFS failures // If Client is tuned to resolve NeoFS API statuses, then NeoFS failures
// codes are returned as error. // codes are returned as error.
// //
// Return errors: // Return errors:
//
// *object.SplitInfoError (returned on virtual objects with PrmObjectRange.MakeRaw). // *object.SplitInfoError (returned on virtual objects with PrmObjectRange.MakeRaw).
// //
// Return statuses: // Return statuses:
@ -629,7 +664,6 @@ func (x *ObjectRangeReader) close(ignoreEOF bool) (*ResObjectRange, error) {
// - *apistatus.ObjectNotFound; // - *apistatus.ObjectNotFound;
// - *apistatus.ObjectAccessDenied; // - *apistatus.ObjectAccessDenied;
// - *apistatus.ObjectAlreadyRemoved; // - *apistatus.ObjectAlreadyRemoved;
// - *apistatus.ObjectOutOfRange;
// - *apistatus.SessionTokenExpired. // - *apistatus.SessionTokenExpired.
func (x *ObjectRangeReader) Close() (*ResObjectRange, error) { func (x *ObjectRangeReader) Close() (*ResObjectRange, error) {
return x.close(true) return x.close(true)
@ -658,62 +692,85 @@ func (x *ObjectRangeReader) Read(p []byte) (int, error) {
} }
// ObjectRangeInit initiates reading an object's payload range through a remote // ObjectRangeInit initiates reading an object's payload range through a remote
// server using FrostFS API protocol. // server using NeoFS API protocol.
// //
// The call only opens the transmission channel, explicit fetching is done using the ObjectRangeReader. // The call only opens the transmission channel, explicit fetching is done using the ObjectRangeReader.
// Exactly one return value is non-nil. Resulting reader must be finally closed. // Exactly one return value is non-nil. Resulting reader must be finally closed.
// //
// Returns an error if parameters are set incorrectly (see PrmObjectRange docs). // Immediately panics if parameters are set incorrectly (see PrmObjectRange docs).
// Context is required and must not be nil. It is used for network communication. // Context is required and must not be nil. It is used for network communication.
func (c *Client) ObjectRangeInit(ctx context.Context, prm PrmObjectRange) (*ObjectRangeReader, error) { func (c *Client) ObjectRangeInit(ctx context.Context, prm PrmObjectRange) (*ObjectRangeReader, error) {
// check parameters // check parameters
switch { switch {
case ctx == nil: case ctx == nil:
return nil, errorMissingContext panic(panicMsgMissingContext)
case prm.addr.GetContainerID() == nil: case !prm.cnrSet:
return nil, errorMissingContainer panic(panicMsgMissingContainer)
case prm.addr.GetObjectID() == nil: case !prm.objSet:
return nil, errorMissingObject panic("missing object")
case prm.rng.GetLength() == 0: case prm.ln == 0:
return nil, errorZeroRangeLength panic("zero range length")
} }
var addr v2refs.Address
addr.SetContainerID(prm.cnr.ToV2())
addr.SetObjectID(prm.obj.ToV2())
var rng v2object.Range
rng.SetOffset(prm.off)
rng.SetLength(prm.ln)
// form request body // form request body
var body v2object.GetRangeRequestBody var body v2object.GetRangeRequestBody
body.SetRaw(prm.raw) body.SetRaw(prm.raw)
body.SetAddress(&prm.addr) body.SetAddress(&addr)
body.SetRange(&prm.rng) body.SetRange(&rng)
// form meta header
var meta v2session.RequestMetaHeader
prm.prmObjectRead.writeToMetaHeader(&meta)
// form request // form request
var req v2object.GetRangeRequest var req v2object.GetRangeRequest
req.SetBody(&body) req.SetBody(&body)
c.prepareRequest(&req, &prm.meta) req.SetMetaHeader(&meta)
key := prm.key // init reader
if key == nil { var (
key = &c.prm.key r ObjectRangeReader
} resp v2object.GetRangeResponse
stream *rpcapi.ObjectRangeResponseReader
)
err := signature.SignServiceMessage(key, &req) r.remainingPayloadLen = int(prm.ln)
ctx, r.cancelCtxStream = context.WithCancel(ctx)
resp.SetBody(&r.bodyResp)
// init call context
c.initCallContext(&r.ctxCall)
r.ctxCall.req = &req
r.ctxCall.statusRes = new(ResObjectRange)
r.ctxCall.resp = &resp
r.ctxCall.wReq = func() error {
var err error
stream, err = rpcapi.GetObjectRange(&c.c, &req, client.WithContext(ctx))
if err != nil { if err != nil {
return nil, fmt.Errorf("sign request: %w", err) return fmt.Errorf("open stream: %w", err)
} }
ctx, cancel := context.WithCancel(ctx) return nil
}
stream, err := rpcapi.GetObjectRange(&c.c, &req, client.WithContext(ctx)) r.ctxCall.rResp = func() error {
if err != nil { return stream.Read(&resp)
cancel()
return nil, fmt.Errorf("open stream: %w", err)
} }
var r ObjectRangeReader
r.remainingPayloadLen = int(prm.rng.GetLength())
r.cancelCtxStream = cancel
r.stream = stream
r.client = c
return &r, nil return &r, nil
} }

View file

@ -2,21 +2,16 @@ package client
import ( import (
"context" "context"
"crypto/ecdsa"
"fmt"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl" v2object "github.com/nspcc-dev/neofs-api-go/v2/object"
v2object "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object" v2refs "github.com/nspcc-dev/neofs-api-go/v2/refs"
v2refs "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs" rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc" "github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client" v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session" cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature" oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer" "github.com/nspcc-dev/neofs-sdk-go/session"
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status" "github.com/nspcc-dev/neofs-sdk-go/token"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
) )
// PrmObjectHash groups parameters of ObjectHash operation. // PrmObjectHash groups parameters of ObjectHash operation.
@ -25,19 +20,9 @@ type PrmObjectHash struct {
body v2object.GetRangeHashRequestBody body v2object.GetRangeHashRequestBody
csAlgo v2refs.ChecksumType tillichZemor bool
addr v2refs.Address addr v2refs.Address
keySet bool
key ecdsa.PrivateKey
}
// UseKey specifies private key to sign the requests.
// If key is not provided, then Client default key is used.
func (x *PrmObjectHash) UseKey(key ecdsa.PrivateKey) {
x.keySet = true
x.key = key
} }
// MarkLocal tells the server to execute the operation locally. // MarkLocal tells the server to execute the operation locally.
@ -51,11 +36,8 @@ func (x *PrmObjectHash) MarkLocal() {
// This may affect the execution of an operation (e.g. access control). // This may affect the execution of an operation (e.g. access control).
// //
// Must be signed. // Must be signed.
func (x *PrmObjectHash) WithinSession(t session.Object) { func (x *PrmObjectHash) WithinSession(t session.Token) {
var tv2 v2session.Token x.meta.SetSessionToken(t.ToV2())
t.WriteToV2(&tv2)
x.meta.SetSessionToken(&tv2)
} }
// WithBearerToken attaches bearer token to be used for the operation. // WithBearerToken attaches bearer token to be used for the operation.
@ -63,28 +45,20 @@ func (x *PrmObjectHash) WithinSession(t session.Object) {
// If set, underlying eACL rules will be used in access control. // If set, underlying eACL rules will be used in access control.
// //
// Must be signed. // Must be signed.
func (x *PrmObjectHash) WithBearerToken(t bearer.Token) { func (x *PrmObjectHash) WithBearerToken(t token.BearerToken) {
var v2token acl.BearerToken x.meta.SetBearerToken(t.ToV2())
t.WriteToV2(&v2token)
x.meta.SetBearerToken(&v2token)
} }
// FromContainer specifies FrostFS container of the object. // FromContainer specifies NeoFS container of the object.
// Required parameter. // Required parameter.
func (x *PrmObjectHash) FromContainer(id cid.ID) { func (x *PrmObjectHash) FromContainer(id cid.ID) {
var cidV2 v2refs.ContainerID x.addr.SetContainerID(id.ToV2())
id.WriteToV2(&cidV2)
x.addr.SetContainerID(&cidV2)
} }
// ByID specifies identifier of the requested object. // ByID specifies identifier of the requested object.
// Required parameter. // Required parameter.
func (x *PrmObjectHash) ByID(id oid.ID) { func (x *PrmObjectHash) ByID(id oid.ID) {
var idV2 v2refs.ObjectID x.addr.SetObjectID(id.ToV2())
id.WriteToV2(&idV2)
x.addr.SetObjectID(&idV2)
} }
// SetRangeList sets list of ranges in (offset, length) pair format. // SetRangeList sets list of ranges in (offset, length) pair format.
@ -112,7 +86,7 @@ func (x *PrmObjectHash) SetRangeList(r ...uint64) {
// //
// By default, SHA256 hash function is used. // By default, SHA256 hash function is used.
func (x *PrmObjectHash) TillichZemorAlgo() { func (x *PrmObjectHash) TillichZemorAlgo() {
x.csAlgo = v2refs.TillichZemor x.tillichZemor = true
} }
// UseSalt sets the salt to XOR the data range before hashing. // UseSalt sets the salt to XOR the data range before hashing.
@ -127,7 +101,11 @@ func (x *PrmObjectHash) UseSalt(salt []byte) {
// //
// Slice must not be mutated until the operation completes. // Slice must not be mutated until the operation completes.
func (x *PrmObjectHash) WithXHeaders(hs ...string) { func (x *PrmObjectHash) WithXHeaders(hs ...string) {
writeXHeadersToMeta(hs, &x.meta) if len(hs)%2 != 0 {
panic("slice of X-Headers with odd length")
}
prmCommonMeta{xHeaders: hs}.writeToMetaHeader(&x.meta)
} }
// ResObjectHash groups resulting values of ObjectHash operation. // ResObjectHash groups resulting values of ObjectHash operation.
@ -143,18 +121,18 @@ func (x ResObjectHash) Checksums() [][]byte {
} }
// ObjectHash requests checksum of the range list of the object payload using // ObjectHash requests checksum of the range list of the object payload using
// FrostFS API protocol. // NeoFS API protocol.
// //
// Returns a list of checksums in raw form: the format of hashes and their number // Returns a list of checksums in raw form: the format of hashes and their number
// is left for the caller to check. Client preserves the order of the server's response. // is left for the caller to check. Client preserves the order of the server's response.
// //
// Exactly one return value is non-nil. By default, server status is returned in res structure. // Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as `error`, // Any client's internal or transport errors are returned as `error`,
// If PrmInit.ResolveFrostFSFailures has been called, unsuccessful // If WithNeoFSErrorParsing option has been provided, unsuccessful
// FrostFS status codes are returned as `error`, otherwise, are included // NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure. // in the returned result structure.
// //
// Returns an error if parameters are set incorrectly (see PrmObjectHash docs). // Immediately panics if parameters are set incorrectly (see PrmObjectHash docs).
// Context is required and must not be nil. It is used for network communication. // Context is required and must not be nil. It is used for network communication.
// //
// Return statuses: // Return statuses:
@ -162,59 +140,53 @@ func (x ResObjectHash) Checksums() [][]byte {
// - *apistatus.ContainerNotFound; // - *apistatus.ContainerNotFound;
// - *apistatus.ObjectNotFound; // - *apistatus.ObjectNotFound;
// - *apistatus.ObjectAccessDenied; // - *apistatus.ObjectAccessDenied;
// - *apistatus.ObjectOutOfRange;
// - *apistatus.SessionTokenExpired. // - *apistatus.SessionTokenExpired.
func (c *Client) ObjectHash(ctx context.Context, prm PrmObjectHash) (*ResObjectHash, error) { func (c *Client) ObjectHash(ctx context.Context, prm PrmObjectHash) (*ResObjectHash, error) {
switch { switch {
case ctx == nil: case ctx == nil:
return nil, errorMissingContext panic(panicMsgMissingContext)
case prm.addr.GetContainerID() == nil: case prm.addr.GetContainerID() == nil:
return nil, errorMissingContainer panic(panicMsgMissingContainer)
case prm.addr.GetObjectID() == nil: case prm.addr.GetObjectID() == nil:
return nil, errorMissingObject panic("missing object")
case len(prm.body.GetRanges()) == 0: case len(prm.body.GetRanges()) == 0:
return nil, errorMissingRanges panic("missing ranges")
} }
// form request body
prm.body.SetAddress(&prm.addr) prm.body.SetAddress(&prm.addr)
if prm.csAlgo == v2refs.UnknownChecksum { // ranges and salt are already by prm setters
prm.body.SetType(v2refs.SHA256)
if prm.tillichZemor {
prm.body.SetType(v2refs.TillichZemor)
} else { } else {
prm.body.SetType(prm.csAlgo) prm.body.SetType(v2refs.SHA256)
} }
// form request
var req v2object.GetRangeHashRequest var req v2object.GetRangeHashRequest
c.prepareRequest(&req, &prm.meta)
req.SetBody(&prm.body) req.SetBody(&prm.body)
req.SetMetaHeader(&prm.meta)
key := c.prm.key // init call context
if prm.keySet { var (
key = prm.key cc contextCall
res ResObjectHash
)
c.initCallContext(&cc)
cc.req = &req
cc.statusRes = &res
cc.call = func() (responseV2, error) {
return rpcapi.HashObjectRange(&c.c, &req, client.WithContext(ctx))
}
cc.result = func(r responseV2) {
res.checksums = r.(*v2object.GetRangeHashResponse).GetBody().GetHashList()
} }
err := signature.SignServiceMessage(&key, &req) // process call
if err != nil { if !cc.processCall() {
return nil, fmt.Errorf("sign request: %w", err) return nil, cc.err
}
resp, err := rpcapi.HashObjectRange(&c.c, &req, client.WithContext(ctx))
if err != nil {
return nil, fmt.Errorf("write request: %w", err)
}
var res ResObjectHash
res.st, err = c.processResponse(resp)
if err != nil {
return nil, err
}
if !apistatus.IsSuccessful(res.st) {
return &res, nil
}
res.checksums = resp.GetBody().GetHashList()
if len(res.checksums) == 0 {
return nil, newErrMissingResponseField("hash list")
} }
return &res, nil return &res, nil

View file

@ -3,106 +3,97 @@ package client
import ( import (
"context" "context"
"crypto/ecdsa" "crypto/ecdsa"
"errors"
"fmt" "fmt"
"io"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl" v2object "github.com/nspcc-dev/neofs-api-go/v2/object"
v2object "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object" rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc" "github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client" v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session" "github.com/nspcc-dev/neofs-sdk-go/object"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature" oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer" "github.com/nspcc-dev/neofs-sdk-go/session"
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status" "github.com/nspcc-dev/neofs-sdk-go/token"
"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"
) )
// PrmObjectPutInit groups parameters of ObjectPutInit operation. // PrmObjectPutInit groups parameters of ObjectPutInit operation.
type PrmObjectPutInit struct { //
copyNum uint32 // At the moment the operation is not parameterized, however,
key *ecdsa.PrivateKey // the structure is still declared for backward compatibility.
meta v2session.RequestMetaHeader type PrmObjectPutInit struct{}
}
// SetCopiesNumber sets number of object copies that is enough to consider put successful.
func (x *PrmObjectPutInit) SetCopiesNumber(copiesNumber uint32) {
x.copyNum = copiesNumber
}
// ResObjectPut groups the final result values of ObjectPutInit operation. // ResObjectPut groups the final result values of ObjectPutInit operation.
type ResObjectPut struct { type ResObjectPut struct {
statusRes statusRes
obj oid.ID resp v2object.PutResponse
} }
// StoredObjectID returns identifier of the saved object. // ReadStoredObjectID reads identifier of the saved object.
func (x ResObjectPut) StoredObjectID() oid.ID { // Returns false if ID is missing (not read).
return x.obj func (x *ResObjectPut) ReadStoredObjectID(id *oid.ID) bool {
idv2 := x.resp.GetBody().GetObjectID()
if idv2 == nil {
return false
}
*id = *oid.NewIDFromV2(idv2) // need smth better
return true
} }
// ObjectWriter is designed to write one object to FrostFS system. // ObjectWriter is designed to write one object to NeoFS system.
// //
// Must be initialized using Client.ObjectPutInit, any other // Must be initialized using Client.ObjectPutInit, any other
// usage is unsafe. // usage is unsafe.
type ObjectWriter struct { type ObjectWriter struct {
cancelCtxStream context.CancelFunc cancelCtxStream context.CancelFunc
client *Client ctxCall contextCall
stream interface {
Write(*v2object.PutRequest) error
Close() error
}
key *ecdsa.PrivateKey // initially bound tp contextCall
res ResObjectPut metaHdr v2session.RequestMetaHeader
err error
// initially bound to contextCall
partInit v2object.PutObjectPartInit
chunkCalled bool chunkCalled bool
respV2 v2object.PutResponse
req v2object.PutRequest
partInit v2object.PutObjectPartInit
partChunk v2object.PutObjectPartChunk partChunk v2object.PutObjectPartChunk
} }
// UseKey specifies private key to sign the requests. // UseKey specifies private key to sign the requests.
// If key is not provided, then Client default key is used. // If key is not provided, then Client default key is used.
func (x *PrmObjectPutInit) UseKey(key ecdsa.PrivateKey) { func (x *ObjectWriter) UseKey(key ecdsa.PrivateKey) {
x.key = &key x.ctxCall.key = key
} }
// WithBearerToken attaches bearer token to be used for the operation. // WithBearerToken attaches bearer token to be used for the operation.
// Should be called once before any writing steps. // Should be called once before any writing steps.
func (x *PrmObjectPutInit) WithBearerToken(t bearer.Token) { func (x *ObjectWriter) WithBearerToken(t token.BearerToken) {
var v2token acl.BearerToken x.metaHdr.SetBearerToken(t.ToV2())
t.WriteToV2(&v2token)
x.meta.SetBearerToken(&v2token)
} }
// WithinSession specifies session within which object should be stored. // WithinSession specifies session within which object should be stored.
// Should be called once before any writing steps. // Should be called once before any writing steps.
func (x *PrmObjectPutInit) WithinSession(t session.Object) { func (x *ObjectWriter) WithinSession(t session.Token) {
var tv2 v2session.Token x.metaHdr.SetSessionToken(t.ToV2())
t.WriteToV2(&tv2)
x.meta.SetSessionToken(&tv2)
} }
// MarkLocal tells the server to execute the operation locally. // MarkLocal tells the server to execute the operation locally.
func (x *PrmObjectPutInit) MarkLocal() { func (x *ObjectWriter) MarkLocal() {
x.meta.SetTTL(1) x.metaHdr.SetTTL(1)
} }
// WithXHeaders specifies list of extended headers (string key-value pairs) // WithXHeaders specifies list of extended headers (string key-value pairs)
// to be attached to the request. Must have an even length. // to be attached to the request. Must have an even length.
// //
// Slice must not be mutated until the operation completes. // Slice must not be mutated until the operation completes.
func (x *PrmObjectPutInit) WithXHeaders(hs ...string) { func (x *ObjectWriter) WithXHeaders(hs ...string) {
writeXHeadersToMeta(hs, &x.meta) if len(hs)%2 != 0 {
panic("slice of X-Headers with odd length")
}
prmCommonMeta{xHeaders: hs}.writeToMetaHeader(&x.metaHdr)
} }
// WriteHeader writes header of the object. Result means success. // WriteHeader writes header of the object. Result means success.
@ -114,17 +105,7 @@ func (x *ObjectWriter) WriteHeader(hdr object.Object) bool {
x.partInit.SetHeader(v2Hdr.GetHeader()) x.partInit.SetHeader(v2Hdr.GetHeader())
x.partInit.SetSignature(v2Hdr.GetSignature()) x.partInit.SetSignature(v2Hdr.GetSignature())
x.req.GetBody().SetObjectPart(&x.partInit) return x.ctxCall.writeRequest()
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
} }
// WritePayloadChunk writes chunk of the object payload. Result means success. // WritePayloadChunk writes chunk of the object payload. Result means success.
@ -132,7 +113,7 @@ func (x *ObjectWriter) WriteHeader(hdr object.Object) bool {
func (x *ObjectWriter) WritePayloadChunk(chunk []byte) bool { func (x *ObjectWriter) WritePayloadChunk(chunk []byte) bool {
if !x.chunkCalled { if !x.chunkCalled {
x.chunkCalled = true x.chunkCalled = true
x.req.GetBody().SetObjectPart(&x.partChunk) x.ctxCall.req.(*v2object.PutRequest).GetBody().SetObjectPart(&x.partChunk)
} }
for ln := len(chunk); ln > 0; ln = len(chunk) { for ln := len(chunk); ln > 0; ln = len(chunk) {
@ -157,16 +138,8 @@ func (x *ObjectWriter) WritePayloadChunk(chunk []byte) bool {
// It is mentally assumed that allocating and filling the buffer is better than // It is mentally assumed that allocating and filling the buffer is better than
// synchronous sending, but this needs to be tested. // synchronous sending, but this needs to be tested.
x.partChunk.SetChunk(chunk[:ln]) x.partChunk.SetChunk(chunk[:ln])
x.req.SetVerificationHeader(nil)
x.err = signature.SignServiceMessage(x.key, &x.req) if !x.ctxCall.writeRequest() {
if x.err != nil {
x.err = fmt.Errorf("sign message: %w", x.err)
return false
}
x.err = x.stream.Write(&x.req)
if x.err != nil {
return false return false
} }
@ -181,7 +154,7 @@ func (x *ObjectWriter) WritePayloadChunk(chunk []byte) bool {
// //
// Exactly one return value is non-nil. By default, server status is returned in res structure. // Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as Go built-in error. // Any client's internal or transport errors are returned as Go built-in error.
// If Client is tuned to resolve FrostFS API statuses, then FrostFS failures // If Client is tuned to resolve NeoFS API statuses, then NeoFS failures
// codes are returned as error. // codes are returned as error.
// //
// Return statuses: // Return statuses:
@ -195,73 +168,66 @@ func (x *ObjectWriter) WritePayloadChunk(chunk []byte) bool {
func (x *ObjectWriter) Close() (*ResObjectPut, error) { func (x *ObjectWriter) Close() (*ResObjectPut, error) {
defer x.cancelCtxStream() defer x.cancelCtxStream()
// Ignore io.EOF error, because it is expected error for client-side if x.ctxCall.err != nil {
// stream termination by the server. E.g. when stream contains invalid return nil, x.ctxCall.err
// 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 { if !x.ctxCall.close() {
return nil, x.err return nil, x.ctxCall.err
} }
x.res.st, x.err = x.client.processResponse(&x.respV2) if !x.ctxCall.processResponse() {
if x.err != nil { return nil, x.ctxCall.err
return nil, x.err
} }
if !apistatus.IsSuccessful(x.res.st) { return x.ctxCall.statusRes.(*ResObjectPut), nil
return &x.res, nil
}
const fieldID = "ID"
idV2 := x.respV2.GetBody().GetObjectID()
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
} }
// ObjectPutInit initiates writing an object through a remote server using FrostFS API protocol. // ObjectPutInit initiates writing an object through a remote server using NeoFS API protocol.
// //
// The call only opens the transmission channel, explicit recording is done using the ObjectWriter. // The call only opens the transmission channel, explicit recording is done using the ObjectWriter.
// Exactly one return value is non-nil. Resulting writer must be finally closed. // Exactly one return value is non-nil. Resulting writer must be finally closed.
// //
// Returns an error if parameters are set incorrectly.
// Context is required and must not be nil. It is used for network communication. // Context is required and must not be nil. It is used for network communication.
func (c *Client) ObjectPutInit(ctx context.Context, prm PrmObjectPutInit) (*ObjectWriter, error) { func (c *Client) ObjectPutInit(ctx context.Context, _ PrmObjectPutInit) (*ObjectWriter, error) {
// check parameters // check parameters
if ctx == nil { if ctx == nil {
return nil, errorMissingContext panic(panicMsgMissingContext)
} }
var w ObjectWriter // open stream
var (
res ResObjectPut
w ObjectWriter
)
ctx, cancel := context.WithCancel(ctx) ctx, w.cancelCtxStream = context.WithCancel(ctx)
stream, err := rpcapi.PutObject(&c.c, &w.respV2, client.WithContext(ctx))
stream, err := rpcapi.PutObject(&c.c, &res.resp, client.WithContext(ctx))
if err != nil { if err != nil {
cancel()
return nil, fmt.Errorf("open stream: %w", err) return nil, fmt.Errorf("open stream: %w", err)
} }
w.key = &c.prm.key // form request body
if prm.key != nil { var body v2object.PutRequestBody
w.key = prm.key
// form request
var req v2object.PutRequest
req.SetBody(&body)
req.SetMetaHeader(&w.metaHdr)
body.SetObjectPart(&w.partInit)
// init call context
c.initCallContext(&w.ctxCall)
w.ctxCall.req = &req
w.ctxCall.statusRes = &res
w.ctxCall.resp = &res.resp
w.ctxCall.wReq = func() error {
return stream.Write(&req)
} }
w.cancelCtxStream = cancel w.ctxCall.closer = stream.Close
w.client = c
w.stream = stream
w.partInit.SetCopiesNumber(prm.copyNum)
w.req.SetBody(new(v2object.PutRequestBody))
c.prepareRequest(&w.req, &prm.meta)
return &w, nil return &w, nil
} }

View file

@ -7,36 +7,40 @@ import (
"fmt" "fmt"
"io" "io"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl" v2object "github.com/nspcc-dev/neofs-api-go/v2/object"
v2object "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object" v2refs "github.com/nspcc-dev/neofs-api-go/v2/refs"
v2refs "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs" rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc" "github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client" v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session" apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature" cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer" "github.com/nspcc-dev/neofs-sdk-go/object"
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status" oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" "github.com/nspcc-dev/neofs-sdk-go/session"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object" "github.com/nspcc-dev/neofs-sdk-go/token"
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
) )
// PrmObjectSearch groups parameters of ObjectSearch operation. // PrmObjectSearch groups parameters of ObjectSearch operation.
type PrmObjectSearch struct { type PrmObjectSearch struct {
meta v2session.RequestMetaHeader prmCommonMeta
key *ecdsa.PrivateKey local bool
sessionSet bool
session session.Token
bearerSet bool
bearer token.BearerToken
cnrSet bool cnrSet bool
cnrID cid.ID cnr cid.ID
filters object.SearchFilters filters object.SearchFilters
} }
// MarkLocal tells the server to execute the operation locally. // MarkLocal tells the server to execute the operation locally.
func (x *PrmObjectSearch) MarkLocal() { func (x *PrmObjectSearch) MarkLocal() {
x.meta.SetTTL(1) x.local = true
} }
// WithinSession specifies session within which the search query must be executed. // WithinSession specifies session within which the search query must be executed.
@ -45,10 +49,9 @@ func (x *PrmObjectSearch) MarkLocal() {
// This may affect the execution of an operation (e.g. access control). // This may affect the execution of an operation (e.g. access control).
// //
// Must be signed. // Must be signed.
func (x *PrmObjectSearch) WithinSession(t session.Object) { func (x *PrmObjectSearch) WithinSession(t session.Token) {
var tokv2 v2session.Token x.session = t
t.WriteToV2(&tokv2) x.sessionSet = true
x.meta.SetSessionToken(&tokv2)
} }
// WithBearerToken attaches bearer token to be used for the operation. // WithBearerToken attaches bearer token to be used for the operation.
@ -56,30 +59,15 @@ func (x *PrmObjectSearch) WithinSession(t session.Object) {
// If set, underlying eACL rules will be used in access control. // If set, underlying eACL rules will be used in access control.
// //
// Must be signed. // Must be signed.
func (x *PrmObjectSearch) WithBearerToken(t bearer.Token) { func (x *PrmObjectSearch) WithBearerToken(t token.BearerToken) {
var v2token acl.BearerToken x.bearer = t
t.WriteToV2(&v2token) x.bearerSet = true
x.meta.SetBearerToken(&v2token)
}
// WithXHeaders specifies list of extended headers (string key-value pairs)
// to be attached to the request. Must have an even length.
//
// Slice must not be mutated until the operation completes.
func (x *PrmObjectSearch) WithXHeaders(hs ...string) {
writeXHeadersToMeta(hs, &x.meta)
}
// UseKey specifies private key to sign the requests.
// If key is not provided, then Client default key is used.
func (x *PrmObjectSearch) UseKey(key ecdsa.PrivateKey) {
x.key = &key
} }
// InContainer specifies the container in which to look for objects. // InContainer specifies the container in which to look for objects.
// Required parameter. // Required parameter.
func (x *PrmObjectSearch) InContainer(id cid.ID) { func (x *PrmObjectSearch) InContainer(id cid.ID) {
x.cnrID = id x.cnr = id
x.cnrSet = true x.cnrSet = true
} }
@ -94,20 +82,28 @@ type ResObjectSearch struct {
statusRes statusRes
} }
// ObjectListReader is designed to read list of object identifiers from FrostFS system. // ObjectListReader is designed to read list of object identifiers from NeoFS system.
// //
// Must be initialized using Client.ObjectSearch, any other usage is unsafe. // Must be initialized using Client.ObjectSearch, any other usage is unsafe.
type ObjectListReader struct { type ObjectListReader struct {
client *Client
cancelCtxStream context.CancelFunc cancelCtxStream context.CancelFunc
err error
res ResObjectSearch ctxCall contextCall
stream interface {
Read(resp *v2object.SearchResponse) error reqWritten bool
}
// initially bound to contextCall
bodyResp v2object.SearchResponseBody
tail []v2refs.ObjectID tail []v2refs.ObjectID
} }
// UseKey specifies private key to sign the requests.
// If key is not provided, then Client default key is used.
func (x *ObjectListReader) UseKey(key ecdsa.PrivateKey) {
x.ctxCall.key = key
}
// Read reads another list of the object identifiers. Works similar to // Read reads another list of the object identifiers. Works similar to
// io.Reader.Read but copies oid.ID and returns success flag instead of error. // io.Reader.Read but copies oid.ID and returns success flag instead of error.
// //
@ -119,33 +115,58 @@ func (x *ObjectListReader) Read(buf []oid.ID) (int, bool) {
panic("empty buffer in ObjectListReader.ReadList") panic("empty buffer in ObjectListReader.ReadList")
} }
read := copyIDBuffers(buf, x.tail) if !x.reqWritten {
if !x.ctxCall.writeRequest() {
return 0, false
}
x.reqWritten = true
}
// read remaining tail
read := len(x.tail)
if read > len(buf) {
read = len(buf)
}
for i := 0; i < read; i++ {
buf[i] = *oid.NewIDFromV2(&x.tail[i]) // need smth better
}
x.tail = x.tail[read:] x.tail = x.tail[read:]
if len(buf) == read { if len(buf) == read {
return read, true return read, true
} }
for { var ok bool
var resp v2object.SearchResponse var ids []v2refs.ObjectID
x.err = x.stream.Read(&resp) var i, ln, rem int
if x.err != nil {
return read, false
}
x.res.st, x.err = x.client.processResponse(&resp) for {
if x.err != nil || !apistatus.IsSuccessful(x.res.st) { // receive next message
ok = x.ctxCall.readResponse()
if !ok {
return read, false return read, false
} }
// read new chunk of objects // read new chunk of objects
ids := resp.GetBody().GetIDList() ids = x.bodyResp.GetIDList()
if len(ids) == 0 {
ln = len(ids)
if ln == 0 {
// just skip empty lists since they are not prohibited by protocol // just skip empty lists since they are not prohibited by protocol
continue continue
} }
ln := copyIDBuffers(buf[read:], ids) if rem = len(buf) - read; ln > rem {
ln = rem
}
for i = 0; i < ln; i++ {
buf[read+i] = *oid.NewIDFromV2(&ids[i]) // need smth better
}
read += ln read += ln
if read == len(buf) { if read == len(buf) {
@ -157,14 +178,6 @@ func (x *ObjectListReader) Read(buf []oid.ID) (int, bool) {
} }
} }
func copyIDBuffers(dst []oid.ID, src []v2refs.ObjectID) int {
var i int
for ; i < len(dst) && i < len(src); i++ {
_ = dst[i].ReadFromV2(src[i])
}
return i
}
// Iterate iterates over the list of found object identifiers. // Iterate iterates over the list of found object identifiers.
// f can return true to stop iteration earlier. // f can return true to stop iteration earlier.
// //
@ -194,7 +207,7 @@ func (x *ObjectListReader) Iterate(f func(oid.ID) bool) error {
// //
// Exactly one return value is non-nil. By default, server status is returned in res structure. // Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as Go built-in error. // Any client's internal or transport errors are returned as Go built-in error.
// If Client is tuned to resolve FrostFS API statuses, then FrostFS failures // If Client is tuned to resolve NeoFS API statuses, then NeoFS failures
// codes are returned as error. // codes are returned as error.
// //
// Return statuses: // Return statuses:
@ -205,61 +218,89 @@ func (x *ObjectListReader) Iterate(f func(oid.ID) bool) error {
func (x *ObjectListReader) Close() (*ResObjectSearch, error) { func (x *ObjectListReader) Close() (*ResObjectSearch, error) {
defer x.cancelCtxStream() defer x.cancelCtxStream()
if x.err != nil && !errors.Is(x.err, io.EOF) { if x.ctxCall.err != nil && !errors.Is(x.ctxCall.err, io.EOF) {
return nil, x.err return nil, x.ctxCall.err
} }
return &x.res, nil return x.ctxCall.statusRes.(*ResObjectSearch), nil
} }
// ObjectSearchInit initiates object selection through a remote server using FrostFS API protocol. // ObjectSearchInit initiates object selection through a remote server using NeoFS API protocol.
// //
// The call only opens the transmission channel, explicit fetching of matched objects // The call only opens the transmission channel, explicit fetching of matched objects
// is done using the ObjectListReader. Exactly one return value is non-nil. // is done using the ObjectListReader. Exactly one return value is non-nil.
// Resulting reader must be finally closed. // Resulting reader must be finally closed.
// //
// Returns an error if parameters are set incorrectly (see PrmObjectSearch docs). // Immediately panics if parameters are set incorrectly (see PrmObjectSearch docs).
// Context is required and must not be nil. It is used for network communication. // Context is required and must not be nil. It is used for network communication.
func (c *Client) ObjectSearchInit(ctx context.Context, prm PrmObjectSearch) (*ObjectListReader, error) { func (c *Client) ObjectSearchInit(ctx context.Context, prm PrmObjectSearch) (*ObjectListReader, error) {
// check parameters // check parameters
switch { switch {
case ctx == nil: case ctx == nil:
return nil, errorMissingContext panic(panicMsgMissingContext)
case !prm.cnrSet: case !prm.cnrSet:
return nil, errorMissingContainer panic(panicMsgMissingContainer)
} }
var cidV2 v2refs.ContainerID // form request body
prm.cnrID.WriteToV2(&cidV2)
var body v2object.SearchRequestBody var body v2object.SearchRequestBody
body.SetVersion(1) body.SetVersion(1)
body.SetContainerID(&cidV2) body.SetContainerID(prm.cnr.ToV2())
body.SetFilters(prm.filters.ToV2()) body.SetFilters(prm.filters.ToV2())
// init reader // form meta header
var meta v2session.RequestMetaHeader
if prm.local {
meta.SetTTL(1)
}
if prm.bearerSet {
meta.SetBearerToken(prm.bearer.ToV2())
}
if prm.sessionSet {
meta.SetSessionToken(prm.session.ToV2())
}
prm.prmCommonMeta.writeToMetaHeader(&meta)
// form request
var req v2object.SearchRequest var req v2object.SearchRequest
req.SetBody(&body) req.SetBody(&body)
c.prepareRequest(&req, &prm.meta) req.SetMetaHeader(&meta)
key := prm.key // init reader
if key == nil { var (
key = &c.prm.key r ObjectListReader
} resp v2object.SearchResponse
stream *rpcapi.SearchResponseReader
)
err := signature.SignServiceMessage(key, &req)
if err != nil {
return nil, fmt.Errorf("sign request: %w", err)
}
var r ObjectListReader
ctx, r.cancelCtxStream = context.WithCancel(ctx) ctx, r.cancelCtxStream = context.WithCancel(ctx)
r.stream, err = rpcapi.SearchObjects(&c.c, &req, client.WithContext(ctx)) resp.SetBody(&r.bodyResp)
// init call context
c.initCallContext(&r.ctxCall)
r.ctxCall.req = &req
r.ctxCall.statusRes = new(ResObjectSearch)
r.ctxCall.resp = &resp
r.ctxCall.wReq = func() error {
var err error
stream, err = rpcapi.SearchObjects(&c.c, &req, client.WithContext(ctx))
if err != nil { if err != nil {
return nil, fmt.Errorf("open stream: %w", err) return fmt.Errorf("open stream: %w", err)
}
return nil
}
r.ctxCall.rResp = func() error {
return stream.Read(&resp)
} }
r.client = c
return &r, nil return &r, nil
} }

View file

@ -1,28 +1,26 @@
package client package client
import ( import (
"crypto/ecdsa"
"errors" "errors"
"fmt"
"io" "io"
"testing" "testing"
v2object "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
signatureV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neofs-api-go/v2/object"
"github.com/nspcc-dev/neofs-api-go/v2/refs"
signatureV2 "github.com/nspcc-dev/neofs-api-go/v2/signature"
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
oidtest "github.com/nspcc-dev/neofs-sdk-go/object/id/test"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestObjectSearch(t *testing.T) { func TestObjectSearch(t *testing.T) {
ids := make([]oid.ID, 20) ids := make([]oid.ID, 20)
for i := range ids { for i := range ids {
ids[i] = oidtest.ID() ids[i] = *oidtest.ID()
} }
p, resp := testListReaderResponse(t) resp, setID := testListReaderResponse(t)
buf := make([]oid.ID, 2) buf := make([]oid.ID, 2)
checkRead := func(t *testing.T, expected []oid.ID) { checkRead := func(t *testing.T, expected []oid.ID) {
@ -36,23 +34,38 @@ func TestObjectSearch(t *testing.T) {
require.Panics(t, func() { resp.Read(nil) }) require.Panics(t, func() { resp.Read(nil) })
// both ID fetched // both ID fetched
resp.stream = newSearchStream(p, nil, ids[:3]) setID(ids[:3])
checkRead(t, ids[:2]) checkRead(t, ids[:2])
// one ID cached, second fetched // one ID cached, second fetched
resp.stream = newSearchStream(p, nil, ids[3:6]) setID(ids[3:6])
checkRead(t, ids[2:4]) checkRead(t, ids[2:4])
// both ID cached // both ID cached
resp.stream = nil // shouldn't be called, panic if so resp.ctxCall.resp = nil
checkRead(t, ids[4:6]) checkRead(t, ids[4:6])
// both ID fetched in 2 requests, with empty one in the middle // both ID fetched in 2 requests, with empty one in the middle
resp.stream = newSearchStream(p, nil, ids[6:7], nil, ids[7:8]) var n int
resp.ctxCall.rResp = func() error {
switch n {
case 0:
setID(ids[6:7])
case 1:
setID(nil)
case 2:
setID(ids[7:8])
default:
t.FailNow()
}
n++
return nil
}
checkRead(t, ids[6:8]) checkRead(t, ids[6:8])
// read from tail multiple times // read from tail multiple times
resp.stream = newSearchStream(p, nil, ids[8:11]) resp.ctxCall.rResp = nil
setID(ids[8:11])
buf = buf[:1] buf = buf[:1]
checkRead(t, ids[8:9]) checkRead(t, ids[8:9])
checkRead(t, ids[9:10]) checkRead(t, ids[9:10])
@ -60,20 +73,43 @@ func TestObjectSearch(t *testing.T) {
// handle EOF // handle EOF
buf = buf[:2] buf = buf[:2]
resp.stream = newSearchStream(p, io.EOF, ids[11:12]) n = 0
resp.ctxCall.rResp = func() error {
if n > 0 {
return io.EOF
}
n++
setID(ids[11:12])
return nil
}
checkRead(t, ids[11:12]) checkRead(t, ids[11:12])
} }
func TestObjectIterate(t *testing.T) { func TestObjectIterate(t *testing.T) {
ids := make([]oid.ID, 3) ids := make([]oid.ID, 3)
for i := range ids { for i := range ids {
ids[i] = oidtest.ID() ids[i] = *oidtest.ID()
} }
t.Run("iterate all sequence", func(t *testing.T) { t.Run("iterate all sequence", func(t *testing.T) {
p, resp := testListReaderResponse(t) resp, setID := testListReaderResponse(t)
resp.stream = newSearchStream(p, io.EOF, ids[0:2], nil, ids[2:3]) // Iterate over all sequence
var n int
resp.ctxCall.rResp = func() error {
switch n {
case 0:
setID(ids[0:2])
case 1:
setID(nil)
case 2:
setID(ids[2:3])
default:
return io.EOF
}
n++
return nil
}
var actual []oid.ID var actual []oid.ID
require.NoError(t, resp.Iterate(func(id oid.ID) bool { require.NoError(t, resp.Iterate(func(id oid.ID) bool {
@ -83,10 +119,10 @@ func TestObjectIterate(t *testing.T) {
require.Equal(t, ids[:3], actual) require.Equal(t, ids[:3], actual)
}) })
t.Run("stop by return value", func(t *testing.T) { t.Run("stop by return value", func(t *testing.T) {
p, resp := testListReaderResponse(t) resp, setID := testListReaderResponse(t)
var actual []oid.ID var actual []oid.ID
resp.stream = &singleStreamResponder{key: p, idList: [][]oid.ID{ids}} setID(ids)
require.NoError(t, resp.Iterate(func(id oid.ID) bool { require.NoError(t, resp.Iterate(func(id oid.ID) bool {
actual = append(actual, id) actual = append(actual, id)
return len(actual) == 2 return len(actual) == 2
@ -94,12 +130,22 @@ func TestObjectIterate(t *testing.T) {
require.Equal(t, ids[:2], actual) require.Equal(t, ids[:2], actual)
}) })
t.Run("stop after error", func(t *testing.T) { t.Run("stop after error", func(t *testing.T) {
p, resp := testListReaderResponse(t) resp, setID := testListReaderResponse(t)
expectedErr := errors.New("test error") expectedErr := errors.New("test error")
resp.stream = newSearchStream(p, expectedErr, ids[:2])
var actual []oid.ID var actual []oid.ID
var n int
resp.ctxCall.rResp = func() error {
switch n {
case 0:
setID(ids[:2])
default:
return expectedErr
}
n++
return nil
}
err := resp.Iterate(func(id oid.ID) bool { err := resp.Iterate(func(id oid.ID) bool {
actual = append(actual, id) actual = append(actual, id)
return false return false
@ -109,56 +155,37 @@ func TestObjectIterate(t *testing.T) {
}) })
} }
func testListReaderResponse(t *testing.T) (*ecdsa.PrivateKey, *ObjectListReader) { func testListReaderResponse(t *testing.T) (*ObjectListReader, func(id []oid.ID) *object.SearchResponse) {
p, err := keys.NewPrivateKey() p, err := keys.NewPrivateKey()
require.NoError(t, err) require.NoError(t, err)
return &p.PrivateKey, &ObjectListReader{ obj := &ObjectListReader{
cancelCtxStream: func() {}, cancelCtxStream: func() {},
client: &Client{}, ctxCall: contextCall{
closer: func() error { return nil },
result: func(v2 responseV2) {},
statusRes: new(ResObjectSearch),
},
reqWritten: true,
bodyResp: object.SearchResponseBody{},
tail: nil, tail: nil,
} }
}
func newSearchStream(key *ecdsa.PrivateKey, endError error, idList ...[]oid.ID) *singleStreamResponder { return obj, func(id []oid.ID) *object.SearchResponse {
return &singleStreamResponder{ resp := new(object.SearchResponse)
key: key, resp.SetBody(new(object.SearchResponseBody))
endError: endError,
idList: idList, v2id := make([]refs.ObjectID, len(id))
for i := range id {
v2id[i] = *id[i].ToV2()
} }
} resp.GetBody().SetIDList(v2id)
err := signatureV2.SignServiceMessage(&p.PrivateKey, resp)
type singleStreamResponder struct {
key *ecdsa.PrivateKey
n int
endError error
idList [][]oid.ID
}
func (s *singleStreamResponder) Read(resp *v2object.SearchResponse) error {
if s.n >= len(s.idList) {
if s.endError != nil {
return s.endError
}
panic("unexpected call to `Read`")
}
var body v2object.SearchResponseBody
if s.idList[s.n] != nil {
ids := make([]refs.ObjectID, len(s.idList[s.n]))
for i := range s.idList[s.n] {
s.idList[s.n][i].WriteToV2(&ids[i])
}
body.SetIDList(ids)
}
resp.SetBody(&body)
err := signatureV2.SignServiceMessage(s.key, resp)
if err != nil { if err != nil {
panic(fmt.Errorf("error: %w", err)) t.Fatalf("error: %v", err)
}
obj.ctxCall.resp = resp
obj.bodyResp = *resp.GetBody()
return resp
} }
s.n++
return nil
} }

View file

@ -3,10 +3,10 @@ package client
import ( import (
"context" "context"
v2reputation "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/reputation" v2reputation "github.com/nspcc-dev/neofs-api-go/v2/reputation"
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc" rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client" "github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/reputation" "github.com/nspcc-dev/neofs-sdk-go/reputation"
) )
// PrmAnnounceLocalTrust groups parameters of AnnounceLocalTrust operation. // PrmAnnounceLocalTrust groups parameters of AnnounceLocalTrust operation.
@ -18,13 +18,13 @@ type PrmAnnounceLocalTrust struct {
trusts []reputation.Trust trusts []reputation.Trust
} }
// SetEpoch sets number of FrostFS epoch in which the trust was assessed. // SetEpoch sets number of NeoFS epoch in which the trust was assessed.
// Required parameter, must not be zero. // Required parameter, must not be zero.
func (x *PrmAnnounceLocalTrust) SetEpoch(epoch uint64) { func (x *PrmAnnounceLocalTrust) SetEpoch(epoch uint64) {
x.epoch = epoch x.epoch = epoch
} }
// SetValues sets values describing trust of the client to the FrostFS network participants. // SetValues sets values describing trust of the client to the NeoFS network participants.
// Required parameter. Must not be empty. // Required parameter. Must not be empty.
// //
// Must not be mutated before the end of the operation. // Must not be mutated before the end of the operation.
@ -37,15 +37,15 @@ type ResAnnounceLocalTrust struct {
statusRes statusRes
} }
// AnnounceLocalTrust sends client's trust values to the FrostFS network participants. // AnnounceLocalTrust sends client's trust values to the NeoFS network participants.
// //
// Exactly one return value is non-nil. By default, server status is returned in res structure. // Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as `error`. // Any client's internal or transport errors are returned as `error`.
// If PrmInit.ResolveFrostFSFailures has been called, unsuccessful // If WithNeoFSErrorParsing option has been provided, unsuccessful
// FrostFS status codes are returned as `error`, otherwise, are included // NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure. // in the returned result structure.
// //
// Returns an error if parameters are set incorrectly (see PrmAnnounceLocalTrust docs). // Immediately panics if parameters are set incorrectly (see PrmAnnounceLocalTrust docs).
// Context is required and must not be nil. It is used for network communication. // Context is required and must not be nil. It is used for network communication.
// //
// Return statuses: // Return statuses:
@ -54,24 +54,21 @@ func (c *Client) AnnounceLocalTrust(ctx context.Context, prm PrmAnnounceLocalTru
// check parameters // check parameters
switch { switch {
case ctx == nil: case ctx == nil:
return nil, errorMissingContext panic(panicMsgMissingContext)
case prm.epoch == 0: case prm.epoch == 0:
return nil, errorZeroEpoch panic("zero epoch")
case len(prm.trusts) == 0: case len(prm.trusts) == 0:
return nil, errorMissingTrusts panic("missing trusts")
} }
// form request body // form request body
reqBody := new(v2reputation.AnnounceLocalTrustRequestBody) reqBody := new(v2reputation.AnnounceLocalTrustRequestBody)
reqBody.SetEpoch(prm.epoch) reqBody.SetEpoch(prm.epoch)
trusts := make([]v2reputation.Trust, len(prm.trusts)) trusts := make([]reputation.Trust, len(prm.trusts))
copy(trusts, prm.trusts)
for i := range prm.trusts { reqBody.SetTrusts(reputation.TrustsToV2(trusts))
prm.trusts[i].WriteToV2(&trusts[i])
}
reqBody.SetTrusts(trusts)
// form request // form request
var req v2reputation.AnnounceLocalTrustRequest var req v2reputation.AnnounceLocalTrustRequest
@ -113,7 +110,7 @@ type PrmAnnounceIntermediateTrust struct {
trust reputation.PeerToPeerTrust trust reputation.PeerToPeerTrust
} }
// SetEpoch sets number of FrostFS epoch with which client's calculation algorithm is initialized. // SetEpoch sets number of NeoFS epoch with which client's calculation algorithm is initialized.
// Required parameter, must not be zero. // Required parameter, must not be zero.
func (x *PrmAnnounceIntermediateTrust) SetEpoch(epoch uint64) { func (x *PrmAnnounceIntermediateTrust) SetEpoch(epoch uint64) {
x.epoch = epoch x.epoch = epoch
@ -137,16 +134,16 @@ type ResAnnounceIntermediateTrust struct {
statusRes statusRes
} }
// AnnounceIntermediateTrust sends global trust values calculated for the specified FrostFS network participants // AnnounceIntermediateTrust sends global trust values calculated for the specified NeoFS network participants
// at some stage of client's calculation algorithm. // at some stage of client's calculation algorithm.
// //
// Exactly one return value is non-nil. By default, server status is returned in res structure. // Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as `error`. // Any client's internal or transport errors are returned as `error`.
// If PrmInit.ResolveFrostFSFailures has been called, unsuccessful // If WithNeoFSErrorParsing option has been provided, unsuccessful
// FrostFS status codes are returned as `error`, otherwise, are included // NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure. // in the returned result structure.
// //
// Returns an error if parameters are set incorrectly (see PrmAnnounceIntermediateTrust docs). // Immediately panics if parameters are set incorrectly (see PrmAnnounceIntermediateTrust docs).
// Context is required and must not be nil. It is used for network communication. // Context is required and must not be nil. It is used for network communication.
// //
// Return statuses: // Return statuses:
@ -155,21 +152,18 @@ func (c *Client) AnnounceIntermediateTrust(ctx context.Context, prm PrmAnnounceI
// check parameters // check parameters
switch { switch {
case ctx == nil: case ctx == nil:
return nil, errorMissingContext panic(panicMsgMissingContext)
case prm.epoch == 0: case prm.epoch == 0:
return nil, errorZeroEpoch panic("zero epoch")
case !prm.trustSet: case !prm.trustSet:
return nil, errorTrustNotSet panic("current trust value not set")
} }
var trust v2reputation.PeerToPeerTrust
prm.trust.WriteToV2(&trust)
// form request body // form request body
reqBody := new(v2reputation.AnnounceIntermediateResultRequestBody) reqBody := new(v2reputation.AnnounceIntermediateResultRequestBody)
reqBody.SetEpoch(prm.epoch) reqBody.SetEpoch(prm.epoch)
reqBody.SetIteration(prm.iter) reqBody.SetIteration(prm.iter)
reqBody.SetTrust(&trust) reqBody.SetTrust(prm.trust.ToV2())
// form request // form request
var req v2reputation.AnnounceIntermediateResultRequest var req v2reputation.AnnounceIntermediateResultRequest

View file

@ -1,12 +1,10 @@
package client package client
import "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session" import "github.com/nspcc-dev/neofs-api-go/v2/session"
// ResponseMetaInfo groups meta information about any FrostFS API response. // ResponseMetaInfo groups meta information about any NeoFS API response.
type ResponseMetaInfo struct { type ResponseMetaInfo struct {
key []byte key []byte
epoch uint64
} }
type responseV2 interface { type responseV2 interface {
@ -20,8 +18,3 @@ type responseV2 interface {
func (x ResponseMetaInfo) ResponderKey() []byte { func (x ResponseMetaInfo) ResponderKey() []byte {
return x.key return x.key
} }
// Epoch returns local FrostFS epoch of the server.
func (x ResponseMetaInfo) Epoch() uint64 {
return x.epoch
}

View file

@ -2,13 +2,11 @@ package client
import ( import (
"context" "context"
"crypto/ecdsa"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs" rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc" "github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client" v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session" "github.com/nspcc-dev/neofs-sdk-go/owner"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
) )
// PrmSessionCreate groups parameters of SessionCreate operation. // PrmSessionCreate groups parameters of SessionCreate operation.
@ -16,9 +14,6 @@ type PrmSessionCreate struct {
prmCommonMeta prmCommonMeta
exp uint64 exp uint64
keySet bool
key ecdsa.PrivateKey
} }
// SetExp sets number of the last NepFS epoch in the lifetime of the session after which it will be expired. // SetExp sets number of the last NepFS epoch in the lifetime of the session after which it will be expired.
@ -26,13 +21,6 @@ func (x *PrmSessionCreate) SetExp(exp uint64) {
x.exp = exp x.exp = exp
} }
// UseKey specifies private key to sign the requests and compute token owner.
// If key is not provided, then Client default key is used.
func (x *PrmSessionCreate) UseKey(key ecdsa.PrivateKey) {
x.keySet = true
x.key = key
}
// ResSessionCreate groups resulting values of SessionCreate operation. // ResSessionCreate groups resulting values of SessionCreate operation.
type ResSessionCreate struct { type ResSessionCreate struct {
statusRes statusRes
@ -46,7 +34,7 @@ func (x *ResSessionCreate) setID(id []byte) {
x.id = id x.id = id
} }
// ID returns identifier of the opened session in a binary FrostFS API protocol format. // ID returns identifier of the opened session in a binary NeoFS API protocol format.
// //
// Client doesn't retain value so modification is safe. // Client doesn't retain value so modification is safe.
func (x ResSessionCreate) ID() []byte { func (x ResSessionCreate) ID() []byte {
@ -57,7 +45,7 @@ func (x *ResSessionCreate) setSessionKey(key []byte) {
x.sessionKey = key x.sessionKey = key
} }
// PublicKey returns public key of the opened session in a binary FrostFS API protocol format. // PublicKey returns public key of the opened session in a binary NeoFS API protocol format.
func (x ResSessionCreate) PublicKey() []byte { func (x ResSessionCreate) PublicKey() []byte {
return x.sessionKey return x.sessionKey
} }
@ -68,11 +56,11 @@ func (x ResSessionCreate) PublicKey() []byte {
// //
// Exactly one return value is non-nil. By default, server status is returned in res structure. // Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as `error`. // Any client's internal or transport errors are returned as `error`.
// If PrmInit.ResolveFrostFSFailures has been called, unsuccessful // If WithNeoFSErrorParsing option has been provided, unsuccessful
// FrostFS status codes are returned as `error`, otherwise, are included // NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure. // in the returned result structure.
// //
// Returns an error if parameters are set incorrectly (see PrmSessionCreate docs). // Immediately panics if parameters are set incorrectly (see PrmSessionCreate docs).
// Context is required and must not be nil. It is used for network communication. // Context is required and must not be nil. It is used for network communication.
// //
// Return statuses: // Return statuses:
@ -80,22 +68,14 @@ func (x ResSessionCreate) PublicKey() []byte {
func (c *Client) SessionCreate(ctx context.Context, prm PrmSessionCreate) (*ResSessionCreate, error) { func (c *Client) SessionCreate(ctx context.Context, prm PrmSessionCreate) (*ResSessionCreate, error) {
// check context // check context
if ctx == nil { if ctx == nil {
return nil, errorMissingContext panic(panicMsgMissingContext)
} }
ownerKey := c.prm.key.PublicKey ownerID := owner.NewIDFromPublicKey(&c.prm.key.PublicKey)
if prm.keySet {
ownerKey = prm.key.PublicKey
}
var ownerID user.ID
user.IDFromKey(&ownerID, ownerKey)
var ownerIDV2 refs.OwnerID
ownerID.WriteToV2(&ownerIDV2)
// form request body // form request body
reqBody := new(v2session.CreateRequestBody) reqBody := new(v2session.CreateRequestBody)
reqBody.SetOwnerID(&ownerIDV2) reqBody.SetOwnerID(ownerID.ToV2())
reqBody.SetExpiration(prm.exp) reqBody.SetExpiration(prm.exp)
// for request // for request
@ -111,10 +91,6 @@ func (c *Client) SessionCreate(ctx context.Context, prm PrmSessionCreate) (*ResS
) )
c.initCallContext(&cc) c.initCallContext(&cc)
if prm.keySet {
cc.key = prm.key
}
cc.meta = prm.prmCommonMeta cc.meta = prm.prmCommonMeta
cc.req = &req cc.req = &req
cc.statusRes = &res cc.statusRes = &res

View file

@ -3,7 +3,7 @@ package apistatus
import ( import (
"encoding/binary" "encoding/binary"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status" "github.com/nspcc-dev/neofs-api-go/v2/status"
) )
// ServerInternal describes failure statuses related to internal server errors. // ServerInternal describes failure statuses related to internal server errors.
@ -29,9 +29,9 @@ func (x *ServerInternal) fromStatusV2(st *status.Status) {
// ToStatusV2 implements StatusV2 interface method. // ToStatusV2 implements StatusV2 interface method.
// If the value was returned by FromStatusV2, returns the source message. // If the value was returned by FromStatusV2, returns the source message.
// Otherwise, returns message with // Otherwise, returns message with
// - code: INTERNAL; // * code: INTERNAL;
// - string message: empty; // * string message: empty;
// - details: empty. // * details: empty.
func (x ServerInternal) ToStatusV2() *status.Status { func (x ServerInternal) ToStatusV2() *status.Status {
x.v2.SetCode(globalizeCodeV2(status.Internal, status.GlobalizeCommonFail)) x.v2.SetCode(globalizeCodeV2(status.Internal, status.GlobalizeCommonFail))
return &x.v2 return &x.v2
@ -77,9 +77,9 @@ func (x *WrongMagicNumber) fromStatusV2(st *status.Status) {
// ToStatusV2 implements StatusV2 interface method. // ToStatusV2 implements StatusV2 interface method.
// If the value was returned by FromStatusV2, returns the source message. // If the value was returned by FromStatusV2, returns the source message.
// Otherwise, returns message with // Otherwise, returns message with
// - code: WRONG_MAGIC_NUMBER; // * code: WRONG_MAGIC_NUMBER;
// - string message: empty; // * string message: empty;
// - details: empty. // * details: empty.
func (x WrongMagicNumber) ToStatusV2() *status.Status { func (x WrongMagicNumber) ToStatusV2() *status.Status {
x.v2.SetCode(globalizeCodeV2(status.WrongMagicNumber, status.GlobalizeCommonFail)) x.v2.SetCode(globalizeCodeV2(status.WrongMagicNumber, status.GlobalizeCommonFail))
return &x.v2 return &x.v2
@ -104,9 +104,9 @@ func (x *WrongMagicNumber) WriteCorrectMagic(magic uint64) {
// CorrectMagic returns network magic returned by the server. // CorrectMagic returns network magic returned by the server.
// Second value indicates presence status: // Second value indicates presence status:
// - -1 if number is presented in incorrect format // * -1 if number is presented in incorrect format
// - 0 if number is not presented // * 0 if number is not presented
// - +1 otherwise // * +1 otherwise
func (x WrongMagicNumber) CorrectMagic() (magic uint64, ok int8) { func (x WrongMagicNumber) CorrectMagic() (magic uint64, ok int8) {
x.v2.IterateDetails(func(d *status.Detail) bool { x.v2.IterateDetails(func(d *status.Detail) bool {
if d.ID() == status.DetailIDCorrectMagic { if d.ID() == status.DetailIDCorrectMagic {
@ -123,118 +123,3 @@ func (x WrongMagicNumber) CorrectMagic() (magic uint64, ok int8) {
return return
} }
// SignatureVerification describes failure status related to signature verification.
// Instances provide Status and StatusV2 interfaces.
type SignatureVerification struct {
v2 status.Status
}
const defaultSignatureVerificationMsg = "signature verification failed"
func (x SignatureVerification) Error() string {
msg := x.v2.Message()
if msg == "" {
msg = defaultSignatureVerificationMsg
}
return errMessageStatusV2(
globalizeCodeV2(status.SignatureVerificationFail, status.GlobalizeCommonFail),
msg,
)
}
// implements local interface defined in FromStatusV2 func.
func (x *SignatureVerification) 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: SIGNATURE_VERIFICATION_FAIL;
// - string message: written message via SetMessage or
// "signature verification failed" as a default message;
// - details: empty.
func (x SignatureVerification) ToStatusV2() *status.Status {
x.v2.SetCode(globalizeCodeV2(status.SignatureVerificationFail, status.GlobalizeCommonFail))
if x.v2.Message() == "" {
x.v2.SetMessage(defaultSignatureVerificationMsg)
}
return &x.v2
}
// SetMessage writes signature verification failure message.
// Message should be used for debug purposes only.
//
// See also Message.
func (x *SignatureVerification) SetMessage(v string) {
x.v2.SetMessage(v)
}
// Message returns status message. Zero status returns empty message.
// Message should be used for debug purposes only.
//
// See also SetMessage.
func (x SignatureVerification) Message() string {
return x.v2.Message()
}
// NodeUnderMaintenance describes failure status for nodes being under maintenance.
// Instances provide Status and StatusV2 interfaces.
type NodeUnderMaintenance struct {
v2 status.Status
}
const defaultNodeUnderMaintenanceMsg = "node is under maintenance"
// Error implements the error interface.
func (x NodeUnderMaintenance) Error() string {
msg := x.Message()
if msg == "" {
msg = defaultNodeUnderMaintenanceMsg
}
return errMessageStatusV2(
globalizeCodeV2(status.NodeUnderMaintenance, status.GlobalizeCommonFail),
msg,
)
}
func (x *NodeUnderMaintenance) 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: NODE_UNDER_MAINTENANCE;
// - string message: written message via SetMessage or
// "node is under maintenance" as a default message;
// - details: empty.
func (x NodeUnderMaintenance) ToStatusV2() *status.Status {
x.v2.SetCode(globalizeCodeV2(status.NodeUnderMaintenance, status.GlobalizeCommonFail))
if x.v2.Message() == "" {
x.v2.SetMessage(defaultNodeUnderMaintenanceMsg)
}
return &x.v2
}
// SetMessage writes signature verification failure message.
// Message should be used for debug purposes only.
//
// See also Message.
func (x *NodeUnderMaintenance) SetMessage(v string) {
x.v2.SetMessage(v)
}
// Message returns status message. Zero status returns empty message.
// Message should be used for debug purposes only.
//
// See also SetMessage.
func (x NodeUnderMaintenance) Message() string {
return x.v2.Message()
}

View file

@ -3,8 +3,8 @@ package apistatus_test
import ( import (
"testing" "testing"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status" "github.com/nspcc-dev/neofs-api-go/v2/status"
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status" apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -50,81 +50,3 @@ func TestWrongMagicNumber_CorrectMagic(t *testing.T) {
_, ok = st.CorrectMagic() _, ok = st.CorrectMagic()
require.EqualValues(t, -1, ok) require.EqualValues(t, -1, ok)
} }
func TestSignatureVerification(t *testing.T) {
t.Run("default", func(t *testing.T) {
var st apistatus.SignatureVerification
require.Empty(t, st.Message())
})
t.Run("custom message", func(t *testing.T) {
var st apistatus.SignatureVerification
msg := "some message"
st.SetMessage(msg)
stV2 := st.ToStatusV2()
require.Equal(t, msg, st.Message())
require.Equal(t, msg, stV2.Message())
})
t.Run("empty to V2", func(t *testing.T) {
var st apistatus.SignatureVerification
stV2 := st.ToStatusV2()
require.Equal(t, "signature verification failed", stV2.Message())
})
t.Run("non-empty to V2", func(t *testing.T) {
var st apistatus.SignatureVerification
msg := "some other msg"
st.SetMessage(msg)
stV2 := st.ToStatusV2()
require.Equal(t, msg, stV2.Message())
})
}
func TestNodeUnderMaintenance(t *testing.T) {
t.Run("default", func(t *testing.T) {
var st apistatus.NodeUnderMaintenance
require.Empty(t, st.Message())
})
t.Run("custom message", func(t *testing.T) {
var st apistatus.NodeUnderMaintenance
msg := "some message"
st.SetMessage(msg)
stV2 := st.ToStatusV2()
require.Equal(t, msg, st.Message())
require.Equal(t, msg, stV2.Message())
})
t.Run("empty to V2", func(t *testing.T) {
var st apistatus.NodeUnderMaintenance
stV2 := st.ToStatusV2()
require.Empty(t, "", stV2.Message())
})
t.Run("non-empty to V2", func(t *testing.T) {
var st apistatus.NodeUnderMaintenance
msg := "some other msg"
st.SetMessage(msg)
stV2 := st.ToStatusV2()
require.Equal(t, msg, stV2.Message())
})
}

View file

@ -1,8 +1,8 @@
package apistatus package apistatus
import ( import (
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container" "github.com/nspcc-dev/neofs-api-go/v2/container"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status" "github.com/nspcc-dev/neofs-api-go/v2/status"
) )
// ContainerNotFound describes status of the failure because of the missing container. // ContainerNotFound describes status of the failure because of the missing container.
@ -11,17 +11,10 @@ type ContainerNotFound struct {
v2 status.Status v2 status.Status
} }
const defaultContainerNotFoundMsg = "container not found"
func (x ContainerNotFound) Error() string { func (x ContainerNotFound) Error() string {
msg := x.v2.Message()
if msg == "" {
msg = defaultContainerNotFoundMsg
}
return errMessageStatusV2( return errMessageStatusV2(
globalizeCodeV2(container.StatusNotFound, container.GlobalizeFail), globalizeCodeV2(container.StatusNotFound, container.GlobalizeFail),
msg, x.v2.Message(),
) )
} }
@ -33,49 +26,11 @@ func (x *ContainerNotFound) fromStatusV2(st *status.Status) {
// ToStatusV2 implements StatusV2 interface method. // ToStatusV2 implements StatusV2 interface method.
// If the value was returned by FromStatusV2, returns the source message. // If the value was returned by FromStatusV2, returns the source message.
// Otherwise, returns message with // Otherwise, returns message with
// - code: CONTAINER_NOT_FOUND; // * code: CONTAINER_NOT_FOUND;
// - string message: "container not found"; // * string message: "container not found";
// - details: empty. // * details: empty.
func (x ContainerNotFound) ToStatusV2() *status.Status { func (x ContainerNotFound) ToStatusV2() *status.Status {
x.v2.SetCode(globalizeCodeV2(container.StatusNotFound, container.GlobalizeFail)) x.v2.SetCode(globalizeCodeV2(container.StatusNotFound, container.GlobalizeFail))
x.v2.SetMessage(defaultContainerNotFoundMsg) x.v2.SetMessage("container not found")
return &x.v2
}
// EACLNotFound describes status of the failure because of the missing eACL
// table.
// Instances provide Status and StatusV2 interfaces.
type EACLNotFound struct {
v2 status.Status
}
const defaultEACLNotFoundMsg = "eACL not found"
func (x EACLNotFound) Error() string {
msg := x.v2.Message()
if msg == "" {
msg = defaultEACLNotFoundMsg
}
return errMessageStatusV2(
globalizeCodeV2(container.StatusEACLNotFound, container.GlobalizeFail),
msg,
)
}
// implements local interface defined in FromStatusV2 func.
func (x *EACLNotFound) 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: EACL_NOT_FOUND;
// - string message: "eACL not found";
// - details: empty.
func (x EACLNotFound) ToStatusV2() *status.Status {
x.v2.SetCode(globalizeCodeV2(container.StatusEACLNotFound, container.GlobalizeFail))
x.v2.SetMessage(defaultEACLNotFoundMsg)
return &x.v2 return &x.v2
} }

View file

@ -1,8 +1,8 @@
package apistatus package apistatus
import ( import (
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object" "github.com/nspcc-dev/neofs-api-go/v2/object"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status" "github.com/nspcc-dev/neofs-api-go/v2/status"
) )
// ObjectLocked describes status of the failure because of the locked object. // ObjectLocked describes status of the failure because of the locked object.
@ -11,17 +11,10 @@ type ObjectLocked struct {
v2 status.Status v2 status.Status
} }
const defaultObjectLockedMsg = "object is locked"
func (x ObjectLocked) Error() string { func (x ObjectLocked) Error() string {
msg := x.v2.Message()
if msg == "" {
msg = defaultObjectLockedMsg
}
return errMessageStatusV2( return errMessageStatusV2(
globalizeCodeV2(object.StatusLocked, object.GlobalizeFail), globalizeCodeV2(object.StatusLocked, object.GlobalizeFail),
msg, x.v2.Message(),
) )
} }
@ -33,12 +26,12 @@ func (x *ObjectLocked) fromStatusV2(st *status.Status) {
// ToStatusV2 implements StatusV2 interface method. // ToStatusV2 implements StatusV2 interface method.
// If the value was returned by FromStatusV2, returns the source message. // If the value was returned by FromStatusV2, returns the source message.
// Otherwise, returns message with // Otherwise, returns message with
// - code: LOCKED; // * code: LOCKED;
// - string message: "object is locked"; // * string message: "object is locked";
// - details: empty. // * details: empty.
func (x ObjectLocked) ToStatusV2() *status.Status { func (x ObjectLocked) ToStatusV2() *status.Status {
x.v2.SetCode(globalizeCodeV2(object.StatusLocked, object.GlobalizeFail)) x.v2.SetCode(globalizeCodeV2(object.StatusLocked, object.GlobalizeFail))
x.v2.SetMessage(defaultObjectLockedMsg) x.v2.SetMessage("object is locked")
return &x.v2 return &x.v2
} }
@ -48,17 +41,10 @@ type LockNonRegularObject struct {
v2 status.Status v2 status.Status
} }
const defaultLockNonRegularObjectMsg = "locking non-regular object is forbidden"
func (x LockNonRegularObject) Error() string { func (x LockNonRegularObject) Error() string {
msg := x.v2.Message()
if msg == "" {
msg = defaultLockNonRegularObjectMsg
}
return errMessageStatusV2( return errMessageStatusV2(
globalizeCodeV2(object.StatusLockNonRegularObject, object.GlobalizeFail), globalizeCodeV2(object.StatusLockNonRegularObject, object.GlobalizeFail),
msg, x.v2.Message(),
) )
} }
@ -70,12 +56,12 @@ func (x *LockNonRegularObject) fromStatusV2(st *status.Status) {
// ToStatusV2 implements StatusV2 interface method. // ToStatusV2 implements StatusV2 interface method.
// If the value was returned by FromStatusV2, returns the source message. // If the value was returned by FromStatusV2, returns the source message.
// Otherwise, returns message with // Otherwise, returns message with
// - code: LOCK_NON_REGULAR_OBJECT; // * code: LOCK_NON_REGULAR_OBJECT;
// - string message: "locking non-regular object is forbidden"; // * string message: "locking non-regular object is forbidden";
// - details: empty. // * details: empty.
func (x LockNonRegularObject) ToStatusV2() *status.Status { func (x LockNonRegularObject) ToStatusV2() *status.Status {
x.v2.SetCode(globalizeCodeV2(object.StatusLockNonRegularObject, object.GlobalizeFail)) x.v2.SetCode(globalizeCodeV2(object.StatusLockNonRegularObject, object.GlobalizeFail))
x.v2.SetMessage(defaultLockNonRegularObjectMsg) x.v2.SetMessage("locking non-regular object is forbidden")
return &x.v2 return &x.v2
} }
@ -85,17 +71,10 @@ type ObjectAccessDenied struct {
v2 status.Status v2 status.Status
} }
const defaultObjectAccessDeniedMsg = "access to object operation denied"
func (x ObjectAccessDenied) Error() string { func (x ObjectAccessDenied) Error() string {
msg := x.v2.Message()
if msg == "" {
msg = defaultObjectAccessDeniedMsg
}
return errMessageStatusV2( return errMessageStatusV2(
globalizeCodeV2(object.StatusAccessDenied, object.GlobalizeFail), globalizeCodeV2(object.StatusAccessDenied, object.GlobalizeFail),
msg, x.v2.Message(),
) )
} }
@ -107,12 +86,12 @@ func (x *ObjectAccessDenied) fromStatusV2(st *status.Status) {
// ToStatusV2 implements StatusV2 interface method. // ToStatusV2 implements StatusV2 interface method.
// If the value was returned by FromStatusV2, returns the source message. // If the value was returned by FromStatusV2, returns the source message.
// Otherwise, returns message with // Otherwise, returns message with
// - code: ACCESS_DENIED; // * code: ACCESS_DENIED;
// - string message: "access to object operation denied"; // * string message: "access to object operation denied";
// - details: empty. // * details: empty.
func (x ObjectAccessDenied) ToStatusV2() *status.Status { func (x ObjectAccessDenied) ToStatusV2() *status.Status {
x.v2.SetCode(globalizeCodeV2(object.StatusAccessDenied, object.GlobalizeFail)) x.v2.SetCode(globalizeCodeV2(object.StatusAccessDenied, object.GlobalizeFail))
x.v2.SetMessage(defaultObjectAccessDeniedMsg) x.v2.SetMessage("access to object operation denied")
return &x.v2 return &x.v2
} }
@ -133,17 +112,10 @@ type ObjectNotFound struct {
v2 status.Status v2 status.Status
} }
const defaultObjectNotFoundMsg = "object not found"
func (x ObjectNotFound) Error() string { func (x ObjectNotFound) Error() string {
msg := x.v2.Message()
if msg == "" {
msg = defaultObjectNotFoundMsg
}
return errMessageStatusV2( return errMessageStatusV2(
globalizeCodeV2(object.StatusNotFound, object.GlobalizeFail), globalizeCodeV2(object.StatusNotFound, object.GlobalizeFail),
msg, x.v2.Message(),
) )
} }
@ -155,12 +127,12 @@ func (x *ObjectNotFound) fromStatusV2(st *status.Status) {
// ToStatusV2 implements StatusV2 interface method. // ToStatusV2 implements StatusV2 interface method.
// If the value was returned by FromStatusV2, returns the source message. // If the value was returned by FromStatusV2, returns the source message.
// Otherwise, returns message with // Otherwise, returns message with
// - code: OBJECT_NOT_FOUND; // * code: OBJECT_NOT_FOUND;
// - string message: "object not found"; // * string message: "object not found";
// - details: empty. // * details: empty.
func (x ObjectNotFound) ToStatusV2() *status.Status { func (x ObjectNotFound) ToStatusV2() *status.Status {
x.v2.SetCode(globalizeCodeV2(object.StatusNotFound, object.GlobalizeFail)) x.v2.SetCode(globalizeCodeV2(object.StatusNotFound, object.GlobalizeFail))
x.v2.SetMessage(defaultObjectNotFoundMsg) x.v2.SetMessage("object not found")
return &x.v2 return &x.v2
} }
@ -170,17 +142,10 @@ type ObjectAlreadyRemoved struct {
v2 status.Status v2 status.Status
} }
const defaultObjectAlreadyRemovedMsg = "object already removed"
func (x ObjectAlreadyRemoved) Error() string { func (x ObjectAlreadyRemoved) Error() string {
msg := x.v2.Message()
if msg == "" {
msg = defaultObjectAlreadyRemovedMsg
}
return errMessageStatusV2( return errMessageStatusV2(
globalizeCodeV2(object.StatusAlreadyRemoved, object.GlobalizeFail), globalizeCodeV2(object.StatusAlreadyRemoved, object.GlobalizeFail),
msg, x.v2.Message(),
) )
} }
@ -192,49 +157,11 @@ func (x *ObjectAlreadyRemoved) fromStatusV2(st *status.Status) {
// ToStatusV2 implements StatusV2 interface method. // ToStatusV2 implements StatusV2 interface method.
// If the value was returned by FromStatusV2, returns the source message. // If the value was returned by FromStatusV2, returns the source message.
// Otherwise, returns message with // Otherwise, returns message with
// - code: OBJECT_ALREADY_REMOVED; // * code: OBJECT_ALREADY_REMOVED;
// - string message: "object already removed"; // * string message: "object already removed";
// - details: empty. // * details: empty.
func (x ObjectAlreadyRemoved) ToStatusV2() *status.Status { func (x ObjectAlreadyRemoved) ToStatusV2() *status.Status {
x.v2.SetCode(globalizeCodeV2(object.StatusAlreadyRemoved, object.GlobalizeFail)) x.v2.SetCode(globalizeCodeV2(object.StatusAlreadyRemoved, object.GlobalizeFail))
x.v2.SetMessage(defaultObjectAlreadyRemovedMsg) x.v2.SetMessage("object already removed")
return &x.v2
}
// ObjectOutOfRange describes status of the failure because of the incorrect
// provided object ranges.
// Instances provide Status and StatusV2 interfaces.
type ObjectOutOfRange struct {
v2 status.Status
}
const defaultObjectOutOfRangeMsg = "out of range"
func (x ObjectOutOfRange) Error() string {
msg := x.v2.Message()
if msg == "" {
msg = defaultObjectOutOfRangeMsg
}
return errMessageStatusV2(
globalizeCodeV2(object.StatusOutOfRange, object.GlobalizeFail),
msg,
)
}
// implements local interface defined in FromStatusV2 func.
func (x *ObjectOutOfRange) 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: OUT_OF_RANGE;
// - string message: "out of range";
// - details: empty.
func (x ObjectOutOfRange) ToStatusV2() *status.Status {
x.v2.SetCode(globalizeCodeV2(object.StatusOutOfRange, object.GlobalizeFail))
x.v2.SetMessage(defaultObjectOutOfRangeMsg)
return &x.v2 return &x.v2
} }

View file

@ -3,7 +3,7 @@ package apistatus_test
import ( import (
"testing" "testing"
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status" apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )

View file

@ -1,8 +1,8 @@
package apistatus package apistatus
import ( import (
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session" "github.com/nspcc-dev/neofs-api-go/v2/session"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status" "github.com/nspcc-dev/neofs-api-go/v2/status"
) )
// SessionTokenNotFound describes status of the failure because of the missing session token. // SessionTokenNotFound describes status of the failure because of the missing session token.
@ -11,17 +11,10 @@ type SessionTokenNotFound struct {
v2 status.Status v2 status.Status
} }
const defaultSessionTokenNotFoundMsg = "session token not found"
func (x SessionTokenNotFound) Error() string { func (x SessionTokenNotFound) Error() string {
msg := x.v2.Message()
if msg == "" {
msg = defaultSessionTokenNotFoundMsg
}
return errMessageStatusV2( return errMessageStatusV2(
globalizeCodeV2(session.StatusTokenNotFound, session.GlobalizeFail), globalizeCodeV2(session.StatusTokenNotFound, session.GlobalizeFail),
msg, x.v2.Message(),
) )
} }
@ -33,12 +26,12 @@ func (x *SessionTokenNotFound) fromStatusV2(st *status.Status) {
// ToStatusV2 implements StatusV2 interface method. // ToStatusV2 implements StatusV2 interface method.
// If the value was returned by FromStatusV2, returns the source message. // If the value was returned by FromStatusV2, returns the source message.
// Otherwise, returns message with // Otherwise, returns message with
// - code: TOKEN_NOT_FOUND; // * code: TOKEN_NOT_FOUND;
// - string message: "session token not found"; // * string message: "session token not found";
// - details: empty. // * details: empty.
func (x SessionTokenNotFound) ToStatusV2() *status.Status { func (x SessionTokenNotFound) ToStatusV2() *status.Status {
x.v2.SetCode(globalizeCodeV2(session.StatusTokenNotFound, session.GlobalizeFail)) x.v2.SetCode(globalizeCodeV2(session.StatusTokenNotFound, session.GlobalizeFail))
x.v2.SetMessage(defaultSessionTokenNotFoundMsg) x.v2.SetMessage("session token not found")
return &x.v2 return &x.v2
} }
@ -48,17 +41,10 @@ type SessionTokenExpired struct {
v2 status.Status v2 status.Status
} }
const defaultSessionTokenExpiredMsg = "expired session token"
func (x SessionTokenExpired) Error() string { func (x SessionTokenExpired) Error() string {
msg := x.v2.Message()
if msg == "" {
msg = defaultSessionTokenExpiredMsg
}
return errMessageStatusV2( return errMessageStatusV2(
globalizeCodeV2(session.StatusTokenExpired, session.GlobalizeFail), globalizeCodeV2(session.StatusTokenExpired, session.GlobalizeFail),
msg, x.v2.Message(),
) )
} }
@ -70,11 +56,11 @@ func (x *SessionTokenExpired) fromStatusV2(st *status.Status) {
// ToStatusV2 implements StatusV2 interface method. // ToStatusV2 implements StatusV2 interface method.
// If the value was returned by FromStatusV2, returns the source message. // If the value was returned by FromStatusV2, returns the source message.
// Otherwise, returns message with // Otherwise, returns message with
// - code: TOKEN_EXPIRED; // * code: TOKEN_EXPIRED;
// - string message: "expired session token"; // * string message: "expired session token";
// - details: empty. // * details: empty.
func (x SessionTokenExpired) ToStatusV2() *status.Status { func (x SessionTokenExpired) ToStatusV2() *status.Status {
x.v2.SetCode(globalizeCodeV2(session.StatusTokenExpired, session.GlobalizeFail)) x.v2.SetCode(globalizeCodeV2(session.StatusTokenExpired, session.GlobalizeFail))
x.v2.SetMessage(defaultSessionTokenExpiredMsg) x.v2.SetMessage("expired session token")
return &x.v2 return &x.v2
} }

View file

@ -1,10 +1,10 @@
package apistatus package apistatus
// Status defines a variety of FrostFS API status returns. // Status defines a variety of NeoFS API status returns.
// //
// All statuses are split into two disjoint subsets: successful and failed, and: // All statuses are split into two disjoint subsets: successful and failed, and:
// - statuses that implement the build-in error interface are considered failed statuses; // * statuses that implement the build-in error interface are considered failed statuses;
// - all other value types are considered successes (nil is a default success). // * 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. // 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. // Nil should be considered as a success, and default switch section - as an unrecognized Status.
@ -14,7 +14,7 @@ package apistatus
// IsSuccessful function should be used (try to avoid nil comparison). // IsSuccessful function should be used (try to avoid nil comparison).
// It should be noted that using direct typecasting is not a compatible approach. // It should be noted that using direct typecasting is not a compatible approach.
// //
// To transport statuses using the FrostFS API V2 protocol, see StatusV2 interface and FromStatusV2 and ToStatusV2 functions. // To transport statuses using the NeoFS API V2 protocol, see StatusV2 interface and FromStatusV2 and ToStatusV2 functions.
type Status interface{} type Status interface{}
// ErrFromStatus converts Status instance to error if it is failed. Returns nil on successful Status. // ErrFromStatus converts Status instance to error if it is failed. Returns nil on successful Status.

View file

@ -4,7 +4,7 @@ import (
"errors" "errors"
"testing" "testing"
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status" apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )

View file

@ -1,7 +1,7 @@
package apistatus package apistatus
import ( import (
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status" "github.com/nspcc-dev/neofs-api-go/v2/status"
) )
// SuccessDefaultV2 represents Status instance of default success. Implements StatusV2. // SuccessDefaultV2 represents Status instance of default success. Implements StatusV2.
@ -20,9 +20,9 @@ func (x *SuccessDefaultV2) fromStatusV2(st *status.Status) {
// ToStatusV2 implements StatusV2 interface method. // ToStatusV2 implements StatusV2 interface method.
// If the value was returned by FromStatusV2, returns the source message. // If the value was returned by FromStatusV2, returns the source message.
// Otherwise, returns message with // Otherwise, returns message with
// - code: OK; // * code: OK;
// - string message: empty; // * string message: empty;
// - details: empty. // * details: empty.
func (x SuccessDefaultV2) ToStatusV2() *status.Status { func (x SuccessDefaultV2) ToStatusV2() *status.Status {
if x.isNil || x.v2 != nil { if x.isNil || x.v2 != nil {
return x.v2 return x.v2

View file

@ -1,7 +1,7 @@
package apistatus package apistatus
import ( import (
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status" "github.com/nspcc-dev/neofs-api-go/v2/status"
) )
type unrecognizedStatusV2 struct { type unrecognizedStatusV2 struct {

View file

@ -3,19 +3,19 @@ package apistatus
import ( import (
"fmt" "fmt"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container" "github.com/nspcc-dev/neofs-api-go/v2/container"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object" "github.com/nspcc-dev/neofs-api-go/v2/object"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session" "github.com/nspcc-dev/neofs-api-go/v2/session"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status" "github.com/nspcc-dev/neofs-api-go/v2/status"
) )
// StatusV2 defines a variety of Status instances compatible with FrostFS API V2 protocol. // 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. // Note: it is not recommended to use this type directly, it is intended for documentation of the library functionality.
type StatusV2 interface { type StatusV2 interface {
Status Status
// ToStatusV2 returns the status as git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status.Status message structure. // ToStatusV2 returns the status as github.com/nspcc-dev/neofs-api-go/v2/status.Status message structure.
ToStatusV2() *status.Status ToStatusV2() *status.Status
} }
@ -28,16 +28,15 @@ type StatusV2 interface {
// Note: notice if the return type is a pointer. // Note: notice if the return type is a pointer.
// //
// Successes: // Successes:
// - status.OK: *SuccessDefaultV2 (this also includes nil argument). // * status.OK: *SuccessDefaultV2 (this also includes nil argument).
// //
// Common failures: // Common failures:
// - status.Internal: *ServerInternal; // * status.Internal: *ServerInternal.
// - status.SignatureVerificationFail: *SignatureVerification.
// //
// Object failures: // Object failures:
// - object.StatusLocked: *ObjectLocked; // * object.StatusLocked: *ObjectLocked;
// - object.StatusLockNonRegularObject: *LockNonRegularObject. // * object.StatusLockNonRegularObject: *LockNonRegularObject.
// - object.StatusAccessDenied: *ObjectAccessDenied. // * object.StatusAccessDenied: *ObjectAccessDenied.
func FromStatusV2(st *status.Status) Status { func FromStatusV2(st *status.Status) Status {
var decoder interface { var decoder interface {
fromStatusV2(*status.Status) fromStatusV2(*status.Status)
@ -56,10 +55,6 @@ func FromStatusV2(st *status.Status) Status {
decoder = new(ServerInternal) decoder = new(ServerInternal)
case status.WrongMagicNumber: case status.WrongMagicNumber:
decoder = new(WrongMagicNumber) decoder = new(WrongMagicNumber)
case status.SignatureVerificationFail:
decoder = new(SignatureVerification)
case status.NodeUnderMaintenance:
decoder = new(NodeUnderMaintenance)
} }
case object.LocalizeFailStatus(&code): case object.LocalizeFailStatus(&code):
switch code { switch code {
@ -73,16 +68,12 @@ func FromStatusV2(st *status.Status) Status {
decoder = new(ObjectNotFound) decoder = new(ObjectNotFound)
case object.StatusAlreadyRemoved: case object.StatusAlreadyRemoved:
decoder = new(ObjectAlreadyRemoved) decoder = new(ObjectAlreadyRemoved)
case object.StatusOutOfRange:
decoder = new(ObjectOutOfRange)
} }
case container.LocalizeFailStatus(&code): case container.LocalizeFailStatus(&code):
//nolint:exhaustive //nolint:exhaustive
switch code { switch code {
case container.StatusNotFound: case container.StatusNotFound:
decoder = new(ContainerNotFound) decoder = new(ContainerNotFound)
case container.StatusEACLNotFound:
decoder = new(EACLNotFound)
} }
case session.LocalizeFailStatus(&code): case session.LocalizeFailStatus(&code):
//nolint:exhaustive //nolint:exhaustive
@ -106,8 +97,7 @@ func FromStatusV2(st *status.Status) Status {
// ToStatusV2 converts Status instance to status.Status message structure. Inverse to FromStatusV2 operation. // ToStatusV2 converts Status instance to status.Status message structure. Inverse to FromStatusV2 operation.
// //
// If argument is the StatusV2 instance, it is converted directly. // If argument is the StatusV2 instance, it is converted directly.
// Otherwise, successes are converted with status.OK code w/o details and message, // Otherwise, successes are converted with status.OK code w/o details and message, failures - with status.Internal.
// failures - with status.Internal and error text message w/o details.
func ToStatusV2(st Status) *status.Status { func ToStatusV2(st Status) *status.Status {
if v, ok := st.(StatusV2); ok { if v, ok := st.(StatusV2); ok {
return v.ToStatusV2() return v.ToStatusV2()
@ -117,10 +107,7 @@ func ToStatusV2(st Status) *status.Status {
return newStatusV2WithLocalCode(status.OK, status.GlobalizeSuccess) return newStatusV2WithLocalCode(status.OK, status.GlobalizeSuccess)
} }
internalErrorStatus := newStatusV2WithLocalCode(status.Internal, status.GlobalizeCommonFail) return newStatusV2WithLocalCode(status.Internal, status.GlobalizeCommonFail)
internalErrorStatus.SetMessage(st.(error).Error()) // type cast never panics because IsSuccessful() checks cast
return internalErrorStatus
} }
func errMessageStatusV2(code interface{}, msg string) string { func errMessageStatusV2(code interface{}, msg string) string {

View file

@ -4,7 +4,7 @@ import (
"errors" "errors"
"testing" "testing"
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status" apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -14,12 +14,10 @@ func TestToStatusV2(t *testing.T) {
for _, testItem := range [...]struct { for _, testItem := range [...]struct {
status interface{} // Status or statusConstructor status interface{} // Status or statusConstructor
codeV2 uint64 codeV2 uint64
messageV2 string
}{ }{
{ {
status: errors.New("some error"), status: errors.New("some error"),
codeV2: 1024, codeV2: 1024,
messageV2: "some error",
}, },
{ {
status: 1, status: 1,
@ -95,24 +93,12 @@ func TestToStatusV2(t *testing.T) {
}), }),
codeV2: 2052, codeV2: 2052,
}, },
{
status: (statusConstructor)(func() apistatus.Status {
return new(apistatus.ObjectOutOfRange)
}),
codeV2: 2053,
},
{ {
status: (statusConstructor)(func() apistatus.Status { status: (statusConstructor)(func() apistatus.Status {
return new(apistatus.ContainerNotFound) return new(apistatus.ContainerNotFound)
}), }),
codeV2: 3072, codeV2: 3072,
}, },
{
status: (statusConstructor)(func() apistatus.Status {
return new(apistatus.EACLNotFound)
}),
codeV2: 3073,
},
{ {
status: (statusConstructor)(func() apistatus.Status { status: (statusConstructor)(func() apistatus.Status {
return new(apistatus.SessionTokenNotFound) return new(apistatus.SessionTokenNotFound)
@ -125,12 +111,6 @@ func TestToStatusV2(t *testing.T) {
}), }),
codeV2: 4097, codeV2: 4097,
}, },
{
status: (statusConstructor)(func() apistatus.Status {
return new(apistatus.NodeUnderMaintenance)
}),
codeV2: 1027,
},
} { } {
var st apistatus.Status var st apistatus.Status
@ -144,9 +124,6 @@ func TestToStatusV2(t *testing.T) {
// must generate the same status.Status message // must generate the same status.Status message
require.EqualValues(t, testItem.codeV2, stv2.Code()) require.EqualValues(t, testItem.codeV2, stv2.Code())
if len(testItem.messageV2) > 0 {
require.Equal(t, testItem.messageV2, stv2.Message())
}
_, ok := st.(apistatus.StatusV2) _, ok := st.(apistatus.StatusV2)
if ok { if ok {
@ -167,12 +144,10 @@ func TestFromStatusV2(t *testing.T) {
for _, testItem := range [...]struct { for _, testItem := range [...]struct {
status interface{} // Status or statusConstructor status interface{} // Status or statusConstructor
codeV2 uint64 codeV2 uint64
messageV2 string
}{ }{
{ {
status: errors.New("some error"), status: errors.New("some error"),
codeV2: 1024, codeV2: 1024,
messageV2: "some error",
}, },
{ {
status: 1, status: 1,
@ -248,24 +223,12 @@ func TestFromStatusV2(t *testing.T) {
}), }),
codeV2: 2052, codeV2: 2052,
}, },
{
status: statusConstructor(func() apistatus.Status {
return new(apistatus.ObjectOutOfRange)
}),
codeV2: 2053,
},
{ {
status: (statusConstructor)(func() apistatus.Status { status: (statusConstructor)(func() apistatus.Status {
return new(apistatus.ContainerNotFound) return new(apistatus.ContainerNotFound)
}), }),
codeV2: 3072, codeV2: 3072,
}, },
{
status: (statusConstructor)(func() apistatus.Status {
return new(apistatus.EACLNotFound)
}),
codeV2: 3073,
},
{ {
status: (statusConstructor)(func() apistatus.Status { status: (statusConstructor)(func() apistatus.Status {
return new(apistatus.SessionTokenNotFound) return new(apistatus.SessionTokenNotFound)
@ -278,12 +241,6 @@ func TestFromStatusV2(t *testing.T) {
}), }),
codeV2: 4097, codeV2: 4097,
}, },
{
status: (statusConstructor)(func() apistatus.Status {
return new(apistatus.NodeUnderMaintenance)
}),
codeV2: 1027,
},
} { } {
var st apistatus.Status var st apistatus.Status
@ -297,9 +254,6 @@ func TestFromStatusV2(t *testing.T) {
// must generate the same status.Status message // must generate the same status.Status message
require.EqualValues(t, testItem.codeV2, stv2.Code()) require.EqualValues(t, testItem.codeV2, stv2.Code())
if len(testItem.messageV2) > 0 {
require.Equal(t, testItem.messageV2, stv2.Message())
}
_, ok := st.(apistatus.StatusV2) _, ok := st.(apistatus.StatusV2)
if ok { if ok {

View file

@ -1,81 +0,0 @@
package acl
import "strconv"
// Op enumerates operations under access control inside container.
// Non-positive values are reserved and depend on context (e.g. unsupported op).
//
// Note that type conversion from- and to numerical types is not recommended,
// use corresponding constants and/or methods instead.
type Op uint32
const (
opZero Op = iota // extreme value for testing
OpObjectGet // Object.Get rpc
OpObjectHead // Object.Head rpc
OpObjectPut // Object.Put rpc
OpObjectDelete // Object.Delete rpc
OpObjectSearch // Object.Search rpc
OpObjectRange // Object.GetRange rpc
OpObjectHash // Object.GetRangeHash rpc
opLast // extreme value for testing
)
// String implements fmt.Stringer.
func (x Op) String() string {
switch x {
default:
return "UNKNOWN#" + strconv.FormatUint(uint64(x), 10)
case OpObjectGet:
return "OBJECT_GET"
case OpObjectHead:
return "OBJECT_HEAD"
case OpObjectPut:
return "OBJECT_PUT"
case OpObjectDelete:
return "OBJECT_DELETE"
case OpObjectSearch:
return "OBJECT_SEARCH"
case OpObjectRange:
return "OBJECT_RANGE"
case OpObjectHash:
return "OBJECT_HASH"
}
}
// Role enumerates roles covered by container ACL. Each role represents
// some party which can be authenticated during container op execution.
// Non-positive values are reserved and depend on context (e.g. unsupported role).
//
// Note that type conversion from- and to numerical types is not recommended,
// use corresponding constants and/or methods instead.
type Role uint32
const (
roleZero Role = iota // extreme value for testing
RoleOwner // container owner
RoleContainer // nodes of the related container
RoleInnerRing // Inner Ring nodes
RoleOthers // all others
roleLast // extreme value for testing
)
// String implements fmt.Stringer.
func (x Role) String() string {
switch x {
default:
return "UNKNOWN#" + strconv.FormatUint(uint64(x), 10)
case RoleOwner:
return "OWNER"
case RoleContainer:
return "CONTAINER"
case RoleInnerRing:
return "INNER_RING"
case RoleOthers:
return "OTHERS"
}
}

View file

@ -1,283 +0,0 @@
package acl
import (
"fmt"
"strconv"
"strings"
)
// Basic represents basic part of the FrostFS container's ACL. It includes
// common (pretty simple) access rules for operations inside the container.
// See FrostFS Specification for details.
//
// One can find some similarities with the traditional Unix permission, such as
//
// division into scopes: user, group, others
// op-permissions: read, write, etc.
// sticky bit
//
// However, these similarities should only be used for better understanding,
// in general these mechanisms are different.
//
// Instances can be created using built-in var declaration, but look carefully
// at the default values, and how individual permissions are regulated.
// Some frequently used values are presented in exported variables.
//
// Basic instances are comparable: values can be compared directly using
// == operator.
//
// Note that type conversion from- and to numerical types is not recommended,
// use corresponding constants and/or methods instead.
type Basic uint32
// FromBits decodes Basic from the numerical representation.
//
// See also Bits.
func (x *Basic) FromBits(bits uint32) {
*x = Basic(bits)
}
// Bits returns numerical encoding of Basic.
//
// See also FromBits.
func (x Basic) Bits() uint32 {
return uint32(x)
}
// common bit sections.
const (
opAmount = 7
bitsPerOp = 4
bitPosFinal = opAmount * bitsPerOp
bitPosSticky = bitPosFinal + 1
)
// per-op bit order.
const (
opBitPosBearer uint8 = iota
opBitPosOthers
opBitPosContainer
opBitPosOwner
)
// DisableExtension makes Basic FINAL. FINAL indicates the ACL non-extendability
// in the related container.
//
// See also Extendable.
func (x *Basic) DisableExtension() {
setBit((*uint32)(x), bitPosFinal)
}
// Extendable checks if Basic is NOT made FINAL using DisableExtension.
//
// Zero Basic is extendable.
func (x Basic) Extendable() bool {
return !isBitSet(uint32(x), bitPosFinal)
}
// MakeSticky makes Basic STICKY. STICKY indicates that only the owner of any
// particular object is allowed to operate on it.
//
// See also Sticky.
func (x *Basic) MakeSticky() {
setBit((*uint32)(x), bitPosSticky)
}
// Sticky checks if Basic is made STICKY using MakeSticky.
//
// Zero Basic is NOT STICKY.
func (x Basic) Sticky() bool {
return isBitSet(uint32(x), bitPosSticky)
}
// checks if op is used by the storage nodes within replication mechanism.
func isReplicationOp(op Op) bool {
switch op {
default:
return false
case
OpObjectGet,
OpObjectHead,
OpObjectPut,
OpObjectSearch,
OpObjectHash:
return true
}
}
// AllowOp allows the parties with the given role to the given operation.
// Op MUST be one of the Op enumeration. Role MUST be one of:
//
// RoleOwner
// RoleContainer
// RoleOthers
//
// and if role is RoleContainer, op MUST NOT be:
//
// OpObjectGet
// OpObjectHead
// OpObjectPut
// OpObjectSearch
// OpObjectHash
//
// See also IsOpAllowed.
func (x *Basic) AllowOp(op Op, role Role) {
var bitPos uint8
switch role {
default:
panic(fmt.Sprintf("unable to set rules for unsupported role %v", role))
case RoleInnerRing:
panic("basic ACL MUST NOT be modified for Inner Ring")
case RoleOwner:
bitPos = opBitPosOwner
case RoleContainer:
if isReplicationOp(op) {
panic("basic ACL for container replication ops MUST NOT be modified")
}
bitPos = opBitPosContainer
case RoleOthers:
bitPos = opBitPosOthers
}
setOpBit((*uint32)(x), op, bitPos)
}
// IsOpAllowed checks if parties with the given role are allowed to the given op
// according to the Basic rules. Op MUST be one of the Op enumeration.
// Role MUST be one of the Role enumeration.
//
// Members with RoleContainer role have exclusive default access to the
// operations of the data replication mechanism:
//
// OpObjectGet
// OpObjectHead
// OpObjectPut
// OpObjectSearch
// OpObjectHash
//
// RoleInnerRing members are allowed to data audit ops only:
//
// OpObjectGet
// OpObjectHead
// OpObjectHash
// OpObjectSearch
//
// Zero Basic prevents any role from accessing any operation in the absence
// of default rights.
//
// See also AllowOp.
func (x Basic) IsOpAllowed(op Op, role Role) bool {
var bitPos uint8
switch role {
default:
panic(fmt.Sprintf("role is unsupported %v", role))
case RoleInnerRing:
switch op {
case
OpObjectGet,
OpObjectHead,
OpObjectHash,
OpObjectSearch:
return true
default:
return false
}
case RoleOwner:
bitPos = opBitPosOwner
case RoleContainer:
if isReplicationOp(op) {
return true
}
bitPos = opBitPosContainer
case RoleOthers:
bitPos = opBitPosOthers
}
return isOpBitSet(uint32(x), op, bitPos)
}
// AllowBearerRules allows bearer to provide extended ACL rules for the given
// operation. Bearer rules doesn't depend on container ACL extensibility.
//
// See also AllowedBearerRules.
func (x *Basic) AllowBearerRules(op Op) {
setOpBit((*uint32)(x), op, opBitPosBearer)
}
// AllowedBearerRules checks if bearer rules are allowed using AllowBearerRules.
// Op MUST be one of the Op enumeration.
//
// Zero Basic disallows bearer rules for any op.
func (x Basic) AllowedBearerRules(op Op) bool {
return isOpBitSet(uint32(x), op, opBitPosBearer)
}
// EncodeToString encodes Basic into hexadecimal string.
//
// See also DecodeString.
func (x Basic) EncodeToString() string {
return strconv.FormatUint(uint64(x), 16)
}
// Names of the frequently used Basic values.
const (
NamePrivate = "private"
NamePrivateExtended = "eacl-private"
NamePublicRO = "public-read"
NamePublicROExtended = "eacl-public-read"
NamePublicRW = "public-read-write"
NamePublicRWExtended = "eacl-public-read-write"
NamePublicAppend = "public-append"
NamePublicAppendExtended = "eacl-public-append"
)
// Frequently used Basic values. Bitmasks are taken from the FrostFS Specification.
const (
Private = Basic(0x1C8C8CCC) // private
PrivateExtended = Basic(0x0C8C8CCC) // eacl-private
PublicRO = Basic(0x1FBF8CFF) // public-read
PublicROExtended = Basic(0x0FBF8CFF) // eacl-public-read
PublicRW = Basic(0x1FBFBFFF) // public-read-write
PublicRWExtended = Basic(0x0FBFBFFF) // eacl-public-read-write
PublicAppend = Basic(0x1FBF9FFF) // public-append
PublicAppendExtended = Basic(0x0FBF9FFF) // eacl-public-append
)
// DecodeString decodes string calculated using EncodeToString. Also supports
// human-readable names (Name* constants).
func (x *Basic) DecodeString(s string) (e error) {
switch s {
case NamePrivate:
*x = Private
case NamePrivateExtended:
*x = PrivateExtended
case NamePublicRO:
*x = PublicRO
case NamePublicROExtended:
*x = PublicROExtended
case NamePublicRW:
*x = PublicRW
case NamePublicRWExtended:
*x = PublicRWExtended
case NamePublicAppend:
*x = PublicAppend
case NamePublicAppendExtended:
*x = PublicAppendExtended
default:
s = strings.TrimPrefix(strings.ToLower(s), "0x")
v, err := strconv.ParseUint(s, 16, 32)
if err != nil {
return fmt.Errorf("parse hex: %w", err)
}
*x = Basic(v)
}
return nil
}

View file

@ -1,384 +0,0 @@
package acl
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestBasic_DisableExtension(t *testing.T) {
var val, val2 Basic
require.True(t, val.Extendable())
val2.FromBits(val.Bits())
require.True(t, val2.Extendable())
val.DisableExtension()
require.False(t, val.Extendable())
val2.FromBits(val.Bits())
require.False(t, val2.Extendable())
}
func TestBasic_MakeSticky(t *testing.T) {
var val, val2 Basic
require.False(t, val.Sticky())
val2.FromBits(val.Bits())
require.False(t, val2.Sticky())
val.MakeSticky()
require.True(t, val.Sticky())
val2.FromBits(val.Bits())
require.True(t, val2.Sticky())
}
func TestBasic_AllowBearerRules(t *testing.T) {
var val Basic
require.Panics(t, func() { val.AllowBearerRules(opZero) })
require.Panics(t, func() { val.AllowBearerRules(opLast) })
require.Panics(t, func() { val.AllowedBearerRules(opZero) })
require.Panics(t, func() { val.AllowedBearerRules(opLast) })
for op := opZero + 1; op < opLast; op++ {
val := val
require.False(t, val.AllowedBearerRules(op))
val.AllowBearerRules(op)
for j := opZero + 1; j < opLast; j++ {
if j == op {
require.True(t, val.AllowedBearerRules(j), op)
} else {
require.False(t, val.AllowedBearerRules(j), op)
}
}
}
}
func TestBasic_AllowOp(t *testing.T) {
var val, val2 Basic
require.Panics(t, func() { val.IsOpAllowed(opZero, roleZero+1) })
require.Panics(t, func() { val.IsOpAllowed(opLast, roleZero+1) })
require.Panics(t, func() { val.IsOpAllowed(opZero+1, roleZero) })
require.Panics(t, func() { val.IsOpAllowed(opZero+1, roleLast) })
for op := opZero + 1; op < opLast; op++ {
require.Panics(t, func() { val.AllowOp(op, RoleInnerRing) })
if isReplicationOp(op) {
require.Panics(t, func() { val.AllowOp(op, RoleContainer) })
require.True(t, val.IsOpAllowed(op, RoleContainer))
}
}
require.True(t, val.IsOpAllowed(OpObjectGet, RoleInnerRing))
require.True(t, val.IsOpAllowed(OpObjectHead, RoleInnerRing))
require.True(t, val.IsOpAllowed(OpObjectSearch, RoleInnerRing))
require.True(t, val.IsOpAllowed(OpObjectHash, RoleInnerRing))
const op = opZero + 1
const role = RoleOthers
require.False(t, val.IsOpAllowed(op, role))
val2.FromBits(val.Bits())
require.False(t, val2.IsOpAllowed(op, role))
val.AllowOp(op, role)
require.True(t, val.IsOpAllowed(op, role))
val2.FromBits(val.Bits())
require.True(t, val2.IsOpAllowed(op, role))
}
type opsExpected struct {
owner, container, innerRing, others, bearer bool
}
func testOp(t *testing.T, v Basic, op Op, exp opsExpected) {
require.Equal(t, exp.owner, v.IsOpAllowed(op, RoleOwner), op)
require.Equal(t, exp.container, v.IsOpAllowed(op, RoleContainer), op)
require.Equal(t, exp.innerRing, v.IsOpAllowed(op, RoleInnerRing), op)
require.Equal(t, exp.others, v.IsOpAllowed(op, RoleOthers), op)
require.Equal(t, exp.bearer, v.AllowedBearerRules(op), op)
}
type expected struct {
extendable, sticky bool
mOps map[Op]opsExpected
}
func testBasicPredefined(t *testing.T, val Basic, name string, exp expected) {
require.Equal(t, exp.sticky, val.Sticky())
require.Equal(t, exp.extendable, val.Extendable())
for op, exp := range exp.mOps {
testOp(t, val, op, exp)
}
s := val.EncodeToString()
var val2 Basic
require.NoError(t, val2.DecodeString(s))
require.Equal(t, val, val2)
require.NoError(t, val2.DecodeString(name))
require.Equal(t, val, val2)
}
func TestBasicPredefined(t *testing.T) {
t.Run("private", func(t *testing.T) {
exp := expected{
extendable: false,
sticky: false,
mOps: map[Op]opsExpected{
OpObjectHash: {
owner: true,
container: true,
innerRing: true,
others: false,
bearer: false,
},
OpObjectRange: {
owner: true,
container: false,
innerRing: false,
others: false,
bearer: false,
},
OpObjectSearch: {
owner: true,
container: true,
innerRing: true,
others: false,
bearer: false,
},
OpObjectDelete: {
owner: true,
container: false,
innerRing: false,
others: false,
bearer: false,
},
OpObjectPut: {
owner: true,
container: true,
innerRing: false,
others: false,
bearer: false,
},
OpObjectHead: {
owner: true,
container: true,
innerRing: true,
others: false,
bearer: false,
},
OpObjectGet: {
owner: true,
container: true,
innerRing: true,
others: false,
bearer: false,
},
},
}
testBasicPredefined(t, Private, NamePrivate, exp)
exp.extendable = true
testBasicPredefined(t, PrivateExtended, NamePrivateExtended, exp)
})
t.Run("public-read", func(t *testing.T) {
exp := expected{
extendable: false,
sticky: false,
mOps: map[Op]opsExpected{
OpObjectHash: {
owner: true,
container: true,
innerRing: true,
others: true,
bearer: true,
},
OpObjectRange: {
owner: true,
container: false,
innerRing: false,
others: true,
bearer: true,
},
OpObjectSearch: {
owner: true,
container: true,
innerRing: true,
others: true,
bearer: true,
},
OpObjectDelete: {
owner: true,
container: false,
innerRing: false,
others: false,
bearer: false,
},
OpObjectPut: {
owner: true,
container: true,
innerRing: false,
others: false,
bearer: false,
},
OpObjectHead: {
owner: true,
container: true,
innerRing: true,
others: true,
bearer: true,
},
OpObjectGet: {
owner: true,
container: true,
innerRing: true,
others: true,
bearer: true,
},
},
}
testBasicPredefined(t, PublicRO, NamePublicRO, exp)
exp.extendable = true
testBasicPredefined(t, PublicROExtended, NamePublicROExtended, exp)
})
t.Run("public-read-write", func(t *testing.T) {
exp := expected{
extendable: false,
sticky: false,
mOps: map[Op]opsExpected{
OpObjectHash: {
owner: true,
container: true,
innerRing: true,
others: true,
bearer: true,
},
OpObjectRange: {
owner: true,
container: false,
innerRing: false,
others: true,
bearer: true,
},
OpObjectSearch: {
owner: true,
container: true,
innerRing: true,
others: true,
bearer: true,
},
OpObjectDelete: {
owner: true,
container: false,
innerRing: false,
others: true,
bearer: true,
},
OpObjectPut: {
owner: true,
container: true,
innerRing: false,
others: true,
bearer: true,
},
OpObjectHead: {
owner: true,
container: true,
innerRing: true,
others: true,
bearer: true,
},
OpObjectGet: {
owner: true,
container: true,
innerRing: true,
others: true,
bearer: true,
},
},
}
testBasicPredefined(t, PublicRW, NamePublicRW, exp)
exp.extendable = true
testBasicPredefined(t, PublicRWExtended, NamePublicRWExtended, exp)
})
t.Run("public-append", func(t *testing.T) {
exp := expected{
extendable: false,
sticky: false,
mOps: map[Op]opsExpected{
OpObjectHash: {
owner: true,
container: true,
innerRing: true,
others: true,
bearer: true,
},
OpObjectRange: {
owner: true,
container: false,
innerRing: false,
others: true,
bearer: true,
},
OpObjectSearch: {
owner: true,
container: true,
innerRing: true,
others: true,
bearer: true,
},
OpObjectDelete: {
owner: true,
container: false,
innerRing: false,
others: false,
bearer: true,
},
OpObjectPut: {
owner: true,
container: true,
innerRing: false,
others: true,
bearer: true,
},
OpObjectHead: {
owner: true,
container: true,
innerRing: true,
others: true,
bearer: true,
},
OpObjectGet: {
owner: true,
container: true,
innerRing: true,
others: true,
bearer: true,
},
},
}
testBasicPredefined(t, PublicAppend, NamePublicAppend, exp)
exp.extendable = true
testBasicPredefined(t, PublicAppendExtended, NamePublicAppendExtended, exp)
})
}

View file

@ -1,8 +0,0 @@
/*
Package acl provides functionality to control access to data and operations on them in FrostFS containers.
Type Basic represents basic ACL of the FrostFS container which specifies the general order of data access.
Basic provides interface of rule composition. Package acl also exports some frequently used settings like
private or public.
*/
package acl

View file

@ -1,20 +0,0 @@
package acl
func init() {
// left-to-right order of the object operations
orderedOps := [...]Op{
OpObjectGet,
OpObjectHead,
OpObjectPut,
OpObjectDelete,
OpObjectSearch,
OpObjectRange,
OpObjectHash,
}
mOrder = make(map[Op]uint8, len(orderedOps))
for i := range orderedOps {
mOrder[orderedOps[i]] = uint8(i)
}
}

View file

@ -1,45 +0,0 @@
package acl
import "fmt"
// sets n-th bit in num (starting at 0).
func setBit(num *uint32, n uint8) {
*num |= 1 << n
}
// resets n-th bit in num (starting at 0).
func resetBit(num *uint32, n uint8) {
var mask uint32
setBit(&mask, n)
*num &= ^mask
}
// checks if n-th bit in num is set (starting at 0).
func isBitSet(num uint32, n uint8) bool {
mask := uint32(1 << n)
return mask != 0 && num&mask == mask
}
// maps Op to op-section index in Basic. Filled on init.
var mOrder map[Op]uint8
// sets n-th bit in num for the given op. Panics if op is unsupported.
func setOpBit(num *uint32, op Op, opBitPos uint8) {
n, ok := mOrder[op]
if !ok {
panic(fmt.Sprintf("op is unsupported %v", op))
}
setBit(num, n*bitsPerOp+opBitPos)
}
// checks if n-th bit in num for the given op is set. Panics if op is unsupported.
func isOpBitSet(num uint32, op Op, n uint8) bool {
off, ok := mOrder[op]
if !ok {
panic(fmt.Sprintf("op is unsupported %v", op))
}
return isBitSet(num, bitsPerOp*off+n)
}

View file

@ -1,133 +0,0 @@
package acl
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
func TestBits(t *testing.T) {
num := uint32(0b10110)
require.False(t, isBitSet(num, 0))
require.True(t, isBitSet(num, 1))
require.True(t, isBitSet(num, 2))
require.False(t, isBitSet(num, 3))
require.True(t, isBitSet(num, 4))
require.False(t, isBitSet(num, 5))
setBit(&num, 3)
require.EqualValues(t, 0b11110, num)
setBit(&num, 6)
require.EqualValues(t, 0b1011110, num)
resetBit(&num, 1)
require.EqualValues(t, 0b1011100, num)
}
func TestOpBits(t *testing.T) {
num := uint32(0b_1001_0101_1100_0011_0110_0111_1000_1111)
require.Panics(t, func() { isOpBitSet(num, opZero, 0) })
require.Panics(t, func() { isOpBitSet(num, opLast, 0) })
cpNum := num
require.Panics(t, func() { setOpBit(&num, opZero, 0) })
require.EqualValues(t, cpNum, num)
require.Panics(t, func() { setOpBit(&num, opLast, 0) })
require.EqualValues(t, cpNum, num)
for _, tc := range []struct {
op Op
set [4]bool // is bit set (left-to-right)
bits [4]uint32 // result of setting i-th bit (left-to-right) to zero num
}{
{
op: OpObjectHash,
set: [4]bool{false, true, false, true},
bits: [4]uint32{
0b_0000_1000_0000_0000_0000_0000_0000_0000,
0b_0000_0100_0000_0000_0000_0000_0000_0000,
0b_0000_0010_0000_0000_0000_0000_0000_0000,
0b_0000_0001_0000_0000_0000_0000_0000_0000,
},
},
{
op: OpObjectRange,
set: [4]bool{true, true, false, false},
bits: [4]uint32{
0b_0000_0000_1000_0000_0000_0000_0000_0000,
0b_0000_0000_0100_0000_0000_0000_0000_0000,
0b_0000_0000_0010_0000_0000_0000_0000_0000,
0b_0000_0000_0001_0000_0000_0000_0000_0000,
},
},
{
op: OpObjectSearch,
set: [4]bool{false, false, true, true},
bits: [4]uint32{
0b_0000_0000_0000_1000_0000_0000_0000_0000,
0b_0000_0000_0000_0100_0000_0000_0000_0000,
0b_0000_0000_0000_0010_0000_0000_0000_0000,
0b_0000_0000_0000_0001_0000_0000_0000_0000,
},
},
{
op: OpObjectDelete,
set: [4]bool{false, true, true, false},
bits: [4]uint32{
0b_0000_0000_0000_0000_1000_0000_0000_0000,
0b_0000_0000_0000_0000_0100_0000_0000_0000,
0b_0000_0000_0000_0000_0010_0000_0000_0000,
0b_0000_0000_0000_0000_0001_0000_0000_0000,
},
},
{
op: OpObjectPut,
set: [4]bool{false, true, true, true},
bits: [4]uint32{
0b_0000_0000_0000_0000_0000_1000_0000_0000,
0b_0000_0000_0000_0000_0000_0100_0000_0000,
0b_0000_0000_0000_0000_0000_0010_0000_0000,
0b_0000_0000_0000_0000_0000_0001_0000_0000,
},
},
{
op: OpObjectHead,
set: [4]bool{true, false, false, false},
bits: [4]uint32{
0b_0000_0000_0000_0000_0000_0000_1000_0000,
0b_0000_0000_0000_0000_0000_0000_0100_0000,
0b_0000_0000_0000_0000_0000_0000_0010_0000,
0b_0000_0000_0000_0000_0000_0000_0001_0000,
},
},
{
op: OpObjectGet,
set: [4]bool{true, true, true, true},
bits: [4]uint32{
0b_0000_0000_0000_0000_0000_0000_0000_1000,
0b_0000_0000_0000_0000_0000_0000_0000_0100,
0b_0000_0000_0000_0000_0000_0000_0000_0010,
0b_0000_0000_0000_0000_0000_0000_0000_0001,
},
},
} {
for i := range tc.set {
require.EqualValues(t, tc.set[i], isOpBitSet(num, tc.op, uint8(len(tc.set)-1-i)),
fmt.Sprintf("op %s, left bit #%d", tc.op, i),
)
}
for i := range tc.bits {
num := uint32(0)
setOpBit(&num, tc.op, uint8(len(tc.bits)-1-i))
require.EqualValues(t, tc.bits[i], num)
}
}
}

77
container/announcement.go Normal file
View file

@ -0,0 +1,77 @@
package container
import (
"github.com/nspcc-dev/neofs-api-go/v2/container"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
)
// UsedSpaceAnnouncement is an announcement message used by storage nodes to
// estimate actual container sizes.
type UsedSpaceAnnouncement container.UsedSpaceAnnouncement
// NewAnnouncement initialize empty UsedSpaceAnnouncement message.
//
// Defaults:
// - epoch: 0;
// - usedSpace: 0;
// - cid: nil.
func NewAnnouncement() *UsedSpaceAnnouncement {
return NewAnnouncementFromV2(new(container.UsedSpaceAnnouncement))
}
// NewAnnouncementFromV2 wraps protocol dependent version of
// UsedSpaceAnnouncement message.
//
// Nil container.UsedSpaceAnnouncement converts to nil.
func NewAnnouncementFromV2(v *container.UsedSpaceAnnouncement) *UsedSpaceAnnouncement {
return (*UsedSpaceAnnouncement)(v)
}
// Epoch of the announcement.
func (a *UsedSpaceAnnouncement) Epoch() uint64 {
return (*container.UsedSpaceAnnouncement)(a).GetEpoch()
}
// SetEpoch sets announcement epoch value.
func (a *UsedSpaceAnnouncement) SetEpoch(epoch uint64) {
(*container.UsedSpaceAnnouncement)(a).SetEpoch(epoch)
}
// ContainerID of the announcement.
func (a *UsedSpaceAnnouncement) ContainerID() *cid.ID {
return cid.NewFromV2(
(*container.UsedSpaceAnnouncement)(a).GetContainerID(),
)
}
// SetContainerID sets announcement container value.
func (a *UsedSpaceAnnouncement) SetContainerID(cid *cid.ID) {
(*container.UsedSpaceAnnouncement)(a).SetContainerID(cid.ToV2())
}
// UsedSpace in container.
func (a *UsedSpaceAnnouncement) UsedSpace() uint64 {
return (*container.UsedSpaceAnnouncement)(a).GetUsedSpace()
}
// SetUsedSpace sets used space value by specified container.
func (a *UsedSpaceAnnouncement) SetUsedSpace(value uint64) {
(*container.UsedSpaceAnnouncement)(a).SetUsedSpace(value)
}
// ToV2 returns protocol dependent version of UsedSpaceAnnouncement message.
//
// Nil UsedSpaceAnnouncement converts to nil.
func (a *UsedSpaceAnnouncement) ToV2() *container.UsedSpaceAnnouncement {
return (*container.UsedSpaceAnnouncement)(a)
}
// Marshal marshals UsedSpaceAnnouncement into a protobuf binary form.
func (a *UsedSpaceAnnouncement) Marshal() ([]byte, error) {
return a.ToV2().StableMarshal(nil)
}
// Unmarshal unmarshals protobuf binary representation of UsedSpaceAnnouncement.
func (a *UsedSpaceAnnouncement) Unmarshal(data []byte) error {
return a.ToV2().Unmarshal(data)
}

View file

@ -0,0 +1,99 @@
package container_test
import (
"crypto/sha256"
"testing"
containerv2 "github.com/nspcc-dev/neofs-api-go/v2/container"
"github.com/nspcc-dev/neofs-api-go/v2/refs"
"github.com/nspcc-dev/neofs-sdk-go/container"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test"
containertest "github.com/nspcc-dev/neofs-sdk-go/container/test"
"github.com/stretchr/testify/require"
)
func TestAnnouncement(t *testing.T) {
const epoch, usedSpace uint64 = 10, 100
cidValue := [sha256.Size]byte{1, 2, 3}
id := cidtest.IDWithChecksum(cidValue)
a := container.NewAnnouncement()
a.SetEpoch(epoch)
a.SetContainerID(id)
a.SetUsedSpace(usedSpace)
require.Equal(t, epoch, a.Epoch())
require.Equal(t, usedSpace, a.UsedSpace())
require.Equal(t, id, a.ContainerID())
t.Run("test v2", func(t *testing.T) {
const newEpoch, newUsedSpace uint64 = 20, 200
newCidValue := [32]byte{4, 5, 6}
newCID := new(refs.ContainerID)
newCID.SetValue(newCidValue[:])
v2 := a.ToV2()
require.Equal(t, usedSpace, v2.GetUsedSpace())
require.Equal(t, epoch, v2.GetEpoch())
require.Equal(t, cidValue[:], v2.GetContainerID().GetValue())
v2.SetEpoch(newEpoch)
v2.SetUsedSpace(newUsedSpace)
v2.SetContainerID(newCID)
newA := container.NewAnnouncementFromV2(v2)
require.Equal(t, newEpoch, newA.Epoch())
require.Equal(t, newUsedSpace, newA.UsedSpace())
require.Equal(t, cid.NewFromV2(newCID), newA.ContainerID())
})
}
func TestUsedSpaceEncoding(t *testing.T) {
a := containertest.UsedSpaceAnnouncement()
t.Run("binary", func(t *testing.T) {
data, err := a.Marshal()
require.NoError(t, err)
a2 := container.NewAnnouncement()
require.NoError(t, a2.Unmarshal(data))
require.Equal(t, a, a2)
})
}
func TestUsedSpaceAnnouncement_ToV2(t *testing.T) {
t.Run("nil", func(t *testing.T) {
var x *container.UsedSpaceAnnouncement
require.Nil(t, x.ToV2())
})
t.Run("default values", func(t *testing.T) {
announcement := container.NewAnnouncement()
// check initial values
require.Zero(t, announcement.Epoch())
require.Zero(t, announcement.UsedSpace())
require.Nil(t, announcement.ContainerID())
// convert to v2 message
announcementV2 := announcement.ToV2()
require.Zero(t, announcementV2.GetEpoch())
require.Zero(t, announcementV2.GetUsedSpace())
require.Nil(t, announcementV2.GetContainerID())
})
}
func TestNewAnnouncementFromV2(t *testing.T) {
t.Run("from nil", func(t *testing.T) {
var x *containerv2.UsedSpaceAnnouncement
require.Nil(t, container.NewAnnouncementFromV2(x))
})
}

139
container/attribute.go Normal file
View file

@ -0,0 +1,139 @@
package container
import (
"github.com/nspcc-dev/neofs-api-go/v2/container"
)
type (
Attribute container.Attribute
Attributes []Attribute
)
// NewAttribute creates and initializes blank Attribute.
//
// Defaults:
// - key: "";
// - value: "".
func NewAttribute() *Attribute {
return NewAttributeFromV2(new(container.Attribute))
}
func (a *Attribute) SetKey(v string) {
(*container.Attribute)(a).SetKey(v)
}
func (a *Attribute) SetValue(v string) {
(*container.Attribute)(a).SetValue(v)
}
func (a *Attribute) Key() string {
return (*container.Attribute)(a).GetKey()
}
func (a *Attribute) Value() string {
return (*container.Attribute)(a).GetValue()
}
// NewAttributeFromV2 wraps protocol dependent version of
// Attribute message.
//
// Nil container.Attribute converts to nil.
func NewAttributeFromV2(v *container.Attribute) *Attribute {
return (*Attribute)(v)
}
// ToV2 converts Attribute to v2 Attribute message.
//
// Nil Attribute converts to nil.
func (a *Attribute) ToV2() *container.Attribute {
return (*container.Attribute)(a)
}
func NewAttributesFromV2(v []container.Attribute) Attributes {
if v == nil {
return nil
}
attrs := make(Attributes, len(v))
for i := range v {
attrs[i] = *NewAttributeFromV2(&v[i])
}
return attrs
}
func (a Attributes) ToV2() []container.Attribute {
if a == nil {
return nil
}
attrs := make([]container.Attribute, len(a))
for i := range a {
attrs[i] = *a[i].ToV2()
}
return attrs
}
// sets value of the attribute by key.
func setAttribute(c *Container, key, value string) {
attrs := c.Attributes()
found := false
for i := range attrs {
if attrs[i].Key() == key {
attrs[i].SetValue(value)
found = true
break
}
}
if !found {
index := len(attrs)
attrs = append(attrs, Attribute{})
attrs[index].SetKey(key)
attrs[index].SetValue(value)
}
c.SetAttributes(attrs)
}
// iterates over container attributes. Stops at f's true return.
//
// Handler must not be nil.
func iterateAttributes(c *Container, f func(*Attribute) bool) {
attrs := c.Attributes()
for i := range attrs {
if f(&attrs[i]) {
return
}
}
}
// SetNativeNameWithZone sets container native name and its zone.
//
// Use SetNativeName to set default zone.
func SetNativeNameWithZone(c *Container, name, zone string) {
setAttribute(c, container.SysAttributeName, name)
setAttribute(c, container.SysAttributeZone, zone)
}
// SetNativeName sets container native name with default zone (container).
func SetNativeName(c *Container, name string) {
SetNativeNameWithZone(c, name, container.SysAttributeZoneDefault)
}
// GetNativeNameWithZone returns container native name and its zone.
func GetNativeNameWithZone(c *Container) (name string, zone string) {
iterateAttributes(c, func(a *Attribute) bool {
if key := a.Key(); key == container.SysAttributeName {
name = a.Value()
} else if key == container.SysAttributeZone {
zone = a.Value()
}
return name != "" && zone != ""
})
return
}

152
container/attribute_test.go Normal file
View file

@ -0,0 +1,152 @@
package container_test
import (
"testing"
containerv2 "github.com/nspcc-dev/neofs-api-go/v2/container"
"github.com/nspcc-dev/neofs-sdk-go/container"
"github.com/stretchr/testify/require"
)
func TestAttribute(t *testing.T) {
t.Run("nil", func(t *testing.T) {
var x *container.Attribute
require.Nil(t, x.ToV2())
})
t.Run("default values", func(t *testing.T) {
attr := container.NewAttribute()
// check initial values
require.Empty(t, attr.Key())
require.Empty(t, attr.Value())
// convert to v2 message
attrV2 := attr.ToV2()
require.Empty(t, attrV2.GetKey())
require.Empty(t, attrV2.GetValue())
})
const (
key = "key"
value = "value"
)
attr := container.NewAttribute()
attr.SetKey(key)
attr.SetValue(value)
require.Equal(t, key, attr.Key())
require.Equal(t, value, attr.Value())
t.Run("test v2", func(t *testing.T) {
const (
newKey = "newKey"
newValue = "newValue"
)
v2 := attr.ToV2()
require.Equal(t, key, v2.GetKey())
require.Equal(t, value, v2.GetValue())
v2.SetKey(newKey)
v2.SetValue(newValue)
newAttr := container.NewAttributeFromV2(v2)
require.Equal(t, newKey, newAttr.Key())
require.Equal(t, newValue, newAttr.Value())
})
}
func TestAttributes(t *testing.T) {
t.Run("nil", func(t *testing.T) {
var x container.Attributes
require.Nil(t, x.ToV2())
require.Nil(t, container.NewAttributesFromV2(nil))
})
var (
keys = []string{"key1", "key2", "key3"}
vals = []string{"val1", "val2", "val3"}
)
attrs := make(container.Attributes, len(keys))
for i := range keys {
attrs[i].SetKey(keys[i])
attrs[i].SetValue(vals[i])
}
t.Run("test v2", func(t *testing.T) {
const postfix = "x"
v2 := attrs.ToV2()
require.Len(t, v2, len(keys))
for i := range v2 {
k := v2[i].GetKey()
v := v2[i].GetValue()
require.Equal(t, keys[i], k)
require.Equal(t, vals[i], v)
v2[i].SetKey(k + postfix)
v2[i].SetValue(v + postfix)
}
newAttrs := container.NewAttributesFromV2(v2)
require.Len(t, newAttrs, len(keys))
for i := range newAttrs {
require.Equal(t, keys[i]+postfix, newAttrs[i].Key())
require.Equal(t, vals[i]+postfix, newAttrs[i].Value())
}
})
}
func TestNewAttributeFromV2(t *testing.T) {
t.Run("from nil", func(t *testing.T) {
var x *containerv2.Attribute
require.Nil(t, container.NewAttributeFromV2(x))
})
}
func TestGetNameWithZone(t *testing.T) {
c := container.New()
for _, item := range [...]struct {
name, zone string
}{
{"name1", ""},
{"name1", "zone1"},
{"name2", "zone1"},
{"name2", "zone2"},
{"", "zone2"},
{"", ""},
} {
container.SetNativeNameWithZone(c, item.name, item.zone)
name, zone := container.GetNativeNameWithZone(c)
require.Equal(t, item.name, name, item.name)
require.Equal(t, item.zone, zone, item.zone)
}
}
func TestSetNativeName(t *testing.T) {
c := container.New()
const nameDefZone = "some name"
container.SetNativeName(c, nameDefZone)
name, zone := container.GetNativeNameWithZone(c)
require.Equal(t, nameDefZone, name)
require.Equal(t, containerv2.SysAttributeZoneDefault, zone)
}

View file

@ -1,523 +1,193 @@
package container package container
import ( import (
"crypto/ecdsa"
"crypto/sha256" "crypto/sha256"
"errors"
"fmt"
"strconv"
"time"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
v2netmap "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/netmap"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto"
frostfsecdsa "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto/ecdsa"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
subnetid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/subnet/id"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/version"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/nspcc-dev/neofs-api-go/v2/container"
"github.com/nspcc-dev/neofs-sdk-go/acl"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
"github.com/nspcc-dev/neofs-sdk-go/netmap"
"github.com/nspcc-dev/neofs-sdk-go/owner"
"github.com/nspcc-dev/neofs-sdk-go/session"
"github.com/nspcc-dev/neofs-sdk-go/signature"
"github.com/nspcc-dev/neofs-sdk-go/version"
) )
// Container represents descriptor of the FrostFS container. Container logically
// stores FrostFS objects. Container is one of the basic and at the same time
// necessary data storage units in the FrostFS. Container includes data about the
// owner, rules for placing objects and other information necessary for the
// system functioning.
//
// Container type instances can represent different container states in the
// system, depending on the context. To create new container in FrostFS zero
// instance SHOULD be declared, initialized using Init method and filled using
// dedicated methods. Once container is saved in the FrostFS network, it can't be
// changed: containers stored in the system are immutable, and FrostFS is a CAS
// of containers that are identified by a fixed length value (see cid.ID type).
// Instances for existing containers can be initialized using decoding methods
// (e.g Unmarshal).
//
// Container is mutually compatible with git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container.Container
// message. See ReadFromV2 / WriteToV2 methods.
type Container struct { type Container struct {
v2 container.Container v2 container.Container
token *session.Token
sig *signature.Signature
} }
const ( // New creates, initializes and returns blank Container instance.
attributeName = "Name" //
attributeTimestamp = "Timestamp" // Defaults:
) // - token: nil;
// - sig: nil;
// - basicACL: acl.PrivateBasicRule;
// - version: version.Current;
// - nonce: random UUID;
// - attr: nil;
// - policy: nil;
// - ownerID: nil.
func New(opts ...Option) *Container {
cnrOptions := defaultContainerOptions()
// reads Container from the container.Container message. If checkFieldPresence is set, for i := range opts {
// returns an error on absence of any protocol-required field. opts[i](&cnrOptions)
func (x *Container) readFromV2(m container.Container, checkFieldPresence bool) error {
var err error
ownerV2 := m.GetOwnerID()
if ownerV2 != nil {
var owner user.ID
err = owner.ReadFromV2(*ownerV2)
if err != nil {
return fmt.Errorf("invalid owner: %w", err)
}
} else if checkFieldPresence {
return errors.New("missing owner")
} }
binNonce := m.GetNonce() cnr := new(Container)
if len(binNonce) > 0 { cnr.SetNonceUUID(cnrOptions.nonce)
var nonce uuid.UUID cnr.SetBasicACL(cnrOptions.acl)
err = nonce.UnmarshalBinary(binNonce) if cnrOptions.owner != nil {
if err != nil { cnr.SetOwnerID(cnrOptions.owner)
return fmt.Errorf("invalid nonce: %w", err)
} else if ver := nonce.Version(); ver != 4 {
return fmt.Errorf("invalid nonce UUID version %d", ver)
}
} else if checkFieldPresence {
return errors.New("missing nonce")
} }
ver := m.GetVersion() if cnrOptions.policy != nil {
if checkFieldPresence && ver == nil { cnr.SetPlacementPolicy(cnrOptions.policy)
return errors.New("missing version")
} }
policyV2 := m.GetPlacementPolicy() cnr.SetAttributes(cnrOptions.attributes)
if policyV2 != nil { cnr.SetVersion(version.Current())
var policy netmap.PlacementPolicy
err = policy.ReadFromV2(*policyV2) return cnr
if err != nil { }
return fmt.Errorf("invalid placement policy: %w", err)
}
} else if checkFieldPresence {
return errors.New("missing placement policy")
}
attrs := m.GetAttributes()
mAttr := make(map[string]struct{}, len(attrs))
var key, val string
var was bool
for i := range attrs {
key = attrs[i].GetKey()
if key == "" {
return errors.New("empty attribute key")
}
_, was = mAttr[key]
if was {
return fmt.Errorf("duplicated attribute %s", key)
}
val = attrs[i].GetValue()
if val == "" {
return fmt.Errorf("empty attribute value %s", key)
}
switch key {
case container.SysAttributeSubnet:
err = new(subnetid.ID).DecodeString(val)
case attributeTimestamp:
_, err = strconv.ParseInt(val, 10, 64)
}
if err != nil {
return fmt.Errorf("invalid attribute value %s: %s (%w)", key, val, err)
}
mAttr[key] = struct{}{}
}
x.v2 = m
// ToV2 returns the v2 Container message.
//
// Nil Container converts to nil.
func (c *Container) ToV2() *container.Container {
if c == nil {
return nil return nil
}
return &c.v2
} }
// ReadFromV2 reads Container from the container.Container message. Checks if the // NewVerifiedFromV2 constructs Container from NeoFS API V2 Container message.
// message conforms to FrostFS API V2 protocol.
// //
// See also WriteToV2. // Does not perform if message meets NeoFS API V2 specification. To do this
func (x *Container) ReadFromV2(m container.Container) error { // use NewVerifiedFromV2 constructor.
return x.readFromV2(m, true) func NewContainerFromV2(c *container.Container) *Container {
cnr := new(Container)
if c != nil {
cnr.v2 = *c
}
return cnr
} }
// WriteToV2 writes Container into the container.Container message. // CalculateID calculates container identifier
// The message MUST NOT be nil. // based on its structure.
// func CalculateID(c *Container) *cid.ID {
// See also ReadFromV2. data, err := c.ToV2().StableMarshal(nil)
func (x Container) WriteToV2(m *container.Container) {
*m = x.v2
}
// Marshal encodes Container into a binary format of the FrostFS API protocol
// (Protocol Buffers with direct field order).
//
// See also Unmarshal.
func (x Container) Marshal() []byte {
return x.v2.StableMarshal(nil)
}
// Unmarshal decodes FrostFS API protocol binary format into the Container
// (Protocol Buffers with direct field order). Returns an error describing
// a format violation.
//
// See also Marshal.
func (x *Container) Unmarshal(data []byte) error {
var m container.Container
err := m.Unmarshal(data)
if err != nil { if err != nil {
return err panic(err)
} }
return x.readFromV2(m, false) id := cid.New()
id.SetSHA256(sha256.Sum256(data))
return id
} }
// MarshalJSON encodes Container into a JSON format of the FrostFS API protocol func (c *Container) Version() *version.Version {
// (Protocol Buffers JSON). return version.NewFromV2(c.v2.GetVersion())
}
func (c *Container) SetVersion(v *version.Version) {
c.v2.SetVersion(v.ToV2())
}
func (c *Container) OwnerID() *owner.ID {
return owner.NewIDFromV2(c.v2.GetOwnerID())
}
func (c *Container) SetOwnerID(v *owner.ID) {
c.v2.SetOwnerID(v.ToV2())
}
// Returns container nonce in UUID format.
// //
// See also UnmarshalJSON. // Returns error if container nonce is not a valid UUID.
func (x Container) MarshalJSON() ([]byte, error) { func (c *Container) NonceUUID() (uuid.UUID, error) {
return x.v2.MarshalJSON() return uuid.FromBytes(c.v2.GetNonce())
} }
// UnmarshalJSON decodes FrostFS API protocol JSON format into the Container // SetNonceUUID sets container nonce as UUID.
// (Protocol Buffers JSON). Returns an error describing a format violation. func (c *Container) SetNonceUUID(v uuid.UUID) {
// data, _ := v.MarshalBinary()
// See also MarshalJSON. c.v2.SetNonce(data)
func (x *Container) UnmarshalJSON(data []byte) error {
return x.v2.UnmarshalJSON(data)
} }
// Init initializes all internal data of the Container required by FrostFS API func (c *Container) BasicACL() uint32 {
// protocol. Init MUST be called when creating a new container. Init SHOULD NOT return c.v2.GetBasicACL()
// be called multiple times. Init SHOULD NOT be called if the Container instance
// is used for decoding only.
func (x *Container) Init() {
var ver refs.Version
version.Current().WriteToV2(&ver)
x.v2.SetVersion(&ver)
nonce, err := uuid.New().MarshalBinary()
if err != nil {
panic(fmt.Sprintf("unexpected error from UUID.MarshalBinary: %v", err))
}
x.v2.SetNonce(nonce)
} }
// SetOwner specifies the owner of the Container. Each Container has exactly func (c *Container) SetBasicACL(v acl.BasicACL) {
// one owner, so SetOwner MUST be called for instances to be saved in the c.v2.SetBasicACL(uint32(v))
// FrostFS.
//
// See also Owner.
func (x *Container) SetOwner(owner user.ID) {
var m refs.OwnerID
owner.WriteToV2(&m)
x.v2.SetOwnerID(&m)
} }
// Owner returns owner of the Container set using SetOwner. func (c *Container) Attributes() Attributes {
// return NewAttributesFromV2(c.v2.GetAttributes())
// Zero Container has no owner which is incorrect according to FrostFS API
// protocol.
func (x Container) Owner() (res user.ID) {
m := x.v2.GetOwnerID()
if m != nil {
err := res.ReadFromV2(*m)
if err != nil {
panic(fmt.Sprintf("unexpected error from user.ID.ReadFromV2: %v", err))
}
}
return
} }
// SetBasicACL specifies basic part of the Container ACL. Basic ACL is used func (c *Container) SetAttributes(v Attributes) {
// to control access inside container storage. c.v2.SetAttributes(v.ToV2())
//
// See also BasicACL.
func (x *Container) SetBasicACL(basicACL acl.Basic) {
x.v2.SetBasicACL(basicACL.Bits())
} }
// BasicACL returns basic ACL set using SetBasicACL. func (c *Container) PlacementPolicy() *netmap.PlacementPolicy {
// return netmap.NewPlacementPolicyFromV2(c.v2.GetPlacementPolicy())
// Zero Container has zero basic ACL which structurally correct but doesn't
// make sense since it denies any access to any party.
func (x Container) BasicACL() (res acl.Basic) {
res.FromBits(x.v2.GetBasicACL())
return
} }
// SetPlacementPolicy sets placement policy for the objects within the Container. func (c *Container) SetPlacementPolicy(v *netmap.PlacementPolicy) {
// FrostFS storage layer strives to follow the specified policy. c.v2.SetPlacementPolicy(v.ToV2())
//
// See also PlacementPolicy.
func (x *Container) SetPlacementPolicy(policy netmap.PlacementPolicy) {
var m v2netmap.PlacementPolicy
policy.WriteToV2(&m)
x.v2.SetPlacementPolicy(&m)
} }
// PlacementPolicy returns placement policy set using SetPlacementPolicy. // SessionToken returns token of the session within
// // which container was created.
// Zero Container has no placement policy which is incorrect according to func (c Container) SessionToken() *session.Token {
// FrostFS API protocol. return c.token
func (x Container) PlacementPolicy() (res netmap.PlacementPolicy) {
m := x.v2.GetPlacementPolicy()
if m != nil {
err := res.ReadFromV2(*m)
if err != nil {
panic(fmt.Sprintf("unexpected error from PlacementPolicy.ReadFromV2: %v", err))
}
}
return
} }
// SetAttribute sets Container attribute value by key. Both key and value // SetSessionToken sets token of the session within
// MUST NOT be empty. Attributes set by the creator (owner) are most commonly // which container was created.
// ignored by the FrostFS system and used for application layer. Some attributes func (c *Container) SetSessionToken(t *session.Token) {
// are so-called system or well-known attributes: they are reserved for system c.token = t
// needs. System attributes SHOULD NOT be modified using SetAttribute, use
// corresponding methods/functions. List of the reserved keys is documented
// in the particular protocol version.
//
// SetAttribute overwrites existing attribute value.
//
// See also Attribute, IterateAttributes.
func (x *Container) SetAttribute(key, value string) {
if key == "" {
panic("empty attribute key")
} else if value == "" {
panic("empty attribute value")
}
attrs := x.v2.GetAttributes()
ln := len(attrs)
for i := 0; i < ln; i++ {
if attrs[i].GetKey() == key {
attrs[i].SetValue(value)
return
}
}
attrs = append(attrs, container.Attribute{})
attrs[ln].SetKey(key)
attrs[ln].SetValue(value)
x.v2.SetAttributes(attrs)
} }
// Attribute reads value of the Container attribute by key. Empty result means // Signature returns signature of the marshaled container.
// attribute absence. func (c Container) Signature() *signature.Signature {
// return c.sig
// See also SetAttribute, IterateAttributes.
func (x Container) Attribute(key string) string {
attrs := x.v2.GetAttributes()
for i := range attrs {
if attrs[i].GetKey() == key {
return attrs[i].GetValue()
}
}
return ""
} }
// IterateAttributes iterates over all Container attributes and passes them // SetSignature sets signature of the marshaled container.
// into f. The handler MUST NOT be nil. func (c *Container) SetSignature(sig *signature.Signature) {
// c.sig = sig
// See also SetAttribute, Attribute.
func (x Container) IterateAttributes(f func(key, val string)) {
attrs := x.v2.GetAttributes()
for i := range attrs {
f(attrs[i].GetKey(), attrs[i].GetValue())
}
} }
// SetName sets human-readable name of the Container. Name MUST NOT be empty. // Marshal marshals Container into a protobuf binary form.
// func (c *Container) Marshal() ([]byte, error) {
// See also Name. return c.v2.StableMarshal(nil)
func SetName(cnr *Container, name string) {
cnr.SetAttribute(attributeName, name)
} }
// Name returns container name set using SetName. // Unmarshal unmarshals protobuf binary representation of Container.
// func (c *Container) Unmarshal(data []byte) error {
// Zero Container has no name. return c.v2.Unmarshal(data)
func Name(cnr Container) string {
return cnr.Attribute(attributeName)
} }
// SetCreationTime writes container's creation time in Unix Timestamp format. // MarshalJSON encodes Container to protobuf JSON format.
// func (c *Container) MarshalJSON() ([]byte, error) {
// See also CreatedAt. return c.v2.MarshalJSON()
func SetCreationTime(cnr *Container, t time.Time) {
cnr.SetAttribute(attributeTimestamp, strconv.FormatInt(t.Unix(), 10))
} }
// CreatedAt returns container's creation time set using SetCreationTime. // UnmarshalJSON decodes Container from protobuf JSON format.
// func (c *Container) UnmarshalJSON(data []byte) error {
// Zero Container has zero timestamp (in seconds). return c.v2.UnmarshalJSON(data)
func CreatedAt(cnr Container) time.Time {
var sec int64
attr := cnr.Attribute(attributeTimestamp)
if attr != "" {
var err error
sec, err = strconv.ParseInt(cnr.Attribute(attributeTimestamp), 10, 64)
if err != nil {
panic(fmt.Sprintf("parse container timestamp: %v", err))
}
}
return time.Unix(sec, 0)
}
// SetSubnet places the Container on the specified FrostFS subnet. If called,
// container nodes will only be selected from the given subnet, otherwise from
// the entire network.
func SetSubnet(cnr *Container, subNet subnetid.ID) {
cnr.SetAttribute(container.SysAttributeSubnet, subNet.EncodeToString())
}
// Subnet return container subnet set using SetSubnet.
//
// Zero Container is bound to zero subnet.
func Subnet(cnr Container) (res subnetid.ID) {
val := cnr.Attribute(container.SysAttributeSubnet)
if val != "" {
err := res.DecodeString(val)
if err != nil {
panic(fmt.Sprintf("invalid subnet attribute: %s (%v)", val, err))
}
}
return
}
const attributeHomoHashEnabled = "true"
// DisableHomomorphicHashing sets flag to disable homomorphic hashing of the
// Container data.
//
// See also IsHomomorphicHashingDisabled.
func DisableHomomorphicHashing(cnr *Container) {
cnr.SetAttribute(container.SysAttributeHomomorphicHashing, attributeHomoHashEnabled)
}
// IsHomomorphicHashingDisabled checks if DisableHomomorphicHashing was called.
//
// Zero Container has enabled hashing.
func IsHomomorphicHashingDisabled(cnr Container) bool {
return cnr.Attribute(container.SysAttributeHomomorphicHashing) == attributeHomoHashEnabled ||
cnr.Attribute(container.SysAttributeHomomorphicHashingNeoFS) == attributeHomoHashEnabled
}
// Domain represents information about container domain registered in the NNS
// contract deployed in the FrostFS network.
type Domain struct {
name, zone string
}
// SetName sets human-friendly container domain name.
func (x *Domain) SetName(name string) {
x.name = name
}
// Name returns name set using SetName.
//
// Zero Domain has zero name.
func (x Domain) Name() string {
return x.name
}
// SetZone sets zone which is used as a TLD of a domain name in NNS contract.
func (x *Domain) SetZone(zone string) {
x.zone = zone
}
// Zone returns domain zone set using SetZone.
//
// Zero Domain has "container" zone.
func (x Domain) Zone() string {
if x.zone != "" {
return x.zone
}
return "container"
}
// WriteDomain writes Domain into the Container. Name MUST NOT be empty.
func WriteDomain(cnr *Container, domain Domain) {
cnr.SetAttribute(container.SysAttributeName, domain.Name())
cnr.SetAttribute(container.SysAttributeZone, domain.Zone())
}
// ReadDomain reads Domain from the Container. Returns value with empty name
// if domain is not specified.
func ReadDomain(cnr Container) (res Domain) {
if name := cnr.Attribute(container.SysAttributeName); name != "" {
res.SetName(name)
res.SetZone(cnr.Attribute(container.SysAttributeZone))
} else if name = cnr.Attribute(container.SysAttributeNameNeoFS); name != "" {
res.SetName(name)
res.SetZone(cnr.Attribute(container.SysAttributeZoneNeoFS))
}
return
}
// CalculateSignature calculates signature of the Container using provided signer
// and writes it into dst. Signature instance MUST NOT be nil. CalculateSignature
// is expected to be called after all the Container data is filled and before
// saving the Container in the FrostFS network. Note that мany subsequent change
// will most likely break the signature.
//
// See also VerifySignature.
func CalculateSignature(dst *frostfscrypto.Signature, cnr Container, signer ecdsa.PrivateKey) error {
return dst.Calculate(frostfsecdsa.SignerRFC6979(signer), cnr.Marshal())
}
// VerifySignature verifies Container signature calculated using CalculateSignature.
// Result means signature correctness.
func VerifySignature(sig frostfscrypto.Signature, cnr Container) bool {
return sig.Verify(cnr.Marshal())
}
// CalculateIDFromBinary calculates identifier of the binary-encoded container
// in CAS of the FrostFS containers and writes it into dst. ID instance MUST NOT
// be nil.
//
// See also CalculateID, AssertID.
func CalculateIDFromBinary(dst *cid.ID, cnr []byte) {
dst.SetSHA256(sha256.Sum256(cnr))
}
// CalculateID encodes the given Container and passes the result into
// CalculateIDFromBinary.
//
// See also Container.Marshal, AssertID.
func CalculateID(dst *cid.ID, cnr Container) {
CalculateIDFromBinary(dst, cnr.Marshal())
}
// AssertID checks if the given Container matches its identifier in CAS of the
// FrostFS containers.
//
// See also CalculateID.
func AssertID(id cid.ID, cnr Container) bool {
var id2 cid.ID
CalculateID(&id2, cnr)
return id2.Equals(id)
} }

View file

@ -1,351 +1,136 @@
package container_test package container_test
import ( import (
"crypto/sha256"
"strconv"
"testing" "testing"
"time"
v2container "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
v2netmap "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/netmap"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test"
containertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/test"
frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto"
netmaptest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap/test"
subnetid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/subnet/id"
subnetidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/subnet/id/test"
usertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user/test"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/version"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neofs-sdk-go/acl"
"github.com/nspcc-dev/neofs-sdk-go/container"
containertest "github.com/nspcc-dev/neofs-sdk-go/container/test"
netmaptest "github.com/nspcc-dev/neofs-sdk-go/netmap/test"
ownertest "github.com/nspcc-dev/neofs-sdk-go/owner/test"
sessiontest "github.com/nspcc-dev/neofs-sdk-go/session/test"
sigtest "github.com/nspcc-dev/neofs-sdk-go/signature/test"
"github.com/nspcc-dev/neofs-sdk-go/version"
versiontest "github.com/nspcc-dev/neofs-sdk-go/version/test"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestPlacementPolicyEncoding(t *testing.T) { func TestNewContainer(t *testing.T) {
v := containertest.Container() c := container.New()
nonce := uuid.New()
ownerID := ownertest.ID()
policy := netmaptest.PlacementPolicy()
c.SetBasicACL(acl.PublicBasicRule)
attrs := containertest.Attributes()
c.SetAttributes(attrs)
c.SetPlacementPolicy(policy)
c.SetNonceUUID(nonce)
c.SetOwnerID(ownerID)
ver := versiontest.Version()
c.SetVersion(ver)
v2 := c.ToV2()
newContainer := container.NewContainerFromV2(v2)
require.EqualValues(t, newContainer.PlacementPolicy(), policy)
require.EqualValues(t, newContainer.Attributes(), attrs)
require.EqualValues(t, newContainer.BasicACL(), acl.PublicBasicRule)
newNonce, err := newContainer.NonceUUID()
require.NoError(t, err)
require.EqualValues(t, newNonce, nonce)
require.EqualValues(t, newContainer.OwnerID(), ownerID)
require.EqualValues(t, newContainer.Version(), ver)
}
func TestContainerEncoding(t *testing.T) {
c := containertest.Container()
t.Run("binary", func(t *testing.T) { t.Run("binary", func(t *testing.T) {
var v2 container.Container data, err := c.Marshal()
require.NoError(t, v2.Unmarshal(v.Marshal())) require.NoError(t, err)
require.Equal(t, v, v2) c2 := container.New()
require.NoError(t, c2.Unmarshal(data))
require.Equal(t, c, c2)
}) })
t.Run("json", func(t *testing.T) { t.Run("json", func(t *testing.T) {
data, err := v.MarshalJSON() data, err := c.MarshalJSON()
require.NoError(t, err) require.NoError(t, err)
var v2 container.Container c2 := container.New()
require.NoError(t, v2.UnmarshalJSON(data)) require.NoError(t, c2.UnmarshalJSON(data))
require.Equal(t, v, v2) require.Equal(t, c, c2)
}) })
} }
func TestContainer_Init(t *testing.T) { func TestContainer_SessionToken(t *testing.T) {
val := containertest.Container() tok := sessiontest.Token()
val.Init() cnr := container.New()
var msg v2container.Container cnr.SetSessionToken(tok)
val.WriteToV2(&msg)
binNonce := msg.GetNonce() require.Equal(t, tok, cnr.SessionToken())
var nonce uuid.UUID
require.NoError(t, nonce.UnmarshalBinary(binNonce))
require.EqualValues(t, 4, nonce.Version())
verV2 := msg.GetVersion()
require.NotNil(t, verV2)
var ver version.Version
require.NoError(t, ver.ReadFromV2(*verV2))
require.Equal(t, version.Current(), ver)
var val2 container.Container
require.NoError(t, val2.ReadFromV2(msg))
require.Equal(t, val, val2)
} }
func TestContainer_Owner(t *testing.T) { func TestContainer_Signature(t *testing.T) {
var val container.Container sig := sigtest.Signature()
require.Zero(t, val.Owner()) cnr := container.New()
cnr.SetSignature(sig)
val = containertest.Container() require.Equal(t, sig, cnr.Signature())
owner := *usertest.ID()
val.SetOwner(owner)
var msg v2container.Container
val.WriteToV2(&msg)
var msgOwner refs.OwnerID
owner.WriteToV2(&msgOwner)
require.Equal(t, &msgOwner, msg.GetOwnerID())
var val2 container.Container
require.NoError(t, val2.ReadFromV2(msg))
require.True(t, val2.Owner().Equals(owner))
} }
func TestContainer_BasicACL(t *testing.T) { func TestContainer_ToV2(t *testing.T) {
var val container.Container t.Run("nil", func(t *testing.T) {
var x *container.Container
require.Zero(t, val.BasicACL()) require.Nil(t, x.ToV2())
val = containertest.Container()
basicACL := containertest.BasicACL()
val.SetBasicACL(basicACL)
var msg v2container.Container
val.WriteToV2(&msg)
require.EqualValues(t, basicACL.Bits(), msg.GetBasicACL())
var val2 container.Container
require.NoError(t, val2.ReadFromV2(msg))
require.Equal(t, basicACL, val2.BasicACL())
}
func TestContainer_PlacementPolicy(t *testing.T) {
var val container.Container
require.Zero(t, val.PlacementPolicy())
val = containertest.Container()
pp := netmaptest.PlacementPolicy()
val.SetPlacementPolicy(pp)
var msgPolicy v2netmap.PlacementPolicy
pp.WriteToV2(&msgPolicy)
var msg v2container.Container
val.WriteToV2(&msg)
require.Equal(t, &msgPolicy, msg.GetPlacementPolicy())
var val2 container.Container
require.NoError(t, val2.ReadFromV2(msg))
require.Equal(t, pp, val2.PlacementPolicy())
}
func assertContainsAttribute(t *testing.T, m v2container.Container, key, val string) {
var msgAttr v2container.Attribute
msgAttr.SetKey(key)
msgAttr.SetValue(val)
require.Contains(t, m.GetAttributes(), msgAttr)
}
func TestContainer_Attribute(t *testing.T) {
const attrKey1, attrKey2 = "key1", "key2"
const attrVal1, attrVal2 = "val1", "val2"
val := containertest.Container()
val.SetAttribute(attrKey1, attrVal1)
val.SetAttribute(attrKey2, attrVal2)
var msg v2container.Container
val.WriteToV2(&msg)
require.GreaterOrEqual(t, len(msg.GetAttributes()), 2)
assertContainsAttribute(t, msg, attrKey1, attrVal1)
assertContainsAttribute(t, msg, attrKey2, attrVal2)
var val2 container.Container
require.NoError(t, val2.ReadFromV2(msg))
require.Equal(t, attrVal1, val2.Attribute(attrKey1))
require.Equal(t, attrVal2, val2.Attribute(attrKey2))
m := map[string]string{}
val2.IterateAttributes(func(key, val string) {
m[key] = val
}) })
require.GreaterOrEqual(t, len(m), 2) t.Run("default values", func(t *testing.T) {
require.Equal(t, attrVal1, m[attrKey1]) cnt := container.New()
require.Equal(t, attrVal2, m[attrKey2])
val2.SetAttribute(attrKey1, attrVal1+"_") // check initial values
require.Equal(t, attrVal1+"_", val2.Attribute(attrKey1)) require.Nil(t, cnt.SessionToken())
} require.Nil(t, cnt.Signature())
require.Nil(t, cnt.Attributes())
require.Nil(t, cnt.PlacementPolicy())
require.Nil(t, cnt.OwnerID())
func TestSetName(t *testing.T) { require.EqualValues(t, acl.PrivateBasicRule, cnt.BasicACL())
var val container.Container require.Equal(t, version.Current(), cnt.Version())
require.Panics(t, func() { nonce, err := cnt.NonceUUID()
container.SetName(&val, "") require.NoError(t, err)
}) require.NotNil(t, nonce)
val = containertest.Container() // convert to v2 message
cntV2 := cnt.ToV2()
const name = "some name" nonceV2, err := uuid.FromBytes(cntV2.GetNonce())
container.SetName(&val, name)
var msg v2container.Container
val.WriteToV2(&msg)
assertContainsAttribute(t, msg, "Name", name)
var val2 container.Container
require.NoError(t, val2.ReadFromV2(msg))
require.Equal(t, name, container.Name(val2))
}
func TestSetCreationTime(t *testing.T) {
var val container.Container
require.Zero(t, container.CreatedAt(val).Unix())
val = containertest.Container()
creat := time.Now()
container.SetCreationTime(&val, creat)
var msg v2container.Container
val.WriteToV2(&msg)
assertContainsAttribute(t, msg, "Timestamp", strconv.FormatInt(creat.Unix(), 10))
var val2 container.Container
require.NoError(t, val2.ReadFromV2(msg))
require.Equal(t, creat.Unix(), container.CreatedAt(val2).Unix())
}
func TestSetSubnet(t *testing.T) {
var val container.Container
require.True(t, subnetid.IsZero(container.Subnet(val)))
val = containertest.Container()
sub := subnetidtest.ID()
container.SetSubnet(&val, sub)
var msg v2container.Container
val.WriteToV2(&msg)
assertContainsAttribute(t, msg, v2container.SysAttributeSubnet, sub.EncodeToString())
var val2 container.Container
require.NoError(t, val2.ReadFromV2(msg))
require.Equal(t, sub, container.Subnet(val))
}
func TestDisableHomomorphicHashing(t *testing.T) {
var val container.Container
require.False(t, container.IsHomomorphicHashingDisabled(val))
val = containertest.Container()
container.DisableHomomorphicHashing(&val)
var msg v2container.Container
val.WriteToV2(&msg)
assertContainsAttribute(t, msg, v2container.SysAttributePrefix+"DISABLE_HOMOMORPHIC_HASHING", "true")
var val2 container.Container
require.NoError(t, val2.ReadFromV2(msg))
require.True(t, container.IsHomomorphicHashingDisabled(val2))
}
func TestWriteDomain(t *testing.T) {
var val container.Container
require.Zero(t, container.ReadDomain(val).Name())
val = containertest.Container()
const name = "domain name"
var d container.Domain
d.SetName(name)
container.WriteDomain(&val, d)
var msg v2container.Container
val.WriteToV2(&msg)
assertContainsAttribute(t, msg, v2container.SysAttributeName, name)
assertContainsAttribute(t, msg, v2container.SysAttributeZone, "container")
const zone = "domain zone"
d.SetZone(zone)
container.WriteDomain(&val, d)
val.WriteToV2(&msg)
assertContainsAttribute(t, msg, v2container.SysAttributeZone, zone)
var val2 container.Container
require.NoError(t, val2.ReadFromV2(msg))
require.Equal(t, d, container.ReadDomain(val2))
}
func TestCalculateID(t *testing.T) {
val := containertest.Container()
require.False(t, container.AssertID(cidtest.ID(), val))
var id cid.ID
container.CalculateID(&id, val)
var msg refs.ContainerID
id.WriteToV2(&msg)
h := sha256.Sum256(val.Marshal())
require.Equal(t, h[:], msg.GetValue())
var id2 cid.ID
require.NoError(t, id2.ReadFromV2(msg))
require.True(t, container.AssertID(id2, val))
}
func TestCalculateSignature(t *testing.T) {
key, err := keys.NewPrivateKey()
require.NoError(t, err) require.NoError(t, err)
val := containertest.Container() require.Equal(t, nonce.String(), nonceV2.String())
var sig frostfscrypto.Signature require.Nil(t, cntV2.GetAttributes())
require.Nil(t, cntV2.GetPlacementPolicy())
require.Nil(t, cntV2.GetOwnerID())
require.NoError(t, container.CalculateSignature(&sig, val, key.PrivateKey)) require.Equal(t, uint32(acl.PrivateBasicRule), cntV2.GetBasicACL())
require.Equal(t, version.Current().ToV2(), cntV2.GetVersion())
var msg refs.Signature })
sig.WriteToV2(&msg)
var sig2 frostfscrypto.Signature
require.NoError(t, sig2.ReadFromV2(msg))
require.True(t, container.VerifySignature(sig2, val))
} }

View file

@ -1,49 +0,0 @@
/*
Package container provides functionality related to the FrostFS containers.
The base type is Container. To create new container in the FrostFS network
Container instance should be initialized
var cnr Container
cnr.Init()
// fill all the fields
// encode cnr and send
After the container is persisted in the FrostFS network, applications can process
it using the instance of Container types
// recv binary container
var cnr Container
err := cnr.Unmarshal(bin)
// ...
// process the container data
Instances can be also used to process FrostFS API V2 protocol messages
(see neo.fs.v2.container package in https://git.frostfs.info/TrueCloudLab/frostfs-api).
On client side:
import "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
var msg container.Container
cnr.WriteToV2(&msg)
// send msg
On server side:
// recv msg
var cnr Container
cnr.ReadFromV2(msg)
// process cnr
Using package types in an application is recommended to potentially work with
different protocol versions with which these types are compatible.
*/
package container

View file

@ -1,7 +0,0 @@
/*
Package cid provides primitives to work with container identification in FrostFS.
Using package types in an application is recommended to potentially work with
different protocol versions with which these types are compatible.
*/
package cid

View file

@ -1,115 +1,90 @@
package cid package cid
import ( import (
"bytes"
"crypto/sha256" "crypto/sha256"
"fmt" "errors"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
"github.com/mr-tron/base58" "github.com/mr-tron/base58"
"github.com/nspcc-dev/neofs-api-go/v2/refs"
) )
// ID represents FrostFS container identifier. // ID represents v2-compatible container identifier.
// type ID refs.ContainerID
// ID is mutually compatible with git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs.ContainerID
// message. See ReadFromV2 / WriteToV2 methods.
//
// Instances can be created using built-in var declaration.
//
// Note that direct typecast is not safe and may result in loss of compatibility:
//
// _ = ID([32]byte) // not recommended
type ID [sha256.Size]byte
// ReadFromV2 reads ID from the refs.ContainerID message. // NewFromV2 wraps v2 ContainerID message to ID.
// Returns an error if the message is malformed according
// to the FrostFS API V2 protocol.
// //
// See also WriteToV2. // Nil refs.ContainerID converts to nil.
func (id *ID) ReadFromV2(m refs.ContainerID) error { func NewFromV2(idV2 *refs.ContainerID) *ID {
return id.Decode(m.GetValue()) return (*ID)(idV2)
} }
// WriteToV2 writes ID to the refs.ContainerID message. // New creates and initializes blank ID.
// The message must not be nil.
// //
// See also ReadFromV2. // Defaults:
func (id ID) WriteToV2(m *refs.ContainerID) { // - value: nil.
m.SetValue(id[:]) func New() *ID {
return NewFromV2(new(refs.ContainerID))
} }
// Encode encodes ID into 32 bytes of dst. Panics if // SetSHA256 sets container identifier value to SHA256 checksum of container body.
// dst length is less than 32. func (id *ID) SetSHA256(v [sha256.Size]byte) {
(*refs.ContainerID)(id).SetValue(v[:])
}
// ToV2 returns the v2 container ID message.
// //
// Zero ID is all zeros. // Nil ID converts to nil.
func (id *ID) ToV2() *refs.ContainerID {
return (*refs.ContainerID)(id)
}
// Equal returns true if identifiers are identical.
func (id *ID) Equal(id2 *ID) bool {
return bytes.Equal(
(*refs.ContainerID)(id).GetValue(),
(*refs.ContainerID)(id2).GetValue(),
)
}
// Parse parses string representation of ID.
// //
// See also Decode. // Returns error if s is not a base58 encoded
func (id ID) Encode(dst []byte) { // ID data.
if l := len(dst); l < sha256.Size { func (id *ID) Parse(s string) error {
panic(fmt.Sprintf("destination length is less than %d bytes: %d", sha256.Size, l)) data, err := base58.Decode(s)
if err != nil {
return err
} else if len(data) != sha256.Size {
return errors.New("incorrect format of the string container ID")
} }
copy(dst, id[:]) (*refs.ContainerID)(id).SetValue(data)
}
// Decode decodes src bytes into ID.
//
// Decode expects that src has 32 bytes length. If the input is malformed,
// Decode returns an error describing format violation. In this case ID
// remains unchanged.
//
// Decode doesn't mutate src.
//
// See also Encode.
func (id *ID) Decode(src []byte) error {
if len(src) != sha256.Size {
return fmt.Errorf("invalid length %d", len(src))
}
copy(id[:], src)
return nil return nil
} }
// SetSHA256 sets container identifier value to SHA256 checksum of container structure. // String returns base58 string representation of ID.
func (id *ID) SetSHA256(v [sha256.Size]byte) { func (id *ID) String() string {
copy(id[:], v[:]) return base58.Encode((*refs.ContainerID)(id).GetValue())
} }
// Equals defines a comparison relation between two ID instances. // Marshal marshals ID into a protobuf binary form.
// func (id *ID) Marshal() ([]byte, error) {
// Note that comparison using '==' operator is not recommended since it MAY result return (*refs.ContainerID)(id).StableMarshal(nil)
// in loss of compatibility.
func (id ID) Equals(id2 ID) bool {
return id == id2
} }
// EncodeToString encodes ID into FrostFS API protocol string. // Unmarshal unmarshals protobuf binary representation of ID.
// func (id *ID) Unmarshal(data []byte) error {
// Zero ID is base58 encoding of 32 zeros. return (*refs.ContainerID)(id).Unmarshal(data)
//
// See also DecodeString.
func (id ID) EncodeToString() string {
return base58.Encode(id[:])
} }
// DecodeString decodes string into ID according to FrostFS API protocol. Returns // MarshalJSON encodes ID to protobuf JSON format.
// an error if s is malformed. func (id *ID) MarshalJSON() ([]byte, error) {
// return (*refs.ContainerID)(id).MarshalJSON()
// See also DecodeString.
func (id *ID) DecodeString(s string) error {
data, err := base58.Decode(s)
if err != nil {
return fmt.Errorf("decode base58: %w", err)
}
return id.Decode(data)
} }
// String implements fmt.Stringer. // UnmarshalJSON decodes ID from protobuf JSON format.
// func (id *ID) UnmarshalJSON(data []byte) error {
// String is designed to be human-readable, and its format MAY differ between return (*refs.ContainerID)(id).UnmarshalJSON(data)
// SDK versions. String MAY return same result as EncodeToString. String MUST NOT
// be used to encode ID into FrostFS protocol string.
func (id ID) String() string {
return id.EncodeToString()
} }

View file

@ -5,10 +5,9 @@ import (
"math/rand" "math/rand"
"testing" "testing"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs" "github.com/nspcc-dev/neofs-api-go/v2/refs"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test" cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test"
"github.com/mr-tron/base58"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -17,32 +16,30 @@ func randSHA256Checksum() (cs [sha256.Size]byte) {
return return
} }
const emptyID = "11111111111111111111111111111111"
func TestID_ToV2(t *testing.T) { func TestID_ToV2(t *testing.T) {
t.Run("non-zero", func(t *testing.T) { t.Run("non-nil", func(t *testing.T) {
checksum := randSHA256Checksum() checksum := randSHA256Checksum()
id := cidtest.IDWithChecksum(checksum) id := cidtest.IDWithChecksum(checksum)
var idV2 refs.ContainerID idV2 := id.ToV2()
id.WriteToV2(&idV2)
var newID cid.ID require.Equal(t, id, cid.NewFromV2(idV2))
require.NoError(t, newID.ReadFromV2(idV2))
require.Equal(t, id, newID)
require.Equal(t, checksum[:], idV2.GetValue()) require.Equal(t, checksum[:], idV2.GetValue())
}) })
t.Run("zero", func(t *testing.T) { t.Run("nil", func(t *testing.T) {
var ( var x *cid.ID
x cid.ID
v2 refs.ContainerID
)
x.WriteToV2(&v2) require.Nil(t, x.ToV2())
require.Equal(t, emptyID, base58.Encode(v2.GetValue())) })
t.Run("default values", func(t *testing.T) {
id := cid.New()
// convert to v2 message
cidV2 := id.ToV2()
require.Nil(t, cidV2.GetValue())
}) })
} }
@ -52,57 +49,57 @@ func TestID_Equal(t *testing.T) {
id1 := cidtest.IDWithChecksum(cs) id1 := cidtest.IDWithChecksum(cs)
id2 := cidtest.IDWithChecksum(cs) id2 := cidtest.IDWithChecksum(cs)
require.True(t, id1.Equals(id2)) require.True(t, id1.Equal(id2))
id3 := cidtest.ID() id3 := cidtest.ID()
require.False(t, id1.Equals(id3)) require.False(t, id1.Equal(id3))
} }
func TestID_String(t *testing.T) { func TestID_String(t *testing.T) {
t.Run("DecodeString/EncodeToString", func(t *testing.T) { t.Run("Parse/String", func(t *testing.T) {
id := cidtest.ID() id := cidtest.ID()
var id2 cid.ID id2 := cid.New()
require.NoError(t, id2.DecodeString(id.EncodeToString())) require.NoError(t, id2.Parse(id.String()))
require.Equal(t, id, id2) require.Equal(t, id, id2)
}) })
t.Run("zero", func(t *testing.T) { t.Run("nil", func(t *testing.T) {
var id cid.ID id := cid.New()
require.Equal(t, emptyID, id.EncodeToString()) require.Empty(t, id.String())
})
}
func TestContainerIDEncoding(t *testing.T) {
id := cidtest.ID()
t.Run("binary", func(t *testing.T) {
data, err := id.Marshal()
require.NoError(t, err)
id2 := cid.New()
require.NoError(t, id2.Unmarshal(data))
require.Equal(t, id, id2)
})
t.Run("json", func(t *testing.T) {
data, err := id.MarshalJSON()
require.NoError(t, err)
a2 := cid.New()
require.NoError(t, a2.UnmarshalJSON(data))
require.Equal(t, id, a2)
}) })
} }
func TestNewFromV2(t *testing.T) { func TestNewFromV2(t *testing.T) {
t.Run("from zero", func(t *testing.T) { t.Run("from nil", func(t *testing.T) {
var ( var x *refs.ContainerID
x cid.ID
v2 refs.ContainerID
)
require.Error(t, x.ReadFromV2(v2)) require.Nil(t, cid.NewFromV2(x))
})
}
func TestID_Encode(t *testing.T) {
var id cid.ID
t.Run("panic", func(t *testing.T) {
dst := make([]byte, sha256.Size-1)
require.Panics(t, func() {
id.Encode(dst)
})
})
t.Run("correct", func(t *testing.T) {
dst := make([]byte, sha256.Size)
require.NotPanics(t, func() {
id.Encode(dst)
})
require.Equal(t, emptyID, id.EncodeToString())
}) })
} }

View file

@ -1,13 +0,0 @@
/*
Package cidtest provides functions for convenient testing of cid 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 cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test"
cid := cidtest.ID()
// test the value
*/
package cidtest

View file

@ -4,11 +4,11 @@ import (
"crypto/sha256" "crypto/sha256"
"math/rand" "math/rand"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
) )
// ID returns random cid.ID. // ID returns random cid.ID.
func ID() cid.ID { func ID() *cid.ID {
checksum := [sha256.Size]byte{} checksum := [sha256.Size]byte{}
rand.Read(checksum[:]) rand.Read(checksum[:])
@ -18,8 +18,8 @@ func ID() cid.ID {
// IDWithChecksum returns cid.ID initialized // IDWithChecksum returns cid.ID initialized
// with specified checksum. // with specified checksum.
func IDWithChecksum(cs [sha256.Size]byte) cid.ID { func IDWithChecksum(cs [sha256.Size]byte) *cid.ID {
var id cid.ID id := cid.New()
id.SetSHA256(cs) id.SetSHA256(cs)
return id return id

View file

@ -1,20 +0,0 @@
package container
import (
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
)
// ApplyNetworkConfig applies network configuration to the
// container. Changes the container if it does not satisfy
// network configuration.
func ApplyNetworkConfig(cnr *Container, cfg netmap.NetworkInfo) {
if cfg.HomomorphicHashingDisabled() {
DisableHomomorphicHashing(cnr)
}
}
// AssertNetworkConfig checks if a container matches passed
// network configuration.
func AssertNetworkConfig(cnr Container, cfg netmap.NetworkInfo) bool {
return IsHomomorphicHashingDisabled(cnr) == cfg.HomomorphicHashingDisabled()
}

View file

@ -1,33 +0,0 @@
package container_test
import (
"testing"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
containertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/test"
netmaptest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap/test"
"github.com/stretchr/testify/require"
)
func TestContainer_NetworkConfig(t *testing.T) {
c := containertest.Container()
nc := netmaptest.NetworkInfo()
t.Run("default", func(t *testing.T) {
require.False(t, container.IsHomomorphicHashingDisabled(c))
res := container.AssertNetworkConfig(c, nc)
require.True(t, res)
})
nc.DisableHomomorphicHashing()
t.Run("apply", func(t *testing.T) {
require.False(t, container.IsHomomorphicHashingDisabled(c))
container.ApplyNetworkConfig(&c, nc)
require.True(t, container.IsHomomorphicHashingDisabled(c))
})
}

89
container/opts.go Normal file
View file

@ -0,0 +1,89 @@
package container
import (
"crypto/ecdsa"
"github.com/google/uuid"
"github.com/nspcc-dev/neofs-sdk-go/acl"
"github.com/nspcc-dev/neofs-sdk-go/netmap"
"github.com/nspcc-dev/neofs-sdk-go/owner"
)
type (
Option func(*containerOptions)
containerOptions struct {
acl acl.BasicACL
policy *netmap.PlacementPolicy
attributes Attributes
owner *owner.ID
nonce uuid.UUID
}
)
func defaultContainerOptions() containerOptions {
rand, err := uuid.NewRandom()
if err != nil {
panic("can't create new random " + err.Error())
}
return containerOptions{
acl: acl.PrivateBasicRule,
nonce: rand,
}
}
func WithPublicBasicACL() Option {
return func(option *containerOptions) {
option.acl = acl.PublicBasicRule
}
}
func WithReadOnlyBasicACL() Option {
return func(option *containerOptions) {
option.acl = acl.ReadOnlyBasicRule
}
}
func WithCustomBasicACL(acl acl.BasicACL) Option {
return func(option *containerOptions) {
option.acl = acl
}
}
func WithNonce(nonce uuid.UUID) Option {
return func(option *containerOptions) {
option.nonce = nonce
}
}
func WithOwnerID(id *owner.ID) Option {
return func(option *containerOptions) {
option.owner = id
}
}
func WithOwnerPublicKey(pub *ecdsa.PublicKey) Option {
return func(option *containerOptions) {
if option.owner == nil {
option.owner = new(owner.ID)
}
option.owner.SetPublicKey(pub)
}
}
func WithPolicy(policy *netmap.PlacementPolicy) Option {
return func(option *containerOptions) {
option.policy = policy
}
}
func WithAttribute(key, value string) Option {
return func(option *containerOptions) {
index := len(option.attributes)
option.attributes = append(option.attributes, Attribute{})
option.attributes[index].SetKey(key)
option.attributes[index].SetValue(value)
}
}

View file

@ -1,104 +0,0 @@
package container
import (
"errors"
"fmt"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
)
// SizeEstimation groups information about estimation of the size of the data
// stored in the FrostFS container.
//
// SizeEstimation is mutually compatible with git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container.UsedSpaceAnnouncement
// message. See ReadFromV2 / WriteToV2 methods.
type SizeEstimation struct {
m container.UsedSpaceAnnouncement
}
// ReadFromV2 reads SizeEstimation from the container.UsedSpaceAnnouncement message.
// Checks if the message conforms to FrostFS API V2 protocol.
//
// See also WriteToV2.
func (x *SizeEstimation) ReadFromV2(m container.UsedSpaceAnnouncement) error {
cnrV2 := m.GetContainerID()
if cnrV2 == nil {
return errors.New("missing container")
}
var cnr cid.ID
err := cnr.ReadFromV2(*cnrV2)
if err != nil {
return fmt.Errorf("invalid container: %w", err)
}
x.m = m
return nil
}
// WriteToV2 writes SizeEstimation into the container.UsedSpaceAnnouncement message.
// The message MUST NOT be nil.
//
// See also ReadFromV2.
func (x SizeEstimation) WriteToV2(m *container.UsedSpaceAnnouncement) {
*m = x.m
}
// SetEpoch sets epoch when estimation of the container data size was calculated.
//
// See also Epoch.
func (x *SizeEstimation) SetEpoch(epoch uint64) {
x.m.SetEpoch(epoch)
}
// Epoch return epoch set using SetEpoch.
//
// Zero SizeEstimation represents estimation in zero epoch.
func (x SizeEstimation) Epoch() uint64 {
return x.m.GetEpoch()
}
// SetContainer specifies the container for which the amount of data is estimated.
// Required by the FrostFS API protocol.
//
// See also Container.
func (x *SizeEstimation) SetContainer(cnr cid.ID) {
var cidV2 refs.ContainerID
cnr.WriteToV2(&cidV2)
x.m.SetContainerID(&cidV2)
}
// Container returns container set using SetContainer.
//
// Zero SizeEstimation is not bound to any container (returns zero) which is
// incorrect according to FrostFS API protocol.
func (x SizeEstimation) Container() (res cid.ID) {
m := x.m.GetContainerID()
if m != nil {
err := res.ReadFromV2(*m)
if err != nil {
panic(fmt.Errorf("unexpected error from cid.ID.ReadFromV2: %w", err))
}
}
return
}
// SetValue sets estimated amount of data (in bytes) in the specified container.
//
// See also Value.
func (x *SizeEstimation) SetValue(value uint64) {
x.m.SetUsedSpace(value)
}
// Value returns data size estimation set using SetValue.
//
// Zero SizeEstimation has zero value.
func (x SizeEstimation) Value() uint64 {
return x.m.GetUsedSpace()
}

View file

@ -1,94 +0,0 @@
package container_test
import (
"crypto/sha256"
"testing"
v2container "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test"
"github.com/stretchr/testify/require"
)
func TestSizeEstimation_Epoch(t *testing.T) {
var val container.SizeEstimation
require.Zero(t, val.Epoch())
const epoch = 123
val.SetEpoch(epoch)
require.EqualValues(t, epoch, val.Epoch())
var msg v2container.UsedSpaceAnnouncement
val.WriteToV2(&msg)
require.EqualValues(t, epoch, msg.GetEpoch())
}
func TestSizeEstimation_Container(t *testing.T) {
var val container.SizeEstimation
require.Zero(t, val.Container())
cnr := cidtest.ID()
val.SetContainer(cnr)
require.True(t, val.Container().Equals(cnr))
var msg v2container.UsedSpaceAnnouncement
val.WriteToV2(&msg)
var msgCnr refs.ContainerID
cnr.WriteToV2(&msgCnr)
require.Equal(t, &msgCnr, msg.GetContainerID())
}
func TestSizeEstimation_Value(t *testing.T) {
var val container.SizeEstimation
require.Zero(t, val.Value())
const value = 876
val.SetValue(value)
require.EqualValues(t, value, val.Value())
var msg v2container.UsedSpaceAnnouncement
val.WriteToV2(&msg)
require.EqualValues(t, value, msg.GetUsedSpace())
}
func TestSizeEstimation_ReadFromV2(t *testing.T) {
const epoch = 654
const value = 903
var cnrMsg refs.ContainerID
var msg v2container.UsedSpaceAnnouncement
var val container.SizeEstimation
require.Error(t, val.ReadFromV2(msg))
msg.SetContainerID(&cnrMsg)
require.Error(t, val.ReadFromV2(msg))
cnrMsg.SetValue(make([]byte, sha256.Size))
var cnr cid.ID
require.NoError(t, cnr.ReadFromV2(cnrMsg))
msg.SetEpoch(epoch)
msg.SetUsedSpace(value)
require.NoError(t, val.ReadFromV2(msg))
require.EqualValues(t, epoch, val.Epoch())
require.EqualValues(t, value, val.Value())
require.EqualValues(t, cnr, val.Container())
}

View file

@ -1,39 +1,48 @@
package containertest package containertest
import ( import (
"math/rand" "github.com/nspcc-dev/neofs-sdk-go/container"
cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container" netmaptest "github.com/nspcc-dev/neofs-sdk-go/netmap/test"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl" ownertest "github.com/nspcc-dev/neofs-sdk-go/owner/test"
cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test" versiontest "github.com/nspcc-dev/neofs-sdk-go/version/test"
netmaptest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap/test"
usertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user/test"
) )
// Container returns random container.Container. // Attribute returns random container.Attribute.
func Container() (x container.Container) { func Attribute() *container.Attribute {
owner := usertest.ID() x := container.NewAttribute()
x.Init() x.SetKey("key")
x.SetAttribute("some attribute", "value") x.SetValue("value")
x.SetOwner(*owner)
x.SetBasicACL(BasicACL()) return x
}
// Attributes returns random container.Attributes.
func Attributes() container.Attributes {
return container.Attributes{*Attribute(), *Attribute()}
}
// Container returns random container.Container.
func Container() *container.Container {
x := container.New()
x.SetVersion(versiontest.Version())
x.SetAttributes(Attributes())
x.SetOwnerID(ownertest.ID())
x.SetBasicACL(123)
x.SetPlacementPolicy(netmaptest.PlacementPolicy()) x.SetPlacementPolicy(netmaptest.PlacementPolicy())
return x return x
} }
// SizeEstimation returns random container.SizeEstimation. // UsedSpaceAnnouncement returns random container.UsedSpaceAnnouncement.
func SizeEstimation() (x container.SizeEstimation) { func UsedSpaceAnnouncement() *container.UsedSpaceAnnouncement {
x.SetContainer(cidtest.ID()) x := container.NewAnnouncement()
x.SetEpoch(rand.Uint64())
x.SetValue(rand.Uint64()) x.SetContainerID(cidtest.ID())
x.SetEpoch(55)
x.SetUsedSpace(999)
return x return x
} }
// BasicACL returns random acl.Basic.
func BasicACL() (x acl.Basic) {
x.FromBits(rand.Uint32())
return
}

View file

@ -0,0 +1,11 @@
package container
const (
// AttributeName is an attribute key that is commonly used to denote
// human-friendly name.
AttributeName = "Name"
// AttributeTimestamp is an attribute key that is commonly used to denote
// user-defined local time of container creation in Unix Timestamp format.
AttributeTimestamp = "Timestamp"
)

View file

@ -1,47 +0,0 @@
package frostfscrypto_test
import (
"math/rand"
"testing"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto"
frostfsecdsa "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto/ecdsa"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/stretchr/testify/require"
)
func TestSignature(t *testing.T) {
data := make([]byte, 512)
rand.Read(data)
k, err := keys.NewPrivateKey()
require.NoError(t, err)
var s frostfscrypto.Signature
var m refs.Signature
for _, f := range []func() frostfscrypto.Signer{
func() frostfscrypto.Signer {
return frostfsecdsa.Signer(k.PrivateKey)
},
func() frostfscrypto.Signer {
return frostfsecdsa.SignerRFC6979(k.PrivateKey)
},
func() frostfscrypto.Signer {
return frostfsecdsa.SignerWalletConnect(k.PrivateKey)
},
} {
signer := f()
err := s.Calculate(signer, data)
require.NoError(t, err)
s.WriteToV2(&m)
require.NoError(t, s.ReadFromV2(m))
valid := s.Verify(data)
require.True(t, valid, "type %T", signer)
}
}

View file

@ -1,51 +0,0 @@
/*
Package frostfscrypto collects FrostFS cryptographic primitives.
Signer type unifies entities for signing FrostFS data.
// instantiate Signer
// select data to be signed
var sig Signature
err := sig.Calculate(signer, data)
// ...
// attach signature to the request
SDK natively supports several signature schemes that are implemented
in nested packages.
PublicKey allows to verify signatures.
// get signature to be verified
// compose signed data
isValid := sig.Verify(data)
// ...
Signature can be also used to process FrostFS API V2 protocol messages
(see neo.fs.v2.refs package in https://git.frostfs.info/TrueCloudLab/frostfs-api).
On client side:
import "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
var msg refs.Signature
sig.WriteToV2(&msg)
// send msg
On server side:
// recv msg
var sig frostfscrypto.Signature
sig.ReadFromV2(msg)
// process sig
Using package types in an application is recommended to potentially work with
different protocol versions with which these types are compatible.
*/
package frostfscrypto

View file

@ -1,12 +0,0 @@
/*
Package frostfsecdsa collects ECDSA primitives for FrostFS cryptography.
Signer and PublicKey support ECDSA signature algorithm with SHA-512 hashing.
SignerRFC6979 and PublicKeyRFC6979 implement signature algorithm described in RFC 6979.
All these types provide corresponding interfaces from frostfscrypto package.
Package import causes registration of next signature schemes via frostfscrypto.RegisterScheme:
- frostfscrypto.ECDSA_SHA512
- frostfscrypto.ECDSA_DETERMINISTIC_SHA256
*/
package frostfsecdsa

View file

@ -1,17 +0,0 @@
package frostfsecdsa
import frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto"
func init() {
frostfscrypto.RegisterScheme(frostfscrypto.ECDSA_SHA512, func() frostfscrypto.PublicKey {
return new(PublicKey)
})
frostfscrypto.RegisterScheme(frostfscrypto.ECDSA_DETERMINISTIC_SHA256, func() frostfscrypto.PublicKey {
return new(PublicKeyRFC6979)
})
frostfscrypto.RegisterScheme(frostfscrypto.ECDSA_WALLETCONNECT, func() frostfscrypto.PublicKey {
return new(PublicKeyWalletConnect)
})
}

View file

@ -1,126 +0,0 @@
package frostfsecdsa
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/sha256"
"crypto/sha512"
"fmt"
"math/big"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
)
// PublicKey is a wrapper over ecdsa.PublicKey used for FrostFS needs.
// Provides frostfscrypto.PublicKey interface.
//
// Instances MUST be initialized from ecdsa.PublicKey using type conversion.
type PublicKey ecdsa.PublicKey
// MaxEncodedSize returns size of the compressed ECDSA public key.
func (x PublicKey) MaxEncodedSize() int {
return 33
}
// Encode encodes ECDSA public key in compressed form into buf.
// Uses exactly MaxEncodedSize bytes of the buf.
//
// Encode panics if buf length is less than MaxEncodedSize.
//
// See also Decode.
func (x PublicKey) Encode(buf []byte) int {
if len(buf) < 33 {
panic(fmt.Sprintf("too short buffer %d", len(buf)))
}
return copy(buf, (*keys.PublicKey)(&x).Bytes())
}
// Decode decodes compressed binary representation of the PublicKey.
//
// See also Encode.
func (x *PublicKey) Decode(data []byte) error {
pub, err := keys.NewPublicKeyFromBytes(data, elliptic.P256())
if err != nil {
return err
}
*x = (PublicKey)(*pub)
return nil
}
// similar to elliptic.Unmarshal but without IsOnCurve check.
func unmarshalXY(data []byte) (x *big.Int, y *big.Int) {
if len(data) != 65 {
return
} else if data[0] != 4 { // uncompressed form
return
}
p := elliptic.P256().Params().P
x = new(big.Int).SetBytes(data[1:33])
y = new(big.Int).SetBytes(data[33:])
if x.Cmp(p) >= 0 || y.Cmp(p) >= 0 {
x, y = nil, nil
}
return
}
// Verify verifies data signature calculated by ECDSA algorithm with SHA-512 hashing.
func (x PublicKey) Verify(data, signature []byte) bool {
h := sha512.Sum512(data)
r, s := unmarshalXY(signature)
return r != nil && s != nil && ecdsa.Verify((*ecdsa.PublicKey)(&x), h[:], r, s)
}
// PublicKeyRFC6979 is a wrapper over ecdsa.PublicKey used for FrostFS needs.
// Provides frostfscrypto.PublicKey interface.
//
// Instances MUST be initialized from ecdsa.PublicKey using type conversion.
type PublicKeyRFC6979 ecdsa.PublicKey
// MaxEncodedSize returns size of the compressed ECDSA public key.
func (x PublicKeyRFC6979) MaxEncodedSize() int {
return 33
}
// Encode encodes ECDSA public key in compressed form into buf.
// Uses exactly MaxEncodedSize bytes of the buf.
//
// Encode panics if buf length is less than MaxEncodedSize.
//
// See also Decode.
func (x PublicKeyRFC6979) Encode(buf []byte) int {
if len(buf) < 33 {
panic(fmt.Sprintf("too short buffer %d", len(buf)))
}
return copy(buf, (*keys.PublicKey)(&x).Bytes())
}
// Decode decodes binary representation of the ECDSA public key.
//
// See also Encode.
func (x *PublicKeyRFC6979) Decode(data []byte) error {
pub, err := keys.NewPublicKeyFromBytes(data, elliptic.P256())
if err != nil {
return err
}
*x = (PublicKeyRFC6979)(*pub)
return nil
}
// Verify verifies data signature calculated by deterministic ECDSA algorithm
// with SHA-256 hashing.
//
// See also RFC 6979.
func (x PublicKeyRFC6979) Verify(data, signature []byte) bool {
h := sha256.Sum256(data)
return (*keys.PublicKey)(&x).Verify(signature, h[:])
}

View file

@ -1,77 +0,0 @@
package frostfsecdsa
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha512"
frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
)
// Signer wraps ecdsa.PrivateKey and represents signer based on ECDSA with
// SHA-512 hashing. Provides frostfscrypto.Signer interface.
//
// Instances MUST be initialized from ecdsa.PrivateKey using type conversion.
type Signer ecdsa.PrivateKey
// Scheme returns frostfscrypto.ECDSA_SHA512.
// Implements frostfscrypto.Signer.
func (x Signer) Scheme() frostfscrypto.Scheme {
return frostfscrypto.ECDSA_SHA512
}
// Sign signs data using ECDSA algorithm with SHA-512 hashing.
// Implements frostfscrypto.Signer.
func (x Signer) Sign(data []byte) ([]byte, error) {
h := sha512.Sum512(data)
r, s, err := ecdsa.Sign(rand.Reader, (*ecdsa.PrivateKey)(&x), h[:])
if err != nil {
return nil, err
}
params := elliptic.P256().Params()
curveOrderByteSize := params.P.BitLen() / 8
buf := make([]byte, 1+curveOrderByteSize*2)
buf[0] = 4
_ = r.FillBytes(buf[1 : 1+curveOrderByteSize])
_ = s.FillBytes(buf[1+curveOrderByteSize:])
return buf, nil
}
// Public initializes PublicKey and returns it as frostfscrypto.PublicKey.
// Implements frostfscrypto.Signer.
func (x Signer) Public() frostfscrypto.PublicKey {
return (*PublicKey)(&x.PublicKey)
}
// SignerRFC6979 wraps ecdsa.PrivateKey and represents signer based on deterministic
// ECDSA with SHA-256 hashing (RFC 6979). Provides frostfscrypto.Signer interface.
//
// Instances SHOULD be initialized from ecdsa.PrivateKey using type conversion.
type SignerRFC6979 ecdsa.PrivateKey
// Scheme returns frostfscrypto.ECDSA_DETERMINISTIC_SHA256.
// Implements frostfscrypto.Signer.
func (x SignerRFC6979) Scheme() frostfscrypto.Scheme {
return frostfscrypto.ECDSA_DETERMINISTIC_SHA256
}
// Sign signs data using deterministic ECDSA algorithm with SHA-256 hashing.
// Implements frostfscrypto.Signer.
//
// See also RFC 6979.
func (x SignerRFC6979) Sign(data []byte) ([]byte, error) {
p := keys.PrivateKey{PrivateKey: (ecdsa.PrivateKey)(x)}
return p.Sign(data), nil
}
// Public initializes PublicKeyRFC6979 and returns it as frostfscrypto.PublicKey.
// Implements frostfscrypto.Signer.
func (x SignerRFC6979) Public() frostfscrypto.PublicKey {
return (*PublicKeyRFC6979)(&x.PublicKey)
}

View file

@ -1,85 +0,0 @@
package frostfsecdsa
import (
"crypto/ecdsa"
"crypto/elliptic"
"encoding/base64"
"fmt"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/util/signature/walletconnect"
frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
)
// SignerWalletConnect is similar to SignerRFC6979 with 2 changes:
// 1. The data is base64 encoded before signing/verifying.
// 2. The signature is a concatenation of the signature itself and 16-byte salt.
//
// Instances MUST be initialized from ecdsa.PrivateKey using type conversion.
type SignerWalletConnect ecdsa.PrivateKey
// Scheme returns frostfscrypto.ECDSA_WALLETCONNECT.
// Implements frostfscrypto.Signer.
func (x SignerWalletConnect) Scheme() frostfscrypto.Scheme {
return frostfscrypto.ECDSA_WALLETCONNECT
}
// Sign signs data using ECDSA algorithm with SHA-512 hashing.
// Implements frostfscrypto.Signer.
func (x SignerWalletConnect) Sign(data []byte) ([]byte, error) {
b64 := make([]byte, base64.StdEncoding.EncodedLen(len(data)))
base64.StdEncoding.Encode(b64, data)
return walletconnect.Sign((*ecdsa.PrivateKey)(&x), b64)
}
// Public initializes PublicKey and returns it as frostfscrypto.PublicKey.
// Implements frostfscrypto.Signer.
func (x SignerWalletConnect) Public() frostfscrypto.PublicKey {
return (*PublicKeyWalletConnect)(&x.PublicKey)
}
// PublicKeyWalletConnect is a wrapper over ecdsa.PublicKey used for FrostFS needs.
// Provides frostfscrypto.PublicKey interface.
//
// Instances MUST be initialized from ecdsa.PublicKey using type conversion.
type PublicKeyWalletConnect ecdsa.PublicKey
// MaxEncodedSize returns size of the compressed ECDSA public key.
func (x PublicKeyWalletConnect) MaxEncodedSize() int {
return 33
}
// Encode encodes ECDSA public key in compressed form into buf.
// Uses exactly MaxEncodedSize bytes of the buf.
//
// Encode panics if buf length is less than MaxEncodedSize.
//
// See also Decode.
func (x PublicKeyWalletConnect) Encode(buf []byte) int {
if len(buf) < 33 {
panic(fmt.Sprintf("too short buffer %d", len(buf)))
}
return copy(buf, (*keys.PublicKey)(&x).Bytes())
}
// Decode decodes compressed binary representation of the PublicKeyWalletConnect.
//
// See also Encode.
func (x *PublicKeyWalletConnect) Decode(data []byte) error {
pub, err := keys.NewPublicKeyFromBytes(data, elliptic.P256())
if err != nil {
return err
}
*x = (PublicKeyWalletConnect)(*pub)
return nil
}
// Verify verifies data signature calculated by ECDSA algorithm with SHA-512 hashing.
func (x PublicKeyWalletConnect) Verify(data, signature []byte) bool {
b64 := make([]byte, base64.StdEncoding.EncodedLen(len(data)))
base64.StdEncoding.Encode(b64, data)
return walletconnect.Verify((*ecdsa.PublicKey)(&x), b64, signature)
}

View file

@ -1,102 +0,0 @@
package frostfscrypto
import (
"errors"
"fmt"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
)
// Signature represents a confirmation of data integrity received by the
// digital signature mechanism.
//
// Signature is mutually compatible with git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs.Signature
// message. See ReadFromV2 / WriteToV2 methods.
//
// Note that direct typecast is not safe and may result in loss of compatibility:
//
// _ = Signature(refs.Signature{}) // not recommended
type Signature refs.Signature
// ReadFromV2 reads Signature from the refs.Signature message. Checks if the
// message conforms to FrostFS API V2 protocol.
//
// See also WriteToV2.
func (x *Signature) ReadFromV2(m refs.Signature) error {
if len(m.GetKey()) == 0 {
return errors.New("missing public key")
} else if len(m.GetSign()) == 0 {
return errors.New("missing signature")
}
switch m.GetScheme() {
default:
return fmt.Errorf("unsupported scheme %v", m.GetSign())
case
refs.ECDSA_SHA512,
refs.ECDSA_RFC6979_SHA256,
refs.ECDSA_RFC6979_SHA256_WALLET_CONNECT:
}
*x = Signature(m)
return nil
}
// WriteToV2 writes Signature to the refs.Signature message.
// The message must not be nil.
//
// See also ReadFromV2.
func (x Signature) WriteToV2(m *refs.Signature) {
*m = (refs.Signature)(x)
}
// Calculate signs data using Signer and encodes public key for subsequent
// verification.
//
// Signer MUST NOT be nil.
//
// See also Verify.
func (x *Signature) Calculate(signer Signer, data []byte) error {
signature, err := signer.Sign(data)
if err != nil {
return fmt.Errorf("signer %T failure: %w", signer, err)
}
pub := signer.Public()
key := make([]byte, pub.MaxEncodedSize())
key = key[:pub.Encode(key)]
m := (*refs.Signature)(x)
m.SetScheme(refs.SignatureScheme(signer.Scheme()))
m.SetSign(signature)
m.SetKey(key)
return nil
}
// Verify verifies data signature using encoded public key. True means valid
// signature.
//
// Verify fails if signature scheme is not supported (see RegisterScheme).
//
// See also Calculate.
func (x Signature) Verify(data []byte) bool {
m := (*refs.Signature)(&x)
f, ok := publicKeys[Scheme(m.GetScheme())]
if !ok {
return false
}
key := f()
err := key.Decode(m.GetKey())
if err != nil {
return false
}
return key.Verify(data, m.GetSign())
}

View file

@ -1,94 +0,0 @@
package frostfscrypto
import (
"fmt"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
)
// Scheme represents digital signature algorithm with fixed cryptographic hash function.
//
// Negative values are reserved and depend on context (e.g. unsupported scheme).
type Scheme int32
//nolint:revive
const (
_ Scheme = iota - 1
ECDSA_SHA512 // ECDSA with SHA-512 hashing (FIPS 186-3)
ECDSA_DETERMINISTIC_SHA256 // Deterministic ECDSA with SHA-256 hashing (RFC 6979)
ECDSA_WALLETCONNECT // Wallet Connect signature scheme
)
// String implements fmt.Stringer.
func (x Scheme) String() string {
return refs.SignatureScheme(x).String()
}
// maps Scheme to blank PublicKey constructor.
var publicKeys = make(map[Scheme]func() PublicKey)
// RegisterScheme registers a function that returns a new blank PublicKey
// instance for the given Scheme. This is intended to be called from the init
// function in packages that implement signature schemes.
//
// RegisterScheme panics if function for the given Scheme is already registered.
//
// Note that RegisterScheme isn't tread-safe.
func RegisterScheme(scheme Scheme, f func() PublicKey) {
_, ok := publicKeys[scheme]
if ok {
panic(fmt.Sprintf("scheme %v is already registered", scheme))
}
publicKeys[scheme] = f
}
// Signer is an interface of entities that can be used for signing operations
// in FrostFS. Unites secret and public parts. For example, an ECDSA private key
// or external auth service.
//
// See also PublicKey.
type Signer interface {
// Scheme returns corresponding signature scheme.
Scheme() Scheme
// Sign signs digest of the given data. Implementations encapsulate data
// hashing that depends on Scheme. For example, if scheme uses SHA-256, then
// Sign signs SHA-256 hash of the data.
Sign(data []byte) ([]byte, error)
// Public returns the public key corresponding to the Signer.
Public() PublicKey
}
// PublicKey represents a public key using fixed signature scheme supported by
// FrostFS.
//
// See also Signer.
type PublicKey interface {
// MaxEncodedSize returns maximum size required for binary-encoded
// public key.
//
// MaxEncodedSize MUST NOT return value greater than any return of
// Encode.
MaxEncodedSize() int
// Encode encodes public key into buf. Returns number of bytes
// written.
//
// Encode MUST panic if buffer size is insufficient and less than
// MaxEncodedSize (*). Encode MUST return negative value
// on any failure except (*).
//
// Encode is a reverse operation to Decode.
Encode(buf []byte) int
// Decode decodes binary public key.
//
// Decode is a reverse operation to Encode.
Decode([]byte) error
// Verify checks signature of the given data. True means correct signature.
Verify(data, signature []byte) bool
}

Some files were not shown because too many files have changed in this diff Show more