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
380 changed files with 21658 additions and 38420 deletions

View file

@ -1,21 +0,0 @@
name: DCO
on: [pull_request]
jobs:
dco:
name: DCO
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v3
with:
go-version: '1.23'
- name: Run commit format checker
uses: https://git.frostfs.info/TrueCloudLab/dco-go@v3
with:
from: 'origin/${{ github.event.pull_request.base.ref }}'

View file

@ -1,39 +0,0 @@
name: Tests and linters
on: [pull_request]
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: '1.23'
cache: true
- name: Install linters
run: make lint-install
- name: Run linters
run: make lint
tests:
name: Tests
runs-on: ubuntu-latest
strategy:
matrix:
go_versions: [ '1.22', '1.23' ]
fail-fast: false
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: '${{ matrix.go_versions }}'
- name: Run tests
run: make test

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. -->

21
.github/workflows/dco.yml vendored Normal file
View file

@ -0,0 +1,21 @@
name: DCO check
on:
pull_request:
branches:
- master
jobs:
commits_check_job:
runs-on: ubuntu-latest
name: Commits Check
steps:
- name: Get PR Commits
id: 'get-pr-commits'
uses: tim-actions/get-pr-commits@master
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: DCO Check
uses: tim-actions/dco@master
with:
commits: ${{ steps.get-pr-commits.outputs.commits }}

52
.github/workflows/tests.yml vendored Normal file
View file

@ -0,0 +1,52 @@
name: neofs-sdk-go tests
on:
pull_request:
branches:
- master
types: [opened, synchronize]
paths-ignore:
- '**/*.md'
workflow_dispatch:
jobs:
tests:
name: Tests
runs-on: ubuntu-20.04
strategy:
matrix:
go_versions: [ '1.16.x', '1.17.x' ]
fail-fast: false
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: '${{ matrix.go_versions }}'
- name: Restore Go modules from cache
uses: actions/cache@v2
with:
path: /home/runner/go/pkg/mod
key: deps-${{ hashFiles('go.sum') }}
- name: Update Go modules
run: make dep
- name: Run tests
run: make test
lint:
runs-on: ubuntu-20.04
steps:
- name: Check out code
uses: actions/checkout@v2
- name: golangci-lint
uses: golangci/golangci-lint-action@v2
with:
version: latest
only-new-issues: true

12
.gitignore vendored
View file

@ -20,14 +20,4 @@ vendor/
# coverage
coverage.txt
coverage.html
# antlr tool jar
antlr-*.jar
# tempfiles
.cache
# binary
bin/
release/
coverage.html

View file

@ -4,16 +4,15 @@
# options for analysis running
run:
# timeout for analysis, e.g. 30s, 5m, default is 1m
timeout: 10m
timeout: 5m
# include test files or not, default is true
tests: false
tests: true
# output configuration options
output:
# colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number"
formats:
- format: tab
format: tab
# all available settings of specific linters
linters-settings:
@ -25,13 +24,6 @@ linters-settings:
govet:
# report about shadowed variables
check-shadowing: false
staticcheck:
checks: ["all"]
funlen:
lines: 80 # default 60
statements: 60 # default 40
gocognit:
min-complexity: 40 # default 30
linters:
enable:
@ -40,29 +32,28 @@ linters:
- revive
# some default golangci-lint linters
- deadcode
- errcheck
- gosimple
- godot
- ineffassign
- staticcheck
- structcheck
- typecheck
- unused
- varcheck
# extra linters
- bidichk
- durationcheck
- exhaustive
- exportloopref
- godot
- gofmt
- goimports
- misspell
- predeclared
- reassign
- whitespace
- containedctx
- funlen
- gocognit
- contextcheck
- protogetter
- goimports
disable-all: true
fast: false
issues:
include:
- EXC0002 # should have a comment
- EXC0003 # test/Test ... consider calling this
- EXC0004 # govet
- EXC0005 # C-style breaks

View file

@ -1,36 +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|.interp|.tokens)$"
- repo: local
hooks:
- id: go-unit-tests
name: go unit tests
entry: make test GOFLAGS=''
pass_filenames: false
types: [go]
language: system
- repo: local
hooks:
- id: make-lint
name: Run Make Lint
entry: make lint
language: system
pass_filenames: false

View file

@ -1,4 +0,0 @@
FROM golang:1.22
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install make openjdk-17-jre -y
WORKDIR /work

57
Makefile Executable file → Normal file
View file

@ -1,16 +1,8 @@
#!/usr/bin/make -f
ANTLR_VERSION="4.13.0"
TMP_DIR := .cache
LINT_VERSION ?= 1.60.1
TRUECLOUDLAB_LINT_VERSION ?= 0.0.6
OUTPUT_LINT_DIR ?= $(shell pwd)/bin
LINT_DIR = $(OUTPUT_LINT_DIR)/golangci-lint-$(LINT_VERSION)-v$(TRUECLOUDLAB_LINT_VERSION)
# Run tests
test: GOFLAGS ?= "-cover -count=1"
test:
@GOFLAGS=$(GOFLAGS) go test ./...
@go test ./... -cover
# Pull go dependencies
dep:
@ -21,23 +13,9 @@ dep:
@CGO_ENABLED=0 \
go mod tidy -v && echo OK
# Install linters
lint-install:
@mkdir -p $(TMP_DIR)
@rm -rf $(TMP_DIR)/linters
@git -c advice.detachedHead=false clone --branch v$(TRUECLOUDLAB_LINT_VERSION) https://git.frostfs.info/TrueCloudLab/linters.git $(TMP_DIR)/linters
@@make -C $(TMP_DIR)/linters lib CGO_ENABLED=1 OUT_DIR=$(OUTPUT_LINT_DIR)
@rm -rf $(TMP_DIR)/linters
@rmdir $(TMP_DIR) 2>/dev/null || true
@CGO_ENABLED=1 GOBIN=$(LINT_DIR) go install github.com/golangci/golangci-lint/cmd/golangci-lint@v$(LINT_VERSION)
# Run linters
lint:
@if [ ! -d "$(LINT_DIR)" ]; then \
echo "Run make lint-install"; \
exit 1; \
fi
$(LINT_DIR)/golangci-lint run
@golangci-lint --timeout=5m run
# Run tests with race detection and produce coverage output
cover:
@ -51,23 +29,6 @@ format:
@echo "⇒ Processing goimports check"
@goimports -w ./
policy:
@wget -q https://www.antlr.org/download/antlr-${ANTLR_VERSION}-complete.jar -O antlr4-tool.jar
@java -Xmx500M -cp "`pwd`/antlr4-tool.jar" "org.antlr.v4.Tool" -o `pwd`/netmap/parser/ -Dlanguage=Go -no-listener -visitor `pwd`/netmap/parser/Query.g4 `pwd`/netmap/parser/QueryLexer.g4
# Run `make %` in truecloudlab/frostfs-sdk-go container(Golang+Java)
docker/%:
@docker build -t truecloudlab/frostfs-sdk-go --platform linux/amd64 . > /dev/null
@docker run --rm -t \
-v `pwd`:/work \
-u "$$(id -u):$$(id -g)" \
--env HOME=/work \
truecloudlab/frostfs-sdk-go make $*
# Synchronize tree service
sync-tree:
@./syncTree.sh
# Show this help prompt
help:
@echo ' Usage:'
@ -76,16 +37,4 @@ help:
@echo ''
@echo ' Targets:'
@echo ''
@awk '/^#/{ comment = substr($$0,3) } comment && /^[a-zA-Z][a-zA-Z0-9_-]+ ?:/{ print " ", $$1, comment }' $(MAKEFILE_LIST) | column -t -s ':' | grep -v 'IGNORE' | sort -u
# Activate pre-commit hooks
pre-commit:
pre-commit install --hook-type pre-commit
# Deactivate pre-commit hooks
unpre-commit:
pre-commit uninstall --hook-type pre-commit
# Run pre-commit hooks
pre-commit-run:
@pre-commit run --all-files --hook-stage manual
@awk '/^#/{ comment = substr($$0,3) } comment && /^[a-zA-Z][a-zA-Z0-9_-]+ ?:/{ print " ", $$1, comment }' $(MAKEFILE_LIST) | column -t -s ':' | grep -v 'IGNORE' | sort -u

View file

@ -1,6 +1,6 @@
# frostfs-sdk-go
Go implementation of FrostFS 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
# neofs-sdk-go
Go implementation of NeoFS SDK. It contains high-level version-independent wrappers
for structures from [neofs-api-go](https://github.com/nspcc-dev/neofs-api-go) as well as
helper functions for simplifying node/dApp implementations.
## Repository structure
@ -10,73 +10,63 @@ Contains fixed-point `Decimal` type for performing balance calculations.
### eacl
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
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.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)
in Neo blockchain. Note that for historical reasons it contains
version prefix and checksum in addition to script-hash.
### token
Contains Bearer token type with several FrostFS-specific methods.
Contains Bearer token type with several NeoFS-specific methods.
### ns
In FrostFS 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.
### resolver
In NeoFS there are 2 types of name resolution: DNS and NNS. NNS stands for Neo Name Service
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
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.
### 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
to perform certain actions on behalf of the user.
### client
Contains client for working with FrostFS.
Contains client for working with NeoFS.
```go
var prmInit client.PrmInit
prmInit.SetDefaultPrivateKey(key) // private key for request signing
var c client.Client
c.Init(prmInit)
var prmDial client.PrmDial
prmDial.SetServerURI("grpcs://localhost:40005") // endpoint address
err := c.Dial(prmDial)
if err != nil {
return
}
c, _ := client.New(
client.WithAddress("localhost:40005"), // endpoint address
client.WithDefaultPrivateKey(key), // private key for request signing
client.WithNeoFSErrorHandling(), // enable erroneous status parsing
client.WithTLSConfig(&tls.Config{})) // custom TLS configuration
ctx, cancel := context.WithTimeout(context.Background(), 5 * time.Second)
defer cancel()
var prm client.PrmBalanceGet
prm.SetAccount(acc)
res, err := c.BalanceGet(ctx, prm)
res, err := c.BalanceGet(ctx, owner)
if err != nil {
return
}
fmt.Printf("Balance for %s: %v\n", acc, res.Amount())
fmt.Printf("Balance for %s: %s\n", owner, res.Amount())
```
#### 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
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
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.
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).
[NeoFS API](https://github.com/nspcc-dev/neofs-api/blob/master/status/types.proto). There is also
a `client.WithNeoFSErrorHandling()` to seamlessly convert erroneous statuses into Go error type.
### policy
Contains helpers allowing conversion of placing policy from/to JSON representation
@ -96,19 +86,19 @@ Contains CRUSH-like implementation of container node selection algorithm. Releva
are described in this paper http://ceur-ws.org/Vol-2344/short10.pdf . Note that it can be
outdated in some details.
`netmap/json_tests` subfolder contains language-agnostic tests for selection algorithm.
`netmap/json_tests` subfolder contains language-agnostic tests for selection algorithm.
```go
import (
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
"github.com/nspcc-dev/neofs-sdk-go/netmap"
"github.com/nspcc-dev/neofs-sdk-go/object"
)
func placementNodes(addr *object.Address, p *netmap.PlacementPolicy, frostfsNodes []netmap.NodeInfo) {
// Convert list of nodes in FrostFS API format to the intermediate representation.
func placementNodes(addr *object.Address, p *netmap.PlacementPolicy, neofsNodes []netmap.NodeInfo) {
// Convert list of nodes in NeoFS API format to the intermediate representation.
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).
nm, _ := NewNetmap(nodes)
// Calculate nodes of container.
@ -120,13 +110,13 @@ func placementNodes(addr *object.Address, p *netmap.PlacementPolicy, frostfsNode
```
### pool
Simple pool for managing connections to FrostFS nodes.
Simple pool for managing connections to NeoFS nodes.
### acl, checksum, version, signature
Contain simple API wrappers.
### logger
Wrapper over `zap.Logger` which is used across FrostFS codebase.
Wrapper over `zap.Logger` which is used across NeoFS codebase.
### util
Utilities for working with signature-related code.
Utilities for working with signature-related code.

View file

@ -1,64 +1,69 @@
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 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
// ReadFromV2 reads Decimal from the accounting.Decimal message. Checks if the
// message conforms to FrostFS API V2 protocol.
// NewDecimal creates, initializes and returns empty Decimal instance.
//
// See also WriteToV2.
func (d *Decimal) ReadFromV2(m accounting.Decimal) error {
*d = Decimal(m)
return nil
// Defaults:
// - value: 0
// - precision: 0
func NewDecimal() *Decimal {
return NewDecimalFromV2(new(accounting.Decimal))
}
// WriteToV2 writes Decimal to the accounting.Decimal message.
// The message must not be nil.
// NewDecimalFromV2 converts v2 Decimal to Decimal.
//
// See also ReadFromV2.
func (d Decimal) WriteToV2(m *accounting.Decimal) {
*m = (accounting.Decimal)(d)
// Nil Decimal converts to nil.
func NewDecimalFromV2(d *accounting.Decimal) *Decimal {
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.
//
// Zero Decimal has zero value.
//
// See also SetValue.
func (d Decimal) Value() int64 {
return (*accounting.Decimal)(&d).GetValue()
func (d *Decimal) Value() int64 {
return (*accounting.Decimal)(d).GetValue()
}
// SetValue sets value of the decimal number.
//
// See also Value.
func (d *Decimal) SetValue(v int64) {
(*accounting.Decimal)(d).SetValue(v)
}
// Precision returns precision of the decimal number.
//
// Zero Decimal has zero precision.
//
// See also SetPrecision.
func (d Decimal) Precision() uint32 {
return (*accounting.Decimal)(&d).GetPrecision()
func (d *Decimal) Precision() uint32 {
return (*accounting.Decimal)(d).GetPrecision()
}
// SetPrecision sets precision of the decimal number.
//
// See also Precision.
func (d *Decimal) SetPrecision(p uint32) {
(*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 (
"testing"
v2accounting "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/accounting"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/accounting"
"github.com/nspcc-dev/neofs-sdk-go/accounting"
accountingtest "github.com/nspcc-dev/neofs-sdk-go/accounting/test"
"github.com/stretchr/testify/require"
)
func TestDecimalData(t *testing.T) {
func TestDecimal(t *testing.T) {
const v, p = 4, 2
var d accounting.Decimal
require.Zero(t, d.Value())
require.Zero(t, d.Precision())
d := accounting.NewDecimal()
d.SetValue(v)
d.SetPrecision(p)
@ -23,24 +19,26 @@ func TestDecimalData(t *testing.T) {
require.EqualValues(t, p, d.Precision())
}
func TestDecimalMessageV2(t *testing.T) {
var (
d accounting.Decimal
m v2accounting.Decimal
)
func TestDecimalEncoding(t *testing.T) {
d := accountingtest.Decimal()
m.SetValue(7)
m.SetPrecision(8)
t.Run("binary", func(t *testing.T) {
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.EqualValues(t, m.GetPrecision(), d.Precision())
require.Equal(t, d, d2)
})
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.EqualValues(t, d.Precision(), m2.GetPrecision())
require.Equal(t, d, d2)
})
}

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 (
"math/rand"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/accounting"
"github.com/nspcc-dev/neofs-sdk-go/accounting"
)
// Decimal returns random accounting.Decimal.
func Decimal() *accounting.Decimal {
var d accounting.Decimal
d := accounting.NewDecimal()
d.SetValue(rand.Int63())
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,52 +0,0 @@
package ape
import (
"errors"
"fmt"
apeV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/ape"
)
var (
ErrInvalidChainRepresentation = errors.New("invalid chain representation")
)
// ChainID is Chain's identifier.
type ChainID []byte
// Chain is an SDK representation for v2's Chain.
//
// Note that Chain (as well as v2's Chain) and all related entities
// are NOT operated by Access-Policy-Engine (APE). The client is responsible
// to convert these types to policy-engine entities.
type Chain struct {
// Raw is the encoded chain kind.
// It assumes that Raw's bytes are the result of encoding provided by
// policy-engine package.
Raw []byte
}
// ToV2 converts Chain to v2.
func (c *Chain) ToV2() *apeV2.Chain {
v2ct := new(apeV2.Chain)
if c.Raw != nil {
v2Raw := new(apeV2.ChainRaw)
v2Raw.SetRaw(c.Raw)
v2ct.SetKind(v2Raw)
}
return v2ct
}
// ReadFromV2 fills Chain from v2.
func (c *Chain) ReadFromV2(v2ct *apeV2.Chain) error {
switch v := v2ct.GetKind().(type) {
default:
return fmt.Errorf("unsupported chain kind: %T", v)
case *apeV2.ChainRaw:
raw := v.GetRaw()
c.Raw = raw
}
return nil
}

View file

@ -1,53 +0,0 @@
package ape
import (
apeV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/ape"
)
// TargetType is an SDK representation for v2's TargetType.
type TargetType apeV2.TargetType
const (
TargetTypeUndefined TargetType = iota
TargetTypeNamespace
TargetTypeContainer
TargetTypeUser
TargetTypeGroup
)
// ToV2 converts TargetType to v2.
func (targetType TargetType) ToV2() apeV2.TargetType {
return apeV2.TargetType(targetType)
}
// FromV2 reads TargetType to v2.
func (targetType *TargetType) FromV2(v2targetType apeV2.TargetType) {
*targetType = TargetType(v2targetType)
}
// ChainTarget is an SDK representation for v2's ChainTarget.
//
// Note that ChainTarget (as well as v2's ChainTarget) and all related entities
// are NOT operated by Access-Policy-Engine (APE). The client is responsible
// to convert these types to policy-engine entities.
type ChainTarget struct {
TargetType TargetType
Name string
}
// ToV2 converts ChainTarget to v2.
func (ct *ChainTarget) ToV2() *apeV2.ChainTarget {
v2ct := new(apeV2.ChainTarget)
v2ct.SetTargetType(ct.TargetType.ToV2())
v2ct.SetName(ct.Name)
return v2ct
}
// FromV2 reads ChainTarget frpm v2.
func (ct *ChainTarget) FromV2(v2ct *apeV2.ChainTarget) {
ct.TargetType.FromV2(v2ct.GetTargetType())
ct.Name = v2ct.GetName()
}

View file

@ -1,65 +0,0 @@
package ape_test
import (
"testing"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/ape"
"github.com/stretchr/testify/require"
apeV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/ape"
)
var (
m = map[ape.TargetType]apeV2.TargetType{
ape.TargetTypeUndefined: apeV2.TargetTypeUndefined,
ape.TargetTypeNamespace: apeV2.TargetTypeNamespace,
ape.TargetTypeContainer: apeV2.TargetTypeContainer,
ape.TargetTypeUser: apeV2.TargetTypeUser,
ape.TargetTypeGroup: apeV2.TargetTypeGroup,
}
)
func TestTargetType(t *testing.T) {
for typesdk, typev2 := range m {
t.Run("from sdk to v2 "+typev2.String(), func(t *testing.T) {
v2 := typesdk.ToV2()
require.Equal(t, v2, typev2)
})
t.Run("from v2 to sdk "+typev2.String(), func(t *testing.T) {
var typ ape.TargetType
typ.FromV2(typev2)
require.Equal(t, typesdk, typ)
})
}
}
func TestChainTarget(t *testing.T) {
var (
typ = ape.TargetTypeNamespace
name = "namespaceXXYYZZ"
)
t.Run("from sdk to v2", func(t *testing.T) {
ct := ape.ChainTarget{
TargetType: typ,
Name: name,
}
v2 := ct.ToV2()
require.Equal(t, m[typ], v2.GetTargetType())
require.Equal(t, name, v2.GetName())
})
t.Run("from v2 to sdk", func(t *testing.T) {
v2 := &apeV2.ChainTarget{}
v2.SetTargetType(m[typ])
v2.SetName(name)
var ct ape.ChainTarget
ct.FromV2(v2)
require.Equal(t, typ, ct.TargetType)
require.Equal(t, name, ct.Name)
})
}

View file

@ -1,43 +0,0 @@
package ape_test
import (
"testing"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/ape"
"github.com/stretchr/testify/require"
apeV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/ape"
)
const (
encoded = `{"ID":"","Rules":[{"Status":"Allow","Actions":{"Inverted":false,"Names":["GetObject"]},"Resources":{"Inverted":false,"Names":["native:object/*"]},"Any":false,"Condition":[{"Op":"StringEquals","Object":"Resource","Key":"Department","Value":"HR"}]}],"MatchType":"DenyPriority"}`
)
func TestChainData(t *testing.T) {
t.Run("raw chain", func(t *testing.T) {
var c ape.Chain
b := []byte(encoded)
c.Raw = b
v2, ok := c.ToV2().GetKind().(*apeV2.ChainRaw)
require.True(t, ok)
require.Equal(t, b, v2.Raw)
})
}
func TestChainMessageV2(t *testing.T) {
b := []byte(encoded)
v2Raw := &apeV2.ChainRaw{}
v2Raw.SetRaw(b)
v2 := &apeV2.Chain{}
v2.SetKind(v2Raw)
var c ape.Chain
c.ReadFromV2(v2)
require.NotNil(t, c.Raw)
require.Equal(t, b, c.Raw)
}

291
audit/result.go Normal file
View file

@ -0,0 +1,291 @@
package audit
import (
"github.com/nspcc-dev/neofs-api-go/v2/audit"
"github.com/nspcc-dev/neofs-api-go/v2/refs"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
"github.com/nspcc-dev/neofs-sdk-go/version"
)
// Result represents v2-compatible data audit result.
type Result audit.DataAuditResult
// NewFromV2 wraps v2 DataAuditResult message to Result.
//
// Nil audit.DataAuditResult converts to nil.
func NewResultFromV2(aV2 *audit.DataAuditResult) *Result {
return (*Result)(aV2)
}
// New creates and initializes blank Result.
//
// Defaults:
// - version: version.Current();
// - complete: false;
// - cid: nil;
// - pubKey: nil;
// - passSG, failSG: nil;
// - failNodes, passNodes: nil;
// - hit, miss, fail: 0;
// - requests, retries: 0;
// - auditEpoch: 0.
func NewResult() *Result {
r := NewResultFromV2(new(audit.DataAuditResult))
r.SetVersion(version.Current())
return r
}
// ToV2 converts Result to v2 DataAuditResult message.
//
// 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 {
return (*audit.DataAuditResult)(r).Unmarshal(data)
}
// MarshalJSON encodes Result to protobuf JSON format.
func (r *Result) MarshalJSON() ([]byte, error) {
return (*audit.DataAuditResult)(r).MarshalJSON()
}
// UnmarshalJSON decodes Result from protobuf JSON format.
func (r *Result) UnmarshalJSON(data []byte) error {
return (*audit.DataAuditResult)(r).UnmarshalJSON(data)
}
// Version returns Data Audit structure version.
func (r *Result) Version() *version.Version {
return version.NewFromV2(
(*audit.DataAuditResult)(r).GetVersion())
}
// SetVersion sets Data Audit structure version.
func (r *Result) SetVersion(v *version.Version) {
(*audit.DataAuditResult)(r).SetVersion(v.ToV2())
}
// AuditEpoch returns epoch number when the Data Audit was conducted.
func (r *Result) AuditEpoch() uint64 {
return (*audit.DataAuditResult)(r).GetAuditEpoch()
}
// SetAuditEpoch sets epoch number when the Data Audit was conducted.
func (r *Result) SetAuditEpoch(epoch uint64) {
(*audit.DataAuditResult)(r).SetAuditEpoch(epoch)
}
// ContainerID returns container under audit.
func (r *Result) ContainerID() *cid.ID {
return cid.NewFromV2(
(*audit.DataAuditResult)(r).GetContainerID())
}
// SetContainerID sets container under audit.
func (r *Result) SetContainerID(id *cid.ID) {
(*audit.DataAuditResult)(r).SetContainerID(id.ToV2())
}
// 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
}
m := make([]oid.ID, len(mV2))
for i := range mV2 {
m[i] = *oid.NewIDFromV2(&mV2[i])
}
return m
}
// SetPassSG sets list of Storage Groups that passed audit PoR stage.
func (r *Result) SetPassSG(list []oid.ID) {
mV2 := (*audit.DataAuditResult)(r).
GetPassSG()
if list == nil {
mV2 = nil
} else {
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).SetPassSG(mV2)
}
// FailSG returns list of Storage Groups that failed audit PoR stage.
func (r *Result) FailSG() []oid.ID {
mV2 := (*audit.DataAuditResult)(r).
GetFailSG()
if mV2 == nil {
return nil
}
m := make([]oid.ID, len(mV2))
for i := range mV2 {
m[i] = *oid.NewIDFromV2(&mV2[i])
}
return m
}
// SetFailSG sets list of Storage Groups that failed audit PoR stage.
func (r *Result) SetFailSG(list []oid.ID) {
mV2 := (*audit.DataAuditResult)(r).
GetFailSG()
if list == nil {
mV2 = nil
} else {
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)
}
// Hit returns number of sampled objects under audit placed
// in an optimal way according to the containers placement policy
// when checking PoP.
func (r *Result) Hit() uint32 {
return (*audit.DataAuditResult)(r).GetHit()
}
// SetHit sets number of sampled objects under audit placed
// in an optimal way according to the containers placement policy
// when checking PoP.
func (r *Result) SetHit(hit uint32) {
(*audit.DataAuditResult)(r).SetHit(hit)
}
// Miss returns 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) Miss() uint32 {
return (*audit.DataAuditResult)(r).GetMiss()
}
// 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
// when checking PoP.
func (r *Result) Fail() uint32 {
return (*audit.DataAuditResult)(r).GetFail()
}
// SetFail sets number of sampled objects under audit stored
// in a way not confirming placement policy or not found at all
// when checking PoP.
func (r *Result) SetFail(fail uint32) {
(*audit.DataAuditResult)(r).SetFail(fail)
}
// PassNodes returns list of storage node public keys that
// passed at least one PDP.
func (r *Result) PassNodes() [][]byte {
return (*audit.DataAuditResult)(r).GetPassNodes()
}
// SetPassNodes sets list of storage node public keys that
// passed at least one PDP.
func (r *Result) SetPassNodes(list [][]byte) {
(*audit.DataAuditResult)(r).SetPassNodes(list)
}
// FailNodes returns list of storage node public keys that
// failed at least one PDP.
func (r *Result) FailNodes() [][]byte {
return (*audit.DataAuditResult)(r).GetFailNodes()
}
// SetFailNodes sets list of storage node public keys that
// failed at least one PDP.
func (r *Result) SetFailNodes(list [][]byte) {
(*audit.DataAuditResult)(r).SetFailNodes(list)
}

154
audit/result_test.go Normal file
View file

@ -0,0 +1,154 @@
package audit_test
import (
"testing"
auditv2 "github.com/nspcc-dev/neofs-api-go/v2/audit"
"github.com/nspcc-dev/neofs-sdk-go/audit"
audittest "github.com/nspcc-dev/neofs-sdk-go/audit/test"
cidtest "github.com/nspcc-dev/neofs-sdk-go/container/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"
)
func TestResult(t *testing.T) {
r := audit.NewResult()
require.Equal(t, version.Current(), r.Version())
epoch := uint64(13)
r.SetAuditEpoch(epoch)
require.Equal(t, epoch, r.AuditEpoch())
cid := cidtest.ID()
r.SetContainerID(cid)
require.Equal(t, cid, r.ContainerID())
key := []byte{1, 2, 3}
r.SetPublicKey(key)
require.Equal(t, key, r.PublicKey())
r.SetComplete(true)
require.True(t, r.Complete())
requests := uint32(2)
r.SetRequests(requests)
require.Equal(t, requests, r.Requests())
retries := uint32(1)
r.SetRetries(retries)
require.Equal(t, retries, r.Retries())
passSG := []oid.ID{*oidtest.ID(), *oidtest.ID()}
r.SetPassSG(passSG)
require.Equal(t, passSG, r.PassSG())
failSG := []oid.ID{*oidtest.ID(), *oidtest.ID()}
r.SetFailSG(failSG)
require.Equal(t, failSG, r.FailSG())
hit := uint32(1)
r.SetHit(hit)
require.Equal(t, hit, r.Hit())
miss := uint32(2)
r.SetMiss(miss)
require.Equal(t, miss, r.Miss())
fail := uint32(3)
r.SetFail(fail)
require.Equal(t, fail, r.Fail())
passNodes := [][]byte{{1}, {2}}
r.SetPassNodes(passNodes)
require.Equal(t, passNodes, r.PassNodes())
failNodes := [][]byte{{3}, {4}}
r.SetFailNodes(failNodes)
require.Equal(t, failNodes, r.FailNodes())
}
func TestStorageGroupEncoding(t *testing.T) {
r := audittest.Result()
t.Run("binary", func(t *testing.T) {
data, err := r.Marshal()
require.NoError(t, err)
r2 := audit.NewResult()
require.NoError(t, r2.Unmarshal(data))
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))
})
}

37
audit/test/generate.go Normal file
View file

@ -0,0 +1,37 @@
package audittest
import (
"github.com/nspcc-dev/neofs-sdk-go/audit"
cidtest "github.com/nspcc-dev/neofs-sdk-go/container/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.
func Result() *audit.Result {
x := audit.NewResult()
x.SetVersion(versiontest.Version())
x.SetContainerID(cidtest.ID())
x.SetPublicKey([]byte("key"))
x.SetComplete(true)
x.SetAuditEpoch(44)
x.SetHit(55)
x.SetMiss(66)
x.SetFail(77)
x.SetRetries(88)
x.SetRequests(99)
x.SetFailNodes([][]byte{
[]byte("node1"),
[]byte("node2"),
})
x.SetPassNodes([][]byte{
[]byte("node3"),
[]byte("node4"),
})
x.SetPassSG([]oid.ID{*oidtest.ID(), *oidtest.ID()})
x.SetFailSG([]oid.ID{*oidtest.ID(), *oidtest.ID()})
return x
}

View file

@ -1,497 +0,0 @@
package bearer
import (
"crypto/ecdsa"
"errors"
"fmt"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl"
apeV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/ape"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
apeSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/ape"
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
apeOverrideSet bool
apeOverride APEOverride
impersonate bool
}
// APEOverride is the list of APE chains defined for a target.
// These chains are meant to serve as overrides to the already defined (or even undefined)
// APE chains for the target (see contract `Policy`).
//
// The server-side processing of the bearer token with set APE overrides must verify if a client is permitted
// to override chains for the target, preventing unauthorized access through the APE mechanism.
type APEOverride struct {
// Target for which chains are applied.
Target apeSDK.ChainTarget
// The list of APE chains.
Chains []apeSDK.Chain
}
// Marshal marshals APEOverride into a protobuf binary form.
func (c *APEOverride) Marshal() ([]byte, error) {
return c.ToV2().StableMarshal(nil), nil
}
// Unmarshal unmarshals protobuf binary representation of APEOverride.
func (c *APEOverride) Unmarshal(data []byte) error {
overrideV2 := new(acl.APEOverride)
if err := overrideV2.Unmarshal(data); err != nil {
return err
}
return c.FromV2(overrideV2)
}
// MarshalJSON encodes APEOverride to protobuf JSON format.
func (c *APEOverride) MarshalJSON() ([]byte, error) {
return c.ToV2().MarshalJSON()
}
// UnmarshalJSON decodes APEOverride from protobuf JSON format.
func (c *APEOverride) UnmarshalJSON(data []byte) error {
overrideV2 := new(acl.APEOverride)
if err := overrideV2.UnmarshalJSON(data); err != nil {
return err
}
return c.FromV2(overrideV2)
}
func (c *APEOverride) FromV2(tokenAPEChains *acl.APEOverride) error {
c.Target.FromV2(tokenAPEChains.GetTarget())
if chains := tokenAPEChains.GetChains(); len(chains) > 0 {
c.Chains = make([]apeSDK.Chain, len(chains))
for i := range chains {
if err := c.Chains[i].ReadFromV2(chains[i]); err != nil {
return fmt.Errorf("invalid APE chain: %w", err)
}
}
}
return nil
}
func (c *APEOverride) ToV2() *acl.APEOverride {
if c == nil {
return nil
}
apeOverride := new(acl.APEOverride)
apeOverride.SetTarget(c.Target.ToV2())
chains := make([]*apeV2.Chain, len(c.Chains))
for i := range c.Chains {
chains[i] = c.Chains[i].ToV2()
}
apeOverride.SetChains(chains)
return apeOverride
}
// 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")
}
b.impersonate = body.GetImpersonate()
apeOverrides := body.GetAPEOverride()
eaclTable := body.GetEACL()
if b.eaclTableSet = eaclTable != nil; b.eaclTableSet {
b.eaclTable = *eacl.NewTableFromV2(eaclTable)
} else if checkFieldPresence && !b.impersonate && apeOverrides == nil {
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")
}
if b.apeOverrideSet = apeOverrides != nil; b.apeOverrideSet {
if err = b.apeOverride.FromV2(apeOverrides); err != nil {
return err
}
} else if checkFieldPresence && !b.impersonate && !b.eaclTableSet {
return errors.New("missing APE override")
}
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 && !b.impersonate && !b.apeOverrideSet {
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)
}
if b.apeOverrideSet {
body.SetAPEOverride(b.apeOverride.ToV2())
}
body.SetImpersonate(b.impersonate)
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{}
}
// SetAPEOverride sets APE override to the bearer token.
//
// See also: APEOverride.
func (b *Token) SetAPEOverride(v APEOverride) {
b.apeOverride = v
b.apeOverrideSet = true
}
// APEOverride returns APE override set by SetAPEOverride.
//
// Zero Token has zero APEOverride.
func (b *Token) APEOverride() APEOverride {
if b.apeOverrideSet {
return b.apeOverride
}
return APEOverride{}
}
// SetImpersonate mark token as impersonate to consider token signer as request owner.
// If this field is true extended EACLTable in token body isn't processed.
func (b *Token) SetImpersonate(v bool) {
b.impersonate = v
}
// Impersonate returns true if token is impersonated.
func (b Token) Impersonate() bool {
return b.impersonate
}
// 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,447 +0,0 @@
package bearer_test
import (
"bytes"
"math/rand"
"reflect"
"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_SetAPEOverrides(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.APEOverride())
val2 = filled
jd, err := val.MarshalJSON()
require.NoError(t, err)
require.NoError(t, val2.UnmarshalJSON(jd))
require.Zero(t, val2.APEOverride())
// set value
tApe := bearertest.APEOverride()
val.SetAPEOverride(tApe)
require.Equal(t, tApe, val.APEOverride())
val.WriteToV2(&m)
require.NotNil(t, m.GetBody().GetAPEOverride())
require.True(t, tokenAPEOverridesEqual(tApe.ToV2(), m.GetBody().GetAPEOverride()))
val2 = filled
require.NoError(t, val2.Unmarshal(val.Marshal()))
apeOverride := val2.APEOverride()
require.True(t, tokenAPEOverridesEqual(tApe.ToV2(), apeOverride.ToV2()))
val2 = filled
jd, err = val.MarshalJSON()
require.NoError(t, err)
require.NoError(t, val2.UnmarshalJSON(jd))
apeOverride = val.APEOverride()
require.True(t, tokenAPEOverridesEqual(tApe.ToV2(), apeOverride.ToV2()))
}
func tokenAPEOverridesEqual(lhs, rhs *acl.APEOverride) bool {
return reflect.DeepEqual(lhs, rhs)
}
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))
body.SetEACL(nil)
body.SetImpersonate(true)
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,32 +0,0 @@
package bearertest
import (
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/ape"
"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())
t.SetAPEOverride(APEOverride())
return t
}
func APEOverride() bearer.APEOverride {
return bearer.APEOverride{
Target: ape.ChainTarget{
TargetType: ape.TargetTypeContainer,
Name: "F8JsMnChywiPvbDvpxMbjTjx5KhWHHp6gCDt8BhzL9kF",
},
Chains: []ape.Chain{{Raw: []byte("{}")}},
}
}

View file

@ -1,78 +1,51 @@
package checksum
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
"git.frostfs.info/TrueCloudLab/tzhash/tz"
"github.com/nspcc-dev/neofs-api-go/v2/refs"
)
// Checksum represents checksum of some digital data.
//
// 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
// Checksum represents v2-compatible checksum.
type Checksum refs.Checksum
// Type represents the enumeration
// of checksum types.
type Type refs.ChecksumType
type Type uint8
const (
// Unknown is an undefined checksum type.
Unknown Type = Type(refs.UnknownChecksum)
Unknown Type = iota
// SHA256 is a SHA256 checksum type.
SHA256 = Type(refs.SHA256)
SHA256
// TZ is a Tillich-Zémor checksum type.
TZ = Type(refs.TillichZemor)
// TZ is a Tillich-Zemor checksum type.
TZ
)
// ReadFromV2 reads Checksum from the refs.Checksum message. Checks if the
// message conforms to FrostFS API V2 protocol.
// NewFromV2 wraps v2 Checksum message to Checksum.
//
// See also WriteToV2.
func (c *Checksum) ReadFromV2(m refs.Checksum) error {
if len(m.GetSum()) == 0 {
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
// Nil refs.Checksum converts to nil.
func NewFromV2(cV2 *refs.Checksum) *Checksum {
return (*Checksum)(cV2)
}
// WriteToV2 writes Checksum to the refs.Checksum message.
// The message must not be nil.
// New creates and initializes blank Checksum.
//
// See also ReadFromV2.
func (c Checksum) WriteToV2(m *refs.Checksum) {
*m = (refs.Checksum)(c)
// Defaults:
// - sum: nil;
// - type: Unknown.
func New() *Checksum {
return NewFromV2(new(refs.Checksum))
}
// Type returns checksum type.
//
// Zero Checksum has Unknown checksum type.
//
// See also SetTillichZemor and SetSHA256.
func (c Checksum) Type() Type {
v2 := (refs.Checksum)(c)
switch v2.GetType() {
func (c *Checksum) Type() Type {
switch (*refs.Checksum)(c).GetType() {
case refs.SHA256:
return SHA256
case refs.TillichZemor:
@ -82,70 +55,93 @@ func (c Checksum) Type() Type {
}
}
// Value returns checksum bytes. Return value
// MUST NOT be mutated.
//
// Zero Checksum has nil sum.
//
// See also SetTillichZemor and SetSHA256.
func (c Checksum) Value() []byte {
v2 := (refs.Checksum)(c)
return v2.GetSum()
// Sum returns checksum bytes.
func (c *Checksum) Sum() []byte {
return (*refs.Checksum)(c).GetSum()
}
// SetSHA256 sets checksum to SHA256 hash.
//
// See also Calculate.
func (c *Checksum) SetSHA256(v [sha256.Size]byte) {
v2 := (*refs.Checksum)(c)
checksum := (*refs.Checksum)(c)
v2.SetType(refs.SHA256)
v2.SetSum(v[:])
checksum.SetType(refs.SHA256)
checksum.SetSum(v[:])
}
// Calculate calculates checksum and sets it
// to the passed checksum. Checksum must not be nil.
// SetTillichZemor sets checksum to Tillich-Zemor hash.
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:
// - SHA256;
// - TZ.
//
// Does not mutate the passed value.
//
// See also SetSHA256, SetTillichZemor.
func Calculate(c *Checksum, t Type, v []byte) {
switch t {
case SHA256:
c.SetSHA256(sha256.Sum256(v))
case TZ:
c.SetTillichZemor(tz.Sum(v))
default:
// Nil Checksum converts to nil.
func (c *Checksum) ToV2() *refs.Checksum {
return (*refs.Checksum)(c)
}
func Equal(cs1, cs2 *Checksum) bool {
return cs1.Type() == cs2.Type() && bytes.Equal(cs1.Sum(), cs2.Sum())
}
// Marshal marshals Checksum into a protobuf binary form.
func (c *Checksum) Marshal() ([]byte, error) {
return (*refs.Checksum)(c).StableMarshal(nil)
}
// 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.
func (c *Checksum) SetTillichZemor(v [tz.Size]byte) {
v2 := (*refs.Checksum)(c)
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.
// String mapping:
// * TZ: TZ;
// * SHA256: SHA256;
// * Unknown, default: CHECKSUM_TYPE_UNSPECIFIED.
func (m Type) String() string {
var m2 refs.ChecksumType
@ -160,3 +156,26 @@ func (m Type) String() 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"
"testing"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
"git.frostfs.info/TrueCloudLab/tzhash/tz"
"github.com/nspcc-dev/neofs-api-go/v2/refs"
"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) {
var c Checksum
c := New()
cSHA256 := [sha256.Size]byte{}
_, _ = rand.Read(cSHA256[:])
@ -19,62 +26,150 @@ func TestChecksum(t *testing.T) {
c.SetSHA256(cSHA256)
require.Equal(t, SHA256, c.Type())
require.Equal(t, cSHA256[:], c.Value())
require.Equal(t, cSHA256[:], c.Sum())
var cV2 refs.Checksum
c.WriteToV2(&cV2)
cV2 := c.ToV2()
require.Equal(t, refs.SHA256, cV2.GetType())
require.Equal(t, cSHA256[:], cV2.GetSum())
cTZ := [tz.Size]byte{}
cTZ := [64]byte{}
_, _ = rand.Read(cSHA256[:])
c.SetTillichZemor(cTZ)
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, 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) {
t.Run("default values", func(t *testing.T) {
var chs Checksum
chs := New()
// check initial values
require.Equal(t, Unknown, chs.Type())
require.Nil(t, chs.Value())
require.Nil(t, chs.Sum())
// convert to v2 message
var chsV2 refs.Checksum
chs.WriteToV2(&chsV2)
chsV2 := chs.ToV2()
require.Equal(t, refs.UnknownChecksum, chsV2.GetType())
require.Nil(t, chsV2.GetSum())
})
}
func TestCalculation(t *testing.T) {
var c Checksum
payload := []byte{0, 1, 2, 3, 4, 5}
type enumIface interface {
FromString(string) bool
String() string
}
t.Run("SHA256", func(t *testing.T) {
orig := sha256.Sum256(payload)
type enumStringItem struct {
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) {
orig := tz.Sum(payload)
require.True(t, e.FromString(s), s)
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/rand"
"crypto/sha256"
"fmt"
"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

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

View file

@ -2,107 +2,103 @@ package client
import (
"context"
"fmt"
v2accounting "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/accounting"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/accounting"
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
v2accounting "github.com/nspcc-dev/neofs-api-go/v2/accounting"
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
"github.com/nspcc-dev/neofs-sdk-go/accounting"
"github.com/nspcc-dev/neofs-sdk-go/owner"
)
// PrmBalanceGet groups parameters of BalanceGet operation.
type PrmBalanceGet struct {
XHeaders []string
prmCommonMeta
Account user.ID
ownerSet bool
ownerID owner.ID
}
// SetAccount sets identifier of the FrostFS account for which the balance is requested.
// Required parameter.
//
// Deprecated: Use PrmBalanceGet.Account instead.
func (x *PrmBalanceGet) SetAccount(id user.ID) {
x.Account = id
}
func (x *PrmBalanceGet) buildRequest(c *Client) (*v2accounting.BalanceRequest, error) {
if x.Account.IsEmpty() {
return nil, errorAccountNotSet
}
var accountV2 refs.OwnerID
x.Account.WriteToV2(&accountV2)
var body v2accounting.BalanceRequestBody
body.SetOwnerID(&accountV2)
var req v2accounting.BalanceRequest
req.SetBody(&body)
c.prepareRequest(&req, new(v2session.RequestMetaHeader))
return &req, nil
// SetAccount sets identifier of the NeoFS account for which the balance is requested.
// Required parameter. Must be a valid ID according to NeoFS API protocol.
func (x *PrmBalanceGet) SetAccount(id owner.ID) {
x.ownerID = id
x.ownerSet = true
}
// ResBalanceGet groups resulting values of BalanceGet operation.
type ResBalanceGet struct {
statusRes
amount accounting.Decimal
amount *accounting.Decimal
}
// Amount returns current amount of funds on the FrostFS account as decimal number.
func (x ResBalanceGet) Amount() accounting.Decimal {
func (x *ResBalanceGet) setAmount(v *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
}
// 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.
// Any client's internal or transport errors are returned as `error`,
// If PrmInit.DisableFrostFSFailuresResolution has been called, unsuccessful
// FrostFS status codes are included in the returned result structure,
// otherwise, are also returned as `error`.
// If WithNeoFSErrorParsing option has been provided, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included
// 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.
//
// Return statuses:
// - global (see Client docs).
// - global (see Client docs).
func (c *Client) BalanceGet(ctx context.Context, prm PrmBalanceGet) (*ResBalanceGet, error) {
req, err := prm.buildRequest(c)
if err != nil {
return nil, err
switch {
case ctx == nil:
panic(panicMsgMissingContext)
case !prm.ownerSet:
panic("account not set")
case !prm.ownerID.Valid():
panic("invalid account ID")
}
if err := signature.SignServiceMessage(&c.prm.Key, req); err != nil {
return nil, fmt.Errorf("sign request: %w", err)
// form request body
var body v2accounting.BalanceRequestBody
body.SetOwnerID(prm.ownerID.ToV2())
// form request
var req v2accounting.BalanceRequest
req.SetBody(&body)
// init call context
var (
cc contextCall
res ResBalanceGet
)
c.initCallContext(&cc)
cc.meta = prm.prmCommonMeta
cc.req = &req
cc.statusRes = &res
cc.call = func() (responseV2, error) {
return rpcapi.Balance(&c.c, &req, client.WithContext(ctx))
}
cc.result = func(r responseV2) {
resp := r.(*v2accounting.BalanceResponse)
res.setAmount(accounting.NewDecimalFromV2(resp.GetBody().GetBalance()))
}
resp, err := rpcapi.Balance(&c.c, req, client.WithContext(ctx))
if err != nil {
return nil, err
// process call
if !cc.processCall() {
return nil, cc.err
}
var res ResBalanceGet
res.st, err = c.processResponse(resp)
if err != nil || !apistatus.IsSuccessful(res.st) {
return &res, err
}
const fieldBalance = "balance"
bal := resp.GetBody().GetBalance()
if bal == nil {
return &res, newErrMissingResponseField(fieldBalance)
}
if err := res.amount.ReadFromV2(*bal); err != nil {
return &res, newErrInvalidResponseField(fieldBalance, err)
}
return &res, nil
}

View file

@ -1,79 +0,0 @@
package client
import (
"context"
"fmt"
apemanagerV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/apemanager"
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
sessionV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
apeSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/ape"
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
)
// PrmAPEManagerAddChain groups parameters of APEManagerAddChain operation.
type PrmAPEManagerAddChain struct {
XHeaders []string
ChainTarget apeSDK.ChainTarget
Chain apeSDK.Chain
}
func (prm *PrmAPEManagerAddChain) buildRequest(c *Client) (*apemanagerV2.AddChainRequest, error) {
if len(prm.XHeaders)%2 != 0 {
return nil, errorInvalidXHeaders
}
req := new(apemanagerV2.AddChainRequest)
reqBody := new(apemanagerV2.AddChainRequestBody)
reqBody.SetTarget(prm.ChainTarget.ToV2())
reqBody.SetChain(prm.Chain.ToV2())
req.SetBody(reqBody)
var meta sessionV2.RequestMetaHeader
writeXHeadersToMeta(prm.XHeaders, &meta)
c.prepareRequest(req, &meta)
return req, nil
}
type ResAPEManagerAddChain struct {
statusRes
// ChainID of set Chain. If Chain does not contain chainID before request, then
// ChainID is generated.
ChainID apeSDK.ChainID
}
// APEManagerAddChain sets Chain for ChainTarget.
func (c *Client) APEManagerAddChain(ctx context.Context, prm PrmAPEManagerAddChain) (*ResAPEManagerAddChain, error) {
req, err := prm.buildRequest(c)
if err != nil {
return nil, err
}
if err := signature.SignServiceMessage(&c.prm.Key, req); err != nil {
return nil, fmt.Errorf("sign request: %w", err)
}
resp, err := rpcapi.AddChain(&c.c, req, client.WithContext(ctx))
if err != nil {
return nil, err
}
var res ResAPEManagerAddChain
res.st, err = c.processResponse(resp)
if err != nil || !apistatus.IsSuccessful(res.st) {
return &res, err
}
res.ChainID = resp.GetBody().GetChainID()
return &res, nil
}

View file

@ -1,80 +0,0 @@
package client
import (
"context"
"fmt"
apemanagerV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/apemanager"
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
sessionV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
apeSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/ape"
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
)
// PrmAPEManagerListChains groups parameters of APEManagerListChains operation.
type PrmAPEManagerListChains struct {
XHeaders []string
ChainTarget apeSDK.ChainTarget
}
func (prm *PrmAPEManagerListChains) buildRequest(c *Client) (*apemanagerV2.ListChainsRequest, error) {
if len(prm.XHeaders)%2 != 0 {
return nil, errorInvalidXHeaders
}
req := new(apemanagerV2.ListChainsRequest)
reqBody := new(apemanagerV2.ListChainsRequestBody)
reqBody.SetTarget(prm.ChainTarget.ToV2())
req.SetBody(reqBody)
var meta sessionV2.RequestMetaHeader
writeXHeadersToMeta(prm.XHeaders, &meta)
c.prepareRequest(req, &meta)
return req, nil
}
type ResAPEManagerListChains struct {
statusRes
Chains []apeSDK.Chain
}
// APEManagerListChains lists Chains for ChainTarget.
func (c *Client) APEManagerListChains(ctx context.Context, prm PrmAPEManagerListChains) (*ResAPEManagerListChains, error) {
req, err := prm.buildRequest(c)
if err != nil {
return nil, err
}
if err := signature.SignServiceMessage(&c.prm.Key, req); err != nil {
return nil, fmt.Errorf("sign request: %w", err)
}
resp, err := rpcapi.ListChains(&c.c, req, client.WithContext(ctx))
if err != nil {
return nil, err
}
var res ResAPEManagerListChains
res.st, err = c.processResponse(resp)
if err != nil || !apistatus.IsSuccessful(res.st) {
return nil, err
}
for _, ch := range resp.GetBody().GetChains() {
var chSDK apeSDK.Chain
if err := chSDK.ReadFromV2(ch); err != nil {
return nil, err
}
res.Chains = append(res.Chains, chSDK)
}
return &res, nil
}

View file

@ -1,73 +0,0 @@
package client
import (
"context"
"fmt"
apemanagerV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/apemanager"
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
sessionV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
apeSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/ape"
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
)
// PrmAPEManagerRemoveChain groups parameters of APEManagerRemoveChain operation.
type PrmAPEManagerRemoveChain struct {
XHeaders []string
ChainTarget apeSDK.ChainTarget
ChainID apeSDK.ChainID
}
func (prm *PrmAPEManagerRemoveChain) buildRequest(c *Client) (*apemanagerV2.RemoveChainRequest, error) {
if len(prm.XHeaders)%2 != 0 {
return nil, errorInvalidXHeaders
}
req := new(apemanagerV2.RemoveChainRequest)
reqBody := new(apemanagerV2.RemoveChainRequestBody)
reqBody.SetTarget(prm.ChainTarget.ToV2())
reqBody.SetChainID(prm.ChainID)
req.SetBody(reqBody)
var meta sessionV2.RequestMetaHeader
writeXHeadersToMeta(prm.XHeaders, &meta)
c.prepareRequest(req, &meta)
return req, nil
}
type ResAPEManagerRemoveChain struct {
statusRes
}
// APEManagerRemoveChain removes Chain with ChainID defined for ChainTarget.
func (c *Client) APEManagerRemoveChain(ctx context.Context, prm PrmAPEManagerRemoveChain) (*ResAPEManagerRemoveChain, error) {
req, err := prm.buildRequest(c)
if err != nil {
return nil, err
}
if err := signature.SignServiceMessage(&c.prm.Key, req); err != nil {
return nil, fmt.Errorf("sign request: %w", err)
}
resp, err := rpcapi.RemoveChain(&c.c, req, client.WithContext(ctx))
if err != nil {
return nil, err
}
var res ResAPEManagerRemoveChain
res.st, err = c.processResponse(resp)
if err != nil || !apistatus.IsSuccessful(res.st) {
return &res, err
}
return &res, nil
}

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,26 +1,23 @@
package client
import (
"context"
"crypto/ecdsa"
"crypto/tls"
"errors"
"time"
v2accounting "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/accounting"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
"google.golang.org/grpc"
v2accounting "github.com/nspcc-dev/neofs-api-go/v2/accounting"
"github.com/nspcc-dev/neofs-api-go/v2/rpc"
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
)
// Client represents virtual connection to the FrostFS network to communicate
// with FrostFS server using FrostFS API protocol. It is designed to provide
// Client represents virtual connection to the NeoFS network to communicate
// with NeoFS server using NeoFS API protocol. It is designed to provide
// 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
// 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
// to the mandatory parameters). Using the Client before connecting have
// been established can lead to a panic. After the work, the Client SHOULD BE
@ -29,7 +26,7 @@ import (
// during the communication process step strongly discouraged as it leads to
// 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
// to built-in error instance (or in the returned error if the client is
// configured accordingly). Certain statuses can be checked using `apistatus`
@ -37,9 +34,8 @@ import (
// functions to work with status returns (e.g. IsErrContainerNotFound).
// 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):
// - *apistatus.ServerInternal on internal server error;
// - *apistatus.NodeUnderMaintenance if a server is under maintenance;
// - *apistatus.SuccessDefaultV2 on default success.
// - *apistatus.ServerInternal on internal server error;
// - *apistatus.SuccessDefaultV2 on default success.
//
// Client MUST NOT be copied by value: use pointer to Client instead.
//
@ -48,8 +44,6 @@ type Client struct {
prm PrmInit
c client.Client
server frostFSAPIServer
}
// Init brings the Client instance to its initial state.
@ -62,63 +56,42 @@ func (c *Client) Init(prm PrmInit) {
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
// SHOULD NOT be used.
//
// Uses the context specified by SetContext if it was called with non-nil
// 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
// Panics if required parameters are set incorrectly, look carefully
// at the method documentation.
//
// One-time method call during application start-up stage (after Init ) is expected.
// Calling multiple times leads to undefined behavior.
//
// See also Init / Close.
func (c *Client) Dial(ctx context.Context, prm PrmDial) error {
if prm.Endpoint == "" {
return errorServerAddrUnset
func (c *Client) Dial(prm PrmDial) error {
if prm.endpoint == "" {
panic("server address is unset or empty")
}
if prm.DialTimeout <= 0 {
prm.DialTimeout = defaultDialTimeout
}
if prm.StreamTimeout <= 0 {
prm.StreamTimeout = defaultStreamTimeout
if prm.timeoutDialSet {
if prm.timeoutDial <= 0 {
panic("non-positive timeout")
}
} else {
prm.timeoutDial = 5 * time.Second
}
c.c = *client.New(append(
client.WithNetworkURIAddress(prm.Endpoint, prm.TLSConfig),
client.WithDialTimeout(prm.DialTimeout),
client.WithRWTimeout(prm.StreamTimeout),
client.WithGRPCDialOptions(prm.GRPCDialOptions),
client.WithNetworkURIAddress(prm.endpoint, prm.tlsConfig),
client.WithDialTimeout(prm.timeoutDial),
)...)
c.setFrostFSAPIServer((*coreServer)(&c.c))
// TODO: (neofs-api-go#382) perform generic dial stage of the client.Client
_, err := rpc.Balance(&c.c, new(v2accounting.BalanceRequest),
client.WithContext(ctx),
)
// return context errors since they signal about dial problem
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return err
}
_, _ = rpc.Balance(&c.c, new(v2accounting.BalanceRequest))
return nil
}
// sets underlying provider of frostFSAPIServer. The method is used for testing as an approach
// 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.
// Close closes underlying connection to the NeoFS server. Implements io.Closer.
// MUST NOT be called before successful Dial. Can be called concurrently
// with server operations processing on running goroutines: in this case
// they are likely to fail due to a connection error.
@ -135,117 +108,75 @@ func (c *Client) Close() error {
//
// See also Init.
type PrmInit struct {
DisableFrostFSErrorResolution bool
resolveNeoFSErrors bool
Key ecdsa.PrivateKey
key ecdsa.PrivateKey
ResponseInfoCallback func(ResponseMetaInfo) error
cbRespInfo func(ResponseMetaInfo) error
NetMagic uint64
netMagic uint64
}
// SetDefaultPrivateKey sets Client private key to be used for the protocol
// communication by default.
//
// Required for operations without custom key parametrization (see corresponding Prm* docs).
//
// Deprecated: Use PrmInit.Key instead.
func (x *PrmInit) SetDefaultPrivateKey(key ecdsa.PrivateKey) {
x.Key = key
x.key = key
}
// Deprecated: method is no-op. Option is default.
func (x *PrmInit) ResolveFrostFSFailures() {
}
// DisableFrostFSFailuresResolution makes the Client to preserve failure statuses of the
// FrostFS protocol only in resulting structure (see corresponding Res* docs).
// These errors are returned from each protocol operation. By default, statuses
// are resolved and returned as a Go built-in errors.
//
// Deprecated: Use PrmInit.DisableFrostFSErrorResolution instead.
func (x *PrmInit) DisableFrostFSFailuresResolution() {
x.DisableFrostFSErrorResolution = true
// ResolveNeoFSFailures makes the Client to resolve failure statuses of the
// NeoFS protocol into Go built-in errors. These errors are returned from
// each protocol operation. By default, statuses aren't resolved and written
// to the resulting structure (see corresponding Res* docs).
func (x *PrmInit) ResolveNeoFSFailures() {
x.resolveNeoFSErrors = true
}
// SetResponseInfoCallback makes the Client to pass ResponseMetaInfo from each
// FrostFS server response to f. Nil (default) means ignore response meta info.
//
// Deprecated: Use PrmInit.ResponseInfoCallback instead.
// NeoFS server response to f. Nil (default) means ignore response meta info.
func (x *PrmInit) SetResponseInfoCallback(f func(ResponseMetaInfo) error) {
x.ResponseInfoCallback = f
x.cbRespInfo = f
}
const (
defaultDialTimeout = 5 * time.Second
defaultStreamTimeout = 10 * time.Second
)
// PrmDial groups connection parameters for the Client.
//
// See also Dial.
type PrmDial struct {
Endpoint string
endpoint string
TLSConfig *tls.Config
tlsConfig *tls.Config
// If DialTimeout is non-positive, then it's set to defaultDialTimeout.
DialTimeout time.Duration
// If StreamTimeout is non-positive, then it's set to defaultStreamTimeout.
StreamTimeout time.Duration
GRPCDialOptions []grpc.DialOption
timeoutDialSet bool
timeoutDial time.Duration
}
// SetServerURI sets server URI in the FrostFS network.
// SetServerURI sets server URI in the NeoFS network.
// Required parameter.
//
// Format of the URI:
//
// [scheme://]host:port
// [scheme://]host:port
//
// Supported schemes:
//
// grpc
// grpcs
// grpc
// grpcs
//
// See also SetTLSConfig.
//
// Deprecated: Use PrmDial.Endpoint instead.
func (x *PrmDial) SetServerURI(endpoint string) {
x.Endpoint = endpoint
x.endpoint = endpoint
}
// 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.
//
// Depreacted: Use PrmDial.TLSConfig instead.
func (x *PrmDial) SetTLSConfig(tlsConfig *tls.Config) {
x.TLSConfig = tlsConfig
x.tlsConfig = tlsConfig
}
// SetTimeout sets the timeout for connection to be established.
// MUST BE positive. If not called, 5s timeout will be used by default.
//
// Deprecated: Use PrmDial.DialTimeout instead.
func (x *PrmDial) SetTimeout(timeout time.Duration) {
x.DialTimeout = timeout
}
// SetStreamTimeout sets the timeout for individual operations in streaming RPC.
// MUST BE positive. If not called, 10s timeout will be used by default.
//
// Deprecated: Use PrmDial.StreamTimeout instead.
func (x *PrmDial) SetStreamTimeout(timeout time.Duration) {
x.StreamTimeout = timeout
}
// SetGRPCDialOptions sets the gRPC dial options for new gRPC client connection.
//
// Deprecated: Use PrmDial.GRPCDialOptions instead.
func (x *PrmDial) SetGRPCDialOptions(opts ...grpc.DialOption) {
x.GRPCDialOptions = opts
x.timeoutDialSet = true
x.timeoutDial = timeout
}

View file

@ -1,67 +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 {
prm := PrmInit{
Key: *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
prm := PrmDial{
Endpoint: "localhost:8080",
}
assert := func(ctx context.Context, errExpected error) {
// expect particular context error according to Dial docs
require.ErrorIs(t, c.Dial(ctx, 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

@ -1,22 +1,32 @@
package client
import (
"errors"
"crypto/ecdsa"
"fmt"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/version"
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
"github.com/nspcc-dev/neofs-api-go/v2/signature"
apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
"github.com/nspcc-dev/neofs-sdk-go/session"
"github.com/nspcc-dev/neofs-sdk-go/version"
)
// common interface of resulting structures with API status.
type resCommon interface {
setStatus(apistatus.Status)
}
// structure is embedded to all resulting types in order to inherit status-related methods.
type statusRes struct {
st apistatus.Status
}
// setStatus implements resCommon interface method.
func (x *statusRes) setStatus(st apistatus.Status) {
x.st = st
}
// Status returns server's status return.
//
// Use apistatus package functionality to handle the status.
@ -24,89 +34,287 @@ func (x statusRes) Status() apistatus.Status {
return x.st
}
func writeXHeadersToMeta(xHeaders []string, h *v2session.RequestMetaHeader) {
if len(xHeaders) == 0 {
return
}
type prmSession struct {
tokenSessionSet bool
tokenSession session.Token
}
// TODO (aarifullin): remove the panic when all client parameters will check XHeaders
// within buildRequest invocation.
if len(xHeaders)%2 != 0 {
// 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.
type prmCommonMeta struct {
// NeoFS request X-Headers
xHeaders []string
}
// 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 *prmCommonMeta) WithXHeaders(hs ...string) {
if len(hs)%2 != 0 {
panic("slice of X-Headers with odd length")
}
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)
x.xHeaders = hs
}
// error messages.
var (
errorMissingContainer = errors.New("missing container")
errorMissingObject = errors.New("missing object")
errorAccountNotSet = errors.New("account not set")
errorServerAddrUnset = errors.New("server address is unset or empty")
errorZeroRangeLength = errors.New("zero range length")
errorMissingRanges = errors.New("missing ranges")
errorInvalidXHeaders = errors.New("xheaders must be presented only as key-value pairs")
func (x prmCommonMeta) writeToMetaHeader(h *v2session.RequestMetaHeader) {
if len(x.xHeaders) > 0 {
hs := make([]v2session.XHeader, len(x.xHeaders)/2)
for i := 0; i < len(x.xHeaders); i += 2 {
hs[i].SetKey(x.xHeaders[i])
hs[i].SetValue(x.xHeaders[i+1])
}
h.SetXHeaders(hs)
}
}
// panic messages.
const (
panicMsgMissingContext = "missing context"
panicMsgMissingContainer = "missing container"
)
type request interface {
GetMetaHeader() *v2session.RequestMetaHeader
SetMetaHeader(*v2session.RequestMetaHeader)
SetVerificationHeader(*v2session.RequestVerificationHeader)
}
// groups all the details required to send a single request and process a response to it.
type contextCall struct {
// ==================================================
// state vars that do not require explicit initialization
func (c *Client) prepareRequest(req request, meta *v2session.RequestMetaHeader) {
ttl := meta.GetTTL()
if ttl == 0 {
ttl = 2
// final error to be returned from client method
err error
// received response
resp responseV2
// ==================================================
// shared parameters which are set uniformly on all calls
// request signing key
key ecdsa.PrivateKey
// callback prior to processing the response by the client
callbackResp func(ResponseMetaInfo) error
// if set, protocol errors will be expanded into a final error
resolveAPIFailures bool
// NeoFS network magic
netMagic uint64
// Meta parameters
meta prmCommonMeta
// ==================================================
// custom call parameters
// structure of the call result
statusRes resCommon
// request to be signed with a key and sent
req interface {
GetMetaHeader() *v2session.RequestMetaHeader
SetMetaHeader(*v2session.RequestMetaHeader)
SetVerificationHeader(*v2session.RequestVerificationHeader)
}
verV2 := meta.GetVersion()
if verV2 == nil {
verV2 = new(refs.Version)
version.Current().WriteToV2(verV2)
}
// function to send a request (unary) and receive a response
call func() (responseV2, error)
meta.SetTTL(ttl)
meta.SetVersion(verV2)
meta.SetNetworkMagic(c.prm.NetMagic)
// function to send the request (req field)
wReq func() error
req.SetMetaHeader(meta)
// function to recv the response (resp field)
rResp func() error
// function to close the message stream
closer func() error
// function of writing response fields to the resulting structure (optional)
result func(v2 responseV2)
}
// processResponse verifies response signature and converts status to an error if needed.
func (c *Client) processResponse(resp responseV2) (apistatus.Status, error) {
if c.prm.ResponseInfoCallback != nil {
rmi := ResponseMetaInfo{
key: resp.GetVerificationHeader().GetBodySignature().GetKey(),
epoch: resp.GetMetaHeader().GetEpoch(),
}
if err := c.prm.ResponseInfoCallback(rmi); err != nil {
return nil, fmt.Errorf("response callback error: %w", err)
// sets needed fields of the request meta header.
func (x contextCall) prepareRequest() {
meta := x.req.GetMetaHeader()
if meta == nil {
meta = new(v2session.RequestMetaHeader)
x.req.SetMetaHeader(meta)
}
if meta.GetTTL() == 0 {
meta.SetTTL(2)
}
if meta.GetVersion() == nil {
meta.SetVersion(version.Current().ToV2())
}
meta.SetNetworkMagic(x.netMagic)
x.meta.writeToMetaHeader(meta)
}
// prepares, signs and writes the request. Result means success.
// If failed, contextCall.err contains the reason.
func (x *contextCall) writeRequest() bool {
x.prepareRequest()
x.req.SetVerificationHeader(nil)
// sign the request
x.err = signature.SignServiceMessage(&x.key, x.req)
if x.err != nil {
x.err = fmt.Errorf("sign request: %w", x.err)
return false
}
x.err = x.wReq()
if x.err != nil {
x.err = fmt.Errorf("write request: %w", x.err)
return false
}
return true
}
// performs common actions of response processing and writes any problem as a result status or client error
// (in both cases returns false).
//
// Actions:
// * verify signature (internal);
// * call response callback (internal);
// * unwrap status error (optional).
func (x *contextCall) processResponse() bool {
// call response callback if set
if x.callbackResp != nil {
x.err = x.callbackResp(ResponseMetaInfo{
key: x.resp.GetVerificationHeader().GetBodySignature().GetKey(),
})
if x.err != nil {
x.err = fmt.Errorf("response callback error: %w", x.err)
return false
}
}
err := signature.VerifyServiceMessage(resp)
if err != nil {
return nil, fmt.Errorf("invalid response signature: %w", err)
// note that we call response callback before signature check since it is expected more lightweight
// while verification needs marshaling
// verify response signature
x.err = signature.VerifyServiceMessage(x.resp)
if x.err != nil {
x.err = fmt.Errorf("invalid response signature: %w", x.err)
return false
}
st := apistatus.FromStatusV2(resp.GetMetaHeader().GetStatus())
if !c.prm.DisableFrostFSErrorResolution {
return st, apistatus.ErrFromStatus(st)
// get result status
st := apistatus.FromStatusV2(x.resp.GetMetaHeader().GetStatus())
// unwrap unsuccessful status and return it
// as error if client has been configured so
successfulStatus := apistatus.IsSuccessful(st)
if !successfulStatus && x.resolveAPIFailures {
x.err = apistatus.ErrFromStatus(st)
return false
}
return st, nil
x.statusRes.setStatus(st)
return successfulStatus || !x.resolveAPIFailures
}
// ExecRaw executes f with underlying git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client.Client
// reads response (if rResp is set) and processes it. Result means success.
// If failed, contextCall.err contains the reason.
func (x *contextCall) readResponse() bool {
if x.rResp != nil {
x.err = x.rResp()
if x.err != nil {
x.err = fmt.Errorf("read response: %w", x.err)
return false
}
}
return x.processResponse()
}
// closes the message stream (if closer is set) and writes the results (if result is set).
// Return means success. If failed, contextCall.err contains the reason.
func (x *contextCall) close() bool {
if x.closer != nil {
x.err = x.closer()
if x.err != nil {
x.err = fmt.Errorf("close RPC: %w", x.err)
return false
}
}
// write response to resulting structure
if x.result != nil {
x.result(x.resp)
}
return true
}
// goes through all stages of sending a request and processing a response. Returns true if successful.
// If failed, contextCall.err contains the reason.
func (x *contextCall) processCall() bool {
// set request writer
x.wReq = func() error {
var err error
x.resp, err = x.call()
return err
}
// write request
ok := x.writeRequest()
if !ok {
return false
}
// read response
ok = x.readResponse()
if !ok {
return false
}
// close and write response to resulting structure
ok = x.close()
if !ok {
return false
}
return x.err == nil
}
// initializes static cross-call parameters inherited from client.
func (c *Client) initCallContext(ctx *contextCall) {
ctx.key = c.prm.key
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.netMagic = c.prm.netMagic
}
// 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:
// 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.
//
// The f must not manipulate the client connection passed into it.
@ -115,7 +323,7 @@ func (c *Client) processResponse(resp responseV2) (apistatus.Status, error) {
// before closing the connection.
//
// 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 {
return f(&c.c)
}

View file

@ -2,27 +2,732 @@ package client
import (
"context"
"fmt"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
v2container "github.com/nspcc-dev/neofs-api-go/v2/container"
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
v2signature "github.com/nspcc-dev/neofs-api-go/v2/signature"
"github.com/nspcc-dev/neofs-sdk-go/container"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
"github.com/nspcc-dev/neofs-sdk-go/eacl"
"github.com/nspcc-dev/neofs-sdk-go/owner"
"github.com/nspcc-dev/neofs-sdk-go/session"
"github.com/nspcc-dev/neofs-sdk-go/signature"
sigutil "github.com/nspcc-dev/neofs-sdk-go/util/signature"
)
// SyncContainerWithNetwork requests network configuration using passed client
// and applies it to the container. Container MUST not be nil.
// PrmContainerPut groups parameters of ContainerPut operation.
type PrmContainerPut struct {
prmCommonMeta
cnrSet bool
cnr container.Container
}
// SetContainer sets structured information about new NeoFS container.
// Required parameter.
func (x *PrmContainerPut) SetContainer(cnr container.Container) {
x.cnr = cnr
x.cnrSet = true
}
// ResContainerPut groups resulting values of ContainerPut operation.
type ResContainerPut struct {
statusRes
id *cid.ID
}
// 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
// asynchronously check if the save was successful).
//
// Note: if container does not match network configuration, SyncContainerWithNetwork
// changes it.
// Client doesn't retain value so modification is safe.
func (x ResContainerPut) ID() *cid.ID {
return x.id
}
func (x *ResContainerPut) setID(id *cid.ID) {
x.id = id
}
// ContainerPut sends request to save container in NeoFS.
//
// Returns any network/parsing config errors.
// 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`.
// If WithNeoFSErrorParsing option has been provided, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure.
//
// 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)
// Operation is asynchronous and no guaranteed even in the absence of errors.
// The required time is also not predictable.
//
// Success can be verified by reading by identifier (see ResContainerPut.ID).
//
// Immediately panics if parameters are set incorrectly (see PrmContainerPut docs).
// Context is required and must not be nil. It is used for network communication.
//
// Return statuses:
// - global (see Client docs).
func (c *Client) ContainerPut(ctx context.Context, prm PrmContainerPut) (*ResContainerPut, error) {
// check parameters
switch {
case ctx == nil:
panic(panicMsgMissingContext)
case !prm.cnrSet:
panic(panicMsgMissingContainer)
}
container.ApplyNetworkConfig(cnr, res.Info())
// TODO: check private key is set before forming the request
return nil
// form request body
reqBody := new(v2container.PutRequestBody)
reqBody.SetContainer(prm.cnr.ToV2())
// 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
var meta v2session.RequestMetaHeader
meta.SetSessionToken(prm.cnr.SessionToken().ToV2())
prm.prmCommonMeta.writeToMetaHeader(&meta)
// form request
var req v2container.PutRequest
req.SetBody(reqBody)
req.SetMetaHeader(&meta)
// init call context
var (
cc contextCall
res ResContainerPut
)
c.initCallContext(&cc)
cc.req = &req
cc.statusRes = &res
cc.call = func() (responseV2, error) {
return rpcapi.PutContainer(&c.c, &req, client.WithContext(ctx))
}
cc.result = func(r responseV2) {
resp := r.(*v2container.PutResponse)
res.setID(cid.NewFromV2(resp.GetBody().GetContainerID()))
}
// process call
if !cc.processCall() {
return nil, cc.err
}
return &res, nil
}
// PrmContainerGet groups parameters of ContainerGet operation.
type PrmContainerGet struct {
prmCommonMeta
idSet bool
id cid.ID
}
// SetContainer sets identifier of the container to be read.
// Required parameter.
func (x *PrmContainerGet) SetContainer(id cid.ID) {
x.id = id
x.idSet = true
}
// ResContainerGet groups resulting values of ContainerGet operation.
type ResContainerGet struct {
statusRes
cnr *container.Container
}
// Container returns structured information about the requested container.
//
// Client doesn't retain value so modification is safe.
func (x ResContainerGet) Container() *container.Container {
return x.cnr
}
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.
// Any client's internal or transport errors are returned as `error`.
// If WithNeoFSErrorParsing option has been provided, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure.
//
// Immediately panics if parameters are set incorrectly (see PrmContainerGet docs).
// Context is required and must not be nil. It is used for network communication.
//
// Return statuses:
// - global (see Client docs);
// - *apistatus.ContainerNotFound.
func (c *Client) ContainerGet(ctx context.Context, prm PrmContainerGet) (*ResContainerGet, error) {
switch {
case ctx == nil:
panic(panicMsgMissingContext)
case !prm.idSet:
panic(panicMsgMissingContainer)
}
// form request body
reqBody := new(v2container.GetRequestBody)
reqBody.SetContainerID(prm.id.ToV2())
// form request
var req v2container.GetRequest
req.SetBody(reqBody)
// init call context
var (
cc contextCall
res ResContainerGet
)
c.initCallContext(&cc)
cc.meta = prm.prmCommonMeta
cc.req = &req
cc.statusRes = &res
cc.call = func() (responseV2, error) {
return rpcapi.GetContainer(&c.c, &req, client.WithContext(ctx))
}
cc.result = func(r responseV2) {
resp := r.(*v2container.GetResponse)
body := resp.GetBody()
cnr := container.NewContainerFromV2(body.GetContainer())
cnr.SetSessionToken(
session.NewTokenFromV2(body.GetSessionToken()),
)
cnr.SetSignature(
signature.NewFromV2(body.GetSignature()),
)
res.setContainer(cnr)
}
// process call
if !cc.processCall() {
return nil, cc.err
}
return &res, nil
}
// PrmContainerList groups parameters of ContainerList operation.
type PrmContainerList struct {
prmCommonMeta
ownerSet bool
ownerID owner.ID
}
// SetAccount sets identifier of the NeoFS account to list the containers.
// Required parameter. Must be a valid ID according to NeoFS API protocol.
func (x *PrmContainerList) SetAccount(id owner.ID) {
x.ownerID = id
x.ownerSet = true
}
// ResContainerList groups resulting values of ContainerList operation.
type ResContainerList struct {
statusRes
ids []cid.ID
}
// Containers returns list of identifiers of the account-owned containers.
//
// Client doesn't retain value so modification is safe.
func (x ResContainerList) Containers() []cid.ID {
return x.ids
}
func (x *ResContainerList) setContainers(ids []cid.ID) {
x.ids = ids
}
// ContainerList requests identifiers of the account-owned containers.
//
// 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`.
// If WithNeoFSErrorParsing option has been provided, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure.
//
// Immediately panics if parameters are set incorrectly (see PrmContainerList docs).
// Context is required and must not be nil. It is used for network communication.
//
// Return statuses:
// - global (see Client docs).
func (c *Client) ContainerList(ctx context.Context, prm PrmContainerList) (*ResContainerList, error) {
// check parameters
switch {
case ctx == nil:
panic(panicMsgMissingContext)
case !prm.ownerSet:
panic("account not set")
case !prm.ownerID.Valid():
panic("invalid account")
}
// form request body
reqBody := new(v2container.ListRequestBody)
reqBody.SetOwnerID(prm.ownerID.ToV2())
// form request
var req v2container.ListRequest
req.SetBody(reqBody)
// init call context
var (
cc contextCall
res ResContainerList
)
c.initCallContext(&cc)
cc.meta = prm.prmCommonMeta
cc.req = &req
cc.statusRes = &res
cc.call = func() (responseV2, error) {
return rpcapi.ListContainers(&c.c, &req, client.WithContext(ctx))
}
cc.result = func(r responseV2) {
resp := r.(*v2container.ListResponse)
ids := make([]cid.ID, len(resp.GetBody().GetContainerIDs()))
for i, cidV2 := range resp.GetBody().GetContainerIDs() {
ids[i] = *cid.NewFromV2(&cidV2)
}
res.setContainers(ids)
}
// process call
if !cc.processCall() {
return nil, cc.err
}
return &res, nil
}
// PrmContainerDelete groups parameters of ContainerDelete operation.
type PrmContainerDelete struct {
prmCommonMeta
prmSession
idSet bool
id cid.ID
}
// SetContainer sets identifier of the NeoFS container to be removed.
// Required parameter.
func (x *PrmContainerDelete) SetContainer(id cid.ID) {
x.id = id
x.idSet = true
}
// ResContainerDelete groups resulting values of ContainerDelete operation.
type ResContainerDelete struct {
statusRes
}
// 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.
// Any client's internal or transport errors are returned as `error`.
// If WithNeoFSErrorParsing option has been provided, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure.
//
// Operation is asynchronous and no guaranteed even in the absence of errors.
// The required time is also not predictable.
//
// Success can be verified by reading by identifier (see GetContainer).
//
// Immediately panics if parameters are set incorrectly (see PrmContainerDelete docs).
// 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.
// Reflects all internal errors in second return value (transport problems, response processing, etc.).
//
// Return statuses:
// - global (see Client docs).
func (c *Client) ContainerDelete(ctx context.Context, prm PrmContainerDelete) (*ResContainerDelete, error) {
// check parameters
switch {
case ctx == nil:
panic(panicMsgMissingContext)
case !prm.idSet:
panic(panicMsgMissingContainer)
}
// form request body
reqBody := new(v2container.DeleteRequestBody)
reqBody.SetContainerID(prm.id.ToV2())
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
var meta v2session.RequestMetaHeader
prm.prmSession.writeToMetaHeader(&meta)
prm.prmCommonMeta.writeToMetaHeader(&meta)
// form request
var req v2container.DeleteRequest
req.SetBody(reqBody)
req.SetMetaHeader(&meta)
// init call context
var (
cc contextCall
res ResContainerDelete
)
c.initCallContext(&cc)
cc.req = &req
cc.statusRes = &res
cc.call = func() (responseV2, error) {
return rpcapi.DeleteContainer(&c.c, &req, client.WithContext(ctx))
}
// process call
if !cc.processCall() {
return nil, cc.err
}
return &res, nil
}
// PrmContainerEACL groups parameters of ContainerEACL operation.
type PrmContainerEACL struct {
prmCommonMeta
idSet bool
id cid.ID
}
// SetContainer sets identifier of the NeoFS container to read the eACL table.
// Required parameter.
func (x *PrmContainerEACL) SetContainer(id cid.ID) {
x.id = id
x.idSet = true
}
// ResContainerEACL groups resulting values of ContainerEACL operation.
type ResContainerEACL struct {
statusRes
table *eacl.Table
}
// Table returns eACL table of the requested container.
//
// Client doesn't retain value so modification is safe.
func (x ResContainerEACL) Table() *eacl.Table {
return x.table
}
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.
// Any client's internal or transport errors are returned as `error`.
// If WithNeoFSErrorParsing option has been provided, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure.
//
// Immediately panics if parameters are set incorrectly (see PrmContainerEACL docs).
// Context is required and must not be nil. It is used for network communication.
//
// Return statuses:
// - global (see Client docs);
// - *apistatus.ContainerNotFound.
func (c *Client) ContainerEACL(ctx context.Context, prm PrmContainerEACL) (*ResContainerEACL, error) {
// check parameters
switch {
case ctx == nil:
panic(panicMsgMissingContext)
case !prm.idSet:
panic(panicMsgMissingContainer)
}
// form request body
reqBody := new(v2container.GetExtendedACLRequestBody)
reqBody.SetContainerID(prm.id.ToV2())
// form request
var req v2container.GetExtendedACLRequest
req.SetBody(reqBody)
// init call context
var (
cc contextCall
res ResContainerEACL
)
c.initCallContext(&cc)
cc.meta = prm.prmCommonMeta
cc.req = &req
cc.statusRes = &res
cc.call = func() (responseV2, error) {
return rpcapi.GetEACL(&c.c, &req, client.WithContext(ctx))
}
cc.result = func(r responseV2) {
resp := r.(*v2container.GetExtendedACLResponse)
body := resp.GetBody()
table := eacl.NewTableFromV2(body.GetEACL())
table.SetSessionToken(
session.NewTokenFromV2(body.GetSessionToken()),
)
table.SetSignature(
signature.NewFromV2(body.GetSignature()),
)
res.setTable(table)
}
// process call
if !cc.processCall() {
return nil, cc.err
}
return &res, nil
}
// PrmContainerSetEACL groups parameters of ContainerSetEACL operation.
type PrmContainerSetEACL struct {
prmCommonMeta
tableSet bool
table eacl.Table
}
// SetTable sets eACL table structure to be set for the container.
// Required parameter.
func (x *PrmContainerSetEACL) SetTable(table eacl.Table) {
x.table = table
x.tableSet = true
}
// ResContainerSetEACL groups resulting values of ContainerSetEACL operation.
type ResContainerSetEACL struct {
statusRes
}
// 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.
// Any client's internal or transport errors are returned as `error`.
// If WithNeoFSErrorParsing option has been provided, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure.
//
// Operation is asynchronous and no guaranteed even in the absence of errors.
// The required time is also not predictable.
//
// Success can be verified by reading by identifier (see EACL).
//
// Immediately panics if parameters are set incorrectly (see PrmContainerSetEACL docs).
// Context is required and must not be nil. It is used for network communication.
//
// Return statuses:
// - global (see Client docs).
func (c *Client) ContainerSetEACL(ctx context.Context, prm PrmContainerSetEACL) (*ResContainerSetEACL, error) {
// check parameters
switch {
case ctx == nil:
panic(panicMsgMissingContext)
case !prm.tableSet:
panic("eACL table not set")
}
// form request body
reqBody := new(v2container.SetExtendedACLRequestBody)
reqBody.SetEACL(prm.table.ToV2())
// 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
var meta v2session.RequestMetaHeader
meta.SetSessionToken(prm.table.SessionToken().ToV2())
prm.prmCommonMeta.writeToMetaHeader(&meta)
// form request
var req v2container.SetExtendedACLRequest
req.SetBody(reqBody)
req.SetMetaHeader(&meta)
// init call context
var (
cc contextCall
res ResContainerSetEACL
)
c.initCallContext(&cc)
cc.req = &req
cc.statusRes = &res
cc.call = func() (responseV2, error) {
return rpcapi.SetEACL(&c.c, &req, client.WithContext(ctx))
}
// process call
if !cc.processCall() {
return nil, cc.err
}
return &res, nil
}
// PrmAnnounceSpace groups parameters of ContainerAnnounceUsedSpace operation.
type PrmAnnounceSpace struct {
prmCommonMeta
announcements []container.UsedSpaceAnnouncement
}
// SetValues sets values describing volume of space that is used for the container objects.
// Required parameter. Must not be empty.
//
// Must not be mutated before the end of the operation.
func (x *PrmAnnounceSpace) SetValues(announcements []container.UsedSpaceAnnouncement) {
x.announcements = announcements
}
// ResAnnounceSpace groups resulting values of ContainerAnnounceUsedSpace operation.
type ResAnnounceSpace struct {
statusRes
}
// ContainerAnnounceUsedSpace sends request to announce volume of the space used for the container objects.
//
// 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`.
// If WithNeoFSErrorParsing option has been provided, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure.
//
// Operation is asynchronous and no guaranteed even in the absence of errors.
// The required time is also not predictable.
//
// At this moment success can not be checked.
//
// Immediately panics if parameters are set incorrectly (see PrmAnnounceSpace docs).
// Context is required and must not be nil. It is used for network communication.
//
// Return statuses:
// - global (see Client docs).
func (c *Client) ContainerAnnounceUsedSpace(ctx context.Context, prm PrmAnnounceSpace) (*ResAnnounceSpace, error) {
// check parameters
switch {
case ctx == nil:
panic(panicMsgMissingContext)
case len(prm.announcements) == 0:
panic("missing announcements")
}
// convert list of SDK announcement structures into NeoFS-API v2 list
v2announce := make([]v2container.UsedSpaceAnnouncement, len(prm.announcements))
for i := range prm.announcements {
v2announce[i] = *prm.announcements[i].ToV2()
}
// prepare body of the NeoFS-API v2 request and request itself
reqBody := new(v2container.AnnounceUsedSpaceRequestBody)
reqBody.SetAnnouncements(v2announce)
// form request
var req v2container.AnnounceUsedSpaceRequest
req.SetBody(reqBody)
// init call context
var (
cc contextCall
res ResAnnounceSpace
)
c.initCallContext(&cc)
cc.meta = prm.prmCommonMeta
cc.req = &req
cc.statusRes = &res
cc.call = func() (responseV2, error) {
return rpcapi.AnnounceUsedSpace(&c.c, &req, client.WithContext(ctx))
}
// process call
if !cc.processCall() {
return nil, cc.err
}
return &res, nil
}

View file

@ -1,139 +0,0 @@
package client
import (
"context"
"fmt"
v2container "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
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/session"
)
// PrmContainerDelete groups parameters of ContainerDelete operation.
type PrmContainerDelete struct {
// FrostFS request X-Headers
XHeaders []string
ContainerID *cid.ID
Session *session.Container
}
// SetContainer sets identifier of the FrostFS container to be removed.
// Required parameter.
//
// Deprecated: Use PrmContainerDelete.Container instead.
func (prm *PrmContainerDelete) SetContainer(id cid.ID) {
prm.ContainerID = &id
}
func (prm *PrmContainerDelete) buildRequest(c *Client) (*v2container.DeleteRequest, error) {
if prm.ContainerID == nil {
return nil, errorMissingContainer
}
if len(prm.XHeaders)%2 != 0 {
return nil, errorInvalidXHeaders
}
var cidV2 refs.ContainerID
prm.ContainerID.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)
reqBody := new(v2container.DeleteRequestBody)
reqBody.SetContainerID(&cidV2)
reqBody.SetSignature(&sigv2)
var meta v2session.RequestMetaHeader
writeXHeadersToMeta(prm.XHeaders, &meta)
if prm.Session != nil {
var tokv2 v2session.Token
prm.Session.WriteToV2(&tokv2)
meta.SetSessionToken(&tokv2)
}
var req v2container.DeleteRequest
req.SetBody(reqBody)
c.prepareRequest(&req, &meta)
return &req, nil
}
// 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.
//
// Deprecated: Use PrmContainerDelete.Session instead.
func (prm *PrmContainerDelete) WithinSession(tok session.Container) {
prm.Session = &tok
}
// ResContainerDelete groups resulting values of ContainerDelete operation.
type ResContainerDelete struct {
statusRes
}
// ContainerDelete sends request to remove the FrostFS container.
//
// 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`.
// If PrmInit.DisableFrostFSFailuresResolution has been called, unsuccessful
// FrostFS status codes are included in the returned result structure,
// otherwise, are also returned as `error`.
//
// Operation is asynchronous and no guaranteed even in the absence of errors.
// The required time is also not predictable.
//
// Success can be verified by reading by identifier (see GetContainer).
//
// Returns an error if parameters are set incorrectly (see PrmContainerDelete docs).
// 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.
// Reflects all internal errors in second return value (transport problems, response processing, etc.).
//
// Return statuses:
// - global (see Client docs).
func (c *Client) ContainerDelete(ctx context.Context, prm PrmContainerDelete) (*ResContainerDelete, error) {
req, err := prm.buildRequest(c)
if err != nil {
return nil, err
}
if err := signature.SignServiceMessage(&c.prm.Key, req); err != nil {
return nil, fmt.Errorf("sign request: %w", err)
}
resp, err := rpcapi.DeleteContainer(&c.c, req, client.WithContext(ctx))
if err != nil {
return nil, err
}
var res ResContainerDelete
res.st, err = c.processResponse(resp)
return &res, err
}

View file

@ -1,126 +0,0 @@
package client
import (
"context"
"errors"
"fmt"
v2container "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
)
// PrmContainerGet groups parameters of ContainerGet operation.
type PrmContainerGet struct {
// FrostFS request X-Headers.
XHeaders []string
ContainerID *cid.ID
Session *session.Container
}
// SetContainer sets identifier of the container to be read.
// Required parameter.
//
// Deprecated: Use PrmContainerGet.ContainerID instead.
func (prm *PrmContainerGet) SetContainer(cid cid.ID) {
prm.ContainerID = &cid
}
func (prm *PrmContainerGet) buildRequest(c *Client) (*v2container.GetRequest, error) {
if prm.ContainerID == nil {
return nil, errorMissingContainer
}
if len(prm.XHeaders)%2 != 0 {
return nil, errorInvalidXHeaders
}
var cidV2 refs.ContainerID
prm.ContainerID.WriteToV2(&cidV2)
reqBody := new(v2container.GetRequestBody)
reqBody.SetContainerID(&cidV2)
var meta v2session.RequestMetaHeader
writeXHeadersToMeta(prm.XHeaders, &meta)
if prm.Session != nil {
var tokv2 v2session.Token
prm.Session.WriteToV2(&tokv2)
meta.SetSessionToken(&tokv2)
}
var req v2container.GetRequest
req.SetBody(reqBody)
c.prepareRequest(&req, &meta)
return &req, nil
}
// ResContainerGet groups resulting values of ContainerGet operation.
type ResContainerGet struct {
statusRes
cnr container.Container
}
// Container returns structured information about the requested container.
//
// Client doesn't retain value so modification is safe.
func (x ResContainerGet) Container() container.Container {
return x.cnr
}
// ContainerGet reads FrostFS container by ID.
//
// 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`.
// If PrmInit.DisableFrostFSFailuresResolution has been called, unsuccessful
// FrostFS status codes are included in the returned result structure,
// otherwise, are also returned as `error`.
//
// Returns an error if parameters are set incorrectly (see PrmContainerGet docs).
// Context is required and must not be nil. It is used for network communication.
//
// Return statuses:
// - global (see Client docs);
// - *apistatus.ContainerNotFound.
func (c *Client) ContainerGet(ctx context.Context, prm PrmContainerGet) (*ResContainerGet, error) {
req, err := prm.buildRequest(c)
if err != nil {
return nil, err
}
if err := signature.SignServiceMessage(&c.prm.Key, req); err != nil {
return nil, fmt.Errorf("sign request: %w", err)
}
resp, err := rpcapi.GetContainer(&c.c, req, client.WithContext(ctx))
if err != nil {
return nil, err
}
var res ResContainerGet
res.st, err = c.processResponse(resp)
if err != nil || !apistatus.IsSuccessful(res.st) {
return &res, err
}
cnrV2 := resp.GetBody().GetContainer()
if cnrV2 == nil {
return &res, errors.New("missing container in response")
}
if err := res.cnr.ReadFromV2(*cnrV2); err != nil {
return &res, fmt.Errorf("invalid container in response: %w", err)
}
return &res, nil
}

View file

@ -1,119 +0,0 @@
package client
import (
"context"
"fmt"
v2container "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
)
// PrmContainerList groups parameters of ContainerList operation.
type PrmContainerList struct {
XHeaders []string
Account user.ID
Session *session.Container
}
// SetAccount sets identifier of the FrostFS account to list the containers.
// Required parameter.
//
// Deprecated: Use PrmContainerList.Account instead.
func (x *PrmContainerList) SetAccount(id user.ID) {
x.Account = id
}
func (x *PrmContainerList) buildRequest(c *Client) (*v2container.ListRequest, error) {
if x.Account.IsEmpty() {
return nil, errorAccountNotSet
}
var ownerV2 refs.OwnerID
x.Account.WriteToV2(&ownerV2)
reqBody := new(v2container.ListRequestBody)
reqBody.SetOwnerID(&ownerV2)
var meta v2session.RequestMetaHeader
writeXHeadersToMeta(x.XHeaders, &meta)
if x.Session != nil {
var tokv2 v2session.Token
x.Session.WriteToV2(&tokv2)
meta.SetSessionToken(&tokv2)
}
var req v2container.ListRequest
req.SetBody(reqBody)
c.prepareRequest(&req, &meta)
return &req, nil
}
// ResContainerList groups resulting values of ContainerList operation.
type ResContainerList struct {
statusRes
ids []cid.ID
}
// Containers returns list of identifiers of the account-owned containers.
//
// Client doesn't retain value so modification is safe.
func (x ResContainerList) Containers() []cid.ID {
return x.ids
}
// ContainerList requests identifiers of the account-owned containers.
//
// 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`.
// If PrmInit.DisableFrostFSFailuresResolution has been called, unsuccessful
// FrostFS status codes are included in the returned result structure,
// otherwise, are also returned as `error`.
//
// Returns an error if parameters are set incorrectly (see PrmContainerList docs).
// Context is required and must not be nil. It is used for network communication.
//
// Return statuses:
// - global (see Client docs).
func (c *Client) ContainerList(ctx context.Context, prm PrmContainerList) (*ResContainerList, error) {
req, err := prm.buildRequest(c)
if err != nil {
return nil, err
}
if err := signature.SignServiceMessage(&c.prm.Key, req); err != nil {
return nil, fmt.Errorf("sign request: %w", err)
}
resp, err := rpcapi.ListContainers(&c.c, req, client.WithContext(ctx))
if err != nil {
return nil, err
}
var res ResContainerList
res.st, err = c.processResponse(resp)
if err != nil || !apistatus.IsSuccessful(res.st) {
return &res, err
}
res.ids = make([]cid.ID, len(resp.GetBody().GetContainerIDs()))
for i, cidV2 := range resp.GetBody().GetContainerIDs() {
if err := res.ids[i].ReadFromV2(cidV2); err != nil {
return &res, fmt.Errorf("invalid ID in the response: %w", err)
}
}
return &res, nil
}

View file

@ -1,160 +0,0 @@
package client
import (
"context"
"fmt"
v2container "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
)
// PrmContainerPut groups parameters of ContainerPut operation.
type PrmContainerPut struct {
// FrostFS request X-Headers
XHeaders []string
Container *container.Container
Session *session.Container
}
// SetContainer sets structured information about new FrostFS container.
// Required parameter.
//
// Deprecated: Use PrmContainerPut.Container instead.
func (x *PrmContainerPut) SetContainer(cnr container.Container) {
x.Container = &cnr
}
// 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
//
// Deprecated: Use PrmContainerPut.Session instead.
func (x *PrmContainerPut) WithinSession(s session.Container) {
x.Session = &s
}
func (x *PrmContainerPut) buildRequest(c *Client) (*v2container.PutRequest, error) {
if x.Container == nil {
return nil, errorMissingContainer
}
if len(x.XHeaders)%2 != 0 {
return nil, errorInvalidXHeaders
}
// TODO: check private key is set before forming the request
var cnr v2container.Container
x.Container.WriteToV2(&cnr)
var sig frostfscrypto.Signature
err := container.CalculateSignature(&sig, *x.Container, c.prm.Key)
if err != nil {
return nil, fmt.Errorf("calculate container signature: %w", err)
}
var sigv2 refs.Signature
sig.WriteToV2(&sigv2)
reqBody := new(v2container.PutRequestBody)
reqBody.SetContainer(&cnr)
reqBody.SetSignature(&sigv2)
var meta v2session.RequestMetaHeader
writeXHeadersToMeta(x.XHeaders, &meta)
if x.Session != nil {
var tokv2 v2session.Token
x.Session.WriteToV2(&tokv2)
meta.SetSessionToken(&tokv2)
}
var req v2container.PutRequest
req.SetBody(reqBody)
c.prepareRequest(&req, &meta)
return &req, nil
}
// ResContainerPut groups resulting values of ContainerPut operation.
type ResContainerPut struct {
statusRes
id cid.ID
}
// 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
// asynchronously check if the save was successful).
func (x ResContainerPut) ID() cid.ID {
return x.id
}
// ContainerPut sends request to save container in FrostFS.
//
// 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`.
// If PrmInit.DisableFrostFSFailuresResolution has been called, unsuccessful
// FrostFS status codes are included in the returned result structure,
// otherwise, are also returned as `error`.
//
// Operation is asynchronous and no guaranteed even in the absence of errors.
// The required time is also not predictable.
//
// Success can be verified by reading by identifier (see ResContainerPut.ID).
//
// Returns an error if parameters are set incorrectly (see PrmContainerPut docs).
// Context is required and must not be nil. It is used for network communication.
//
// Return statuses:
// - global (see Client docs).
//
// nolint: funlen
func (c *Client) ContainerPut(ctx context.Context, prm PrmContainerPut) (*ResContainerPut, error) {
req, err := prm.buildRequest(c)
if err != nil {
return nil, err
}
if err := signature.SignServiceMessage(&c.prm.Key, req); err != nil {
return nil, fmt.Errorf("sign request: %w", err)
}
resp, err := rpcapi.PutContainer(&c.c, req, client.WithContext(ctx))
if err != nil {
return nil, err
}
var res ResContainerPut
res.st, err = c.processResponse(resp)
if err != nil || !apistatus.IsSuccessful(res.st) {
return &res, err
}
const fieldCnrID = "container ID"
cidV2 := resp.GetBody().GetContainerID()
if cidV2 == nil {
return &res, newErrMissingResponseField(fieldCnrID)
}
if err := res.id.ReadFromV2(*cidV2); err != nil {
return &res, newErrInvalidResponseField(fieldCnrID, err)
}
return &res, 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
and provides methods for executing operations on the server.
Create client instance:
var c client.Client
Initialize client state:
var prm client.PrmInit
prm.SetDefaultPrivateKey(key)
// ...
c.Init(prm)
Connect to the FrostFS server:
Connect to the NeoFS server:
var prm client.PrmDial
prm.SetServerURI("localhost:8080")
prm.SetDefaultPrivateKey(key)
@ -26,8 +23,7 @@ Connect to the FrostFS server:
err := c.Dial(prm)
// ...
Execute FrostFS operation on the server:
Execute NeoFS operation on the server:
var prm client.PrmContainerPut
prm.SetContainer(cnr)
// ...
@ -40,15 +36,14 @@ Execute FrostFS operation on the server:
// ...
Consume custom service of the server:
syntax = "proto3";
service CustomService {
rpc CustomRPC(CustomRPCRequest) returns (CustomRPCResponse);
}
import "git.frostfs.info/TrueCloudLab/frostfs-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/client"
import "github.com/nspcc-dev/neofs-api-go/v2/rpc/common"
req := new(CustomRPCRequest)
// ...
@ -63,7 +58,6 @@ Consume custom service of the server:
// ...
Close the connection:
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
read-only. To be able to override client behavior (e.g. for tests), abstract it
with an interface:
import "github.com/nspcc-dev/neofs-sdk-go/client"
import "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client"
type FrostFSClient interface {
type NeoFSClient interface {
// Operations according to the application needs
CreateContainer(context.Context, container.Container) error
// ...
@ -87,5 +80,6 @@ with an interface:
func (x *client) CreateContainer(context.Context, container.Container) error {
// ...
}
*/
package client

View file

@ -1,85 +1,42 @@
package client
import (
"fmt"
import apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
)
// wrapsErrType returns true if any error in the error tree of err is of type E.
func wrapsErrType[E error](err error) bool {
switch e := err.(type) {
case E:
return true
case interface{ Unwrap() error }:
return wrapsErrType[E](e.Unwrap())
case interface{ Unwrap() []error }:
for _, ei := range e.Unwrap() {
if wrapsErrType[E](ei) {
return true
}
}
return false
// IsErrContainerNotFound checks if err corresponds to NeoFS status
// return corresponding to missing container.
func IsErrContainerNotFound(err error) bool {
switch err.(type) {
default:
return false
case
apistatus.ContainerNotFound,
*apistatus.ContainerNotFound:
return true
}
}
// IsErrContainerNotFound checks if err corresponds to FrostFS status
// return corresponding to missing container. Supports wrapped errors.
func IsErrContainerNotFound(err error) bool {
return wrapsErrType[*apistatus.ContainerNotFound](err)
}
// IsErrEACLNotFound checks if err corresponds to FrostFS status
// return corresponding to missing eACL table. Supports wrapped errors.
func IsErrEACLNotFound(err error) bool {
return wrapsErrType[*apistatus.EACLNotFound](err)
}
// IsErrObjectNotFound checks if err corresponds to FrostFS status
// return corresponding to missing object. Supports wrapped errors.
// IsErrObjectNotFound checks if err corresponds to NeoFS status
// return corresponding to missing object.
func IsErrObjectNotFound(err error) bool {
return wrapsErrType[*apistatus.ObjectNotFound](err)
switch err.(type) {
default:
return false
case
apistatus.ObjectNotFound,
*apistatus.ObjectNotFound:
return true
}
}
// IsErrObjectAlreadyRemoved checks if err corresponds to FrostFS status
// return corresponding to already removed object. Supports wrapped errors.
// IsErrObjectAlreadyRemoved checks if err corresponds to NeoFS status
// return corresponding to already removed object.
func IsErrObjectAlreadyRemoved(err error) bool {
return wrapsErrType[*apistatus.ObjectAlreadyRemoved](err)
}
// IsErrSessionExpired checks if err corresponds to FrostFS status return
// corresponding to expired session. Supports wrapped errors.
func IsErrSessionExpired(err error) bool {
return wrapsErrType[*apistatus.SessionTokenExpired](err)
}
// IsErrSessionNotFound checks if err corresponds to FrostFS status return
// corresponding to missing session. Supports wrapped errors.
func IsErrSessionNotFound(err error) bool {
return wrapsErrType[*apistatus.SessionTokenNotFound](err)
}
// IsErrAPEManagerAccessDenied checks if err corresponds to FrostFS status return
// corresponding to apemanager access deny. Supports wrapped errors.
func IsErrAPEManagerAccessDenied(err error) bool {
return wrapsErrType[*apistatus.APEManagerAccessDenied](err)
}
// IsErrNodeUnderMaintenance checks if err corresponds to FrostFS status return
// corresponding to nodes being under maintenance. Supports wrapped errors.
func IsErrNodeUnderMaintenance(err error) bool {
return wrapsErrType[*apistatus.NodeUnderMaintenance](err)
}
// 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)
switch err.(type) {
default:
return false
case
apistatus.ObjectAlreadyRemoved,
*apistatus.ObjectAlreadyRemoved:
return true
}
}

View file

@ -1,54 +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) {
errs := []struct {
check func(error) bool
err error
}{
{
check: client.IsErrContainerNotFound,
err: new(apistatus.ContainerNotFound),
},
{
check: client.IsErrEACLNotFound,
err: new(apistatus.EACLNotFound),
},
{
check: client.IsErrObjectNotFound,
err: new(apistatus.ObjectNotFound),
},
{
check: client.IsErrObjectAlreadyRemoved,
err: new(apistatus.ObjectAlreadyRemoved),
},
{
check: client.IsErrSessionExpired,
err: new(apistatus.SessionTokenExpired),
},
{
check: client.IsErrSessionNotFound,
err: new(apistatus.SessionTokenNotFound),
},
{
check: client.IsErrNodeUnderMaintenance,
err: new(apistatus.NodeUnderMaintenance),
},
}
for i := range errs {
for j := range errs {
nestedErr := fmt.Errorf("top-level context: :%w", fmt.Errorf("inner context: %w", errs[j].err))
require.Equal(t, i == j, errs[i].check(errs[j].err))
require.Equal(t, i == j, errs[i].check(nestedErr))
}
}
}

View file

@ -2,261 +2,177 @@ 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"
v2session "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"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/version"
v2netmap "github.com/nspcc-dev/neofs-api-go/v2/netmap"
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
"github.com/nspcc-dev/neofs-sdk-go/netmap"
"github.com/nspcc-dev/neofs-sdk-go/version"
)
// PrmEndpointInfo groups parameters of EndpointInfo operation.
type PrmEndpointInfo struct {
XHeaders []string
}
func (x *PrmEndpointInfo) buildRequest(c *Client) (*v2netmap.LocalNodeInfoRequest, error) {
meta := new(v2session.RequestMetaHeader)
writeXHeadersToMeta(x.XHeaders, meta)
req := new(v2netmap.LocalNodeInfoRequest)
req.SetBody(new(v2netmap.LocalNodeInfoRequestBody))
c.prepareRequest(req, meta)
return req, nil
prmCommonMeta
}
// ResEndpointInfo group resulting values of EndpointInfo operation.
type ResEndpointInfo struct {
statusRes
version version.Version
version *version.Version
ni netmap.NodeInfo
ni *netmap.NodeInfo
}
// LatestVersion returns latest FrostFS API protocol's version in use.
func (x ResEndpointInfo) LatestVersion() version.Version {
// LatestVersion returns latest NeoFS API protocol's version in use.
//
// Client doesn't retain value so modification is safe.
func (x ResEndpointInfo) LatestVersion() *version.Version {
return x.version
}
// NodeInfo returns information about the FrostFS node served on the remote endpoint.
func (x ResEndpointInfo) NodeInfo() netmap.NodeInfo {
func (x *ResEndpointInfo) setLatestVersion(ver *version.Version) {
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
}
func (x *ResEndpointInfo) setNodeInfo(info *netmap.NodeInfo) {
x.ni = info
}
// 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.
//
// Any client's internal or transport errors are returned as `error`.
// If PrmInit.DisableFrostFSFailuresResolution has been called, unsuccessful
// FrostFS status codes are included in the returned result structure,
// otherwise, are also returned as `error`.
// If WithNeoFSErrorParsing option has been provided, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included
// 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.
//
// Exactly one return value is non-nil. Server status return is returned in ResEndpointInfo.
// Reflects all internal errors in second return value (transport problems, response processing, etc.).
//
// Return statuses:
// - global (see Client docs).
// - global (see Client docs).
func (c *Client) EndpointInfo(ctx context.Context, prm PrmEndpointInfo) (*ResEndpointInfo, error) {
req, err := prm.buildRequest(c)
if err != nil {
return nil, err
// check context
if ctx == nil {
panic(panicMsgMissingContext)
}
if err := signature.SignServiceMessage(&c.prm.Key, req); err != nil {
return nil, fmt.Errorf("sign request: %w", err)
// form request
var req v2netmap.LocalNodeInfoRequest
// init call context
var (
cc contextCall
res ResEndpointInfo
)
c.initCallContext(&cc)
cc.meta = prm.prmCommonMeta
cc.req = &req
cc.statusRes = &res
cc.call = func() (responseV2, error) {
return rpcapi.LocalNodeInfo(&c.c, &req, client.WithContext(ctx))
}
cc.result = func(r responseV2) {
resp := r.(*v2netmap.LocalNodeInfoResponse)
body := resp.GetBody()
res.setLatestVersion(version.NewFromV2(body.GetVersion()))
res.setNodeInfo(netmap.NewNodeInfoFromV2(body.GetNodeInfo()))
}
resp, err := rpcapi.LocalNodeInfo(&c.c, req, client.WithContext(ctx))
if err != nil {
return nil, err
// process call
if !cc.processCall() {
return nil, cc.err
}
var res ResEndpointInfo
res.st, err = c.processResponse(resp)
if err != nil || !apistatus.IsSuccessful(res.st) {
return &res, err
}
body := resp.GetBody()
const fieldVersion = "version"
verV2 := body.GetVersion()
if verV2 == nil {
return nil, newErrMissingResponseField(fieldVersion)
}
if err := res.version.ReadFromV2(*verV2); err != nil {
return nil, newErrInvalidResponseField(fieldVersion, err)
}
const fieldNodeInfo = "node info"
nodeInfoV2 := body.GetNodeInfo()
if nodeInfoV2 == nil {
return nil, newErrMissingResponseField(fieldNodeInfo)
}
if err := res.ni.ReadFromV2(*nodeInfoV2); err != nil {
return nil, newErrInvalidResponseField(fieldNodeInfo, err)
}
return &res, nil
}
// PrmNetworkInfo groups parameters of NetworkInfo operation.
type PrmNetworkInfo struct {
XHeaders []string
}
func (x PrmNetworkInfo) buildRequest(c *Client) (*v2netmap.NetworkInfoRequest, error) {
meta := new(v2session.RequestMetaHeader)
writeXHeadersToMeta(x.XHeaders, meta)
var req v2netmap.NetworkInfoRequest
req.SetBody(new(v2netmap.NetworkInfoRequestBody))
c.prepareRequest(&req, meta)
return &req, nil
prmCommonMeta
}
// ResNetworkInfo groups resulting values of NetworkInfo operation.
type ResNetworkInfo struct {
statusRes
info netmap.NetworkInfo
info *netmap.NetworkInfo
}
// Info returns structured information about the FrostFS network.
func (x ResNetworkInfo) Info() netmap.NetworkInfo {
// Info returns structured information about the NeoFS network.
//
// Client doesn't retain value so modification is safe.
func (x ResNetworkInfo) Info() *netmap.NetworkInfo {
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`.
// If PrmInit.DisableFrostFSFailuresResolution has been called, unsuccessful
// FrostFS status codes are included in the returned result structure,
// otherwise, are also returned as `error`.
// If WithNeoFSErrorParsing option has been provided, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included
// 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.
//
// Exactly one return value is non-nil. Server status return is returned in ResNetworkInfo.
// Reflects all internal errors in second return value (transport problems, response processing, etc.).
//
// Return statuses:
// - global (see Client docs).
// - global (see Client docs).
func (c *Client) NetworkInfo(ctx context.Context, prm PrmNetworkInfo) (*ResNetworkInfo, error) {
req, err := prm.buildRequest(c)
if err != nil {
return nil, err
// check context
if ctx == nil {
panic(panicMsgMissingContext)
}
if err := signature.SignServiceMessage(&c.prm.Key, req); err != nil {
return nil, fmt.Errorf("sign request: %w", err)
}
resp, err := rpcapi.NetworkInfo(&c.c, req, client.WithContext(ctx))
if err != nil {
return nil, err
}
var res ResNetworkInfo
res.st, err = c.processResponse(resp)
if err != nil || !apistatus.IsSuccessful(res.st) {
return &res, err
}
const fieldNetInfo = "network info"
netInfoV2 := resp.GetBody().GetNetworkInfo()
if netInfoV2 == nil {
return nil, newErrMissingResponseField(fieldNetInfo)
}
if err := res.info.ReadFromV2(*netInfoV2); err != nil {
return nil, newErrInvalidResponseField(fieldNetInfo, err)
}
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.DisableFrostFSFailuresResolution has been called, unsuccessful
// FrostFS status codes are included in the returned result structure,
// otherwise, are also returned as `error`.
//
// 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) {
// 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)
var req v2netmap.NetworkInfoRequest
err := signature.SignServiceMessage(&c.prm.Key, &req)
if err != nil {
return nil, fmt.Errorf("sign request: %w", err)
// init call context
var (
cc contextCall
res ResNetworkInfo
)
c.initCallContext(&cc)
cc.meta = prm.prmCommonMeta
cc.req = &req
cc.statusRes = &res
cc.call = func() (responseV2, error) {
return rpcapi.NetworkInfo(&c.c, &req, client.WithContext(ctx))
}
cc.result = func(r responseV2) {
resp := r.(*v2netmap.NetworkInfoResponse)
res.setInfo(netmap.NewNetworkInfoFromV2(resp.GetBody().GetNetworkInfo()))
}
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)
// process call
if !cc.processCall() {
return nil, cc.err
}
return &res, nil

View file

@ -1,132 +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)
c.prm.DisableFrostFSFailuresResolution()
ctx := context.Background()
// 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,106 +3,99 @@ package client
import (
"context"
"crypto/ecdsa"
"fmt"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl"
v2object "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
v2refs "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
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"
v2object "github.com/nspcc-dev/neofs-api-go/v2/object"
v2refs "github.com/nspcc-dev/neofs-api-go/v2/refs"
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
"github.com/nspcc-dev/neofs-sdk-go/session"
"github.com/nspcc-dev/neofs-sdk-go/token"
)
// PrmObjectDelete groups parameters of ObjectDelete operation.
type PrmObjectDelete struct {
XHeaders []string
meta v2session.RequestMetaHeader
BearerToken *bearer.Token
body v2object.DeleteRequestBody
Session *session.Object
addr v2refs.Address
ContainerID *cid.ID
keySet bool
key ecdsa.PrivateKey
}
ObjectID *oid.ID
// WithinSession specifies session within which object should be read.
//
// 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 *PrmObjectDelete) WithinSession(t session.Token) {
x.meta.SetSessionToken(t.ToV2())
}
Key *ecdsa.PrivateKey
// WithBearerToken attaches bearer token to be used for the operation.
//
// If set, underlying eACL rules will be used in access control.
//
// Must be signed.
func (x *PrmObjectDelete) WithBearerToken(t token.BearerToken) {
x.meta.SetBearerToken(t.ToV2())
}
// FromContainer specifies NeoFS container of the object.
// Required parameter.
func (x *PrmObjectDelete) FromContainer(id cid.ID) {
x.addr.SetContainerID(id.ToV2())
}
// ByID specifies identifier of the requested object.
// Required parameter.
func (x *PrmObjectDelete) ByID(id oid.ID) {
x.addr.SetObjectID(id.ToV2())
}
// UseKey specifies private key to sign the requests.
// If key is not provided, then Client default key is used.
func (x *PrmObjectDelete) UseKey(key ecdsa.PrivateKey) {
x.keySet = true
x.key = key
}
// WithXHeaders specifies list of extended headers (string key-value pairs)
// to be attached to the request. Must have an even length.
//
// Deprecated: Use PrmObjectDelete.Key instead.
func (prm *PrmObjectDelete) UseKey(key ecdsa.PrivateKey) {
prm.Key = &key
// Slice must not be mutated until the operation completes.
func (x *PrmObjectDelete) WithXHeaders(hs ...string) {
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.
type ResObjectDelete struct {
statusRes
tomb oid.ID
idTomb *v2refs.ObjectID
}
// Tombstone returns identifier of the created tombstone object.
func (x ResObjectDelete) Tombstone() oid.ID {
return x.tomb
// ReadTombstoneID reads identifier of the created tombstone object.
// Returns false if ID is missing (not read).
func (x ResObjectDelete) ReadTombstoneID(dst *oid.ID) bool {
if x.idTomb != nil {
*dst = *oid.NewIDFromV2(x.idTomb) // need smth better
return true
}
return false
}
func (prm *PrmObjectDelete) buildRequest(c *Client) (*v2object.DeleteRequest, error) {
if prm.ContainerID == nil {
return nil, errorMissingContainer
}
if prm.ObjectID == nil {
return nil, errorMissingObject
}
if len(prm.XHeaders)%2 != 0 {
return nil, errorInvalidXHeaders
}
meta := new(v2session.RequestMetaHeader)
writeXHeadersToMeta(prm.XHeaders, meta)
if prm.BearerToken != nil {
v2BearerToken := new(acl.BearerToken)
prm.BearerToken.WriteToV2(v2BearerToken)
meta.SetBearerToken(v2BearerToken)
}
if prm.Session != nil {
v2SessionToken := new(v2session.Token)
prm.Session.WriteToV2(v2SessionToken)
meta.SetSessionToken(v2SessionToken)
}
addr := new(v2refs.Address)
cnrV2 := new(v2refs.ContainerID)
prm.ContainerID.WriteToV2(cnrV2)
addr.SetContainerID(cnrV2)
objV2 := new(v2refs.ObjectID)
prm.ObjectID.WriteToV2(objV2)
addr.SetObjectID(objV2)
body := new(v2object.DeleteRequestBody)
body.SetAddress(addr)
req := new(v2object.DeleteRequest)
req.SetBody(body)
c.prepareRequest(req, meta)
return req, nil
}
// 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.
// 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.
@ -112,11 +105,11 @@ func (prm *PrmObjectDelete) buildRequest(c *Client) (*v2object.DeleteRequest, er
//
// 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`,
// If PrmInit.DisableFrostFSFailuresResolution has been called, unsuccessful
// FrostFS status codes are included in the returned result structure,
// otherwise, are also returned as `error`.
// If WithNeoFSErrorParsing option has been provided, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included
// 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.
//
// Return statuses:
@ -126,46 +119,48 @@ func (prm *PrmObjectDelete) buildRequest(c *Client) (*v2object.DeleteRequest, er
// - *apistatus.ObjectLocked;
// - *apistatus.SessionTokenExpired.
func (c *Client) ObjectDelete(ctx context.Context, prm PrmObjectDelete) (*ResObjectDelete, error) {
req, err := prm.buildRequest(c)
if err != nil {
return nil, err
switch {
case ctx == nil:
panic(panicMsgMissingContext)
case prm.addr.GetContainerID() == nil:
panic(panicMsgMissingContainer)
case prm.addr.GetObjectID() == nil:
panic("missing object")
}
key := c.prm.Key
if prm.Key != nil {
key = *prm.Key
// form request body
prm.body.SetAddress(&prm.addr)
// form request
var req v2object.DeleteRequest
req.SetBody(&prm.body)
req.SetMetaHeader(&prm.meta)
// init call context
var (
cc contextCall
res ResObjectDelete
)
if prm.keySet {
c.initCallContextWithoutKey(&cc)
cc.key = prm.key
} else {
c.initCallContext(&cc)
}
err = signature.SignServiceMessage(&key, req)
if err != nil {
return nil, fmt.Errorf("sign request: %w", err)
cc.req = &req
cc.statusRes = &res
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))
if err != nil {
return nil, 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)
// process call
if !cc.processCall() {
return nil, cc.err
}
return &res, nil

File diff suppressed because it is too large Load diff

View file

@ -2,64 +2,110 @@ package client
import (
"context"
"crypto/ecdsa"
"fmt"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl"
v2object "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
v2refs "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/checksum"
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
"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"
v2object "github.com/nspcc-dev/neofs-api-go/v2/object"
v2refs "github.com/nspcc-dev/neofs-api-go/v2/refs"
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
"github.com/nspcc-dev/neofs-sdk-go/session"
"github.com/nspcc-dev/neofs-sdk-go/token"
)
// PrmObjectHash groups parameters of ObjectHash operation.
type PrmObjectHash struct {
XHeaders []string
meta v2session.RequestMetaHeader
BearerToken *bearer.Token
body v2object.GetRangeHashRequestBody
Session *session.Object
tillichZemor bool
Local bool
Ranges []object.Range
Salt []byte
ChecksumType checksum.Type
ContainerID *cid.ID
ObjectID *oid.ID
Key *ecdsa.PrivateKey
addr v2refs.Address
}
// UseKey specifies private key to sign the requests.
// If key is not provided, then Client default key is used.
// MarkLocal tells the server to execute the operation locally.
func (x *PrmObjectHash) MarkLocal() {
x.meta.SetTTL(1)
}
// WithinSession specifies session within which object should be read.
//
// Deprecated: Use PrmObjectHash.Key instead.
func (prm *PrmObjectHash) UseKey(key ecdsa.PrivateKey) {
prm.Key = &key
// 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 *PrmObjectHash) WithinSession(t session.Token) {
x.meta.SetSessionToken(t.ToV2())
}
// WithBearerToken attaches bearer token to be used for the operation.
//
// If set, underlying eACL rules will be used in access control.
//
// Must be signed.
func (x *PrmObjectHash) WithBearerToken(t token.BearerToken) {
x.meta.SetBearerToken(t.ToV2())
}
// FromContainer specifies NeoFS container of the object.
// Required parameter.
func (x *PrmObjectHash) FromContainer(id cid.ID) {
x.addr.SetContainerID(id.ToV2())
}
// ByID specifies identifier of the requested object.
// Required parameter.
func (x *PrmObjectHash) ByID(id oid.ID) {
x.addr.SetObjectID(id.ToV2())
}
// SetRangeList sets list of ranges in (offset, length) pair format.
// Required parameter.
//
// If passed as slice, then it must not be mutated before the operation completes.
func (x *PrmObjectHash) SetRangeList(r ...uint64) {
ln := len(r)
if ln%2 != 0 {
panic("odd number of range parameters")
}
rs := make([]v2object.Range, ln/2)
for i := 0; i < ln/2; i++ {
rs[i].SetOffset(r[2*i])
rs[i].SetLength(r[2*i+1])
}
x.body.SetRanges(rs)
}
// TillichZemorAlgo changes the hash function to Tillich-Zemor
// (https://link.springer.com/content/pdf/10.1007/3-540-48658-5_5.pdf).
//
// By default, SHA256 hash function is used/.
// By default, SHA256 hash function is used.
func (x *PrmObjectHash) TillichZemorAlgo() {
x.tillichZemor = true
}
// UseSalt sets the salt to XOR the data range before hashing.
//
// Deprecated: Use PrmObjectHash.ChecksumType instead.
func (prm *PrmObjectHash) TillichZemorAlgo() {
prm.ChecksumType = checksum.TZ
// Must not be mutated before the operation completes.
func (x *PrmObjectHash) UseSalt(salt []byte) {
x.body.SetSalt(salt)
}
// 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 *PrmObjectHash) WithXHeaders(hs ...string) {
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.
@ -74,89 +120,19 @@ func (x ResObjectHash) Checksums() [][]byte {
return x.checksums
}
func (prm *PrmObjectHash) buildRequest(c *Client) (*v2object.GetRangeHashRequest, error) {
if prm.ContainerID == nil {
return nil, errorMissingContainer
}
if prm.ObjectID == nil {
return nil, errorMissingObject
}
if len(prm.XHeaders)%2 != 0 {
return nil, errorInvalidXHeaders
}
if len(prm.Ranges) == 0 {
return nil, errorMissingRanges
}
meta := new(v2session.RequestMetaHeader)
writeXHeadersToMeta(prm.XHeaders, meta)
if prm.BearerToken != nil {
v2BearerToken := new(acl.BearerToken)
prm.BearerToken.WriteToV2(v2BearerToken)
meta.SetBearerToken(v2BearerToken)
}
if prm.Session != nil {
v2SessionToken := new(v2session.Token)
prm.Session.WriteToV2(v2SessionToken)
meta.SetSessionToken(v2SessionToken)
}
if prm.Local {
meta.SetTTL(1)
}
addr := new(v2refs.Address)
cnrV2 := new(v2refs.ContainerID)
prm.ContainerID.WriteToV2(cnrV2)
addr.SetContainerID(cnrV2)
objV2 := new(v2refs.ObjectID)
prm.ObjectID.WriteToV2(objV2)
addr.SetObjectID(objV2)
rs := make([]v2object.Range, len(prm.Ranges))
for i := range prm.Ranges {
rs[i].SetOffset(prm.Ranges[i].GetOffset())
rs[i].SetLength(prm.Ranges[i].GetLength())
}
body := new(v2object.GetRangeHashRequestBody)
body.SetAddress(addr)
body.SetRanges(rs)
body.SetSalt(prm.Salt)
if prm.ChecksumType == checksum.Unknown {
body.SetType(v2refs.SHA256)
} else {
body.SetType(v2refs.ChecksumType(prm.ChecksumType))
}
req := new(v2object.GetRangeHashRequest)
req.SetBody(body)
c.prepareRequest(req, meta)
return req, nil
}
// 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
// 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.
// Any client's internal or transport errors are returned as `error`,
// If PrmInit.DisableFrostFSFailuresResolution has been called, unsuccessful
// FrostFS status codes are included in the returned result structure,
// otherwise, are also returned as `error`.
// If WithNeoFSErrorParsing option has been provided, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included
// 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.
//
// Return statuses:
@ -164,42 +140,53 @@ func (prm *PrmObjectHash) buildRequest(c *Client) (*v2object.GetRangeHashRequest
// - *apistatus.ContainerNotFound;
// - *apistatus.ObjectNotFound;
// - *apistatus.ObjectAccessDenied;
// - *apistatus.ObjectOutOfRange;
// - *apistatus.SessionTokenExpired.
func (c *Client) ObjectHash(ctx context.Context, prm PrmObjectHash) (*ResObjectHash, error) {
req, err := prm.buildRequest(c)
if err != nil {
return nil, err
switch {
case ctx == nil:
panic(panicMsgMissingContext)
case prm.addr.GetContainerID() == nil:
panic(panicMsgMissingContainer)
case prm.addr.GetObjectID() == nil:
panic("missing object")
case len(prm.body.GetRanges()) == 0:
panic("missing ranges")
}
key := c.prm.Key
if prm.Key != nil {
key = *prm.Key
// form request body
prm.body.SetAddress(&prm.addr)
// ranges and salt are already by prm setters
if prm.tillichZemor {
prm.body.SetType(v2refs.TillichZemor)
} else {
prm.body.SetType(v2refs.SHA256)
}
err = signature.SignServiceMessage(&key, req)
if err != nil {
return nil, fmt.Errorf("sign request: %w", err)
// form request
var req v2object.GetRangeHashRequest
req.SetBody(&prm.body)
req.SetMetaHeader(&prm.meta)
// init call context
var (
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()
}
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")
// process call
if !cc.processCall() {
return nil, cc.err
}
return &res, nil

View file

@ -1,270 +0,0 @@
package client
import (
"context"
"crypto/ecdsa"
"errors"
"fmt"
"io"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl"
v2object "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
)
// ObjectPatcher is designed to patch an object.
//
// Must be initialized using Client.ObjectPatchInit, any other
// usage is unsafe.
type ObjectPatcher interface {
// PatchAttributes patches attributes. Attributes can be patched no more than once,
// otherwise, the server returns an error.
//
// Result means success. Failure reason can be received via Close.
PatchAttributes(ctx context.Context, newAttrs []object.Attribute, replace bool) bool
// PatchPayload patches the object's payload.
//
// PatchPayload receives `payloadReader` and thus the payload of the patch is read and sent by chunks of
// `MaxChunkLength` length.
//
// Result means success. Failure reason can be received via Close.
PatchPayload(ctx context.Context, rng *object.Range, payloadReader io.Reader) bool
// Close ends patching the object and returns the result of the operation
// along with the final results. Must be called after using the ObjectPatcher.
//
// 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.
// If Client is tuned to resolve FrostFS API statuses, then FrostFS failures
// codes are returned as error.
//
// Return statuses:
// - global (see Client docs);
// - *apistatus.ContainerNotFound;
// - *apistatus.ContainerAccessDenied;
// - *apistatus.ObjectAccessDenied;
// - *apistatus.ObjectAlreadyRemoved;
// - *apistatus.ObjectLocked;
// - *apistatus.ObjectOutOfRange;
// - *apistatus.SessionTokenNotFound;
// - *apistatus.SessionTokenExpired.
Close(_ context.Context) (*ResObjectPatch, error)
}
// ResObjectPatch groups resulting values of ObjectPatch operation.
type ResObjectPatch struct {
statusRes
obj oid.ID
}
// ObjectID returns an object ID of the patched object.
func (r ResObjectPatch) ObjectID() oid.ID {
return r.obj
}
// PrmObjectPatch groups parameters of ObjectPatch operation.
type PrmObjectPatch struct {
XHeaders []string
Address oid.Address
BearerToken *bearer.Token
Session *session.Object
Key *ecdsa.PrivateKey
MaxChunkLength int
}
// ObjectPatchInit initializes object patcher.
func (c *Client) ObjectPatchInit(ctx context.Context, prm PrmObjectPatch) (ObjectPatcher, error) {
if len(prm.XHeaders)%2 != 0 {
return nil, errorInvalidXHeaders
}
var objectPatcher objectPatcher
stream, err := rpcapi.Patch(&c.c, &objectPatcher.respV2, client.WithContext(ctx))
if err != nil {
return nil, fmt.Errorf("open stream: %w", err)
}
objectPatcher.addr = prm.Address
objectPatcher.key = &c.prm.Key
if prm.Key != nil {
objectPatcher.key = prm.Key
}
objectPatcher.client = c
objectPatcher.stream = stream
if prm.MaxChunkLength > 0 {
objectPatcher.maxChunkLen = prm.MaxChunkLength
} else {
objectPatcher.maxChunkLen = defaultGRPCPayloadChunkLen
}
objectPatcher.req.SetBody(&v2object.PatchRequestBody{})
meta := new(v2session.RequestMetaHeader)
writeXHeadersToMeta(prm.XHeaders, meta)
if prm.BearerToken != nil {
v2BearerToken := new(acl.BearerToken)
prm.BearerToken.WriteToV2(v2BearerToken)
meta.SetBearerToken(v2BearerToken)
}
if prm.Session != nil {
v2SessionToken := new(v2session.Token)
prm.Session.WriteToV2(v2SessionToken)
meta.SetSessionToken(v2SessionToken)
}
c.prepareRequest(&objectPatcher.req, meta)
return &objectPatcher, nil
}
type objectPatcher struct {
client *Client
stream interface {
Write(*v2object.PatchRequest) error
Close() error
}
key *ecdsa.PrivateKey
res ResObjectPatch
err error
addr oid.Address
req v2object.PatchRequest
respV2 v2object.PatchResponse
maxChunkLen int
}
func (x *objectPatcher) PatchAttributes(_ context.Context, newAttrs []object.Attribute, replace bool) bool {
return x.patch(&object.Patch{
Address: x.addr,
NewAttributes: newAttrs,
ReplaceAttributes: replace,
})
}
func (x *objectPatcher) PatchPayload(_ context.Context, rng *object.Range, payloadReader io.Reader) bool {
offset := rng.GetOffset()
buf := make([]byte, x.maxChunkLen)
for patchIter := 0; ; patchIter++ {
n, err := payloadReader.Read(buf)
if err != nil && err != io.EOF {
x.err = fmt.Errorf("read payload: %w", err)
return false
}
if n == 0 {
if patchIter == 0 {
if rng.GetLength() == 0 {
x.err = errors.New("zero-length empty payload patch can't be applied")
return false
}
if !x.patch(&object.Patch{
Address: x.addr,
PayloadPatch: &object.PayloadPatch{
Range: rng,
Chunk: []byte{},
},
}) {
return false
}
}
break
}
rngPart := object.NewRange()
if patchIter == 0 {
rngPart.SetOffset(offset)
rngPart.SetLength(rng.GetLength())
} else {
rngPart.SetOffset(offset + rng.GetLength())
}
if !x.patch(&object.Patch{
Address: x.addr,
PayloadPatch: &object.PayloadPatch{
Range: rngPart,
Chunk: buf[:n],
},
}) {
return false
}
if err == io.EOF {
break
}
}
return true
}
func (x *objectPatcher) patch(patch *object.Patch) bool {
x.req.SetBody(patch.ToV2())
x.req.SetVerificationHeader(nil)
x.err = signature.SignServiceMessage(x.key, &x.req)
if x.err != nil {
x.err = fmt.Errorf("sign message: %w", x.err)
return false
}
x.err = x.stream.Write(&x.req)
return x.err == nil
}
func (x *objectPatcher) Close(_ context.Context) (*ResObjectPatch, error) {
// Ignore io.EOF error, because it is expected error for client-side
// stream termination by the server. E.g. when stream contains invalid
// message. Server returns an error in response message (in status).
if x.err != nil && !errors.Is(x.err, io.EOF) {
return nil, x.err
}
if x.err = x.stream.Close(); x.err != nil {
return nil, x.err
}
x.res.st, x.err = x.client.processResponse(&x.respV2)
if x.err != nil {
return nil, x.err
}
if !apistatus.IsSuccessful(x.res.st) {
return &x.res, nil
}
const fieldID = "ID"
idV2 := x.respV2.Body.ObjectID
if idV2 == nil {
return nil, newErrMissingResponseField(fieldID)
}
x.err = x.res.obj.ReadFromV2(*idV2)
if x.err != nil {
x.err = newErrInvalidResponseField(fieldID, x.err)
}
return &x.res, nil
}

View file

@ -1,295 +0,0 @@
package client
import (
"bytes"
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"testing"
v2object "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test"
"github.com/stretchr/testify/require"
)
type mockPatchStream struct {
streamedPayloadPatches []*object.PayloadPatch
}
func (m *mockPatchStream) Write(r *v2object.PatchRequest) error {
pp := new(object.PayloadPatch)
pp.FromV2(r.GetBody().GetPatch())
if r.GetBody().GetPatch() != nil {
bodyChunk := r.GetBody().GetPatch().Chunk
pp.Chunk = make([]byte, len(bodyChunk))
copy(pp.Chunk, bodyChunk)
}
m.streamedPayloadPatches = append(m.streamedPayloadPatches, pp)
return nil
}
func (m *mockPatchStream) Close() error {
return nil
}
func TestObjectPatcher(t *testing.T) {
type part struct {
offset int
length int
chunk string
}
for _, test := range []struct {
name string
patchPayload string
rng *object.Range
maxChunkLen int
expectParts []part
}{
{
name: "no split payload patch",
patchPayload: "011111",
rng: newRange(0, 6),
maxChunkLen: defaultGRPCPayloadChunkLen,
expectParts: []part{
{
offset: 0,
length: 6,
chunk: "011111",
},
},
},
{
name: "splitted payload patch",
patchPayload: "012345",
rng: newRange(0, 6),
maxChunkLen: 2,
expectParts: []part{
{
offset: 0,
length: 6,
chunk: "01",
},
{
offset: 6,
length: 0,
chunk: "23",
},
{
offset: 6,
length: 0,
chunk: "45",
},
},
},
{
name: "splitted payload patch with zero-length subpatches",
patchPayload: "0123456789!@",
rng: newRange(0, 4),
maxChunkLen: 2,
expectParts: []part{
{
offset: 0,
length: 4,
chunk: "01",
},
{
offset: 4,
length: 0,
chunk: "23",
},
{
offset: 4,
length: 0,
chunk: "45",
},
{
offset: 4,
length: 0,
chunk: "67",
},
{
offset: 4,
length: 0,
chunk: "89",
},
{
offset: 4,
length: 0,
chunk: "!@",
},
},
},
{
name: "splitted payload patch with zero-length subpatches only",
patchPayload: "0123456789!@",
rng: newRange(0, 0),
maxChunkLen: 2,
expectParts: []part{
{
offset: 0,
length: 0,
chunk: "01",
},
{
offset: 0,
length: 0,
chunk: "23",
},
{
offset: 0,
length: 0,
chunk: "45",
},
{
offset: 0,
length: 0,
chunk: "67",
},
{
offset: 0,
length: 0,
chunk: "89",
},
{
offset: 0,
length: 0,
chunk: "!@",
},
},
},
} {
t.Run(test.name, func(t *testing.T) {
m := &mockPatchStream{}
pk, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
patcher := objectPatcher{
client: &Client{},
stream: m,
addr: oidtest.Address(),
key: pk,
maxChunkLen: test.maxChunkLen,
}
success := patcher.PatchAttributes(context.Background(), nil, false)
require.True(t, success)
success = patcher.PatchPayload(context.Background(), test.rng, bytes.NewReader([]byte(test.patchPayload)))
require.True(t, success)
require.Len(t, m.streamedPayloadPatches, len(test.expectParts)+1)
// m.streamedPayloadPatches[0] is attribute patch, so skip it
for i, part := range test.expectParts {
requireRangeChunk(t, m.streamedPayloadPatches[i+1], part.offset, part.length, part.chunk)
}
})
}
}
func TestRepeatPayloadPatch(t *testing.T) {
t.Run("no payload patch partioning", func(t *testing.T) {
m := &mockPatchStream{}
pk, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
const maxChunkLen = 20
patcher := objectPatcher{
client: &Client{},
stream: m,
addr: oidtest.Address(),
key: pk,
maxChunkLen: maxChunkLen,
}
for _, pp := range []struct {
patchPayload string
rng *object.Range
}{
{
patchPayload: "xxxxxxxxxx",
rng: newRange(1, 6),
},
{
patchPayload: "yyyyyyyyyy",
rng: newRange(5, 9),
},
{
patchPayload: "zzzzzzzzzz",
rng: newRange(10, 0),
},
} {
success := patcher.PatchPayload(context.Background(), pp.rng, bytes.NewReader([]byte(pp.patchPayload)))
require.True(t, success)
}
requireRangeChunk(t, m.streamedPayloadPatches[0], 1, 6, "xxxxxxxxxx")
requireRangeChunk(t, m.streamedPayloadPatches[1], 5, 9, "yyyyyyyyyy")
requireRangeChunk(t, m.streamedPayloadPatches[2], 10, 0, "zzzzzzzzzz")
})
t.Run("payload patch partioning", func(t *testing.T) {
m := &mockPatchStream{}
pk, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
const maxChunkLen = 5
patcher := objectPatcher{
client: &Client{},
stream: m,
addr: oidtest.Address(),
key: pk,
maxChunkLen: maxChunkLen,
}
for _, pp := range []struct {
patchPayload string
rng *object.Range
}{
{
patchPayload: "xxxxxxxxxx",
rng: newRange(1, 6),
},
{
patchPayload: "yyyyyyyyyy",
rng: newRange(5, 9),
},
{
patchPayload: "zzzzzzzzzz",
rng: newRange(10, 0),
},
} {
success := patcher.PatchPayload(context.Background(), pp.rng, bytes.NewReader([]byte(pp.patchPayload)))
require.True(t, success)
}
requireRangeChunk(t, m.streamedPayloadPatches[0], 1, 6, "xxxxx")
requireRangeChunk(t, m.streamedPayloadPatches[1], 7, 0, "xxxxx")
requireRangeChunk(t, m.streamedPayloadPatches[2], 5, 9, "yyyyy")
requireRangeChunk(t, m.streamedPayloadPatches[3], 14, 0, "yyyyy")
requireRangeChunk(t, m.streamedPayloadPatches[4], 10, 0, "zzzzz")
requireRangeChunk(t, m.streamedPayloadPatches[5], 10, 0, "zzzzz")
})
}
func requireRangeChunk(t *testing.T, pp *object.PayloadPatch, offset, length int, chunk string) {
require.NotNil(t, pp)
require.Equal(t, uint64(offset), pp.Range.GetOffset())
require.Equal(t, uint64(length), pp.Range.GetLength())
require.Equal(t, []byte(chunk), pp.Chunk)
}
func newRange(offest, length uint64) *object.Range {
rng := &object.Range{}
rng.SetOffset(offest)
rng.SetLength(length)
return rng
}

View file

@ -3,194 +3,231 @@ package client
import (
"context"
"crypto/ecdsa"
"fmt"
buffPool "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/util/pool"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
"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/object/transformer"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
v2object "github.com/nspcc-dev/neofs-api-go/v2/object"
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
"github.com/nspcc-dev/neofs-sdk-go/object"
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
"github.com/nspcc-dev/neofs-sdk-go/session"
"github.com/nspcc-dev/neofs-sdk-go/token"
)
// defaultGRPCPayloadChunkLen default value for maxChunkLen.
// See PrmObjectPutInit.SetGRPCPayloadChunkLen for details.
const defaultGRPCPayloadChunkLen = 3 << 20
// PrmObjectPutInit groups parameters of ObjectPutInit operation.
type PrmObjectPutInit struct {
XHeaders []string
BearerToken *bearer.Token
Session *session.Object
Local bool
CopiesNumber []uint32
MaxChunkLength int
MaxSize uint64
EpochSource transformer.EpochSource
WithoutHomomorphHash bool
Key *ecdsa.PrivateKey
Pool *buffPool.BufferPool
}
// SetCopiesNumber sets number of object copies that is enough to consider put successful.
//
// Deprecated: Use PrmObjectPutInit.CopiesNumber instead.
func (x *PrmObjectPutInit) SetCopiesNumber(copiesNumber uint32) {
x.CopiesNumber = []uint32{copiesNumber}
}
// SetCopiesNumberByVectors sets ordered list of minimal required object copies numbers
// per placement vector. List's length MUST equal container's placement vector number,
// otherwise request will fail.
//
// Deprecated: Use PrmObjectPutInit.CopiesNumber instead.
func (x *PrmObjectPutInit) SetCopiesNumberByVectors(copiesNumbers []uint32) {
x.CopiesNumber = copiesNumbers
}
// SetGRPCPayloadChunkLen sets maximum chunk length value for gRPC Put request.
// Maximum chunk length restricts maximum byte length of the chunk
// transmitted in a single stream message. It depends on
// server settings and other message fields.
// If not specified or negative value set, default value of 3MiB will be used.
//
// Deprecated: Use PrmObjectPutInit.MaxChunkLength instead.
func (x *PrmObjectPutInit) SetGRPCPayloadChunkLen(v int) {
x.MaxChunkLength = v
}
// At the moment the operation is not parameterized, however,
// the structure is still declared for backward compatibility.
type PrmObjectPutInit struct{}
// ResObjectPut groups the final result values of ObjectPutInit operation.
type ResObjectPut struct {
statusRes
obj oid.ID
epoch uint64
resp v2object.PutResponse
}
// StoredObjectID returns identifier of the saved object.
func (x ResObjectPut) StoredObjectID() oid.ID {
return x.obj
// ReadStoredObjectID reads identifier of the saved object.
// Returns false if ID is missing (not read).
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
}
// StoredEpoch returns creation epoch of the saved object.
func (x ResObjectPut) StoredEpoch() uint64 {
return x.epoch
}
// ObjectWriter is designed to write one object or
// multiple parts of one object to FrostFS system.
// ObjectWriter is designed to write one object to NeoFS system.
//
// Must be initialized using Client.ObjectPutInit, any other
// usage is unsafe.
type ObjectWriter interface {
// WriteHeader writes header of the object. Result means success.
// Failure reason can be received via Close.
WriteHeader(context.Context, object.Object) bool
// WritePayloadChunk writes chunk of the object payload. Result means success.
// Failure reason can be received via Close.
WritePayloadChunk(context.Context, []byte) bool
// Close ends writing the object and returns the result of the operation
// along with the final results. Must be called after using the ObjectWriter.
//
// 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.
// If Client is tuned to resolve FrostFS API statuses, then FrostFS failures
// codes are returned as error.
//
// Return statuses:
// - global (see Client docs);
// - *apistatus.ContainerNotFound;
// - *apistatus.ObjectAccessDenied;
// - *apistatus.ObjectLocked;
// - *apistatus.LockNonRegularObject;
// - *apistatus.SessionTokenNotFound;
// - *apistatus.SessionTokenExpired.
Close(context.Context) (*ResObjectPut, error)
type ObjectWriter struct {
cancelCtxStream context.CancelFunc
ctxCall contextCall
// initially bound tp contextCall
metaHdr v2session.RequestMetaHeader
// initially bound to contextCall
partInit v2object.PutObjectPartInit
chunkCalled bool
partChunk v2object.PutObjectPartChunk
}
// UseKey specifies private key to sign the requests.
// If key is not provided, then Client default key is used.
//
// Deprecated: Use PrmObjectPutInit.Key instead.
func (x *PrmObjectPutInit) UseKey(key ecdsa.PrivateKey) {
x.Key = &key
func (x *ObjectWriter) UseKey(key ecdsa.PrivateKey) {
x.ctxCall.key = key
}
// WithBearerToken attaches bearer token to be used for the operation.
// Should be called once before any writing steps.
//
// Deprecated: Use PrmObjectPutInit.BearerToken instead.
func (x *PrmObjectPutInit) WithBearerToken(t bearer.Token) {
x.BearerToken = &t
func (x *ObjectWriter) WithBearerToken(t token.BearerToken) {
x.metaHdr.SetBearerToken(t.ToV2())
}
// WithinSession specifies session within which object should be stored.
// Should be called once before any writing steps.
//
// Deprecated: Use PrmObjectPutInit.Session instead.
func (x *PrmObjectPutInit) WithinSession(t session.Object) {
x.Session = &t
func (x *ObjectWriter) WithinSession(t session.Token) {
x.metaHdr.SetSessionToken(t.ToV2())
}
// MarkLocal tells the server to execute the operation locally.
//
// Deprecated: Use PrmObjectPutInit.Local instead.
func (x *PrmObjectPutInit) MarkLocal() {
x.Local = true
func (x *ObjectWriter) MarkLocal() {
x.metaHdr.SetTTL(1)
}
// 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.
//
// Deprecated: Use PrmObjectPutInit.XHeaders instead.
func (x *PrmObjectPutInit) WithXHeaders(hs ...string) {
x.XHeaders = hs
func (x *ObjectWriter) WithXHeaders(hs ...string) {
if len(hs)%2 != 0 {
panic("slice of X-Headers with odd length")
}
prmCommonMeta{xHeaders: hs}.writeToMetaHeader(&x.metaHdr)
}
// WithObjectMaxSize specifies max object size value and use it during object splitting.
// When specified, start writing to the stream only after the object is formed.
// Continue processing the input only when the previous formed object has been successfully written.
//
// Deprecated: Use PrmObjectPutInit.MaxSize instead.
func (x *PrmObjectPutInit) WithObjectMaxSize(maxSize uint64) {
x.MaxSize = maxSize
// WriteHeader writes header of the object. Result means success.
// Failure reason can be received via Close.
func (x *ObjectWriter) WriteHeader(hdr object.Object) bool {
v2Hdr := hdr.ToV2()
x.partInit.SetObjectID(v2Hdr.GetObjectID())
x.partInit.SetHeader(v2Hdr.GetHeader())
x.partInit.SetSignature(v2Hdr.GetSignature())
return x.ctxCall.writeRequest()
}
// WithoutHomomorphicHash if set to true do not use Tillich-Zémor hash for payload.
//
// Deprecated: Use PrmObjectPutInit.WithoutHomomorphHash instead.
func (x *PrmObjectPutInit) WithoutHomomorphicHash(v bool) {
x.WithoutHomomorphHash = v
// WritePayloadChunk writes chunk of the object payload. Result means success.
// Failure reason can be received via Close.
func (x *ObjectWriter) WritePayloadChunk(chunk []byte) bool {
if !x.chunkCalled {
x.chunkCalled = true
x.ctxCall.req.(*v2object.PutRequest).GetBody().SetObjectPart(&x.partChunk)
}
for ln := len(chunk); ln > 0; ln = len(chunk) {
// maxChunkLen restricts maximum byte length of the chunk
// transmitted in a single stream message. It depends on
// server settings and other message fields, but for now
// we simply assume that 3MB is large enough to reduce the
// number of messages, and not to exceed the limit
// (4MB by default for gRPC servers).
const maxChunkLen = 3 << 20
if ln > maxChunkLen {
ln = maxChunkLen
}
// we deal with size limit overflow above, but there is another case:
// what if method is called with "small" chunk many times? We write
// a message to the stream on each call. Alternatively, we could use buffering.
// In most cases, the chunk length does not vary between calls. Given this
// assumption, as well as the length of the payload from the header, it is
// possible to buffer the data of intermediate chunks, and send a message when
// the allocated buffer is filled, or when the last chunk is received.
// It is mentally assumed that allocating and filling the buffer is better than
// synchronous sending, but this needs to be tested.
x.partChunk.SetChunk(chunk[:ln])
if !x.ctxCall.writeRequest() {
return false
}
chunk = chunk[ln:]
}
return true
}
// WithEpochSource specifies epoch for object when split it on client side.
// Close ends writing the object and returns the result of the operation
// along with the final results. Must be called after using the ObjectWriter.
//
// Deprecated: Use PrmObjectPutInit.EpochSource instead.
func (x *PrmObjectPutInit) WithEpochSource(es transformer.EpochSource) {
x.EpochSource = es
// 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.
// If Client is tuned to resolve NeoFS API statuses, then NeoFS failures
// codes are returned as error.
//
// Return statuses:
// - global (see Client docs);
// - *apistatus.ContainerNotFound;
// - *apistatus.ObjectAccessDenied;
// - *apistatus.ObjectLocked;
// - *apistatus.LockNonRegularObject;
// - *apistatus.SessionTokenNotFound;
// - *apistatus.SessionTokenExpired.
func (x *ObjectWriter) Close() (*ResObjectPut, error) {
defer x.cancelCtxStream()
if x.ctxCall.err != nil {
return nil, x.ctxCall.err
}
if !x.ctxCall.close() {
return nil, x.ctxCall.err
}
if !x.ctxCall.processResponse() {
return nil, x.ctxCall.err
}
return x.ctxCall.statusRes.(*ResObjectPut), 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.
// 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.
func (c *Client) ObjectPutInit(ctx context.Context, prm PrmObjectPutInit) (ObjectWriter, error) {
if prm.MaxSize > 0 {
return c.objectPutInitTransformer(prm)
func (c *Client) ObjectPutInit(ctx context.Context, _ PrmObjectPutInit) (*ObjectWriter, error) {
// check parameters
if ctx == nil {
panic(panicMsgMissingContext)
}
return c.objectPutInitRaw(ctx, prm)
// open stream
var (
res ResObjectPut
w ObjectWriter
)
ctx, w.cancelCtxStream = context.WithCancel(ctx)
stream, err := rpcapi.PutObject(&c.c, &res.resp, client.WithContext(ctx))
if err != nil {
return nil, fmt.Errorf("open stream: %w", err)
}
// form request body
var body v2object.PutRequestBody
// 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.ctxCall.closer = stream.Close
return &w, nil
}

View file

@ -1,181 +0,0 @@
package client
import (
"context"
"crypto/ecdsa"
"errors"
"fmt"
"io"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl"
v2object "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
)
func (c *Client) objectPutInitRaw(ctx context.Context, prm PrmObjectPutInit) (*objectWriterRaw, error) {
if len(prm.XHeaders)%2 != 0 {
return nil, errorInvalidXHeaders
}
var w objectWriterRaw
stream, err := rpcapi.PutObject(&c.c, &w.respV2, client.WithContext(ctx))
if err != nil {
return nil, fmt.Errorf("open stream: %w", err)
}
w.key = &c.prm.Key
if prm.Key != nil {
w.key = prm.Key
}
w.client = c
w.stream = stream
w.partInit.SetCopiesNumber(prm.CopiesNumber)
w.req.SetBody(new(v2object.PutRequestBody))
if prm.MaxChunkLength > 0 {
w.maxChunkLen = prm.MaxChunkLength
} else {
w.maxChunkLen = defaultGRPCPayloadChunkLen
}
meta := new(v2session.RequestMetaHeader)
writeXHeadersToMeta(prm.XHeaders, meta)
if prm.BearerToken != nil {
v2BearerToken := new(acl.BearerToken)
prm.BearerToken.WriteToV2(v2BearerToken)
meta.SetBearerToken(v2BearerToken)
}
if prm.Session != nil {
v2SessionToken := new(v2session.Token)
prm.Session.WriteToV2(v2SessionToken)
meta.SetSessionToken(v2SessionToken)
}
if prm.Local {
meta.SetTTL(1)
}
c.prepareRequest(&w.req, meta)
return &w, nil
}
type objectWriterRaw struct {
client *Client
stream interface {
Write(*v2object.PutRequest) error
Close() error
}
key *ecdsa.PrivateKey
res ResObjectPut
err error
chunkCalled bool
respV2 v2object.PutResponse
req v2object.PutRequest
partInit v2object.PutObjectPartInit
partChunk v2object.PutObjectPartChunk
maxChunkLen int
}
func (x *objectWriterRaw) WriteHeader(_ context.Context, hdr object.Object) bool {
v2Hdr := hdr.ToV2()
x.partInit.SetObjectID(v2Hdr.GetObjectID())
x.partInit.SetHeader(v2Hdr.GetHeader())
x.partInit.SetSignature(v2Hdr.GetSignature())
x.req.GetBody().SetObjectPart(&x.partInit)
x.req.SetVerificationHeader(nil)
x.err = signature.SignServiceMessage(x.key, &x.req)
if x.err != nil {
x.err = fmt.Errorf("sign message: %w", x.err)
return false
}
x.err = x.stream.Write(&x.req)
return x.err == nil
}
func (x *objectWriterRaw) WritePayloadChunk(_ context.Context, chunk []byte) bool {
if !x.chunkCalled {
x.chunkCalled = true
x.req.GetBody().SetObjectPart(&x.partChunk)
}
for ln := len(chunk); ln > 0; ln = len(chunk) {
if ln > x.maxChunkLen {
ln = x.maxChunkLen
}
// we deal with size limit overflow above, but there is another case:
// what if method is called with "small" chunk many times? We write
// a message to the stream on each call. Alternatively, we could use buffering.
// In most cases, the chunk length does not vary between calls. Given this
// assumption, as well as the length of the payload from the header, it is
// possible to buffer the data of intermediate chunks, and send a message when
// the allocated buffer is filled, or when the last chunk is received.
// It is mentally assumed that allocating and filling the buffer is better than
// synchronous sending, but this needs to be tested.
x.partChunk.SetChunk(chunk[:ln])
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)
if x.err != nil {
return false
}
chunk = chunk[ln:]
}
return true
}
func (x *objectWriterRaw) Close(_ context.Context) (*ResObjectPut, error) {
// Ignore io.EOF error, because it is expected error for client-side
// stream termination by the server. E.g. when stream contains invalid
// message. Server returns an error in response message (in status).
if x.err != nil && !errors.Is(x.err, io.EOF) {
return nil, x.err
}
if x.err = x.stream.Close(); x.err != nil {
return nil, x.err
}
x.res.st, x.err = x.client.processResponse(&x.respV2)
if x.err != nil {
return nil, x.err
}
if !apistatus.IsSuccessful(x.res.st) {
return &x.res, nil
}
const fieldID = "ID"
idV2 := x.respV2.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)
}
x.res.epoch = x.respV2.GetMetaHeader().GetEpoch()
return &x.res, nil
}

View file

@ -1,175 +0,0 @@
package client
import (
"context"
"crypto/ecdsa"
"fmt"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl"
v2object "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
)
// PrmObjectPutSingle groups parameters of PutSingle operation.
type PrmObjectPutSingle struct {
XHeaders []string
BearerToken *bearer.Token
Session *session.Object
Local bool
CopiesNumber []uint32
Object *object.Object
Key *ecdsa.PrivateKey
}
// SetCopiesNumber sets ordered list of minimal required object copies numbers
// per placement vector. List's length MUST equal container's placement vector number,
// otherwise request will fail.
//
// Deprecated: Use PrmObjectPutSingle.CopiesNumber instead.
func (prm *PrmObjectPutSingle) SetCopiesNumber(v []uint32) {
prm.CopiesNumber = v
}
// UseKey specifies private key to sign the requests.
// If key is not provided, then Client default key is used.
//
// Deprecated: Use PrmObjectPutSingle.Key instead.
func (prm *PrmObjectPutSingle) UseKey(key *ecdsa.PrivateKey) {
prm.Key = key
}
// WithBearerToken attaches bearer token to be used for the operation.
// Should be called once before any writing steps.
//
// Deprecated: Use PrmObjectPutSingle.BearerToken instead.
func (prm *PrmObjectPutSingle) WithBearerToken(t bearer.Token) {
prm.BearerToken = &t
}
// WithinSession specifies session within which object should be stored.
// Should be called once before any writing steps.
//
// Deprecated: Use PrmObjectPutSingle.Session instead.
func (prm *PrmObjectPutSingle) WithinSession(t session.Object) {
prm.Session = &t
}
// ExecuteLocal tells the server to execute the operation locally.
//
// Deprecated: Use PrmObjectPutSingle.Local instead.
func (prm *PrmObjectPutSingle) ExecuteLocal() {
prm.Local = true
}
// 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.
//
// Deprecated: Use PrmObjectPutSingle.XHeaders instead.
func (prm *PrmObjectPutSingle) WithXHeaders(hs ...string) {
prm.XHeaders = hs
}
// SetObject specifies prepared object to put.
//
// Deprecated: Use PrmObjectPutSingle.Object instead.
func (prm *PrmObjectPutSingle) SetObject(o *v2object.Object) {
prm.Object = object.NewFromV2(o)
}
// ResObjectPutSingle groups resulting values of PutSingle operation.
type ResObjectPutSingle struct {
statusRes
epoch uint64
}
// Epoch returns creation epoch of the saved object.
func (r *ResObjectPutSingle) Epoch() uint64 {
return r.epoch
}
func (prm *PrmObjectPutSingle) buildRequest(c *Client) (*v2object.PutSingleRequest, error) {
if len(prm.XHeaders)%2 != 0 {
return nil, errorInvalidXHeaders
}
body := new(v2object.PutSingleRequestBody)
body.SetCopiesNumber(prm.CopiesNumber)
body.SetObject(prm.Object.ToV2())
meta := new(v2session.RequestMetaHeader)
writeXHeadersToMeta(prm.XHeaders, meta)
if prm.BearerToken != nil {
v2BearerToken := new(acl.BearerToken)
prm.BearerToken.WriteToV2(v2BearerToken)
meta.SetBearerToken(v2BearerToken)
}
if prm.Session != nil {
v2SessionToken := new(v2session.Token)
prm.Session.WriteToV2(v2SessionToken)
meta.SetSessionToken(v2SessionToken)
}
if prm.Local {
meta.SetTTL(1)
}
req := &v2object.PutSingleRequest{}
req.SetBody(body)
c.prepareRequest(req, meta)
return req, nil
}
// ObjectPutSingle writes prepared object to FrostFS.
// Object must have payload, also containerID, objectID, ownerID, payload hash, payload length of an object must be set.
// 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.
// If Client is tuned to resolve FrostFS API statuses, then FrostFS failures
// codes are returned as error.
func (c *Client) ObjectPutSingle(ctx context.Context, prm PrmObjectPutSingle) (*ResObjectPutSingle, error) {
req, err := prm.buildRequest(c)
if err != nil {
return nil, err
}
key := &c.prm.Key
if prm.Key != nil {
key = prm.Key
}
err = signature.SignServiceMessage(key, req)
if err != nil {
return nil, fmt.Errorf("sign request: %w", err)
}
resp, err := rpcapi.PutSingleObject(&c.c, req, client.WithContext(ctx))
if err != nil {
return nil, err
}
var res ResObjectPutSingle
res.st, err = c.processResponse(resp)
if err != nil {
return nil, err
}
res.epoch = resp.GetMetaHeader().GetEpoch()
return &res, nil
}

View file

@ -1,144 +0,0 @@
package client
import (
"context"
buffPool "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/util/pool"
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/transformer"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func (c *Client) objectPutInitTransformer(prm PrmObjectPutInit) (*objectWriterTransformer, error) {
var w objectWriterTransformer
w.it = internalTarget{
client: c,
prm: prm,
}
key := &c.prm.Key
if prm.Key != nil {
key = prm.Key
}
w.ot = transformer.NewPayloadSizeLimiter(transformer.Params{
Key: key,
NextTargetInit: func() transformer.ObjectWriter { return &w.it },
MaxSize: prm.MaxSize,
WithoutHomomorphicHash: prm.WithoutHomomorphHash,
NetworkState: prm.EpochSource,
Pool: prm.Pool,
})
return &w, nil
}
type objectWriterTransformer struct {
ot transformer.ChunkedObjectWriter
it internalTarget
err error
}
func (x *objectWriterTransformer) WriteHeader(ctx context.Context, hdr object.Object) bool {
x.err = x.ot.WriteHeader(ctx, &hdr)
return x.err == nil
}
func (x *objectWriterTransformer) WritePayloadChunk(ctx context.Context, chunk []byte) bool {
_, x.err = x.ot.Write(ctx, chunk)
return x.err == nil
}
func (x *objectWriterTransformer) Close(ctx context.Context) (*ResObjectPut, error) {
if x.err != nil {
return nil, x.err
}
ai, err := x.ot.Close(ctx)
if err != nil {
return nil, err
}
if ai != nil {
x.it.res.epoch = ai.Epoch
if ai.ParentID != nil {
x.it.res.obj = *ai.ParentID
}
}
return x.it.res, nil
}
type internalTarget struct {
client *Client
res *ResObjectPut
prm PrmObjectPutInit
useStream bool
}
func (it *internalTarget) WriteObject(ctx context.Context, o *object.Object) error {
putSingleImplemented, err := it.tryPutSingle(ctx, o)
if putSingleImplemented {
return err
}
it.useStream = true
return it.putAsStream(ctx, o)
}
func (it *internalTarget) putAsStream(ctx context.Context, o *object.Object) error {
wrt, err := it.client.objectPutInitRaw(ctx, it.prm)
if err != nil {
return err
}
if wrt.WriteHeader(ctx, *o) {
wrt.WritePayloadChunk(ctx, o.Payload())
}
it.res, err = wrt.Close(ctx)
if err == nil && it.client.prm.DisableFrostFSErrorResolution && !apistatus.IsSuccessful(it.res.st) {
err = apistatus.ErrFromStatus(it.res.st)
}
return err
}
func (it *internalTarget) tryPutSingle(ctx context.Context, o *object.Object) (bool, error) {
if it.useStream {
return false, nil
}
prm := PrmObjectPutSingle{
XHeaders: it.prm.XHeaders,
BearerToken: it.prm.BearerToken,
Session: it.prm.Session,
Local: it.prm.Local,
CopiesNumber: it.prm.CopiesNumber,
Object: o,
Key: it.prm.Key,
}
res, err := it.client.ObjectPutSingle(ctx, prm)
if err != nil && status.Code(err) == codes.Unimplemented {
return false, err
}
if err == nil {
it.returnBuffPool(o.Payload())
id, _ := o.ID()
it.res = &ResObjectPut{
statusRes: res.statusRes,
obj: id,
epoch: res.epoch,
}
if it.client.prm.DisableFrostFSErrorResolution && !apistatus.IsSuccessful(it.res.st) {
return true, apistatus.ErrFromStatus(it.res.st)
}
return true, nil
}
return true, err
}
func (it *internalTarget) returnBuffPool(playback []byte) {
if it.prm.Pool == nil {
return
}
var buffer buffPool.Buffer
buffer.Data = playback
it.prm.Pool.Put(&buffer)
}

View file

@ -7,43 +7,40 @@ import (
"fmt"
"io"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl"
v2object "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
v2refs "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
"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"
v2object "github.com/nspcc-dev/neofs-api-go/v2/object"
v2refs "github.com/nspcc-dev/neofs-api-go/v2/refs"
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
"github.com/nspcc-dev/neofs-sdk-go/object"
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
"github.com/nspcc-dev/neofs-sdk-go/session"
"github.com/nspcc-dev/neofs-sdk-go/token"
)
// PrmObjectSearch groups parameters of ObjectSearch operation.
type PrmObjectSearch struct {
XHeaders []string
prmCommonMeta
Local bool
local bool
BearerToken *bearer.Token
sessionSet bool
session session.Token
Session *session.Object
bearerSet bool
bearer token.BearerToken
ContainerID *cid.ID
cnrSet bool
cnr cid.ID
Key *ecdsa.PrivateKey
Filters object.SearchFilters
filters object.SearchFilters
}
// MarkLocal tells the server to execute the operation locally.
//
// Deprecated: Use PrmObjectSearch.Local instead.
func (x *PrmObjectSearch) MarkLocal() {
x.Local = true
x.local = true
}
// WithinSession specifies session within which the search query must be executed.
@ -52,10 +49,9 @@ func (x *PrmObjectSearch) MarkLocal() {
// This may affect the execution of an operation (e.g. access control).
//
// Must be signed.
//
// Deprecated: Use PrmObjectSearch.Session instead.
func (x *PrmObjectSearch) WithinSession(t session.Object) {
x.Session = &t
func (x *PrmObjectSearch) WithinSession(t session.Token) {
x.session = t
x.sessionSet = true
}
// WithBearerToken attaches bearer token to be used for the operation.
@ -63,44 +59,22 @@ func (x *PrmObjectSearch) WithinSession(t session.Object) {
// If set, underlying eACL rules will be used in access control.
//
// Must be signed.
//
// Deprecated: Use PrmObjectSearch.BearerToken instead.
func (x *PrmObjectSearch) WithBearerToken(t bearer.Token) {
x.BearerToken = &t
}
// 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.
//
// Deprecated: Use PrmObjectSearch.XHeaders instead.
func (x *PrmObjectSearch) WithXHeaders(hs ...string) {
x.XHeaders = hs
}
// UseKey specifies private key to sign the requests.
// If key is not provided, then Client default key is used.
//
// Deprecated: Use PrmObjectSearch.Key instead.
func (x *PrmObjectSearch) UseKey(key ecdsa.PrivateKey) {
x.Key = &key
func (x *PrmObjectSearch) WithBearerToken(t token.BearerToken) {
x.bearer = t
x.bearerSet = true
}
// InContainer specifies the container in which to look for objects.
// Required parameter.
//
// Deprecated: Use PrmObjectSearch.ContainerID instead.
func (x *PrmObjectSearch) InContainer(id cid.ID) {
x.ContainerID = &id
x.cnr = id
x.cnrSet = true
}
// SetFilters sets filters by which to select objects. All container objects
// match unset/empty filters.
//
// Deprecated: Use PrmObjectSearch.Filters instead.
func (x *PrmObjectSearch) SetFilters(filters object.SearchFilters) {
x.Filters = filters
x.filters = filters
}
// ResObjectSearch groups the final result values of ObjectSearch operation.
@ -108,20 +82,28 @@ type ResObjectSearch struct {
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.
type ObjectListReader struct {
client *Client
cancelCtxStream context.CancelFunc
err error
res ResObjectSearch
stream interface {
Read(resp *v2object.SearchResponse) error
}
ctxCall contextCall
reqWritten bool
// initially bound to contextCall
bodyResp v2object.SearchResponseBody
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
// io.Reader.Read but copies oid.ID and returns success flag instead of error.
//
@ -133,33 +115,58 @@ func (x *ObjectListReader) Read(buf []oid.ID) (int, bool) {
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:]
if len(buf) == read {
return read, true
}
for {
var resp v2object.SearchResponse
x.err = x.stream.Read(&resp)
if x.err != nil {
return read, false
}
var ok bool
var ids []v2refs.ObjectID
var i, ln, rem int
x.res.st, x.err = x.client.processResponse(&resp)
if x.err != nil || !apistatus.IsSuccessful(x.res.st) {
for {
// receive next message
ok = x.ctxCall.readResponse()
if !ok {
return read, false
}
// read new chunk of objects
ids := resp.GetBody().GetIDList()
if len(ids) == 0 {
ids = x.bodyResp.GetIDList()
ln = len(ids)
if ln == 0 {
// just skip empty lists since they are not prohibited by protocol
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
if read == len(buf) {
@ -171,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.
// f can return true to stop iteration earlier.
//
@ -208,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.
// 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.
//
// Return statuses:
@ -219,87 +218,89 @@ func (x *ObjectListReader) Iterate(f func(oid.ID) bool) error {
func (x *ObjectListReader) Close() (*ResObjectSearch, error) {
defer x.cancelCtxStream()
if x.err != nil && !errors.Is(x.err, io.EOF) {
return nil, x.err
if x.ctxCall.err != nil && !errors.Is(x.ctxCall.err, io.EOF) {
return nil, x.ctxCall.err
}
return &x.res, nil
return x.ctxCall.statusRes.(*ResObjectSearch), nil
}
func (x *PrmObjectSearch) buildRequest(c *Client) (*v2object.SearchRequest, error) {
if x.ContainerID == nil {
return nil, errorMissingContainer
}
if len(x.XHeaders)%2 != 0 {
return nil, errorInvalidXHeaders
}
meta := new(v2session.RequestMetaHeader)
writeXHeadersToMeta(x.XHeaders, meta)
if x.BearerToken != nil {
v2BearerToken := new(acl.BearerToken)
x.BearerToken.WriteToV2(v2BearerToken)
meta.SetBearerToken(v2BearerToken)
}
if x.Session != nil {
v2SessionToken := new(v2session.Token)
x.Session.WriteToV2(v2SessionToken)
meta.SetSessionToken(v2SessionToken)
}
if x.Local {
meta.SetTTL(1)
}
cnrV2 := new(v2refs.ContainerID)
x.ContainerID.WriteToV2(cnrV2)
body := new(v2object.SearchRequestBody)
body.SetVersion(1)
body.SetContainerID(cnrV2)
body.SetFilters(x.Filters.ToV2())
req := new(v2object.SearchRequest)
req.SetBody(body)
c.prepareRequest(req, meta)
return req, 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
// is done using the ObjectListReader. Exactly one return value is non-nil.
// 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.
func (c *Client) ObjectSearchInit(ctx context.Context, prm PrmObjectSearch) (*ObjectListReader, error) {
req, err := prm.buildRequest(c)
if err != nil {
return nil, err
// check parameters
switch {
case ctx == nil:
panic(panicMsgMissingContext)
case !prm.cnrSet:
panic(panicMsgMissingContainer)
}
key := prm.Key
if key == nil {
key = &c.prm.Key
// form request body
var body v2object.SearchRequestBody
body.SetVersion(1)
body.SetContainerID(prm.cnr.ToV2())
body.SetFilters(prm.filters.ToV2())
// form meta header
var meta v2session.RequestMetaHeader
if prm.local {
meta.SetTTL(1)
}
err = signature.SignServiceMessage(key, req)
if err != nil {
return nil, fmt.Errorf("sign request: %w", err)
if prm.bearerSet {
meta.SetBearerToken(prm.bearer.ToV2())
}
var r ObjectListReader
if prm.sessionSet {
meta.SetSessionToken(prm.session.ToV2())
}
prm.prmCommonMeta.writeToMetaHeader(&meta)
// form request
var req v2object.SearchRequest
req.SetBody(&body)
req.SetMetaHeader(&meta)
// init reader
var (
r ObjectListReader
resp v2object.SearchResponse
stream *rpcapi.SearchResponseReader
)
ctx, r.cancelCtxStream = context.WithCancel(ctx)
r.stream, err = rpcapi.SearchObjects(&c.c, req, client.WithContext(ctx))
if err != nil {
return nil, fmt.Errorf("open stream: %w", err)
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 {
return fmt.Errorf("open stream: %w", err)
}
return nil
}
r.ctxCall.rResp = func() error {
return stream.Read(&resp)
}
r.client = c
return &r, nil
}

View file

@ -1,28 +1,26 @@
package client
import (
"crypto/ecdsa"
"errors"
"fmt"
"io"
"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/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"
)
func TestObjectSearch(t *testing.T) {
ids := make([]oid.ID, 20)
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)
checkRead := func(t *testing.T, expected []oid.ID) {
@ -36,23 +34,38 @@ func TestObjectSearch(t *testing.T) {
require.Panics(t, func() { resp.Read(nil) })
// both ID fetched
resp.stream = newSearchStream(p, nil, ids[:3])
setID(ids[:3])
checkRead(t, ids[:2])
// one ID cached, second fetched
resp.stream = newSearchStream(p, nil, ids[3:6])
setID(ids[3:6])
checkRead(t, ids[2:4])
// both ID cached
resp.stream = nil // shouldn't be called, panic if so
resp.ctxCall.resp = nil
checkRead(t, ids[4:6])
// 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])
// read from tail multiple times
resp.stream = newSearchStream(p, nil, ids[8:11])
resp.ctxCall.rResp = nil
setID(ids[8:11])
buf = buf[:1]
checkRead(t, ids[8:9])
checkRead(t, ids[9:10])
@ -60,20 +73,43 @@ func TestObjectSearch(t *testing.T) {
// handle EOF
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])
}
func TestObjectIterate(t *testing.T) {
ids := make([]oid.ID, 3)
for i := range ids {
ids[i] = oidtest.ID()
ids[i] = *oidtest.ID()
}
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
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)
})
t.Run("stop by return value", func(t *testing.T) {
p, resp := testListReaderResponse(t)
resp, setID := testListReaderResponse(t)
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 {
actual = append(actual, id)
return len(actual) == 2
@ -94,12 +130,22 @@ func TestObjectIterate(t *testing.T) {
require.Equal(t, ids[:2], actual)
})
t.Run("stop after error", func(t *testing.T) {
p, resp := testListReaderResponse(t)
resp, setID := testListReaderResponse(t)
expectedErr := errors.New("test error")
resp.stream = newSearchStream(p, expectedErr, ids[:2])
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 {
actual = append(actual, id)
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()
require.NoError(t, err)
return &p.PrivateKey, &ObjectListReader{
obj := &ObjectListReader{
cancelCtxStream: func() {},
client: &Client{},
tail: nil,
ctxCall: contextCall{
closer: func() error { return nil },
result: func(v2 responseV2) {},
statusRes: new(ResObjectSearch),
},
reqWritten: true,
bodyResp: object.SearchResponseBody{},
tail: nil,
}
}
func newSearchStream(key *ecdsa.PrivateKey, endError error, idList ...[]oid.ID) *singleStreamResponder {
return &singleStreamResponder{
key: key,
endError: endError,
idList: idList,
}
}
return obj, func(id []oid.ID) *object.SearchResponse {
resp := new(object.SearchResponse)
resp.SetBody(new(object.SearchResponseBody))
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
v2id := make([]refs.ObjectID, len(id))
for i := range id {
v2id[i] = *id[i].ToV2()
}
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])
resp.GetBody().SetIDList(v2id)
err := signatureV2.SignServiceMessage(&p.PrivateKey, resp)
if err != nil {
t.Fatalf("error: %v", err)
}
body.SetIDList(ids)
obj.ctxCall.resp = resp
obj.bodyResp = *resp.GetBody()
return resp
}
resp.SetBody(&body)
err := signatureV2.SignServiceMessage(s.key, resp)
if err != nil {
panic(fmt.Errorf("error: %w", err))
}
s.n++
return nil
}

194
client/reputation.go Normal file
View file

@ -0,0 +1,194 @@
package client
import (
"context"
v2reputation "github.com/nspcc-dev/neofs-api-go/v2/reputation"
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
"github.com/nspcc-dev/neofs-sdk-go/reputation"
)
// PrmAnnounceLocalTrust groups parameters of AnnounceLocalTrust operation.
type PrmAnnounceLocalTrust struct {
prmCommonMeta
epoch uint64
trusts []reputation.Trust
}
// SetEpoch sets number of NeoFS epoch in which the trust was assessed.
// Required parameter, must not be zero.
func (x *PrmAnnounceLocalTrust) SetEpoch(epoch uint64) {
x.epoch = epoch
}
// SetValues sets values describing trust of the client to the NeoFS network participants.
// Required parameter. Must not be empty.
//
// Must not be mutated before the end of the operation.
func (x *PrmAnnounceLocalTrust) SetValues(trusts []reputation.Trust) {
x.trusts = trusts
}
// ResAnnounceLocalTrust groups results of AnnounceLocalTrust operation.
type ResAnnounceLocalTrust struct {
statusRes
}
// 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.
// Any client's internal or transport errors are returned as `error`.
// If WithNeoFSErrorParsing option has been provided, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure.
//
// Immediately panics if parameters are set incorrectly (see PrmAnnounceLocalTrust docs).
// Context is required and must not be nil. It is used for network communication.
//
// Return statuses:
// - global (see Client docs).
func (c *Client) AnnounceLocalTrust(ctx context.Context, prm PrmAnnounceLocalTrust) (*ResAnnounceLocalTrust, error) {
// check parameters
switch {
case ctx == nil:
panic(panicMsgMissingContext)
case prm.epoch == 0:
panic("zero epoch")
case len(prm.trusts) == 0:
panic("missing trusts")
}
// form request body
reqBody := new(v2reputation.AnnounceLocalTrustRequestBody)
reqBody.SetEpoch(prm.epoch)
trusts := make([]reputation.Trust, len(prm.trusts))
copy(trusts, prm.trusts)
reqBody.SetTrusts(reputation.TrustsToV2(trusts))
// form request
var req v2reputation.AnnounceLocalTrustRequest
req.SetBody(reqBody)
// init call context
var (
cc contextCall
res ResAnnounceLocalTrust
)
c.initCallContext(&cc)
cc.meta = prm.prmCommonMeta
cc.req = &req
cc.statusRes = &res
cc.call = func() (responseV2, error) {
return rpcapi.AnnounceLocalTrust(&c.c, &req, client.WithContext(ctx))
}
// process call
if !cc.processCall() {
return nil, cc.err
}
return &res, nil
}
// PrmAnnounceIntermediateTrust groups parameters of AnnounceIntermediateTrust operation.
type PrmAnnounceIntermediateTrust struct {
prmCommonMeta
epoch uint64
iter uint32
trustSet bool
trust reputation.PeerToPeerTrust
}
// SetEpoch sets number of NeoFS epoch with which client's calculation algorithm is initialized.
// Required parameter, must not be zero.
func (x *PrmAnnounceIntermediateTrust) SetEpoch(epoch uint64) {
x.epoch = epoch
}
// SetIteration sets current sequence number of the client's calculation algorithm.
// By default, corresponds to initial (zero) iteration.
func (x *PrmAnnounceIntermediateTrust) SetIteration(iter uint32) {
x.iter = iter
}
// SetCurrentValue sets current global trust value computed at the specified iteration
// of the client's calculation algorithm. Required parameter.
func (x *PrmAnnounceIntermediateTrust) SetCurrentValue(trust reputation.PeerToPeerTrust) {
x.trust = trust
x.trustSet = true
}
// ResAnnounceIntermediateTrust groups results of AnnounceIntermediateTrust operation.
type ResAnnounceIntermediateTrust struct {
statusRes
}
// AnnounceIntermediateTrust sends global trust values calculated for the specified NeoFS network participants
// at some stage of client's calculation algorithm.
//
// 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`.
// If WithNeoFSErrorParsing option has been provided, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included
// in the returned result structure.
//
// Immediately panics if parameters are set incorrectly (see PrmAnnounceIntermediateTrust docs).
// Context is required and must not be nil. It is used for network communication.
//
// Return statuses:
// - global (see Client docs).
func (c *Client) AnnounceIntermediateTrust(ctx context.Context, prm PrmAnnounceIntermediateTrust) (*ResAnnounceIntermediateTrust, error) {
// check parameters
switch {
case ctx == nil:
panic(panicMsgMissingContext)
case prm.epoch == 0:
panic("zero epoch")
case !prm.trustSet:
panic("current trust value not set")
}
// form request body
reqBody := new(v2reputation.AnnounceIntermediateResultRequestBody)
reqBody.SetEpoch(prm.epoch)
reqBody.SetIteration(prm.iter)
reqBody.SetTrust(prm.trust.ToV2())
// form request
var req v2reputation.AnnounceIntermediateResultRequest
req.SetBody(reqBody)
// init call context
var (
cc contextCall
res ResAnnounceIntermediateTrust
)
c.initCallContext(&cc)
cc.meta = prm.prmCommonMeta
cc.req = &req
cc.statusRes = &res
cc.call = func() (responseV2, error) {
return rpcapi.AnnounceIntermediateResult(&c.c, &req, client.WithContext(ctx))
}
// process call
if !cc.processCall() {
return nil, cc.err
}
return &res, nil
}

View file

@ -1,12 +1,10 @@
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 {
key []byte
epoch uint64
}
type responseV2 interface {
@ -20,8 +18,3 @@ type responseV2 interface {
func (x ResponseMetaInfo) ResponderKey() []byte {
return x.key
}
// Epoch returns local FrostFS epoch of the server.
func (x ResponseMetaInfo) Epoch() uint64 {
return x.epoch
}

View file

@ -2,64 +2,23 @@ package client
import (
"context"
"crypto/ecdsa"
"fmt"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
"github.com/nspcc-dev/neofs-sdk-go/owner"
)
// PrmSessionCreate groups parameters of SessionCreate operation.
type PrmSessionCreate struct {
XHeaders []string
prmCommonMeta
Expiration uint64
Key *ecdsa.PrivateKey
exp uint64
}
// SetExp sets number of the last NepFS epoch in the lifetime of the session after which it will be expired.
//
// Deprecated: Use PrmSessionCreate.Expiration instead.
func (x *PrmSessionCreate) SetExp(exp uint64) {
x.Expiration = exp
}
// UseKey specifies private key to sign the requests and compute token owner.
// If key is not provided, then Client default key is used.
//
// Deprecated: Use PrmSessionCreate.Key instead.
func (x *PrmSessionCreate) UseKey(key ecdsa.PrivateKey) {
x.Key = &key
}
func (x *PrmSessionCreate) buildRequest(c *Client) (*v2session.CreateRequest, error) {
ownerKey := c.prm.Key.PublicKey
if x.Key != nil {
ownerKey = x.Key.PublicKey
}
var ownerID user.ID
user.IDFromKey(&ownerID, ownerKey)
var ownerIDV2 refs.OwnerID
ownerID.WriteToV2(&ownerIDV2)
reqBody := new(v2session.CreateRequestBody)
reqBody.SetOwnerID(&ownerIDV2)
reqBody.SetExpiration(x.Expiration)
var meta v2session.RequestMetaHeader
writeXHeadersToMeta(x.XHeaders, &meta)
var req v2session.CreateRequest
req.SetBody(reqBody)
c.prepareRequest(&req, &meta)
return &req, nil
x.exp = exp
}
// ResSessionCreate groups resulting values of SessionCreate operation.
@ -71,14 +30,22 @@ type ResSessionCreate struct {
sessionKey []byte
}
// ID returns identifier of the opened session in a binary FrostFS API protocol format.
func (x *ResSessionCreate) setID(id []byte) {
x.id = id
}
// ID returns identifier of the opened session in a binary NeoFS API protocol format.
//
// Client doesn't retain value so modification is safe.
func (x ResSessionCreate) ID() []byte {
return x.id
}
// PublicKey returns public key of the opened session in a binary FrostFS API protocol format.
func (x *ResSessionCreate) setSessionKey(key []byte) {
x.sessionKey = key
}
// PublicKey returns public key of the opened session in a binary NeoFS API protocol format.
func (x ResSessionCreate) PublicKey() []byte {
return x.sessionKey
}
@ -89,38 +56,60 @@ func (x ResSessionCreate) PublicKey() []byte {
//
// 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`.
// If PrmInit.DisableFrostFSFailuresResolution has been called, unsuccessful
// FrostFS status codes are included in the returned result structure,
// otherwise, are also returned as `error`.
// If WithNeoFSErrorParsing option has been provided, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included
// 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.
//
// Return statuses:
// - global (see Client docs).
// - global (see Client docs).
func (c *Client) SessionCreate(ctx context.Context, prm PrmSessionCreate) (*ResSessionCreate, error) {
req, err := prm.buildRequest(c)
if err != nil {
return nil, err
// check context
if ctx == nil {
panic(panicMsgMissingContext)
}
if err := signature.SignServiceMessage(&c.prm.Key, req); err != nil {
return nil, fmt.Errorf("sign request: %w", err)
ownerID := owner.NewIDFromPublicKey(&c.prm.key.PublicKey)
// form request body
reqBody := new(v2session.CreateRequestBody)
reqBody.SetOwnerID(ownerID.ToV2())
reqBody.SetExpiration(prm.exp)
// for request
var req v2session.CreateRequest
req.SetBody(reqBody)
// init call context
var (
cc contextCall
res ResSessionCreate
)
c.initCallContext(&cc)
cc.meta = prm.prmCommonMeta
cc.req = &req
cc.statusRes = &res
cc.call = func() (responseV2, error) {
return rpcapi.CreateSession(&c.c, &req, client.WithContext(ctx))
}
cc.result = func(r responseV2) {
resp := r.(*v2session.CreateResponse)
body := resp.GetBody()
res.setID(body.GetID())
res.setSessionKey(body.GetSessionKey())
}
resp, err := rpcapi.CreateSession(&c.c, req, client.WithContext(ctx))
if err != nil {
return nil, err
// process call
if !cc.processCall() {
return nil, cc.err
}
var res ResSessionCreate
res.st, err = c.processResponse(resp)
if err != nil || !apistatus.IsSuccessful(res.st) {
return &res, err
}
body := resp.GetBody()
res.id = body.GetID()
res.sessionKey = body.GetSessionKey()
return &res, nil
}

View file

@ -1,53 +0,0 @@
package apistatus
import (
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/apemanager"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status"
)
// APEManagerAccessDenied describes status of the failure because of the access control violation.
// Instances provide Status and StatusV2 interfaces.
type APEManagerAccessDenied struct {
v2 status.Status
}
const defaultAPEManagerAccessDeniedMsg = "apemanager access denied"
func (x *APEManagerAccessDenied) Error() string {
msg := x.v2.Message()
if msg == "" {
msg = defaultAPEManagerAccessDeniedMsg
}
return errMessageStatusV2(
globalizeCodeV2(apemanager.StatusAPEManagerAccessDenied, apemanager.GlobalizeFail),
msg,
)
}
func (x *APEManagerAccessDenied) fromStatusV2(st *status.Status) {
x.v2 = *st
}
// ToStatusV2 converts APEManagerAccessDenied to v2's Status.
// If the value was returned by FromStatusV2, returns the source message.
// Otherwise, returns message with
// - code: APE_MANAGER_ACCESS_DENIED;
// - string message: "apemanager access denied";
// - details: empty.
func (x APEManagerAccessDenied) ToStatusV2() *status.Status {
x.v2.SetCode(globalizeCodeV2(apemanager.StatusAPEManagerAccessDenied, apemanager.GlobalizeFail))
x.v2.SetMessage(defaultAPEManagerAccessDeniedMsg)
return &x.v2
}
// WriteReason writes human-readable access rejection reason.
func (x *APEManagerAccessDenied) WriteReason(reason string) {
apemanager.WriteAccessDeniedDesc(&x.v2, reason)
}
// Reason returns human-readable access rejection reason returned by the server.
// Returns empty value is reason is not presented.
func (x APEManagerAccessDenied) Reason() string {
return apemanager.ReadAccessDeniedDesc(x.v2)
}

View file

@ -3,7 +3,7 @@ package apistatus
import (
"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.
@ -14,7 +14,7 @@ type ServerInternal struct {
v2 status.Status
}
func (x *ServerInternal) Error() string {
func (x ServerInternal) Error() string {
return errMessageStatusV2(
globalizeCodeV2(status.Internal, status.GlobalizeCommonFail),
x.v2.Message(),
@ -29,9 +29,9 @@ func (x *ServerInternal) fromStatusV2(st *status.Status) {
// ToStatusV2 implements StatusV2 interface method.
// If the value was returned by FromStatusV2, returns the source message.
// Otherwise, returns message with
// - code: INTERNAL;
// - string message: empty;
// - details: empty.
// * code: INTERNAL;
// * string message: empty;
// * details: empty.
func (x ServerInternal) ToStatusV2() *status.Status {
x.v2.SetCode(globalizeCodeV2(status.Internal, status.GlobalizeCommonFail))
return &x.v2
@ -62,7 +62,7 @@ type WrongMagicNumber struct {
v2 status.Status
}
func (x *WrongMagicNumber) Error() string {
func (x WrongMagicNumber) Error() string {
return errMessageStatusV2(
globalizeCodeV2(status.WrongMagicNumber, status.GlobalizeCommonFail),
x.v2.Message(),
@ -77,9 +77,9 @@ func (x *WrongMagicNumber) fromStatusV2(st *status.Status) {
// ToStatusV2 implements StatusV2 interface method.
// If the value was returned by FromStatusV2, returns the source message.
// Otherwise, returns message with
// - code: WRONG_MAGIC_NUMBER;
// - string message: empty;
// - details: empty.
// * code: WRONG_MAGIC_NUMBER;
// * string message: empty;
// * details: empty.
func (x WrongMagicNumber) ToStatusV2() *status.Status {
x.v2.SetCode(globalizeCodeV2(status.WrongMagicNumber, status.GlobalizeCommonFail))
return &x.v2
@ -104,9 +104,9 @@ func (x *WrongMagicNumber) WriteCorrectMagic(magic uint64) {
// CorrectMagic returns network magic returned by the server.
// Second value indicates presence status:
// - -1 if number is presented in incorrect format
// - 0 if number is not presented
// - +1 otherwise
// * -1 if number is presented in incorrect format
// * 0 if number is not presented
// * +1 otherwise
func (x WrongMagicNumber) CorrectMagic() (magic uint64, ok int8) {
x.v2.IterateDetails(func(d *status.Detail) bool {
if d.ID() == status.DetailIDCorrectMagic {
@ -123,118 +123,3 @@ func (x WrongMagicNumber) CorrectMagic() (magic uint64, ok int8) {
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 (
"testing"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status"
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
"github.com/nspcc-dev/neofs-api-go/v2/status"
apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
"github.com/stretchr/testify/require"
)
@ -50,81 +50,3 @@ func TestWrongMagicNumber_CorrectMagic(t *testing.T) {
_, ok = st.CorrectMagic()
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
import (
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status"
"github.com/nspcc-dev/neofs-api-go/v2/container"
"github.com/nspcc-dev/neofs-api-go/v2/status"
)
// ContainerNotFound describes status of the failure because of the missing container.
@ -11,17 +11,10 @@ type ContainerNotFound struct {
v2 status.Status
}
const defaultContainerNotFoundMsg = "container not found"
func (x *ContainerNotFound) Error() string {
msg := x.v2.Message()
if msg == "" {
msg = defaultContainerNotFoundMsg
}
func (x ContainerNotFound) Error() string {
return errMessageStatusV2(
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.
// If the value was returned by FromStatusV2, returns the source message.
// Otherwise, returns message with
// - code: CONTAINER_NOT_FOUND;
// - string message: "container not found";
// - details: empty.
// * code: CONTAINER_NOT_FOUND;
// * string message: "container not found";
// * details: empty.
func (x ContainerNotFound) ToStatusV2() *status.Status {
x.v2.SetCode(globalizeCodeV2(container.StatusNotFound, container.GlobalizeFail))
x.v2.SetMessage(defaultContainerNotFoundMsg)
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)
x.v2.SetMessage("container not found")
return &x.v2
}

View file

@ -1,8 +1,8 @@
package apistatus
import (
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status"
"github.com/nspcc-dev/neofs-api-go/v2/object"
"github.com/nspcc-dev/neofs-api-go/v2/status"
)
// ObjectLocked describes status of the failure because of the locked object.
@ -11,17 +11,10 @@ type ObjectLocked struct {
v2 status.Status
}
const defaultObjectLockedMsg = "object is locked"
func (x *ObjectLocked) Error() string {
msg := x.v2.Message()
if msg == "" {
msg = defaultObjectLockedMsg
}
func (x ObjectLocked) Error() string {
return errMessageStatusV2(
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.
// If the value was returned by FromStatusV2, returns the source message.
// Otherwise, returns message with
// - code: LOCKED;
// - string message: "object is locked";
// - details: empty.
// * code: LOCKED;
// * string message: "object is locked";
// * details: empty.
func (x ObjectLocked) ToStatusV2() *status.Status {
x.v2.SetCode(globalizeCodeV2(object.StatusLocked, object.GlobalizeFail))
x.v2.SetMessage(defaultObjectLockedMsg)
x.v2.SetMessage("object is locked")
return &x.v2
}
@ -48,17 +41,10 @@ type LockNonRegularObject struct {
v2 status.Status
}
const defaultLockNonRegularObjectMsg = "locking non-regular object is forbidden"
func (x *LockNonRegularObject) Error() string {
msg := x.v2.Message()
if msg == "" {
msg = defaultLockNonRegularObjectMsg
}
func (x LockNonRegularObject) Error() string {
return errMessageStatusV2(
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.
// If the value was returned by FromStatusV2, returns the source message.
// Otherwise, returns message with
// - code: LOCK_NON_REGULAR_OBJECT;
// - string message: "locking non-regular object is forbidden";
// - details: empty.
// * code: LOCK_NON_REGULAR_OBJECT;
// * string message: "locking non-regular object is forbidden";
// * details: empty.
func (x LockNonRegularObject) ToStatusV2() *status.Status {
x.v2.SetCode(globalizeCodeV2(object.StatusLockNonRegularObject, object.GlobalizeFail))
x.v2.SetMessage(defaultLockNonRegularObjectMsg)
x.v2.SetMessage("locking non-regular object is forbidden")
return &x.v2
}
@ -85,17 +71,10 @@ type ObjectAccessDenied struct {
v2 status.Status
}
const defaultObjectAccessDeniedMsg = "access to object operation denied"
func (x *ObjectAccessDenied) Error() string {
msg := x.v2.Message()
if msg == "" {
msg = defaultObjectAccessDeniedMsg
}
func (x ObjectAccessDenied) Error() string {
return errMessageStatusV2(
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.
// If the value was returned by FromStatusV2, returns the source message.
// Otherwise, returns message with
// - code: ACCESS_DENIED;
// - string message: "access to object operation denied";
// - details: empty.
// * code: ACCESS_DENIED;
// * string message: "access to object operation denied";
// * details: empty.
func (x ObjectAccessDenied) ToStatusV2() *status.Status {
x.v2.SetCode(globalizeCodeV2(object.StatusAccessDenied, object.GlobalizeFail))
x.v2.SetMessage(defaultObjectAccessDeniedMsg)
x.v2.SetMessage("access to object operation denied")
return &x.v2
}
@ -133,17 +112,10 @@ type ObjectNotFound struct {
v2 status.Status
}
const defaultObjectNotFoundMsg = "object not found"
func (x *ObjectNotFound) Error() string {
msg := x.v2.Message()
if msg == "" {
msg = defaultObjectNotFoundMsg
}
func (x ObjectNotFound) Error() string {
return errMessageStatusV2(
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.
// If the value was returned by FromStatusV2, returns the source message.
// Otherwise, returns message with
// - code: OBJECT_NOT_FOUND;
// - string message: "object not found";
// - details: empty.
// * code: OBJECT_NOT_FOUND;
// * string message: "object not found";
// * details: empty.
func (x ObjectNotFound) ToStatusV2() *status.Status {
x.v2.SetCode(globalizeCodeV2(object.StatusNotFound, object.GlobalizeFail))
x.v2.SetMessage(defaultObjectNotFoundMsg)
x.v2.SetMessage("object not found")
return &x.v2
}
@ -170,17 +142,10 @@ type ObjectAlreadyRemoved struct {
v2 status.Status
}
const defaultObjectAlreadyRemovedMsg = "object already removed"
func (x *ObjectAlreadyRemoved) Error() string {
msg := x.v2.Message()
if msg == "" {
msg = defaultObjectAlreadyRemovedMsg
}
func (x ObjectAlreadyRemoved) Error() string {
return errMessageStatusV2(
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.
// If the value was returned by FromStatusV2, returns the source message.
// Otherwise, returns message with
// - code: OBJECT_ALREADY_REMOVED;
// - string message: "object already removed";
// - details: empty.
// * code: OBJECT_ALREADY_REMOVED;
// * string message: "object already removed";
// * details: empty.
func (x ObjectAlreadyRemoved) ToStatusV2() *status.Status {
x.v2.SetCode(globalizeCodeV2(object.StatusAlreadyRemoved, object.GlobalizeFail))
x.v2.SetMessage(defaultObjectAlreadyRemovedMsg)
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)
x.v2.SetMessage("object already removed")
return &x.v2
}

View file

@ -3,7 +3,7 @@ package apistatus_test
import (
"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"
)

View file

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

View file

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

View file

@ -4,7 +4,7 @@ import (
"errors"
"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"
)

View file

@ -1,7 +1,7 @@
package apistatus
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.
@ -20,9 +20,9 @@ func (x *SuccessDefaultV2) fromStatusV2(st *status.Status) {
// ToStatusV2 implements StatusV2 interface method.
// If the value was returned by FromStatusV2, returns the source message.
// Otherwise, returns message with
// - code: OK;
// - string message: empty;
// - details: empty.
// * code: OK;
// * string message: empty;
// * details: empty.
func (x SuccessDefaultV2) ToStatusV2() *status.Status {
if x.isNil || x.v2 != nil {
return x.v2

View file

@ -1,14 +1,14 @@
package apistatus
import (
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status"
"github.com/nspcc-dev/neofs-api-go/v2/status"
)
type unrecognizedStatusV2 struct {
v2 status.Status
}
func (x *unrecognizedStatusV2) Error() string {
func (x unrecognizedStatusV2) Error() string {
return errMessageStatusV2("unrecognized", x.v2.Message())
}

View file

@ -3,20 +3,19 @@ package apistatus
import (
"fmt"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/apemanager"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status"
"github.com/nspcc-dev/neofs-api-go/v2/container"
"github.com/nspcc-dev/neofs-api-go/v2/object"
"github.com/nspcc-dev/neofs-api-go/v2/session"
"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.
type StatusV2 interface {
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
}
@ -29,16 +28,15 @@ type StatusV2 interface {
// Note: notice if the return type is a pointer.
//
// Successes:
// - status.OK: *SuccessDefaultV2 (this also includes nil argument).
// * status.OK: *SuccessDefaultV2 (this also includes nil argument).
//
// Common failures:
// - status.Internal: *ServerInternal;
// - status.SignatureVerificationFail: *SignatureVerification.
// * status.Internal: *ServerInternal.
//
// Object failures:
// - object.StatusLocked: *ObjectLocked;
// - object.StatusLockNonRegularObject: *LockNonRegularObject.
// - object.StatusAccessDenied: *ObjectAccessDenied.
// * object.StatusLocked: *ObjectLocked;
// * object.StatusLockNonRegularObject: *LockNonRegularObject.
// * object.StatusAccessDenied: *ObjectAccessDenied.
func FromStatusV2(st *status.Status) Status {
var decoder interface {
fromStatusV2(*status.Status)
@ -57,10 +55,6 @@ func FromStatusV2(st *status.Status) Status {
decoder = new(ServerInternal)
case status.WrongMagicNumber:
decoder = new(WrongMagicNumber)
case status.SignatureVerificationFail:
decoder = new(SignatureVerification)
case status.NodeUnderMaintenance:
decoder = new(NodeUnderMaintenance)
}
case object.LocalizeFailStatus(&code):
switch code {
@ -74,16 +68,12 @@ func FromStatusV2(st *status.Status) Status {
decoder = new(ObjectNotFound)
case object.StatusAlreadyRemoved:
decoder = new(ObjectAlreadyRemoved)
case object.StatusOutOfRange:
decoder = new(ObjectOutOfRange)
}
case container.LocalizeFailStatus(&code):
//nolint:exhaustive
switch code {
case container.StatusNotFound:
decoder = new(ContainerNotFound)
case container.StatusEACLNotFound:
decoder = new(EACLNotFound)
}
case session.LocalizeFailStatus(&code):
//nolint:exhaustive
@ -93,12 +83,6 @@ func FromStatusV2(st *status.Status) Status {
case session.StatusTokenExpired:
decoder = new(SessionTokenExpired)
}
case apemanager.LocalizeFailStatus(&code):
//nolint:exhaustive
switch code {
case apemanager.StatusAPEManagerAccessDenied:
decoder = new(APEManagerAccessDenied)
}
}
if decoder == nil {
@ -113,8 +97,7 @@ func FromStatusV2(st *status.Status) Status {
// ToStatusV2 converts Status instance to status.Status message structure. Inverse to FromStatusV2 operation.
//
// If argument is the StatusV2 instance, it is converted directly.
// Otherwise, successes are converted with status.OK code w/o details and message,
// failures - with status.Internal and error text message w/o details.
// Otherwise, successes are converted with status.OK code w/o details and message, failures - with status.Internal.
func ToStatusV2(st Status) *status.Status {
if v, ok := st.(StatusV2); ok {
return v.ToStatusV2()
@ -124,13 +107,10 @@ func ToStatusV2(st Status) *status.Status {
return newStatusV2WithLocalCode(status.OK, status.GlobalizeSuccess)
}
internalErrorStatus := newStatusV2WithLocalCode(status.Internal, status.GlobalizeCommonFail)
internalErrorStatus.SetMessage(st.(error).Error()) // type cast never panics because IsSuccessful() checks cast
return internalErrorStatus
return newStatusV2WithLocalCode(status.Internal, status.GlobalizeCommonFail)
}
func errMessageStatusV2(code any, msg string) string {
func errMessageStatusV2(code interface{}, msg string) string {
const (
noMsgFmt = "status: code = %v"
msgFmt = noMsgFmt + " message = %s"

View file

@ -4,7 +4,7 @@ import (
"errors"
"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"
)
@ -12,14 +12,12 @@ func TestToStatusV2(t *testing.T) {
type statusConstructor func() apistatus.Status
for _, testItem := range [...]struct {
status any // Status or statusConstructor
codeV2 uint64
messageV2 string
status interface{} // Status or statusConstructor
codeV2 uint64
}{
{
status: errors.New("some error"),
codeV2: 1024,
messageV2: "some error",
status: errors.New("some error"),
codeV2: 1024,
},
{
status: 1,
@ -95,24 +93,12 @@ func TestToStatusV2(t *testing.T) {
}),
codeV2: 2052,
},
{
status: (statusConstructor)(func() apistatus.Status {
return new(apistatus.ObjectOutOfRange)
}),
codeV2: 2053,
},
{
status: (statusConstructor)(func() apistatus.Status {
return new(apistatus.ContainerNotFound)
}),
codeV2: 3072,
},
{
status: (statusConstructor)(func() apistatus.Status {
return new(apistatus.EACLNotFound)
}),
codeV2: 3073,
},
{
status: (statusConstructor)(func() apistatus.Status {
return new(apistatus.SessionTokenNotFound)
@ -125,18 +111,6 @@ func TestToStatusV2(t *testing.T) {
}),
codeV2: 4097,
},
{
status: (statusConstructor)(func() apistatus.Status {
return new(apistatus.APEManagerAccessDenied)
}),
codeV2: 5120,
},
{
status: (statusConstructor)(func() apistatus.Status {
return new(apistatus.NodeUnderMaintenance)
}),
codeV2: 1027,
},
} {
var st apistatus.Status
@ -150,9 +124,6 @@ func TestToStatusV2(t *testing.T) {
// must generate the same status.Status message
require.EqualValues(t, testItem.codeV2, stv2.Code())
if len(testItem.messageV2) > 0 {
require.Equal(t, testItem.messageV2, stv2.Message())
}
_, ok := st.(apistatus.StatusV2)
if ok {
@ -171,14 +142,12 @@ func TestFromStatusV2(t *testing.T) {
type statusConstructor func() apistatus.Status
for _, testItem := range [...]struct {
status any // Status or statusConstructor
codeV2 uint64
messageV2 string
status interface{} // Status or statusConstructor
codeV2 uint64
}{
{
status: errors.New("some error"),
codeV2: 1024,
messageV2: "some error",
status: errors.New("some error"),
codeV2: 1024,
},
{
status: 1,
@ -254,24 +223,12 @@ func TestFromStatusV2(t *testing.T) {
}),
codeV2: 2052,
},
{
status: statusConstructor(func() apistatus.Status {
return new(apistatus.ObjectOutOfRange)
}),
codeV2: 2053,
},
{
status: (statusConstructor)(func() apistatus.Status {
return new(apistatus.ContainerNotFound)
}),
codeV2: 3072,
},
{
status: (statusConstructor)(func() apistatus.Status {
return new(apistatus.EACLNotFound)
}),
codeV2: 3073,
},
{
status: (statusConstructor)(func() apistatus.Status {
return new(apistatus.SessionTokenNotFound)
@ -284,18 +241,6 @@ func TestFromStatusV2(t *testing.T) {
}),
codeV2: 4097,
},
{
status: (statusConstructor)(func() apistatus.Status {
return new(apistatus.APEManagerAccessDenied)
}),
codeV2: 5120,
},
{
status: (statusConstructor)(func() apistatus.Status {
return new(apistatus.NodeUnderMaintenance)
}),
codeV2: 1027,
},
} {
var st apistatus.Status
@ -309,9 +254,6 @@ func TestFromStatusV2(t *testing.T) {
// must generate the same status.Status message
require.EqualValues(t, testItem.codeV2, stv2.Code())
if len(testItem.messageV2) > 0 {
require.Equal(t, testItem.messageV2, stv2.Message())
}
_, ok := st.(apistatus.StatusV2)
if ok {

View file

@ -1,83 +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
// nolint: unused
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
// nolint: unused
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,37 +0,0 @@
package acl
import "fmt"
// sets n-th bit in num (starting at 0).
func setBit(num *uint32, n uint8) {
*num |= 1 << n
}
// 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,130 +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)
}
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,521 +1,193 @@
package container
import (
"crypto/ecdsa"
"crypto/sha256"
"errors"
"fmt"
"strconv"
"strings"
"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"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/version"
"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 {
v2 container.Container
token *session.Token
sig *signature.Signature
}
const (
attributeName = "Name"
attributeTimestamp = "Timestamp"
)
// reads Container from the container.Container message. If checkFieldPresence is set,
// returns an error on absence of any protocol-required field.
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()
if len(binNonce) > 0 {
var nonce uuid.UUID
err = nonce.UnmarshalBinary(binNonce)
if err != nil {
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 checkFieldPresence && ver == nil {
return errors.New("missing version")
}
policyV2 := m.GetPlacementPolicy()
if policyV2 != nil {
var policy netmap.PlacementPolicy
err = policy.ReadFromV2(*policyV2)
if err != nil {
return fmt.Errorf("invalid placement policy: %w", err)
}
} else if checkFieldPresence {
return errors.New("missing placement policy")
}
if err := checkAttributes(m); err != nil {
return err
}
x.v2 = m
return nil
}
func checkAttributes(m container.Container) error {
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)
}
var err error
if key == 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{}{}
}
return nil
}
// ReadFromV2 reads Container from the container.Container message. Checks if the
// message conforms to FrostFS API V2 protocol.
// New creates, initializes and returns blank Container instance.
//
// See also WriteToV2.
func (x *Container) ReadFromV2(m container.Container) error {
return x.readFromV2(m, true)
// 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()
for i := range opts {
opts[i](&cnrOptions)
}
cnr := new(Container)
cnr.SetNonceUUID(cnrOptions.nonce)
cnr.SetBasicACL(cnrOptions.acl)
if cnrOptions.owner != nil {
cnr.SetOwnerID(cnrOptions.owner)
}
if cnrOptions.policy != nil {
cnr.SetPlacementPolicy(cnrOptions.policy)
}
cnr.SetAttributes(cnrOptions.attributes)
cnr.SetVersion(version.Current())
return cnr
}
// WriteToV2 writes Container into the container.Container message.
// The message MUST NOT be nil.
// ToV2 returns the v2 Container message.
//
// See also ReadFromV2.
func (x Container) WriteToV2(m *container.Container) {
*m = x.v2
// Nil Container converts to nil.
func (c *Container) ToV2() *container.Container {
if c == nil {
return nil
}
return &c.v2
}
// Marshal encodes Container into a binary format of the FrostFS API protocol
// (Protocol Buffers with direct field order).
// NewVerifiedFromV2 constructs Container from NeoFS API V2 Container message.
//
// See also Unmarshal.
func (x Container) Marshal() []byte {
return x.v2.StableMarshal(nil)
// Does not perform if message meets NeoFS API V2 specification. To do this
// use NewVerifiedFromV2 constructor.
func NewContainerFromV2(c *container.Container) *Container {
cnr := new(Container)
if c != nil {
cnr.v2 = *c
}
return cnr
}
// 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)
// CalculateID calculates container identifier
// based on its structure.
func CalculateID(c *Container) *cid.ID {
data, err := c.ToV2().StableMarshal(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
// (Protocol Buffers JSON).
func (c *Container) Version() *version.Version {
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.
func (x Container) MarshalJSON() ([]byte, error) {
return x.v2.MarshalJSON()
// Returns error if container nonce is not a valid UUID.
func (c *Container) NonceUUID() (uuid.UUID, error) {
return uuid.FromBytes(c.v2.GetNonce())
}
// UnmarshalJSON decodes FrostFS API protocol JSON format into the Container
// (Protocol Buffers JSON). Returns an error describing a format violation.
//
// See also MarshalJSON.
func (x *Container) UnmarshalJSON(data []byte) error {
return x.v2.UnmarshalJSON(data)
// SetNonceUUID sets container nonce as UUID.
func (c *Container) SetNonceUUID(v uuid.UUID) {
data, _ := v.MarshalBinary()
c.v2.SetNonce(data)
}
// Init initializes all internal data of the Container required by FrostFS API
// protocol. Init MUST be called when creating a new container. Init SHOULD NOT
// 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)
func (c *Container) BasicACL() uint32 {
return c.v2.GetBasicACL()
}
// SetOwner specifies the owner of the Container. Each Container has exactly
// one owner, so SetOwner MUST be called for instances to be saved in the
// FrostFS.
//
// See also Owner.
func (x *Container) SetOwner(owner user.ID) {
var m refs.OwnerID
owner.WriteToV2(&m)
x.v2.SetOwnerID(&m)
func (c *Container) SetBasicACL(v acl.BasicACL) {
c.v2.SetBasicACL(uint32(v))
}
// Owner returns owner of the Container set using SetOwner.
//
// 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
func (c *Container) Attributes() Attributes {
return NewAttributesFromV2(c.v2.GetAttributes())
}
// SetBasicACL specifies basic part of the Container ACL. Basic ACL is used
// to control access inside container storage.
//
// See also BasicACL.
func (x *Container) SetBasicACL(basicACL acl.Basic) {
x.v2.SetBasicACL(basicACL.Bits())
func (c *Container) SetAttributes(v Attributes) {
c.v2.SetAttributes(v.ToV2())
}
// BasicACL returns basic ACL set using SetBasicACL.
//
// 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
func (c *Container) PlacementPolicy() *netmap.PlacementPolicy {
return netmap.NewPlacementPolicyFromV2(c.v2.GetPlacementPolicy())
}
// SetPlacementPolicy sets placement policy for the objects within the Container.
// FrostFS storage layer strives to follow the specified policy.
//
// See also PlacementPolicy.
func (x *Container) SetPlacementPolicy(policy netmap.PlacementPolicy) {
var m v2netmap.PlacementPolicy
policy.WriteToV2(&m)
x.v2.SetPlacementPolicy(&m)
func (c *Container) SetPlacementPolicy(v *netmap.PlacementPolicy) {
c.v2.SetPlacementPolicy(v.ToV2())
}
// PlacementPolicy returns placement policy set using SetPlacementPolicy.
//
// Zero Container has no placement policy which is incorrect according to
// FrostFS API protocol.
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
// SessionToken returns token of the session within
// which container was created.
func (c Container) SessionToken() *session.Token {
return c.token
}
// SetAttribute sets Container attribute value by key. Both key and value
// MUST NOT be empty. Attributes set by the creator (owner) are most commonly
// ignored by the FrostFS system and used for application layer. Some attributes
// are so-called system or well-known attributes: they are reserved for system
// 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, IterateUserAttributes.
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 := range ln {
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)
// SetSessionToken sets token of the session within
// which container was created.
func (c *Container) SetSessionToken(t *session.Token) {
c.token = t
}
// Attribute reads value of the Container attribute by key. Empty result means
// attribute absence.
//
// See also SetAttribute, IterateAttributes, IterateUserAttributes.
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 ""
// Signature returns signature of the marshaled container.
func (c Container) Signature() *signature.Signature {
return c.sig
}
// IterateAttributes iterates over all Container attributes and passes them
// into f. The handler MUST NOT be nil.
//
// 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())
}
// SetSignature sets signature of the marshaled container.
func (c *Container) SetSignature(sig *signature.Signature) {
c.sig = sig
}
// IterateUserAttributes iterates over user Container attributes and passes them
// into f. The handler MUST NOT be nil.
//
// See also SetAttribute, Attribute.
func (x Container) IterateUserAttributes(f func(key, val string)) {
attrs := x.v2.GetAttributes()
for _, attr := range attrs {
key := attr.GetKey()
if !strings.HasPrefix(key, container.SysAttributePrefix) &&
!strings.HasPrefix(key, container.SysAttributePrefixNeoFS) {
f(key, attr.GetValue())
}
}
// Marshal marshals Container into a protobuf binary form.
func (c *Container) Marshal() ([]byte, error) {
return c.v2.StableMarshal(nil)
}
// SetName sets human-readable name of the Container. Name MUST NOT be empty.
//
// See also Name.
func SetName(cnr *Container, name string) {
cnr.SetAttribute(attributeName, name)
// Unmarshal unmarshals protobuf binary representation of Container.
func (c *Container) Unmarshal(data []byte) error {
return c.v2.Unmarshal(data)
}
// Name returns container name set using SetName.
//
// Zero Container has no name.
func Name(cnr Container) string {
return cnr.Attribute(attributeName)
// MarshalJSON encodes Container to protobuf JSON format.
func (c *Container) MarshalJSON() ([]byte, error) {
return c.v2.MarshalJSON()
}
// SetCreationTime writes container's creation time in Unix Timestamp format.
//
// See also CreatedAt.
func SetCreationTime(cnr *Container, t time.Time) {
cnr.SetAttribute(attributeTimestamp, strconv.FormatInt(t.Unix(), 10))
}
// CreatedAt returns container's creation time set using SetCreationTime.
//
// Zero Container has zero timestamp (in seconds).
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)
}
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)
// UnmarshalJSON decodes Container from protobuf JSON format.
func (c *Container) UnmarshalJSON(data []byte) error {
return c.v2.UnmarshalJSON(data)
}

View file

@ -1,333 +1,136 @@
package container_test
import (
"crypto/sha256"
"strconv"
"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"
usertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user/test"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/version"
"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"
)
func TestPlacementPolicyEncoding(t *testing.T) {
v := containertest.Container()
func TestNewContainer(t *testing.T) {
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) {
var v2 container.Container
require.NoError(t, v2.Unmarshal(v.Marshal()))
data, err := c.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) {
data, err := v.MarshalJSON()
data, err := c.MarshalJSON()
require.NoError(t, err)
var v2 container.Container
require.NoError(t, v2.UnmarshalJSON(data))
c2 := container.New()
require.NoError(t, c2.UnmarshalJSON(data))
require.Equal(t, v, v2)
require.Equal(t, c, c2)
})
}
func TestContainer_Init(t *testing.T) {
val := containertest.Container()
func TestContainer_SessionToken(t *testing.T) {
tok := sessiontest.Token()
val.Init()
cnr := container.New()
var msg v2container.Container
val.WriteToV2(&msg)
cnr.SetSessionToken(tok)
binNonce := msg.GetNonce()
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)
require.Equal(t, tok, cnr.SessionToken())
}
func TestContainer_Owner(t *testing.T) {
var val container.Container
func TestContainer_Signature(t *testing.T) {
sig := sigtest.Signature()
require.Zero(t, val.Owner())
cnr := container.New()
cnr.SetSignature(sig)
val = containertest.Container()
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))
require.Equal(t, sig, cnr.Signature())
}
func TestContainer_BasicACL(t *testing.T) {
var val container.Container
func TestContainer_ToV2(t *testing.T) {
t.Run("nil", func(t *testing.T) {
var x *container.Container
require.Zero(t, val.BasicACL())
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 = v2container.SysAttributePrefix + "key1", v2container.SysAttributePrefixNeoFS + "key2"
const attrVal1, attrVal2 = "val1", "val2"
val := containertest.Container()
val.SetAttribute(attrKey1, attrVal1)
val.SetAttribute(attrKey2, attrVal2)
var i int
val.IterateUserAttributes(func(key, val string) {
i++
})
require.Equal(t, 1, i)
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.Nil(t, x.ToV2())
})
require.GreaterOrEqual(t, len(m), 2)
require.Equal(t, attrVal1, m[attrKey1])
require.Equal(t, attrVal2, m[attrKey2])
t.Run("default values", func(t *testing.T) {
cnt := container.New()
val2.SetAttribute(attrKey1, attrVal1+"_")
require.Equal(t, attrVal1+"_", val2.Attribute(attrKey1))
}
// check initial values
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) {
var val container.Container
require.EqualValues(t, acl.PrivateBasicRule, cnt.BasicACL())
require.Equal(t, version.Current(), cnt.Version())
require.Panics(t, func() {
container.SetName(&val, "")
nonce, err := cnt.NonceUUID()
require.NoError(t, err)
require.NotNil(t, nonce)
// convert to v2 message
cntV2 := cnt.ToV2()
nonceV2, err := uuid.FromBytes(cntV2.GetNonce())
require.NoError(t, err)
require.Equal(t, nonce.String(), nonceV2.String())
require.Nil(t, cntV2.GetAttributes())
require.Nil(t, cntV2.GetPlacementPolicy())
require.Nil(t, cntV2.GetOwnerID())
require.Equal(t, uint32(acl.PrivateBasicRule), cntV2.GetBasicACL())
require.Equal(t, version.Current().ToV2(), cntV2.GetVersion())
})
val = containertest.Container()
const name = "some name"
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 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)
val := containertest.Container()
var sig frostfscrypto.Signature
require.NoError(t, container.CalculateSignature(&sig, val, key.PrivateKey))
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
import (
"bytes"
"crypto/sha256"
"fmt"
"errors"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
"github.com/mr-tron/base58"
"github.com/nspcc-dev/neofs-api-go/v2/refs"
)
// ID represents FrostFS container identifier.
//
// 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
// ID represents v2-compatible container identifier.
type ID refs.ContainerID
// ReadFromV2 reads ID from the refs.ContainerID message.
// Returns an error if the message is malformed according
// to the FrostFS API V2 protocol.
// NewFromV2 wraps v2 ContainerID message to ID.
//
// See also WriteToV2.
func (id *ID) ReadFromV2(m refs.ContainerID) error {
return id.Decode(m.GetValue())
// Nil refs.ContainerID converts to nil.
func NewFromV2(idV2 *refs.ContainerID) *ID {
return (*ID)(idV2)
}
// WriteToV2 writes ID to the refs.ContainerID message.
// The message must not be nil.
// New creates and initializes blank ID.
//
// See also ReadFromV2.
func (id ID) WriteToV2(m *refs.ContainerID) {
m.SetValue(id[:])
// Defaults:
// - value: nil.
func New() *ID {
return NewFromV2(new(refs.ContainerID))
}
// Encode encodes ID into 32 bytes of dst. Panics if
// dst length is less than 32.
// SetSHA256 sets container identifier value to SHA256 checksum of container body.
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.
func (id ID) Encode(dst []byte) {
if l := len(dst); l < sha256.Size {
panic(fmt.Sprintf("destination length is less than %d bytes: %d", sha256.Size, l))
// Returns error if s is not a base58 encoded
// ID data.
func (id *ID) Parse(s string) error {
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[:])
}
// 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)
(*refs.ContainerID)(id).SetValue(data)
return nil
}
// SetSHA256 sets container identifier value to SHA256 checksum of container structure.
func (id *ID) SetSHA256(v [sha256.Size]byte) {
copy(id[:], v[:])
// String returns base58 string representation of ID.
func (id *ID) String() string {
return base58.Encode((*refs.ContainerID)(id).GetValue())
}
// Equals defines a comparison relation between two ID instances.
//
// Note that comparison using '==' operator is not recommended since it MAY result
// in loss of compatibility.
func (id ID) Equals(id2 ID) bool {
return id == id2
// Marshal marshals ID into a protobuf binary form.
func (id *ID) Marshal() ([]byte, error) {
return (*refs.ContainerID)(id).StableMarshal(nil)
}
// EncodeToString encodes ID into FrostFS API protocol string.
//
// Zero ID is base58 encoding of 32 zeros.
//
// See also DecodeString.
func (id ID) EncodeToString() string {
return base58.Encode(id[:])
// Unmarshal unmarshals protobuf binary representation of ID.
func (id *ID) Unmarshal(data []byte) error {
return (*refs.ContainerID)(id).Unmarshal(data)
}
// DecodeString decodes string into ID according to FrostFS API protocol. Returns
// an error if s is malformed.
//
// 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)
// MarshalJSON encodes ID to protobuf JSON format.
func (id *ID) MarshalJSON() ([]byte, error) {
return (*refs.ContainerID)(id).MarshalJSON()
}
// String implements fmt.Stringer.
//
// String is designed to be human-readable, and its format MAY differ between
// 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()
// UnmarshalJSON decodes ID from protobuf JSON format.
func (id *ID) UnmarshalJSON(data []byte) error {
return (*refs.ContainerID)(id).UnmarshalJSON(data)
}

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