forked from TrueCloudLab/frostfs-sdk-go
Compare commits
3 commits
master
...
support/v1
Author | SHA1 | Date | |
---|---|---|---|
|
a1e17ea1f2 | ||
|
129012748e | ||
|
0367c83f5a |
380 changed files with 21658 additions and 38420 deletions
|
@ -1,21 +0,0 @@
|
||||||
name: DCO
|
|
||||||
on: [pull_request]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
dco:
|
|
||||||
name: DCO
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup Go
|
|
||||||
uses: actions/setup-go@v3
|
|
||||||
with:
|
|
||||||
go-version: '1.23'
|
|
||||||
|
|
||||||
- name: Run commit format checker
|
|
||||||
uses: https://git.frostfs.info/TrueCloudLab/dco-go@v3
|
|
||||||
with:
|
|
||||||
from: 'origin/${{ github.event.pull_request.base.ref }}'
|
|
|
@ -1,39 +0,0 @@
|
||||||
name: Tests and linters
|
|
||||||
on: [pull_request]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
lint:
|
|
||||||
name: Lint
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v3
|
|
||||||
with:
|
|
||||||
go-version: '1.23'
|
|
||||||
cache: true
|
|
||||||
|
|
||||||
- name: Install linters
|
|
||||||
run: make lint-install
|
|
||||||
|
|
||||||
- name: Run linters
|
|
||||||
run: make lint
|
|
||||||
|
|
||||||
tests:
|
|
||||||
name: Tests
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
go_versions: [ '1.22', '1.23' ]
|
|
||||||
fail-fast: false
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v3
|
|
||||||
with:
|
|
||||||
go-version: '${{ matrix.go_versions }}'
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: make test
|
|
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -1 +0,0 @@
|
||||||
* @TrueCloudLab/storage-core @TrueCloudLab/storage-services
|
|
45
.github/ISSUE_TEMPLATE/bug_report.md
vendored
45
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -1,45 +0,0 @@
|
||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Create a report to help us improve
|
|
||||||
title: ''
|
|
||||||
labels: community, triage, bug
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<!--- Provide a general summary of the issue in the Title above -->
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
<!--- If you're describing a bug, tell us what should happen -->
|
|
||||||
<!--- If you're suggesting a change/improvement, tell us how it should work -->
|
|
||||||
|
|
||||||
## Current Behavior
|
|
||||||
<!--- If describing a bug, tell us what happens instead of the expected behavior -->
|
|
||||||
<!--- If suggesting a change/improvement, explain the difference from current behavior -->
|
|
||||||
|
|
||||||
## Possible Solution
|
|
||||||
<!--- Not obligatory -->
|
|
||||||
<!--- If no reason/fix/additions for the bug can be suggested, -->
|
|
||||||
<!--- uncomment the following phrase: -->
|
|
||||||
|
|
||||||
<!--- No fix can be suggested by a QA engineer. Further solutions shall be up to developers. -->
|
|
||||||
|
|
||||||
## Steps to Reproduce (for bugs)
|
|
||||||
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
|
|
||||||
<!--- reproduce this bug. -->
|
|
||||||
|
|
||||||
1.
|
|
||||||
|
|
||||||
## Context
|
|
||||||
<!--- How has this issue affected you? What are you trying to accomplish? -->
|
|
||||||
<!--- Providing context helps us come up with a solution that is most useful in the real world -->
|
|
||||||
|
|
||||||
## Regression
|
|
||||||
<!-- Is this issue a regression? (Yes / No) -->
|
|
||||||
<!-- If Yes, optionally please include version or commit id or PR# that caused this regression, if you have these details. -->
|
|
||||||
|
|
||||||
## Your Environment
|
|
||||||
<!--- Include as many relevant details about the environment you experienced the bug in -->
|
|
||||||
* Version used:
|
|
||||||
* Server setup and configuration:
|
|
||||||
* Operating System and version (`uname -a`):
|
|
1
.github/ISSUE_TEMPLATE/config.yml
vendored
1
.github/ISSUE_TEMPLATE/config.yml
vendored
|
@ -1 +0,0 @@
|
||||||
blank_issues_enabled: false
|
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
@ -1,20 +0,0 @@
|
||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea for this project
|
|
||||||
title: ''
|
|
||||||
labels: community, triage
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Is your feature request related to a problem? Please describe.
|
|
||||||
<!--- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
|
|
||||||
|
|
||||||
## Describe the solution you'd like
|
|
||||||
<!--- A clear and concise description of what you want to happen. -->
|
|
||||||
|
|
||||||
## Describe alternatives you've considered
|
|
||||||
<!--- A clear and concise description of any alternative solutions or features you've considered. -->
|
|
||||||
|
|
||||||
## Additional context
|
|
||||||
<!--- Add any other context or screenshots about the feature request here. -->
|
|
21
.github/workflows/dco.yml
vendored
Normal file
21
.github/workflows/dco.yml
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
name: DCO check
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
commits_check_job:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Commits Check
|
||||||
|
steps:
|
||||||
|
- name: Get PR Commits
|
||||||
|
id: 'get-pr-commits'
|
||||||
|
uses: tim-actions/get-pr-commits@master
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: DCO Check
|
||||||
|
uses: tim-actions/dco@master
|
||||||
|
with:
|
||||||
|
commits: ${{ steps.get-pr-commits.outputs.commits }}
|
52
.github/workflows/tests.yml
vendored
Normal file
52
.github/workflows/tests.yml
vendored
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
name: neofs-sdk-go tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
types: [opened, synchronize]
|
||||||
|
paths-ignore:
|
||||||
|
- '**/*.md'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
tests:
|
||||||
|
name: Tests
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go_versions: [ '1.16.x', '1.17.x' ]
|
||||||
|
fail-fast: false
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: '${{ matrix.go_versions }}'
|
||||||
|
|
||||||
|
- name: Restore Go modules from cache
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: /home/runner/go/pkg/mod
|
||||||
|
key: deps-${{ hashFiles('go.sum') }}
|
||||||
|
|
||||||
|
- name: Update Go modules
|
||||||
|
run: make dep
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: make test
|
||||||
|
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: golangci-lint
|
||||||
|
uses: golangci/golangci-lint-action@v2
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
only-new-issues: true
|
12
.gitignore
vendored
12
.gitignore
vendored
|
@ -20,14 +20,4 @@ vendor/
|
||||||
|
|
||||||
# coverage
|
# coverage
|
||||||
coverage.txt
|
coverage.txt
|
||||||
coverage.html
|
coverage.html
|
||||||
|
|
||||||
# antlr tool jar
|
|
||||||
antlr-*.jar
|
|
||||||
|
|
||||||
# tempfiles
|
|
||||||
.cache
|
|
||||||
|
|
||||||
# binary
|
|
||||||
bin/
|
|
||||||
release/
|
|
|
@ -4,16 +4,15 @@
|
||||||
# options for analysis running
|
# options for analysis running
|
||||||
run:
|
run:
|
||||||
# timeout for analysis, e.g. 30s, 5m, default is 1m
|
# timeout for analysis, e.g. 30s, 5m, default is 1m
|
||||||
timeout: 10m
|
timeout: 5m
|
||||||
|
|
||||||
# include test files or not, default is true
|
# include test files or not, default is true
|
||||||
tests: false
|
tests: true
|
||||||
|
|
||||||
# output configuration options
|
# output configuration options
|
||||||
output:
|
output:
|
||||||
# colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number"
|
# colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number"
|
||||||
formats:
|
format: tab
|
||||||
- format: tab
|
|
||||||
|
|
||||||
# all available settings of specific linters
|
# all available settings of specific linters
|
||||||
linters-settings:
|
linters-settings:
|
||||||
|
@ -25,13 +24,6 @@ linters-settings:
|
||||||
govet:
|
govet:
|
||||||
# report about shadowed variables
|
# report about shadowed variables
|
||||||
check-shadowing: false
|
check-shadowing: false
|
||||||
staticcheck:
|
|
||||||
checks: ["all"]
|
|
||||||
funlen:
|
|
||||||
lines: 80 # default 60
|
|
||||||
statements: 60 # default 40
|
|
||||||
gocognit:
|
|
||||||
min-complexity: 40 # default 30
|
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
enable:
|
enable:
|
||||||
|
@ -40,29 +32,28 @@ linters:
|
||||||
- revive
|
- revive
|
||||||
|
|
||||||
# some default golangci-lint linters
|
# some default golangci-lint linters
|
||||||
|
- deadcode
|
||||||
- errcheck
|
- errcheck
|
||||||
- gosimple
|
- gosimple
|
||||||
- godot
|
|
||||||
- ineffassign
|
- ineffassign
|
||||||
- staticcheck
|
- staticcheck
|
||||||
|
- structcheck
|
||||||
- typecheck
|
- typecheck
|
||||||
- unused
|
- unused
|
||||||
|
- varcheck
|
||||||
|
|
||||||
# extra linters
|
# extra linters
|
||||||
- bidichk
|
|
||||||
- durationcheck
|
|
||||||
- exhaustive
|
- exhaustive
|
||||||
- exportloopref
|
- godot
|
||||||
- gofmt
|
- gofmt
|
||||||
- goimports
|
|
||||||
- misspell
|
|
||||||
- predeclared
|
|
||||||
- reassign
|
|
||||||
- whitespace
|
- whitespace
|
||||||
- containedctx
|
- goimports
|
||||||
- funlen
|
|
||||||
- gocognit
|
|
||||||
- contextcheck
|
|
||||||
- protogetter
|
|
||||||
disable-all: true
|
disable-all: true
|
||||||
fast: false
|
fast: false
|
||||||
|
|
||||||
|
issues:
|
||||||
|
include:
|
||||||
|
- EXC0002 # should have a comment
|
||||||
|
- EXC0003 # test/Test ... consider calling this
|
||||||
|
- EXC0004 # govet
|
||||||
|
- EXC0005 # C-style breaks
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
ci:
|
|
||||||
autofix_prs: false
|
|
||||||
|
|
||||||
repos:
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
||||||
rev: v4.4.0
|
|
||||||
hooks:
|
|
||||||
- id: check-added-large-files
|
|
||||||
- id: check-case-conflict
|
|
||||||
- id: check-executables-have-shebangs
|
|
||||||
- id: check-shebang-scripts-are-executable
|
|
||||||
- id: check-merge-conflict
|
|
||||||
- id: check-json
|
|
||||||
- id: check-xml
|
|
||||||
- id: check-yaml
|
|
||||||
- id: trailing-whitespace
|
|
||||||
args: [--markdown-linebreak-ext=md]
|
|
||||||
- id: end-of-file-fixer
|
|
||||||
exclude: "(.key|.interp|.tokens)$"
|
|
||||||
|
|
||||||
- repo: local
|
|
||||||
hooks:
|
|
||||||
- id: go-unit-tests
|
|
||||||
name: go unit tests
|
|
||||||
entry: make test GOFLAGS=''
|
|
||||||
pass_filenames: false
|
|
||||||
types: [go]
|
|
||||||
language: system
|
|
||||||
|
|
||||||
- repo: local
|
|
||||||
hooks:
|
|
||||||
- id: make-lint
|
|
||||||
name: Run Make Lint
|
|
||||||
entry: make lint
|
|
||||||
language: system
|
|
||||||
pass_filenames: false
|
|
|
@ -1,4 +0,0 @@
|
||||||
FROM golang:1.22
|
|
||||||
|
|
||||||
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install make openjdk-17-jre -y
|
|
||||||
WORKDIR /work
|
|
57
Makefile
Executable file → Normal file
57
Makefile
Executable file → Normal file
|
@ -1,16 +1,8 @@
|
||||||
#!/usr/bin/make -f
|
#!/usr/bin/make -f
|
||||||
|
|
||||||
ANTLR_VERSION="4.13.0"
|
|
||||||
TMP_DIR := .cache
|
|
||||||
LINT_VERSION ?= 1.60.1
|
|
||||||
TRUECLOUDLAB_LINT_VERSION ?= 0.0.6
|
|
||||||
OUTPUT_LINT_DIR ?= $(shell pwd)/bin
|
|
||||||
LINT_DIR = $(OUTPUT_LINT_DIR)/golangci-lint-$(LINT_VERSION)-v$(TRUECLOUDLAB_LINT_VERSION)
|
|
||||||
|
|
||||||
# Run tests
|
# Run tests
|
||||||
test: GOFLAGS ?= "-cover -count=1"
|
|
||||||
test:
|
test:
|
||||||
@GOFLAGS=$(GOFLAGS) go test ./...
|
@go test ./... -cover
|
||||||
|
|
||||||
# Pull go dependencies
|
# Pull go dependencies
|
||||||
dep:
|
dep:
|
||||||
|
@ -21,23 +13,9 @@ dep:
|
||||||
@CGO_ENABLED=0 \
|
@CGO_ENABLED=0 \
|
||||||
go mod tidy -v && echo OK
|
go mod tidy -v && echo OK
|
||||||
|
|
||||||
# Install linters
|
|
||||||
lint-install:
|
|
||||||
@mkdir -p $(TMP_DIR)
|
|
||||||
@rm -rf $(TMP_DIR)/linters
|
|
||||||
@git -c advice.detachedHead=false clone --branch v$(TRUECLOUDLAB_LINT_VERSION) https://git.frostfs.info/TrueCloudLab/linters.git $(TMP_DIR)/linters
|
|
||||||
@@make -C $(TMP_DIR)/linters lib CGO_ENABLED=1 OUT_DIR=$(OUTPUT_LINT_DIR)
|
|
||||||
@rm -rf $(TMP_DIR)/linters
|
|
||||||
@rmdir $(TMP_DIR) 2>/dev/null || true
|
|
||||||
@CGO_ENABLED=1 GOBIN=$(LINT_DIR) go install github.com/golangci/golangci-lint/cmd/golangci-lint@v$(LINT_VERSION)
|
|
||||||
|
|
||||||
# Run linters
|
# Run linters
|
||||||
lint:
|
lint:
|
||||||
@if [ ! -d "$(LINT_DIR)" ]; then \
|
@golangci-lint --timeout=5m run
|
||||||
echo "Run make lint-install"; \
|
|
||||||
exit 1; \
|
|
||||||
fi
|
|
||||||
$(LINT_DIR)/golangci-lint run
|
|
||||||
|
|
||||||
# Run tests with race detection and produce coverage output
|
# Run tests with race detection and produce coverage output
|
||||||
cover:
|
cover:
|
||||||
|
@ -51,23 +29,6 @@ format:
|
||||||
@echo "⇒ Processing goimports check"
|
@echo "⇒ Processing goimports check"
|
||||||
@goimports -w ./
|
@goimports -w ./
|
||||||
|
|
||||||
policy:
|
|
||||||
@wget -q https://www.antlr.org/download/antlr-${ANTLR_VERSION}-complete.jar -O antlr4-tool.jar
|
|
||||||
@java -Xmx500M -cp "`pwd`/antlr4-tool.jar" "org.antlr.v4.Tool" -o `pwd`/netmap/parser/ -Dlanguage=Go -no-listener -visitor `pwd`/netmap/parser/Query.g4 `pwd`/netmap/parser/QueryLexer.g4
|
|
||||||
|
|
||||||
# Run `make %` in truecloudlab/frostfs-sdk-go container(Golang+Java)
|
|
||||||
docker/%:
|
|
||||||
@docker build -t truecloudlab/frostfs-sdk-go --platform linux/amd64 . > /dev/null
|
|
||||||
@docker run --rm -t \
|
|
||||||
-v `pwd`:/work \
|
|
||||||
-u "$$(id -u):$$(id -g)" \
|
|
||||||
--env HOME=/work \
|
|
||||||
truecloudlab/frostfs-sdk-go make $*
|
|
||||||
|
|
||||||
# Synchronize tree service
|
|
||||||
sync-tree:
|
|
||||||
@./syncTree.sh
|
|
||||||
|
|
||||||
# Show this help prompt
|
# Show this help prompt
|
||||||
help:
|
help:
|
||||||
@echo ' Usage:'
|
@echo ' Usage:'
|
||||||
|
@ -76,16 +37,4 @@ help:
|
||||||
@echo ''
|
@echo ''
|
||||||
@echo ' Targets:'
|
@echo ' Targets:'
|
||||||
@echo ''
|
@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
|
||||||
|
|
||||||
# Activate pre-commit hooks
|
|
||||||
pre-commit:
|
|
||||||
pre-commit install --hook-type pre-commit
|
|
||||||
|
|
||||||
# Deactivate pre-commit hooks
|
|
||||||
unpre-commit:
|
|
||||||
pre-commit uninstall --hook-type pre-commit
|
|
||||||
|
|
||||||
# Run pre-commit hooks
|
|
||||||
pre-commit-run:
|
|
||||||
@pre-commit run --all-files --hook-stage manual
|
|
74
README.md
74
README.md
|
@ -1,6 +1,6 @@
|
||||||
# frostfs-sdk-go
|
# neofs-sdk-go
|
||||||
Go implementation of FrostFS SDK. It contains high-level version-independent wrappers
|
Go implementation of NeoFS SDK. It contains high-level version-independent wrappers
|
||||||
for structures from [frostfs-api-go](https://git.frostfs.info/TrueCloudLab/frostfs-api-go) as well as
|
for structures from [neofs-api-go](https://github.com/nspcc-dev/neofs-api-go) as well as
|
||||||
helper functions for simplifying node/dApp implementations.
|
helper functions for simplifying node/dApp implementations.
|
||||||
|
|
||||||
## Repository structure
|
## Repository structure
|
||||||
|
@ -10,73 +10,63 @@ Contains fixed-point `Decimal` type for performing balance calculations.
|
||||||
|
|
||||||
### eacl
|
### eacl
|
||||||
Contains Extended ACL types for fine-grained access control.
|
Contains Extended ACL types for fine-grained access control.
|
||||||
There is also a reference implementation of checking algorithm which is used in FrostFS node.
|
There is also a reference implementation of checking algorithm which is used in NeoFS node.
|
||||||
|
|
||||||
### checksum
|
### checksum
|
||||||
Contains `Checksum` type encapsulating checksum as well as it's kind.
|
Contains `Checksum` type encapsulating checksum as well as it's kind.
|
||||||
Currently Sha256 and [Tillich-Zemor hashsum](https://git.frostfs.info/TrueCloudLab/tzhash) are in use.
|
Currently Sha256 and [Tillich-Zemor hashsum](https://github.com/nspcc-dev/tzhash) are in use.
|
||||||
|
|
||||||
### owner
|
### owner
|
||||||
`owner.ID` type represents single account interacting with FrostFS. In v2 version of protocol
|
`owner.ID` type represents single account interacting with NeoFS. In v2 version of protocol
|
||||||
it is just raw bytes behing [base58-encoded address](https://docs.neo.org/docs/en-us/basic/concept/wallets.html#address)
|
it is just raw bytes behing [base58-encoded address](https://docs.neo.org/docs/en-us/basic/concept/wallets.html#address)
|
||||||
in Neo blockchain. Note that for historical reasons it contains
|
in Neo blockchain. Note that for historical reasons it contains
|
||||||
version prefix and checksum in addition to script-hash.
|
version prefix and checksum in addition to script-hash.
|
||||||
|
|
||||||
### token
|
### token
|
||||||
Contains Bearer token type with several FrostFS-specific methods.
|
Contains Bearer token type with several NeoFS-specific methods.
|
||||||
|
|
||||||
### ns
|
### resolver
|
||||||
In FrostFS there are 2 types of name resolution: DNS and NNS. NNS stands for Neo Name Service
|
In NeoFS there are 2 types of name resolution: DNS and NNS. NNS stands for Neo Name Service
|
||||||
is just a [contract](https://git.frostfs.info/TrueCloudLab/frostfs-contract) deployed on a Neo blockchain.
|
is just a [contract](https://github.com/nspcc-dev/neofs-contract/) deployed on a Neo blockchain.
|
||||||
Basically, NNS is just a DNS-on-chain which can be used for resolving container nice-names as well
|
Basically, NNS is just a DNS-on-chain which can be used for resolving container nice-names as well
|
||||||
as any other name in dApps. See our [CoreDNS plugin](https://github.com/nspcc-dev/coredns/tree/master/plugin/nns)
|
as any other name in dApps. See our [CoreDNS plugin](https://github.com/nspcc-dev/coredns/tree/master/plugin/nns)
|
||||||
for the example of how NNS can be integrated in DNS.
|
for the example of how NNS can be integrated in DNS.
|
||||||
|
|
||||||
### session
|
### session
|
||||||
To help lightweight clients interact with FrostFS without sacrificing trust, FrostFS has a concept
|
To help lightweight clients interact with NeoFS without sacrificing trust, NeoFS has a concept
|
||||||
of session token. It is signed by client and allows any node with which a session is established
|
of session token. It is signed by client and allows any node with which a session is established
|
||||||
to perform certain actions on behalf of the user.
|
to perform certain actions on behalf of the user.
|
||||||
|
|
||||||
### client
|
### client
|
||||||
Contains client for working with FrostFS.
|
Contains client for working with NeoFS.
|
||||||
```go
|
```go
|
||||||
var prmInit client.PrmInit
|
c, _ := client.New(
|
||||||
prmInit.SetDefaultPrivateKey(key) // private key for request signing
|
client.WithAddress("localhost:40005"), // endpoint address
|
||||||
|
client.WithDefaultPrivateKey(key), // private key for request signing
|
||||||
var c client.Client
|
client.WithNeoFSErrorHandling(), // enable erroneous status parsing
|
||||||
c.Init(prmInit)
|
client.WithTLSConfig(&tls.Config{})) // custom TLS configuration
|
||||||
|
|
||||||
var prmDial client.PrmDial
|
|
||||||
prmDial.SetServerURI("grpcs://localhost:40005") // endpoint address
|
|
||||||
|
|
||||||
err := c.Dial(prmDial)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5 * time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5 * time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
var prm client.PrmBalanceGet
|
res, err := c.BalanceGet(ctx, owner)
|
||||||
prm.SetAccount(acc)
|
|
||||||
|
|
||||||
res, err := c.BalanceGet(ctx, prm)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Balance for %s: %v\n", acc, res.Amount())
|
fmt.Printf("Balance for %s: %s\n", owner, res.Amount())
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Response status
|
#### Response status
|
||||||
In FrostFS every operation can fail on multiple levels, so a single `error` doesn't suffice,
|
In NeoFS every operation can fail on multiple levels, so a single `error` doesn't suffice,
|
||||||
e.g. consider a case when object was put on 4 out of 5 replicas. Thus, all request execution
|
e.g. consider a case when object was put on 4 out of 5 replicas. Thus, all request execution
|
||||||
details are contained in `Status` returned from every RPC call. dApp can inspect them
|
details are contained in `Status` returned from every RPC call. dApp can inspect them
|
||||||
if needed and perform any desired action. In the case above we may want to report
|
if needed and perform any desired action. In the case above we may want to report
|
||||||
these details to the user as well as retry an operation, possibly with different parameters.
|
these details to the user as well as retry an operation, possibly with different parameters.
|
||||||
Status wire-format is extendable and each node can report any set of details it wants.
|
Status wire-format is extendable and each node can report any set of details it wants.
|
||||||
The set of reserved status codes can be found in
|
The set of reserved status codes can be found in
|
||||||
[FrostFS API](https://git.frostfs.info/TrueCloudLab/frostfs-api/src/branch/master/status/types.proto).
|
[NeoFS API](https://github.com/nspcc-dev/neofs-api/blob/master/status/types.proto). There is also
|
||||||
|
a `client.WithNeoFSErrorHandling()` to seamlessly convert erroneous statuses into Go error type.
|
||||||
|
|
||||||
### policy
|
### policy
|
||||||
Contains helpers allowing conversion of placing policy from/to JSON representation
|
Contains helpers allowing conversion of placing policy from/to JSON representation
|
||||||
|
@ -96,19 +86,19 @@ Contains CRUSH-like implementation of container node selection algorithm. Releva
|
||||||
are described in this paper http://ceur-ws.org/Vol-2344/short10.pdf . Note that it can be
|
are described in this paper http://ceur-ws.org/Vol-2344/short10.pdf . Note that it can be
|
||||||
outdated in some details.
|
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
|
```go
|
||||||
import (
|
import (
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
"github.com/nspcc-dev/neofs-sdk-go/netmap"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
"github.com/nspcc-dev/neofs-sdk-go/object"
|
||||||
)
|
)
|
||||||
|
|
||||||
func placementNodes(addr *object.Address, p *netmap.PlacementPolicy, frostfsNodes []netmap.NodeInfo) {
|
func placementNodes(addr *object.Address, p *netmap.PlacementPolicy, neofsNodes []netmap.NodeInfo) {
|
||||||
// Convert list of nodes in FrostFS API format to the intermediate representation.
|
// Convert list of nodes in NeoFS API format to the intermediate representation.
|
||||||
nodes := netmap.NodesFromInfo(nodes)
|
nodes := netmap.NodesFromInfo(nodes)
|
||||||
|
|
||||||
// Create new netmap (errors are skipped for the sake of clarity).
|
// Create new netmap (errors are skipped for the sake of clarity).
|
||||||
nm, _ := NewNetmap(nodes)
|
nm, _ := NewNetmap(nodes)
|
||||||
|
|
||||||
// Calculate nodes of container.
|
// Calculate nodes of container.
|
||||||
|
@ -120,13 +110,13 @@ func placementNodes(addr *object.Address, p *netmap.PlacementPolicy, frostfsNode
|
||||||
```
|
```
|
||||||
|
|
||||||
### pool
|
### pool
|
||||||
Simple pool for managing connections to FrostFS nodes.
|
Simple pool for managing connections to NeoFS nodes.
|
||||||
|
|
||||||
### acl, checksum, version, signature
|
### acl, checksum, version, signature
|
||||||
Contain simple API wrappers.
|
Contain simple API wrappers.
|
||||||
|
|
||||||
### logger
|
### logger
|
||||||
Wrapper over `zap.Logger` which is used across FrostFS codebase.
|
Wrapper over `zap.Logger` which is used across NeoFS codebase.
|
||||||
|
|
||||||
### util
|
### util
|
||||||
Utilities for working with signature-related code.
|
Utilities for working with signature-related code.
|
|
@ -1,64 +1,69 @@
|
||||||
package accounting
|
package accounting
|
||||||
|
|
||||||
import "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/accounting"
|
import "github.com/nspcc-dev/neofs-api-go/v2/accounting"
|
||||||
|
|
||||||
// Decimal represents decimal number for accounting operations.
|
// Decimal represents decimal number for accounting operations.
|
||||||
//
|
|
||||||
// Decimal is mutually compatible with git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/accounting.Decimal
|
|
||||||
// message. See ReadFromV2 / WriteToV2 methods.
|
|
||||||
//
|
|
||||||
// Instances can be created using built-in var declaration.
|
|
||||||
//
|
|
||||||
// Note that direct typecast is not safe and may result in loss of compatibility:
|
|
||||||
//
|
|
||||||
// _ = Decimal(accounting.Decimal{}) // not recommended
|
|
||||||
type Decimal accounting.Decimal
|
type Decimal accounting.Decimal
|
||||||
|
|
||||||
// ReadFromV2 reads Decimal from the accounting.Decimal message. Checks if the
|
// NewDecimal creates, initializes and returns empty Decimal instance.
|
||||||
// message conforms to FrostFS API V2 protocol.
|
|
||||||
//
|
//
|
||||||
// See also WriteToV2.
|
// Defaults:
|
||||||
func (d *Decimal) ReadFromV2(m accounting.Decimal) error {
|
// - value: 0
|
||||||
*d = Decimal(m)
|
// - precision: 0
|
||||||
return nil
|
func NewDecimal() *Decimal {
|
||||||
|
return NewDecimalFromV2(new(accounting.Decimal))
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteToV2 writes Decimal to the accounting.Decimal message.
|
// NewDecimalFromV2 converts v2 Decimal to Decimal.
|
||||||
// The message must not be nil.
|
|
||||||
//
|
//
|
||||||
// See also ReadFromV2.
|
// Nil Decimal converts to nil.
|
||||||
func (d Decimal) WriteToV2(m *accounting.Decimal) {
|
func NewDecimalFromV2(d *accounting.Decimal) *Decimal {
|
||||||
*m = (accounting.Decimal)(d)
|
return (*Decimal)(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToV2 returns the v2 Decimal message.
|
||||||
|
//
|
||||||
|
// Nil Decimal converts to nil.
|
||||||
|
func (d *Decimal) ToV2() *accounting.Decimal {
|
||||||
|
return (*accounting.Decimal)(d)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Value returns value of the decimal number.
|
// Value returns value of the decimal number.
|
||||||
//
|
func (d *Decimal) Value() int64 {
|
||||||
// Zero Decimal has zero value.
|
return (*accounting.Decimal)(d).GetValue()
|
||||||
//
|
|
||||||
// See also SetValue.
|
|
||||||
func (d Decimal) Value() int64 {
|
|
||||||
return (*accounting.Decimal)(&d).GetValue()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetValue sets value of the decimal number.
|
// SetValue sets value of the decimal number.
|
||||||
//
|
|
||||||
// See also Value.
|
|
||||||
func (d *Decimal) SetValue(v int64) {
|
func (d *Decimal) SetValue(v int64) {
|
||||||
(*accounting.Decimal)(d).SetValue(v)
|
(*accounting.Decimal)(d).SetValue(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Precision returns precision of the decimal number.
|
// Precision returns precision of the decimal number.
|
||||||
//
|
func (d *Decimal) Precision() uint32 {
|
||||||
// Zero Decimal has zero precision.
|
return (*accounting.Decimal)(d).GetPrecision()
|
||||||
//
|
|
||||||
// See also SetPrecision.
|
|
||||||
func (d Decimal) Precision() uint32 {
|
|
||||||
return (*accounting.Decimal)(&d).GetPrecision()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetPrecision sets precision of the decimal number.
|
// SetPrecision sets precision of the decimal number.
|
||||||
//
|
|
||||||
// See also Precision.
|
|
||||||
func (d *Decimal) SetPrecision(p uint32) {
|
func (d *Decimal) SetPrecision(p uint32) {
|
||||||
(*accounting.Decimal)(d).SetPrecision(p)
|
(*accounting.Decimal)(d).SetPrecision(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Marshal marshals Decimal into a protobuf binary form.
|
||||||
|
func (d *Decimal) Marshal() ([]byte, error) {
|
||||||
|
return (*accounting.Decimal)(d).StableMarshal(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal unmarshalls protobuf binary representation of Decimal.
|
||||||
|
func (d *Decimal) Unmarshal(data []byte) error {
|
||||||
|
return (*accounting.Decimal)(d).Unmarshal(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON encodes Decimal to protobuf JSON format.
|
||||||
|
func (d *Decimal) MarshalJSON() ([]byte, error) {
|
||||||
|
return (*accounting.Decimal)(d).MarshalJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON decodes Decimal from protobuf JSON format.
|
||||||
|
func (d *Decimal) UnmarshalJSON(data []byte) error {
|
||||||
|
return (*accounting.Decimal)(d).UnmarshalJSON(data)
|
||||||
|
}
|
||||||
|
|
|
@ -3,19 +3,15 @@ package accounting_test
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
v2accounting "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/accounting"
|
"github.com/nspcc-dev/neofs-sdk-go/accounting"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/accounting"
|
accountingtest "github.com/nspcc-dev/neofs-sdk-go/accounting/test"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDecimalData(t *testing.T) {
|
func TestDecimal(t *testing.T) {
|
||||||
const v, p = 4, 2
|
const v, p = 4, 2
|
||||||
|
|
||||||
var d accounting.Decimal
|
d := accounting.NewDecimal()
|
||||||
|
|
||||||
require.Zero(t, d.Value())
|
|
||||||
require.Zero(t, d.Precision())
|
|
||||||
|
|
||||||
d.SetValue(v)
|
d.SetValue(v)
|
||||||
d.SetPrecision(p)
|
d.SetPrecision(p)
|
||||||
|
|
||||||
|
@ -23,24 +19,26 @@ func TestDecimalData(t *testing.T) {
|
||||||
require.EqualValues(t, p, d.Precision())
|
require.EqualValues(t, p, d.Precision())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDecimalMessageV2(t *testing.T) {
|
func TestDecimalEncoding(t *testing.T) {
|
||||||
var (
|
d := accountingtest.Decimal()
|
||||||
d accounting.Decimal
|
|
||||||
m v2accounting.Decimal
|
|
||||||
)
|
|
||||||
|
|
||||||
m.SetValue(7)
|
t.Run("binary", func(t *testing.T) {
|
||||||
m.SetPrecision(8)
|
data, err := d.Marshal()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.NoError(t, d.ReadFromV2(m))
|
d2 := accounting.NewDecimal()
|
||||||
|
require.NoError(t, d2.Unmarshal(data))
|
||||||
|
|
||||||
require.EqualValues(t, m.GetValue(), d.Value())
|
require.Equal(t, d, d2)
|
||||||
require.EqualValues(t, m.GetPrecision(), d.Precision())
|
})
|
||||||
|
|
||||||
var m2 v2accounting.Decimal
|
t.Run("json", func(t *testing.T) {
|
||||||
|
data, err := d.MarshalJSON()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
d.WriteToV2(&m2)
|
d2 := accounting.NewDecimal()
|
||||||
|
require.NoError(t, d2.UnmarshalJSON(data))
|
||||||
|
|
||||||
require.EqualValues(t, d.Value(), m2.GetValue())
|
require.Equal(t, d, d2)
|
||||||
require.EqualValues(t, d.Precision(), m2.GetPrecision())
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
/*
|
|
||||||
Package accounting provides primitives to perform accounting operations in FrostFS.
|
|
||||||
|
|
||||||
Decimal type provides functionality to process user balances. For example, when
|
|
||||||
working with Fixed8 balance precision:
|
|
||||||
|
|
||||||
var dec accounting.Decimal
|
|
||||||
dec.SetValue(val)
|
|
||||||
dec.SetPrecision(8)
|
|
||||||
|
|
||||||
Instances can be also used to process FrostFS API V2 protocol messages
|
|
||||||
(see neo.fs.v2.accounting package in https://git.frostfs.info/TrueCloudLab/frostfs-api).
|
|
||||||
|
|
||||||
On client side:
|
|
||||||
|
|
||||||
import "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/accounting"
|
|
||||||
|
|
||||||
var msg accounting.Decimal
|
|
||||||
dec.WriteToV2(&msg)
|
|
||||||
|
|
||||||
// send msg
|
|
||||||
|
|
||||||
On server side:
|
|
||||||
|
|
||||||
// recv msg
|
|
||||||
|
|
||||||
var dec accounting.Decimal
|
|
||||||
dec.ReadFromV2(msg)
|
|
||||||
|
|
||||||
// process dec
|
|
||||||
|
|
||||||
Using package types in an application is recommended to potentially work with
|
|
||||||
different protocol versions with which these types are compatible.
|
|
||||||
*/
|
|
||||||
package accounting
|
|
|
@ -3,14 +3,14 @@ package accountingtest
|
||||||
import (
|
import (
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/accounting"
|
"github.com/nspcc-dev/neofs-sdk-go/accounting"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Decimal returns random accounting.Decimal.
|
// Decimal returns random accounting.Decimal.
|
||||||
func Decimal() *accounting.Decimal {
|
func Decimal() *accounting.Decimal {
|
||||||
var d accounting.Decimal
|
d := accounting.NewDecimal()
|
||||||
d.SetValue(rand.Int63())
|
d.SetValue(rand.Int63())
|
||||||
d.SetPrecision(rand.Uint32())
|
d.SetPrecision(rand.Uint32())
|
||||||
|
|
||||||
return &d
|
return d
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
/*
|
|
||||||
Package accountingtest provides functions for convenient testing of accounting package API.
|
|
||||||
|
|
||||||
Note that importing the package into source files is highly discouraged.
|
|
||||||
|
|
||||||
Random instance generation functions can be useful when testing expects any value, e.g.:
|
|
||||||
|
|
||||||
import accountingtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/accounting/test"
|
|
||||||
|
|
||||||
dec := accountingtest.Decimal()
|
|
||||||
// test the value
|
|
||||||
*/
|
|
||||||
package accountingtest
|
|
105
acl/types.go
Normal file
105
acl/types.go
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
package acl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BasicACL is Access Control List that defines who can interact with containers and what exactly they can do.
|
||||||
|
type BasicACL uint32
|
||||||
|
|
||||||
|
func (a BasicACL) String() string {
|
||||||
|
return fmt.Sprintf("0x%08x", uint32(a))
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// PublicBasicRule is a basic ACL value for final public-read-write container for which extended ACL CANNOT be set.
|
||||||
|
PublicBasicRule BasicACL = 0x1FBFBFFF
|
||||||
|
|
||||||
|
// PrivateBasicRule is a basic ACL value for final private container for which extended ACL CANNOT be set.
|
||||||
|
PrivateBasicRule BasicACL = 0x1C8C8CCC
|
||||||
|
|
||||||
|
// ReadOnlyBasicRule is a basic ACL value for final public-read container for which extended ACL CANNOT be set.
|
||||||
|
ReadOnlyBasicRule BasicACL = 0x1FBF8CFF
|
||||||
|
|
||||||
|
// PublicAppendRule is a basic ACL value for final public-append container for which extended ACL CANNOT be set.
|
||||||
|
PublicAppendRule BasicACL = 0x1FBF9FFF
|
||||||
|
|
||||||
|
// EACLPublicBasicRule is a basic ACL value for non-final public-read-write container for which extended ACL CAN be set.
|
||||||
|
EACLPublicBasicRule BasicACL = 0x0FBFBFFF
|
||||||
|
|
||||||
|
// EACLPrivateBasicRule is a basic ACL value for non-final private container for which extended ACL CAN be set.
|
||||||
|
EACLPrivateBasicRule BasicACL = 0x0C8C8CCC
|
||||||
|
|
||||||
|
// EACLReadOnlyBasicRule is a basic ACL value for non-final public-read container for which extended ACL CAN be set.
|
||||||
|
EACLReadOnlyBasicRule BasicACL = 0x0FBF8CFF
|
||||||
|
|
||||||
|
// EACLPublicAppendRule is a basic ACL value for non-final public-append container for which extended ACL CAN be set.
|
||||||
|
EACLPublicAppendRule BasicACL = 0x0FBF9FFF
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// PublicBasicName is a well-known name for 0x1FBFBFFF basic ACL.
|
||||||
|
// It represents fully-public container without eACL.
|
||||||
|
PublicBasicName = "public-read-write"
|
||||||
|
|
||||||
|
// PrivateBasicName is a well-known name for 0x1C8C8CCC basic ACL.
|
||||||
|
// It represents fully-private container without eACL.
|
||||||
|
PrivateBasicName = "private"
|
||||||
|
|
||||||
|
// ReadOnlyBasicName is a well-known name for 0x1FBF8CFF basic ACL.
|
||||||
|
// It represents public read-only container without eACL.
|
||||||
|
ReadOnlyBasicName = "public-read"
|
||||||
|
|
||||||
|
// PublicAppendName is a well-known name for 0x1FBF9FFF basic ACL.
|
||||||
|
// It represents fully-public container without eACL except DELETE operation is only allowed on the owner.
|
||||||
|
PublicAppendName = "public-append"
|
||||||
|
|
||||||
|
// EACLPublicBasicName is a well-known name for 0x0FBFBFFF basic ACL.
|
||||||
|
// It represents fully-public container that allows eACL.
|
||||||
|
EACLPublicBasicName = "eacl-public-read-write"
|
||||||
|
|
||||||
|
// EACLPrivateBasicName is a well-known name for 0x0C8C8CCC basic ACL.
|
||||||
|
// It represents fully-private container that allows eACL.
|
||||||
|
EACLPrivateBasicName = "eacl-private"
|
||||||
|
|
||||||
|
// EACLReadOnlyBasicName is a well-known name for 0x0FBF8CFF basic ACL.
|
||||||
|
// It represents public read-only container that allows eACL.
|
||||||
|
EACLReadOnlyBasicName = "eacl-public-read"
|
||||||
|
|
||||||
|
// EACLPublicAppendName is a well-known name for 0x0FBF9FFF basic ACL.
|
||||||
|
// It represents fully-public container that allows eACL except DELETE operation is only allowed on the owner.
|
||||||
|
EACLPublicAppendName = "eacl-public-append"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseBasicACL parse string ACL (well-known names or hex representation).
|
||||||
|
func ParseBasicACL(basicACL string) (BasicACL, error) {
|
||||||
|
switch basicACL {
|
||||||
|
case PublicBasicName:
|
||||||
|
return PublicBasicRule, nil
|
||||||
|
case PrivateBasicName:
|
||||||
|
return PrivateBasicRule, nil
|
||||||
|
case ReadOnlyBasicName:
|
||||||
|
return ReadOnlyBasicRule, nil
|
||||||
|
case PublicAppendName:
|
||||||
|
return PublicAppendRule, nil
|
||||||
|
case EACLPublicBasicName:
|
||||||
|
return EACLPublicBasicRule, nil
|
||||||
|
case EACLPrivateBasicName:
|
||||||
|
return EACLPrivateBasicRule, nil
|
||||||
|
case EACLReadOnlyBasicName:
|
||||||
|
return EACLReadOnlyBasicRule, nil
|
||||||
|
case EACLPublicAppendName:
|
||||||
|
return EACLPublicAppendRule, nil
|
||||||
|
default:
|
||||||
|
basicACL = strings.TrimPrefix(strings.ToLower(basicACL), "0x")
|
||||||
|
|
||||||
|
value, err := strconv.ParseUint(basicACL, 16, 32)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("can't parse basic ACL: %s", basicACL)
|
||||||
|
}
|
||||||
|
|
||||||
|
return BasicACL(value), nil
|
||||||
|
}
|
||||||
|
}
|
82
acl/types_test.go
Normal file
82
acl/types_test.go
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
package acl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParser(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
acl string
|
||||||
|
expected BasicACL
|
||||||
|
err bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
acl: PublicBasicName,
|
||||||
|
expected: PublicBasicRule,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
acl: PrivateBasicName,
|
||||||
|
expected: PrivateBasicRule,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
acl: ReadOnlyBasicName,
|
||||||
|
expected: ReadOnlyBasicRule,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
acl: PublicAppendName,
|
||||||
|
expected: PublicAppendRule,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
acl: EACLPublicBasicName,
|
||||||
|
expected: EACLPublicBasicRule,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
acl: EACLPrivateBasicName,
|
||||||
|
expected: EACLPrivateBasicRule,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
acl: EACLReadOnlyBasicName,
|
||||||
|
expected: EACLReadOnlyBasicRule,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
acl: EACLPublicAppendName,
|
||||||
|
expected: EACLPublicAppendRule,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
acl: "0x1C8C8CCC",
|
||||||
|
expected: 0x1C8C8CCC,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
acl: "1C8C8CCC",
|
||||||
|
expected: 0x1C8C8CCC,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
acl: "123456789",
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
acl: "0x1C8C8CCG",
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
actual, err := ParseBasicACL(tc.acl)
|
||||||
|
if tc.err {
|
||||||
|
require.Error(t, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tc.expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestString(t *testing.T) {
|
||||||
|
acl := BasicACL(0x1fbfbfff)
|
||||||
|
require.Equal(t, "0x1fbfbfff", acl.String())
|
||||||
|
|
||||||
|
acl2, err := ParseBasicACL(PrivateBasicName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "0x1c8c8ccc", acl2.String())
|
||||||
|
}
|
52
ape/chain.go
52
ape/chain.go
|
@ -1,52 +0,0 @@
|
||||||
package ape
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
apeV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/ape"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrInvalidChainRepresentation = errors.New("invalid chain representation")
|
|
||||||
)
|
|
||||||
|
|
||||||
// ChainID is Chain's identifier.
|
|
||||||
type ChainID []byte
|
|
||||||
|
|
||||||
// Chain is an SDK representation for v2's Chain.
|
|
||||||
//
|
|
||||||
// Note that Chain (as well as v2's Chain) and all related entities
|
|
||||||
// are NOT operated by Access-Policy-Engine (APE). The client is responsible
|
|
||||||
// to convert these types to policy-engine entities.
|
|
||||||
type Chain struct {
|
|
||||||
// Raw is the encoded chain kind.
|
|
||||||
// It assumes that Raw's bytes are the result of encoding provided by
|
|
||||||
// policy-engine package.
|
|
||||||
Raw []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToV2 converts Chain to v2.
|
|
||||||
func (c *Chain) ToV2() *apeV2.Chain {
|
|
||||||
v2ct := new(apeV2.Chain)
|
|
||||||
|
|
||||||
if c.Raw != nil {
|
|
||||||
v2Raw := new(apeV2.ChainRaw)
|
|
||||||
v2Raw.SetRaw(c.Raw)
|
|
||||||
v2ct.SetKind(v2Raw)
|
|
||||||
}
|
|
||||||
|
|
||||||
return v2ct
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadFromV2 fills Chain from v2.
|
|
||||||
func (c *Chain) ReadFromV2(v2ct *apeV2.Chain) error {
|
|
||||||
switch v := v2ct.GetKind().(type) {
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported chain kind: %T", v)
|
|
||||||
case *apeV2.ChainRaw:
|
|
||||||
raw := v.GetRaw()
|
|
||||||
c.Raw = raw
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
package ape
|
|
||||||
|
|
||||||
import (
|
|
||||||
apeV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/ape"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TargetType is an SDK representation for v2's TargetType.
|
|
||||||
type TargetType apeV2.TargetType
|
|
||||||
|
|
||||||
const (
|
|
||||||
TargetTypeUndefined TargetType = iota
|
|
||||||
TargetTypeNamespace
|
|
||||||
TargetTypeContainer
|
|
||||||
TargetTypeUser
|
|
||||||
TargetTypeGroup
|
|
||||||
)
|
|
||||||
|
|
||||||
// ToV2 converts TargetType to v2.
|
|
||||||
func (targetType TargetType) ToV2() apeV2.TargetType {
|
|
||||||
return apeV2.TargetType(targetType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromV2 reads TargetType to v2.
|
|
||||||
func (targetType *TargetType) FromV2(v2targetType apeV2.TargetType) {
|
|
||||||
*targetType = TargetType(v2targetType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChainTarget is an SDK representation for v2's ChainTarget.
|
|
||||||
//
|
|
||||||
// Note that ChainTarget (as well as v2's ChainTarget) and all related entities
|
|
||||||
// are NOT operated by Access-Policy-Engine (APE). The client is responsible
|
|
||||||
// to convert these types to policy-engine entities.
|
|
||||||
type ChainTarget struct {
|
|
||||||
TargetType TargetType
|
|
||||||
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToV2 converts ChainTarget to v2.
|
|
||||||
func (ct *ChainTarget) ToV2() *apeV2.ChainTarget {
|
|
||||||
v2ct := new(apeV2.ChainTarget)
|
|
||||||
|
|
||||||
v2ct.SetTargetType(ct.TargetType.ToV2())
|
|
||||||
v2ct.SetName(ct.Name)
|
|
||||||
|
|
||||||
return v2ct
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromV2 reads ChainTarget frpm v2.
|
|
||||||
func (ct *ChainTarget) FromV2(v2ct *apeV2.ChainTarget) {
|
|
||||||
ct.TargetType.FromV2(v2ct.GetTargetType())
|
|
||||||
ct.Name = v2ct.GetName()
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
package ape_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/ape"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
apeV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/ape"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
m = map[ape.TargetType]apeV2.TargetType{
|
|
||||||
ape.TargetTypeUndefined: apeV2.TargetTypeUndefined,
|
|
||||||
ape.TargetTypeNamespace: apeV2.TargetTypeNamespace,
|
|
||||||
ape.TargetTypeContainer: apeV2.TargetTypeContainer,
|
|
||||||
ape.TargetTypeUser: apeV2.TargetTypeUser,
|
|
||||||
ape.TargetTypeGroup: apeV2.TargetTypeGroup,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestTargetType(t *testing.T) {
|
|
||||||
for typesdk, typev2 := range m {
|
|
||||||
t.Run("from sdk to v2 "+typev2.String(), func(t *testing.T) {
|
|
||||||
v2 := typesdk.ToV2()
|
|
||||||
require.Equal(t, v2, typev2)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("from v2 to sdk "+typev2.String(), func(t *testing.T) {
|
|
||||||
var typ ape.TargetType
|
|
||||||
typ.FromV2(typev2)
|
|
||||||
require.Equal(t, typesdk, typ)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestChainTarget(t *testing.T) {
|
|
||||||
var (
|
|
||||||
typ = ape.TargetTypeNamespace
|
|
||||||
name = "namespaceXXYYZZ"
|
|
||||||
)
|
|
||||||
|
|
||||||
t.Run("from sdk to v2", func(t *testing.T) {
|
|
||||||
ct := ape.ChainTarget{
|
|
||||||
TargetType: typ,
|
|
||||||
Name: name,
|
|
||||||
}
|
|
||||||
|
|
||||||
v2 := ct.ToV2()
|
|
||||||
require.Equal(t, m[typ], v2.GetTargetType())
|
|
||||||
require.Equal(t, name, v2.GetName())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("from v2 to sdk", func(t *testing.T) {
|
|
||||||
v2 := &apeV2.ChainTarget{}
|
|
||||||
v2.SetTargetType(m[typ])
|
|
||||||
v2.SetName(name)
|
|
||||||
|
|
||||||
var ct ape.ChainTarget
|
|
||||||
ct.FromV2(v2)
|
|
||||||
|
|
||||||
require.Equal(t, typ, ct.TargetType)
|
|
||||||
require.Equal(t, name, ct.Name)
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
package ape_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/ape"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
apeV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/ape"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
encoded = `{"ID":"","Rules":[{"Status":"Allow","Actions":{"Inverted":false,"Names":["GetObject"]},"Resources":{"Inverted":false,"Names":["native:object/*"]},"Any":false,"Condition":[{"Op":"StringEquals","Object":"Resource","Key":"Department","Value":"HR"}]}],"MatchType":"DenyPriority"}`
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestChainData(t *testing.T) {
|
|
||||||
t.Run("raw chain", func(t *testing.T) {
|
|
||||||
var c ape.Chain
|
|
||||||
|
|
||||||
b := []byte(encoded)
|
|
||||||
c.Raw = b
|
|
||||||
|
|
||||||
v2, ok := c.ToV2().GetKind().(*apeV2.ChainRaw)
|
|
||||||
require.True(t, ok)
|
|
||||||
require.Equal(t, b, v2.Raw)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestChainMessageV2(t *testing.T) {
|
|
||||||
b := []byte(encoded)
|
|
||||||
|
|
||||||
v2Raw := &apeV2.ChainRaw{}
|
|
||||||
v2Raw.SetRaw(b)
|
|
||||||
|
|
||||||
v2 := &apeV2.Chain{}
|
|
||||||
v2.SetKind(v2Raw)
|
|
||||||
|
|
||||||
var c ape.Chain
|
|
||||||
c.ReadFromV2(v2)
|
|
||||||
|
|
||||||
require.NotNil(t, c.Raw)
|
|
||||||
require.Equal(t, b, c.Raw)
|
|
||||||
}
|
|
291
audit/result.go
Normal file
291
audit/result.go
Normal file
|
@ -0,0 +1,291 @@
|
||||||
|
package audit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/nspcc-dev/neofs-api-go/v2/audit"
|
||||||
|
"github.com/nspcc-dev/neofs-api-go/v2/refs"
|
||||||
|
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
|
||||||
|
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
|
||||||
|
"github.com/nspcc-dev/neofs-sdk-go/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Result represents v2-compatible data audit result.
|
||||||
|
type Result audit.DataAuditResult
|
||||||
|
|
||||||
|
// NewFromV2 wraps v2 DataAuditResult message to Result.
|
||||||
|
//
|
||||||
|
// Nil audit.DataAuditResult converts to nil.
|
||||||
|
func NewResultFromV2(aV2 *audit.DataAuditResult) *Result {
|
||||||
|
return (*Result)(aV2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates and initializes blank Result.
|
||||||
|
//
|
||||||
|
// Defaults:
|
||||||
|
// - version: version.Current();
|
||||||
|
// - complete: false;
|
||||||
|
// - cid: nil;
|
||||||
|
// - pubKey: nil;
|
||||||
|
// - passSG, failSG: nil;
|
||||||
|
// - failNodes, passNodes: nil;
|
||||||
|
// - hit, miss, fail: 0;
|
||||||
|
// - requests, retries: 0;
|
||||||
|
// - auditEpoch: 0.
|
||||||
|
func NewResult() *Result {
|
||||||
|
r := NewResultFromV2(new(audit.DataAuditResult))
|
||||||
|
r.SetVersion(version.Current())
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToV2 converts Result to v2 DataAuditResult message.
|
||||||
|
//
|
||||||
|
// Nil Result converts to nil.
|
||||||
|
func (r *Result) ToV2() *audit.DataAuditResult {
|
||||||
|
return (*audit.DataAuditResult)(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal marshals Result into a protobuf binary form.
|
||||||
|
func (r *Result) Marshal() ([]byte, error) {
|
||||||
|
return (*audit.DataAuditResult)(r).StableMarshal(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal unmarshals protobuf binary representation of Result.
|
||||||
|
func (r *Result) Unmarshal(data []byte) error {
|
||||||
|
return (*audit.DataAuditResult)(r).Unmarshal(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON encodes Result to protobuf JSON format.
|
||||||
|
func (r *Result) MarshalJSON() ([]byte, error) {
|
||||||
|
return (*audit.DataAuditResult)(r).MarshalJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON decodes Result from protobuf JSON format.
|
||||||
|
func (r *Result) UnmarshalJSON(data []byte) error {
|
||||||
|
return (*audit.DataAuditResult)(r).UnmarshalJSON(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version returns Data Audit structure version.
|
||||||
|
func (r *Result) Version() *version.Version {
|
||||||
|
return version.NewFromV2(
|
||||||
|
(*audit.DataAuditResult)(r).GetVersion())
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetVersion sets Data Audit structure version.
|
||||||
|
func (r *Result) SetVersion(v *version.Version) {
|
||||||
|
(*audit.DataAuditResult)(r).SetVersion(v.ToV2())
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuditEpoch returns epoch number when the Data Audit was conducted.
|
||||||
|
func (r *Result) AuditEpoch() uint64 {
|
||||||
|
return (*audit.DataAuditResult)(r).GetAuditEpoch()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAuditEpoch sets epoch number when the Data Audit was conducted.
|
||||||
|
func (r *Result) SetAuditEpoch(epoch uint64) {
|
||||||
|
(*audit.DataAuditResult)(r).SetAuditEpoch(epoch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainerID returns container under audit.
|
||||||
|
func (r *Result) ContainerID() *cid.ID {
|
||||||
|
return cid.NewFromV2(
|
||||||
|
(*audit.DataAuditResult)(r).GetContainerID())
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetContainerID sets container under audit.
|
||||||
|
func (r *Result) SetContainerID(id *cid.ID) {
|
||||||
|
(*audit.DataAuditResult)(r).SetContainerID(id.ToV2())
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublicKey returns public key of the auditing InnerRing node in a binary format.
|
||||||
|
func (r *Result) PublicKey() []byte {
|
||||||
|
return (*audit.DataAuditResult)(r).GetPublicKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPublicKey sets public key of the auditing InnerRing node in a binary format.
|
||||||
|
func (r *Result) SetPublicKey(key []byte) {
|
||||||
|
(*audit.DataAuditResult)(r).SetPublicKey(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete returns completion state of audit result.
|
||||||
|
func (r *Result) Complete() bool {
|
||||||
|
return (*audit.DataAuditResult)(r).GetComplete()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetComplete sets completion state of audit result.
|
||||||
|
func (r *Result) SetComplete(v bool) {
|
||||||
|
(*audit.DataAuditResult)(r).SetComplete(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Requests returns number of requests made by PoR audit check to get
|
||||||
|
// all headers of the objects inside storage groups.
|
||||||
|
func (r *Result) Requests() uint32 {
|
||||||
|
return (*audit.DataAuditResult)(r).GetRequests()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRequests sets number of requests made by PoR audit check to get
|
||||||
|
// all headers of the objects inside storage groups.
|
||||||
|
func (r *Result) SetRequests(v uint32) {
|
||||||
|
(*audit.DataAuditResult)(r).SetRequests(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retries returns number of retries made by PoR audit check to get
|
||||||
|
// all headers of the objects inside storage groups.
|
||||||
|
func (r *Result) Retries() uint32 {
|
||||||
|
return (*audit.DataAuditResult)(r).GetRetries()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRetries sets number of retries made by PoR audit check to get
|
||||||
|
// all headers of the objects inside storage groups.
|
||||||
|
func (r *Result) SetRetries(v uint32) {
|
||||||
|
(*audit.DataAuditResult)(r).SetRetries(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PassSG returns list of Storage Groups that passed audit PoR stage.
|
||||||
|
func (r *Result) PassSG() []oid.ID {
|
||||||
|
mV2 := (*audit.DataAuditResult)(r).
|
||||||
|
GetPassSG()
|
||||||
|
|
||||||
|
if mV2 == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m := make([]oid.ID, len(mV2))
|
||||||
|
|
||||||
|
for i := range mV2 {
|
||||||
|
m[i] = *oid.NewIDFromV2(&mV2[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPassSG sets list of Storage Groups that passed audit PoR stage.
|
||||||
|
func (r *Result) SetPassSG(list []oid.ID) {
|
||||||
|
mV2 := (*audit.DataAuditResult)(r).
|
||||||
|
GetPassSG()
|
||||||
|
|
||||||
|
if list == nil {
|
||||||
|
mV2 = nil
|
||||||
|
} else {
|
||||||
|
ln := len(list)
|
||||||
|
|
||||||
|
if cap(mV2) >= ln {
|
||||||
|
mV2 = mV2[:0]
|
||||||
|
} else {
|
||||||
|
mV2 = make([]refs.ObjectID, ln)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < ln; i++ {
|
||||||
|
mV2[i] = *list[i].ToV2()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(*audit.DataAuditResult)(r).SetPassSG(mV2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FailSG returns list of Storage Groups that failed audit PoR stage.
|
||||||
|
func (r *Result) FailSG() []oid.ID {
|
||||||
|
mV2 := (*audit.DataAuditResult)(r).
|
||||||
|
GetFailSG()
|
||||||
|
|
||||||
|
if mV2 == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m := make([]oid.ID, len(mV2))
|
||||||
|
|
||||||
|
for i := range mV2 {
|
||||||
|
m[i] = *oid.NewIDFromV2(&mV2[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFailSG sets list of Storage Groups that failed audit PoR stage.
|
||||||
|
func (r *Result) SetFailSG(list []oid.ID) {
|
||||||
|
mV2 := (*audit.DataAuditResult)(r).
|
||||||
|
GetFailSG()
|
||||||
|
|
||||||
|
if list == nil {
|
||||||
|
mV2 = nil
|
||||||
|
} else {
|
||||||
|
ln := len(list)
|
||||||
|
|
||||||
|
if cap(mV2) >= ln {
|
||||||
|
mV2 = mV2[:0]
|
||||||
|
} else {
|
||||||
|
mV2 = make([]refs.ObjectID, ln)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < ln; i++ {
|
||||||
|
mV2[i] = *list[i].ToV2()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(*audit.DataAuditResult)(r).SetFailSG(mV2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hit returns number of sampled objects under audit placed
|
||||||
|
// in an optimal way according to the containers placement policy
|
||||||
|
// when checking PoP.
|
||||||
|
func (r *Result) Hit() uint32 {
|
||||||
|
return (*audit.DataAuditResult)(r).GetHit()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHit sets number of sampled objects under audit placed
|
||||||
|
// in an optimal way according to the containers placement policy
|
||||||
|
// when checking PoP.
|
||||||
|
func (r *Result) SetHit(hit uint32) {
|
||||||
|
(*audit.DataAuditResult)(r).SetHit(hit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Miss returns number of sampled objects under audit placed
|
||||||
|
// in suboptimal way according to the containers placement policy,
|
||||||
|
// but still at a satisfactory level when checking PoP.
|
||||||
|
func (r *Result) Miss() uint32 {
|
||||||
|
return (*audit.DataAuditResult)(r).GetMiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMiss sets number of sampled objects under audit placed
|
||||||
|
// in suboptimal way according to the containers placement policy,
|
||||||
|
// but still at a satisfactory level when checking PoP.
|
||||||
|
func (r *Result) SetMiss(miss uint32) {
|
||||||
|
(*audit.DataAuditResult)(r).SetMiss(miss)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fail returns number of sampled objects under audit stored
|
||||||
|
// in a way not confirming placement policy or not found at all
|
||||||
|
// when checking PoP.
|
||||||
|
func (r *Result) Fail() uint32 {
|
||||||
|
return (*audit.DataAuditResult)(r).GetFail()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFail sets number of sampled objects under audit stored
|
||||||
|
// in a way not confirming placement policy or not found at all
|
||||||
|
// when checking PoP.
|
||||||
|
func (r *Result) SetFail(fail uint32) {
|
||||||
|
(*audit.DataAuditResult)(r).SetFail(fail)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PassNodes returns list of storage node public keys that
|
||||||
|
// passed at least one PDP.
|
||||||
|
func (r *Result) PassNodes() [][]byte {
|
||||||
|
return (*audit.DataAuditResult)(r).GetPassNodes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPassNodes sets list of storage node public keys that
|
||||||
|
// passed at least one PDP.
|
||||||
|
func (r *Result) SetPassNodes(list [][]byte) {
|
||||||
|
(*audit.DataAuditResult)(r).SetPassNodes(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FailNodes returns list of storage node public keys that
|
||||||
|
// failed at least one PDP.
|
||||||
|
func (r *Result) FailNodes() [][]byte {
|
||||||
|
return (*audit.DataAuditResult)(r).GetFailNodes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFailNodes sets list of storage node public keys that
|
||||||
|
// failed at least one PDP.
|
||||||
|
func (r *Result) SetFailNodes(list [][]byte) {
|
||||||
|
(*audit.DataAuditResult)(r).SetFailNodes(list)
|
||||||
|
}
|
154
audit/result_test.go
Normal file
154
audit/result_test.go
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
package audit_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
auditv2 "github.com/nspcc-dev/neofs-api-go/v2/audit"
|
||||||
|
"github.com/nspcc-dev/neofs-sdk-go/audit"
|
||||||
|
audittest "github.com/nspcc-dev/neofs-sdk-go/audit/test"
|
||||||
|
cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test"
|
||||||
|
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
|
||||||
|
oidtest "github.com/nspcc-dev/neofs-sdk-go/object/id/test"
|
||||||
|
"github.com/nspcc-dev/neofs-sdk-go/version"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResult(t *testing.T) {
|
||||||
|
r := audit.NewResult()
|
||||||
|
require.Equal(t, version.Current(), r.Version())
|
||||||
|
|
||||||
|
epoch := uint64(13)
|
||||||
|
r.SetAuditEpoch(epoch)
|
||||||
|
require.Equal(t, epoch, r.AuditEpoch())
|
||||||
|
|
||||||
|
cid := cidtest.ID()
|
||||||
|
r.SetContainerID(cid)
|
||||||
|
require.Equal(t, cid, r.ContainerID())
|
||||||
|
|
||||||
|
key := []byte{1, 2, 3}
|
||||||
|
r.SetPublicKey(key)
|
||||||
|
require.Equal(t, key, r.PublicKey())
|
||||||
|
|
||||||
|
r.SetComplete(true)
|
||||||
|
require.True(t, r.Complete())
|
||||||
|
|
||||||
|
requests := uint32(2)
|
||||||
|
r.SetRequests(requests)
|
||||||
|
require.Equal(t, requests, r.Requests())
|
||||||
|
|
||||||
|
retries := uint32(1)
|
||||||
|
r.SetRetries(retries)
|
||||||
|
require.Equal(t, retries, r.Retries())
|
||||||
|
|
||||||
|
passSG := []oid.ID{*oidtest.ID(), *oidtest.ID()}
|
||||||
|
r.SetPassSG(passSG)
|
||||||
|
require.Equal(t, passSG, r.PassSG())
|
||||||
|
|
||||||
|
failSG := []oid.ID{*oidtest.ID(), *oidtest.ID()}
|
||||||
|
r.SetFailSG(failSG)
|
||||||
|
require.Equal(t, failSG, r.FailSG())
|
||||||
|
|
||||||
|
hit := uint32(1)
|
||||||
|
r.SetHit(hit)
|
||||||
|
require.Equal(t, hit, r.Hit())
|
||||||
|
|
||||||
|
miss := uint32(2)
|
||||||
|
r.SetMiss(miss)
|
||||||
|
require.Equal(t, miss, r.Miss())
|
||||||
|
|
||||||
|
fail := uint32(3)
|
||||||
|
r.SetFail(fail)
|
||||||
|
require.Equal(t, fail, r.Fail())
|
||||||
|
|
||||||
|
passNodes := [][]byte{{1}, {2}}
|
||||||
|
r.SetPassNodes(passNodes)
|
||||||
|
require.Equal(t, passNodes, r.PassNodes())
|
||||||
|
|
||||||
|
failNodes := [][]byte{{3}, {4}}
|
||||||
|
r.SetFailNodes(failNodes)
|
||||||
|
require.Equal(t, failNodes, r.FailNodes())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStorageGroupEncoding(t *testing.T) {
|
||||||
|
r := audittest.Result()
|
||||||
|
|
||||||
|
t.Run("binary", func(t *testing.T) {
|
||||||
|
data, err := r.Marshal()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
r2 := audit.NewResult()
|
||||||
|
require.NoError(t, r2.Unmarshal(data))
|
||||||
|
|
||||||
|
require.Equal(t, r, r2)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("json", func(t *testing.T) {
|
||||||
|
data, err := r.MarshalJSON()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
r2 := audit.NewResult()
|
||||||
|
require.NoError(t, r2.UnmarshalJSON(data))
|
||||||
|
|
||||||
|
require.Equal(t, r, r2)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResult_ToV2(t *testing.T) {
|
||||||
|
t.Run("nil", func(t *testing.T) {
|
||||||
|
var x *audit.Result
|
||||||
|
|
||||||
|
require.Nil(t, x.ToV2())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("default values", func(t *testing.T) {
|
||||||
|
result := audit.NewResult()
|
||||||
|
|
||||||
|
// check initial values
|
||||||
|
require.Equal(t, version.Current(), result.Version())
|
||||||
|
|
||||||
|
require.False(t, result.Complete())
|
||||||
|
|
||||||
|
require.Nil(t, result.ContainerID())
|
||||||
|
require.Nil(t, result.PublicKey())
|
||||||
|
require.Nil(t, result.PassSG())
|
||||||
|
require.Nil(t, result.FailSG())
|
||||||
|
require.Nil(t, result.PassNodes())
|
||||||
|
require.Nil(t, result.FailNodes())
|
||||||
|
|
||||||
|
require.Zero(t, result.Hit())
|
||||||
|
require.Zero(t, result.Miss())
|
||||||
|
require.Zero(t, result.Fail())
|
||||||
|
require.Zero(t, result.Requests())
|
||||||
|
require.Zero(t, result.Retries())
|
||||||
|
require.Zero(t, result.AuditEpoch())
|
||||||
|
|
||||||
|
// convert to v2 message
|
||||||
|
resultV2 := result.ToV2()
|
||||||
|
|
||||||
|
require.Equal(t, version.Current().ToV2(), resultV2.GetVersion())
|
||||||
|
|
||||||
|
require.False(t, resultV2.GetComplete())
|
||||||
|
|
||||||
|
require.Nil(t, resultV2.GetContainerID())
|
||||||
|
require.Nil(t, resultV2.GetPublicKey())
|
||||||
|
require.Nil(t, resultV2.GetPassSG())
|
||||||
|
require.Nil(t, resultV2.GetFailSG())
|
||||||
|
require.Nil(t, resultV2.GetPassNodes())
|
||||||
|
require.Nil(t, resultV2.GetFailNodes())
|
||||||
|
|
||||||
|
require.Zero(t, resultV2.GetHit())
|
||||||
|
require.Zero(t, resultV2.GetMiss())
|
||||||
|
require.Zero(t, resultV2.GetFail())
|
||||||
|
require.Zero(t, resultV2.GetRequests())
|
||||||
|
require.Zero(t, resultV2.GetRetries())
|
||||||
|
require.Zero(t, resultV2.GetAuditEpoch())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewResultFromV2(t *testing.T) {
|
||||||
|
t.Run("from nil", func(t *testing.T) {
|
||||||
|
var x *auditv2.DataAuditResult
|
||||||
|
|
||||||
|
require.Nil(t, audit.NewResultFromV2(x))
|
||||||
|
})
|
||||||
|
}
|
37
audit/test/generate.go
Normal file
37
audit/test/generate.go
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
package audittest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/nspcc-dev/neofs-sdk-go/audit"
|
||||||
|
cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test"
|
||||||
|
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
|
||||||
|
oidtest "github.com/nspcc-dev/neofs-sdk-go/object/id/test"
|
||||||
|
versiontest "github.com/nspcc-dev/neofs-sdk-go/version/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Result returns random audit.Result.
|
||||||
|
func Result() *audit.Result {
|
||||||
|
x := audit.NewResult()
|
||||||
|
|
||||||
|
x.SetVersion(versiontest.Version())
|
||||||
|
x.SetContainerID(cidtest.ID())
|
||||||
|
x.SetPublicKey([]byte("key"))
|
||||||
|
x.SetComplete(true)
|
||||||
|
x.SetAuditEpoch(44)
|
||||||
|
x.SetHit(55)
|
||||||
|
x.SetMiss(66)
|
||||||
|
x.SetFail(77)
|
||||||
|
x.SetRetries(88)
|
||||||
|
x.SetRequests(99)
|
||||||
|
x.SetFailNodes([][]byte{
|
||||||
|
[]byte("node1"),
|
||||||
|
[]byte("node2"),
|
||||||
|
})
|
||||||
|
x.SetPassNodes([][]byte{
|
||||||
|
[]byte("node3"),
|
||||||
|
[]byte("node4"),
|
||||||
|
})
|
||||||
|
x.SetPassSG([]oid.ID{*oidtest.ID(), *oidtest.ID()})
|
||||||
|
x.SetFailSG([]oid.ID{*oidtest.ID(), *oidtest.ID()})
|
||||||
|
|
||||||
|
return x
|
||||||
|
}
|
497
bearer/bearer.go
497
bearer/bearer.go
|
@ -1,497 +0,0 @@
|
||||||
package bearer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl"
|
|
||||||
apeV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/ape"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
|
||||||
apeSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/ape"
|
|
||||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
|
||||||
frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto"
|
|
||||||
frostfsecdsa "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto/ecdsa"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Token represents bearer token for object service operations.
|
|
||||||
//
|
|
||||||
// Token is mutually compatible with git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl.BearerToken
|
|
||||||
// message. See ReadFromV2 / WriteToV2 methods.
|
|
||||||
//
|
|
||||||
// Instances can be created using built-in var declaration.
|
|
||||||
type Token struct {
|
|
||||||
targetUserSet bool
|
|
||||||
targetUser user.ID
|
|
||||||
|
|
||||||
eaclTableSet bool
|
|
||||||
eaclTable eacl.Table
|
|
||||||
|
|
||||||
lifetimeSet bool
|
|
||||||
iat, nbf, exp uint64
|
|
||||||
|
|
||||||
sigSet bool
|
|
||||||
sig refs.Signature
|
|
||||||
|
|
||||||
apeOverrideSet bool
|
|
||||||
apeOverride APEOverride
|
|
||||||
|
|
||||||
impersonate bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// APEOverride is the list of APE chains defined for a target.
|
|
||||||
// These chains are meant to serve as overrides to the already defined (or even undefined)
|
|
||||||
// APE chains for the target (see contract `Policy`).
|
|
||||||
//
|
|
||||||
// The server-side processing of the bearer token with set APE overrides must verify if a client is permitted
|
|
||||||
// to override chains for the target, preventing unauthorized access through the APE mechanism.
|
|
||||||
type APEOverride struct {
|
|
||||||
// Target for which chains are applied.
|
|
||||||
Target apeSDK.ChainTarget
|
|
||||||
|
|
||||||
// The list of APE chains.
|
|
||||||
Chains []apeSDK.Chain
|
|
||||||
}
|
|
||||||
|
|
||||||
// Marshal marshals APEOverride into a protobuf binary form.
|
|
||||||
func (c *APEOverride) Marshal() ([]byte, error) {
|
|
||||||
return c.ToV2().StableMarshal(nil), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal unmarshals protobuf binary representation of APEOverride.
|
|
||||||
func (c *APEOverride) Unmarshal(data []byte) error {
|
|
||||||
overrideV2 := new(acl.APEOverride)
|
|
||||||
if err := overrideV2.Unmarshal(data); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.FromV2(overrideV2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalJSON encodes APEOverride to protobuf JSON format.
|
|
||||||
func (c *APEOverride) MarshalJSON() ([]byte, error) {
|
|
||||||
return c.ToV2().MarshalJSON()
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalJSON decodes APEOverride from protobuf JSON format.
|
|
||||||
func (c *APEOverride) UnmarshalJSON(data []byte) error {
|
|
||||||
overrideV2 := new(acl.APEOverride)
|
|
||||||
if err := overrideV2.UnmarshalJSON(data); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.FromV2(overrideV2)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *APEOverride) FromV2(tokenAPEChains *acl.APEOverride) error {
|
|
||||||
c.Target.FromV2(tokenAPEChains.GetTarget())
|
|
||||||
if chains := tokenAPEChains.GetChains(); len(chains) > 0 {
|
|
||||||
c.Chains = make([]apeSDK.Chain, len(chains))
|
|
||||||
for i := range chains {
|
|
||||||
if err := c.Chains[i].ReadFromV2(chains[i]); err != nil {
|
|
||||||
return fmt.Errorf("invalid APE chain: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *APEOverride) ToV2() *acl.APEOverride {
|
|
||||||
if c == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
apeOverride := new(acl.APEOverride)
|
|
||||||
apeOverride.SetTarget(c.Target.ToV2())
|
|
||||||
chains := make([]*apeV2.Chain, len(c.Chains))
|
|
||||||
for i := range c.Chains {
|
|
||||||
chains[i] = c.Chains[i].ToV2()
|
|
||||||
}
|
|
||||||
apeOverride.SetChains(chains)
|
|
||||||
|
|
||||||
return apeOverride
|
|
||||||
}
|
|
||||||
|
|
||||||
// reads Token from the acl.BearerToken message. If checkFieldPresence is set,
|
|
||||||
// returns an error on absence of any protocol-required field.
|
|
||||||
func (b *Token) readFromV2(m acl.BearerToken, checkFieldPresence bool) error {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
body := m.GetBody()
|
|
||||||
if checkFieldPresence && body == nil {
|
|
||||||
return errors.New("missing token body")
|
|
||||||
}
|
|
||||||
|
|
||||||
b.impersonate = body.GetImpersonate()
|
|
||||||
|
|
||||||
apeOverrides := body.GetAPEOverride()
|
|
||||||
eaclTable := body.GetEACL()
|
|
||||||
if b.eaclTableSet = eaclTable != nil; b.eaclTableSet {
|
|
||||||
b.eaclTable = *eacl.NewTableFromV2(eaclTable)
|
|
||||||
} else if checkFieldPresence && !b.impersonate && apeOverrides == nil {
|
|
||||||
return errors.New("missing eACL table")
|
|
||||||
}
|
|
||||||
|
|
||||||
targetUser := body.GetOwnerID()
|
|
||||||
if b.targetUserSet = targetUser != nil; b.targetUserSet {
|
|
||||||
err = b.targetUser.ReadFromV2(*targetUser)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid target user: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lifetime := body.GetLifetime()
|
|
||||||
if b.lifetimeSet = lifetime != nil; b.lifetimeSet {
|
|
||||||
b.iat = lifetime.GetIat()
|
|
||||||
b.nbf = lifetime.GetNbf()
|
|
||||||
b.exp = lifetime.GetExp()
|
|
||||||
} else if checkFieldPresence {
|
|
||||||
return errors.New("missing token lifetime")
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.apeOverrideSet = apeOverrides != nil; b.apeOverrideSet {
|
|
||||||
if err = b.apeOverride.FromV2(apeOverrides); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else if checkFieldPresence && !b.impersonate && !b.eaclTableSet {
|
|
||||||
return errors.New("missing APE override")
|
|
||||||
}
|
|
||||||
|
|
||||||
sig := m.GetSignature()
|
|
||||||
if b.sigSet = sig != nil; sig != nil {
|
|
||||||
b.sig = *sig
|
|
||||||
} else if checkFieldPresence {
|
|
||||||
return errors.New("missing body signature")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadFromV2 reads Token from the acl.BearerToken message.
|
|
||||||
//
|
|
||||||
// See also WriteToV2.
|
|
||||||
func (b *Token) ReadFromV2(m acl.BearerToken) error {
|
|
||||||
return b.readFromV2(m, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b Token) fillBody() *acl.BearerTokenBody {
|
|
||||||
if !b.eaclTableSet && !b.targetUserSet && !b.lifetimeSet && !b.impersonate && !b.apeOverrideSet {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var body acl.BearerTokenBody
|
|
||||||
|
|
||||||
if b.eaclTableSet {
|
|
||||||
body.SetEACL(b.eaclTable.ToV2())
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.targetUserSet {
|
|
||||||
var targetUser refs.OwnerID
|
|
||||||
b.targetUser.WriteToV2(&targetUser)
|
|
||||||
|
|
||||||
body.SetOwnerID(&targetUser)
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.lifetimeSet {
|
|
||||||
var lifetime acl.TokenLifetime
|
|
||||||
lifetime.SetIat(b.iat)
|
|
||||||
lifetime.SetNbf(b.nbf)
|
|
||||||
lifetime.SetExp(b.exp)
|
|
||||||
|
|
||||||
body.SetLifetime(&lifetime)
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.apeOverrideSet {
|
|
||||||
body.SetAPEOverride(b.apeOverride.ToV2())
|
|
||||||
}
|
|
||||||
|
|
||||||
body.SetImpersonate(b.impersonate)
|
|
||||||
|
|
||||||
return &body
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b Token) signedData() []byte {
|
|
||||||
return b.fillBody().StableMarshal(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteToV2 writes Token to the acl.BearerToken message.
|
|
||||||
// The message must not be nil.
|
|
||||||
//
|
|
||||||
// See also ReadFromV2.
|
|
||||||
func (b Token) WriteToV2(m *acl.BearerToken) {
|
|
||||||
m.SetBody(b.fillBody())
|
|
||||||
|
|
||||||
var sig *refs.Signature
|
|
||||||
|
|
||||||
if b.sigSet {
|
|
||||||
sig = &b.sig
|
|
||||||
}
|
|
||||||
|
|
||||||
m.SetSignature(sig)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetExp sets "exp" (expiration time) claim which identifies the
|
|
||||||
// expiration time (in FrostFS epochs) after which the Token MUST NOT be
|
|
||||||
// accepted for processing. The processing of the "exp" claim requires
|
|
||||||
// that the current epoch MUST be before or equal to the expiration epoch
|
|
||||||
// listed in the "exp" claim.
|
|
||||||
//
|
|
||||||
// Naming is inspired by https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4.
|
|
||||||
//
|
|
||||||
// See also InvalidAt.
|
|
||||||
func (b *Token) SetExp(exp uint64) {
|
|
||||||
b.exp = exp
|
|
||||||
b.lifetimeSet = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetNbf sets "nbf" (not before) claim which identifies the time (in
|
|
||||||
// FrostFS epochs) before which the Token MUST NOT be accepted for processing. The
|
|
||||||
// processing of the "nbf" claim requires that the current epoch MUST be
|
|
||||||
// after or equal to the not-before epoch listed in the "nbf" claim.
|
|
||||||
//
|
|
||||||
// Naming is inspired by https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5.
|
|
||||||
//
|
|
||||||
// See also InvalidAt.
|
|
||||||
func (b *Token) SetNbf(nbf uint64) {
|
|
||||||
b.nbf = nbf
|
|
||||||
b.lifetimeSet = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetIat sets "iat" (issued at) claim which identifies the time (in FrostFS
|
|
||||||
// epochs) at which the Token was issued. This claim can be used to determine
|
|
||||||
// the age of the Token.
|
|
||||||
//
|
|
||||||
// Naming is inspired by https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6.
|
|
||||||
//
|
|
||||||
// See also InvalidAt.
|
|
||||||
func (b *Token) SetIat(iat uint64) {
|
|
||||||
b.iat = iat
|
|
||||||
b.lifetimeSet = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// InvalidAt asserts "exp", "nbf" and "iat" claims for the given epoch.
|
|
||||||
//
|
|
||||||
// Zero Container is invalid in any epoch.
|
|
||||||
//
|
|
||||||
// See also SetExp, SetNbf, SetIat.
|
|
||||||
func (b Token) InvalidAt(epoch uint64) bool {
|
|
||||||
return !b.lifetimeSet || b.nbf > epoch || b.iat > epoch || b.exp < epoch
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetEACLTable sets eacl.Table that replaces the one from the issuer's
|
|
||||||
// container. If table has specified container, bearer token can be used only
|
|
||||||
// for operations within this specific container. Otherwise, Token can be used
|
|
||||||
// within any issuer's container.
|
|
||||||
//
|
|
||||||
// SetEACLTable MUST be called if Token is going to be transmitted over
|
|
||||||
// FrostFS API V2 protocol.
|
|
||||||
//
|
|
||||||
// See also EACLTable, AssertContainer.
|
|
||||||
func (b *Token) SetEACLTable(table eacl.Table) {
|
|
||||||
b.eaclTable = table
|
|
||||||
b.eaclTableSet = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// EACLTable returns extended ACL table set by SetEACLTable.
|
|
||||||
//
|
|
||||||
// Zero Token has zero eacl.Table.
|
|
||||||
func (b Token) EACLTable() eacl.Table {
|
|
||||||
if b.eaclTableSet {
|
|
||||||
return b.eaclTable
|
|
||||||
}
|
|
||||||
|
|
||||||
return eacl.Table{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetAPEOverride sets APE override to the bearer token.
|
|
||||||
//
|
|
||||||
// See also: APEOverride.
|
|
||||||
func (b *Token) SetAPEOverride(v APEOverride) {
|
|
||||||
b.apeOverride = v
|
|
||||||
b.apeOverrideSet = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// APEOverride returns APE override set by SetAPEOverride.
|
|
||||||
//
|
|
||||||
// Zero Token has zero APEOverride.
|
|
||||||
func (b *Token) APEOverride() APEOverride {
|
|
||||||
if b.apeOverrideSet {
|
|
||||||
return b.apeOverride
|
|
||||||
}
|
|
||||||
|
|
||||||
return APEOverride{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetImpersonate mark token as impersonate to consider token signer as request owner.
|
|
||||||
// If this field is true extended EACLTable in token body isn't processed.
|
|
||||||
func (b *Token) SetImpersonate(v bool) {
|
|
||||||
b.impersonate = v
|
|
||||||
}
|
|
||||||
|
|
||||||
// Impersonate returns true if token is impersonated.
|
|
||||||
func (b Token) Impersonate() bool {
|
|
||||||
return b.impersonate
|
|
||||||
}
|
|
||||||
|
|
||||||
// AssertContainer checks if the token is valid within the given container.
|
|
||||||
//
|
|
||||||
// Note: cnr is assumed to refer to the issuer's container, otherwise the check
|
|
||||||
// is meaningless.
|
|
||||||
//
|
|
||||||
// Zero Token is valid in any container.
|
|
||||||
//
|
|
||||||
// See also SetEACLTable.
|
|
||||||
func (b Token) AssertContainer(cnr cid.ID) bool {
|
|
||||||
if !b.eaclTableSet {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
cnrTable, set := b.eaclTable.CID()
|
|
||||||
return !set || cnrTable.Equals(cnr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ForUser specifies ID of the user who can use the Token for the operations
|
|
||||||
// within issuer's container(s).
|
|
||||||
//
|
|
||||||
// Optional: by default, any user has access to Token usage.
|
|
||||||
//
|
|
||||||
// See also AssertUser.
|
|
||||||
func (b *Token) ForUser(id user.ID) {
|
|
||||||
b.targetUser = id
|
|
||||||
b.targetUserSet = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// AssertUser checks if the Token is issued to the given user.
|
|
||||||
//
|
|
||||||
// Zero Token is available to any user.
|
|
||||||
//
|
|
||||||
// See also ForUser.
|
|
||||||
func (b Token) AssertUser(id user.ID) bool {
|
|
||||||
return !b.targetUserSet || b.targetUser.Equals(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sign calculates and writes signature of the Token data using issuer's secret.
|
|
||||||
// Returns signature calculation errors.
|
|
||||||
//
|
|
||||||
// Sign MUST be called if Token is going to be transmitted over
|
|
||||||
// FrostFS API V2 protocol.
|
|
||||||
//
|
|
||||||
// Note that any Token mutation is likely to break the signature, so it is
|
|
||||||
// expected to be calculated as a final stage of Token formation.
|
|
||||||
//
|
|
||||||
// See also VerifySignature, Issuer.
|
|
||||||
func (b *Token) Sign(key ecdsa.PrivateKey) error {
|
|
||||||
var sig frostfscrypto.Signature
|
|
||||||
|
|
||||||
err := sig.Calculate(frostfsecdsa.Signer(key), b.signedData())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
sig.WriteToV2(&b.sig)
|
|
||||||
b.sigSet = true
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// VerifySignature checks if Token signature is presented and valid.
|
|
||||||
//
|
|
||||||
// Zero Token fails the check.
|
|
||||||
//
|
|
||||||
// See also Sign.
|
|
||||||
func (b Token) VerifySignature() bool {
|
|
||||||
if !b.sigSet {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
var sig frostfscrypto.Signature
|
|
||||||
|
|
||||||
// TODO: (#233) check owner<->key relation
|
|
||||||
return sig.ReadFromV2(b.sig) == nil && sig.Verify(b.signedData())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Marshal encodes Token into a binary format of the FrostFS API protocol
|
|
||||||
// (Protocol Buffers V3 with direct field order).
|
|
||||||
//
|
|
||||||
// See also Unmarshal.
|
|
||||||
func (b Token) Marshal() []byte {
|
|
||||||
var m acl.BearerToken
|
|
||||||
b.WriteToV2(&m)
|
|
||||||
|
|
||||||
return m.StableMarshal(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal decodes FrostFS API protocol binary data into the Token
|
|
||||||
// (Protocol Buffers V3 with direct field order). Returns an error describing
|
|
||||||
// a format violation.
|
|
||||||
//
|
|
||||||
// See also Marshal.
|
|
||||||
func (b *Token) Unmarshal(data []byte) error {
|
|
||||||
var m acl.BearerToken
|
|
||||||
|
|
||||||
err := m.Unmarshal(data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.readFromV2(m, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalJSON encodes Token into a JSON format of the FrostFS API protocol
|
|
||||||
// (Protocol Buffers V3 JSON).
|
|
||||||
//
|
|
||||||
// See also UnmarshalJSON.
|
|
||||||
func (b Token) MarshalJSON() ([]byte, error) {
|
|
||||||
var m acl.BearerToken
|
|
||||||
b.WriteToV2(&m)
|
|
||||||
|
|
||||||
return m.MarshalJSON()
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalJSON decodes FrostFS API protocol JSON data into the Token
|
|
||||||
// (Protocol Buffers V3 JSON). Returns an error describing a format violation.
|
|
||||||
//
|
|
||||||
// See also MarshalJSON.
|
|
||||||
func (b *Token) UnmarshalJSON(data []byte) error {
|
|
||||||
var m acl.BearerToken
|
|
||||||
|
|
||||||
err := m.UnmarshalJSON(data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.readFromV2(m, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SigningKeyBytes returns issuer's public key in a binary format of
|
|
||||||
// FrostFS API protocol.
|
|
||||||
//
|
|
||||||
// Unsigned Token has empty key.
|
|
||||||
//
|
|
||||||
// See also ResolveIssuer.
|
|
||||||
func (b Token) SigningKeyBytes() []byte {
|
|
||||||
if b.sigSet {
|
|
||||||
return b.sig.GetKey()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResolveIssuer resolves issuer's user.ID from the key used for Token signing.
|
|
||||||
// Returns zero user.ID if Token is unsigned or key has incorrect format.
|
|
||||||
//
|
|
||||||
// See also SigningKeyBytes.
|
|
||||||
func ResolveIssuer(b Token) (usr user.ID) {
|
|
||||||
binKey := b.SigningKeyBytes()
|
|
||||||
|
|
||||||
if len(binKey) != 0 {
|
|
||||||
var key frostfsecdsa.PublicKey
|
|
||||||
if key.Decode(binKey) == nil {
|
|
||||||
user.IDFromKey(&usr, ecdsa.PublicKey(key))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
|
@ -1,447 +0,0 @@
|
||||||
package bearer_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"math/rand"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
|
|
||||||
bearertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer/test"
|
|
||||||
cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test"
|
|
||||||
frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto"
|
|
||||||
frostfsecdsa "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto/ecdsa"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl"
|
|
||||||
eacltest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl/test"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
|
|
||||||
usertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user/test"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
// compares binary representations of two eacl.Table instances.
|
|
||||||
func isEqualEACLTables(t1, t2 eacl.Table) bool {
|
|
||||||
d1, err := t1.Marshal()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
d2, err := t2.Marshal()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return bytes.Equal(d1, d2)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestToken_SetEACLTable(t *testing.T) {
|
|
||||||
var val bearer.Token
|
|
||||||
var m acl.BearerToken
|
|
||||||
filled := bearertest.Token()
|
|
||||||
|
|
||||||
val.WriteToV2(&m)
|
|
||||||
require.Zero(t, m.GetBody())
|
|
||||||
|
|
||||||
val2 := filled
|
|
||||||
|
|
||||||
require.NoError(t, val2.Unmarshal(val.Marshal()))
|
|
||||||
require.Zero(t, val2.EACLTable())
|
|
||||||
|
|
||||||
val2 = filled
|
|
||||||
|
|
||||||
jd, err := val.MarshalJSON()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.NoError(t, val2.UnmarshalJSON(jd))
|
|
||||||
require.Zero(t, val2.EACLTable())
|
|
||||||
|
|
||||||
// set value
|
|
||||||
|
|
||||||
eaclTable := *eacltest.Table()
|
|
||||||
|
|
||||||
val.SetEACLTable(eaclTable)
|
|
||||||
require.True(t, isEqualEACLTables(eaclTable, val.EACLTable()))
|
|
||||||
|
|
||||||
val.WriteToV2(&m)
|
|
||||||
eaclTableV2 := eaclTable.ToV2()
|
|
||||||
require.Equal(t, eaclTableV2, m.GetBody().GetEACL())
|
|
||||||
|
|
||||||
val2 = filled
|
|
||||||
|
|
||||||
require.NoError(t, val2.Unmarshal(val.Marshal()))
|
|
||||||
require.True(t, isEqualEACLTables(eaclTable, val.EACLTable()))
|
|
||||||
|
|
||||||
val2 = filled
|
|
||||||
|
|
||||||
jd, err = val.MarshalJSON()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.NoError(t, val2.UnmarshalJSON(jd))
|
|
||||||
require.True(t, isEqualEACLTables(eaclTable, val.EACLTable()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestToken_SetAPEOverrides(t *testing.T) {
|
|
||||||
var val bearer.Token
|
|
||||||
var m acl.BearerToken
|
|
||||||
filled := bearertest.Token()
|
|
||||||
|
|
||||||
val.WriteToV2(&m)
|
|
||||||
require.Zero(t, m.GetBody())
|
|
||||||
|
|
||||||
val2 := filled
|
|
||||||
|
|
||||||
require.NoError(t, val2.Unmarshal(val.Marshal()))
|
|
||||||
require.Zero(t, val2.APEOverride())
|
|
||||||
|
|
||||||
val2 = filled
|
|
||||||
|
|
||||||
jd, err := val.MarshalJSON()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.NoError(t, val2.UnmarshalJSON(jd))
|
|
||||||
require.Zero(t, val2.APEOverride())
|
|
||||||
|
|
||||||
// set value
|
|
||||||
|
|
||||||
tApe := bearertest.APEOverride()
|
|
||||||
|
|
||||||
val.SetAPEOverride(tApe)
|
|
||||||
require.Equal(t, tApe, val.APEOverride())
|
|
||||||
|
|
||||||
val.WriteToV2(&m)
|
|
||||||
require.NotNil(t, m.GetBody().GetAPEOverride())
|
|
||||||
require.True(t, tokenAPEOverridesEqual(tApe.ToV2(), m.GetBody().GetAPEOverride()))
|
|
||||||
|
|
||||||
val2 = filled
|
|
||||||
|
|
||||||
require.NoError(t, val2.Unmarshal(val.Marshal()))
|
|
||||||
apeOverride := val2.APEOverride()
|
|
||||||
require.True(t, tokenAPEOverridesEqual(tApe.ToV2(), apeOverride.ToV2()))
|
|
||||||
|
|
||||||
val2 = filled
|
|
||||||
|
|
||||||
jd, err = val.MarshalJSON()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.NoError(t, val2.UnmarshalJSON(jd))
|
|
||||||
apeOverride = val.APEOverride()
|
|
||||||
require.True(t, tokenAPEOverridesEqual(tApe.ToV2(), apeOverride.ToV2()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func tokenAPEOverridesEqual(lhs, rhs *acl.APEOverride) bool {
|
|
||||||
return reflect.DeepEqual(lhs, rhs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestToken_ForUser(t *testing.T) {
|
|
||||||
var val bearer.Token
|
|
||||||
var m acl.BearerToken
|
|
||||||
filled := bearertest.Token()
|
|
||||||
|
|
||||||
val.WriteToV2(&m)
|
|
||||||
require.Zero(t, m.GetBody())
|
|
||||||
|
|
||||||
val2 := filled
|
|
||||||
|
|
||||||
require.NoError(t, val2.Unmarshal(val.Marshal()))
|
|
||||||
|
|
||||||
val2.WriteToV2(&m)
|
|
||||||
require.Zero(t, m.GetBody())
|
|
||||||
|
|
||||||
val2 = filled
|
|
||||||
|
|
||||||
jd, err := val.MarshalJSON()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.NoError(t, val2.UnmarshalJSON(jd))
|
|
||||||
|
|
||||||
val2.WriteToV2(&m)
|
|
||||||
require.Zero(t, m.GetBody())
|
|
||||||
|
|
||||||
// set value
|
|
||||||
usr := usertest.ID()
|
|
||||||
|
|
||||||
var usrV2 refs.OwnerID
|
|
||||||
usr.WriteToV2(&usrV2)
|
|
||||||
|
|
||||||
val.ForUser(usr)
|
|
||||||
|
|
||||||
val.WriteToV2(&m)
|
|
||||||
require.Equal(t, usrV2, *m.GetBody().GetOwnerID())
|
|
||||||
|
|
||||||
val2 = filled
|
|
||||||
|
|
||||||
require.NoError(t, val2.Unmarshal(val.Marshal()))
|
|
||||||
|
|
||||||
val2.WriteToV2(&m)
|
|
||||||
require.Equal(t, usrV2, *m.GetBody().GetOwnerID())
|
|
||||||
|
|
||||||
val2 = filled
|
|
||||||
|
|
||||||
jd, err = val.MarshalJSON()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.NoError(t, val2.UnmarshalJSON(jd))
|
|
||||||
|
|
||||||
val2.WriteToV2(&m)
|
|
||||||
require.Equal(t, usrV2, *m.GetBody().GetOwnerID())
|
|
||||||
}
|
|
||||||
|
|
||||||
func testLifetimeClaim(t *testing.T, setter func(*bearer.Token, uint64), getter func(*acl.BearerToken) uint64) {
|
|
||||||
var val bearer.Token
|
|
||||||
var m acl.BearerToken
|
|
||||||
filled := bearertest.Token()
|
|
||||||
|
|
||||||
val.WriteToV2(&m)
|
|
||||||
require.Zero(t, m.GetBody())
|
|
||||||
|
|
||||||
val2 := filled
|
|
||||||
|
|
||||||
require.NoError(t, val2.Unmarshal(val.Marshal()))
|
|
||||||
|
|
||||||
val2.WriteToV2(&m)
|
|
||||||
require.Zero(t, m.GetBody())
|
|
||||||
|
|
||||||
val2 = filled
|
|
||||||
|
|
||||||
jd, err := val.MarshalJSON()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.NoError(t, val2.UnmarshalJSON(jd))
|
|
||||||
|
|
||||||
val2.WriteToV2(&m)
|
|
||||||
require.Zero(t, m.GetBody())
|
|
||||||
|
|
||||||
// set value
|
|
||||||
exp := rand.Uint64()
|
|
||||||
|
|
||||||
setter(&val, exp)
|
|
||||||
|
|
||||||
val.WriteToV2(&m)
|
|
||||||
require.Equal(t, exp, getter(&m))
|
|
||||||
|
|
||||||
val2 = filled
|
|
||||||
|
|
||||||
require.NoError(t, val2.Unmarshal(val.Marshal()))
|
|
||||||
|
|
||||||
val2.WriteToV2(&m)
|
|
||||||
require.Equal(t, exp, getter(&m))
|
|
||||||
|
|
||||||
val2 = filled
|
|
||||||
|
|
||||||
jd, err = val.MarshalJSON()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.NoError(t, val2.UnmarshalJSON(jd))
|
|
||||||
|
|
||||||
val2.WriteToV2(&m)
|
|
||||||
require.Equal(t, exp, getter(&m))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestToken_SetLifetime(t *testing.T) {
|
|
||||||
t.Run("iat", func(t *testing.T) {
|
|
||||||
testLifetimeClaim(t, (*bearer.Token).SetIat, func(token *acl.BearerToken) uint64 {
|
|
||||||
return token.GetBody().GetLifetime().GetIat()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("nbf", func(t *testing.T) {
|
|
||||||
testLifetimeClaim(t, (*bearer.Token).SetNbf, func(token *acl.BearerToken) uint64 {
|
|
||||||
return token.GetBody().GetLifetime().GetNbf()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("exp", func(t *testing.T) {
|
|
||||||
testLifetimeClaim(t, (*bearer.Token).SetExp, func(token *acl.BearerToken) uint64 {
|
|
||||||
return token.GetBody().GetLifetime().GetExp()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestToken_InvalidAt(t *testing.T) {
|
|
||||||
var val bearer.Token
|
|
||||||
|
|
||||||
require.True(t, val.InvalidAt(0))
|
|
||||||
require.True(t, val.InvalidAt(1))
|
|
||||||
|
|
||||||
val.SetIat(1)
|
|
||||||
val.SetNbf(2)
|
|
||||||
val.SetExp(4)
|
|
||||||
|
|
||||||
require.True(t, val.InvalidAt(0))
|
|
||||||
require.True(t, val.InvalidAt(1))
|
|
||||||
require.False(t, val.InvalidAt(2))
|
|
||||||
require.False(t, val.InvalidAt(3))
|
|
||||||
require.False(t, val.InvalidAt(4))
|
|
||||||
require.True(t, val.InvalidAt(5))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestToken_AssertContainer(t *testing.T) {
|
|
||||||
var val bearer.Token
|
|
||||||
cnr := cidtest.ID()
|
|
||||||
|
|
||||||
require.True(t, val.AssertContainer(cnr))
|
|
||||||
|
|
||||||
eaclTable := *eacltest.Table()
|
|
||||||
|
|
||||||
eaclTable.SetCID(cidtest.ID())
|
|
||||||
val.SetEACLTable(eaclTable)
|
|
||||||
require.False(t, val.AssertContainer(cnr))
|
|
||||||
|
|
||||||
eaclTable.SetCID(cnr)
|
|
||||||
val.SetEACLTable(eaclTable)
|
|
||||||
require.True(t, val.AssertContainer(cnr))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestToken_AssertUser(t *testing.T) {
|
|
||||||
var val bearer.Token
|
|
||||||
usr := usertest.ID()
|
|
||||||
|
|
||||||
require.True(t, val.AssertUser(usr))
|
|
||||||
|
|
||||||
val.ForUser(usertest.ID())
|
|
||||||
require.False(t, val.AssertUser(usr))
|
|
||||||
|
|
||||||
val.ForUser(usr)
|
|
||||||
require.True(t, val.AssertUser(usr))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestToken_Sign(t *testing.T) {
|
|
||||||
var val bearer.Token
|
|
||||||
|
|
||||||
require.False(t, val.VerifySignature())
|
|
||||||
|
|
||||||
k, err := keys.NewPrivateKey()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
key := k.PrivateKey
|
|
||||||
val = bearertest.Token()
|
|
||||||
|
|
||||||
require.NoError(t, val.Sign(key))
|
|
||||||
|
|
||||||
require.True(t, val.VerifySignature())
|
|
||||||
|
|
||||||
var m acl.BearerToken
|
|
||||||
val.WriteToV2(&m)
|
|
||||||
|
|
||||||
require.NotZero(t, m.GetSignature().GetKey())
|
|
||||||
require.NotZero(t, m.GetSignature().GetSign())
|
|
||||||
|
|
||||||
val2 := bearertest.Token()
|
|
||||||
|
|
||||||
require.NoError(t, val2.Unmarshal(val.Marshal()))
|
|
||||||
require.True(t, val2.VerifySignature())
|
|
||||||
|
|
||||||
jd, err := val.MarshalJSON()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
val2 = bearertest.Token()
|
|
||||||
require.NoError(t, val2.UnmarshalJSON(jd))
|
|
||||||
require.True(t, val2.VerifySignature())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestToken_ReadFromV2(t *testing.T) {
|
|
||||||
var val bearer.Token
|
|
||||||
var m acl.BearerToken
|
|
||||||
|
|
||||||
require.Error(t, val.ReadFromV2(m))
|
|
||||||
|
|
||||||
var body acl.BearerTokenBody
|
|
||||||
m.SetBody(&body)
|
|
||||||
|
|
||||||
require.Error(t, val.ReadFromV2(m))
|
|
||||||
|
|
||||||
eaclTable := eacltest.Table().ToV2()
|
|
||||||
body.SetEACL(eaclTable)
|
|
||||||
|
|
||||||
require.Error(t, val.ReadFromV2(m))
|
|
||||||
|
|
||||||
var lifetime acl.TokenLifetime
|
|
||||||
body.SetLifetime(&lifetime)
|
|
||||||
|
|
||||||
require.Error(t, val.ReadFromV2(m))
|
|
||||||
|
|
||||||
const iat, nbf, exp = 1, 2, 3
|
|
||||||
lifetime.SetIat(iat)
|
|
||||||
lifetime.SetNbf(nbf)
|
|
||||||
lifetime.SetExp(exp)
|
|
||||||
|
|
||||||
body.SetLifetime(&lifetime)
|
|
||||||
|
|
||||||
require.Error(t, val.ReadFromV2(m))
|
|
||||||
|
|
||||||
var sig refs.Signature
|
|
||||||
m.SetSignature(&sig)
|
|
||||||
|
|
||||||
require.NoError(t, val.ReadFromV2(m))
|
|
||||||
|
|
||||||
body.SetEACL(nil)
|
|
||||||
body.SetImpersonate(true)
|
|
||||||
require.NoError(t, val.ReadFromV2(m))
|
|
||||||
|
|
||||||
var m2 acl.BearerToken
|
|
||||||
|
|
||||||
val.WriteToV2(&m2)
|
|
||||||
require.Equal(t, m, m2)
|
|
||||||
|
|
||||||
usr, usr2 := usertest.ID(), usertest.ID()
|
|
||||||
|
|
||||||
require.True(t, val.AssertUser(usr))
|
|
||||||
require.True(t, val.AssertUser(usr2))
|
|
||||||
|
|
||||||
var usrV2 refs.OwnerID
|
|
||||||
usr.WriteToV2(&usrV2)
|
|
||||||
|
|
||||||
body.SetOwnerID(&usrV2)
|
|
||||||
|
|
||||||
require.NoError(t, val.ReadFromV2(m))
|
|
||||||
|
|
||||||
val.WriteToV2(&m2)
|
|
||||||
require.Equal(t, m, m2)
|
|
||||||
|
|
||||||
require.True(t, val.AssertUser(usr))
|
|
||||||
require.False(t, val.AssertUser(usr2))
|
|
||||||
|
|
||||||
k, err := keys.NewPrivateKey()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
signer := frostfsecdsa.Signer(k.PrivateKey)
|
|
||||||
|
|
||||||
var s frostfscrypto.Signature
|
|
||||||
|
|
||||||
require.NoError(t, s.Calculate(signer, body.StableMarshal(nil)))
|
|
||||||
|
|
||||||
s.WriteToV2(&sig)
|
|
||||||
|
|
||||||
require.NoError(t, val.ReadFromV2(m))
|
|
||||||
require.True(t, val.VerifySignature())
|
|
||||||
require.Equal(t, sig.GetKey(), val.SigningKeyBytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResolveIssuer(t *testing.T) {
|
|
||||||
k, err := keys.NewPrivateKey()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
var val bearer.Token
|
|
||||||
|
|
||||||
require.Zero(t, bearer.ResolveIssuer(val))
|
|
||||||
|
|
||||||
var m acl.BearerToken
|
|
||||||
|
|
||||||
var sig refs.Signature
|
|
||||||
sig.SetKey([]byte("invalid key"))
|
|
||||||
|
|
||||||
m.SetSignature(&sig)
|
|
||||||
|
|
||||||
require.NoError(t, val.Unmarshal(m.StableMarshal(nil)))
|
|
||||||
|
|
||||||
require.Zero(t, bearer.ResolveIssuer(val))
|
|
||||||
|
|
||||||
require.NoError(t, val.Sign(k.PrivateKey))
|
|
||||||
|
|
||||||
var usr user.ID
|
|
||||||
user.IDFromKey(&usr, k.PrivateKey.PublicKey)
|
|
||||||
|
|
||||||
require.Equal(t, usr, bearer.ResolveIssuer(val))
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
/*
|
|
||||||
Package bearer provides bearer token definition.
|
|
||||||
|
|
||||||
Bearer token is attached to the object service requests, and it overwrites
|
|
||||||
extended ACL of the container. Mainly it is used to provide access of private
|
|
||||||
data for specific user. Therefore, it must be signed by owner of the container.
|
|
||||||
|
|
||||||
Define bearer token by setting correct lifetime, extended ACL and owner ID of
|
|
||||||
the user that will attach token to its requests.
|
|
||||||
|
|
||||||
var bearerToken bearer.Token
|
|
||||||
bearerToken.SetExpiration(500)
|
|
||||||
bearerToken.SetIssuedAt(10)
|
|
||||||
bearerToken.SetNotBefore(10)
|
|
||||||
bearerToken.SetEACL(eaclTable)
|
|
||||||
bearerToken.SetOwner(ownerID)
|
|
||||||
|
|
||||||
Bearer token must be signed by owner of the container.
|
|
||||||
|
|
||||||
err := bearerToken.Sign(privateKey)
|
|
||||||
|
|
||||||
Provide signed token in JSON or binary format to the request sender. Request
|
|
||||||
sender can attach this bearer token to the object service requests:
|
|
||||||
|
|
||||||
import sdkClient "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client"
|
|
||||||
|
|
||||||
var headParams sdkClient.PrmObjectHead
|
|
||||||
headParams.WithBearerToken(bearerToken)
|
|
||||||
response, err := client.ObjectHead(ctx, headParams)
|
|
||||||
*/
|
|
||||||
package bearer
|
|
|
@ -1,6 +0,0 @@
|
||||||
/*
|
|
||||||
Package bearertest provides functions for testing bearer package.
|
|
||||||
|
|
||||||
Note that this package intended only for tests.
|
|
||||||
*/
|
|
||||||
package bearertest
|
|
|
@ -1,32 +0,0 @@
|
||||||
package bearertest
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/ape"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
|
|
||||||
eacltest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl/test"
|
|
||||||
usertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Token returns random bearer.Token.
|
|
||||||
//
|
|
||||||
// Resulting token is unsigned.
|
|
||||||
func Token() (t bearer.Token) {
|
|
||||||
t.SetExp(3)
|
|
||||||
t.SetNbf(2)
|
|
||||||
t.SetIat(1)
|
|
||||||
t.ForUser(usertest.ID())
|
|
||||||
t.SetEACLTable(*eacltest.Table())
|
|
||||||
t.SetAPEOverride(APEOverride())
|
|
||||||
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
|
|
||||||
func APEOverride() bearer.APEOverride {
|
|
||||||
return bearer.APEOverride{
|
|
||||||
Target: ape.ChainTarget{
|
|
||||||
TargetType: ape.TargetTypeContainer,
|
|
||||||
Name: "F8JsMnChywiPvbDvpxMbjTjx5KhWHHp6gCDt8BhzL9kF",
|
|
||||||
},
|
|
||||||
Chains: []ape.Chain{{Raw: []byte("{}")}},
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,78 +1,51 @@
|
||||||
package checksum
|
package checksum
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
"github.com/nspcc-dev/neofs-api-go/v2/refs"
|
||||||
"git.frostfs.info/TrueCloudLab/tzhash/tz"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Checksum represents checksum of some digital data.
|
// Checksum represents v2-compatible checksum.
|
||||||
//
|
|
||||||
// Checksum is mutually compatible with git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs.Checksum
|
|
||||||
// message. See ReadFromV2 / WriteToV2 methods.
|
|
||||||
//
|
|
||||||
// Instances can be created using built-in var declaration.
|
|
||||||
//
|
|
||||||
// Note that direct typecast is not safe and may result in loss of compatibility:
|
|
||||||
//
|
|
||||||
// _ = Checksum(refs.Checksum{}) // not recommended
|
|
||||||
type Checksum refs.Checksum
|
type Checksum refs.Checksum
|
||||||
|
|
||||||
// Type represents the enumeration
|
// Type represents the enumeration
|
||||||
// of checksum types.
|
// of checksum types.
|
||||||
type Type refs.ChecksumType
|
type Type uint8
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Unknown is an undefined checksum type.
|
// Unknown is an undefined checksum type.
|
||||||
Unknown Type = Type(refs.UnknownChecksum)
|
Unknown Type = iota
|
||||||
|
|
||||||
// SHA256 is a SHA256 checksum type.
|
// SHA256 is a SHA256 checksum type.
|
||||||
SHA256 = Type(refs.SHA256)
|
SHA256
|
||||||
|
|
||||||
// TZ is a Tillich-Zémor checksum type.
|
// TZ is a Tillich-Zemor checksum type.
|
||||||
TZ = Type(refs.TillichZemor)
|
TZ
|
||||||
)
|
)
|
||||||
|
|
||||||
// ReadFromV2 reads Checksum from the refs.Checksum message. Checks if the
|
// NewFromV2 wraps v2 Checksum message to Checksum.
|
||||||
// message conforms to FrostFS API V2 protocol.
|
|
||||||
//
|
//
|
||||||
// See also WriteToV2.
|
// Nil refs.Checksum converts to nil.
|
||||||
func (c *Checksum) ReadFromV2(m refs.Checksum) error {
|
func NewFromV2(cV2 *refs.Checksum) *Checksum {
|
||||||
if len(m.GetSum()) == 0 {
|
return (*Checksum)(cV2)
|
||||||
return errors.New("missing value")
|
|
||||||
}
|
|
||||||
|
|
||||||
switch m.GetType() {
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported type %v", m.GetType())
|
|
||||||
case refs.SHA256, refs.TillichZemor:
|
|
||||||
}
|
|
||||||
|
|
||||||
*c = Checksum(m)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteToV2 writes Checksum to the refs.Checksum message.
|
// New creates and initializes blank Checksum.
|
||||||
// The message must not be nil.
|
|
||||||
//
|
//
|
||||||
// See also ReadFromV2.
|
// Defaults:
|
||||||
func (c Checksum) WriteToV2(m *refs.Checksum) {
|
// - sum: nil;
|
||||||
*m = (refs.Checksum)(c)
|
// - type: Unknown.
|
||||||
|
func New() *Checksum {
|
||||||
|
return NewFromV2(new(refs.Checksum))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type returns checksum type.
|
// Type returns checksum type.
|
||||||
//
|
func (c *Checksum) Type() Type {
|
||||||
// Zero Checksum has Unknown checksum type.
|
switch (*refs.Checksum)(c).GetType() {
|
||||||
//
|
|
||||||
// See also SetTillichZemor and SetSHA256.
|
|
||||||
func (c Checksum) Type() Type {
|
|
||||||
v2 := (refs.Checksum)(c)
|
|
||||||
switch v2.GetType() {
|
|
||||||
case refs.SHA256:
|
case refs.SHA256:
|
||||||
return SHA256
|
return SHA256
|
||||||
case refs.TillichZemor:
|
case refs.TillichZemor:
|
||||||
|
@ -82,70 +55,93 @@ func (c Checksum) Type() Type {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Value returns checksum bytes. Return value
|
// Sum returns checksum bytes.
|
||||||
// MUST NOT be mutated.
|
func (c *Checksum) Sum() []byte {
|
||||||
//
|
return (*refs.Checksum)(c).GetSum()
|
||||||
// Zero Checksum has nil sum.
|
|
||||||
//
|
|
||||||
// See also SetTillichZemor and SetSHA256.
|
|
||||||
func (c Checksum) Value() []byte {
|
|
||||||
v2 := (refs.Checksum)(c)
|
|
||||||
return v2.GetSum()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetSHA256 sets checksum to SHA256 hash.
|
// SetSHA256 sets checksum to SHA256 hash.
|
||||||
//
|
|
||||||
// See also Calculate.
|
|
||||||
func (c *Checksum) SetSHA256(v [sha256.Size]byte) {
|
func (c *Checksum) SetSHA256(v [sha256.Size]byte) {
|
||||||
v2 := (*refs.Checksum)(c)
|
checksum := (*refs.Checksum)(c)
|
||||||
|
|
||||||
v2.SetType(refs.SHA256)
|
checksum.SetType(refs.SHA256)
|
||||||
v2.SetSum(v[:])
|
checksum.SetSum(v[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate calculates checksum and sets it
|
// SetTillichZemor sets checksum to Tillich-Zemor hash.
|
||||||
// to the passed checksum. Checksum must not be nil.
|
func (c *Checksum) SetTillichZemor(v [64]byte) {
|
||||||
|
checksum := (*refs.Checksum)(c)
|
||||||
|
|
||||||
|
checksum.SetType(refs.TillichZemor)
|
||||||
|
checksum.SetSum(v[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToV2 converts Checksum to v2 Checksum message.
|
||||||
//
|
//
|
||||||
// Does nothing if the passed type is not one of the:
|
// Nil Checksum converts to nil.
|
||||||
// - SHA256;
|
func (c *Checksum) ToV2() *refs.Checksum {
|
||||||
// - TZ.
|
return (*refs.Checksum)(c)
|
||||||
//
|
}
|
||||||
// Does not mutate the passed value.
|
|
||||||
//
|
func Equal(cs1, cs2 *Checksum) bool {
|
||||||
// See also SetSHA256, SetTillichZemor.
|
return cs1.Type() == cs2.Type() && bytes.Equal(cs1.Sum(), cs2.Sum())
|
||||||
func Calculate(c *Checksum, t Type, v []byte) {
|
}
|
||||||
switch t {
|
|
||||||
case SHA256:
|
// Marshal marshals Checksum into a protobuf binary form.
|
||||||
c.SetSHA256(sha256.Sum256(v))
|
func (c *Checksum) Marshal() ([]byte, error) {
|
||||||
case TZ:
|
return (*refs.Checksum)(c).StableMarshal(nil)
|
||||||
c.SetTillichZemor(tz.Sum(v))
|
}
|
||||||
default:
|
|
||||||
|
// Unmarshal unmarshals protobuf binary representation of Checksum.
|
||||||
|
func (c *Checksum) Unmarshal(data []byte) error {
|
||||||
|
return (*refs.Checksum)(c).Unmarshal(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON encodes Checksum to protobuf JSON format.
|
||||||
|
func (c *Checksum) MarshalJSON() ([]byte, error) {
|
||||||
|
return (*refs.Checksum)(c).MarshalJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON decodes Checksum from protobuf JSON format.
|
||||||
|
func (c *Checksum) UnmarshalJSON(data []byte) error {
|
||||||
|
return (*refs.Checksum)(c).UnmarshalJSON(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Checksum) String() string {
|
||||||
|
return hex.EncodeToString((*refs.Checksum)(c).GetSum())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse parses Checksum from its string representation.
|
||||||
|
func (c *Checksum) Parse(s string) error {
|
||||||
|
data, err := hex.DecodeString(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var typ refs.ChecksumType
|
||||||
|
|
||||||
|
switch ln := len(data); ln {
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported checksum length %d", ln)
|
||||||
|
case sha256.Size:
|
||||||
|
typ = refs.SHA256
|
||||||
|
case 64:
|
||||||
|
typ = refs.TillichZemor
|
||||||
|
}
|
||||||
|
|
||||||
|
cV2 := (*refs.Checksum)(c)
|
||||||
|
cV2.SetType(typ)
|
||||||
|
cV2.SetSum(data)
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetTillichZemor sets checksum to Tillich-Zémor hash.
|
// String returns string representation of Type.
|
||||||
//
|
//
|
||||||
// See also Calculate.
|
// String mapping:
|
||||||
func (c *Checksum) SetTillichZemor(v [tz.Size]byte) {
|
// * TZ: TZ;
|
||||||
v2 := (*refs.Checksum)(c)
|
// * SHA256: SHA256;
|
||||||
|
// * Unknown, default: CHECKSUM_TYPE_UNSPECIFIED.
|
||||||
v2.SetType(refs.TillichZemor)
|
|
||||||
v2.SetSum(v[:])
|
|
||||||
}
|
|
||||||
|
|
||||||
// String implements fmt.Stringer.
|
|
||||||
//
|
|
||||||
// String is designed to be human-readable, and its format MAY differ between
|
|
||||||
// SDK versions.
|
|
||||||
func (c Checksum) String() string {
|
|
||||||
v2 := (refs.Checksum)(c)
|
|
||||||
return fmt.Sprintf("%s:%s", c.Type(), hex.EncodeToString(v2.GetSum()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// String implements fmt.Stringer.
|
|
||||||
//
|
|
||||||
// String is designed to be human-readable, and its format MAY differ between
|
|
||||||
// SDK versions.
|
|
||||||
func (m Type) String() string {
|
func (m Type) String() string {
|
||||||
var m2 refs.ChecksumType
|
var m2 refs.ChecksumType
|
||||||
|
|
||||||
|
@ -160,3 +156,26 @@ func (m Type) String() string {
|
||||||
|
|
||||||
return m2.String()
|
return m2.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FromString parses Type from a string representation.
|
||||||
|
// It is a reverse action to String().
|
||||||
|
//
|
||||||
|
// Returns true if s was parsed successfully.
|
||||||
|
func (m *Type) FromString(s string) bool {
|
||||||
|
var g refs.ChecksumType
|
||||||
|
|
||||||
|
ok := g.FromString(s)
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
switch g {
|
||||||
|
default:
|
||||||
|
*m = Unknown
|
||||||
|
case refs.TillichZemor:
|
||||||
|
*m = TZ
|
||||||
|
case refs.SHA256:
|
||||||
|
*m = SHA256
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
|
@ -5,13 +5,20 @@ import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
"github.com/nspcc-dev/neofs-api-go/v2/refs"
|
||||||
"git.frostfs.info/TrueCloudLab/tzhash/tz"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func randSHA256(t *testing.T) [sha256.Size]byte {
|
||||||
|
cSHA256 := [sha256.Size]byte{}
|
||||||
|
_, err := rand.Read(cSHA256[:])
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return cSHA256
|
||||||
|
}
|
||||||
|
|
||||||
func TestChecksum(t *testing.T) {
|
func TestChecksum(t *testing.T) {
|
||||||
var c Checksum
|
c := New()
|
||||||
|
|
||||||
cSHA256 := [sha256.Size]byte{}
|
cSHA256 := [sha256.Size]byte{}
|
||||||
_, _ = rand.Read(cSHA256[:])
|
_, _ = rand.Read(cSHA256[:])
|
||||||
|
@ -19,62 +26,150 @@ func TestChecksum(t *testing.T) {
|
||||||
c.SetSHA256(cSHA256)
|
c.SetSHA256(cSHA256)
|
||||||
|
|
||||||
require.Equal(t, SHA256, c.Type())
|
require.Equal(t, SHA256, c.Type())
|
||||||
require.Equal(t, cSHA256[:], c.Value())
|
require.Equal(t, cSHA256[:], c.Sum())
|
||||||
|
|
||||||
var cV2 refs.Checksum
|
cV2 := c.ToV2()
|
||||||
c.WriteToV2(&cV2)
|
|
||||||
|
|
||||||
require.Equal(t, refs.SHA256, cV2.GetType())
|
require.Equal(t, refs.SHA256, cV2.GetType())
|
||||||
require.Equal(t, cSHA256[:], cV2.GetSum())
|
require.Equal(t, cSHA256[:], cV2.GetSum())
|
||||||
|
|
||||||
cTZ := [tz.Size]byte{}
|
cTZ := [64]byte{}
|
||||||
_, _ = rand.Read(cSHA256[:])
|
_, _ = rand.Read(cSHA256[:])
|
||||||
|
|
||||||
c.SetTillichZemor(cTZ)
|
c.SetTillichZemor(cTZ)
|
||||||
|
|
||||||
require.Equal(t, TZ, c.Type())
|
require.Equal(t, TZ, c.Type())
|
||||||
require.Equal(t, cTZ[:], c.Value())
|
require.Equal(t, cTZ[:], c.Sum())
|
||||||
|
|
||||||
c.WriteToV2(&cV2)
|
cV2 = c.ToV2()
|
||||||
|
|
||||||
require.Equal(t, refs.TillichZemor, cV2.GetType())
|
require.Equal(t, refs.TillichZemor, cV2.GetType())
|
||||||
require.Equal(t, cTZ[:], cV2.GetSum())
|
require.Equal(t, cTZ[:], cV2.GetSum())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEqualChecksums(t *testing.T) {
|
||||||
|
require.True(t, Equal(nil, nil))
|
||||||
|
|
||||||
|
csSHA := [sha256.Size]byte{}
|
||||||
|
_, _ = rand.Read(csSHA[:])
|
||||||
|
|
||||||
|
cs1 := New()
|
||||||
|
cs1.SetSHA256(csSHA)
|
||||||
|
|
||||||
|
cs2 := New()
|
||||||
|
cs2.SetSHA256(csSHA)
|
||||||
|
|
||||||
|
require.True(t, Equal(cs1, cs2))
|
||||||
|
|
||||||
|
csSHA[0]++
|
||||||
|
cs2.SetSHA256(csSHA)
|
||||||
|
|
||||||
|
require.False(t, Equal(cs1, cs2))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChecksumEncoding(t *testing.T) {
|
||||||
|
cs := New()
|
||||||
|
cs.SetSHA256(randSHA256(t))
|
||||||
|
|
||||||
|
t.Run("binary", func(t *testing.T) {
|
||||||
|
data, err := cs.Marshal()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
c2 := New()
|
||||||
|
require.NoError(t, c2.Unmarshal(data))
|
||||||
|
|
||||||
|
require.Equal(t, cs, c2)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("json", func(t *testing.T) {
|
||||||
|
data, err := cs.MarshalJSON()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cs2 := New()
|
||||||
|
require.NoError(t, cs2.UnmarshalJSON(data))
|
||||||
|
|
||||||
|
require.Equal(t, cs, cs2)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("string", func(t *testing.T) {
|
||||||
|
cs2 := New()
|
||||||
|
|
||||||
|
require.NoError(t, cs2.Parse(cs.String()))
|
||||||
|
|
||||||
|
require.Equal(t, cs, cs2)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewChecksumFromV2(t *testing.T) {
|
||||||
|
t.Run("from nil", func(t *testing.T) {
|
||||||
|
var x *refs.Checksum
|
||||||
|
|
||||||
|
require.Nil(t, NewFromV2(x))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChecksum_ToV2(t *testing.T) {
|
||||||
|
t.Run("nil", func(t *testing.T) {
|
||||||
|
var x *Checksum
|
||||||
|
|
||||||
|
require.Nil(t, x.ToV2())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestNewChecksum(t *testing.T) {
|
func TestNewChecksum(t *testing.T) {
|
||||||
t.Run("default values", func(t *testing.T) {
|
t.Run("default values", func(t *testing.T) {
|
||||||
var chs Checksum
|
chs := New()
|
||||||
|
|
||||||
// check initial values
|
// check initial values
|
||||||
require.Equal(t, Unknown, chs.Type())
|
require.Equal(t, Unknown, chs.Type())
|
||||||
require.Nil(t, chs.Value())
|
require.Nil(t, chs.Sum())
|
||||||
|
|
||||||
// convert to v2 message
|
// convert to v2 message
|
||||||
var chsV2 refs.Checksum
|
chsV2 := chs.ToV2()
|
||||||
chs.WriteToV2(&chsV2)
|
|
||||||
|
|
||||||
require.Equal(t, refs.UnknownChecksum, chsV2.GetType())
|
require.Equal(t, refs.UnknownChecksum, chsV2.GetType())
|
||||||
require.Nil(t, chsV2.GetSum())
|
require.Nil(t, chsV2.GetSum())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCalculation(t *testing.T) {
|
type enumIface interface {
|
||||||
var c Checksum
|
FromString(string) bool
|
||||||
payload := []byte{0, 1, 2, 3, 4, 5}
|
String() string
|
||||||
|
}
|
||||||
|
|
||||||
t.Run("SHA256", func(t *testing.T) {
|
type enumStringItem struct {
|
||||||
orig := sha256.Sum256(payload)
|
val enumIface
|
||||||
|
str string
|
||||||
|
}
|
||||||
|
|
||||||
Calculate(&c, SHA256, payload)
|
func testEnumStrings(t *testing.T, e enumIface, items []enumStringItem) {
|
||||||
|
for _, item := range items {
|
||||||
|
require.Equal(t, item.str, item.val.String())
|
||||||
|
|
||||||
require.Equal(t, orig[:], c.Value())
|
s := item.val.String()
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("TZ", func(t *testing.T) {
|
require.True(t, e.FromString(s), s)
|
||||||
orig := tz.Sum(payload)
|
|
||||||
|
|
||||||
Calculate(&c, TZ, payload)
|
require.EqualValues(t, item.val, e, item.val)
|
||||||
|
}
|
||||||
|
|
||||||
require.Equal(t, orig[:], c.Value())
|
// incorrect strings
|
||||||
|
for _, str := range []string{
|
||||||
|
"some string",
|
||||||
|
"undefined",
|
||||||
|
} {
|
||||||
|
require.False(t, e.FromString(str))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChecksumType_String(t *testing.T) {
|
||||||
|
toPtr := func(v Type) *Type {
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
|
testEnumStrings(t, new(Type), []enumStringItem{
|
||||||
|
{val: toPtr(TZ), str: "TZ"},
|
||||||
|
{val: toPtr(SHA256), str: "SHA256"},
|
||||||
|
{val: toPtr(Unknown), str: "CHECKSUM_TYPE_UNSPECIFIED"},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
/*
|
|
||||||
Package checksum provides primitives to work with checksums.
|
|
||||||
|
|
||||||
Checksum is a basic type of data checksums.
|
|
||||||
For example, calculating checksums:
|
|
||||||
|
|
||||||
// retrieving any payload for hashing
|
|
||||||
|
|
||||||
var sha256Sum Checksum
|
|
||||||
Calculate(&sha256Sum, SHA256, payload) // sha256Sum contains SHA256 hash of the payload
|
|
||||||
|
|
||||||
var tzSum Checksum
|
|
||||||
Calculate(&tzSum, TZ, payload) // tzSum contains TZ hash of the payload
|
|
||||||
|
|
||||||
Using package types in an application is recommended to potentially work with
|
|
||||||
different protocol versions with which these types are compatible.
|
|
||||||
*/
|
|
||||||
package checksum
|
|
|
@ -1,34 +0,0 @@
|
||||||
package checksum
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/sha256"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ExampleCalculate() {
|
|
||||||
payload := []byte{0, 1, 2, 3, 4, 5, 6}
|
|
||||||
var cs Checksum
|
|
||||||
|
|
||||||
Calculate(&cs, SHA256, payload)
|
|
||||||
Calculate(&cs, TZ, payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleChecksum_WriteToV2() {
|
|
||||||
var (
|
|
||||||
csRaw [sha256.Size]byte
|
|
||||||
csV2 refs.Checksum
|
|
||||||
cs Checksum
|
|
||||||
)
|
|
||||||
|
|
||||||
rand.Read(csRaw[:])
|
|
||||||
cs.SetSHA256(csRaw)
|
|
||||||
|
|
||||||
cs.WriteToV2(&csV2)
|
|
||||||
|
|
||||||
fmt.Println(bytes.Equal(cs.Value(), csV2.GetSum()))
|
|
||||||
// Output: true
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
/*
|
|
||||||
Package checksumtest provides functions for convenient testing of checksum package API.
|
|
||||||
|
|
||||||
Note that importing the package into source files is highly discouraged.
|
|
||||||
|
|
||||||
Random instance generation functions can be useful when testing expects any value, e.g.:
|
|
||||||
|
|
||||||
import checksumtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/checksum/test"
|
|
||||||
|
|
||||||
cs := checksumtest.Checksum()
|
|
||||||
// test the value
|
|
||||||
*/
|
|
||||||
package checksumtest
|
|
|
@ -1,19 +1,19 @@
|
||||||
package checksumtest
|
package checksumtest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"math/rand"
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/checksum"
|
"github.com/nspcc-dev/neofs-sdk-go/checksum"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Checksum returns random checksum.Checksum.
|
// Checksum returns random checksum.Checksum.
|
||||||
func Checksum() checksum.Checksum {
|
func Checksum() *checksum.Checksum {
|
||||||
var cs [sha256.Size]byte
|
var cs [sha256.Size]byte
|
||||||
|
|
||||||
_, _ = rand.Read(cs[:])
|
rand.Read(cs[:])
|
||||||
|
|
||||||
var x checksum.Checksum
|
x := checksum.New()
|
||||||
|
|
||||||
x.SetSHA256(cs)
|
x.SetSHA256(cs)
|
||||||
|
|
||||||
|
|
|
@ -2,107 +2,103 @@ package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
v2accounting "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/accounting"
|
v2accounting "github.com/nspcc-dev/neofs-api-go/v2/accounting"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
|
||||||
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
|
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
"github.com/nspcc-dev/neofs-sdk-go/accounting"
|
||||||
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
"github.com/nspcc-dev/neofs-sdk-go/owner"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/accounting"
|
|
||||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// PrmBalanceGet groups parameters of BalanceGet operation.
|
// PrmBalanceGet groups parameters of BalanceGet operation.
|
||||||
type PrmBalanceGet struct {
|
type PrmBalanceGet struct {
|
||||||
XHeaders []string
|
prmCommonMeta
|
||||||
|
|
||||||
Account user.ID
|
ownerSet bool
|
||||||
|
ownerID owner.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetAccount sets identifier of the FrostFS account for which the balance is requested.
|
// SetAccount sets identifier of the NeoFS account for which the balance is requested.
|
||||||
// Required parameter.
|
// Required parameter. Must be a valid ID according to NeoFS API protocol.
|
||||||
//
|
func (x *PrmBalanceGet) SetAccount(id owner.ID) {
|
||||||
// Deprecated: Use PrmBalanceGet.Account instead.
|
x.ownerID = id
|
||||||
func (x *PrmBalanceGet) SetAccount(id user.ID) {
|
x.ownerSet = true
|
||||||
x.Account = id
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *PrmBalanceGet) buildRequest(c *Client) (*v2accounting.BalanceRequest, error) {
|
|
||||||
if x.Account.IsEmpty() {
|
|
||||||
return nil, errorAccountNotSet
|
|
||||||
}
|
|
||||||
|
|
||||||
var accountV2 refs.OwnerID
|
|
||||||
x.Account.WriteToV2(&accountV2)
|
|
||||||
|
|
||||||
var body v2accounting.BalanceRequestBody
|
|
||||||
body.SetOwnerID(&accountV2)
|
|
||||||
|
|
||||||
var req v2accounting.BalanceRequest
|
|
||||||
req.SetBody(&body)
|
|
||||||
|
|
||||||
c.prepareRequest(&req, new(v2session.RequestMetaHeader))
|
|
||||||
return &req, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResBalanceGet groups resulting values of BalanceGet operation.
|
// ResBalanceGet groups resulting values of BalanceGet operation.
|
||||||
type ResBalanceGet struct {
|
type ResBalanceGet struct {
|
||||||
statusRes
|
statusRes
|
||||||
|
|
||||||
amount accounting.Decimal
|
amount *accounting.Decimal
|
||||||
}
|
}
|
||||||
|
|
||||||
// Amount returns current amount of funds on the FrostFS account as decimal number.
|
func (x *ResBalanceGet) setAmount(v *accounting.Decimal) {
|
||||||
func (x ResBalanceGet) Amount() accounting.Decimal {
|
x.amount = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Amount returns current amount of funds on the NeoFS account as decimal number.
|
||||||
|
//
|
||||||
|
// Client doesn't retain value so modification is safe.
|
||||||
|
func (x ResBalanceGet) Amount() *accounting.Decimal {
|
||||||
return x.amount
|
return x.amount
|
||||||
}
|
}
|
||||||
|
|
||||||
// BalanceGet requests current balance of the FrostFS account.
|
// BalanceGet requests current balance of the NeoFS account.
|
||||||
//
|
//
|
||||||
// Exactly one return value is non-nil. By default, server status is returned in res structure.
|
// Exactly one return value is non-nil. By default, server status is returned in res structure.
|
||||||
// Any client's internal or transport errors are returned as `error`,
|
// Any client's internal or transport errors are returned as `error`,
|
||||||
// If PrmInit.DisableFrostFSFailuresResolution has been called, unsuccessful
|
// If WithNeoFSErrorParsing option has been provided, unsuccessful
|
||||||
// FrostFS status codes are included in the returned result structure,
|
// NeoFS status codes are returned as `error`, otherwise, are included
|
||||||
// otherwise, are also returned as `error`.
|
// in the returned result structure.
|
||||||
//
|
//
|
||||||
// Returns an error if parameters are set incorrectly (see PrmBalanceGet docs).
|
// Immediately panics if parameters are set incorrectly (see PrmBalanceGet docs).
|
||||||
// Context is required and must not be nil. It is used for network communication.
|
// Context is required and must not be nil. It is used for network communication.
|
||||||
//
|
//
|
||||||
// Return statuses:
|
// Return statuses:
|
||||||
// - global (see Client docs).
|
// - global (see Client docs).
|
||||||
func (c *Client) BalanceGet(ctx context.Context, prm PrmBalanceGet) (*ResBalanceGet, error) {
|
func (c *Client) BalanceGet(ctx context.Context, prm PrmBalanceGet) (*ResBalanceGet, error) {
|
||||||
req, err := prm.buildRequest(c)
|
switch {
|
||||||
if err != nil {
|
case ctx == nil:
|
||||||
return nil, err
|
panic(panicMsgMissingContext)
|
||||||
|
case !prm.ownerSet:
|
||||||
|
panic("account not set")
|
||||||
|
case !prm.ownerID.Valid():
|
||||||
|
panic("invalid account ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := signature.SignServiceMessage(&c.prm.Key, req); err != nil {
|
// form request body
|
||||||
return nil, fmt.Errorf("sign request: %w", err)
|
var body v2accounting.BalanceRequestBody
|
||||||
|
|
||||||
|
body.SetOwnerID(prm.ownerID.ToV2())
|
||||||
|
|
||||||
|
// form request
|
||||||
|
var req v2accounting.BalanceRequest
|
||||||
|
|
||||||
|
req.SetBody(&body)
|
||||||
|
|
||||||
|
// init call context
|
||||||
|
|
||||||
|
var (
|
||||||
|
cc contextCall
|
||||||
|
res ResBalanceGet
|
||||||
|
)
|
||||||
|
|
||||||
|
c.initCallContext(&cc)
|
||||||
|
cc.meta = prm.prmCommonMeta
|
||||||
|
cc.req = &req
|
||||||
|
cc.statusRes = &res
|
||||||
|
cc.call = func() (responseV2, error) {
|
||||||
|
return rpcapi.Balance(&c.c, &req, client.WithContext(ctx))
|
||||||
|
}
|
||||||
|
cc.result = func(r responseV2) {
|
||||||
|
resp := r.(*v2accounting.BalanceResponse)
|
||||||
|
res.setAmount(accounting.NewDecimalFromV2(resp.GetBody().GetBalance()))
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := rpcapi.Balance(&c.c, req, client.WithContext(ctx))
|
// process call
|
||||||
if err != nil {
|
if !cc.processCall() {
|
||||||
return nil, err
|
return nil, cc.err
|
||||||
}
|
}
|
||||||
|
|
||||||
var res ResBalanceGet
|
|
||||||
res.st, err = c.processResponse(resp)
|
|
||||||
if err != nil || !apistatus.IsSuccessful(res.st) {
|
|
||||||
return &res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldBalance = "balance"
|
|
||||||
|
|
||||||
bal := resp.GetBody().GetBalance()
|
|
||||||
if bal == nil {
|
|
||||||
return &res, newErrMissingResponseField(fieldBalance)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := res.amount.ReadFromV2(*bal); err != nil {
|
|
||||||
return &res, newErrInvalidResponseField(fieldBalance, err)
|
|
||||||
}
|
|
||||||
return &res, nil
|
return &res, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,79 +0,0 @@
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
apemanagerV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/apemanager"
|
|
||||||
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
|
||||||
sessionV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
|
|
||||||
apeSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/ape"
|
|
||||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PrmAPEManagerAddChain groups parameters of APEManagerAddChain operation.
|
|
||||||
type PrmAPEManagerAddChain struct {
|
|
||||||
XHeaders []string
|
|
||||||
|
|
||||||
ChainTarget apeSDK.ChainTarget
|
|
||||||
|
|
||||||
Chain apeSDK.Chain
|
|
||||||
}
|
|
||||||
|
|
||||||
func (prm *PrmAPEManagerAddChain) buildRequest(c *Client) (*apemanagerV2.AddChainRequest, error) {
|
|
||||||
if len(prm.XHeaders)%2 != 0 {
|
|
||||||
return nil, errorInvalidXHeaders
|
|
||||||
}
|
|
||||||
|
|
||||||
req := new(apemanagerV2.AddChainRequest)
|
|
||||||
reqBody := new(apemanagerV2.AddChainRequestBody)
|
|
||||||
|
|
||||||
reqBody.SetTarget(prm.ChainTarget.ToV2())
|
|
||||||
reqBody.SetChain(prm.Chain.ToV2())
|
|
||||||
|
|
||||||
req.SetBody(reqBody)
|
|
||||||
|
|
||||||
var meta sessionV2.RequestMetaHeader
|
|
||||||
writeXHeadersToMeta(prm.XHeaders, &meta)
|
|
||||||
|
|
||||||
c.prepareRequest(req, &meta)
|
|
||||||
|
|
||||||
return req, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResAPEManagerAddChain struct {
|
|
||||||
statusRes
|
|
||||||
|
|
||||||
// ChainID of set Chain. If Chain does not contain chainID before request, then
|
|
||||||
// ChainID is generated.
|
|
||||||
ChainID apeSDK.ChainID
|
|
||||||
}
|
|
||||||
|
|
||||||
// APEManagerAddChain sets Chain for ChainTarget.
|
|
||||||
func (c *Client) APEManagerAddChain(ctx context.Context, prm PrmAPEManagerAddChain) (*ResAPEManagerAddChain, error) {
|
|
||||||
req, err := prm.buildRequest(c)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := signature.SignServiceMessage(&c.prm.Key, req); err != nil {
|
|
||||||
return nil, fmt.Errorf("sign request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := rpcapi.AddChain(&c.c, req, client.WithContext(ctx))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var res ResAPEManagerAddChain
|
|
||||||
res.st, err = c.processResponse(resp)
|
|
||||||
if err != nil || !apistatus.IsSuccessful(res.st) {
|
|
||||||
return &res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
res.ChainID = resp.GetBody().GetChainID()
|
|
||||||
|
|
||||||
return &res, nil
|
|
||||||
}
|
|
|
@ -1,80 +0,0 @@
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
apemanagerV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/apemanager"
|
|
||||||
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
|
||||||
sessionV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
|
|
||||||
apeSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/ape"
|
|
||||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PrmAPEManagerListChains groups parameters of APEManagerListChains operation.
|
|
||||||
type PrmAPEManagerListChains struct {
|
|
||||||
XHeaders []string
|
|
||||||
|
|
||||||
ChainTarget apeSDK.ChainTarget
|
|
||||||
}
|
|
||||||
|
|
||||||
func (prm *PrmAPEManagerListChains) buildRequest(c *Client) (*apemanagerV2.ListChainsRequest, error) {
|
|
||||||
if len(prm.XHeaders)%2 != 0 {
|
|
||||||
return nil, errorInvalidXHeaders
|
|
||||||
}
|
|
||||||
|
|
||||||
req := new(apemanagerV2.ListChainsRequest)
|
|
||||||
reqBody := new(apemanagerV2.ListChainsRequestBody)
|
|
||||||
|
|
||||||
reqBody.SetTarget(prm.ChainTarget.ToV2())
|
|
||||||
|
|
||||||
req.SetBody(reqBody)
|
|
||||||
|
|
||||||
var meta sessionV2.RequestMetaHeader
|
|
||||||
writeXHeadersToMeta(prm.XHeaders, &meta)
|
|
||||||
|
|
||||||
c.prepareRequest(req, &meta)
|
|
||||||
|
|
||||||
return req, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResAPEManagerListChains struct {
|
|
||||||
statusRes
|
|
||||||
|
|
||||||
Chains []apeSDK.Chain
|
|
||||||
}
|
|
||||||
|
|
||||||
// APEManagerListChains lists Chains for ChainTarget.
|
|
||||||
func (c *Client) APEManagerListChains(ctx context.Context, prm PrmAPEManagerListChains) (*ResAPEManagerListChains, error) {
|
|
||||||
req, err := prm.buildRequest(c)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := signature.SignServiceMessage(&c.prm.Key, req); err != nil {
|
|
||||||
return nil, fmt.Errorf("sign request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := rpcapi.ListChains(&c.c, req, client.WithContext(ctx))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var res ResAPEManagerListChains
|
|
||||||
res.st, err = c.processResponse(resp)
|
|
||||||
if err != nil || !apistatus.IsSuccessful(res.st) {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, ch := range resp.GetBody().GetChains() {
|
|
||||||
var chSDK apeSDK.Chain
|
|
||||||
if err := chSDK.ReadFromV2(ch); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
res.Chains = append(res.Chains, chSDK)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &res, nil
|
|
||||||
}
|
|
|
@ -1,73 +0,0 @@
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
apemanagerV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/apemanager"
|
|
||||||
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
|
||||||
sessionV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
|
|
||||||
apeSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/ape"
|
|
||||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PrmAPEManagerRemoveChain groups parameters of APEManagerRemoveChain operation.
|
|
||||||
type PrmAPEManagerRemoveChain struct {
|
|
||||||
XHeaders []string
|
|
||||||
|
|
||||||
ChainTarget apeSDK.ChainTarget
|
|
||||||
|
|
||||||
ChainID apeSDK.ChainID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (prm *PrmAPEManagerRemoveChain) buildRequest(c *Client) (*apemanagerV2.RemoveChainRequest, error) {
|
|
||||||
if len(prm.XHeaders)%2 != 0 {
|
|
||||||
return nil, errorInvalidXHeaders
|
|
||||||
}
|
|
||||||
|
|
||||||
req := new(apemanagerV2.RemoveChainRequest)
|
|
||||||
reqBody := new(apemanagerV2.RemoveChainRequestBody)
|
|
||||||
|
|
||||||
reqBody.SetTarget(prm.ChainTarget.ToV2())
|
|
||||||
reqBody.SetChainID(prm.ChainID)
|
|
||||||
|
|
||||||
req.SetBody(reqBody)
|
|
||||||
|
|
||||||
var meta sessionV2.RequestMetaHeader
|
|
||||||
writeXHeadersToMeta(prm.XHeaders, &meta)
|
|
||||||
|
|
||||||
c.prepareRequest(req, &meta)
|
|
||||||
|
|
||||||
return req, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResAPEManagerRemoveChain struct {
|
|
||||||
statusRes
|
|
||||||
}
|
|
||||||
|
|
||||||
// APEManagerRemoveChain removes Chain with ChainID defined for ChainTarget.
|
|
||||||
func (c *Client) APEManagerRemoveChain(ctx context.Context, prm PrmAPEManagerRemoveChain) (*ResAPEManagerRemoveChain, error) {
|
|
||||||
req, err := prm.buildRequest(c)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := signature.SignServiceMessage(&c.prm.Key, req); err != nil {
|
|
||||||
return nil, fmt.Errorf("sign request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := rpcapi.RemoveChain(&c.c, req, client.WithContext(ctx))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var res ResAPEManagerRemoveChain
|
|
||||||
res.st, err = c.processResponse(resp)
|
|
||||||
if err != nil || !apistatus.IsSuccessful(res.st) {
|
|
||||||
return &res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &res, nil
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
v2netmap "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/netmap"
|
|
||||||
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
|
||||||
)
|
|
||||||
|
|
||||||
// interface of FrostFS API server. Exists for test purposes only.
|
|
||||||
type frostFSAPIServer interface {
|
|
||||||
netMapSnapshot(context.Context, v2netmap.SnapshotRequest) (*v2netmap.SnapshotResponse, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// wrapper over real client connection which communicates over FrostFS API protocol.
|
|
||||||
// Provides frostFSAPIServer for Client instances used in real applications.
|
|
||||||
type coreServer client.Client
|
|
||||||
|
|
||||||
// unifies errors of all RPC.
|
|
||||||
func rpcErr(e error) error {
|
|
||||||
return fmt.Errorf("rpc failure: %w", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
// executes NetmapService.NetmapSnapshot RPC declared in FrostFS API protocol
|
|
||||||
// using underlying client.Client.
|
|
||||||
func (x *coreServer) netMapSnapshot(ctx context.Context, req v2netmap.SnapshotRequest) (*v2netmap.SnapshotResponse, error) {
|
|
||||||
resp, err := rpcapi.NetMapSnapshot((*client.Client)(x), &req, client.WithContext(ctx))
|
|
||||||
if err != nil {
|
|
||||||
return nil, rpcErr(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
171
client/client.go
171
client/client.go
|
@ -1,26 +1,23 @@
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
v2accounting "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/accounting"
|
v2accounting "github.com/nspcc-dev/neofs-api-go/v2/accounting"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
|
"github.com/nspcc-dev/neofs-api-go/v2/rpc"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
|
||||||
"google.golang.org/grpc"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Client represents virtual connection to the FrostFS network to communicate
|
// Client represents virtual connection to the NeoFS network to communicate
|
||||||
// with FrostFS server using FrostFS API protocol. It is designed to provide
|
// with NeoFS server using NeoFS API protocol. It is designed to provide
|
||||||
// an abstraction interface from the protocol details of data transfer over
|
// an abstraction interface from the protocol details of data transfer over
|
||||||
// a network in FrostFS.
|
// a network in NeoFS.
|
||||||
//
|
//
|
||||||
// Client can be created using simple Go variable declaration. Before starting
|
// Client can be created using simple Go variable declaration. Before starting
|
||||||
// work with the Client, it SHOULD BE correctly initialized (see Init method).
|
// work with the Client, it SHOULD BE correctly initialized (see Init method).
|
||||||
// Before executing the FrostFS operations using the Client, connection to the
|
// Before executing the NeoFS operations using the Client, connection to the
|
||||||
// server MUST BE correctly established (see Dial method and pay attention
|
// server MUST BE correctly established (see Dial method and pay attention
|
||||||
// to the mandatory parameters). Using the Client before connecting have
|
// to the mandatory parameters). Using the Client before connecting have
|
||||||
// been established can lead to a panic. After the work, the Client SHOULD BE
|
// been established can lead to a panic. After the work, the Client SHOULD BE
|
||||||
|
@ -29,7 +26,7 @@ import (
|
||||||
// during the communication process step strongly discouraged as it leads to
|
// during the communication process step strongly discouraged as it leads to
|
||||||
// undefined behavior.
|
// undefined behavior.
|
||||||
//
|
//
|
||||||
// Each method which produces a FrostFS API call may return a server response.
|
// Each method which produces a NeoFS API call may return a server response.
|
||||||
// Status responses are returned in the result structure, and can be cast
|
// Status responses are returned in the result structure, and can be cast
|
||||||
// to built-in error instance (or in the returned error if the client is
|
// to built-in error instance (or in the returned error if the client is
|
||||||
// configured accordingly). Certain statuses can be checked using `apistatus`
|
// configured accordingly). Certain statuses can be checked using `apistatus`
|
||||||
|
@ -37,9 +34,8 @@ import (
|
||||||
// functions to work with status returns (e.g. IsErrContainerNotFound).
|
// functions to work with status returns (e.g. IsErrContainerNotFound).
|
||||||
// All possible responses are documented in methods, however, some may be
|
// All possible responses are documented in methods, however, some may be
|
||||||
// returned from all of them (pay attention to the presence of the pointer sign):
|
// returned from all of them (pay attention to the presence of the pointer sign):
|
||||||
// - *apistatus.ServerInternal on internal server error;
|
// - *apistatus.ServerInternal on internal server error;
|
||||||
// - *apistatus.NodeUnderMaintenance if a server is under maintenance;
|
// - *apistatus.SuccessDefaultV2 on default success.
|
||||||
// - *apistatus.SuccessDefaultV2 on default success.
|
|
||||||
//
|
//
|
||||||
// Client MUST NOT be copied by value: use pointer to Client instead.
|
// Client MUST NOT be copied by value: use pointer to Client instead.
|
||||||
//
|
//
|
||||||
|
@ -48,8 +44,6 @@ type Client struct {
|
||||||
prm PrmInit
|
prm PrmInit
|
||||||
|
|
||||||
c client.Client
|
c client.Client
|
||||||
|
|
||||||
server frostFSAPIServer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init brings the Client instance to its initial state.
|
// Init brings the Client instance to its initial state.
|
||||||
|
@ -62,63 +56,42 @@ func (c *Client) Init(prm PrmInit) {
|
||||||
c.prm = prm
|
c.prm = prm
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dial establishes a connection to the server from the FrostFS network.
|
// Dial establishes a connection to the server from the NeoFS network.
|
||||||
// Returns an error describing failure reason. If failed, the Client
|
// Returns an error describing failure reason. If failed, the Client
|
||||||
// SHOULD NOT be used.
|
// SHOULD NOT be used.
|
||||||
//
|
//
|
||||||
// Uses the context specified by SetContext if it was called with non-nil
|
// Panics if required parameters are set incorrectly, look carefully
|
||||||
// argument, otherwise context.Background() is used. Dial returns context
|
|
||||||
// errors, see context package docs for details.
|
|
||||||
//
|
|
||||||
// Returns an error if required parameters are set incorrectly, look carefully
|
|
||||||
// at the method documentation.
|
// at the method documentation.
|
||||||
//
|
//
|
||||||
// One-time method call during application start-up stage (after Init ) is expected.
|
// One-time method call during application start-up stage (after Init ) is expected.
|
||||||
// Calling multiple times leads to undefined behavior.
|
// Calling multiple times leads to undefined behavior.
|
||||||
//
|
//
|
||||||
// See also Init / Close.
|
// See also Init / Close.
|
||||||
func (c *Client) Dial(ctx context.Context, prm PrmDial) error {
|
func (c *Client) Dial(prm PrmDial) error {
|
||||||
if prm.Endpoint == "" {
|
if prm.endpoint == "" {
|
||||||
return errorServerAddrUnset
|
panic("server address is unset or empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
if prm.DialTimeout <= 0 {
|
if prm.timeoutDialSet {
|
||||||
prm.DialTimeout = defaultDialTimeout
|
if prm.timeoutDial <= 0 {
|
||||||
}
|
panic("non-positive timeout")
|
||||||
if prm.StreamTimeout <= 0 {
|
}
|
||||||
prm.StreamTimeout = defaultStreamTimeout
|
} else {
|
||||||
|
prm.timeoutDial = 5 * time.Second
|
||||||
}
|
}
|
||||||
|
|
||||||
c.c = *client.New(append(
|
c.c = *client.New(append(
|
||||||
client.WithNetworkURIAddress(prm.Endpoint, prm.TLSConfig),
|
client.WithNetworkURIAddress(prm.endpoint, prm.tlsConfig),
|
||||||
client.WithDialTimeout(prm.DialTimeout),
|
client.WithDialTimeout(prm.timeoutDial),
|
||||||
client.WithRWTimeout(prm.StreamTimeout),
|
|
||||||
client.WithGRPCDialOptions(prm.GRPCDialOptions),
|
|
||||||
)...)
|
)...)
|
||||||
|
|
||||||
c.setFrostFSAPIServer((*coreServer)(&c.c))
|
|
||||||
|
|
||||||
// TODO: (neofs-api-go#382) perform generic dial stage of the client.Client
|
// TODO: (neofs-api-go#382) perform generic dial stage of the client.Client
|
||||||
_, err := rpc.Balance(&c.c, new(v2accounting.BalanceRequest),
|
_, _ = rpc.Balance(&c.c, new(v2accounting.BalanceRequest))
|
||||||
client.WithContext(ctx),
|
|
||||||
)
|
|
||||||
// return context errors since they signal about dial problem
|
|
||||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// sets underlying provider of frostFSAPIServer. The method is used for testing as an approach
|
// Close closes underlying connection to the NeoFS server. Implements io.Closer.
|
||||||
// to skip Dial stage and override FrostFS API server. MUST NOT be used outside test code.
|
|
||||||
// In real applications wrapper over git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client
|
|
||||||
// is statically used.
|
|
||||||
func (c *Client) setFrostFSAPIServer(server frostFSAPIServer) {
|
|
||||||
c.server = server
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes underlying connection to the FrostFS server. Implements io.Closer.
|
|
||||||
// MUST NOT be called before successful Dial. Can be called concurrently
|
// MUST NOT be called before successful Dial. Can be called concurrently
|
||||||
// with server operations processing on running goroutines: in this case
|
// with server operations processing on running goroutines: in this case
|
||||||
// they are likely to fail due to a connection error.
|
// they are likely to fail due to a connection error.
|
||||||
|
@ -135,117 +108,75 @@ func (c *Client) Close() error {
|
||||||
//
|
//
|
||||||
// See also Init.
|
// See also Init.
|
||||||
type PrmInit struct {
|
type PrmInit struct {
|
||||||
DisableFrostFSErrorResolution bool
|
resolveNeoFSErrors bool
|
||||||
|
|
||||||
Key ecdsa.PrivateKey
|
key ecdsa.PrivateKey
|
||||||
|
|
||||||
ResponseInfoCallback func(ResponseMetaInfo) error
|
cbRespInfo func(ResponseMetaInfo) error
|
||||||
|
|
||||||
NetMagic uint64
|
netMagic uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDefaultPrivateKey sets Client private key to be used for the protocol
|
// SetDefaultPrivateKey sets Client private key to be used for the protocol
|
||||||
// communication by default.
|
// communication by default.
|
||||||
//
|
//
|
||||||
// Required for operations without custom key parametrization (see corresponding Prm* docs).
|
// Required for operations without custom key parametrization (see corresponding Prm* docs).
|
||||||
//
|
|
||||||
// Deprecated: Use PrmInit.Key instead.
|
|
||||||
func (x *PrmInit) SetDefaultPrivateKey(key ecdsa.PrivateKey) {
|
func (x *PrmInit) SetDefaultPrivateKey(key ecdsa.PrivateKey) {
|
||||||
x.Key = key
|
x.key = key
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deprecated: method is no-op. Option is default.
|
// ResolveNeoFSFailures makes the Client to resolve failure statuses of the
|
||||||
func (x *PrmInit) ResolveFrostFSFailures() {
|
// NeoFS protocol into Go built-in errors. These errors are returned from
|
||||||
}
|
// each protocol operation. By default, statuses aren't resolved and written
|
||||||
|
// to the resulting structure (see corresponding Res* docs).
|
||||||
// DisableFrostFSFailuresResolution makes the Client to preserve failure statuses of the
|
func (x *PrmInit) ResolveNeoFSFailures() {
|
||||||
// FrostFS protocol only in resulting structure (see corresponding Res* docs).
|
x.resolveNeoFSErrors = true
|
||||||
// These errors are returned from each protocol operation. By default, statuses
|
|
||||||
// are resolved and returned as a Go built-in errors.
|
|
||||||
//
|
|
||||||
// Deprecated: Use PrmInit.DisableFrostFSErrorResolution instead.
|
|
||||||
func (x *PrmInit) DisableFrostFSFailuresResolution() {
|
|
||||||
x.DisableFrostFSErrorResolution = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetResponseInfoCallback makes the Client to pass ResponseMetaInfo from each
|
// SetResponseInfoCallback makes the Client to pass ResponseMetaInfo from each
|
||||||
// FrostFS server response to f. Nil (default) means ignore response meta info.
|
// NeoFS server response to f. Nil (default) means ignore response meta info.
|
||||||
//
|
|
||||||
// Deprecated: Use PrmInit.ResponseInfoCallback instead.
|
|
||||||
func (x *PrmInit) SetResponseInfoCallback(f func(ResponseMetaInfo) error) {
|
func (x *PrmInit) SetResponseInfoCallback(f func(ResponseMetaInfo) error) {
|
||||||
x.ResponseInfoCallback = f
|
x.cbRespInfo = f
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
|
||||||
defaultDialTimeout = 5 * time.Second
|
|
||||||
defaultStreamTimeout = 10 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
// PrmDial groups connection parameters for the Client.
|
// PrmDial groups connection parameters for the Client.
|
||||||
//
|
//
|
||||||
// See also Dial.
|
// See also Dial.
|
||||||
type PrmDial struct {
|
type PrmDial struct {
|
||||||
Endpoint string
|
endpoint string
|
||||||
|
|
||||||
TLSConfig *tls.Config
|
tlsConfig *tls.Config
|
||||||
|
|
||||||
// If DialTimeout is non-positive, then it's set to defaultDialTimeout.
|
timeoutDialSet bool
|
||||||
DialTimeout time.Duration
|
timeoutDial time.Duration
|
||||||
|
|
||||||
// If StreamTimeout is non-positive, then it's set to defaultStreamTimeout.
|
|
||||||
StreamTimeout time.Duration
|
|
||||||
|
|
||||||
GRPCDialOptions []grpc.DialOption
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetServerURI sets server URI in the FrostFS network.
|
// SetServerURI sets server URI in the NeoFS network.
|
||||||
// Required parameter.
|
// Required parameter.
|
||||||
//
|
//
|
||||||
// Format of the URI:
|
// Format of the URI:
|
||||||
//
|
// [scheme://]host:port
|
||||||
// [scheme://]host:port
|
|
||||||
//
|
//
|
||||||
// Supported schemes:
|
// Supported schemes:
|
||||||
//
|
// grpc
|
||||||
// grpc
|
// grpcs
|
||||||
// grpcs
|
|
||||||
//
|
//
|
||||||
// See also SetTLSConfig.
|
// See also SetTLSConfig.
|
||||||
//
|
|
||||||
// Deprecated: Use PrmDial.Endpoint instead.
|
|
||||||
func (x *PrmDial) SetServerURI(endpoint string) {
|
func (x *PrmDial) SetServerURI(endpoint string) {
|
||||||
x.Endpoint = endpoint
|
x.endpoint = endpoint
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetTLSConfig sets tls.Config to open TLS client connection
|
// SetTLSConfig sets tls.Config to open TLS client connection
|
||||||
// to the FrostFS server. Nil (default) means insecure connection.
|
// to the NeoFS server. Nil (default) means insecure connection.
|
||||||
//
|
//
|
||||||
// See also SetServerURI.
|
// See also SetServerURI.
|
||||||
//
|
|
||||||
// Depreacted: Use PrmDial.TLSConfig instead.
|
|
||||||
func (x *PrmDial) SetTLSConfig(tlsConfig *tls.Config) {
|
func (x *PrmDial) SetTLSConfig(tlsConfig *tls.Config) {
|
||||||
x.TLSConfig = tlsConfig
|
x.tlsConfig = tlsConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetTimeout sets the timeout for connection to be established.
|
// SetTimeout sets the timeout for connection to be established.
|
||||||
// MUST BE positive. If not called, 5s timeout will be used by default.
|
// MUST BE positive. If not called, 5s timeout will be used by default.
|
||||||
//
|
|
||||||
// Deprecated: Use PrmDial.DialTimeout instead.
|
|
||||||
func (x *PrmDial) SetTimeout(timeout time.Duration) {
|
func (x *PrmDial) SetTimeout(timeout time.Duration) {
|
||||||
x.DialTimeout = timeout
|
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.
|
|
||||||
//
|
|
||||||
// Deprecated: Use PrmDial.StreamTimeout instead.
|
|
||||||
func (x *PrmDial) SetStreamTimeout(timeout time.Duration) {
|
|
||||||
x.StreamTimeout = timeout
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetGRPCDialOptions sets the gRPC dial options for new gRPC client connection.
|
|
||||||
//
|
|
||||||
// Deprecated: Use PrmDial.GRPCDialOptions instead.
|
|
||||||
func (x *PrmDial) SetGRPCDialOptions(opts ...grpc.DialOption) {
|
|
||||||
x.GRPCDialOptions = opts
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,67 +0,0 @@
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/elliptic"
|
|
||||||
"crypto/rand"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
|
||||||
File contains common functionality used for client package testing.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var key, _ = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
||||||
|
|
||||||
var statusErr apistatus.ServerInternal
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
statusErr.SetMessage("test status error")
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertStatusErr(tb testing.TB, res interface{ Status() apistatus.Status }) {
|
|
||||||
require.IsType(tb, &statusErr, res.Status())
|
|
||||||
require.Equal(tb, statusErr.Message(), res.Status().(*apistatus.ServerInternal).Message())
|
|
||||||
}
|
|
||||||
|
|
||||||
func newClient(server frostFSAPIServer) *Client {
|
|
||||||
prm := PrmInit{
|
|
||||||
Key: *key,
|
|
||||||
}
|
|
||||||
|
|
||||||
var c Client
|
|
||||||
c.Init(prm)
|
|
||||||
c.setFrostFSAPIServer(server)
|
|
||||||
|
|
||||||
return &c
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestClient_DialContext(t *testing.T) {
|
|
||||||
var c Client
|
|
||||||
|
|
||||||
// try to connect to any host
|
|
||||||
prm := PrmDial{
|
|
||||||
Endpoint: "localhost:8080",
|
|
||||||
}
|
|
||||||
|
|
||||||
assert := func(ctx context.Context, errExpected error) {
|
|
||||||
// expect particular context error according to Dial docs
|
|
||||||
require.ErrorIs(t, c.Dial(ctx, prm), errExpected)
|
|
||||||
}
|
|
||||||
|
|
||||||
// create pre-abandoned context
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
cancel()
|
|
||||||
|
|
||||||
assert(ctx, context.Canceled)
|
|
||||||
|
|
||||||
// create "pre-deadlined" context
|
|
||||||
ctx, cancel = context.WithTimeout(context.Background(), 0)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
assert(ctx, context.DeadlineExceeded)
|
|
||||||
}
|
|
342
client/common.go
342
client/common.go
|
@ -1,22 +1,32 @@
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"crypto/ecdsa"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
|
||||||
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
"github.com/nspcc-dev/neofs-api-go/v2/signature"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
|
apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
|
||||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
"github.com/nspcc-dev/neofs-sdk-go/session"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/version"
|
"github.com/nspcc-dev/neofs-sdk-go/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// common interface of resulting structures with API status.
|
||||||
|
type resCommon interface {
|
||||||
|
setStatus(apistatus.Status)
|
||||||
|
}
|
||||||
|
|
||||||
// structure is embedded to all resulting types in order to inherit status-related methods.
|
// structure is embedded to all resulting types in order to inherit status-related methods.
|
||||||
type statusRes struct {
|
type statusRes struct {
|
||||||
st apistatus.Status
|
st apistatus.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setStatus implements resCommon interface method.
|
||||||
|
func (x *statusRes) setStatus(st apistatus.Status) {
|
||||||
|
x.st = st
|
||||||
|
}
|
||||||
|
|
||||||
// Status returns server's status return.
|
// Status returns server's status return.
|
||||||
//
|
//
|
||||||
// Use apistatus package functionality to handle the status.
|
// Use apistatus package functionality to handle the status.
|
||||||
|
@ -24,89 +34,287 @@ func (x statusRes) Status() apistatus.Status {
|
||||||
return x.st
|
return x.st
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeXHeadersToMeta(xHeaders []string, h *v2session.RequestMetaHeader) {
|
type prmSession struct {
|
||||||
if len(xHeaders) == 0 {
|
tokenSessionSet bool
|
||||||
return
|
tokenSession session.Token
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO (aarifullin): remove the panic when all client parameters will check XHeaders
|
// SetSessionToken sets token of the session within which request should be sent.
|
||||||
// within buildRequest invocation.
|
func (x *prmSession) SetSessionToken(tok session.Token) {
|
||||||
if len(xHeaders)%2 != 0 {
|
x.tokenSession = tok
|
||||||
|
x.tokenSessionSet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x prmSession) writeToMetaHeader(meta *v2session.RequestMetaHeader) {
|
||||||
|
if x.tokenSessionSet {
|
||||||
|
meta.SetSessionToken(x.tokenSession.ToV2())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// groups meta parameters shared between all Client operations.
|
||||||
|
type prmCommonMeta struct {
|
||||||
|
// NeoFS request X-Headers
|
||||||
|
xHeaders []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithXHeaders specifies list of extended headers (string key-value pairs)
|
||||||
|
// to be attached to the request. Must have an even length.
|
||||||
|
//
|
||||||
|
// Slice must not be mutated until the operation completes.
|
||||||
|
func (x *prmCommonMeta) WithXHeaders(hs ...string) {
|
||||||
|
if len(hs)%2 != 0 {
|
||||||
panic("slice of X-Headers with odd length")
|
panic("slice of X-Headers with odd length")
|
||||||
}
|
}
|
||||||
|
|
||||||
hs := make([]v2session.XHeader, len(xHeaders)/2)
|
x.xHeaders = hs
|
||||||
for i := 0; i < len(xHeaders); i += 2 {
|
|
||||||
hs[i].SetKey(xHeaders[i])
|
|
||||||
hs[i].SetValue(xHeaders[i+1])
|
|
||||||
}
|
|
||||||
|
|
||||||
h.SetXHeaders(hs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// error messages.
|
func (x prmCommonMeta) writeToMetaHeader(h *v2session.RequestMetaHeader) {
|
||||||
var (
|
if len(x.xHeaders) > 0 {
|
||||||
errorMissingContainer = errors.New("missing container")
|
hs := make([]v2session.XHeader, len(x.xHeaders)/2)
|
||||||
errorMissingObject = errors.New("missing object")
|
|
||||||
errorAccountNotSet = errors.New("account not set")
|
for i := 0; i < len(x.xHeaders); i += 2 {
|
||||||
errorServerAddrUnset = errors.New("server address is unset or empty")
|
hs[i].SetKey(x.xHeaders[i])
|
||||||
errorZeroRangeLength = errors.New("zero range length")
|
hs[i].SetValue(x.xHeaders[i+1])
|
||||||
errorMissingRanges = errors.New("missing ranges")
|
}
|
||||||
errorInvalidXHeaders = errors.New("xheaders must be presented only as key-value pairs")
|
|
||||||
|
h.SetXHeaders(hs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// panic messages.
|
||||||
|
const (
|
||||||
|
panicMsgMissingContext = "missing context"
|
||||||
|
panicMsgMissingContainer = "missing container"
|
||||||
)
|
)
|
||||||
|
|
||||||
type request interface {
|
// groups all the details required to send a single request and process a response to it.
|
||||||
GetMetaHeader() *v2session.RequestMetaHeader
|
type contextCall struct {
|
||||||
SetMetaHeader(*v2session.RequestMetaHeader)
|
// ==================================================
|
||||||
SetVerificationHeader(*v2session.RequestVerificationHeader)
|
// state vars that do not require explicit initialization
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) prepareRequest(req request, meta *v2session.RequestMetaHeader) {
|
// final error to be returned from client method
|
||||||
ttl := meta.GetTTL()
|
err error
|
||||||
if ttl == 0 {
|
|
||||||
ttl = 2
|
// received response
|
||||||
|
resp responseV2
|
||||||
|
|
||||||
|
// ==================================================
|
||||||
|
// shared parameters which are set uniformly on all calls
|
||||||
|
|
||||||
|
// request signing key
|
||||||
|
key ecdsa.PrivateKey
|
||||||
|
|
||||||
|
// callback prior to processing the response by the client
|
||||||
|
callbackResp func(ResponseMetaInfo) error
|
||||||
|
|
||||||
|
// if set, protocol errors will be expanded into a final error
|
||||||
|
resolveAPIFailures bool
|
||||||
|
|
||||||
|
// NeoFS network magic
|
||||||
|
netMagic uint64
|
||||||
|
|
||||||
|
// Meta parameters
|
||||||
|
meta prmCommonMeta
|
||||||
|
|
||||||
|
// ==================================================
|
||||||
|
// custom call parameters
|
||||||
|
|
||||||
|
// structure of the call result
|
||||||
|
statusRes resCommon
|
||||||
|
|
||||||
|
// request to be signed with a key and sent
|
||||||
|
req interface {
|
||||||
|
GetMetaHeader() *v2session.RequestMetaHeader
|
||||||
|
SetMetaHeader(*v2session.RequestMetaHeader)
|
||||||
|
SetVerificationHeader(*v2session.RequestVerificationHeader)
|
||||||
}
|
}
|
||||||
|
|
||||||
verV2 := meta.GetVersion()
|
// function to send a request (unary) and receive a response
|
||||||
if verV2 == nil {
|
call func() (responseV2, error)
|
||||||
verV2 = new(refs.Version)
|
|
||||||
version.Current().WriteToV2(verV2)
|
|
||||||
}
|
|
||||||
|
|
||||||
meta.SetTTL(ttl)
|
// function to send the request (req field)
|
||||||
meta.SetVersion(verV2)
|
wReq func() error
|
||||||
meta.SetNetworkMagic(c.prm.NetMagic)
|
|
||||||
|
|
||||||
req.SetMetaHeader(meta)
|
// function to recv the response (resp field)
|
||||||
|
rResp func() error
|
||||||
|
|
||||||
|
// function to close the message stream
|
||||||
|
closer func() error
|
||||||
|
|
||||||
|
// function of writing response fields to the resulting structure (optional)
|
||||||
|
result func(v2 responseV2)
|
||||||
}
|
}
|
||||||
|
|
||||||
// processResponse verifies response signature and converts status to an error if needed.
|
// sets needed fields of the request meta header.
|
||||||
func (c *Client) processResponse(resp responseV2) (apistatus.Status, error) {
|
func (x contextCall) prepareRequest() {
|
||||||
if c.prm.ResponseInfoCallback != nil {
|
meta := x.req.GetMetaHeader()
|
||||||
rmi := ResponseMetaInfo{
|
if meta == nil {
|
||||||
key: resp.GetVerificationHeader().GetBodySignature().GetKey(),
|
meta = new(v2session.RequestMetaHeader)
|
||||||
epoch: resp.GetMetaHeader().GetEpoch(),
|
x.req.SetMetaHeader(meta)
|
||||||
}
|
}
|
||||||
if err := c.prm.ResponseInfoCallback(rmi); err != nil {
|
|
||||||
return nil, fmt.Errorf("response callback error: %w", err)
|
if meta.GetTTL() == 0 {
|
||||||
|
meta.SetTTL(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
if meta.GetVersion() == nil {
|
||||||
|
meta.SetVersion(version.Current().ToV2())
|
||||||
|
}
|
||||||
|
|
||||||
|
meta.SetNetworkMagic(x.netMagic)
|
||||||
|
|
||||||
|
x.meta.writeToMetaHeader(meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepares, signs and writes the request. Result means success.
|
||||||
|
// If failed, contextCall.err contains the reason.
|
||||||
|
func (x *contextCall) writeRequest() bool {
|
||||||
|
x.prepareRequest()
|
||||||
|
|
||||||
|
x.req.SetVerificationHeader(nil)
|
||||||
|
|
||||||
|
// sign the request
|
||||||
|
x.err = signature.SignServiceMessage(&x.key, x.req)
|
||||||
|
if x.err != nil {
|
||||||
|
x.err = fmt.Errorf("sign request: %w", x.err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
x.err = x.wReq()
|
||||||
|
if x.err != nil {
|
||||||
|
x.err = fmt.Errorf("write request: %w", x.err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// performs common actions of response processing and writes any problem as a result status or client error
|
||||||
|
// (in both cases returns false).
|
||||||
|
//
|
||||||
|
// Actions:
|
||||||
|
// * verify signature (internal);
|
||||||
|
// * call response callback (internal);
|
||||||
|
// * unwrap status error (optional).
|
||||||
|
func (x *contextCall) processResponse() bool {
|
||||||
|
// call response callback if set
|
||||||
|
if x.callbackResp != nil {
|
||||||
|
x.err = x.callbackResp(ResponseMetaInfo{
|
||||||
|
key: x.resp.GetVerificationHeader().GetBodySignature().GetKey(),
|
||||||
|
})
|
||||||
|
if x.err != nil {
|
||||||
|
x.err = fmt.Errorf("response callback error: %w", x.err)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err := signature.VerifyServiceMessage(resp)
|
// note that we call response callback before signature check since it is expected more lightweight
|
||||||
if err != nil {
|
// while verification needs marshaling
|
||||||
return nil, fmt.Errorf("invalid response signature: %w", err)
|
|
||||||
|
// verify response signature
|
||||||
|
x.err = signature.VerifyServiceMessage(x.resp)
|
||||||
|
if x.err != nil {
|
||||||
|
x.err = fmt.Errorf("invalid response signature: %w", x.err)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
st := apistatus.FromStatusV2(resp.GetMetaHeader().GetStatus())
|
// get result status
|
||||||
if !c.prm.DisableFrostFSErrorResolution {
|
st := apistatus.FromStatusV2(x.resp.GetMetaHeader().GetStatus())
|
||||||
return st, apistatus.ErrFromStatus(st)
|
|
||||||
|
// unwrap unsuccessful status and return it
|
||||||
|
// as error if client has been configured so
|
||||||
|
successfulStatus := apistatus.IsSuccessful(st)
|
||||||
|
if !successfulStatus && x.resolveAPIFailures {
|
||||||
|
x.err = apistatus.ErrFromStatus(st)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
return st, nil
|
|
||||||
|
x.statusRes.setStatus(st)
|
||||||
|
|
||||||
|
return successfulStatus || !x.resolveAPIFailures
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecRaw executes f with underlying git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client.Client
|
// reads response (if rResp is set) and processes it. Result means success.
|
||||||
|
// If failed, contextCall.err contains the reason.
|
||||||
|
func (x *contextCall) readResponse() bool {
|
||||||
|
if x.rResp != nil {
|
||||||
|
x.err = x.rResp()
|
||||||
|
if x.err != nil {
|
||||||
|
x.err = fmt.Errorf("read response: %w", x.err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return x.processResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
// closes the message stream (if closer is set) and writes the results (if result is set).
|
||||||
|
// Return means success. If failed, contextCall.err contains the reason.
|
||||||
|
func (x *contextCall) close() bool {
|
||||||
|
if x.closer != nil {
|
||||||
|
x.err = x.closer()
|
||||||
|
if x.err != nil {
|
||||||
|
x.err = fmt.Errorf("close RPC: %w", x.err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// write response to resulting structure
|
||||||
|
if x.result != nil {
|
||||||
|
x.result(x.resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// goes through all stages of sending a request and processing a response. Returns true if successful.
|
||||||
|
// If failed, contextCall.err contains the reason.
|
||||||
|
func (x *contextCall) processCall() bool {
|
||||||
|
// set request writer
|
||||||
|
x.wReq = func() error {
|
||||||
|
var err error
|
||||||
|
x.resp, err = x.call()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// write request
|
||||||
|
ok := x.writeRequest()
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// read response
|
||||||
|
ok = x.readResponse()
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// close and write response to resulting structure
|
||||||
|
ok = x.close()
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return x.err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// initializes static cross-call parameters inherited from client.
|
||||||
|
func (c *Client) initCallContext(ctx *contextCall) {
|
||||||
|
ctx.key = c.prm.key
|
||||||
|
c.initCallContextWithoutKey(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// initializes static cross-call parameters inherited from client except private key.
|
||||||
|
func (c *Client) initCallContextWithoutKey(ctx *contextCall) {
|
||||||
|
ctx.resolveAPIFailures = c.prm.resolveNeoFSErrors
|
||||||
|
ctx.callbackResp = c.prm.cbRespInfo
|
||||||
|
ctx.netMagic = c.prm.netMagic
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecRaw executes f with underlying github.com/nspcc-dev/neofs-api-go/v2/rpc/client.Client
|
||||||
// instance. Communicate over the Protocol Buffers protocol in a more flexible way:
|
// instance. Communicate over the Protocol Buffers protocol in a more flexible way:
|
||||||
// most often used to transmit data over a fixed version of the FrostFS protocol, as well
|
// most often used to transmit data over a fixed version of the NeoFS protocol, as well
|
||||||
// as to support custom services.
|
// as to support custom services.
|
||||||
//
|
//
|
||||||
// The f must not manipulate the client connection passed into it.
|
// The f must not manipulate the client connection passed into it.
|
||||||
|
@ -115,7 +323,7 @@ func (c *Client) processResponse(resp responseV2) (apistatus.Status, error) {
|
||||||
// before closing the connection.
|
// before closing the connection.
|
||||||
//
|
//
|
||||||
// See also Dial and Close.
|
// See also Dial and Close.
|
||||||
// See also git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client package docs.
|
// See also github.com/nspcc-dev/neofs-api-go/v2/rpc/client package docs.
|
||||||
func (c *Client) ExecRaw(f func(client *client.Client) error) error {
|
func (c *Client) ExecRaw(f func(client *client.Client) error) error {
|
||||||
return f(&c.c)
|
return f(&c.c)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,27 +2,732 @@ package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
|
v2container "github.com/nspcc-dev/neofs-api-go/v2/container"
|
||||||
|
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
|
||||||
|
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
|
||||||
|
v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
|
||||||
|
v2signature "github.com/nspcc-dev/neofs-api-go/v2/signature"
|
||||||
|
"github.com/nspcc-dev/neofs-sdk-go/container"
|
||||||
|
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
|
||||||
|
"github.com/nspcc-dev/neofs-sdk-go/eacl"
|
||||||
|
"github.com/nspcc-dev/neofs-sdk-go/owner"
|
||||||
|
"github.com/nspcc-dev/neofs-sdk-go/session"
|
||||||
|
"github.com/nspcc-dev/neofs-sdk-go/signature"
|
||||||
|
sigutil "github.com/nspcc-dev/neofs-sdk-go/util/signature"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SyncContainerWithNetwork requests network configuration using passed client
|
// PrmContainerPut groups parameters of ContainerPut operation.
|
||||||
// and applies it to the container. Container MUST not be nil.
|
type PrmContainerPut struct {
|
||||||
|
prmCommonMeta
|
||||||
|
|
||||||
|
cnrSet bool
|
||||||
|
cnr container.Container
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetContainer sets structured information about new NeoFS container.
|
||||||
|
// Required parameter.
|
||||||
|
func (x *PrmContainerPut) SetContainer(cnr container.Container) {
|
||||||
|
x.cnr = cnr
|
||||||
|
x.cnrSet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResContainerPut groups resulting values of ContainerPut operation.
|
||||||
|
type ResContainerPut struct {
|
||||||
|
statusRes
|
||||||
|
|
||||||
|
id *cid.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID returns identifier of the container declared to be stored in the system.
|
||||||
|
// Used as a link to information about the container (in particular, you can
|
||||||
|
// asynchronously check if the save was successful).
|
||||||
//
|
//
|
||||||
// Note: if container does not match network configuration, SyncContainerWithNetwork
|
// Client doesn't retain value so modification is safe.
|
||||||
// changes it.
|
func (x ResContainerPut) ID() *cid.ID {
|
||||||
|
return x.id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ResContainerPut) setID(id *cid.ID) {
|
||||||
|
x.id = id
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainerPut sends request to save container in NeoFS.
|
||||||
//
|
//
|
||||||
// Returns any network/parsing config errors.
|
// Exactly one return value is non-nil. By default, server status is returned in res structure.
|
||||||
|
// Any client's internal or transport errors are returned as `error`.
|
||||||
|
// If WithNeoFSErrorParsing option has been provided, unsuccessful
|
||||||
|
// NeoFS status codes are returned as `error`, otherwise, are included
|
||||||
|
// in the returned result structure.
|
||||||
//
|
//
|
||||||
// See also NetworkInfo, container.ApplyNetworkConfig.
|
// Operation is asynchronous and no guaranteed even in the absence of errors.
|
||||||
func SyncContainerWithNetwork(ctx context.Context, cnr *container.Container, c *Client) error {
|
// The required time is also not predictable.
|
||||||
res, err := c.NetworkInfo(ctx, PrmNetworkInfo{})
|
//
|
||||||
if err != nil {
|
// Success can be verified by reading by identifier (see ResContainerPut.ID).
|
||||||
return fmt.Errorf("network info call: %w", err)
|
//
|
||||||
|
// Immediately panics if parameters are set incorrectly (see PrmContainerPut docs).
|
||||||
|
// Context is required and must not be nil. It is used for network communication.
|
||||||
|
//
|
||||||
|
// Return statuses:
|
||||||
|
// - global (see Client docs).
|
||||||
|
func (c *Client) ContainerPut(ctx context.Context, prm PrmContainerPut) (*ResContainerPut, error) {
|
||||||
|
// check parameters
|
||||||
|
switch {
|
||||||
|
case ctx == nil:
|
||||||
|
panic(panicMsgMissingContext)
|
||||||
|
case !prm.cnrSet:
|
||||||
|
panic(panicMsgMissingContainer)
|
||||||
}
|
}
|
||||||
|
|
||||||
container.ApplyNetworkConfig(cnr, res.Info())
|
// TODO: check private key is set before forming the request
|
||||||
|
|
||||||
return nil
|
// form request body
|
||||||
|
reqBody := new(v2container.PutRequestBody)
|
||||||
|
reqBody.SetContainer(prm.cnr.ToV2())
|
||||||
|
|
||||||
|
// sign container
|
||||||
|
signWrapper := v2signature.StableMarshalerWrapper{SM: reqBody.GetContainer()}
|
||||||
|
|
||||||
|
sig, err := sigutil.SignData(&c.prm.key, signWrapper, sigutil.SignWithRFC6979())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
reqBody.SetSignature(sig.ToV2())
|
||||||
|
|
||||||
|
// form meta header
|
||||||
|
var meta v2session.RequestMetaHeader
|
||||||
|
meta.SetSessionToken(prm.cnr.SessionToken().ToV2())
|
||||||
|
prm.prmCommonMeta.writeToMetaHeader(&meta)
|
||||||
|
|
||||||
|
// form request
|
||||||
|
var req v2container.PutRequest
|
||||||
|
|
||||||
|
req.SetBody(reqBody)
|
||||||
|
req.SetMetaHeader(&meta)
|
||||||
|
|
||||||
|
// init call context
|
||||||
|
|
||||||
|
var (
|
||||||
|
cc contextCall
|
||||||
|
res ResContainerPut
|
||||||
|
)
|
||||||
|
|
||||||
|
c.initCallContext(&cc)
|
||||||
|
cc.req = &req
|
||||||
|
cc.statusRes = &res
|
||||||
|
cc.call = func() (responseV2, error) {
|
||||||
|
return rpcapi.PutContainer(&c.c, &req, client.WithContext(ctx))
|
||||||
|
}
|
||||||
|
cc.result = func(r responseV2) {
|
||||||
|
resp := r.(*v2container.PutResponse)
|
||||||
|
res.setID(cid.NewFromV2(resp.GetBody().GetContainerID()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// process call
|
||||||
|
if !cc.processCall() {
|
||||||
|
return nil, cc.err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrmContainerGet groups parameters of ContainerGet operation.
|
||||||
|
type PrmContainerGet struct {
|
||||||
|
prmCommonMeta
|
||||||
|
|
||||||
|
idSet bool
|
||||||
|
id cid.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetContainer sets identifier of the container to be read.
|
||||||
|
// Required parameter.
|
||||||
|
func (x *PrmContainerGet) SetContainer(id cid.ID) {
|
||||||
|
x.id = id
|
||||||
|
x.idSet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResContainerGet groups resulting values of ContainerGet operation.
|
||||||
|
type ResContainerGet struct {
|
||||||
|
statusRes
|
||||||
|
|
||||||
|
cnr *container.Container
|
||||||
|
}
|
||||||
|
|
||||||
|
// Container returns structured information about the requested container.
|
||||||
|
//
|
||||||
|
// Client doesn't retain value so modification is safe.
|
||||||
|
func (x ResContainerGet) Container() *container.Container {
|
||||||
|
return x.cnr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ResContainerGet) setContainer(cnr *container.Container) {
|
||||||
|
x.cnr = cnr
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainerGet reads NeoFS container by ID.
|
||||||
|
//
|
||||||
|
// Exactly one return value is non-nil. By default, server status is returned in res structure.
|
||||||
|
// Any client's internal or transport errors are returned as `error`.
|
||||||
|
// If WithNeoFSErrorParsing option has been provided, unsuccessful
|
||||||
|
// NeoFS status codes are returned as `error`, otherwise, are included
|
||||||
|
// in the returned result structure.
|
||||||
|
//
|
||||||
|
// Immediately panics if parameters are set incorrectly (see PrmContainerGet docs).
|
||||||
|
// Context is required and must not be nil. It is used for network communication.
|
||||||
|
//
|
||||||
|
// Return statuses:
|
||||||
|
// - global (see Client docs);
|
||||||
|
// - *apistatus.ContainerNotFound.
|
||||||
|
func (c *Client) ContainerGet(ctx context.Context, prm PrmContainerGet) (*ResContainerGet, error) {
|
||||||
|
switch {
|
||||||
|
case ctx == nil:
|
||||||
|
panic(panicMsgMissingContext)
|
||||||
|
case !prm.idSet:
|
||||||
|
panic(panicMsgMissingContainer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// form request body
|
||||||
|
reqBody := new(v2container.GetRequestBody)
|
||||||
|
reqBody.SetContainerID(prm.id.ToV2())
|
||||||
|
|
||||||
|
// form request
|
||||||
|
var req v2container.GetRequest
|
||||||
|
|
||||||
|
req.SetBody(reqBody)
|
||||||
|
|
||||||
|
// init call context
|
||||||
|
|
||||||
|
var (
|
||||||
|
cc contextCall
|
||||||
|
res ResContainerGet
|
||||||
|
)
|
||||||
|
|
||||||
|
c.initCallContext(&cc)
|
||||||
|
cc.meta = prm.prmCommonMeta
|
||||||
|
cc.req = &req
|
||||||
|
cc.statusRes = &res
|
||||||
|
cc.call = func() (responseV2, error) {
|
||||||
|
return rpcapi.GetContainer(&c.c, &req, client.WithContext(ctx))
|
||||||
|
}
|
||||||
|
cc.result = func(r responseV2) {
|
||||||
|
resp := r.(*v2container.GetResponse)
|
||||||
|
|
||||||
|
body := resp.GetBody()
|
||||||
|
|
||||||
|
cnr := container.NewContainerFromV2(body.GetContainer())
|
||||||
|
|
||||||
|
cnr.SetSessionToken(
|
||||||
|
session.NewTokenFromV2(body.GetSessionToken()),
|
||||||
|
)
|
||||||
|
|
||||||
|
cnr.SetSignature(
|
||||||
|
signature.NewFromV2(body.GetSignature()),
|
||||||
|
)
|
||||||
|
|
||||||
|
res.setContainer(cnr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// process call
|
||||||
|
if !cc.processCall() {
|
||||||
|
return nil, cc.err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrmContainerList groups parameters of ContainerList operation.
|
||||||
|
type PrmContainerList struct {
|
||||||
|
prmCommonMeta
|
||||||
|
|
||||||
|
ownerSet bool
|
||||||
|
ownerID owner.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAccount sets identifier of the NeoFS account to list the containers.
|
||||||
|
// Required parameter. Must be a valid ID according to NeoFS API protocol.
|
||||||
|
func (x *PrmContainerList) SetAccount(id owner.ID) {
|
||||||
|
x.ownerID = id
|
||||||
|
x.ownerSet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResContainerList groups resulting values of ContainerList operation.
|
||||||
|
type ResContainerList struct {
|
||||||
|
statusRes
|
||||||
|
|
||||||
|
ids []cid.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Containers returns list of identifiers of the account-owned containers.
|
||||||
|
//
|
||||||
|
// Client doesn't retain value so modification is safe.
|
||||||
|
func (x ResContainerList) Containers() []cid.ID {
|
||||||
|
return x.ids
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ResContainerList) setContainers(ids []cid.ID) {
|
||||||
|
x.ids = ids
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainerList requests identifiers of the account-owned containers.
|
||||||
|
//
|
||||||
|
// Exactly one return value is non-nil. By default, server status is returned in res structure.
|
||||||
|
// Any client's internal or transport errors are returned as `error`.
|
||||||
|
// If WithNeoFSErrorParsing option has been provided, unsuccessful
|
||||||
|
// NeoFS status codes are returned as `error`, otherwise, are included
|
||||||
|
// in the returned result structure.
|
||||||
|
//
|
||||||
|
// Immediately panics if parameters are set incorrectly (see PrmContainerList docs).
|
||||||
|
// Context is required and must not be nil. It is used for network communication.
|
||||||
|
//
|
||||||
|
// Return statuses:
|
||||||
|
// - global (see Client docs).
|
||||||
|
func (c *Client) ContainerList(ctx context.Context, prm PrmContainerList) (*ResContainerList, error) {
|
||||||
|
// check parameters
|
||||||
|
switch {
|
||||||
|
case ctx == nil:
|
||||||
|
panic(panicMsgMissingContext)
|
||||||
|
case !prm.ownerSet:
|
||||||
|
panic("account not set")
|
||||||
|
case !prm.ownerID.Valid():
|
||||||
|
panic("invalid account")
|
||||||
|
}
|
||||||
|
|
||||||
|
// form request body
|
||||||
|
reqBody := new(v2container.ListRequestBody)
|
||||||
|
reqBody.SetOwnerID(prm.ownerID.ToV2())
|
||||||
|
|
||||||
|
// form request
|
||||||
|
var req v2container.ListRequest
|
||||||
|
|
||||||
|
req.SetBody(reqBody)
|
||||||
|
|
||||||
|
// init call context
|
||||||
|
|
||||||
|
var (
|
||||||
|
cc contextCall
|
||||||
|
res ResContainerList
|
||||||
|
)
|
||||||
|
|
||||||
|
c.initCallContext(&cc)
|
||||||
|
cc.meta = prm.prmCommonMeta
|
||||||
|
cc.req = &req
|
||||||
|
cc.statusRes = &res
|
||||||
|
cc.call = func() (responseV2, error) {
|
||||||
|
return rpcapi.ListContainers(&c.c, &req, client.WithContext(ctx))
|
||||||
|
}
|
||||||
|
cc.result = func(r responseV2) {
|
||||||
|
resp := r.(*v2container.ListResponse)
|
||||||
|
|
||||||
|
ids := make([]cid.ID, len(resp.GetBody().GetContainerIDs()))
|
||||||
|
|
||||||
|
for i, cidV2 := range resp.GetBody().GetContainerIDs() {
|
||||||
|
ids[i] = *cid.NewFromV2(&cidV2)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.setContainers(ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
// process call
|
||||||
|
if !cc.processCall() {
|
||||||
|
return nil, cc.err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrmContainerDelete groups parameters of ContainerDelete operation.
|
||||||
|
type PrmContainerDelete struct {
|
||||||
|
prmCommonMeta
|
||||||
|
prmSession
|
||||||
|
|
||||||
|
idSet bool
|
||||||
|
id cid.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetContainer sets identifier of the NeoFS container to be removed.
|
||||||
|
// Required parameter.
|
||||||
|
func (x *PrmContainerDelete) SetContainer(id cid.ID) {
|
||||||
|
x.id = id
|
||||||
|
x.idSet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResContainerDelete groups resulting values of ContainerDelete operation.
|
||||||
|
type ResContainerDelete struct {
|
||||||
|
statusRes
|
||||||
|
}
|
||||||
|
|
||||||
|
// implements github.com/nspcc-dev/neofs-sdk-go/util/signature.DataSource.
|
||||||
|
type delContainerSignWrapper struct {
|
||||||
|
body *v2container.DeleteRequestBody
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c delContainerSignWrapper) ReadSignedData([]byte) ([]byte, error) {
|
||||||
|
return c.body.GetContainerID().GetValue(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c delContainerSignWrapper) SignedDataSize() int {
|
||||||
|
return len(c.body.GetContainerID().GetValue())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainerDelete sends request to remove the NeoFS container.
|
||||||
|
//
|
||||||
|
// Exactly one return value is non-nil. By default, server status is returned in res structure.
|
||||||
|
// Any client's internal or transport errors are returned as `error`.
|
||||||
|
// If WithNeoFSErrorParsing option has been provided, unsuccessful
|
||||||
|
// NeoFS status codes are returned as `error`, otherwise, are included
|
||||||
|
// in the returned result structure.
|
||||||
|
//
|
||||||
|
// Operation is asynchronous and no guaranteed even in the absence of errors.
|
||||||
|
// The required time is also not predictable.
|
||||||
|
//
|
||||||
|
// Success can be verified by reading by identifier (see GetContainer).
|
||||||
|
//
|
||||||
|
// Immediately panics if parameters are set incorrectly (see PrmContainerDelete docs).
|
||||||
|
// Context is required and must not be nil. It is used for network communication.
|
||||||
|
//
|
||||||
|
// Exactly one return value is non-nil. Server status return is returned in ResContainerDelete.
|
||||||
|
// Reflects all internal errors in second return value (transport problems, response processing, etc.).
|
||||||
|
//
|
||||||
|
// Return statuses:
|
||||||
|
// - global (see Client docs).
|
||||||
|
func (c *Client) ContainerDelete(ctx context.Context, prm PrmContainerDelete) (*ResContainerDelete, error) {
|
||||||
|
// check parameters
|
||||||
|
switch {
|
||||||
|
case ctx == nil:
|
||||||
|
panic(panicMsgMissingContext)
|
||||||
|
case !prm.idSet:
|
||||||
|
panic(panicMsgMissingContainer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// form request body
|
||||||
|
reqBody := new(v2container.DeleteRequestBody)
|
||||||
|
reqBody.SetContainerID(prm.id.ToV2())
|
||||||
|
|
||||||
|
signWrapper := delContainerSignWrapper{body: reqBody}
|
||||||
|
|
||||||
|
// sign container
|
||||||
|
sig, err := sigutil.SignData(&c.prm.key, signWrapper, sigutil.SignWithRFC6979())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
reqBody.SetSignature(sig.ToV2())
|
||||||
|
|
||||||
|
// form meta header
|
||||||
|
var meta v2session.RequestMetaHeader
|
||||||
|
|
||||||
|
prm.prmSession.writeToMetaHeader(&meta)
|
||||||
|
prm.prmCommonMeta.writeToMetaHeader(&meta)
|
||||||
|
|
||||||
|
// form request
|
||||||
|
var req v2container.DeleteRequest
|
||||||
|
|
||||||
|
req.SetBody(reqBody)
|
||||||
|
req.SetMetaHeader(&meta)
|
||||||
|
|
||||||
|
// init call context
|
||||||
|
|
||||||
|
var (
|
||||||
|
cc contextCall
|
||||||
|
res ResContainerDelete
|
||||||
|
)
|
||||||
|
|
||||||
|
c.initCallContext(&cc)
|
||||||
|
cc.req = &req
|
||||||
|
cc.statusRes = &res
|
||||||
|
cc.call = func() (responseV2, error) {
|
||||||
|
return rpcapi.DeleteContainer(&c.c, &req, client.WithContext(ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
// process call
|
||||||
|
if !cc.processCall() {
|
||||||
|
return nil, cc.err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrmContainerEACL groups parameters of ContainerEACL operation.
|
||||||
|
type PrmContainerEACL struct {
|
||||||
|
prmCommonMeta
|
||||||
|
|
||||||
|
idSet bool
|
||||||
|
id cid.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetContainer sets identifier of the NeoFS container to read the eACL table.
|
||||||
|
// Required parameter.
|
||||||
|
func (x *PrmContainerEACL) SetContainer(id cid.ID) {
|
||||||
|
x.id = id
|
||||||
|
x.idSet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResContainerEACL groups resulting values of ContainerEACL operation.
|
||||||
|
type ResContainerEACL struct {
|
||||||
|
statusRes
|
||||||
|
|
||||||
|
table *eacl.Table
|
||||||
|
}
|
||||||
|
|
||||||
|
// Table returns eACL table of the requested container.
|
||||||
|
//
|
||||||
|
// Client doesn't retain value so modification is safe.
|
||||||
|
func (x ResContainerEACL) Table() *eacl.Table {
|
||||||
|
return x.table
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ResContainerEACL) setTable(table *eacl.Table) {
|
||||||
|
x.table = table
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainerEACL reads eACL table of the NeoFS container.
|
||||||
|
//
|
||||||
|
// Exactly one return value is non-nil. By default, server status is returned in res structure.
|
||||||
|
// Any client's internal or transport errors are returned as `error`.
|
||||||
|
// If WithNeoFSErrorParsing option has been provided, unsuccessful
|
||||||
|
// NeoFS status codes are returned as `error`, otherwise, are included
|
||||||
|
// in the returned result structure.
|
||||||
|
//
|
||||||
|
// Immediately panics if parameters are set incorrectly (see PrmContainerEACL docs).
|
||||||
|
// Context is required and must not be nil. It is used for network communication.
|
||||||
|
//
|
||||||
|
// Return statuses:
|
||||||
|
// - global (see Client docs);
|
||||||
|
// - *apistatus.ContainerNotFound.
|
||||||
|
func (c *Client) ContainerEACL(ctx context.Context, prm PrmContainerEACL) (*ResContainerEACL, error) {
|
||||||
|
// check parameters
|
||||||
|
switch {
|
||||||
|
case ctx == nil:
|
||||||
|
panic(panicMsgMissingContext)
|
||||||
|
case !prm.idSet:
|
||||||
|
panic(panicMsgMissingContainer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// form request body
|
||||||
|
reqBody := new(v2container.GetExtendedACLRequestBody)
|
||||||
|
reqBody.SetContainerID(prm.id.ToV2())
|
||||||
|
|
||||||
|
// form request
|
||||||
|
var req v2container.GetExtendedACLRequest
|
||||||
|
|
||||||
|
req.SetBody(reqBody)
|
||||||
|
|
||||||
|
// init call context
|
||||||
|
|
||||||
|
var (
|
||||||
|
cc contextCall
|
||||||
|
res ResContainerEACL
|
||||||
|
)
|
||||||
|
|
||||||
|
c.initCallContext(&cc)
|
||||||
|
cc.meta = prm.prmCommonMeta
|
||||||
|
cc.req = &req
|
||||||
|
cc.statusRes = &res
|
||||||
|
cc.call = func() (responseV2, error) {
|
||||||
|
return rpcapi.GetEACL(&c.c, &req, client.WithContext(ctx))
|
||||||
|
}
|
||||||
|
cc.result = func(r responseV2) {
|
||||||
|
resp := r.(*v2container.GetExtendedACLResponse)
|
||||||
|
|
||||||
|
body := resp.GetBody()
|
||||||
|
|
||||||
|
table := eacl.NewTableFromV2(body.GetEACL())
|
||||||
|
|
||||||
|
table.SetSessionToken(
|
||||||
|
session.NewTokenFromV2(body.GetSessionToken()),
|
||||||
|
)
|
||||||
|
|
||||||
|
table.SetSignature(
|
||||||
|
signature.NewFromV2(body.GetSignature()),
|
||||||
|
)
|
||||||
|
|
||||||
|
res.setTable(table)
|
||||||
|
}
|
||||||
|
|
||||||
|
// process call
|
||||||
|
if !cc.processCall() {
|
||||||
|
return nil, cc.err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrmContainerSetEACL groups parameters of ContainerSetEACL operation.
|
||||||
|
type PrmContainerSetEACL struct {
|
||||||
|
prmCommonMeta
|
||||||
|
|
||||||
|
tableSet bool
|
||||||
|
table eacl.Table
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTable sets eACL table structure to be set for the container.
|
||||||
|
// Required parameter.
|
||||||
|
func (x *PrmContainerSetEACL) SetTable(table eacl.Table) {
|
||||||
|
x.table = table
|
||||||
|
x.tableSet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResContainerSetEACL groups resulting values of ContainerSetEACL operation.
|
||||||
|
type ResContainerSetEACL struct {
|
||||||
|
statusRes
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainerSetEACL sends request to update eACL table of the NeoFS container.
|
||||||
|
//
|
||||||
|
// Exactly one return value is non-nil. By default, server status is returned in res structure.
|
||||||
|
// Any client's internal or transport errors are returned as `error`.
|
||||||
|
// If WithNeoFSErrorParsing option has been provided, unsuccessful
|
||||||
|
// NeoFS status codes are returned as `error`, otherwise, are included
|
||||||
|
// in the returned result structure.
|
||||||
|
//
|
||||||
|
// Operation is asynchronous and no guaranteed even in the absence of errors.
|
||||||
|
// The required time is also not predictable.
|
||||||
|
//
|
||||||
|
// Success can be verified by reading by identifier (see EACL).
|
||||||
|
//
|
||||||
|
// Immediately panics if parameters are set incorrectly (see PrmContainerSetEACL docs).
|
||||||
|
// Context is required and must not be nil. It is used for network communication.
|
||||||
|
//
|
||||||
|
// Return statuses:
|
||||||
|
// - global (see Client docs).
|
||||||
|
func (c *Client) ContainerSetEACL(ctx context.Context, prm PrmContainerSetEACL) (*ResContainerSetEACL, error) {
|
||||||
|
// check parameters
|
||||||
|
switch {
|
||||||
|
case ctx == nil:
|
||||||
|
panic(panicMsgMissingContext)
|
||||||
|
case !prm.tableSet:
|
||||||
|
panic("eACL table not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
// form request body
|
||||||
|
reqBody := new(v2container.SetExtendedACLRequestBody)
|
||||||
|
reqBody.SetEACL(prm.table.ToV2())
|
||||||
|
|
||||||
|
// sign the eACL table
|
||||||
|
signWrapper := v2signature.StableMarshalerWrapper{SM: reqBody.GetEACL()}
|
||||||
|
|
||||||
|
sig, err := sigutil.SignData(&c.prm.key, signWrapper, sigutil.SignWithRFC6979())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
reqBody.SetSignature(sig.ToV2())
|
||||||
|
|
||||||
|
// form meta header
|
||||||
|
var meta v2session.RequestMetaHeader
|
||||||
|
meta.SetSessionToken(prm.table.SessionToken().ToV2())
|
||||||
|
prm.prmCommonMeta.writeToMetaHeader(&meta)
|
||||||
|
|
||||||
|
// form request
|
||||||
|
var req v2container.SetExtendedACLRequest
|
||||||
|
|
||||||
|
req.SetBody(reqBody)
|
||||||
|
req.SetMetaHeader(&meta)
|
||||||
|
|
||||||
|
// init call context
|
||||||
|
|
||||||
|
var (
|
||||||
|
cc contextCall
|
||||||
|
res ResContainerSetEACL
|
||||||
|
)
|
||||||
|
|
||||||
|
c.initCallContext(&cc)
|
||||||
|
cc.req = &req
|
||||||
|
cc.statusRes = &res
|
||||||
|
cc.call = func() (responseV2, error) {
|
||||||
|
return rpcapi.SetEACL(&c.c, &req, client.WithContext(ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
// process call
|
||||||
|
if !cc.processCall() {
|
||||||
|
return nil, cc.err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrmAnnounceSpace groups parameters of ContainerAnnounceUsedSpace operation.
|
||||||
|
type PrmAnnounceSpace struct {
|
||||||
|
prmCommonMeta
|
||||||
|
|
||||||
|
announcements []container.UsedSpaceAnnouncement
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetValues sets values describing volume of space that is used for the container objects.
|
||||||
|
// Required parameter. Must not be empty.
|
||||||
|
//
|
||||||
|
// Must not be mutated before the end of the operation.
|
||||||
|
func (x *PrmAnnounceSpace) SetValues(announcements []container.UsedSpaceAnnouncement) {
|
||||||
|
x.announcements = announcements
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResAnnounceSpace groups resulting values of ContainerAnnounceUsedSpace operation.
|
||||||
|
type ResAnnounceSpace struct {
|
||||||
|
statusRes
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainerAnnounceUsedSpace sends request to announce volume of the space used for the container objects.
|
||||||
|
//
|
||||||
|
// Exactly one return value is non-nil. By default, server status is returned in res structure.
|
||||||
|
// Any client's internal or transport errors are returned as `error`.
|
||||||
|
// If WithNeoFSErrorParsing option has been provided, unsuccessful
|
||||||
|
// NeoFS status codes are returned as `error`, otherwise, are included
|
||||||
|
// in the returned result structure.
|
||||||
|
//
|
||||||
|
// Operation is asynchronous and no guaranteed even in the absence of errors.
|
||||||
|
// The required time is also not predictable.
|
||||||
|
//
|
||||||
|
// At this moment success can not be checked.
|
||||||
|
//
|
||||||
|
// Immediately panics if parameters are set incorrectly (see PrmAnnounceSpace docs).
|
||||||
|
// Context is required and must not be nil. It is used for network communication.
|
||||||
|
//
|
||||||
|
// Return statuses:
|
||||||
|
// - global (see Client docs).
|
||||||
|
func (c *Client) ContainerAnnounceUsedSpace(ctx context.Context, prm PrmAnnounceSpace) (*ResAnnounceSpace, error) {
|
||||||
|
// check parameters
|
||||||
|
switch {
|
||||||
|
case ctx == nil:
|
||||||
|
panic(panicMsgMissingContext)
|
||||||
|
case len(prm.announcements) == 0:
|
||||||
|
panic("missing announcements")
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert list of SDK announcement structures into NeoFS-API v2 list
|
||||||
|
v2announce := make([]v2container.UsedSpaceAnnouncement, len(prm.announcements))
|
||||||
|
for i := range prm.announcements {
|
||||||
|
v2announce[i] = *prm.announcements[i].ToV2()
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare body of the NeoFS-API v2 request and request itself
|
||||||
|
reqBody := new(v2container.AnnounceUsedSpaceRequestBody)
|
||||||
|
reqBody.SetAnnouncements(v2announce)
|
||||||
|
|
||||||
|
// form request
|
||||||
|
var req v2container.AnnounceUsedSpaceRequest
|
||||||
|
|
||||||
|
req.SetBody(reqBody)
|
||||||
|
|
||||||
|
// init call context
|
||||||
|
|
||||||
|
var (
|
||||||
|
cc contextCall
|
||||||
|
res ResAnnounceSpace
|
||||||
|
)
|
||||||
|
|
||||||
|
c.initCallContext(&cc)
|
||||||
|
cc.meta = prm.prmCommonMeta
|
||||||
|
cc.req = &req
|
||||||
|
cc.statusRes = &res
|
||||||
|
cc.call = func() (responseV2, error) {
|
||||||
|
return rpcapi.AnnounceUsedSpace(&c.c, &req, client.WithContext(ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
// process call
|
||||||
|
if !cc.processCall() {
|
||||||
|
return nil, cc.err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &res, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,139 +0,0 @@
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
v2container "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
|
||||||
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
|
||||||
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
|
|
||||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
|
||||||
frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto"
|
|
||||||
frostfsecdsa "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto/ecdsa"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PrmContainerDelete groups parameters of ContainerDelete operation.
|
|
||||||
type PrmContainerDelete struct {
|
|
||||||
// FrostFS request X-Headers
|
|
||||||
XHeaders []string
|
|
||||||
|
|
||||||
ContainerID *cid.ID
|
|
||||||
|
|
||||||
Session *session.Container
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetContainer sets identifier of the FrostFS container to be removed.
|
|
||||||
// Required parameter.
|
|
||||||
//
|
|
||||||
// Deprecated: Use PrmContainerDelete.Container instead.
|
|
||||||
func (prm *PrmContainerDelete) SetContainer(id cid.ID) {
|
|
||||||
prm.ContainerID = &id
|
|
||||||
}
|
|
||||||
|
|
||||||
func (prm *PrmContainerDelete) buildRequest(c *Client) (*v2container.DeleteRequest, error) {
|
|
||||||
if prm.ContainerID == nil {
|
|
||||||
return nil, errorMissingContainer
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(prm.XHeaders)%2 != 0 {
|
|
||||||
return nil, errorInvalidXHeaders
|
|
||||||
}
|
|
||||||
|
|
||||||
var cidV2 refs.ContainerID
|
|
||||||
prm.ContainerID.WriteToV2(&cidV2)
|
|
||||||
|
|
||||||
// Container contract expects signature of container ID value,
|
|
||||||
// don't get confused with stable marshaled protobuf container.ID structure.
|
|
||||||
data := cidV2.GetValue()
|
|
||||||
|
|
||||||
var sig frostfscrypto.Signature
|
|
||||||
|
|
||||||
err := sig.Calculate(frostfsecdsa.SignerRFC6979(c.prm.Key), data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("calculate signature: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var sigv2 refs.Signature
|
|
||||||
sig.WriteToV2(&sigv2)
|
|
||||||
|
|
||||||
reqBody := new(v2container.DeleteRequestBody)
|
|
||||||
reqBody.SetContainerID(&cidV2)
|
|
||||||
reqBody.SetSignature(&sigv2)
|
|
||||||
|
|
||||||
var meta v2session.RequestMetaHeader
|
|
||||||
writeXHeadersToMeta(prm.XHeaders, &meta)
|
|
||||||
|
|
||||||
if prm.Session != nil {
|
|
||||||
var tokv2 v2session.Token
|
|
||||||
prm.Session.WriteToV2(&tokv2)
|
|
||||||
|
|
||||||
meta.SetSessionToken(&tokv2)
|
|
||||||
}
|
|
||||||
|
|
||||||
var req v2container.DeleteRequest
|
|
||||||
req.SetBody(reqBody)
|
|
||||||
c.prepareRequest(&req, &meta)
|
|
||||||
return &req, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithinSession specifies session within which container should be removed.
|
|
||||||
//
|
|
||||||
// Creator of the session acquires the authorship of the request.
|
|
||||||
// This may affect the execution of an operation (e.g. access control).
|
|
||||||
//
|
|
||||||
// Must be signed.
|
|
||||||
//
|
|
||||||
// Deprecated: Use PrmContainerDelete.Session instead.
|
|
||||||
func (prm *PrmContainerDelete) WithinSession(tok session.Container) {
|
|
||||||
prm.Session = &tok
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResContainerDelete groups resulting values of ContainerDelete operation.
|
|
||||||
type ResContainerDelete struct {
|
|
||||||
statusRes
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContainerDelete sends request to remove the FrostFS container.
|
|
||||||
//
|
|
||||||
// Exactly one return value is non-nil. By default, server status is returned in res structure.
|
|
||||||
// Any client's internal or transport errors are returned as `error`.
|
|
||||||
// If PrmInit.DisableFrostFSFailuresResolution has been called, unsuccessful
|
|
||||||
// FrostFS status codes are included in the returned result structure,
|
|
||||||
// otherwise, are also returned as `error`.
|
|
||||||
//
|
|
||||||
// Operation is asynchronous and no guaranteed even in the absence of errors.
|
|
||||||
// The required time is also not predictable.
|
|
||||||
//
|
|
||||||
// Success can be verified by reading by identifier (see GetContainer).
|
|
||||||
//
|
|
||||||
// Returns an error if parameters are set incorrectly (see PrmContainerDelete docs).
|
|
||||||
// Context is required and must not be nil. It is used for network communication.
|
|
||||||
//
|
|
||||||
// Exactly one return value is non-nil. Server status return is returned in ResContainerDelete.
|
|
||||||
// Reflects all internal errors in second return value (transport problems, response processing, etc.).
|
|
||||||
//
|
|
||||||
// Return statuses:
|
|
||||||
// - global (see Client docs).
|
|
||||||
func (c *Client) ContainerDelete(ctx context.Context, prm PrmContainerDelete) (*ResContainerDelete, error) {
|
|
||||||
req, err := prm.buildRequest(c)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := signature.SignServiceMessage(&c.prm.Key, req); err != nil {
|
|
||||||
return nil, fmt.Errorf("sign request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := rpcapi.DeleteContainer(&c.c, req, client.WithContext(ctx))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var res ResContainerDelete
|
|
||||||
res.st, err = c.processResponse(resp)
|
|
||||||
return &res, err
|
|
||||||
}
|
|
|
@ -1,126 +0,0 @@
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
v2container "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
|
||||||
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
|
||||||
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
|
|
||||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
|
|
||||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PrmContainerGet groups parameters of ContainerGet operation.
|
|
||||||
type PrmContainerGet struct {
|
|
||||||
// FrostFS request X-Headers.
|
|
||||||
XHeaders []string
|
|
||||||
|
|
||||||
ContainerID *cid.ID
|
|
||||||
|
|
||||||
Session *session.Container
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetContainer sets identifier of the container to be read.
|
|
||||||
// Required parameter.
|
|
||||||
//
|
|
||||||
// Deprecated: Use PrmContainerGet.ContainerID instead.
|
|
||||||
func (prm *PrmContainerGet) SetContainer(cid cid.ID) {
|
|
||||||
prm.ContainerID = &cid
|
|
||||||
}
|
|
||||||
|
|
||||||
func (prm *PrmContainerGet) buildRequest(c *Client) (*v2container.GetRequest, error) {
|
|
||||||
if prm.ContainerID == nil {
|
|
||||||
return nil, errorMissingContainer
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(prm.XHeaders)%2 != 0 {
|
|
||||||
return nil, errorInvalidXHeaders
|
|
||||||
}
|
|
||||||
|
|
||||||
var cidV2 refs.ContainerID
|
|
||||||
prm.ContainerID.WriteToV2(&cidV2)
|
|
||||||
|
|
||||||
reqBody := new(v2container.GetRequestBody)
|
|
||||||
reqBody.SetContainerID(&cidV2)
|
|
||||||
|
|
||||||
var meta v2session.RequestMetaHeader
|
|
||||||
writeXHeadersToMeta(prm.XHeaders, &meta)
|
|
||||||
|
|
||||||
if prm.Session != nil {
|
|
||||||
var tokv2 v2session.Token
|
|
||||||
prm.Session.WriteToV2(&tokv2)
|
|
||||||
|
|
||||||
meta.SetSessionToken(&tokv2)
|
|
||||||
}
|
|
||||||
|
|
||||||
var req v2container.GetRequest
|
|
||||||
req.SetBody(reqBody)
|
|
||||||
c.prepareRequest(&req, &meta)
|
|
||||||
return &req, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResContainerGet groups resulting values of ContainerGet operation.
|
|
||||||
type ResContainerGet struct {
|
|
||||||
statusRes
|
|
||||||
|
|
||||||
cnr container.Container
|
|
||||||
}
|
|
||||||
|
|
||||||
// Container returns structured information about the requested container.
|
|
||||||
//
|
|
||||||
// Client doesn't retain value so modification is safe.
|
|
||||||
func (x ResContainerGet) Container() container.Container {
|
|
||||||
return x.cnr
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContainerGet reads FrostFS container by ID.
|
|
||||||
//
|
|
||||||
// Exactly one return value is non-nil. By default, server status is returned in res structure.
|
|
||||||
// Any client's internal or transport errors are returned as `error`.
|
|
||||||
// If PrmInit.DisableFrostFSFailuresResolution has been called, unsuccessful
|
|
||||||
// FrostFS status codes are included in the returned result structure,
|
|
||||||
// otherwise, are also returned as `error`.
|
|
||||||
//
|
|
||||||
// Returns an error if parameters are set incorrectly (see PrmContainerGet docs).
|
|
||||||
// Context is required and must not be nil. It is used for network communication.
|
|
||||||
//
|
|
||||||
// Return statuses:
|
|
||||||
// - global (see Client docs);
|
|
||||||
// - *apistatus.ContainerNotFound.
|
|
||||||
func (c *Client) ContainerGet(ctx context.Context, prm PrmContainerGet) (*ResContainerGet, error) {
|
|
||||||
req, err := prm.buildRequest(c)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := signature.SignServiceMessage(&c.prm.Key, req); err != nil {
|
|
||||||
return nil, fmt.Errorf("sign request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := rpcapi.GetContainer(&c.c, req, client.WithContext(ctx))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var res ResContainerGet
|
|
||||||
res.st, err = c.processResponse(resp)
|
|
||||||
if err != nil || !apistatus.IsSuccessful(res.st) {
|
|
||||||
return &res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cnrV2 := resp.GetBody().GetContainer()
|
|
||||||
if cnrV2 == nil {
|
|
||||||
return &res, errors.New("missing container in response")
|
|
||||||
}
|
|
||||||
if err := res.cnr.ReadFromV2(*cnrV2); err != nil {
|
|
||||||
return &res, fmt.Errorf("invalid container in response: %w", err)
|
|
||||||
}
|
|
||||||
return &res, nil
|
|
||||||
}
|
|
|
@ -1,119 +0,0 @@
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
v2container "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
|
||||||
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
|
||||||
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
|
|
||||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
|
||||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PrmContainerList groups parameters of ContainerList operation.
|
|
||||||
type PrmContainerList struct {
|
|
||||||
XHeaders []string
|
|
||||||
|
|
||||||
Account user.ID
|
|
||||||
|
|
||||||
Session *session.Container
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetAccount sets identifier of the FrostFS account to list the containers.
|
|
||||||
// Required parameter.
|
|
||||||
//
|
|
||||||
// Deprecated: Use PrmContainerList.Account instead.
|
|
||||||
func (x *PrmContainerList) SetAccount(id user.ID) {
|
|
||||||
x.Account = id
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *PrmContainerList) buildRequest(c *Client) (*v2container.ListRequest, error) {
|
|
||||||
if x.Account.IsEmpty() {
|
|
||||||
return nil, errorAccountNotSet
|
|
||||||
}
|
|
||||||
|
|
||||||
var ownerV2 refs.OwnerID
|
|
||||||
x.Account.WriteToV2(&ownerV2)
|
|
||||||
|
|
||||||
reqBody := new(v2container.ListRequestBody)
|
|
||||||
reqBody.SetOwnerID(&ownerV2)
|
|
||||||
|
|
||||||
var meta v2session.RequestMetaHeader
|
|
||||||
writeXHeadersToMeta(x.XHeaders, &meta)
|
|
||||||
|
|
||||||
if x.Session != nil {
|
|
||||||
var tokv2 v2session.Token
|
|
||||||
x.Session.WriteToV2(&tokv2)
|
|
||||||
|
|
||||||
meta.SetSessionToken(&tokv2)
|
|
||||||
}
|
|
||||||
|
|
||||||
var req v2container.ListRequest
|
|
||||||
req.SetBody(reqBody)
|
|
||||||
c.prepareRequest(&req, &meta)
|
|
||||||
return &req, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResContainerList groups resulting values of ContainerList operation.
|
|
||||||
type ResContainerList struct {
|
|
||||||
statusRes
|
|
||||||
|
|
||||||
ids []cid.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
// Containers returns list of identifiers of the account-owned containers.
|
|
||||||
//
|
|
||||||
// Client doesn't retain value so modification is safe.
|
|
||||||
func (x ResContainerList) Containers() []cid.ID {
|
|
||||||
return x.ids
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContainerList requests identifiers of the account-owned containers.
|
|
||||||
//
|
|
||||||
// Exactly one return value is non-nil. By default, server status is returned in res structure.
|
|
||||||
// Any client's internal or transport errors are returned as `error`.
|
|
||||||
// If PrmInit.DisableFrostFSFailuresResolution has been called, unsuccessful
|
|
||||||
// FrostFS status codes are included in the returned result structure,
|
|
||||||
// otherwise, are also returned as `error`.
|
|
||||||
//
|
|
||||||
// Returns an error if parameters are set incorrectly (see PrmContainerList docs).
|
|
||||||
// Context is required and must not be nil. It is used for network communication.
|
|
||||||
//
|
|
||||||
// Return statuses:
|
|
||||||
// - global (see Client docs).
|
|
||||||
func (c *Client) ContainerList(ctx context.Context, prm PrmContainerList) (*ResContainerList, error) {
|
|
||||||
req, err := prm.buildRequest(c)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := signature.SignServiceMessage(&c.prm.Key, req); err != nil {
|
|
||||||
return nil, fmt.Errorf("sign request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := rpcapi.ListContainers(&c.c, req, client.WithContext(ctx))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var res ResContainerList
|
|
||||||
res.st, err = c.processResponse(resp)
|
|
||||||
if err != nil || !apistatus.IsSuccessful(res.st) {
|
|
||||||
return &res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
res.ids = make([]cid.ID, len(resp.GetBody().GetContainerIDs()))
|
|
||||||
for i, cidV2 := range resp.GetBody().GetContainerIDs() {
|
|
||||||
if err := res.ids[i].ReadFromV2(cidV2); err != nil {
|
|
||||||
return &res, fmt.Errorf("invalid ID in the response: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &res, nil
|
|
||||||
}
|
|
|
@ -1,160 +0,0 @@
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
v2container "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
|
||||||
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
|
||||||
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
|
|
||||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
|
|
||||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
|
||||||
frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PrmContainerPut groups parameters of ContainerPut operation.
|
|
||||||
type PrmContainerPut struct {
|
|
||||||
// FrostFS request X-Headers
|
|
||||||
XHeaders []string
|
|
||||||
|
|
||||||
Container *container.Container
|
|
||||||
|
|
||||||
Session *session.Container
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetContainer sets structured information about new FrostFS container.
|
|
||||||
// Required parameter.
|
|
||||||
//
|
|
||||||
// Deprecated: Use PrmContainerPut.Container instead.
|
|
||||||
func (x *PrmContainerPut) SetContainer(cnr container.Container) {
|
|
||||||
x.Container = &cnr
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithinSession specifies session within which container should be saved.
|
|
||||||
//
|
|
||||||
// Creator of the session acquires the authorship of the request. This affects
|
|
||||||
// the execution of an operation (e.g. access control).
|
|
||||||
//
|
|
||||||
// Session is optional, if set the following requirements apply:
|
|
||||||
// - session operation MUST be session.VerbContainerPut (ForVerb)
|
|
||||||
// - token MUST be signed using private key of the owner of the container to be saved
|
|
||||||
//
|
|
||||||
// Deprecated: Use PrmContainerPut.Session instead.
|
|
||||||
func (x *PrmContainerPut) WithinSession(s session.Container) {
|
|
||||||
x.Session = &s
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *PrmContainerPut) buildRequest(c *Client) (*v2container.PutRequest, error) {
|
|
||||||
if x.Container == nil {
|
|
||||||
return nil, errorMissingContainer
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(x.XHeaders)%2 != 0 {
|
|
||||||
return nil, errorInvalidXHeaders
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: check private key is set before forming the request
|
|
||||||
var cnr v2container.Container
|
|
||||||
x.Container.WriteToV2(&cnr)
|
|
||||||
|
|
||||||
var sig frostfscrypto.Signature
|
|
||||||
|
|
||||||
err := container.CalculateSignature(&sig, *x.Container, c.prm.Key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("calculate container signature: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var sigv2 refs.Signature
|
|
||||||
sig.WriteToV2(&sigv2)
|
|
||||||
|
|
||||||
reqBody := new(v2container.PutRequestBody)
|
|
||||||
reqBody.SetContainer(&cnr)
|
|
||||||
reqBody.SetSignature(&sigv2)
|
|
||||||
|
|
||||||
var meta v2session.RequestMetaHeader
|
|
||||||
writeXHeadersToMeta(x.XHeaders, &meta)
|
|
||||||
|
|
||||||
if x.Session != nil {
|
|
||||||
var tokv2 v2session.Token
|
|
||||||
x.Session.WriteToV2(&tokv2)
|
|
||||||
|
|
||||||
meta.SetSessionToken(&tokv2)
|
|
||||||
}
|
|
||||||
|
|
||||||
var req v2container.PutRequest
|
|
||||||
req.SetBody(reqBody)
|
|
||||||
c.prepareRequest(&req, &meta)
|
|
||||||
return &req, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResContainerPut groups resulting values of ContainerPut operation.
|
|
||||||
type ResContainerPut struct {
|
|
||||||
statusRes
|
|
||||||
|
|
||||||
id cid.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
// ID returns identifier of the container declared to be stored in the system.
|
|
||||||
// Used as a link to information about the container (in particular, you can
|
|
||||||
// asynchronously check if the save was successful).
|
|
||||||
func (x ResContainerPut) ID() cid.ID {
|
|
||||||
return x.id
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContainerPut sends request to save container in FrostFS.
|
|
||||||
//
|
|
||||||
// Exactly one return value is non-nil. By default, server status is returned in res structure.
|
|
||||||
// Any client's internal or transport errors are returned as `error`.
|
|
||||||
// If PrmInit.DisableFrostFSFailuresResolution has been called, unsuccessful
|
|
||||||
// FrostFS status codes are included in the returned result structure,
|
|
||||||
// otherwise, are also returned as `error`.
|
|
||||||
//
|
|
||||||
// Operation is asynchronous and no guaranteed even in the absence of errors.
|
|
||||||
// The required time is also not predictable.
|
|
||||||
//
|
|
||||||
// Success can be verified by reading by identifier (see ResContainerPut.ID).
|
|
||||||
//
|
|
||||||
// Returns an error if parameters are set incorrectly (see PrmContainerPut docs).
|
|
||||||
// Context is required and must not be nil. It is used for network communication.
|
|
||||||
//
|
|
||||||
// Return statuses:
|
|
||||||
// - global (see Client docs).
|
|
||||||
//
|
|
||||||
// nolint: funlen
|
|
||||||
func (c *Client) ContainerPut(ctx context.Context, prm PrmContainerPut) (*ResContainerPut, error) {
|
|
||||||
req, err := prm.buildRequest(c)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := signature.SignServiceMessage(&c.prm.Key, req); err != nil {
|
|
||||||
return nil, fmt.Errorf("sign request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := rpcapi.PutContainer(&c.c, req, client.WithContext(ctx))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var res ResContainerPut
|
|
||||||
res.st, err = c.processResponse(resp)
|
|
||||||
if err != nil || !apistatus.IsSuccessful(res.st) {
|
|
||||||
return &res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldCnrID = "container ID"
|
|
||||||
|
|
||||||
cidV2 := resp.GetBody().GetContainerID()
|
|
||||||
if cidV2 == nil {
|
|
||||||
return &res, newErrMissingResponseField(fieldCnrID)
|
|
||||||
}
|
|
||||||
if err := res.id.ReadFromV2(*cidV2); err != nil {
|
|
||||||
return &res, newErrInvalidResponseField(fieldCnrID, err)
|
|
||||||
}
|
|
||||||
return &res, nil
|
|
||||||
}
|
|
|
@ -1,23 +1,20 @@
|
||||||
/*
|
/*
|
||||||
Package client provides FrostFS API client implementation.
|
Package client provides NeoFS API client implementation.
|
||||||
|
|
||||||
The main component is Client type. It is a virtual connection to the network
|
The main component is Client type. It is a virtual connection to the network
|
||||||
and provides methods for executing operations on the server.
|
and provides methods for executing operations on the server.
|
||||||
|
|
||||||
Create client instance:
|
Create client instance:
|
||||||
|
|
||||||
var c client.Client
|
var c client.Client
|
||||||
|
|
||||||
Initialize client state:
|
Initialize client state:
|
||||||
|
|
||||||
var prm client.PrmInit
|
var prm client.PrmInit
|
||||||
prm.SetDefaultPrivateKey(key)
|
prm.SetDefaultPrivateKey(key)
|
||||||
// ...
|
// ...
|
||||||
|
|
||||||
c.Init(prm)
|
c.Init(prm)
|
||||||
|
|
||||||
Connect to the FrostFS server:
|
Connect to the NeoFS server:
|
||||||
|
|
||||||
var prm client.PrmDial
|
var prm client.PrmDial
|
||||||
prm.SetServerURI("localhost:8080")
|
prm.SetServerURI("localhost:8080")
|
||||||
prm.SetDefaultPrivateKey(key)
|
prm.SetDefaultPrivateKey(key)
|
||||||
|
@ -26,8 +23,7 @@ Connect to the FrostFS server:
|
||||||
err := c.Dial(prm)
|
err := c.Dial(prm)
|
||||||
// ...
|
// ...
|
||||||
|
|
||||||
Execute FrostFS operation on the server:
|
Execute NeoFS operation on the server:
|
||||||
|
|
||||||
var prm client.PrmContainerPut
|
var prm client.PrmContainerPut
|
||||||
prm.SetContainer(cnr)
|
prm.SetContainer(cnr)
|
||||||
// ...
|
// ...
|
||||||
|
@ -40,15 +36,14 @@ Execute FrostFS operation on the server:
|
||||||
// ...
|
// ...
|
||||||
|
|
||||||
Consume custom service of the server:
|
Consume custom service of the server:
|
||||||
|
|
||||||
syntax = "proto3";
|
syntax = "proto3";
|
||||||
|
|
||||||
service CustomService {
|
service CustomService {
|
||||||
rpc CustomRPC(CustomRPCRequest) returns (CustomRPCResponse);
|
rpc CustomRPC(CustomRPCRequest) returns (CustomRPCResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
import "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
import "github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
|
||||||
import "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/common"
|
import "github.com/nspcc-dev/neofs-api-go/v2/rpc/common"
|
||||||
|
|
||||||
req := new(CustomRPCRequest)
|
req := new(CustomRPCRequest)
|
||||||
// ...
|
// ...
|
||||||
|
@ -63,7 +58,6 @@ Consume custom service of the server:
|
||||||
// ...
|
// ...
|
||||||
|
|
||||||
Close the connection:
|
Close the connection:
|
||||||
|
|
||||||
err := c.Close()
|
err := c.Close()
|
||||||
// ...
|
// ...
|
||||||
|
|
||||||
|
@ -71,10 +65,9 @@ Note that it's not allowed to override Client behaviour directly: the parameters
|
||||||
for the all operations are write-only and the results of the all operations are
|
for the all operations are write-only and the results of the all operations are
|
||||||
read-only. To be able to override client behavior (e.g. for tests), abstract it
|
read-only. To be able to override client behavior (e.g. for tests), abstract it
|
||||||
with an interface:
|
with an interface:
|
||||||
|
import "github.com/nspcc-dev/neofs-sdk-go/client"
|
||||||
|
|
||||||
import "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client"
|
type NeoFSClient interface {
|
||||||
|
|
||||||
type FrostFSClient interface {
|
|
||||||
// Operations according to the application needs
|
// Operations according to the application needs
|
||||||
CreateContainer(context.Context, container.Container) error
|
CreateContainer(context.Context, container.Container) error
|
||||||
// ...
|
// ...
|
||||||
|
@ -87,5 +80,6 @@ with an interface:
|
||||||
func (x *client) CreateContainer(context.Context, container.Container) error {
|
func (x *client) CreateContainer(context.Context, container.Container) error {
|
||||||
// ...
|
// ...
|
||||||
}
|
}
|
||||||
|
|
||||||
*/
|
*/
|
||||||
package client
|
package client
|
||||||
|
|
101
client/errors.go
101
client/errors.go
|
@ -1,85 +1,42 @@
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
// IsErrContainerNotFound checks if err corresponds to NeoFS status
|
||||||
)
|
// return corresponding to missing container.
|
||||||
|
func IsErrContainerNotFound(err error) bool {
|
||||||
// wrapsErrType returns true if any error in the error tree of err is of type E.
|
switch err.(type) {
|
||||||
func wrapsErrType[E error](err error) bool {
|
|
||||||
switch e := err.(type) {
|
|
||||||
case E:
|
|
||||||
return true
|
|
||||||
case interface{ Unwrap() error }:
|
|
||||||
return wrapsErrType[E](e.Unwrap())
|
|
||||||
case interface{ Unwrap() []error }:
|
|
||||||
for _, ei := range e.Unwrap() {
|
|
||||||
if wrapsErrType[E](ei) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
case
|
||||||
|
apistatus.ContainerNotFound,
|
||||||
|
*apistatus.ContainerNotFound:
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsErrContainerNotFound checks if err corresponds to FrostFS status
|
// IsErrObjectNotFound checks if err corresponds to NeoFS status
|
||||||
// return corresponding to missing container. Supports wrapped errors.
|
// return corresponding to missing object.
|
||||||
func IsErrContainerNotFound(err error) bool {
|
|
||||||
return wrapsErrType[*apistatus.ContainerNotFound](err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsErrEACLNotFound checks if err corresponds to FrostFS status
|
|
||||||
// return corresponding to missing eACL table. Supports wrapped errors.
|
|
||||||
func IsErrEACLNotFound(err error) bool {
|
|
||||||
return wrapsErrType[*apistatus.EACLNotFound](err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsErrObjectNotFound checks if err corresponds to FrostFS status
|
|
||||||
// return corresponding to missing object. Supports wrapped errors.
|
|
||||||
func IsErrObjectNotFound(err error) bool {
|
func IsErrObjectNotFound(err error) bool {
|
||||||
return wrapsErrType[*apistatus.ObjectNotFound](err)
|
switch err.(type) {
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
case
|
||||||
|
apistatus.ObjectNotFound,
|
||||||
|
*apistatus.ObjectNotFound:
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsErrObjectAlreadyRemoved checks if err corresponds to FrostFS status
|
// IsErrObjectAlreadyRemoved checks if err corresponds to NeoFS status
|
||||||
// return corresponding to already removed object. Supports wrapped errors.
|
// return corresponding to already removed object.
|
||||||
func IsErrObjectAlreadyRemoved(err error) bool {
|
func IsErrObjectAlreadyRemoved(err error) bool {
|
||||||
return wrapsErrType[*apistatus.ObjectAlreadyRemoved](err)
|
switch err.(type) {
|
||||||
}
|
default:
|
||||||
|
return false
|
||||||
// IsErrSessionExpired checks if err corresponds to FrostFS status return
|
case
|
||||||
// corresponding to expired session. Supports wrapped errors.
|
apistatus.ObjectAlreadyRemoved,
|
||||||
func IsErrSessionExpired(err error) bool {
|
*apistatus.ObjectAlreadyRemoved:
|
||||||
return wrapsErrType[*apistatus.SessionTokenExpired](err)
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsErrSessionNotFound checks if err corresponds to FrostFS status return
|
|
||||||
// corresponding to missing session. Supports wrapped errors.
|
|
||||||
func IsErrSessionNotFound(err error) bool {
|
|
||||||
return wrapsErrType[*apistatus.SessionTokenNotFound](err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsErrAPEManagerAccessDenied checks if err corresponds to FrostFS status return
|
|
||||||
// corresponding to apemanager access deny. Supports wrapped errors.
|
|
||||||
func IsErrAPEManagerAccessDenied(err error) bool {
|
|
||||||
return wrapsErrType[*apistatus.APEManagerAccessDenied](err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsErrNodeUnderMaintenance checks if err corresponds to FrostFS status return
|
|
||||||
// corresponding to nodes being under maintenance. Supports wrapped errors.
|
|
||||||
func IsErrNodeUnderMaintenance(err error) bool {
|
|
||||||
return wrapsErrType[*apistatus.NodeUnderMaintenance](err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// returns error describing missing field with the given name.
|
|
||||||
func newErrMissingResponseField(name string) error {
|
|
||||||
return fmt.Errorf("missing %s field in the response", name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// returns error describing invalid field (according to the FrostFS protocol)
|
|
||||||
// with the given name and format violation err.
|
|
||||||
func newErrInvalidResponseField(name string, err error) error {
|
|
||||||
return fmt.Errorf("invalid %s field in the response: %w", name, err)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
package client_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client"
|
|
||||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestErrors(t *testing.T) {
|
|
||||||
errs := []struct {
|
|
||||||
check func(error) bool
|
|
||||||
err error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
check: client.IsErrContainerNotFound,
|
|
||||||
err: new(apistatus.ContainerNotFound),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
check: client.IsErrEACLNotFound,
|
|
||||||
err: new(apistatus.EACLNotFound),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
check: client.IsErrObjectNotFound,
|
|
||||||
err: new(apistatus.ObjectNotFound),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
check: client.IsErrObjectAlreadyRemoved,
|
|
||||||
err: new(apistatus.ObjectAlreadyRemoved),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
check: client.IsErrSessionExpired,
|
|
||||||
err: new(apistatus.SessionTokenExpired),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
check: client.IsErrSessionNotFound,
|
|
||||||
err: new(apistatus.SessionTokenNotFound),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
check: client.IsErrNodeUnderMaintenance,
|
|
||||||
err: new(apistatus.NodeUnderMaintenance),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range errs {
|
|
||||||
for j := range errs {
|
|
||||||
nestedErr := fmt.Errorf("top-level context: :%w", fmt.Errorf("inner context: %w", errs[j].err))
|
|
||||||
require.Equal(t, i == j, errs[i].check(errs[j].err))
|
|
||||||
require.Equal(t, i == j, errs[i].check(nestedErr))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
284
client/netmap.go
284
client/netmap.go
|
@ -2,261 +2,177 @@ package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
v2netmap "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/netmap"
|
v2netmap "github.com/nspcc-dev/neofs-api-go/v2/netmap"
|
||||||
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
|
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
|
||||||
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
"github.com/nspcc-dev/neofs-sdk-go/netmap"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
|
"github.com/nspcc-dev/neofs-sdk-go/version"
|
||||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/version"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// PrmEndpointInfo groups parameters of EndpointInfo operation.
|
// PrmEndpointInfo groups parameters of EndpointInfo operation.
|
||||||
type PrmEndpointInfo struct {
|
type PrmEndpointInfo struct {
|
||||||
XHeaders []string
|
prmCommonMeta
|
||||||
}
|
|
||||||
|
|
||||||
func (x *PrmEndpointInfo) buildRequest(c *Client) (*v2netmap.LocalNodeInfoRequest, error) {
|
|
||||||
meta := new(v2session.RequestMetaHeader)
|
|
||||||
writeXHeadersToMeta(x.XHeaders, meta)
|
|
||||||
|
|
||||||
req := new(v2netmap.LocalNodeInfoRequest)
|
|
||||||
req.SetBody(new(v2netmap.LocalNodeInfoRequestBody))
|
|
||||||
c.prepareRequest(req, meta)
|
|
||||||
return req, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResEndpointInfo group resulting values of EndpointInfo operation.
|
// ResEndpointInfo group resulting values of EndpointInfo operation.
|
||||||
type ResEndpointInfo struct {
|
type ResEndpointInfo struct {
|
||||||
statusRes
|
statusRes
|
||||||
|
|
||||||
version version.Version
|
version *version.Version
|
||||||
|
|
||||||
ni netmap.NodeInfo
|
ni *netmap.NodeInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
// LatestVersion returns latest FrostFS API protocol's version in use.
|
// LatestVersion returns latest NeoFS API protocol's version in use.
|
||||||
func (x ResEndpointInfo) LatestVersion() version.Version {
|
//
|
||||||
|
// Client doesn't retain value so modification is safe.
|
||||||
|
func (x ResEndpointInfo) LatestVersion() *version.Version {
|
||||||
return x.version
|
return x.version
|
||||||
}
|
}
|
||||||
|
|
||||||
// NodeInfo returns information about the FrostFS node served on the remote endpoint.
|
func (x *ResEndpointInfo) setLatestVersion(ver *version.Version) {
|
||||||
func (x ResEndpointInfo) NodeInfo() netmap.NodeInfo {
|
x.version = ver
|
||||||
|
}
|
||||||
|
|
||||||
|
// NodeInfo returns information about the NeoFS node served on the remote endpoint.
|
||||||
|
//
|
||||||
|
// Client doesn't retain value so modification is safe.
|
||||||
|
func (x ResEndpointInfo) NodeInfo() *netmap.NodeInfo {
|
||||||
return x.ni
|
return x.ni
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *ResEndpointInfo) setNodeInfo(info *netmap.NodeInfo) {
|
||||||
|
x.ni = info
|
||||||
|
}
|
||||||
|
|
||||||
// EndpointInfo requests information about the storage node served on the remote endpoint.
|
// EndpointInfo requests information about the storage node served on the remote endpoint.
|
||||||
//
|
//
|
||||||
// Method can be used as a health check to see if node is alive and responds to requests.
|
// Method can be used as a health check to see if node is alive and responds to requests.
|
||||||
//
|
//
|
||||||
// Any client's internal or transport errors are returned as `error`.
|
// Any client's internal or transport errors are returned as `error`.
|
||||||
// If PrmInit.DisableFrostFSFailuresResolution has been called, unsuccessful
|
// If WithNeoFSErrorParsing option has been provided, unsuccessful
|
||||||
// FrostFS status codes are included in the returned result structure,
|
// NeoFS status codes are returned as `error`, otherwise, are included
|
||||||
// otherwise, are also returned as `error`.
|
// in the returned result structure.
|
||||||
//
|
//
|
||||||
// Returns an error if parameters are set incorrectly (see PrmEndpointInfo docs).
|
// Immediately panics if parameters are set incorrectly (see PrmEndpointInfo docs).
|
||||||
// Context is required and must not be nil. It is used for network communication.
|
// Context is required and must not be nil. It is used for network communication.
|
||||||
//
|
//
|
||||||
// Exactly one return value is non-nil. Server status return is returned in ResEndpointInfo.
|
// Exactly one return value is non-nil. Server status return is returned in ResEndpointInfo.
|
||||||
// Reflects all internal errors in second return value (transport problems, response processing, etc.).
|
// Reflects all internal errors in second return value (transport problems, response processing, etc.).
|
||||||
//
|
//
|
||||||
// Return statuses:
|
// Return statuses:
|
||||||
// - global (see Client docs).
|
// - global (see Client docs).
|
||||||
func (c *Client) EndpointInfo(ctx context.Context, prm PrmEndpointInfo) (*ResEndpointInfo, error) {
|
func (c *Client) EndpointInfo(ctx context.Context, prm PrmEndpointInfo) (*ResEndpointInfo, error) {
|
||||||
req, err := prm.buildRequest(c)
|
// check context
|
||||||
if err != nil {
|
if ctx == nil {
|
||||||
return nil, err
|
panic(panicMsgMissingContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := signature.SignServiceMessage(&c.prm.Key, req); err != nil {
|
// form request
|
||||||
return nil, fmt.Errorf("sign request: %w", err)
|
var req v2netmap.LocalNodeInfoRequest
|
||||||
|
|
||||||
|
// init call context
|
||||||
|
|
||||||
|
var (
|
||||||
|
cc contextCall
|
||||||
|
res ResEndpointInfo
|
||||||
|
)
|
||||||
|
|
||||||
|
c.initCallContext(&cc)
|
||||||
|
cc.meta = prm.prmCommonMeta
|
||||||
|
cc.req = &req
|
||||||
|
cc.statusRes = &res
|
||||||
|
cc.call = func() (responseV2, error) {
|
||||||
|
return rpcapi.LocalNodeInfo(&c.c, &req, client.WithContext(ctx))
|
||||||
|
}
|
||||||
|
cc.result = func(r responseV2) {
|
||||||
|
resp := r.(*v2netmap.LocalNodeInfoResponse)
|
||||||
|
|
||||||
|
body := resp.GetBody()
|
||||||
|
|
||||||
|
res.setLatestVersion(version.NewFromV2(body.GetVersion()))
|
||||||
|
res.setNodeInfo(netmap.NewNodeInfoFromV2(body.GetNodeInfo()))
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := rpcapi.LocalNodeInfo(&c.c, req, client.WithContext(ctx))
|
// process call
|
||||||
if err != nil {
|
if !cc.processCall() {
|
||||||
return nil, err
|
return nil, cc.err
|
||||||
}
|
}
|
||||||
|
|
||||||
var res ResEndpointInfo
|
|
||||||
res.st, err = c.processResponse(resp)
|
|
||||||
if err != nil || !apistatus.IsSuccessful(res.st) {
|
|
||||||
return &res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
body := resp.GetBody()
|
|
||||||
|
|
||||||
const fieldVersion = "version"
|
|
||||||
|
|
||||||
verV2 := body.GetVersion()
|
|
||||||
if verV2 == nil {
|
|
||||||
return nil, newErrMissingResponseField(fieldVersion)
|
|
||||||
}
|
|
||||||
if err := res.version.ReadFromV2(*verV2); err != nil {
|
|
||||||
return nil, newErrInvalidResponseField(fieldVersion, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldNodeInfo = "node info"
|
|
||||||
|
|
||||||
nodeInfoV2 := body.GetNodeInfo()
|
|
||||||
if nodeInfoV2 == nil {
|
|
||||||
return nil, newErrMissingResponseField(fieldNodeInfo)
|
|
||||||
}
|
|
||||||
if err := res.ni.ReadFromV2(*nodeInfoV2); err != nil {
|
|
||||||
return nil, newErrInvalidResponseField(fieldNodeInfo, err)
|
|
||||||
}
|
|
||||||
return &res, nil
|
return &res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrmNetworkInfo groups parameters of NetworkInfo operation.
|
// PrmNetworkInfo groups parameters of NetworkInfo operation.
|
||||||
type PrmNetworkInfo struct {
|
type PrmNetworkInfo struct {
|
||||||
XHeaders []string
|
prmCommonMeta
|
||||||
}
|
|
||||||
|
|
||||||
func (x PrmNetworkInfo) buildRequest(c *Client) (*v2netmap.NetworkInfoRequest, error) {
|
|
||||||
meta := new(v2session.RequestMetaHeader)
|
|
||||||
writeXHeadersToMeta(x.XHeaders, meta)
|
|
||||||
|
|
||||||
var req v2netmap.NetworkInfoRequest
|
|
||||||
req.SetBody(new(v2netmap.NetworkInfoRequestBody))
|
|
||||||
c.prepareRequest(&req, meta)
|
|
||||||
return &req, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResNetworkInfo groups resulting values of NetworkInfo operation.
|
// ResNetworkInfo groups resulting values of NetworkInfo operation.
|
||||||
type ResNetworkInfo struct {
|
type ResNetworkInfo struct {
|
||||||
statusRes
|
statusRes
|
||||||
|
|
||||||
info netmap.NetworkInfo
|
info *netmap.NetworkInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info returns structured information about the FrostFS network.
|
// Info returns structured information about the NeoFS network.
|
||||||
func (x ResNetworkInfo) Info() netmap.NetworkInfo {
|
//
|
||||||
|
// Client doesn't retain value so modification is safe.
|
||||||
|
func (x ResNetworkInfo) Info() *netmap.NetworkInfo {
|
||||||
return x.info
|
return x.info
|
||||||
}
|
}
|
||||||
|
|
||||||
// NetworkInfo requests information about the FrostFS network of which the remote server is a part.
|
func (x *ResNetworkInfo) setInfo(info *netmap.NetworkInfo) {
|
||||||
|
x.info = info
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkInfo requests information about the NeoFS network of which the remote server is a part.
|
||||||
//
|
//
|
||||||
// Any client's internal or transport errors are returned as `error`.
|
// Any client's internal or transport errors are returned as `error`.
|
||||||
// If PrmInit.DisableFrostFSFailuresResolution has been called, unsuccessful
|
// If WithNeoFSErrorParsing option has been provided, unsuccessful
|
||||||
// FrostFS status codes are included in the returned result structure,
|
// NeoFS status codes are returned as `error`, otherwise, are included
|
||||||
// otherwise, are also returned as `error`.
|
// in the returned result structure.
|
||||||
//
|
//
|
||||||
// Returns an error if parameters are set incorrectly (see PrmNetworkInfo docs).
|
// Immediately panics if parameters are set incorrectly (see PrmNetworkInfo docs).
|
||||||
// Context is required and must not be nil. It is used for network communication.
|
// Context is required and must not be nil. It is used for network communication.
|
||||||
//
|
//
|
||||||
// Exactly one return value is non-nil. Server status return is returned in ResNetworkInfo.
|
// Exactly one return value is non-nil. Server status return is returned in ResNetworkInfo.
|
||||||
// Reflects all internal errors in second return value (transport problems, response processing, etc.).
|
// Reflects all internal errors in second return value (transport problems, response processing, etc.).
|
||||||
//
|
//
|
||||||
// Return statuses:
|
// Return statuses:
|
||||||
// - global (see Client docs).
|
// - global (see Client docs).
|
||||||
func (c *Client) NetworkInfo(ctx context.Context, prm PrmNetworkInfo) (*ResNetworkInfo, error) {
|
func (c *Client) NetworkInfo(ctx context.Context, prm PrmNetworkInfo) (*ResNetworkInfo, error) {
|
||||||
req, err := prm.buildRequest(c)
|
// check context
|
||||||
if err != nil {
|
if ctx == nil {
|
||||||
return nil, err
|
panic(panicMsgMissingContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := signature.SignServiceMessage(&c.prm.Key, req); err != nil {
|
|
||||||
return nil, fmt.Errorf("sign request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := rpcapi.NetworkInfo(&c.c, req, client.WithContext(ctx))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var res ResNetworkInfo
|
|
||||||
res.st, err = c.processResponse(resp)
|
|
||||||
if err != nil || !apistatus.IsSuccessful(res.st) {
|
|
||||||
return &res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldNetInfo = "network info"
|
|
||||||
|
|
||||||
netInfoV2 := resp.GetBody().GetNetworkInfo()
|
|
||||||
if netInfoV2 == nil {
|
|
||||||
return nil, newErrMissingResponseField(fieldNetInfo)
|
|
||||||
}
|
|
||||||
if err := res.info.ReadFromV2(*netInfoV2); err != nil {
|
|
||||||
return nil, newErrInvalidResponseField(fieldNetInfo, err)
|
|
||||||
}
|
|
||||||
return &res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrmNetMapSnapshot groups parameters of NetMapSnapshot operation.
|
|
||||||
type PrmNetMapSnapshot struct{}
|
|
||||||
|
|
||||||
// ResNetMapSnapshot groups resulting values of NetMapSnapshot operation.
|
|
||||||
type ResNetMapSnapshot struct {
|
|
||||||
statusRes
|
|
||||||
|
|
||||||
netMap netmap.NetMap
|
|
||||||
}
|
|
||||||
|
|
||||||
// NetMap returns current server's local network map.
|
|
||||||
func (x ResNetMapSnapshot) NetMap() netmap.NetMap {
|
|
||||||
return x.netMap
|
|
||||||
}
|
|
||||||
|
|
||||||
// NetMapSnapshot requests current network view of the remote server.
|
|
||||||
//
|
|
||||||
// Any client's internal or transport errors are returned as `error`.
|
|
||||||
// If PrmInit.DisableFrostFSFailuresResolution has been called, unsuccessful
|
|
||||||
// FrostFS status codes are included in the returned result structure,
|
|
||||||
// otherwise, are also returned as `error`.
|
|
||||||
//
|
|
||||||
// Returns an error if parameters are set incorrectly.
|
|
||||||
// Context is required and MUST NOT be nil. It is used for network communication.
|
|
||||||
//
|
|
||||||
// Exactly one return value is non-nil. Server status return is returned in ResNetMapSnapshot.
|
|
||||||
// Reflects all internal errors in second return value (transport problems, response processing, etc.).
|
|
||||||
//
|
|
||||||
// Return statuses:
|
|
||||||
// - global (see Client docs).
|
|
||||||
func (c *Client) NetMapSnapshot(ctx context.Context, _ PrmNetMapSnapshot) (*ResNetMapSnapshot, error) {
|
|
||||||
// form request body
|
|
||||||
var body v2netmap.SnapshotRequestBody
|
|
||||||
|
|
||||||
// form meta header
|
|
||||||
var meta v2session.RequestMetaHeader
|
|
||||||
|
|
||||||
// form request
|
// form request
|
||||||
var req v2netmap.SnapshotRequest
|
var req v2netmap.NetworkInfoRequest
|
||||||
req.SetBody(&body)
|
|
||||||
c.prepareRequest(&req, &meta)
|
|
||||||
|
|
||||||
err := signature.SignServiceMessage(&c.prm.Key, &req)
|
// init call context
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("sign request: %w", err)
|
var (
|
||||||
|
cc contextCall
|
||||||
|
res ResNetworkInfo
|
||||||
|
)
|
||||||
|
|
||||||
|
c.initCallContext(&cc)
|
||||||
|
cc.meta = prm.prmCommonMeta
|
||||||
|
cc.req = &req
|
||||||
|
cc.statusRes = &res
|
||||||
|
cc.call = func() (responseV2, error) {
|
||||||
|
return rpcapi.NetworkInfo(&c.c, &req, client.WithContext(ctx))
|
||||||
|
}
|
||||||
|
cc.result = func(r responseV2) {
|
||||||
|
resp := r.(*v2netmap.NetworkInfoResponse)
|
||||||
|
|
||||||
|
res.setInfo(netmap.NewNetworkInfoFromV2(resp.GetBody().GetNetworkInfo()))
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := c.server.netMapSnapshot(ctx, req)
|
// process call
|
||||||
if err != nil {
|
if !cc.processCall() {
|
||||||
return nil, err
|
return nil, cc.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
|
return &res, nil
|
||||||
|
|
|
@ -1,132 +0,0 @@
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
v2netmap "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/netmap"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
|
|
||||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
type serverNetMap struct {
|
|
||||||
errTransport error
|
|
||||||
|
|
||||||
signResponse bool
|
|
||||||
|
|
||||||
statusOK bool
|
|
||||||
|
|
||||||
setNetMap bool
|
|
||||||
netMap v2netmap.NetMap
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *serverNetMap) netMapSnapshot(ctx context.Context, req v2netmap.SnapshotRequest) (*v2netmap.SnapshotResponse, error) {
|
|
||||||
err := signature.VerifyServiceMessage(&req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if x.errTransport != nil {
|
|
||||||
return nil, x.errTransport
|
|
||||||
}
|
|
||||||
|
|
||||||
var body v2netmap.SnapshotResponseBody
|
|
||||||
|
|
||||||
if x.setNetMap {
|
|
||||||
body.SetNetMap(&x.netMap)
|
|
||||||
}
|
|
||||||
|
|
||||||
var meta session.ResponseMetaHeader
|
|
||||||
|
|
||||||
if !x.statusOK {
|
|
||||||
meta.SetStatus(statusErr.ToStatusV2())
|
|
||||||
}
|
|
||||||
|
|
||||||
var resp v2netmap.SnapshotResponse
|
|
||||||
resp.SetBody(&body)
|
|
||||||
resp.SetMetaHeader(&meta)
|
|
||||||
|
|
||||||
if x.signResponse {
|
|
||||||
err = signature.SignServiceMessage(key, &resp)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("sign response: %v", err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestClient_NetMapSnapshot(t *testing.T) {
|
|
||||||
var err error
|
|
||||||
var prm PrmNetMapSnapshot
|
|
||||||
var res *ResNetMapSnapshot
|
|
||||||
var srv serverNetMap
|
|
||||||
c := newClient(&srv)
|
|
||||||
c.prm.DisableFrostFSFailuresResolution()
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
// request signature
|
|
||||||
srv.errTransport = errors.New("any error")
|
|
||||||
|
|
||||||
_, err = c.NetMapSnapshot(ctx, prm)
|
|
||||||
require.ErrorIs(t, err, srv.errTransport)
|
|
||||||
|
|
||||||
srv.errTransport = nil
|
|
||||||
|
|
||||||
// unsigned response
|
|
||||||
_, err = c.NetMapSnapshot(ctx, prm)
|
|
||||||
require.Error(t, err)
|
|
||||||
|
|
||||||
srv.signResponse = true
|
|
||||||
|
|
||||||
// status failure
|
|
||||||
res, err = c.NetMapSnapshot(ctx, prm)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assertStatusErr(t, res)
|
|
||||||
|
|
||||||
srv.statusOK = true
|
|
||||||
|
|
||||||
// missing netmap field
|
|
||||||
_, err = c.NetMapSnapshot(ctx, prm)
|
|
||||||
require.Error(t, err)
|
|
||||||
|
|
||||||
srv.setNetMap = true
|
|
||||||
|
|
||||||
// invalid network map
|
|
||||||
var netMap netmap.NetMap
|
|
||||||
|
|
||||||
var node netmap.NodeInfo
|
|
||||||
// TODO: #260 use instance corrupter
|
|
||||||
|
|
||||||
var nodeV2 v2netmap.NodeInfo
|
|
||||||
|
|
||||||
node.WriteToV2(&nodeV2)
|
|
||||||
require.Error(t, new(netmap.NodeInfo).ReadFromV2(nodeV2))
|
|
||||||
|
|
||||||
netMap.SetNodes([]netmap.NodeInfo{node})
|
|
||||||
netMap.WriteToV2(&srv.netMap)
|
|
||||||
|
|
||||||
_, err = c.NetMapSnapshot(ctx, prm)
|
|
||||||
require.Error(t, err)
|
|
||||||
|
|
||||||
// correct network map
|
|
||||||
// TODO: #260 use instance normalizer
|
|
||||||
node.SetPublicKey([]byte{1, 2, 3})
|
|
||||||
node.SetNetworkEndpoints("1", "2", "3")
|
|
||||||
|
|
||||||
node.WriteToV2(&nodeV2)
|
|
||||||
require.NoError(t, new(netmap.NodeInfo).ReadFromV2(nodeV2))
|
|
||||||
|
|
||||||
netMap.SetNodes([]netmap.NodeInfo{node})
|
|
||||||
netMap.WriteToV2(&srv.netMap)
|
|
||||||
|
|
||||||
res, err = c.NetMapSnapshot(ctx, prm)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.True(t, apistatus.IsSuccessful(res.Status()))
|
|
||||||
require.Equal(t, netMap, res.NetMap())
|
|
||||||
}
|
|
|
@ -3,106 +3,99 @@ package client
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl"
|
v2object "github.com/nspcc-dev/neofs-api-go/v2/object"
|
||||||
v2object "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
|
v2refs "github.com/nspcc-dev/neofs-api-go/v2/refs"
|
||||||
v2refs "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
|
||||||
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
|
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
|
||||||
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
|
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
|
"github.com/nspcc-dev/neofs-sdk-go/session"
|
||||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
"github.com/nspcc-dev/neofs-sdk-go/token"
|
||||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
|
||||||
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// PrmObjectDelete groups parameters of ObjectDelete operation.
|
// PrmObjectDelete groups parameters of ObjectDelete operation.
|
||||||
type PrmObjectDelete struct {
|
type PrmObjectDelete struct {
|
||||||
XHeaders []string
|
meta v2session.RequestMetaHeader
|
||||||
|
|
||||||
BearerToken *bearer.Token
|
body v2object.DeleteRequestBody
|
||||||
|
|
||||||
Session *session.Object
|
addr v2refs.Address
|
||||||
|
|
||||||
ContainerID *cid.ID
|
keySet bool
|
||||||
|
key ecdsa.PrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
ObjectID *oid.ID
|
// WithinSession specifies session within which object should be read.
|
||||||
|
//
|
||||||
|
// Creator of the session acquires the authorship of the request.
|
||||||
|
// This may affect the execution of an operation (e.g. access control).
|
||||||
|
//
|
||||||
|
// Must be signed.
|
||||||
|
func (x *PrmObjectDelete) WithinSession(t session.Token) {
|
||||||
|
x.meta.SetSessionToken(t.ToV2())
|
||||||
|
}
|
||||||
|
|
||||||
Key *ecdsa.PrivateKey
|
// WithBearerToken attaches bearer token to be used for the operation.
|
||||||
|
//
|
||||||
|
// If set, underlying eACL rules will be used in access control.
|
||||||
|
//
|
||||||
|
// Must be signed.
|
||||||
|
func (x *PrmObjectDelete) WithBearerToken(t token.BearerToken) {
|
||||||
|
x.meta.SetBearerToken(t.ToV2())
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromContainer specifies NeoFS container of the object.
|
||||||
|
// Required parameter.
|
||||||
|
func (x *PrmObjectDelete) FromContainer(id cid.ID) {
|
||||||
|
x.addr.SetContainerID(id.ToV2())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByID specifies identifier of the requested object.
|
||||||
|
// Required parameter.
|
||||||
|
func (x *PrmObjectDelete) ByID(id oid.ID) {
|
||||||
|
x.addr.SetObjectID(id.ToV2())
|
||||||
}
|
}
|
||||||
|
|
||||||
// UseKey specifies private key to sign the requests.
|
// UseKey specifies private key to sign the requests.
|
||||||
// If key is not provided, then Client default key is used.
|
// If key is not provided, then Client default key is used.
|
||||||
|
func (x *PrmObjectDelete) UseKey(key ecdsa.PrivateKey) {
|
||||||
|
x.keySet = true
|
||||||
|
x.key = key
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithXHeaders specifies list of extended headers (string key-value pairs)
|
||||||
|
// to be attached to the request. Must have an even length.
|
||||||
//
|
//
|
||||||
// Deprecated: Use PrmObjectDelete.Key instead.
|
// Slice must not be mutated until the operation completes.
|
||||||
func (prm *PrmObjectDelete) UseKey(key ecdsa.PrivateKey) {
|
func (x *PrmObjectDelete) WithXHeaders(hs ...string) {
|
||||||
prm.Key = &key
|
if len(hs)%2 != 0 {
|
||||||
|
panic("slice of X-Headers with odd length")
|
||||||
|
}
|
||||||
|
|
||||||
|
prmCommonMeta{xHeaders: hs}.writeToMetaHeader(&x.meta)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResObjectDelete groups resulting values of ObjectDelete operation.
|
// ResObjectDelete groups resulting values of ObjectDelete operation.
|
||||||
type ResObjectDelete struct {
|
type ResObjectDelete struct {
|
||||||
statusRes
|
statusRes
|
||||||
|
|
||||||
tomb oid.ID
|
idTomb *v2refs.ObjectID
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tombstone returns identifier of the created tombstone object.
|
// ReadTombstoneID reads identifier of the created tombstone object.
|
||||||
func (x ResObjectDelete) Tombstone() oid.ID {
|
// Returns false if ID is missing (not read).
|
||||||
return x.tomb
|
func (x ResObjectDelete) ReadTombstoneID(dst *oid.ID) bool {
|
||||||
|
if x.idTomb != nil {
|
||||||
|
*dst = *oid.NewIDFromV2(x.idTomb) // need smth better
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (prm *PrmObjectDelete) buildRequest(c *Client) (*v2object.DeleteRequest, error) {
|
// ObjectDelete marks an object for deletion from the container using NeoFS API protocol.
|
||||||
if prm.ContainerID == nil {
|
|
||||||
return nil, errorMissingContainer
|
|
||||||
}
|
|
||||||
|
|
||||||
if prm.ObjectID == nil {
|
|
||||||
return nil, errorMissingObject
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(prm.XHeaders)%2 != 0 {
|
|
||||||
return nil, errorInvalidXHeaders
|
|
||||||
}
|
|
||||||
|
|
||||||
meta := new(v2session.RequestMetaHeader)
|
|
||||||
writeXHeadersToMeta(prm.XHeaders, meta)
|
|
||||||
|
|
||||||
if prm.BearerToken != nil {
|
|
||||||
v2BearerToken := new(acl.BearerToken)
|
|
||||||
prm.BearerToken.WriteToV2(v2BearerToken)
|
|
||||||
meta.SetBearerToken(v2BearerToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
if prm.Session != nil {
|
|
||||||
v2SessionToken := new(v2session.Token)
|
|
||||||
prm.Session.WriteToV2(v2SessionToken)
|
|
||||||
meta.SetSessionToken(v2SessionToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
addr := new(v2refs.Address)
|
|
||||||
|
|
||||||
cnrV2 := new(v2refs.ContainerID)
|
|
||||||
prm.ContainerID.WriteToV2(cnrV2)
|
|
||||||
addr.SetContainerID(cnrV2)
|
|
||||||
|
|
||||||
objV2 := new(v2refs.ObjectID)
|
|
||||||
prm.ObjectID.WriteToV2(objV2)
|
|
||||||
addr.SetObjectID(objV2)
|
|
||||||
|
|
||||||
body := new(v2object.DeleteRequestBody)
|
|
||||||
body.SetAddress(addr)
|
|
||||||
|
|
||||||
req := new(v2object.DeleteRequest)
|
|
||||||
req.SetBody(body)
|
|
||||||
c.prepareRequest(req, meta)
|
|
||||||
|
|
||||||
return req, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ObjectDelete marks an object for deletion from the container using FrostFS API protocol.
|
|
||||||
// As a marker, a special unit called a tombstone is placed in the container.
|
// As a marker, a special unit called a tombstone is placed in the container.
|
||||||
// It confirms the user's intent to delete the object, and is itself a container object.
|
// It confirms the user's intent to delete the object, and is itself a container object.
|
||||||
// Explicit deletion is done asynchronously, and is generally not guaranteed.
|
// Explicit deletion is done asynchronously, and is generally not guaranteed.
|
||||||
|
@ -112,11 +105,11 @@ func (prm *PrmObjectDelete) buildRequest(c *Client) (*v2object.DeleteRequest, er
|
||||||
//
|
//
|
||||||
// Exactly one return value is non-nil. By default, server status is returned in res structure.
|
// Exactly one return value is non-nil. By default, server status is returned in res structure.
|
||||||
// Any client's internal or transport errors are returned as `error`,
|
// Any client's internal or transport errors are returned as `error`,
|
||||||
// If PrmInit.DisableFrostFSFailuresResolution has been called, unsuccessful
|
// If WithNeoFSErrorParsing option has been provided, unsuccessful
|
||||||
// FrostFS status codes are included in the returned result structure,
|
// NeoFS status codes are returned as `error`, otherwise, are included
|
||||||
// otherwise, are also returned as `error`.
|
// in the returned result structure.
|
||||||
//
|
//
|
||||||
// Returns an error if parameters are set incorrectly (see PrmObjectDelete docs).
|
// Immediately panics if parameters are set incorrectly (see PrmObjectDelete docs).
|
||||||
// Context is required and must not be nil. It is used for network communication.
|
// Context is required and must not be nil. It is used for network communication.
|
||||||
//
|
//
|
||||||
// Return statuses:
|
// Return statuses:
|
||||||
|
@ -126,46 +119,48 @@ func (prm *PrmObjectDelete) buildRequest(c *Client) (*v2object.DeleteRequest, er
|
||||||
// - *apistatus.ObjectLocked;
|
// - *apistatus.ObjectLocked;
|
||||||
// - *apistatus.SessionTokenExpired.
|
// - *apistatus.SessionTokenExpired.
|
||||||
func (c *Client) ObjectDelete(ctx context.Context, prm PrmObjectDelete) (*ResObjectDelete, error) {
|
func (c *Client) ObjectDelete(ctx context.Context, prm PrmObjectDelete) (*ResObjectDelete, error) {
|
||||||
req, err := prm.buildRequest(c)
|
switch {
|
||||||
if err != nil {
|
case ctx == nil:
|
||||||
return nil, err
|
panic(panicMsgMissingContext)
|
||||||
|
case prm.addr.GetContainerID() == nil:
|
||||||
|
panic(panicMsgMissingContainer)
|
||||||
|
case prm.addr.GetObjectID() == nil:
|
||||||
|
panic("missing object")
|
||||||
}
|
}
|
||||||
|
|
||||||
key := c.prm.Key
|
// form request body
|
||||||
if prm.Key != nil {
|
prm.body.SetAddress(&prm.addr)
|
||||||
key = *prm.Key
|
|
||||||
|
// form request
|
||||||
|
var req v2object.DeleteRequest
|
||||||
|
req.SetBody(&prm.body)
|
||||||
|
req.SetMetaHeader(&prm.meta)
|
||||||
|
|
||||||
|
// init call context
|
||||||
|
var (
|
||||||
|
cc contextCall
|
||||||
|
res ResObjectDelete
|
||||||
|
)
|
||||||
|
|
||||||
|
if prm.keySet {
|
||||||
|
c.initCallContextWithoutKey(&cc)
|
||||||
|
cc.key = prm.key
|
||||||
|
} else {
|
||||||
|
c.initCallContext(&cc)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = signature.SignServiceMessage(&key, req)
|
cc.req = &req
|
||||||
if err != nil {
|
cc.statusRes = &res
|
||||||
return nil, fmt.Errorf("sign request: %w", err)
|
cc.call = func() (responseV2, error) {
|
||||||
|
return rpcapi.DeleteObject(&c.c, &req, client.WithContext(ctx))
|
||||||
|
}
|
||||||
|
cc.result = func(r responseV2) {
|
||||||
|
res.idTomb = r.(*v2object.DeleteResponse).GetBody().GetTombstone().GetObjectID()
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := rpcapi.DeleteObject(&c.c, req, client.WithContext(ctx))
|
// process call
|
||||||
if err != nil {
|
if !cc.processCall() {
|
||||||
return nil, err
|
return nil, cc.err
|
||||||
}
|
|
||||||
|
|
||||||
var res ResObjectDelete
|
|
||||||
res.st, err = c.processResponse(resp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !apistatus.IsSuccessful(res.st) {
|
|
||||||
return &res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldTombstone = "tombstone"
|
|
||||||
|
|
||||||
idTombV2 := resp.GetBody().GetTombstone().GetObjectID()
|
|
||||||
if idTombV2 == nil {
|
|
||||||
return nil, newErrMissingResponseField(fieldTombstone)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = res.tomb.ReadFromV2(*idTombV2)
|
|
||||||
if err != nil {
|
|
||||||
return nil, newErrInvalidResponseField(fieldTombstone, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &res, nil
|
return &res, nil
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -2,64 +2,110 @@ package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/ecdsa"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl"
|
v2object "github.com/nspcc-dev/neofs-api-go/v2/object"
|
||||||
v2object "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
|
v2refs "github.com/nspcc-dev/neofs-api-go/v2/refs"
|
||||||
v2refs "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
|
||||||
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
|
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
|
||||||
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
|
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
|
"github.com/nspcc-dev/neofs-sdk-go/session"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/checksum"
|
"github.com/nspcc-dev/neofs-sdk-go/token"
|
||||||
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"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// PrmObjectHash groups parameters of ObjectHash operation.
|
// PrmObjectHash groups parameters of ObjectHash operation.
|
||||||
type PrmObjectHash struct {
|
type PrmObjectHash struct {
|
||||||
XHeaders []string
|
meta v2session.RequestMetaHeader
|
||||||
|
|
||||||
BearerToken *bearer.Token
|
body v2object.GetRangeHashRequestBody
|
||||||
|
|
||||||
Session *session.Object
|
tillichZemor bool
|
||||||
|
|
||||||
Local bool
|
addr v2refs.Address
|
||||||
|
|
||||||
Ranges []object.Range
|
|
||||||
|
|
||||||
Salt []byte
|
|
||||||
|
|
||||||
ChecksumType checksum.Type
|
|
||||||
|
|
||||||
ContainerID *cid.ID
|
|
||||||
|
|
||||||
ObjectID *oid.ID
|
|
||||||
|
|
||||||
Key *ecdsa.PrivateKey
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UseKey specifies private key to sign the requests.
|
// MarkLocal tells the server to execute the operation locally.
|
||||||
// If key is not provided, then Client default key is used.
|
func (x *PrmObjectHash) MarkLocal() {
|
||||||
|
x.meta.SetTTL(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithinSession specifies session within which object should be read.
|
||||||
//
|
//
|
||||||
// Deprecated: Use PrmObjectHash.Key instead.
|
// Creator of the session acquires the authorship of the request.
|
||||||
func (prm *PrmObjectHash) UseKey(key ecdsa.PrivateKey) {
|
// This may affect the execution of an operation (e.g. access control).
|
||||||
prm.Key = &key
|
//
|
||||||
|
// Must be signed.
|
||||||
|
func (x *PrmObjectHash) WithinSession(t session.Token) {
|
||||||
|
x.meta.SetSessionToken(t.ToV2())
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithBearerToken attaches bearer token to be used for the operation.
|
||||||
|
//
|
||||||
|
// If set, underlying eACL rules will be used in access control.
|
||||||
|
//
|
||||||
|
// Must be signed.
|
||||||
|
func (x *PrmObjectHash) WithBearerToken(t token.BearerToken) {
|
||||||
|
x.meta.SetBearerToken(t.ToV2())
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromContainer specifies NeoFS container of the object.
|
||||||
|
// Required parameter.
|
||||||
|
func (x *PrmObjectHash) FromContainer(id cid.ID) {
|
||||||
|
x.addr.SetContainerID(id.ToV2())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByID specifies identifier of the requested object.
|
||||||
|
// Required parameter.
|
||||||
|
func (x *PrmObjectHash) ByID(id oid.ID) {
|
||||||
|
x.addr.SetObjectID(id.ToV2())
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRangeList sets list of ranges in (offset, length) pair format.
|
||||||
|
// Required parameter.
|
||||||
|
//
|
||||||
|
// If passed as slice, then it must not be mutated before the operation completes.
|
||||||
|
func (x *PrmObjectHash) SetRangeList(r ...uint64) {
|
||||||
|
ln := len(r)
|
||||||
|
if ln%2 != 0 {
|
||||||
|
panic("odd number of range parameters")
|
||||||
|
}
|
||||||
|
|
||||||
|
rs := make([]v2object.Range, ln/2)
|
||||||
|
|
||||||
|
for i := 0; i < ln/2; i++ {
|
||||||
|
rs[i].SetOffset(r[2*i])
|
||||||
|
rs[i].SetLength(r[2*i+1])
|
||||||
|
}
|
||||||
|
|
||||||
|
x.body.SetRanges(rs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TillichZemorAlgo changes the hash function to Tillich-Zemor
|
// TillichZemorAlgo changes the hash function to Tillich-Zemor
|
||||||
// (https://link.springer.com/content/pdf/10.1007/3-540-48658-5_5.pdf).
|
// (https://link.springer.com/content/pdf/10.1007/3-540-48658-5_5.pdf).
|
||||||
//
|
//
|
||||||
// By default, SHA256 hash function is used/.
|
// By default, SHA256 hash function is used.
|
||||||
|
func (x *PrmObjectHash) TillichZemorAlgo() {
|
||||||
|
x.tillichZemor = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseSalt sets the salt to XOR the data range before hashing.
|
||||||
//
|
//
|
||||||
// Deprecated: Use PrmObjectHash.ChecksumType instead.
|
// Must not be mutated before the operation completes.
|
||||||
func (prm *PrmObjectHash) TillichZemorAlgo() {
|
func (x *PrmObjectHash) UseSalt(salt []byte) {
|
||||||
prm.ChecksumType = checksum.TZ
|
x.body.SetSalt(salt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithXHeaders specifies list of extended headers (string key-value pairs)
|
||||||
|
// to be attached to the request. Must have an even length.
|
||||||
|
//
|
||||||
|
// Slice must not be mutated until the operation completes.
|
||||||
|
func (x *PrmObjectHash) WithXHeaders(hs ...string) {
|
||||||
|
if len(hs)%2 != 0 {
|
||||||
|
panic("slice of X-Headers with odd length")
|
||||||
|
}
|
||||||
|
|
||||||
|
prmCommonMeta{xHeaders: hs}.writeToMetaHeader(&x.meta)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResObjectHash groups resulting values of ObjectHash operation.
|
// ResObjectHash groups resulting values of ObjectHash operation.
|
||||||
|
@ -74,89 +120,19 @@ func (x ResObjectHash) Checksums() [][]byte {
|
||||||
return x.checksums
|
return x.checksums
|
||||||
}
|
}
|
||||||
|
|
||||||
func (prm *PrmObjectHash) buildRequest(c *Client) (*v2object.GetRangeHashRequest, error) {
|
|
||||||
if prm.ContainerID == nil {
|
|
||||||
return nil, errorMissingContainer
|
|
||||||
}
|
|
||||||
|
|
||||||
if prm.ObjectID == nil {
|
|
||||||
return nil, errorMissingObject
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(prm.XHeaders)%2 != 0 {
|
|
||||||
return nil, errorInvalidXHeaders
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(prm.Ranges) == 0 {
|
|
||||||
return nil, errorMissingRanges
|
|
||||||
}
|
|
||||||
|
|
||||||
meta := new(v2session.RequestMetaHeader)
|
|
||||||
writeXHeadersToMeta(prm.XHeaders, meta)
|
|
||||||
|
|
||||||
if prm.BearerToken != nil {
|
|
||||||
v2BearerToken := new(acl.BearerToken)
|
|
||||||
prm.BearerToken.WriteToV2(v2BearerToken)
|
|
||||||
meta.SetBearerToken(v2BearerToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
if prm.Session != nil {
|
|
||||||
v2SessionToken := new(v2session.Token)
|
|
||||||
prm.Session.WriteToV2(v2SessionToken)
|
|
||||||
meta.SetSessionToken(v2SessionToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
if prm.Local {
|
|
||||||
meta.SetTTL(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
addr := new(v2refs.Address)
|
|
||||||
|
|
||||||
cnrV2 := new(v2refs.ContainerID)
|
|
||||||
prm.ContainerID.WriteToV2(cnrV2)
|
|
||||||
addr.SetContainerID(cnrV2)
|
|
||||||
|
|
||||||
objV2 := new(v2refs.ObjectID)
|
|
||||||
prm.ObjectID.WriteToV2(objV2)
|
|
||||||
addr.SetObjectID(objV2)
|
|
||||||
|
|
||||||
rs := make([]v2object.Range, len(prm.Ranges))
|
|
||||||
for i := range prm.Ranges {
|
|
||||||
rs[i].SetOffset(prm.Ranges[i].GetOffset())
|
|
||||||
rs[i].SetLength(prm.Ranges[i].GetLength())
|
|
||||||
}
|
|
||||||
|
|
||||||
body := new(v2object.GetRangeHashRequestBody)
|
|
||||||
body.SetAddress(addr)
|
|
||||||
body.SetRanges(rs)
|
|
||||||
body.SetSalt(prm.Salt)
|
|
||||||
|
|
||||||
if prm.ChecksumType == checksum.Unknown {
|
|
||||||
body.SetType(v2refs.SHA256)
|
|
||||||
} else {
|
|
||||||
body.SetType(v2refs.ChecksumType(prm.ChecksumType))
|
|
||||||
}
|
|
||||||
|
|
||||||
req := new(v2object.GetRangeHashRequest)
|
|
||||||
req.SetBody(body)
|
|
||||||
c.prepareRequest(req, meta)
|
|
||||||
|
|
||||||
return req, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ObjectHash requests checksum of the range list of the object payload using
|
// ObjectHash requests checksum of the range list of the object payload using
|
||||||
// FrostFS API protocol.
|
// NeoFS API protocol.
|
||||||
//
|
//
|
||||||
// Returns a list of checksums in raw form: the format of hashes and their number
|
// Returns a list of checksums in raw form: the format of hashes and their number
|
||||||
// is left for the caller to check. Client preserves the order of the server's response.
|
// is left for the caller to check. Client preserves the order of the server's response.
|
||||||
//
|
//
|
||||||
// Exactly one return value is non-nil. By default, server status is returned in res structure.
|
// Exactly one return value is non-nil. By default, server status is returned in res structure.
|
||||||
// Any client's internal or transport errors are returned as `error`,
|
// Any client's internal or transport errors are returned as `error`,
|
||||||
// If PrmInit.DisableFrostFSFailuresResolution has been called, unsuccessful
|
// If WithNeoFSErrorParsing option has been provided, unsuccessful
|
||||||
// FrostFS status codes are included in the returned result structure,
|
// NeoFS status codes are returned as `error`, otherwise, are included
|
||||||
// otherwise, are also returned as `error`.
|
// in the returned result structure.
|
||||||
//
|
//
|
||||||
// Returns an error if parameters are set incorrectly (see PrmObjectHash docs).
|
// Immediately panics if parameters are set incorrectly (see PrmObjectHash docs).
|
||||||
// Context is required and must not be nil. It is used for network communication.
|
// Context is required and must not be nil. It is used for network communication.
|
||||||
//
|
//
|
||||||
// Return statuses:
|
// Return statuses:
|
||||||
|
@ -164,42 +140,53 @@ func (prm *PrmObjectHash) buildRequest(c *Client) (*v2object.GetRangeHashRequest
|
||||||
// - *apistatus.ContainerNotFound;
|
// - *apistatus.ContainerNotFound;
|
||||||
// - *apistatus.ObjectNotFound;
|
// - *apistatus.ObjectNotFound;
|
||||||
// - *apistatus.ObjectAccessDenied;
|
// - *apistatus.ObjectAccessDenied;
|
||||||
// - *apistatus.ObjectOutOfRange;
|
|
||||||
// - *apistatus.SessionTokenExpired.
|
// - *apistatus.SessionTokenExpired.
|
||||||
func (c *Client) ObjectHash(ctx context.Context, prm PrmObjectHash) (*ResObjectHash, error) {
|
func (c *Client) ObjectHash(ctx context.Context, prm PrmObjectHash) (*ResObjectHash, error) {
|
||||||
req, err := prm.buildRequest(c)
|
switch {
|
||||||
if err != nil {
|
case ctx == nil:
|
||||||
return nil, err
|
panic(panicMsgMissingContext)
|
||||||
|
case prm.addr.GetContainerID() == nil:
|
||||||
|
panic(panicMsgMissingContainer)
|
||||||
|
case prm.addr.GetObjectID() == nil:
|
||||||
|
panic("missing object")
|
||||||
|
case len(prm.body.GetRanges()) == 0:
|
||||||
|
panic("missing ranges")
|
||||||
}
|
}
|
||||||
|
|
||||||
key := c.prm.Key
|
// form request body
|
||||||
if prm.Key != nil {
|
prm.body.SetAddress(&prm.addr)
|
||||||
key = *prm.Key
|
// ranges and salt are already by prm setters
|
||||||
|
|
||||||
|
if prm.tillichZemor {
|
||||||
|
prm.body.SetType(v2refs.TillichZemor)
|
||||||
|
} else {
|
||||||
|
prm.body.SetType(v2refs.SHA256)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = signature.SignServiceMessage(&key, req)
|
// form request
|
||||||
if err != nil {
|
var req v2object.GetRangeHashRequest
|
||||||
return nil, fmt.Errorf("sign request: %w", err)
|
req.SetBody(&prm.body)
|
||||||
|
req.SetMetaHeader(&prm.meta)
|
||||||
|
|
||||||
|
// init call context
|
||||||
|
var (
|
||||||
|
cc contextCall
|
||||||
|
res ResObjectHash
|
||||||
|
)
|
||||||
|
|
||||||
|
c.initCallContext(&cc)
|
||||||
|
cc.req = &req
|
||||||
|
cc.statusRes = &res
|
||||||
|
cc.call = func() (responseV2, error) {
|
||||||
|
return rpcapi.HashObjectRange(&c.c, &req, client.WithContext(ctx))
|
||||||
|
}
|
||||||
|
cc.result = func(r responseV2) {
|
||||||
|
res.checksums = r.(*v2object.GetRangeHashResponse).GetBody().GetHashList()
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := rpcapi.HashObjectRange(&c.c, req, client.WithContext(ctx))
|
// process call
|
||||||
if err != nil {
|
if !cc.processCall() {
|
||||||
return nil, fmt.Errorf("write request: %w", err)
|
return nil, cc.err
|
||||||
}
|
|
||||||
|
|
||||||
var res ResObjectHash
|
|
||||||
res.st, err = c.processResponse(resp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !apistatus.IsSuccessful(res.st) {
|
|
||||||
return &res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
res.checksums = resp.GetBody().GetHashList()
|
|
||||||
if len(res.checksums) == 0 {
|
|
||||||
return nil, newErrMissingResponseField("hash list")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &res, nil
|
return &res, nil
|
||||||
|
|
|
@ -1,270 +0,0 @@
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl"
|
|
||||||
v2object "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
|
|
||||||
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
|
||||||
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
|
|
||||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
|
||||||
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ObjectPatcher is designed to patch an object.
|
|
||||||
//
|
|
||||||
// Must be initialized using Client.ObjectPatchInit, any other
|
|
||||||
// usage is unsafe.
|
|
||||||
type ObjectPatcher interface {
|
|
||||||
// PatchAttributes patches attributes. Attributes can be patched no more than once,
|
|
||||||
// otherwise, the server returns an error.
|
|
||||||
//
|
|
||||||
// Result means success. Failure reason can be received via Close.
|
|
||||||
PatchAttributes(ctx context.Context, newAttrs []object.Attribute, replace bool) bool
|
|
||||||
|
|
||||||
// PatchPayload patches the object's payload.
|
|
||||||
//
|
|
||||||
// PatchPayload receives `payloadReader` and thus the payload of the patch is read and sent by chunks of
|
|
||||||
// `MaxChunkLength` length.
|
|
||||||
//
|
|
||||||
// Result means success. Failure reason can be received via Close.
|
|
||||||
PatchPayload(ctx context.Context, rng *object.Range, payloadReader io.Reader) bool
|
|
||||||
|
|
||||||
// Close ends patching the object and returns the result of the operation
|
|
||||||
// along with the final results. Must be called after using the ObjectPatcher.
|
|
||||||
//
|
|
||||||
// Exactly one return value is non-nil. By default, server status is returned in res structure.
|
|
||||||
// Any client's internal or transport errors are returned as Go built-in error.
|
|
||||||
// If Client is tuned to resolve FrostFS API statuses, then FrostFS failures
|
|
||||||
// codes are returned as error.
|
|
||||||
//
|
|
||||||
// Return statuses:
|
|
||||||
// - global (see Client docs);
|
|
||||||
// - *apistatus.ContainerNotFound;
|
|
||||||
// - *apistatus.ContainerAccessDenied;
|
|
||||||
// - *apistatus.ObjectAccessDenied;
|
|
||||||
// - *apistatus.ObjectAlreadyRemoved;
|
|
||||||
// - *apistatus.ObjectLocked;
|
|
||||||
// - *apistatus.ObjectOutOfRange;
|
|
||||||
// - *apistatus.SessionTokenNotFound;
|
|
||||||
// - *apistatus.SessionTokenExpired.
|
|
||||||
Close(_ context.Context) (*ResObjectPatch, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResObjectPatch groups resulting values of ObjectPatch operation.
|
|
||||||
type ResObjectPatch struct {
|
|
||||||
statusRes
|
|
||||||
|
|
||||||
obj oid.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
// ObjectID returns an object ID of the patched object.
|
|
||||||
func (r ResObjectPatch) ObjectID() oid.ID {
|
|
||||||
return r.obj
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrmObjectPatch groups parameters of ObjectPatch operation.
|
|
||||||
type PrmObjectPatch struct {
|
|
||||||
XHeaders []string
|
|
||||||
|
|
||||||
Address oid.Address
|
|
||||||
|
|
||||||
BearerToken *bearer.Token
|
|
||||||
|
|
||||||
Session *session.Object
|
|
||||||
|
|
||||||
Key *ecdsa.PrivateKey
|
|
||||||
|
|
||||||
MaxChunkLength int
|
|
||||||
}
|
|
||||||
|
|
||||||
// ObjectPatchInit initializes object patcher.
|
|
||||||
func (c *Client) ObjectPatchInit(ctx context.Context, prm PrmObjectPatch) (ObjectPatcher, error) {
|
|
||||||
if len(prm.XHeaders)%2 != 0 {
|
|
||||||
return nil, errorInvalidXHeaders
|
|
||||||
}
|
|
||||||
|
|
||||||
var objectPatcher objectPatcher
|
|
||||||
stream, err := rpcapi.Patch(&c.c, &objectPatcher.respV2, client.WithContext(ctx))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("open stream: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
objectPatcher.addr = prm.Address
|
|
||||||
objectPatcher.key = &c.prm.Key
|
|
||||||
if prm.Key != nil {
|
|
||||||
objectPatcher.key = prm.Key
|
|
||||||
}
|
|
||||||
objectPatcher.client = c
|
|
||||||
objectPatcher.stream = stream
|
|
||||||
|
|
||||||
if prm.MaxChunkLength > 0 {
|
|
||||||
objectPatcher.maxChunkLen = prm.MaxChunkLength
|
|
||||||
} else {
|
|
||||||
objectPatcher.maxChunkLen = defaultGRPCPayloadChunkLen
|
|
||||||
}
|
|
||||||
|
|
||||||
objectPatcher.req.SetBody(&v2object.PatchRequestBody{})
|
|
||||||
|
|
||||||
meta := new(v2session.RequestMetaHeader)
|
|
||||||
writeXHeadersToMeta(prm.XHeaders, meta)
|
|
||||||
|
|
||||||
if prm.BearerToken != nil {
|
|
||||||
v2BearerToken := new(acl.BearerToken)
|
|
||||||
prm.BearerToken.WriteToV2(v2BearerToken)
|
|
||||||
meta.SetBearerToken(v2BearerToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
if prm.Session != nil {
|
|
||||||
v2SessionToken := new(v2session.Token)
|
|
||||||
prm.Session.WriteToV2(v2SessionToken)
|
|
||||||
meta.SetSessionToken(v2SessionToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.prepareRequest(&objectPatcher.req, meta)
|
|
||||||
|
|
||||||
return &objectPatcher, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type objectPatcher struct {
|
|
||||||
client *Client
|
|
||||||
|
|
||||||
stream interface {
|
|
||||||
Write(*v2object.PatchRequest) error
|
|
||||||
Close() error
|
|
||||||
}
|
|
||||||
|
|
||||||
key *ecdsa.PrivateKey
|
|
||||||
res ResObjectPatch
|
|
||||||
err error
|
|
||||||
|
|
||||||
addr oid.Address
|
|
||||||
|
|
||||||
req v2object.PatchRequest
|
|
||||||
respV2 v2object.PatchResponse
|
|
||||||
|
|
||||||
maxChunkLen int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *objectPatcher) PatchAttributes(_ context.Context, newAttrs []object.Attribute, replace bool) bool {
|
|
||||||
return x.patch(&object.Patch{
|
|
||||||
Address: x.addr,
|
|
||||||
NewAttributes: newAttrs,
|
|
||||||
ReplaceAttributes: replace,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *objectPatcher) PatchPayload(_ context.Context, rng *object.Range, payloadReader io.Reader) bool {
|
|
||||||
offset := rng.GetOffset()
|
|
||||||
|
|
||||||
buf := make([]byte, x.maxChunkLen)
|
|
||||||
|
|
||||||
for patchIter := 0; ; patchIter++ {
|
|
||||||
n, err := payloadReader.Read(buf)
|
|
||||||
if err != nil && err != io.EOF {
|
|
||||||
x.err = fmt.Errorf("read payload: %w", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if n == 0 {
|
|
||||||
if patchIter == 0 {
|
|
||||||
if rng.GetLength() == 0 {
|
|
||||||
x.err = errors.New("zero-length empty payload patch can't be applied")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if !x.patch(&object.Patch{
|
|
||||||
Address: x.addr,
|
|
||||||
PayloadPatch: &object.PayloadPatch{
|
|
||||||
Range: rng,
|
|
||||||
Chunk: []byte{},
|
|
||||||
},
|
|
||||||
}) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
rngPart := object.NewRange()
|
|
||||||
if patchIter == 0 {
|
|
||||||
rngPart.SetOffset(offset)
|
|
||||||
rngPart.SetLength(rng.GetLength())
|
|
||||||
} else {
|
|
||||||
rngPart.SetOffset(offset + rng.GetLength())
|
|
||||||
}
|
|
||||||
|
|
||||||
if !x.patch(&object.Patch{
|
|
||||||
Address: x.addr,
|
|
||||||
PayloadPatch: &object.PayloadPatch{
|
|
||||||
Range: rngPart,
|
|
||||||
Chunk: buf[:n],
|
|
||||||
},
|
|
||||||
}) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if err == io.EOF {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *objectPatcher) patch(patch *object.Patch) bool {
|
|
||||||
x.req.SetBody(patch.ToV2())
|
|
||||||
x.req.SetVerificationHeader(nil)
|
|
||||||
|
|
||||||
x.err = signature.SignServiceMessage(x.key, &x.req)
|
|
||||||
if x.err != nil {
|
|
||||||
x.err = fmt.Errorf("sign message: %w", x.err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
x.err = x.stream.Write(&x.req)
|
|
||||||
return x.err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *objectPatcher) Close(_ context.Context) (*ResObjectPatch, error) {
|
|
||||||
// Ignore io.EOF error, because it is expected error for client-side
|
|
||||||
// stream termination by the server. E.g. when stream contains invalid
|
|
||||||
// message. Server returns an error in response message (in status).
|
|
||||||
if x.err != nil && !errors.Is(x.err, io.EOF) {
|
|
||||||
return nil, x.err
|
|
||||||
}
|
|
||||||
|
|
||||||
if x.err = x.stream.Close(); x.err != nil {
|
|
||||||
return nil, x.err
|
|
||||||
}
|
|
||||||
|
|
||||||
x.res.st, x.err = x.client.processResponse(&x.respV2)
|
|
||||||
if x.err != nil {
|
|
||||||
return nil, x.err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !apistatus.IsSuccessful(x.res.st) {
|
|
||||||
return &x.res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldID = "ID"
|
|
||||||
|
|
||||||
idV2 := x.respV2.Body.ObjectID
|
|
||||||
if idV2 == nil {
|
|
||||||
return nil, newErrMissingResponseField(fieldID)
|
|
||||||
}
|
|
||||||
|
|
||||||
x.err = x.res.obj.ReadFromV2(*idV2)
|
|
||||||
if x.err != nil {
|
|
||||||
x.err = newErrInvalidResponseField(fieldID, x.err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &x.res, nil
|
|
||||||
}
|
|
|
@ -1,295 +0,0 @@
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/elliptic"
|
|
||||||
"crypto/rand"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
v2object "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
|
||||||
oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
type mockPatchStream struct {
|
|
||||||
streamedPayloadPatches []*object.PayloadPatch
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockPatchStream) Write(r *v2object.PatchRequest) error {
|
|
||||||
pp := new(object.PayloadPatch)
|
|
||||||
pp.FromV2(r.GetBody().GetPatch())
|
|
||||||
|
|
||||||
if r.GetBody().GetPatch() != nil {
|
|
||||||
bodyChunk := r.GetBody().GetPatch().Chunk
|
|
||||||
pp.Chunk = make([]byte, len(bodyChunk))
|
|
||||||
copy(pp.Chunk, bodyChunk)
|
|
||||||
}
|
|
||||||
|
|
||||||
m.streamedPayloadPatches = append(m.streamedPayloadPatches, pp)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockPatchStream) Close() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestObjectPatcher(t *testing.T) {
|
|
||||||
type part struct {
|
|
||||||
offset int
|
|
||||||
length int
|
|
||||||
chunk string
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range []struct {
|
|
||||||
name string
|
|
||||||
patchPayload string
|
|
||||||
rng *object.Range
|
|
||||||
maxChunkLen int
|
|
||||||
expectParts []part
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "no split payload patch",
|
|
||||||
patchPayload: "011111",
|
|
||||||
rng: newRange(0, 6),
|
|
||||||
maxChunkLen: defaultGRPCPayloadChunkLen,
|
|
||||||
expectParts: []part{
|
|
||||||
{
|
|
||||||
offset: 0,
|
|
||||||
length: 6,
|
|
||||||
chunk: "011111",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "splitted payload patch",
|
|
||||||
patchPayload: "012345",
|
|
||||||
rng: newRange(0, 6),
|
|
||||||
maxChunkLen: 2,
|
|
||||||
expectParts: []part{
|
|
||||||
{
|
|
||||||
offset: 0,
|
|
||||||
length: 6,
|
|
||||||
chunk: "01",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
offset: 6,
|
|
||||||
length: 0,
|
|
||||||
chunk: "23",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
offset: 6,
|
|
||||||
length: 0,
|
|
||||||
chunk: "45",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "splitted payload patch with zero-length subpatches",
|
|
||||||
patchPayload: "0123456789!@",
|
|
||||||
rng: newRange(0, 4),
|
|
||||||
maxChunkLen: 2,
|
|
||||||
expectParts: []part{
|
|
||||||
{
|
|
||||||
offset: 0,
|
|
||||||
length: 4,
|
|
||||||
chunk: "01",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
offset: 4,
|
|
||||||
length: 0,
|
|
||||||
chunk: "23",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
offset: 4,
|
|
||||||
length: 0,
|
|
||||||
chunk: "45",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
offset: 4,
|
|
||||||
length: 0,
|
|
||||||
chunk: "67",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
offset: 4,
|
|
||||||
length: 0,
|
|
||||||
chunk: "89",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
offset: 4,
|
|
||||||
length: 0,
|
|
||||||
chunk: "!@",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "splitted payload patch with zero-length subpatches only",
|
|
||||||
patchPayload: "0123456789!@",
|
|
||||||
rng: newRange(0, 0),
|
|
||||||
maxChunkLen: 2,
|
|
||||||
expectParts: []part{
|
|
||||||
{
|
|
||||||
offset: 0,
|
|
||||||
length: 0,
|
|
||||||
chunk: "01",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
offset: 0,
|
|
||||||
length: 0,
|
|
||||||
chunk: "23",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
offset: 0,
|
|
||||||
length: 0,
|
|
||||||
chunk: "45",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
offset: 0,
|
|
||||||
length: 0,
|
|
||||||
chunk: "67",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
offset: 0,
|
|
||||||
length: 0,
|
|
||||||
chunk: "89",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
offset: 0,
|
|
||||||
length: 0,
|
|
||||||
chunk: "!@",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
m := &mockPatchStream{}
|
|
||||||
|
|
||||||
pk, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
||||||
|
|
||||||
patcher := objectPatcher{
|
|
||||||
client: &Client{},
|
|
||||||
stream: m,
|
|
||||||
addr: oidtest.Address(),
|
|
||||||
key: pk,
|
|
||||||
maxChunkLen: test.maxChunkLen,
|
|
||||||
}
|
|
||||||
|
|
||||||
success := patcher.PatchAttributes(context.Background(), nil, false)
|
|
||||||
require.True(t, success)
|
|
||||||
|
|
||||||
success = patcher.PatchPayload(context.Background(), test.rng, bytes.NewReader([]byte(test.patchPayload)))
|
|
||||||
require.True(t, success)
|
|
||||||
|
|
||||||
require.Len(t, m.streamedPayloadPatches, len(test.expectParts)+1)
|
|
||||||
|
|
||||||
// m.streamedPayloadPatches[0] is attribute patch, so skip it
|
|
||||||
for i, part := range test.expectParts {
|
|
||||||
requireRangeChunk(t, m.streamedPayloadPatches[i+1], part.offset, part.length, part.chunk)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRepeatPayloadPatch(t *testing.T) {
|
|
||||||
t.Run("no payload patch partioning", func(t *testing.T) {
|
|
||||||
m := &mockPatchStream{}
|
|
||||||
|
|
||||||
pk, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
||||||
|
|
||||||
const maxChunkLen = 20
|
|
||||||
|
|
||||||
patcher := objectPatcher{
|
|
||||||
client: &Client{},
|
|
||||||
stream: m,
|
|
||||||
addr: oidtest.Address(),
|
|
||||||
key: pk,
|
|
||||||
maxChunkLen: maxChunkLen,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, pp := range []struct {
|
|
||||||
patchPayload string
|
|
||||||
rng *object.Range
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
patchPayload: "xxxxxxxxxx",
|
|
||||||
rng: newRange(1, 6),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patchPayload: "yyyyyyyyyy",
|
|
||||||
rng: newRange(5, 9),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patchPayload: "zzzzzzzzzz",
|
|
||||||
rng: newRange(10, 0),
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
success := patcher.PatchPayload(context.Background(), pp.rng, bytes.NewReader([]byte(pp.patchPayload)))
|
|
||||||
require.True(t, success)
|
|
||||||
}
|
|
||||||
|
|
||||||
requireRangeChunk(t, m.streamedPayloadPatches[0], 1, 6, "xxxxxxxxxx")
|
|
||||||
requireRangeChunk(t, m.streamedPayloadPatches[1], 5, 9, "yyyyyyyyyy")
|
|
||||||
requireRangeChunk(t, m.streamedPayloadPatches[2], 10, 0, "zzzzzzzzzz")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("payload patch partioning", func(t *testing.T) {
|
|
||||||
m := &mockPatchStream{}
|
|
||||||
|
|
||||||
pk, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
||||||
|
|
||||||
const maxChunkLen = 5
|
|
||||||
|
|
||||||
patcher := objectPatcher{
|
|
||||||
client: &Client{},
|
|
||||||
stream: m,
|
|
||||||
addr: oidtest.Address(),
|
|
||||||
key: pk,
|
|
||||||
maxChunkLen: maxChunkLen,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, pp := range []struct {
|
|
||||||
patchPayload string
|
|
||||||
rng *object.Range
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
patchPayload: "xxxxxxxxxx",
|
|
||||||
rng: newRange(1, 6),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patchPayload: "yyyyyyyyyy",
|
|
||||||
rng: newRange(5, 9),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patchPayload: "zzzzzzzzzz",
|
|
||||||
rng: newRange(10, 0),
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
success := patcher.PatchPayload(context.Background(), pp.rng, bytes.NewReader([]byte(pp.patchPayload)))
|
|
||||||
require.True(t, success)
|
|
||||||
}
|
|
||||||
|
|
||||||
requireRangeChunk(t, m.streamedPayloadPatches[0], 1, 6, "xxxxx")
|
|
||||||
requireRangeChunk(t, m.streamedPayloadPatches[1], 7, 0, "xxxxx")
|
|
||||||
requireRangeChunk(t, m.streamedPayloadPatches[2], 5, 9, "yyyyy")
|
|
||||||
requireRangeChunk(t, m.streamedPayloadPatches[3], 14, 0, "yyyyy")
|
|
||||||
requireRangeChunk(t, m.streamedPayloadPatches[4], 10, 0, "zzzzz")
|
|
||||||
requireRangeChunk(t, m.streamedPayloadPatches[5], 10, 0, "zzzzz")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func requireRangeChunk(t *testing.T, pp *object.PayloadPatch, offset, length int, chunk string) {
|
|
||||||
require.NotNil(t, pp)
|
|
||||||
require.Equal(t, uint64(offset), pp.Range.GetOffset())
|
|
||||||
require.Equal(t, uint64(length), pp.Range.GetLength())
|
|
||||||
require.Equal(t, []byte(chunk), pp.Chunk)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newRange(offest, length uint64) *object.Range {
|
|
||||||
rng := &object.Range{}
|
|
||||||
rng.SetOffset(offest)
|
|
||||||
rng.SetLength(length)
|
|
||||||
return rng
|
|
||||||
}
|
|
|
@ -3,194 +3,231 @@ package client
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
buffPool "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/util/pool"
|
v2object "github.com/nspcc-dev/neofs-api-go/v2/object"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
|
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
|
||||||
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/transformer"
|
"github.com/nspcc-dev/neofs-sdk-go/object"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
|
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
|
||||||
|
"github.com/nspcc-dev/neofs-sdk-go/session"
|
||||||
|
"github.com/nspcc-dev/neofs-sdk-go/token"
|
||||||
)
|
)
|
||||||
|
|
||||||
// defaultGRPCPayloadChunkLen default value for maxChunkLen.
|
|
||||||
// See PrmObjectPutInit.SetGRPCPayloadChunkLen for details.
|
|
||||||
const defaultGRPCPayloadChunkLen = 3 << 20
|
|
||||||
|
|
||||||
// PrmObjectPutInit groups parameters of ObjectPutInit operation.
|
// PrmObjectPutInit groups parameters of ObjectPutInit operation.
|
||||||
type PrmObjectPutInit struct {
|
|
||||||
XHeaders []string
|
|
||||||
|
|
||||||
BearerToken *bearer.Token
|
|
||||||
|
|
||||||
Session *session.Object
|
|
||||||
|
|
||||||
Local bool
|
|
||||||
|
|
||||||
CopiesNumber []uint32
|
|
||||||
|
|
||||||
MaxChunkLength int
|
|
||||||
|
|
||||||
MaxSize uint64
|
|
||||||
|
|
||||||
EpochSource transformer.EpochSource
|
|
||||||
|
|
||||||
WithoutHomomorphHash bool
|
|
||||||
|
|
||||||
Key *ecdsa.PrivateKey
|
|
||||||
|
|
||||||
Pool *buffPool.BufferPool
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCopiesNumber sets number of object copies that is enough to consider put successful.
|
|
||||||
//
|
//
|
||||||
// Deprecated: Use PrmObjectPutInit.CopiesNumber instead.
|
// At the moment the operation is not parameterized, however,
|
||||||
func (x *PrmObjectPutInit) SetCopiesNumber(copiesNumber uint32) {
|
// the structure is still declared for backward compatibility.
|
||||||
x.CopiesNumber = []uint32{copiesNumber}
|
type PrmObjectPutInit struct{}
|
||||||
}
|
|
||||||
|
|
||||||
// SetCopiesNumberByVectors sets ordered list of minimal required object copies numbers
|
|
||||||
// per placement vector. List's length MUST equal container's placement vector number,
|
|
||||||
// otherwise request will fail.
|
|
||||||
//
|
|
||||||
// Deprecated: Use PrmObjectPutInit.CopiesNumber instead.
|
|
||||||
func (x *PrmObjectPutInit) SetCopiesNumberByVectors(copiesNumbers []uint32) {
|
|
||||||
x.CopiesNumber = copiesNumbers
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetGRPCPayloadChunkLen sets maximum chunk length value for gRPC Put request.
|
|
||||||
// Maximum chunk length restricts maximum byte length of the chunk
|
|
||||||
// transmitted in a single stream message. It depends on
|
|
||||||
// server settings and other message fields.
|
|
||||||
// If not specified or negative value set, default value of 3MiB will be used.
|
|
||||||
//
|
|
||||||
// Deprecated: Use PrmObjectPutInit.MaxChunkLength instead.
|
|
||||||
func (x *PrmObjectPutInit) SetGRPCPayloadChunkLen(v int) {
|
|
||||||
x.MaxChunkLength = v
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResObjectPut groups the final result values of ObjectPutInit operation.
|
// ResObjectPut groups the final result values of ObjectPutInit operation.
|
||||||
type ResObjectPut struct {
|
type ResObjectPut struct {
|
||||||
statusRes
|
statusRes
|
||||||
|
|
||||||
obj oid.ID
|
resp v2object.PutResponse
|
||||||
epoch uint64
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// StoredObjectID returns identifier of the saved object.
|
// ReadStoredObjectID reads identifier of the saved object.
|
||||||
func (x ResObjectPut) StoredObjectID() oid.ID {
|
// Returns false if ID is missing (not read).
|
||||||
return x.obj
|
func (x *ResObjectPut) ReadStoredObjectID(id *oid.ID) bool {
|
||||||
|
idv2 := x.resp.GetBody().GetObjectID()
|
||||||
|
if idv2 == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
*id = *oid.NewIDFromV2(idv2) // need smth better
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// StoredEpoch returns creation epoch of the saved object.
|
// ObjectWriter is designed to write one object to NeoFS system.
|
||||||
func (x ResObjectPut) StoredEpoch() uint64 {
|
|
||||||
return x.epoch
|
|
||||||
}
|
|
||||||
|
|
||||||
// ObjectWriter is designed to write one object or
|
|
||||||
// multiple parts of one object to FrostFS system.
|
|
||||||
//
|
//
|
||||||
// Must be initialized using Client.ObjectPutInit, any other
|
// Must be initialized using Client.ObjectPutInit, any other
|
||||||
// usage is unsafe.
|
// usage is unsafe.
|
||||||
type ObjectWriter interface {
|
type ObjectWriter struct {
|
||||||
// WriteHeader writes header of the object. Result means success.
|
cancelCtxStream context.CancelFunc
|
||||||
// Failure reason can be received via Close.
|
|
||||||
WriteHeader(context.Context, object.Object) bool
|
ctxCall contextCall
|
||||||
// WritePayloadChunk writes chunk of the object payload. Result means success.
|
|
||||||
// Failure reason can be received via Close.
|
// initially bound tp contextCall
|
||||||
WritePayloadChunk(context.Context, []byte) bool
|
metaHdr v2session.RequestMetaHeader
|
||||||
// Close ends writing the object and returns the result of the operation
|
|
||||||
// along with the final results. Must be called after using the ObjectWriter.
|
// initially bound to contextCall
|
||||||
//
|
partInit v2object.PutObjectPartInit
|
||||||
// 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.
|
chunkCalled bool
|
||||||
// If Client is tuned to resolve FrostFS API statuses, then FrostFS failures
|
|
||||||
// codes are returned as error.
|
partChunk v2object.PutObjectPartChunk
|
||||||
//
|
|
||||||
// Return statuses:
|
|
||||||
// - global (see Client docs);
|
|
||||||
// - *apistatus.ContainerNotFound;
|
|
||||||
// - *apistatus.ObjectAccessDenied;
|
|
||||||
// - *apistatus.ObjectLocked;
|
|
||||||
// - *apistatus.LockNonRegularObject;
|
|
||||||
// - *apistatus.SessionTokenNotFound;
|
|
||||||
// - *apistatus.SessionTokenExpired.
|
|
||||||
Close(context.Context) (*ResObjectPut, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UseKey specifies private key to sign the requests.
|
// UseKey specifies private key to sign the requests.
|
||||||
// If key is not provided, then Client default key is used.
|
// If key is not provided, then Client default key is used.
|
||||||
//
|
func (x *ObjectWriter) UseKey(key ecdsa.PrivateKey) {
|
||||||
// Deprecated: Use PrmObjectPutInit.Key instead.
|
x.ctxCall.key = key
|
||||||
func (x *PrmObjectPutInit) UseKey(key ecdsa.PrivateKey) {
|
|
||||||
x.Key = &key
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithBearerToken attaches bearer token to be used for the operation.
|
// WithBearerToken attaches bearer token to be used for the operation.
|
||||||
// Should be called once before any writing steps.
|
// Should be called once before any writing steps.
|
||||||
//
|
func (x *ObjectWriter) WithBearerToken(t token.BearerToken) {
|
||||||
// Deprecated: Use PrmObjectPutInit.BearerToken instead.
|
x.metaHdr.SetBearerToken(t.ToV2())
|
||||||
func (x *PrmObjectPutInit) WithBearerToken(t bearer.Token) {
|
|
||||||
x.BearerToken = &t
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithinSession specifies session within which object should be stored.
|
// WithinSession specifies session within which object should be stored.
|
||||||
// Should be called once before any writing steps.
|
// Should be called once before any writing steps.
|
||||||
//
|
func (x *ObjectWriter) WithinSession(t session.Token) {
|
||||||
// Deprecated: Use PrmObjectPutInit.Session instead.
|
x.metaHdr.SetSessionToken(t.ToV2())
|
||||||
func (x *PrmObjectPutInit) WithinSession(t session.Object) {
|
|
||||||
x.Session = &t
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarkLocal tells the server to execute the operation locally.
|
// MarkLocal tells the server to execute the operation locally.
|
||||||
//
|
func (x *ObjectWriter) MarkLocal() {
|
||||||
// Deprecated: Use PrmObjectPutInit.Local instead.
|
x.metaHdr.SetTTL(1)
|
||||||
func (x *PrmObjectPutInit) MarkLocal() {
|
|
||||||
x.Local = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithXHeaders specifies list of extended headers (string key-value pairs)
|
// WithXHeaders specifies list of extended headers (string key-value pairs)
|
||||||
// to be attached to the request. Must have an even length.
|
// to be attached to the request. Must have an even length.
|
||||||
//
|
//
|
||||||
// Slice must not be mutated until the operation completes.
|
// Slice must not be mutated until the operation completes.
|
||||||
//
|
func (x *ObjectWriter) WithXHeaders(hs ...string) {
|
||||||
// Deprecated: Use PrmObjectPutInit.XHeaders instead.
|
if len(hs)%2 != 0 {
|
||||||
func (x *PrmObjectPutInit) WithXHeaders(hs ...string) {
|
panic("slice of X-Headers with odd length")
|
||||||
x.XHeaders = hs
|
}
|
||||||
|
|
||||||
|
prmCommonMeta{xHeaders: hs}.writeToMetaHeader(&x.metaHdr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithObjectMaxSize specifies max object size value and use it during object splitting.
|
// WriteHeader writes header of the object. Result means success.
|
||||||
// When specified, start writing to the stream only after the object is formed.
|
// Failure reason can be received via Close.
|
||||||
// Continue processing the input only when the previous formed object has been successfully written.
|
func (x *ObjectWriter) WriteHeader(hdr object.Object) bool {
|
||||||
//
|
v2Hdr := hdr.ToV2()
|
||||||
// Deprecated: Use PrmObjectPutInit.MaxSize instead.
|
|
||||||
func (x *PrmObjectPutInit) WithObjectMaxSize(maxSize uint64) {
|
x.partInit.SetObjectID(v2Hdr.GetObjectID())
|
||||||
x.MaxSize = maxSize
|
x.partInit.SetHeader(v2Hdr.GetHeader())
|
||||||
|
x.partInit.SetSignature(v2Hdr.GetSignature())
|
||||||
|
|
||||||
|
return x.ctxCall.writeRequest()
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithoutHomomorphicHash if set to true do not use Tillich-Zémor hash for payload.
|
// WritePayloadChunk writes chunk of the object payload. Result means success.
|
||||||
//
|
// Failure reason can be received via Close.
|
||||||
// Deprecated: Use PrmObjectPutInit.WithoutHomomorphHash instead.
|
func (x *ObjectWriter) WritePayloadChunk(chunk []byte) bool {
|
||||||
func (x *PrmObjectPutInit) WithoutHomomorphicHash(v bool) {
|
if !x.chunkCalled {
|
||||||
x.WithoutHomomorphHash = v
|
x.chunkCalled = true
|
||||||
|
x.ctxCall.req.(*v2object.PutRequest).GetBody().SetObjectPart(&x.partChunk)
|
||||||
|
}
|
||||||
|
|
||||||
|
for ln := len(chunk); ln > 0; ln = len(chunk) {
|
||||||
|
// maxChunkLen restricts maximum byte length of the chunk
|
||||||
|
// transmitted in a single stream message. It depends on
|
||||||
|
// server settings and other message fields, but for now
|
||||||
|
// we simply assume that 3MB is large enough to reduce the
|
||||||
|
// number of messages, and not to exceed the limit
|
||||||
|
// (4MB by default for gRPC servers).
|
||||||
|
const maxChunkLen = 3 << 20
|
||||||
|
if ln > maxChunkLen {
|
||||||
|
ln = maxChunkLen
|
||||||
|
}
|
||||||
|
|
||||||
|
// we deal with size limit overflow above, but there is another case:
|
||||||
|
// what if method is called with "small" chunk many times? We write
|
||||||
|
// a message to the stream on each call. Alternatively, we could use buffering.
|
||||||
|
// In most cases, the chunk length does not vary between calls. Given this
|
||||||
|
// assumption, as well as the length of the payload from the header, it is
|
||||||
|
// possible to buffer the data of intermediate chunks, and send a message when
|
||||||
|
// the allocated buffer is filled, or when the last chunk is received.
|
||||||
|
// It is mentally assumed that allocating and filling the buffer is better than
|
||||||
|
// synchronous sending, but this needs to be tested.
|
||||||
|
x.partChunk.SetChunk(chunk[:ln])
|
||||||
|
|
||||||
|
if !x.ctxCall.writeRequest() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
chunk = chunk[ln:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithEpochSource specifies epoch for object when split it on client side.
|
// Close ends writing the object and returns the result of the operation
|
||||||
|
// along with the final results. Must be called after using the ObjectWriter.
|
||||||
//
|
//
|
||||||
// Deprecated: Use PrmObjectPutInit.EpochSource instead.
|
// Exactly one return value is non-nil. By default, server status is returned in res structure.
|
||||||
func (x *PrmObjectPutInit) WithEpochSource(es transformer.EpochSource) {
|
// Any client's internal or transport errors are returned as Go built-in error.
|
||||||
x.EpochSource = es
|
// If Client is tuned to resolve NeoFS API statuses, then NeoFS failures
|
||||||
|
// codes are returned as error.
|
||||||
|
//
|
||||||
|
// Return statuses:
|
||||||
|
// - global (see Client docs);
|
||||||
|
// - *apistatus.ContainerNotFound;
|
||||||
|
// - *apistatus.ObjectAccessDenied;
|
||||||
|
// - *apistatus.ObjectLocked;
|
||||||
|
// - *apistatus.LockNonRegularObject;
|
||||||
|
// - *apistatus.SessionTokenNotFound;
|
||||||
|
// - *apistatus.SessionTokenExpired.
|
||||||
|
func (x *ObjectWriter) Close() (*ResObjectPut, error) {
|
||||||
|
defer x.cancelCtxStream()
|
||||||
|
|
||||||
|
if x.ctxCall.err != nil {
|
||||||
|
return nil, x.ctxCall.err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !x.ctxCall.close() {
|
||||||
|
return nil, x.ctxCall.err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !x.ctxCall.processResponse() {
|
||||||
|
return nil, x.ctxCall.err
|
||||||
|
}
|
||||||
|
|
||||||
|
return x.ctxCall.statusRes.(*ResObjectPut), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObjectPutInit initiates writing an object through a remote server using FrostFS API protocol.
|
// ObjectPutInit initiates writing an object through a remote server using NeoFS API protocol.
|
||||||
//
|
//
|
||||||
// The call only opens the transmission channel, explicit recording is done using the ObjectWriter.
|
// The call only opens the transmission channel, explicit recording is done using the ObjectWriter.
|
||||||
// Exactly one return value is non-nil. Resulting writer must be finally closed.
|
// Exactly one return value is non-nil. Resulting writer must be finally closed.
|
||||||
//
|
//
|
||||||
// Returns an error if parameters are set incorrectly.
|
|
||||||
// Context is required and must not be nil. It is used for network communication.
|
// Context is required and must not be nil. It is used for network communication.
|
||||||
func (c *Client) ObjectPutInit(ctx context.Context, prm PrmObjectPutInit) (ObjectWriter, error) {
|
func (c *Client) ObjectPutInit(ctx context.Context, _ PrmObjectPutInit) (*ObjectWriter, error) {
|
||||||
if prm.MaxSize > 0 {
|
// check parameters
|
||||||
return c.objectPutInitTransformer(prm)
|
if ctx == nil {
|
||||||
|
panic(panicMsgMissingContext)
|
||||||
}
|
}
|
||||||
return c.objectPutInitRaw(ctx, prm)
|
|
||||||
|
// open stream
|
||||||
|
var (
|
||||||
|
res ResObjectPut
|
||||||
|
w ObjectWriter
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx, w.cancelCtxStream = context.WithCancel(ctx)
|
||||||
|
|
||||||
|
stream, err := rpcapi.PutObject(&c.c, &res.resp, client.WithContext(ctx))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("open stream: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// form request body
|
||||||
|
var body v2object.PutRequestBody
|
||||||
|
|
||||||
|
// form request
|
||||||
|
var req v2object.PutRequest
|
||||||
|
|
||||||
|
req.SetBody(&body)
|
||||||
|
|
||||||
|
req.SetMetaHeader(&w.metaHdr)
|
||||||
|
body.SetObjectPart(&w.partInit)
|
||||||
|
|
||||||
|
// init call context
|
||||||
|
c.initCallContext(&w.ctxCall)
|
||||||
|
w.ctxCall.req = &req
|
||||||
|
w.ctxCall.statusRes = &res
|
||||||
|
w.ctxCall.resp = &res.resp
|
||||||
|
w.ctxCall.wReq = func() error {
|
||||||
|
return stream.Write(&req)
|
||||||
|
}
|
||||||
|
w.ctxCall.closer = stream.Close
|
||||||
|
|
||||||
|
return &w, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,181 +0,0 @@
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl"
|
|
||||||
v2object "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
|
|
||||||
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
|
||||||
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
|
|
||||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c *Client) objectPutInitRaw(ctx context.Context, prm PrmObjectPutInit) (*objectWriterRaw, error) {
|
|
||||||
if len(prm.XHeaders)%2 != 0 {
|
|
||||||
return nil, errorInvalidXHeaders
|
|
||||||
}
|
|
||||||
|
|
||||||
var w objectWriterRaw
|
|
||||||
stream, err := rpcapi.PutObject(&c.c, &w.respV2, client.WithContext(ctx))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("open stream: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
w.key = &c.prm.Key
|
|
||||||
if prm.Key != nil {
|
|
||||||
w.key = prm.Key
|
|
||||||
}
|
|
||||||
w.client = c
|
|
||||||
w.stream = stream
|
|
||||||
w.partInit.SetCopiesNumber(prm.CopiesNumber)
|
|
||||||
w.req.SetBody(new(v2object.PutRequestBody))
|
|
||||||
if prm.MaxChunkLength > 0 {
|
|
||||||
w.maxChunkLen = prm.MaxChunkLength
|
|
||||||
} else {
|
|
||||||
w.maxChunkLen = defaultGRPCPayloadChunkLen
|
|
||||||
}
|
|
||||||
|
|
||||||
meta := new(v2session.RequestMetaHeader)
|
|
||||||
writeXHeadersToMeta(prm.XHeaders, meta)
|
|
||||||
|
|
||||||
if prm.BearerToken != nil {
|
|
||||||
v2BearerToken := new(acl.BearerToken)
|
|
||||||
prm.BearerToken.WriteToV2(v2BearerToken)
|
|
||||||
meta.SetBearerToken(v2BearerToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
if prm.Session != nil {
|
|
||||||
v2SessionToken := new(v2session.Token)
|
|
||||||
prm.Session.WriteToV2(v2SessionToken)
|
|
||||||
meta.SetSessionToken(v2SessionToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
if prm.Local {
|
|
||||||
meta.SetTTL(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.prepareRequest(&w.req, meta)
|
|
||||||
return &w, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type objectWriterRaw struct {
|
|
||||||
client *Client
|
|
||||||
stream interface {
|
|
||||||
Write(*v2object.PutRequest) error
|
|
||||||
Close() error
|
|
||||||
}
|
|
||||||
|
|
||||||
key *ecdsa.PrivateKey
|
|
||||||
res ResObjectPut
|
|
||||||
err error
|
|
||||||
chunkCalled bool
|
|
||||||
respV2 v2object.PutResponse
|
|
||||||
req v2object.PutRequest
|
|
||||||
partInit v2object.PutObjectPartInit
|
|
||||||
partChunk v2object.PutObjectPartChunk
|
|
||||||
maxChunkLen int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *objectWriterRaw) WriteHeader(_ context.Context, hdr object.Object) bool {
|
|
||||||
v2Hdr := hdr.ToV2()
|
|
||||||
|
|
||||||
x.partInit.SetObjectID(v2Hdr.GetObjectID())
|
|
||||||
x.partInit.SetHeader(v2Hdr.GetHeader())
|
|
||||||
x.partInit.SetSignature(v2Hdr.GetSignature())
|
|
||||||
|
|
||||||
x.req.GetBody().SetObjectPart(&x.partInit)
|
|
||||||
x.req.SetVerificationHeader(nil)
|
|
||||||
|
|
||||||
x.err = signature.SignServiceMessage(x.key, &x.req)
|
|
||||||
if x.err != nil {
|
|
||||||
x.err = fmt.Errorf("sign message: %w", x.err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
x.err = x.stream.Write(&x.req)
|
|
||||||
return x.err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *objectWriterRaw) WritePayloadChunk(_ context.Context, chunk []byte) bool {
|
|
||||||
if !x.chunkCalled {
|
|
||||||
x.chunkCalled = true
|
|
||||||
x.req.GetBody().SetObjectPart(&x.partChunk)
|
|
||||||
}
|
|
||||||
|
|
||||||
for ln := len(chunk); ln > 0; ln = len(chunk) {
|
|
||||||
if ln > x.maxChunkLen {
|
|
||||||
ln = x.maxChunkLen
|
|
||||||
}
|
|
||||||
|
|
||||||
// we deal with size limit overflow above, but there is another case:
|
|
||||||
// what if method is called with "small" chunk many times? We write
|
|
||||||
// a message to the stream on each call. Alternatively, we could use buffering.
|
|
||||||
// In most cases, the chunk length does not vary between calls. Given this
|
|
||||||
// assumption, as well as the length of the payload from the header, it is
|
|
||||||
// possible to buffer the data of intermediate chunks, and send a message when
|
|
||||||
// the allocated buffer is filled, or when the last chunk is received.
|
|
||||||
// It is mentally assumed that allocating and filling the buffer is better than
|
|
||||||
// synchronous sending, but this needs to be tested.
|
|
||||||
x.partChunk.SetChunk(chunk[:ln])
|
|
||||||
x.req.SetVerificationHeader(nil)
|
|
||||||
|
|
||||||
x.err = signature.SignServiceMessage(x.key, &x.req)
|
|
||||||
if x.err != nil {
|
|
||||||
x.err = fmt.Errorf("sign message: %w", x.err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
x.err = x.stream.Write(&x.req)
|
|
||||||
if x.err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
chunk = chunk[ln:]
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *objectWriterRaw) Close(_ context.Context) (*ResObjectPut, error) {
|
|
||||||
// Ignore io.EOF error, because it is expected error for client-side
|
|
||||||
// stream termination by the server. E.g. when stream contains invalid
|
|
||||||
// message. Server returns an error in response message (in status).
|
|
||||||
if x.err != nil && !errors.Is(x.err, io.EOF) {
|
|
||||||
return nil, x.err
|
|
||||||
}
|
|
||||||
|
|
||||||
if x.err = x.stream.Close(); x.err != nil {
|
|
||||||
return nil, x.err
|
|
||||||
}
|
|
||||||
|
|
||||||
x.res.st, x.err = x.client.processResponse(&x.respV2)
|
|
||||||
if x.err != nil {
|
|
||||||
return nil, x.err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !apistatus.IsSuccessful(x.res.st) {
|
|
||||||
return &x.res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldID = "ID"
|
|
||||||
|
|
||||||
idV2 := x.respV2.GetBody().GetObjectID()
|
|
||||||
if idV2 == nil {
|
|
||||||
return nil, newErrMissingResponseField(fieldID)
|
|
||||||
}
|
|
||||||
|
|
||||||
x.err = x.res.obj.ReadFromV2(*idV2)
|
|
||||||
if x.err != nil {
|
|
||||||
x.err = newErrInvalidResponseField(fieldID, x.err)
|
|
||||||
}
|
|
||||||
x.res.epoch = x.respV2.GetMetaHeader().GetEpoch()
|
|
||||||
|
|
||||||
return &x.res, nil
|
|
||||||
}
|
|
|
@ -1,175 +0,0 @@
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl"
|
|
||||||
v2object "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
|
|
||||||
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
|
||||||
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PrmObjectPutSingle groups parameters of PutSingle operation.
|
|
||||||
type PrmObjectPutSingle struct {
|
|
||||||
XHeaders []string
|
|
||||||
|
|
||||||
BearerToken *bearer.Token
|
|
||||||
|
|
||||||
Session *session.Object
|
|
||||||
|
|
||||||
Local bool
|
|
||||||
|
|
||||||
CopiesNumber []uint32
|
|
||||||
|
|
||||||
Object *object.Object
|
|
||||||
|
|
||||||
Key *ecdsa.PrivateKey
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCopiesNumber sets ordered list of minimal required object copies numbers
|
|
||||||
// per placement vector. List's length MUST equal container's placement vector number,
|
|
||||||
// otherwise request will fail.
|
|
||||||
//
|
|
||||||
// Deprecated: Use PrmObjectPutSingle.CopiesNumber instead.
|
|
||||||
func (prm *PrmObjectPutSingle) SetCopiesNumber(v []uint32) {
|
|
||||||
prm.CopiesNumber = v
|
|
||||||
}
|
|
||||||
|
|
||||||
// UseKey specifies private key to sign the requests.
|
|
||||||
// If key is not provided, then Client default key is used.
|
|
||||||
//
|
|
||||||
// Deprecated: Use PrmObjectPutSingle.Key instead.
|
|
||||||
func (prm *PrmObjectPutSingle) UseKey(key *ecdsa.PrivateKey) {
|
|
||||||
prm.Key = key
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithBearerToken attaches bearer token to be used for the operation.
|
|
||||||
// Should be called once before any writing steps.
|
|
||||||
//
|
|
||||||
// Deprecated: Use PrmObjectPutSingle.BearerToken instead.
|
|
||||||
func (prm *PrmObjectPutSingle) WithBearerToken(t bearer.Token) {
|
|
||||||
prm.BearerToken = &t
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithinSession specifies session within which object should be stored.
|
|
||||||
// Should be called once before any writing steps.
|
|
||||||
//
|
|
||||||
// Deprecated: Use PrmObjectPutSingle.Session instead.
|
|
||||||
func (prm *PrmObjectPutSingle) WithinSession(t session.Object) {
|
|
||||||
prm.Session = &t
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExecuteLocal tells the server to execute the operation locally.
|
|
||||||
//
|
|
||||||
// Deprecated: Use PrmObjectPutSingle.Local instead.
|
|
||||||
func (prm *PrmObjectPutSingle) ExecuteLocal() {
|
|
||||||
prm.Local = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithXHeaders specifies list of extended headers (string key-value pairs)
|
|
||||||
// to be attached to the request. Must have an even length.
|
|
||||||
//
|
|
||||||
// Slice must not be mutated until the operation completes.
|
|
||||||
//
|
|
||||||
// Deprecated: Use PrmObjectPutSingle.XHeaders instead.
|
|
||||||
func (prm *PrmObjectPutSingle) WithXHeaders(hs ...string) {
|
|
||||||
prm.XHeaders = hs
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetObject specifies prepared object to put.
|
|
||||||
//
|
|
||||||
// Deprecated: Use PrmObjectPutSingle.Object instead.
|
|
||||||
func (prm *PrmObjectPutSingle) SetObject(o *v2object.Object) {
|
|
||||||
prm.Object = object.NewFromV2(o)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResObjectPutSingle groups resulting values of PutSingle operation.
|
|
||||||
type ResObjectPutSingle struct {
|
|
||||||
statusRes
|
|
||||||
|
|
||||||
epoch uint64
|
|
||||||
}
|
|
||||||
|
|
||||||
// Epoch returns creation epoch of the saved object.
|
|
||||||
func (r *ResObjectPutSingle) Epoch() uint64 {
|
|
||||||
return r.epoch
|
|
||||||
}
|
|
||||||
|
|
||||||
func (prm *PrmObjectPutSingle) buildRequest(c *Client) (*v2object.PutSingleRequest, error) {
|
|
||||||
if len(prm.XHeaders)%2 != 0 {
|
|
||||||
return nil, errorInvalidXHeaders
|
|
||||||
}
|
|
||||||
|
|
||||||
body := new(v2object.PutSingleRequestBody)
|
|
||||||
body.SetCopiesNumber(prm.CopiesNumber)
|
|
||||||
body.SetObject(prm.Object.ToV2())
|
|
||||||
|
|
||||||
meta := new(v2session.RequestMetaHeader)
|
|
||||||
writeXHeadersToMeta(prm.XHeaders, meta)
|
|
||||||
|
|
||||||
if prm.BearerToken != nil {
|
|
||||||
v2BearerToken := new(acl.BearerToken)
|
|
||||||
prm.BearerToken.WriteToV2(v2BearerToken)
|
|
||||||
meta.SetBearerToken(v2BearerToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
if prm.Session != nil {
|
|
||||||
v2SessionToken := new(v2session.Token)
|
|
||||||
prm.Session.WriteToV2(v2SessionToken)
|
|
||||||
meta.SetSessionToken(v2SessionToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
if prm.Local {
|
|
||||||
meta.SetTTL(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
req := &v2object.PutSingleRequest{}
|
|
||||||
req.SetBody(body)
|
|
||||||
c.prepareRequest(req, meta)
|
|
||||||
|
|
||||||
return req, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ObjectPutSingle writes prepared object to FrostFS.
|
|
||||||
// Object must have payload, also containerID, objectID, ownerID, payload hash, payload length of an object must be set.
|
|
||||||
// Exactly one return value is non-nil. By default, server status is returned in res structure.
|
|
||||||
// Any client's internal or transport errors are returned as Go built-in error.
|
|
||||||
// If Client is tuned to resolve FrostFS API statuses, then FrostFS failures
|
|
||||||
// codes are returned as error.
|
|
||||||
func (c *Client) ObjectPutSingle(ctx context.Context, prm PrmObjectPutSingle) (*ResObjectPutSingle, error) {
|
|
||||||
req, err := prm.buildRequest(c)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
key := &c.prm.Key
|
|
||||||
if prm.Key != nil {
|
|
||||||
key = prm.Key
|
|
||||||
}
|
|
||||||
|
|
||||||
err = signature.SignServiceMessage(key, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("sign request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := rpcapi.PutSingleObject(&c.c, req, client.WithContext(ctx))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var res ResObjectPutSingle
|
|
||||||
res.st, err = c.processResponse(resp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
res.epoch = resp.GetMetaHeader().GetEpoch()
|
|
||||||
|
|
||||||
return &res, nil
|
|
||||||
}
|
|
|
@ -1,144 +0,0 @@
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
buffPool "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/util/pool"
|
|
||||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/transformer"
|
|
||||||
"google.golang.org/grpc/codes"
|
|
||||||
"google.golang.org/grpc/status"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c *Client) objectPutInitTransformer(prm PrmObjectPutInit) (*objectWriterTransformer, error) {
|
|
||||||
var w objectWriterTransformer
|
|
||||||
w.it = internalTarget{
|
|
||||||
client: c,
|
|
||||||
prm: prm,
|
|
||||||
}
|
|
||||||
key := &c.prm.Key
|
|
||||||
if prm.Key != nil {
|
|
||||||
key = prm.Key
|
|
||||||
}
|
|
||||||
w.ot = transformer.NewPayloadSizeLimiter(transformer.Params{
|
|
||||||
Key: key,
|
|
||||||
NextTargetInit: func() transformer.ObjectWriter { return &w.it },
|
|
||||||
MaxSize: prm.MaxSize,
|
|
||||||
WithoutHomomorphicHash: prm.WithoutHomomorphHash,
|
|
||||||
NetworkState: prm.EpochSource,
|
|
||||||
Pool: prm.Pool,
|
|
||||||
})
|
|
||||||
return &w, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type objectWriterTransformer struct {
|
|
||||||
ot transformer.ChunkedObjectWriter
|
|
||||||
it internalTarget
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *objectWriterTransformer) WriteHeader(ctx context.Context, hdr object.Object) bool {
|
|
||||||
x.err = x.ot.WriteHeader(ctx, &hdr)
|
|
||||||
return x.err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *objectWriterTransformer) WritePayloadChunk(ctx context.Context, chunk []byte) bool {
|
|
||||||
_, x.err = x.ot.Write(ctx, chunk)
|
|
||||||
return x.err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *objectWriterTransformer) Close(ctx context.Context) (*ResObjectPut, error) {
|
|
||||||
if x.err != nil {
|
|
||||||
return nil, x.err
|
|
||||||
}
|
|
||||||
|
|
||||||
ai, err := x.ot.Close(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if ai != nil {
|
|
||||||
x.it.res.epoch = ai.Epoch
|
|
||||||
if ai.ParentID != nil {
|
|
||||||
x.it.res.obj = *ai.ParentID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return x.it.res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type internalTarget struct {
|
|
||||||
client *Client
|
|
||||||
res *ResObjectPut
|
|
||||||
prm PrmObjectPutInit
|
|
||||||
useStream bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *internalTarget) WriteObject(ctx context.Context, o *object.Object) error {
|
|
||||||
putSingleImplemented, err := it.tryPutSingle(ctx, o)
|
|
||||||
if putSingleImplemented {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
it.useStream = true
|
|
||||||
return it.putAsStream(ctx, o)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *internalTarget) putAsStream(ctx context.Context, o *object.Object) error {
|
|
||||||
wrt, err := it.client.objectPutInitRaw(ctx, it.prm)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if wrt.WriteHeader(ctx, *o) {
|
|
||||||
wrt.WritePayloadChunk(ctx, o.Payload())
|
|
||||||
}
|
|
||||||
it.res, err = wrt.Close(ctx)
|
|
||||||
if err == nil && it.client.prm.DisableFrostFSErrorResolution && !apistatus.IsSuccessful(it.res.st) {
|
|
||||||
err = apistatus.ErrFromStatus(it.res.st)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *internalTarget) tryPutSingle(ctx context.Context, o *object.Object) (bool, error) {
|
|
||||||
if it.useStream {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
prm := PrmObjectPutSingle{
|
|
||||||
XHeaders: it.prm.XHeaders,
|
|
||||||
BearerToken: it.prm.BearerToken,
|
|
||||||
Session: it.prm.Session,
|
|
||||||
Local: it.prm.Local,
|
|
||||||
CopiesNumber: it.prm.CopiesNumber,
|
|
||||||
Object: o,
|
|
||||||
Key: it.prm.Key,
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := it.client.ObjectPutSingle(ctx, prm)
|
|
||||||
if err != nil && status.Code(err) == codes.Unimplemented {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
it.returnBuffPool(o.Payload())
|
|
||||||
id, _ := o.ID()
|
|
||||||
it.res = &ResObjectPut{
|
|
||||||
statusRes: res.statusRes,
|
|
||||||
obj: id,
|
|
||||||
epoch: res.epoch,
|
|
||||||
}
|
|
||||||
if it.client.prm.DisableFrostFSErrorResolution && !apistatus.IsSuccessful(it.res.st) {
|
|
||||||
return true, apistatus.ErrFromStatus(it.res.st)
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
return true, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *internalTarget) returnBuffPool(playback []byte) {
|
|
||||||
if it.prm.Pool == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var buffer buffPool.Buffer
|
|
||||||
buffer.Data = playback
|
|
||||||
it.prm.Pool.Put(&buffer)
|
|
||||||
}
|
|
|
@ -7,43 +7,40 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl"
|
v2object "github.com/nspcc-dev/neofs-api-go/v2/object"
|
||||||
v2object "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
|
v2refs "github.com/nspcc-dev/neofs-api-go/v2/refs"
|
||||||
v2refs "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
|
||||||
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
|
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
|
||||||
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
|
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
|
"github.com/nspcc-dev/neofs-sdk-go/object"
|
||||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
|
||||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
"github.com/nspcc-dev/neofs-sdk-go/session"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
"github.com/nspcc-dev/neofs-sdk-go/token"
|
||||||
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// PrmObjectSearch groups parameters of ObjectSearch operation.
|
// PrmObjectSearch groups parameters of ObjectSearch operation.
|
||||||
type PrmObjectSearch struct {
|
type PrmObjectSearch struct {
|
||||||
XHeaders []string
|
prmCommonMeta
|
||||||
|
|
||||||
Local bool
|
local bool
|
||||||
|
|
||||||
BearerToken *bearer.Token
|
sessionSet bool
|
||||||
|
session session.Token
|
||||||
|
|
||||||
Session *session.Object
|
bearerSet bool
|
||||||
|
bearer token.BearerToken
|
||||||
|
|
||||||
ContainerID *cid.ID
|
cnrSet bool
|
||||||
|
cnr cid.ID
|
||||||
|
|
||||||
Key *ecdsa.PrivateKey
|
filters object.SearchFilters
|
||||||
|
|
||||||
Filters object.SearchFilters
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarkLocal tells the server to execute the operation locally.
|
// MarkLocal tells the server to execute the operation locally.
|
||||||
//
|
|
||||||
// Deprecated: Use PrmObjectSearch.Local instead.
|
|
||||||
func (x *PrmObjectSearch) MarkLocal() {
|
func (x *PrmObjectSearch) MarkLocal() {
|
||||||
x.Local = true
|
x.local = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithinSession specifies session within which the search query must be executed.
|
// WithinSession specifies session within which the search query must be executed.
|
||||||
|
@ -52,10 +49,9 @@ func (x *PrmObjectSearch) MarkLocal() {
|
||||||
// This may affect the execution of an operation (e.g. access control).
|
// This may affect the execution of an operation (e.g. access control).
|
||||||
//
|
//
|
||||||
// Must be signed.
|
// Must be signed.
|
||||||
//
|
func (x *PrmObjectSearch) WithinSession(t session.Token) {
|
||||||
// Deprecated: Use PrmObjectSearch.Session instead.
|
x.session = t
|
||||||
func (x *PrmObjectSearch) WithinSession(t session.Object) {
|
x.sessionSet = true
|
||||||
x.Session = &t
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithBearerToken attaches bearer token to be used for the operation.
|
// WithBearerToken attaches bearer token to be used for the operation.
|
||||||
|
@ -63,44 +59,22 @@ func (x *PrmObjectSearch) WithinSession(t session.Object) {
|
||||||
// If set, underlying eACL rules will be used in access control.
|
// If set, underlying eACL rules will be used in access control.
|
||||||
//
|
//
|
||||||
// Must be signed.
|
// Must be signed.
|
||||||
//
|
func (x *PrmObjectSearch) WithBearerToken(t token.BearerToken) {
|
||||||
// Deprecated: Use PrmObjectSearch.BearerToken instead.
|
x.bearer = t
|
||||||
func (x *PrmObjectSearch) WithBearerToken(t bearer.Token) {
|
x.bearerSet = true
|
||||||
x.BearerToken = &t
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithXHeaders specifies list of extended headers (string key-value pairs)
|
|
||||||
// to be attached to the request. Must have an even length.
|
|
||||||
//
|
|
||||||
// Slice must not be mutated until the operation completes.
|
|
||||||
//
|
|
||||||
// Deprecated: Use PrmObjectSearch.XHeaders instead.
|
|
||||||
func (x *PrmObjectSearch) WithXHeaders(hs ...string) {
|
|
||||||
x.XHeaders = hs
|
|
||||||
}
|
|
||||||
|
|
||||||
// UseKey specifies private key to sign the requests.
|
|
||||||
// If key is not provided, then Client default key is used.
|
|
||||||
//
|
|
||||||
// Deprecated: Use PrmObjectSearch.Key instead.
|
|
||||||
func (x *PrmObjectSearch) UseKey(key ecdsa.PrivateKey) {
|
|
||||||
x.Key = &key
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// InContainer specifies the container in which to look for objects.
|
// InContainer specifies the container in which to look for objects.
|
||||||
// Required parameter.
|
// Required parameter.
|
||||||
//
|
|
||||||
// Deprecated: Use PrmObjectSearch.ContainerID instead.
|
|
||||||
func (x *PrmObjectSearch) InContainer(id cid.ID) {
|
func (x *PrmObjectSearch) InContainer(id cid.ID) {
|
||||||
x.ContainerID = &id
|
x.cnr = id
|
||||||
|
x.cnrSet = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetFilters sets filters by which to select objects. All container objects
|
// SetFilters sets filters by which to select objects. All container objects
|
||||||
// match unset/empty filters.
|
// match unset/empty filters.
|
||||||
//
|
|
||||||
// Deprecated: Use PrmObjectSearch.Filters instead.
|
|
||||||
func (x *PrmObjectSearch) SetFilters(filters object.SearchFilters) {
|
func (x *PrmObjectSearch) SetFilters(filters object.SearchFilters) {
|
||||||
x.Filters = filters
|
x.filters = filters
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResObjectSearch groups the final result values of ObjectSearch operation.
|
// ResObjectSearch groups the final result values of ObjectSearch operation.
|
||||||
|
@ -108,20 +82,28 @@ type ResObjectSearch struct {
|
||||||
statusRes
|
statusRes
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObjectListReader is designed to read list of object identifiers from FrostFS system.
|
// ObjectListReader is designed to read list of object identifiers from NeoFS system.
|
||||||
//
|
//
|
||||||
// Must be initialized using Client.ObjectSearch, any other usage is unsafe.
|
// Must be initialized using Client.ObjectSearch, any other usage is unsafe.
|
||||||
type ObjectListReader struct {
|
type ObjectListReader struct {
|
||||||
client *Client
|
|
||||||
cancelCtxStream context.CancelFunc
|
cancelCtxStream context.CancelFunc
|
||||||
err error
|
|
||||||
res ResObjectSearch
|
ctxCall contextCall
|
||||||
stream interface {
|
|
||||||
Read(resp *v2object.SearchResponse) error
|
reqWritten bool
|
||||||
}
|
|
||||||
|
// initially bound to contextCall
|
||||||
|
bodyResp v2object.SearchResponseBody
|
||||||
|
|
||||||
tail []v2refs.ObjectID
|
tail []v2refs.ObjectID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UseKey specifies private key to sign the requests.
|
||||||
|
// If key is not provided, then Client default key is used.
|
||||||
|
func (x *ObjectListReader) UseKey(key ecdsa.PrivateKey) {
|
||||||
|
x.ctxCall.key = key
|
||||||
|
}
|
||||||
|
|
||||||
// Read reads another list of the object identifiers. Works similar to
|
// Read reads another list of the object identifiers. Works similar to
|
||||||
// io.Reader.Read but copies oid.ID and returns success flag instead of error.
|
// io.Reader.Read but copies oid.ID and returns success flag instead of error.
|
||||||
//
|
//
|
||||||
|
@ -133,33 +115,58 @@ func (x *ObjectListReader) Read(buf []oid.ID) (int, bool) {
|
||||||
panic("empty buffer in ObjectListReader.ReadList")
|
panic("empty buffer in ObjectListReader.ReadList")
|
||||||
}
|
}
|
||||||
|
|
||||||
read := copyIDBuffers(buf, x.tail)
|
if !x.reqWritten {
|
||||||
|
if !x.ctxCall.writeRequest() {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
x.reqWritten = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// read remaining tail
|
||||||
|
read := len(x.tail)
|
||||||
|
if read > len(buf) {
|
||||||
|
read = len(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < read; i++ {
|
||||||
|
buf[i] = *oid.NewIDFromV2(&x.tail[i]) // need smth better
|
||||||
|
}
|
||||||
|
|
||||||
x.tail = x.tail[read:]
|
x.tail = x.tail[read:]
|
||||||
|
|
||||||
if len(buf) == read {
|
if len(buf) == read {
|
||||||
return read, true
|
return read, true
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
var ok bool
|
||||||
var resp v2object.SearchResponse
|
var ids []v2refs.ObjectID
|
||||||
x.err = x.stream.Read(&resp)
|
var i, ln, rem int
|
||||||
if x.err != nil {
|
|
||||||
return read, false
|
|
||||||
}
|
|
||||||
|
|
||||||
x.res.st, x.err = x.client.processResponse(&resp)
|
for {
|
||||||
if x.err != nil || !apistatus.IsSuccessful(x.res.st) {
|
// receive next message
|
||||||
|
ok = x.ctxCall.readResponse()
|
||||||
|
if !ok {
|
||||||
return read, false
|
return read, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// read new chunk of objects
|
// read new chunk of objects
|
||||||
ids := resp.GetBody().GetIDList()
|
ids = x.bodyResp.GetIDList()
|
||||||
if len(ids) == 0 {
|
|
||||||
|
ln = len(ids)
|
||||||
|
if ln == 0 {
|
||||||
// just skip empty lists since they are not prohibited by protocol
|
// just skip empty lists since they are not prohibited by protocol
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
ln := copyIDBuffers(buf[read:], ids)
|
if rem = len(buf) - read; ln > rem {
|
||||||
|
ln = rem
|
||||||
|
}
|
||||||
|
|
||||||
|
for i = 0; i < ln; i++ {
|
||||||
|
buf[read+i] = *oid.NewIDFromV2(&ids[i]) // need smth better
|
||||||
|
}
|
||||||
|
|
||||||
read += ln
|
read += ln
|
||||||
|
|
||||||
if read == len(buf) {
|
if read == len(buf) {
|
||||||
|
@ -171,14 +178,6 @@ func (x *ObjectListReader) Read(buf []oid.ID) (int, bool) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyIDBuffers(dst []oid.ID, src []v2refs.ObjectID) int {
|
|
||||||
var i int
|
|
||||||
for ; i < len(dst) && i < len(src); i++ {
|
|
||||||
_ = dst[i].ReadFromV2(src[i])
|
|
||||||
}
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
|
|
||||||
// Iterate iterates over the list of found object identifiers.
|
// Iterate iterates over the list of found object identifiers.
|
||||||
// f can return true to stop iteration earlier.
|
// f can return true to stop iteration earlier.
|
||||||
//
|
//
|
||||||
|
@ -208,7 +207,7 @@ func (x *ObjectListReader) Iterate(f func(oid.ID) bool) error {
|
||||||
//
|
//
|
||||||
// Exactly one return value is non-nil. By default, server status is returned in res structure.
|
// Exactly one return value is non-nil. By default, server status is returned in res structure.
|
||||||
// Any client's internal or transport errors are returned as Go built-in error.
|
// Any client's internal or transport errors are returned as Go built-in error.
|
||||||
// If Client is tuned to resolve FrostFS API statuses, then FrostFS failures
|
// If Client is tuned to resolve NeoFS API statuses, then NeoFS failures
|
||||||
// codes are returned as error.
|
// codes are returned as error.
|
||||||
//
|
//
|
||||||
// Return statuses:
|
// Return statuses:
|
||||||
|
@ -219,87 +218,89 @@ func (x *ObjectListReader) Iterate(f func(oid.ID) bool) error {
|
||||||
func (x *ObjectListReader) Close() (*ResObjectSearch, error) {
|
func (x *ObjectListReader) Close() (*ResObjectSearch, error) {
|
||||||
defer x.cancelCtxStream()
|
defer x.cancelCtxStream()
|
||||||
|
|
||||||
if x.err != nil && !errors.Is(x.err, io.EOF) {
|
if x.ctxCall.err != nil && !errors.Is(x.ctxCall.err, io.EOF) {
|
||||||
return nil, x.err
|
return nil, x.ctxCall.err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &x.res, nil
|
return x.ctxCall.statusRes.(*ResObjectSearch), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *PrmObjectSearch) buildRequest(c *Client) (*v2object.SearchRequest, error) {
|
// ObjectSearchInit initiates object selection through a remote server using NeoFS API protocol.
|
||||||
if x.ContainerID == nil {
|
|
||||||
return nil, errorMissingContainer
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(x.XHeaders)%2 != 0 {
|
|
||||||
return nil, errorInvalidXHeaders
|
|
||||||
}
|
|
||||||
|
|
||||||
meta := new(v2session.RequestMetaHeader)
|
|
||||||
writeXHeadersToMeta(x.XHeaders, meta)
|
|
||||||
|
|
||||||
if x.BearerToken != nil {
|
|
||||||
v2BearerToken := new(acl.BearerToken)
|
|
||||||
x.BearerToken.WriteToV2(v2BearerToken)
|
|
||||||
meta.SetBearerToken(v2BearerToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
if x.Session != nil {
|
|
||||||
v2SessionToken := new(v2session.Token)
|
|
||||||
x.Session.WriteToV2(v2SessionToken)
|
|
||||||
meta.SetSessionToken(v2SessionToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
if x.Local {
|
|
||||||
meta.SetTTL(1)
|
|
||||||
}
|
|
||||||
cnrV2 := new(v2refs.ContainerID)
|
|
||||||
x.ContainerID.WriteToV2(cnrV2)
|
|
||||||
|
|
||||||
body := new(v2object.SearchRequestBody)
|
|
||||||
body.SetVersion(1)
|
|
||||||
body.SetContainerID(cnrV2)
|
|
||||||
body.SetFilters(x.Filters.ToV2())
|
|
||||||
|
|
||||||
req := new(v2object.SearchRequest)
|
|
||||||
req.SetBody(body)
|
|
||||||
c.prepareRequest(req, meta)
|
|
||||||
|
|
||||||
return req, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ObjectSearchInit initiates object selection through a remote server using FrostFS API protocol.
|
|
||||||
//
|
//
|
||||||
// The call only opens the transmission channel, explicit fetching of matched objects
|
// The call only opens the transmission channel, explicit fetching of matched objects
|
||||||
// is done using the ObjectListReader. Exactly one return value is non-nil.
|
// is done using the ObjectListReader. Exactly one return value is non-nil.
|
||||||
// Resulting reader must be finally closed.
|
// Resulting reader must be finally closed.
|
||||||
//
|
//
|
||||||
// Returns an error if parameters are set incorrectly (see PrmObjectSearch docs).
|
// Immediately panics if parameters are set incorrectly (see PrmObjectSearch docs).
|
||||||
// Context is required and must not be nil. It is used for network communication.
|
// Context is required and must not be nil. It is used for network communication.
|
||||||
func (c *Client) ObjectSearchInit(ctx context.Context, prm PrmObjectSearch) (*ObjectListReader, error) {
|
func (c *Client) ObjectSearchInit(ctx context.Context, prm PrmObjectSearch) (*ObjectListReader, error) {
|
||||||
req, err := prm.buildRequest(c)
|
// check parameters
|
||||||
if err != nil {
|
switch {
|
||||||
return nil, err
|
case ctx == nil:
|
||||||
|
panic(panicMsgMissingContext)
|
||||||
|
case !prm.cnrSet:
|
||||||
|
panic(panicMsgMissingContainer)
|
||||||
}
|
}
|
||||||
|
|
||||||
key := prm.Key
|
// form request body
|
||||||
if key == nil {
|
var body v2object.SearchRequestBody
|
||||||
key = &c.prm.Key
|
|
||||||
|
body.SetVersion(1)
|
||||||
|
body.SetContainerID(prm.cnr.ToV2())
|
||||||
|
body.SetFilters(prm.filters.ToV2())
|
||||||
|
|
||||||
|
// form meta header
|
||||||
|
var meta v2session.RequestMetaHeader
|
||||||
|
|
||||||
|
if prm.local {
|
||||||
|
meta.SetTTL(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = signature.SignServiceMessage(key, req)
|
if prm.bearerSet {
|
||||||
if err != nil {
|
meta.SetBearerToken(prm.bearer.ToV2())
|
||||||
return nil, fmt.Errorf("sign request: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var r ObjectListReader
|
if prm.sessionSet {
|
||||||
|
meta.SetSessionToken(prm.session.ToV2())
|
||||||
|
}
|
||||||
|
|
||||||
|
prm.prmCommonMeta.writeToMetaHeader(&meta)
|
||||||
|
|
||||||
|
// form request
|
||||||
|
var req v2object.SearchRequest
|
||||||
|
|
||||||
|
req.SetBody(&body)
|
||||||
|
req.SetMetaHeader(&meta)
|
||||||
|
|
||||||
|
// init reader
|
||||||
|
var (
|
||||||
|
r ObjectListReader
|
||||||
|
resp v2object.SearchResponse
|
||||||
|
stream *rpcapi.SearchResponseReader
|
||||||
|
)
|
||||||
|
|
||||||
ctx, r.cancelCtxStream = context.WithCancel(ctx)
|
ctx, r.cancelCtxStream = context.WithCancel(ctx)
|
||||||
|
|
||||||
r.stream, err = rpcapi.SearchObjects(&c.c, req, client.WithContext(ctx))
|
resp.SetBody(&r.bodyResp)
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("open stream: %w", err)
|
// init call context
|
||||||
|
c.initCallContext(&r.ctxCall)
|
||||||
|
r.ctxCall.req = &req
|
||||||
|
r.ctxCall.statusRes = new(ResObjectSearch)
|
||||||
|
r.ctxCall.resp = &resp
|
||||||
|
r.ctxCall.wReq = func() error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
stream, err = rpcapi.SearchObjects(&c.c, &req, client.WithContext(ctx))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("open stream: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
r.ctxCall.rResp = func() error {
|
||||||
|
return stream.Read(&resp)
|
||||||
}
|
}
|
||||||
r.client = c
|
|
||||||
|
|
||||||
return &r, nil
|
return &r, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +1,26 @@
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/ecdsa"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
v2object "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
|
||||||
signatureV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
|
|
||||||
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
|
||||||
oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||||
|
"github.com/nspcc-dev/neofs-api-go/v2/object"
|
||||||
|
"github.com/nspcc-dev/neofs-api-go/v2/refs"
|
||||||
|
signatureV2 "github.com/nspcc-dev/neofs-api-go/v2/signature"
|
||||||
|
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
|
||||||
|
oidtest "github.com/nspcc-dev/neofs-sdk-go/object/id/test"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestObjectSearch(t *testing.T) {
|
func TestObjectSearch(t *testing.T) {
|
||||||
ids := make([]oid.ID, 20)
|
ids := make([]oid.ID, 20)
|
||||||
for i := range ids {
|
for i := range ids {
|
||||||
ids[i] = oidtest.ID()
|
ids[i] = *oidtest.ID()
|
||||||
}
|
}
|
||||||
|
|
||||||
p, resp := testListReaderResponse(t)
|
resp, setID := testListReaderResponse(t)
|
||||||
|
|
||||||
buf := make([]oid.ID, 2)
|
buf := make([]oid.ID, 2)
|
||||||
checkRead := func(t *testing.T, expected []oid.ID) {
|
checkRead := func(t *testing.T, expected []oid.ID) {
|
||||||
|
@ -36,23 +34,38 @@ func TestObjectSearch(t *testing.T) {
|
||||||
require.Panics(t, func() { resp.Read(nil) })
|
require.Panics(t, func() { resp.Read(nil) })
|
||||||
|
|
||||||
// both ID fetched
|
// both ID fetched
|
||||||
resp.stream = newSearchStream(p, nil, ids[:3])
|
setID(ids[:3])
|
||||||
checkRead(t, ids[:2])
|
checkRead(t, ids[:2])
|
||||||
|
|
||||||
// one ID cached, second fetched
|
// one ID cached, second fetched
|
||||||
resp.stream = newSearchStream(p, nil, ids[3:6])
|
setID(ids[3:6])
|
||||||
checkRead(t, ids[2:4])
|
checkRead(t, ids[2:4])
|
||||||
|
|
||||||
// both ID cached
|
// both ID cached
|
||||||
resp.stream = nil // shouldn't be called, panic if so
|
resp.ctxCall.resp = nil
|
||||||
checkRead(t, ids[4:6])
|
checkRead(t, ids[4:6])
|
||||||
|
|
||||||
// both ID fetched in 2 requests, with empty one in the middle
|
// both ID fetched in 2 requests, with empty one in the middle
|
||||||
resp.stream = newSearchStream(p, nil, ids[6:7], nil, ids[7:8])
|
var n int
|
||||||
|
resp.ctxCall.rResp = func() error {
|
||||||
|
switch n {
|
||||||
|
case 0:
|
||||||
|
setID(ids[6:7])
|
||||||
|
case 1:
|
||||||
|
setID(nil)
|
||||||
|
case 2:
|
||||||
|
setID(ids[7:8])
|
||||||
|
default:
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
checkRead(t, ids[6:8])
|
checkRead(t, ids[6:8])
|
||||||
|
|
||||||
// read from tail multiple times
|
// read from tail multiple times
|
||||||
resp.stream = newSearchStream(p, nil, ids[8:11])
|
resp.ctxCall.rResp = nil
|
||||||
|
setID(ids[8:11])
|
||||||
buf = buf[:1]
|
buf = buf[:1]
|
||||||
checkRead(t, ids[8:9])
|
checkRead(t, ids[8:9])
|
||||||
checkRead(t, ids[9:10])
|
checkRead(t, ids[9:10])
|
||||||
|
@ -60,20 +73,43 @@ func TestObjectSearch(t *testing.T) {
|
||||||
|
|
||||||
// handle EOF
|
// handle EOF
|
||||||
buf = buf[:2]
|
buf = buf[:2]
|
||||||
resp.stream = newSearchStream(p, io.EOF, ids[11:12])
|
n = 0
|
||||||
|
resp.ctxCall.rResp = func() error {
|
||||||
|
if n > 0 {
|
||||||
|
return io.EOF
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
setID(ids[11:12])
|
||||||
|
return nil
|
||||||
|
}
|
||||||
checkRead(t, ids[11:12])
|
checkRead(t, ids[11:12])
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestObjectIterate(t *testing.T) {
|
func TestObjectIterate(t *testing.T) {
|
||||||
ids := make([]oid.ID, 3)
|
ids := make([]oid.ID, 3)
|
||||||
for i := range ids {
|
for i := range ids {
|
||||||
ids[i] = oidtest.ID()
|
ids[i] = *oidtest.ID()
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("iterate all sequence", func(t *testing.T) {
|
t.Run("iterate all sequence", func(t *testing.T) {
|
||||||
p, resp := testListReaderResponse(t)
|
resp, setID := testListReaderResponse(t)
|
||||||
|
|
||||||
resp.stream = newSearchStream(p, io.EOF, ids[0:2], nil, ids[2:3])
|
// Iterate over all sequence
|
||||||
|
var n int
|
||||||
|
resp.ctxCall.rResp = func() error {
|
||||||
|
switch n {
|
||||||
|
case 0:
|
||||||
|
setID(ids[0:2])
|
||||||
|
case 1:
|
||||||
|
setID(nil)
|
||||||
|
case 2:
|
||||||
|
setID(ids[2:3])
|
||||||
|
default:
|
||||||
|
return io.EOF
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var actual []oid.ID
|
var actual []oid.ID
|
||||||
require.NoError(t, resp.Iterate(func(id oid.ID) bool {
|
require.NoError(t, resp.Iterate(func(id oid.ID) bool {
|
||||||
|
@ -83,10 +119,10 @@ func TestObjectIterate(t *testing.T) {
|
||||||
require.Equal(t, ids[:3], actual)
|
require.Equal(t, ids[:3], actual)
|
||||||
})
|
})
|
||||||
t.Run("stop by return value", func(t *testing.T) {
|
t.Run("stop by return value", func(t *testing.T) {
|
||||||
p, resp := testListReaderResponse(t)
|
resp, setID := testListReaderResponse(t)
|
||||||
|
|
||||||
var actual []oid.ID
|
var actual []oid.ID
|
||||||
resp.stream = &singleStreamResponder{key: p, idList: [][]oid.ID{ids}}
|
setID(ids)
|
||||||
require.NoError(t, resp.Iterate(func(id oid.ID) bool {
|
require.NoError(t, resp.Iterate(func(id oid.ID) bool {
|
||||||
actual = append(actual, id)
|
actual = append(actual, id)
|
||||||
return len(actual) == 2
|
return len(actual) == 2
|
||||||
|
@ -94,12 +130,22 @@ func TestObjectIterate(t *testing.T) {
|
||||||
require.Equal(t, ids[:2], actual)
|
require.Equal(t, ids[:2], actual)
|
||||||
})
|
})
|
||||||
t.Run("stop after error", func(t *testing.T) {
|
t.Run("stop after error", func(t *testing.T) {
|
||||||
p, resp := testListReaderResponse(t)
|
resp, setID := testListReaderResponse(t)
|
||||||
expectedErr := errors.New("test error")
|
expectedErr := errors.New("test error")
|
||||||
|
|
||||||
resp.stream = newSearchStream(p, expectedErr, ids[:2])
|
|
||||||
|
|
||||||
var actual []oid.ID
|
var actual []oid.ID
|
||||||
|
var n int
|
||||||
|
resp.ctxCall.rResp = func() error {
|
||||||
|
switch n {
|
||||||
|
case 0:
|
||||||
|
setID(ids[:2])
|
||||||
|
default:
|
||||||
|
return expectedErr
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
err := resp.Iterate(func(id oid.ID) bool {
|
err := resp.Iterate(func(id oid.ID) bool {
|
||||||
actual = append(actual, id)
|
actual = append(actual, id)
|
||||||
return false
|
return false
|
||||||
|
@ -109,56 +155,37 @@ func TestObjectIterate(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func testListReaderResponse(t *testing.T) (*ecdsa.PrivateKey, *ObjectListReader) {
|
func testListReaderResponse(t *testing.T) (*ObjectListReader, func(id []oid.ID) *object.SearchResponse) {
|
||||||
p, err := keys.NewPrivateKey()
|
p, err := keys.NewPrivateKey()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
return &p.PrivateKey, &ObjectListReader{
|
obj := &ObjectListReader{
|
||||||
cancelCtxStream: func() {},
|
cancelCtxStream: func() {},
|
||||||
client: &Client{},
|
ctxCall: contextCall{
|
||||||
tail: nil,
|
closer: func() error { return nil },
|
||||||
|
result: func(v2 responseV2) {},
|
||||||
|
statusRes: new(ResObjectSearch),
|
||||||
|
},
|
||||||
|
reqWritten: true,
|
||||||
|
bodyResp: object.SearchResponseBody{},
|
||||||
|
tail: nil,
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func newSearchStream(key *ecdsa.PrivateKey, endError error, idList ...[]oid.ID) *singleStreamResponder {
|
return obj, func(id []oid.ID) *object.SearchResponse {
|
||||||
return &singleStreamResponder{
|
resp := new(object.SearchResponse)
|
||||||
key: key,
|
resp.SetBody(new(object.SearchResponseBody))
|
||||||
endError: endError,
|
|
||||||
idList: idList,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type singleStreamResponder struct {
|
v2id := make([]refs.ObjectID, len(id))
|
||||||
key *ecdsa.PrivateKey
|
for i := range id {
|
||||||
n int
|
v2id[i] = *id[i].ToV2()
|
||||||
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`")
|
resp.GetBody().SetIDList(v2id)
|
||||||
}
|
err := signatureV2.SignServiceMessage(&p.PrivateKey, resp)
|
||||||
|
if err != nil {
|
||||||
var body v2object.SearchResponseBody
|
t.Fatalf("error: %v", err)
|
||||||
|
|
||||||
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)
|
obj.ctxCall.resp = resp
|
||||||
|
obj.bodyResp = *resp.GetBody()
|
||||||
|
return resp
|
||||||
}
|
}
|
||||||
resp.SetBody(&body)
|
|
||||||
|
|
||||||
err := signatureV2.SignServiceMessage(s.key, resp)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("error: %w", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
s.n++
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
194
client/reputation.go
Normal file
194
client/reputation.go
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
v2reputation "github.com/nspcc-dev/neofs-api-go/v2/reputation"
|
||||||
|
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
|
||||||
|
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
|
||||||
|
"github.com/nspcc-dev/neofs-sdk-go/reputation"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PrmAnnounceLocalTrust groups parameters of AnnounceLocalTrust operation.
|
||||||
|
type PrmAnnounceLocalTrust struct {
|
||||||
|
prmCommonMeta
|
||||||
|
|
||||||
|
epoch uint64
|
||||||
|
|
||||||
|
trusts []reputation.Trust
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetEpoch sets number of NeoFS epoch in which the trust was assessed.
|
||||||
|
// Required parameter, must not be zero.
|
||||||
|
func (x *PrmAnnounceLocalTrust) SetEpoch(epoch uint64) {
|
||||||
|
x.epoch = epoch
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetValues sets values describing trust of the client to the NeoFS network participants.
|
||||||
|
// Required parameter. Must not be empty.
|
||||||
|
//
|
||||||
|
// Must not be mutated before the end of the operation.
|
||||||
|
func (x *PrmAnnounceLocalTrust) SetValues(trusts []reputation.Trust) {
|
||||||
|
x.trusts = trusts
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResAnnounceLocalTrust groups results of AnnounceLocalTrust operation.
|
||||||
|
type ResAnnounceLocalTrust struct {
|
||||||
|
statusRes
|
||||||
|
}
|
||||||
|
|
||||||
|
// AnnounceLocalTrust sends client's trust values to the NeoFS network participants.
|
||||||
|
//
|
||||||
|
// Exactly one return value is non-nil. By default, server status is returned in res structure.
|
||||||
|
// Any client's internal or transport errors are returned as `error`.
|
||||||
|
// If WithNeoFSErrorParsing option has been provided, unsuccessful
|
||||||
|
// NeoFS status codes are returned as `error`, otherwise, are included
|
||||||
|
// in the returned result structure.
|
||||||
|
//
|
||||||
|
// Immediately panics if parameters are set incorrectly (see PrmAnnounceLocalTrust docs).
|
||||||
|
// Context is required and must not be nil. It is used for network communication.
|
||||||
|
//
|
||||||
|
// Return statuses:
|
||||||
|
// - global (see Client docs).
|
||||||
|
func (c *Client) AnnounceLocalTrust(ctx context.Context, prm PrmAnnounceLocalTrust) (*ResAnnounceLocalTrust, error) {
|
||||||
|
// check parameters
|
||||||
|
switch {
|
||||||
|
case ctx == nil:
|
||||||
|
panic(panicMsgMissingContext)
|
||||||
|
case prm.epoch == 0:
|
||||||
|
panic("zero epoch")
|
||||||
|
case len(prm.trusts) == 0:
|
||||||
|
panic("missing trusts")
|
||||||
|
}
|
||||||
|
|
||||||
|
// form request body
|
||||||
|
reqBody := new(v2reputation.AnnounceLocalTrustRequestBody)
|
||||||
|
reqBody.SetEpoch(prm.epoch)
|
||||||
|
|
||||||
|
trusts := make([]reputation.Trust, len(prm.trusts))
|
||||||
|
copy(trusts, prm.trusts)
|
||||||
|
|
||||||
|
reqBody.SetTrusts(reputation.TrustsToV2(trusts))
|
||||||
|
|
||||||
|
// form request
|
||||||
|
var req v2reputation.AnnounceLocalTrustRequest
|
||||||
|
|
||||||
|
req.SetBody(reqBody)
|
||||||
|
|
||||||
|
// init call context
|
||||||
|
|
||||||
|
var (
|
||||||
|
cc contextCall
|
||||||
|
res ResAnnounceLocalTrust
|
||||||
|
)
|
||||||
|
|
||||||
|
c.initCallContext(&cc)
|
||||||
|
cc.meta = prm.prmCommonMeta
|
||||||
|
cc.req = &req
|
||||||
|
cc.statusRes = &res
|
||||||
|
cc.call = func() (responseV2, error) {
|
||||||
|
return rpcapi.AnnounceLocalTrust(&c.c, &req, client.WithContext(ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
// process call
|
||||||
|
if !cc.processCall() {
|
||||||
|
return nil, cc.err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrmAnnounceIntermediateTrust groups parameters of AnnounceIntermediateTrust operation.
|
||||||
|
type PrmAnnounceIntermediateTrust struct {
|
||||||
|
prmCommonMeta
|
||||||
|
|
||||||
|
epoch uint64
|
||||||
|
|
||||||
|
iter uint32
|
||||||
|
|
||||||
|
trustSet bool
|
||||||
|
trust reputation.PeerToPeerTrust
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetEpoch sets number of NeoFS epoch with which client's calculation algorithm is initialized.
|
||||||
|
// Required parameter, must not be zero.
|
||||||
|
func (x *PrmAnnounceIntermediateTrust) SetEpoch(epoch uint64) {
|
||||||
|
x.epoch = epoch
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetIteration sets current sequence number of the client's calculation algorithm.
|
||||||
|
// By default, corresponds to initial (zero) iteration.
|
||||||
|
func (x *PrmAnnounceIntermediateTrust) SetIteration(iter uint32) {
|
||||||
|
x.iter = iter
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCurrentValue sets current global trust value computed at the specified iteration
|
||||||
|
// of the client's calculation algorithm. Required parameter.
|
||||||
|
func (x *PrmAnnounceIntermediateTrust) SetCurrentValue(trust reputation.PeerToPeerTrust) {
|
||||||
|
x.trust = trust
|
||||||
|
x.trustSet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResAnnounceIntermediateTrust groups results of AnnounceIntermediateTrust operation.
|
||||||
|
type ResAnnounceIntermediateTrust struct {
|
||||||
|
statusRes
|
||||||
|
}
|
||||||
|
|
||||||
|
// AnnounceIntermediateTrust sends global trust values calculated for the specified NeoFS network participants
|
||||||
|
// at some stage of client's calculation algorithm.
|
||||||
|
//
|
||||||
|
// Exactly one return value is non-nil. By default, server status is returned in res structure.
|
||||||
|
// Any client's internal or transport errors are returned as `error`.
|
||||||
|
// If WithNeoFSErrorParsing option has been provided, unsuccessful
|
||||||
|
// NeoFS status codes are returned as `error`, otherwise, are included
|
||||||
|
// in the returned result structure.
|
||||||
|
//
|
||||||
|
// Immediately panics if parameters are set incorrectly (see PrmAnnounceIntermediateTrust docs).
|
||||||
|
// Context is required and must not be nil. It is used for network communication.
|
||||||
|
//
|
||||||
|
// Return statuses:
|
||||||
|
// - global (see Client docs).
|
||||||
|
func (c *Client) AnnounceIntermediateTrust(ctx context.Context, prm PrmAnnounceIntermediateTrust) (*ResAnnounceIntermediateTrust, error) {
|
||||||
|
// check parameters
|
||||||
|
switch {
|
||||||
|
case ctx == nil:
|
||||||
|
panic(panicMsgMissingContext)
|
||||||
|
case prm.epoch == 0:
|
||||||
|
panic("zero epoch")
|
||||||
|
case !prm.trustSet:
|
||||||
|
panic("current trust value not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
// form request body
|
||||||
|
reqBody := new(v2reputation.AnnounceIntermediateResultRequestBody)
|
||||||
|
reqBody.SetEpoch(prm.epoch)
|
||||||
|
reqBody.SetIteration(prm.iter)
|
||||||
|
reqBody.SetTrust(prm.trust.ToV2())
|
||||||
|
|
||||||
|
// form request
|
||||||
|
var req v2reputation.AnnounceIntermediateResultRequest
|
||||||
|
|
||||||
|
req.SetBody(reqBody)
|
||||||
|
|
||||||
|
// init call context
|
||||||
|
|
||||||
|
var (
|
||||||
|
cc contextCall
|
||||||
|
res ResAnnounceIntermediateTrust
|
||||||
|
)
|
||||||
|
|
||||||
|
c.initCallContext(&cc)
|
||||||
|
cc.meta = prm.prmCommonMeta
|
||||||
|
cc.req = &req
|
||||||
|
cc.statusRes = &res
|
||||||
|
cc.call = func() (responseV2, error) {
|
||||||
|
return rpcapi.AnnounceIntermediateResult(&c.c, &req, client.WithContext(ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
// process call
|
||||||
|
if !cc.processCall() {
|
||||||
|
return nil, cc.err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &res, nil
|
||||||
|
}
|
|
@ -1,12 +1,10 @@
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
import "github.com/nspcc-dev/neofs-api-go/v2/session"
|
||||||
|
|
||||||
// ResponseMetaInfo groups meta information about any FrostFS API response.
|
// ResponseMetaInfo groups meta information about any NeoFS API response.
|
||||||
type ResponseMetaInfo struct {
|
type ResponseMetaInfo struct {
|
||||||
key []byte
|
key []byte
|
||||||
|
|
||||||
epoch uint64
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type responseV2 interface {
|
type responseV2 interface {
|
||||||
|
@ -20,8 +18,3 @@ type responseV2 interface {
|
||||||
func (x ResponseMetaInfo) ResponderKey() []byte {
|
func (x ResponseMetaInfo) ResponderKey() []byte {
|
||||||
return x.key
|
return x.key
|
||||||
}
|
}
|
||||||
|
|
||||||
// Epoch returns local FrostFS epoch of the server.
|
|
||||||
func (x ResponseMetaInfo) Epoch() uint64 {
|
|
||||||
return x.epoch
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,64 +2,23 @@ package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/ecdsa"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
|
||||||
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
|
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
|
||||||
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
"github.com/nspcc-dev/neofs-sdk-go/owner"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
|
|
||||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// PrmSessionCreate groups parameters of SessionCreate operation.
|
// PrmSessionCreate groups parameters of SessionCreate operation.
|
||||||
type PrmSessionCreate struct {
|
type PrmSessionCreate struct {
|
||||||
XHeaders []string
|
prmCommonMeta
|
||||||
|
|
||||||
Expiration uint64
|
exp uint64
|
||||||
|
|
||||||
Key *ecdsa.PrivateKey
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetExp sets number of the last NepFS epoch in the lifetime of the session after which it will be expired.
|
// SetExp sets number of the last NepFS epoch in the lifetime of the session after which it will be expired.
|
||||||
//
|
|
||||||
// Deprecated: Use PrmSessionCreate.Expiration instead.
|
|
||||||
func (x *PrmSessionCreate) SetExp(exp uint64) {
|
func (x *PrmSessionCreate) SetExp(exp uint64) {
|
||||||
x.Expiration = exp
|
x.exp = exp
|
||||||
}
|
|
||||||
|
|
||||||
// UseKey specifies private key to sign the requests and compute token owner.
|
|
||||||
// If key is not provided, then Client default key is used.
|
|
||||||
//
|
|
||||||
// Deprecated: Use PrmSessionCreate.Key instead.
|
|
||||||
func (x *PrmSessionCreate) UseKey(key ecdsa.PrivateKey) {
|
|
||||||
x.Key = &key
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *PrmSessionCreate) buildRequest(c *Client) (*v2session.CreateRequest, error) {
|
|
||||||
ownerKey := c.prm.Key.PublicKey
|
|
||||||
if x.Key != nil {
|
|
||||||
ownerKey = x.Key.PublicKey
|
|
||||||
}
|
|
||||||
var ownerID user.ID
|
|
||||||
user.IDFromKey(&ownerID, ownerKey)
|
|
||||||
|
|
||||||
var ownerIDV2 refs.OwnerID
|
|
||||||
ownerID.WriteToV2(&ownerIDV2)
|
|
||||||
|
|
||||||
reqBody := new(v2session.CreateRequestBody)
|
|
||||||
reqBody.SetOwnerID(&ownerIDV2)
|
|
||||||
reqBody.SetExpiration(x.Expiration)
|
|
||||||
|
|
||||||
var meta v2session.RequestMetaHeader
|
|
||||||
writeXHeadersToMeta(x.XHeaders, &meta)
|
|
||||||
|
|
||||||
var req v2session.CreateRequest
|
|
||||||
req.SetBody(reqBody)
|
|
||||||
c.prepareRequest(&req, &meta)
|
|
||||||
return &req, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResSessionCreate groups resulting values of SessionCreate operation.
|
// ResSessionCreate groups resulting values of SessionCreate operation.
|
||||||
|
@ -71,14 +30,22 @@ type ResSessionCreate struct {
|
||||||
sessionKey []byte
|
sessionKey []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// ID returns identifier of the opened session in a binary FrostFS API protocol format.
|
func (x *ResSessionCreate) setID(id []byte) {
|
||||||
|
x.id = id
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID returns identifier of the opened session in a binary NeoFS API protocol format.
|
||||||
//
|
//
|
||||||
// Client doesn't retain value so modification is safe.
|
// Client doesn't retain value so modification is safe.
|
||||||
func (x ResSessionCreate) ID() []byte {
|
func (x ResSessionCreate) ID() []byte {
|
||||||
return x.id
|
return x.id
|
||||||
}
|
}
|
||||||
|
|
||||||
// PublicKey returns public key of the opened session in a binary FrostFS API protocol format.
|
func (x *ResSessionCreate) setSessionKey(key []byte) {
|
||||||
|
x.sessionKey = key
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublicKey returns public key of the opened session in a binary NeoFS API protocol format.
|
||||||
func (x ResSessionCreate) PublicKey() []byte {
|
func (x ResSessionCreate) PublicKey() []byte {
|
||||||
return x.sessionKey
|
return x.sessionKey
|
||||||
}
|
}
|
||||||
|
@ -89,38 +56,60 @@ func (x ResSessionCreate) PublicKey() []byte {
|
||||||
//
|
//
|
||||||
// Exactly one return value is non-nil. By default, server status is returned in res structure.
|
// Exactly one return value is non-nil. By default, server status is returned in res structure.
|
||||||
// Any client's internal or transport errors are returned as `error`.
|
// Any client's internal or transport errors are returned as `error`.
|
||||||
// If PrmInit.DisableFrostFSFailuresResolution has been called, unsuccessful
|
// If WithNeoFSErrorParsing option has been provided, unsuccessful
|
||||||
// FrostFS status codes are included in the returned result structure,
|
// NeoFS status codes are returned as `error`, otherwise, are included
|
||||||
// otherwise, are also returned as `error`.
|
// in the returned result structure.
|
||||||
//
|
//
|
||||||
// Returns an error if parameters are set incorrectly (see PrmSessionCreate docs).
|
// Immediately panics if parameters are set incorrectly (see PrmSessionCreate docs).
|
||||||
// Context is required and must not be nil. It is used for network communication.
|
// Context is required and must not be nil. It is used for network communication.
|
||||||
//
|
//
|
||||||
// Return statuses:
|
// Return statuses:
|
||||||
// - global (see Client docs).
|
// - global (see Client docs).
|
||||||
func (c *Client) SessionCreate(ctx context.Context, prm PrmSessionCreate) (*ResSessionCreate, error) {
|
func (c *Client) SessionCreate(ctx context.Context, prm PrmSessionCreate) (*ResSessionCreate, error) {
|
||||||
req, err := prm.buildRequest(c)
|
// check context
|
||||||
if err != nil {
|
if ctx == nil {
|
||||||
return nil, err
|
panic(panicMsgMissingContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := signature.SignServiceMessage(&c.prm.Key, req); err != nil {
|
ownerID := owner.NewIDFromPublicKey(&c.prm.key.PublicKey)
|
||||||
return nil, fmt.Errorf("sign request: %w", err)
|
|
||||||
|
// form request body
|
||||||
|
reqBody := new(v2session.CreateRequestBody)
|
||||||
|
reqBody.SetOwnerID(ownerID.ToV2())
|
||||||
|
reqBody.SetExpiration(prm.exp)
|
||||||
|
|
||||||
|
// for request
|
||||||
|
var req v2session.CreateRequest
|
||||||
|
|
||||||
|
req.SetBody(reqBody)
|
||||||
|
|
||||||
|
// init call context
|
||||||
|
|
||||||
|
var (
|
||||||
|
cc contextCall
|
||||||
|
res ResSessionCreate
|
||||||
|
)
|
||||||
|
|
||||||
|
c.initCallContext(&cc)
|
||||||
|
cc.meta = prm.prmCommonMeta
|
||||||
|
cc.req = &req
|
||||||
|
cc.statusRes = &res
|
||||||
|
cc.call = func() (responseV2, error) {
|
||||||
|
return rpcapi.CreateSession(&c.c, &req, client.WithContext(ctx))
|
||||||
|
}
|
||||||
|
cc.result = func(r responseV2) {
|
||||||
|
resp := r.(*v2session.CreateResponse)
|
||||||
|
|
||||||
|
body := resp.GetBody()
|
||||||
|
|
||||||
|
res.setID(body.GetID())
|
||||||
|
res.setSessionKey(body.GetSessionKey())
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := rpcapi.CreateSession(&c.c, req, client.WithContext(ctx))
|
// process call
|
||||||
if err != nil {
|
if !cc.processCall() {
|
||||||
return nil, err
|
return nil, cc.err
|
||||||
}
|
}
|
||||||
|
|
||||||
var res ResSessionCreate
|
|
||||||
res.st, err = c.processResponse(resp)
|
|
||||||
if err != nil || !apistatus.IsSuccessful(res.st) {
|
|
||||||
return &res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
body := resp.GetBody()
|
|
||||||
res.id = body.GetID()
|
|
||||||
res.sessionKey = body.GetSessionKey()
|
|
||||||
return &res, nil
|
return &res, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,53 +0,0 @@
|
||||||
package apistatus
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/apemanager"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status"
|
|
||||||
)
|
|
||||||
|
|
||||||
// APEManagerAccessDenied describes status of the failure because of the access control violation.
|
|
||||||
// Instances provide Status and StatusV2 interfaces.
|
|
||||||
type APEManagerAccessDenied struct {
|
|
||||||
v2 status.Status
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultAPEManagerAccessDeniedMsg = "apemanager access denied"
|
|
||||||
|
|
||||||
func (x *APEManagerAccessDenied) Error() string {
|
|
||||||
msg := x.v2.Message()
|
|
||||||
if msg == "" {
|
|
||||||
msg = defaultAPEManagerAccessDeniedMsg
|
|
||||||
}
|
|
||||||
|
|
||||||
return errMessageStatusV2(
|
|
||||||
globalizeCodeV2(apemanager.StatusAPEManagerAccessDenied, apemanager.GlobalizeFail),
|
|
||||||
msg,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *APEManagerAccessDenied) fromStatusV2(st *status.Status) {
|
|
||||||
x.v2 = *st
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToStatusV2 converts APEManagerAccessDenied to v2's Status.
|
|
||||||
// If the value was returned by FromStatusV2, returns the source message.
|
|
||||||
// Otherwise, returns message with
|
|
||||||
// - code: APE_MANAGER_ACCESS_DENIED;
|
|
||||||
// - string message: "apemanager access denied";
|
|
||||||
// - details: empty.
|
|
||||||
func (x APEManagerAccessDenied) ToStatusV2() *status.Status {
|
|
||||||
x.v2.SetCode(globalizeCodeV2(apemanager.StatusAPEManagerAccessDenied, apemanager.GlobalizeFail))
|
|
||||||
x.v2.SetMessage(defaultAPEManagerAccessDeniedMsg)
|
|
||||||
return &x.v2
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteReason writes human-readable access rejection reason.
|
|
||||||
func (x *APEManagerAccessDenied) WriteReason(reason string) {
|
|
||||||
apemanager.WriteAccessDeniedDesc(&x.v2, reason)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reason returns human-readable access rejection reason returned by the server.
|
|
||||||
// Returns empty value is reason is not presented.
|
|
||||||
func (x APEManagerAccessDenied) Reason() string {
|
|
||||||
return apemanager.ReadAccessDeniedDesc(x.v2)
|
|
||||||
}
|
|
|
@ -3,7 +3,7 @@ package apistatus
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status"
|
"github.com/nspcc-dev/neofs-api-go/v2/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ServerInternal describes failure statuses related to internal server errors.
|
// ServerInternal describes failure statuses related to internal server errors.
|
||||||
|
@ -14,7 +14,7 @@ type ServerInternal struct {
|
||||||
v2 status.Status
|
v2 status.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ServerInternal) Error() string {
|
func (x ServerInternal) Error() string {
|
||||||
return errMessageStatusV2(
|
return errMessageStatusV2(
|
||||||
globalizeCodeV2(status.Internal, status.GlobalizeCommonFail),
|
globalizeCodeV2(status.Internal, status.GlobalizeCommonFail),
|
||||||
x.v2.Message(),
|
x.v2.Message(),
|
||||||
|
@ -29,9 +29,9 @@ func (x *ServerInternal) fromStatusV2(st *status.Status) {
|
||||||
// ToStatusV2 implements StatusV2 interface method.
|
// ToStatusV2 implements StatusV2 interface method.
|
||||||
// If the value was returned by FromStatusV2, returns the source message.
|
// If the value was returned by FromStatusV2, returns the source message.
|
||||||
// Otherwise, returns message with
|
// Otherwise, returns message with
|
||||||
// - code: INTERNAL;
|
// * code: INTERNAL;
|
||||||
// - string message: empty;
|
// * string message: empty;
|
||||||
// - details: empty.
|
// * details: empty.
|
||||||
func (x ServerInternal) ToStatusV2() *status.Status {
|
func (x ServerInternal) ToStatusV2() *status.Status {
|
||||||
x.v2.SetCode(globalizeCodeV2(status.Internal, status.GlobalizeCommonFail))
|
x.v2.SetCode(globalizeCodeV2(status.Internal, status.GlobalizeCommonFail))
|
||||||
return &x.v2
|
return &x.v2
|
||||||
|
@ -62,7 +62,7 @@ type WrongMagicNumber struct {
|
||||||
v2 status.Status
|
v2 status.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *WrongMagicNumber) Error() string {
|
func (x WrongMagicNumber) Error() string {
|
||||||
return errMessageStatusV2(
|
return errMessageStatusV2(
|
||||||
globalizeCodeV2(status.WrongMagicNumber, status.GlobalizeCommonFail),
|
globalizeCodeV2(status.WrongMagicNumber, status.GlobalizeCommonFail),
|
||||||
x.v2.Message(),
|
x.v2.Message(),
|
||||||
|
@ -77,9 +77,9 @@ func (x *WrongMagicNumber) fromStatusV2(st *status.Status) {
|
||||||
// ToStatusV2 implements StatusV2 interface method.
|
// ToStatusV2 implements StatusV2 interface method.
|
||||||
// If the value was returned by FromStatusV2, returns the source message.
|
// If the value was returned by FromStatusV2, returns the source message.
|
||||||
// Otherwise, returns message with
|
// Otherwise, returns message with
|
||||||
// - code: WRONG_MAGIC_NUMBER;
|
// * code: WRONG_MAGIC_NUMBER;
|
||||||
// - string message: empty;
|
// * string message: empty;
|
||||||
// - details: empty.
|
// * details: empty.
|
||||||
func (x WrongMagicNumber) ToStatusV2() *status.Status {
|
func (x WrongMagicNumber) ToStatusV2() *status.Status {
|
||||||
x.v2.SetCode(globalizeCodeV2(status.WrongMagicNumber, status.GlobalizeCommonFail))
|
x.v2.SetCode(globalizeCodeV2(status.WrongMagicNumber, status.GlobalizeCommonFail))
|
||||||
return &x.v2
|
return &x.v2
|
||||||
|
@ -104,9 +104,9 @@ func (x *WrongMagicNumber) WriteCorrectMagic(magic uint64) {
|
||||||
|
|
||||||
// CorrectMagic returns network magic returned by the server.
|
// CorrectMagic returns network magic returned by the server.
|
||||||
// Second value indicates presence status:
|
// Second value indicates presence status:
|
||||||
// - -1 if number is presented in incorrect format
|
// * -1 if number is presented in incorrect format
|
||||||
// - 0 if number is not presented
|
// * 0 if number is not presented
|
||||||
// - +1 otherwise
|
// * +1 otherwise
|
||||||
func (x WrongMagicNumber) CorrectMagic() (magic uint64, ok int8) {
|
func (x WrongMagicNumber) CorrectMagic() (magic uint64, ok int8) {
|
||||||
x.v2.IterateDetails(func(d *status.Detail) bool {
|
x.v2.IterateDetails(func(d *status.Detail) bool {
|
||||||
if d.ID() == status.DetailIDCorrectMagic {
|
if d.ID() == status.DetailIDCorrectMagic {
|
||||||
|
@ -123,118 +123,3 @@ func (x WrongMagicNumber) CorrectMagic() (magic uint64, ok int8) {
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// SignatureVerification describes failure status related to signature verification.
|
|
||||||
// Instances provide Status and StatusV2 interfaces.
|
|
||||||
type SignatureVerification struct {
|
|
||||||
v2 status.Status
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultSignatureVerificationMsg = "signature verification failed"
|
|
||||||
|
|
||||||
func (x *SignatureVerification) Error() string {
|
|
||||||
msg := x.v2.Message()
|
|
||||||
if msg == "" {
|
|
||||||
msg = defaultSignatureVerificationMsg
|
|
||||||
}
|
|
||||||
|
|
||||||
return errMessageStatusV2(
|
|
||||||
globalizeCodeV2(status.SignatureVerificationFail, status.GlobalizeCommonFail),
|
|
||||||
msg,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// implements local interface defined in FromStatusV2 func.
|
|
||||||
func (x *SignatureVerification) fromStatusV2(st *status.Status) {
|
|
||||||
x.v2 = *st
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToStatusV2 implements StatusV2 interface method.
|
|
||||||
// If the value was returned by FromStatusV2, returns the source message.
|
|
||||||
// Otherwise, returns message with
|
|
||||||
// - code: SIGNATURE_VERIFICATION_FAIL;
|
|
||||||
// - string message: written message via SetMessage or
|
|
||||||
// "signature verification failed" as a default message;
|
|
||||||
// - details: empty.
|
|
||||||
func (x SignatureVerification) ToStatusV2() *status.Status {
|
|
||||||
x.v2.SetCode(globalizeCodeV2(status.SignatureVerificationFail, status.GlobalizeCommonFail))
|
|
||||||
|
|
||||||
if x.v2.Message() == "" {
|
|
||||||
x.v2.SetMessage(defaultSignatureVerificationMsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &x.v2
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetMessage writes signature verification failure message.
|
|
||||||
// Message should be used for debug purposes only.
|
|
||||||
//
|
|
||||||
// See also Message.
|
|
||||||
func (x *SignatureVerification) SetMessage(v string) {
|
|
||||||
x.v2.SetMessage(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Message returns status message. Zero status returns empty message.
|
|
||||||
// Message should be used for debug purposes only.
|
|
||||||
//
|
|
||||||
// See also SetMessage.
|
|
||||||
func (x SignatureVerification) Message() string {
|
|
||||||
return x.v2.Message()
|
|
||||||
}
|
|
||||||
|
|
||||||
// NodeUnderMaintenance describes failure status for nodes being under maintenance.
|
|
||||||
// Instances provide Status and StatusV2 interfaces.
|
|
||||||
type NodeUnderMaintenance struct {
|
|
||||||
v2 status.Status
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultNodeUnderMaintenanceMsg = "node is under maintenance"
|
|
||||||
|
|
||||||
// Error implements the error interface.
|
|
||||||
func (x *NodeUnderMaintenance) Error() string {
|
|
||||||
msg := x.Message()
|
|
||||||
if msg == "" {
|
|
||||||
msg = defaultNodeUnderMaintenanceMsg
|
|
||||||
}
|
|
||||||
|
|
||||||
return errMessageStatusV2(
|
|
||||||
globalizeCodeV2(status.NodeUnderMaintenance, status.GlobalizeCommonFail),
|
|
||||||
msg,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *NodeUnderMaintenance) fromStatusV2(st *status.Status) {
|
|
||||||
x.v2 = *st
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToStatusV2 implements StatusV2 interface method.
|
|
||||||
// If the value was returned by FromStatusV2, returns the source message.
|
|
||||||
// Otherwise, returns message with
|
|
||||||
// - code: NODE_UNDER_MAINTENANCE;
|
|
||||||
// - string message: written message via SetMessage or
|
|
||||||
// "node is under maintenance" as a default message;
|
|
||||||
// - details: empty.
|
|
||||||
func (x NodeUnderMaintenance) ToStatusV2() *status.Status {
|
|
||||||
x.v2.SetCode(globalizeCodeV2(status.NodeUnderMaintenance, status.GlobalizeCommonFail))
|
|
||||||
if x.v2.Message() == "" {
|
|
||||||
x.v2.SetMessage(defaultNodeUnderMaintenanceMsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &x.v2
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetMessage writes signature verification failure message.
|
|
||||||
// Message should be used for debug purposes only.
|
|
||||||
//
|
|
||||||
// See also Message.
|
|
||||||
func (x *NodeUnderMaintenance) SetMessage(v string) {
|
|
||||||
x.v2.SetMessage(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Message returns status message. Zero status returns empty message.
|
|
||||||
// Message should be used for debug purposes only.
|
|
||||||
//
|
|
||||||
// See also SetMessage.
|
|
||||||
func (x NodeUnderMaintenance) Message() string {
|
|
||||||
return x.v2.Message()
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,8 +3,8 @@ package apistatus_test
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status"
|
"github.com/nspcc-dev/neofs-api-go/v2/status"
|
||||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -50,81 +50,3 @@ func TestWrongMagicNumber_CorrectMagic(t *testing.T) {
|
||||||
_, ok = st.CorrectMagic()
|
_, ok = st.CorrectMagic()
|
||||||
require.EqualValues(t, -1, ok)
|
require.EqualValues(t, -1, ok)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSignatureVerification(t *testing.T) {
|
|
||||||
t.Run("default", func(t *testing.T) {
|
|
||||||
var st apistatus.SignatureVerification
|
|
||||||
|
|
||||||
require.Empty(t, st.Message())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("custom message", func(t *testing.T) {
|
|
||||||
var st apistatus.SignatureVerification
|
|
||||||
msg := "some message"
|
|
||||||
|
|
||||||
st.SetMessage(msg)
|
|
||||||
|
|
||||||
stV2 := st.ToStatusV2()
|
|
||||||
|
|
||||||
require.Equal(t, msg, st.Message())
|
|
||||||
require.Equal(t, msg, stV2.Message())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("empty to V2", func(t *testing.T) {
|
|
||||||
var st apistatus.SignatureVerification
|
|
||||||
|
|
||||||
stV2 := st.ToStatusV2()
|
|
||||||
|
|
||||||
require.Equal(t, "signature verification failed", stV2.Message())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("non-empty to V2", func(t *testing.T) {
|
|
||||||
var st apistatus.SignatureVerification
|
|
||||||
msg := "some other msg"
|
|
||||||
|
|
||||||
st.SetMessage(msg)
|
|
||||||
|
|
||||||
stV2 := st.ToStatusV2()
|
|
||||||
|
|
||||||
require.Equal(t, msg, stV2.Message())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNodeUnderMaintenance(t *testing.T) {
|
|
||||||
t.Run("default", func(t *testing.T) {
|
|
||||||
var st apistatus.NodeUnderMaintenance
|
|
||||||
|
|
||||||
require.Empty(t, st.Message())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("custom message", func(t *testing.T) {
|
|
||||||
var st apistatus.NodeUnderMaintenance
|
|
||||||
msg := "some message"
|
|
||||||
|
|
||||||
st.SetMessage(msg)
|
|
||||||
|
|
||||||
stV2 := st.ToStatusV2()
|
|
||||||
|
|
||||||
require.Equal(t, msg, st.Message())
|
|
||||||
require.Equal(t, msg, stV2.Message())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("empty to V2", func(t *testing.T) {
|
|
||||||
var st apistatus.NodeUnderMaintenance
|
|
||||||
|
|
||||||
stV2 := st.ToStatusV2()
|
|
||||||
|
|
||||||
require.Empty(t, "", stV2.Message())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("non-empty to V2", func(t *testing.T) {
|
|
||||||
var st apistatus.NodeUnderMaintenance
|
|
||||||
msg := "some other msg"
|
|
||||||
|
|
||||||
st.SetMessage(msg)
|
|
||||||
|
|
||||||
stV2 := st.ToStatusV2()
|
|
||||||
|
|
||||||
require.Equal(t, msg, stV2.Message())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package apistatus
|
package apistatus
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
|
"github.com/nspcc-dev/neofs-api-go/v2/container"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status"
|
"github.com/nspcc-dev/neofs-api-go/v2/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ContainerNotFound describes status of the failure because of the missing container.
|
// ContainerNotFound describes status of the failure because of the missing container.
|
||||||
|
@ -11,17 +11,10 @@ type ContainerNotFound struct {
|
||||||
v2 status.Status
|
v2 status.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultContainerNotFoundMsg = "container not found"
|
func (x ContainerNotFound) Error() string {
|
||||||
|
|
||||||
func (x *ContainerNotFound) Error() string {
|
|
||||||
msg := x.v2.Message()
|
|
||||||
if msg == "" {
|
|
||||||
msg = defaultContainerNotFoundMsg
|
|
||||||
}
|
|
||||||
|
|
||||||
return errMessageStatusV2(
|
return errMessageStatusV2(
|
||||||
globalizeCodeV2(container.StatusNotFound, container.GlobalizeFail),
|
globalizeCodeV2(container.StatusNotFound, container.GlobalizeFail),
|
||||||
msg,
|
x.v2.Message(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,49 +26,11 @@ func (x *ContainerNotFound) fromStatusV2(st *status.Status) {
|
||||||
// ToStatusV2 implements StatusV2 interface method.
|
// ToStatusV2 implements StatusV2 interface method.
|
||||||
// If the value was returned by FromStatusV2, returns the source message.
|
// If the value was returned by FromStatusV2, returns the source message.
|
||||||
// Otherwise, returns message with
|
// Otherwise, returns message with
|
||||||
// - code: CONTAINER_NOT_FOUND;
|
// * code: CONTAINER_NOT_FOUND;
|
||||||
// - string message: "container not found";
|
// * string message: "container not found";
|
||||||
// - details: empty.
|
// * details: empty.
|
||||||
func (x ContainerNotFound) ToStatusV2() *status.Status {
|
func (x ContainerNotFound) ToStatusV2() *status.Status {
|
||||||
x.v2.SetCode(globalizeCodeV2(container.StatusNotFound, container.GlobalizeFail))
|
x.v2.SetCode(globalizeCodeV2(container.StatusNotFound, container.GlobalizeFail))
|
||||||
x.v2.SetMessage(defaultContainerNotFoundMsg)
|
x.v2.SetMessage("container not found")
|
||||||
return &x.v2
|
|
||||||
}
|
|
||||||
|
|
||||||
// EACLNotFound describes status of the failure because of the missing eACL
|
|
||||||
// table.
|
|
||||||
// Instances provide Status and StatusV2 interfaces.
|
|
||||||
type EACLNotFound struct {
|
|
||||||
v2 status.Status
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultEACLNotFoundMsg = "eACL not found"
|
|
||||||
|
|
||||||
func (x *EACLNotFound) Error() string {
|
|
||||||
msg := x.v2.Message()
|
|
||||||
if msg == "" {
|
|
||||||
msg = defaultEACLNotFoundMsg
|
|
||||||
}
|
|
||||||
|
|
||||||
return errMessageStatusV2(
|
|
||||||
globalizeCodeV2(container.StatusEACLNotFound, container.GlobalizeFail),
|
|
||||||
msg,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// implements local interface defined in FromStatusV2 func.
|
|
||||||
func (x *EACLNotFound) fromStatusV2(st *status.Status) {
|
|
||||||
x.v2 = *st
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToStatusV2 implements StatusV2 interface method.
|
|
||||||
// If the value was returned by FromStatusV2, returns the source message.
|
|
||||||
// Otherwise, returns message with
|
|
||||||
// - code: EACL_NOT_FOUND;
|
|
||||||
// - string message: "eACL not found";
|
|
||||||
// - details: empty.
|
|
||||||
func (x EACLNotFound) ToStatusV2() *status.Status {
|
|
||||||
x.v2.SetCode(globalizeCodeV2(container.StatusEACLNotFound, container.GlobalizeFail))
|
|
||||||
x.v2.SetMessage(defaultEACLNotFoundMsg)
|
|
||||||
return &x.v2
|
return &x.v2
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package apistatus
|
package apistatus
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
|
"github.com/nspcc-dev/neofs-api-go/v2/object"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status"
|
"github.com/nspcc-dev/neofs-api-go/v2/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ObjectLocked describes status of the failure because of the locked object.
|
// ObjectLocked describes status of the failure because of the locked object.
|
||||||
|
@ -11,17 +11,10 @@ type ObjectLocked struct {
|
||||||
v2 status.Status
|
v2 status.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultObjectLockedMsg = "object is locked"
|
func (x ObjectLocked) Error() string {
|
||||||
|
|
||||||
func (x *ObjectLocked) Error() string {
|
|
||||||
msg := x.v2.Message()
|
|
||||||
if msg == "" {
|
|
||||||
msg = defaultObjectLockedMsg
|
|
||||||
}
|
|
||||||
|
|
||||||
return errMessageStatusV2(
|
return errMessageStatusV2(
|
||||||
globalizeCodeV2(object.StatusLocked, object.GlobalizeFail),
|
globalizeCodeV2(object.StatusLocked, object.GlobalizeFail),
|
||||||
msg,
|
x.v2.Message(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,12 +26,12 @@ func (x *ObjectLocked) fromStatusV2(st *status.Status) {
|
||||||
// ToStatusV2 implements StatusV2 interface method.
|
// ToStatusV2 implements StatusV2 interface method.
|
||||||
// If the value was returned by FromStatusV2, returns the source message.
|
// If the value was returned by FromStatusV2, returns the source message.
|
||||||
// Otherwise, returns message with
|
// Otherwise, returns message with
|
||||||
// - code: LOCKED;
|
// * code: LOCKED;
|
||||||
// - string message: "object is locked";
|
// * string message: "object is locked";
|
||||||
// - details: empty.
|
// * details: empty.
|
||||||
func (x ObjectLocked) ToStatusV2() *status.Status {
|
func (x ObjectLocked) ToStatusV2() *status.Status {
|
||||||
x.v2.SetCode(globalizeCodeV2(object.StatusLocked, object.GlobalizeFail))
|
x.v2.SetCode(globalizeCodeV2(object.StatusLocked, object.GlobalizeFail))
|
||||||
x.v2.SetMessage(defaultObjectLockedMsg)
|
x.v2.SetMessage("object is locked")
|
||||||
return &x.v2
|
return &x.v2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,17 +41,10 @@ type LockNonRegularObject struct {
|
||||||
v2 status.Status
|
v2 status.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultLockNonRegularObjectMsg = "locking non-regular object is forbidden"
|
func (x LockNonRegularObject) Error() string {
|
||||||
|
|
||||||
func (x *LockNonRegularObject) Error() string {
|
|
||||||
msg := x.v2.Message()
|
|
||||||
if msg == "" {
|
|
||||||
msg = defaultLockNonRegularObjectMsg
|
|
||||||
}
|
|
||||||
|
|
||||||
return errMessageStatusV2(
|
return errMessageStatusV2(
|
||||||
globalizeCodeV2(object.StatusLockNonRegularObject, object.GlobalizeFail),
|
globalizeCodeV2(object.StatusLockNonRegularObject, object.GlobalizeFail),
|
||||||
msg,
|
x.v2.Message(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,12 +56,12 @@ func (x *LockNonRegularObject) fromStatusV2(st *status.Status) {
|
||||||
// ToStatusV2 implements StatusV2 interface method.
|
// ToStatusV2 implements StatusV2 interface method.
|
||||||
// If the value was returned by FromStatusV2, returns the source message.
|
// If the value was returned by FromStatusV2, returns the source message.
|
||||||
// Otherwise, returns message with
|
// Otherwise, returns message with
|
||||||
// - code: LOCK_NON_REGULAR_OBJECT;
|
// * code: LOCK_NON_REGULAR_OBJECT;
|
||||||
// - string message: "locking non-regular object is forbidden";
|
// * string message: "locking non-regular object is forbidden";
|
||||||
// - details: empty.
|
// * details: empty.
|
||||||
func (x LockNonRegularObject) ToStatusV2() *status.Status {
|
func (x LockNonRegularObject) ToStatusV2() *status.Status {
|
||||||
x.v2.SetCode(globalizeCodeV2(object.StatusLockNonRegularObject, object.GlobalizeFail))
|
x.v2.SetCode(globalizeCodeV2(object.StatusLockNonRegularObject, object.GlobalizeFail))
|
||||||
x.v2.SetMessage(defaultLockNonRegularObjectMsg)
|
x.v2.SetMessage("locking non-regular object is forbidden")
|
||||||
return &x.v2
|
return &x.v2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,17 +71,10 @@ type ObjectAccessDenied struct {
|
||||||
v2 status.Status
|
v2 status.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultObjectAccessDeniedMsg = "access to object operation denied"
|
func (x ObjectAccessDenied) Error() string {
|
||||||
|
|
||||||
func (x *ObjectAccessDenied) Error() string {
|
|
||||||
msg := x.v2.Message()
|
|
||||||
if msg == "" {
|
|
||||||
msg = defaultObjectAccessDeniedMsg
|
|
||||||
}
|
|
||||||
|
|
||||||
return errMessageStatusV2(
|
return errMessageStatusV2(
|
||||||
globalizeCodeV2(object.StatusAccessDenied, object.GlobalizeFail),
|
globalizeCodeV2(object.StatusAccessDenied, object.GlobalizeFail),
|
||||||
msg,
|
x.v2.Message(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,12 +86,12 @@ func (x *ObjectAccessDenied) fromStatusV2(st *status.Status) {
|
||||||
// ToStatusV2 implements StatusV2 interface method.
|
// ToStatusV2 implements StatusV2 interface method.
|
||||||
// If the value was returned by FromStatusV2, returns the source message.
|
// If the value was returned by FromStatusV2, returns the source message.
|
||||||
// Otherwise, returns message with
|
// Otherwise, returns message with
|
||||||
// - code: ACCESS_DENIED;
|
// * code: ACCESS_DENIED;
|
||||||
// - string message: "access to object operation denied";
|
// * string message: "access to object operation denied";
|
||||||
// - details: empty.
|
// * details: empty.
|
||||||
func (x ObjectAccessDenied) ToStatusV2() *status.Status {
|
func (x ObjectAccessDenied) ToStatusV2() *status.Status {
|
||||||
x.v2.SetCode(globalizeCodeV2(object.StatusAccessDenied, object.GlobalizeFail))
|
x.v2.SetCode(globalizeCodeV2(object.StatusAccessDenied, object.GlobalizeFail))
|
||||||
x.v2.SetMessage(defaultObjectAccessDeniedMsg)
|
x.v2.SetMessage("access to object operation denied")
|
||||||
return &x.v2
|
return &x.v2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,17 +112,10 @@ type ObjectNotFound struct {
|
||||||
v2 status.Status
|
v2 status.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultObjectNotFoundMsg = "object not found"
|
func (x ObjectNotFound) Error() string {
|
||||||
|
|
||||||
func (x *ObjectNotFound) Error() string {
|
|
||||||
msg := x.v2.Message()
|
|
||||||
if msg == "" {
|
|
||||||
msg = defaultObjectNotFoundMsg
|
|
||||||
}
|
|
||||||
|
|
||||||
return errMessageStatusV2(
|
return errMessageStatusV2(
|
||||||
globalizeCodeV2(object.StatusNotFound, object.GlobalizeFail),
|
globalizeCodeV2(object.StatusNotFound, object.GlobalizeFail),
|
||||||
msg,
|
x.v2.Message(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,12 +127,12 @@ func (x *ObjectNotFound) fromStatusV2(st *status.Status) {
|
||||||
// ToStatusV2 implements StatusV2 interface method.
|
// ToStatusV2 implements StatusV2 interface method.
|
||||||
// If the value was returned by FromStatusV2, returns the source message.
|
// If the value was returned by FromStatusV2, returns the source message.
|
||||||
// Otherwise, returns message with
|
// Otherwise, returns message with
|
||||||
// - code: OBJECT_NOT_FOUND;
|
// * code: OBJECT_NOT_FOUND;
|
||||||
// - string message: "object not found";
|
// * string message: "object not found";
|
||||||
// - details: empty.
|
// * details: empty.
|
||||||
func (x ObjectNotFound) ToStatusV2() *status.Status {
|
func (x ObjectNotFound) ToStatusV2() *status.Status {
|
||||||
x.v2.SetCode(globalizeCodeV2(object.StatusNotFound, object.GlobalizeFail))
|
x.v2.SetCode(globalizeCodeV2(object.StatusNotFound, object.GlobalizeFail))
|
||||||
x.v2.SetMessage(defaultObjectNotFoundMsg)
|
x.v2.SetMessage("object not found")
|
||||||
return &x.v2
|
return &x.v2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -170,17 +142,10 @@ type ObjectAlreadyRemoved struct {
|
||||||
v2 status.Status
|
v2 status.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultObjectAlreadyRemovedMsg = "object already removed"
|
func (x ObjectAlreadyRemoved) Error() string {
|
||||||
|
|
||||||
func (x *ObjectAlreadyRemoved) Error() string {
|
|
||||||
msg := x.v2.Message()
|
|
||||||
if msg == "" {
|
|
||||||
msg = defaultObjectAlreadyRemovedMsg
|
|
||||||
}
|
|
||||||
|
|
||||||
return errMessageStatusV2(
|
return errMessageStatusV2(
|
||||||
globalizeCodeV2(object.StatusAlreadyRemoved, object.GlobalizeFail),
|
globalizeCodeV2(object.StatusAlreadyRemoved, object.GlobalizeFail),
|
||||||
msg,
|
x.v2.Message(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,49 +157,11 @@ func (x *ObjectAlreadyRemoved) fromStatusV2(st *status.Status) {
|
||||||
// ToStatusV2 implements StatusV2 interface method.
|
// ToStatusV2 implements StatusV2 interface method.
|
||||||
// If the value was returned by FromStatusV2, returns the source message.
|
// If the value was returned by FromStatusV2, returns the source message.
|
||||||
// Otherwise, returns message with
|
// Otherwise, returns message with
|
||||||
// - code: OBJECT_ALREADY_REMOVED;
|
// * code: OBJECT_ALREADY_REMOVED;
|
||||||
// - string message: "object already removed";
|
// * string message: "object already removed";
|
||||||
// - details: empty.
|
// * details: empty.
|
||||||
func (x ObjectAlreadyRemoved) ToStatusV2() *status.Status {
|
func (x ObjectAlreadyRemoved) ToStatusV2() *status.Status {
|
||||||
x.v2.SetCode(globalizeCodeV2(object.StatusAlreadyRemoved, object.GlobalizeFail))
|
x.v2.SetCode(globalizeCodeV2(object.StatusAlreadyRemoved, object.GlobalizeFail))
|
||||||
x.v2.SetMessage(defaultObjectAlreadyRemovedMsg)
|
x.v2.SetMessage("object already removed")
|
||||||
return &x.v2
|
|
||||||
}
|
|
||||||
|
|
||||||
// ObjectOutOfRange describes status of the failure because of the incorrect
|
|
||||||
// provided object ranges.
|
|
||||||
// Instances provide Status and StatusV2 interfaces.
|
|
||||||
type ObjectOutOfRange struct {
|
|
||||||
v2 status.Status
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultObjectOutOfRangeMsg = "out of range"
|
|
||||||
|
|
||||||
func (x *ObjectOutOfRange) Error() string {
|
|
||||||
msg := x.v2.Message()
|
|
||||||
if msg == "" {
|
|
||||||
msg = defaultObjectOutOfRangeMsg
|
|
||||||
}
|
|
||||||
|
|
||||||
return errMessageStatusV2(
|
|
||||||
globalizeCodeV2(object.StatusOutOfRange, object.GlobalizeFail),
|
|
||||||
msg,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// implements local interface defined in FromStatusV2 func.
|
|
||||||
func (x *ObjectOutOfRange) fromStatusV2(st *status.Status) {
|
|
||||||
x.v2 = *st
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToStatusV2 implements StatusV2 interface method.
|
|
||||||
// If the value was returned by FromStatusV2, returns the source message.
|
|
||||||
// Otherwise, returns message with
|
|
||||||
// - code: OUT_OF_RANGE;
|
|
||||||
// - string message: "out of range";
|
|
||||||
// - details: empty.
|
|
||||||
func (x ObjectOutOfRange) ToStatusV2() *status.Status {
|
|
||||||
x.v2.SetCode(globalizeCodeV2(object.StatusOutOfRange, object.GlobalizeFail))
|
|
||||||
x.v2.SetMessage(defaultObjectOutOfRangeMsg)
|
|
||||||
return &x.v2
|
return &x.v2
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ package apistatus_test
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package apistatus
|
package apistatus
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
"github.com/nspcc-dev/neofs-api-go/v2/session"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status"
|
"github.com/nspcc-dev/neofs-api-go/v2/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SessionTokenNotFound describes status of the failure because of the missing session token.
|
// SessionTokenNotFound describes status of the failure because of the missing session token.
|
||||||
|
@ -11,17 +11,10 @@ type SessionTokenNotFound struct {
|
||||||
v2 status.Status
|
v2 status.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultSessionTokenNotFoundMsg = "session token not found"
|
func (x SessionTokenNotFound) Error() string {
|
||||||
|
|
||||||
func (x *SessionTokenNotFound) Error() string {
|
|
||||||
msg := x.v2.Message()
|
|
||||||
if msg == "" {
|
|
||||||
msg = defaultSessionTokenNotFoundMsg
|
|
||||||
}
|
|
||||||
|
|
||||||
return errMessageStatusV2(
|
return errMessageStatusV2(
|
||||||
globalizeCodeV2(session.StatusTokenNotFound, session.GlobalizeFail),
|
globalizeCodeV2(session.StatusTokenNotFound, session.GlobalizeFail),
|
||||||
msg,
|
x.v2.Message(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,12 +26,12 @@ func (x *SessionTokenNotFound) fromStatusV2(st *status.Status) {
|
||||||
// ToStatusV2 implements StatusV2 interface method.
|
// ToStatusV2 implements StatusV2 interface method.
|
||||||
// If the value was returned by FromStatusV2, returns the source message.
|
// If the value was returned by FromStatusV2, returns the source message.
|
||||||
// Otherwise, returns message with
|
// Otherwise, returns message with
|
||||||
// - code: TOKEN_NOT_FOUND;
|
// * code: TOKEN_NOT_FOUND;
|
||||||
// - string message: "session token not found";
|
// * string message: "session token not found";
|
||||||
// - details: empty.
|
// * details: empty.
|
||||||
func (x SessionTokenNotFound) ToStatusV2() *status.Status {
|
func (x SessionTokenNotFound) ToStatusV2() *status.Status {
|
||||||
x.v2.SetCode(globalizeCodeV2(session.StatusTokenNotFound, session.GlobalizeFail))
|
x.v2.SetCode(globalizeCodeV2(session.StatusTokenNotFound, session.GlobalizeFail))
|
||||||
x.v2.SetMessage(defaultSessionTokenNotFoundMsg)
|
x.v2.SetMessage("session token not found")
|
||||||
return &x.v2
|
return &x.v2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,17 +41,10 @@ type SessionTokenExpired struct {
|
||||||
v2 status.Status
|
v2 status.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultSessionTokenExpiredMsg = "expired session token"
|
func (x SessionTokenExpired) Error() string {
|
||||||
|
|
||||||
func (x *SessionTokenExpired) Error() string {
|
|
||||||
msg := x.v2.Message()
|
|
||||||
if msg == "" {
|
|
||||||
msg = defaultSessionTokenExpiredMsg
|
|
||||||
}
|
|
||||||
|
|
||||||
return errMessageStatusV2(
|
return errMessageStatusV2(
|
||||||
globalizeCodeV2(session.StatusTokenExpired, session.GlobalizeFail),
|
globalizeCodeV2(session.StatusTokenExpired, session.GlobalizeFail),
|
||||||
msg,
|
x.v2.Message(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,11 +56,11 @@ func (x *SessionTokenExpired) fromStatusV2(st *status.Status) {
|
||||||
// ToStatusV2 implements StatusV2 interface method.
|
// ToStatusV2 implements StatusV2 interface method.
|
||||||
// If the value was returned by FromStatusV2, returns the source message.
|
// If the value was returned by FromStatusV2, returns the source message.
|
||||||
// Otherwise, returns message with
|
// Otherwise, returns message with
|
||||||
// - code: TOKEN_EXPIRED;
|
// * code: TOKEN_EXPIRED;
|
||||||
// - string message: "expired session token";
|
// * string message: "expired session token";
|
||||||
// - details: empty.
|
// * details: empty.
|
||||||
func (x SessionTokenExpired) ToStatusV2() *status.Status {
|
func (x SessionTokenExpired) ToStatusV2() *status.Status {
|
||||||
x.v2.SetCode(globalizeCodeV2(session.StatusTokenExpired, session.GlobalizeFail))
|
x.v2.SetCode(globalizeCodeV2(session.StatusTokenExpired, session.GlobalizeFail))
|
||||||
x.v2.SetMessage(defaultSessionTokenExpiredMsg)
|
x.v2.SetMessage("expired session token")
|
||||||
return &x.v2
|
return &x.v2
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
package apistatus
|
package apistatus
|
||||||
|
|
||||||
// Status defines a variety of FrostFS API status returns.
|
// Status defines a variety of NeoFS API status returns.
|
||||||
//
|
//
|
||||||
// All statuses are split into two disjoint subsets: successful and failed, and:
|
// All statuses are split into two disjoint subsets: successful and failed, and:
|
||||||
// - statuses that implement the build-in error interface are considered failed statuses;
|
// * statuses that implement the build-in error interface are considered failed statuses;
|
||||||
// - all other value types are considered successes (nil is a default success).
|
// * all other value types are considered successes (nil is a default success).
|
||||||
//
|
//
|
||||||
// In Go code type of success can be determined by a type switch, failure - by a switch with errors.As calls.
|
// In Go code type of success can be determined by a type switch, failure - by a switch with errors.As calls.
|
||||||
// Nil should be considered as a success, and default switch section - as an unrecognized Status.
|
// Nil should be considered as a success, and default switch section - as an unrecognized Status.
|
||||||
|
@ -14,8 +14,8 @@ package apistatus
|
||||||
// IsSuccessful function should be used (try to avoid nil comparison).
|
// IsSuccessful function should be used (try to avoid nil comparison).
|
||||||
// It should be noted that using direct typecasting is not a compatible approach.
|
// It should be noted that using direct typecasting is not a compatible approach.
|
||||||
//
|
//
|
||||||
// To transport statuses using the FrostFS API V2 protocol, see StatusV2 interface and FromStatusV2 and ToStatusV2 functions.
|
// To transport statuses using the NeoFS API V2 protocol, see StatusV2 interface and FromStatusV2 and ToStatusV2 functions.
|
||||||
type Status any
|
type Status interface{}
|
||||||
|
|
||||||
// ErrFromStatus converts Status instance to error if it is failed. Returns nil on successful Status.
|
// ErrFromStatus converts Status instance to error if it is failed. Returns nil on successful Status.
|
||||||
//
|
//
|
||||||
|
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package apistatus
|
package apistatus
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status"
|
"github.com/nspcc-dev/neofs-api-go/v2/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SuccessDefaultV2 represents Status instance of default success. Implements StatusV2.
|
// SuccessDefaultV2 represents Status instance of default success. Implements StatusV2.
|
||||||
|
@ -20,9 +20,9 @@ func (x *SuccessDefaultV2) fromStatusV2(st *status.Status) {
|
||||||
// ToStatusV2 implements StatusV2 interface method.
|
// ToStatusV2 implements StatusV2 interface method.
|
||||||
// If the value was returned by FromStatusV2, returns the source message.
|
// If the value was returned by FromStatusV2, returns the source message.
|
||||||
// Otherwise, returns message with
|
// Otherwise, returns message with
|
||||||
// - code: OK;
|
// * code: OK;
|
||||||
// - string message: empty;
|
// * string message: empty;
|
||||||
// - details: empty.
|
// * details: empty.
|
||||||
func (x SuccessDefaultV2) ToStatusV2() *status.Status {
|
func (x SuccessDefaultV2) ToStatusV2() *status.Status {
|
||||||
if x.isNil || x.v2 != nil {
|
if x.isNil || x.v2 != nil {
|
||||||
return x.v2
|
return x.v2
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
package apistatus
|
package apistatus
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status"
|
"github.com/nspcc-dev/neofs-api-go/v2/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
type unrecognizedStatusV2 struct {
|
type unrecognizedStatusV2 struct {
|
||||||
v2 status.Status
|
v2 status.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *unrecognizedStatusV2) Error() string {
|
func (x unrecognizedStatusV2) Error() string {
|
||||||
return errMessageStatusV2("unrecognized", x.v2.Message())
|
return errMessageStatusV2("unrecognized", x.v2.Message())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,20 +3,19 @@ package apistatus
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/apemanager"
|
"github.com/nspcc-dev/neofs-api-go/v2/container"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
|
"github.com/nspcc-dev/neofs-api-go/v2/object"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
|
"github.com/nspcc-dev/neofs-api-go/v2/session"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
"github.com/nspcc-dev/neofs-api-go/v2/status"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// StatusV2 defines a variety of Status instances compatible with FrostFS API V2 protocol.
|
// StatusV2 defines a variety of Status instances compatible with NeoFS API V2 protocol.
|
||||||
//
|
//
|
||||||
// Note: it is not recommended to use this type directly, it is intended for documentation of the library functionality.
|
// Note: it is not recommended to use this type directly, it is intended for documentation of the library functionality.
|
||||||
type StatusV2 interface {
|
type StatusV2 interface {
|
||||||
Status
|
Status
|
||||||
|
|
||||||
// ToStatusV2 returns the status as git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status.Status message structure.
|
// ToStatusV2 returns the status as github.com/nspcc-dev/neofs-api-go/v2/status.Status message structure.
|
||||||
ToStatusV2() *status.Status
|
ToStatusV2() *status.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,16 +28,15 @@ type StatusV2 interface {
|
||||||
// Note: notice if the return type is a pointer.
|
// Note: notice if the return type is a pointer.
|
||||||
//
|
//
|
||||||
// Successes:
|
// Successes:
|
||||||
// - status.OK: *SuccessDefaultV2 (this also includes nil argument).
|
// * status.OK: *SuccessDefaultV2 (this also includes nil argument).
|
||||||
//
|
//
|
||||||
// Common failures:
|
// Common failures:
|
||||||
// - status.Internal: *ServerInternal;
|
// * status.Internal: *ServerInternal.
|
||||||
// - status.SignatureVerificationFail: *SignatureVerification.
|
|
||||||
//
|
//
|
||||||
// Object failures:
|
// Object failures:
|
||||||
// - object.StatusLocked: *ObjectLocked;
|
// * object.StatusLocked: *ObjectLocked;
|
||||||
// - object.StatusLockNonRegularObject: *LockNonRegularObject.
|
// * object.StatusLockNonRegularObject: *LockNonRegularObject.
|
||||||
// - object.StatusAccessDenied: *ObjectAccessDenied.
|
// * object.StatusAccessDenied: *ObjectAccessDenied.
|
||||||
func FromStatusV2(st *status.Status) Status {
|
func FromStatusV2(st *status.Status) Status {
|
||||||
var decoder interface {
|
var decoder interface {
|
||||||
fromStatusV2(*status.Status)
|
fromStatusV2(*status.Status)
|
||||||
|
@ -57,10 +55,6 @@ func FromStatusV2(st *status.Status) Status {
|
||||||
decoder = new(ServerInternal)
|
decoder = new(ServerInternal)
|
||||||
case status.WrongMagicNumber:
|
case status.WrongMagicNumber:
|
||||||
decoder = new(WrongMagicNumber)
|
decoder = new(WrongMagicNumber)
|
||||||
case status.SignatureVerificationFail:
|
|
||||||
decoder = new(SignatureVerification)
|
|
||||||
case status.NodeUnderMaintenance:
|
|
||||||
decoder = new(NodeUnderMaintenance)
|
|
||||||
}
|
}
|
||||||
case object.LocalizeFailStatus(&code):
|
case object.LocalizeFailStatus(&code):
|
||||||
switch code {
|
switch code {
|
||||||
|
@ -74,16 +68,12 @@ func FromStatusV2(st *status.Status) Status {
|
||||||
decoder = new(ObjectNotFound)
|
decoder = new(ObjectNotFound)
|
||||||
case object.StatusAlreadyRemoved:
|
case object.StatusAlreadyRemoved:
|
||||||
decoder = new(ObjectAlreadyRemoved)
|
decoder = new(ObjectAlreadyRemoved)
|
||||||
case object.StatusOutOfRange:
|
|
||||||
decoder = new(ObjectOutOfRange)
|
|
||||||
}
|
}
|
||||||
case container.LocalizeFailStatus(&code):
|
case container.LocalizeFailStatus(&code):
|
||||||
//nolint:exhaustive
|
//nolint:exhaustive
|
||||||
switch code {
|
switch code {
|
||||||
case container.StatusNotFound:
|
case container.StatusNotFound:
|
||||||
decoder = new(ContainerNotFound)
|
decoder = new(ContainerNotFound)
|
||||||
case container.StatusEACLNotFound:
|
|
||||||
decoder = new(EACLNotFound)
|
|
||||||
}
|
}
|
||||||
case session.LocalizeFailStatus(&code):
|
case session.LocalizeFailStatus(&code):
|
||||||
//nolint:exhaustive
|
//nolint:exhaustive
|
||||||
|
@ -93,12 +83,6 @@ func FromStatusV2(st *status.Status) Status {
|
||||||
case session.StatusTokenExpired:
|
case session.StatusTokenExpired:
|
||||||
decoder = new(SessionTokenExpired)
|
decoder = new(SessionTokenExpired)
|
||||||
}
|
}
|
||||||
case apemanager.LocalizeFailStatus(&code):
|
|
||||||
//nolint:exhaustive
|
|
||||||
switch code {
|
|
||||||
case apemanager.StatusAPEManagerAccessDenied:
|
|
||||||
decoder = new(APEManagerAccessDenied)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if decoder == nil {
|
if decoder == nil {
|
||||||
|
@ -113,8 +97,7 @@ func FromStatusV2(st *status.Status) Status {
|
||||||
// ToStatusV2 converts Status instance to status.Status message structure. Inverse to FromStatusV2 operation.
|
// ToStatusV2 converts Status instance to status.Status message structure. Inverse to FromStatusV2 operation.
|
||||||
//
|
//
|
||||||
// If argument is the StatusV2 instance, it is converted directly.
|
// If argument is the StatusV2 instance, it is converted directly.
|
||||||
// Otherwise, successes are converted with status.OK code w/o details and message,
|
// Otherwise, successes are converted with status.OK code w/o details and message, failures - with status.Internal.
|
||||||
// failures - with status.Internal and error text message w/o details.
|
|
||||||
func ToStatusV2(st Status) *status.Status {
|
func ToStatusV2(st Status) *status.Status {
|
||||||
if v, ok := st.(StatusV2); ok {
|
if v, ok := st.(StatusV2); ok {
|
||||||
return v.ToStatusV2()
|
return v.ToStatusV2()
|
||||||
|
@ -124,13 +107,10 @@ func ToStatusV2(st Status) *status.Status {
|
||||||
return newStatusV2WithLocalCode(status.OK, status.GlobalizeSuccess)
|
return newStatusV2WithLocalCode(status.OK, status.GlobalizeSuccess)
|
||||||
}
|
}
|
||||||
|
|
||||||
internalErrorStatus := newStatusV2WithLocalCode(status.Internal, status.GlobalizeCommonFail)
|
return newStatusV2WithLocalCode(status.Internal, status.GlobalizeCommonFail)
|
||||||
internalErrorStatus.SetMessage(st.(error).Error()) // type cast never panics because IsSuccessful() checks cast
|
|
||||||
|
|
||||||
return internalErrorStatus
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func errMessageStatusV2(code any, msg string) string {
|
func errMessageStatusV2(code interface{}, msg string) string {
|
||||||
const (
|
const (
|
||||||
noMsgFmt = "status: code = %v"
|
noMsgFmt = "status: code = %v"
|
||||||
msgFmt = noMsgFmt + " message = %s"
|
msgFmt = noMsgFmt + " message = %s"
|
||||||
|
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -12,14 +12,12 @@ func TestToStatusV2(t *testing.T) {
|
||||||
type statusConstructor func() apistatus.Status
|
type statusConstructor func() apistatus.Status
|
||||||
|
|
||||||
for _, testItem := range [...]struct {
|
for _, testItem := range [...]struct {
|
||||||
status any // Status or statusConstructor
|
status interface{} // Status or statusConstructor
|
||||||
codeV2 uint64
|
codeV2 uint64
|
||||||
messageV2 string
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
status: errors.New("some error"),
|
status: errors.New("some error"),
|
||||||
codeV2: 1024,
|
codeV2: 1024,
|
||||||
messageV2: "some error",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
status: 1,
|
status: 1,
|
||||||
|
@ -95,24 +93,12 @@ func TestToStatusV2(t *testing.T) {
|
||||||
}),
|
}),
|
||||||
codeV2: 2052,
|
codeV2: 2052,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
status: (statusConstructor)(func() apistatus.Status {
|
|
||||||
return new(apistatus.ObjectOutOfRange)
|
|
||||||
}),
|
|
||||||
codeV2: 2053,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
status: (statusConstructor)(func() apistatus.Status {
|
status: (statusConstructor)(func() apistatus.Status {
|
||||||
return new(apistatus.ContainerNotFound)
|
return new(apistatus.ContainerNotFound)
|
||||||
}),
|
}),
|
||||||
codeV2: 3072,
|
codeV2: 3072,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
status: (statusConstructor)(func() apistatus.Status {
|
|
||||||
return new(apistatus.EACLNotFound)
|
|
||||||
}),
|
|
||||||
codeV2: 3073,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
status: (statusConstructor)(func() apistatus.Status {
|
status: (statusConstructor)(func() apistatus.Status {
|
||||||
return new(apistatus.SessionTokenNotFound)
|
return new(apistatus.SessionTokenNotFound)
|
||||||
|
@ -125,18 +111,6 @@ func TestToStatusV2(t *testing.T) {
|
||||||
}),
|
}),
|
||||||
codeV2: 4097,
|
codeV2: 4097,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
status: (statusConstructor)(func() apistatus.Status {
|
|
||||||
return new(apistatus.APEManagerAccessDenied)
|
|
||||||
}),
|
|
||||||
codeV2: 5120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: (statusConstructor)(func() apistatus.Status {
|
|
||||||
return new(apistatus.NodeUnderMaintenance)
|
|
||||||
}),
|
|
||||||
codeV2: 1027,
|
|
||||||
},
|
|
||||||
} {
|
} {
|
||||||
var st apistatus.Status
|
var st apistatus.Status
|
||||||
|
|
||||||
|
@ -150,9 +124,6 @@ func TestToStatusV2(t *testing.T) {
|
||||||
|
|
||||||
// must generate the same status.Status message
|
// must generate the same status.Status message
|
||||||
require.EqualValues(t, testItem.codeV2, stv2.Code())
|
require.EqualValues(t, testItem.codeV2, stv2.Code())
|
||||||
if len(testItem.messageV2) > 0 {
|
|
||||||
require.Equal(t, testItem.messageV2, stv2.Message())
|
|
||||||
}
|
|
||||||
|
|
||||||
_, ok := st.(apistatus.StatusV2)
|
_, ok := st.(apistatus.StatusV2)
|
||||||
if ok {
|
if ok {
|
||||||
|
@ -171,14 +142,12 @@ func TestFromStatusV2(t *testing.T) {
|
||||||
type statusConstructor func() apistatus.Status
|
type statusConstructor func() apistatus.Status
|
||||||
|
|
||||||
for _, testItem := range [...]struct {
|
for _, testItem := range [...]struct {
|
||||||
status any // Status or statusConstructor
|
status interface{} // Status or statusConstructor
|
||||||
codeV2 uint64
|
codeV2 uint64
|
||||||
messageV2 string
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
status: errors.New("some error"),
|
status: errors.New("some error"),
|
||||||
codeV2: 1024,
|
codeV2: 1024,
|
||||||
messageV2: "some error",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
status: 1,
|
status: 1,
|
||||||
|
@ -254,24 +223,12 @@ func TestFromStatusV2(t *testing.T) {
|
||||||
}),
|
}),
|
||||||
codeV2: 2052,
|
codeV2: 2052,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
status: statusConstructor(func() apistatus.Status {
|
|
||||||
return new(apistatus.ObjectOutOfRange)
|
|
||||||
}),
|
|
||||||
codeV2: 2053,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
status: (statusConstructor)(func() apistatus.Status {
|
status: (statusConstructor)(func() apistatus.Status {
|
||||||
return new(apistatus.ContainerNotFound)
|
return new(apistatus.ContainerNotFound)
|
||||||
}),
|
}),
|
||||||
codeV2: 3072,
|
codeV2: 3072,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
status: (statusConstructor)(func() apistatus.Status {
|
|
||||||
return new(apistatus.EACLNotFound)
|
|
||||||
}),
|
|
||||||
codeV2: 3073,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
status: (statusConstructor)(func() apistatus.Status {
|
status: (statusConstructor)(func() apistatus.Status {
|
||||||
return new(apistatus.SessionTokenNotFound)
|
return new(apistatus.SessionTokenNotFound)
|
||||||
|
@ -284,18 +241,6 @@ func TestFromStatusV2(t *testing.T) {
|
||||||
}),
|
}),
|
||||||
codeV2: 4097,
|
codeV2: 4097,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
status: (statusConstructor)(func() apistatus.Status {
|
|
||||||
return new(apistatus.APEManagerAccessDenied)
|
|
||||||
}),
|
|
||||||
codeV2: 5120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: (statusConstructor)(func() apistatus.Status {
|
|
||||||
return new(apistatus.NodeUnderMaintenance)
|
|
||||||
}),
|
|
||||||
codeV2: 1027,
|
|
||||||
},
|
|
||||||
} {
|
} {
|
||||||
var st apistatus.Status
|
var st apistatus.Status
|
||||||
|
|
||||||
|
@ -309,9 +254,6 @@ func TestFromStatusV2(t *testing.T) {
|
||||||
|
|
||||||
// must generate the same status.Status message
|
// must generate the same status.Status message
|
||||||
require.EqualValues(t, testItem.codeV2, stv2.Code())
|
require.EqualValues(t, testItem.codeV2, stv2.Code())
|
||||||
if len(testItem.messageV2) > 0 {
|
|
||||||
require.Equal(t, testItem.messageV2, stv2.Message())
|
|
||||||
}
|
|
||||||
|
|
||||||
_, ok := st.(apistatus.StatusV2)
|
_, ok := st.(apistatus.StatusV2)
|
||||||
if ok {
|
if ok {
|
||||||
|
|
|
@ -1,83 +0,0 @@
|
||||||
package acl
|
|
||||||
|
|
||||||
import "strconv"
|
|
||||||
|
|
||||||
// Op enumerates operations under access control inside container.
|
|
||||||
// Non-positive values are reserved and depend on context (e.g. unsupported op).
|
|
||||||
//
|
|
||||||
// Note that type conversion from- and to numerical types is not recommended,
|
|
||||||
// use corresponding constants and/or methods instead.
|
|
||||||
type Op uint32
|
|
||||||
|
|
||||||
// nolint: unused
|
|
||||||
const (
|
|
||||||
opZero Op = iota // extreme value for testing
|
|
||||||
|
|
||||||
OpObjectGet // Object.Get rpc
|
|
||||||
OpObjectHead // Object.Head rpc
|
|
||||||
OpObjectPut // Object.Put rpc
|
|
||||||
OpObjectDelete // Object.Delete rpc
|
|
||||||
OpObjectSearch // Object.Search rpc
|
|
||||||
OpObjectRange // Object.GetRange rpc
|
|
||||||
OpObjectHash // Object.GetRangeHash rpc
|
|
||||||
|
|
||||||
opLast // extreme value for testing
|
|
||||||
)
|
|
||||||
|
|
||||||
// String implements fmt.Stringer.
|
|
||||||
func (x Op) String() string {
|
|
||||||
switch x {
|
|
||||||
default:
|
|
||||||
return "UNKNOWN#" + strconv.FormatUint(uint64(x), 10)
|
|
||||||
case OpObjectGet:
|
|
||||||
return "OBJECT_GET"
|
|
||||||
case OpObjectHead:
|
|
||||||
return "OBJECT_HEAD"
|
|
||||||
case OpObjectPut:
|
|
||||||
return "OBJECT_PUT"
|
|
||||||
case OpObjectDelete:
|
|
||||||
return "OBJECT_DELETE"
|
|
||||||
case OpObjectSearch:
|
|
||||||
return "OBJECT_SEARCH"
|
|
||||||
case OpObjectRange:
|
|
||||||
return "OBJECT_RANGE"
|
|
||||||
case OpObjectHash:
|
|
||||||
return "OBJECT_HASH"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Role enumerates roles covered by container ACL. Each role represents
|
|
||||||
// some party which can be authenticated during container op execution.
|
|
||||||
// Non-positive values are reserved and depend on context (e.g. unsupported role).
|
|
||||||
//
|
|
||||||
// Note that type conversion from- and to numerical types is not recommended,
|
|
||||||
// use corresponding constants and/or methods instead.
|
|
||||||
type Role uint32
|
|
||||||
|
|
||||||
// nolint: unused
|
|
||||||
const (
|
|
||||||
roleZero Role = iota // extreme value for testing
|
|
||||||
|
|
||||||
RoleOwner // container owner
|
|
||||||
RoleContainer // nodes of the related container
|
|
||||||
RoleInnerRing // Inner Ring nodes
|
|
||||||
RoleOthers // all others
|
|
||||||
|
|
||||||
roleLast // extreme value for testing
|
|
||||||
)
|
|
||||||
|
|
||||||
// String implements fmt.Stringer.
|
|
||||||
func (x Role) String() string {
|
|
||||||
switch x {
|
|
||||||
default:
|
|
||||||
return "UNKNOWN#" + strconv.FormatUint(uint64(x), 10)
|
|
||||||
case RoleOwner:
|
|
||||||
return "OWNER"
|
|
||||||
case RoleContainer:
|
|
||||||
return "CONTAINER"
|
|
||||||
case RoleInnerRing:
|
|
||||||
return "INNER_RING"
|
|
||||||
case RoleOthers:
|
|
||||||
return "OTHERS"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,283 +0,0 @@
|
||||||
package acl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Basic represents basic part of the FrostFS container's ACL. It includes
|
|
||||||
// common (pretty simple) access rules for operations inside the container.
|
|
||||||
// See FrostFS Specification for details.
|
|
||||||
//
|
|
||||||
// One can find some similarities with the traditional Unix permission, such as
|
|
||||||
//
|
|
||||||
// division into scopes: user, group, others
|
|
||||||
// op-permissions: read, write, etc.
|
|
||||||
// sticky bit
|
|
||||||
//
|
|
||||||
// However, these similarities should only be used for better understanding,
|
|
||||||
// in general these mechanisms are different.
|
|
||||||
//
|
|
||||||
// Instances can be created using built-in var declaration, but look carefully
|
|
||||||
// at the default values, and how individual permissions are regulated.
|
|
||||||
// Some frequently used values are presented in exported variables.
|
|
||||||
//
|
|
||||||
// Basic instances are comparable: values can be compared directly using
|
|
||||||
// == operator.
|
|
||||||
//
|
|
||||||
// Note that type conversion from- and to numerical types is not recommended,
|
|
||||||
// use corresponding constants and/or methods instead.
|
|
||||||
type Basic uint32
|
|
||||||
|
|
||||||
// FromBits decodes Basic from the numerical representation.
|
|
||||||
//
|
|
||||||
// See also Bits.
|
|
||||||
func (x *Basic) FromBits(bits uint32) {
|
|
||||||
*x = Basic(bits)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bits returns numerical encoding of Basic.
|
|
||||||
//
|
|
||||||
// See also FromBits.
|
|
||||||
func (x Basic) Bits() uint32 {
|
|
||||||
return uint32(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
// common bit sections.
|
|
||||||
const (
|
|
||||||
opAmount = 7
|
|
||||||
bitsPerOp = 4
|
|
||||||
|
|
||||||
bitPosFinal = opAmount * bitsPerOp
|
|
||||||
bitPosSticky = bitPosFinal + 1
|
|
||||||
)
|
|
||||||
|
|
||||||
// per-op bit order.
|
|
||||||
const (
|
|
||||||
opBitPosBearer uint8 = iota
|
|
||||||
opBitPosOthers
|
|
||||||
opBitPosContainer
|
|
||||||
opBitPosOwner
|
|
||||||
)
|
|
||||||
|
|
||||||
// DisableExtension makes Basic FINAL. FINAL indicates the ACL non-extendability
|
|
||||||
// in the related container.
|
|
||||||
//
|
|
||||||
// See also Extendable.
|
|
||||||
func (x *Basic) DisableExtension() {
|
|
||||||
setBit((*uint32)(x), bitPosFinal)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extendable checks if Basic is NOT made FINAL using DisableExtension.
|
|
||||||
//
|
|
||||||
// Zero Basic is extendable.
|
|
||||||
func (x Basic) Extendable() bool {
|
|
||||||
return !isBitSet(uint32(x), bitPosFinal)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MakeSticky makes Basic STICKY. STICKY indicates that only the owner of any
|
|
||||||
// particular object is allowed to operate on it.
|
|
||||||
//
|
|
||||||
// See also Sticky.
|
|
||||||
func (x *Basic) MakeSticky() {
|
|
||||||
setBit((*uint32)(x), bitPosSticky)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sticky checks if Basic is made STICKY using MakeSticky.
|
|
||||||
//
|
|
||||||
// Zero Basic is NOT STICKY.
|
|
||||||
func (x Basic) Sticky() bool {
|
|
||||||
return isBitSet(uint32(x), bitPosSticky)
|
|
||||||
}
|
|
||||||
|
|
||||||
// checks if op is used by the storage nodes within replication mechanism.
|
|
||||||
func isReplicationOp(op Op) bool {
|
|
||||||
switch op {
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
case
|
|
||||||
OpObjectGet,
|
|
||||||
OpObjectHead,
|
|
||||||
OpObjectPut,
|
|
||||||
OpObjectSearch,
|
|
||||||
OpObjectHash:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AllowOp allows the parties with the given role to the given operation.
|
|
||||||
// Op MUST be one of the Op enumeration. Role MUST be one of:
|
|
||||||
//
|
|
||||||
// RoleOwner
|
|
||||||
// RoleContainer
|
|
||||||
// RoleOthers
|
|
||||||
//
|
|
||||||
// and if role is RoleContainer, op MUST NOT be:
|
|
||||||
//
|
|
||||||
// OpObjectGet
|
|
||||||
// OpObjectHead
|
|
||||||
// OpObjectPut
|
|
||||||
// OpObjectSearch
|
|
||||||
// OpObjectHash
|
|
||||||
//
|
|
||||||
// See also IsOpAllowed.
|
|
||||||
func (x *Basic) AllowOp(op Op, role Role) {
|
|
||||||
var bitPos uint8
|
|
||||||
|
|
||||||
switch role {
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("unable to set rules for unsupported role %v", role))
|
|
||||||
case RoleInnerRing:
|
|
||||||
panic("basic ACL MUST NOT be modified for Inner Ring")
|
|
||||||
case RoleOwner:
|
|
||||||
bitPos = opBitPosOwner
|
|
||||||
case RoleContainer:
|
|
||||||
if isReplicationOp(op) {
|
|
||||||
panic("basic ACL for container replication ops MUST NOT be modified")
|
|
||||||
}
|
|
||||||
|
|
||||||
bitPos = opBitPosContainer
|
|
||||||
case RoleOthers:
|
|
||||||
bitPos = opBitPosOthers
|
|
||||||
}
|
|
||||||
|
|
||||||
setOpBit((*uint32)(x), op, bitPos)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsOpAllowed checks if parties with the given role are allowed to the given op
|
|
||||||
// according to the Basic rules. Op MUST be one of the Op enumeration.
|
|
||||||
// Role MUST be one of the Role enumeration.
|
|
||||||
//
|
|
||||||
// Members with RoleContainer role have exclusive default access to the
|
|
||||||
// operations of the data replication mechanism:
|
|
||||||
//
|
|
||||||
// OpObjectGet
|
|
||||||
// OpObjectHead
|
|
||||||
// OpObjectPut
|
|
||||||
// OpObjectSearch
|
|
||||||
// OpObjectHash
|
|
||||||
//
|
|
||||||
// RoleInnerRing members are allowed to data audit ops only:
|
|
||||||
//
|
|
||||||
// OpObjectGet
|
|
||||||
// OpObjectHead
|
|
||||||
// OpObjectHash
|
|
||||||
// OpObjectSearch
|
|
||||||
//
|
|
||||||
// Zero Basic prevents any role from accessing any operation in the absence
|
|
||||||
// of default rights.
|
|
||||||
//
|
|
||||||
// See also AllowOp.
|
|
||||||
func (x Basic) IsOpAllowed(op Op, role Role) bool {
|
|
||||||
var bitPos uint8
|
|
||||||
|
|
||||||
switch role {
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("role is unsupported %v", role))
|
|
||||||
case RoleInnerRing:
|
|
||||||
switch op {
|
|
||||||
case
|
|
||||||
OpObjectGet,
|
|
||||||
OpObjectHead,
|
|
||||||
OpObjectHash,
|
|
||||||
OpObjectSearch:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
case RoleOwner:
|
|
||||||
bitPos = opBitPosOwner
|
|
||||||
case RoleContainer:
|
|
||||||
if isReplicationOp(op) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
bitPos = opBitPosContainer
|
|
||||||
case RoleOthers:
|
|
||||||
bitPos = opBitPosOthers
|
|
||||||
}
|
|
||||||
|
|
||||||
return isOpBitSet(uint32(x), op, bitPos)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AllowBearerRules allows bearer to provide extended ACL rules for the given
|
|
||||||
// operation. Bearer rules doesn't depend on container ACL extensibility.
|
|
||||||
//
|
|
||||||
// See also AllowedBearerRules.
|
|
||||||
func (x *Basic) AllowBearerRules(op Op) {
|
|
||||||
setOpBit((*uint32)(x), op, opBitPosBearer)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AllowedBearerRules checks if bearer rules are allowed using AllowBearerRules.
|
|
||||||
// Op MUST be one of the Op enumeration.
|
|
||||||
//
|
|
||||||
// Zero Basic disallows bearer rules for any op.
|
|
||||||
func (x Basic) AllowedBearerRules(op Op) bool {
|
|
||||||
return isOpBitSet(uint32(x), op, opBitPosBearer)
|
|
||||||
}
|
|
||||||
|
|
||||||
// EncodeToString encodes Basic into hexadecimal string.
|
|
||||||
//
|
|
||||||
// See also DecodeString.
|
|
||||||
func (x Basic) EncodeToString() string {
|
|
||||||
return strconv.FormatUint(uint64(x), 16)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Names of the frequently used Basic values.
|
|
||||||
const (
|
|
||||||
NamePrivate = "private"
|
|
||||||
NamePrivateExtended = "eacl-private"
|
|
||||||
NamePublicRO = "public-read"
|
|
||||||
NamePublicROExtended = "eacl-public-read"
|
|
||||||
NamePublicRW = "public-read-write"
|
|
||||||
NamePublicRWExtended = "eacl-public-read-write"
|
|
||||||
NamePublicAppend = "public-append"
|
|
||||||
NamePublicAppendExtended = "eacl-public-append"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Frequently used Basic values. Bitmasks are taken from the FrostFS Specification.
|
|
||||||
const (
|
|
||||||
Private = Basic(0x1C8C8CCC) // private
|
|
||||||
PrivateExtended = Basic(0x0C8C8CCC) // eacl-private
|
|
||||||
PublicRO = Basic(0x1FBF8CFF) // public-read
|
|
||||||
PublicROExtended = Basic(0x0FBF8CFF) // eacl-public-read
|
|
||||||
PublicRW = Basic(0x1FBFBFFF) // public-read-write
|
|
||||||
PublicRWExtended = Basic(0x0FBFBFFF) // eacl-public-read-write
|
|
||||||
PublicAppend = Basic(0x1FBF9FFF) // public-append
|
|
||||||
PublicAppendExtended = Basic(0x0FBF9FFF) // eacl-public-append
|
|
||||||
)
|
|
||||||
|
|
||||||
// DecodeString decodes string calculated using EncodeToString. Also supports
|
|
||||||
// human-readable names (Name* constants).
|
|
||||||
func (x *Basic) DecodeString(s string) (e error) {
|
|
||||||
switch s {
|
|
||||||
case NamePrivate:
|
|
||||||
*x = Private
|
|
||||||
case NamePrivateExtended:
|
|
||||||
*x = PrivateExtended
|
|
||||||
case NamePublicRO:
|
|
||||||
*x = PublicRO
|
|
||||||
case NamePublicROExtended:
|
|
||||||
*x = PublicROExtended
|
|
||||||
case NamePublicRW:
|
|
||||||
*x = PublicRW
|
|
||||||
case NamePublicRWExtended:
|
|
||||||
*x = PublicRWExtended
|
|
||||||
case NamePublicAppend:
|
|
||||||
*x = PublicAppend
|
|
||||||
case NamePublicAppendExtended:
|
|
||||||
*x = PublicAppendExtended
|
|
||||||
default:
|
|
||||||
s = strings.TrimPrefix(strings.ToLower(s), "0x")
|
|
||||||
|
|
||||||
v, err := strconv.ParseUint(s, 16, 32)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("parse hex: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
*x = Basic(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,384 +0,0 @@
|
||||||
package acl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestBasic_DisableExtension(t *testing.T) {
|
|
||||||
var val, val2 Basic
|
|
||||||
|
|
||||||
require.True(t, val.Extendable())
|
|
||||||
val2.FromBits(val.Bits())
|
|
||||||
require.True(t, val2.Extendable())
|
|
||||||
|
|
||||||
val.DisableExtension()
|
|
||||||
|
|
||||||
require.False(t, val.Extendable())
|
|
||||||
val2.FromBits(val.Bits())
|
|
||||||
require.False(t, val2.Extendable())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBasic_MakeSticky(t *testing.T) {
|
|
||||||
var val, val2 Basic
|
|
||||||
|
|
||||||
require.False(t, val.Sticky())
|
|
||||||
val2.FromBits(val.Bits())
|
|
||||||
require.False(t, val2.Sticky())
|
|
||||||
|
|
||||||
val.MakeSticky()
|
|
||||||
|
|
||||||
require.True(t, val.Sticky())
|
|
||||||
val2.FromBits(val.Bits())
|
|
||||||
require.True(t, val2.Sticky())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBasic_AllowBearerRules(t *testing.T) {
|
|
||||||
var val Basic
|
|
||||||
|
|
||||||
require.Panics(t, func() { val.AllowBearerRules(opZero) })
|
|
||||||
require.Panics(t, func() { val.AllowBearerRules(opLast) })
|
|
||||||
|
|
||||||
require.Panics(t, func() { val.AllowedBearerRules(opZero) })
|
|
||||||
require.Panics(t, func() { val.AllowedBearerRules(opLast) })
|
|
||||||
|
|
||||||
for op := opZero + 1; op < opLast; op++ {
|
|
||||||
val := val
|
|
||||||
|
|
||||||
require.False(t, val.AllowedBearerRules(op))
|
|
||||||
|
|
||||||
val.AllowBearerRules(op)
|
|
||||||
|
|
||||||
for j := opZero + 1; j < opLast; j++ {
|
|
||||||
if j == op {
|
|
||||||
require.True(t, val.AllowedBearerRules(j), op)
|
|
||||||
} else {
|
|
||||||
require.False(t, val.AllowedBearerRules(j), op)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBasic_AllowOp(t *testing.T) {
|
|
||||||
var val, val2 Basic
|
|
||||||
|
|
||||||
require.Panics(t, func() { val.IsOpAllowed(opZero, roleZero+1) })
|
|
||||||
require.Panics(t, func() { val.IsOpAllowed(opLast, roleZero+1) })
|
|
||||||
require.Panics(t, func() { val.IsOpAllowed(opZero+1, roleZero) })
|
|
||||||
require.Panics(t, func() { val.IsOpAllowed(opZero+1, roleLast) })
|
|
||||||
|
|
||||||
for op := opZero + 1; op < opLast; op++ {
|
|
||||||
require.Panics(t, func() { val.AllowOp(op, RoleInnerRing) })
|
|
||||||
|
|
||||||
if isReplicationOp(op) {
|
|
||||||
require.Panics(t, func() { val.AllowOp(op, RoleContainer) })
|
|
||||||
require.True(t, val.IsOpAllowed(op, RoleContainer))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
require.True(t, val.IsOpAllowed(OpObjectGet, RoleInnerRing))
|
|
||||||
require.True(t, val.IsOpAllowed(OpObjectHead, RoleInnerRing))
|
|
||||||
require.True(t, val.IsOpAllowed(OpObjectSearch, RoleInnerRing))
|
|
||||||
require.True(t, val.IsOpAllowed(OpObjectHash, RoleInnerRing))
|
|
||||||
|
|
||||||
const op = opZero + 1
|
|
||||||
const role = RoleOthers
|
|
||||||
|
|
||||||
require.False(t, val.IsOpAllowed(op, role))
|
|
||||||
val2.FromBits(val.Bits())
|
|
||||||
require.False(t, val2.IsOpAllowed(op, role))
|
|
||||||
|
|
||||||
val.AllowOp(op, role)
|
|
||||||
|
|
||||||
require.True(t, val.IsOpAllowed(op, role))
|
|
||||||
val2.FromBits(val.Bits())
|
|
||||||
require.True(t, val2.IsOpAllowed(op, role))
|
|
||||||
}
|
|
||||||
|
|
||||||
type opsExpected struct {
|
|
||||||
owner, container, innerRing, others, bearer bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func testOp(t *testing.T, v Basic, op Op, exp opsExpected) {
|
|
||||||
require.Equal(t, exp.owner, v.IsOpAllowed(op, RoleOwner), op)
|
|
||||||
require.Equal(t, exp.container, v.IsOpAllowed(op, RoleContainer), op)
|
|
||||||
require.Equal(t, exp.innerRing, v.IsOpAllowed(op, RoleInnerRing), op)
|
|
||||||
require.Equal(t, exp.others, v.IsOpAllowed(op, RoleOthers), op)
|
|
||||||
require.Equal(t, exp.bearer, v.AllowedBearerRules(op), op)
|
|
||||||
}
|
|
||||||
|
|
||||||
type expected struct {
|
|
||||||
extendable, sticky bool
|
|
||||||
|
|
||||||
mOps map[Op]opsExpected
|
|
||||||
}
|
|
||||||
|
|
||||||
func testBasicPredefined(t *testing.T, val Basic, name string, exp expected) {
|
|
||||||
require.Equal(t, exp.sticky, val.Sticky())
|
|
||||||
require.Equal(t, exp.extendable, val.Extendable())
|
|
||||||
|
|
||||||
for op, exp := range exp.mOps {
|
|
||||||
testOp(t, val, op, exp)
|
|
||||||
}
|
|
||||||
|
|
||||||
s := val.EncodeToString()
|
|
||||||
|
|
||||||
var val2 Basic
|
|
||||||
|
|
||||||
require.NoError(t, val2.DecodeString(s))
|
|
||||||
require.Equal(t, val, val2)
|
|
||||||
|
|
||||||
require.NoError(t, val2.DecodeString(name))
|
|
||||||
require.Equal(t, val, val2)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBasicPredefined(t *testing.T) {
|
|
||||||
t.Run("private", func(t *testing.T) {
|
|
||||||
exp := expected{
|
|
||||||
extendable: false,
|
|
||||||
sticky: false,
|
|
||||||
mOps: map[Op]opsExpected{
|
|
||||||
OpObjectHash: {
|
|
||||||
owner: true,
|
|
||||||
container: true,
|
|
||||||
innerRing: true,
|
|
||||||
others: false,
|
|
||||||
bearer: false,
|
|
||||||
},
|
|
||||||
OpObjectRange: {
|
|
||||||
owner: true,
|
|
||||||
container: false,
|
|
||||||
innerRing: false,
|
|
||||||
others: false,
|
|
||||||
bearer: false,
|
|
||||||
},
|
|
||||||
OpObjectSearch: {
|
|
||||||
owner: true,
|
|
||||||
container: true,
|
|
||||||
innerRing: true,
|
|
||||||
others: false,
|
|
||||||
bearer: false,
|
|
||||||
},
|
|
||||||
OpObjectDelete: {
|
|
||||||
owner: true,
|
|
||||||
container: false,
|
|
||||||
innerRing: false,
|
|
||||||
others: false,
|
|
||||||
bearer: false,
|
|
||||||
},
|
|
||||||
OpObjectPut: {
|
|
||||||
owner: true,
|
|
||||||
container: true,
|
|
||||||
innerRing: false,
|
|
||||||
others: false,
|
|
||||||
bearer: false,
|
|
||||||
},
|
|
||||||
OpObjectHead: {
|
|
||||||
owner: true,
|
|
||||||
container: true,
|
|
||||||
innerRing: true,
|
|
||||||
others: false,
|
|
||||||
bearer: false,
|
|
||||||
},
|
|
||||||
OpObjectGet: {
|
|
||||||
owner: true,
|
|
||||||
container: true,
|
|
||||||
innerRing: true,
|
|
||||||
others: false,
|
|
||||||
bearer: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
testBasicPredefined(t, Private, NamePrivate, exp)
|
|
||||||
exp.extendable = true
|
|
||||||
testBasicPredefined(t, PrivateExtended, NamePrivateExtended, exp)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("public-read", func(t *testing.T) {
|
|
||||||
exp := expected{
|
|
||||||
extendable: false,
|
|
||||||
sticky: false,
|
|
||||||
mOps: map[Op]opsExpected{
|
|
||||||
OpObjectHash: {
|
|
||||||
owner: true,
|
|
||||||
container: true,
|
|
||||||
innerRing: true,
|
|
||||||
others: true,
|
|
||||||
bearer: true,
|
|
||||||
},
|
|
||||||
OpObjectRange: {
|
|
||||||
owner: true,
|
|
||||||
container: false,
|
|
||||||
innerRing: false,
|
|
||||||
others: true,
|
|
||||||
bearer: true,
|
|
||||||
},
|
|
||||||
OpObjectSearch: {
|
|
||||||
owner: true,
|
|
||||||
container: true,
|
|
||||||
innerRing: true,
|
|
||||||
others: true,
|
|
||||||
bearer: true,
|
|
||||||
},
|
|
||||||
OpObjectDelete: {
|
|
||||||
owner: true,
|
|
||||||
container: false,
|
|
||||||
innerRing: false,
|
|
||||||
others: false,
|
|
||||||
bearer: false,
|
|
||||||
},
|
|
||||||
OpObjectPut: {
|
|
||||||
owner: true,
|
|
||||||
container: true,
|
|
||||||
innerRing: false,
|
|
||||||
others: false,
|
|
||||||
bearer: false,
|
|
||||||
},
|
|
||||||
OpObjectHead: {
|
|
||||||
owner: true,
|
|
||||||
container: true,
|
|
||||||
innerRing: true,
|
|
||||||
others: true,
|
|
||||||
bearer: true,
|
|
||||||
},
|
|
||||||
OpObjectGet: {
|
|
||||||
owner: true,
|
|
||||||
container: true,
|
|
||||||
innerRing: true,
|
|
||||||
others: true,
|
|
||||||
bearer: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
testBasicPredefined(t, PublicRO, NamePublicRO, exp)
|
|
||||||
exp.extendable = true
|
|
||||||
testBasicPredefined(t, PublicROExtended, NamePublicROExtended, exp)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("public-read-write", func(t *testing.T) {
|
|
||||||
exp := expected{
|
|
||||||
extendable: false,
|
|
||||||
sticky: false,
|
|
||||||
mOps: map[Op]opsExpected{
|
|
||||||
OpObjectHash: {
|
|
||||||
owner: true,
|
|
||||||
container: true,
|
|
||||||
innerRing: true,
|
|
||||||
others: true,
|
|
||||||
bearer: true,
|
|
||||||
},
|
|
||||||
OpObjectRange: {
|
|
||||||
owner: true,
|
|
||||||
container: false,
|
|
||||||
innerRing: false,
|
|
||||||
others: true,
|
|
||||||
bearer: true,
|
|
||||||
},
|
|
||||||
OpObjectSearch: {
|
|
||||||
owner: true,
|
|
||||||
container: true,
|
|
||||||
innerRing: true,
|
|
||||||
others: true,
|
|
||||||
bearer: true,
|
|
||||||
},
|
|
||||||
OpObjectDelete: {
|
|
||||||
owner: true,
|
|
||||||
container: false,
|
|
||||||
innerRing: false,
|
|
||||||
others: true,
|
|
||||||
bearer: true,
|
|
||||||
},
|
|
||||||
OpObjectPut: {
|
|
||||||
owner: true,
|
|
||||||
container: true,
|
|
||||||
innerRing: false,
|
|
||||||
others: true,
|
|
||||||
bearer: true,
|
|
||||||
},
|
|
||||||
OpObjectHead: {
|
|
||||||
owner: true,
|
|
||||||
container: true,
|
|
||||||
innerRing: true,
|
|
||||||
others: true,
|
|
||||||
bearer: true,
|
|
||||||
},
|
|
||||||
OpObjectGet: {
|
|
||||||
owner: true,
|
|
||||||
container: true,
|
|
||||||
innerRing: true,
|
|
||||||
others: true,
|
|
||||||
bearer: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
testBasicPredefined(t, PublicRW, NamePublicRW, exp)
|
|
||||||
exp.extendable = true
|
|
||||||
testBasicPredefined(t, PublicRWExtended, NamePublicRWExtended, exp)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("public-append", func(t *testing.T) {
|
|
||||||
exp := expected{
|
|
||||||
extendable: false,
|
|
||||||
sticky: false,
|
|
||||||
mOps: map[Op]opsExpected{
|
|
||||||
OpObjectHash: {
|
|
||||||
owner: true,
|
|
||||||
container: true,
|
|
||||||
innerRing: true,
|
|
||||||
others: true,
|
|
||||||
bearer: true,
|
|
||||||
},
|
|
||||||
OpObjectRange: {
|
|
||||||
owner: true,
|
|
||||||
container: false,
|
|
||||||
innerRing: false,
|
|
||||||
others: true,
|
|
||||||
bearer: true,
|
|
||||||
},
|
|
||||||
OpObjectSearch: {
|
|
||||||
owner: true,
|
|
||||||
container: true,
|
|
||||||
innerRing: true,
|
|
||||||
others: true,
|
|
||||||
bearer: true,
|
|
||||||
},
|
|
||||||
OpObjectDelete: {
|
|
||||||
owner: true,
|
|
||||||
container: false,
|
|
||||||
innerRing: false,
|
|
||||||
others: false,
|
|
||||||
bearer: true,
|
|
||||||
},
|
|
||||||
OpObjectPut: {
|
|
||||||
owner: true,
|
|
||||||
container: true,
|
|
||||||
innerRing: false,
|
|
||||||
others: true,
|
|
||||||
bearer: true,
|
|
||||||
},
|
|
||||||
OpObjectHead: {
|
|
||||||
owner: true,
|
|
||||||
container: true,
|
|
||||||
innerRing: true,
|
|
||||||
others: true,
|
|
||||||
bearer: true,
|
|
||||||
},
|
|
||||||
OpObjectGet: {
|
|
||||||
owner: true,
|
|
||||||
container: true,
|
|
||||||
innerRing: true,
|
|
||||||
others: true,
|
|
||||||
bearer: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
testBasicPredefined(t, PublicAppend, NamePublicAppend, exp)
|
|
||||||
exp.extendable = true
|
|
||||||
testBasicPredefined(t, PublicAppendExtended, NamePublicAppendExtended, exp)
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
/*
|
|
||||||
Package acl provides functionality to control access to data and operations on them in FrostFS containers.
|
|
||||||
|
|
||||||
Type Basic represents basic ACL of the FrostFS container which specifies the general order of data access.
|
|
||||||
Basic provides interface of rule composition. Package acl also exports some frequently used settings like
|
|
||||||
private or public.
|
|
||||||
*/
|
|
||||||
package acl
|
|
|
@ -1,20 +0,0 @@
|
||||||
package acl
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
// left-to-right order of the object operations
|
|
||||||
orderedOps := [...]Op{
|
|
||||||
OpObjectGet,
|
|
||||||
OpObjectHead,
|
|
||||||
OpObjectPut,
|
|
||||||
OpObjectDelete,
|
|
||||||
OpObjectSearch,
|
|
||||||
OpObjectRange,
|
|
||||||
OpObjectHash,
|
|
||||||
}
|
|
||||||
|
|
||||||
mOrder = make(map[Op]uint8, len(orderedOps))
|
|
||||||
|
|
||||||
for i := range orderedOps {
|
|
||||||
mOrder[orderedOps[i]] = uint8(i)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
package acl
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
// sets n-th bit in num (starting at 0).
|
|
||||||
func setBit(num *uint32, n uint8) {
|
|
||||||
*num |= 1 << n
|
|
||||||
}
|
|
||||||
|
|
||||||
// checks if n-th bit in num is set (starting at 0).
|
|
||||||
func isBitSet(num uint32, n uint8) bool {
|
|
||||||
mask := uint32(1 << n)
|
|
||||||
return mask != 0 && num&mask == mask
|
|
||||||
}
|
|
||||||
|
|
||||||
// maps Op to op-section index in Basic. Filled on init.
|
|
||||||
var mOrder map[Op]uint8
|
|
||||||
|
|
||||||
// sets n-th bit in num for the given op. Panics if op is unsupported.
|
|
||||||
func setOpBit(num *uint32, op Op, opBitPos uint8) {
|
|
||||||
n, ok := mOrder[op]
|
|
||||||
if !ok {
|
|
||||||
panic(fmt.Sprintf("op is unsupported %v", op))
|
|
||||||
}
|
|
||||||
|
|
||||||
setBit(num, n*bitsPerOp+opBitPos)
|
|
||||||
}
|
|
||||||
|
|
||||||
// checks if n-th bit in num for the given op is set. Panics if op is unsupported.
|
|
||||||
func isOpBitSet(num uint32, op Op, n uint8) bool {
|
|
||||||
off, ok := mOrder[op]
|
|
||||||
if !ok {
|
|
||||||
panic(fmt.Sprintf("op is unsupported %v", op))
|
|
||||||
}
|
|
||||||
|
|
||||||
return isBitSet(num, bitsPerOp*off+n)
|
|
||||||
}
|
|
|
@ -1,130 +0,0 @@
|
||||||
package acl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestBits(t *testing.T) {
|
|
||||||
num := uint32(0b10110)
|
|
||||||
|
|
||||||
require.False(t, isBitSet(num, 0))
|
|
||||||
require.True(t, isBitSet(num, 1))
|
|
||||||
require.True(t, isBitSet(num, 2))
|
|
||||||
require.False(t, isBitSet(num, 3))
|
|
||||||
require.True(t, isBitSet(num, 4))
|
|
||||||
require.False(t, isBitSet(num, 5))
|
|
||||||
|
|
||||||
setBit(&num, 3)
|
|
||||||
require.EqualValues(t, 0b11110, num)
|
|
||||||
|
|
||||||
setBit(&num, 6)
|
|
||||||
require.EqualValues(t, 0b1011110, num)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOpBits(t *testing.T) {
|
|
||||||
num := uint32(0b_1001_0101_1100_0011_0110_0111_1000_1111)
|
|
||||||
|
|
||||||
require.Panics(t, func() { isOpBitSet(num, opZero, 0) })
|
|
||||||
require.Panics(t, func() { isOpBitSet(num, opLast, 0) })
|
|
||||||
|
|
||||||
cpNum := num
|
|
||||||
|
|
||||||
require.Panics(t, func() { setOpBit(&num, opZero, 0) })
|
|
||||||
require.EqualValues(t, cpNum, num)
|
|
||||||
require.Panics(t, func() { setOpBit(&num, opLast, 0) })
|
|
||||||
require.EqualValues(t, cpNum, num)
|
|
||||||
|
|
||||||
for _, tc := range []struct {
|
|
||||||
op Op
|
|
||||||
set [4]bool // is bit set (left-to-right)
|
|
||||||
bits [4]uint32 // result of setting i-th bit (left-to-right) to zero num
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
op: OpObjectHash,
|
|
||||||
set: [4]bool{false, true, false, true},
|
|
||||||
bits: [4]uint32{
|
|
||||||
0b_0000_1000_0000_0000_0000_0000_0000_0000,
|
|
||||||
0b_0000_0100_0000_0000_0000_0000_0000_0000,
|
|
||||||
0b_0000_0010_0000_0000_0000_0000_0000_0000,
|
|
||||||
0b_0000_0001_0000_0000_0000_0000_0000_0000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
op: OpObjectRange,
|
|
||||||
set: [4]bool{true, true, false, false},
|
|
||||||
bits: [4]uint32{
|
|
||||||
0b_0000_0000_1000_0000_0000_0000_0000_0000,
|
|
||||||
0b_0000_0000_0100_0000_0000_0000_0000_0000,
|
|
||||||
0b_0000_0000_0010_0000_0000_0000_0000_0000,
|
|
||||||
0b_0000_0000_0001_0000_0000_0000_0000_0000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
op: OpObjectSearch,
|
|
||||||
set: [4]bool{false, false, true, true},
|
|
||||||
bits: [4]uint32{
|
|
||||||
0b_0000_0000_0000_1000_0000_0000_0000_0000,
|
|
||||||
0b_0000_0000_0000_0100_0000_0000_0000_0000,
|
|
||||||
0b_0000_0000_0000_0010_0000_0000_0000_0000,
|
|
||||||
0b_0000_0000_0000_0001_0000_0000_0000_0000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
op: OpObjectDelete,
|
|
||||||
set: [4]bool{false, true, true, false},
|
|
||||||
bits: [4]uint32{
|
|
||||||
0b_0000_0000_0000_0000_1000_0000_0000_0000,
|
|
||||||
0b_0000_0000_0000_0000_0100_0000_0000_0000,
|
|
||||||
0b_0000_0000_0000_0000_0010_0000_0000_0000,
|
|
||||||
0b_0000_0000_0000_0000_0001_0000_0000_0000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
op: OpObjectPut,
|
|
||||||
set: [4]bool{false, true, true, true},
|
|
||||||
bits: [4]uint32{
|
|
||||||
0b_0000_0000_0000_0000_0000_1000_0000_0000,
|
|
||||||
0b_0000_0000_0000_0000_0000_0100_0000_0000,
|
|
||||||
0b_0000_0000_0000_0000_0000_0010_0000_0000,
|
|
||||||
0b_0000_0000_0000_0000_0000_0001_0000_0000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
op: OpObjectHead,
|
|
||||||
set: [4]bool{true, false, false, false},
|
|
||||||
bits: [4]uint32{
|
|
||||||
0b_0000_0000_0000_0000_0000_0000_1000_0000,
|
|
||||||
0b_0000_0000_0000_0000_0000_0000_0100_0000,
|
|
||||||
0b_0000_0000_0000_0000_0000_0000_0010_0000,
|
|
||||||
0b_0000_0000_0000_0000_0000_0000_0001_0000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
op: OpObjectGet,
|
|
||||||
set: [4]bool{true, true, true, true},
|
|
||||||
bits: [4]uint32{
|
|
||||||
0b_0000_0000_0000_0000_0000_0000_0000_1000,
|
|
||||||
0b_0000_0000_0000_0000_0000_0000_0000_0100,
|
|
||||||
0b_0000_0000_0000_0000_0000_0000_0000_0010,
|
|
||||||
0b_0000_0000_0000_0000_0000_0000_0000_0001,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
for i := range tc.set {
|
|
||||||
require.EqualValues(t, tc.set[i], isOpBitSet(num, tc.op, uint8(len(tc.set)-1-i)),
|
|
||||||
fmt.Sprintf("op %s, left bit #%d", tc.op, i),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range tc.bits {
|
|
||||||
num := uint32(0)
|
|
||||||
|
|
||||||
setOpBit(&num, tc.op, uint8(len(tc.bits)-1-i))
|
|
||||||
|
|
||||||
require.EqualValues(t, tc.bits[i], num)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
77
container/announcement.go
Normal file
77
container/announcement.go
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/nspcc-dev/neofs-api-go/v2/container"
|
||||||
|
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UsedSpaceAnnouncement is an announcement message used by storage nodes to
|
||||||
|
// estimate actual container sizes.
|
||||||
|
type UsedSpaceAnnouncement container.UsedSpaceAnnouncement
|
||||||
|
|
||||||
|
// NewAnnouncement initialize empty UsedSpaceAnnouncement message.
|
||||||
|
//
|
||||||
|
// Defaults:
|
||||||
|
// - epoch: 0;
|
||||||
|
// - usedSpace: 0;
|
||||||
|
// - cid: nil.
|
||||||
|
func NewAnnouncement() *UsedSpaceAnnouncement {
|
||||||
|
return NewAnnouncementFromV2(new(container.UsedSpaceAnnouncement))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAnnouncementFromV2 wraps protocol dependent version of
|
||||||
|
// UsedSpaceAnnouncement message.
|
||||||
|
//
|
||||||
|
// Nil container.UsedSpaceAnnouncement converts to nil.
|
||||||
|
func NewAnnouncementFromV2(v *container.UsedSpaceAnnouncement) *UsedSpaceAnnouncement {
|
||||||
|
return (*UsedSpaceAnnouncement)(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Epoch of the announcement.
|
||||||
|
func (a *UsedSpaceAnnouncement) Epoch() uint64 {
|
||||||
|
return (*container.UsedSpaceAnnouncement)(a).GetEpoch()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetEpoch sets announcement epoch value.
|
||||||
|
func (a *UsedSpaceAnnouncement) SetEpoch(epoch uint64) {
|
||||||
|
(*container.UsedSpaceAnnouncement)(a).SetEpoch(epoch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainerID of the announcement.
|
||||||
|
func (a *UsedSpaceAnnouncement) ContainerID() *cid.ID {
|
||||||
|
return cid.NewFromV2(
|
||||||
|
(*container.UsedSpaceAnnouncement)(a).GetContainerID(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetContainerID sets announcement container value.
|
||||||
|
func (a *UsedSpaceAnnouncement) SetContainerID(cid *cid.ID) {
|
||||||
|
(*container.UsedSpaceAnnouncement)(a).SetContainerID(cid.ToV2())
|
||||||
|
}
|
||||||
|
|
||||||
|
// UsedSpace in container.
|
||||||
|
func (a *UsedSpaceAnnouncement) UsedSpace() uint64 {
|
||||||
|
return (*container.UsedSpaceAnnouncement)(a).GetUsedSpace()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUsedSpace sets used space value by specified container.
|
||||||
|
func (a *UsedSpaceAnnouncement) SetUsedSpace(value uint64) {
|
||||||
|
(*container.UsedSpaceAnnouncement)(a).SetUsedSpace(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToV2 returns protocol dependent version of UsedSpaceAnnouncement message.
|
||||||
|
//
|
||||||
|
// Nil UsedSpaceAnnouncement converts to nil.
|
||||||
|
func (a *UsedSpaceAnnouncement) ToV2() *container.UsedSpaceAnnouncement {
|
||||||
|
return (*container.UsedSpaceAnnouncement)(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal marshals UsedSpaceAnnouncement into a protobuf binary form.
|
||||||
|
func (a *UsedSpaceAnnouncement) Marshal() ([]byte, error) {
|
||||||
|
return a.ToV2().StableMarshal(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal unmarshals protobuf binary representation of UsedSpaceAnnouncement.
|
||||||
|
func (a *UsedSpaceAnnouncement) Unmarshal(data []byte) error {
|
||||||
|
return a.ToV2().Unmarshal(data)
|
||||||
|
}
|
99
container/announcement_test.go
Normal file
99
container/announcement_test.go
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
package container_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
containerv2 "github.com/nspcc-dev/neofs-api-go/v2/container"
|
||||||
|
"github.com/nspcc-dev/neofs-api-go/v2/refs"
|
||||||
|
"github.com/nspcc-dev/neofs-sdk-go/container"
|
||||||
|
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
|
||||||
|
cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test"
|
||||||
|
containertest "github.com/nspcc-dev/neofs-sdk-go/container/test"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAnnouncement(t *testing.T) {
|
||||||
|
const epoch, usedSpace uint64 = 10, 100
|
||||||
|
|
||||||
|
cidValue := [sha256.Size]byte{1, 2, 3}
|
||||||
|
id := cidtest.IDWithChecksum(cidValue)
|
||||||
|
|
||||||
|
a := container.NewAnnouncement()
|
||||||
|
a.SetEpoch(epoch)
|
||||||
|
a.SetContainerID(id)
|
||||||
|
a.SetUsedSpace(usedSpace)
|
||||||
|
|
||||||
|
require.Equal(t, epoch, a.Epoch())
|
||||||
|
require.Equal(t, usedSpace, a.UsedSpace())
|
||||||
|
require.Equal(t, id, a.ContainerID())
|
||||||
|
|
||||||
|
t.Run("test v2", func(t *testing.T) {
|
||||||
|
const newEpoch, newUsedSpace uint64 = 20, 200
|
||||||
|
|
||||||
|
newCidValue := [32]byte{4, 5, 6}
|
||||||
|
newCID := new(refs.ContainerID)
|
||||||
|
newCID.SetValue(newCidValue[:])
|
||||||
|
|
||||||
|
v2 := a.ToV2()
|
||||||
|
require.Equal(t, usedSpace, v2.GetUsedSpace())
|
||||||
|
require.Equal(t, epoch, v2.GetEpoch())
|
||||||
|
require.Equal(t, cidValue[:], v2.GetContainerID().GetValue())
|
||||||
|
|
||||||
|
v2.SetEpoch(newEpoch)
|
||||||
|
v2.SetUsedSpace(newUsedSpace)
|
||||||
|
v2.SetContainerID(newCID)
|
||||||
|
|
||||||
|
newA := container.NewAnnouncementFromV2(v2)
|
||||||
|
|
||||||
|
require.Equal(t, newEpoch, newA.Epoch())
|
||||||
|
require.Equal(t, newUsedSpace, newA.UsedSpace())
|
||||||
|
require.Equal(t, cid.NewFromV2(newCID), newA.ContainerID())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUsedSpaceEncoding(t *testing.T) {
|
||||||
|
a := containertest.UsedSpaceAnnouncement()
|
||||||
|
|
||||||
|
t.Run("binary", func(t *testing.T) {
|
||||||
|
data, err := a.Marshal()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
a2 := container.NewAnnouncement()
|
||||||
|
require.NoError(t, a2.Unmarshal(data))
|
||||||
|
|
||||||
|
require.Equal(t, a, a2)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUsedSpaceAnnouncement_ToV2(t *testing.T) {
|
||||||
|
t.Run("nil", func(t *testing.T) {
|
||||||
|
var x *container.UsedSpaceAnnouncement
|
||||||
|
|
||||||
|
require.Nil(t, x.ToV2())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("default values", func(t *testing.T) {
|
||||||
|
announcement := container.NewAnnouncement()
|
||||||
|
|
||||||
|
// check initial values
|
||||||
|
require.Zero(t, announcement.Epoch())
|
||||||
|
require.Zero(t, announcement.UsedSpace())
|
||||||
|
require.Nil(t, announcement.ContainerID())
|
||||||
|
|
||||||
|
// convert to v2 message
|
||||||
|
announcementV2 := announcement.ToV2()
|
||||||
|
|
||||||
|
require.Zero(t, announcementV2.GetEpoch())
|
||||||
|
require.Zero(t, announcementV2.GetUsedSpace())
|
||||||
|
require.Nil(t, announcementV2.GetContainerID())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewAnnouncementFromV2(t *testing.T) {
|
||||||
|
t.Run("from nil", func(t *testing.T) {
|
||||||
|
var x *containerv2.UsedSpaceAnnouncement
|
||||||
|
|
||||||
|
require.Nil(t, container.NewAnnouncementFromV2(x))
|
||||||
|
})
|
||||||
|
}
|
139
container/attribute.go
Normal file
139
container/attribute.go
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/nspcc-dev/neofs-api-go/v2/container"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Attribute container.Attribute
|
||||||
|
Attributes []Attribute
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewAttribute creates and initializes blank Attribute.
|
||||||
|
//
|
||||||
|
// Defaults:
|
||||||
|
// - key: "";
|
||||||
|
// - value: "".
|
||||||
|
func NewAttribute() *Attribute {
|
||||||
|
return NewAttributeFromV2(new(container.Attribute))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Attribute) SetKey(v string) {
|
||||||
|
(*container.Attribute)(a).SetKey(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Attribute) SetValue(v string) {
|
||||||
|
(*container.Attribute)(a).SetValue(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Attribute) Key() string {
|
||||||
|
return (*container.Attribute)(a).GetKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Attribute) Value() string {
|
||||||
|
return (*container.Attribute)(a).GetValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAttributeFromV2 wraps protocol dependent version of
|
||||||
|
// Attribute message.
|
||||||
|
//
|
||||||
|
// Nil container.Attribute converts to nil.
|
||||||
|
func NewAttributeFromV2(v *container.Attribute) *Attribute {
|
||||||
|
return (*Attribute)(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToV2 converts Attribute to v2 Attribute message.
|
||||||
|
//
|
||||||
|
// Nil Attribute converts to nil.
|
||||||
|
func (a *Attribute) ToV2() *container.Attribute {
|
||||||
|
return (*container.Attribute)(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAttributesFromV2(v []container.Attribute) Attributes {
|
||||||
|
if v == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs := make(Attributes, len(v))
|
||||||
|
for i := range v {
|
||||||
|
attrs[i] = *NewAttributeFromV2(&v[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return attrs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Attributes) ToV2() []container.Attribute {
|
||||||
|
if a == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs := make([]container.Attribute, len(a))
|
||||||
|
for i := range a {
|
||||||
|
attrs[i] = *a[i].ToV2()
|
||||||
|
}
|
||||||
|
|
||||||
|
return attrs
|
||||||
|
}
|
||||||
|
|
||||||
|
// sets value of the attribute by key.
|
||||||
|
func setAttribute(c *Container, key, value string) {
|
||||||
|
attrs := c.Attributes()
|
||||||
|
found := false
|
||||||
|
|
||||||
|
for i := range attrs {
|
||||||
|
if attrs[i].Key() == key {
|
||||||
|
attrs[i].SetValue(value)
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
index := len(attrs)
|
||||||
|
attrs = append(attrs, Attribute{})
|
||||||
|
attrs[index].SetKey(key)
|
||||||
|
attrs[index].SetValue(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.SetAttributes(attrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// iterates over container attributes. Stops at f's true return.
|
||||||
|
//
|
||||||
|
// Handler must not be nil.
|
||||||
|
func iterateAttributes(c *Container, f func(*Attribute) bool) {
|
||||||
|
attrs := c.Attributes()
|
||||||
|
for i := range attrs {
|
||||||
|
if f(&attrs[i]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNativeNameWithZone sets container native name and its zone.
|
||||||
|
//
|
||||||
|
// Use SetNativeName to set default zone.
|
||||||
|
func SetNativeNameWithZone(c *Container, name, zone string) {
|
||||||
|
setAttribute(c, container.SysAttributeName, name)
|
||||||
|
setAttribute(c, container.SysAttributeZone, zone)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNativeName sets container native name with default zone (container).
|
||||||
|
func SetNativeName(c *Container, name string) {
|
||||||
|
SetNativeNameWithZone(c, name, container.SysAttributeZoneDefault)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNativeNameWithZone returns container native name and its zone.
|
||||||
|
func GetNativeNameWithZone(c *Container) (name string, zone string) {
|
||||||
|
iterateAttributes(c, func(a *Attribute) bool {
|
||||||
|
if key := a.Key(); key == container.SysAttributeName {
|
||||||
|
name = a.Value()
|
||||||
|
} else if key == container.SysAttributeZone {
|
||||||
|
zone = a.Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
return name != "" && zone != ""
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
152
container/attribute_test.go
Normal file
152
container/attribute_test.go
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
package container_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
containerv2 "github.com/nspcc-dev/neofs-api-go/v2/container"
|
||||||
|
"github.com/nspcc-dev/neofs-sdk-go/container"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAttribute(t *testing.T) {
|
||||||
|
t.Run("nil", func(t *testing.T) {
|
||||||
|
var x *container.Attribute
|
||||||
|
|
||||||
|
require.Nil(t, x.ToV2())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("default values", func(t *testing.T) {
|
||||||
|
attr := container.NewAttribute()
|
||||||
|
|
||||||
|
// check initial values
|
||||||
|
require.Empty(t, attr.Key())
|
||||||
|
require.Empty(t, attr.Value())
|
||||||
|
|
||||||
|
// convert to v2 message
|
||||||
|
attrV2 := attr.ToV2()
|
||||||
|
require.Empty(t, attrV2.GetKey())
|
||||||
|
require.Empty(t, attrV2.GetValue())
|
||||||
|
})
|
||||||
|
|
||||||
|
const (
|
||||||
|
key = "key"
|
||||||
|
value = "value"
|
||||||
|
)
|
||||||
|
|
||||||
|
attr := container.NewAttribute()
|
||||||
|
attr.SetKey(key)
|
||||||
|
attr.SetValue(value)
|
||||||
|
|
||||||
|
require.Equal(t, key, attr.Key())
|
||||||
|
require.Equal(t, value, attr.Value())
|
||||||
|
|
||||||
|
t.Run("test v2", func(t *testing.T) {
|
||||||
|
const (
|
||||||
|
newKey = "newKey"
|
||||||
|
newValue = "newValue"
|
||||||
|
)
|
||||||
|
|
||||||
|
v2 := attr.ToV2()
|
||||||
|
require.Equal(t, key, v2.GetKey())
|
||||||
|
require.Equal(t, value, v2.GetValue())
|
||||||
|
|
||||||
|
v2.SetKey(newKey)
|
||||||
|
v2.SetValue(newValue)
|
||||||
|
|
||||||
|
newAttr := container.NewAttributeFromV2(v2)
|
||||||
|
|
||||||
|
require.Equal(t, newKey, newAttr.Key())
|
||||||
|
require.Equal(t, newValue, newAttr.Value())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAttributes(t *testing.T) {
|
||||||
|
t.Run("nil", func(t *testing.T) {
|
||||||
|
var x container.Attributes
|
||||||
|
|
||||||
|
require.Nil(t, x.ToV2())
|
||||||
|
|
||||||
|
require.Nil(t, container.NewAttributesFromV2(nil))
|
||||||
|
})
|
||||||
|
|
||||||
|
var (
|
||||||
|
keys = []string{"key1", "key2", "key3"}
|
||||||
|
vals = []string{"val1", "val2", "val3"}
|
||||||
|
)
|
||||||
|
|
||||||
|
attrs := make(container.Attributes, len(keys))
|
||||||
|
|
||||||
|
for i := range keys {
|
||||||
|
attrs[i].SetKey(keys[i])
|
||||||
|
attrs[i].SetValue(vals[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("test v2", func(t *testing.T) {
|
||||||
|
const postfix = "x"
|
||||||
|
|
||||||
|
v2 := attrs.ToV2()
|
||||||
|
require.Len(t, v2, len(keys))
|
||||||
|
|
||||||
|
for i := range v2 {
|
||||||
|
k := v2[i].GetKey()
|
||||||
|
v := v2[i].GetValue()
|
||||||
|
|
||||||
|
require.Equal(t, keys[i], k)
|
||||||
|
require.Equal(t, vals[i], v)
|
||||||
|
|
||||||
|
v2[i].SetKey(k + postfix)
|
||||||
|
v2[i].SetValue(v + postfix)
|
||||||
|
}
|
||||||
|
|
||||||
|
newAttrs := container.NewAttributesFromV2(v2)
|
||||||
|
require.Len(t, newAttrs, len(keys))
|
||||||
|
|
||||||
|
for i := range newAttrs {
|
||||||
|
require.Equal(t, keys[i]+postfix, newAttrs[i].Key())
|
||||||
|
require.Equal(t, vals[i]+postfix, newAttrs[i].Value())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewAttributeFromV2(t *testing.T) {
|
||||||
|
t.Run("from nil", func(t *testing.T) {
|
||||||
|
var x *containerv2.Attribute
|
||||||
|
|
||||||
|
require.Nil(t, container.NewAttributeFromV2(x))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetNameWithZone(t *testing.T) {
|
||||||
|
c := container.New()
|
||||||
|
|
||||||
|
for _, item := range [...]struct {
|
||||||
|
name, zone string
|
||||||
|
}{
|
||||||
|
{"name1", ""},
|
||||||
|
{"name1", "zone1"},
|
||||||
|
{"name2", "zone1"},
|
||||||
|
{"name2", "zone2"},
|
||||||
|
{"", "zone2"},
|
||||||
|
{"", ""},
|
||||||
|
} {
|
||||||
|
container.SetNativeNameWithZone(c, item.name, item.zone)
|
||||||
|
|
||||||
|
name, zone := container.GetNativeNameWithZone(c)
|
||||||
|
|
||||||
|
require.Equal(t, item.name, name, item.name)
|
||||||
|
require.Equal(t, item.zone, zone, item.zone)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetNativeName(t *testing.T) {
|
||||||
|
c := container.New()
|
||||||
|
|
||||||
|
const nameDefZone = "some name"
|
||||||
|
|
||||||
|
container.SetNativeName(c, nameDefZone)
|
||||||
|
|
||||||
|
name, zone := container.GetNativeNameWithZone(c)
|
||||||
|
|
||||||
|
require.Equal(t, nameDefZone, name)
|
||||||
|
require.Equal(t, containerv2.SysAttributeZoneDefault, zone)
|
||||||
|
}
|
|
@ -1,521 +1,193 @@
|
||||||
package container
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
|
|
||||||
v2netmap "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/netmap"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl"
|
|
||||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
|
||||||
frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto"
|
|
||||||
frostfsecdsa "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto/ecdsa"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/version"
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/nspcc-dev/neofs-api-go/v2/container"
|
||||||
|
"github.com/nspcc-dev/neofs-sdk-go/acl"
|
||||||
|
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
|
||||||
|
"github.com/nspcc-dev/neofs-sdk-go/netmap"
|
||||||
|
"github.com/nspcc-dev/neofs-sdk-go/owner"
|
||||||
|
"github.com/nspcc-dev/neofs-sdk-go/session"
|
||||||
|
"github.com/nspcc-dev/neofs-sdk-go/signature"
|
||||||
|
"github.com/nspcc-dev/neofs-sdk-go/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Container represents descriptor of the FrostFS container. Container logically
|
|
||||||
// stores FrostFS objects. Container is one of the basic and at the same time
|
|
||||||
// necessary data storage units in the FrostFS. Container includes data about the
|
|
||||||
// owner, rules for placing objects and other information necessary for the
|
|
||||||
// system functioning.
|
|
||||||
//
|
|
||||||
// Container type instances can represent different container states in the
|
|
||||||
// system, depending on the context. To create new container in FrostFS zero
|
|
||||||
// instance SHOULD be declared, initialized using Init method and filled using
|
|
||||||
// dedicated methods. Once container is saved in the FrostFS network, it can't be
|
|
||||||
// changed: containers stored in the system are immutable, and FrostFS is a CAS
|
|
||||||
// of containers that are identified by a fixed length value (see cid.ID type).
|
|
||||||
// Instances for existing containers can be initialized using decoding methods
|
|
||||||
// (e.g Unmarshal).
|
|
||||||
//
|
|
||||||
// Container is mutually compatible with git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container.Container
|
|
||||||
// message. See ReadFromV2 / WriteToV2 methods.
|
|
||||||
type Container struct {
|
type Container struct {
|
||||||
v2 container.Container
|
v2 container.Container
|
||||||
|
|
||||||
|
token *session.Token
|
||||||
|
|
||||||
|
sig *signature.Signature
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
// New creates, initializes and returns blank Container instance.
|
||||||
attributeName = "Name"
|
|
||||||
attributeTimestamp = "Timestamp"
|
|
||||||
)
|
|
||||||
|
|
||||||
// reads Container from the container.Container message. If checkFieldPresence is set,
|
|
||||||
// returns an error on absence of any protocol-required field.
|
|
||||||
func (x *Container) readFromV2(m container.Container, checkFieldPresence bool) error {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
ownerV2 := m.GetOwnerID()
|
|
||||||
if ownerV2 != nil {
|
|
||||||
var owner user.ID
|
|
||||||
|
|
||||||
err = owner.ReadFromV2(*ownerV2)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid owner: %w", err)
|
|
||||||
}
|
|
||||||
} else if checkFieldPresence {
|
|
||||||
return errors.New("missing owner")
|
|
||||||
}
|
|
||||||
|
|
||||||
binNonce := m.GetNonce()
|
|
||||||
if len(binNonce) > 0 {
|
|
||||||
var nonce uuid.UUID
|
|
||||||
|
|
||||||
err = nonce.UnmarshalBinary(binNonce)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid nonce: %w", err)
|
|
||||||
} else if ver := nonce.Version(); ver != 4 {
|
|
||||||
return fmt.Errorf("invalid nonce UUID version %d", ver)
|
|
||||||
}
|
|
||||||
} else if checkFieldPresence {
|
|
||||||
return errors.New("missing nonce")
|
|
||||||
}
|
|
||||||
|
|
||||||
ver := m.GetVersion()
|
|
||||||
if checkFieldPresence && ver == nil {
|
|
||||||
return errors.New("missing version")
|
|
||||||
}
|
|
||||||
|
|
||||||
policyV2 := m.GetPlacementPolicy()
|
|
||||||
if policyV2 != nil {
|
|
||||||
var policy netmap.PlacementPolicy
|
|
||||||
|
|
||||||
err = policy.ReadFromV2(*policyV2)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid placement policy: %w", err)
|
|
||||||
}
|
|
||||||
} else if checkFieldPresence {
|
|
||||||
return errors.New("missing placement policy")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := checkAttributes(m); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
x.v2 = m
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkAttributes(m container.Container) error {
|
|
||||||
attrs := m.GetAttributes()
|
|
||||||
mAttr := make(map[string]struct{}, len(attrs))
|
|
||||||
var key, val string
|
|
||||||
var was bool
|
|
||||||
|
|
||||||
for i := range attrs {
|
|
||||||
key = attrs[i].GetKey()
|
|
||||||
if key == "" {
|
|
||||||
return errors.New("empty attribute key")
|
|
||||||
}
|
|
||||||
|
|
||||||
_, was = mAttr[key]
|
|
||||||
if was {
|
|
||||||
return fmt.Errorf("duplicated attribute %s", key)
|
|
||||||
}
|
|
||||||
|
|
||||||
val = attrs[i].GetValue()
|
|
||||||
if val == "" {
|
|
||||||
return fmt.Errorf("empty attribute value %s", key)
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
if key == attributeTimestamp {
|
|
||||||
_, err = strconv.ParseInt(val, 10, 64)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid attribute value %s: %s (%w)", key, val, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
mAttr[key] = struct{}{}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadFromV2 reads Container from the container.Container message. Checks if the
|
|
||||||
// message conforms to FrostFS API V2 protocol.
|
|
||||||
//
|
//
|
||||||
// See also WriteToV2.
|
// Defaults:
|
||||||
func (x *Container) ReadFromV2(m container.Container) error {
|
// - token: nil;
|
||||||
return x.readFromV2(m, true)
|
// - sig: nil;
|
||||||
|
// - basicACL: acl.PrivateBasicRule;
|
||||||
|
// - version: version.Current;
|
||||||
|
// - nonce: random UUID;
|
||||||
|
// - attr: nil;
|
||||||
|
// - policy: nil;
|
||||||
|
// - ownerID: nil.
|
||||||
|
func New(opts ...Option) *Container {
|
||||||
|
cnrOptions := defaultContainerOptions()
|
||||||
|
|
||||||
|
for i := range opts {
|
||||||
|
opts[i](&cnrOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
cnr := new(Container)
|
||||||
|
cnr.SetNonceUUID(cnrOptions.nonce)
|
||||||
|
cnr.SetBasicACL(cnrOptions.acl)
|
||||||
|
|
||||||
|
if cnrOptions.owner != nil {
|
||||||
|
cnr.SetOwnerID(cnrOptions.owner)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cnrOptions.policy != nil {
|
||||||
|
cnr.SetPlacementPolicy(cnrOptions.policy)
|
||||||
|
}
|
||||||
|
|
||||||
|
cnr.SetAttributes(cnrOptions.attributes)
|
||||||
|
cnr.SetVersion(version.Current())
|
||||||
|
|
||||||
|
return cnr
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteToV2 writes Container into the container.Container message.
|
// ToV2 returns the v2 Container message.
|
||||||
// The message MUST NOT be nil.
|
|
||||||
//
|
//
|
||||||
// See also ReadFromV2.
|
// Nil Container converts to nil.
|
||||||
func (x Container) WriteToV2(m *container.Container) {
|
func (c *Container) ToV2() *container.Container {
|
||||||
*m = x.v2
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &c.v2
|
||||||
}
|
}
|
||||||
|
|
||||||
// Marshal encodes Container into a binary format of the FrostFS API protocol
|
// NewVerifiedFromV2 constructs Container from NeoFS API V2 Container message.
|
||||||
// (Protocol Buffers with direct field order).
|
|
||||||
//
|
//
|
||||||
// See also Unmarshal.
|
// Does not perform if message meets NeoFS API V2 specification. To do this
|
||||||
func (x Container) Marshal() []byte {
|
// use NewVerifiedFromV2 constructor.
|
||||||
return x.v2.StableMarshal(nil)
|
func NewContainerFromV2(c *container.Container) *Container {
|
||||||
|
cnr := new(Container)
|
||||||
|
|
||||||
|
if c != nil {
|
||||||
|
cnr.v2 = *c
|
||||||
|
}
|
||||||
|
|
||||||
|
return cnr
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unmarshal decodes FrostFS API protocol binary format into the Container
|
// CalculateID calculates container identifier
|
||||||
// (Protocol Buffers with direct field order). Returns an error describing
|
// based on its structure.
|
||||||
// a format violation.
|
func CalculateID(c *Container) *cid.ID {
|
||||||
//
|
data, err := c.ToV2().StableMarshal(nil)
|
||||||
// See also Marshal.
|
|
||||||
func (x *Container) Unmarshal(data []byte) error {
|
|
||||||
var m container.Container
|
|
||||||
|
|
||||||
err := m.Unmarshal(data)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return x.readFromV2(m, false)
|
id := cid.New()
|
||||||
|
id.SetSHA256(sha256.Sum256(data))
|
||||||
|
|
||||||
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalJSON encodes Container into a JSON format of the FrostFS API protocol
|
func (c *Container) Version() *version.Version {
|
||||||
// (Protocol Buffers JSON).
|
return version.NewFromV2(c.v2.GetVersion())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Container) SetVersion(v *version.Version) {
|
||||||
|
c.v2.SetVersion(v.ToV2())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Container) OwnerID() *owner.ID {
|
||||||
|
return owner.NewIDFromV2(c.v2.GetOwnerID())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Container) SetOwnerID(v *owner.ID) {
|
||||||
|
c.v2.SetOwnerID(v.ToV2())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns container nonce in UUID format.
|
||||||
//
|
//
|
||||||
// See also UnmarshalJSON.
|
// Returns error if container nonce is not a valid UUID.
|
||||||
func (x Container) MarshalJSON() ([]byte, error) {
|
func (c *Container) NonceUUID() (uuid.UUID, error) {
|
||||||
return x.v2.MarshalJSON()
|
return uuid.FromBytes(c.v2.GetNonce())
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalJSON decodes FrostFS API protocol JSON format into the Container
|
// SetNonceUUID sets container nonce as UUID.
|
||||||
// (Protocol Buffers JSON). Returns an error describing a format violation.
|
func (c *Container) SetNonceUUID(v uuid.UUID) {
|
||||||
//
|
data, _ := v.MarshalBinary()
|
||||||
// See also MarshalJSON.
|
c.v2.SetNonce(data)
|
||||||
func (x *Container) UnmarshalJSON(data []byte) error {
|
|
||||||
return x.v2.UnmarshalJSON(data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init initializes all internal data of the Container required by FrostFS API
|
func (c *Container) BasicACL() uint32 {
|
||||||
// protocol. Init MUST be called when creating a new container. Init SHOULD NOT
|
return c.v2.GetBasicACL()
|
||||||
// be called multiple times. Init SHOULD NOT be called if the Container instance
|
|
||||||
// is used for decoding only.
|
|
||||||
func (x *Container) Init() {
|
|
||||||
var ver refs.Version
|
|
||||||
version.Current().WriteToV2(&ver)
|
|
||||||
|
|
||||||
x.v2.SetVersion(&ver)
|
|
||||||
|
|
||||||
nonce, err := uuid.New().MarshalBinary()
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("unexpected error from UUID.MarshalBinary: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
x.v2.SetNonce(nonce)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetOwner specifies the owner of the Container. Each Container has exactly
|
func (c *Container) SetBasicACL(v acl.BasicACL) {
|
||||||
// one owner, so SetOwner MUST be called for instances to be saved in the
|
c.v2.SetBasicACL(uint32(v))
|
||||||
// FrostFS.
|
|
||||||
//
|
|
||||||
// See also Owner.
|
|
||||||
func (x *Container) SetOwner(owner user.ID) {
|
|
||||||
var m refs.OwnerID
|
|
||||||
owner.WriteToV2(&m)
|
|
||||||
|
|
||||||
x.v2.SetOwnerID(&m)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Owner returns owner of the Container set using SetOwner.
|
func (c *Container) Attributes() Attributes {
|
||||||
//
|
return NewAttributesFromV2(c.v2.GetAttributes())
|
||||||
// Zero Container has no owner which is incorrect according to FrostFS API
|
|
||||||
// protocol.
|
|
||||||
func (x Container) Owner() (res user.ID) {
|
|
||||||
m := x.v2.GetOwnerID()
|
|
||||||
if m != nil {
|
|
||||||
err := res.ReadFromV2(*m)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("unexpected error from user.ID.ReadFromV2: %v", err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetBasicACL specifies basic part of the Container ACL. Basic ACL is used
|
func (c *Container) SetAttributes(v Attributes) {
|
||||||
// to control access inside container storage.
|
c.v2.SetAttributes(v.ToV2())
|
||||||
//
|
|
||||||
// See also BasicACL.
|
|
||||||
func (x *Container) SetBasicACL(basicACL acl.Basic) {
|
|
||||||
x.v2.SetBasicACL(basicACL.Bits())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// BasicACL returns basic ACL set using SetBasicACL.
|
func (c *Container) PlacementPolicy() *netmap.PlacementPolicy {
|
||||||
//
|
return netmap.NewPlacementPolicyFromV2(c.v2.GetPlacementPolicy())
|
||||||
// Zero Container has zero basic ACL which structurally correct but doesn't
|
|
||||||
// make sense since it denies any access to any party.
|
|
||||||
func (x Container) BasicACL() (res acl.Basic) {
|
|
||||||
res.FromBits(x.v2.GetBasicACL())
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetPlacementPolicy sets placement policy for the objects within the Container.
|
func (c *Container) SetPlacementPolicy(v *netmap.PlacementPolicy) {
|
||||||
// FrostFS storage layer strives to follow the specified policy.
|
c.v2.SetPlacementPolicy(v.ToV2())
|
||||||
//
|
|
||||||
// See also PlacementPolicy.
|
|
||||||
func (x *Container) SetPlacementPolicy(policy netmap.PlacementPolicy) {
|
|
||||||
var m v2netmap.PlacementPolicy
|
|
||||||
policy.WriteToV2(&m)
|
|
||||||
|
|
||||||
x.v2.SetPlacementPolicy(&m)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PlacementPolicy returns placement policy set using SetPlacementPolicy.
|
// SessionToken returns token of the session within
|
||||||
//
|
// which container was created.
|
||||||
// Zero Container has no placement policy which is incorrect according to
|
func (c Container) SessionToken() *session.Token {
|
||||||
// FrostFS API protocol.
|
return c.token
|
||||||
func (x Container) PlacementPolicy() (res netmap.PlacementPolicy) {
|
|
||||||
m := x.v2.GetPlacementPolicy()
|
|
||||||
if m != nil {
|
|
||||||
err := res.ReadFromV2(*m)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("unexpected error from PlacementPolicy.ReadFromV2: %v", err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetAttribute sets Container attribute value by key. Both key and value
|
// SetSessionToken sets token of the session within
|
||||||
// MUST NOT be empty. Attributes set by the creator (owner) are most commonly
|
// which container was created.
|
||||||
// ignored by the FrostFS system and used for application layer. Some attributes
|
func (c *Container) SetSessionToken(t *session.Token) {
|
||||||
// are so-called system or well-known attributes: they are reserved for system
|
c.token = t
|
||||||
// needs. System attributes SHOULD NOT be modified using SetAttribute, use
|
|
||||||
// corresponding methods/functions. List of the reserved keys is documented
|
|
||||||
// in the particular protocol version.
|
|
||||||
//
|
|
||||||
// SetAttribute overwrites existing attribute value.
|
|
||||||
//
|
|
||||||
// See also Attribute, IterateAttributes, IterateUserAttributes.
|
|
||||||
func (x *Container) SetAttribute(key, value string) {
|
|
||||||
if key == "" {
|
|
||||||
panic("empty attribute key")
|
|
||||||
} else if value == "" {
|
|
||||||
panic("empty attribute value")
|
|
||||||
}
|
|
||||||
|
|
||||||
attrs := x.v2.GetAttributes()
|
|
||||||
ln := len(attrs)
|
|
||||||
|
|
||||||
for i := range ln {
|
|
||||||
if attrs[i].GetKey() == key {
|
|
||||||
attrs[i].SetValue(value)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
attrs = append(attrs, container.Attribute{})
|
|
||||||
attrs[ln].SetKey(key)
|
|
||||||
attrs[ln].SetValue(value)
|
|
||||||
|
|
||||||
x.v2.SetAttributes(attrs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attribute reads value of the Container attribute by key. Empty result means
|
// Signature returns signature of the marshaled container.
|
||||||
// attribute absence.
|
func (c Container) Signature() *signature.Signature {
|
||||||
//
|
return c.sig
|
||||||
// See also SetAttribute, IterateAttributes, IterateUserAttributes.
|
|
||||||
func (x Container) Attribute(key string) string {
|
|
||||||
attrs := x.v2.GetAttributes()
|
|
||||||
for i := range attrs {
|
|
||||||
if attrs[i].GetKey() == key {
|
|
||||||
return attrs[i].GetValue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IterateAttributes iterates over all Container attributes and passes them
|
// SetSignature sets signature of the marshaled container.
|
||||||
// into f. The handler MUST NOT be nil.
|
func (c *Container) SetSignature(sig *signature.Signature) {
|
||||||
//
|
c.sig = sig
|
||||||
// See also SetAttribute, Attribute.
|
|
||||||
func (x Container) IterateAttributes(f func(key, val string)) {
|
|
||||||
attrs := x.v2.GetAttributes()
|
|
||||||
for i := range attrs {
|
|
||||||
f(attrs[i].GetKey(), attrs[i].GetValue())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IterateUserAttributes iterates over user Container attributes and passes them
|
// Marshal marshals Container into a protobuf binary form.
|
||||||
// into f. The handler MUST NOT be nil.
|
func (c *Container) Marshal() ([]byte, error) {
|
||||||
//
|
return c.v2.StableMarshal(nil)
|
||||||
// See also SetAttribute, Attribute.
|
|
||||||
func (x Container) IterateUserAttributes(f func(key, val string)) {
|
|
||||||
attrs := x.v2.GetAttributes()
|
|
||||||
for _, attr := range attrs {
|
|
||||||
key := attr.GetKey()
|
|
||||||
if !strings.HasPrefix(key, container.SysAttributePrefix) &&
|
|
||||||
!strings.HasPrefix(key, container.SysAttributePrefixNeoFS) {
|
|
||||||
f(key, attr.GetValue())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetName sets human-readable name of the Container. Name MUST NOT be empty.
|
// Unmarshal unmarshals protobuf binary representation of Container.
|
||||||
//
|
func (c *Container) Unmarshal(data []byte) error {
|
||||||
// See also Name.
|
return c.v2.Unmarshal(data)
|
||||||
func SetName(cnr *Container, name string) {
|
|
||||||
cnr.SetAttribute(attributeName, name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name returns container name set using SetName.
|
// MarshalJSON encodes Container to protobuf JSON format.
|
||||||
//
|
func (c *Container) MarshalJSON() ([]byte, error) {
|
||||||
// Zero Container has no name.
|
return c.v2.MarshalJSON()
|
||||||
func Name(cnr Container) string {
|
|
||||||
return cnr.Attribute(attributeName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetCreationTime writes container's creation time in Unix Timestamp format.
|
// UnmarshalJSON decodes Container from protobuf JSON format.
|
||||||
//
|
func (c *Container) UnmarshalJSON(data []byte) error {
|
||||||
// See also CreatedAt.
|
return c.v2.UnmarshalJSON(data)
|
||||||
func SetCreationTime(cnr *Container, t time.Time) {
|
|
||||||
cnr.SetAttribute(attributeTimestamp, strconv.FormatInt(t.Unix(), 10))
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreatedAt returns container's creation time set using SetCreationTime.
|
|
||||||
//
|
|
||||||
// Zero Container has zero timestamp (in seconds).
|
|
||||||
func CreatedAt(cnr Container) time.Time {
|
|
||||||
var sec int64
|
|
||||||
|
|
||||||
attr := cnr.Attribute(attributeTimestamp)
|
|
||||||
if attr != "" {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
sec, err = strconv.ParseInt(cnr.Attribute(attributeTimestamp), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("parse container timestamp: %v", err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return time.Unix(sec, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
const attributeHomoHashEnabled = "true"
|
|
||||||
|
|
||||||
// DisableHomomorphicHashing sets flag to disable homomorphic hashing of the
|
|
||||||
// Container data.
|
|
||||||
//
|
|
||||||
// See also IsHomomorphicHashingDisabled.
|
|
||||||
func DisableHomomorphicHashing(cnr *Container) {
|
|
||||||
cnr.SetAttribute(container.SysAttributeHomomorphicHashing, attributeHomoHashEnabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsHomomorphicHashingDisabled checks if DisableHomomorphicHashing was called.
|
|
||||||
//
|
|
||||||
// Zero Container has enabled hashing.
|
|
||||||
func IsHomomorphicHashingDisabled(cnr Container) bool {
|
|
||||||
return cnr.Attribute(container.SysAttributeHomomorphicHashing) == attributeHomoHashEnabled ||
|
|
||||||
cnr.Attribute(container.SysAttributeHomomorphicHashingNeoFS) == attributeHomoHashEnabled
|
|
||||||
}
|
|
||||||
|
|
||||||
// Domain represents information about container domain registered in the NNS
|
|
||||||
// contract deployed in the FrostFS network.
|
|
||||||
type Domain struct {
|
|
||||||
name, zone string
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetName sets human-friendly container domain name.
|
|
||||||
func (x *Domain) SetName(name string) {
|
|
||||||
x.name = name
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name returns name set using SetName.
|
|
||||||
//
|
|
||||||
// Zero Domain has zero name.
|
|
||||||
func (x Domain) Name() string {
|
|
||||||
return x.name
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetZone sets zone which is used as a TLD of a domain name in NNS contract.
|
|
||||||
func (x *Domain) SetZone(zone string) {
|
|
||||||
x.zone = zone
|
|
||||||
}
|
|
||||||
|
|
||||||
// Zone returns domain zone set using SetZone.
|
|
||||||
//
|
|
||||||
// Zero Domain has "container" zone.
|
|
||||||
func (x Domain) Zone() string {
|
|
||||||
if x.zone != "" {
|
|
||||||
return x.zone
|
|
||||||
}
|
|
||||||
|
|
||||||
return "container"
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteDomain writes Domain into the Container. Name MUST NOT be empty.
|
|
||||||
func WriteDomain(cnr *Container, domain Domain) {
|
|
||||||
cnr.SetAttribute(container.SysAttributeName, domain.Name())
|
|
||||||
cnr.SetAttribute(container.SysAttributeZone, domain.Zone())
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadDomain reads Domain from the Container. Returns value with empty name
|
|
||||||
// if domain is not specified.
|
|
||||||
func ReadDomain(cnr Container) (res Domain) {
|
|
||||||
if name := cnr.Attribute(container.SysAttributeName); name != "" {
|
|
||||||
res.SetName(name)
|
|
||||||
res.SetZone(cnr.Attribute(container.SysAttributeZone))
|
|
||||||
} else if name = cnr.Attribute(container.SysAttributeNameNeoFS); name != "" {
|
|
||||||
res.SetName(name)
|
|
||||||
res.SetZone(cnr.Attribute(container.SysAttributeZoneNeoFS))
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// CalculateSignature calculates signature of the Container using provided signer
|
|
||||||
// and writes it into dst. Signature instance MUST NOT be nil. CalculateSignature
|
|
||||||
// is expected to be called after all the Container data is filled and before
|
|
||||||
// saving the Container in the FrostFS network. Note that мany subsequent change
|
|
||||||
// will most likely break the signature.
|
|
||||||
//
|
|
||||||
// See also VerifySignature.
|
|
||||||
func CalculateSignature(dst *frostfscrypto.Signature, cnr Container, signer ecdsa.PrivateKey) error {
|
|
||||||
return dst.Calculate(frostfsecdsa.SignerRFC6979(signer), cnr.Marshal())
|
|
||||||
}
|
|
||||||
|
|
||||||
// VerifySignature verifies Container signature calculated using CalculateSignature.
|
|
||||||
// Result means signature correctness.
|
|
||||||
func VerifySignature(sig frostfscrypto.Signature, cnr Container) bool {
|
|
||||||
return sig.Verify(cnr.Marshal())
|
|
||||||
}
|
|
||||||
|
|
||||||
// CalculateIDFromBinary calculates identifier of the binary-encoded container
|
|
||||||
// in CAS of the FrostFS containers and writes it into dst. ID instance MUST NOT
|
|
||||||
// be nil.
|
|
||||||
//
|
|
||||||
// See also CalculateID, AssertID.
|
|
||||||
func CalculateIDFromBinary(dst *cid.ID, cnr []byte) {
|
|
||||||
dst.SetSHA256(sha256.Sum256(cnr))
|
|
||||||
}
|
|
||||||
|
|
||||||
// CalculateID encodes the given Container and passes the result into
|
|
||||||
// CalculateIDFromBinary.
|
|
||||||
//
|
|
||||||
// See also Container.Marshal, AssertID.
|
|
||||||
func CalculateID(dst *cid.ID, cnr Container) {
|
|
||||||
CalculateIDFromBinary(dst, cnr.Marshal())
|
|
||||||
}
|
|
||||||
|
|
||||||
// AssertID checks if the given Container matches its identifier in CAS of the
|
|
||||||
// FrostFS containers.
|
|
||||||
//
|
|
||||||
// See also CalculateID.
|
|
||||||
func AssertID(id cid.ID, cnr Container) bool {
|
|
||||||
var id2 cid.ID
|
|
||||||
CalculateID(&id2, cnr)
|
|
||||||
|
|
||||||
return id2.Equals(id)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,333 +1,136 @@
|
||||||
package container_test
|
package container_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
|
||||||
"strconv"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
v2container "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
|
|
||||||
v2netmap "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/netmap"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
|
|
||||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
|
||||||
cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test"
|
|
||||||
containertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/test"
|
|
||||||
frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto"
|
|
||||||
netmaptest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap/test"
|
|
||||||
usertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user/test"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/version"
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
"github.com/nspcc-dev/neofs-sdk-go/acl"
|
||||||
|
"github.com/nspcc-dev/neofs-sdk-go/container"
|
||||||
|
containertest "github.com/nspcc-dev/neofs-sdk-go/container/test"
|
||||||
|
netmaptest "github.com/nspcc-dev/neofs-sdk-go/netmap/test"
|
||||||
|
ownertest "github.com/nspcc-dev/neofs-sdk-go/owner/test"
|
||||||
|
sessiontest "github.com/nspcc-dev/neofs-sdk-go/session/test"
|
||||||
|
sigtest "github.com/nspcc-dev/neofs-sdk-go/signature/test"
|
||||||
|
"github.com/nspcc-dev/neofs-sdk-go/version"
|
||||||
|
versiontest "github.com/nspcc-dev/neofs-sdk-go/version/test"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPlacementPolicyEncoding(t *testing.T) {
|
func TestNewContainer(t *testing.T) {
|
||||||
v := containertest.Container()
|
c := container.New()
|
||||||
|
|
||||||
|
nonce := uuid.New()
|
||||||
|
|
||||||
|
ownerID := ownertest.ID()
|
||||||
|
policy := netmaptest.PlacementPolicy()
|
||||||
|
|
||||||
|
c.SetBasicACL(acl.PublicBasicRule)
|
||||||
|
|
||||||
|
attrs := containertest.Attributes()
|
||||||
|
c.SetAttributes(attrs)
|
||||||
|
|
||||||
|
c.SetPlacementPolicy(policy)
|
||||||
|
c.SetNonceUUID(nonce)
|
||||||
|
c.SetOwnerID(ownerID)
|
||||||
|
|
||||||
|
ver := versiontest.Version()
|
||||||
|
c.SetVersion(ver)
|
||||||
|
|
||||||
|
v2 := c.ToV2()
|
||||||
|
newContainer := container.NewContainerFromV2(v2)
|
||||||
|
|
||||||
|
require.EqualValues(t, newContainer.PlacementPolicy(), policy)
|
||||||
|
require.EqualValues(t, newContainer.Attributes(), attrs)
|
||||||
|
require.EqualValues(t, newContainer.BasicACL(), acl.PublicBasicRule)
|
||||||
|
|
||||||
|
newNonce, err := newContainer.NonceUUID()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.EqualValues(t, newNonce, nonce)
|
||||||
|
require.EqualValues(t, newContainer.OwnerID(), ownerID)
|
||||||
|
require.EqualValues(t, newContainer.Version(), ver)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContainerEncoding(t *testing.T) {
|
||||||
|
c := containertest.Container()
|
||||||
|
|
||||||
t.Run("binary", func(t *testing.T) {
|
t.Run("binary", func(t *testing.T) {
|
||||||
var v2 container.Container
|
data, err := c.Marshal()
|
||||||
require.NoError(t, v2.Unmarshal(v.Marshal()))
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.Equal(t, v, v2)
|
c2 := container.New()
|
||||||
|
require.NoError(t, c2.Unmarshal(data))
|
||||||
|
|
||||||
|
require.Equal(t, c, c2)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("json", func(t *testing.T) {
|
t.Run("json", func(t *testing.T) {
|
||||||
data, err := v.MarshalJSON()
|
data, err := c.MarshalJSON()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var v2 container.Container
|
c2 := container.New()
|
||||||
require.NoError(t, v2.UnmarshalJSON(data))
|
require.NoError(t, c2.UnmarshalJSON(data))
|
||||||
|
|
||||||
require.Equal(t, v, v2)
|
require.Equal(t, c, c2)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestContainer_Init(t *testing.T) {
|
func TestContainer_SessionToken(t *testing.T) {
|
||||||
val := containertest.Container()
|
tok := sessiontest.Token()
|
||||||
|
|
||||||
val.Init()
|
cnr := container.New()
|
||||||
|
|
||||||
var msg v2container.Container
|
cnr.SetSessionToken(tok)
|
||||||
val.WriteToV2(&msg)
|
|
||||||
|
|
||||||
binNonce := msg.GetNonce()
|
require.Equal(t, tok, cnr.SessionToken())
|
||||||
|
|
||||||
var nonce uuid.UUID
|
|
||||||
require.NoError(t, nonce.UnmarshalBinary(binNonce))
|
|
||||||
require.EqualValues(t, 4, nonce.Version())
|
|
||||||
|
|
||||||
verV2 := msg.GetVersion()
|
|
||||||
require.NotNil(t, verV2)
|
|
||||||
|
|
||||||
var ver version.Version
|
|
||||||
require.NoError(t, ver.ReadFromV2(*verV2))
|
|
||||||
|
|
||||||
require.Equal(t, version.Current(), ver)
|
|
||||||
|
|
||||||
var val2 container.Container
|
|
||||||
require.NoError(t, val2.ReadFromV2(msg))
|
|
||||||
|
|
||||||
require.Equal(t, val, val2)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestContainer_Owner(t *testing.T) {
|
func TestContainer_Signature(t *testing.T) {
|
||||||
var val container.Container
|
sig := sigtest.Signature()
|
||||||
|
|
||||||
require.Zero(t, val.Owner())
|
cnr := container.New()
|
||||||
|
cnr.SetSignature(sig)
|
||||||
|
|
||||||
val = containertest.Container()
|
require.Equal(t, sig, cnr.Signature())
|
||||||
|
|
||||||
owner := usertest.ID()
|
|
||||||
|
|
||||||
val.SetOwner(owner)
|
|
||||||
|
|
||||||
var msg v2container.Container
|
|
||||||
val.WriteToV2(&msg)
|
|
||||||
|
|
||||||
var msgOwner refs.OwnerID
|
|
||||||
owner.WriteToV2(&msgOwner)
|
|
||||||
|
|
||||||
require.Equal(t, &msgOwner, msg.GetOwnerID())
|
|
||||||
|
|
||||||
var val2 container.Container
|
|
||||||
require.NoError(t, val2.ReadFromV2(msg))
|
|
||||||
|
|
||||||
require.True(t, val2.Owner().Equals(owner))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestContainer_BasicACL(t *testing.T) {
|
func TestContainer_ToV2(t *testing.T) {
|
||||||
var val container.Container
|
t.Run("nil", func(t *testing.T) {
|
||||||
|
var x *container.Container
|
||||||
|
|
||||||
require.Zero(t, val.BasicACL())
|
require.Nil(t, x.ToV2())
|
||||||
|
|
||||||
val = containertest.Container()
|
|
||||||
|
|
||||||
basicACL := containertest.BasicACL()
|
|
||||||
val.SetBasicACL(basicACL)
|
|
||||||
|
|
||||||
var msg v2container.Container
|
|
||||||
val.WriteToV2(&msg)
|
|
||||||
|
|
||||||
require.EqualValues(t, basicACL.Bits(), msg.GetBasicACL())
|
|
||||||
|
|
||||||
var val2 container.Container
|
|
||||||
require.NoError(t, val2.ReadFromV2(msg))
|
|
||||||
|
|
||||||
require.Equal(t, basicACL, val2.BasicACL())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestContainer_PlacementPolicy(t *testing.T) {
|
|
||||||
var val container.Container
|
|
||||||
|
|
||||||
require.Zero(t, val.PlacementPolicy())
|
|
||||||
|
|
||||||
val = containertest.Container()
|
|
||||||
|
|
||||||
pp := netmaptest.PlacementPolicy()
|
|
||||||
val.SetPlacementPolicy(pp)
|
|
||||||
|
|
||||||
var msgPolicy v2netmap.PlacementPolicy
|
|
||||||
pp.WriteToV2(&msgPolicy)
|
|
||||||
|
|
||||||
var msg v2container.Container
|
|
||||||
val.WriteToV2(&msg)
|
|
||||||
|
|
||||||
require.Equal(t, &msgPolicy, msg.GetPlacementPolicy())
|
|
||||||
|
|
||||||
var val2 container.Container
|
|
||||||
require.NoError(t, val2.ReadFromV2(msg))
|
|
||||||
|
|
||||||
require.Equal(t, pp, val2.PlacementPolicy())
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertContainsAttribute(t *testing.T, m v2container.Container, key, val string) {
|
|
||||||
var msgAttr v2container.Attribute
|
|
||||||
|
|
||||||
msgAttr.SetKey(key)
|
|
||||||
msgAttr.SetValue(val)
|
|
||||||
require.Contains(t, m.GetAttributes(), msgAttr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestContainer_Attribute(t *testing.T) {
|
|
||||||
const attrKey1, attrKey2 = v2container.SysAttributePrefix + "key1", v2container.SysAttributePrefixNeoFS + "key2"
|
|
||||||
const attrVal1, attrVal2 = "val1", "val2"
|
|
||||||
|
|
||||||
val := containertest.Container()
|
|
||||||
|
|
||||||
val.SetAttribute(attrKey1, attrVal1)
|
|
||||||
val.SetAttribute(attrKey2, attrVal2)
|
|
||||||
|
|
||||||
var i int
|
|
||||||
val.IterateUserAttributes(func(key, val string) {
|
|
||||||
i++
|
|
||||||
})
|
|
||||||
require.Equal(t, 1, i)
|
|
||||||
|
|
||||||
var msg v2container.Container
|
|
||||||
val.WriteToV2(&msg)
|
|
||||||
|
|
||||||
require.GreaterOrEqual(t, len(msg.GetAttributes()), 2)
|
|
||||||
assertContainsAttribute(t, msg, attrKey1, attrVal1)
|
|
||||||
assertContainsAttribute(t, msg, attrKey2, attrVal2)
|
|
||||||
|
|
||||||
var val2 container.Container
|
|
||||||
require.NoError(t, val2.ReadFromV2(msg))
|
|
||||||
|
|
||||||
require.Equal(t, attrVal1, val2.Attribute(attrKey1))
|
|
||||||
require.Equal(t, attrVal2, val2.Attribute(attrKey2))
|
|
||||||
|
|
||||||
m := map[string]string{}
|
|
||||||
|
|
||||||
val2.IterateAttributes(func(key, val string) {
|
|
||||||
m[key] = val
|
|
||||||
})
|
})
|
||||||
|
|
||||||
require.GreaterOrEqual(t, len(m), 2)
|
t.Run("default values", func(t *testing.T) {
|
||||||
require.Equal(t, attrVal1, m[attrKey1])
|
cnt := container.New()
|
||||||
require.Equal(t, attrVal2, m[attrKey2])
|
|
||||||
|
|
||||||
val2.SetAttribute(attrKey1, attrVal1+"_")
|
// check initial values
|
||||||
require.Equal(t, attrVal1+"_", val2.Attribute(attrKey1))
|
require.Nil(t, cnt.SessionToken())
|
||||||
}
|
require.Nil(t, cnt.Signature())
|
||||||
|
require.Nil(t, cnt.Attributes())
|
||||||
|
require.Nil(t, cnt.PlacementPolicy())
|
||||||
|
require.Nil(t, cnt.OwnerID())
|
||||||
|
|
||||||
func TestSetName(t *testing.T) {
|
require.EqualValues(t, acl.PrivateBasicRule, cnt.BasicACL())
|
||||||
var val container.Container
|
require.Equal(t, version.Current(), cnt.Version())
|
||||||
|
|
||||||
require.Panics(t, func() {
|
nonce, err := cnt.NonceUUID()
|
||||||
container.SetName(&val, "")
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, nonce)
|
||||||
|
|
||||||
|
// convert to v2 message
|
||||||
|
cntV2 := cnt.ToV2()
|
||||||
|
|
||||||
|
nonceV2, err := uuid.FromBytes(cntV2.GetNonce())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, nonce.String(), nonceV2.String())
|
||||||
|
|
||||||
|
require.Nil(t, cntV2.GetAttributes())
|
||||||
|
require.Nil(t, cntV2.GetPlacementPolicy())
|
||||||
|
require.Nil(t, cntV2.GetOwnerID())
|
||||||
|
|
||||||
|
require.Equal(t, uint32(acl.PrivateBasicRule), cntV2.GetBasicACL())
|
||||||
|
require.Equal(t, version.Current().ToV2(), cntV2.GetVersion())
|
||||||
})
|
})
|
||||||
|
|
||||||
val = containertest.Container()
|
|
||||||
|
|
||||||
const name = "some name"
|
|
||||||
|
|
||||||
container.SetName(&val, name)
|
|
||||||
|
|
||||||
var msg v2container.Container
|
|
||||||
val.WriteToV2(&msg)
|
|
||||||
|
|
||||||
assertContainsAttribute(t, msg, "Name", name)
|
|
||||||
|
|
||||||
var val2 container.Container
|
|
||||||
require.NoError(t, val2.ReadFromV2(msg))
|
|
||||||
|
|
||||||
require.Equal(t, name, container.Name(val2))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSetCreationTime(t *testing.T) {
|
|
||||||
var val container.Container
|
|
||||||
|
|
||||||
require.Zero(t, container.CreatedAt(val).Unix())
|
|
||||||
|
|
||||||
val = containertest.Container()
|
|
||||||
|
|
||||||
creat := time.Now()
|
|
||||||
|
|
||||||
container.SetCreationTime(&val, creat)
|
|
||||||
|
|
||||||
var msg v2container.Container
|
|
||||||
val.WriteToV2(&msg)
|
|
||||||
|
|
||||||
assertContainsAttribute(t, msg, "Timestamp", strconv.FormatInt(creat.Unix(), 10))
|
|
||||||
|
|
||||||
var val2 container.Container
|
|
||||||
require.NoError(t, val2.ReadFromV2(msg))
|
|
||||||
|
|
||||||
require.Equal(t, creat.Unix(), container.CreatedAt(val2).Unix())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDisableHomomorphicHashing(t *testing.T) {
|
|
||||||
var val container.Container
|
|
||||||
|
|
||||||
require.False(t, container.IsHomomorphicHashingDisabled(val))
|
|
||||||
|
|
||||||
val = containertest.Container()
|
|
||||||
|
|
||||||
container.DisableHomomorphicHashing(&val)
|
|
||||||
|
|
||||||
var msg v2container.Container
|
|
||||||
val.WriteToV2(&msg)
|
|
||||||
|
|
||||||
assertContainsAttribute(t, msg, v2container.SysAttributePrefix+"DISABLE_HOMOMORPHIC_HASHING", "true")
|
|
||||||
|
|
||||||
var val2 container.Container
|
|
||||||
require.NoError(t, val2.ReadFromV2(msg))
|
|
||||||
|
|
||||||
require.True(t, container.IsHomomorphicHashingDisabled(val2))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWriteDomain(t *testing.T) {
|
|
||||||
var val container.Container
|
|
||||||
|
|
||||||
require.Zero(t, container.ReadDomain(val).Name())
|
|
||||||
|
|
||||||
val = containertest.Container()
|
|
||||||
|
|
||||||
const name = "domain name"
|
|
||||||
|
|
||||||
var d container.Domain
|
|
||||||
d.SetName(name)
|
|
||||||
|
|
||||||
container.WriteDomain(&val, d)
|
|
||||||
|
|
||||||
var msg v2container.Container
|
|
||||||
val.WriteToV2(&msg)
|
|
||||||
|
|
||||||
assertContainsAttribute(t, msg, v2container.SysAttributeName, name)
|
|
||||||
assertContainsAttribute(t, msg, v2container.SysAttributeZone, "container")
|
|
||||||
|
|
||||||
const zone = "domain zone"
|
|
||||||
|
|
||||||
d.SetZone(zone)
|
|
||||||
|
|
||||||
container.WriteDomain(&val, d)
|
|
||||||
|
|
||||||
val.WriteToV2(&msg)
|
|
||||||
|
|
||||||
assertContainsAttribute(t, msg, v2container.SysAttributeZone, zone)
|
|
||||||
|
|
||||||
var val2 container.Container
|
|
||||||
require.NoError(t, val2.ReadFromV2(msg))
|
|
||||||
|
|
||||||
require.Equal(t, d, container.ReadDomain(val2))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCalculateID(t *testing.T) {
|
|
||||||
val := containertest.Container()
|
|
||||||
|
|
||||||
require.False(t, container.AssertID(cidtest.ID(), val))
|
|
||||||
|
|
||||||
var id cid.ID
|
|
||||||
container.CalculateID(&id, val)
|
|
||||||
|
|
||||||
var msg refs.ContainerID
|
|
||||||
id.WriteToV2(&msg)
|
|
||||||
|
|
||||||
h := sha256.Sum256(val.Marshal())
|
|
||||||
require.Equal(t, h[:], msg.GetValue())
|
|
||||||
|
|
||||||
var id2 cid.ID
|
|
||||||
require.NoError(t, id2.ReadFromV2(msg))
|
|
||||||
|
|
||||||
require.True(t, container.AssertID(id2, val))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCalculateSignature(t *testing.T) {
|
|
||||||
key, err := keys.NewPrivateKey()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
val := containertest.Container()
|
|
||||||
|
|
||||||
var sig frostfscrypto.Signature
|
|
||||||
|
|
||||||
require.NoError(t, container.CalculateSignature(&sig, val, key.PrivateKey))
|
|
||||||
|
|
||||||
var msg refs.Signature
|
|
||||||
sig.WriteToV2(&msg)
|
|
||||||
|
|
||||||
var sig2 frostfscrypto.Signature
|
|
||||||
require.NoError(t, sig2.ReadFromV2(msg))
|
|
||||||
|
|
||||||
require.True(t, container.VerifySignature(sig2, val))
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
/*
|
|
||||||
Package container provides functionality related to the FrostFS containers.
|
|
||||||
|
|
||||||
The base type is Container. To create new container in the FrostFS network
|
|
||||||
Container instance should be initialized
|
|
||||||
|
|
||||||
var cnr Container
|
|
||||||
cnr.Init()
|
|
||||||
// fill all the fields
|
|
||||||
|
|
||||||
// encode cnr and send
|
|
||||||
|
|
||||||
After the container is persisted in the FrostFS network, applications can process
|
|
||||||
it using the instance of Container types
|
|
||||||
|
|
||||||
// recv binary container
|
|
||||||
|
|
||||||
var cnr Container
|
|
||||||
|
|
||||||
err := cnr.Unmarshal(bin)
|
|
||||||
// ...
|
|
||||||
|
|
||||||
// process the container data
|
|
||||||
|
|
||||||
Instances can be also used to process FrostFS API V2 protocol messages
|
|
||||||
(see neo.fs.v2.container package in https://git.frostfs.info/TrueCloudLab/frostfs-api).
|
|
||||||
|
|
||||||
On client side:
|
|
||||||
|
|
||||||
import "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
|
|
||||||
|
|
||||||
var msg container.Container
|
|
||||||
cnr.WriteToV2(&msg)
|
|
||||||
|
|
||||||
// send msg
|
|
||||||
|
|
||||||
On server side:
|
|
||||||
|
|
||||||
// recv msg
|
|
||||||
|
|
||||||
var cnr Container
|
|
||||||
cnr.ReadFromV2(msg)
|
|
||||||
|
|
||||||
// process cnr
|
|
||||||
|
|
||||||
Using package types in an application is recommended to potentially work with
|
|
||||||
different protocol versions with which these types are compatible.
|
|
||||||
*/
|
|
||||||
package container
|
|
|
@ -1,7 +0,0 @@
|
||||||
/*
|
|
||||||
Package cid provides primitives to work with container identification in FrostFS.
|
|
||||||
|
|
||||||
Using package types in an application is recommended to potentially work with
|
|
||||||
different protocol versions with which these types are compatible.
|
|
||||||
*/
|
|
||||||
package cid
|
|
|
@ -1,115 +1,90 @@
|
||||||
package cid
|
package cid
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"fmt"
|
"errors"
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
|
||||||
"github.com/mr-tron/base58"
|
"github.com/mr-tron/base58"
|
||||||
|
"github.com/nspcc-dev/neofs-api-go/v2/refs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ID represents FrostFS container identifier.
|
// ID represents v2-compatible container identifier.
|
||||||
//
|
type ID refs.ContainerID
|
||||||
// ID is mutually compatible with git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs.ContainerID
|
|
||||||
// message. See ReadFromV2 / WriteToV2 methods.
|
|
||||||
//
|
|
||||||
// Instances can be created using built-in var declaration.
|
|
||||||
//
|
|
||||||
// Note that direct typecast is not safe and may result in loss of compatibility:
|
|
||||||
//
|
|
||||||
// _ = ID([32]byte) // not recommended
|
|
||||||
type ID [sha256.Size]byte
|
|
||||||
|
|
||||||
// ReadFromV2 reads ID from the refs.ContainerID message.
|
// NewFromV2 wraps v2 ContainerID message to ID.
|
||||||
// Returns an error if the message is malformed according
|
|
||||||
// to the FrostFS API V2 protocol.
|
|
||||||
//
|
//
|
||||||
// See also WriteToV2.
|
// Nil refs.ContainerID converts to nil.
|
||||||
func (id *ID) ReadFromV2(m refs.ContainerID) error {
|
func NewFromV2(idV2 *refs.ContainerID) *ID {
|
||||||
return id.Decode(m.GetValue())
|
return (*ID)(idV2)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteToV2 writes ID to the refs.ContainerID message.
|
// New creates and initializes blank ID.
|
||||||
// The message must not be nil.
|
|
||||||
//
|
//
|
||||||
// See also ReadFromV2.
|
// Defaults:
|
||||||
func (id ID) WriteToV2(m *refs.ContainerID) {
|
// - value: nil.
|
||||||
m.SetValue(id[:])
|
func New() *ID {
|
||||||
|
return NewFromV2(new(refs.ContainerID))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encode encodes ID into 32 bytes of dst. Panics if
|
// SetSHA256 sets container identifier value to SHA256 checksum of container body.
|
||||||
// dst length is less than 32.
|
func (id *ID) SetSHA256(v [sha256.Size]byte) {
|
||||||
|
(*refs.ContainerID)(id).SetValue(v[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToV2 returns the v2 container ID message.
|
||||||
//
|
//
|
||||||
// Zero ID is all zeros.
|
// Nil ID converts to nil.
|
||||||
|
func (id *ID) ToV2() *refs.ContainerID {
|
||||||
|
return (*refs.ContainerID)(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal returns true if identifiers are identical.
|
||||||
|
func (id *ID) Equal(id2 *ID) bool {
|
||||||
|
return bytes.Equal(
|
||||||
|
(*refs.ContainerID)(id).GetValue(),
|
||||||
|
(*refs.ContainerID)(id2).GetValue(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse parses string representation of ID.
|
||||||
//
|
//
|
||||||
// See also Decode.
|
// Returns error if s is not a base58 encoded
|
||||||
func (id ID) Encode(dst []byte) {
|
// ID data.
|
||||||
if l := len(dst); l < sha256.Size {
|
func (id *ID) Parse(s string) error {
|
||||||
panic(fmt.Sprintf("destination length is less than %d bytes: %d", sha256.Size, l))
|
data, err := base58.Decode(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if len(data) != sha256.Size {
|
||||||
|
return errors.New("incorrect format of the string container ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
copy(dst, id[:])
|
(*refs.ContainerID)(id).SetValue(data)
|
||||||
}
|
|
||||||
|
|
||||||
// Decode decodes src bytes into ID.
|
|
||||||
//
|
|
||||||
// Decode expects that src has 32 bytes length. If the input is malformed,
|
|
||||||
// Decode returns an error describing format violation. In this case ID
|
|
||||||
// remains unchanged.
|
|
||||||
//
|
|
||||||
// Decode doesn't mutate src.
|
|
||||||
//
|
|
||||||
// See also Encode.
|
|
||||||
func (id *ID) Decode(src []byte) error {
|
|
||||||
if len(src) != sha256.Size {
|
|
||||||
return fmt.Errorf("invalid length %d", len(src))
|
|
||||||
}
|
|
||||||
|
|
||||||
copy(id[:], src)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetSHA256 sets container identifier value to SHA256 checksum of container structure.
|
// String returns base58 string representation of ID.
|
||||||
func (id *ID) SetSHA256(v [sha256.Size]byte) {
|
func (id *ID) String() string {
|
||||||
copy(id[:], v[:])
|
return base58.Encode((*refs.ContainerID)(id).GetValue())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Equals defines a comparison relation between two ID instances.
|
// Marshal marshals ID into a protobuf binary form.
|
||||||
//
|
func (id *ID) Marshal() ([]byte, error) {
|
||||||
// Note that comparison using '==' operator is not recommended since it MAY result
|
return (*refs.ContainerID)(id).StableMarshal(nil)
|
||||||
// in loss of compatibility.
|
|
||||||
func (id ID) Equals(id2 ID) bool {
|
|
||||||
return id == id2
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// EncodeToString encodes ID into FrostFS API protocol string.
|
// Unmarshal unmarshals protobuf binary representation of ID.
|
||||||
//
|
func (id *ID) Unmarshal(data []byte) error {
|
||||||
// Zero ID is base58 encoding of 32 zeros.
|
return (*refs.ContainerID)(id).Unmarshal(data)
|
||||||
//
|
|
||||||
// See also DecodeString.
|
|
||||||
func (id ID) EncodeToString() string {
|
|
||||||
return base58.Encode(id[:])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecodeString decodes string into ID according to FrostFS API protocol. Returns
|
// MarshalJSON encodes ID to protobuf JSON format.
|
||||||
// an error if s is malformed.
|
func (id *ID) MarshalJSON() ([]byte, error) {
|
||||||
//
|
return (*refs.ContainerID)(id).MarshalJSON()
|
||||||
// See also DecodeString.
|
|
||||||
func (id *ID) DecodeString(s string) error {
|
|
||||||
data, err := base58.Decode(s)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("decode base58: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return id.Decode(data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// String implements fmt.Stringer.
|
// UnmarshalJSON decodes ID from protobuf JSON format.
|
||||||
//
|
func (id *ID) UnmarshalJSON(data []byte) error {
|
||||||
// String is designed to be human-readable, and its format MAY differ between
|
return (*refs.ContainerID)(id).UnmarshalJSON(data)
|
||||||
// SDK versions. String MAY return same result as EncodeToString. String MUST NOT
|
|
||||||
// be used to encode ID into FrostFS protocol string.
|
|
||||||
func (id ID) String() string {
|
|
||||||
return id.EncodeToString()
|
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue