forked from TrueCloudLab/frostfs-sdk-go
Compare commits
268 commits
support/v1
...
master
Author | SHA1 | Date | |
---|---|---|---|
4438f115fb | |||
bec77f280a | |||
df2090c2be | |||
7e6592b28e | |||
d589d51509 | |||
25588ee3be | |||
9407f30248 | |||
94c0a607b5 | |||
e45647de3c | |||
611e20587b | |||
eba6831125 | |||
7e3810d654 | |||
cc0fef2c55 | |||
b696d3c70e | |||
1c94309d7a | |||
f43f18ecda | |||
ac8442bf99 | |||
0ad877288e | |||
0e1999c965 | |||
b461aa64b8 | |||
b761fd8070 | |||
94476f9055 | |||
5e759bf089 | |||
d4f5bba459 | |||
e9c1a2ab2b | |||
2cbc585edd | |||
e355e5eeba | |||
f08069ceeb | |||
dad99bad48 | |||
0d3a238d9c | |||
cf64ddfb14 | |||
cf9a54dcda | |||
|
b2a37543d3 | ||
f0ac49b8f0 | |||
54696acf48 | |||
dd88a5c5e0 | |||
4ff9c00de3 | |||
b204a62da1 | |||
377a5a0517 | |||
43c046f343 | |||
|
8c0c7789ca | ||
|
339e2702f8 | ||
|
a1748ae0e7 | ||
|
4c779423f5 | ||
|
45a6e7a7c2 | ||
|
b4b07a3c4e | ||
|
d047289182 | ||
|
1cacf472a3 | ||
|
e35f0df1ca | ||
|
5f9d846fb4 | ||
|
2eefdab0e4 | ||
|
7a2a76af95 | ||
|
da4ddcf337 | ||
|
231ec66054 | ||
|
a02a3870a5 | ||
|
9d4b36a8e6 | ||
|
d3a09cbe7f | ||
|
f21d63bded | ||
21eef1ae7f | |||
|
d2f3929b51 | ||
|
8c682641bf | ||
|
452a50e9d5 | ||
|
1325b4f272 | ||
|
c6576c8112 | ||
|
8e3173eacd | ||
|
89124d442d | ||
|
664392afc2 | ||
|
f2f97f656d | ||
|
be9a1aca90 | ||
|
3d6b5d807b | ||
|
3dad44232e | ||
|
4662d39886 | ||
|
f75a5feba3 | ||
|
1929b634a1 | ||
|
402d72e629 | ||
|
43a57d42dd | ||
|
71891029da | ||
|
d808f72c38 | ||
|
511886b8d2 | ||
|
01c238ddc0 | ||
|
2e5c66934c | ||
|
74234623b2 | ||
|
ee92df3203 | ||
|
cf7bee3087 | ||
|
456167e777 | ||
|
0e4d07fb06 | ||
|
d6d6a41f5d | ||
|
724d30db1a | ||
|
1f593d0fb2 | ||
|
f543ba68d3 | ||
|
dd5826d071 | ||
|
02bc2bc236 | ||
|
992b26a3ff | ||
|
6a43accf96 | ||
|
5d7650c3e7 | ||
|
a926e5a1de | ||
|
8c5333ea55 | ||
|
0236b03fa7 | ||
|
2f843de3ed | ||
|
84888854ab | ||
|
7537fa0dec | ||
|
7578b54fac | ||
|
737e690482 | ||
|
f8148c954b | ||
|
4e31b4f231 | ||
|
30bf79f075 | ||
|
7a99cc916c | ||
|
a0f7c903d3 | ||
|
7de66159d4 | ||
|
90255e9efa | ||
|
828cfdc5bf | ||
|
90f1cc7a1a | ||
|
3a95686aab | ||
|
f4ac75423c | ||
|
b4f4ee4f79 | ||
|
54145916a9 | ||
|
0d54757545 | ||
|
58fe1768cc | ||
|
0b8c53ebc9 | ||
|
1b30d228da | ||
|
423804de84 | ||
|
99e185690e | ||
|
9d3a1835d1 | ||
|
e6cb5f2ee1 | ||
|
7d10b432d1 | ||
|
0d862d8568 | ||
|
48ba86f4dd | ||
|
f91b1facd5 | ||
|
c6fee6d01a | ||
|
a6ecf6b881 | ||
|
9996b3be01 | ||
|
0cd790cfe0 | ||
|
df6538c68c | ||
|
27fe9c19a7 | ||
|
6994eb0e55 | ||
|
cec2373b50 | ||
|
ff09b1cfe1 | ||
|
041e1ef2b6 | ||
|
30d27c3050 | ||
|
2ad89085a3 | ||
|
70845147f6 | ||
|
86a447bc80 | ||
|
ab4d1e34a8 | ||
|
dea3d9c419 | ||
|
d2cd9ebfbd | ||
|
1e1139f305 | ||
|
dcaf454c1d | ||
|
09ed6077f9 | ||
|
40942affe9 | ||
|
aa5ee1dcde | ||
|
3b15a01327 | ||
|
c4ebe8d854 | ||
|
e82a2d86ef | ||
|
af7e20073b | ||
|
596f43a540 | ||
|
721df386c5 | ||
|
e986f47807 | ||
|
e999fb00c3 | ||
|
d51a324147 | ||
|
d648b86776 | ||
|
ca523f1ff1 | ||
|
5bfdb64251 | ||
|
86bdc670d5 | ||
|
6796b4a29a | ||
|
723ba5ee45 | ||
|
2b21146185 | ||
|
9c47fca7c2 | ||
|
a8fd2ef954 | ||
|
ea21cdf731 | ||
|
eb3b990812 | ||
|
6709b00c89 | ||
|
517d7a1e4a | ||
|
6ac9deabb8 | ||
|
5518b63432 | ||
|
031eac2f48 | ||
|
67ff996dc3 | ||
|
bf8312a547 | ||
|
0ef49cf851 | ||
|
458c882ff4 | ||
|
0bb40b3245 | ||
|
d3b998d672 | ||
|
c65be6d469 | ||
|
82d762f536 | ||
|
6cb513c976 | ||
|
3953c2166e | ||
|
85e3c7b087 | ||
|
3bbf7ee15d | ||
|
85affc3c93 | ||
|
f0a5eb6dbc | ||
|
bef4618cd6 | ||
|
1f7fe6864d | ||
|
60ef026923 | ||
|
c976332e20 | ||
|
12ea1e8d74 | ||
|
539ac9915e | ||
|
526b45e207 | ||
|
5fe6d96bf1 | ||
|
4cbbbdd3e2 | ||
|
f0134ef26e | ||
|
e468f409d7 | ||
|
4dc3a7669e | ||
|
48434d9999 | ||
|
552c7875bf | ||
|
497053c785 | ||
|
6e81e13e1b | ||
|
1ed426b8a6 | ||
|
d20999113a | ||
|
6554c681c8 | ||
|
9f20d74d76 | ||
|
7fe75d2cd9 | ||
|
bcbffd516a | ||
|
2f9cc50fec | ||
|
ea043f4ca3 | ||
|
2deaaeef05 | ||
|
9b63c07c59 | ||
|
1186f2f703 | ||
|
f7172adf18 | ||
|
24d6c2221f | ||
|
27cd721422 | ||
|
96892d7bc4 | ||
|
168b3ee7a4 | ||
|
caa055236b | ||
|
fd13e61266 | ||
|
9c502a9cae | ||
|
d51d18d5f3 | ||
|
55283d3c91 | ||
|
75a5b6588d | ||
|
146fc4f07a | ||
|
c961aea144 | ||
|
ade8822a2f | ||
|
3e75660802 | ||
|
53e064e556 | ||
|
385f1b10f9 | ||
|
1e8aa6a99f | ||
|
97c8274dc0 | ||
|
9e8e12f6ae | ||
|
aa6a4950d5 | ||
|
e56eef495d | ||
|
e50e6d2828 | ||
|
df0573d521 | ||
|
c4adb03f8e | ||
|
fcfae4a249 | ||
|
9814748958 | ||
|
7df00fb0eb | ||
|
11a25bb413 | ||
|
b8d2158acd | ||
|
a709cf5444 | ||
|
0dbea5452a | ||
|
47345a33da | ||
|
d568458fab | ||
|
2104945f9e | ||
|
f38a24e8b5 | ||
|
e0281c3b34 | ||
|
f18b4a2a75 | ||
|
58d4f4a55f | ||
|
030bbce2cf | ||
|
59b49dd7e6 | ||
|
191d85e607 | ||
|
9be9697856 | ||
|
52548fe176 | ||
|
7811d8eefc | ||
|
d03523a3bc | ||
|
ec5c223f29 | ||
|
f5cabe26cb | ||
|
aeb4ac638a | ||
|
7d31de57ec | ||
|
48150852f3 | ||
|
a55ffa4796 |
311 changed files with 20759 additions and 13720 deletions
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
* @TrueCloudLab/storage-core @TrueCloudLab/storage-services
|
45
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
45
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
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`):
|
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
blank_issues_enabled: false
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
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. -->
|
12
.github/workflows/tests.yml
vendored
12
.github/workflows/tests.yml
vendored
|
@ -15,20 +15,20 @@ jobs:
|
|||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
go_versions: [ '1.16.x', '1.17.x' ]
|
||||
go_versions: [ '1.18.x', '1.19.x', '1.20.x' ]
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '${{ matrix.go_versions }}'
|
||||
|
||||
- name: Restore Go modules from cache
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: /home/runner/go/pkg/mod
|
||||
key: deps-${{ hashFiles('go.sum') }}
|
||||
|
@ -43,10 +43,10 @@ jobs:
|
|||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v2
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
version: latest
|
||||
only-new-issues: true
|
||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -20,4 +20,4 @@ vendor/
|
|||
|
||||
# coverage
|
||||
coverage.txt
|
||||
coverage.html
|
||||
coverage.html
|
||||
|
|
10
.gitlint
Normal file
10
.gitlint
Normal file
|
@ -0,0 +1,10 @@
|
|||
[general]
|
||||
fail-without-commits=true
|
||||
contrib=CC1
|
||||
|
||||
[title-match-regex]
|
||||
regex=^\[\#[0-9]+\]\s
|
||||
|
||||
[ignore-by-title]
|
||||
regex=^Release(.*)
|
||||
ignore=title-match-regex
|
|
@ -24,6 +24,9 @@ linters-settings:
|
|||
govet:
|
||||
# report about shadowed variables
|
||||
check-shadowing: false
|
||||
staticcheck:
|
||||
checks: ["all", "-SA1019"] # TODO Enable SA1019 after deprecated warning are fixed.
|
||||
|
||||
|
||||
linters:
|
||||
enable:
|
||||
|
@ -32,15 +35,12 @@ linters:
|
|||
- revive
|
||||
|
||||
# some default golangci-lint linters
|
||||
- deadcode
|
||||
- errcheck
|
||||
- gosimple
|
||||
- ineffassign
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- typecheck
|
||||
- unused
|
||||
- varcheck
|
||||
|
||||
# extra linters
|
||||
- exhaustive
|
||||
|
|
30
.pre-commit-config.yaml
Normal file
30
.pre-commit-config.yaml
Normal file
|
@ -0,0 +1,30 @@
|
|||
ci:
|
||||
autofix_prs: false
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0
|
||||
hooks:
|
||||
- id: check-added-large-files
|
||||
- id: check-case-conflict
|
||||
- id: check-executables-have-shebangs
|
||||
- id: check-shebang-scripts-are-executable
|
||||
- id: check-merge-conflict
|
||||
- id: check-json
|
||||
- id: check-xml
|
||||
- id: check-yaml
|
||||
- id: trailing-whitespace
|
||||
args: [--markdown-linebreak-ext=md]
|
||||
- id: end-of-file-fixer
|
||||
exclude: ".key$"
|
||||
|
||||
- repo: https://github.com/golangci/golangci-lint
|
||||
rev: v1.51.2
|
||||
hooks:
|
||||
- id: golangci-lint
|
||||
|
||||
- repo: https://github.com/jorisroovers/gitlint
|
||||
rev: v0.18.0
|
||||
hooks:
|
||||
- id: gitlint
|
||||
stages: [commit-msg]
|
2
Makefile
Normal file → Executable file
2
Makefile
Normal file → Executable file
|
@ -37,4 +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
|
||||
@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
|
||||
|
|
80
README.md
80
README.md
|
@ -1,6 +1,6 @@
|
|||
# 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
|
||||
# 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
|
||||
helper functions for simplifying node/dApp implementations.
|
||||
|
||||
## Repository structure
|
||||
|
@ -10,63 +10,75 @@ 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 NeoFS node.
|
||||
There is also a reference implementation of checking algorithm which is used in FrostFS node.
|
||||
|
||||
### checksum
|
||||
Contains `Checksum` type encapsulating checksum as well as it's kind.
|
||||
Currently Sha256 and [Tillich-Zemor hashsum](https://github.com/nspcc-dev/tzhash) are in use.
|
||||
Currently Sha256 and [Tillich-Zemor hashsum](https://git.frostfs.info/TrueCloudLab/tzhash) are in use.
|
||||
|
||||
### owner
|
||||
`owner.ID` type represents single account interacting with NeoFS. In v2 version of protocol
|
||||
`owner.ID` type represents single account interacting with FrostFS. 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 NeoFS-specific methods.
|
||||
Contains Bearer token type with several FrostFS-specific methods.
|
||||
|
||||
### 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.
|
||||
### 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.
|
||||
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 NeoFS without sacrificing trust, NeoFS has a concept
|
||||
To help lightweight clients interact with FrostFS without sacrificing trust, FrostFS 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 NeoFS.
|
||||
Contains client for working with FrostFS.
|
||||
```go
|
||||
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 prmInit client.PrmInit
|
||||
prmInit.SetDefaultPrivateKey(key) // private key for request signing
|
||||
prmInit.ResolveFrostFSFailures() // enable erroneous status parsing
|
||||
|
||||
res, err := c.BalanceGet(ctx, owner)
|
||||
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
|
||||
}
|
||||
|
||||
fmt.Printf("Balance for %s: %s\n", owner, res.Amount())
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5 * time.Second)
|
||||
defer cancel()
|
||||
|
||||
var prm client.PrmBalanceGet
|
||||
prm.SetAccount(acc)
|
||||
|
||||
res, err := c.BalanceGet(ctx, prm)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Balance for %s: %v\n", acc, res.Amount())
|
||||
```
|
||||
|
||||
#### Response status
|
||||
In NeoFS every operation can fail on multiple levels, so a single `error` doesn't suffice,
|
||||
In FrostFS 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
|
||||
[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.
|
||||
[FrostFS API](https://git.frostfs.info/TrueCloudLab/frostfs-api/src/branch/master/status/types.proto). There is also
|
||||
a `client.PrmInit.ResolveFrostFSFailures()` to seamlessly convert erroneous statuses into Go error type.
|
||||
|
||||
### policy
|
||||
Contains helpers allowing conversion of placing policy from/to JSON representation
|
||||
|
@ -86,19 +98,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 (
|
||||
"github.com/nspcc-dev/neofs-sdk-go/netmap"
|
||||
"github.com/nspcc-dev/neofs-sdk-go/object"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
||||
)
|
||||
|
||||
func placementNodes(addr *object.Address, p *netmap.PlacementPolicy, neofsNodes []netmap.NodeInfo) {
|
||||
// Convert list of nodes in NeoFS API format to the intermediate representation.
|
||||
func placementNodes(addr *object.Address, p *netmap.PlacementPolicy, frostfsNodes []netmap.NodeInfo) {
|
||||
// Convert list of nodes in FrostFS 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.
|
||||
|
@ -110,13 +122,13 @@ func placementNodes(addr *object.Address, p *netmap.PlacementPolicy, neofsNodes
|
|||
```
|
||||
|
||||
### pool
|
||||
Simple pool for managing connections to NeoFS nodes.
|
||||
Simple pool for managing connections to FrostFS nodes.
|
||||
|
||||
### acl, checksum, version, signature
|
||||
Contain simple API wrappers.
|
||||
|
||||
### logger
|
||||
Wrapper over `zap.Logger` which is used across NeoFS codebase.
|
||||
Wrapper over `zap.Logger` which is used across FrostFS codebase.
|
||||
|
||||
### util
|
||||
Utilities for working with signature-related code.
|
||||
Utilities for working with signature-related code.
|
||||
|
|
|
@ -1,69 +1,64 @@
|
|||
package accounting
|
||||
|
||||
import "github.com/nspcc-dev/neofs-api-go/v2/accounting"
|
||||
import "git.frostfs.info/TrueCloudLab/frostfs-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
|
||||
|
||||
// NewDecimal creates, initializes and returns empty Decimal instance.
|
||||
// ReadFromV2 reads Decimal from the accounting.Decimal message. Checks if the
|
||||
// message conforms to FrostFS API V2 protocol.
|
||||
//
|
||||
// Defaults:
|
||||
// - value: 0
|
||||
// - precision: 0
|
||||
func NewDecimal() *Decimal {
|
||||
return NewDecimalFromV2(new(accounting.Decimal))
|
||||
// See also WriteToV2.
|
||||
func (d *Decimal) ReadFromV2(m accounting.Decimal) error {
|
||||
*d = Decimal(m)
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewDecimalFromV2 converts v2 Decimal to Decimal.
|
||||
// WriteToV2 writes Decimal to the accounting.Decimal message.
|
||||
// The message must not be nil.
|
||||
//
|
||||
// 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)
|
||||
// See also ReadFromV2.
|
||||
func (d Decimal) WriteToV2(m *accounting.Decimal) {
|
||||
*m = (accounting.Decimal)(d)
|
||||
}
|
||||
|
||||
// Value returns value of the decimal number.
|
||||
func (d *Decimal) Value() int64 {
|
||||
return (*accounting.Decimal)(d).GetValue()
|
||||
//
|
||||
// Zero Decimal has zero value.
|
||||
//
|
||||
// See also SetValue.
|
||||
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.
|
||||
func (d *Decimal) Precision() uint32 {
|
||||
return (*accounting.Decimal)(d).GetPrecision()
|
||||
//
|
||||
// Zero Decimal has zero precision.
|
||||
//
|
||||
// See also SetPrecision.
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -3,15 +3,19 @@ package accounting_test
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/nspcc-dev/neofs-sdk-go/accounting"
|
||||
accountingtest "github.com/nspcc-dev/neofs-sdk-go/accounting/test"
|
||||
v2accounting "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/accounting"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/accounting"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDecimal(t *testing.T) {
|
||||
func TestDecimalData(t *testing.T) {
|
||||
const v, p = 4, 2
|
||||
|
||||
d := accounting.NewDecimal()
|
||||
var d accounting.Decimal
|
||||
|
||||
require.Zero(t, d.Value())
|
||||
require.Zero(t, d.Precision())
|
||||
|
||||
d.SetValue(v)
|
||||
d.SetPrecision(p)
|
||||
|
||||
|
@ -19,26 +23,24 @@ func TestDecimal(t *testing.T) {
|
|||
require.EqualValues(t, p, d.Precision())
|
||||
}
|
||||
|
||||
func TestDecimalEncoding(t *testing.T) {
|
||||
d := accountingtest.Decimal()
|
||||
func TestDecimalMessageV2(t *testing.T) {
|
||||
var (
|
||||
d accounting.Decimal
|
||||
m v2accounting.Decimal
|
||||
)
|
||||
|
||||
t.Run("binary", func(t *testing.T) {
|
||||
data, err := d.Marshal()
|
||||
require.NoError(t, err)
|
||||
m.SetValue(7)
|
||||
m.SetPrecision(8)
|
||||
|
||||
d2 := accounting.NewDecimal()
|
||||
require.NoError(t, d2.Unmarshal(data))
|
||||
require.NoError(t, d.ReadFromV2(m))
|
||||
|
||||
require.Equal(t, d, d2)
|
||||
})
|
||||
require.EqualValues(t, m.GetValue(), d.Value())
|
||||
require.EqualValues(t, m.GetPrecision(), d.Precision())
|
||||
|
||||
t.Run("json", func(t *testing.T) {
|
||||
data, err := d.MarshalJSON()
|
||||
require.NoError(t, err)
|
||||
var m2 v2accounting.Decimal
|
||||
|
||||
d2 := accounting.NewDecimal()
|
||||
require.NoError(t, d2.UnmarshalJSON(data))
|
||||
d.WriteToV2(&m2)
|
||||
|
||||
require.Equal(t, d, d2)
|
||||
})
|
||||
require.EqualValues(t, d.Value(), m2.GetValue())
|
||||
require.EqualValues(t, d.Precision(), m2.GetPrecision())
|
||||
}
|
||||
|
|
35
accounting/doc.go
Normal file
35
accounting/doc.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
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
|
|
@ -3,14 +3,14 @@ package accountingtest
|
|||
import (
|
||||
"math/rand"
|
||||
|
||||
"github.com/nspcc-dev/neofs-sdk-go/accounting"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/accounting"
|
||||
)
|
||||
|
||||
// Decimal returns random accounting.Decimal.
|
||||
func Decimal() *accounting.Decimal {
|
||||
d := accounting.NewDecimal()
|
||||
var d accounting.Decimal
|
||||
d.SetValue(rand.Int63())
|
||||
d.SetPrecision(rand.Uint32())
|
||||
|
||||
return d
|
||||
return &d
|
||||
}
|
||||
|
|
13
accounting/test/doc.go
Normal file
13
accounting/test/doc.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
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
105
acl/types.go
|
@ -1,105 +0,0 @@
|
|||
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
|
||||
}
|
||||
}
|
|
@ -1,82 +0,0 @@
|
|||
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())
|
||||
}
|
26
audit/doc.go
Normal file
26
audit/doc.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
Package audit provides features to process data audit in FrostFS system.
|
||||
|
||||
Result type groups values which can be gathered during data audit process:
|
||||
|
||||
var res audit.Result
|
||||
res.ForEpoch(32)
|
||||
res.ForContainer(cnr)
|
||||
// ...
|
||||
res.Complete()
|
||||
|
||||
Result instances can be stored in a binary format. On reporter side:
|
||||
|
||||
data := res.Marshal()
|
||||
// send data
|
||||
|
||||
On receiver side:
|
||||
|
||||
var res audit.Result
|
||||
err := res.Unmarshal(data)
|
||||
// ...
|
||||
|
||||
Using package types in an application is recommended to potentially work with
|
||||
different protocol versions with which these types are compatible.
|
||||
*/
|
||||
package audit
|
568
audit/result.go
568
audit/result.go
|
@ -1,291 +1,377 @@
|
|||
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"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/audit"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/version"
|
||||
)
|
||||
|
||||
// Result represents v2-compatible data audit result.
|
||||
type Result audit.DataAuditResult
|
||||
|
||||
// NewFromV2 wraps v2 DataAuditResult message to Result.
|
||||
// Result represents report on the results of the data audit in FrostFS system.
|
||||
//
|
||||
// Nil audit.DataAuditResult converts to nil.
|
||||
func NewResultFromV2(aV2 *audit.DataAuditResult) *Result {
|
||||
return (*Result)(aV2)
|
||||
}
|
||||
|
||||
// New creates and initializes blank Result.
|
||||
// Result is mutually binary-compatible with git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/audit.DataAuditResult
|
||||
// message. See Marshal / Unmarshal methods.
|
||||
//
|
||||
// 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())
|
||||
// Instances can be created using built-in var declaration.
|
||||
type Result struct {
|
||||
versionEncoded bool
|
||||
|
||||
return r
|
||||
v2 audit.DataAuditResult
|
||||
}
|
||||
|
||||
// ToV2 converts Result to v2 DataAuditResult message.
|
||||
// Marshal encodes Result into a canonical FrostFS binary format (Protocol Buffers
|
||||
// with direct field order).
|
||||
//
|
||||
// Nil Result converts to nil.
|
||||
func (r *Result) ToV2() *audit.DataAuditResult {
|
||||
return (*audit.DataAuditResult)(r)
|
||||
// Writes version.Current() protocol version into the resulting message if Result
|
||||
// hasn't been already decoded from such a message using Unmarshal.
|
||||
//
|
||||
// See also Unmarshal.
|
||||
func (r *Result) Marshal() []byte {
|
||||
if !r.versionEncoded {
|
||||
var verV2 refs.Version
|
||||
version.Current().WriteToV2(&verV2)
|
||||
r.v2.SetVersion(&verV2)
|
||||
r.versionEncoded = true
|
||||
}
|
||||
|
||||
return r.v2.StableMarshal(nil)
|
||||
}
|
||||
|
||||
// Marshal marshals Result into a protobuf binary form.
|
||||
func (r *Result) Marshal() ([]byte, error) {
|
||||
return (*audit.DataAuditResult)(r).StableMarshal(nil)
|
||||
}
|
||||
var errCIDNotSet = errors.New("container ID is not set")
|
||||
|
||||
// Unmarshal unmarshals protobuf binary representation of Result.
|
||||
// Unmarshal decodes Result from its canonical FrostFS binary format (Protocol Buffers
|
||||
// with direct field order). Returns an error describing a format violation.
|
||||
//
|
||||
// See also Marshal.
|
||||
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
|
||||
err := r.v2.Unmarshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m := make([]oid.ID, len(mV2))
|
||||
r.versionEncoded = true
|
||||
|
||||
for i := range mV2 {
|
||||
m[i] = *oid.NewIDFromV2(&mV2[i])
|
||||
// format checks
|
||||
|
||||
var cID cid.ID
|
||||
|
||||
cidV2 := r.v2.GetContainerID()
|
||||
if cidV2 == nil {
|
||||
return errCIDNotSet
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
err = cID.ReadFromV2(*cidV2)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not convert V2 container ID: %w", err)
|
||||
}
|
||||
|
||||
// SetPassSG sets list of Storage Groups that passed audit PoR stage.
|
||||
func (r *Result) SetPassSG(list []oid.ID) {
|
||||
mV2 := (*audit.DataAuditResult)(r).
|
||||
GetPassSG()
|
||||
var (
|
||||
oID oid.ID
|
||||
oidV2 refs.ObjectID
|
||||
)
|
||||
|
||||
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()
|
||||
for _, oidV2 = range r.v2.GetPassSG() {
|
||||
err = oID.ReadFromV2(oidV2)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid passed storage group ID: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
(*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()
|
||||
for _, oidV2 = range r.v2.GetFailSG() {
|
||||
err = oID.ReadFromV2(oidV2)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid failed storage group ID: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
(*audit.DataAuditResult)(r).SetFailSG(mV2)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Hit returns number of sampled objects under audit placed
|
||||
// Epoch returns FrostFS epoch when the data associated with the Result was audited.
|
||||
//
|
||||
// Zero Result has zero epoch.
|
||||
//
|
||||
// See also ForEpoch.
|
||||
func (r Result) Epoch() uint64 {
|
||||
return r.v2.GetAuditEpoch()
|
||||
}
|
||||
|
||||
// ForEpoch specifies FrostFS epoch when the data associated with the Result was audited.
|
||||
//
|
||||
// See also Epoch.
|
||||
func (r *Result) ForEpoch(epoch uint64) {
|
||||
r.v2.SetAuditEpoch(epoch)
|
||||
}
|
||||
|
||||
// Container returns identifier of the container with which the data audit Result
|
||||
// is associated and a bool that indicates container ID field presence in the Result.
|
||||
//
|
||||
// Zero Result does not have container ID.
|
||||
//
|
||||
// See also ForContainer.
|
||||
func (r Result) Container() (cid.ID, bool) {
|
||||
var cID cid.ID
|
||||
|
||||
cidV2 := r.v2.GetContainerID()
|
||||
if cidV2 != nil {
|
||||
_ = cID.ReadFromV2(*cidV2)
|
||||
return cID, true
|
||||
}
|
||||
|
||||
return cID, false
|
||||
}
|
||||
|
||||
// ForContainer sets identifier of the container with which the data audit Result
|
||||
// is associated.
|
||||
//
|
||||
// See also Container.
|
||||
func (r *Result) ForContainer(cnr cid.ID) {
|
||||
var cidV2 refs.ContainerID
|
||||
cnr.WriteToV2(&cidV2)
|
||||
|
||||
r.v2.SetContainerID(&cidV2)
|
||||
}
|
||||
|
||||
// AuditorKey returns public key of the auditing FrostFS Inner Ring node in
|
||||
// a FrostFS binary key format.
|
||||
//
|
||||
// Zero Result has nil key. Return value MUST NOT be mutated: to do this,
|
||||
// first make a copy.
|
||||
//
|
||||
// See also SetAuditorPublicKey.
|
||||
func (r Result) AuditorKey() []byte {
|
||||
return r.v2.GetPublicKey()
|
||||
}
|
||||
|
||||
// SetAuditorKey specifies public key of the auditing FrostFS Inner Ring node in
|
||||
// a FrostFS binary key format.
|
||||
//
|
||||
// Argument MUST NOT be mutated at least until the end of using the Result.
|
||||
//
|
||||
// See also AuditorKey.
|
||||
func (r *Result) SetAuditorKey(key []byte) {
|
||||
r.v2.SetPublicKey(key)
|
||||
}
|
||||
|
||||
// Completed returns completion state of the data audit associated with the Result.
|
||||
//
|
||||
// Zero Result corresponds to incomplete data audit.
|
||||
//
|
||||
// See also Complete.
|
||||
func (r Result) Completed() bool {
|
||||
return r.v2.GetComplete()
|
||||
}
|
||||
|
||||
// Complete marks the data audit associated with the Result as completed.
|
||||
//
|
||||
// See also Completed.
|
||||
func (r *Result) Complete() {
|
||||
r.v2.SetComplete(true)
|
||||
}
|
||||
|
||||
// RequestsPoR returns number of requests made by Proof-of-Retrievability
|
||||
// audit check to get all headers of the objects inside storage groups.
|
||||
//
|
||||
// Zero Result has zero requests.
|
||||
//
|
||||
// See also SetRequestsPoR.
|
||||
func (r Result) RequestsPoR() uint32 {
|
||||
return r.v2.GetRequests()
|
||||
}
|
||||
|
||||
// SetRequestsPoR sets number of requests made by Proof-of-Retrievability
|
||||
// audit check to get all headers of the objects inside storage groups.
|
||||
//
|
||||
// See also RequestsPoR.
|
||||
func (r *Result) SetRequestsPoR(v uint32) {
|
||||
r.v2.SetRequests(v)
|
||||
}
|
||||
|
||||
// RetriesPoR returns number of retries made by Proof-of-Retrievability
|
||||
// audit check to get all headers of the objects inside storage groups.
|
||||
//
|
||||
// Zero Result has zero retries.
|
||||
//
|
||||
// See also SetRetriesPoR.
|
||||
func (r Result) RetriesPoR() uint32 {
|
||||
return r.v2.GetRetries()
|
||||
}
|
||||
|
||||
// SetRetriesPoR sets number of retries made by Proof-of-Retrievability
|
||||
// audit check to get all headers of the objects inside storage groups.
|
||||
//
|
||||
// See also RetriesPoR.
|
||||
func (r *Result) SetRetriesPoR(v uint32) {
|
||||
r.v2.SetRetries(v)
|
||||
}
|
||||
|
||||
// IteratePassedStorageGroups iterates over all storage groups that passed
|
||||
// Proof-of-Retrievability audit check and passes them into f. Breaks on f's
|
||||
// false return, f MUST NOT be nil.
|
||||
//
|
||||
// Zero Result has no passed storage groups and doesn't call f.
|
||||
//
|
||||
// See also SubmitPassedStorageGroup.
|
||||
func (r Result) IteratePassedStorageGroups(f func(oid.ID) bool) {
|
||||
r2 := r.v2.GetPassSG()
|
||||
|
||||
var id oid.ID
|
||||
|
||||
for i := range r2 {
|
||||
_ = id.ReadFromV2(r2[i])
|
||||
|
||||
if !f(id) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SubmitPassedStorageGroup marks storage group as passed Proof-of-Retrievability
|
||||
// audit check.
|
||||
//
|
||||
// See also IteratePassedStorageGroups.
|
||||
func (r *Result) SubmitPassedStorageGroup(sg oid.ID) {
|
||||
var idV2 refs.ObjectID
|
||||
sg.WriteToV2(&idV2)
|
||||
|
||||
r.v2.SetPassSG(append(r.v2.GetPassSG(), idV2))
|
||||
}
|
||||
|
||||
// IterateFailedStorageGroups is similar to IteratePassedStorageGroups but for failed groups.
|
||||
//
|
||||
// See also SubmitFailedStorageGroup.
|
||||
func (r Result) IterateFailedStorageGroups(f func(oid.ID) bool) {
|
||||
v := r.v2.GetFailSG()
|
||||
var id oid.ID
|
||||
|
||||
for i := range v {
|
||||
_ = id.ReadFromV2(v[i])
|
||||
if !f(id) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SubmitFailedStorageGroup is similar to SubmitPassedStorageGroup but for failed groups.
|
||||
//
|
||||
// See also IterateFailedStorageGroups.
|
||||
func (r *Result) SubmitFailedStorageGroup(sg oid.ID) {
|
||||
var idV2 refs.ObjectID
|
||||
sg.WriteToV2(&idV2)
|
||||
|
||||
r.v2.SetFailSG(append(r.v2.GetFailSG(), idV2))
|
||||
}
|
||||
|
||||
// Hits returns number of sampled objects under audit placed
|
||||
// in an optimal way according to the container's placement policy
|
||||
// when checking Proof-of-Placement.
|
||||
//
|
||||
// Zero result has zero hits.
|
||||
//
|
||||
// See also SetHits.
|
||||
func (r Result) Hits() uint32 {
|
||||
return r.v2.GetHit()
|
||||
}
|
||||
|
||||
// SetHits sets number of sampled objects under audit placed
|
||||
// in an optimal way according to the containers placement policy
|
||||
// when checking PoP.
|
||||
func (r *Result) Hit() uint32 {
|
||||
return (*audit.DataAuditResult)(r).GetHit()
|
||||
// when checking Proof-of-Placement.
|
||||
//
|
||||
// See also Hits.
|
||||
func (r *Result) SetHits(hit uint32) {
|
||||
r.v2.SetHit(hit)
|
||||
}
|
||||
|
||||
// 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)
|
||||
// Misses returns number of sampled objects under audit placed
|
||||
// in suboptimal way according to the container's placement policy,
|
||||
// but still at a satisfactory level when checking Proof-of-Placement.
|
||||
//
|
||||
// Zero Result has zero misses.
|
||||
//
|
||||
// See also SetMisses.
|
||||
func (r Result) Misses() uint32 {
|
||||
return r.v2.GetMiss()
|
||||
}
|
||||
|
||||
// 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()
|
||||
// SetMisses sets number of sampled objects under audit placed
|
||||
// in suboptimal way according to the container's placement policy,
|
||||
// but still at a satisfactory level when checking Proof-of-Placement.
|
||||
//
|
||||
// See also Misses.
|
||||
func (r *Result) SetMisses(miss uint32) {
|
||||
r.v2.SetMiss(miss)
|
||||
}
|
||||
|
||||
// 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
|
||||
// Failures 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()
|
||||
// when checking Proof-of-Placement.
|
||||
//
|
||||
// Zero result has zero failures.
|
||||
//
|
||||
// See also SetFailures.
|
||||
func (r Result) Failures() uint32 {
|
||||
return r.v2.GetFail()
|
||||
}
|
||||
|
||||
// SetFail sets number of sampled objects under audit stored
|
||||
// SetFailures 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)
|
||||
// when checking Proof-of-Placement.
|
||||
//
|
||||
// See also Failures.
|
||||
func (r *Result) SetFailures(fail uint32) {
|
||||
r.v2.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()
|
||||
// IteratePassedStorageNodes iterates over all storage nodes that passed at least one
|
||||
// Proof-of-Data-Possession audit check and passes their public keys into f. Breaks on
|
||||
// f's false return.
|
||||
//
|
||||
// f MUST NOT be nil and MUST NOT mutate parameter passed into it at least until
|
||||
// the end of using the Result.
|
||||
//
|
||||
// Zero Result has no passed storage nodes and doesn't call f.
|
||||
//
|
||||
// See also SubmitPassedStorageNode.
|
||||
func (r Result) IteratePassedStorageNodes(f func([]byte) bool) {
|
||||
v := r.v2.GetPassNodes()
|
||||
|
||||
for i := range v {
|
||||
if !f(v[i]) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
// SubmitPassedStorageNodes marks storage node list as passed Proof-of-Data-Possession
|
||||
// audit check. The list contains public keys.
|
||||
//
|
||||
// Argument and its elements MUST NOT be mutated at least until the end of using the Result.
|
||||
//
|
||||
// See also IteratePassedStorageNodes.
|
||||
func (r *Result) SubmitPassedStorageNodes(list [][]byte) {
|
||||
r.v2.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()
|
||||
// IterateFailedStorageNodes is similar to IteratePassedStorageNodes but for failed nodes.
|
||||
//
|
||||
// See also SubmitPassedStorageNodes.
|
||||
func (r Result) IterateFailedStorageNodes(f func([]byte) bool) {
|
||||
v := r.v2.GetFailNodes()
|
||||
|
||||
for i := range v {
|
||||
if !f(v[i]) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
// SubmitFailedStorageNodes is similar to SubmitPassedStorageNodes but for failed nodes.
|
||||
//
|
||||
// See also IterateFailedStorageNodes.
|
||||
func (r *Result) SubmitFailedStorageNodes(list [][]byte) {
|
||||
r.v2.SetFailNodes(list)
|
||||
}
|
||||
|
|
|
@ -1,154 +1,191 @@
|
|||
package audit_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"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"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/audit"
|
||||
audittest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/audit/test"
|
||||
cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test"
|
||||
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestResult(t *testing.T) {
|
||||
r := audit.NewResult()
|
||||
require.Equal(t, version.Current(), r.Version())
|
||||
func TestResultData(t *testing.T) {
|
||||
var r audit.Result
|
||||
|
||||
countSG := func(passed bool, f func(oid.ID)) int {
|
||||
called := 0
|
||||
|
||||
ff := func(arg oid.ID) bool {
|
||||
called++
|
||||
|
||||
if f != nil {
|
||||
f(arg)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
if passed {
|
||||
r.IteratePassedStorageGroups(ff)
|
||||
} else {
|
||||
r.IterateFailedStorageGroups(ff)
|
||||
}
|
||||
|
||||
return called
|
||||
}
|
||||
|
||||
countPassSG := func(f func(oid.ID)) int { return countSG(true, f) }
|
||||
countFailSG := func(f func(oid.ID)) int { return countSG(false, f) }
|
||||
|
||||
countNodes := func(passed bool, f func([]byte)) int {
|
||||
called := 0
|
||||
|
||||
ff := func(arg []byte) bool {
|
||||
called++
|
||||
|
||||
if f != nil {
|
||||
f(arg)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
if passed {
|
||||
r.IteratePassedStorageNodes(ff)
|
||||
} else {
|
||||
r.IterateFailedStorageNodes(ff)
|
||||
}
|
||||
|
||||
return called
|
||||
}
|
||||
|
||||
countPassNodes := func(f func([]byte)) int { return countNodes(true, f) }
|
||||
countFailNodes := func(f func([]byte)) int { return countNodes(false, f) }
|
||||
|
||||
require.Zero(t, r.Epoch())
|
||||
_, set := r.Container()
|
||||
require.False(t, set)
|
||||
require.Nil(t, r.AuditorKey())
|
||||
require.False(t, r.Completed())
|
||||
require.Zero(t, r.RequestsPoR())
|
||||
require.Zero(t, r.RetriesPoR())
|
||||
require.Zero(t, countPassSG(nil))
|
||||
require.Zero(t, countFailSG(nil))
|
||||
require.Zero(t, countPassNodes(nil))
|
||||
require.Zero(t, countFailNodes(nil))
|
||||
|
||||
epoch := uint64(13)
|
||||
r.SetAuditEpoch(epoch)
|
||||
require.Equal(t, epoch, r.AuditEpoch())
|
||||
r.ForEpoch(epoch)
|
||||
require.Equal(t, epoch, r.Epoch())
|
||||
|
||||
cid := cidtest.ID()
|
||||
r.SetContainerID(cid)
|
||||
require.Equal(t, cid, r.ContainerID())
|
||||
cnr := cidtest.ID()
|
||||
r.ForContainer(cnr)
|
||||
cID, set := r.Container()
|
||||
require.True(t, set)
|
||||
require.Equal(t, cnr, cID)
|
||||
|
||||
key := []byte{1, 2, 3}
|
||||
r.SetPublicKey(key)
|
||||
require.Equal(t, key, r.PublicKey())
|
||||
r.SetAuditorKey(key)
|
||||
require.Equal(t, key, r.AuditorKey())
|
||||
|
||||
r.SetComplete(true)
|
||||
require.True(t, r.Complete())
|
||||
r.Complete()
|
||||
require.True(t, r.Completed())
|
||||
|
||||
requests := uint32(2)
|
||||
r.SetRequests(requests)
|
||||
require.Equal(t, requests, r.Requests())
|
||||
r.SetRequestsPoR(requests)
|
||||
require.Equal(t, requests, r.RequestsPoR())
|
||||
|
||||
retries := uint32(1)
|
||||
r.SetRetries(retries)
|
||||
require.Equal(t, retries, r.Retries())
|
||||
r.SetRetriesPoR(retries)
|
||||
require.Equal(t, retries, r.RetriesPoR())
|
||||
|
||||
passSG := []oid.ID{*oidtest.ID(), *oidtest.ID()}
|
||||
r.SetPassSG(passSG)
|
||||
require.Equal(t, passSG, r.PassSG())
|
||||
passSG1, passSG2 := oidtest.ID(), oidtest.ID()
|
||||
r.SubmitPassedStorageGroup(passSG1)
|
||||
r.SubmitPassedStorageGroup(passSG2)
|
||||
|
||||
failSG := []oid.ID{*oidtest.ID(), *oidtest.ID()}
|
||||
r.SetFailSG(failSG)
|
||||
require.Equal(t, failSG, r.FailSG())
|
||||
called1, called2 := false, false
|
||||
|
||||
require.EqualValues(t, 2, countPassSG(func(id oid.ID) {
|
||||
if id.Equals(passSG1) {
|
||||
called1 = true
|
||||
} else if id.Equals(passSG2) {
|
||||
called2 = true
|
||||
}
|
||||
}))
|
||||
require.True(t, called1)
|
||||
require.True(t, called2)
|
||||
|
||||
failSG1, failSG2 := oidtest.ID(), oidtest.ID()
|
||||
r.SubmitFailedStorageGroup(failSG1)
|
||||
r.SubmitFailedStorageGroup(failSG2)
|
||||
|
||||
called1, called2 = false, false
|
||||
|
||||
require.EqualValues(t, 2, countFailSG(func(id oid.ID) {
|
||||
if id.Equals(failSG1) {
|
||||
called1 = true
|
||||
} else if id.Equals(failSG2) {
|
||||
called2 = true
|
||||
}
|
||||
}))
|
||||
require.True(t, called1)
|
||||
require.True(t, called2)
|
||||
|
||||
hit := uint32(1)
|
||||
r.SetHit(hit)
|
||||
require.Equal(t, hit, r.Hit())
|
||||
r.SetHits(hit)
|
||||
require.Equal(t, hit, r.Hits())
|
||||
|
||||
miss := uint32(2)
|
||||
r.SetMiss(miss)
|
||||
require.Equal(t, miss, r.Miss())
|
||||
r.SetMisses(miss)
|
||||
require.Equal(t, miss, r.Misses())
|
||||
|
||||
fail := uint32(3)
|
||||
r.SetFail(fail)
|
||||
require.Equal(t, fail, r.Fail())
|
||||
r.SetFailures(fail)
|
||||
require.Equal(t, fail, r.Failures())
|
||||
|
||||
passNodes := [][]byte{{1}, {2}}
|
||||
r.SetPassNodes(passNodes)
|
||||
require.Equal(t, passNodes, r.PassNodes())
|
||||
r.SubmitPassedStorageNodes(passNodes)
|
||||
|
||||
called1, called2 = false, false
|
||||
|
||||
require.EqualValues(t, 2, countPassNodes(func(arg []byte) {
|
||||
if bytes.Equal(arg, passNodes[0]) {
|
||||
called1 = true
|
||||
} else if bytes.Equal(arg, passNodes[1]) {
|
||||
called2 = true
|
||||
}
|
||||
}))
|
||||
require.True(t, called1)
|
||||
require.True(t, called2)
|
||||
|
||||
failNodes := [][]byte{{3}, {4}}
|
||||
r.SetFailNodes(failNodes)
|
||||
require.Equal(t, failNodes, r.FailNodes())
|
||||
r.SubmitFailedStorageNodes(failNodes)
|
||||
|
||||
called1, called2 = false, false
|
||||
|
||||
require.EqualValues(t, 2, countFailNodes(func(arg []byte) {
|
||||
if bytes.Equal(arg, failNodes[0]) {
|
||||
called1 = true
|
||||
} else if bytes.Equal(arg, failNodes[1]) {
|
||||
called2 = true
|
||||
}
|
||||
}))
|
||||
require.True(t, called1)
|
||||
require.True(t, called2)
|
||||
}
|
||||
|
||||
func TestStorageGroupEncoding(t *testing.T) {
|
||||
r := audittest.Result()
|
||||
func TestResultEncoding(t *testing.T) {
|
||||
r := *audittest.Result()
|
||||
|
||||
t.Run("binary", func(t *testing.T) {
|
||||
data, err := r.Marshal()
|
||||
require.NoError(t, err)
|
||||
data := r.Marshal()
|
||||
|
||||
r2 := audit.NewResult()
|
||||
var r2 audit.Result
|
||||
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))
|
||||
})
|
||||
}
|
||||
|
|
13
audit/test/doc.go
Normal file
13
audit/test/doc.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
Package audittest provides functions for convenient testing of audit package API.
|
||||
|
||||
Note that importing the package into source files is highly discouraged.
|
||||
|
||||
Random instance generation functions can be useful when testing expects any value, e.g.:
|
||||
|
||||
import audittest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/audit/test"
|
||||
|
||||
dec := audittest.Result()
|
||||
// test the value
|
||||
*/
|
||||
package audittest
|
|
@ -1,37 +1,36 @@
|
|||
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"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/audit"
|
||||
cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test"
|
||||
oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test"
|
||||
)
|
||||
|
||||
// Result returns random audit.Result.
|
||||
func Result() *audit.Result {
|
||||
x := audit.NewResult()
|
||||
var x audit.Result
|
||||
|
||||
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{
|
||||
x.ForContainer(cidtest.ID())
|
||||
x.SetAuditorKey([]byte("key"))
|
||||
x.Complete()
|
||||
x.ForEpoch(44)
|
||||
x.SetHits(55)
|
||||
x.SetMisses(66)
|
||||
x.SetFailures(77)
|
||||
x.SetRequestsPoR(88)
|
||||
x.SetRequestsPoR(99)
|
||||
x.SubmitFailedStorageNodes([][]byte{
|
||||
[]byte("node1"),
|
||||
[]byte("node2"),
|
||||
})
|
||||
x.SetPassNodes([][]byte{
|
||||
x.SubmitPassedStorageNodes([][]byte{
|
||||
[]byte("node3"),
|
||||
[]byte("node4"),
|
||||
})
|
||||
x.SetPassSG([]oid.ID{*oidtest.ID(), *oidtest.ID()})
|
||||
x.SetFailSG([]oid.ID{*oidtest.ID(), *oidtest.ID()})
|
||||
x.SubmitPassedStorageGroup(oidtest.ID())
|
||||
x.SubmitPassedStorageGroup(oidtest.ID())
|
||||
x.SubmitFailedStorageGroup(oidtest.ID())
|
||||
x.SubmitFailedStorageGroup(oidtest.ID())
|
||||
|
||||
return x
|
||||
return &x
|
||||
}
|
||||
|
|
370
bearer/bearer.go
Normal file
370
bearer/bearer.go
Normal file
|
@ -0,0 +1,370 @@
|
|||
package bearer
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto"
|
||||
frostfsecdsa "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto/ecdsa"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
|
||||
)
|
||||
|
||||
// Token represents bearer token for object service operations.
|
||||
//
|
||||
// Token is mutually compatible with git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl.BearerToken
|
||||
// message. See ReadFromV2 / WriteToV2 methods.
|
||||
//
|
||||
// Instances can be created using built-in var declaration.
|
||||
type Token struct {
|
||||
targetUserSet bool
|
||||
targetUser user.ID
|
||||
|
||||
eaclTableSet bool
|
||||
eaclTable eacl.Table
|
||||
|
||||
lifetimeSet bool
|
||||
iat, nbf, exp uint64
|
||||
|
||||
sigSet bool
|
||||
sig refs.Signature
|
||||
}
|
||||
|
||||
// reads Token from the acl.BearerToken message. If checkFieldPresence is set,
|
||||
// returns an error on absence of any protocol-required field.
|
||||
func (b *Token) readFromV2(m acl.BearerToken, checkFieldPresence bool) error {
|
||||
var err error
|
||||
|
||||
body := m.GetBody()
|
||||
if checkFieldPresence && body == nil {
|
||||
return errors.New("missing token body")
|
||||
}
|
||||
|
||||
eaclTable := body.GetEACL()
|
||||
if b.eaclTableSet = eaclTable != nil; b.eaclTableSet {
|
||||
b.eaclTable = *eacl.NewTableFromV2(eaclTable)
|
||||
} else if checkFieldPresence {
|
||||
return errors.New("missing eACL table")
|
||||
}
|
||||
|
||||
targetUser := body.GetOwnerID()
|
||||
if b.targetUserSet = targetUser != nil; b.targetUserSet {
|
||||
err = b.targetUser.ReadFromV2(*targetUser)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid target user: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
lifetime := body.GetLifetime()
|
||||
if b.lifetimeSet = lifetime != nil; b.lifetimeSet {
|
||||
b.iat = lifetime.GetIat()
|
||||
b.nbf = lifetime.GetNbf()
|
||||
b.exp = lifetime.GetExp()
|
||||
} else if checkFieldPresence {
|
||||
return errors.New("missing token lifetime")
|
||||
}
|
||||
|
||||
sig := m.GetSignature()
|
||||
if b.sigSet = sig != nil; sig != nil {
|
||||
b.sig = *sig
|
||||
} else if checkFieldPresence {
|
||||
return errors.New("missing body signature")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadFromV2 reads Token from the acl.BearerToken message.
|
||||
//
|
||||
// See also WriteToV2.
|
||||
func (b *Token) ReadFromV2(m acl.BearerToken) error {
|
||||
return b.readFromV2(m, true)
|
||||
}
|
||||
|
||||
func (b Token) fillBody() *acl.BearerTokenBody {
|
||||
if !b.eaclTableSet && !b.targetUserSet && !b.lifetimeSet {
|
||||
return nil
|
||||
}
|
||||
|
||||
var body acl.BearerTokenBody
|
||||
|
||||
if b.eaclTableSet {
|
||||
body.SetEACL(b.eaclTable.ToV2())
|
||||
}
|
||||
|
||||
if b.targetUserSet {
|
||||
var targetUser refs.OwnerID
|
||||
b.targetUser.WriteToV2(&targetUser)
|
||||
|
||||
body.SetOwnerID(&targetUser)
|
||||
}
|
||||
|
||||
if b.lifetimeSet {
|
||||
var lifetime acl.TokenLifetime
|
||||
lifetime.SetIat(b.iat)
|
||||
lifetime.SetNbf(b.nbf)
|
||||
lifetime.SetExp(b.exp)
|
||||
|
||||
body.SetLifetime(&lifetime)
|
||||
}
|
||||
|
||||
return &body
|
||||
}
|
||||
|
||||
func (b Token) signedData() []byte {
|
||||
return b.fillBody().StableMarshal(nil)
|
||||
}
|
||||
|
||||
// WriteToV2 writes Token to the acl.BearerToken message.
|
||||
// The message must not be nil.
|
||||
//
|
||||
// See also ReadFromV2.
|
||||
func (b Token) WriteToV2(m *acl.BearerToken) {
|
||||
m.SetBody(b.fillBody())
|
||||
|
||||
var sig *refs.Signature
|
||||
|
||||
if b.sigSet {
|
||||
sig = &b.sig
|
||||
}
|
||||
|
||||
m.SetSignature(sig)
|
||||
}
|
||||
|
||||
// SetExp sets "exp" (expiration time) claim which identifies the
|
||||
// expiration time (in FrostFS epochs) after which the Token MUST NOT be
|
||||
// accepted for processing. The processing of the "exp" claim requires
|
||||
// that the current epoch MUST be before or equal to the expiration epoch
|
||||
// listed in the "exp" claim.
|
||||
//
|
||||
// Naming is inspired by https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4.
|
||||
//
|
||||
// See also InvalidAt.
|
||||
func (b *Token) SetExp(exp uint64) {
|
||||
b.exp = exp
|
||||
b.lifetimeSet = true
|
||||
}
|
||||
|
||||
// SetNbf sets "nbf" (not before) claim which identifies the time (in
|
||||
// FrostFS epochs) before which the Token MUST NOT be accepted for processing. The
|
||||
// processing of the "nbf" claim requires that the current epoch MUST be
|
||||
// after or equal to the not-before epoch listed in the "nbf" claim.
|
||||
//
|
||||
// Naming is inspired by https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5.
|
||||
//
|
||||
// See also InvalidAt.
|
||||
func (b *Token) SetNbf(nbf uint64) {
|
||||
b.nbf = nbf
|
||||
b.lifetimeSet = true
|
||||
}
|
||||
|
||||
// SetIat sets "iat" (issued at) claim which identifies the time (in FrostFS
|
||||
// epochs) at which the Token was issued. This claim can be used to determine
|
||||
// the age of the Token.
|
||||
//
|
||||
// Naming is inspired by https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6.
|
||||
//
|
||||
// See also InvalidAt.
|
||||
func (b *Token) SetIat(iat uint64) {
|
||||
b.iat = iat
|
||||
b.lifetimeSet = true
|
||||
}
|
||||
|
||||
// InvalidAt asserts "exp", "nbf" and "iat" claims for the given epoch.
|
||||
//
|
||||
// Zero Container is invalid in any epoch.
|
||||
//
|
||||
// See also SetExp, SetNbf, SetIat.
|
||||
func (b Token) InvalidAt(epoch uint64) bool {
|
||||
return !b.lifetimeSet || b.nbf > epoch || b.iat > epoch || b.exp < epoch
|
||||
}
|
||||
|
||||
// SetEACLTable sets eacl.Table that replaces the one from the issuer's
|
||||
// container. If table has specified container, bearer token can be used only
|
||||
// for operations within this specific container. Otherwise, Token can be used
|
||||
// within any issuer's container.
|
||||
//
|
||||
// SetEACLTable MUST be called if Token is going to be transmitted over
|
||||
// FrostFS API V2 protocol.
|
||||
//
|
||||
// See also EACLTable, AssertContainer.
|
||||
func (b *Token) SetEACLTable(table eacl.Table) {
|
||||
b.eaclTable = table
|
||||
b.eaclTableSet = true
|
||||
}
|
||||
|
||||
// EACLTable returns extended ACL table set by SetEACLTable.
|
||||
//
|
||||
// Zero Token has zero eacl.Table.
|
||||
func (b Token) EACLTable() eacl.Table {
|
||||
if b.eaclTableSet {
|
||||
return b.eaclTable
|
||||
}
|
||||
|
||||
return eacl.Table{}
|
||||
}
|
||||
|
||||
// AssertContainer checks if the token is valid within the given container.
|
||||
//
|
||||
// Note: cnr is assumed to refer to the issuer's container, otherwise the check
|
||||
// is meaningless.
|
||||
//
|
||||
// Zero Token is valid in any container.
|
||||
//
|
||||
// See also SetEACLTable.
|
||||
func (b Token) AssertContainer(cnr cid.ID) bool {
|
||||
if !b.eaclTableSet {
|
||||
return true
|
||||
}
|
||||
|
||||
cnrTable, set := b.eaclTable.CID()
|
||||
return !set || cnrTable.Equals(cnr)
|
||||
}
|
||||
|
||||
// ForUser specifies ID of the user who can use the Token for the operations
|
||||
// within issuer's container(s).
|
||||
//
|
||||
// Optional: by default, any user has access to Token usage.
|
||||
//
|
||||
// See also AssertUser.
|
||||
func (b *Token) ForUser(id user.ID) {
|
||||
b.targetUser = id
|
||||
b.targetUserSet = true
|
||||
}
|
||||
|
||||
// AssertUser checks if the Token is issued to the given user.
|
||||
//
|
||||
// Zero Token is available to any user.
|
||||
//
|
||||
// See also ForUser.
|
||||
func (b Token) AssertUser(id user.ID) bool {
|
||||
return !b.targetUserSet || b.targetUser.Equals(id)
|
||||
}
|
||||
|
||||
// Sign calculates and writes signature of the Token data using issuer's secret.
|
||||
// Returns signature calculation errors.
|
||||
//
|
||||
// Sign MUST be called if Token is going to be transmitted over
|
||||
// FrostFS API V2 protocol.
|
||||
//
|
||||
// Note that any Token mutation is likely to break the signature, so it is
|
||||
// expected to be calculated as a final stage of Token formation.
|
||||
//
|
||||
// See also VerifySignature, Issuer.
|
||||
func (b *Token) Sign(key ecdsa.PrivateKey) error {
|
||||
var sig frostfscrypto.Signature
|
||||
|
||||
err := sig.Calculate(frostfsecdsa.Signer(key), b.signedData())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sig.WriteToV2(&b.sig)
|
||||
b.sigSet = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifySignature checks if Token signature is presented and valid.
|
||||
//
|
||||
// Zero Token fails the check.
|
||||
//
|
||||
// See also Sign.
|
||||
func (b Token) VerifySignature() bool {
|
||||
if !b.sigSet {
|
||||
return false
|
||||
}
|
||||
|
||||
var sig frostfscrypto.Signature
|
||||
|
||||
// TODO: (#233) check owner<->key relation
|
||||
return sig.ReadFromV2(b.sig) == nil && sig.Verify(b.signedData())
|
||||
}
|
||||
|
||||
// Marshal encodes Token into a binary format of the FrostFS API protocol
|
||||
// (Protocol Buffers V3 with direct field order).
|
||||
//
|
||||
// See also Unmarshal.
|
||||
func (b Token) Marshal() []byte {
|
||||
var m acl.BearerToken
|
||||
b.WriteToV2(&m)
|
||||
|
||||
return m.StableMarshal(nil)
|
||||
}
|
||||
|
||||
// Unmarshal decodes FrostFS API protocol binary data into the Token
|
||||
// (Protocol Buffers V3 with direct field order). Returns an error describing
|
||||
// a format violation.
|
||||
//
|
||||
// See also Marshal.
|
||||
func (b *Token) Unmarshal(data []byte) error {
|
||||
var m acl.BearerToken
|
||||
|
||||
err := m.Unmarshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return b.readFromV2(m, false)
|
||||
}
|
||||
|
||||
// MarshalJSON encodes Token into a JSON format of the FrostFS API protocol
|
||||
// (Protocol Buffers V3 JSON).
|
||||
//
|
||||
// See also UnmarshalJSON.
|
||||
func (b Token) MarshalJSON() ([]byte, error) {
|
||||
var m acl.BearerToken
|
||||
b.WriteToV2(&m)
|
||||
|
||||
return m.MarshalJSON()
|
||||
}
|
||||
|
||||
// UnmarshalJSON decodes FrostFS API protocol JSON data into the Token
|
||||
// (Protocol Buffers V3 JSON). Returns an error describing a format violation.
|
||||
//
|
||||
// See also MarshalJSON.
|
||||
func (b *Token) UnmarshalJSON(data []byte) error {
|
||||
var m acl.BearerToken
|
||||
|
||||
err := m.UnmarshalJSON(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return b.readFromV2(m, false)
|
||||
}
|
||||
|
||||
// SigningKeyBytes returns issuer's public key in a binary format of
|
||||
// FrostFS API protocol.
|
||||
//
|
||||
// Unsigned Token has empty key.
|
||||
//
|
||||
// See also ResolveIssuer.
|
||||
func (b Token) SigningKeyBytes() []byte {
|
||||
if b.sigSet {
|
||||
return b.sig.GetKey()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResolveIssuer resolves issuer's user.ID from the key used for Token signing.
|
||||
// Returns zero user.ID if Token is unsigned or key has incorrect format.
|
||||
//
|
||||
// See also SigningKeyBytes.
|
||||
func ResolveIssuer(b Token) (usr user.ID) {
|
||||
binKey := b.SigningKeyBytes()
|
||||
|
||||
if len(binKey) != 0 {
|
||||
var key frostfsecdsa.PublicKey
|
||||
if key.Decode(binKey) == nil {
|
||||
user.IDFromKey(&usr, ecdsa.PublicKey(key))
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
390
bearer/bearer_test.go
Normal file
390
bearer/bearer_test.go
Normal file
|
@ -0,0 +1,390 @@
|
|||
package bearer_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
|
||||
bearertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer/test"
|
||||
cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test"
|
||||
frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto"
|
||||
frostfsecdsa "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto/ecdsa"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl"
|
||||
eacltest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl/test"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
|
||||
usertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user/test"
|
||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// compares binary representations of two eacl.Table instances.
|
||||
func isEqualEACLTables(t1, t2 eacl.Table) bool {
|
||||
d1, err := t1.Marshal()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
d2, err := t2.Marshal()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return bytes.Equal(d1, d2)
|
||||
}
|
||||
|
||||
func TestToken_SetEACLTable(t *testing.T) {
|
||||
var val bearer.Token
|
||||
var m acl.BearerToken
|
||||
filled := bearertest.Token()
|
||||
|
||||
val.WriteToV2(&m)
|
||||
require.Zero(t, m.GetBody())
|
||||
|
||||
val2 := filled
|
||||
|
||||
require.NoError(t, val2.Unmarshal(val.Marshal()))
|
||||
require.Zero(t, val2.EACLTable())
|
||||
|
||||
val2 = filled
|
||||
|
||||
jd, err := val.MarshalJSON()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, val2.UnmarshalJSON(jd))
|
||||
require.Zero(t, val2.EACLTable())
|
||||
|
||||
// set value
|
||||
|
||||
eaclTable := *eacltest.Table()
|
||||
|
||||
val.SetEACLTable(eaclTable)
|
||||
require.True(t, isEqualEACLTables(eaclTable, val.EACLTable()))
|
||||
|
||||
val.WriteToV2(&m)
|
||||
eaclTableV2 := eaclTable.ToV2()
|
||||
require.Equal(t, eaclTableV2, m.GetBody().GetEACL())
|
||||
|
||||
val2 = filled
|
||||
|
||||
require.NoError(t, val2.Unmarshal(val.Marshal()))
|
||||
require.True(t, isEqualEACLTables(eaclTable, val.EACLTable()))
|
||||
|
||||
val2 = filled
|
||||
|
||||
jd, err = val.MarshalJSON()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, val2.UnmarshalJSON(jd))
|
||||
require.True(t, isEqualEACLTables(eaclTable, val.EACLTable()))
|
||||
}
|
||||
|
||||
func TestToken_ForUser(t *testing.T) {
|
||||
var val bearer.Token
|
||||
var m acl.BearerToken
|
||||
filled := bearertest.Token()
|
||||
|
||||
val.WriteToV2(&m)
|
||||
require.Zero(t, m.GetBody())
|
||||
|
||||
val2 := filled
|
||||
|
||||
require.NoError(t, val2.Unmarshal(val.Marshal()))
|
||||
|
||||
val2.WriteToV2(&m)
|
||||
require.Zero(t, m.GetBody())
|
||||
|
||||
val2 = filled
|
||||
|
||||
jd, err := val.MarshalJSON()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, val2.UnmarshalJSON(jd))
|
||||
|
||||
val2.WriteToV2(&m)
|
||||
require.Zero(t, m.GetBody())
|
||||
|
||||
// set value
|
||||
usr := *usertest.ID()
|
||||
|
||||
var usrV2 refs.OwnerID
|
||||
usr.WriteToV2(&usrV2)
|
||||
|
||||
val.ForUser(usr)
|
||||
|
||||
val.WriteToV2(&m)
|
||||
require.Equal(t, usrV2, *m.GetBody().GetOwnerID())
|
||||
|
||||
val2 = filled
|
||||
|
||||
require.NoError(t, val2.Unmarshal(val.Marshal()))
|
||||
|
||||
val2.WriteToV2(&m)
|
||||
require.Equal(t, usrV2, *m.GetBody().GetOwnerID())
|
||||
|
||||
val2 = filled
|
||||
|
||||
jd, err = val.MarshalJSON()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, val2.UnmarshalJSON(jd))
|
||||
|
||||
val2.WriteToV2(&m)
|
||||
require.Equal(t, usrV2, *m.GetBody().GetOwnerID())
|
||||
}
|
||||
|
||||
func testLifetimeClaim(t *testing.T, setter func(*bearer.Token, uint64), getter func(*acl.BearerToken) uint64) {
|
||||
var val bearer.Token
|
||||
var m acl.BearerToken
|
||||
filled := bearertest.Token()
|
||||
|
||||
val.WriteToV2(&m)
|
||||
require.Zero(t, m.GetBody())
|
||||
|
||||
val2 := filled
|
||||
|
||||
require.NoError(t, val2.Unmarshal(val.Marshal()))
|
||||
|
||||
val2.WriteToV2(&m)
|
||||
require.Zero(t, m.GetBody())
|
||||
|
||||
val2 = filled
|
||||
|
||||
jd, err := val.MarshalJSON()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, val2.UnmarshalJSON(jd))
|
||||
|
||||
val2.WriteToV2(&m)
|
||||
require.Zero(t, m.GetBody())
|
||||
|
||||
// set value
|
||||
exp := rand.Uint64()
|
||||
|
||||
setter(&val, exp)
|
||||
|
||||
val.WriteToV2(&m)
|
||||
require.Equal(t, exp, getter(&m))
|
||||
|
||||
val2 = filled
|
||||
|
||||
require.NoError(t, val2.Unmarshal(val.Marshal()))
|
||||
|
||||
val2.WriteToV2(&m)
|
||||
require.Equal(t, exp, getter(&m))
|
||||
|
||||
val2 = filled
|
||||
|
||||
jd, err = val.MarshalJSON()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, val2.UnmarshalJSON(jd))
|
||||
|
||||
val2.WriteToV2(&m)
|
||||
require.Equal(t, exp, getter(&m))
|
||||
}
|
||||
|
||||
func TestToken_SetLifetime(t *testing.T) {
|
||||
t.Run("iat", func(t *testing.T) {
|
||||
testLifetimeClaim(t, (*bearer.Token).SetIat, func(token *acl.BearerToken) uint64 {
|
||||
return token.GetBody().GetLifetime().GetIat()
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("nbf", func(t *testing.T) {
|
||||
testLifetimeClaim(t, (*bearer.Token).SetNbf, func(token *acl.BearerToken) uint64 {
|
||||
return token.GetBody().GetLifetime().GetNbf()
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("exp", func(t *testing.T) {
|
||||
testLifetimeClaim(t, (*bearer.Token).SetExp, func(token *acl.BearerToken) uint64 {
|
||||
return token.GetBody().GetLifetime().GetExp()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestToken_InvalidAt(t *testing.T) {
|
||||
var val bearer.Token
|
||||
|
||||
require.True(t, val.InvalidAt(0))
|
||||
require.True(t, val.InvalidAt(1))
|
||||
|
||||
val.SetIat(1)
|
||||
val.SetNbf(2)
|
||||
val.SetExp(4)
|
||||
|
||||
require.True(t, val.InvalidAt(0))
|
||||
require.True(t, val.InvalidAt(1))
|
||||
require.False(t, val.InvalidAt(2))
|
||||
require.False(t, val.InvalidAt(3))
|
||||
require.False(t, val.InvalidAt(4))
|
||||
require.True(t, val.InvalidAt(5))
|
||||
}
|
||||
|
||||
func TestToken_AssertContainer(t *testing.T) {
|
||||
var val bearer.Token
|
||||
cnr := cidtest.ID()
|
||||
|
||||
require.True(t, val.AssertContainer(cnr))
|
||||
|
||||
eaclTable := *eacltest.Table()
|
||||
|
||||
eaclTable.SetCID(cidtest.ID())
|
||||
val.SetEACLTable(eaclTable)
|
||||
require.False(t, val.AssertContainer(cnr))
|
||||
|
||||
eaclTable.SetCID(cnr)
|
||||
val.SetEACLTable(eaclTable)
|
||||
require.True(t, val.AssertContainer(cnr))
|
||||
}
|
||||
|
||||
func TestToken_AssertUser(t *testing.T) {
|
||||
var val bearer.Token
|
||||
usr := *usertest.ID()
|
||||
|
||||
require.True(t, val.AssertUser(usr))
|
||||
|
||||
val.ForUser(*usertest.ID())
|
||||
require.False(t, val.AssertUser(usr))
|
||||
|
||||
val.ForUser(usr)
|
||||
require.True(t, val.AssertUser(usr))
|
||||
}
|
||||
|
||||
func TestToken_Sign(t *testing.T) {
|
||||
var val bearer.Token
|
||||
|
||||
require.False(t, val.VerifySignature())
|
||||
|
||||
k, err := keys.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
|
||||
key := k.PrivateKey
|
||||
val = bearertest.Token()
|
||||
|
||||
require.NoError(t, val.Sign(key))
|
||||
|
||||
require.True(t, val.VerifySignature())
|
||||
|
||||
var m acl.BearerToken
|
||||
val.WriteToV2(&m)
|
||||
|
||||
require.NotZero(t, m.GetSignature().GetKey())
|
||||
require.NotZero(t, m.GetSignature().GetSign())
|
||||
|
||||
val2 := bearertest.Token()
|
||||
|
||||
require.NoError(t, val2.Unmarshal(val.Marshal()))
|
||||
require.True(t, val2.VerifySignature())
|
||||
|
||||
jd, err := val.MarshalJSON()
|
||||
require.NoError(t, err)
|
||||
|
||||
val2 = bearertest.Token()
|
||||
require.NoError(t, val2.UnmarshalJSON(jd))
|
||||
require.True(t, val2.VerifySignature())
|
||||
}
|
||||
|
||||
func TestToken_ReadFromV2(t *testing.T) {
|
||||
var val bearer.Token
|
||||
var m acl.BearerToken
|
||||
|
||||
require.Error(t, val.ReadFromV2(m))
|
||||
|
||||
var body acl.BearerTokenBody
|
||||
m.SetBody(&body)
|
||||
|
||||
require.Error(t, val.ReadFromV2(m))
|
||||
|
||||
eaclTable := eacltest.Table().ToV2()
|
||||
body.SetEACL(eaclTable)
|
||||
|
||||
require.Error(t, val.ReadFromV2(m))
|
||||
|
||||
var lifetime acl.TokenLifetime
|
||||
body.SetLifetime(&lifetime)
|
||||
|
||||
require.Error(t, val.ReadFromV2(m))
|
||||
|
||||
const iat, nbf, exp = 1, 2, 3
|
||||
lifetime.SetIat(iat)
|
||||
lifetime.SetNbf(nbf)
|
||||
lifetime.SetExp(exp)
|
||||
|
||||
body.SetLifetime(&lifetime)
|
||||
|
||||
require.Error(t, val.ReadFromV2(m))
|
||||
|
||||
var sig refs.Signature
|
||||
m.SetSignature(&sig)
|
||||
|
||||
require.NoError(t, val.ReadFromV2(m))
|
||||
|
||||
var m2 acl.BearerToken
|
||||
|
||||
val.WriteToV2(&m2)
|
||||
require.Equal(t, m, m2)
|
||||
|
||||
usr, usr2 := *usertest.ID(), *usertest.ID()
|
||||
|
||||
require.True(t, val.AssertUser(usr))
|
||||
require.True(t, val.AssertUser(usr2))
|
||||
|
||||
var usrV2 refs.OwnerID
|
||||
usr.WriteToV2(&usrV2)
|
||||
|
||||
body.SetOwnerID(&usrV2)
|
||||
|
||||
require.NoError(t, val.ReadFromV2(m))
|
||||
|
||||
val.WriteToV2(&m2)
|
||||
require.Equal(t, m, m2)
|
||||
|
||||
require.True(t, val.AssertUser(usr))
|
||||
require.False(t, val.AssertUser(usr2))
|
||||
|
||||
k, err := keys.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
|
||||
signer := frostfsecdsa.Signer(k.PrivateKey)
|
||||
|
||||
var s frostfscrypto.Signature
|
||||
|
||||
require.NoError(t, s.Calculate(signer, body.StableMarshal(nil)))
|
||||
|
||||
s.WriteToV2(&sig)
|
||||
|
||||
require.NoError(t, val.ReadFromV2(m))
|
||||
require.True(t, val.VerifySignature())
|
||||
require.Equal(t, sig.GetKey(), val.SigningKeyBytes())
|
||||
}
|
||||
|
||||
func TestResolveIssuer(t *testing.T) {
|
||||
k, err := keys.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
|
||||
var val bearer.Token
|
||||
|
||||
require.Zero(t, bearer.ResolveIssuer(val))
|
||||
|
||||
var m acl.BearerToken
|
||||
|
||||
var sig refs.Signature
|
||||
sig.SetKey([]byte("invalid key"))
|
||||
|
||||
m.SetSignature(&sig)
|
||||
|
||||
require.NoError(t, val.Unmarshal(m.StableMarshal(nil)))
|
||||
|
||||
require.Zero(t, bearer.ResolveIssuer(val))
|
||||
|
||||
require.NoError(t, val.Sign(k.PrivateKey))
|
||||
|
||||
var usr user.ID
|
||||
user.IDFromKey(&usr, k.PrivateKey.PublicKey)
|
||||
|
||||
require.Equal(t, usr, bearer.ResolveIssuer(val))
|
||||
}
|
31
bearer/doc.go
Normal file
31
bearer/doc.go
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
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
|
6
bearer/test/doc.go
Normal file
6
bearer/test/doc.go
Normal file
|
@ -0,0 +1,6 @@
|
|||
/*
|
||||
Package bearertest provides functions for testing bearer package.
|
||||
|
||||
Note that this package intended only for tests.
|
||||
*/
|
||||
package bearertest
|
20
bearer/test/generate.go
Normal file
20
bearer/test/generate.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
package bearertest
|
||||
|
||||
import (
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
|
||||
eacltest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl/test"
|
||||
usertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user/test"
|
||||
)
|
||||
|
||||
// Token returns random bearer.Token.
|
||||
//
|
||||
// Resulting token is unsigned.
|
||||
func Token() (t bearer.Token) {
|
||||
t.SetExp(3)
|
||||
t.SetNbf(2)
|
||||
t.SetIat(1)
|
||||
t.ForUser(*usertest.ID())
|
||||
t.SetEACLTable(*eacltest.Table())
|
||||
|
||||
return t
|
||||
}
|
|
@ -1,15 +1,25 @@
|
|||
package checksum
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/nspcc-dev/neofs-api-go/v2/refs"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
||||
"git.frostfs.info/TrueCloudLab/tzhash/tz"
|
||||
)
|
||||
|
||||
// Checksum represents v2-compatible checksum.
|
||||
// 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
|
||||
type Checksum refs.Checksum
|
||||
|
||||
// Type represents the enumeration
|
||||
|
@ -23,29 +33,46 @@ const (
|
|||
// SHA256 is a SHA256 checksum type.
|
||||
SHA256
|
||||
|
||||
// TZ is a Tillich-Zemor checksum type.
|
||||
// TZ is a Tillich-Zémor checksum type.
|
||||
TZ
|
||||
)
|
||||
|
||||
// NewFromV2 wraps v2 Checksum message to Checksum.
|
||||
// ReadFromV2 reads Checksum from the refs.Checksum message. Checks if the
|
||||
// message conforms to FrostFS API V2 protocol.
|
||||
//
|
||||
// Nil refs.Checksum converts to nil.
|
||||
func NewFromV2(cV2 *refs.Checksum) *Checksum {
|
||||
return (*Checksum)(cV2)
|
||||
// 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
|
||||
}
|
||||
|
||||
// New creates and initializes blank Checksum.
|
||||
// WriteToV2 writes Checksum to the refs.Checksum message.
|
||||
// The message must not be nil.
|
||||
//
|
||||
// Defaults:
|
||||
// - sum: nil;
|
||||
// - type: Unknown.
|
||||
func New() *Checksum {
|
||||
return NewFromV2(new(refs.Checksum))
|
||||
// See also ReadFromV2.
|
||||
func (c Checksum) WriteToV2(m *refs.Checksum) {
|
||||
*m = (refs.Checksum)(c)
|
||||
}
|
||||
|
||||
// Type returns checksum type.
|
||||
func (c *Checksum) Type() Type {
|
||||
switch (*refs.Checksum)(c).GetType() {
|
||||
//
|
||||
// Zero Checksum has Unknown checksum type.
|
||||
//
|
||||
// See also SetTillichZemor and SetSHA256.
|
||||
func (c Checksum) Type() Type {
|
||||
v2 := (refs.Checksum)(c)
|
||||
switch v2.GetType() {
|
||||
case refs.SHA256:
|
||||
return SHA256
|
||||
case refs.TillichZemor:
|
||||
|
@ -55,93 +82,70 @@ func (c *Checksum) Type() Type {
|
|||
}
|
||||
}
|
||||
|
||||
// Sum returns checksum bytes.
|
||||
func (c *Checksum) Sum() []byte {
|
||||
return (*refs.Checksum)(c).GetSum()
|
||||
// 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()
|
||||
}
|
||||
|
||||
// SetSHA256 sets checksum to SHA256 hash.
|
||||
//
|
||||
// See also Calculate.
|
||||
func (c *Checksum) SetSHA256(v [sha256.Size]byte) {
|
||||
checksum := (*refs.Checksum)(c)
|
||||
v2 := (*refs.Checksum)(c)
|
||||
|
||||
checksum.SetType(refs.SHA256)
|
||||
checksum.SetSum(v[:])
|
||||
v2.SetType(refs.SHA256)
|
||||
v2.SetSum(v[:])
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Calculate calculates checksum and sets it
|
||||
// to the passed checksum. Checksum must not be nil.
|
||||
//
|
||||
// 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 {
|
||||
// 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:
|
||||
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
|
||||
}
|
||||
|
||||
// String returns string representation of Type.
|
||||
// SetTillichZemor sets checksum to Tillich-Zémor hash.
|
||||
//
|
||||
// String mapping:
|
||||
// * TZ: TZ;
|
||||
// * SHA256: SHA256;
|
||||
// * Unknown, default: CHECKSUM_TYPE_UNSPECIFIED.
|
||||
// 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.
|
||||
func (m Type) String() string {
|
||||
var m2 refs.ChecksumType
|
||||
|
||||
|
@ -156,26 +160,3 @@ 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
|
||||
}
|
||||
|
|
|
@ -5,20 +5,13 @@ import (
|
|||
"crypto/sha256"
|
||||
"testing"
|
||||
|
||||
"github.com/nspcc-dev/neofs-api-go/v2/refs"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
||||
"git.frostfs.info/TrueCloudLab/tzhash/tz"
|
||||
"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) {
|
||||
c := New()
|
||||
var c Checksum
|
||||
|
||||
cSHA256 := [sha256.Size]byte{}
|
||||
_, _ = rand.Read(cSHA256[:])
|
||||
|
@ -26,150 +19,62 @@ func TestChecksum(t *testing.T) {
|
|||
c.SetSHA256(cSHA256)
|
||||
|
||||
require.Equal(t, SHA256, c.Type())
|
||||
require.Equal(t, cSHA256[:], c.Sum())
|
||||
require.Equal(t, cSHA256[:], c.Value())
|
||||
|
||||
cV2 := c.ToV2()
|
||||
var cV2 refs.Checksum
|
||||
c.WriteToV2(&cV2)
|
||||
|
||||
require.Equal(t, refs.SHA256, cV2.GetType())
|
||||
require.Equal(t, cSHA256[:], cV2.GetSum())
|
||||
|
||||
cTZ := [64]byte{}
|
||||
cTZ := [tz.Size]byte{}
|
||||
_, _ = rand.Read(cSHA256[:])
|
||||
|
||||
c.SetTillichZemor(cTZ)
|
||||
|
||||
require.Equal(t, TZ, c.Type())
|
||||
require.Equal(t, cTZ[:], c.Sum())
|
||||
require.Equal(t, cTZ[:], c.Value())
|
||||
|
||||
cV2 = c.ToV2()
|
||||
c.WriteToV2(&cV2)
|
||||
|
||||
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) {
|
||||
chs := New()
|
||||
var chs Checksum
|
||||
|
||||
// check initial values
|
||||
require.Equal(t, Unknown, chs.Type())
|
||||
require.Nil(t, chs.Sum())
|
||||
require.Nil(t, chs.Value())
|
||||
|
||||
// convert to v2 message
|
||||
chsV2 := chs.ToV2()
|
||||
var chsV2 refs.Checksum
|
||||
chs.WriteToV2(&chsV2)
|
||||
|
||||
require.Equal(t, refs.UnknownChecksum, chsV2.GetType())
|
||||
require.Nil(t, chsV2.GetSum())
|
||||
})
|
||||
}
|
||||
|
||||
type enumIface interface {
|
||||
FromString(string) bool
|
||||
String() string
|
||||
}
|
||||
func TestCalculation(t *testing.T) {
|
||||
var c Checksum
|
||||
payload := []byte{0, 1, 2, 3, 4, 5}
|
||||
|
||||
type enumStringItem struct {
|
||||
val enumIface
|
||||
str string
|
||||
}
|
||||
t.Run("SHA256", func(t *testing.T) {
|
||||
orig := sha256.Sum256(payload)
|
||||
|
||||
func testEnumStrings(t *testing.T, e enumIface, items []enumStringItem) {
|
||||
for _, item := range items {
|
||||
require.Equal(t, item.str, item.val.String())
|
||||
Calculate(&c, SHA256, payload)
|
||||
|
||||
s := item.val.String()
|
||||
require.Equal(t, orig[:], c.Value())
|
||||
})
|
||||
|
||||
require.True(t, e.FromString(s), s)
|
||||
t.Run("TZ", func(t *testing.T) {
|
||||
orig := tz.Sum(payload)
|
||||
|
||||
require.EqualValues(t, item.val, e, item.val)
|
||||
}
|
||||
Calculate(&c, TZ, payload)
|
||||
|
||||
// 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"},
|
||||
require.Equal(t, orig[:], c.Value())
|
||||
})
|
||||
}
|
||||
|
|
18
checksum/doc.go
Normal file
18
checksum/doc.go
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
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
|
34
checksum/example_test.go
Normal file
34
checksum/example_test.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
package checksum
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
||||
)
|
||||
|
||||
func ExampleCalculate() {
|
||||
payload := []byte{0, 1, 2, 3, 4, 5, 6}
|
||||
var cs Checksum
|
||||
|
||||
Calculate(&cs, SHA256, payload)
|
||||
Calculate(&cs, TZ, payload)
|
||||
}
|
||||
|
||||
func ExampleChecksum_WriteToV2() {
|
||||
var (
|
||||
csRaw [sha256.Size]byte
|
||||
csV2 refs.Checksum
|
||||
cs Checksum
|
||||
)
|
||||
|
||||
rand.Read(csRaw[:])
|
||||
cs.SetSHA256(csRaw)
|
||||
|
||||
cs.WriteToV2(&csV2)
|
||||
|
||||
fmt.Println(bytes.Equal(cs.Value(), csV2.GetSum()))
|
||||
// Output: true
|
||||
}
|
13
checksum/test/doc.go
Normal file
13
checksum/test/doc.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
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
|
|
@ -4,16 +4,16 @@ import (
|
|||
"crypto/sha256"
|
||||
"math/rand"
|
||||
|
||||
"github.com/nspcc-dev/neofs-sdk-go/checksum"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/checksum"
|
||||
)
|
||||
|
||||
// Checksum returns random checksum.Checksum.
|
||||
func Checksum() *checksum.Checksum {
|
||||
func Checksum() checksum.Checksum {
|
||||
var cs [sha256.Size]byte
|
||||
|
||||
rand.Read(cs[:])
|
||||
|
||||
x := checksum.New()
|
||||
var x checksum.Checksum
|
||||
|
||||
x.SetSHA256(cs)
|
||||
|
||||
|
|
|
@ -3,73 +3,68 @@ package client
|
|||
import (
|
||||
"context"
|
||||
|
||||
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"
|
||||
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"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/accounting"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
|
||||
)
|
||||
|
||||
// PrmBalanceGet groups parameters of BalanceGet operation.
|
||||
type PrmBalanceGet struct {
|
||||
prmCommonMeta
|
||||
|
||||
ownerSet bool
|
||||
ownerID owner.ID
|
||||
accountSet bool
|
||||
account user.ID
|
||||
}
|
||||
|
||||
// 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
|
||||
// SetAccount sets identifier of the FrostFS account for which the balance is requested.
|
||||
// Required parameter.
|
||||
func (x *PrmBalanceGet) SetAccount(id user.ID) {
|
||||
x.account = id
|
||||
x.accountSet = true
|
||||
}
|
||||
|
||||
// ResBalanceGet groups resulting values of BalanceGet operation.
|
||||
type ResBalanceGet struct {
|
||||
statusRes
|
||||
|
||||
amount *accounting.Decimal
|
||||
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 {
|
||||
// Amount returns current amount of funds on the FrostFS account as decimal number.
|
||||
func (x ResBalanceGet) Amount() accounting.Decimal {
|
||||
return x.amount
|
||||
}
|
||||
|
||||
// BalanceGet requests current balance of the NeoFS account.
|
||||
// BalanceGet requests current balance of the FrostFS 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 WithNeoFSErrorParsing option has been provided, unsuccessful
|
||||
// NeoFS status codes are returned as `error`, otherwise, are included
|
||||
// If PrmInit.ResolveFrostFSFailures has been called, unsuccessful
|
||||
// FrostFS status codes are returned as `error`, otherwise, are included
|
||||
// in the returned result structure.
|
||||
//
|
||||
// Immediately panics if parameters are set incorrectly (see PrmBalanceGet docs).
|
||||
// Returns an error 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) {
|
||||
switch {
|
||||
case ctx == nil:
|
||||
panic(panicMsgMissingContext)
|
||||
case !prm.ownerSet:
|
||||
panic("account not set")
|
||||
case !prm.ownerID.Valid():
|
||||
panic("invalid account ID")
|
||||
return nil, errorMissingContext
|
||||
case !prm.accountSet:
|
||||
return nil, errorAccountNotSet
|
||||
}
|
||||
|
||||
// form request body
|
||||
var body v2accounting.BalanceRequestBody
|
||||
var accountV2 refs.OwnerID
|
||||
prm.account.WriteToV2(&accountV2)
|
||||
|
||||
body.SetOwnerID(prm.ownerID.ToV2())
|
||||
var body v2accounting.BalanceRequestBody
|
||||
body.SetOwnerID(&accountV2)
|
||||
|
||||
// form request
|
||||
var req v2accounting.BalanceRequest
|
||||
|
@ -92,7 +87,19 @@ func (c *Client) BalanceGet(ctx context.Context, prm PrmBalanceGet) (*ResBalance
|
|||
}
|
||||
cc.result = func(r responseV2) {
|
||||
resp := r.(*v2accounting.BalanceResponse)
|
||||
res.setAmount(accounting.NewDecimalFromV2(resp.GetBody().GetBalance()))
|
||||
|
||||
const fieldBalance = "balance"
|
||||
|
||||
bal := resp.GetBody().GetBalance()
|
||||
if bal == nil {
|
||||
cc.err = newErrMissingResponseField(fieldBalance)
|
||||
return
|
||||
}
|
||||
|
||||
cc.err = res.amount.ReadFromV2(*bal)
|
||||
if cc.err != nil {
|
||||
cc.err = newErrInvalidResponseField(fieldBalance, cc.err)
|
||||
}
|
||||
}
|
||||
|
||||
// process call
|
||||
|
|
35
client/api.go
Normal file
35
client/api.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
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
|
||||
}
|
114
client/client.go
114
client/client.go
|
@ -1,23 +1,25 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
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"
|
||||
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"
|
||||
)
|
||||
|
||||
// Client represents virtual connection to the NeoFS network to communicate
|
||||
// with NeoFS server using NeoFS API protocol. It is designed to provide
|
||||
// Client represents virtual connection to the FrostFS network to communicate
|
||||
// with FrostFS server using FrostFS API protocol. It is designed to provide
|
||||
// an abstraction interface from the protocol details of data transfer over
|
||||
// a network in NeoFS.
|
||||
// a network in FrostFS.
|
||||
//
|
||||
// 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 NeoFS operations using the Client, connection to the
|
||||
// Before executing the FrostFS 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
|
||||
|
@ -26,7 +28,7 @@ import (
|
|||
// during the communication process step strongly discouraged as it leads to
|
||||
// undefined behavior.
|
||||
//
|
||||
// Each method which produces a NeoFS API call may return a server response.
|
||||
// Each method which produces a FrostFS 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`
|
||||
|
@ -34,8 +36,9 @@ 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.SuccessDefaultV2 on default success.
|
||||
// - *apistatus.ServerInternal on internal server error;
|
||||
// - *apistatus.NodeUnderMaintenance if a server is under maintenance;
|
||||
// - *apistatus.SuccessDefaultV2 on default success.
|
||||
//
|
||||
// Client MUST NOT be copied by value: use pointer to Client instead.
|
||||
//
|
||||
|
@ -44,6 +47,8 @@ type Client struct {
|
|||
prm PrmInit
|
||||
|
||||
c client.Client
|
||||
|
||||
server frostFSAPIServer
|
||||
}
|
||||
|
||||
// Init brings the Client instance to its initial state.
|
||||
|
@ -56,11 +61,15 @@ func (c *Client) Init(prm PrmInit) {
|
|||
c.prm = prm
|
||||
}
|
||||
|
||||
// Dial establishes a connection to the server from the NeoFS network.
|
||||
// Dial establishes a connection to the server from the FrostFS network.
|
||||
// Returns an error describing failure reason. If failed, the Client
|
||||
// SHOULD NOT be used.
|
||||
//
|
||||
// Panics if required parameters are set incorrectly, look carefully
|
||||
// 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
|
||||
// at the method documentation.
|
||||
//
|
||||
// One-time method call during application start-up stage (after Init ) is expected.
|
||||
|
@ -69,29 +78,58 @@ func (c *Client) Init(prm PrmInit) {
|
|||
// See also Init / Close.
|
||||
func (c *Client) Dial(prm PrmDial) error {
|
||||
if prm.endpoint == "" {
|
||||
panic("server address is unset or empty")
|
||||
return errorServerAddrUnset
|
||||
}
|
||||
|
||||
if prm.timeoutDialSet {
|
||||
if prm.timeoutDial <= 0 {
|
||||
panic("non-positive timeout")
|
||||
return errorNonPositiveTimeout
|
||||
}
|
||||
} else {
|
||||
prm.timeoutDial = 5 * time.Second
|
||||
}
|
||||
|
||||
if prm.streamTimeoutSet {
|
||||
if prm.streamTimeout <= 0 {
|
||||
return errorNonPositiveTimeout
|
||||
}
|
||||
} else {
|
||||
prm.streamTimeout = 10 * time.Second
|
||||
}
|
||||
|
||||
c.c = *client.New(append(
|
||||
client.WithNetworkURIAddress(prm.endpoint, prm.tlsConfig),
|
||||
client.WithDialTimeout(prm.timeoutDial),
|
||||
client.WithRWTimeout(prm.streamTimeout),
|
||||
)...)
|
||||
|
||||
c.setFrostFSAPIServer((*coreServer)(&c.c))
|
||||
|
||||
if prm.parentCtx == nil {
|
||||
prm.parentCtx = context.Background()
|
||||
}
|
||||
|
||||
// TODO: (neofs-api-go#382) perform generic dial stage of the client.Client
|
||||
_, _ = rpc.Balance(&c.c, new(v2accounting.BalanceRequest))
|
||||
_, err := rpc.Balance(&c.c, new(v2accounting.BalanceRequest),
|
||||
client.WithContext(prm.parentCtx),
|
||||
)
|
||||
// return context errors since they signal about dial problem
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes underlying connection to the NeoFS server. Implements io.Closer.
|
||||
// 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.
|
||||
// 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.
|
||||
|
@ -108,7 +146,7 @@ func (c *Client) Close() error {
|
|||
//
|
||||
// See also Init.
|
||||
type PrmInit struct {
|
||||
resolveNeoFSErrors bool
|
||||
resolveFrostFSErrors bool
|
||||
|
||||
key ecdsa.PrivateKey
|
||||
|
||||
|
@ -125,16 +163,16 @@ func (x *PrmInit) SetDefaultPrivateKey(key ecdsa.PrivateKey) {
|
|||
x.key = key
|
||||
}
|
||||
|
||||
// ResolveNeoFSFailures makes the Client to resolve failure statuses of the
|
||||
// NeoFS protocol into Go built-in errors. These errors are returned from
|
||||
// ResolveFrostFSFailures makes the Client to resolve failure statuses of the
|
||||
// FrostFS 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
|
||||
func (x *PrmInit) ResolveFrostFSFailures() {
|
||||
x.resolveFrostFSErrors = true
|
||||
}
|
||||
|
||||
// SetResponseInfoCallback makes the Client to pass ResponseMetaInfo from each
|
||||
// NeoFS server response to f. Nil (default) means ignore response meta info.
|
||||
// FrostFS server response to f. Nil (default) means ignore response meta info.
|
||||
func (x *PrmInit) SetResponseInfoCallback(f func(ResponseMetaInfo) error) {
|
||||
x.cbRespInfo = f
|
||||
}
|
||||
|
@ -149,17 +187,24 @@ type PrmDial struct {
|
|||
|
||||
timeoutDialSet bool
|
||||
timeoutDial time.Duration
|
||||
|
||||
streamTimeoutSet bool
|
||||
streamTimeout time.Duration
|
||||
|
||||
parentCtx context.Context
|
||||
}
|
||||
|
||||
// SetServerURI sets server URI in the NeoFS network.
|
||||
// SetServerURI sets server URI in the FrostFS network.
|
||||
// Required parameter.
|
||||
//
|
||||
// Format of the URI:
|
||||
// [scheme://]host:port
|
||||
//
|
||||
// [scheme://]host:port
|
||||
//
|
||||
// Supported schemes:
|
||||
// grpc
|
||||
// grpcs
|
||||
//
|
||||
// grpc
|
||||
// grpcs
|
||||
//
|
||||
// See also SetTLSConfig.
|
||||
func (x *PrmDial) SetServerURI(endpoint string) {
|
||||
|
@ -167,7 +212,7 @@ func (x *PrmDial) SetServerURI(endpoint string) {
|
|||
}
|
||||
|
||||
// SetTLSConfig sets tls.Config to open TLS client connection
|
||||
// to the NeoFS server. Nil (default) means insecure connection.
|
||||
// to the FrostFS server. Nil (default) means insecure connection.
|
||||
//
|
||||
// See also SetServerURI.
|
||||
func (x *PrmDial) SetTLSConfig(tlsConfig *tls.Config) {
|
||||
|
@ -180,3 +225,18 @@ func (x *PrmDial) SetTimeout(timeout time.Duration) {
|
|||
x.timeoutDialSet = true
|
||||
x.timeoutDial = timeout
|
||||
}
|
||||
|
||||
// SetStreamTimeout sets the timeout for individual operations in streaming RPC.
|
||||
// MUST BE positive. If not called, 10s timeout will be used by default.
|
||||
func (x *PrmDial) SetStreamTimeout(timeout time.Duration) {
|
||||
x.streamTimeoutSet = true
|
||||
x.streamTimeout = timeout
|
||||
}
|
||||
|
||||
// SetContext allows to specify optional base context within which connection
|
||||
// should be established.
|
||||
//
|
||||
// Context SHOULD NOT be nil.
|
||||
func (x *PrmDial) SetContext(ctx context.Context) {
|
||||
x.parentCtx = ctx
|
||||
}
|
||||
|
|
68
client/client_test.go
Normal file
68
client/client_test.go
Normal file
|
@ -0,0 +1,68 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"testing"
|
||||
|
||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
/*
|
||||
File contains common functionality used for client package testing.
|
||||
*/
|
||||
|
||||
var key, _ = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
|
||||
var statusErr apistatus.ServerInternal
|
||||
|
||||
func init() {
|
||||
statusErr.SetMessage("test status error")
|
||||
}
|
||||
|
||||
func assertStatusErr(tb testing.TB, res interface{ Status() apistatus.Status }) {
|
||||
require.IsType(tb, &statusErr, res.Status())
|
||||
require.Equal(tb, statusErr.Message(), res.Status().(*apistatus.ServerInternal).Message())
|
||||
}
|
||||
|
||||
func newClient(server frostFSAPIServer) *Client {
|
||||
var prm PrmInit
|
||||
prm.SetDefaultPrivateKey(*key)
|
||||
|
||||
var c Client
|
||||
c.Init(prm)
|
||||
c.setFrostFSAPIServer(server)
|
||||
|
||||
return &c
|
||||
}
|
||||
|
||||
func TestClient_DialContext(t *testing.T) {
|
||||
var c Client
|
||||
|
||||
// try to connect to any host
|
||||
var prm PrmDial
|
||||
prm.SetServerURI("localhost:8080")
|
||||
|
||||
assert := func(ctx context.Context, errExpected error) {
|
||||
// use the particular context
|
||||
prm.SetContext(ctx)
|
||||
|
||||
// expect particular context error according to Dial docs
|
||||
require.ErrorIs(t, c.Dial(prm), errExpected)
|
||||
}
|
||||
|
||||
// create pre-abandoned context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
assert(ctx, context.Canceled)
|
||||
|
||||
// create "pre-deadlined" context
|
||||
ctx, cancel = context.WithTimeout(context.Background(), 0)
|
||||
defer cancel()
|
||||
|
||||
assert(ctx, context.DeadlineExceeded)
|
||||
}
|
165
client/common.go
165
client/common.go
|
@ -2,14 +2,15 @@ package client
|
|||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"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"
|
||||
"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"
|
||||
)
|
||||
|
||||
// common interface of resulting structures with API status.
|
||||
|
@ -34,26 +35,9 @@ func (x statusRes) Status() apistatus.Status {
|
|||
return x.st
|
||||
}
|
||||
|
||||
type prmSession struct {
|
||||
tokenSessionSet bool
|
||||
tokenSession session.Token
|
||||
}
|
||||
|
||||
// SetSessionToken sets token of the session within which request should be sent.
|
||||
func (x *prmSession) SetSessionToken(tok session.Token) {
|
||||
x.tokenSession = tok
|
||||
x.tokenSessionSet = true
|
||||
}
|
||||
|
||||
func (x prmSession) writeToMetaHeader(meta *v2session.RequestMetaHeader) {
|
||||
if x.tokenSessionSet {
|
||||
meta.SetSessionToken(x.tokenSession.ToV2())
|
||||
}
|
||||
}
|
||||
|
||||
// groups meta parameters shared between all Client operations.
|
||||
type prmCommonMeta struct {
|
||||
// NeoFS request X-Headers
|
||||
// FrostFS request X-Headers
|
||||
xHeaders []string
|
||||
}
|
||||
|
||||
|
@ -69,23 +53,39 @@ func (x *prmCommonMeta) WithXHeaders(hs ...string) {
|
|||
x.xHeaders = hs
|
||||
}
|
||||
|
||||
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)
|
||||
func writeXHeadersToMeta(xHeaders []string, h *v2session.RequestMetaHeader) {
|
||||
if len(xHeaders) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if len(xHeaders)%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)
|
||||
}
|
||||
|
||||
// panic messages.
|
||||
const (
|
||||
panicMsgMissingContext = "missing context"
|
||||
panicMsgMissingContainer = "missing container"
|
||||
// error messages.
|
||||
var (
|
||||
errorMissingContext = errors.New("missing context")
|
||||
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")
|
||||
errorNonPositiveTimeout = errors.New("non-positive timeout")
|
||||
errorEACLTableNotSet = errors.New("eACL table not set")
|
||||
errorMissingAnnouncements = errors.New("missing announcements")
|
||||
errorZeroRangeLength = errors.New("zero range length")
|
||||
errorMissingRanges = errors.New("missing ranges")
|
||||
errorZeroEpoch = errors.New("zero epoch")
|
||||
errorMissingTrusts = errors.New("missing trusts")
|
||||
errorTrustNotSet = errors.New("current trust value not set")
|
||||
)
|
||||
|
||||
// groups all the details required to send a single request and process a response to it.
|
||||
|
@ -111,7 +111,7 @@ type contextCall struct {
|
|||
// if set, protocol errors will be expanded into a final error
|
||||
resolveAPIFailures bool
|
||||
|
||||
// NeoFS network magic
|
||||
// FrostFS network magic
|
||||
netMagic uint64
|
||||
|
||||
// Meta parameters
|
||||
|
@ -124,11 +124,7 @@ type contextCall struct {
|
|||
statusRes resCommon
|
||||
|
||||
// request to be signed with a key and sent
|
||||
req interface {
|
||||
GetMetaHeader() *v2session.RequestMetaHeader
|
||||
SetMetaHeader(*v2session.RequestMetaHeader)
|
||||
SetVerificationHeader(*v2session.RequestVerificationHeader)
|
||||
}
|
||||
req request
|
||||
|
||||
// function to send a request (unary) and receive a response
|
||||
call func() (responseV2, error)
|
||||
|
@ -146,6 +142,12 @@ type contextCall struct {
|
|||
result func(v2 responseV2)
|
||||
}
|
||||
|
||||
type request interface {
|
||||
GetMetaHeader() *v2session.RequestMetaHeader
|
||||
SetMetaHeader(*v2session.RequestMetaHeader)
|
||||
SetVerificationHeader(*v2session.RequestVerificationHeader)
|
||||
}
|
||||
|
||||
// sets needed fields of the request meta header.
|
||||
func (x contextCall) prepareRequest() {
|
||||
meta := x.req.GetMetaHeader()
|
||||
|
@ -159,12 +161,33 @@ func (x contextCall) prepareRequest() {
|
|||
}
|
||||
|
||||
if meta.GetVersion() == nil {
|
||||
meta.SetVersion(version.Current().ToV2())
|
||||
var verV2 refs.Version
|
||||
version.Current().WriteToV2(&verV2)
|
||||
meta.SetVersion(&verV2)
|
||||
}
|
||||
|
||||
meta.SetNetworkMagic(x.netMagic)
|
||||
|
||||
x.meta.writeToMetaHeader(meta)
|
||||
writeXHeadersToMeta(x.meta.xHeaders, meta)
|
||||
}
|
||||
|
||||
func (c *Client) prepareRequest(req request, meta *v2session.RequestMetaHeader) {
|
||||
ttl := meta.GetTTL()
|
||||
if ttl == 0 {
|
||||
ttl = 2
|
||||
}
|
||||
|
||||
verV2 := meta.GetVersion()
|
||||
if verV2 == nil {
|
||||
verV2 = new(refs.Version)
|
||||
version.Current().WriteToV2(verV2)
|
||||
}
|
||||
|
||||
meta.SetTTL(ttl)
|
||||
meta.SetVersion(verV2)
|
||||
meta.SetNetworkMagic(c.prm.netMagic)
|
||||
|
||||
req.SetMetaHeader(meta)
|
||||
}
|
||||
|
||||
// prepares, signs and writes the request. Result means success.
|
||||
|
@ -194,14 +217,15 @@ func (x *contextCall) writeRequest() bool {
|
|||
// (in both cases returns false).
|
||||
//
|
||||
// Actions:
|
||||
// * verify signature (internal);
|
||||
// * call response callback (internal);
|
||||
// * unwrap status error (optional).
|
||||
// - 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(),
|
||||
key: x.resp.GetVerificationHeader().GetBodySignature().GetKey(),
|
||||
epoch: x.resp.GetMetaHeader().GetEpoch(),
|
||||
})
|
||||
if x.err != nil {
|
||||
x.err = fmt.Errorf("response callback error: %w", x.err)
|
||||
|
@ -225,18 +249,32 @@ func (x *contextCall) processResponse() bool {
|
|||
// unwrap unsuccessful status and return it
|
||||
// as error if client has been configured so
|
||||
successfulStatus := apistatus.IsSuccessful(st)
|
||||
if !successfulStatus && x.resolveAPIFailures {
|
||||
|
||||
if x.resolveAPIFailures {
|
||||
x.err = apistatus.ErrFromStatus(st)
|
||||
return false
|
||||
} else {
|
||||
x.statusRes.setStatus(st)
|
||||
}
|
||||
|
||||
x.statusRes.setStatus(st)
|
||||
return successfulStatus
|
||||
}
|
||||
|
||||
return successfulStatus || !x.resolveAPIFailures
|
||||
// processResponse verifies response signature and converts status to an error if needed.
|
||||
func (c *Client) processResponse(resp responseV2) (apistatus.Status, error) {
|
||||
err := signature.VerifyServiceMessage(resp)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid response signature: %w", err)
|
||||
}
|
||||
|
||||
st := apistatus.FromStatusV2(resp.GetMetaHeader().GetStatus())
|
||||
if c.prm.resolveFrostFSErrors {
|
||||
return st, apistatus.ErrFromStatus(st)
|
||||
}
|
||||
return st, nil
|
||||
}
|
||||
|
||||
// reads response (if rResp is set) and processes it. Result means success.
|
||||
// If failed, contextCall.err contains the reason.
|
||||
// If failed, contextCall.err (or statusRes if resolveAPIFailures is set) contains the reason.
|
||||
func (x *contextCall) readResponse() bool {
|
||||
if x.rResp != nil {
|
||||
x.err = x.rResp()
|
||||
|
@ -265,7 +303,7 @@ func (x *contextCall) close() bool {
|
|||
x.result(x.resp)
|
||||
}
|
||||
|
||||
return true
|
||||
return x.err == nil
|
||||
}
|
||||
|
||||
// goes through all stages of sending a request and processing a response. Returns true if successful.
|
||||
|
@ -287,7 +325,7 @@ func (x *contextCall) processCall() bool {
|
|||
// read response
|
||||
ok = x.readResponse()
|
||||
if !ok {
|
||||
return false
|
||||
return x.err == nil
|
||||
}
|
||||
|
||||
// close and write response to resulting structure
|
||||
|
@ -302,19 +340,14 @@ func (x *contextCall) processCall() bool {
|
|||
// 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.resolveAPIFailures = c.prm.resolveFrostFSErrors
|
||||
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
|
||||
// ExecRaw executes f with underlying git.frostfs.info/TrueCloudLab/frostfs-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 NeoFS protocol, as well
|
||||
// most often used to transmit data over a fixed version of the FrostFS protocol, as well
|
||||
// as to support custom services.
|
||||
//
|
||||
// The f must not manipulate the client connection passed into it.
|
||||
|
@ -323,7 +356,7 @@ func (c *Client) initCallContextWithoutKey(ctx *contextCall) {
|
|||
// before closing the connection.
|
||||
//
|
||||
// See also Dial and Close.
|
||||
// See also github.com/nspcc-dev/neofs-api-go/v2/rpc/client package docs.
|
||||
// See also git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client package docs.
|
||||
func (c *Client) ExecRaw(f func(client *client.Client) error) error {
|
||||
return f(&c.c)
|
||||
}
|
||||
|
|
|
@ -2,19 +2,21 @@ package client
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
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"
|
||||
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-sdk-go/container"
|
||||
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/session"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
|
||||
)
|
||||
|
||||
// PrmContainerPut groups parameters of ContainerPut operation.
|
||||
|
@ -23,41 +25,51 @@ type PrmContainerPut struct {
|
|||
|
||||
cnrSet bool
|
||||
cnr container.Container
|
||||
|
||||
sessionSet bool
|
||||
session session.Container
|
||||
}
|
||||
|
||||
// SetContainer sets structured information about new NeoFS container.
|
||||
// SetContainer sets structured information about new FrostFS container.
|
||||
// Required parameter.
|
||||
func (x *PrmContainerPut) SetContainer(cnr container.Container) {
|
||||
x.cnr = cnr
|
||||
x.cnrSet = true
|
||||
}
|
||||
|
||||
// WithinSession specifies session within which container should be saved.
|
||||
//
|
||||
// Creator of the session acquires the authorship of the request. This affects
|
||||
// the execution of an operation (e.g. access control).
|
||||
//
|
||||
// Session is optional, if set the following requirements apply:
|
||||
// - session operation MUST be session.VerbContainerPut (ForVerb)
|
||||
// - token MUST be signed using private key of the owner of the container to be saved
|
||||
func (x *PrmContainerPut) WithinSession(s session.Container) {
|
||||
x.session = s
|
||||
x.sessionSet = true
|
||||
}
|
||||
|
||||
// ResContainerPut groups resulting values of ContainerPut operation.
|
||||
type ResContainerPut struct {
|
||||
statusRes
|
||||
|
||||
id *cid.ID
|
||||
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).
|
||||
//
|
||||
// Client doesn't retain value so modification is safe.
|
||||
func (x ResContainerPut) ID() *cid.ID {
|
||||
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.
|
||||
// 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 WithNeoFSErrorParsing option has been provided, unsuccessful
|
||||
// NeoFS status codes are returned as `error`, otherwise, are included
|
||||
// If PrmInit.ResolveFrostFSFailures has been called, unsuccessful
|
||||
// FrostFS 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.
|
||||
|
@ -65,40 +77,51 @@ func (x *ResContainerPut) setID(id *cid.ID) {
|
|||
//
|
||||
// Success can be verified by reading by identifier (see ResContainerPut.ID).
|
||||
//
|
||||
// Immediately panics if parameters are set incorrectly (see PrmContainerPut docs).
|
||||
// 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).
|
||||
// - global (see Client docs).
|
||||
func (c *Client) ContainerPut(ctx context.Context, prm PrmContainerPut) (*ResContainerPut, error) {
|
||||
// check parameters
|
||||
switch {
|
||||
case ctx == nil:
|
||||
panic(panicMsgMissingContext)
|
||||
return nil, errorMissingContext
|
||||
case !prm.cnrSet:
|
||||
panic(panicMsgMissingContainer)
|
||||
return nil, errorMissingContainer
|
||||
}
|
||||
|
||||
// TODO: check private key is set before forming the request
|
||||
// sign container
|
||||
var cnr v2container.Container
|
||||
prm.cnr.WriteToV2(&cnr)
|
||||
|
||||
var sig frostfscrypto.Signature
|
||||
|
||||
err := container.CalculateSignature(&sig, prm.cnr, c.prm.key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("calculate container signature: %w", err)
|
||||
}
|
||||
|
||||
var sigv2 refs.Signature
|
||||
|
||||
sig.WriteToV2(&sigv2)
|
||||
|
||||
// form request body
|
||||
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())
|
||||
reqBody.SetContainer(&cnr)
|
||||
reqBody.SetSignature(&sigv2)
|
||||
|
||||
// form meta header
|
||||
var meta v2session.RequestMetaHeader
|
||||
meta.SetSessionToken(prm.cnr.SessionToken().ToV2())
|
||||
prm.prmCommonMeta.writeToMetaHeader(&meta)
|
||||
writeXHeadersToMeta(prm.prmCommonMeta.xHeaders, &meta)
|
||||
|
||||
if prm.sessionSet {
|
||||
var tokv2 v2session.Token
|
||||
prm.session.WriteToV2(&tokv2)
|
||||
|
||||
meta.SetSessionToken(&tokv2)
|
||||
}
|
||||
|
||||
// form request
|
||||
var req v2container.PutRequest
|
||||
|
@ -121,7 +144,19 @@ func (c *Client) ContainerPut(ctx context.Context, prm PrmContainerPut) (*ResCon
|
|||
}
|
||||
cc.result = func(r responseV2) {
|
||||
resp := r.(*v2container.PutResponse)
|
||||
res.setID(cid.NewFromV2(resp.GetBody().GetContainerID()))
|
||||
|
||||
const fieldCnrID = "container ID"
|
||||
|
||||
cidV2 := resp.GetBody().GetContainerID()
|
||||
if cidV2 == nil {
|
||||
cc.err = newErrMissingResponseField(fieldCnrID)
|
||||
return
|
||||
}
|
||||
|
||||
cc.err = res.id.ReadFromV2(*cidV2)
|
||||
if cc.err != nil {
|
||||
cc.err = newErrInvalidResponseField(fieldCnrID, cc.err)
|
||||
}
|
||||
}
|
||||
|
||||
// process call
|
||||
|
@ -151,29 +186,25 @@ func (x *PrmContainerGet) SetContainer(id cid.ID) {
|
|||
type ResContainerGet struct {
|
||||
statusRes
|
||||
|
||||
cnr *container.Container
|
||||
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 {
|
||||
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.
|
||||
// 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 WithNeoFSErrorParsing option has been provided, unsuccessful
|
||||
// NeoFS status codes are returned as `error`, otherwise, are included
|
||||
// If PrmInit.ResolveFrostFSFailures has been called, unsuccessful
|
||||
// FrostFS status codes are returned as `error`, otherwise, are included
|
||||
// in the returned result structure.
|
||||
//
|
||||
// Immediately panics if parameters are set incorrectly (see PrmContainerGet docs).
|
||||
// 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:
|
||||
|
@ -182,14 +213,17 @@ func (x *ResContainerGet) setContainer(cnr *container.Container) {
|
|||
func (c *Client) ContainerGet(ctx context.Context, prm PrmContainerGet) (*ResContainerGet, error) {
|
||||
switch {
|
||||
case ctx == nil:
|
||||
panic(panicMsgMissingContext)
|
||||
return nil, errorMissingContext
|
||||
case !prm.idSet:
|
||||
panic(panicMsgMissingContainer)
|
||||
return nil, errorMissingContainer
|
||||
}
|
||||
|
||||
var cidV2 refs.ContainerID
|
||||
prm.id.WriteToV2(&cidV2)
|
||||
|
||||
// form request body
|
||||
reqBody := new(v2container.GetRequestBody)
|
||||
reqBody.SetContainerID(prm.id.ToV2())
|
||||
reqBody.SetContainerID(&cidV2)
|
||||
|
||||
// form request
|
||||
var req v2container.GetRequest
|
||||
|
@ -213,19 +247,16 @@ func (c *Client) ContainerGet(ctx context.Context, prm PrmContainerGet) (*ResCon
|
|||
cc.result = func(r responseV2) {
|
||||
resp := r.(*v2container.GetResponse)
|
||||
|
||||
body := resp.GetBody()
|
||||
cnrV2 := resp.GetBody().GetContainer()
|
||||
if cnrV2 == nil {
|
||||
cc.err = errors.New("missing container in response")
|
||||
return
|
||||
}
|
||||
|
||||
cnr := container.NewContainerFromV2(body.GetContainer())
|
||||
|
||||
cnr.SetSessionToken(
|
||||
session.NewTokenFromV2(body.GetSessionToken()),
|
||||
)
|
||||
|
||||
cnr.SetSignature(
|
||||
signature.NewFromV2(body.GetSignature()),
|
||||
)
|
||||
|
||||
res.setContainer(cnr)
|
||||
cc.err = res.cnr.ReadFromV2(*cnrV2)
|
||||
if cc.err != nil {
|
||||
cc.err = fmt.Errorf("invalid container in response: %w", cc.err)
|
||||
}
|
||||
}
|
||||
|
||||
// process call
|
||||
|
@ -241,12 +272,12 @@ type PrmContainerList struct {
|
|||
prmCommonMeta
|
||||
|
||||
ownerSet bool
|
||||
ownerID owner.ID
|
||||
ownerID user.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) {
|
||||
// SetAccount sets identifier of the FrostFS account to list the containers.
|
||||
// Required parameter.
|
||||
func (x *PrmContainerList) SetAccount(id user.ID) {
|
||||
x.ownerID = id
|
||||
x.ownerSet = true
|
||||
}
|
||||
|
@ -265,37 +296,34 @@ 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
|
||||
// If PrmInit.ResolveFrostFSFailures has been called, unsuccessful
|
||||
// FrostFS status codes are returned as `error`, otherwise, are included
|
||||
// in the returned result structure.
|
||||
//
|
||||
// Immediately panics if parameters are set incorrectly (see PrmContainerList docs).
|
||||
// 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).
|
||||
// - global (see Client docs).
|
||||
func (c *Client) ContainerList(ctx context.Context, prm PrmContainerList) (*ResContainerList, error) {
|
||||
// check parameters
|
||||
switch {
|
||||
case ctx == nil:
|
||||
panic(panicMsgMissingContext)
|
||||
return nil, errorMissingContext
|
||||
case !prm.ownerSet:
|
||||
panic("account not set")
|
||||
case !prm.ownerID.Valid():
|
||||
panic("invalid account")
|
||||
return nil, errorAccountNotSet
|
||||
}
|
||||
|
||||
// form request body
|
||||
var ownerV2 refs.OwnerID
|
||||
prm.ownerID.WriteToV2(&ownerV2)
|
||||
|
||||
reqBody := new(v2container.ListRequestBody)
|
||||
reqBody.SetOwnerID(prm.ownerID.ToV2())
|
||||
reqBody.SetOwnerID(&ownerV2)
|
||||
|
||||
// form request
|
||||
var req v2container.ListRequest
|
||||
|
@ -319,13 +347,15 @@ func (c *Client) ContainerList(ctx context.Context, prm PrmContainerList) (*ResC
|
|||
cc.result = func(r responseV2) {
|
||||
resp := r.(*v2container.ListResponse)
|
||||
|
||||
ids := make([]cid.ID, len(resp.GetBody().GetContainerIDs()))
|
||||
res.ids = make([]cid.ID, len(resp.GetBody().GetContainerIDs()))
|
||||
|
||||
for i, cidV2 := range resp.GetBody().GetContainerIDs() {
|
||||
ids[i] = *cid.NewFromV2(&cidV2)
|
||||
cc.err = res.ids[i].ReadFromV2(cidV2)
|
||||
if cc.err != nil {
|
||||
cc.err = fmt.Errorf("invalid ID in the response: %w", cc.err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
res.setContainers(ids)
|
||||
}
|
||||
|
||||
// process call
|
||||
|
@ -339,43 +369,43 @@ func (c *Client) ContainerList(ctx context.Context, prm PrmContainerList) (*ResC
|
|||
// PrmContainerDelete groups parameters of ContainerDelete operation.
|
||||
type PrmContainerDelete struct {
|
||||
prmCommonMeta
|
||||
prmSession
|
||||
|
||||
idSet bool
|
||||
id cid.ID
|
||||
|
||||
tokSet bool
|
||||
tok session.Container
|
||||
}
|
||||
|
||||
// SetContainer sets identifier of the NeoFS container to be removed.
|
||||
// SetContainer sets identifier of the FrostFS container to be removed.
|
||||
// Required parameter.
|
||||
func (x *PrmContainerDelete) SetContainer(id cid.ID) {
|
||||
x.id = id
|
||||
x.idSet = true
|
||||
}
|
||||
|
||||
// WithinSession specifies session within which container should be removed.
|
||||
//
|
||||
// Creator of the session acquires the authorship of the request.
|
||||
// This may affect the execution of an operation (e.g. access control).
|
||||
//
|
||||
// Must be signed.
|
||||
func (x *PrmContainerDelete) WithinSession(tok session.Container) {
|
||||
x.tok = tok
|
||||
x.tokSet = true
|
||||
}
|
||||
|
||||
// ResContainerDelete groups resulting values of ContainerDelete operation.
|
||||
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.
|
||||
// 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 WithNeoFSErrorParsing option has been provided, unsuccessful
|
||||
// NeoFS status codes are returned as `error`, otherwise, are included
|
||||
// If PrmInit.ResolveFrostFSFailures has been called, unsuccessful
|
||||
// FrostFS 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.
|
||||
|
@ -383,42 +413,57 @@ func (c delContainerSignWrapper) SignedDataSize() int {
|
|||
//
|
||||
// Success can be verified by reading by identifier (see GetContainer).
|
||||
//
|
||||
// Immediately panics if parameters are set incorrectly (see PrmContainerDelete docs).
|
||||
// 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).
|
||||
// - global (see Client docs).
|
||||
func (c *Client) ContainerDelete(ctx context.Context, prm PrmContainerDelete) (*ResContainerDelete, error) {
|
||||
// check parameters
|
||||
switch {
|
||||
case ctx == nil:
|
||||
panic(panicMsgMissingContext)
|
||||
return nil, errorMissingContext
|
||||
case !prm.idSet:
|
||||
panic(panicMsgMissingContainer)
|
||||
return nil, errorMissingContainer
|
||||
}
|
||||
|
||||
// sign container ID
|
||||
var cidV2 refs.ContainerID
|
||||
prm.id.WriteToV2(&cidV2)
|
||||
|
||||
// container contract expects signature of container ID value
|
||||
// don't get confused with stable marshaled protobuf container.ID structure
|
||||
data := cidV2.GetValue()
|
||||
|
||||
var sig frostfscrypto.Signature
|
||||
|
||||
err := sig.Calculate(frostfsecdsa.SignerRFC6979(c.prm.key), data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("calculate signature: %w", err)
|
||||
}
|
||||
|
||||
var sigv2 refs.Signature
|
||||
|
||||
sig.WriteToV2(&sigv2)
|
||||
|
||||
// form request body
|
||||
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())
|
||||
reqBody.SetContainerID(&cidV2)
|
||||
reqBody.SetSignature(&sigv2)
|
||||
|
||||
// form meta header
|
||||
var meta v2session.RequestMetaHeader
|
||||
writeXHeadersToMeta(prm.prmCommonMeta.xHeaders, &meta)
|
||||
|
||||
prm.prmSession.writeToMetaHeader(&meta)
|
||||
prm.prmCommonMeta.writeToMetaHeader(&meta)
|
||||
if prm.tokSet {
|
||||
var tokv2 v2session.Token
|
||||
prm.tok.WriteToV2(&tokv2)
|
||||
|
||||
meta.SetSessionToken(&tokv2)
|
||||
}
|
||||
|
||||
// form request
|
||||
var req v2container.DeleteRequest
|
||||
|
@ -456,7 +501,7 @@ type PrmContainerEACL struct {
|
|||
id cid.ID
|
||||
}
|
||||
|
||||
// SetContainer sets identifier of the NeoFS container to read the eACL table.
|
||||
// SetContainer sets identifier of the FrostFS container to read the eACL table.
|
||||
// Required parameter.
|
||||
func (x *PrmContainerEACL) SetContainer(id cid.ID) {
|
||||
x.id = id
|
||||
|
@ -467,46 +512,44 @@ func (x *PrmContainerEACL) SetContainer(id cid.ID) {
|
|||
type ResContainerEACL struct {
|
||||
statusRes
|
||||
|
||||
table *eacl.Table
|
||||
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 {
|
||||
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.
|
||||
// ContainerEACL reads eACL table of 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 WithNeoFSErrorParsing option has been provided, unsuccessful
|
||||
// NeoFS status codes are returned as `error`, otherwise, are included
|
||||
// If PrmInit.ResolveFrostFSFailures has been called, unsuccessful
|
||||
// FrostFS status codes are returned as `error`, otherwise, are included
|
||||
// in the returned result structure.
|
||||
//
|
||||
// Immediately panics if parameters are set incorrectly (see PrmContainerEACL docs).
|
||||
// Returns an error 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.
|
||||
// - *apistatus.ContainerNotFound;
|
||||
// - *apistatus.EACLNotFound.
|
||||
func (c *Client) ContainerEACL(ctx context.Context, prm PrmContainerEACL) (*ResContainerEACL, error) {
|
||||
// check parameters
|
||||
switch {
|
||||
case ctx == nil:
|
||||
panic(panicMsgMissingContext)
|
||||
return nil, errorMissingContext
|
||||
case !prm.idSet:
|
||||
panic(panicMsgMissingContainer)
|
||||
return nil, errorMissingContainer
|
||||
}
|
||||
|
||||
var cidV2 refs.ContainerID
|
||||
prm.id.WriteToV2(&cidV2)
|
||||
|
||||
// form request body
|
||||
reqBody := new(v2container.GetExtendedACLRequestBody)
|
||||
reqBody.SetContainerID(prm.id.ToV2())
|
||||
reqBody.SetContainerID(&cidV2)
|
||||
|
||||
// form request
|
||||
var req v2container.GetExtendedACLRequest
|
||||
|
@ -530,19 +573,13 @@ func (c *Client) ContainerEACL(ctx context.Context, prm PrmContainerEACL) (*ResC
|
|||
cc.result = func(r responseV2) {
|
||||
resp := r.(*v2container.GetExtendedACLResponse)
|
||||
|
||||
body := resp.GetBody()
|
||||
eACL := resp.GetBody().GetEACL()
|
||||
if eACL == nil {
|
||||
cc.err = newErrMissingResponseField("eACL")
|
||||
return
|
||||
}
|
||||
|
||||
table := eacl.NewTableFromV2(body.GetEACL())
|
||||
|
||||
table.SetSessionToken(
|
||||
session.NewTokenFromV2(body.GetSessionToken()),
|
||||
)
|
||||
|
||||
table.SetSignature(
|
||||
signature.NewFromV2(body.GetSignature()),
|
||||
)
|
||||
|
||||
res.setTable(table)
|
||||
res.table = *eacl.NewTableFromV2(eACL)
|
||||
}
|
||||
|
||||
// process call
|
||||
|
@ -559,6 +596,9 @@ type PrmContainerSetEACL struct {
|
|||
|
||||
tableSet bool
|
||||
table eacl.Table
|
||||
|
||||
sessionSet bool
|
||||
session session.Container
|
||||
}
|
||||
|
||||
// SetTable sets eACL table structure to be set for the container.
|
||||
|
@ -568,17 +608,33 @@ func (x *PrmContainerSetEACL) SetTable(table eacl.Table) {
|
|||
x.tableSet = true
|
||||
}
|
||||
|
||||
// WithinSession specifies session within which extended ACL of the container
|
||||
// should be saved.
|
||||
//
|
||||
// Creator of the session acquires the authorship of the request. This affects
|
||||
// the execution of an operation (e.g. access control).
|
||||
//
|
||||
// Session is optional, if set the following requirements apply:
|
||||
// - if particular container is specified (ApplyOnlyTo), it MUST equal the container
|
||||
// for which extended ACL is going to be set
|
||||
// - session operation MUST be session.VerbContainerSetEACL (ForVerb)
|
||||
// - token MUST be signed using private key of the owner of the container to be saved
|
||||
func (x *PrmContainerSetEACL) WithinSession(s session.Container) {
|
||||
x.session = s
|
||||
x.sessionSet = true
|
||||
}
|
||||
|
||||
// ResContainerSetEACL groups resulting values of ContainerSetEACL operation.
|
||||
type ResContainerSetEACL struct {
|
||||
statusRes
|
||||
}
|
||||
|
||||
// ContainerSetEACL sends request to update eACL table of the NeoFS container.
|
||||
// ContainerSetEACL sends request to update eACL table of 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 WithNeoFSErrorParsing option has been provided, unsuccessful
|
||||
// NeoFS status codes are returned as `error`, otherwise, are included
|
||||
// If PrmInit.ResolveFrostFSFailures has been called, unsuccessful
|
||||
// FrostFS 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.
|
||||
|
@ -586,38 +642,49 @@ type ResContainerSetEACL struct {
|
|||
//
|
||||
// Success can be verified by reading by identifier (see EACL).
|
||||
//
|
||||
// Immediately panics if parameters are set incorrectly (see PrmContainerSetEACL docs).
|
||||
// Returns an error 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).
|
||||
// - global (see Client docs).
|
||||
func (c *Client) ContainerSetEACL(ctx context.Context, prm PrmContainerSetEACL) (*ResContainerSetEACL, error) {
|
||||
// check parameters
|
||||
switch {
|
||||
case ctx == nil:
|
||||
panic(panicMsgMissingContext)
|
||||
return nil, errorMissingContext
|
||||
case !prm.tableSet:
|
||||
panic("eACL table not set")
|
||||
return nil, errorEACLTableNotSet
|
||||
}
|
||||
|
||||
// sign the eACL table
|
||||
eaclV2 := prm.table.ToV2()
|
||||
|
||||
var sig frostfscrypto.Signature
|
||||
|
||||
err := sig.Calculate(frostfsecdsa.SignerRFC6979(c.prm.key), eaclV2.StableMarshal(nil))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("calculate signature: %w", err)
|
||||
}
|
||||
|
||||
var sigv2 refs.Signature
|
||||
|
||||
sig.WriteToV2(&sigv2)
|
||||
|
||||
// form request body
|
||||
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())
|
||||
reqBody.SetEACL(eaclV2)
|
||||
reqBody.SetSignature(&sigv2)
|
||||
|
||||
// form meta header
|
||||
var meta v2session.RequestMetaHeader
|
||||
meta.SetSessionToken(prm.table.SessionToken().ToV2())
|
||||
prm.prmCommonMeta.writeToMetaHeader(&meta)
|
||||
writeXHeadersToMeta(prm.prmCommonMeta.xHeaders, &meta)
|
||||
|
||||
if prm.sessionSet {
|
||||
var tokv2 v2session.Token
|
||||
prm.session.WriteToV2(&tokv2)
|
||||
|
||||
meta.SetSessionToken(&tokv2)
|
||||
}
|
||||
|
||||
// form request
|
||||
var req v2container.SetExtendedACLRequest
|
||||
|
@ -651,15 +718,15 @@ func (c *Client) ContainerSetEACL(ctx context.Context, prm PrmContainerSetEACL)
|
|||
type PrmAnnounceSpace struct {
|
||||
prmCommonMeta
|
||||
|
||||
announcements []container.UsedSpaceAnnouncement
|
||||
announcements []container.SizeEstimation
|
||||
}
|
||||
|
||||
// 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
|
||||
func (x *PrmAnnounceSpace) SetValues(vs []container.SizeEstimation) {
|
||||
x.announcements = vs
|
||||
}
|
||||
|
||||
// ResAnnounceSpace groups resulting values of ContainerAnnounceUsedSpace operation.
|
||||
|
@ -671,8 +738,8 @@ type ResAnnounceSpace struct {
|
|||
//
|
||||
// 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
|
||||
// If PrmInit.ResolveFrostFSFailures has been called, unsuccessful
|
||||
// FrostFS 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.
|
||||
|
@ -680,27 +747,27 @@ type ResAnnounceSpace struct {
|
|||
//
|
||||
// At this moment success can not be checked.
|
||||
//
|
||||
// Immediately panics if parameters are set incorrectly (see PrmAnnounceSpace docs).
|
||||
// Returns an error 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).
|
||||
// - global (see Client docs).
|
||||
func (c *Client) ContainerAnnounceUsedSpace(ctx context.Context, prm PrmAnnounceSpace) (*ResAnnounceSpace, error) {
|
||||
// check parameters
|
||||
switch {
|
||||
case ctx == nil:
|
||||
panic(panicMsgMissingContext)
|
||||
return nil, errorMissingContext
|
||||
case len(prm.announcements) == 0:
|
||||
panic("missing announcements")
|
||||
return nil, errorMissingAnnouncements
|
||||
}
|
||||
|
||||
// convert list of SDK announcement structures into NeoFS-API v2 list
|
||||
// convert list of SDK announcement structures into FrostFS-API v2 list
|
||||
v2announce := make([]v2container.UsedSpaceAnnouncement, len(prm.announcements))
|
||||
for i := range prm.announcements {
|
||||
v2announce[i] = *prm.announcements[i].ToV2()
|
||||
prm.announcements[i].WriteToV2(&v2announce[i])
|
||||
}
|
||||
|
||||
// prepare body of the NeoFS-API v2 request and request itself
|
||||
// prepare body of the FrostFS-API v2 request and request itself
|
||||
reqBody := new(v2container.AnnounceUsedSpaceRequestBody)
|
||||
reqBody.SetAnnouncements(v2announce)
|
||||
|
||||
|
@ -731,3 +798,23 @@ func (c *Client) ContainerAnnounceUsedSpace(ctx context.Context, prm PrmAnnounce
|
|||
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
// SyncContainerWithNetwork requests network configuration using passed client
|
||||
// and applies it to the container. Container MUST not be nil.
|
||||
//
|
||||
// Note: if container does not match network configuration, SyncContainerWithNetwork
|
||||
// changes it.
|
||||
//
|
||||
// Returns any network/parsing config errors.
|
||||
//
|
||||
// See also NetworkInfo, container.ApplyNetworkConfig.
|
||||
func SyncContainerWithNetwork(ctx context.Context, cnr *container.Container, c *Client) error {
|
||||
res, err := c.NetworkInfo(ctx, PrmNetworkInfo{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("network info call: %w", err)
|
||||
}
|
||||
|
||||
container.ApplyNetworkConfig(cnr, res.Info())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,20 +1,23 @@
|
|||
/*
|
||||
Package client provides NeoFS API client implementation.
|
||||
Package client provides FrostFS 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 NeoFS server:
|
||||
Connect to the FrostFS server:
|
||||
|
||||
var prm client.PrmDial
|
||||
prm.SetServerURI("localhost:8080")
|
||||
prm.SetDefaultPrivateKey(key)
|
||||
|
@ -23,7 +26,8 @@ Connect to the NeoFS server:
|
|||
err := c.Dial(prm)
|
||||
// ...
|
||||
|
||||
Execute NeoFS operation on the server:
|
||||
Execute FrostFS operation on the server:
|
||||
|
||||
var prm client.PrmContainerPut
|
||||
prm.SetContainer(cnr)
|
||||
// ...
|
||||
|
@ -36,14 +40,15 @@ Execute NeoFS operation on the server:
|
|||
// ...
|
||||
|
||||
Consume custom service of the server:
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
service CustomService {
|
||||
rpc CustomRPC(CustomRPCRequest) returns (CustomRPCResponse);
|
||||
}
|
||||
|
||||
import "github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
|
||||
import "github.com/nspcc-dev/neofs-api-go/v2/rpc/common"
|
||||
import "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
||||
import "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/common"
|
||||
|
||||
req := new(CustomRPCRequest)
|
||||
// ...
|
||||
|
@ -58,6 +63,7 @@ Consume custom service of the server:
|
|||
// ...
|
||||
|
||||
Close the connection:
|
||||
|
||||
err := c.Close()
|
||||
// ...
|
||||
|
||||
|
@ -65,9 +71,10 @@ 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"
|
||||
|
||||
type NeoFSClient interface {
|
||||
import "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client"
|
||||
|
||||
type FrostFSClient interface {
|
||||
// Operations according to the application needs
|
||||
CreateContainer(context.Context, container.Container) error
|
||||
// ...
|
||||
|
@ -80,6 +87,5 @@ with an interface:
|
|||
func (x *client) CreateContainer(context.Context, container.Container) error {
|
||||
// ...
|
||||
}
|
||||
|
||||
*/
|
||||
package client
|
||||
|
|
|
@ -1,11 +1,25 @@
|
|||
package client
|
||||
|
||||
import apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
// IsErrContainerNotFound checks if err corresponds to NeoFS status
|
||||
// return corresponding to missing container.
|
||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
||||
)
|
||||
|
||||
// unwraps err using errors.Unwrap and returns the result.
|
||||
func unwrapErr(err error) error {
|
||||
for e := errors.Unwrap(err); e != nil; e = errors.Unwrap(err) {
|
||||
err = e
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// IsErrContainerNotFound checks if err corresponds to FrostFS status
|
||||
// return corresponding to missing container. Supports wrapped errors.
|
||||
func IsErrContainerNotFound(err error) bool {
|
||||
switch err.(type) {
|
||||
switch unwrapErr(err).(type) {
|
||||
default:
|
||||
return false
|
||||
case
|
||||
|
@ -15,10 +29,23 @@ func IsErrContainerNotFound(err error) bool {
|
|||
}
|
||||
}
|
||||
|
||||
// IsErrObjectNotFound checks if err corresponds to NeoFS status
|
||||
// return corresponding to missing object.
|
||||
// IsErrEACLNotFound checks if err corresponds to FrostFS status
|
||||
// return corresponding to missing eACL table. Supports wrapped errors.
|
||||
func IsErrEACLNotFound(err error) bool {
|
||||
switch unwrapErr(err).(type) {
|
||||
default:
|
||||
return false
|
||||
case
|
||||
apistatus.EACLNotFound,
|
||||
*apistatus.EACLNotFound:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// IsErrObjectNotFound checks if err corresponds to FrostFS status
|
||||
// return corresponding to missing object. Supports wrapped errors.
|
||||
func IsErrObjectNotFound(err error) bool {
|
||||
switch err.(type) {
|
||||
switch unwrapErr(err).(type) {
|
||||
default:
|
||||
return false
|
||||
case
|
||||
|
@ -28,10 +55,10 @@ func IsErrObjectNotFound(err error) bool {
|
|||
}
|
||||
}
|
||||
|
||||
// IsErrObjectAlreadyRemoved checks if err corresponds to NeoFS status
|
||||
// return corresponding to already removed object.
|
||||
// IsErrObjectAlreadyRemoved checks if err corresponds to FrostFS status
|
||||
// return corresponding to already removed object. Supports wrapped errors.
|
||||
func IsErrObjectAlreadyRemoved(err error) bool {
|
||||
switch err.(type) {
|
||||
switch unwrapErr(err).(type) {
|
||||
default:
|
||||
return false
|
||||
case
|
||||
|
@ -40,3 +67,40 @@ func IsErrObjectAlreadyRemoved(err error) bool {
|
|||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// IsErrSessionExpired checks if err corresponds to FrostFS status return
|
||||
// corresponding to expired session. Supports wrapped errors.
|
||||
func IsErrSessionExpired(err error) bool {
|
||||
switch unwrapErr(err).(type) {
|
||||
default:
|
||||
return false
|
||||
case
|
||||
apistatus.SessionTokenExpired,
|
||||
*apistatus.SessionTokenExpired:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// IsErrSessionNotFound checks if err corresponds to FrostFS status return
|
||||
// corresponding to missing session. Supports wrapped errors.
|
||||
func IsErrSessionNotFound(err error) bool {
|
||||
switch unwrapErr(err).(type) {
|
||||
default:
|
||||
return false
|
||||
case
|
||||
apistatus.SessionTokenNotFound,
|
||||
*apistatus.SessionTokenNotFound:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// returns error describing missing field with the given name.
|
||||
func newErrMissingResponseField(name string) error {
|
||||
return fmt.Errorf("missing %s field in the response", name)
|
||||
}
|
||||
|
||||
// returns error describing invalid field (according to the FrostFS protocol)
|
||||
// with the given name and format violation err.
|
||||
func newErrInvalidResponseField(name string, err error) error {
|
||||
return fmt.Errorf("invalid %s field in the response: %w", name, err)
|
||||
}
|
||||
|
|
68
client/errors_test.go
Normal file
68
client/errors_test.go
Normal file
|
@ -0,0 +1,68 @@
|
|||
package client_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client"
|
||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestErrors(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
check func(error) bool
|
||||
errs []error
|
||||
}{
|
||||
{
|
||||
check: client.IsErrContainerNotFound,
|
||||
errs: []error{
|
||||
apistatus.ContainerNotFound{},
|
||||
new(apistatus.ContainerNotFound),
|
||||
},
|
||||
},
|
||||
{
|
||||
check: client.IsErrEACLNotFound,
|
||||
errs: []error{
|
||||
apistatus.EACLNotFound{},
|
||||
new(apistatus.EACLNotFound),
|
||||
},
|
||||
},
|
||||
{
|
||||
check: client.IsErrObjectNotFound,
|
||||
errs: []error{
|
||||
apistatus.ObjectNotFound{},
|
||||
new(apistatus.ObjectNotFound),
|
||||
},
|
||||
},
|
||||
{
|
||||
check: client.IsErrObjectAlreadyRemoved,
|
||||
errs: []error{
|
||||
apistatus.ObjectAlreadyRemoved{},
|
||||
new(apistatus.ObjectAlreadyRemoved),
|
||||
},
|
||||
},
|
||||
{
|
||||
check: client.IsErrSessionExpired,
|
||||
errs: []error{
|
||||
apistatus.SessionTokenExpired{},
|
||||
new(apistatus.SessionTokenExpired),
|
||||
},
|
||||
}, {
|
||||
check: client.IsErrSessionNotFound,
|
||||
errs: []error{
|
||||
apistatus.SessionTokenNotFound{},
|
||||
new(apistatus.SessionTokenNotFound),
|
||||
},
|
||||
},
|
||||
} {
|
||||
require.NotEmpty(t, tc.errs)
|
||||
|
||||
for i := range tc.errs {
|
||||
require.True(t, tc.check(tc.errs[i]), tc.errs[i])
|
||||
require.True(t, tc.check(fmt.Errorf("top-level context: :%w",
|
||||
fmt.Errorf("inner context: %w", tc.errs[i])),
|
||||
), tc.errs[i])
|
||||
}
|
||||
}
|
||||
}
|
198
client/netmap.go
198
client/netmap.go
|
@ -2,12 +2,16 @@ package client
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
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"
|
||||
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"
|
||||
)
|
||||
|
||||
// PrmEndpointInfo groups parameters of EndpointInfo operation.
|
||||
|
@ -19,54 +23,42 @@ type PrmEndpointInfo struct {
|
|||
type ResEndpointInfo struct {
|
||||
statusRes
|
||||
|
||||
version *version.Version
|
||||
version version.Version
|
||||
|
||||
ni *netmap.NodeInfo
|
||||
ni netmap.NodeInfo
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// LatestVersion returns latest FrostFS API protocol's version in use.
|
||||
func (x ResEndpointInfo) LatestVersion() version.Version {
|
||||
return x.version
|
||||
}
|
||||
|
||||
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 {
|
||||
// NodeInfo returns information about the FrostFS node served on the remote endpoint.
|
||||
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 WithNeoFSErrorParsing option has been provided, unsuccessful
|
||||
// NeoFS status codes are returned as `error`, otherwise, are included
|
||||
// If PrmInit.ResolveFrostFSFailures has been called, unsuccessful
|
||||
// FrostFS status codes are returned as `error`, otherwise, are included
|
||||
// in the returned result structure.
|
||||
//
|
||||
// Immediately panics if parameters are set incorrectly (see PrmEndpointInfo docs).
|
||||
// Returns an error 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) {
|
||||
// check context
|
||||
if ctx == nil {
|
||||
panic(panicMsgMissingContext)
|
||||
return nil, errorMissingContext
|
||||
}
|
||||
|
||||
// form request
|
||||
|
@ -91,8 +83,33 @@ func (c *Client) EndpointInfo(ctx context.Context, prm PrmEndpointInfo) (*ResEnd
|
|||
|
||||
body := resp.GetBody()
|
||||
|
||||
res.setLatestVersion(version.NewFromV2(body.GetVersion()))
|
||||
res.setNodeInfo(netmap.NewNodeInfoFromV2(body.GetNodeInfo()))
|
||||
const fieldVersion = "version"
|
||||
|
||||
verV2 := body.GetVersion()
|
||||
if verV2 == nil {
|
||||
cc.err = newErrMissingResponseField(fieldVersion)
|
||||
return
|
||||
}
|
||||
|
||||
cc.err = res.version.ReadFromV2(*verV2)
|
||||
if cc.err != nil {
|
||||
cc.err = newErrInvalidResponseField(fieldVersion, cc.err)
|
||||
return
|
||||
}
|
||||
|
||||
const fieldNodeInfo = "node info"
|
||||
|
||||
nodeInfoV2 := body.GetNodeInfo()
|
||||
if nodeInfoV2 == nil {
|
||||
cc.err = newErrMissingResponseField(fieldNodeInfo)
|
||||
return
|
||||
}
|
||||
|
||||
cc.err = res.ni.ReadFromV2(*nodeInfoV2)
|
||||
if cc.err != nil {
|
||||
cc.err = newErrInvalidResponseField(fieldNodeInfo, cc.err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// process call
|
||||
|
@ -112,39 +129,33 @@ type PrmNetworkInfo struct {
|
|||
type ResNetworkInfo struct {
|
||||
statusRes
|
||||
|
||||
info *netmap.NetworkInfo
|
||||
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 {
|
||||
// Info returns structured information about the FrostFS network.
|
||||
func (x ResNetworkInfo) Info() netmap.NetworkInfo {
|
||||
return x.info
|
||||
}
|
||||
|
||||
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.
|
||||
// NetworkInfo requests information about the FrostFS network of which the remote server is a part.
|
||||
//
|
||||
// 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
|
||||
// If PrmInit.ResolveFrostFSFailures has been called, unsuccessful
|
||||
// FrostFS status codes are returned as `error`, otherwise, are included
|
||||
// in the returned result structure.
|
||||
//
|
||||
// Immediately panics if parameters are set incorrectly (see PrmNetworkInfo docs).
|
||||
// Returns an error 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) {
|
||||
// check context
|
||||
if ctx == nil {
|
||||
panic(panicMsgMissingContext)
|
||||
return nil, errorMissingContext
|
||||
}
|
||||
|
||||
// form request
|
||||
|
@ -167,7 +178,19 @@ func (c *Client) NetworkInfo(ctx context.Context, prm PrmNetworkInfo) (*ResNetwo
|
|||
cc.result = func(r responseV2) {
|
||||
resp := r.(*v2netmap.NetworkInfoResponse)
|
||||
|
||||
res.setInfo(netmap.NewNetworkInfoFromV2(resp.GetBody().GetNetworkInfo()))
|
||||
const fieldNetInfo = "network info"
|
||||
|
||||
netInfoV2 := resp.GetBody().GetNetworkInfo()
|
||||
if netInfoV2 == nil {
|
||||
cc.err = newErrMissingResponseField(fieldNetInfo)
|
||||
return
|
||||
}
|
||||
|
||||
cc.err = res.info.ReadFromV2(*netInfoV2)
|
||||
if cc.err != nil {
|
||||
cc.err = newErrInvalidResponseField(fieldNetInfo, cc.err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// process call
|
||||
|
@ -177,3 +200,86 @@ func (c *Client) NetworkInfo(ctx context.Context, prm PrmNetworkInfo) (*ResNetwo
|
|||
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
// PrmNetMapSnapshot groups parameters of NetMapSnapshot operation.
|
||||
type PrmNetMapSnapshot struct {
|
||||
}
|
||||
|
||||
// ResNetMapSnapshot groups resulting values of NetMapSnapshot operation.
|
||||
type ResNetMapSnapshot struct {
|
||||
statusRes
|
||||
|
||||
netMap netmap.NetMap
|
||||
}
|
||||
|
||||
// NetMap returns current server's local network map.
|
||||
func (x ResNetMapSnapshot) NetMap() netmap.NetMap {
|
||||
return x.netMap
|
||||
}
|
||||
|
||||
// NetMapSnapshot requests current network view of the remote server.
|
||||
//
|
||||
// Any client's internal or transport errors are returned as `error`.
|
||||
// If PrmInit.ResolveFrostFSFailures has been called, unsuccessful
|
||||
// FrostFS status codes are returned as `error`, otherwise, are included
|
||||
// in the returned result structure.
|
||||
//
|
||||
// Returns an error if parameters are set incorrectly.
|
||||
// Context is required and MUST NOT be nil. It is used for network communication.
|
||||
//
|
||||
// Exactly one return value is non-nil. Server status return is returned in ResNetMapSnapshot.
|
||||
// Reflects all internal errors in second return value (transport problems, response processing, etc.).
|
||||
//
|
||||
// Return statuses:
|
||||
// - global (see Client docs).
|
||||
func (c *Client) NetMapSnapshot(ctx context.Context, _ PrmNetMapSnapshot) (*ResNetMapSnapshot, error) {
|
||||
// check context
|
||||
if ctx == nil {
|
||||
return nil, errorMissingContext
|
||||
}
|
||||
|
||||
// form request body
|
||||
var body v2netmap.SnapshotRequestBody
|
||||
|
||||
// form meta header
|
||||
var meta v2session.RequestMetaHeader
|
||||
|
||||
// form request
|
||||
var req v2netmap.SnapshotRequest
|
||||
req.SetBody(&body)
|
||||
c.prepareRequest(&req, &meta)
|
||||
|
||||
err := signature.SignServiceMessage(&c.prm.key, &req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sign request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := c.server.netMapSnapshot(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var res ResNetMapSnapshot
|
||||
res.st, err = c.processResponse(resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !apistatus.IsSuccessful(res.st) {
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
const fieldNetMap = "network map"
|
||||
|
||||
netMapV2 := resp.GetBody().NetMap()
|
||||
if netMapV2 == nil {
|
||||
return nil, newErrMissingResponseField(fieldNetMap)
|
||||
}
|
||||
|
||||
err = res.netMap.ReadFromV2(*netMapV2)
|
||||
if err != nil {
|
||||
return nil, newErrInvalidResponseField(fieldNetMap, err)
|
||||
}
|
||||
|
||||
return &res, nil
|
||||
}
|
||||
|
|
136
client/netmap_test.go
Normal file
136
client/netmap_test.go
Normal file
|
@ -0,0 +1,136 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
v2netmap "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/netmap"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
|
||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type serverNetMap struct {
|
||||
errTransport error
|
||||
|
||||
signResponse bool
|
||||
|
||||
statusOK bool
|
||||
|
||||
setNetMap bool
|
||||
netMap v2netmap.NetMap
|
||||
}
|
||||
|
||||
func (x *serverNetMap) netMapSnapshot(ctx context.Context, req v2netmap.SnapshotRequest) (*v2netmap.SnapshotResponse, error) {
|
||||
err := signature.VerifyServiceMessage(&req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if x.errTransport != nil {
|
||||
return nil, x.errTransport
|
||||
}
|
||||
|
||||
var body v2netmap.SnapshotResponseBody
|
||||
|
||||
if x.setNetMap {
|
||||
body.SetNetMap(&x.netMap)
|
||||
}
|
||||
|
||||
var meta session.ResponseMetaHeader
|
||||
|
||||
if !x.statusOK {
|
||||
meta.SetStatus(statusErr.ToStatusV2())
|
||||
}
|
||||
|
||||
var resp v2netmap.SnapshotResponse
|
||||
resp.SetBody(&body)
|
||||
resp.SetMetaHeader(&meta)
|
||||
|
||||
if x.signResponse {
|
||||
err = signature.SignServiceMessage(key, &resp)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("sign response: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func TestClient_NetMapSnapshot(t *testing.T) {
|
||||
var err error
|
||||
var prm PrmNetMapSnapshot
|
||||
var res *ResNetMapSnapshot
|
||||
var srv serverNetMap
|
||||
c := newClient(&srv)
|
||||
ctx := context.Background()
|
||||
|
||||
// missing context
|
||||
//nolint:staticcheck
|
||||
_, err = c.NetMapSnapshot(nil, prm)
|
||||
require.ErrorIs(t, err, errorMissingContext, "")
|
||||
|
||||
// request signature
|
||||
srv.errTransport = errors.New("any error")
|
||||
|
||||
_, err = c.NetMapSnapshot(ctx, prm)
|
||||
require.ErrorIs(t, err, srv.errTransport)
|
||||
|
||||
srv.errTransport = nil
|
||||
|
||||
// unsigned response
|
||||
_, err = c.NetMapSnapshot(ctx, prm)
|
||||
require.Error(t, err)
|
||||
|
||||
srv.signResponse = true
|
||||
|
||||
// status failure
|
||||
res, err = c.NetMapSnapshot(ctx, prm)
|
||||
require.NoError(t, err)
|
||||
assertStatusErr(t, res)
|
||||
|
||||
srv.statusOK = true
|
||||
|
||||
// missing netmap field
|
||||
_, err = c.NetMapSnapshot(ctx, prm)
|
||||
require.Error(t, err)
|
||||
|
||||
srv.setNetMap = true
|
||||
|
||||
// invalid network map
|
||||
var netMap netmap.NetMap
|
||||
|
||||
var node netmap.NodeInfo
|
||||
// TODO: #260 use instance corrupter
|
||||
|
||||
var nodeV2 v2netmap.NodeInfo
|
||||
|
||||
node.WriteToV2(&nodeV2)
|
||||
require.Error(t, new(netmap.NodeInfo).ReadFromV2(nodeV2))
|
||||
|
||||
netMap.SetNodes([]netmap.NodeInfo{node})
|
||||
netMap.WriteToV2(&srv.netMap)
|
||||
|
||||
_, err = c.NetMapSnapshot(ctx, prm)
|
||||
require.Error(t, err)
|
||||
|
||||
// correct network map
|
||||
// TODO: #260 use instance normalizer
|
||||
node.SetPublicKey([]byte{1, 2, 3})
|
||||
node.SetNetworkEndpoints("1", "2", "3")
|
||||
|
||||
node.WriteToV2(&nodeV2)
|
||||
require.NoError(t, new(netmap.NodeInfo).ReadFromV2(nodeV2))
|
||||
|
||||
netMap.SetNodes([]netmap.NodeInfo{node})
|
||||
netMap.WriteToV2(&srv.netMap)
|
||||
|
||||
res, err = c.NetMapSnapshot(ctx, prm)
|
||||
require.NoError(t, err)
|
||||
require.True(t, apistatus.IsSuccessful(res.Status()))
|
||||
require.Equal(t, netMap, res.NetMap())
|
||||
}
|
|
@ -3,16 +3,20 @@ package client
|
|||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"fmt"
|
||||
|
||||
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"
|
||||
"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"
|
||||
)
|
||||
|
||||
// PrmObjectDelete groups parameters of ObjectDelete operation.
|
||||
|
@ -33,8 +37,11 @@ type PrmObjectDelete struct {
|
|||
// 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())
|
||||
func (x *PrmObjectDelete) WithinSession(t session.Object) {
|
||||
var tv2 v2session.Token
|
||||
t.WriteToV2(&tv2)
|
||||
|
||||
x.meta.SetSessionToken(&tv2)
|
||||
}
|
||||
|
||||
// WithBearerToken attaches bearer token to be used for the operation.
|
||||
|
@ -42,20 +49,28 @@ func (x *PrmObjectDelete) WithinSession(t session.Token) {
|
|||
// 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())
|
||||
func (x *PrmObjectDelete) WithBearerToken(t bearer.Token) {
|
||||
var v2token acl.BearerToken
|
||||
t.WriteToV2(&v2token)
|
||||
x.meta.SetBearerToken(&v2token)
|
||||
}
|
||||
|
||||
// FromContainer specifies NeoFS container of the object.
|
||||
// FromContainer specifies FrostFS container of the object.
|
||||
// Required parameter.
|
||||
func (x *PrmObjectDelete) FromContainer(id cid.ID) {
|
||||
x.addr.SetContainerID(id.ToV2())
|
||||
var cidV2 v2refs.ContainerID
|
||||
id.WriteToV2(&cidV2)
|
||||
|
||||
x.addr.SetContainerID(&cidV2)
|
||||
}
|
||||
|
||||
// ByID specifies identifier of the requested object.
|
||||
// Required parameter.
|
||||
func (x *PrmObjectDelete) ByID(id oid.ID) {
|
||||
x.addr.SetObjectID(id.ToV2())
|
||||
var idV2 v2refs.ObjectID
|
||||
id.WriteToV2(&idV2)
|
||||
|
||||
x.addr.SetObjectID(&idV2)
|
||||
}
|
||||
|
||||
// UseKey specifies private key to sign the requests.
|
||||
|
@ -70,32 +85,22 @@ func (x *PrmObjectDelete) UseKey(key ecdsa.PrivateKey) {
|
|||
//
|
||||
// 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)
|
||||
writeXHeadersToMeta(hs, &x.meta)
|
||||
}
|
||||
|
||||
// ResObjectDelete groups resulting values of ObjectDelete operation.
|
||||
type ResObjectDelete struct {
|
||||
statusRes
|
||||
|
||||
idTomb *v2refs.ObjectID
|
||||
tomb oid.ID
|
||||
}
|
||||
|
||||
// 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
|
||||
// Tombstone returns identifier of the created tombstone object.
|
||||
func (x ResObjectDelete) Tombstone() oid.ID {
|
||||
return x.tomb
|
||||
}
|
||||
|
||||
// ObjectDelete marks an object for deletion from the container using NeoFS API protocol.
|
||||
// ObjectDelete marks an object for deletion from the container using FrostFS 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.
|
||||
|
@ -105,11 +110,11 @@ func (x ResObjectDelete) ReadTombstoneID(dst *oid.ID) bool {
|
|||
//
|
||||
// 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
|
||||
// If PrmInit.ResolveFrostFSFailures has been called, unsuccessful
|
||||
// FrostFS status codes are returned as `error`, otherwise, are included
|
||||
// in the returned result structure.
|
||||
//
|
||||
// Immediately panics if parameters are set incorrectly (see PrmObjectDelete docs).
|
||||
// Returns an error if parameters are set incorrectly (see PrmObjectDelete docs).
|
||||
// Context is required and must not be nil. It is used for network communication.
|
||||
//
|
||||
// Return statuses:
|
||||
|
@ -121,11 +126,11 @@ func (x ResObjectDelete) ReadTombstoneID(dst *oid.ID) bool {
|
|||
func (c *Client) ObjectDelete(ctx context.Context, prm PrmObjectDelete) (*ResObjectDelete, error) {
|
||||
switch {
|
||||
case ctx == nil:
|
||||
panic(panicMsgMissingContext)
|
||||
return nil, errorMissingContext
|
||||
case prm.addr.GetContainerID() == nil:
|
||||
panic(panicMsgMissingContainer)
|
||||
return nil, errorMissingContainer
|
||||
case prm.addr.GetObjectID() == nil:
|
||||
panic("missing object")
|
||||
return nil, errorMissingObject
|
||||
}
|
||||
|
||||
// form request body
|
||||
|
@ -134,33 +139,43 @@ func (c *Client) ObjectDelete(ctx context.Context, prm PrmObjectDelete) (*ResObj
|
|||
// form request
|
||||
var req v2object.DeleteRequest
|
||||
req.SetBody(&prm.body)
|
||||
req.SetMetaHeader(&prm.meta)
|
||||
|
||||
// init call context
|
||||
var (
|
||||
cc contextCall
|
||||
res ResObjectDelete
|
||||
)
|
||||
c.prepareRequest(&req, &prm.meta)
|
||||
|
||||
key := c.prm.key
|
||||
if prm.keySet {
|
||||
c.initCallContextWithoutKey(&cc)
|
||||
cc.key = prm.key
|
||||
} else {
|
||||
c.initCallContext(&cc)
|
||||
key = prm.key
|
||||
}
|
||||
|
||||
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()
|
||||
err := signature.SignServiceMessage(&key, &req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sign request: %w", err)
|
||||
}
|
||||
|
||||
// process call
|
||||
if !cc.processCall() {
|
||||
return nil, cc.err
|
||||
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)
|
||||
}
|
||||
|
||||
return &res, nil
|
||||
|
|
|
@ -7,54 +7,36 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
|
||||
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"
|
||||
"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"
|
||||
)
|
||||
|
||||
// shared parameters of GET/HEAD/RANGE.
|
||||
type prmObjectRead struct {
|
||||
prmCommonMeta
|
||||
meta v2session.RequestMetaHeader
|
||||
|
||||
raw bool
|
||||
|
||||
local bool
|
||||
|
||||
sessionSet bool
|
||||
session session.Token
|
||||
|
||||
bearerSet bool
|
||||
bearer token.BearerToken
|
||||
|
||||
cnrSet bool
|
||||
cnr cid.ID
|
||||
|
||||
objSet bool
|
||||
obj oid.ID
|
||||
addr v2refs.Address
|
||||
}
|
||||
|
||||
func (x prmObjectRead) writeToMetaHeader(h *v2session.RequestMetaHeader) {
|
||||
if x.local {
|
||||
h.SetTTL(1)
|
||||
}
|
||||
|
||||
if x.bearerSet {
|
||||
h.SetBearerToken(x.bearer.ToV2())
|
||||
}
|
||||
|
||||
if x.sessionSet {
|
||||
h.SetSessionToken(x.session.ToV2())
|
||||
}
|
||||
|
||||
x.prmCommonMeta.writeToMetaHeader(h)
|
||||
// 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 *prmObjectRead) WithXHeaders(hs ...string) {
|
||||
writeXHeadersToMeta(hs, &x.meta)
|
||||
}
|
||||
|
||||
// MarkRaw marks an intent to read physically stored object.
|
||||
|
@ -64,7 +46,7 @@ func (x *prmObjectRead) MarkRaw() {
|
|||
|
||||
// MarkLocal tells the server to execute the operation locally.
|
||||
func (x *prmObjectRead) MarkLocal() {
|
||||
x.local = true
|
||||
x.meta.SetTTL(1)
|
||||
}
|
||||
|
||||
// WithinSession specifies session within which object should be read.
|
||||
|
@ -73,9 +55,10 @@ func (x *prmObjectRead) MarkLocal() {
|
|||
// This may affect the execution of an operation (e.g. access control).
|
||||
//
|
||||
// Must be signed.
|
||||
func (x *prmObjectRead) WithinSession(t session.Token) {
|
||||
x.session = t
|
||||
x.sessionSet = true
|
||||
func (x *prmObjectRead) WithinSession(t session.Object) {
|
||||
var tokv2 v2session.Token
|
||||
t.WriteToV2(&tokv2)
|
||||
x.meta.SetSessionToken(&tokv2)
|
||||
}
|
||||
|
||||
// WithBearerToken attaches bearer token to be used for the operation.
|
||||
|
@ -83,28 +66,33 @@ func (x *prmObjectRead) WithinSession(t session.Token) {
|
|||
// If set, underlying eACL rules will be used in access control.
|
||||
//
|
||||
// Must be signed.
|
||||
func (x *prmObjectRead) WithBearerToken(t token.BearerToken) {
|
||||
x.bearer = t
|
||||
x.bearerSet = true
|
||||
func (x *prmObjectRead) WithBearerToken(t bearer.Token) {
|
||||
var v2token acl.BearerToken
|
||||
t.WriteToV2(&v2token)
|
||||
x.meta.SetBearerToken(&v2token)
|
||||
}
|
||||
|
||||
// FromContainer specifies NeoFS container of the object.
|
||||
// FromContainer specifies FrostFS container of the object.
|
||||
// Required parameter.
|
||||
func (x *prmObjectRead) FromContainer(id cid.ID) {
|
||||
x.cnr = id
|
||||
x.cnrSet = true
|
||||
var cnrV2 v2refs.ContainerID
|
||||
id.WriteToV2(&cnrV2)
|
||||
x.addr.SetContainerID(&cnrV2)
|
||||
}
|
||||
|
||||
// ByID specifies identifier of the requested object.
|
||||
// Required parameter.
|
||||
func (x *prmObjectRead) ByID(id oid.ID) {
|
||||
x.obj = id
|
||||
x.objSet = true
|
||||
var objV2 v2refs.ObjectID
|
||||
id.WriteToV2(&objV2)
|
||||
x.addr.SetObjectID(&objV2)
|
||||
}
|
||||
|
||||
// PrmObjectGet groups parameters of ObjectGetInit operation.
|
||||
type PrmObjectGet struct {
|
||||
prmObjectRead
|
||||
|
||||
key *ecdsa.PrivateKey
|
||||
}
|
||||
|
||||
// ResObjectGet groups the final result values of ObjectGetInit operation.
|
||||
|
@ -112,17 +100,20 @@ type ResObjectGet struct {
|
|||
statusRes
|
||||
}
|
||||
|
||||
// ObjectReader is designed to read one object from NeoFS system.
|
||||
// ObjectReader is designed to read one object from FrostFS system.
|
||||
//
|
||||
// Must be initialized using Client.ObjectGetInit, any other
|
||||
// usage is unsafe.
|
||||
type ObjectReader struct {
|
||||
cancelCtxStream context.CancelFunc
|
||||
|
||||
ctxCall contextCall
|
||||
client *Client
|
||||
stream interface {
|
||||
Read(resp *v2object.GetResponse) error
|
||||
}
|
||||
|
||||
// initially bound to contextCall
|
||||
bodyResp v2object.GetResponseBody
|
||||
res ResObjectGet
|
||||
err error
|
||||
|
||||
tailPayload []byte
|
||||
|
||||
|
@ -131,29 +122,32 @@ type ObjectReader struct {
|
|||
|
||||
// UseKey specifies private key to sign the requests.
|
||||
// If key is not provided, then Client default key is used.
|
||||
func (x *ObjectReader) UseKey(key ecdsa.PrivateKey) {
|
||||
x.ctxCall.key = key
|
||||
}
|
||||
|
||||
func handleSplitInfo(ctx *contextCall, i *v2object.SplitInfo) {
|
||||
ctx.err = object.NewSplitInfoError(object.NewSplitInfoFromV2(i))
|
||||
func (x *PrmObjectGet) UseKey(key ecdsa.PrivateKey) {
|
||||
x.key = &key
|
||||
}
|
||||
|
||||
// ReadHeader reads header of the object. Result means success.
|
||||
// Failure reason can be received via Close.
|
||||
func (x *ObjectReader) ReadHeader(dst *object.Object) bool {
|
||||
if !x.ctxCall.writeRequest() || !x.ctxCall.readResponse() {
|
||||
var resp v2object.GetResponse
|
||||
x.err = x.stream.Read(&resp)
|
||||
if x.err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
x.res.st, x.err = x.client.processResponse(&resp)
|
||||
if x.err != nil || !apistatus.IsSuccessful(x.res.st) {
|
||||
return false
|
||||
}
|
||||
|
||||
var partInit *v2object.GetObjectPartInit
|
||||
|
||||
switch v := x.bodyResp.GetObjectPart().(type) {
|
||||
switch v := resp.GetBody().GetObjectPart().(type) {
|
||||
default:
|
||||
x.ctxCall.err = fmt.Errorf("unexpected message instead of heading part: %T", v)
|
||||
x.err = fmt.Errorf("unexpected message instead of heading part: %T", v)
|
||||
return false
|
||||
case *v2object.SplitInfo:
|
||||
handleSplitInfo(&x.ctxCall, v)
|
||||
x.err = object.NewSplitInfoError(object.NewSplitInfoFromV2(v))
|
||||
return false
|
||||
case *v2object.GetObjectPartInit:
|
||||
partInit = v
|
||||
|
@ -184,26 +178,25 @@ func (x *ObjectReader) readChunk(buf []byte) (int, bool) {
|
|||
return read, true
|
||||
}
|
||||
|
||||
var ok bool
|
||||
var part v2object.GetObjectPart
|
||||
var chunk []byte
|
||||
var lastRead int
|
||||
|
||||
for {
|
||||
// receive next message
|
||||
ok = x.ctxCall.readResponse()
|
||||
if !ok {
|
||||
var resp v2object.GetResponse
|
||||
x.err = x.stream.Read(&resp)
|
||||
if x.err != nil {
|
||||
return read, false
|
||||
}
|
||||
|
||||
// get chunk part message
|
||||
part = x.bodyResp.GetObjectPart()
|
||||
x.res.st, x.err = x.client.processResponse(&resp)
|
||||
if x.err != nil || !apistatus.IsSuccessful(x.res.st) {
|
||||
return read, false
|
||||
}
|
||||
|
||||
var partChunk *v2object.GetObjectPartChunk
|
||||
|
||||
partChunk, ok = part.(*v2object.GetObjectPartChunk)
|
||||
part := resp.GetBody().GetObjectPart()
|
||||
partChunk, ok := part.(*v2object.GetObjectPartChunk)
|
||||
if !ok {
|
||||
x.ctxCall.err = fmt.Errorf("unexpected message instead of chunk part: %T", part)
|
||||
x.err = fmt.Errorf("unexpected message instead of chunk part: %T", part)
|
||||
return read, false
|
||||
}
|
||||
|
||||
|
@ -238,9 +231,9 @@ func (x *ObjectReader) ReadChunk(buf []byte) (int, bool) {
|
|||
func (x *ObjectReader) close(ignoreEOF bool) (*ResObjectGet, error) {
|
||||
defer x.cancelCtxStream()
|
||||
|
||||
if x.ctxCall.err != nil {
|
||||
if !errors.Is(x.ctxCall.err, io.EOF) {
|
||||
return nil, x.ctxCall.err
|
||||
if x.err != nil {
|
||||
if !errors.Is(x.err, io.EOF) {
|
||||
return nil, x.err
|
||||
} else if !ignoreEOF {
|
||||
if x.remainingPayloadLen > 0 {
|
||||
return nil, io.ErrUnexpectedEOF
|
||||
|
@ -250,7 +243,7 @@ func (x *ObjectReader) close(ignoreEOF bool) (*ResObjectGet, error) {
|
|||
}
|
||||
}
|
||||
|
||||
return x.ctxCall.statusRes.(*ResObjectGet), nil
|
||||
return &x.res, nil
|
||||
}
|
||||
|
||||
// Close ends reading the object and returns the result of the operation
|
||||
|
@ -258,11 +251,12 @@ func (x *ObjectReader) close(ignoreEOF bool) (*ResObjectGet, 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 NeoFS API statuses, then NeoFS failures
|
||||
// If Client is tuned to resolve FrostFS API statuses, then FrostFS failures
|
||||
// codes are returned as error.
|
||||
//
|
||||
// Return errors:
|
||||
// *object.SplitInfoError (returned on virtual objects with PrmObjectGet.MakeRaw).
|
||||
//
|
||||
// *object.SplitInfoError (returned on virtual objects with PrmObjectGet.MakeRaw).
|
||||
//
|
||||
// Return statuses:
|
||||
// - global (see Client docs);
|
||||
|
@ -297,76 +291,59 @@ func (x *ObjectReader) Read(p []byte) (int, error) {
|
|||
return n, nil
|
||||
}
|
||||
|
||||
// ObjectGetInit initiates reading an object through a remote server using NeoFS API protocol.
|
||||
// ObjectGetInit initiates reading an object through a remote server using FrostFS API protocol.
|
||||
//
|
||||
// The call only opens the transmission channel, explicit fetching is done using the ObjectReader.
|
||||
// Exactly one return value is non-nil. Resulting reader must be finally closed.
|
||||
//
|
||||
// Immediately panics if parameters are set incorrectly (see PrmObjectGet docs).
|
||||
// Returns an error if parameters are set incorrectly (see PrmObjectGet docs).
|
||||
// Context is required and must not be nil. It is used for network communication.
|
||||
func (c *Client) ObjectGetInit(ctx context.Context, prm PrmObjectGet) (*ObjectReader, error) {
|
||||
// check parameters
|
||||
switch {
|
||||
case ctx == nil:
|
||||
panic(panicMsgMissingContext)
|
||||
case !prm.cnrSet:
|
||||
panic(panicMsgMissingContainer)
|
||||
case !prm.objSet:
|
||||
panic("missing object")
|
||||
return nil, errorMissingContext
|
||||
case prm.addr.GetContainerID() == nil:
|
||||
return nil, errorMissingContainer
|
||||
case prm.addr.GetObjectID() == nil:
|
||||
return nil, errorMissingObject
|
||||
}
|
||||
|
||||
var addr v2refs.Address
|
||||
|
||||
addr.SetContainerID(prm.cnr.ToV2())
|
||||
addr.SetObjectID(prm.obj.ToV2())
|
||||
|
||||
// form request body
|
||||
var body v2object.GetRequestBody
|
||||
|
||||
body.SetRaw(prm.raw)
|
||||
body.SetAddress(&addr)
|
||||
|
||||
// form meta header
|
||||
var meta v2session.RequestMetaHeader
|
||||
|
||||
prm.prmObjectRead.writeToMetaHeader(&meta)
|
||||
body.SetAddress(&prm.addr)
|
||||
|
||||
// form request
|
||||
var req v2object.GetRequest
|
||||
|
||||
req.SetBody(&body)
|
||||
req.SetMetaHeader(&meta)
|
||||
c.prepareRequest(&req, &prm.meta)
|
||||
|
||||
// init reader
|
||||
var (
|
||||
r ObjectReader
|
||||
resp v2object.GetResponse
|
||||
stream *rpcapi.GetResponseReader
|
||||
)
|
||||
|
||||
ctx, r.cancelCtxStream = context.WithCancel(ctx)
|
||||
|
||||
resp.SetBody(&r.bodyResp)
|
||||
|
||||
// init call context
|
||||
c.initCallContext(&r.ctxCall)
|
||||
r.ctxCall.req = &req
|
||||
r.ctxCall.statusRes = new(ResObjectGet)
|
||||
r.ctxCall.resp = &resp
|
||||
r.ctxCall.wReq = func() error {
|
||||
var err error
|
||||
|
||||
stream, err = rpcapi.GetObject(&c.c, &req, client.WithContext(ctx))
|
||||
if err != nil {
|
||||
return fmt.Errorf("open stream: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
key := prm.key
|
||||
if key == nil {
|
||||
key = &c.prm.key
|
||||
}
|
||||
r.ctxCall.rResp = func() error {
|
||||
return stream.Read(&resp)
|
||||
|
||||
err := signature.SignServiceMessage(key, &req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sign request: %w", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
|
||||
stream, err := rpcapi.GetObject(&c.c, &req, client.WithContext(ctx))
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, fmt.Errorf("open stream: %w", err)
|
||||
}
|
||||
|
||||
var r ObjectReader
|
||||
r.cancelCtxStream = cancel
|
||||
r.stream = stream
|
||||
r.client = c
|
||||
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
|
@ -408,26 +385,27 @@ func (x *ResObjectHead) ReadHeader(dst *object.Object) bool {
|
|||
objv2.SetSignature(x.hdr.GetSignature())
|
||||
|
||||
obj := object.NewFromV2(&objv2)
|
||||
obj.SetID(&x.idObj)
|
||||
obj.SetID(x.idObj)
|
||||
|
||||
*dst = *obj
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// ObjectHead reads object header through a remote server using NeoFS API protocol.
|
||||
// ObjectHead reads object header through a remote server using FrostFS API protocol.
|
||||
//
|
||||
// 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
|
||||
// If PrmInit.ResolveFrostFSFailures has been called, unsuccessful
|
||||
// FrostFS status codes are returned as `error`, otherwise, are included
|
||||
// in the returned result structure.
|
||||
//
|
||||
// Immediately panics if parameters are set incorrectly (see PrmObjectHead docs).
|
||||
// Returns an error if parameters are set incorrectly (see PrmObjectHead docs).
|
||||
// Context is required and must not be nil. It is used for network communication.
|
||||
//
|
||||
// Return errors:
|
||||
// *object.SplitInfoError (returned on virtual objects with PrmObjectHead.MakeRaw).
|
||||
//
|
||||
// *object.SplitInfoError (returned on virtual objects with PrmObjectHead.MakeRaw).
|
||||
//
|
||||
// Return statuses:
|
||||
// - global (see Client docs);
|
||||
|
@ -439,70 +417,56 @@ func (x *ResObjectHead) ReadHeader(dst *object.Object) bool {
|
|||
func (c *Client) ObjectHead(ctx context.Context, prm PrmObjectHead) (*ResObjectHead, error) {
|
||||
switch {
|
||||
case ctx == nil:
|
||||
panic(panicMsgMissingContext)
|
||||
case !prm.cnrSet:
|
||||
panic(panicMsgMissingContainer)
|
||||
case !prm.objSet:
|
||||
panic("missing object")
|
||||
return nil, errorMissingContext
|
||||
case prm.addr.GetContainerID() == nil:
|
||||
return nil, errorMissingContainer
|
||||
case prm.addr.GetObjectID() == nil:
|
||||
return nil, errorMissingObject
|
||||
}
|
||||
|
||||
var addr v2refs.Address
|
||||
|
||||
addr.SetContainerID(prm.cnr.ToV2())
|
||||
addr.SetObjectID(prm.obj.ToV2())
|
||||
|
||||
// form request body
|
||||
var body v2object.HeadRequestBody
|
||||
|
||||
body.SetRaw(prm.raw)
|
||||
body.SetAddress(&addr)
|
||||
body.SetAddress(&prm.addr)
|
||||
|
||||
// form meta header
|
||||
var meta v2session.RequestMetaHeader
|
||||
|
||||
prm.prmObjectRead.writeToMetaHeader(&meta)
|
||||
|
||||
// form request
|
||||
var req v2object.HeadRequest
|
||||
|
||||
req.SetBody(&body)
|
||||
req.SetMetaHeader(&meta)
|
||||
|
||||
// init call context
|
||||
|
||||
var (
|
||||
cc contextCall
|
||||
res ResObjectHead
|
||||
)
|
||||
|
||||
res.idObj = prm.obj
|
||||
c.prepareRequest(&req, &prm.meta)
|
||||
|
||||
key := c.prm.key
|
||||
if prm.keySet {
|
||||
c.initCallContextWithoutKey(&cc)
|
||||
cc.key = prm.key
|
||||
} else {
|
||||
c.initCallContext(&cc)
|
||||
key = prm.key
|
||||
}
|
||||
|
||||
cc.req = &req
|
||||
cc.statusRes = &res
|
||||
cc.call = func() (responseV2, error) {
|
||||
return rpcapi.HeadObject(&c.c, &req, client.WithContext(ctx))
|
||||
}
|
||||
cc.result = func(r responseV2) {
|
||||
switch v := r.(*v2object.HeadResponse).GetBody().GetHeaderPart().(type) {
|
||||
default:
|
||||
cc.err = fmt.Errorf("unexpected header type %T", v)
|
||||
case *v2object.SplitInfo:
|
||||
handleSplitInfo(&cc, v)
|
||||
case *v2object.HeaderWithSignature:
|
||||
res.hdr = v
|
||||
}
|
||||
// sign the request
|
||||
err := signature.SignServiceMessage(&key, &req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sign request: %w", err)
|
||||
}
|
||||
|
||||
// process call
|
||||
if !cc.processCall() {
|
||||
return nil, cc.err
|
||||
resp, err := rpcapi.HeadObject(&c.c, &req, client.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("write request: %w", err)
|
||||
}
|
||||
|
||||
var res ResObjectHead
|
||||
res.st, err = c.processResponse(resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !apistatus.IsSuccessful(res.st) {
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
_ = res.idObj.ReadFromV2(*prm.addr.GetObjectID())
|
||||
|
||||
switch v := resp.GetBody().GetHeaderPart().(type) {
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected header type %T", v)
|
||||
case *v2object.SplitInfo:
|
||||
return nil, object.NewSplitInfoError(object.NewSplitInfoFromV2(v))
|
||||
case *v2object.HeaderWithSignature:
|
||||
res.hdr = v
|
||||
}
|
||||
|
||||
return &res, nil
|
||||
|
@ -512,19 +476,27 @@ func (c *Client) ObjectHead(ctx context.Context, prm PrmObjectHead) (*ResObjectH
|
|||
type PrmObjectRange struct {
|
||||
prmObjectRead
|
||||
|
||||
off, ln uint64
|
||||
key *ecdsa.PrivateKey
|
||||
|
||||
rng v2object.Range
|
||||
}
|
||||
|
||||
// SetOffset sets offset of the payload range to be read.
|
||||
// Zero by default.
|
||||
func (x *PrmObjectRange) SetOffset(off uint64) {
|
||||
x.off = off
|
||||
x.rng.SetOffset(off)
|
||||
}
|
||||
|
||||
// SetLength sets length of the payload range to be read.
|
||||
// Must be positive.
|
||||
func (x *PrmObjectRange) SetLength(ln uint64) {
|
||||
x.ln = ln
|
||||
x.rng.SetLength(ln)
|
||||
}
|
||||
|
||||
// UseKey specifies private key to sign the requests.
|
||||
// If key is not provided, then Client default key is used.
|
||||
func (x *PrmObjectRange) UseKey(key ecdsa.PrivateKey) {
|
||||
x.key = &key
|
||||
}
|
||||
|
||||
// ResObjectRange groups the final result values of ObjectRange operation.
|
||||
|
@ -533,40 +505,28 @@ type ResObjectRange struct {
|
|||
}
|
||||
|
||||
// ObjectRangeReader is designed to read payload range of one object
|
||||
// from NeoFS system.
|
||||
// from FrostFS system.
|
||||
//
|
||||
// Must be initialized using Client.ObjectRangeInit, any other
|
||||
// usage is unsafe.
|
||||
type ObjectRangeReader struct {
|
||||
cancelCtxStream context.CancelFunc
|
||||
|
||||
ctxCall contextCall
|
||||
client *Client
|
||||
|
||||
reqWritten bool
|
||||
res ResObjectRange
|
||||
err error
|
||||
|
||||
// initially bound to contextCall
|
||||
bodyResp v2object.GetRangeResponseBody
|
||||
stream interface {
|
||||
Read(resp *v2object.GetRangeResponse) error
|
||||
}
|
||||
|
||||
tailPayload []byte
|
||||
|
||||
remainingPayloadLen int
|
||||
}
|
||||
|
||||
// UseKey specifies private key to sign the requests.
|
||||
// If key is not provided, then Client default key is used.
|
||||
func (x *ObjectRangeReader) UseKey(key ecdsa.PrivateKey) {
|
||||
x.ctxCall.key = key
|
||||
}
|
||||
|
||||
func (x *ObjectRangeReader) readChunk(buf []byte) (int, bool) {
|
||||
if !x.reqWritten {
|
||||
if !x.ctxCall.writeRequest() {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
x.reqWritten = true
|
||||
}
|
||||
|
||||
var read int
|
||||
|
||||
// read remaining tail
|
||||
|
@ -578,25 +538,29 @@ func (x *ObjectRangeReader) readChunk(buf []byte) (int, bool) {
|
|||
return read, true
|
||||
}
|
||||
|
||||
var ok bool
|
||||
var partChunk *v2object.GetRangePartChunk
|
||||
var chunk []byte
|
||||
var lastRead int
|
||||
|
||||
for {
|
||||
// receive next message
|
||||
ok = x.ctxCall.readResponse()
|
||||
if !ok {
|
||||
var resp v2object.GetRangeResponse
|
||||
x.err = x.stream.Read(&resp)
|
||||
if x.err != nil {
|
||||
return read, false
|
||||
}
|
||||
|
||||
x.res.st, x.err = x.client.processResponse(&resp)
|
||||
if x.err != nil || !apistatus.IsSuccessful(x.res.st) {
|
||||
return read, false
|
||||
}
|
||||
|
||||
// get chunk message
|
||||
switch v := x.bodyResp.GetRangePart().(type) {
|
||||
switch v := resp.GetBody().GetRangePart().(type) {
|
||||
default:
|
||||
x.ctxCall.err = fmt.Errorf("unexpected message received: %T", v)
|
||||
x.err = fmt.Errorf("unexpected message received: %T", v)
|
||||
return read, false
|
||||
case *v2object.SplitInfo:
|
||||
handleSplitInfo(&x.ctxCall, v)
|
||||
x.err = object.NewSplitInfoError(object.NewSplitInfoFromV2(v))
|
||||
return read, false
|
||||
case *v2object.GetRangePartChunk:
|
||||
partChunk = v
|
||||
|
@ -632,9 +596,9 @@ func (x *ObjectRangeReader) ReadChunk(buf []byte) (int, bool) {
|
|||
func (x *ObjectRangeReader) close(ignoreEOF bool) (*ResObjectRange, error) {
|
||||
defer x.cancelCtxStream()
|
||||
|
||||
if x.ctxCall.err != nil {
|
||||
if !errors.Is(x.ctxCall.err, io.EOF) {
|
||||
return nil, x.ctxCall.err
|
||||
if x.err != nil {
|
||||
if !errors.Is(x.err, io.EOF) {
|
||||
return nil, x.err
|
||||
} else if !ignoreEOF {
|
||||
if x.remainingPayloadLen > 0 {
|
||||
return nil, io.ErrUnexpectedEOF
|
||||
|
@ -644,7 +608,7 @@ func (x *ObjectRangeReader) close(ignoreEOF bool) (*ResObjectRange, error) {
|
|||
}
|
||||
}
|
||||
|
||||
return x.ctxCall.statusRes.(*ResObjectRange), nil
|
||||
return &x.res, nil
|
||||
}
|
||||
|
||||
// Close ends reading the payload range and returns the result of the operation
|
||||
|
@ -652,11 +616,12 @@ func (x *ObjectRangeReader) close(ignoreEOF bool) (*ResObjectRange, 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 NeoFS API statuses, then NeoFS failures
|
||||
// If Client is tuned to resolve FrostFS API statuses, then FrostFS failures
|
||||
// codes are returned as error.
|
||||
//
|
||||
// Return errors:
|
||||
// *object.SplitInfoError (returned on virtual objects with PrmObjectRange.MakeRaw).
|
||||
//
|
||||
// *object.SplitInfoError (returned on virtual objects with PrmObjectRange.MakeRaw).
|
||||
//
|
||||
// Return statuses:
|
||||
// - global (see Client docs);
|
||||
|
@ -664,6 +629,7 @@ func (x *ObjectRangeReader) close(ignoreEOF bool) (*ResObjectRange, error) {
|
|||
// - *apistatus.ObjectNotFound;
|
||||
// - *apistatus.ObjectAccessDenied;
|
||||
// - *apistatus.ObjectAlreadyRemoved;
|
||||
// - *apistatus.ObjectOutOfRange;
|
||||
// - *apistatus.SessionTokenExpired.
|
||||
func (x *ObjectRangeReader) Close() (*ResObjectRange, error) {
|
||||
return x.close(true)
|
||||
|
@ -692,85 +658,62 @@ func (x *ObjectRangeReader) Read(p []byte) (int, error) {
|
|||
}
|
||||
|
||||
// ObjectRangeInit initiates reading an object's payload range through a remote
|
||||
// server using NeoFS API protocol.
|
||||
// server using FrostFS API protocol.
|
||||
//
|
||||
// The call only opens the transmission channel, explicit fetching is done using the ObjectRangeReader.
|
||||
// Exactly one return value is non-nil. Resulting reader must be finally closed.
|
||||
//
|
||||
// Immediately panics if parameters are set incorrectly (see PrmObjectRange docs).
|
||||
// Returns an error if parameters are set incorrectly (see PrmObjectRange docs).
|
||||
// Context is required and must not be nil. It is used for network communication.
|
||||
func (c *Client) ObjectRangeInit(ctx context.Context, prm PrmObjectRange) (*ObjectRangeReader, error) {
|
||||
// check parameters
|
||||
switch {
|
||||
case ctx == nil:
|
||||
panic(panicMsgMissingContext)
|
||||
case !prm.cnrSet:
|
||||
panic(panicMsgMissingContainer)
|
||||
case !prm.objSet:
|
||||
panic("missing object")
|
||||
case prm.ln == 0:
|
||||
panic("zero range length")
|
||||
return nil, errorMissingContext
|
||||
case prm.addr.GetContainerID() == nil:
|
||||
return nil, errorMissingContainer
|
||||
case prm.addr.GetObjectID() == nil:
|
||||
return nil, errorMissingObject
|
||||
case prm.rng.GetLength() == 0:
|
||||
return nil, errorZeroRangeLength
|
||||
}
|
||||
|
||||
var addr v2refs.Address
|
||||
|
||||
addr.SetContainerID(prm.cnr.ToV2())
|
||||
addr.SetObjectID(prm.obj.ToV2())
|
||||
|
||||
var rng v2object.Range
|
||||
|
||||
rng.SetOffset(prm.off)
|
||||
rng.SetLength(prm.ln)
|
||||
|
||||
// form request body
|
||||
var body v2object.GetRangeRequestBody
|
||||
|
||||
body.SetRaw(prm.raw)
|
||||
body.SetAddress(&addr)
|
||||
body.SetRange(&rng)
|
||||
|
||||
// form meta header
|
||||
var meta v2session.RequestMetaHeader
|
||||
|
||||
prm.prmObjectRead.writeToMetaHeader(&meta)
|
||||
body.SetAddress(&prm.addr)
|
||||
body.SetRange(&prm.rng)
|
||||
|
||||
// form request
|
||||
var req v2object.GetRangeRequest
|
||||
|
||||
req.SetBody(&body)
|
||||
req.SetMetaHeader(&meta)
|
||||
c.prepareRequest(&req, &prm.meta)
|
||||
|
||||
// init reader
|
||||
var (
|
||||
r ObjectRangeReader
|
||||
resp v2object.GetRangeResponse
|
||||
stream *rpcapi.ObjectRangeResponseReader
|
||||
)
|
||||
|
||||
r.remainingPayloadLen = int(prm.ln)
|
||||
|
||||
ctx, r.cancelCtxStream = context.WithCancel(ctx)
|
||||
|
||||
resp.SetBody(&r.bodyResp)
|
||||
|
||||
// init call context
|
||||
c.initCallContext(&r.ctxCall)
|
||||
r.ctxCall.req = &req
|
||||
r.ctxCall.statusRes = new(ResObjectRange)
|
||||
r.ctxCall.resp = &resp
|
||||
r.ctxCall.wReq = func() error {
|
||||
var err error
|
||||
|
||||
stream, err = rpcapi.GetObjectRange(&c.c, &req, client.WithContext(ctx))
|
||||
if err != nil {
|
||||
return fmt.Errorf("open stream: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
key := prm.key
|
||||
if key == nil {
|
||||
key = &c.prm.key
|
||||
}
|
||||
r.ctxCall.rResp = func() error {
|
||||
return stream.Read(&resp)
|
||||
|
||||
err := signature.SignServiceMessage(key, &req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sign request: %w", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
|
||||
stream, err := rpcapi.GetObjectRange(&c.c, &req, client.WithContext(ctx))
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, fmt.Errorf("open stream: %w", err)
|
||||
}
|
||||
|
||||
var r ObjectRangeReader
|
||||
r.remainingPayloadLen = int(prm.rng.GetLength())
|
||||
r.cancelCtxStream = cancel
|
||||
r.stream = stream
|
||||
r.client = c
|
||||
|
||||
return &r, nil
|
||||
}
|
||||
|
|
|
@ -2,16 +2,21 @@ package client
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"fmt"
|
||||
|
||||
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"
|
||||
"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"
|
||||
)
|
||||
|
||||
// PrmObjectHash groups parameters of ObjectHash operation.
|
||||
|
@ -20,9 +25,19 @@ type PrmObjectHash struct {
|
|||
|
||||
body v2object.GetRangeHashRequestBody
|
||||
|
||||
tillichZemor bool
|
||||
csAlgo v2refs.ChecksumType
|
||||
|
||||
addr v2refs.Address
|
||||
|
||||
keySet bool
|
||||
key ecdsa.PrivateKey
|
||||
}
|
||||
|
||||
// UseKey specifies private key to sign the requests.
|
||||
// If key is not provided, then Client default key is used.
|
||||
func (x *PrmObjectHash) UseKey(key ecdsa.PrivateKey) {
|
||||
x.keySet = true
|
||||
x.key = key
|
||||
}
|
||||
|
||||
// MarkLocal tells the server to execute the operation locally.
|
||||
|
@ -36,8 +51,11 @@ func (x *PrmObjectHash) MarkLocal() {
|
|||
// 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())
|
||||
func (x *PrmObjectHash) WithinSession(t session.Object) {
|
||||
var tv2 v2session.Token
|
||||
t.WriteToV2(&tv2)
|
||||
|
||||
x.meta.SetSessionToken(&tv2)
|
||||
}
|
||||
|
||||
// WithBearerToken attaches bearer token to be used for the operation.
|
||||
|
@ -45,20 +63,28 @@ func (x *PrmObjectHash) WithinSession(t session.Token) {
|
|||
// 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())
|
||||
func (x *PrmObjectHash) WithBearerToken(t bearer.Token) {
|
||||
var v2token acl.BearerToken
|
||||
t.WriteToV2(&v2token)
|
||||
x.meta.SetBearerToken(&v2token)
|
||||
}
|
||||
|
||||
// FromContainer specifies NeoFS container of the object.
|
||||
// FromContainer specifies FrostFS container of the object.
|
||||
// Required parameter.
|
||||
func (x *PrmObjectHash) FromContainer(id cid.ID) {
|
||||
x.addr.SetContainerID(id.ToV2())
|
||||
var cidV2 v2refs.ContainerID
|
||||
id.WriteToV2(&cidV2)
|
||||
|
||||
x.addr.SetContainerID(&cidV2)
|
||||
}
|
||||
|
||||
// ByID specifies identifier of the requested object.
|
||||
// Required parameter.
|
||||
func (x *PrmObjectHash) ByID(id oid.ID) {
|
||||
x.addr.SetObjectID(id.ToV2())
|
||||
var idV2 v2refs.ObjectID
|
||||
id.WriteToV2(&idV2)
|
||||
|
||||
x.addr.SetObjectID(&idV2)
|
||||
}
|
||||
|
||||
// SetRangeList sets list of ranges in (offset, length) pair format.
|
||||
|
@ -86,7 +112,7 @@ func (x *PrmObjectHash) SetRangeList(r ...uint64) {
|
|||
//
|
||||
// By default, SHA256 hash function is used.
|
||||
func (x *PrmObjectHash) TillichZemorAlgo() {
|
||||
x.tillichZemor = true
|
||||
x.csAlgo = v2refs.TillichZemor
|
||||
}
|
||||
|
||||
// UseSalt sets the salt to XOR the data range before hashing.
|
||||
|
@ -101,11 +127,7 @@ func (x *PrmObjectHash) UseSalt(salt []byte) {
|
|||
//
|
||||
// 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)
|
||||
writeXHeadersToMeta(hs, &x.meta)
|
||||
}
|
||||
|
||||
// ResObjectHash groups resulting values of ObjectHash operation.
|
||||
|
@ -121,18 +143,18 @@ func (x ResObjectHash) Checksums() [][]byte {
|
|||
}
|
||||
|
||||
// ObjectHash requests checksum of the range list of the object payload using
|
||||
// NeoFS API protocol.
|
||||
// FrostFS 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 WithNeoFSErrorParsing option has been provided, unsuccessful
|
||||
// NeoFS status codes are returned as `error`, otherwise, are included
|
||||
// If PrmInit.ResolveFrostFSFailures has been called, unsuccessful
|
||||
// FrostFS status codes are returned as `error`, otherwise, are included
|
||||
// in the returned result structure.
|
||||
//
|
||||
// Immediately panics if parameters are set incorrectly (see PrmObjectHash docs).
|
||||
// Returns an error if parameters are set incorrectly (see PrmObjectHash docs).
|
||||
// Context is required and must not be nil. It is used for network communication.
|
||||
//
|
||||
// Return statuses:
|
||||
|
@ -140,53 +162,59 @@ func (x ResObjectHash) Checksums() [][]byte {
|
|||
// - *apistatus.ContainerNotFound;
|
||||
// - *apistatus.ObjectNotFound;
|
||||
// - *apistatus.ObjectAccessDenied;
|
||||
// - *apistatus.ObjectOutOfRange;
|
||||
// - *apistatus.SessionTokenExpired.
|
||||
func (c *Client) ObjectHash(ctx context.Context, prm PrmObjectHash) (*ResObjectHash, error) {
|
||||
switch {
|
||||
case ctx == nil:
|
||||
panic(panicMsgMissingContext)
|
||||
return nil, errorMissingContext
|
||||
case prm.addr.GetContainerID() == nil:
|
||||
panic(panicMsgMissingContainer)
|
||||
return nil, errorMissingContainer
|
||||
case prm.addr.GetObjectID() == nil:
|
||||
panic("missing object")
|
||||
return nil, errorMissingObject
|
||||
case len(prm.body.GetRanges()) == 0:
|
||||
panic("missing ranges")
|
||||
return nil, errorMissingRanges
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if prm.csAlgo == v2refs.UnknownChecksum {
|
||||
prm.body.SetType(v2refs.SHA256)
|
||||
} else {
|
||||
prm.body.SetType(prm.csAlgo)
|
||||
}
|
||||
|
||||
// form request
|
||||
var req v2object.GetRangeHashRequest
|
||||
c.prepareRequest(&req, &prm.meta)
|
||||
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()
|
||||
key := c.prm.key
|
||||
if prm.keySet {
|
||||
key = prm.key
|
||||
}
|
||||
|
||||
// process call
|
||||
if !cc.processCall() {
|
||||
return nil, cc.err
|
||||
err := signature.SignServiceMessage(&key, &req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sign request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := rpcapi.HashObjectRange(&c.c, &req, client.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("write request: %w", err)
|
||||
}
|
||||
|
||||
var res ResObjectHash
|
||||
res.st, err = c.processResponse(resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !apistatus.IsSuccessful(res.st) {
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
res.checksums = resp.GetBody().GetHashList()
|
||||
if len(res.checksums) == 0 {
|
||||
return nil, newErrMissingResponseField("hash list")
|
||||
}
|
||||
|
||||
return &res, nil
|
||||
|
|
|
@ -3,97 +3,106 @@ package client
|
|||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
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"
|
||||
"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"
|
||||
)
|
||||
|
||||
// PrmObjectPutInit groups parameters of ObjectPutInit operation.
|
||||
//
|
||||
// At the moment the operation is not parameterized, however,
|
||||
// the structure is still declared for backward compatibility.
|
||||
type PrmObjectPutInit struct{}
|
||||
type PrmObjectPutInit struct {
|
||||
copyNum uint32
|
||||
key *ecdsa.PrivateKey
|
||||
meta v2session.RequestMetaHeader
|
||||
}
|
||||
|
||||
// SetCopiesNumber sets number of object copies that is enough to consider put successful.
|
||||
func (x *PrmObjectPutInit) SetCopiesNumber(copiesNumber uint32) {
|
||||
x.copyNum = copiesNumber
|
||||
}
|
||||
|
||||
// ResObjectPut groups the final result values of ObjectPutInit operation.
|
||||
type ResObjectPut struct {
|
||||
statusRes
|
||||
|
||||
resp v2object.PutResponse
|
||||
obj oid.ID
|
||||
}
|
||||
|
||||
// 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
|
||||
// StoredObjectID returns identifier of the saved object.
|
||||
func (x ResObjectPut) StoredObjectID() oid.ID {
|
||||
return x.obj
|
||||
}
|
||||
|
||||
// ObjectWriter is designed to write one object to NeoFS system.
|
||||
// ObjectWriter is designed to write one object to FrostFS system.
|
||||
//
|
||||
// Must be initialized using Client.ObjectPutInit, any other
|
||||
// usage is unsafe.
|
||||
type ObjectWriter struct {
|
||||
cancelCtxStream context.CancelFunc
|
||||
|
||||
ctxCall contextCall
|
||||
client *Client
|
||||
stream interface {
|
||||
Write(*v2object.PutRequest) error
|
||||
Close() error
|
||||
}
|
||||
|
||||
// initially bound tp contextCall
|
||||
metaHdr v2session.RequestMetaHeader
|
||||
|
||||
// initially bound to contextCall
|
||||
partInit v2object.PutObjectPartInit
|
||||
key *ecdsa.PrivateKey
|
||||
res ResObjectPut
|
||||
err error
|
||||
|
||||
chunkCalled bool
|
||||
|
||||
respV2 v2object.PutResponse
|
||||
req v2object.PutRequest
|
||||
partInit v2object.PutObjectPartInit
|
||||
partChunk v2object.PutObjectPartChunk
|
||||
}
|
||||
|
||||
// UseKey specifies private key to sign the requests.
|
||||
// If key is not provided, then Client default key is used.
|
||||
func (x *ObjectWriter) UseKey(key ecdsa.PrivateKey) {
|
||||
x.ctxCall.key = key
|
||||
func (x *PrmObjectPutInit) UseKey(key ecdsa.PrivateKey) {
|
||||
x.key = &key
|
||||
}
|
||||
|
||||
// WithBearerToken attaches bearer token to be used for the operation.
|
||||
// Should be called once before any writing steps.
|
||||
func (x *ObjectWriter) WithBearerToken(t token.BearerToken) {
|
||||
x.metaHdr.SetBearerToken(t.ToV2())
|
||||
func (x *PrmObjectPutInit) WithBearerToken(t bearer.Token) {
|
||||
var v2token acl.BearerToken
|
||||
t.WriteToV2(&v2token)
|
||||
x.meta.SetBearerToken(&v2token)
|
||||
}
|
||||
|
||||
// WithinSession specifies session within which object should be stored.
|
||||
// Should be called once before any writing steps.
|
||||
func (x *ObjectWriter) WithinSession(t session.Token) {
|
||||
x.metaHdr.SetSessionToken(t.ToV2())
|
||||
func (x *PrmObjectPutInit) WithinSession(t session.Object) {
|
||||
var tv2 v2session.Token
|
||||
t.WriteToV2(&tv2)
|
||||
|
||||
x.meta.SetSessionToken(&tv2)
|
||||
}
|
||||
|
||||
// MarkLocal tells the server to execute the operation locally.
|
||||
func (x *ObjectWriter) MarkLocal() {
|
||||
x.metaHdr.SetTTL(1)
|
||||
func (x *PrmObjectPutInit) MarkLocal() {
|
||||
x.meta.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.
|
||||
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)
|
||||
func (x *PrmObjectPutInit) WithXHeaders(hs ...string) {
|
||||
writeXHeadersToMeta(hs, &x.meta)
|
||||
}
|
||||
|
||||
// WriteHeader writes header of the object. Result means success.
|
||||
|
@ -105,7 +114,17 @@ func (x *ObjectWriter) WriteHeader(hdr object.Object) bool {
|
|||
x.partInit.SetHeader(v2Hdr.GetHeader())
|
||||
x.partInit.SetSignature(v2Hdr.GetSignature())
|
||||
|
||||
return x.ctxCall.writeRequest()
|
||||
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
|
||||
}
|
||||
|
||||
// WritePayloadChunk writes chunk of the object payload. Result means success.
|
||||
|
@ -113,7 +132,7 @@ func (x *ObjectWriter) WriteHeader(hdr object.Object) bool {
|
|||
func (x *ObjectWriter) WritePayloadChunk(chunk []byte) bool {
|
||||
if !x.chunkCalled {
|
||||
x.chunkCalled = true
|
||||
x.ctxCall.req.(*v2object.PutRequest).GetBody().SetObjectPart(&x.partChunk)
|
||||
x.req.GetBody().SetObjectPart(&x.partChunk)
|
||||
}
|
||||
|
||||
for ln := len(chunk); ln > 0; ln = len(chunk) {
|
||||
|
@ -138,8 +157,16 @@ func (x *ObjectWriter) WritePayloadChunk(chunk []byte) bool {
|
|||
// 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)
|
||||
|
||||
if !x.ctxCall.writeRequest() {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -154,7 +181,7 @@ func (x *ObjectWriter) WritePayloadChunk(chunk []byte) bool {
|
|||
//
|
||||
// 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
|
||||
// If Client is tuned to resolve FrostFS API statuses, then FrostFS failures
|
||||
// codes are returned as error.
|
||||
//
|
||||
// Return statuses:
|
||||
|
@ -168,66 +195,73 @@ func (x *ObjectWriter) WritePayloadChunk(chunk []byte) bool {
|
|||
func (x *ObjectWriter) Close() (*ResObjectPut, error) {
|
||||
defer x.cancelCtxStream()
|
||||
|
||||
if x.ctxCall.err != nil {
|
||||
return nil, x.ctxCall.err
|
||||
// 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.ctxCall.close() {
|
||||
return nil, x.ctxCall.err
|
||||
if x.err = x.stream.Close(); x.err != nil {
|
||||
return nil, x.err
|
||||
}
|
||||
|
||||
if !x.ctxCall.processResponse() {
|
||||
return nil, x.ctxCall.err
|
||||
x.res.st, x.err = x.client.processResponse(&x.respV2)
|
||||
if x.err != nil {
|
||||
return nil, x.err
|
||||
}
|
||||
|
||||
return x.ctxCall.statusRes.(*ResObjectPut), nil
|
||||
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)
|
||||
}
|
||||
|
||||
return &x.res, nil
|
||||
}
|
||||
|
||||
// ObjectPutInit initiates writing an object through a remote server using NeoFS API protocol.
|
||||
// ObjectPutInit initiates writing an object through a remote server using FrostFS 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, _ PrmObjectPutInit) (*ObjectWriter, error) {
|
||||
func (c *Client) ObjectPutInit(ctx context.Context, prm PrmObjectPutInit) (*ObjectWriter, error) {
|
||||
// check parameters
|
||||
if ctx == nil {
|
||||
panic(panicMsgMissingContext)
|
||||
return nil, errorMissingContext
|
||||
}
|
||||
|
||||
// open stream
|
||||
var (
|
||||
res ResObjectPut
|
||||
w ObjectWriter
|
||||
)
|
||||
var w ObjectWriter
|
||||
|
||||
ctx, w.cancelCtxStream = context.WithCancel(ctx)
|
||||
|
||||
stream, err := rpcapi.PutObject(&c.c, &res.resp, client.WithContext(ctx))
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
stream, err := rpcapi.PutObject(&c.c, &w.respV2, client.WithContext(ctx))
|
||||
if err != nil {
|
||||
cancel()
|
||||
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.key = &c.prm.key
|
||||
if prm.key != nil {
|
||||
w.key = prm.key
|
||||
}
|
||||
w.ctxCall.closer = stream.Close
|
||||
w.cancelCtxStream = cancel
|
||||
w.client = c
|
||||
w.stream = stream
|
||||
w.partInit.SetCopiesNumber(prm.copyNum)
|
||||
w.req.SetBody(new(v2object.PutRequestBody))
|
||||
c.prepareRequest(&w.req, &prm.meta)
|
||||
|
||||
return &w, nil
|
||||
}
|
||||
|
|
|
@ -7,40 +7,36 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
|
||||
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"
|
||||
"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"
|
||||
)
|
||||
|
||||
// PrmObjectSearch groups parameters of ObjectSearch operation.
|
||||
type PrmObjectSearch struct {
|
||||
prmCommonMeta
|
||||
meta v2session.RequestMetaHeader
|
||||
|
||||
local bool
|
||||
|
||||
sessionSet bool
|
||||
session session.Token
|
||||
|
||||
bearerSet bool
|
||||
bearer token.BearerToken
|
||||
key *ecdsa.PrivateKey
|
||||
|
||||
cnrSet bool
|
||||
cnr cid.ID
|
||||
cnrID cid.ID
|
||||
|
||||
filters object.SearchFilters
|
||||
}
|
||||
|
||||
// MarkLocal tells the server to execute the operation locally.
|
||||
func (x *PrmObjectSearch) MarkLocal() {
|
||||
x.local = true
|
||||
x.meta.SetTTL(1)
|
||||
}
|
||||
|
||||
// WithinSession specifies session within which the search query must be executed.
|
||||
|
@ -49,9 +45,10 @@ func (x *PrmObjectSearch) MarkLocal() {
|
|||
// This may affect the execution of an operation (e.g. access control).
|
||||
//
|
||||
// Must be signed.
|
||||
func (x *PrmObjectSearch) WithinSession(t session.Token) {
|
||||
x.session = t
|
||||
x.sessionSet = true
|
||||
func (x *PrmObjectSearch) WithinSession(t session.Object) {
|
||||
var tokv2 v2session.Token
|
||||
t.WriteToV2(&tokv2)
|
||||
x.meta.SetSessionToken(&tokv2)
|
||||
}
|
||||
|
||||
// WithBearerToken attaches bearer token to be used for the operation.
|
||||
|
@ -59,15 +56,30 @@ func (x *PrmObjectSearch) WithinSession(t session.Token) {
|
|||
// If set, underlying eACL rules will be used in access control.
|
||||
//
|
||||
// Must be signed.
|
||||
func (x *PrmObjectSearch) WithBearerToken(t token.BearerToken) {
|
||||
x.bearer = t
|
||||
x.bearerSet = true
|
||||
func (x *PrmObjectSearch) WithBearerToken(t bearer.Token) {
|
||||
var v2token acl.BearerToken
|
||||
t.WriteToV2(&v2token)
|
||||
x.meta.SetBearerToken(&v2token)
|
||||
}
|
||||
|
||||
// WithXHeaders specifies list of extended headers (string key-value pairs)
|
||||
// to be attached to the request. Must have an even length.
|
||||
//
|
||||
// Slice must not be mutated until the operation completes.
|
||||
func (x *PrmObjectSearch) WithXHeaders(hs ...string) {
|
||||
writeXHeadersToMeta(hs, &x.meta)
|
||||
}
|
||||
|
||||
// UseKey specifies private key to sign the requests.
|
||||
// If key is not provided, then Client default key is used.
|
||||
func (x *PrmObjectSearch) UseKey(key ecdsa.PrivateKey) {
|
||||
x.key = &key
|
||||
}
|
||||
|
||||
// InContainer specifies the container in which to look for objects.
|
||||
// Required parameter.
|
||||
func (x *PrmObjectSearch) InContainer(id cid.ID) {
|
||||
x.cnr = id
|
||||
x.cnrID = id
|
||||
x.cnrSet = true
|
||||
}
|
||||
|
||||
|
@ -82,28 +94,20 @@ type ResObjectSearch struct {
|
|||
statusRes
|
||||
}
|
||||
|
||||
// ObjectListReader is designed to read list of object identifiers from NeoFS system.
|
||||
// ObjectListReader is designed to read list of object identifiers from FrostFS system.
|
||||
//
|
||||
// Must be initialized using Client.ObjectSearch, any other usage is unsafe.
|
||||
type ObjectListReader struct {
|
||||
client *Client
|
||||
cancelCtxStream context.CancelFunc
|
||||
|
||||
ctxCall contextCall
|
||||
|
||||
reqWritten bool
|
||||
|
||||
// initially bound to contextCall
|
||||
bodyResp v2object.SearchResponseBody
|
||||
|
||||
err error
|
||||
res ResObjectSearch
|
||||
stream interface {
|
||||
Read(resp *v2object.SearchResponse) error
|
||||
}
|
||||
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.
|
||||
//
|
||||
|
@ -115,58 +119,33 @@ func (x *ObjectListReader) Read(buf []oid.ID) (int, bool) {
|
|||
panic("empty buffer in ObjectListReader.ReadList")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
read := copyIDBuffers(buf, x.tail)
|
||||
x.tail = x.tail[read:]
|
||||
|
||||
if len(buf) == read {
|
||||
return read, true
|
||||
}
|
||||
|
||||
var ok bool
|
||||
var ids []v2refs.ObjectID
|
||||
var i, ln, rem int
|
||||
|
||||
for {
|
||||
// receive next message
|
||||
ok = x.ctxCall.readResponse()
|
||||
if !ok {
|
||||
var resp v2object.SearchResponse
|
||||
x.err = x.stream.Read(&resp)
|
||||
if x.err != nil {
|
||||
return read, false
|
||||
}
|
||||
|
||||
x.res.st, x.err = x.client.processResponse(&resp)
|
||||
if x.err != nil || !apistatus.IsSuccessful(x.res.st) {
|
||||
return read, false
|
||||
}
|
||||
|
||||
// read new chunk of objects
|
||||
ids = x.bodyResp.GetIDList()
|
||||
|
||||
ln = len(ids)
|
||||
if ln == 0 {
|
||||
ids := resp.GetBody().GetIDList()
|
||||
if len(ids) == 0 {
|
||||
// just skip empty lists since they are not prohibited by protocol
|
||||
continue
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
ln := copyIDBuffers(buf[read:], ids)
|
||||
read += ln
|
||||
|
||||
if read == len(buf) {
|
||||
|
@ -178,6 +157,14 @@ 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.
|
||||
//
|
||||
|
@ -207,7 +194,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 NeoFS API statuses, then NeoFS failures
|
||||
// If Client is tuned to resolve FrostFS API statuses, then FrostFS failures
|
||||
// codes are returned as error.
|
||||
//
|
||||
// Return statuses:
|
||||
|
@ -218,89 +205,61 @@ func (x *ObjectListReader) Iterate(f func(oid.ID) bool) error {
|
|||
func (x *ObjectListReader) Close() (*ResObjectSearch, error) {
|
||||
defer x.cancelCtxStream()
|
||||
|
||||
if x.ctxCall.err != nil && !errors.Is(x.ctxCall.err, io.EOF) {
|
||||
return nil, x.ctxCall.err
|
||||
if x.err != nil && !errors.Is(x.err, io.EOF) {
|
||||
return nil, x.err
|
||||
}
|
||||
|
||||
return x.ctxCall.statusRes.(*ResObjectSearch), nil
|
||||
return &x.res, nil
|
||||
}
|
||||
|
||||
// ObjectSearchInit initiates object selection through a remote server using NeoFS API protocol.
|
||||
// ObjectSearchInit initiates object selection through a remote server using FrostFS 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.
|
||||
//
|
||||
// Immediately panics if parameters are set incorrectly (see PrmObjectSearch docs).
|
||||
// Returns an error 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) {
|
||||
// check parameters
|
||||
switch {
|
||||
case ctx == nil:
|
||||
panic(panicMsgMissingContext)
|
||||
return nil, errorMissingContext
|
||||
case !prm.cnrSet:
|
||||
panic(panicMsgMissingContainer)
|
||||
return nil, errorMissingContainer
|
||||
}
|
||||
|
||||
// form request body
|
||||
var body v2object.SearchRequestBody
|
||||
var cidV2 v2refs.ContainerID
|
||||
prm.cnrID.WriteToV2(&cidV2)
|
||||
|
||||
var body v2object.SearchRequestBody
|
||||
body.SetVersion(1)
|
||||
body.SetContainerID(prm.cnr.ToV2())
|
||||
body.SetContainerID(&cidV2)
|
||||
body.SetFilters(prm.filters.ToV2())
|
||||
|
||||
// form meta header
|
||||
var meta v2session.RequestMetaHeader
|
||||
|
||||
if prm.local {
|
||||
meta.SetTTL(1)
|
||||
}
|
||||
|
||||
if prm.bearerSet {
|
||||
meta.SetBearerToken(prm.bearer.ToV2())
|
||||
}
|
||||
|
||||
if prm.sessionSet {
|
||||
meta.SetSessionToken(prm.session.ToV2())
|
||||
}
|
||||
|
||||
prm.prmCommonMeta.writeToMetaHeader(&meta)
|
||||
|
||||
// form request
|
||||
var req v2object.SearchRequest
|
||||
|
||||
req.SetBody(&body)
|
||||
req.SetMetaHeader(&meta)
|
||||
|
||||
// init reader
|
||||
var (
|
||||
r ObjectListReader
|
||||
resp v2object.SearchResponse
|
||||
stream *rpcapi.SearchResponseReader
|
||||
)
|
||||
var req v2object.SearchRequest
|
||||
req.SetBody(&body)
|
||||
c.prepareRequest(&req, &prm.meta)
|
||||
|
||||
key := prm.key
|
||||
if key == nil {
|
||||
key = &c.prm.key
|
||||
}
|
||||
|
||||
err := signature.SignServiceMessage(key, &req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sign request: %w", err)
|
||||
}
|
||||
|
||||
var r ObjectListReader
|
||||
ctx, r.cancelCtxStream = context.WithCancel(ctx)
|
||||
|
||||
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.stream, err = rpcapi.SearchObjects(&c.c, &req, client.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open stream: %w", err)
|
||||
}
|
||||
r.client = c
|
||||
|
||||
return &r, nil
|
||||
}
|
||||
|
|
|
@ -1,26 +1,28 @@
|
|||
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()
|
||||
}
|
||||
|
||||
resp, setID := testListReaderResponse(t)
|
||||
p, resp := testListReaderResponse(t)
|
||||
|
||||
buf := make([]oid.ID, 2)
|
||||
checkRead := func(t *testing.T, expected []oid.ID) {
|
||||
|
@ -34,38 +36,23 @@ func TestObjectSearch(t *testing.T) {
|
|||
require.Panics(t, func() { resp.Read(nil) })
|
||||
|
||||
// both ID fetched
|
||||
setID(ids[:3])
|
||||
resp.stream = newSearchStream(p, nil, ids[:3])
|
||||
checkRead(t, ids[:2])
|
||||
|
||||
// one ID cached, second fetched
|
||||
setID(ids[3:6])
|
||||
resp.stream = newSearchStream(p, nil, ids[3:6])
|
||||
checkRead(t, ids[2:4])
|
||||
|
||||
// both ID cached
|
||||
resp.ctxCall.resp = nil
|
||||
resp.stream = nil // shouldn't be called, panic if so
|
||||
checkRead(t, ids[4:6])
|
||||
|
||||
// both ID fetched in 2 requests, with empty one in the middle
|
||||
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
|
||||
}
|
||||
resp.stream = newSearchStream(p, nil, ids[6:7], nil, ids[7:8])
|
||||
checkRead(t, ids[6:8])
|
||||
|
||||
// read from tail multiple times
|
||||
resp.ctxCall.rResp = nil
|
||||
setID(ids[8:11])
|
||||
resp.stream = newSearchStream(p, nil, ids[8:11])
|
||||
buf = buf[:1]
|
||||
checkRead(t, ids[8:9])
|
||||
checkRead(t, ids[9:10])
|
||||
|
@ -73,43 +60,20 @@ func TestObjectSearch(t *testing.T) {
|
|||
|
||||
// handle EOF
|
||||
buf = buf[:2]
|
||||
n = 0
|
||||
resp.ctxCall.rResp = func() error {
|
||||
if n > 0 {
|
||||
return io.EOF
|
||||
}
|
||||
n++
|
||||
setID(ids[11:12])
|
||||
return nil
|
||||
}
|
||||
resp.stream = newSearchStream(p, io.EOF, ids[11:12])
|
||||
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) {
|
||||
resp, setID := testListReaderResponse(t)
|
||||
p, resp := testListReaderResponse(t)
|
||||
|
||||
// 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
|
||||
}
|
||||
resp.stream = newSearchStream(p, io.EOF, ids[0:2], nil, ids[2:3])
|
||||
|
||||
var actual []oid.ID
|
||||
require.NoError(t, resp.Iterate(func(id oid.ID) bool {
|
||||
|
@ -119,10 +83,10 @@ func TestObjectIterate(t *testing.T) {
|
|||
require.Equal(t, ids[:3], actual)
|
||||
})
|
||||
t.Run("stop by return value", func(t *testing.T) {
|
||||
resp, setID := testListReaderResponse(t)
|
||||
p, resp := testListReaderResponse(t)
|
||||
|
||||
var actual []oid.ID
|
||||
setID(ids)
|
||||
resp.stream = &singleStreamResponder{key: p, idList: [][]oid.ID{ids}}
|
||||
require.NoError(t, resp.Iterate(func(id oid.ID) bool {
|
||||
actual = append(actual, id)
|
||||
return len(actual) == 2
|
||||
|
@ -130,22 +94,12 @@ func TestObjectIterate(t *testing.T) {
|
|||
require.Equal(t, ids[:2], actual)
|
||||
})
|
||||
t.Run("stop after error", func(t *testing.T) {
|
||||
resp, setID := testListReaderResponse(t)
|
||||
p, resp := testListReaderResponse(t)
|
||||
expectedErr := errors.New("test error")
|
||||
|
||||
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
|
||||
}
|
||||
resp.stream = newSearchStream(p, expectedErr, ids[:2])
|
||||
|
||||
var actual []oid.ID
|
||||
err := resp.Iterate(func(id oid.ID) bool {
|
||||
actual = append(actual, id)
|
||||
return false
|
||||
|
@ -155,37 +109,56 @@ func TestObjectIterate(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func testListReaderResponse(t *testing.T) (*ObjectListReader, func(id []oid.ID) *object.SearchResponse) {
|
||||
func testListReaderResponse(t *testing.T) (*ecdsa.PrivateKey, *ObjectListReader) {
|
||||
p, err := keys.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
|
||||
obj := &ObjectListReader{
|
||||
return &p.PrivateKey, &ObjectListReader{
|
||||
cancelCtxStream: func() {},
|
||||
ctxCall: contextCall{
|
||||
closer: func() error { return nil },
|
||||
result: func(v2 responseV2) {},
|
||||
statusRes: new(ResObjectSearch),
|
||||
},
|
||||
reqWritten: true,
|
||||
bodyResp: object.SearchResponseBody{},
|
||||
tail: nil,
|
||||
}
|
||||
|
||||
return obj, func(id []oid.ID) *object.SearchResponse {
|
||||
resp := new(object.SearchResponse)
|
||||
resp.SetBody(new(object.SearchResponseBody))
|
||||
|
||||
v2id := make([]refs.ObjectID, len(id))
|
||||
for i := range id {
|
||||
v2id[i] = *id[i].ToV2()
|
||||
}
|
||||
resp.GetBody().SetIDList(v2id)
|
||||
err := signatureV2.SignServiceMessage(&p.PrivateKey, resp)
|
||||
if err != nil {
|
||||
t.Fatalf("error: %v", err)
|
||||
}
|
||||
obj.ctxCall.resp = resp
|
||||
obj.bodyResp = *resp.GetBody()
|
||||
return resp
|
||||
client: &Client{},
|
||||
tail: nil,
|
||||
}
|
||||
}
|
||||
|
||||
func newSearchStream(key *ecdsa.PrivateKey, endError error, idList ...[]oid.ID) *singleStreamResponder {
|
||||
return &singleStreamResponder{
|
||||
key: key,
|
||||
endError: endError,
|
||||
idList: idList,
|
||||
}
|
||||
}
|
||||
|
||||
type singleStreamResponder struct {
|
||||
key *ecdsa.PrivateKey
|
||||
n int
|
||||
endError error
|
||||
idList [][]oid.ID
|
||||
}
|
||||
|
||||
func (s *singleStreamResponder) Read(resp *v2object.SearchResponse) error {
|
||||
if s.n >= len(s.idList) {
|
||||
if s.endError != nil {
|
||||
return s.endError
|
||||
}
|
||||
panic("unexpected call to `Read`")
|
||||
}
|
||||
|
||||
var body v2object.SearchResponseBody
|
||||
|
||||
if s.idList[s.n] != nil {
|
||||
ids := make([]refs.ObjectID, len(s.idList[s.n]))
|
||||
for i := range s.idList[s.n] {
|
||||
s.idList[s.n][i].WriteToV2(&ids[i])
|
||||
}
|
||||
body.SetIDList(ids)
|
||||
}
|
||||
resp.SetBody(&body)
|
||||
|
||||
err := signatureV2.SignServiceMessage(s.key, resp)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("error: %w", err))
|
||||
}
|
||||
|
||||
s.n++
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -3,10 +3,10 @@ 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"
|
||||
v2reputation "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/reputation"
|
||||
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/reputation"
|
||||
)
|
||||
|
||||
// PrmAnnounceLocalTrust groups parameters of AnnounceLocalTrust operation.
|
||||
|
@ -18,13 +18,13 @@ type PrmAnnounceLocalTrust struct {
|
|||
trusts []reputation.Trust
|
||||
}
|
||||
|
||||
// SetEpoch sets number of NeoFS epoch in which the trust was assessed.
|
||||
// SetEpoch sets number of FrostFS 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.
|
||||
// SetValues sets values describing trust of the client to the FrostFS network participants.
|
||||
// Required parameter. Must not be empty.
|
||||
//
|
||||
// Must not be mutated before the end of the operation.
|
||||
|
@ -37,38 +37,41 @@ type ResAnnounceLocalTrust struct {
|
|||
statusRes
|
||||
}
|
||||
|
||||
// AnnounceLocalTrust sends client's trust values to the NeoFS network participants.
|
||||
// AnnounceLocalTrust sends client's trust values to the FrostFS 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
|
||||
// If PrmInit.ResolveFrostFSFailures has been called, unsuccessful
|
||||
// FrostFS status codes are returned as `error`, otherwise, are included
|
||||
// in the returned result structure.
|
||||
//
|
||||
// Immediately panics if parameters are set incorrectly (see PrmAnnounceLocalTrust docs).
|
||||
// Returns an error 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).
|
||||
// - global (see Client docs).
|
||||
func (c *Client) AnnounceLocalTrust(ctx context.Context, prm PrmAnnounceLocalTrust) (*ResAnnounceLocalTrust, error) {
|
||||
// check parameters
|
||||
switch {
|
||||
case ctx == nil:
|
||||
panic(panicMsgMissingContext)
|
||||
return nil, errorMissingContext
|
||||
case prm.epoch == 0:
|
||||
panic("zero epoch")
|
||||
return nil, errorZeroEpoch
|
||||
case len(prm.trusts) == 0:
|
||||
panic("missing trusts")
|
||||
return nil, errorMissingTrusts
|
||||
}
|
||||
|
||||
// form request body
|
||||
reqBody := new(v2reputation.AnnounceLocalTrustRequestBody)
|
||||
reqBody.SetEpoch(prm.epoch)
|
||||
|
||||
trusts := make([]reputation.Trust, len(prm.trusts))
|
||||
copy(trusts, prm.trusts)
|
||||
trusts := make([]v2reputation.Trust, len(prm.trusts))
|
||||
|
||||
reqBody.SetTrusts(reputation.TrustsToV2(trusts))
|
||||
for i := range prm.trusts {
|
||||
prm.trusts[i].WriteToV2(&trusts[i])
|
||||
}
|
||||
|
||||
reqBody.SetTrusts(trusts)
|
||||
|
||||
// form request
|
||||
var req v2reputation.AnnounceLocalTrustRequest
|
||||
|
@ -110,7 +113,7 @@ type PrmAnnounceIntermediateTrust struct {
|
|||
trust reputation.PeerToPeerTrust
|
||||
}
|
||||
|
||||
// SetEpoch sets number of NeoFS epoch with which client's calculation algorithm is initialized.
|
||||
// SetEpoch sets number of FrostFS epoch with which client's calculation algorithm is initialized.
|
||||
// Required parameter, must not be zero.
|
||||
func (x *PrmAnnounceIntermediateTrust) SetEpoch(epoch uint64) {
|
||||
x.epoch = epoch
|
||||
|
@ -134,36 +137,39 @@ type ResAnnounceIntermediateTrust struct {
|
|||
statusRes
|
||||
}
|
||||
|
||||
// AnnounceIntermediateTrust sends global trust values calculated for the specified NeoFS network participants
|
||||
// AnnounceIntermediateTrust sends global trust values calculated for the specified FrostFS 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
|
||||
// If PrmInit.ResolveFrostFSFailures has been called, unsuccessful
|
||||
// FrostFS status codes are returned as `error`, otherwise, are included
|
||||
// in the returned result structure.
|
||||
//
|
||||
// Immediately panics if parameters are set incorrectly (see PrmAnnounceIntermediateTrust docs).
|
||||
// Returns an error 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).
|
||||
// - global (see Client docs).
|
||||
func (c *Client) AnnounceIntermediateTrust(ctx context.Context, prm PrmAnnounceIntermediateTrust) (*ResAnnounceIntermediateTrust, error) {
|
||||
// check parameters
|
||||
switch {
|
||||
case ctx == nil:
|
||||
panic(panicMsgMissingContext)
|
||||
return nil, errorMissingContext
|
||||
case prm.epoch == 0:
|
||||
panic("zero epoch")
|
||||
return nil, errorZeroEpoch
|
||||
case !prm.trustSet:
|
||||
panic("current trust value not set")
|
||||
return nil, errorTrustNotSet
|
||||
}
|
||||
|
||||
var trust v2reputation.PeerToPeerTrust
|
||||
prm.trust.WriteToV2(&trust)
|
||||
|
||||
// form request body
|
||||
reqBody := new(v2reputation.AnnounceIntermediateResultRequestBody)
|
||||
reqBody.SetEpoch(prm.epoch)
|
||||
reqBody.SetIteration(prm.iter)
|
||||
reqBody.SetTrust(prm.trust.ToV2())
|
||||
reqBody.SetTrust(&trust)
|
||||
|
||||
// form request
|
||||
var req v2reputation.AnnounceIntermediateResultRequest
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
package client
|
||||
|
||||
import "github.com/nspcc-dev/neofs-api-go/v2/session"
|
||||
import "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
||||
|
||||
// ResponseMetaInfo groups meta information about any NeoFS API response.
|
||||
// ResponseMetaInfo groups meta information about any FrostFS API response.
|
||||
type ResponseMetaInfo struct {
|
||||
key []byte
|
||||
|
||||
epoch uint64
|
||||
}
|
||||
|
||||
type responseV2 interface {
|
||||
|
@ -18,3 +20,8 @@ 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
|
||||
}
|
||||
|
|
|
@ -2,11 +2,13 @@ package client
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
|
||||
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"
|
||||
"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-sdk-go/user"
|
||||
)
|
||||
|
||||
// PrmSessionCreate groups parameters of SessionCreate operation.
|
||||
|
@ -14,6 +16,9 @@ type PrmSessionCreate struct {
|
|||
prmCommonMeta
|
||||
|
||||
exp uint64
|
||||
|
||||
keySet bool
|
||||
key ecdsa.PrivateKey
|
||||
}
|
||||
|
||||
// SetExp sets number of the last NepFS epoch in the lifetime of the session after which it will be expired.
|
||||
|
@ -21,6 +26,13 @@ func (x *PrmSessionCreate) SetExp(exp uint64) {
|
|||
x.exp = exp
|
||||
}
|
||||
|
||||
// UseKey specifies private key to sign the requests and compute token owner.
|
||||
// If key is not provided, then Client default key is used.
|
||||
func (x *PrmSessionCreate) UseKey(key ecdsa.PrivateKey) {
|
||||
x.keySet = true
|
||||
x.key = key
|
||||
}
|
||||
|
||||
// ResSessionCreate groups resulting values of SessionCreate operation.
|
||||
type ResSessionCreate struct {
|
||||
statusRes
|
||||
|
@ -34,7 +46,7 @@ func (x *ResSessionCreate) setID(id []byte) {
|
|||
x.id = id
|
||||
}
|
||||
|
||||
// ID returns identifier of the opened session in a binary NeoFS API protocol format.
|
||||
// ID returns identifier of the opened session in a binary FrostFS API protocol format.
|
||||
//
|
||||
// Client doesn't retain value so modification is safe.
|
||||
func (x ResSessionCreate) ID() []byte {
|
||||
|
@ -45,7 +57,7 @@ func (x *ResSessionCreate) setSessionKey(key []byte) {
|
|||
x.sessionKey = key
|
||||
}
|
||||
|
||||
// PublicKey returns public key of the opened session in a binary NeoFS API protocol format.
|
||||
// PublicKey returns public key of the opened session in a binary FrostFS API protocol format.
|
||||
func (x ResSessionCreate) PublicKey() []byte {
|
||||
return x.sessionKey
|
||||
}
|
||||
|
@ -56,26 +68,34 @@ 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 WithNeoFSErrorParsing option has been provided, unsuccessful
|
||||
// NeoFS status codes are returned as `error`, otherwise, are included
|
||||
// If PrmInit.ResolveFrostFSFailures has been called, unsuccessful
|
||||
// FrostFS status codes are returned as `error`, otherwise, are included
|
||||
// in the returned result structure.
|
||||
//
|
||||
// Immediately panics if parameters are set incorrectly (see PrmSessionCreate docs).
|
||||
// Returns an error 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) {
|
||||
// check context
|
||||
if ctx == nil {
|
||||
panic(panicMsgMissingContext)
|
||||
return nil, errorMissingContext
|
||||
}
|
||||
|
||||
ownerID := owner.NewIDFromPublicKey(&c.prm.key.PublicKey)
|
||||
ownerKey := c.prm.key.PublicKey
|
||||
if prm.keySet {
|
||||
ownerKey = prm.key.PublicKey
|
||||
}
|
||||
var ownerID user.ID
|
||||
user.IDFromKey(&ownerID, ownerKey)
|
||||
|
||||
var ownerIDV2 refs.OwnerID
|
||||
ownerID.WriteToV2(&ownerIDV2)
|
||||
|
||||
// form request body
|
||||
reqBody := new(v2session.CreateRequestBody)
|
||||
reqBody.SetOwnerID(ownerID.ToV2())
|
||||
reqBody.SetOwnerID(&ownerIDV2)
|
||||
reqBody.SetExpiration(prm.exp)
|
||||
|
||||
// for request
|
||||
|
@ -91,6 +111,10 @@ func (c *Client) SessionCreate(ctx context.Context, prm PrmSessionCreate) (*ResS
|
|||
)
|
||||
|
||||
c.initCallContext(&cc)
|
||||
if prm.keySet {
|
||||
cc.key = prm.key
|
||||
}
|
||||
|
||||
cc.meta = prm.prmCommonMeta
|
||||
cc.req = &req
|
||||
cc.statusRes = &res
|
||||
|
|
|
@ -3,7 +3,7 @@ package apistatus
|
|||
import (
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/nspcc-dev/neofs-api-go/v2/status"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status"
|
||||
)
|
||||
|
||||
// ServerInternal describes failure statuses related to internal server errors.
|
||||
|
@ -29,9 +29,9 @@ func (x *ServerInternal) fromStatusV2(st *status.Status) {
|
|||
// ToStatusV2 implements StatusV2 interface method.
|
||||
// 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
|
||||
|
@ -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,3 +123,118 @@ 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()
|
||||
}
|
||||
|
|
|
@ -3,8 +3,8 @@ package apistatus_test
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/nspcc-dev/neofs-api-go/v2/status"
|
||||
apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status"
|
||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
@ -50,3 +50,81 @@ 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())
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package apistatus
|
||||
|
||||
import (
|
||||
"github.com/nspcc-dev/neofs-api-go/v2/container"
|
||||
"github.com/nspcc-dev/neofs-api-go/v2/status"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status"
|
||||
)
|
||||
|
||||
// ContainerNotFound describes status of the failure because of the missing container.
|
||||
|
@ -11,10 +11,17 @@ type ContainerNotFound struct {
|
|||
v2 status.Status
|
||||
}
|
||||
|
||||
const defaultContainerNotFoundMsg = "container not found"
|
||||
|
||||
func (x ContainerNotFound) Error() string {
|
||||
msg := x.v2.Message()
|
||||
if msg == "" {
|
||||
msg = defaultContainerNotFoundMsg
|
||||
}
|
||||
|
||||
return errMessageStatusV2(
|
||||
globalizeCodeV2(container.StatusNotFound, container.GlobalizeFail),
|
||||
x.v2.Message(),
|
||||
msg,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -26,11 +33,49 @@ 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("container not found")
|
||||
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)
|
||||
return &x.v2
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package apistatus
|
||||
|
||||
import (
|
||||
"github.com/nspcc-dev/neofs-api-go/v2/object"
|
||||
"github.com/nspcc-dev/neofs-api-go/v2/status"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status"
|
||||
)
|
||||
|
||||
// ObjectLocked describes status of the failure because of the locked object.
|
||||
|
@ -11,10 +11,17 @@ type ObjectLocked struct {
|
|||
v2 status.Status
|
||||
}
|
||||
|
||||
const defaultObjectLockedMsg = "object is locked"
|
||||
|
||||
func (x ObjectLocked) Error() string {
|
||||
msg := x.v2.Message()
|
||||
if msg == "" {
|
||||
msg = defaultObjectLockedMsg
|
||||
}
|
||||
|
||||
return errMessageStatusV2(
|
||||
globalizeCodeV2(object.StatusLocked, object.GlobalizeFail),
|
||||
x.v2.Message(),
|
||||
msg,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -26,12 +33,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("object is locked")
|
||||
x.v2.SetMessage(defaultObjectLockedMsg)
|
||||
return &x.v2
|
||||
}
|
||||
|
||||
|
@ -41,10 +48,17 @@ 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
|
||||
}
|
||||
|
||||
return errMessageStatusV2(
|
||||
globalizeCodeV2(object.StatusLockNonRegularObject, object.GlobalizeFail),
|
||||
x.v2.Message(),
|
||||
msg,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -56,12 +70,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("locking non-regular object is forbidden")
|
||||
x.v2.SetMessage(defaultLockNonRegularObjectMsg)
|
||||
return &x.v2
|
||||
}
|
||||
|
||||
|
@ -71,10 +85,17 @@ 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
|
||||
}
|
||||
|
||||
return errMessageStatusV2(
|
||||
globalizeCodeV2(object.StatusAccessDenied, object.GlobalizeFail),
|
||||
x.v2.Message(),
|
||||
msg,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -86,12 +107,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("access to object operation denied")
|
||||
x.v2.SetMessage(defaultObjectAccessDeniedMsg)
|
||||
return &x.v2
|
||||
}
|
||||
|
||||
|
@ -112,10 +133,17 @@ type ObjectNotFound struct {
|
|||
v2 status.Status
|
||||
}
|
||||
|
||||
const defaultObjectNotFoundMsg = "object not found"
|
||||
|
||||
func (x ObjectNotFound) Error() string {
|
||||
msg := x.v2.Message()
|
||||
if msg == "" {
|
||||
msg = defaultObjectNotFoundMsg
|
||||
}
|
||||
|
||||
return errMessageStatusV2(
|
||||
globalizeCodeV2(object.StatusNotFound, object.GlobalizeFail),
|
||||
x.v2.Message(),
|
||||
msg,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -127,12 +155,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("object not found")
|
||||
x.v2.SetMessage(defaultObjectNotFoundMsg)
|
||||
return &x.v2
|
||||
}
|
||||
|
||||
|
@ -142,10 +170,17 @@ type ObjectAlreadyRemoved struct {
|
|||
v2 status.Status
|
||||
}
|
||||
|
||||
const defaultObjectAlreadyRemovedMsg = "object already removed"
|
||||
|
||||
func (x ObjectAlreadyRemoved) Error() string {
|
||||
msg := x.v2.Message()
|
||||
if msg == "" {
|
||||
msg = defaultObjectAlreadyRemovedMsg
|
||||
}
|
||||
|
||||
return errMessageStatusV2(
|
||||
globalizeCodeV2(object.StatusAlreadyRemoved, object.GlobalizeFail),
|
||||
x.v2.Message(),
|
||||
msg,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -157,11 +192,49 @@ 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("object already removed")
|
||||
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)
|
||||
return &x.v2
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ package apistatus_test
|
|||
import (
|
||||
"testing"
|
||||
|
||||
apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
|
||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package apistatus
|
||||
|
||||
import (
|
||||
"github.com/nspcc-dev/neofs-api-go/v2/session"
|
||||
"github.com/nspcc-dev/neofs-api-go/v2/status"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status"
|
||||
)
|
||||
|
||||
// SessionTokenNotFound describes status of the failure because of the missing session token.
|
||||
|
@ -11,10 +11,17 @@ 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
|
||||
}
|
||||
|
||||
return errMessageStatusV2(
|
||||
globalizeCodeV2(session.StatusTokenNotFound, session.GlobalizeFail),
|
||||
x.v2.Message(),
|
||||
msg,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -26,12 +33,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("session token not found")
|
||||
x.v2.SetMessage(defaultSessionTokenNotFoundMsg)
|
||||
return &x.v2
|
||||
}
|
||||
|
||||
|
@ -41,10 +48,17 @@ type SessionTokenExpired struct {
|
|||
v2 status.Status
|
||||
}
|
||||
|
||||
const defaultSessionTokenExpiredMsg = "expired session token"
|
||||
|
||||
func (x SessionTokenExpired) Error() string {
|
||||
msg := x.v2.Message()
|
||||
if msg == "" {
|
||||
msg = defaultSessionTokenExpiredMsg
|
||||
}
|
||||
|
||||
return errMessageStatusV2(
|
||||
globalizeCodeV2(session.StatusTokenExpired, session.GlobalizeFail),
|
||||
x.v2.Message(),
|
||||
msg,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -56,11 +70,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("expired session token")
|
||||
x.v2.SetMessage(defaultSessionTokenExpiredMsg)
|
||||
return &x.v2
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
package apistatus
|
||||
|
||||
// Status defines a variety of NeoFS API status returns.
|
||||
// Status defines a variety of FrostFS 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,7 +14,7 @@ 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 NeoFS API V2 protocol, see StatusV2 interface and FromStatusV2 and ToStatusV2 functions.
|
||||
// To transport statuses using the FrostFS 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.
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"errors"
|
||||
"testing"
|
||||
|
||||
apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
|
||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package apistatus
|
||||
|
||||
import (
|
||||
"github.com/nspcc-dev/neofs-api-go/v2/status"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-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
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package apistatus
|
||||
|
||||
import (
|
||||
"github.com/nspcc-dev/neofs-api-go/v2/status"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status"
|
||||
)
|
||||
|
||||
type unrecognizedStatusV2 struct {
|
||||
|
|
|
@ -3,19 +3,19 @@ package apistatus
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"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"
|
||||
"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"
|
||||
)
|
||||
|
||||
// StatusV2 defines a variety of Status instances compatible with NeoFS API V2 protocol.
|
||||
// StatusV2 defines a variety of Status instances compatible with FrostFS API V2 protocol.
|
||||
//
|
||||
// Note: it is not recommended to use this type directly, it is intended for documentation of the library functionality.
|
||||
type StatusV2 interface {
|
||||
Status
|
||||
|
||||
// ToStatusV2 returns the status as github.com/nspcc-dev/neofs-api-go/v2/status.Status message structure.
|
||||
// ToStatusV2 returns the status as git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status.Status message structure.
|
||||
ToStatusV2() *status.Status
|
||||
}
|
||||
|
||||
|
@ -28,15 +28,16 @@ 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.Internal: *ServerInternal;
|
||||
// - status.SignatureVerificationFail: *SignatureVerification.
|
||||
//
|
||||
// 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)
|
||||
|
@ -55,6 +56,10 @@ 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 {
|
||||
|
@ -68,12 +73,16 @@ 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
|
||||
|
@ -97,7 +106,8 @@ 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.
|
||||
// Otherwise, successes are converted with status.OK code w/o details and message,
|
||||
// failures - with status.Internal and error text message w/o details.
|
||||
func ToStatusV2(st Status) *status.Status {
|
||||
if v, ok := st.(StatusV2); ok {
|
||||
return v.ToStatusV2()
|
||||
|
@ -107,7 +117,10 @@ func ToStatusV2(st Status) *status.Status {
|
|||
return newStatusV2WithLocalCode(status.OK, status.GlobalizeSuccess)
|
||||
}
|
||||
|
||||
return newStatusV2WithLocalCode(status.Internal, status.GlobalizeCommonFail)
|
||||
internalErrorStatus := newStatusV2WithLocalCode(status.Internal, status.GlobalizeCommonFail)
|
||||
internalErrorStatus.SetMessage(st.(error).Error()) // type cast never panics because IsSuccessful() checks cast
|
||||
|
||||
return internalErrorStatus
|
||||
}
|
||||
|
||||
func errMessageStatusV2(code interface{}, msg string) string {
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"errors"
|
||||
"testing"
|
||||
|
||||
apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
|
||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
@ -12,12 +12,14 @@ func TestToStatusV2(t *testing.T) {
|
|||
type statusConstructor func() apistatus.Status
|
||||
|
||||
for _, testItem := range [...]struct {
|
||||
status interface{} // Status or statusConstructor
|
||||
codeV2 uint64
|
||||
status interface{} // Status or statusConstructor
|
||||
codeV2 uint64
|
||||
messageV2 string
|
||||
}{
|
||||
{
|
||||
status: errors.New("some error"),
|
||||
codeV2: 1024,
|
||||
status: errors.New("some error"),
|
||||
codeV2: 1024,
|
||||
messageV2: "some error",
|
||||
},
|
||||
{
|
||||
status: 1,
|
||||
|
@ -93,12 +95,24 @@ 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)
|
||||
|
@ -111,6 +125,12 @@ func TestToStatusV2(t *testing.T) {
|
|||
}),
|
||||
codeV2: 4097,
|
||||
},
|
||||
{
|
||||
status: (statusConstructor)(func() apistatus.Status {
|
||||
return new(apistatus.NodeUnderMaintenance)
|
||||
}),
|
||||
codeV2: 1027,
|
||||
},
|
||||
} {
|
||||
var st apistatus.Status
|
||||
|
||||
|
@ -124,6 +144,9 @@ 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 {
|
||||
|
@ -142,12 +165,14 @@ func TestFromStatusV2(t *testing.T) {
|
|||
type statusConstructor func() apistatus.Status
|
||||
|
||||
for _, testItem := range [...]struct {
|
||||
status interface{} // Status or statusConstructor
|
||||
codeV2 uint64
|
||||
status interface{} // Status or statusConstructor
|
||||
codeV2 uint64
|
||||
messageV2 string
|
||||
}{
|
||||
{
|
||||
status: errors.New("some error"),
|
||||
codeV2: 1024,
|
||||
status: errors.New("some error"),
|
||||
codeV2: 1024,
|
||||
messageV2: "some error",
|
||||
},
|
||||
{
|
||||
status: 1,
|
||||
|
@ -223,12 +248,24 @@ 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)
|
||||
|
@ -241,6 +278,12 @@ func TestFromStatusV2(t *testing.T) {
|
|||
}),
|
||||
codeV2: 4097,
|
||||
},
|
||||
{
|
||||
status: (statusConstructor)(func() apistatus.Status {
|
||||
return new(apistatus.NodeUnderMaintenance)
|
||||
}),
|
||||
codeV2: 1027,
|
||||
},
|
||||
} {
|
||||
var st apistatus.Status
|
||||
|
||||
|
@ -254,6 +297,9 @@ 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 {
|
||||
|
|
81
container/acl/acl.go
Normal file
81
container/acl/acl.go
Normal file
|
@ -0,0 +1,81 @@
|
|||
package acl
|
||||
|
||||
import "strconv"
|
||||
|
||||
// Op enumerates operations under access control inside container.
|
||||
// Non-positive values are reserved and depend on context (e.g. unsupported op).
|
||||
//
|
||||
// Note that type conversion from- and to numerical types is not recommended,
|
||||
// use corresponding constants and/or methods instead.
|
||||
type Op uint32
|
||||
|
||||
const (
|
||||
opZero Op = iota // extreme value for testing
|
||||
|
||||
OpObjectGet // Object.Get rpc
|
||||
OpObjectHead // Object.Head rpc
|
||||
OpObjectPut // Object.Put rpc
|
||||
OpObjectDelete // Object.Delete rpc
|
||||
OpObjectSearch // Object.Search rpc
|
||||
OpObjectRange // Object.GetRange rpc
|
||||
OpObjectHash // Object.GetRangeHash rpc
|
||||
|
||||
opLast // extreme value for testing
|
||||
)
|
||||
|
||||
// String implements fmt.Stringer.
|
||||
func (x Op) String() string {
|
||||
switch x {
|
||||
default:
|
||||
return "UNKNOWN#" + strconv.FormatUint(uint64(x), 10)
|
||||
case OpObjectGet:
|
||||
return "OBJECT_GET"
|
||||
case OpObjectHead:
|
||||
return "OBJECT_HEAD"
|
||||
case OpObjectPut:
|
||||
return "OBJECT_PUT"
|
||||
case OpObjectDelete:
|
||||
return "OBJECT_DELETE"
|
||||
case OpObjectSearch:
|
||||
return "OBJECT_SEARCH"
|
||||
case OpObjectRange:
|
||||
return "OBJECT_RANGE"
|
||||
case OpObjectHash:
|
||||
return "OBJECT_HASH"
|
||||
}
|
||||
}
|
||||
|
||||
// Role enumerates roles covered by container ACL. Each role represents
|
||||
// some party which can be authenticated during container op execution.
|
||||
// Non-positive values are reserved and depend on context (e.g. unsupported role).
|
||||
//
|
||||
// Note that type conversion from- and to numerical types is not recommended,
|
||||
// use corresponding constants and/or methods instead.
|
||||
type Role uint32
|
||||
|
||||
const (
|
||||
roleZero Role = iota // extreme value for testing
|
||||
|
||||
RoleOwner // container owner
|
||||
RoleContainer // nodes of the related container
|
||||
RoleInnerRing // Inner Ring nodes
|
||||
RoleOthers // all others
|
||||
|
||||
roleLast // extreme value for testing
|
||||
)
|
||||
|
||||
// String implements fmt.Stringer.
|
||||
func (x Role) String() string {
|
||||
switch x {
|
||||
default:
|
||||
return "UNKNOWN#" + strconv.FormatUint(uint64(x), 10)
|
||||
case RoleOwner:
|
||||
return "OWNER"
|
||||
case RoleContainer:
|
||||
return "CONTAINER"
|
||||
case RoleInnerRing:
|
||||
return "INNER_RING"
|
||||
case RoleOthers:
|
||||
return "OTHERS"
|
||||
}
|
||||
}
|
283
container/acl/acl_basic.go
Normal file
283
container/acl/acl_basic.go
Normal file
|
@ -0,0 +1,283 @@
|
|||
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
|
||||
}
|
384
container/acl/acl_basic_test.go
Normal file
384
container/acl/acl_basic_test.go
Normal file
|
@ -0,0 +1,384 @@
|
|||
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)
|
||||
})
|
||||
}
|
8
container/acl/doc.go
Normal file
8
container/acl/doc.go
Normal file
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
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
|
20
container/acl/init.go
Normal file
20
container/acl/init.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
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)
|
||||
}
|
||||
}
|
45
container/acl/util.go
Normal file
45
container/acl/util.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
package acl
|
||||
|
||||
import "fmt"
|
||||
|
||||
// sets n-th bit in num (starting at 0).
|
||||
func setBit(num *uint32, n uint8) {
|
||||
*num |= 1 << n
|
||||
}
|
||||
|
||||
// resets n-th bit in num (starting at 0).
|
||||
func resetBit(num *uint32, n uint8) {
|
||||
var mask uint32
|
||||
setBit(&mask, n)
|
||||
|
||||
*num &= ^mask
|
||||
}
|
||||
|
||||
// checks if n-th bit in num is set (starting at 0).
|
||||
func isBitSet(num uint32, n uint8) bool {
|
||||
mask := uint32(1 << n)
|
||||
return mask != 0 && num&mask == mask
|
||||
}
|
||||
|
||||
// maps Op to op-section index in Basic. Filled on init.
|
||||
var mOrder map[Op]uint8
|
||||
|
||||
// sets n-th bit in num for the given op. Panics if op is unsupported.
|
||||
func setOpBit(num *uint32, op Op, opBitPos uint8) {
|
||||
n, ok := mOrder[op]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("op is unsupported %v", op))
|
||||
}
|
||||
|
||||
setBit(num, n*bitsPerOp+opBitPos)
|
||||
}
|
||||
|
||||
// checks if n-th bit in num for the given op is set. Panics if op is unsupported.
|
||||
func isOpBitSet(num uint32, op Op, n uint8) bool {
|
||||
off, ok := mOrder[op]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("op is unsupported %v", op))
|
||||
}
|
||||
|
||||
return isBitSet(num, bitsPerOp*off+n)
|
||||
}
|
133
container/acl/util_test.go
Normal file
133
container/acl/util_test.go
Normal file
|
@ -0,0 +1,133 @@
|
|||
package acl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBits(t *testing.T) {
|
||||
num := uint32(0b10110)
|
||||
|
||||
require.False(t, isBitSet(num, 0))
|
||||
require.True(t, isBitSet(num, 1))
|
||||
require.True(t, isBitSet(num, 2))
|
||||
require.False(t, isBitSet(num, 3))
|
||||
require.True(t, isBitSet(num, 4))
|
||||
require.False(t, isBitSet(num, 5))
|
||||
|
||||
setBit(&num, 3)
|
||||
require.EqualValues(t, 0b11110, num)
|
||||
|
||||
setBit(&num, 6)
|
||||
require.EqualValues(t, 0b1011110, num)
|
||||
|
||||
resetBit(&num, 1)
|
||||
require.EqualValues(t, 0b1011100, num)
|
||||
}
|
||||
|
||||
func TestOpBits(t *testing.T) {
|
||||
num := uint32(0b_1001_0101_1100_0011_0110_0111_1000_1111)
|
||||
|
||||
require.Panics(t, func() { isOpBitSet(num, opZero, 0) })
|
||||
require.Panics(t, func() { isOpBitSet(num, opLast, 0) })
|
||||
|
||||
cpNum := num
|
||||
|
||||
require.Panics(t, func() { setOpBit(&num, opZero, 0) })
|
||||
require.EqualValues(t, cpNum, num)
|
||||
require.Panics(t, func() { setOpBit(&num, opLast, 0) })
|
||||
require.EqualValues(t, cpNum, num)
|
||||
|
||||
for _, tc := range []struct {
|
||||
op Op
|
||||
set [4]bool // is bit set (left-to-right)
|
||||
bits [4]uint32 // result of setting i-th bit (left-to-right) to zero num
|
||||
}{
|
||||
{
|
||||
op: OpObjectHash,
|
||||
set: [4]bool{false, true, false, true},
|
||||
bits: [4]uint32{
|
||||
0b_0000_1000_0000_0000_0000_0000_0000_0000,
|
||||
0b_0000_0100_0000_0000_0000_0000_0000_0000,
|
||||
0b_0000_0010_0000_0000_0000_0000_0000_0000,
|
||||
0b_0000_0001_0000_0000_0000_0000_0000_0000,
|
||||
},
|
||||
},
|
||||
{
|
||||
op: OpObjectRange,
|
||||
set: [4]bool{true, true, false, false},
|
||||
bits: [4]uint32{
|
||||
0b_0000_0000_1000_0000_0000_0000_0000_0000,
|
||||
0b_0000_0000_0100_0000_0000_0000_0000_0000,
|
||||
0b_0000_0000_0010_0000_0000_0000_0000_0000,
|
||||
0b_0000_0000_0001_0000_0000_0000_0000_0000,
|
||||
},
|
||||
},
|
||||
{
|
||||
op: OpObjectSearch,
|
||||
set: [4]bool{false, false, true, true},
|
||||
bits: [4]uint32{
|
||||
0b_0000_0000_0000_1000_0000_0000_0000_0000,
|
||||
0b_0000_0000_0000_0100_0000_0000_0000_0000,
|
||||
0b_0000_0000_0000_0010_0000_0000_0000_0000,
|
||||
0b_0000_0000_0000_0001_0000_0000_0000_0000,
|
||||
},
|
||||
},
|
||||
{
|
||||
op: OpObjectDelete,
|
||||
set: [4]bool{false, true, true, false},
|
||||
bits: [4]uint32{
|
||||
0b_0000_0000_0000_0000_1000_0000_0000_0000,
|
||||
0b_0000_0000_0000_0000_0100_0000_0000_0000,
|
||||
0b_0000_0000_0000_0000_0010_0000_0000_0000,
|
||||
0b_0000_0000_0000_0000_0001_0000_0000_0000,
|
||||
},
|
||||
},
|
||||
{
|
||||
op: OpObjectPut,
|
||||
set: [4]bool{false, true, true, true},
|
||||
bits: [4]uint32{
|
||||
0b_0000_0000_0000_0000_0000_1000_0000_0000,
|
||||
0b_0000_0000_0000_0000_0000_0100_0000_0000,
|
||||
0b_0000_0000_0000_0000_0000_0010_0000_0000,
|
||||
0b_0000_0000_0000_0000_0000_0001_0000_0000,
|
||||
},
|
||||
},
|
||||
{
|
||||
op: OpObjectHead,
|
||||
set: [4]bool{true, false, false, false},
|
||||
bits: [4]uint32{
|
||||
0b_0000_0000_0000_0000_0000_0000_1000_0000,
|
||||
0b_0000_0000_0000_0000_0000_0000_0100_0000,
|
||||
0b_0000_0000_0000_0000_0000_0000_0010_0000,
|
||||
0b_0000_0000_0000_0000_0000_0000_0001_0000,
|
||||
},
|
||||
},
|
||||
{
|
||||
op: OpObjectGet,
|
||||
set: [4]bool{true, true, true, true},
|
||||
bits: [4]uint32{
|
||||
0b_0000_0000_0000_0000_0000_0000_0000_1000,
|
||||
0b_0000_0000_0000_0000_0000_0000_0000_0100,
|
||||
0b_0000_0000_0000_0000_0000_0000_0000_0010,
|
||||
0b_0000_0000_0000_0000_0000_0000_0000_0001,
|
||||
},
|
||||
},
|
||||
} {
|
||||
for i := range tc.set {
|
||||
require.EqualValues(t, tc.set[i], isOpBitSet(num, tc.op, uint8(len(tc.set)-1-i)),
|
||||
fmt.Sprintf("op %s, left bit #%d", tc.op, i),
|
||||
)
|
||||
}
|
||||
|
||||
for i := range tc.bits {
|
||||
num := uint32(0)
|
||||
|
||||
setOpBit(&num, tc.op, uint8(len(tc.bits)-1-i))
|
||||
|
||||
require.EqualValues(t, tc.bits[i], num)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
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)
|
||||
}
|
|
@ -1,99 +0,0 @@
|
|||
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))
|
||||
})
|
||||
}
|
|
@ -1,139 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,152 +0,0 @@
|
|||
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)
|
||||
}
|
|
@ -1,193 +1,523 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
|
||||
v2netmap "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/netmap"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl"
|
||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto"
|
||||
frostfsecdsa "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto/ecdsa"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
||||
subnetid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/subnet/id"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/version"
|
||||
"github.com/google/uuid"
|
||||
"github.com/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
|
||||
}
|
||||
|
||||
// New creates, initializes and returns blank Container instance.
|
||||
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")
|
||||
}
|
||||
|
||||
attrs := m.GetAttributes()
|
||||
mAttr := make(map[string]struct{}, len(attrs))
|
||||
var key, val string
|
||||
var was bool
|
||||
|
||||
for i := range attrs {
|
||||
key = attrs[i].GetKey()
|
||||
if key == "" {
|
||||
return errors.New("empty attribute key")
|
||||
}
|
||||
|
||||
_, was = mAttr[key]
|
||||
if was {
|
||||
return fmt.Errorf("duplicated attribute %s", key)
|
||||
}
|
||||
|
||||
val = attrs[i].GetValue()
|
||||
if val == "" {
|
||||
return fmt.Errorf("empty attribute value %s", key)
|
||||
}
|
||||
|
||||
switch key {
|
||||
case container.SysAttributeSubnet:
|
||||
err = new(subnetid.ID).DecodeString(val)
|
||||
case attributeTimestamp:
|
||||
_, err = strconv.ParseInt(val, 10, 64)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid attribute value %s: %s (%w)", key, val, err)
|
||||
}
|
||||
|
||||
mAttr[key] = struct{}{}
|
||||
}
|
||||
|
||||
x.v2 = m
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadFromV2 reads Container from the container.Container message. Checks if the
|
||||
// message conforms to FrostFS API V2 protocol.
|
||||
//
|
||||
// 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
|
||||
// See also WriteToV2.
|
||||
func (x *Container) ReadFromV2(m container.Container) error {
|
||||
return x.readFromV2(m, true)
|
||||
}
|
||||
|
||||
// ToV2 returns the v2 Container message.
|
||||
// WriteToV2 writes Container into the container.Container message.
|
||||
// The message MUST NOT be nil.
|
||||
//
|
||||
// Nil Container converts to nil.
|
||||
func (c *Container) ToV2() *container.Container {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &c.v2
|
||||
// See also ReadFromV2.
|
||||
func (x Container) WriteToV2(m *container.Container) {
|
||||
*m = x.v2
|
||||
}
|
||||
|
||||
// NewVerifiedFromV2 constructs Container from NeoFS API V2 Container message.
|
||||
// Marshal encodes Container into a binary format of the FrostFS API protocol
|
||||
// (Protocol Buffers with direct field order).
|
||||
//
|
||||
// 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
|
||||
// See also Unmarshal.
|
||||
func (x Container) Marshal() []byte {
|
||||
return x.v2.StableMarshal(nil)
|
||||
}
|
||||
|
||||
// CalculateID calculates container identifier
|
||||
// based on its structure.
|
||||
func CalculateID(c *Container) *cid.ID {
|
||||
data, err := c.ToV2().StableMarshal(nil)
|
||||
// Unmarshal decodes FrostFS API protocol binary format into the Container
|
||||
// (Protocol Buffers with direct field order). Returns an error describing
|
||||
// a format violation.
|
||||
//
|
||||
// See also Marshal.
|
||||
func (x *Container) Unmarshal(data []byte) error {
|
||||
var m container.Container
|
||||
|
||||
err := m.Unmarshal(data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return err
|
||||
}
|
||||
|
||||
id := cid.New()
|
||||
id.SetSHA256(sha256.Sum256(data))
|
||||
|
||||
return id
|
||||
return x.readFromV2(m, false)
|
||||
}
|
||||
|
||||
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.
|
||||
// MarshalJSON encodes Container into a JSON format of the FrostFS API protocol
|
||||
// (Protocol Buffers JSON).
|
||||
//
|
||||
// Returns error if container nonce is not a valid UUID.
|
||||
func (c *Container) NonceUUID() (uuid.UUID, error) {
|
||||
return uuid.FromBytes(c.v2.GetNonce())
|
||||
// See also UnmarshalJSON.
|
||||
func (x Container) MarshalJSON() ([]byte, error) {
|
||||
return x.v2.MarshalJSON()
|
||||
}
|
||||
|
||||
// SetNonceUUID sets container nonce as UUID.
|
||||
func (c *Container) SetNonceUUID(v uuid.UUID) {
|
||||
data, _ := v.MarshalBinary()
|
||||
c.v2.SetNonce(data)
|
||||
// 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)
|
||||
}
|
||||
|
||||
func (c *Container) BasicACL() uint32 {
|
||||
return c.v2.GetBasicACL()
|
||||
// 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) SetBasicACL(v acl.BasicACL) {
|
||||
c.v2.SetBasicACL(uint32(v))
|
||||
// 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) Attributes() Attributes {
|
||||
return NewAttributesFromV2(c.v2.GetAttributes())
|
||||
// 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) SetAttributes(v Attributes) {
|
||||
c.v2.SetAttributes(v.ToV2())
|
||||
// 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) PlacementPolicy() *netmap.PlacementPolicy {
|
||||
return netmap.NewPlacementPolicyFromV2(c.v2.GetPlacementPolicy())
|
||||
// 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) SetPlacementPolicy(v *netmap.PlacementPolicy) {
|
||||
c.v2.SetPlacementPolicy(v.ToV2())
|
||||
// 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)
|
||||
}
|
||||
|
||||
// SessionToken returns token of the session within
|
||||
// which container was created.
|
||||
func (c Container) SessionToken() *session.Token {
|
||||
return c.token
|
||||
// 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
|
||||
}
|
||||
|
||||
// SetSessionToken sets token of the session within
|
||||
// which container was created.
|
||||
func (c *Container) SetSessionToken(t *session.Token) {
|
||||
c.token = t
|
||||
// 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.
|
||||
func (x *Container) SetAttribute(key, value string) {
|
||||
if key == "" {
|
||||
panic("empty attribute key")
|
||||
} else if value == "" {
|
||||
panic("empty attribute value")
|
||||
}
|
||||
|
||||
attrs := x.v2.GetAttributes()
|
||||
ln := len(attrs)
|
||||
|
||||
for i := 0; i < ln; i++ {
|
||||
if attrs[i].GetKey() == key {
|
||||
attrs[i].SetValue(value)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
attrs = append(attrs, container.Attribute{})
|
||||
attrs[ln].SetKey(key)
|
||||
attrs[ln].SetValue(value)
|
||||
|
||||
x.v2.SetAttributes(attrs)
|
||||
}
|
||||
|
||||
// Signature returns signature of the marshaled container.
|
||||
func (c Container) Signature() *signature.Signature {
|
||||
return c.sig
|
||||
// Attribute reads value of the Container attribute by key. Empty result means
|
||||
// attribute absence.
|
||||
//
|
||||
// See also SetAttribute, IterateAttributes.
|
||||
func (x Container) Attribute(key string) string {
|
||||
attrs := x.v2.GetAttributes()
|
||||
for i := range attrs {
|
||||
if attrs[i].GetKey() == key {
|
||||
return attrs[i].GetValue()
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// SetSignature sets signature of the marshaled container.
|
||||
func (c *Container) SetSignature(sig *signature.Signature) {
|
||||
c.sig = 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())
|
||||
}
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
// UnmarshalJSON decodes Container from protobuf JSON format.
|
||||
func (c *Container) UnmarshalJSON(data []byte) error {
|
||||
return c.v2.UnmarshalJSON(data)
|
||||
// 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)
|
||||
}
|
||||
|
||||
// SetSubnet places the Container on the specified FrostFS subnet. If called,
|
||||
// container nodes will only be selected from the given subnet, otherwise from
|
||||
// the entire network.
|
||||
func SetSubnet(cnr *Container, subNet subnetid.ID) {
|
||||
cnr.SetAttribute(container.SysAttributeSubnet, subNet.EncodeToString())
|
||||
}
|
||||
|
||||
// Subnet return container subnet set using SetSubnet.
|
||||
//
|
||||
// Zero Container is bound to zero subnet.
|
||||
func Subnet(cnr Container) (res subnetid.ID) {
|
||||
val := cnr.Attribute(container.SysAttributeSubnet)
|
||||
if val != "" {
|
||||
err := res.DecodeString(val)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("invalid subnet attribute: %s (%v)", val, err))
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const attributeHomoHashEnabled = "true"
|
||||
|
||||
// DisableHomomorphicHashing sets flag to disable homomorphic hashing of the
|
||||
// Container data.
|
||||
//
|
||||
// See also IsHomomorphicHashingDisabled.
|
||||
func DisableHomomorphicHashing(cnr *Container) {
|
||||
cnr.SetAttribute(container.SysAttributeHomomorphicHashing, attributeHomoHashEnabled)
|
||||
}
|
||||
|
||||
// IsHomomorphicHashingDisabled checks if DisableHomomorphicHashing was called.
|
||||
//
|
||||
// Zero Container has enabled hashing.
|
||||
func IsHomomorphicHashingDisabled(cnr Container) bool {
|
||||
return cnr.Attribute(container.SysAttributeHomomorphicHashing) == attributeHomoHashEnabled ||
|
||||
cnr.Attribute(container.SysAttributeHomomorphicHashingNeoFS) == attributeHomoHashEnabled
|
||||
}
|
||||
|
||||
// Domain represents information about container domain registered in the NNS
|
||||
// contract deployed in the FrostFS network.
|
||||
type Domain struct {
|
||||
name, zone string
|
||||
}
|
||||
|
||||
// SetName sets human-friendly container domain name.
|
||||
func (x *Domain) SetName(name string) {
|
||||
x.name = name
|
||||
}
|
||||
|
||||
// Name returns name set using SetName.
|
||||
//
|
||||
// Zero Domain has zero name.
|
||||
func (x Domain) Name() string {
|
||||
return x.name
|
||||
}
|
||||
|
||||
// SetZone sets zone which is used as a TLD of a domain name in NNS contract.
|
||||
func (x *Domain) SetZone(zone string) {
|
||||
x.zone = zone
|
||||
}
|
||||
|
||||
// Zone returns domain zone set using SetZone.
|
||||
//
|
||||
// Zero Domain has "container" zone.
|
||||
func (x Domain) Zone() string {
|
||||
if x.zone != "" {
|
||||
return x.zone
|
||||
}
|
||||
|
||||
return "container"
|
||||
}
|
||||
|
||||
// WriteDomain writes Domain into the Container. Name MUST NOT be empty.
|
||||
func WriteDomain(cnr *Container, domain Domain) {
|
||||
cnr.SetAttribute(container.SysAttributeName, domain.Name())
|
||||
cnr.SetAttribute(container.SysAttributeZone, domain.Zone())
|
||||
}
|
||||
|
||||
// ReadDomain reads Domain from the Container. Returns value with empty name
|
||||
// if domain is not specified.
|
||||
func ReadDomain(cnr Container) (res Domain) {
|
||||
if name := cnr.Attribute(container.SysAttributeName); name != "" {
|
||||
res.SetName(name)
|
||||
res.SetZone(cnr.Attribute(container.SysAttributeZone))
|
||||
} else if name = cnr.Attribute(container.SysAttributeNameNeoFS); name != "" {
|
||||
res.SetName(name)
|
||||
res.SetZone(cnr.Attribute(container.SysAttributeZoneNeoFS))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// CalculateSignature calculates signature of the Container using provided signer
|
||||
// and writes it into dst. Signature instance MUST NOT be nil. CalculateSignature
|
||||
// is expected to be called after all the Container data is filled and before
|
||||
// saving the Container in the FrostFS network. Note that мany subsequent change
|
||||
// will most likely break the signature.
|
||||
//
|
||||
// See also VerifySignature.
|
||||
func CalculateSignature(dst *frostfscrypto.Signature, cnr Container, signer ecdsa.PrivateKey) error {
|
||||
return dst.Calculate(frostfsecdsa.SignerRFC6979(signer), cnr.Marshal())
|
||||
}
|
||||
|
||||
// VerifySignature verifies Container signature calculated using CalculateSignature.
|
||||
// Result means signature correctness.
|
||||
func VerifySignature(sig frostfscrypto.Signature, cnr Container) bool {
|
||||
return sig.Verify(cnr.Marshal())
|
||||
}
|
||||
|
||||
// CalculateIDFromBinary calculates identifier of the binary-encoded container
|
||||
// in CAS of the FrostFS containers and writes it into dst. ID instance MUST NOT
|
||||
// be nil.
|
||||
//
|
||||
// See also CalculateID, AssertID.
|
||||
func CalculateIDFromBinary(dst *cid.ID, cnr []byte) {
|
||||
dst.SetSHA256(sha256.Sum256(cnr))
|
||||
}
|
||||
|
||||
// CalculateID encodes the given Container and passes the result into
|
||||
// CalculateIDFromBinary.
|
||||
//
|
||||
// See also Container.Marshal, AssertID.
|
||||
func CalculateID(dst *cid.ID, cnr Container) {
|
||||
CalculateIDFromBinary(dst, cnr.Marshal())
|
||||
}
|
||||
|
||||
// AssertID checks if the given Container matches its identifier in CAS of the
|
||||
// FrostFS containers.
|
||||
//
|
||||
// See also CalculateID.
|
||||
func AssertID(id cid.ID, cnr Container) bool {
|
||||
var id2 cid.ID
|
||||
CalculateID(&id2, cnr)
|
||||
|
||||
return id2.Equals(id)
|
||||
}
|
||||
|
|
|
@ -1,136 +1,351 @@
|
|||
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"
|
||||
subnetid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/subnet/id"
|
||||
subnetidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/subnet/id/test"
|
||||
usertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user/test"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/version"
|
||||
"github.com/google/uuid"
|
||||
"github.com/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/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
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()
|
||||
func TestPlacementPolicyEncoding(t *testing.T) {
|
||||
v := containertest.Container()
|
||||
|
||||
t.Run("binary", func(t *testing.T) {
|
||||
data, err := c.Marshal()
|
||||
require.NoError(t, err)
|
||||
var v2 container.Container
|
||||
require.NoError(t, v2.Unmarshal(v.Marshal()))
|
||||
|
||||
c2 := container.New()
|
||||
require.NoError(t, c2.Unmarshal(data))
|
||||
|
||||
require.Equal(t, c, c2)
|
||||
require.Equal(t, v, v2)
|
||||
})
|
||||
|
||||
t.Run("json", func(t *testing.T) {
|
||||
data, err := c.MarshalJSON()
|
||||
data, err := v.MarshalJSON()
|
||||
require.NoError(t, err)
|
||||
|
||||
c2 := container.New()
|
||||
require.NoError(t, c2.UnmarshalJSON(data))
|
||||
var v2 container.Container
|
||||
require.NoError(t, v2.UnmarshalJSON(data))
|
||||
|
||||
require.Equal(t, c, c2)
|
||||
require.Equal(t, v, v2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestContainer_SessionToken(t *testing.T) {
|
||||
tok := sessiontest.Token()
|
||||
func TestContainer_Init(t *testing.T) {
|
||||
val := containertest.Container()
|
||||
|
||||
cnr := container.New()
|
||||
val.Init()
|
||||
|
||||
cnr.SetSessionToken(tok)
|
||||
var msg v2container.Container
|
||||
val.WriteToV2(&msg)
|
||||
|
||||
require.Equal(t, tok, cnr.SessionToken())
|
||||
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)
|
||||
}
|
||||
|
||||
func TestContainer_Signature(t *testing.T) {
|
||||
sig := sigtest.Signature()
|
||||
func TestContainer_Owner(t *testing.T) {
|
||||
var val container.Container
|
||||
|
||||
cnr := container.New()
|
||||
cnr.SetSignature(sig)
|
||||
require.Zero(t, val.Owner())
|
||||
|
||||
require.Equal(t, sig, cnr.Signature())
|
||||
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))
|
||||
}
|
||||
|
||||
func TestContainer_ToV2(t *testing.T) {
|
||||
t.Run("nil", func(t *testing.T) {
|
||||
var x *container.Container
|
||||
func TestContainer_BasicACL(t *testing.T) {
|
||||
var val container.Container
|
||||
|
||||
require.Nil(t, x.ToV2())
|
||||
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 = "key1", "key2"
|
||||
const attrVal1, attrVal2 = "val1", "val2"
|
||||
|
||||
val := containertest.Container()
|
||||
|
||||
val.SetAttribute(attrKey1, attrVal1)
|
||||
val.SetAttribute(attrKey2, attrVal2)
|
||||
|
||||
var msg v2container.Container
|
||||
val.WriteToV2(&msg)
|
||||
|
||||
require.GreaterOrEqual(t, len(msg.GetAttributes()), 2)
|
||||
assertContainsAttribute(t, msg, attrKey1, attrVal1)
|
||||
assertContainsAttribute(t, msg, attrKey2, attrVal2)
|
||||
|
||||
var val2 container.Container
|
||||
require.NoError(t, val2.ReadFromV2(msg))
|
||||
|
||||
require.Equal(t, attrVal1, val2.Attribute(attrKey1))
|
||||
require.Equal(t, attrVal2, val2.Attribute(attrKey2))
|
||||
|
||||
m := map[string]string{}
|
||||
|
||||
val2.IterateAttributes(func(key, val string) {
|
||||
m[key] = val
|
||||
})
|
||||
|
||||
t.Run("default values", func(t *testing.T) {
|
||||
cnt := container.New()
|
||||
require.GreaterOrEqual(t, len(m), 2)
|
||||
require.Equal(t, attrVal1, m[attrKey1])
|
||||
require.Equal(t, attrVal2, m[attrKey2])
|
||||
|
||||
// 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())
|
||||
|
||||
require.EqualValues(t, acl.PrivateBasicRule, cnt.BasicACL())
|
||||
require.Equal(t, version.Current(), cnt.Version())
|
||||
|
||||
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())
|
||||
})
|
||||
val2.SetAttribute(attrKey1, attrVal1+"_")
|
||||
require.Equal(t, attrVal1+"_", val2.Attribute(attrKey1))
|
||||
}
|
||||
|
||||
func TestSetName(t *testing.T) {
|
||||
var val container.Container
|
||||
|
||||
require.Panics(t, func() {
|
||||
container.SetName(&val, "")
|
||||
})
|
||||
|
||||
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 TestSetSubnet(t *testing.T) {
|
||||
var val container.Container
|
||||
|
||||
require.True(t, subnetid.IsZero(container.Subnet(val)))
|
||||
|
||||
val = containertest.Container()
|
||||
|
||||
sub := subnetidtest.ID()
|
||||
|
||||
container.SetSubnet(&val, sub)
|
||||
|
||||
var msg v2container.Container
|
||||
val.WriteToV2(&msg)
|
||||
|
||||
assertContainsAttribute(t, msg, v2container.SysAttributeSubnet, sub.EncodeToString())
|
||||
|
||||
var val2 container.Container
|
||||
require.NoError(t, val2.ReadFromV2(msg))
|
||||
|
||||
require.Equal(t, sub, container.Subnet(val))
|
||||
}
|
||||
|
||||
func TestDisableHomomorphicHashing(t *testing.T) {
|
||||
var val container.Container
|
||||
|
||||
require.False(t, container.IsHomomorphicHashingDisabled(val))
|
||||
|
||||
val = containertest.Container()
|
||||
|
||||
container.DisableHomomorphicHashing(&val)
|
||||
|
||||
var msg v2container.Container
|
||||
val.WriteToV2(&msg)
|
||||
|
||||
assertContainsAttribute(t, msg, v2container.SysAttributePrefix+"DISABLE_HOMOMORPHIC_HASHING", "true")
|
||||
|
||||
var val2 container.Container
|
||||
require.NoError(t, val2.ReadFromV2(msg))
|
||||
|
||||
require.True(t, container.IsHomomorphicHashingDisabled(val2))
|
||||
}
|
||||
|
||||
func TestWriteDomain(t *testing.T) {
|
||||
var val container.Container
|
||||
|
||||
require.Zero(t, container.ReadDomain(val).Name())
|
||||
|
||||
val = containertest.Container()
|
||||
|
||||
const name = "domain name"
|
||||
|
||||
var d container.Domain
|
||||
d.SetName(name)
|
||||
|
||||
container.WriteDomain(&val, d)
|
||||
|
||||
var msg v2container.Container
|
||||
val.WriteToV2(&msg)
|
||||
|
||||
assertContainsAttribute(t, msg, v2container.SysAttributeName, name)
|
||||
assertContainsAttribute(t, msg, v2container.SysAttributeZone, "container")
|
||||
|
||||
const zone = "domain zone"
|
||||
|
||||
d.SetZone(zone)
|
||||
|
||||
container.WriteDomain(&val, d)
|
||||
|
||||
val.WriteToV2(&msg)
|
||||
|
||||
assertContainsAttribute(t, msg, v2container.SysAttributeZone, zone)
|
||||
|
||||
var val2 container.Container
|
||||
require.NoError(t, val2.ReadFromV2(msg))
|
||||
|
||||
require.Equal(t, d, container.ReadDomain(val2))
|
||||
}
|
||||
|
||||
func TestCalculateID(t *testing.T) {
|
||||
val := containertest.Container()
|
||||
|
||||
require.False(t, container.AssertID(cidtest.ID(), val))
|
||||
|
||||
var id cid.ID
|
||||
container.CalculateID(&id, val)
|
||||
|
||||
var msg refs.ContainerID
|
||||
id.WriteToV2(&msg)
|
||||
|
||||
h := sha256.Sum256(val.Marshal())
|
||||
require.Equal(t, h[:], msg.GetValue())
|
||||
|
||||
var id2 cid.ID
|
||||
require.NoError(t, id2.ReadFromV2(msg))
|
||||
|
||||
require.True(t, container.AssertID(id2, val))
|
||||
}
|
||||
|
||||
func TestCalculateSignature(t *testing.T) {
|
||||
key, err := keys.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
|
||||
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))
|
||||
}
|
||||
|
|
49
container/doc.go
Normal file
49
container/doc.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
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
|
7
container/id/doc.go
Normal file
7
container/id/doc.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
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
|
|
@ -1,90 +1,115 @@
|
|||
package cid
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"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 v2-compatible container identifier.
|
||||
type ID refs.ContainerID
|
||||
|
||||
// NewFromV2 wraps v2 ContainerID message to ID.
|
||||
// ID represents FrostFS container identifier.
|
||||
//
|
||||
// Nil refs.ContainerID converts to nil.
|
||||
func NewFromV2(idV2 *refs.ContainerID) *ID {
|
||||
return (*ID)(idV2)
|
||||
}
|
||||
|
||||
// New creates and initializes blank ID.
|
||||
// ID is mutually compatible with git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs.ContainerID
|
||||
// message. See ReadFromV2 / WriteToV2 methods.
|
||||
//
|
||||
// Defaults:
|
||||
// - value: nil.
|
||||
func New() *ID {
|
||||
return NewFromV2(new(refs.ContainerID))
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Instances can be created using built-in var declaration.
|
||||
//
|
||||
// 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.
|
||||
// Note that direct typecast is not safe and may result in loss of compatibility:
|
||||
//
|
||||
// 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")
|
||||
// _ = ID([32]byte) // not recommended
|
||||
type ID [sha256.Size]byte
|
||||
|
||||
// ReadFromV2 reads ID from the refs.ContainerID message.
|
||||
// Returns an error if the message is malformed according
|
||||
// to the FrostFS API V2 protocol.
|
||||
//
|
||||
// See also WriteToV2.
|
||||
func (id *ID) ReadFromV2(m refs.ContainerID) error {
|
||||
return id.Decode(m.GetValue())
|
||||
}
|
||||
|
||||
// WriteToV2 writes ID to the refs.ContainerID message.
|
||||
// The message must not be nil.
|
||||
//
|
||||
// See also ReadFromV2.
|
||||
func (id ID) WriteToV2(m *refs.ContainerID) {
|
||||
m.SetValue(id[:])
|
||||
}
|
||||
|
||||
// Encode encodes ID into 32 bytes of dst. Panics if
|
||||
// dst length is less than 32.
|
||||
//
|
||||
// Zero ID is all zeros.
|
||||
//
|
||||
// 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))
|
||||
}
|
||||
|
||||
(*refs.ContainerID)(id).SetValue(data)
|
||||
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)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// String returns base58 string representation of ID.
|
||||
func (id *ID) String() string {
|
||||
return base58.Encode((*refs.ContainerID)(id).GetValue())
|
||||
// SetSHA256 sets container identifier value to SHA256 checksum of container structure.
|
||||
func (id *ID) SetSHA256(v [sha256.Size]byte) {
|
||||
copy(id[:], v[:])
|
||||
}
|
||||
|
||||
// Marshal marshals ID into a protobuf binary form.
|
||||
func (id *ID) Marshal() ([]byte, error) {
|
||||
return (*refs.ContainerID)(id).StableMarshal(nil)
|
||||
// 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
|
||||
}
|
||||
|
||||
// Unmarshal unmarshals protobuf binary representation of ID.
|
||||
func (id *ID) Unmarshal(data []byte) error {
|
||||
return (*refs.ContainerID)(id).Unmarshal(data)
|
||||
// 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[:])
|
||||
}
|
||||
|
||||
// MarshalJSON encodes ID to protobuf JSON format.
|
||||
func (id *ID) MarshalJSON() ([]byte, error) {
|
||||
return (*refs.ContainerID)(id).MarshalJSON()
|
||||
// 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)
|
||||
}
|
||||
|
||||
// UnmarshalJSON decodes ID from protobuf JSON format.
|
||||
func (id *ID) UnmarshalJSON(data []byte) error {
|
||||
return (*refs.ContainerID)(id).UnmarshalJSON(data)
|
||||
// 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()
|
||||
}
|
||||
|
|
|
@ -5,9 +5,10 @@ import (
|
|||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/nspcc-dev/neofs-api-go/v2/refs"
|
||||
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
|
||||
cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test"
|
||||
"github.com/mr-tron/base58"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
@ -16,30 +17,32 @@ func randSHA256Checksum() (cs [sha256.Size]byte) {
|
|||
return
|
||||
}
|
||||
|
||||
const emptyID = "11111111111111111111111111111111"
|
||||
|
||||
func TestID_ToV2(t *testing.T) {
|
||||
t.Run("non-nil", func(t *testing.T) {
|
||||
t.Run("non-zero", func(t *testing.T) {
|
||||
checksum := randSHA256Checksum()
|
||||
|
||||
id := cidtest.IDWithChecksum(checksum)
|
||||
|
||||
idV2 := id.ToV2()
|
||||
var idV2 refs.ContainerID
|
||||
id.WriteToV2(&idV2)
|
||||
|
||||
require.Equal(t, id, cid.NewFromV2(idV2))
|
||||
var newID cid.ID
|
||||
require.NoError(t, newID.ReadFromV2(idV2))
|
||||
|
||||
require.Equal(t, id, newID)
|
||||
require.Equal(t, checksum[:], idV2.GetValue())
|
||||
})
|
||||
|
||||
t.Run("nil", func(t *testing.T) {
|
||||
var x *cid.ID
|
||||
t.Run("zero", func(t *testing.T) {
|
||||
var (
|
||||
x cid.ID
|
||||
v2 refs.ContainerID
|
||||
)
|
||||
|
||||
require.Nil(t, x.ToV2())
|
||||
})
|
||||
|
||||
t.Run("default values", func(t *testing.T) {
|
||||
id := cid.New()
|
||||
|
||||
// convert to v2 message
|
||||
cidV2 := id.ToV2()
|
||||
require.Nil(t, cidV2.GetValue())
|
||||
x.WriteToV2(&v2)
|
||||
require.Equal(t, emptyID, base58.Encode(v2.GetValue()))
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -49,57 +52,57 @@ func TestID_Equal(t *testing.T) {
|
|||
id1 := cidtest.IDWithChecksum(cs)
|
||||
id2 := cidtest.IDWithChecksum(cs)
|
||||
|
||||
require.True(t, id1.Equal(id2))
|
||||
require.True(t, id1.Equals(id2))
|
||||
|
||||
id3 := cidtest.ID()
|
||||
|
||||
require.False(t, id1.Equal(id3))
|
||||
require.False(t, id1.Equals(id3))
|
||||
}
|
||||
|
||||
func TestID_String(t *testing.T) {
|
||||
t.Run("Parse/String", func(t *testing.T) {
|
||||
t.Run("DecodeString/EncodeToString", func(t *testing.T) {
|
||||
id := cidtest.ID()
|
||||
id2 := cid.New()
|
||||
var id2 cid.ID
|
||||
|
||||
require.NoError(t, id2.Parse(id.String()))
|
||||
require.NoError(t, id2.DecodeString(id.EncodeToString()))
|
||||
require.Equal(t, id, id2)
|
||||
})
|
||||
|
||||
t.Run("nil", func(t *testing.T) {
|
||||
id := cid.New()
|
||||
t.Run("zero", func(t *testing.T) {
|
||||
var id cid.ID
|
||||
|
||||
require.Empty(t, id.String())
|
||||
})
|
||||
}
|
||||
|
||||
func TestContainerIDEncoding(t *testing.T) {
|
||||
id := cidtest.ID()
|
||||
|
||||
t.Run("binary", func(t *testing.T) {
|
||||
data, err := id.Marshal()
|
||||
require.NoError(t, err)
|
||||
|
||||
id2 := cid.New()
|
||||
require.NoError(t, id2.Unmarshal(data))
|
||||
|
||||
require.Equal(t, id, id2)
|
||||
})
|
||||
|
||||
t.Run("json", func(t *testing.T) {
|
||||
data, err := id.MarshalJSON()
|
||||
require.NoError(t, err)
|
||||
|
||||
a2 := cid.New()
|
||||
require.NoError(t, a2.UnmarshalJSON(data))
|
||||
|
||||
require.Equal(t, id, a2)
|
||||
require.Equal(t, emptyID, id.EncodeToString())
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewFromV2(t *testing.T) {
|
||||
t.Run("from nil", func(t *testing.T) {
|
||||
var x *refs.ContainerID
|
||||
t.Run("from zero", func(t *testing.T) {
|
||||
var (
|
||||
x cid.ID
|
||||
v2 refs.ContainerID
|
||||
)
|
||||
|
||||
require.Nil(t, cid.NewFromV2(x))
|
||||
require.Error(t, x.ReadFromV2(v2))
|
||||
})
|
||||
}
|
||||
|
||||
func TestID_Encode(t *testing.T) {
|
||||
var id cid.ID
|
||||
|
||||
t.Run("panic", func(t *testing.T) {
|
||||
dst := make([]byte, sha256.Size-1)
|
||||
|
||||
require.Panics(t, func() {
|
||||
id.Encode(dst)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("correct", func(t *testing.T) {
|
||||
dst := make([]byte, sha256.Size)
|
||||
|
||||
require.NotPanics(t, func() {
|
||||
id.Encode(dst)
|
||||
})
|
||||
require.Equal(t, emptyID, id.EncodeToString())
|
||||
})
|
||||
}
|
||||
|
|
13
container/id/test/doc.go
Normal file
13
container/id/test/doc.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
Package cidtest provides functions for convenient testing of cid package API.
|
||||
|
||||
Note that importing the package into source files is highly discouraged.
|
||||
|
||||
Random instance generation functions can be useful when testing expects any value, e.g.:
|
||||
|
||||
import cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test"
|
||||
|
||||
cid := cidtest.ID()
|
||||
// test the value
|
||||
*/
|
||||
package cidtest
|
|
@ -4,11 +4,11 @@ import (
|
|||
"crypto/sha256"
|
||||
"math/rand"
|
||||
|
||||
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
|
||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
)
|
||||
|
||||
// ID returns random cid.ID.
|
||||
func ID() *cid.ID {
|
||||
func ID() cid.ID {
|
||||
checksum := [sha256.Size]byte{}
|
||||
|
||||
rand.Read(checksum[:])
|
||||
|
@ -18,8 +18,8 @@ func ID() *cid.ID {
|
|||
|
||||
// IDWithChecksum returns cid.ID initialized
|
||||
// with specified checksum.
|
||||
func IDWithChecksum(cs [sha256.Size]byte) *cid.ID {
|
||||
id := cid.New()
|
||||
func IDWithChecksum(cs [sha256.Size]byte) cid.ID {
|
||||
var id cid.ID
|
||||
id.SetSHA256(cs)
|
||||
|
||||
return id
|
||||
|
|
20
container/network.go
Normal file
20
container/network.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
||||
)
|
||||
|
||||
// ApplyNetworkConfig applies network configuration to the
|
||||
// container. Changes the container if it does not satisfy
|
||||
// network configuration.
|
||||
func ApplyNetworkConfig(cnr *Container, cfg netmap.NetworkInfo) {
|
||||
if cfg.HomomorphicHashingDisabled() {
|
||||
DisableHomomorphicHashing(cnr)
|
||||
}
|
||||
}
|
||||
|
||||
// AssertNetworkConfig checks if a container matches passed
|
||||
// network configuration.
|
||||
func AssertNetworkConfig(cnr Container, cfg netmap.NetworkInfo) bool {
|
||||
return IsHomomorphicHashingDisabled(cnr) == cfg.HomomorphicHashingDisabled()
|
||||
}
|
33
container/network_test.go
Normal file
33
container/network_test.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
package container_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
|
||||
containertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/test"
|
||||
netmaptest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestContainer_NetworkConfig(t *testing.T) {
|
||||
c := containertest.Container()
|
||||
nc := netmaptest.NetworkInfo()
|
||||
|
||||
t.Run("default", func(t *testing.T) {
|
||||
require.False(t, container.IsHomomorphicHashingDisabled(c))
|
||||
|
||||
res := container.AssertNetworkConfig(c, nc)
|
||||
|
||||
require.True(t, res)
|
||||
})
|
||||
|
||||
nc.DisableHomomorphicHashing()
|
||||
|
||||
t.Run("apply", func(t *testing.T) {
|
||||
require.False(t, container.IsHomomorphicHashingDisabled(c))
|
||||
|
||||
container.ApplyNetworkConfig(&c, nc)
|
||||
|
||||
require.True(t, container.IsHomomorphicHashingDisabled(c))
|
||||
})
|
||||
}
|
|
@ -1,89 +0,0 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/nspcc-dev/neofs-sdk-go/acl"
|
||||
"github.com/nspcc-dev/neofs-sdk-go/netmap"
|
||||
"github.com/nspcc-dev/neofs-sdk-go/owner"
|
||||
)
|
||||
|
||||
type (
|
||||
Option func(*containerOptions)
|
||||
|
||||
containerOptions struct {
|
||||
acl acl.BasicACL
|
||||
policy *netmap.PlacementPolicy
|
||||
attributes Attributes
|
||||
owner *owner.ID
|
||||
nonce uuid.UUID
|
||||
}
|
||||
)
|
||||
|
||||
func defaultContainerOptions() containerOptions {
|
||||
rand, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
panic("can't create new random " + err.Error())
|
||||
}
|
||||
|
||||
return containerOptions{
|
||||
acl: acl.PrivateBasicRule,
|
||||
nonce: rand,
|
||||
}
|
||||
}
|
||||
|
||||
func WithPublicBasicACL() Option {
|
||||
return func(option *containerOptions) {
|
||||
option.acl = acl.PublicBasicRule
|
||||
}
|
||||
}
|
||||
|
||||
func WithReadOnlyBasicACL() Option {
|
||||
return func(option *containerOptions) {
|
||||
option.acl = acl.ReadOnlyBasicRule
|
||||
}
|
||||
}
|
||||
|
||||
func WithCustomBasicACL(acl acl.BasicACL) Option {
|
||||
return func(option *containerOptions) {
|
||||
option.acl = acl
|
||||
}
|
||||
}
|
||||
|
||||
func WithNonce(nonce uuid.UUID) Option {
|
||||
return func(option *containerOptions) {
|
||||
option.nonce = nonce
|
||||
}
|
||||
}
|
||||
|
||||
func WithOwnerID(id *owner.ID) Option {
|
||||
return func(option *containerOptions) {
|
||||
option.owner = id
|
||||
}
|
||||
}
|
||||
|
||||
func WithOwnerPublicKey(pub *ecdsa.PublicKey) Option {
|
||||
return func(option *containerOptions) {
|
||||
if option.owner == nil {
|
||||
option.owner = new(owner.ID)
|
||||
}
|
||||
|
||||
option.owner.SetPublicKey(pub)
|
||||
}
|
||||
}
|
||||
|
||||
func WithPolicy(policy *netmap.PlacementPolicy) Option {
|
||||
return func(option *containerOptions) {
|
||||
option.policy = policy
|
||||
}
|
||||
}
|
||||
|
||||
func WithAttribute(key, value string) Option {
|
||||
return func(option *containerOptions) {
|
||||
index := len(option.attributes)
|
||||
option.attributes = append(option.attributes, Attribute{})
|
||||
option.attributes[index].SetKey(key)
|
||||
option.attributes[index].SetValue(value)
|
||||
}
|
||||
}
|
104
container/size.go
Normal file
104
container/size.go
Normal file
|
@ -0,0 +1,104 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
)
|
||||
|
||||
// SizeEstimation groups information about estimation of the size of the data
|
||||
// stored in the FrostFS container.
|
||||
//
|
||||
// SizeEstimation is mutually compatible with git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container.UsedSpaceAnnouncement
|
||||
// message. See ReadFromV2 / WriteToV2 methods.
|
||||
type SizeEstimation struct {
|
||||
m container.UsedSpaceAnnouncement
|
||||
}
|
||||
|
||||
// ReadFromV2 reads SizeEstimation from the container.UsedSpaceAnnouncement message.
|
||||
// Checks if the message conforms to FrostFS API V2 protocol.
|
||||
//
|
||||
// See also WriteToV2.
|
||||
func (x *SizeEstimation) ReadFromV2(m container.UsedSpaceAnnouncement) error {
|
||||
cnrV2 := m.GetContainerID()
|
||||
if cnrV2 == nil {
|
||||
return errors.New("missing container")
|
||||
}
|
||||
|
||||
var cnr cid.ID
|
||||
|
||||
err := cnr.ReadFromV2(*cnrV2)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid container: %w", err)
|
||||
}
|
||||
|
||||
x.m = m
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteToV2 writes SizeEstimation into the container.UsedSpaceAnnouncement message.
|
||||
// The message MUST NOT be nil.
|
||||
//
|
||||
// See also ReadFromV2.
|
||||
func (x SizeEstimation) WriteToV2(m *container.UsedSpaceAnnouncement) {
|
||||
*m = x.m
|
||||
}
|
||||
|
||||
// SetEpoch sets epoch when estimation of the container data size was calculated.
|
||||
//
|
||||
// See also Epoch.
|
||||
func (x *SizeEstimation) SetEpoch(epoch uint64) {
|
||||
x.m.SetEpoch(epoch)
|
||||
}
|
||||
|
||||
// Epoch return epoch set using SetEpoch.
|
||||
//
|
||||
// Zero SizeEstimation represents estimation in zero epoch.
|
||||
func (x SizeEstimation) Epoch() uint64 {
|
||||
return x.m.GetEpoch()
|
||||
}
|
||||
|
||||
// SetContainer specifies the container for which the amount of data is estimated.
|
||||
// Required by the FrostFS API protocol.
|
||||
//
|
||||
// See also Container.
|
||||
func (x *SizeEstimation) SetContainer(cnr cid.ID) {
|
||||
var cidV2 refs.ContainerID
|
||||
cnr.WriteToV2(&cidV2)
|
||||
|
||||
x.m.SetContainerID(&cidV2)
|
||||
}
|
||||
|
||||
// Container returns container set using SetContainer.
|
||||
//
|
||||
// Zero SizeEstimation is not bound to any container (returns zero) which is
|
||||
// incorrect according to FrostFS API protocol.
|
||||
func (x SizeEstimation) Container() (res cid.ID) {
|
||||
m := x.m.GetContainerID()
|
||||
if m != nil {
|
||||
err := res.ReadFromV2(*m)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("unexpected error from cid.ID.ReadFromV2: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SetValue sets estimated amount of data (in bytes) in the specified container.
|
||||
//
|
||||
// See also Value.
|
||||
func (x *SizeEstimation) SetValue(value uint64) {
|
||||
x.m.SetUsedSpace(value)
|
||||
}
|
||||
|
||||
// Value returns data size estimation set using SetValue.
|
||||
//
|
||||
// Zero SizeEstimation has zero value.
|
||||
func (x SizeEstimation) Value() uint64 {
|
||||
return x.m.GetUsedSpace()
|
||||
}
|
94
container/size_test.go
Normal file
94
container/size_test.go
Normal file
|
@ -0,0 +1,94 @@
|
|||
package container_test
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"testing"
|
||||
|
||||
v2container "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
|
||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSizeEstimation_Epoch(t *testing.T) {
|
||||
var val container.SizeEstimation
|
||||
|
||||
require.Zero(t, val.Epoch())
|
||||
|
||||
const epoch = 123
|
||||
|
||||
val.SetEpoch(epoch)
|
||||
require.EqualValues(t, epoch, val.Epoch())
|
||||
|
||||
var msg v2container.UsedSpaceAnnouncement
|
||||
val.WriteToV2(&msg)
|
||||
|
||||
require.EqualValues(t, epoch, msg.GetEpoch())
|
||||
}
|
||||
|
||||
func TestSizeEstimation_Container(t *testing.T) {
|
||||
var val container.SizeEstimation
|
||||
|
||||
require.Zero(t, val.Container())
|
||||
|
||||
cnr := cidtest.ID()
|
||||
|
||||
val.SetContainer(cnr)
|
||||
require.True(t, val.Container().Equals(cnr))
|
||||
|
||||
var msg v2container.UsedSpaceAnnouncement
|
||||
val.WriteToV2(&msg)
|
||||
|
||||
var msgCnr refs.ContainerID
|
||||
cnr.WriteToV2(&msgCnr)
|
||||
|
||||
require.Equal(t, &msgCnr, msg.GetContainerID())
|
||||
}
|
||||
|
||||
func TestSizeEstimation_Value(t *testing.T) {
|
||||
var val container.SizeEstimation
|
||||
|
||||
require.Zero(t, val.Value())
|
||||
|
||||
const value = 876
|
||||
|
||||
val.SetValue(value)
|
||||
require.EqualValues(t, value, val.Value())
|
||||
|
||||
var msg v2container.UsedSpaceAnnouncement
|
||||
val.WriteToV2(&msg)
|
||||
|
||||
require.EqualValues(t, value, msg.GetUsedSpace())
|
||||
}
|
||||
|
||||
func TestSizeEstimation_ReadFromV2(t *testing.T) {
|
||||
const epoch = 654
|
||||
const value = 903
|
||||
var cnrMsg refs.ContainerID
|
||||
|
||||
var msg v2container.UsedSpaceAnnouncement
|
||||
|
||||
var val container.SizeEstimation
|
||||
|
||||
require.Error(t, val.ReadFromV2(msg))
|
||||
|
||||
msg.SetContainerID(&cnrMsg)
|
||||
|
||||
require.Error(t, val.ReadFromV2(msg))
|
||||
|
||||
cnrMsg.SetValue(make([]byte, sha256.Size))
|
||||
|
||||
var cnr cid.ID
|
||||
require.NoError(t, cnr.ReadFromV2(cnrMsg))
|
||||
|
||||
msg.SetEpoch(epoch)
|
||||
msg.SetUsedSpace(value)
|
||||
|
||||
require.NoError(t, val.ReadFromV2(msg))
|
||||
|
||||
require.EqualValues(t, epoch, val.Epoch())
|
||||
require.EqualValues(t, value, val.Value())
|
||||
require.EqualValues(t, cnr, val.Container())
|
||||
}
|
|
@ -1,48 +1,39 @@
|
|||
package containertest
|
||||
|
||||
import (
|
||||
"github.com/nspcc-dev/neofs-sdk-go/container"
|
||||
cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test"
|
||||
netmaptest "github.com/nspcc-dev/neofs-sdk-go/netmap/test"
|
||||
ownertest "github.com/nspcc-dev/neofs-sdk-go/owner/test"
|
||||
versiontest "github.com/nspcc-dev/neofs-sdk-go/version/test"
|
||||
"math/rand"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl"
|
||||
cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test"
|
||||
netmaptest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap/test"
|
||||
usertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user/test"
|
||||
)
|
||||
|
||||
// Attribute returns random container.Attribute.
|
||||
func Attribute() *container.Attribute {
|
||||
x := container.NewAttribute()
|
||||
|
||||
x.SetKey("key")
|
||||
x.SetValue("value")
|
||||
|
||||
return x
|
||||
}
|
||||
|
||||
// Attributes returns random container.Attributes.
|
||||
func Attributes() container.Attributes {
|
||||
return container.Attributes{*Attribute(), *Attribute()}
|
||||
}
|
||||
|
||||
// Container returns random container.Container.
|
||||
func Container() *container.Container {
|
||||
x := container.New()
|
||||
func Container() (x container.Container) {
|
||||
owner := usertest.ID()
|
||||
|
||||
x.SetVersion(versiontest.Version())
|
||||
x.SetAttributes(Attributes())
|
||||
x.SetOwnerID(ownertest.ID())
|
||||
x.SetBasicACL(123)
|
||||
x.Init()
|
||||
x.SetAttribute("some attribute", "value")
|
||||
x.SetOwner(*owner)
|
||||
x.SetBasicACL(BasicACL())
|
||||
x.SetPlacementPolicy(netmaptest.PlacementPolicy())
|
||||
|
||||
return x
|
||||
}
|
||||
|
||||
// UsedSpaceAnnouncement returns random container.UsedSpaceAnnouncement.
|
||||
func UsedSpaceAnnouncement() *container.UsedSpaceAnnouncement {
|
||||
x := container.NewAnnouncement()
|
||||
|
||||
x.SetContainerID(cidtest.ID())
|
||||
x.SetEpoch(55)
|
||||
x.SetUsedSpace(999)
|
||||
// SizeEstimation returns random container.SizeEstimation.
|
||||
func SizeEstimation() (x container.SizeEstimation) {
|
||||
x.SetContainer(cidtest.ID())
|
||||
x.SetEpoch(rand.Uint64())
|
||||
x.SetValue(rand.Uint64())
|
||||
|
||||
return x
|
||||
}
|
||||
|
||||
// BasicACL returns random acl.Basic.
|
||||
func BasicACL() (x acl.Basic) {
|
||||
x.FromBits(rand.Uint32())
|
||||
return
|
||||
}
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
package container
|
||||
|
||||
const (
|
||||
// AttributeName is an attribute key that is commonly used to denote
|
||||
// human-friendly name.
|
||||
AttributeName = "Name"
|
||||
|
||||
// AttributeTimestamp is an attribute key that is commonly used to denote
|
||||
// user-defined local time of container creation in Unix Timestamp format.
|
||||
AttributeTimestamp = "Timestamp"
|
||||
)
|
47
crypto/crypto_test.go
Normal file
47
crypto/crypto_test.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
package frostfscrypto_test
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
||||
frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto"
|
||||
frostfsecdsa "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto/ecdsa"
|
||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSignature(t *testing.T) {
|
||||
data := make([]byte, 512)
|
||||
rand.Read(data)
|
||||
|
||||
k, err := keys.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
|
||||
var s frostfscrypto.Signature
|
||||
var m refs.Signature
|
||||
|
||||
for _, f := range []func() frostfscrypto.Signer{
|
||||
func() frostfscrypto.Signer {
|
||||
return frostfsecdsa.Signer(k.PrivateKey)
|
||||
},
|
||||
func() frostfscrypto.Signer {
|
||||
return frostfsecdsa.SignerRFC6979(k.PrivateKey)
|
||||
},
|
||||
func() frostfscrypto.Signer {
|
||||
return frostfsecdsa.SignerWalletConnect(k.PrivateKey)
|
||||
},
|
||||
} {
|
||||
signer := f()
|
||||
|
||||
err := s.Calculate(signer, data)
|
||||
require.NoError(t, err)
|
||||
|
||||
s.WriteToV2(&m)
|
||||
|
||||
require.NoError(t, s.ReadFromV2(m))
|
||||
|
||||
valid := s.Verify(data)
|
||||
require.True(t, valid, "type %T", signer)
|
||||
}
|
||||
}
|
51
crypto/doc.go
Normal file
51
crypto/doc.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
Package frostfscrypto collects FrostFS cryptographic primitives.
|
||||
|
||||
Signer type unifies entities for signing FrostFS data.
|
||||
|
||||
// instantiate Signer
|
||||
// select data to be signed
|
||||
|
||||
var sig Signature
|
||||
|
||||
err := sig.Calculate(signer, data)
|
||||
// ...
|
||||
|
||||
// attach signature to the request
|
||||
|
||||
SDK natively supports several signature schemes that are implemented
|
||||
in nested packages.
|
||||
|
||||
PublicKey allows to verify signatures.
|
||||
|
||||
// get signature to be verified
|
||||
// compose signed data
|
||||
|
||||
isValid := sig.Verify(data)
|
||||
// ...
|
||||
|
||||
Signature can be also used to process FrostFS API V2 protocol messages
|
||||
(see neo.fs.v2.refs package in https://git.frostfs.info/TrueCloudLab/frostfs-api).
|
||||
|
||||
On client side:
|
||||
|
||||
import "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
||||
|
||||
var msg refs.Signature
|
||||
sig.WriteToV2(&msg)
|
||||
|
||||
// send msg
|
||||
|
||||
On server side:
|
||||
|
||||
// recv msg
|
||||
|
||||
var sig frostfscrypto.Signature
|
||||
sig.ReadFromV2(msg)
|
||||
|
||||
// process sig
|
||||
|
||||
Using package types in an application is recommended to potentially work with
|
||||
different protocol versions with which these types are compatible.
|
||||
*/
|
||||
package frostfscrypto
|
12
crypto/ecdsa/doc.go
Normal file
12
crypto/ecdsa/doc.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
Package frostfsecdsa collects ECDSA primitives for FrostFS cryptography.
|
||||
|
||||
Signer and PublicKey support ECDSA signature algorithm with SHA-512 hashing.
|
||||
SignerRFC6979 and PublicKeyRFC6979 implement signature algorithm described in RFC 6979.
|
||||
All these types provide corresponding interfaces from frostfscrypto package.
|
||||
|
||||
Package import causes registration of next signature schemes via frostfscrypto.RegisterScheme:
|
||||
- frostfscrypto.ECDSA_SHA512
|
||||
- frostfscrypto.ECDSA_DETERMINISTIC_SHA256
|
||||
*/
|
||||
package frostfsecdsa
|
17
crypto/ecdsa/init.go
Normal file
17
crypto/ecdsa/init.go
Normal file
|
@ -0,0 +1,17 @@
|
|||
package frostfsecdsa
|
||||
|
||||
import frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto"
|
||||
|
||||
func init() {
|
||||
frostfscrypto.RegisterScheme(frostfscrypto.ECDSA_SHA512, func() frostfscrypto.PublicKey {
|
||||
return new(PublicKey)
|
||||
})
|
||||
|
||||
frostfscrypto.RegisterScheme(frostfscrypto.ECDSA_DETERMINISTIC_SHA256, func() frostfscrypto.PublicKey {
|
||||
return new(PublicKeyRFC6979)
|
||||
})
|
||||
|
||||
frostfscrypto.RegisterScheme(frostfscrypto.ECDSA_WALLETCONNECT, func() frostfscrypto.PublicKey {
|
||||
return new(PublicKeyWalletConnect)
|
||||
})
|
||||
}
|
126
crypto/ecdsa/public.go
Normal file
126
crypto/ecdsa/public.go
Normal file
|
@ -0,0 +1,126 @@
|
|||
package frostfsecdsa
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||
)
|
||||
|
||||
// PublicKey is a wrapper over ecdsa.PublicKey used for FrostFS needs.
|
||||
// Provides frostfscrypto.PublicKey interface.
|
||||
//
|
||||
// Instances MUST be initialized from ecdsa.PublicKey using type conversion.
|
||||
type PublicKey ecdsa.PublicKey
|
||||
|
||||
// MaxEncodedSize returns size of the compressed ECDSA public key.
|
||||
func (x PublicKey) MaxEncodedSize() int {
|
||||
return 33
|
||||
}
|
||||
|
||||
// Encode encodes ECDSA public key in compressed form into buf.
|
||||
// Uses exactly MaxEncodedSize bytes of the buf.
|
||||
//
|
||||
// Encode panics if buf length is less than MaxEncodedSize.
|
||||
//
|
||||
// See also Decode.
|
||||
func (x PublicKey) Encode(buf []byte) int {
|
||||
if len(buf) < 33 {
|
||||
panic(fmt.Sprintf("too short buffer %d", len(buf)))
|
||||
}
|
||||
|
||||
return copy(buf, (*keys.PublicKey)(&x).Bytes())
|
||||
}
|
||||
|
||||
// Decode decodes compressed binary representation of the PublicKey.
|
||||
//
|
||||
// See also Encode.
|
||||
func (x *PublicKey) Decode(data []byte) error {
|
||||
pub, err := keys.NewPublicKeyFromBytes(data, elliptic.P256())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*x = (PublicKey)(*pub)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// similar to elliptic.Unmarshal but without IsOnCurve check.
|
||||
func unmarshalXY(data []byte) (x *big.Int, y *big.Int) {
|
||||
if len(data) != 65 {
|
||||
return
|
||||
} else if data[0] != 4 { // uncompressed form
|
||||
return
|
||||
}
|
||||
|
||||
p := elliptic.P256().Params().P
|
||||
x = new(big.Int).SetBytes(data[1:33])
|
||||
y = new(big.Int).SetBytes(data[33:])
|
||||
|
||||
if x.Cmp(p) >= 0 || y.Cmp(p) >= 0 {
|
||||
x, y = nil, nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Verify verifies data signature calculated by ECDSA algorithm with SHA-512 hashing.
|
||||
func (x PublicKey) Verify(data, signature []byte) bool {
|
||||
h := sha512.Sum512(data)
|
||||
r, s := unmarshalXY(signature)
|
||||
|
||||
return r != nil && s != nil && ecdsa.Verify((*ecdsa.PublicKey)(&x), h[:], r, s)
|
||||
}
|
||||
|
||||
// PublicKeyRFC6979 is a wrapper over ecdsa.PublicKey used for FrostFS needs.
|
||||
// Provides frostfscrypto.PublicKey interface.
|
||||
//
|
||||
// Instances MUST be initialized from ecdsa.PublicKey using type conversion.
|
||||
type PublicKeyRFC6979 ecdsa.PublicKey
|
||||
|
||||
// MaxEncodedSize returns size of the compressed ECDSA public key.
|
||||
func (x PublicKeyRFC6979) MaxEncodedSize() int {
|
||||
return 33
|
||||
}
|
||||
|
||||
// Encode encodes ECDSA public key in compressed form into buf.
|
||||
// Uses exactly MaxEncodedSize bytes of the buf.
|
||||
//
|
||||
// Encode panics if buf length is less than MaxEncodedSize.
|
||||
//
|
||||
// See also Decode.
|
||||
func (x PublicKeyRFC6979) Encode(buf []byte) int {
|
||||
if len(buf) < 33 {
|
||||
panic(fmt.Sprintf("too short buffer %d", len(buf)))
|
||||
}
|
||||
|
||||
return copy(buf, (*keys.PublicKey)(&x).Bytes())
|
||||
}
|
||||
|
||||
// Decode decodes binary representation of the ECDSA public key.
|
||||
//
|
||||
// See also Encode.
|
||||
func (x *PublicKeyRFC6979) Decode(data []byte) error {
|
||||
pub, err := keys.NewPublicKeyFromBytes(data, elliptic.P256())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*x = (PublicKeyRFC6979)(*pub)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Verify verifies data signature calculated by deterministic ECDSA algorithm
|
||||
// with SHA-256 hashing.
|
||||
//
|
||||
// See also RFC 6979.
|
||||
func (x PublicKeyRFC6979) Verify(data, signature []byte) bool {
|
||||
h := sha256.Sum256(data)
|
||||
return (*keys.PublicKey)(&x).Verify(signature, h[:])
|
||||
}
|
77
crypto/ecdsa/signer.go
Normal file
77
crypto/ecdsa/signer.go
Normal file
|
@ -0,0 +1,77 @@
|
|||
package frostfsecdsa
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/sha512"
|
||||
|
||||
frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto"
|
||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||
)
|
||||
|
||||
// Signer wraps ecdsa.PrivateKey and represents signer based on ECDSA with
|
||||
// SHA-512 hashing. Provides frostfscrypto.Signer interface.
|
||||
//
|
||||
// Instances MUST be initialized from ecdsa.PrivateKey using type conversion.
|
||||
type Signer ecdsa.PrivateKey
|
||||
|
||||
// Scheme returns frostfscrypto.ECDSA_SHA512.
|
||||
// Implements frostfscrypto.Signer.
|
||||
func (x Signer) Scheme() frostfscrypto.Scheme {
|
||||
return frostfscrypto.ECDSA_SHA512
|
||||
}
|
||||
|
||||
// Sign signs data using ECDSA algorithm with SHA-512 hashing.
|
||||
// Implements frostfscrypto.Signer.
|
||||
func (x Signer) Sign(data []byte) ([]byte, error) {
|
||||
h := sha512.Sum512(data)
|
||||
r, s, err := ecdsa.Sign(rand.Reader, (*ecdsa.PrivateKey)(&x), h[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
params := elliptic.P256().Params()
|
||||
curveOrderByteSize := params.P.BitLen() / 8
|
||||
|
||||
buf := make([]byte, 1+curveOrderByteSize*2)
|
||||
buf[0] = 4
|
||||
|
||||
_ = r.FillBytes(buf[1 : 1+curveOrderByteSize])
|
||||
_ = s.FillBytes(buf[1+curveOrderByteSize:])
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
// Public initializes PublicKey and returns it as frostfscrypto.PublicKey.
|
||||
// Implements frostfscrypto.Signer.
|
||||
func (x Signer) Public() frostfscrypto.PublicKey {
|
||||
return (*PublicKey)(&x.PublicKey)
|
||||
}
|
||||
|
||||
// SignerRFC6979 wraps ecdsa.PrivateKey and represents signer based on deterministic
|
||||
// ECDSA with SHA-256 hashing (RFC 6979). Provides frostfscrypto.Signer interface.
|
||||
//
|
||||
// Instances SHOULD be initialized from ecdsa.PrivateKey using type conversion.
|
||||
type SignerRFC6979 ecdsa.PrivateKey
|
||||
|
||||
// Scheme returns frostfscrypto.ECDSA_DETERMINISTIC_SHA256.
|
||||
// Implements frostfscrypto.Signer.
|
||||
func (x SignerRFC6979) Scheme() frostfscrypto.Scheme {
|
||||
return frostfscrypto.ECDSA_DETERMINISTIC_SHA256
|
||||
}
|
||||
|
||||
// Sign signs data using deterministic ECDSA algorithm with SHA-256 hashing.
|
||||
// Implements frostfscrypto.Signer.
|
||||
//
|
||||
// See also RFC 6979.
|
||||
func (x SignerRFC6979) Sign(data []byte) ([]byte, error) {
|
||||
p := keys.PrivateKey{PrivateKey: (ecdsa.PrivateKey)(x)}
|
||||
return p.Sign(data), nil
|
||||
}
|
||||
|
||||
// Public initializes PublicKeyRFC6979 and returns it as frostfscrypto.PublicKey.
|
||||
// Implements frostfscrypto.Signer.
|
||||
func (x SignerRFC6979) Public() frostfscrypto.PublicKey {
|
||||
return (*PublicKeyRFC6979)(&x.PublicKey)
|
||||
}
|
85
crypto/ecdsa/wallet_connect.go
Normal file
85
crypto/ecdsa/wallet_connect.go
Normal file
|
@ -0,0 +1,85 @@
|
|||
package frostfsecdsa
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/util/signature/walletconnect"
|
||||
frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto"
|
||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||
)
|
||||
|
||||
// SignerWalletConnect is similar to SignerRFC6979 with 2 changes:
|
||||
// 1. The data is base64 encoded before signing/verifying.
|
||||
// 2. The signature is a concatenation of the signature itself and 16-byte salt.
|
||||
//
|
||||
// Instances MUST be initialized from ecdsa.PrivateKey using type conversion.
|
||||
type SignerWalletConnect ecdsa.PrivateKey
|
||||
|
||||
// Scheme returns frostfscrypto.ECDSA_WALLETCONNECT.
|
||||
// Implements frostfscrypto.Signer.
|
||||
func (x SignerWalletConnect) Scheme() frostfscrypto.Scheme {
|
||||
return frostfscrypto.ECDSA_WALLETCONNECT
|
||||
}
|
||||
|
||||
// Sign signs data using ECDSA algorithm with SHA-512 hashing.
|
||||
// Implements frostfscrypto.Signer.
|
||||
func (x SignerWalletConnect) Sign(data []byte) ([]byte, error) {
|
||||
b64 := make([]byte, base64.StdEncoding.EncodedLen(len(data)))
|
||||
base64.StdEncoding.Encode(b64, data)
|
||||
return walletconnect.Sign((*ecdsa.PrivateKey)(&x), b64)
|
||||
}
|
||||
|
||||
// Public initializes PublicKey and returns it as frostfscrypto.PublicKey.
|
||||
// Implements frostfscrypto.Signer.
|
||||
func (x SignerWalletConnect) Public() frostfscrypto.PublicKey {
|
||||
return (*PublicKeyWalletConnect)(&x.PublicKey)
|
||||
}
|
||||
|
||||
// PublicKeyWalletConnect is a wrapper over ecdsa.PublicKey used for FrostFS needs.
|
||||
// Provides frostfscrypto.PublicKey interface.
|
||||
//
|
||||
// Instances MUST be initialized from ecdsa.PublicKey using type conversion.
|
||||
type PublicKeyWalletConnect ecdsa.PublicKey
|
||||
|
||||
// MaxEncodedSize returns size of the compressed ECDSA public key.
|
||||
func (x PublicKeyWalletConnect) MaxEncodedSize() int {
|
||||
return 33
|
||||
}
|
||||
|
||||
// Encode encodes ECDSA public key in compressed form into buf.
|
||||
// Uses exactly MaxEncodedSize bytes of the buf.
|
||||
//
|
||||
// Encode panics if buf length is less than MaxEncodedSize.
|
||||
//
|
||||
// See also Decode.
|
||||
func (x PublicKeyWalletConnect) Encode(buf []byte) int {
|
||||
if len(buf) < 33 {
|
||||
panic(fmt.Sprintf("too short buffer %d", len(buf)))
|
||||
}
|
||||
|
||||
return copy(buf, (*keys.PublicKey)(&x).Bytes())
|
||||
}
|
||||
|
||||
// Decode decodes compressed binary representation of the PublicKeyWalletConnect.
|
||||
//
|
||||
// See also Encode.
|
||||
func (x *PublicKeyWalletConnect) Decode(data []byte) error {
|
||||
pub, err := keys.NewPublicKeyFromBytes(data, elliptic.P256())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*x = (PublicKeyWalletConnect)(*pub)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Verify verifies data signature calculated by ECDSA algorithm with SHA-512 hashing.
|
||||
func (x PublicKeyWalletConnect) Verify(data, signature []byte) bool {
|
||||
b64 := make([]byte, base64.StdEncoding.EncodedLen(len(data)))
|
||||
base64.StdEncoding.Encode(b64, data)
|
||||
return walletconnect.Verify((*ecdsa.PublicKey)(&x), b64, signature)
|
||||
}
|
102
crypto/signature.go
Normal file
102
crypto/signature.go
Normal file
|
@ -0,0 +1,102 @@
|
|||
package frostfscrypto
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
||||
)
|
||||
|
||||
// Signature represents a confirmation of data integrity received by the
|
||||
// digital signature mechanism.
|
||||
//
|
||||
// Signature is mutually compatible with git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs.Signature
|
||||
// message. See ReadFromV2 / WriteToV2 methods.
|
||||
//
|
||||
// Note that direct typecast is not safe and may result in loss of compatibility:
|
||||
//
|
||||
// _ = Signature(refs.Signature{}) // not recommended
|
||||
type Signature refs.Signature
|
||||
|
||||
// ReadFromV2 reads Signature from the refs.Signature message. Checks if the
|
||||
// message conforms to FrostFS API V2 protocol.
|
||||
//
|
||||
// See also WriteToV2.
|
||||
func (x *Signature) ReadFromV2(m refs.Signature) error {
|
||||
if len(m.GetKey()) == 0 {
|
||||
return errors.New("missing public key")
|
||||
} else if len(m.GetSign()) == 0 {
|
||||
return errors.New("missing signature")
|
||||
}
|
||||
|
||||
switch m.GetScheme() {
|
||||
default:
|
||||
return fmt.Errorf("unsupported scheme %v", m.GetSign())
|
||||
case
|
||||
refs.ECDSA_SHA512,
|
||||
refs.ECDSA_RFC6979_SHA256,
|
||||
refs.ECDSA_RFC6979_SHA256_WALLET_CONNECT:
|
||||
}
|
||||
|
||||
*x = Signature(m)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteToV2 writes Signature to the refs.Signature message.
|
||||
// The message must not be nil.
|
||||
//
|
||||
// See also ReadFromV2.
|
||||
func (x Signature) WriteToV2(m *refs.Signature) {
|
||||
*m = (refs.Signature)(x)
|
||||
}
|
||||
|
||||
// Calculate signs data using Signer and encodes public key for subsequent
|
||||
// verification.
|
||||
//
|
||||
// Signer MUST NOT be nil.
|
||||
//
|
||||
// See also Verify.
|
||||
func (x *Signature) Calculate(signer Signer, data []byte) error {
|
||||
signature, err := signer.Sign(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("signer %T failure: %w", signer, err)
|
||||
}
|
||||
|
||||
pub := signer.Public()
|
||||
|
||||
key := make([]byte, pub.MaxEncodedSize())
|
||||
key = key[:pub.Encode(key)]
|
||||
|
||||
m := (*refs.Signature)(x)
|
||||
|
||||
m.SetScheme(refs.SignatureScheme(signer.Scheme()))
|
||||
m.SetSign(signature)
|
||||
m.SetKey(key)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Verify verifies data signature using encoded public key. True means valid
|
||||
// signature.
|
||||
//
|
||||
// Verify fails if signature scheme is not supported (see RegisterScheme).
|
||||
//
|
||||
// See also Calculate.
|
||||
func (x Signature) Verify(data []byte) bool {
|
||||
m := (*refs.Signature)(&x)
|
||||
|
||||
f, ok := publicKeys[Scheme(m.GetScheme())]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
key := f()
|
||||
|
||||
err := key.Decode(m.GetKey())
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return key.Verify(data, m.GetSign())
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue