forked from TrueCloudLab/frostfs-sdk-go
Compare commits
518 commits
support/v1
...
master
Author | SHA1 | Date | |
---|---|---|---|
d342c0bc16 | |||
f0c599d06d | |||
7d84d104fb | |||
812126a8ff | |||
d86223ed56 | |||
76a0cfdadb | |||
46ee543899 | |||
8f751d9dd0 | |||
3c00f4eeac | |||
f0b9493ce3 | |||
28f140bf06 | |||
9115d3f281 | |||
cf225be0df | |||
338d1ef254 | |||
6dd7be11d1 | |||
203bba65a0 | |||
98aabc45a7 | |||
908c96a94d | |||
2077b35736 | |||
92c7596157 | |||
a15b1264f5 | |||
5d58519253 | |||
93171b3319 | |||
3ba7446157 | |||
335aa18dc6 | |||
361739e860 | |||
6dd500def9 | |||
e83d6b7c6a | |||
9da46f566f | |||
fa89999d91 | |||
7e94a6adf2 | |||
ce8270568d | |||
7c06cdff2d | |||
e18b916231 | |||
c4ff8a6cda | |||
fc7c524fcb | |||
e977b8a94c | |||
6729f54c4e | |||
159a50fcf0 | |||
a69f00903c | |||
9d89f08c7b | |||
51cefd4908 | |||
560cbbd1f1 | |||
27e965007d | |||
1a5886e776 | |||
ebd8fcd168 | |||
717a7d00ef | |||
dd23c6fd2b | |||
6a52487edd | |||
c5c6272029 | |||
3de256d05e | |||
09b79d13f3 | |||
d4e6f4e125 | |||
b2ad1f3b3e | |||
32a975a20d | |||
eaf36706a2 | |||
02c936f397 | |||
99e02858af | |||
12ddefe078 | |||
20ab57bf7e | |||
3790142b10 | |||
ec0cb2169f | |||
425d48f68b | |||
6d0da3f861 | |||
1af9b6d18b | |||
bd2d350b09 | |||
e9be3e6d94 | |||
70e9e40c7f | |||
d33b54d280 | |||
6f248436a5 | |||
edd40474e8 | |||
d9ec7c1988 | |||
64b83f8220 | |||
7212f38115 | |||
8081445ff2 | |||
6a7ef9d8c3 | |||
6fe4e2541d | |||
a5fab572ff | |||
a86170f53a | |||
aa41f71dcc | |||
3a00fd51e4 | |||
65b4525b3b | |||
7efff9d53d | |||
110b7e4170 | |||
56debcfa56 | |||
157a9930e8 | |||
1c07098740 | |||
03d35dd1f3 | |||
dea8759762 | |||
3787477133 | |||
e91d40e250 | |||
ab75edd709 | |||
8999d2f080 | |||
6fbe1595cb | |||
a9237aabd2 | |||
a487033505 | |||
51c3618850 | |||
665e5807bc | |||
a02c0bfac8 | |||
20d325e307 | |||
670619d242 | |||
0d79d10482 | |||
9727beb47d | |||
84315fab6a | |||
71335489ae | |||
4c1feaf2cb | |||
5804128ff3 | |||
abd38c918e | |||
fc4551b843 | |||
eb5288f4a5 | |||
60463871db | |||
8a04638749 | |||
ddbfb758c9 | |||
d71a0e0755 | |||
163b3e1961 | |||
84b9d29fc9 | |||
99c273f499 | |||
555ccc63b2 | |||
0550438b53 | |||
c899163860 | |||
ac8fc6d440 | |||
0a0b590df3 | |||
4df642e941 | |||
8bc64e088e | |||
49ad985cad | |||
aa12d8c6a6 | |||
303508328a | |||
55699d1480 | |||
55a1f23e71 | |||
291a71ba84 | |||
5a471e5002 | |||
b5fe52d6bd | |||
84e7e69f98 | |||
46a214d065 | |||
202412230a | |||
3cb3841073 | |||
faeeeab87a | |||
cae215534f | |||
518fb79bc0 | |||
342524159a | |||
22978303f8 | |||
6fdbe75517 | |||
3353940554 | |||
a3b5d4d4f5 | |||
0314b326d3 | |||
0382785763 | |||
548a81d3e6 | |||
d48788c7a9 | |||
6353df8bca | |||
936e6d230b | |||
be28b89312 | |||
9e5faaf829 | |||
3dc8129ed7 | |||
55c52c8d5d | |||
d376302a3b | |||
363f153eaf | |||
95b987b818 | |||
13d0b170d2 | |||
18a9e4bceb | |||
0fe0d71678 | |||
78d1439b2c | |||
0886d80083 | |||
ecb1fef78c | |||
5defed4ab4 | |||
fb05f7dc5e | |||
b91f9d8c79 | |||
b9afe7a2f9 | |||
998fe1a7ab | |||
c359a7465a | |||
d70ef2187b | |||
ac95b87e7c | |||
863be6034f | |||
35346a01c9 | |||
fe35373d8f | |||
388d1ca1de | |||
14ed3e177d | |||
fe28c33277 | |||
98cab7ed61 | |||
37e22b33ad | |||
769f6eec05 | |||
5d62cef27e | |||
c0c0c588b5 | |||
2f88460172 | |||
66cb5dcf34 | |||
91e80ba743 | |||
c243b443bc | |||
aa8ffebc63 | |||
9d40228cec | |||
af40dc68f0 | |||
981d24a493 | |||
19adb4dffa | |||
51e022ab8c | |||
0d3dacb515 | |||
b2e302624d | |||
fcbf96add6 | |||
4f48f6c9e0 | |||
ec59ebfd88 | |||
030ff2f122 | |||
0f7455ff7a | |||
e6b662cfa6 | |||
406c2324d4 | |||
10482ffbed | |||
f5b23eb225 | |||
70f23dd1ea | |||
57f874048b | |||
a397d1fd15 | |||
9803c2816a | |||
d04d96b42e | |||
9a072a8f49 | |||
15b4287092 | |||
d4fe9a193d | |||
c42a6119ff | |||
29b188db57 | |||
38b03ff28b | |||
0fa23a9b14 | |||
d0762d037d | |||
db5b89496d | |||
7c75db2f2d | |||
dce55a436a | |||
cae2f37cdd | |||
f60bea4be5 | |||
a16fc40c39 | |||
40d966bec2 | |||
|
d0c5d837d2 | ||
237b90f744 | |||
c8e620ad24 | |||
591dd1247d | |||
57619fbbe4 | |||
8bc8f1f365 | |||
09ed2863fc | |||
8e2f77890f | |||
8852b262f2 | |||
b2c66cb99e | |||
6c9b92c9dc | |||
772fa90983 | |||
1bfa9ecdb0 | |||
55b06cd764 | |||
|
f41860f9bd | ||
|
423b320f91 | ||
f8c34b45f3 | |||
fa9573e857 | |||
bc62e2f712 | |||
31271ad8b1 | |||
25e9336d68 | |||
4cd755877c | |||
1395b282fe | |||
708d933fe3 | |||
4fa52312c7 | |||
552219b8e1 | |||
cfb8a7b914 | |||
4438f115fb | |||
bec77f280a | |||
df2090c2be | |||
7e6592b28e | |||
d589d51509 | |||
25588ee3be | |||
9407f30248 | |||
94c0a607b5 | |||
e45647de3c | |||
611e20587b | |||
eba6831125 | |||
7e3810d654 | |||
cc0fef2c55 | |||
b696d3c70e | |||
1c94309d7a | |||
f43f18ecda | |||
ac8442bf99 | |||
0ad877288e | |||
0e1999c965 | |||
b461aa64b8 | |||
b761fd8070 | |||
94476f9055 | |||
5e759bf089 | |||
d4f5bba459 | |||
e9c1a2ab2b | |||
2cbc585edd | |||
e355e5eeba | |||
f08069ceeb | |||
dad99bad48 | |||
0d3a238d9c | |||
cf64ddfb14 | |||
cf9a54dcda | |||
|
b2a37543d3 | ||
f0ac49b8f0 | |||
54696acf48 | |||
dd88a5c5e0 | |||
4ff9c00de3 | |||
b204a62da1 | |||
377a5a0517 | |||
43c046f343 | |||
|
8c0c7789ca | ||
|
339e2702f8 | ||
|
a1748ae0e7 | ||
|
4c779423f5 | ||
|
45a6e7a7c2 | ||
|
b4b07a3c4e | ||
|
d047289182 | ||
|
1cacf472a3 | ||
|
e35f0df1ca | ||
|
5f9d846fb4 | ||
|
2eefdab0e4 | ||
|
7a2a76af95 | ||
|
da4ddcf337 | ||
|
231ec66054 | ||
|
a02a3870a5 | ||
|
9d4b36a8e6 | ||
|
d3a09cbe7f | ||
|
f21d63bded | ||
21eef1ae7f | |||
|
d2f3929b51 | ||
|
8c682641bf | ||
|
452a50e9d5 | ||
|
1325b4f272 | ||
|
c6576c8112 | ||
|
8e3173eacd | ||
|
89124d442d | ||
|
664392afc2 | ||
|
f2f97f656d | ||
|
be9a1aca90 | ||
|
3d6b5d807b | ||
|
3dad44232e | ||
|
4662d39886 | ||
|
f75a5feba3 | ||
|
1929b634a1 | ||
|
402d72e629 | ||
|
43a57d42dd | ||
|
71891029da | ||
|
d808f72c38 | ||
|
511886b8d2 | ||
|
01c238ddc0 | ||
|
2e5c66934c | ||
|
74234623b2 | ||
|
ee92df3203 | ||
|
cf7bee3087 | ||
|
456167e777 | ||
|
0e4d07fb06 | ||
|
d6d6a41f5d | ||
|
724d30db1a | ||
|
1f593d0fb2 | ||
|
f543ba68d3 | ||
|
dd5826d071 | ||
|
02bc2bc236 | ||
|
992b26a3ff | ||
|
6a43accf96 | ||
|
5d7650c3e7 | ||
|
a926e5a1de | ||
|
8c5333ea55 | ||
|
0236b03fa7 | ||
|
2f843de3ed | ||
|
84888854ab | ||
|
7537fa0dec | ||
|
7578b54fac | ||
|
737e690482 | ||
|
f8148c954b | ||
|
4e31b4f231 | ||
|
30bf79f075 | ||
|
7a99cc916c | ||
|
a0f7c903d3 | ||
|
7de66159d4 | ||
|
90255e9efa | ||
|
828cfdc5bf | ||
|
90f1cc7a1a | ||
|
3a95686aab | ||
|
f4ac75423c | ||
|
b4f4ee4f79 | ||
|
54145916a9 | ||
|
0d54757545 | ||
|
58fe1768cc | ||
|
0b8c53ebc9 | ||
|
1b30d228da | ||
|
423804de84 | ||
|
99e185690e | ||
|
9d3a1835d1 | ||
|
e6cb5f2ee1 | ||
|
7d10b432d1 | ||
|
0d862d8568 | ||
|
48ba86f4dd | ||
|
f91b1facd5 | ||
|
c6fee6d01a | ||
|
a6ecf6b881 | ||
|
9996b3be01 | ||
|
0cd790cfe0 | ||
|
df6538c68c | ||
|
27fe9c19a7 | ||
|
6994eb0e55 | ||
|
cec2373b50 | ||
|
ff09b1cfe1 | ||
|
041e1ef2b6 | ||
|
30d27c3050 | ||
|
2ad89085a3 | ||
|
70845147f6 | ||
|
86a447bc80 | ||
|
ab4d1e34a8 | ||
|
dea3d9c419 | ||
|
d2cd9ebfbd | ||
|
1e1139f305 | ||
|
dcaf454c1d | ||
|
09ed6077f9 | ||
|
40942affe9 | ||
|
aa5ee1dcde | ||
|
3b15a01327 | ||
|
c4ebe8d854 | ||
|
e82a2d86ef | ||
|
af7e20073b | ||
|
596f43a540 | ||
|
721df386c5 | ||
|
e986f47807 | ||
|
e999fb00c3 | ||
|
d51a324147 | ||
|
d648b86776 | ||
|
ca523f1ff1 | ||
|
5bfdb64251 | ||
|
86bdc670d5 | ||
|
6796b4a29a | ||
|
723ba5ee45 | ||
|
2b21146185 | ||
|
9c47fca7c2 | ||
|
a8fd2ef954 | ||
|
ea21cdf731 | ||
|
eb3b990812 | ||
|
6709b00c89 | ||
|
517d7a1e4a | ||
|
6ac9deabb8 | ||
|
5518b63432 | ||
|
031eac2f48 | ||
|
67ff996dc3 | ||
|
bf8312a547 | ||
|
0ef49cf851 | ||
|
458c882ff4 | ||
|
0bb40b3245 | ||
|
d3b998d672 | ||
|
c65be6d469 | ||
|
82d762f536 | ||
|
6cb513c976 | ||
|
3953c2166e | ||
|
85e3c7b087 | ||
|
3bbf7ee15d | ||
|
85affc3c93 | ||
|
f0a5eb6dbc | ||
|
bef4618cd6 | ||
|
1f7fe6864d | ||
|
60ef026923 | ||
|
c976332e20 | ||
|
12ea1e8d74 | ||
|
539ac9915e | ||
|
526b45e207 | ||
|
5fe6d96bf1 | ||
|
4cbbbdd3e2 | ||
|
f0134ef26e | ||
|
e468f409d7 | ||
|
4dc3a7669e | ||
|
48434d9999 | ||
|
552c7875bf | ||
|
497053c785 | ||
|
6e81e13e1b | ||
|
1ed426b8a6 | ||
|
d20999113a | ||
|
6554c681c8 | ||
|
9f20d74d76 | ||
|
7fe75d2cd9 | ||
|
bcbffd516a | ||
|
2f9cc50fec | ||
|
ea043f4ca3 | ||
|
2deaaeef05 | ||
|
9b63c07c59 | ||
|
1186f2f703 | ||
|
f7172adf18 | ||
|
24d6c2221f | ||
|
27cd721422 | ||
|
96892d7bc4 | ||
|
168b3ee7a4 | ||
|
caa055236b | ||
|
fd13e61266 | ||
|
9c502a9cae | ||
|
d51d18d5f3 | ||
|
55283d3c91 | ||
|
75a5b6588d | ||
|
146fc4f07a | ||
|
c961aea144 | ||
|
ade8822a2f | ||
|
3e75660802 | ||
|
53e064e556 | ||
|
385f1b10f9 | ||
|
1e8aa6a99f | ||
|
97c8274dc0 | ||
|
9e8e12f6ae | ||
|
aa6a4950d5 | ||
|
e56eef495d | ||
|
e50e6d2828 | ||
|
df0573d521 | ||
|
c4adb03f8e | ||
|
fcfae4a249 | ||
|
9814748958 | ||
|
7df00fb0eb | ||
|
11a25bb413 | ||
|
b8d2158acd | ||
|
a709cf5444 | ||
|
0dbea5452a | ||
|
47345a33da | ||
|
d568458fab | ||
|
2104945f9e | ||
|
f38a24e8b5 | ||
|
e0281c3b34 | ||
|
f18b4a2a75 | ||
|
58d4f4a55f | ||
|
030bbce2cf | ||
|
59b49dd7e6 | ||
|
191d85e607 | ||
|
9be9697856 | ||
|
52548fe176 | ||
|
7811d8eefc | ||
|
d03523a3bc | ||
|
ec5c223f29 | ||
|
f5cabe26cb | ||
|
aeb4ac638a | ||
|
7d31de57ec | ||
|
48150852f3 | ||
|
a55ffa4796 |
380 changed files with 38471 additions and 21709 deletions
21
.forgejo/workflows/dco.yml
Normal file
21
.forgejo/workflows/dco.yml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
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 }}'
|
39
.forgejo/workflows/tests.yml
Normal file
39
.forgejo/workflows/tests.yml
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
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
Normal file
1
.github/CODEOWNERS
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
* @TrueCloudLab/storage-core @TrueCloudLab/storage-services
|
45
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
45
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: community, triage, bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--- Provide a general summary of the issue in the Title above -->
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
<!--- If you're describing a bug, tell us what should happen -->
|
||||||
|
<!--- If you're suggesting a change/improvement, tell us how it should work -->
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
<!--- If describing a bug, tell us what happens instead of the expected behavior -->
|
||||||
|
<!--- If suggesting a change/improvement, explain the difference from current behavior -->
|
||||||
|
|
||||||
|
## Possible Solution
|
||||||
|
<!--- Not obligatory -->
|
||||||
|
<!--- If no reason/fix/additions for the bug can be suggested, -->
|
||||||
|
<!--- uncomment the following phrase: -->
|
||||||
|
|
||||||
|
<!--- No fix can be suggested by a QA engineer. Further solutions shall be up to developers. -->
|
||||||
|
|
||||||
|
## Steps to Reproduce (for bugs)
|
||||||
|
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
|
||||||
|
<!--- reproduce this bug. -->
|
||||||
|
|
||||||
|
1.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
<!--- How has this issue affected you? What are you trying to accomplish? -->
|
||||||
|
<!--- Providing context helps us come up with a solution that is most useful in the real world -->
|
||||||
|
|
||||||
|
## Regression
|
||||||
|
<!-- Is this issue a regression? (Yes / No) -->
|
||||||
|
<!-- If Yes, optionally please include version or commit id or PR# that caused this regression, if you have these details. -->
|
||||||
|
|
||||||
|
## Your Environment
|
||||||
|
<!--- Include as many relevant details about the environment you experienced the bug in -->
|
||||||
|
* Version used:
|
||||||
|
* Server setup and configuration:
|
||||||
|
* Operating System and version (`uname -a`):
|
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
blank_issues_enabled: false
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: community, triage
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Is your feature request related to a problem? Please describe.
|
||||||
|
<!--- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
|
||||||
|
|
||||||
|
## Describe the solution you'd like
|
||||||
|
<!--- A clear and concise description of what you want to happen. -->
|
||||||
|
|
||||||
|
## Describe alternatives you've considered
|
||||||
|
<!--- A clear and concise description of any alternative solutions or features you've considered. -->
|
||||||
|
|
||||||
|
## Additional context
|
||||||
|
<!--- Add any other context or screenshots about the feature request here. -->
|
21
.github/workflows/dco.yml
vendored
21
.github/workflows/dco.yml
vendored
|
@ -1,21 +0,0 @@
|
||||||
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
52
.github/workflows/tests.yml
vendored
|
@ -1,52 +0,0 @@
|
||||||
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,4 +20,14 @@ vendor/
|
||||||
|
|
||||||
# coverage
|
# coverage
|
||||||
coverage.txt
|
coverage.txt
|
||||||
coverage.html
|
coverage.html
|
||||||
|
|
||||||
|
# antlr tool jar
|
||||||
|
antlr-*.jar
|
||||||
|
|
||||||
|
# tempfiles
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# binary
|
||||||
|
bin/
|
||||||
|
release/
|
||||||
|
|
|
@ -4,15 +4,16 @@
|
||||||
# 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: 5m
|
timeout: 10m
|
||||||
|
|
||||||
# include test files or not, default is true
|
# include test files or not, default is true
|
||||||
tests: true
|
tests: false
|
||||||
|
|
||||||
# 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"
|
||||||
format: tab
|
formats:
|
||||||
|
- format: tab
|
||||||
|
|
||||||
# all available settings of specific linters
|
# all available settings of specific linters
|
||||||
linters-settings:
|
linters-settings:
|
||||||
|
@ -24,6 +25,13 @@ 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:
|
||||||
|
@ -32,28 +40,29 @@ 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
|
||||||
- godot
|
- exportloopref
|
||||||
- gofmt
|
- gofmt
|
||||||
- whitespace
|
|
||||||
- goimports
|
- goimports
|
||||||
|
- misspell
|
||||||
|
- predeclared
|
||||||
|
- reassign
|
||||||
|
- whitespace
|
||||||
|
- containedctx
|
||||||
|
- 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
|
|
||||||
|
|
36
.pre-commit-config.yaml
Normal file
36
.pre-commit-config.yaml
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
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
|
4
Dockerfile
Normal file
4
Dockerfile
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
FROM golang:1.22
|
||||||
|
|
||||||
|
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install make openjdk-17-jre -y
|
||||||
|
WORKDIR /work
|
57
Makefile
Normal file → Executable file
57
Makefile
Normal file → Executable file
|
@ -1,8 +1,16 @@
|
||||||
#!/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:
|
||||||
@go test ./... -cover
|
@GOFLAGS=$(GOFLAGS) go test ./...
|
||||||
|
|
||||||
# Pull go dependencies
|
# Pull go dependencies
|
||||||
dep:
|
dep:
|
||||||
|
@ -13,9 +21,23 @@ 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:
|
||||||
@golangci-lint --timeout=5m run
|
@if [ ! -d "$(LINT_DIR)" ]; then \
|
||||||
|
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:
|
||||||
|
@ -29,6 +51,23 @@ 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:'
|
||||||
|
@ -37,4 +76,16 @@ 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
|
||||||
|
|
78
README.md
78
README.md
|
@ -1,6 +1,6 @@
|
||||||
# neofs-sdk-go
|
# frostfs-sdk-go
|
||||||
Go implementation of NeoFS SDK. It contains high-level version-independent wrappers
|
Go implementation of FrostFS SDK. It contains high-level version-independent wrappers
|
||||||
for structures from [neofs-api-go](https://github.com/nspcc-dev/neofs-api-go) as well as
|
for structures from [frostfs-api-go](https://git.frostfs.info/TrueCloudLab/frostfs-api-go) as well as
|
||||||
helper functions for simplifying node/dApp implementations.
|
helper functions for simplifying node/dApp implementations.
|
||||||
|
|
||||||
## Repository structure
|
## Repository structure
|
||||||
|
@ -10,63 +10,73 @@ 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 NeoFS node.
|
There is also a reference implementation of checking algorithm which is used in FrostFS 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://github.com/nspcc-dev/tzhash) are in use.
|
Currently Sha256 and [Tillich-Zemor hashsum](https://git.frostfs.info/TrueCloudLab/tzhash) are in use.
|
||||||
|
|
||||||
### owner
|
### owner
|
||||||
`owner.ID` type represents single account interacting with NeoFS. In v2 version of protocol
|
`owner.ID` type represents single account interacting with FrostFS. In v2 version of protocol
|
||||||
it is just raw bytes behing [base58-encoded address](https://docs.neo.org/docs/en-us/basic/concept/wallets.html#address)
|
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 NeoFS-specific methods.
|
Contains Bearer token type with several FrostFS-specific methods.
|
||||||
|
|
||||||
### resolver
|
### ns
|
||||||
In NeoFS there are 2 types of name resolution: DNS and NNS. NNS stands for Neo Name Service
|
In FrostFS there are 2 types of name resolution: DNS and NNS. NNS stands for Neo Name Service
|
||||||
is just a [contract](https://github.com/nspcc-dev/neofs-contract/) deployed on a Neo blockchain.
|
is just a [contract](https://git.frostfs.info/TrueCloudLab/frostfs-contract) deployed on a Neo blockchain.
|
||||||
Basically, NNS is just a DNS-on-chain which can be used for resolving container nice-names as well
|
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 NeoFS without sacrificing trust, NeoFS has a concept
|
To help lightweight clients interact with FrostFS without sacrificing trust, FrostFS has a concept
|
||||||
of session token. It is signed by client and allows any node with which a session is established
|
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 NeoFS.
|
Contains client for working with FrostFS.
|
||||||
```go
|
```go
|
||||||
c, _ := client.New(
|
var prmInit client.PrmInit
|
||||||
client.WithAddress("localhost:40005"), // endpoint address
|
prmInit.SetDefaultPrivateKey(key) // private key for request signing
|
||||||
client.WithDefaultPrivateKey(key), // private key for request signing
|
|
||||||
client.WithNeoFSErrorHandling(), // enable erroneous status parsing
|
|
||||||
client.WithTLSConfig(&tls.Config{})) // custom TLS configuration
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5 * time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
res, err := c.BalanceGet(ctx, owner)
|
var c client.Client
|
||||||
|
c.Init(prmInit)
|
||||||
|
|
||||||
|
var prmDial client.PrmDial
|
||||||
|
prmDial.SetServerURI("grpcs://localhost:40005") // endpoint address
|
||||||
|
|
||||||
|
err := c.Dial(prmDial)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Balance for %s: %s\n", owner, res.Amount())
|
ctx, cancel := context.WithTimeout(context.Background(), 5 * time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
var prm client.PrmBalanceGet
|
||||||
|
prm.SetAccount(acc)
|
||||||
|
|
||||||
|
res, err := c.BalanceGet(ctx, prm)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Balance for %s: %v\n", acc, res.Amount())
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Response status
|
#### Response status
|
||||||
In NeoFS every operation can fail on multiple levels, so a single `error` doesn't suffice,
|
In FrostFS every operation can fail on multiple levels, so a single `error` doesn't suffice,
|
||||||
e.g. consider a case when object was put on 4 out of 5 replicas. Thus, all request execution
|
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
|
||||||
[NeoFS API](https://github.com/nspcc-dev/neofs-api/blob/master/status/types.proto). There is also
|
[FrostFS API](https://git.frostfs.info/TrueCloudLab/frostfs-api/src/branch/master/status/types.proto).
|
||||||
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
|
||||||
|
@ -86,19 +96,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 (
|
||||||
"github.com/nspcc-dev/neofs-sdk-go/netmap"
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
||||||
"github.com/nspcc-dev/neofs-sdk-go/object"
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
||||||
)
|
)
|
||||||
|
|
||||||
func placementNodes(addr *object.Address, p *netmap.PlacementPolicy, neofsNodes []netmap.NodeInfo) {
|
func placementNodes(addr *object.Address, p *netmap.PlacementPolicy, frostfsNodes []netmap.NodeInfo) {
|
||||||
// Convert list of nodes in NeoFS API format to the intermediate representation.
|
// Convert list of nodes in FrostFS 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.
|
||||||
|
@ -110,13 +120,13 @@ func placementNodes(addr *object.Address, p *netmap.PlacementPolicy, neofsNodes
|
||||||
```
|
```
|
||||||
|
|
||||||
### pool
|
### pool
|
||||||
Simple pool for managing connections to NeoFS nodes.
|
Simple pool for managing connections to FrostFS 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 NeoFS codebase.
|
Wrapper over `zap.Logger` which is used across FrostFS codebase.
|
||||||
|
|
||||||
### util
|
### util
|
||||||
Utilities for working with signature-related code.
|
Utilities for working with signature-related code.
|
||||||
|
|
|
@ -1,69 +1,64 @@
|
||||||
package accounting
|
package accounting
|
||||||
|
|
||||||
import "github.com/nspcc-dev/neofs-api-go/v2/accounting"
|
import "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/accounting"
|
||||||
|
|
||||||
// Decimal represents decimal number for accounting operations.
|
// Decimal 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
|
||||||
|
|
||||||
// NewDecimal creates, initializes and returns empty Decimal instance.
|
// ReadFromV2 reads Decimal from the accounting.Decimal message. Checks if the
|
||||||
|
// message conforms to FrostFS API V2 protocol.
|
||||||
//
|
//
|
||||||
// Defaults:
|
// See also WriteToV2.
|
||||||
// - value: 0
|
func (d *Decimal) ReadFromV2(m accounting.Decimal) error {
|
||||||
// - precision: 0
|
*d = Decimal(m)
|
||||||
func NewDecimal() *Decimal {
|
return nil
|
||||||
return NewDecimalFromV2(new(accounting.Decimal))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDecimalFromV2 converts v2 Decimal to Decimal.
|
// WriteToV2 writes Decimal to the accounting.Decimal message.
|
||||||
|
// The message must not be nil.
|
||||||
//
|
//
|
||||||
// Nil Decimal converts to nil.
|
// See also ReadFromV2.
|
||||||
func NewDecimalFromV2(d *accounting.Decimal) *Decimal {
|
func (d Decimal) WriteToV2(m *accounting.Decimal) {
|
||||||
return (*Decimal)(d)
|
*m = (accounting.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 {
|
//
|
||||||
return (*accounting.Decimal)(d).GetValue()
|
// Zero Decimal has zero value.
|
||||||
|
//
|
||||||
|
// See also SetValue.
|
||||||
|
func (d Decimal) Value() int64 {
|
||||||
|
return (*accounting.Decimal)(&d).GetValue()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetValue sets value of the decimal number.
|
// 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 {
|
//
|
||||||
return (*accounting.Decimal)(d).GetPrecision()
|
// Zero Decimal has zero precision.
|
||||||
|
//
|
||||||
|
// See also SetPrecision.
|
||||||
|
func (d Decimal) Precision() uint32 {
|
||||||
|
return (*accounting.Decimal)(&d).GetPrecision()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetPrecision sets precision of the decimal number.
|
// 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,15 +3,19 @@ package accounting_test
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/nspcc-dev/neofs-sdk-go/accounting"
|
v2accounting "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/accounting"
|
||||||
accountingtest "github.com/nspcc-dev/neofs-sdk-go/accounting/test"
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/accounting"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDecimal(t *testing.T) {
|
func TestDecimalData(t *testing.T) {
|
||||||
const v, p = 4, 2
|
const v, p = 4, 2
|
||||||
|
|
||||||
d := accounting.NewDecimal()
|
var d accounting.Decimal
|
||||||
|
|
||||||
|
require.Zero(t, d.Value())
|
||||||
|
require.Zero(t, d.Precision())
|
||||||
|
|
||||||
d.SetValue(v)
|
d.SetValue(v)
|
||||||
d.SetPrecision(p)
|
d.SetPrecision(p)
|
||||||
|
|
||||||
|
@ -19,26 +23,24 @@ func TestDecimal(t *testing.T) {
|
||||||
require.EqualValues(t, p, d.Precision())
|
require.EqualValues(t, p, d.Precision())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDecimalEncoding(t *testing.T) {
|
func TestDecimalMessageV2(t *testing.T) {
|
||||||
d := accountingtest.Decimal()
|
var (
|
||||||
|
d accounting.Decimal
|
||||||
|
m v2accounting.Decimal
|
||||||
|
)
|
||||||
|
|
||||||
t.Run("binary", func(t *testing.T) {
|
m.SetValue(7)
|
||||||
data, err := d.Marshal()
|
m.SetPrecision(8)
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
d2 := accounting.NewDecimal()
|
require.NoError(t, d.ReadFromV2(m))
|
||||||
require.NoError(t, d2.Unmarshal(data))
|
|
||||||
|
|
||||||
require.Equal(t, d, d2)
|
require.EqualValues(t, m.GetValue(), d.Value())
|
||||||
})
|
require.EqualValues(t, m.GetPrecision(), d.Precision())
|
||||||
|
|
||||||
t.Run("json", func(t *testing.T) {
|
var m2 v2accounting.Decimal
|
||||||
data, err := d.MarshalJSON()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
d2 := accounting.NewDecimal()
|
d.WriteToV2(&m2)
|
||||||
require.NoError(t, d2.UnmarshalJSON(data))
|
|
||||||
|
|
||||||
require.Equal(t, d, d2)
|
require.EqualValues(t, d.Value(), m2.GetValue())
|
||||||
})
|
require.EqualValues(t, d.Precision(), m2.GetPrecision())
|
||||||
}
|
}
|
||||||
|
|
35
accounting/doc.go
Normal file
35
accounting/doc.go
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
Package accounting provides primitives to perform accounting operations in FrostFS.
|
||||||
|
|
||||||
|
Decimal type provides functionality to process user balances. For example, when
|
||||||
|
working with Fixed8 balance precision:
|
||||||
|
|
||||||
|
var dec accounting.Decimal
|
||||||
|
dec.SetValue(val)
|
||||||
|
dec.SetPrecision(8)
|
||||||
|
|
||||||
|
Instances can be also used to process FrostFS API V2 protocol messages
|
||||||
|
(see neo.fs.v2.accounting package in https://git.frostfs.info/TrueCloudLab/frostfs-api).
|
||||||
|
|
||||||
|
On client side:
|
||||||
|
|
||||||
|
import "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/accounting"
|
||||||
|
|
||||||
|
var msg accounting.Decimal
|
||||||
|
dec.WriteToV2(&msg)
|
||||||
|
|
||||||
|
// send msg
|
||||||
|
|
||||||
|
On server side:
|
||||||
|
|
||||||
|
// recv msg
|
||||||
|
|
||||||
|
var dec accounting.Decimal
|
||||||
|
dec.ReadFromV2(msg)
|
||||||
|
|
||||||
|
// process dec
|
||||||
|
|
||||||
|
Using package types in an application is recommended to potentially work with
|
||||||
|
different protocol versions with which these types are compatible.
|
||||||
|
*/
|
||||||
|
package accounting
|
|
@ -3,14 +3,14 @@ package accountingtest
|
||||||
import (
|
import (
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
|
||||||
"github.com/nspcc-dev/neofs-sdk-go/accounting"
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/accounting"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Decimal returns random accounting.Decimal.
|
// Decimal returns random accounting.Decimal.
|
||||||
func Decimal() *accounting.Decimal {
|
func Decimal() *accounting.Decimal {
|
||||||
d := accounting.NewDecimal()
|
var d accounting.Decimal
|
||||||
d.SetValue(rand.Int63())
|
d.SetValue(rand.Int63())
|
||||||
d.SetPrecision(rand.Uint32())
|
d.SetPrecision(rand.Uint32())
|
||||||
|
|
||||||
return d
|
return &d
|
||||||
}
|
}
|
||||||
|
|
13
accounting/test/doc.go
Normal file
13
accounting/test/doc.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
/*
|
||||||
|
Package accountingtest provides functions for convenient testing of accounting package API.
|
||||||
|
|
||||||
|
Note that importing the package into source files is highly discouraged.
|
||||||
|
|
||||||
|
Random instance generation functions can be useful when testing expects any value, e.g.:
|
||||||
|
|
||||||
|
import accountingtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/accounting/test"
|
||||||
|
|
||||||
|
dec := accountingtest.Decimal()
|
||||||
|
// test the value
|
||||||
|
*/
|
||||||
|
package accountingtest
|
105
acl/types.go
105
acl/types.go
|
@ -1,105 +0,0 @@
|
||||||
package acl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// BasicACL is Access Control List that defines who can interact with containers and what exactly they can do.
|
|
||||||
type BasicACL uint32
|
|
||||||
|
|
||||||
func (a BasicACL) String() string {
|
|
||||||
return fmt.Sprintf("0x%08x", uint32(a))
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
// PublicBasicRule is a basic ACL value for final public-read-write container for which extended ACL CANNOT be set.
|
|
||||||
PublicBasicRule BasicACL = 0x1FBFBFFF
|
|
||||||
|
|
||||||
// PrivateBasicRule is a basic ACL value for final private container for which extended ACL CANNOT be set.
|
|
||||||
PrivateBasicRule BasicACL = 0x1C8C8CCC
|
|
||||||
|
|
||||||
// ReadOnlyBasicRule is a basic ACL value for final public-read container for which extended ACL CANNOT be set.
|
|
||||||
ReadOnlyBasicRule BasicACL = 0x1FBF8CFF
|
|
||||||
|
|
||||||
// PublicAppendRule is a basic ACL value for final public-append container for which extended ACL CANNOT be set.
|
|
||||||
PublicAppendRule BasicACL = 0x1FBF9FFF
|
|
||||||
|
|
||||||
// EACLPublicBasicRule is a basic ACL value for non-final public-read-write container for which extended ACL CAN be set.
|
|
||||||
EACLPublicBasicRule BasicACL = 0x0FBFBFFF
|
|
||||||
|
|
||||||
// EACLPrivateBasicRule is a basic ACL value for non-final private container for which extended ACL CAN be set.
|
|
||||||
EACLPrivateBasicRule BasicACL = 0x0C8C8CCC
|
|
||||||
|
|
||||||
// EACLReadOnlyBasicRule is a basic ACL value for non-final public-read container for which extended ACL CAN be set.
|
|
||||||
EACLReadOnlyBasicRule BasicACL = 0x0FBF8CFF
|
|
||||||
|
|
||||||
// EACLPublicAppendRule is a basic ACL value for non-final public-append container for which extended ACL CAN be set.
|
|
||||||
EACLPublicAppendRule BasicACL = 0x0FBF9FFF
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// PublicBasicName is a well-known name for 0x1FBFBFFF basic ACL.
|
|
||||||
// It represents fully-public container without eACL.
|
|
||||||
PublicBasicName = "public-read-write"
|
|
||||||
|
|
||||||
// PrivateBasicName is a well-known name for 0x1C8C8CCC basic ACL.
|
|
||||||
// It represents fully-private container without eACL.
|
|
||||||
PrivateBasicName = "private"
|
|
||||||
|
|
||||||
// ReadOnlyBasicName is a well-known name for 0x1FBF8CFF basic ACL.
|
|
||||||
// It represents public read-only container without eACL.
|
|
||||||
ReadOnlyBasicName = "public-read"
|
|
||||||
|
|
||||||
// PublicAppendName is a well-known name for 0x1FBF9FFF basic ACL.
|
|
||||||
// It represents fully-public container without eACL except DELETE operation is only allowed on the owner.
|
|
||||||
PublicAppendName = "public-append"
|
|
||||||
|
|
||||||
// EACLPublicBasicName is a well-known name for 0x0FBFBFFF basic ACL.
|
|
||||||
// It represents fully-public container that allows eACL.
|
|
||||||
EACLPublicBasicName = "eacl-public-read-write"
|
|
||||||
|
|
||||||
// EACLPrivateBasicName is a well-known name for 0x0C8C8CCC basic ACL.
|
|
||||||
// It represents fully-private container that allows eACL.
|
|
||||||
EACLPrivateBasicName = "eacl-private"
|
|
||||||
|
|
||||||
// EACLReadOnlyBasicName is a well-known name for 0x0FBF8CFF basic ACL.
|
|
||||||
// It represents public read-only container that allows eACL.
|
|
||||||
EACLReadOnlyBasicName = "eacl-public-read"
|
|
||||||
|
|
||||||
// EACLPublicAppendName is a well-known name for 0x0FBF9FFF basic ACL.
|
|
||||||
// It represents fully-public container that allows eACL except DELETE operation is only allowed on the owner.
|
|
||||||
EACLPublicAppendName = "eacl-public-append"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ParseBasicACL parse string ACL (well-known names or hex representation).
|
|
||||||
func ParseBasicACL(basicACL string) (BasicACL, error) {
|
|
||||||
switch basicACL {
|
|
||||||
case PublicBasicName:
|
|
||||||
return PublicBasicRule, nil
|
|
||||||
case PrivateBasicName:
|
|
||||||
return PrivateBasicRule, nil
|
|
||||||
case ReadOnlyBasicName:
|
|
||||||
return ReadOnlyBasicRule, nil
|
|
||||||
case PublicAppendName:
|
|
||||||
return PublicAppendRule, nil
|
|
||||||
case EACLPublicBasicName:
|
|
||||||
return EACLPublicBasicRule, nil
|
|
||||||
case EACLPrivateBasicName:
|
|
||||||
return EACLPrivateBasicRule, nil
|
|
||||||
case EACLReadOnlyBasicName:
|
|
||||||
return EACLReadOnlyBasicRule, nil
|
|
||||||
case EACLPublicAppendName:
|
|
||||||
return EACLPublicAppendRule, nil
|
|
||||||
default:
|
|
||||||
basicACL = strings.TrimPrefix(strings.ToLower(basicACL), "0x")
|
|
||||||
|
|
||||||
value, err := strconv.ParseUint(basicACL, 16, 32)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("can't parse basic ACL: %s", basicACL)
|
|
||||||
}
|
|
||||||
|
|
||||||
return BasicACL(value), nil
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,82 +0,0 @@
|
||||||
package acl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestParser(t *testing.T) {
|
|
||||||
for _, tc := range []struct {
|
|
||||||
acl string
|
|
||||||
expected BasicACL
|
|
||||||
err bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
acl: PublicBasicName,
|
|
||||||
expected: PublicBasicRule,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
acl: PrivateBasicName,
|
|
||||||
expected: PrivateBasicRule,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
acl: ReadOnlyBasicName,
|
|
||||||
expected: ReadOnlyBasicRule,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
acl: PublicAppendName,
|
|
||||||
expected: PublicAppendRule,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
acl: EACLPublicBasicName,
|
|
||||||
expected: EACLPublicBasicRule,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
acl: EACLPrivateBasicName,
|
|
||||||
expected: EACLPrivateBasicRule,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
acl: EACLReadOnlyBasicName,
|
|
||||||
expected: EACLReadOnlyBasicRule,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
acl: EACLPublicAppendName,
|
|
||||||
expected: EACLPublicAppendRule,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
acl: "0x1C8C8CCC",
|
|
||||||
expected: 0x1C8C8CCC,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
acl: "1C8C8CCC",
|
|
||||||
expected: 0x1C8C8CCC,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
acl: "123456789",
|
|
||||||
err: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
acl: "0x1C8C8CCG",
|
|
||||||
err: true,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
actual, err := ParseBasicACL(tc.acl)
|
|
||||||
if tc.err {
|
|
||||||
require.Error(t, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, tc.expected, actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestString(t *testing.T) {
|
|
||||||
acl := BasicACL(0x1fbfbfff)
|
|
||||||
require.Equal(t, "0x1fbfbfff", acl.String())
|
|
||||||
|
|
||||||
acl2, err := ParseBasicACL(PrivateBasicName)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, "0x1c8c8ccc", acl2.String())
|
|
||||||
}
|
|
52
ape/chain.go
Normal file
52
ape/chain.go
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
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
|
||||||
|
}
|
53
ape/chain_target.go
Normal file
53
ape/chain_target.go
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
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()
|
||||||
|
}
|
65
ape/chain_target_test.go
Normal file
65
ape/chain_target_test.go
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
43
ape/chain_test.go
Normal file
43
ape/chain_test.go
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
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
291
audit/result.go
|
@ -1,291 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
|
@ -1,154 +0,0 @@
|
||||||
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))
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
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
Normal file
497
bearer/bearer.go
Normal file
|
@ -0,0 +1,497 @@
|
||||||
|
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
|
||||||
|
}
|
447
bearer/bearer_test.go
Normal file
447
bearer/bearer_test.go
Normal file
|
@ -0,0 +1,447 @@
|
||||||
|
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))
|
||||||
|
}
|
31
bearer/doc.go
Normal file
31
bearer/doc.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
Package bearer provides bearer token definition.
|
||||||
|
|
||||||
|
Bearer token is attached to the object service requests, and it overwrites
|
||||||
|
extended ACL of the container. Mainly it is used to provide access of private
|
||||||
|
data for specific user. Therefore, it must be signed by owner of the container.
|
||||||
|
|
||||||
|
Define bearer token by setting correct lifetime, extended ACL and owner ID of
|
||||||
|
the user that will attach token to its requests.
|
||||||
|
|
||||||
|
var bearerToken bearer.Token
|
||||||
|
bearerToken.SetExpiration(500)
|
||||||
|
bearerToken.SetIssuedAt(10)
|
||||||
|
bearerToken.SetNotBefore(10)
|
||||||
|
bearerToken.SetEACL(eaclTable)
|
||||||
|
bearerToken.SetOwner(ownerID)
|
||||||
|
|
||||||
|
Bearer token must be signed by owner of the container.
|
||||||
|
|
||||||
|
err := bearerToken.Sign(privateKey)
|
||||||
|
|
||||||
|
Provide signed token in JSON or binary format to the request sender. Request
|
||||||
|
sender can attach this bearer token to the object service requests:
|
||||||
|
|
||||||
|
import sdkClient "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client"
|
||||||
|
|
||||||
|
var headParams sdkClient.PrmObjectHead
|
||||||
|
headParams.WithBearerToken(bearerToken)
|
||||||
|
response, err := client.ObjectHead(ctx, headParams)
|
||||||
|
*/
|
||||||
|
package bearer
|
6
bearer/test/doc.go
Normal file
6
bearer/test/doc.go
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
/*
|
||||||
|
Package bearertest provides functions for testing bearer package.
|
||||||
|
|
||||||
|
Note that this package intended only for tests.
|
||||||
|
*/
|
||||||
|
package bearertest
|
32
bearer/test/generate.go
Normal file
32
bearer/test/generate.go
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
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,51 +1,78 @@
|
||||||
package checksum
|
package checksum
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/nspcc-dev/neofs-api-go/v2/refs"
|
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
||||||
|
"git.frostfs.info/TrueCloudLab/tzhash/tz"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Checksum represents v2-compatible checksum.
|
// Checksum represents checksum of some digital data.
|
||||||
|
//
|
||||||
|
// Checksum is mutually compatible with git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs.Checksum
|
||||||
|
// message. See ReadFromV2 / WriteToV2 methods.
|
||||||
|
//
|
||||||
|
// Instances can be created using built-in var declaration.
|
||||||
|
//
|
||||||
|
// Note that direct typecast is not safe and may result in loss of compatibility:
|
||||||
|
//
|
||||||
|
// _ = Checksum(refs.Checksum{}) // not recommended
|
||||||
type Checksum refs.Checksum
|
type Checksum refs.Checksum
|
||||||
|
|
||||||
// Type represents the enumeration
|
// Type represents the enumeration
|
||||||
// of checksum types.
|
// of checksum types.
|
||||||
type Type uint8
|
type Type refs.ChecksumType
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Unknown is an undefined checksum type.
|
// Unknown is an undefined checksum type.
|
||||||
Unknown Type = iota
|
Unknown Type = Type(refs.UnknownChecksum)
|
||||||
|
|
||||||
// SHA256 is a SHA256 checksum type.
|
// SHA256 is a SHA256 checksum type.
|
||||||
SHA256
|
SHA256 = Type(refs.SHA256)
|
||||||
|
|
||||||
// TZ is a Tillich-Zemor checksum type.
|
// TZ is a Tillich-Zémor checksum type.
|
||||||
TZ
|
TZ = Type(refs.TillichZemor)
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewFromV2 wraps v2 Checksum message to Checksum.
|
// ReadFromV2 reads Checksum from the refs.Checksum message. Checks if the
|
||||||
|
// message conforms to FrostFS API V2 protocol.
|
||||||
//
|
//
|
||||||
// Nil refs.Checksum converts to nil.
|
// See also WriteToV2.
|
||||||
func NewFromV2(cV2 *refs.Checksum) *Checksum {
|
func (c *Checksum) ReadFromV2(m refs.Checksum) error {
|
||||||
return (*Checksum)(cV2)
|
if len(m.GetSum()) == 0 {
|
||||||
|
return errors.New("missing value")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch m.GetType() {
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported type %v", m.GetType())
|
||||||
|
case refs.SHA256, refs.TillichZemor:
|
||||||
|
}
|
||||||
|
|
||||||
|
*c = Checksum(m)
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates and initializes blank Checksum.
|
// WriteToV2 writes Checksum to the refs.Checksum message.
|
||||||
|
// The message must not be nil.
|
||||||
//
|
//
|
||||||
// Defaults:
|
// See also ReadFromV2.
|
||||||
// - sum: nil;
|
func (c Checksum) WriteToV2(m *refs.Checksum) {
|
||||||
// - type: Unknown.
|
*m = (refs.Checksum)(c)
|
||||||
func New() *Checksum {
|
|
||||||
return NewFromV2(new(refs.Checksum))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type returns checksum type.
|
// Type returns checksum type.
|
||||||
func (c *Checksum) Type() Type {
|
//
|
||||||
switch (*refs.Checksum)(c).GetType() {
|
// Zero Checksum has Unknown checksum type.
|
||||||
|
//
|
||||||
|
// See also SetTillichZemor and SetSHA256.
|
||||||
|
func (c Checksum) Type() Type {
|
||||||
|
v2 := (refs.Checksum)(c)
|
||||||
|
switch v2.GetType() {
|
||||||
case refs.SHA256:
|
case refs.SHA256:
|
||||||
return SHA256
|
return SHA256
|
||||||
case refs.TillichZemor:
|
case refs.TillichZemor:
|
||||||
|
@ -55,93 +82,70 @@ func (c *Checksum) Type() Type {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sum returns checksum bytes.
|
// Value returns checksum bytes. Return value
|
||||||
func (c *Checksum) Sum() []byte {
|
// MUST NOT be mutated.
|
||||||
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) {
|
||||||
checksum := (*refs.Checksum)(c)
|
v2 := (*refs.Checksum)(c)
|
||||||
|
|
||||||
checksum.SetType(refs.SHA256)
|
v2.SetType(refs.SHA256)
|
||||||
checksum.SetSum(v[:])
|
v2.SetSum(v[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetTillichZemor sets checksum to Tillich-Zemor hash.
|
// Calculate calculates checksum and sets it
|
||||||
func (c *Checksum) SetTillichZemor(v [64]byte) {
|
// to the passed checksum. Checksum must not be nil.
|
||||||
checksum := (*refs.Checksum)(c)
|
|
||||||
|
|
||||||
checksum.SetType(refs.TillichZemor)
|
|
||||||
checksum.SetSum(v[:])
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToV2 converts Checksum to v2 Checksum message.
|
|
||||||
//
|
//
|
||||||
// Nil Checksum converts to nil.
|
// Does nothing if the passed type is not one of the:
|
||||||
func (c *Checksum) ToV2() *refs.Checksum {
|
// - SHA256;
|
||||||
return (*refs.Checksum)(c)
|
// - TZ.
|
||||||
}
|
//
|
||||||
|
// Does not mutate the passed value.
|
||||||
func Equal(cs1, cs2 *Checksum) bool {
|
//
|
||||||
return cs1.Type() == cs2.Type() && bytes.Equal(cs1.Sum(), cs2.Sum())
|
// See also SetSHA256, SetTillichZemor.
|
||||||
}
|
func Calculate(c *Checksum, t Type, v []byte) {
|
||||||
|
switch t {
|
||||||
// Marshal marshals Checksum into a protobuf binary form.
|
case SHA256:
|
||||||
func (c *Checksum) Marshal() ([]byte, error) {
|
c.SetSHA256(sha256.Sum256(v))
|
||||||
return (*refs.Checksum)(c).StableMarshal(nil)
|
case TZ:
|
||||||
}
|
c.SetTillichZemor(tz.Sum(v))
|
||||||
|
|
||||||
// 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:
|
default:
|
||||||
return fmt.Errorf("unsupported checksum length %d", ln)
|
|
||||||
case sha256.Size:
|
|
||||||
typ = refs.SHA256
|
|
||||||
case 64:
|
|
||||||
typ = refs.TillichZemor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cV2 := (*refs.Checksum)(c)
|
|
||||||
cV2.SetType(typ)
|
|
||||||
cV2.SetSum(data)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// String returns string representation of Type.
|
// SetTillichZemor sets checksum to Tillich-Zémor hash.
|
||||||
//
|
//
|
||||||
// String mapping:
|
// See also Calculate.
|
||||||
// * TZ: TZ;
|
func (c *Checksum) SetTillichZemor(v [tz.Size]byte) {
|
||||||
// * SHA256: SHA256;
|
v2 := (*refs.Checksum)(c)
|
||||||
// * 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
|
||||||
|
|
||||||
|
@ -156,26 +160,3 @@ 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,20 +5,13 @@ import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/nspcc-dev/neofs-api-go/v2/refs"
|
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
||||||
|
"git.frostfs.info/TrueCloudLab/tzhash/tz"
|
||||||
"github.com/stretchr/testify/require"
|
"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) {
|
||||||
c := New()
|
var c Checksum
|
||||||
|
|
||||||
cSHA256 := [sha256.Size]byte{}
|
cSHA256 := [sha256.Size]byte{}
|
||||||
_, _ = rand.Read(cSHA256[:])
|
_, _ = rand.Read(cSHA256[:])
|
||||||
|
@ -26,150 +19,62 @@ 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.Sum())
|
require.Equal(t, cSHA256[:], c.Value())
|
||||||
|
|
||||||
cV2 := c.ToV2()
|
var cV2 refs.Checksum
|
||||||
|
c.WriteToV2(&cV2)
|
||||||
|
|
||||||
require.Equal(t, refs.SHA256, cV2.GetType())
|
require.Equal(t, refs.SHA256, cV2.GetType())
|
||||||
require.Equal(t, cSHA256[:], cV2.GetSum())
|
require.Equal(t, cSHA256[:], cV2.GetSum())
|
||||||
|
|
||||||
cTZ := [64]byte{}
|
cTZ := [tz.Size]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.Sum())
|
require.Equal(t, cTZ[:], c.Value())
|
||||||
|
|
||||||
cV2 = c.ToV2()
|
c.WriteToV2(&cV2)
|
||||||
|
|
||||||
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) {
|
||||||
chs := New()
|
var chs Checksum
|
||||||
|
|
||||||
// check initial values
|
// check initial values
|
||||||
require.Equal(t, Unknown, chs.Type())
|
require.Equal(t, Unknown, chs.Type())
|
||||||
require.Nil(t, chs.Sum())
|
require.Nil(t, chs.Value())
|
||||||
|
|
||||||
// convert to v2 message
|
// convert to v2 message
|
||||||
chsV2 := chs.ToV2()
|
var chsV2 refs.Checksum
|
||||||
|
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())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type enumIface interface {
|
func TestCalculation(t *testing.T) {
|
||||||
FromString(string) bool
|
var c Checksum
|
||||||
String() string
|
payload := []byte{0, 1, 2, 3, 4, 5}
|
||||||
}
|
|
||||||
|
|
||||||
type enumStringItem struct {
|
t.Run("SHA256", func(t *testing.T) {
|
||||||
val enumIface
|
orig := sha256.Sum256(payload)
|
||||||
str string
|
|
||||||
}
|
|
||||||
|
|
||||||
func testEnumStrings(t *testing.T, e enumIface, items []enumStringItem) {
|
Calculate(&c, SHA256, payload)
|
||||||
for _, item := range items {
|
|
||||||
require.Equal(t, item.str, item.val.String())
|
|
||||||
|
|
||||||
s := item.val.String()
|
require.Equal(t, orig[:], c.Value())
|
||||||
|
})
|
||||||
|
|
||||||
require.True(t, e.FromString(s), s)
|
t.Run("TZ", func(t *testing.T) {
|
||||||
|
orig := tz.Sum(payload)
|
||||||
|
|
||||||
require.EqualValues(t, item.val, e, item.val)
|
Calculate(&c, TZ, payload)
|
||||||
}
|
|
||||||
|
|
||||||
// incorrect strings
|
require.Equal(t, orig[:], c.Value())
|
||||||
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"},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
18
checksum/doc.go
Normal file
18
checksum/doc.go
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
/*
|
||||||
|
Package checksum provides primitives to work with checksums.
|
||||||
|
|
||||||
|
Checksum is a basic type of data checksums.
|
||||||
|
For example, calculating checksums:
|
||||||
|
|
||||||
|
// retrieving any payload for hashing
|
||||||
|
|
||||||
|
var sha256Sum Checksum
|
||||||
|
Calculate(&sha256Sum, SHA256, payload) // sha256Sum contains SHA256 hash of the payload
|
||||||
|
|
||||||
|
var tzSum Checksum
|
||||||
|
Calculate(&tzSum, TZ, payload) // tzSum contains TZ hash of the payload
|
||||||
|
|
||||||
|
Using package types in an application is recommended to potentially work with
|
||||||
|
different protocol versions with which these types are compatible.
|
||||||
|
*/
|
||||||
|
package checksum
|
34
checksum/example_test.go
Normal file
34
checksum/example_test.go
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
package checksum
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/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
|
||||||
|
}
|
13
checksum/test/doc.go
Normal file
13
checksum/test/doc.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
/*
|
||||||
|
Package checksumtest provides functions for convenient testing of checksum package API.
|
||||||
|
|
||||||
|
Note that importing the package into source files is highly discouraged.
|
||||||
|
|
||||||
|
Random instance generation functions can be useful when testing expects any value, e.g.:
|
||||||
|
|
||||||
|
import checksumtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/checksum/test"
|
||||||
|
|
||||||
|
cs := checksumtest.Checksum()
|
||||||
|
// test the value
|
||||||
|
*/
|
||||||
|
package checksumtest
|
|
@ -1,19 +1,19 @@
|
||||||
package checksumtest
|
package checksumtest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"math/rand"
|
|
||||||
|
|
||||||
"github.com/nspcc-dev/neofs-sdk-go/checksum"
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/checksum"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Checksum returns random checksum.Checksum.
|
// 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[:])
|
||||||
|
|
||||||
x := checksum.New()
|
var x checksum.Checksum
|
||||||
|
|
||||||
x.SetSHA256(cs)
|
x.SetSHA256(cs)
|
||||||
|
|
||||||
|
|
|
@ -2,103 +2,107 @@ package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
v2accounting "github.com/nspcc-dev/neofs-api-go/v2/accounting"
|
v2accounting "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/accounting"
|
||||||
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
|
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
||||||
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
|
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
|
||||||
"github.com/nspcc-dev/neofs-sdk-go/accounting"
|
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
||||||
"github.com/nspcc-dev/neofs-sdk-go/owner"
|
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/accounting"
|
||||||
|
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PrmBalanceGet groups parameters of BalanceGet operation.
|
// PrmBalanceGet groups parameters of BalanceGet operation.
|
||||||
type PrmBalanceGet struct {
|
type PrmBalanceGet struct {
|
||||||
prmCommonMeta
|
XHeaders []string
|
||||||
|
|
||||||
ownerSet bool
|
Account user.ID
|
||||||
ownerID owner.ID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetAccount sets identifier of the NeoFS account for which the balance is requested.
|
// SetAccount sets identifier of the FrostFS account for which the balance is requested.
|
||||||
// Required parameter. Must be a valid ID according to NeoFS API protocol.
|
// Required parameter.
|
||||||
func (x *PrmBalanceGet) SetAccount(id owner.ID) {
|
//
|
||||||
x.ownerID = id
|
// Deprecated: Use PrmBalanceGet.Account instead.
|
||||||
x.ownerSet = true
|
func (x *PrmBalanceGet) SetAccount(id user.ID) {
|
||||||
|
x.Account = id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PrmBalanceGet) buildRequest(c *Client) (*v2accounting.BalanceRequest, error) {
|
||||||
|
if x.Account.IsEmpty() {
|
||||||
|
return nil, errorAccountNotSet
|
||||||
|
}
|
||||||
|
|
||||||
|
var accountV2 refs.OwnerID
|
||||||
|
x.Account.WriteToV2(&accountV2)
|
||||||
|
|
||||||
|
var body v2accounting.BalanceRequestBody
|
||||||
|
body.SetOwnerID(&accountV2)
|
||||||
|
|
||||||
|
var req v2accounting.BalanceRequest
|
||||||
|
req.SetBody(&body)
|
||||||
|
|
||||||
|
c.prepareRequest(&req, new(v2session.RequestMetaHeader))
|
||||||
|
return &req, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ResBalanceGet) setAmount(v *accounting.Decimal) {
|
// Amount returns current amount of funds on the FrostFS account as decimal number.
|
||||||
x.amount = v
|
func (x ResBalanceGet) Amount() accounting.Decimal {
|
||||||
}
|
|
||||||
|
|
||||||
// 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 NeoFS account.
|
// BalanceGet requests current balance of the FrostFS account.
|
||||||
//
|
//
|
||||||
// Exactly one return value is non-nil. By default, server status is returned in res structure.
|
// 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 WithNeoFSErrorParsing option has been provided, unsuccessful
|
// If PrmInit.DisableFrostFSFailuresResolution has been called, unsuccessful
|
||||||
// NeoFS status codes are returned as `error`, otherwise, are included
|
// FrostFS status codes are included in the returned result structure,
|
||||||
// in the returned result structure.
|
// otherwise, are also returned as `error`.
|
||||||
//
|
//
|
||||||
// Immediately panics if parameters are set incorrectly (see PrmBalanceGet docs).
|
// Returns an error if parameters are set incorrectly (see PrmBalanceGet docs).
|
||||||
// Context is required and must not be nil. It is used for network communication.
|
// 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) {
|
||||||
switch {
|
req, err := prm.buildRequest(c)
|
||||||
case ctx == nil:
|
if err != nil {
|
||||||
panic(panicMsgMissingContext)
|
return nil, err
|
||||||
case !prm.ownerSet:
|
|
||||||
panic("account not set")
|
|
||||||
case !prm.ownerID.Valid():
|
|
||||||
panic("invalid account ID")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// form request body
|
if err := signature.SignServiceMessage(&c.prm.Key, req); err != nil {
|
||||||
var body v2accounting.BalanceRequestBody
|
return nil, fmt.Errorf("sign request: %w", err)
|
||||||
|
|
||||||
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()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// process call
|
resp, err := rpcapi.Balance(&c.c, req, client.WithContext(ctx))
|
||||||
if !cc.processCall() {
|
if err != nil {
|
||||||
return nil, cc.err
|
return nil, 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
|
||||||
}
|
}
|
||||||
|
|
79
client/apemanager_add_chain.go
Normal file
79
client/apemanager_add_chain.go
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
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
|
||||||
|
}
|
80
client/apemanager_list_chains.go
Normal file
80
client/apemanager_list_chains.go
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
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
|
||||||
|
}
|
73
client/apemanager_remove_chain.go
Normal file
73
client/apemanager_remove_chain.go
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
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
|
||||||
|
}
|
35
client/api.go
Normal file
35
client/api.go
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
v2netmap "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/netmap"
|
||||||
|
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
// interface of FrostFS API server. Exists for test purposes only.
|
||||||
|
type frostFSAPIServer interface {
|
||||||
|
netMapSnapshot(context.Context, v2netmap.SnapshotRequest) (*v2netmap.SnapshotResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrapper over real client connection which communicates over FrostFS API protocol.
|
||||||
|
// Provides frostFSAPIServer for Client instances used in real applications.
|
||||||
|
type coreServer client.Client
|
||||||
|
|
||||||
|
// unifies errors of all RPC.
|
||||||
|
func rpcErr(e error) error {
|
||||||
|
return fmt.Errorf("rpc failure: %w", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// executes NetmapService.NetmapSnapshot RPC declared in FrostFS API protocol
|
||||||
|
// using underlying client.Client.
|
||||||
|
func (x *coreServer) netMapSnapshot(ctx context.Context, req v2netmap.SnapshotRequest) (*v2netmap.SnapshotResponse, error) {
|
||||||
|
resp, err := rpcapi.NetMapSnapshot((*client.Client)(x), &req, client.WithContext(ctx))
|
||||||
|
if err != nil {
|
||||||
|
return nil, rpcErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
171
client/client.go
171
client/client.go
|
@ -1,23 +1,26 @@
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
v2accounting "github.com/nspcc-dev/neofs-api-go/v2/accounting"
|
v2accounting "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/accounting"
|
||||||
"github.com/nspcc-dev/neofs-api-go/v2/rpc"
|
"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"
|
||||||
|
"google.golang.org/grpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Client represents virtual connection to the NeoFS network to communicate
|
// Client represents virtual connection to the FrostFS network to communicate
|
||||||
// with NeoFS server using NeoFS API protocol. It is designed to provide
|
// with FrostFS server using FrostFS 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 NeoFS.
|
// a network in FrostFS.
|
||||||
//
|
//
|
||||||
// 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 NeoFS operations using the Client, connection to the
|
// Before executing the FrostFS operations using the Client, connection to the
|
||||||
// server MUST BE correctly established (see Dial method and pay attention
|
// 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
|
||||||
|
@ -26,7 +29,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 NeoFS API call may return a server response.
|
// Each method which produces a FrostFS API call may return a server response.
|
||||||
// Status responses are returned in the result structure, and can be cast
|
// 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`
|
||||||
|
@ -34,8 +37,9 @@ 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.SuccessDefaultV2 on default success.
|
// - *apistatus.NodeUnderMaintenance if a server is under maintenance;
|
||||||
|
// - *apistatus.SuccessDefaultV2 on default success.
|
||||||
//
|
//
|
||||||
// Client MUST NOT be copied by value: use pointer to Client instead.
|
// Client MUST NOT be copied by value: use pointer to Client instead.
|
||||||
//
|
//
|
||||||
|
@ -44,6 +48,8 @@ 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.
|
||||||
|
@ -56,42 +62,63 @@ func (c *Client) Init(prm PrmInit) {
|
||||||
c.prm = prm
|
c.prm = prm
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dial establishes a connection to the server from the NeoFS network.
|
// Dial establishes a connection to the server from the FrostFS network.
|
||||||
// Returns an error describing failure reason. If failed, the Client
|
// Returns an error describing failure reason. If failed, the Client
|
||||||
// SHOULD NOT be used.
|
// SHOULD NOT be used.
|
||||||
//
|
//
|
||||||
// Panics if required parameters are set incorrectly, look carefully
|
// Uses the context specified by SetContext if it was called with non-nil
|
||||||
|
// argument, otherwise context.Background() is used. Dial returns context
|
||||||
|
// errors, see context package docs for details.
|
||||||
|
//
|
||||||
|
// Returns an error if required parameters are set incorrectly, look carefully
|
||||||
// at the method documentation.
|
// 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(prm PrmDial) error {
|
func (c *Client) Dial(ctx context.Context, prm PrmDial) error {
|
||||||
if prm.endpoint == "" {
|
if prm.Endpoint == "" {
|
||||||
panic("server address is unset or empty")
|
return errorServerAddrUnset
|
||||||
}
|
}
|
||||||
|
|
||||||
if prm.timeoutDialSet {
|
if prm.DialTimeout <= 0 {
|
||||||
if prm.timeoutDial <= 0 {
|
prm.DialTimeout = defaultDialTimeout
|
||||||
panic("non-positive timeout")
|
}
|
||||||
}
|
if prm.StreamTimeout <= 0 {
|
||||||
} else {
|
prm.StreamTimeout = defaultStreamTimeout
|
||||||
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.timeoutDial),
|
client.WithDialTimeout(prm.DialTimeout),
|
||||||
|
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
|
||||||
_, _ = rpc.Balance(&c.c, new(v2accounting.BalanceRequest))
|
_, err := rpc.Balance(&c.c, new(v2accounting.BalanceRequest),
|
||||||
|
client.WithContext(ctx),
|
||||||
|
)
|
||||||
|
// return context errors since they signal about dial problem
|
||||||
|
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close closes underlying connection to the NeoFS server. Implements io.Closer.
|
// sets underlying provider of frostFSAPIServer. The method is used for testing as an approach
|
||||||
|
// to skip Dial stage and override FrostFS API server. MUST NOT be used outside test code.
|
||||||
|
// In real applications wrapper over git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client
|
||||||
|
// is statically used.
|
||||||
|
func (c *Client) setFrostFSAPIServer(server frostFSAPIServer) {
|
||||||
|
c.server = server
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes underlying connection to the FrostFS server. Implements io.Closer.
|
||||||
// MUST NOT be called before successful Dial. Can be called concurrently
|
// 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.
|
||||||
|
@ -108,75 +135,117 @@ func (c *Client) Close() error {
|
||||||
//
|
//
|
||||||
// See also Init.
|
// See also Init.
|
||||||
type PrmInit struct {
|
type PrmInit struct {
|
||||||
resolveNeoFSErrors bool
|
DisableFrostFSErrorResolution bool
|
||||||
|
|
||||||
key ecdsa.PrivateKey
|
Key ecdsa.PrivateKey
|
||||||
|
|
||||||
cbRespInfo func(ResponseMetaInfo) error
|
ResponseInfoCallback 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
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolveNeoFSFailures makes the Client to resolve failure statuses of the
|
// Deprecated: method is no-op. Option is default.
|
||||||
// NeoFS protocol into Go built-in errors. These errors are returned from
|
func (x *PrmInit) ResolveFrostFSFailures() {
|
||||||
// each protocol operation. By default, statuses aren't resolved and written
|
}
|
||||||
// to the resulting structure (see corresponding Res* docs).
|
|
||||||
func (x *PrmInit) ResolveNeoFSFailures() {
|
// DisableFrostFSFailuresResolution makes the Client to preserve failure statuses of the
|
||||||
x.resolveNeoFSErrors = true
|
// FrostFS protocol only in resulting structure (see corresponding Res* docs).
|
||||||
|
// These errors are returned from each protocol operation. By default, statuses
|
||||||
|
// are resolved and returned as a Go built-in errors.
|
||||||
|
//
|
||||||
|
// Deprecated: Use PrmInit.DisableFrostFSErrorResolution instead.
|
||||||
|
func (x *PrmInit) DisableFrostFSFailuresResolution() {
|
||||||
|
x.DisableFrostFSErrorResolution = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetResponseInfoCallback makes the Client to pass ResponseMetaInfo from each
|
// SetResponseInfoCallback makes the Client to pass ResponseMetaInfo from each
|
||||||
// NeoFS server response to f. Nil (default) means ignore response meta info.
|
// FrostFS server response to f. Nil (default) means ignore response meta info.
|
||||||
|
//
|
||||||
|
// Deprecated: Use PrmInit.ResponseInfoCallback instead.
|
||||||
func (x *PrmInit) SetResponseInfoCallback(f func(ResponseMetaInfo) error) {
|
func (x *PrmInit) SetResponseInfoCallback(f func(ResponseMetaInfo) error) {
|
||||||
x.cbRespInfo = f
|
x.ResponseInfoCallback = 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
|
||||||
|
|
||||||
timeoutDialSet bool
|
// If DialTimeout is non-positive, then it's set to defaultDialTimeout.
|
||||||
timeoutDial time.Duration
|
DialTimeout 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 NeoFS network.
|
// SetServerURI sets server URI in the FrostFS network.
|
||||||
// Required parameter.
|
// Required parameter.
|
||||||
//
|
//
|
||||||
// Format of the URI:
|
// Format of the URI:
|
||||||
// [scheme://]host:port
|
//
|
||||||
|
// [scheme://]host:port
|
||||||
//
|
//
|
||||||
// Supported schemes:
|
// Supported schemes:
|
||||||
// grpc
|
//
|
||||||
// grpcs
|
// grpc
|
||||||
|
// 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 NeoFS server. Nil (default) means insecure connection.
|
// to the FrostFS 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.timeoutDialSet = true
|
x.DialTimeout = timeout
|
||||||
x.timeoutDial = timeout
|
}
|
||||||
|
|
||||||
|
// SetStreamTimeout sets the timeout for individual operations in streaming RPC.
|
||||||
|
// MUST BE positive. If not called, 10s timeout will be used by default.
|
||||||
|
//
|
||||||
|
// 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
|
||||||
}
|
}
|
||||||
|
|
67
client/client_test.go
Normal file
67
client/client_test.go
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
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)
|
||||||
|
}
|
334
client/common.go
334
client/common.go
|
@ -1,32 +1,22 @@
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/ecdsa"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
|
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
||||||
v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
|
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
||||||
"github.com/nspcc-dev/neofs-api-go/v2/signature"
|
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"
|
||||||
"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/version"
|
"git.frostfs.info/TrueCloudLab/frostfs-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.
|
||||||
|
@ -34,287 +24,89 @@ func (x statusRes) Status() apistatus.Status {
|
||||||
return x.st
|
return x.st
|
||||||
}
|
}
|
||||||
|
|
||||||
type prmSession struct {
|
func writeXHeadersToMeta(xHeaders []string, h *v2session.RequestMetaHeader) {
|
||||||
tokenSessionSet bool
|
if len(xHeaders) == 0 {
|
||||||
tokenSession session.Token
|
return
|
||||||
}
|
|
||||||
|
|
||||||
// SetSessionToken sets token of the session within which request should be sent.
|
|
||||||
func (x *prmSession) SetSessionToken(tok session.Token) {
|
|
||||||
x.tokenSession = tok
|
|
||||||
x.tokenSessionSet = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x prmSession) writeToMetaHeader(meta *v2session.RequestMetaHeader) {
|
|
||||||
if x.tokenSessionSet {
|
|
||||||
meta.SetSessionToken(x.tokenSession.ToV2())
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// groups meta parameters shared between all Client operations.
|
// TODO (aarifullin): remove the panic when all client parameters will check XHeaders
|
||||||
type prmCommonMeta struct {
|
// within buildRequest invocation.
|
||||||
// NeoFS request X-Headers
|
if len(xHeaders)%2 != 0 {
|
||||||
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
x.xHeaders = hs
|
hs := make([]v2session.XHeader, len(xHeaders)/2)
|
||||||
}
|
for i := 0; i < len(xHeaders); i += 2 {
|
||||||
|
hs[i].SetKey(xHeaders[i])
|
||||||
func (x prmCommonMeta) writeToMetaHeader(h *v2session.RequestMetaHeader) {
|
hs[i].SetValue(xHeaders[i+1])
|
||||||
if len(x.xHeaders) > 0 {
|
|
||||||
hs := make([]v2session.XHeader, len(x.xHeaders)/2)
|
|
||||||
|
|
||||||
for i := 0; i < len(x.xHeaders); i += 2 {
|
|
||||||
hs[i].SetKey(x.xHeaders[i])
|
|
||||||
hs[i].SetValue(x.xHeaders[i+1])
|
|
||||||
}
|
|
||||||
|
|
||||||
h.SetXHeaders(hs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h.SetXHeaders(hs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// panic messages.
|
// error messages.
|
||||||
const (
|
var (
|
||||||
panicMsgMissingContext = "missing context"
|
errorMissingContainer = errors.New("missing container")
|
||||||
panicMsgMissingContainer = "missing container"
|
errorMissingObject = errors.New("missing object")
|
||||||
|
errorAccountNotSet = errors.New("account not set")
|
||||||
|
errorServerAddrUnset = errors.New("server address is unset or empty")
|
||||||
|
errorZeroRangeLength = errors.New("zero range length")
|
||||||
|
errorMissingRanges = errors.New("missing ranges")
|
||||||
|
errorInvalidXHeaders = errors.New("xheaders must be presented only as key-value pairs")
|
||||||
)
|
)
|
||||||
|
|
||||||
// groups all the details required to send a single request and process a response to it.
|
type request interface {
|
||||||
type contextCall struct {
|
GetMetaHeader() *v2session.RequestMetaHeader
|
||||||
// ==================================================
|
SetMetaHeader(*v2session.RequestMetaHeader)
|
||||||
// state vars that do not require explicit initialization
|
SetVerificationHeader(*v2session.RequestVerificationHeader)
|
||||||
|
|
||||||
// final error to be returned from client method
|
|
||||||
err error
|
|
||||||
|
|
||||||
// received response
|
|
||||||
resp responseV2
|
|
||||||
|
|
||||||
// ==================================================
|
|
||||||
// shared parameters which are set uniformly on all calls
|
|
||||||
|
|
||||||
// request signing key
|
|
||||||
key ecdsa.PrivateKey
|
|
||||||
|
|
||||||
// callback prior to processing the response by the client
|
|
||||||
callbackResp func(ResponseMetaInfo) error
|
|
||||||
|
|
||||||
// if set, protocol errors will be expanded into a final error
|
|
||||||
resolveAPIFailures bool
|
|
||||||
|
|
||||||
// NeoFS network magic
|
|
||||||
netMagic uint64
|
|
||||||
|
|
||||||
// Meta parameters
|
|
||||||
meta prmCommonMeta
|
|
||||||
|
|
||||||
// ==================================================
|
|
||||||
// custom call parameters
|
|
||||||
|
|
||||||
// structure of the call result
|
|
||||||
statusRes resCommon
|
|
||||||
|
|
||||||
// request to be signed with a key and sent
|
|
||||||
req interface {
|
|
||||||
GetMetaHeader() *v2session.RequestMetaHeader
|
|
||||||
SetMetaHeader(*v2session.RequestMetaHeader)
|
|
||||||
SetVerificationHeader(*v2session.RequestVerificationHeader)
|
|
||||||
}
|
|
||||||
|
|
||||||
// function to send a request (unary) and receive a response
|
|
||||||
call func() (responseV2, error)
|
|
||||||
|
|
||||||
// function to send the request (req field)
|
|
||||||
wReq func() error
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// sets needed fields of the request meta header.
|
func (c *Client) prepareRequest(req request, meta *v2session.RequestMetaHeader) {
|
||||||
func (x contextCall) prepareRequest() {
|
ttl := meta.GetTTL()
|
||||||
meta := x.req.GetMetaHeader()
|
if ttl == 0 {
|
||||||
if meta == nil {
|
ttl = 2
|
||||||
meta = new(v2session.RequestMetaHeader)
|
|
||||||
x.req.SetMetaHeader(meta)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if meta.GetTTL() == 0 {
|
verV2 := meta.GetVersion()
|
||||||
meta.SetTTL(2)
|
if verV2 == nil {
|
||||||
|
verV2 = new(refs.Version)
|
||||||
|
version.Current().WriteToV2(verV2)
|
||||||
}
|
}
|
||||||
|
|
||||||
if meta.GetVersion() == nil {
|
meta.SetTTL(ttl)
|
||||||
meta.SetVersion(version.Current().ToV2())
|
meta.SetVersion(verV2)
|
||||||
}
|
meta.SetNetworkMagic(c.prm.NetMagic)
|
||||||
|
|
||||||
meta.SetNetworkMagic(x.netMagic)
|
req.SetMetaHeader(meta)
|
||||||
|
|
||||||
x.meta.writeToMetaHeader(meta)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// prepares, signs and writes the request. Result means success.
|
// processResponse verifies response signature and converts status to an error if needed.
|
||||||
// If failed, contextCall.err contains the reason.
|
func (c *Client) processResponse(resp responseV2) (apistatus.Status, error) {
|
||||||
func (x *contextCall) writeRequest() bool {
|
if c.prm.ResponseInfoCallback != nil {
|
||||||
x.prepareRequest()
|
rmi := ResponseMetaInfo{
|
||||||
|
key: resp.GetVerificationHeader().GetBodySignature().GetKey(),
|
||||||
x.req.SetVerificationHeader(nil)
|
epoch: resp.GetMetaHeader().GetEpoch(),
|
||||||
|
}
|
||||||
// sign the request
|
if err := c.prm.ResponseInfoCallback(rmi); err != nil {
|
||||||
x.err = signature.SignServiceMessage(&x.key, x.req)
|
return nil, fmt.Errorf("response callback error: %w", err)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// note that we call response callback before signature check since it is expected more lightweight
|
err := signature.VerifyServiceMessage(resp)
|
||||||
// while verification needs marshaling
|
if err != nil {
|
||||||
|
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// get result status
|
st := apistatus.FromStatusV2(resp.GetMetaHeader().GetStatus())
|
||||||
st := apistatus.FromStatusV2(x.resp.GetMetaHeader().GetStatus())
|
if !c.prm.DisableFrostFSErrorResolution {
|
||||||
|
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// reads response (if rResp is set) and processes it. Result means success.
|
// ExecRaw executes f with underlying git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client.Client
|
||||||
// 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 NeoFS protocol, as well
|
// most often used to transmit data over a fixed version of the FrostFS protocol, as well
|
||||||
// as to support custom services.
|
// 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.
|
||||||
|
@ -323,7 +115,7 @@ func (c *Client) initCallContextWithoutKey(ctx *contextCall) {
|
||||||
// before closing the connection.
|
// before closing the connection.
|
||||||
//
|
//
|
||||||
// See also Dial and Close.
|
// See also Dial and Close.
|
||||||
// See also github.com/nspcc-dev/neofs-api-go/v2/rpc/client package docs.
|
// See also git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client package docs.
|
||||||
func (c *Client) ExecRaw(f func(client *client.Client) error) error {
|
func (c *Client) ExecRaw(f func(client *client.Client) error) error {
|
||||||
return f(&c.c)
|
return f(&c.c)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,732 +2,27 @@ package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
v2container "github.com/nspcc-dev/neofs-api-go/v2/container"
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/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"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// PrmContainerPut groups parameters of ContainerPut operation.
|
// SyncContainerWithNetwork requests network configuration using passed client
|
||||||
type PrmContainerPut struct {
|
// and applies it to the container. Container MUST not be nil.
|
||||||
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).
|
|
||||||
//
|
//
|
||||||
// Client doesn't retain value so modification is safe.
|
// Note: if container does not match network configuration, SyncContainerWithNetwork
|
||||||
func (x ResContainerPut) ID() *cid.ID {
|
// changes it.
|
||||||
return x.id
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *ResContainerPut) setID(id *cid.ID) {
|
|
||||||
x.id = id
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContainerPut sends request to save container in NeoFS.
|
|
||||||
//
|
//
|
||||||
// Exactly one return value is non-nil. By default, server status is returned in res structure.
|
// Returns any network/parsing config errors.
|
||||||
// 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.
|
// See also NetworkInfo, container.ApplyNetworkConfig.
|
||||||
// The required time is also not predictable.
|
func SyncContainerWithNetwork(ctx context.Context, cnr *container.Container, c *Client) error {
|
||||||
//
|
res, err := c.NetworkInfo(ctx, PrmNetworkInfo{})
|
||||||
// Success can be verified by reading by identifier (see ResContainerPut.ID).
|
|
||||||
//
|
|
||||||
// Immediately panics if parameters are set incorrectly (see PrmContainerPut docs).
|
|
||||||
// Context is required and must not be nil. It is used for network communication.
|
|
||||||
//
|
|
||||||
// Return statuses:
|
|
||||||
// - global (see Client docs).
|
|
||||||
func (c *Client) ContainerPut(ctx context.Context, prm PrmContainerPut) (*ResContainerPut, error) {
|
|
||||||
// check parameters
|
|
||||||
switch {
|
|
||||||
case ctx == nil:
|
|
||||||
panic(panicMsgMissingContext)
|
|
||||||
case !prm.cnrSet:
|
|
||||||
panic(panicMsgMissingContainer)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: check private key is set before forming the request
|
|
||||||
|
|
||||||
// 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 {
|
if err != nil {
|
||||||
return nil, err
|
return fmt.Errorf("network info call: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
reqBody.SetSignature(sig.ToV2())
|
container.ApplyNetworkConfig(cnr, res.Info())
|
||||||
|
|
||||||
// form meta header
|
return nil
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
139
client/container_delete.go
Normal file
139
client/container_delete.go
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
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
|
||||||
|
}
|
126
client/container_get.go
Normal file
126
client/container_get.go
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
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
|
||||||
|
}
|
119
client/container_list.go
Normal file
119
client/container_list.go
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
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
|
||||||
|
}
|
160
client/container_put.go
Normal file
160
client/container_put.go
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
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,20 +1,23 @@
|
||||||
/*
|
/*
|
||||||
Package client provides NeoFS API client implementation.
|
Package client provides FrostFS API client implementation.
|
||||||
|
|
||||||
The main component is Client type. It is a virtual connection to the network
|
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 NeoFS server:
|
Connect to the FrostFS server:
|
||||||
|
|
||||||
var prm client.PrmDial
|
var prm client.PrmDial
|
||||||
prm.SetServerURI("localhost:8080")
|
prm.SetServerURI("localhost:8080")
|
||||||
prm.SetDefaultPrivateKey(key)
|
prm.SetDefaultPrivateKey(key)
|
||||||
|
@ -23,7 +26,8 @@ Connect to the NeoFS server:
|
||||||
err := c.Dial(prm)
|
err := c.Dial(prm)
|
||||||
// ...
|
// ...
|
||||||
|
|
||||||
Execute NeoFS operation on the server:
|
Execute FrostFS operation on the server:
|
||||||
|
|
||||||
var prm client.PrmContainerPut
|
var prm client.PrmContainerPut
|
||||||
prm.SetContainer(cnr)
|
prm.SetContainer(cnr)
|
||||||
// ...
|
// ...
|
||||||
|
@ -36,14 +40,15 @@ Execute NeoFS 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 "github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
|
import "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
||||||
import "github.com/nspcc-dev/neofs-api-go/v2/rpc/common"
|
import "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/common"
|
||||||
|
|
||||||
req := new(CustomRPCRequest)
|
req := new(CustomRPCRequest)
|
||||||
// ...
|
// ...
|
||||||
|
@ -58,6 +63,7 @@ Consume custom service of the server:
|
||||||
// ...
|
// ...
|
||||||
|
|
||||||
Close the connection:
|
Close the connection:
|
||||||
|
|
||||||
err := c.Close()
|
err := c.Close()
|
||||||
// ...
|
// ...
|
||||||
|
|
||||||
|
@ -65,9 +71,10 @@ Note that it's not allowed to override Client behaviour directly: the parameters
|
||||||
for the all operations are write-only and the results of the all operations are
|
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"
|
|
||||||
|
|
||||||
type NeoFSClient interface {
|
import "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client"
|
||||||
|
|
||||||
|
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
|
||||||
// ...
|
// ...
|
||||||
|
@ -80,6 +87,5 @@ 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
|
||||||
|
|
105
client/errors.go
105
client/errors.go
|
@ -1,42 +1,85 @@
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
// IsErrContainerNotFound checks if err corresponds to NeoFS status
|
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
||||||
// return corresponding to missing container.
|
)
|
||||||
|
|
||||||
|
// wrapsErrType returns true if any error in the error tree of err is of type E.
|
||||||
|
func wrapsErrType[E error](err error) bool {
|
||||||
|
switch e := err.(type) {
|
||||||
|
case E:
|
||||||
|
return true
|
||||||
|
case interface{ Unwrap() error }:
|
||||||
|
return wrapsErrType[E](e.Unwrap())
|
||||||
|
case interface{ Unwrap() []error }:
|
||||||
|
for _, ei := range e.Unwrap() {
|
||||||
|
if wrapsErrType[E](ei) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsErrContainerNotFound checks if err corresponds to FrostFS status
|
||||||
|
// return corresponding to missing container. Supports wrapped errors.
|
||||||
func IsErrContainerNotFound(err error) bool {
|
func IsErrContainerNotFound(err error) bool {
|
||||||
switch err.(type) {
|
return wrapsErrType[*apistatus.ContainerNotFound](err)
|
||||||
default:
|
|
||||||
return false
|
|
||||||
case
|
|
||||||
apistatus.ContainerNotFound,
|
|
||||||
*apistatus.ContainerNotFound:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsErrObjectNotFound checks if err corresponds to NeoFS status
|
// IsErrEACLNotFound checks if err corresponds to FrostFS status
|
||||||
// return corresponding to missing object.
|
// 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 {
|
||||||
switch err.(type) {
|
return wrapsErrType[*apistatus.ObjectNotFound](err)
|
||||||
default:
|
|
||||||
return false
|
|
||||||
case
|
|
||||||
apistatus.ObjectNotFound,
|
|
||||||
*apistatus.ObjectNotFound:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsErrObjectAlreadyRemoved checks if err corresponds to NeoFS status
|
// IsErrObjectAlreadyRemoved checks if err corresponds to FrostFS status
|
||||||
// return corresponding to already removed object.
|
// return corresponding to already removed object. Supports wrapped errors.
|
||||||
func IsErrObjectAlreadyRemoved(err error) bool {
|
func IsErrObjectAlreadyRemoved(err error) bool {
|
||||||
switch err.(type) {
|
return wrapsErrType[*apistatus.ObjectAlreadyRemoved](err)
|
||||||
default:
|
}
|
||||||
return false
|
|
||||||
case
|
// IsErrSessionExpired checks if err corresponds to FrostFS status return
|
||||||
apistatus.ObjectAlreadyRemoved,
|
// corresponding to expired session. Supports wrapped errors.
|
||||||
*apistatus.ObjectAlreadyRemoved:
|
func IsErrSessionExpired(err error) bool {
|
||||||
return true
|
return wrapsErrType[*apistatus.SessionTokenExpired](err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsErrSessionNotFound checks if err corresponds to FrostFS status return
|
||||||
|
// corresponding to missing session. Supports wrapped errors.
|
||||||
|
func IsErrSessionNotFound(err error) bool {
|
||||||
|
return wrapsErrType[*apistatus.SessionTokenNotFound](err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsErrAPEManagerAccessDenied checks if err corresponds to FrostFS status return
|
||||||
|
// corresponding to apemanager access deny. Supports wrapped errors.
|
||||||
|
func IsErrAPEManagerAccessDenied(err error) bool {
|
||||||
|
return wrapsErrType[*apistatus.APEManagerAccessDenied](err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsErrNodeUnderMaintenance checks if err corresponds to FrostFS status return
|
||||||
|
// corresponding to nodes being under maintenance. Supports wrapped errors.
|
||||||
|
func IsErrNodeUnderMaintenance(err error) bool {
|
||||||
|
return wrapsErrType[*apistatus.NodeUnderMaintenance](err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns error describing missing field with the given name.
|
||||||
|
func newErrMissingResponseField(name string) error {
|
||||||
|
return fmt.Errorf("missing %s field in the response", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns error describing invalid field (according to the FrostFS protocol)
|
||||||
|
// with the given name and format violation err.
|
||||||
|
func newErrInvalidResponseField(name string, err error) error {
|
||||||
|
return fmt.Errorf("invalid %s field in the response: %w", name, err)
|
||||||
}
|
}
|
||||||
|
|
54
client/errors_test.go
Normal file
54
client/errors_test.go
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
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,177 +2,261 @@ package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
v2netmap "github.com/nspcc-dev/neofs-api-go/v2/netmap"
|
v2netmap "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/netmap"
|
||||||
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/netmap"
|
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
||||||
"github.com/nspcc-dev/neofs-sdk-go/version"
|
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
|
||||||
|
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PrmEndpointInfo groups parameters of EndpointInfo operation.
|
// PrmEndpointInfo groups parameters of EndpointInfo operation.
|
||||||
type PrmEndpointInfo struct {
|
type PrmEndpointInfo struct {
|
||||||
prmCommonMeta
|
XHeaders []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PrmEndpointInfo) buildRequest(c *Client) (*v2netmap.LocalNodeInfoRequest, error) {
|
||||||
|
meta := new(v2session.RequestMetaHeader)
|
||||||
|
writeXHeadersToMeta(x.XHeaders, meta)
|
||||||
|
|
||||||
|
req := new(v2netmap.LocalNodeInfoRequest)
|
||||||
|
req.SetBody(new(v2netmap.LocalNodeInfoRequestBody))
|
||||||
|
c.prepareRequest(req, meta)
|
||||||
|
return req, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 NeoFS API protocol's version in use.
|
// LatestVersion returns latest FrostFS 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
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ResEndpointInfo) setLatestVersion(ver *version.Version) {
|
// NodeInfo returns information about the FrostFS node served on the remote endpoint.
|
||||||
x.version = ver
|
func (x ResEndpointInfo) NodeInfo() netmap.NodeInfo {
|
||||||
}
|
|
||||||
|
|
||||||
// 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 WithNeoFSErrorParsing option has been provided, unsuccessful
|
// If PrmInit.DisableFrostFSFailuresResolution has been called, unsuccessful
|
||||||
// NeoFS status codes are returned as `error`, otherwise, are included
|
// FrostFS status codes are included in the returned result structure,
|
||||||
// in the returned result structure.
|
// otherwise, are also returned as `error`.
|
||||||
//
|
//
|
||||||
// Immediately panics if parameters are set incorrectly (see PrmEndpointInfo docs).
|
// Returns an error if parameters are set incorrectly (see PrmEndpointInfo docs).
|
||||||
// Context is required and must not be nil. It is used for network communication.
|
// 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) {
|
||||||
// check context
|
req, err := prm.buildRequest(c)
|
||||||
if ctx == nil {
|
if err != nil {
|
||||||
panic(panicMsgMissingContext)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// form request
|
if err := signature.SignServiceMessage(&c.prm.Key, req); err != nil {
|
||||||
var req v2netmap.LocalNodeInfoRequest
|
return nil, fmt.Errorf("sign request: %w", err)
|
||||||
|
|
||||||
// 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()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// process call
|
resp, err := rpcapi.LocalNodeInfo(&c.c, req, client.WithContext(ctx))
|
||||||
if !cc.processCall() {
|
if err != nil {
|
||||||
return nil, cc.err
|
return nil, 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 {
|
||||||
prmCommonMeta
|
XHeaders []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x PrmNetworkInfo) buildRequest(c *Client) (*v2netmap.NetworkInfoRequest, error) {
|
||||||
|
meta := new(v2session.RequestMetaHeader)
|
||||||
|
writeXHeadersToMeta(x.XHeaders, meta)
|
||||||
|
|
||||||
|
var req v2netmap.NetworkInfoRequest
|
||||||
|
req.SetBody(new(v2netmap.NetworkInfoRequestBody))
|
||||||
|
c.prepareRequest(&req, meta)
|
||||||
|
return &req, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 NeoFS network.
|
// Info returns structured information about the FrostFS 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
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ResNetworkInfo) setInfo(info *netmap.NetworkInfo) {
|
// NetworkInfo requests information about the FrostFS network of which the remote server is a part.
|
||||||
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 WithNeoFSErrorParsing option has been provided, unsuccessful
|
// If PrmInit.DisableFrostFSFailuresResolution has been called, unsuccessful
|
||||||
// NeoFS status codes are returned as `error`, otherwise, are included
|
// FrostFS status codes are included in the returned result structure,
|
||||||
// in the returned result structure.
|
// otherwise, are also returned as `error`.
|
||||||
//
|
//
|
||||||
// Immediately panics if parameters are set incorrectly (see PrmNetworkInfo docs).
|
// Returns an error if parameters are set incorrectly (see PrmNetworkInfo docs).
|
||||||
// Context is required and must not be nil. It is used for network communication.
|
// 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) {
|
||||||
// check context
|
req, err := prm.buildRequest(c)
|
||||||
if ctx == nil {
|
if err != nil {
|
||||||
panic(panicMsgMissingContext)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.NetworkInfoRequest
|
var req v2netmap.SnapshotRequest
|
||||||
|
req.SetBody(&body)
|
||||||
|
c.prepareRequest(&req, &meta)
|
||||||
|
|
||||||
// init call context
|
err := signature.SignServiceMessage(&c.prm.Key, &req)
|
||||||
|
if err != nil {
|
||||||
var (
|
return nil, fmt.Errorf("sign request: %w", err)
|
||||||
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()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// process call
|
resp, err := c.server.netMapSnapshot(ctx, req)
|
||||||
if !cc.processCall() {
|
if err != nil {
|
||||||
return nil, cc.err
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var res ResNetMapSnapshot
|
||||||
|
res.st, err = c.processResponse(resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !apistatus.IsSuccessful(res.st) {
|
||||||
|
return &res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldNetMap = "network map"
|
||||||
|
|
||||||
|
netMapV2 := resp.GetBody().NetMap()
|
||||||
|
if netMapV2 == nil {
|
||||||
|
return nil, newErrMissingResponseField(fieldNetMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = res.netMap.ReadFromV2(*netMapV2)
|
||||||
|
if err != nil {
|
||||||
|
return nil, newErrInvalidResponseField(fieldNetMap, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &res, nil
|
return &res, nil
|
||||||
|
|
132
client/netmap_test.go
Normal file
132
client/netmap_test.go
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
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,99 +3,106 @@ package client
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
v2object "github.com/nspcc-dev/neofs-api-go/v2/object"
|
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl"
|
||||||
v2refs "github.com/nspcc-dev/neofs-api-go/v2/refs"
|
v2object "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
|
||||||
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
|
v2refs "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
||||||
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
|
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
|
||||||
v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
|
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
||||||
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
|
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
||||||
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
|
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
|
||||||
"github.com/nspcc-dev/neofs-sdk-go/session"
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
|
||||||
"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"
|
||||||
|
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 {
|
||||||
meta v2session.RequestMetaHeader
|
XHeaders []string
|
||||||
|
|
||||||
body v2object.DeleteRequestBody
|
BearerToken *bearer.Token
|
||||||
|
|
||||||
addr v2refs.Address
|
Session *session.Object
|
||||||
|
|
||||||
keySet bool
|
ContainerID *cid.ID
|
||||||
key ecdsa.PrivateKey
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithinSession specifies session within which object should be read.
|
ObjectID *oid.ID
|
||||||
//
|
|
||||||
// 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())
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithBearerToken attaches bearer token to be used for the operation.
|
Key *ecdsa.PrivateKey
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
//
|
//
|
||||||
// Slice must not be mutated until the operation completes.
|
// Deprecated: Use PrmObjectDelete.Key instead.
|
||||||
func (x *PrmObjectDelete) WithXHeaders(hs ...string) {
|
func (prm *PrmObjectDelete) UseKey(key ecdsa.PrivateKey) {
|
||||||
if len(hs)%2 != 0 {
|
prm.Key = &key
|
||||||
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
|
||||||
|
|
||||||
idTomb *v2refs.ObjectID
|
tomb oid.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadTombstoneID reads identifier of the created tombstone object.
|
// Tombstone returns identifier of the created tombstone object.
|
||||||
// Returns false if ID is missing (not read).
|
func (x ResObjectDelete) Tombstone() oid.ID {
|
||||||
func (x ResObjectDelete) ReadTombstoneID(dst *oid.ID) bool {
|
return x.tomb
|
||||||
if x.idTomb != nil {
|
}
|
||||||
*dst = *oid.NewIDFromV2(x.idTomb) // need smth better
|
|
||||||
return true
|
func (prm *PrmObjectDelete) buildRequest(c *Client) (*v2object.DeleteRequest, error) {
|
||||||
|
if prm.ContainerID == nil {
|
||||||
|
return nil, errorMissingContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
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 NeoFS API protocol.
|
// ObjectDelete marks an object for deletion from the container using FrostFS API protocol.
|
||||||
// As a marker, a special unit called a tombstone is placed in the container.
|
// 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.
|
||||||
|
@ -105,11 +112,11 @@ func (x ResObjectDelete) ReadTombstoneID(dst *oid.ID) bool {
|
||||||
//
|
//
|
||||||
// Exactly one return value is non-nil. By default, server status is returned in res structure.
|
// 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 WithNeoFSErrorParsing option has been provided, unsuccessful
|
// If PrmInit.DisableFrostFSFailuresResolution has been called, unsuccessful
|
||||||
// NeoFS status codes are returned as `error`, otherwise, are included
|
// FrostFS status codes are included in the returned result structure,
|
||||||
// in the returned result structure.
|
// otherwise, are also returned as `error`.
|
||||||
//
|
//
|
||||||
// Immediately panics if parameters are set incorrectly (see PrmObjectDelete docs).
|
// Returns an error if parameters are set incorrectly (see PrmObjectDelete docs).
|
||||||
// Context is required and must not be nil. It is used for network communication.
|
// Context is required and must not be nil. It is used for network communication.
|
||||||
//
|
//
|
||||||
// Return statuses:
|
// Return statuses:
|
||||||
|
@ -119,48 +126,46 @@ func (x ResObjectDelete) ReadTombstoneID(dst *oid.ID) bool {
|
||||||
// - *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) {
|
||||||
switch {
|
req, err := prm.buildRequest(c)
|
||||||
case ctx == nil:
|
if err != nil {
|
||||||
panic(panicMsgMissingContext)
|
return nil, err
|
||||||
case prm.addr.GetContainerID() == nil:
|
|
||||||
panic(panicMsgMissingContainer)
|
|
||||||
case prm.addr.GetObjectID() == nil:
|
|
||||||
panic("missing object")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// form request body
|
key := c.prm.Key
|
||||||
prm.body.SetAddress(&prm.addr)
|
if prm.Key != nil {
|
||||||
|
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cc.req = &req
|
err = signature.SignServiceMessage(&key, req)
|
||||||
cc.statusRes = &res
|
if err != nil {
|
||||||
cc.call = func() (responseV2, error) {
|
return nil, fmt.Errorf("sign request: %w", err)
|
||||||
return rpcapi.DeleteObject(&c.c, &req, client.WithContext(ctx))
|
|
||||||
}
|
|
||||||
cc.result = func(r responseV2) {
|
|
||||||
res.idTomb = r.(*v2object.DeleteResponse).GetBody().GetTombstone().GetObjectID()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// process call
|
resp, err := rpcapi.DeleteObject(&c.c, req, client.WithContext(ctx))
|
||||||
if !cc.processCall() {
|
if err != nil {
|
||||||
return nil, cc.err
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var res ResObjectDelete
|
||||||
|
res.st, err = c.processResponse(resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !apistatus.IsSuccessful(res.st) {
|
||||||
|
return &res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldTombstone = "tombstone"
|
||||||
|
|
||||||
|
idTombV2 := resp.GetBody().GetTombstone().GetObjectID()
|
||||||
|
if idTombV2 == nil {
|
||||||
|
return nil, newErrMissingResponseField(fieldTombstone)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = res.tomb.ReadFromV2(*idTombV2)
|
||||||
|
if err != nil {
|
||||||
|
return nil, newErrInvalidResponseField(fieldTombstone, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &res, nil
|
return &res, nil
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -2,110 +2,64 @@ package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
v2object "github.com/nspcc-dev/neofs-api-go/v2/object"
|
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl"
|
||||||
v2refs "github.com/nspcc-dev/neofs-api-go/v2/refs"
|
v2object "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
|
||||||
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
|
v2refs "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
||||||
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
|
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
|
||||||
v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
|
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
||||||
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
|
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
||||||
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
|
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
|
||||||
"github.com/nspcc-dev/neofs-sdk-go/session"
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
|
||||||
"github.com/nspcc-dev/neofs-sdk-go/token"
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/checksum"
|
||||||
|
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
||||||
|
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
||||||
|
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PrmObjectHash groups parameters of ObjectHash operation.
|
// PrmObjectHash groups parameters of ObjectHash operation.
|
||||||
type PrmObjectHash struct {
|
type PrmObjectHash struct {
|
||||||
meta v2session.RequestMetaHeader
|
XHeaders []string
|
||||||
|
|
||||||
body v2object.GetRangeHashRequestBody
|
BearerToken *bearer.Token
|
||||||
|
|
||||||
tillichZemor bool
|
Session *session.Object
|
||||||
|
|
||||||
addr v2refs.Address
|
Local bool
|
||||||
|
|
||||||
|
Ranges []object.Range
|
||||||
|
|
||||||
|
Salt []byte
|
||||||
|
|
||||||
|
ChecksumType checksum.Type
|
||||||
|
|
||||||
|
ContainerID *cid.ID
|
||||||
|
|
||||||
|
ObjectID *oid.ID
|
||||||
|
|
||||||
|
Key *ecdsa.PrivateKey
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarkLocal tells the server to execute the operation locally.
|
// UseKey specifies private key to sign the requests.
|
||||||
func (x *PrmObjectHash) MarkLocal() {
|
// If key is not provided, then Client default key is used.
|
||||||
x.meta.SetTTL(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithinSession specifies session within which object should be read.
|
|
||||||
//
|
//
|
||||||
// Creator of the session acquires the authorship of the request.
|
// Deprecated: Use PrmObjectHash.Key instead.
|
||||||
// This may affect the execution of an operation (e.g. access control).
|
func (prm *PrmObjectHash) UseKey(key ecdsa.PrivateKey) {
|
||||||
//
|
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.
|
|
||||||
//
|
//
|
||||||
// Must not be mutated before the operation completes.
|
// Deprecated: Use PrmObjectHash.ChecksumType instead.
|
||||||
func (x *PrmObjectHash) UseSalt(salt []byte) {
|
func (prm *PrmObjectHash) TillichZemorAlgo() {
|
||||||
x.body.SetSalt(salt)
|
prm.ChecksumType = checksum.TZ
|
||||||
}
|
|
||||||
|
|
||||||
// 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.
|
||||||
|
@ -120,19 +74,89 @@ 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
|
||||||
// NeoFS API protocol.
|
// FrostFS 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 WithNeoFSErrorParsing option has been provided, unsuccessful
|
// If PrmInit.DisableFrostFSFailuresResolution has been called, unsuccessful
|
||||||
// NeoFS status codes are returned as `error`, otherwise, are included
|
// FrostFS status codes are included in the returned result structure,
|
||||||
// in the returned result structure.
|
// otherwise, are also returned as `error`.
|
||||||
//
|
//
|
||||||
// Immediately panics if parameters are set incorrectly (see PrmObjectHash docs).
|
// Returns an error if parameters are set incorrectly (see PrmObjectHash docs).
|
||||||
// Context is required and must not be nil. It is used for network communication.
|
// Context is required and must not be nil. It is used for network communication.
|
||||||
//
|
//
|
||||||
// Return statuses:
|
// Return statuses:
|
||||||
|
@ -140,53 +164,42 @@ func (x ResObjectHash) Checksums() [][]byte {
|
||||||
// - *apistatus.ContainerNotFound;
|
// - *apistatus.ContainerNotFound;
|
||||||
// - *apistatus.ObjectNotFound;
|
// - *apistatus.ObjectNotFound;
|
||||||
// - *apistatus.ObjectAccessDenied;
|
// - *apistatus.ObjectAccessDenied;
|
||||||
|
// - *apistatus.ObjectOutOfRange;
|
||||||
// - *apistatus.SessionTokenExpired.
|
// - *apistatus.SessionTokenExpired.
|
||||||
func (c *Client) ObjectHash(ctx context.Context, prm PrmObjectHash) (*ResObjectHash, error) {
|
func (c *Client) ObjectHash(ctx context.Context, prm PrmObjectHash) (*ResObjectHash, error) {
|
||||||
switch {
|
req, err := prm.buildRequest(c)
|
||||||
case ctx == nil:
|
if err != nil {
|
||||||
panic(panicMsgMissingContext)
|
return nil, err
|
||||||
case prm.addr.GetContainerID() == nil:
|
|
||||||
panic(panicMsgMissingContainer)
|
|
||||||
case prm.addr.GetObjectID() == nil:
|
|
||||||
panic("missing object")
|
|
||||||
case len(prm.body.GetRanges()) == 0:
|
|
||||||
panic("missing ranges")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// form request body
|
key := c.prm.Key
|
||||||
prm.body.SetAddress(&prm.addr)
|
if prm.Key != nil {
|
||||||
// ranges and salt are already by prm setters
|
key = *prm.Key
|
||||||
|
|
||||||
if prm.tillichZemor {
|
|
||||||
prm.body.SetType(v2refs.TillichZemor)
|
|
||||||
} else {
|
|
||||||
prm.body.SetType(v2refs.SHA256)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// form request
|
err = signature.SignServiceMessage(&key, req)
|
||||||
var req v2object.GetRangeHashRequest
|
if err != nil {
|
||||||
req.SetBody(&prm.body)
|
return nil, fmt.Errorf("sign request: %w", err)
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// process call
|
resp, err := rpcapi.HashObjectRange(&c.c, req, client.WithContext(ctx))
|
||||||
if !cc.processCall() {
|
if err != nil {
|
||||||
return nil, cc.err
|
return nil, fmt.Errorf("write request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var res ResObjectHash
|
||||||
|
res.st, err = c.processResponse(resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !apistatus.IsSuccessful(res.st) {
|
||||||
|
return &res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
res.checksums = resp.GetBody().GetHashList()
|
||||||
|
if len(res.checksums) == 0 {
|
||||||
|
return nil, newErrMissingResponseField("hash list")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &res, nil
|
return &res, nil
|
||||||
|
|
270
client/object_patch.go
Normal file
270
client/object_patch.go
Normal file
|
@ -0,0 +1,270 @@
|
||||||
|
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
|
||||||
|
}
|
295
client/object_patch_test.go
Normal file
295
client/object_patch_test.go
Normal file
|
@ -0,0 +1,295 @@
|
||||||
|
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,231 +3,194 @@ package client
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
v2object "github.com/nspcc-dev/neofs-api-go/v2/object"
|
buffPool "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/util/pool"
|
||||||
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
|
||||||
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
||||||
v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
|
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||||
"github.com/nspcc-dev/neofs-sdk-go/object"
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/transformer"
|
||||||
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
|
||||||
"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.
|
||||||
//
|
//
|
||||||
// At the moment the operation is not parameterized, however,
|
// Deprecated: Use PrmObjectPutInit.CopiesNumber instead.
|
||||||
// the structure is still declared for backward compatibility.
|
func (x *PrmObjectPutInit) SetCopiesNumber(copiesNumber uint32) {
|
||||||
type PrmObjectPutInit struct{}
|
x.CopiesNumber = []uint32{copiesNumber}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCopiesNumberByVectors sets ordered list of minimal required object copies numbers
|
||||||
|
// per placement vector. List's length MUST equal container's placement vector number,
|
||||||
|
// otherwise request will fail.
|
||||||
|
//
|
||||||
|
// Deprecated: Use PrmObjectPutInit.CopiesNumber instead.
|
||||||
|
func (x *PrmObjectPutInit) SetCopiesNumberByVectors(copiesNumbers []uint32) {
|
||||||
|
x.CopiesNumber = copiesNumbers
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetGRPCPayloadChunkLen sets maximum chunk length value for gRPC Put request.
|
||||||
|
// Maximum chunk length restricts maximum byte length of the chunk
|
||||||
|
// transmitted in a single stream message. It depends on
|
||||||
|
// server settings and other message fields.
|
||||||
|
// If not specified or negative value set, default value of 3MiB will be used.
|
||||||
|
//
|
||||||
|
// Deprecated: Use PrmObjectPutInit.MaxChunkLength instead.
|
||||||
|
func (x *PrmObjectPutInit) SetGRPCPayloadChunkLen(v int) {
|
||||||
|
x.MaxChunkLength = v
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
|
||||||
resp v2object.PutResponse
|
obj oid.ID
|
||||||
|
epoch uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadStoredObjectID reads identifier of the saved object.
|
// StoredObjectID returns identifier of the saved object.
|
||||||
// Returns false if ID is missing (not read).
|
func (x ResObjectPut) StoredObjectID() oid.ID {
|
||||||
func (x *ResObjectPut) ReadStoredObjectID(id *oid.ID) bool {
|
return x.obj
|
||||||
idv2 := x.resp.GetBody().GetObjectID()
|
|
||||||
if idv2 == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
*id = *oid.NewIDFromV2(idv2) // need smth better
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObjectWriter is designed to write one object to NeoFS system.
|
// StoredEpoch returns creation epoch of the saved object.
|
||||||
|
func (x ResObjectPut) StoredEpoch() uint64 {
|
||||||
|
return x.epoch
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObjectWriter is designed to write one object or
|
||||||
|
// multiple parts of one object to FrostFS system.
|
||||||
//
|
//
|
||||||
// Must be initialized using Client.ObjectPutInit, any other
|
// Must be initialized using Client.ObjectPutInit, any other
|
||||||
// usage is unsafe.
|
// usage is unsafe.
|
||||||
type ObjectWriter struct {
|
type ObjectWriter interface {
|
||||||
cancelCtxStream context.CancelFunc
|
// WriteHeader writes header of the object. Result means success.
|
||||||
|
// Failure reason can be received via Close.
|
||||||
ctxCall contextCall
|
WriteHeader(context.Context, object.Object) bool
|
||||||
|
// WritePayloadChunk writes chunk of the object payload. Result means success.
|
||||||
// initially bound tp contextCall
|
// Failure reason can be received via Close.
|
||||||
metaHdr v2session.RequestMetaHeader
|
WritePayloadChunk(context.Context, []byte) bool
|
||||||
|
// Close ends writing the object and returns the result of the operation
|
||||||
// initially bound to contextCall
|
// along with the final results. Must be called after using the ObjectWriter.
|
||||||
partInit v2object.PutObjectPartInit
|
//
|
||||||
|
// Exactly one return value is non-nil. By default, server status is returned in res structure.
|
||||||
chunkCalled bool
|
// 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
|
||||||
partChunk v2object.PutObjectPartChunk
|
// codes are returned as error.
|
||||||
|
//
|
||||||
|
// Return statuses:
|
||||||
|
// - global (see Client docs);
|
||||||
|
// - *apistatus.ContainerNotFound;
|
||||||
|
// - *apistatus.ObjectAccessDenied;
|
||||||
|
// - *apistatus.ObjectLocked;
|
||||||
|
// - *apistatus.LockNonRegularObject;
|
||||||
|
// - *apistatus.SessionTokenNotFound;
|
||||||
|
// - *apistatus.SessionTokenExpired.
|
||||||
|
Close(context.Context) (*ResObjectPut, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
//
|
||||||
x.ctxCall.key = key
|
// Deprecated: Use PrmObjectPutInit.Key instead.
|
||||||
|
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) {
|
//
|
||||||
x.metaHdr.SetBearerToken(t.ToV2())
|
// Deprecated: Use PrmObjectPutInit.BearerToken instead.
|
||||||
|
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) {
|
//
|
||||||
x.metaHdr.SetSessionToken(t.ToV2())
|
// Deprecated: Use PrmObjectPutInit.Session instead.
|
||||||
|
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() {
|
//
|
||||||
x.metaHdr.SetTTL(1)
|
// Deprecated: Use PrmObjectPutInit.Local instead.
|
||||||
|
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) {
|
|
||||||
if len(hs)%2 != 0 {
|
|
||||||
panic("slice of X-Headers with odd length")
|
|
||||||
}
|
|
||||||
|
|
||||||
prmCommonMeta{xHeaders: hs}.writeToMetaHeader(&x.metaHdr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteHeader writes header of the object. Result means success.
|
|
||||||
// Failure reason can be received via Close.
|
|
||||||
func (x *ObjectWriter) WriteHeader(hdr object.Object) bool {
|
|
||||||
v2Hdr := hdr.ToV2()
|
|
||||||
|
|
||||||
x.partInit.SetObjectID(v2Hdr.GetObjectID())
|
|
||||||
x.partInit.SetHeader(v2Hdr.GetHeader())
|
|
||||||
x.partInit.SetSignature(v2Hdr.GetSignature())
|
|
||||||
|
|
||||||
return x.ctxCall.writeRequest()
|
|
||||||
}
|
|
||||||
|
|
||||||
// WritePayloadChunk writes chunk of the object payload. Result means success.
|
|
||||||
// Failure reason can be received via Close.
|
|
||||||
func (x *ObjectWriter) WritePayloadChunk(chunk []byte) bool {
|
|
||||||
if !x.chunkCalled {
|
|
||||||
x.chunkCalled = true
|
|
||||||
x.ctxCall.req.(*v2object.PutRequest).GetBody().SetObjectPart(&x.partChunk)
|
|
||||||
}
|
|
||||||
|
|
||||||
for ln := len(chunk); ln > 0; ln = len(chunk) {
|
|
||||||
// maxChunkLen restricts maximum byte length of the chunk
|
|
||||||
// transmitted in a single stream message. It depends on
|
|
||||||
// server settings and other message fields, but for now
|
|
||||||
// we simply assume that 3MB is large enough to reduce the
|
|
||||||
// number of messages, and not to exceed the limit
|
|
||||||
// (4MB by default for gRPC servers).
|
|
||||||
const maxChunkLen = 3 << 20
|
|
||||||
if ln > maxChunkLen {
|
|
||||||
ln = maxChunkLen
|
|
||||||
}
|
|
||||||
|
|
||||||
// we deal with size limit overflow above, but there is another case:
|
|
||||||
// what if method is called with "small" chunk many times? We write
|
|
||||||
// a message to the stream on each call. Alternatively, we could use buffering.
|
|
||||||
// In most cases, the chunk length does not vary between calls. Given this
|
|
||||||
// assumption, as well as the length of the payload from the header, it is
|
|
||||||
// possible to buffer the data of intermediate chunks, and send a message when
|
|
||||||
// the allocated buffer is filled, or when the last chunk is received.
|
|
||||||
// It is mentally assumed that allocating and filling the buffer is better than
|
|
||||||
// synchronous sending, but this needs to be tested.
|
|
||||||
x.partChunk.SetChunk(chunk[:ln])
|
|
||||||
|
|
||||||
if !x.ctxCall.writeRequest() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
chunk = chunk[ln:]
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close ends writing the object and returns the result of the operation
|
|
||||||
// along with the final results. Must be called after using the ObjectWriter.
|
|
||||||
//
|
//
|
||||||
// Exactly one return value is non-nil. By default, server status is returned in res structure.
|
// Deprecated: Use PrmObjectPutInit.XHeaders instead.
|
||||||
// Any client's internal or transport errors are returned as Go built-in error.
|
func (x *PrmObjectPutInit) WithXHeaders(hs ...string) {
|
||||||
// If Client is tuned to resolve NeoFS API statuses, then NeoFS failures
|
x.XHeaders = hs
|
||||||
// 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 NeoFS API protocol.
|
// WithObjectMaxSize specifies max object size value and use it during object splitting.
|
||||||
|
// When specified, start writing to the stream only after the object is formed.
|
||||||
|
// Continue processing the input only when the previous formed object has been successfully written.
|
||||||
|
//
|
||||||
|
// Deprecated: Use PrmObjectPutInit.MaxSize instead.
|
||||||
|
func (x *PrmObjectPutInit) WithObjectMaxSize(maxSize uint64) {
|
||||||
|
x.MaxSize = maxSize
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithoutHomomorphicHash if set to true do not use Tillich-Zémor hash for payload.
|
||||||
|
//
|
||||||
|
// Deprecated: Use PrmObjectPutInit.WithoutHomomorphHash instead.
|
||||||
|
func (x *PrmObjectPutInit) WithoutHomomorphicHash(v bool) {
|
||||||
|
x.WithoutHomomorphHash = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithEpochSource specifies epoch for object when split it on client side.
|
||||||
|
//
|
||||||
|
// Deprecated: Use PrmObjectPutInit.EpochSource instead.
|
||||||
|
func (x *PrmObjectPutInit) WithEpochSource(es transformer.EpochSource) {
|
||||||
|
x.EpochSource = es
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObjectPutInit initiates writing an object through a remote server using FrostFS API protocol.
|
||||||
//
|
//
|
||||||
// The call only opens the transmission channel, explicit recording is done using the ObjectWriter.
|
// 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, _ PrmObjectPutInit) (*ObjectWriter, error) {
|
func (c *Client) ObjectPutInit(ctx context.Context, prm PrmObjectPutInit) (ObjectWriter, error) {
|
||||||
// check parameters
|
if prm.MaxSize > 0 {
|
||||||
if ctx == nil {
|
return c.objectPutInitTransformer(prm)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
181
client/object_put_raw.go
Normal file
181
client/object_put_raw.go
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
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
|
||||||
|
}
|
175
client/object_put_single.go
Normal file
175
client/object_put_single.go
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
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
|
||||||
|
}
|
144
client/object_put_transformer.go
Normal file
144
client/object_put_transformer.go
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
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,40 +7,43 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
v2object "github.com/nspcc-dev/neofs-api-go/v2/object"
|
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl"
|
||||||
v2refs "github.com/nspcc-dev/neofs-api-go/v2/refs"
|
v2object "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
|
||||||
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
|
v2refs "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
||||||
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
|
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
|
||||||
v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
|
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
||||||
apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
|
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"
|
||||||
"github.com/nspcc-dev/neofs-sdk-go/object"
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
|
||||||
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
|
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
||||||
"github.com/nspcc-dev/neofs-sdk-go/session"
|
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||||
"github.com/nspcc-dev/neofs-sdk-go/token"
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
||||||
|
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PrmObjectSearch groups parameters of ObjectSearch operation.
|
// PrmObjectSearch groups parameters of ObjectSearch operation.
|
||||||
type PrmObjectSearch struct {
|
type PrmObjectSearch struct {
|
||||||
prmCommonMeta
|
XHeaders []string
|
||||||
|
|
||||||
local bool
|
Local bool
|
||||||
|
|
||||||
sessionSet bool
|
BearerToken *bearer.Token
|
||||||
session session.Token
|
|
||||||
|
|
||||||
bearerSet bool
|
Session *session.Object
|
||||||
bearer token.BearerToken
|
|
||||||
|
|
||||||
cnrSet bool
|
ContainerID *cid.ID
|
||||||
cnr cid.ID
|
|
||||||
|
|
||||||
filters object.SearchFilters
|
Key *ecdsa.PrivateKey
|
||||||
|
|
||||||
|
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.
|
||||||
|
@ -49,9 +52,10 @@ 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) {
|
//
|
||||||
x.session = t
|
// Deprecated: Use PrmObjectSearch.Session instead.
|
||||||
x.sessionSet = true
|
func (x *PrmObjectSearch) WithinSession(t session.Object) {
|
||||||
|
x.Session = &t
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithBearerToken attaches bearer token to be used for the operation.
|
// WithBearerToken attaches bearer token to be used for the operation.
|
||||||
|
@ -59,22 +63,44 @@ func (x *PrmObjectSearch) WithinSession(t session.Token) {
|
||||||
// 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) {
|
//
|
||||||
x.bearer = t
|
// Deprecated: Use PrmObjectSearch.BearerToken instead.
|
||||||
x.bearerSet = true
|
func (x *PrmObjectSearch) WithBearerToken(t bearer.Token) {
|
||||||
|
x.BearerToken = &t
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithXHeaders specifies list of extended headers (string key-value pairs)
|
||||||
|
// to be attached to the request. Must have an even length.
|
||||||
|
//
|
||||||
|
// Slice must not be mutated until the operation completes.
|
||||||
|
//
|
||||||
|
// Deprecated: Use PrmObjectSearch.XHeaders instead.
|
||||||
|
func (x *PrmObjectSearch) WithXHeaders(hs ...string) {
|
||||||
|
x.XHeaders = hs
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseKey specifies private key to sign the requests.
|
||||||
|
// If key is not provided, then Client default key is used.
|
||||||
|
//
|
||||||
|
// Deprecated: Use PrmObjectSearch.Key instead.
|
||||||
|
func (x *PrmObjectSearch) UseKey(key ecdsa.PrivateKey) {
|
||||||
|
x.Key = &key
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.cnr = id
|
x.ContainerID = &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.
|
||||||
|
@ -82,28 +108,20 @@ type ResObjectSearch struct {
|
||||||
statusRes
|
statusRes
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObjectListReader is designed to read list of object identifiers from NeoFS system.
|
// ObjectListReader is designed to read list of object identifiers from FrostFS system.
|
||||||
//
|
//
|
||||||
// Must be initialized using Client.ObjectSearch, any other usage is unsafe.
|
// 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
|
||||||
ctxCall contextCall
|
res ResObjectSearch
|
||||||
|
stream interface {
|
||||||
reqWritten bool
|
Read(resp *v2object.SearchResponse) error
|
||||||
|
}
|
||||||
// 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.
|
||||||
//
|
//
|
||||||
|
@ -115,58 +133,33 @@ func (x *ObjectListReader) Read(buf []oid.ID) (int, bool) {
|
||||||
panic("empty buffer in ObjectListReader.ReadList")
|
panic("empty buffer in ObjectListReader.ReadList")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !x.reqWritten {
|
read := copyIDBuffers(buf, x.tail)
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
var ok bool
|
|
||||||
var ids []v2refs.ObjectID
|
|
||||||
var i, ln, rem int
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
// receive next message
|
var resp v2object.SearchResponse
|
||||||
ok = x.ctxCall.readResponse()
|
x.err = x.stream.Read(&resp)
|
||||||
if !ok {
|
if x.err != nil {
|
||||||
|
return read, false
|
||||||
|
}
|
||||||
|
|
||||||
|
x.res.st, x.err = x.client.processResponse(&resp)
|
||||||
|
if x.err != nil || !apistatus.IsSuccessful(x.res.st) {
|
||||||
return read, false
|
return read, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// read new chunk of objects
|
// read new chunk of objects
|
||||||
ids = x.bodyResp.GetIDList()
|
ids := resp.GetBody().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
|
||||||
}
|
}
|
||||||
|
|
||||||
if rem = len(buf) - read; ln > rem {
|
ln := copyIDBuffers(buf[read:], ids)
|
||||||
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) {
|
||||||
|
@ -178,6 +171,14 @@ func (x *ObjectListReader) Read(buf []oid.ID) (int, bool) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func copyIDBuffers(dst []oid.ID, src []v2refs.ObjectID) int {
|
||||||
|
var i int
|
||||||
|
for ; i < len(dst) && i < len(src); i++ {
|
||||||
|
_ = dst[i].ReadFromV2(src[i])
|
||||||
|
}
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
// Iterate iterates over the list of found object identifiers.
|
// 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.
|
||||||
//
|
//
|
||||||
|
@ -207,7 +208,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 NeoFS API statuses, then NeoFS failures
|
// If Client is tuned to resolve FrostFS API statuses, then FrostFS failures
|
||||||
// codes are returned as error.
|
// codes are returned as error.
|
||||||
//
|
//
|
||||||
// Return statuses:
|
// Return statuses:
|
||||||
|
@ -218,89 +219,87 @@ 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.ctxCall.err != nil && !errors.Is(x.ctxCall.err, io.EOF) {
|
if x.err != nil && !errors.Is(x.err, io.EOF) {
|
||||||
return nil, x.ctxCall.err
|
return nil, x.err
|
||||||
}
|
}
|
||||||
|
|
||||||
return x.ctxCall.statusRes.(*ResObjectSearch), nil
|
return &x.res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObjectSearchInit initiates object selection through a remote server using NeoFS API protocol.
|
func (x *PrmObjectSearch) buildRequest(c *Client) (*v2object.SearchRequest, error) {
|
||||||
|
if x.ContainerID == nil {
|
||||||
|
return nil, errorMissingContainer
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(x.XHeaders)%2 != 0 {
|
||||||
|
return nil, errorInvalidXHeaders
|
||||||
|
}
|
||||||
|
|
||||||
|
meta := new(v2session.RequestMetaHeader)
|
||||||
|
writeXHeadersToMeta(x.XHeaders, meta)
|
||||||
|
|
||||||
|
if x.BearerToken != nil {
|
||||||
|
v2BearerToken := new(acl.BearerToken)
|
||||||
|
x.BearerToken.WriteToV2(v2BearerToken)
|
||||||
|
meta.SetBearerToken(v2BearerToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
if x.Session != nil {
|
||||||
|
v2SessionToken := new(v2session.Token)
|
||||||
|
x.Session.WriteToV2(v2SessionToken)
|
||||||
|
meta.SetSessionToken(v2SessionToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
if x.Local {
|
||||||
|
meta.SetTTL(1)
|
||||||
|
}
|
||||||
|
cnrV2 := new(v2refs.ContainerID)
|
||||||
|
x.ContainerID.WriteToV2(cnrV2)
|
||||||
|
|
||||||
|
body := new(v2object.SearchRequestBody)
|
||||||
|
body.SetVersion(1)
|
||||||
|
body.SetContainerID(cnrV2)
|
||||||
|
body.SetFilters(x.Filters.ToV2())
|
||||||
|
|
||||||
|
req := new(v2object.SearchRequest)
|
||||||
|
req.SetBody(body)
|
||||||
|
c.prepareRequest(req, meta)
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObjectSearchInit initiates object selection through a remote server using FrostFS API protocol.
|
||||||
//
|
//
|
||||||
// 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.
|
||||||
//
|
//
|
||||||
// Immediately panics if parameters are set incorrectly (see PrmObjectSearch docs).
|
// Returns an error if parameters are set incorrectly (see PrmObjectSearch docs).
|
||||||
// Context is required and must not be nil. It is used for network communication.
|
// Context is required and must not be nil. It is used for network communication.
|
||||||
func (c *Client) ObjectSearchInit(ctx context.Context, prm PrmObjectSearch) (*ObjectListReader, error) {
|
func (c *Client) ObjectSearchInit(ctx context.Context, prm PrmObjectSearch) (*ObjectListReader, error) {
|
||||||
// check parameters
|
req, err := prm.buildRequest(c)
|
||||||
switch {
|
if err != nil {
|
||||||
case ctx == nil:
|
return nil, err
|
||||||
panic(panicMsgMissingContext)
|
|
||||||
case !prm.cnrSet:
|
|
||||||
panic(panicMsgMissingContainer)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// form request body
|
key := prm.Key
|
||||||
var body v2object.SearchRequestBody
|
if key == nil {
|
||||||
|
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if prm.bearerSet {
|
err = signature.SignServiceMessage(key, req)
|
||||||
meta.SetBearerToken(prm.bearer.ToV2())
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("sign request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if prm.sessionSet {
|
var r ObjectListReader
|
||||||
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)
|
||||||
|
|
||||||
resp.SetBody(&r.bodyResp)
|
r.stream, err = rpcapi.SearchObjects(&c.c, req, client.WithContext(ctx))
|
||||||
|
if err != nil {
|
||||||
// init call context
|
return nil, fmt.Errorf("open stream: %w", err)
|
||||||
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,26 +1,28 @@
|
||||||
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, setID := testListReaderResponse(t)
|
p, resp := 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) {
|
||||||
|
@ -34,38 +36,23 @@ 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
|
||||||
setID(ids[:3])
|
resp.stream = newSearchStream(p, nil, ids[:3])
|
||||||
checkRead(t, ids[:2])
|
checkRead(t, ids[:2])
|
||||||
|
|
||||||
// one ID cached, second fetched
|
// one ID cached, second fetched
|
||||||
setID(ids[3:6])
|
resp.stream = newSearchStream(p, nil, ids[3:6])
|
||||||
checkRead(t, ids[2:4])
|
checkRead(t, ids[2:4])
|
||||||
|
|
||||||
// both ID cached
|
// both ID cached
|
||||||
resp.ctxCall.resp = nil
|
resp.stream = nil // shouldn't be called, panic if so
|
||||||
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
|
||||||
var n int
|
resp.stream = newSearchStream(p, nil, ids[6:7], nil, ids[7:8])
|
||||||
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.ctxCall.rResp = nil
|
resp.stream = newSearchStream(p, nil, ids[8:11])
|
||||||
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])
|
||||||
|
@ -73,43 +60,20 @@ func TestObjectSearch(t *testing.T) {
|
||||||
|
|
||||||
// handle EOF
|
// handle EOF
|
||||||
buf = buf[:2]
|
buf = buf[:2]
|
||||||
n = 0
|
resp.stream = newSearchStream(p, io.EOF, ids[11:12])
|
||||||
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) {
|
||||||
resp, setID := testListReaderResponse(t)
|
p, resp := testListReaderResponse(t)
|
||||||
|
|
||||||
// Iterate over all sequence
|
resp.stream = newSearchStream(p, io.EOF, ids[0:2], nil, ids[2:3])
|
||||||
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 {
|
||||||
|
@ -119,10 +83,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) {
|
||||||
resp, setID := testListReaderResponse(t)
|
p, resp := testListReaderResponse(t)
|
||||||
|
|
||||||
var actual []oid.ID
|
var actual []oid.ID
|
||||||
setID(ids)
|
resp.stream = &singleStreamResponder{key: p, idList: [][]oid.ID{ids}}
|
||||||
require.NoError(t, resp.Iterate(func(id oid.ID) bool {
|
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
|
||||||
|
@ -130,22 +94,12 @@ 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) {
|
||||||
resp, setID := testListReaderResponse(t)
|
p, resp := testListReaderResponse(t)
|
||||||
expectedErr := errors.New("test error")
|
expectedErr := errors.New("test error")
|
||||||
|
|
||||||
var actual []oid.ID
|
resp.stream = newSearchStream(p, expectedErr, ids[:2])
|
||||||
var n int
|
|
||||||
resp.ctxCall.rResp = func() error {
|
|
||||||
switch n {
|
|
||||||
case 0:
|
|
||||||
setID(ids[:2])
|
|
||||||
default:
|
|
||||||
return expectedErr
|
|
||||||
}
|
|
||||||
n++
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
|
var actual []oid.ID
|
||||||
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
|
||||||
|
@ -155,37 +109,56 @@ func TestObjectIterate(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func testListReaderResponse(t *testing.T) (*ObjectListReader, func(id []oid.ID) *object.SearchResponse) {
|
func testListReaderResponse(t *testing.T) (*ecdsa.PrivateKey, *ObjectListReader) {
|
||||||
p, err := keys.NewPrivateKey()
|
p, err := keys.NewPrivateKey()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
obj := &ObjectListReader{
|
return &p.PrivateKey, &ObjectListReader{
|
||||||
cancelCtxStream: func() {},
|
cancelCtxStream: func() {},
|
||||||
ctxCall: contextCall{
|
client: &Client{},
|
||||||
closer: func() error { return nil },
|
tail: nil,
|
||||||
result: func(v2 responseV2) {},
|
|
||||||
statusRes: new(ResObjectSearch),
|
|
||||||
},
|
|
||||||
reqWritten: true,
|
|
||||||
bodyResp: object.SearchResponseBody{},
|
|
||||||
tail: nil,
|
|
||||||
}
|
|
||||||
|
|
||||||
return obj, func(id []oid.ID) *object.SearchResponse {
|
|
||||||
resp := new(object.SearchResponse)
|
|
||||||
resp.SetBody(new(object.SearchResponseBody))
|
|
||||||
|
|
||||||
v2id := make([]refs.ObjectID, len(id))
|
|
||||||
for i := range id {
|
|
||||||
v2id[i] = *id[i].ToV2()
|
|
||||||
}
|
|
||||||
resp.GetBody().SetIDList(v2id)
|
|
||||||
err := signatureV2.SignServiceMessage(&p.PrivateKey, resp)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error: %v", err)
|
|
||||||
}
|
|
||||||
obj.ctxCall.resp = resp
|
|
||||||
obj.bodyResp = *resp.GetBody()
|
|
||||||
return resp
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newSearchStream(key *ecdsa.PrivateKey, endError error, idList ...[]oid.ID) *singleStreamResponder {
|
||||||
|
return &singleStreamResponder{
|
||||||
|
key: key,
|
||||||
|
endError: endError,
|
||||||
|
idList: idList,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type singleStreamResponder struct {
|
||||||
|
key *ecdsa.PrivateKey
|
||||||
|
n int
|
||||||
|
endError error
|
||||||
|
idList [][]oid.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *singleStreamResponder) Read(resp *v2object.SearchResponse) error {
|
||||||
|
if s.n >= len(s.idList) {
|
||||||
|
if s.endError != nil {
|
||||||
|
return s.endError
|
||||||
|
}
|
||||||
|
panic("unexpected call to `Read`")
|
||||||
|
}
|
||||||
|
|
||||||
|
var body v2object.SearchResponseBody
|
||||||
|
|
||||||
|
if s.idList[s.n] != nil {
|
||||||
|
ids := make([]refs.ObjectID, len(s.idList[s.n]))
|
||||||
|
for i := range s.idList[s.n] {
|
||||||
|
s.idList[s.n][i].WriteToV2(&ids[i])
|
||||||
|
}
|
||||||
|
body.SetIDList(ids)
|
||||||
|
}
|
||||||
|
resp.SetBody(&body)
|
||||||
|
|
||||||
|
err := signatureV2.SignServiceMessage(s.key, resp)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("error: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
s.n++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,194 +0,0 @@
|
||||||
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,10 +1,12 @@
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import "github.com/nspcc-dev/neofs-api-go/v2/session"
|
import "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
||||||
|
|
||||||
// ResponseMetaInfo groups meta information about any NeoFS API response.
|
// ResponseMetaInfo groups meta information about any FrostFS API response.
|
||||||
type ResponseMetaInfo struct {
|
type ResponseMetaInfo struct {
|
||||||
key []byte
|
key []byte
|
||||||
|
|
||||||
|
epoch uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
type responseV2 interface {
|
type responseV2 interface {
|
||||||
|
@ -18,3 +20,8 @@ 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,23 +2,64 @@ package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
|
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
||||||
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
|
rpcapi "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc"
|
||||||
v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
|
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
||||||
"github.com/nspcc-dev/neofs-sdk-go/owner"
|
v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature"
|
||||||
|
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PrmSessionCreate groups parameters of SessionCreate operation.
|
// PrmSessionCreate groups parameters of SessionCreate operation.
|
||||||
type PrmSessionCreate struct {
|
type PrmSessionCreate struct {
|
||||||
prmCommonMeta
|
XHeaders []string
|
||||||
|
|
||||||
exp uint64
|
Expiration 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.exp = exp
|
x.Expiration = exp
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseKey specifies private key to sign the requests and compute token owner.
|
||||||
|
// If key is not provided, then Client default key is used.
|
||||||
|
//
|
||||||
|
// Deprecated: Use PrmSessionCreate.Key instead.
|
||||||
|
func (x *PrmSessionCreate) UseKey(key ecdsa.PrivateKey) {
|
||||||
|
x.Key = &key
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PrmSessionCreate) buildRequest(c *Client) (*v2session.CreateRequest, error) {
|
||||||
|
ownerKey := c.prm.Key.PublicKey
|
||||||
|
if x.Key != nil {
|
||||||
|
ownerKey = x.Key.PublicKey
|
||||||
|
}
|
||||||
|
var ownerID user.ID
|
||||||
|
user.IDFromKey(&ownerID, ownerKey)
|
||||||
|
|
||||||
|
var ownerIDV2 refs.OwnerID
|
||||||
|
ownerID.WriteToV2(&ownerIDV2)
|
||||||
|
|
||||||
|
reqBody := new(v2session.CreateRequestBody)
|
||||||
|
reqBody.SetOwnerID(&ownerIDV2)
|
||||||
|
reqBody.SetExpiration(x.Expiration)
|
||||||
|
|
||||||
|
var meta v2session.RequestMetaHeader
|
||||||
|
writeXHeadersToMeta(x.XHeaders, &meta)
|
||||||
|
|
||||||
|
var req v2session.CreateRequest
|
||||||
|
req.SetBody(reqBody)
|
||||||
|
c.prepareRequest(&req, &meta)
|
||||||
|
return &req, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResSessionCreate groups resulting values of SessionCreate operation.
|
// ResSessionCreate groups resulting values of SessionCreate operation.
|
||||||
|
@ -30,22 +71,14 @@ type ResSessionCreate struct {
|
||||||
sessionKey []byte
|
sessionKey []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ResSessionCreate) setID(id []byte) {
|
// ID returns identifier of the opened session in a binary FrostFS API protocol format.
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ResSessionCreate) setSessionKey(key []byte) {
|
// PublicKey returns public key of the opened session in a binary FrostFS API protocol format.
|
||||||
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
|
||||||
}
|
}
|
||||||
|
@ -56,60 +89,38 @@ 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 WithNeoFSErrorParsing option has been provided, unsuccessful
|
// If PrmInit.DisableFrostFSFailuresResolution has been called, unsuccessful
|
||||||
// NeoFS status codes are returned as `error`, otherwise, are included
|
// FrostFS status codes are included in the returned result structure,
|
||||||
// in the returned result structure.
|
// otherwise, are also returned as `error`.
|
||||||
//
|
//
|
||||||
// Immediately panics if parameters are set incorrectly (see PrmSessionCreate docs).
|
// Returns an error if parameters are set incorrectly (see PrmSessionCreate docs).
|
||||||
// Context is required and must not be nil. It is used for network communication.
|
// 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) {
|
||||||
// check context
|
req, err := prm.buildRequest(c)
|
||||||
if ctx == nil {
|
if err != nil {
|
||||||
panic(panicMsgMissingContext)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ownerID := owner.NewIDFromPublicKey(&c.prm.key.PublicKey)
|
if err := signature.SignServiceMessage(&c.prm.Key, req); err != nil {
|
||||||
|
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())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// process call
|
resp, err := rpcapi.CreateSession(&c.c, req, client.WithContext(ctx))
|
||||||
if !cc.processCall() {
|
if err != nil {
|
||||||
return nil, cc.err
|
return nil, 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
|
||||||
}
|
}
|
||||||
|
|
53
client/status/apemanager.go
Normal file
53
client/status/apemanager.go
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
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"
|
||||||
|
|
||||||
"github.com/nspcc-dev/neofs-api-go/v2/status"
|
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ServerInternal describes failure statuses related to internal server errors.
|
// 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,3 +123,118 @@ 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"
|
||||||
|
|
||||||
"github.com/nspcc-dev/neofs-api-go/v2/status"
|
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status"
|
||||||
apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
|
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -50,3 +50,81 @@ 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 (
|
||||||
"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/status"
|
"git.frostfs.info/TrueCloudLab/frostfs-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,10 +11,17 @@ type ContainerNotFound struct {
|
||||||
v2 status.Status
|
v2 status.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x ContainerNotFound) Error() string {
|
const defaultContainerNotFoundMsg = "container not found"
|
||||||
|
|
||||||
|
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),
|
||||||
x.v2.Message(),
|
msg,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,11 +33,49 @@ 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("container not found")
|
x.v2.SetMessage(defaultContainerNotFoundMsg)
|
||||||
|
return &x.v2
|
||||||
|
}
|
||||||
|
|
||||||
|
// EACLNotFound describes status of the failure because of the missing eACL
|
||||||
|
// table.
|
||||||
|
// Instances provide Status and StatusV2 interfaces.
|
||||||
|
type EACLNotFound struct {
|
||||||
|
v2 status.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultEACLNotFoundMsg = "eACL not found"
|
||||||
|
|
||||||
|
func (x *EACLNotFound) Error() string {
|
||||||
|
msg := x.v2.Message()
|
||||||
|
if msg == "" {
|
||||||
|
msg = defaultEACLNotFoundMsg
|
||||||
|
}
|
||||||
|
|
||||||
|
return errMessageStatusV2(
|
||||||
|
globalizeCodeV2(container.StatusEACLNotFound, container.GlobalizeFail),
|
||||||
|
msg,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// implements local interface defined in FromStatusV2 func.
|
||||||
|
func (x *EACLNotFound) fromStatusV2(st *status.Status) {
|
||||||
|
x.v2 = *st
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToStatusV2 implements StatusV2 interface method.
|
||||||
|
// If the value was returned by FromStatusV2, returns the source message.
|
||||||
|
// Otherwise, returns message with
|
||||||
|
// - code: EACL_NOT_FOUND;
|
||||||
|
// - string message: "eACL not found";
|
||||||
|
// - details: empty.
|
||||||
|
func (x EACLNotFound) ToStatusV2() *status.Status {
|
||||||
|
x.v2.SetCode(globalizeCodeV2(container.StatusEACLNotFound, container.GlobalizeFail))
|
||||||
|
x.v2.SetMessage(defaultEACLNotFoundMsg)
|
||||||
return &x.v2
|
return &x.v2
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package apistatus
|
package apistatus
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"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/status"
|
"git.frostfs.info/TrueCloudLab/frostfs-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,10 +11,17 @@ type ObjectLocked struct {
|
||||||
v2 status.Status
|
v2 status.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x ObjectLocked) Error() string {
|
const defaultObjectLockedMsg = "object is locked"
|
||||||
|
|
||||||
|
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),
|
||||||
x.v2.Message(),
|
msg,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,12 +33,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("object is locked")
|
x.v2.SetMessage(defaultObjectLockedMsg)
|
||||||
return &x.v2
|
return &x.v2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,10 +48,17 @@ type LockNonRegularObject struct {
|
||||||
v2 status.Status
|
v2 status.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x LockNonRegularObject) Error() string {
|
const defaultLockNonRegularObjectMsg = "locking non-regular object is forbidden"
|
||||||
|
|
||||||
|
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),
|
||||||
x.v2.Message(),
|
msg,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,12 +70,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("locking non-regular object is forbidden")
|
x.v2.SetMessage(defaultLockNonRegularObjectMsg)
|
||||||
return &x.v2
|
return &x.v2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,10 +85,17 @@ type ObjectAccessDenied struct {
|
||||||
v2 status.Status
|
v2 status.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x ObjectAccessDenied) Error() string {
|
const defaultObjectAccessDeniedMsg = "access to object operation denied"
|
||||||
|
|
||||||
|
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),
|
||||||
x.v2.Message(),
|
msg,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,12 +107,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("access to object operation denied")
|
x.v2.SetMessage(defaultObjectAccessDeniedMsg)
|
||||||
return &x.v2
|
return &x.v2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,10 +133,17 @@ type ObjectNotFound struct {
|
||||||
v2 status.Status
|
v2 status.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x ObjectNotFound) Error() string {
|
const defaultObjectNotFoundMsg = "object not found"
|
||||||
|
|
||||||
|
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),
|
||||||
x.v2.Message(),
|
msg,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,12 +155,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("object not found")
|
x.v2.SetMessage(defaultObjectNotFoundMsg)
|
||||||
return &x.v2
|
return &x.v2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,10 +170,17 @@ type ObjectAlreadyRemoved struct {
|
||||||
v2 status.Status
|
v2 status.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x ObjectAlreadyRemoved) Error() string {
|
const defaultObjectAlreadyRemovedMsg = "object already removed"
|
||||||
|
|
||||||
|
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),
|
||||||
x.v2.Message(),
|
msg,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,11 +192,49 @@ 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("object already removed")
|
x.v2.SetMessage(defaultObjectAlreadyRemovedMsg)
|
||||||
|
return &x.v2
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObjectOutOfRange describes status of the failure because of the incorrect
|
||||||
|
// provided object ranges.
|
||||||
|
// Instances provide Status and StatusV2 interfaces.
|
||||||
|
type ObjectOutOfRange struct {
|
||||||
|
v2 status.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultObjectOutOfRangeMsg = "out of range"
|
||||||
|
|
||||||
|
func (x *ObjectOutOfRange) Error() string {
|
||||||
|
msg := x.v2.Message()
|
||||||
|
if msg == "" {
|
||||||
|
msg = defaultObjectOutOfRangeMsg
|
||||||
|
}
|
||||||
|
|
||||||
|
return errMessageStatusV2(
|
||||||
|
globalizeCodeV2(object.StatusOutOfRange, object.GlobalizeFail),
|
||||||
|
msg,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// implements local interface defined in FromStatusV2 func.
|
||||||
|
func (x *ObjectOutOfRange) fromStatusV2(st *status.Status) {
|
||||||
|
x.v2 = *st
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToStatusV2 implements StatusV2 interface method.
|
||||||
|
// If the value was returned by FromStatusV2, returns the source message.
|
||||||
|
// Otherwise, returns message with
|
||||||
|
// - code: OUT_OF_RANGE;
|
||||||
|
// - string message: "out of range";
|
||||||
|
// - details: empty.
|
||||||
|
func (x ObjectOutOfRange) ToStatusV2() *status.Status {
|
||||||
|
x.v2.SetCode(globalizeCodeV2(object.StatusOutOfRange, object.GlobalizeFail))
|
||||||
|
x.v2.SetMessage(defaultObjectOutOfRangeMsg)
|
||||||
return &x.v2
|
return &x.v2
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ package apistatus_test
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
|
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package apistatus
|
package apistatus
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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,10 +11,17 @@ type SessionTokenNotFound struct {
|
||||||
v2 status.Status
|
v2 status.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x SessionTokenNotFound) Error() string {
|
const defaultSessionTokenNotFoundMsg = "session token not found"
|
||||||
|
|
||||||
|
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),
|
||||||
x.v2.Message(),
|
msg,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,12 +33,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("session token not found")
|
x.v2.SetMessage(defaultSessionTokenNotFoundMsg)
|
||||||
return &x.v2
|
return &x.v2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,10 +48,17 @@ type SessionTokenExpired struct {
|
||||||
v2 status.Status
|
v2 status.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x SessionTokenExpired) Error() string {
|
const defaultSessionTokenExpiredMsg = "expired session token"
|
||||||
|
|
||||||
|
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),
|
||||||
x.v2.Message(),
|
msg,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,11 +70,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("expired session token")
|
x.v2.SetMessage(defaultSessionTokenExpiredMsg)
|
||||||
return &x.v2
|
return &x.v2
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
package apistatus
|
package apistatus
|
||||||
|
|
||||||
// Status defines a variety of NeoFS API status returns.
|
// Status defines a variety of FrostFS API status returns.
|
||||||
//
|
//
|
||||||
// All statuses are split into two disjoint subsets: successful and failed, and:
|
// 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 NeoFS API V2 protocol, see StatusV2 interface and FromStatusV2 and ToStatusV2 functions.
|
// To transport statuses using the FrostFS API V2 protocol, see StatusV2 interface and FromStatusV2 and ToStatusV2 functions.
|
||||||
type Status interface{}
|
type Status any
|
||||||
|
|
||||||
// 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 "github.com/nspcc-dev/neofs-sdk-go/client/status"
|
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package apistatus
|
package apistatus
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/nspcc-dev/neofs-api-go/v2/status"
|
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SuccessDefaultV2 represents Status instance of default success. Implements StatusV2.
|
// 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 (
|
||||||
"github.com/nspcc-dev/neofs-api-go/v2/status"
|
"git.frostfs.info/TrueCloudLab/frostfs-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,19 +3,20 @@ package apistatus
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/nspcc-dev/neofs-api-go/v2/container"
|
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/apemanager"
|
||||||
"github.com/nspcc-dev/neofs-api-go/v2/object"
|
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
|
||||||
"github.com/nspcc-dev/neofs-api-go/v2/session"
|
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
|
||||||
"github.com/nspcc-dev/neofs-api-go/v2/status"
|
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
// StatusV2 defines a variety of Status instances compatible with NeoFS API V2 protocol.
|
// StatusV2 defines a variety of Status instances compatible with FrostFS API V2 protocol.
|
||||||
//
|
//
|
||||||
// Note: it is not recommended to use this type directly, it is intended for documentation of the library functionality.
|
// 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 github.com/nspcc-dev/neofs-api-go/v2/status.Status message structure.
|
// ToStatusV2 returns the status as git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status.Status message structure.
|
||||||
ToStatusV2() *status.Status
|
ToStatusV2() *status.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,15 +29,16 @@ 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)
|
||||||
|
@ -55,6 +57,10 @@ 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 {
|
||||||
|
@ -68,12 +74,16 @@ 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
|
||||||
|
@ -83,6 +93,12 @@ 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 {
|
||||||
|
@ -97,7 +113,8 @@ 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, failures - with status.Internal.
|
// Otherwise, successes are converted with status.OK code w/o details and message,
|
||||||
|
// failures - with status.Internal and error text message w/o details.
|
||||||
func ToStatusV2(st Status) *status.Status {
|
func ToStatusV2(st Status) *status.Status {
|
||||||
if v, ok := st.(StatusV2); ok {
|
if v, ok := st.(StatusV2); ok {
|
||||||
return v.ToStatusV2()
|
return v.ToStatusV2()
|
||||||
|
@ -107,10 +124,13 @@ func ToStatusV2(st Status) *status.Status {
|
||||||
return newStatusV2WithLocalCode(status.OK, status.GlobalizeSuccess)
|
return newStatusV2WithLocalCode(status.OK, status.GlobalizeSuccess)
|
||||||
}
|
}
|
||||||
|
|
||||||
return newStatusV2WithLocalCode(status.Internal, status.GlobalizeCommonFail)
|
internalErrorStatus := newStatusV2WithLocalCode(status.Internal, status.GlobalizeCommonFail)
|
||||||
|
internalErrorStatus.SetMessage(st.(error).Error()) // type cast never panics because IsSuccessful() checks cast
|
||||||
|
|
||||||
|
return internalErrorStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
func errMessageStatusV2(code interface{}, msg string) string {
|
func errMessageStatusV2(code any, 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 "github.com/nspcc-dev/neofs-sdk-go/client/status"
|
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -12,12 +12,14 @@ func TestToStatusV2(t *testing.T) {
|
||||||
type statusConstructor func() apistatus.Status
|
type statusConstructor func() apistatus.Status
|
||||||
|
|
||||||
for _, testItem := range [...]struct {
|
for _, testItem := range [...]struct {
|
||||||
status interface{} // Status or statusConstructor
|
status any // 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,
|
||||||
|
@ -93,12 +95,24 @@ 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)
|
||||||
|
@ -111,6 +125,18 @@ 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
|
||||||
|
|
||||||
|
@ -124,6 +150,9 @@ 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 {
|
||||||
|
@ -142,12 +171,14 @@ func TestFromStatusV2(t *testing.T) {
|
||||||
type statusConstructor func() apistatus.Status
|
type statusConstructor func() apistatus.Status
|
||||||
|
|
||||||
for _, testItem := range [...]struct {
|
for _, testItem := range [...]struct {
|
||||||
status interface{} // Status or statusConstructor
|
status any // 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,
|
||||||
|
@ -223,12 +254,24 @@ 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)
|
||||||
|
@ -241,6 +284,18 @@ 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
|
||||||
|
|
||||||
|
@ -254,6 +309,9 @@ 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 {
|
||||||
|
|
83
container/acl/acl.go
Normal file
83
container/acl/acl.go
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
283
container/acl/acl_basic.go
Normal file
283
container/acl/acl_basic.go
Normal file
|
@ -0,0 +1,283 @@
|
||||||
|
package acl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Basic represents basic part of the FrostFS container's ACL. It includes
|
||||||
|
// common (pretty simple) access rules for operations inside the container.
|
||||||
|
// See FrostFS Specification for details.
|
||||||
|
//
|
||||||
|
// One can find some similarities with the traditional Unix permission, such as
|
||||||
|
//
|
||||||
|
// division into scopes: user, group, others
|
||||||
|
// op-permissions: read, write, etc.
|
||||||
|
// sticky bit
|
||||||
|
//
|
||||||
|
// However, these similarities should only be used for better understanding,
|
||||||
|
// in general these mechanisms are different.
|
||||||
|
//
|
||||||
|
// Instances can be created using built-in var declaration, but look carefully
|
||||||
|
// at the default values, and how individual permissions are regulated.
|
||||||
|
// Some frequently used values are presented in exported variables.
|
||||||
|
//
|
||||||
|
// Basic instances are comparable: values can be compared directly using
|
||||||
|
// == operator.
|
||||||
|
//
|
||||||
|
// Note that type conversion from- and to numerical types is not recommended,
|
||||||
|
// use corresponding constants and/or methods instead.
|
||||||
|
type Basic uint32
|
||||||
|
|
||||||
|
// FromBits decodes Basic from the numerical representation.
|
||||||
|
//
|
||||||
|
// See also Bits.
|
||||||
|
func (x *Basic) FromBits(bits uint32) {
|
||||||
|
*x = Basic(bits)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bits returns numerical encoding of Basic.
|
||||||
|
//
|
||||||
|
// See also FromBits.
|
||||||
|
func (x Basic) Bits() uint32 {
|
||||||
|
return uint32(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// common bit sections.
|
||||||
|
const (
|
||||||
|
opAmount = 7
|
||||||
|
bitsPerOp = 4
|
||||||
|
|
||||||
|
bitPosFinal = opAmount * bitsPerOp
|
||||||
|
bitPosSticky = bitPosFinal + 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// per-op bit order.
|
||||||
|
const (
|
||||||
|
opBitPosBearer uint8 = iota
|
||||||
|
opBitPosOthers
|
||||||
|
opBitPosContainer
|
||||||
|
opBitPosOwner
|
||||||
|
)
|
||||||
|
|
||||||
|
// DisableExtension makes Basic FINAL. FINAL indicates the ACL non-extendability
|
||||||
|
// in the related container.
|
||||||
|
//
|
||||||
|
// See also Extendable.
|
||||||
|
func (x *Basic) DisableExtension() {
|
||||||
|
setBit((*uint32)(x), bitPosFinal)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extendable checks if Basic is NOT made FINAL using DisableExtension.
|
||||||
|
//
|
||||||
|
// Zero Basic is extendable.
|
||||||
|
func (x Basic) Extendable() bool {
|
||||||
|
return !isBitSet(uint32(x), bitPosFinal)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeSticky makes Basic STICKY. STICKY indicates that only the owner of any
|
||||||
|
// particular object is allowed to operate on it.
|
||||||
|
//
|
||||||
|
// See also Sticky.
|
||||||
|
func (x *Basic) MakeSticky() {
|
||||||
|
setBit((*uint32)(x), bitPosSticky)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sticky checks if Basic is made STICKY using MakeSticky.
|
||||||
|
//
|
||||||
|
// Zero Basic is NOT STICKY.
|
||||||
|
func (x Basic) Sticky() bool {
|
||||||
|
return isBitSet(uint32(x), bitPosSticky)
|
||||||
|
}
|
||||||
|
|
||||||
|
// checks if op is used by the storage nodes within replication mechanism.
|
||||||
|
func isReplicationOp(op Op) bool {
|
||||||
|
switch op {
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
case
|
||||||
|
OpObjectGet,
|
||||||
|
OpObjectHead,
|
||||||
|
OpObjectPut,
|
||||||
|
OpObjectSearch,
|
||||||
|
OpObjectHash:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllowOp allows the parties with the given role to the given operation.
|
||||||
|
// Op MUST be one of the Op enumeration. Role MUST be one of:
|
||||||
|
//
|
||||||
|
// RoleOwner
|
||||||
|
// RoleContainer
|
||||||
|
// RoleOthers
|
||||||
|
//
|
||||||
|
// and if role is RoleContainer, op MUST NOT be:
|
||||||
|
//
|
||||||
|
// OpObjectGet
|
||||||
|
// OpObjectHead
|
||||||
|
// OpObjectPut
|
||||||
|
// OpObjectSearch
|
||||||
|
// OpObjectHash
|
||||||
|
//
|
||||||
|
// See also IsOpAllowed.
|
||||||
|
func (x *Basic) AllowOp(op Op, role Role) {
|
||||||
|
var bitPos uint8
|
||||||
|
|
||||||
|
switch role {
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("unable to set rules for unsupported role %v", role))
|
||||||
|
case RoleInnerRing:
|
||||||
|
panic("basic ACL MUST NOT be modified for Inner Ring")
|
||||||
|
case RoleOwner:
|
||||||
|
bitPos = opBitPosOwner
|
||||||
|
case RoleContainer:
|
||||||
|
if isReplicationOp(op) {
|
||||||
|
panic("basic ACL for container replication ops MUST NOT be modified")
|
||||||
|
}
|
||||||
|
|
||||||
|
bitPos = opBitPosContainer
|
||||||
|
case RoleOthers:
|
||||||
|
bitPos = opBitPosOthers
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpBit((*uint32)(x), op, bitPos)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsOpAllowed checks if parties with the given role are allowed to the given op
|
||||||
|
// according to the Basic rules. Op MUST be one of the Op enumeration.
|
||||||
|
// Role MUST be one of the Role enumeration.
|
||||||
|
//
|
||||||
|
// Members with RoleContainer role have exclusive default access to the
|
||||||
|
// operations of the data replication mechanism:
|
||||||
|
//
|
||||||
|
// OpObjectGet
|
||||||
|
// OpObjectHead
|
||||||
|
// OpObjectPut
|
||||||
|
// OpObjectSearch
|
||||||
|
// OpObjectHash
|
||||||
|
//
|
||||||
|
// RoleInnerRing members are allowed to data audit ops only:
|
||||||
|
//
|
||||||
|
// OpObjectGet
|
||||||
|
// OpObjectHead
|
||||||
|
// OpObjectHash
|
||||||
|
// OpObjectSearch
|
||||||
|
//
|
||||||
|
// Zero Basic prevents any role from accessing any operation in the absence
|
||||||
|
// of default rights.
|
||||||
|
//
|
||||||
|
// See also AllowOp.
|
||||||
|
func (x Basic) IsOpAllowed(op Op, role Role) bool {
|
||||||
|
var bitPos uint8
|
||||||
|
|
||||||
|
switch role {
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("role is unsupported %v", role))
|
||||||
|
case RoleInnerRing:
|
||||||
|
switch op {
|
||||||
|
case
|
||||||
|
OpObjectGet,
|
||||||
|
OpObjectHead,
|
||||||
|
OpObjectHash,
|
||||||
|
OpObjectSearch:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case RoleOwner:
|
||||||
|
bitPos = opBitPosOwner
|
||||||
|
case RoleContainer:
|
||||||
|
if isReplicationOp(op) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
bitPos = opBitPosContainer
|
||||||
|
case RoleOthers:
|
||||||
|
bitPos = opBitPosOthers
|
||||||
|
}
|
||||||
|
|
||||||
|
return isOpBitSet(uint32(x), op, bitPos)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllowBearerRules allows bearer to provide extended ACL rules for the given
|
||||||
|
// operation. Bearer rules doesn't depend on container ACL extensibility.
|
||||||
|
//
|
||||||
|
// See also AllowedBearerRules.
|
||||||
|
func (x *Basic) AllowBearerRules(op Op) {
|
||||||
|
setOpBit((*uint32)(x), op, opBitPosBearer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllowedBearerRules checks if bearer rules are allowed using AllowBearerRules.
|
||||||
|
// Op MUST be one of the Op enumeration.
|
||||||
|
//
|
||||||
|
// Zero Basic disallows bearer rules for any op.
|
||||||
|
func (x Basic) AllowedBearerRules(op Op) bool {
|
||||||
|
return isOpBitSet(uint32(x), op, opBitPosBearer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeToString encodes Basic into hexadecimal string.
|
||||||
|
//
|
||||||
|
// See also DecodeString.
|
||||||
|
func (x Basic) EncodeToString() string {
|
||||||
|
return strconv.FormatUint(uint64(x), 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Names of the frequently used Basic values.
|
||||||
|
const (
|
||||||
|
NamePrivate = "private"
|
||||||
|
NamePrivateExtended = "eacl-private"
|
||||||
|
NamePublicRO = "public-read"
|
||||||
|
NamePublicROExtended = "eacl-public-read"
|
||||||
|
NamePublicRW = "public-read-write"
|
||||||
|
NamePublicRWExtended = "eacl-public-read-write"
|
||||||
|
NamePublicAppend = "public-append"
|
||||||
|
NamePublicAppendExtended = "eacl-public-append"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Frequently used Basic values. Bitmasks are taken from the FrostFS Specification.
|
||||||
|
const (
|
||||||
|
Private = Basic(0x1C8C8CCC) // private
|
||||||
|
PrivateExtended = Basic(0x0C8C8CCC) // eacl-private
|
||||||
|
PublicRO = Basic(0x1FBF8CFF) // public-read
|
||||||
|
PublicROExtended = Basic(0x0FBF8CFF) // eacl-public-read
|
||||||
|
PublicRW = Basic(0x1FBFBFFF) // public-read-write
|
||||||
|
PublicRWExtended = Basic(0x0FBFBFFF) // eacl-public-read-write
|
||||||
|
PublicAppend = Basic(0x1FBF9FFF) // public-append
|
||||||
|
PublicAppendExtended = Basic(0x0FBF9FFF) // eacl-public-append
|
||||||
|
)
|
||||||
|
|
||||||
|
// DecodeString decodes string calculated using EncodeToString. Also supports
|
||||||
|
// human-readable names (Name* constants).
|
||||||
|
func (x *Basic) DecodeString(s string) (e error) {
|
||||||
|
switch s {
|
||||||
|
case NamePrivate:
|
||||||
|
*x = Private
|
||||||
|
case NamePrivateExtended:
|
||||||
|
*x = PrivateExtended
|
||||||
|
case NamePublicRO:
|
||||||
|
*x = PublicRO
|
||||||
|
case NamePublicROExtended:
|
||||||
|
*x = PublicROExtended
|
||||||
|
case NamePublicRW:
|
||||||
|
*x = PublicRW
|
||||||
|
case NamePublicRWExtended:
|
||||||
|
*x = PublicRWExtended
|
||||||
|
case NamePublicAppend:
|
||||||
|
*x = PublicAppend
|
||||||
|
case NamePublicAppendExtended:
|
||||||
|
*x = PublicAppendExtended
|
||||||
|
default:
|
||||||
|
s = strings.TrimPrefix(strings.ToLower(s), "0x")
|
||||||
|
|
||||||
|
v, err := strconv.ParseUint(s, 16, 32)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse hex: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*x = Basic(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
384
container/acl/acl_basic_test.go
Normal file
384
container/acl/acl_basic_test.go
Normal file
|
@ -0,0 +1,384 @@
|
||||||
|
package acl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBasic_DisableExtension(t *testing.T) {
|
||||||
|
var val, val2 Basic
|
||||||
|
|
||||||
|
require.True(t, val.Extendable())
|
||||||
|
val2.FromBits(val.Bits())
|
||||||
|
require.True(t, val2.Extendable())
|
||||||
|
|
||||||
|
val.DisableExtension()
|
||||||
|
|
||||||
|
require.False(t, val.Extendable())
|
||||||
|
val2.FromBits(val.Bits())
|
||||||
|
require.False(t, val2.Extendable())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBasic_MakeSticky(t *testing.T) {
|
||||||
|
var val, val2 Basic
|
||||||
|
|
||||||
|
require.False(t, val.Sticky())
|
||||||
|
val2.FromBits(val.Bits())
|
||||||
|
require.False(t, val2.Sticky())
|
||||||
|
|
||||||
|
val.MakeSticky()
|
||||||
|
|
||||||
|
require.True(t, val.Sticky())
|
||||||
|
val2.FromBits(val.Bits())
|
||||||
|
require.True(t, val2.Sticky())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBasic_AllowBearerRules(t *testing.T) {
|
||||||
|
var val Basic
|
||||||
|
|
||||||
|
require.Panics(t, func() { val.AllowBearerRules(opZero) })
|
||||||
|
require.Panics(t, func() { val.AllowBearerRules(opLast) })
|
||||||
|
|
||||||
|
require.Panics(t, func() { val.AllowedBearerRules(opZero) })
|
||||||
|
require.Panics(t, func() { val.AllowedBearerRules(opLast) })
|
||||||
|
|
||||||
|
for op := opZero + 1; op < opLast; op++ {
|
||||||
|
val := val
|
||||||
|
|
||||||
|
require.False(t, val.AllowedBearerRules(op))
|
||||||
|
|
||||||
|
val.AllowBearerRules(op)
|
||||||
|
|
||||||
|
for j := opZero + 1; j < opLast; j++ {
|
||||||
|
if j == op {
|
||||||
|
require.True(t, val.AllowedBearerRules(j), op)
|
||||||
|
} else {
|
||||||
|
require.False(t, val.AllowedBearerRules(j), op)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBasic_AllowOp(t *testing.T) {
|
||||||
|
var val, val2 Basic
|
||||||
|
|
||||||
|
require.Panics(t, func() { val.IsOpAllowed(opZero, roleZero+1) })
|
||||||
|
require.Panics(t, func() { val.IsOpAllowed(opLast, roleZero+1) })
|
||||||
|
require.Panics(t, func() { val.IsOpAllowed(opZero+1, roleZero) })
|
||||||
|
require.Panics(t, func() { val.IsOpAllowed(opZero+1, roleLast) })
|
||||||
|
|
||||||
|
for op := opZero + 1; op < opLast; op++ {
|
||||||
|
require.Panics(t, func() { val.AllowOp(op, RoleInnerRing) })
|
||||||
|
|
||||||
|
if isReplicationOp(op) {
|
||||||
|
require.Panics(t, func() { val.AllowOp(op, RoleContainer) })
|
||||||
|
require.True(t, val.IsOpAllowed(op, RoleContainer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
require.True(t, val.IsOpAllowed(OpObjectGet, RoleInnerRing))
|
||||||
|
require.True(t, val.IsOpAllowed(OpObjectHead, RoleInnerRing))
|
||||||
|
require.True(t, val.IsOpAllowed(OpObjectSearch, RoleInnerRing))
|
||||||
|
require.True(t, val.IsOpAllowed(OpObjectHash, RoleInnerRing))
|
||||||
|
|
||||||
|
const op = opZero + 1
|
||||||
|
const role = RoleOthers
|
||||||
|
|
||||||
|
require.False(t, val.IsOpAllowed(op, role))
|
||||||
|
val2.FromBits(val.Bits())
|
||||||
|
require.False(t, val2.IsOpAllowed(op, role))
|
||||||
|
|
||||||
|
val.AllowOp(op, role)
|
||||||
|
|
||||||
|
require.True(t, val.IsOpAllowed(op, role))
|
||||||
|
val2.FromBits(val.Bits())
|
||||||
|
require.True(t, val2.IsOpAllowed(op, role))
|
||||||
|
}
|
||||||
|
|
||||||
|
type opsExpected struct {
|
||||||
|
owner, container, innerRing, others, bearer bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func testOp(t *testing.T, v Basic, op Op, exp opsExpected) {
|
||||||
|
require.Equal(t, exp.owner, v.IsOpAllowed(op, RoleOwner), op)
|
||||||
|
require.Equal(t, exp.container, v.IsOpAllowed(op, RoleContainer), op)
|
||||||
|
require.Equal(t, exp.innerRing, v.IsOpAllowed(op, RoleInnerRing), op)
|
||||||
|
require.Equal(t, exp.others, v.IsOpAllowed(op, RoleOthers), op)
|
||||||
|
require.Equal(t, exp.bearer, v.AllowedBearerRules(op), op)
|
||||||
|
}
|
||||||
|
|
||||||
|
type expected struct {
|
||||||
|
extendable, sticky bool
|
||||||
|
|
||||||
|
mOps map[Op]opsExpected
|
||||||
|
}
|
||||||
|
|
||||||
|
func testBasicPredefined(t *testing.T, val Basic, name string, exp expected) {
|
||||||
|
require.Equal(t, exp.sticky, val.Sticky())
|
||||||
|
require.Equal(t, exp.extendable, val.Extendable())
|
||||||
|
|
||||||
|
for op, exp := range exp.mOps {
|
||||||
|
testOp(t, val, op, exp)
|
||||||
|
}
|
||||||
|
|
||||||
|
s := val.EncodeToString()
|
||||||
|
|
||||||
|
var val2 Basic
|
||||||
|
|
||||||
|
require.NoError(t, val2.DecodeString(s))
|
||||||
|
require.Equal(t, val, val2)
|
||||||
|
|
||||||
|
require.NoError(t, val2.DecodeString(name))
|
||||||
|
require.Equal(t, val, val2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBasicPredefined(t *testing.T) {
|
||||||
|
t.Run("private", func(t *testing.T) {
|
||||||
|
exp := expected{
|
||||||
|
extendable: false,
|
||||||
|
sticky: false,
|
||||||
|
mOps: map[Op]opsExpected{
|
||||||
|
OpObjectHash: {
|
||||||
|
owner: true,
|
||||||
|
container: true,
|
||||||
|
innerRing: true,
|
||||||
|
others: false,
|
||||||
|
bearer: false,
|
||||||
|
},
|
||||||
|
OpObjectRange: {
|
||||||
|
owner: true,
|
||||||
|
container: false,
|
||||||
|
innerRing: false,
|
||||||
|
others: false,
|
||||||
|
bearer: false,
|
||||||
|
},
|
||||||
|
OpObjectSearch: {
|
||||||
|
owner: true,
|
||||||
|
container: true,
|
||||||
|
innerRing: true,
|
||||||
|
others: false,
|
||||||
|
bearer: false,
|
||||||
|
},
|
||||||
|
OpObjectDelete: {
|
||||||
|
owner: true,
|
||||||
|
container: false,
|
||||||
|
innerRing: false,
|
||||||
|
others: false,
|
||||||
|
bearer: false,
|
||||||
|
},
|
||||||
|
OpObjectPut: {
|
||||||
|
owner: true,
|
||||||
|
container: true,
|
||||||
|
innerRing: false,
|
||||||
|
others: false,
|
||||||
|
bearer: false,
|
||||||
|
},
|
||||||
|
OpObjectHead: {
|
||||||
|
owner: true,
|
||||||
|
container: true,
|
||||||
|
innerRing: true,
|
||||||
|
others: false,
|
||||||
|
bearer: false,
|
||||||
|
},
|
||||||
|
OpObjectGet: {
|
||||||
|
owner: true,
|
||||||
|
container: true,
|
||||||
|
innerRing: true,
|
||||||
|
others: false,
|
||||||
|
bearer: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testBasicPredefined(t, Private, NamePrivate, exp)
|
||||||
|
exp.extendable = true
|
||||||
|
testBasicPredefined(t, PrivateExtended, NamePrivateExtended, exp)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("public-read", func(t *testing.T) {
|
||||||
|
exp := expected{
|
||||||
|
extendable: false,
|
||||||
|
sticky: false,
|
||||||
|
mOps: map[Op]opsExpected{
|
||||||
|
OpObjectHash: {
|
||||||
|
owner: true,
|
||||||
|
container: true,
|
||||||
|
innerRing: true,
|
||||||
|
others: true,
|
||||||
|
bearer: true,
|
||||||
|
},
|
||||||
|
OpObjectRange: {
|
||||||
|
owner: true,
|
||||||
|
container: false,
|
||||||
|
innerRing: false,
|
||||||
|
others: true,
|
||||||
|
bearer: true,
|
||||||
|
},
|
||||||
|
OpObjectSearch: {
|
||||||
|
owner: true,
|
||||||
|
container: true,
|
||||||
|
innerRing: true,
|
||||||
|
others: true,
|
||||||
|
bearer: true,
|
||||||
|
},
|
||||||
|
OpObjectDelete: {
|
||||||
|
owner: true,
|
||||||
|
container: false,
|
||||||
|
innerRing: false,
|
||||||
|
others: false,
|
||||||
|
bearer: false,
|
||||||
|
},
|
||||||
|
OpObjectPut: {
|
||||||
|
owner: true,
|
||||||
|
container: true,
|
||||||
|
innerRing: false,
|
||||||
|
others: false,
|
||||||
|
bearer: false,
|
||||||
|
},
|
||||||
|
OpObjectHead: {
|
||||||
|
owner: true,
|
||||||
|
container: true,
|
||||||
|
innerRing: true,
|
||||||
|
others: true,
|
||||||
|
bearer: true,
|
||||||
|
},
|
||||||
|
OpObjectGet: {
|
||||||
|
owner: true,
|
||||||
|
container: true,
|
||||||
|
innerRing: true,
|
||||||
|
others: true,
|
||||||
|
bearer: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testBasicPredefined(t, PublicRO, NamePublicRO, exp)
|
||||||
|
exp.extendable = true
|
||||||
|
testBasicPredefined(t, PublicROExtended, NamePublicROExtended, exp)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("public-read-write", func(t *testing.T) {
|
||||||
|
exp := expected{
|
||||||
|
extendable: false,
|
||||||
|
sticky: false,
|
||||||
|
mOps: map[Op]opsExpected{
|
||||||
|
OpObjectHash: {
|
||||||
|
owner: true,
|
||||||
|
container: true,
|
||||||
|
innerRing: true,
|
||||||
|
others: true,
|
||||||
|
bearer: true,
|
||||||
|
},
|
||||||
|
OpObjectRange: {
|
||||||
|
owner: true,
|
||||||
|
container: false,
|
||||||
|
innerRing: false,
|
||||||
|
others: true,
|
||||||
|
bearer: true,
|
||||||
|
},
|
||||||
|
OpObjectSearch: {
|
||||||
|
owner: true,
|
||||||
|
container: true,
|
||||||
|
innerRing: true,
|
||||||
|
others: true,
|
||||||
|
bearer: true,
|
||||||
|
},
|
||||||
|
OpObjectDelete: {
|
||||||
|
owner: true,
|
||||||
|
container: false,
|
||||||
|
innerRing: false,
|
||||||
|
others: true,
|
||||||
|
bearer: true,
|
||||||
|
},
|
||||||
|
OpObjectPut: {
|
||||||
|
owner: true,
|
||||||
|
container: true,
|
||||||
|
innerRing: false,
|
||||||
|
others: true,
|
||||||
|
bearer: true,
|
||||||
|
},
|
||||||
|
OpObjectHead: {
|
||||||
|
owner: true,
|
||||||
|
container: true,
|
||||||
|
innerRing: true,
|
||||||
|
others: true,
|
||||||
|
bearer: true,
|
||||||
|
},
|
||||||
|
OpObjectGet: {
|
||||||
|
owner: true,
|
||||||
|
container: true,
|
||||||
|
innerRing: true,
|
||||||
|
others: true,
|
||||||
|
bearer: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testBasicPredefined(t, PublicRW, NamePublicRW, exp)
|
||||||
|
exp.extendable = true
|
||||||
|
testBasicPredefined(t, PublicRWExtended, NamePublicRWExtended, exp)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("public-append", func(t *testing.T) {
|
||||||
|
exp := expected{
|
||||||
|
extendable: false,
|
||||||
|
sticky: false,
|
||||||
|
mOps: map[Op]opsExpected{
|
||||||
|
OpObjectHash: {
|
||||||
|
owner: true,
|
||||||
|
container: true,
|
||||||
|
innerRing: true,
|
||||||
|
others: true,
|
||||||
|
bearer: true,
|
||||||
|
},
|
||||||
|
OpObjectRange: {
|
||||||
|
owner: true,
|
||||||
|
container: false,
|
||||||
|
innerRing: false,
|
||||||
|
others: true,
|
||||||
|
bearer: true,
|
||||||
|
},
|
||||||
|
OpObjectSearch: {
|
||||||
|
owner: true,
|
||||||
|
container: true,
|
||||||
|
innerRing: true,
|
||||||
|
others: true,
|
||||||
|
bearer: true,
|
||||||
|
},
|
||||||
|
OpObjectDelete: {
|
||||||
|
owner: true,
|
||||||
|
container: false,
|
||||||
|
innerRing: false,
|
||||||
|
others: false,
|
||||||
|
bearer: true,
|
||||||
|
},
|
||||||
|
OpObjectPut: {
|
||||||
|
owner: true,
|
||||||
|
container: true,
|
||||||
|
innerRing: false,
|
||||||
|
others: true,
|
||||||
|
bearer: true,
|
||||||
|
},
|
||||||
|
OpObjectHead: {
|
||||||
|
owner: true,
|
||||||
|
container: true,
|
||||||
|
innerRing: true,
|
||||||
|
others: true,
|
||||||
|
bearer: true,
|
||||||
|
},
|
||||||
|
OpObjectGet: {
|
||||||
|
owner: true,
|
||||||
|
container: true,
|
||||||
|
innerRing: true,
|
||||||
|
others: true,
|
||||||
|
bearer: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testBasicPredefined(t, PublicAppend, NamePublicAppend, exp)
|
||||||
|
exp.extendable = true
|
||||||
|
testBasicPredefined(t, PublicAppendExtended, NamePublicAppendExtended, exp)
|
||||||
|
})
|
||||||
|
}
|
8
container/acl/doc.go
Normal file
8
container/acl/doc.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
/*
|
||||||
|
Package acl provides functionality to control access to data and operations on them in FrostFS containers.
|
||||||
|
|
||||||
|
Type Basic represents basic ACL of the FrostFS container which specifies the general order of data access.
|
||||||
|
Basic provides interface of rule composition. Package acl also exports some frequently used settings like
|
||||||
|
private or public.
|
||||||
|
*/
|
||||||
|
package acl
|
20
container/acl/init.go
Normal file
20
container/acl/init.go
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
package acl
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// left-to-right order of the object operations
|
||||||
|
orderedOps := [...]Op{
|
||||||
|
OpObjectGet,
|
||||||
|
OpObjectHead,
|
||||||
|
OpObjectPut,
|
||||||
|
OpObjectDelete,
|
||||||
|
OpObjectSearch,
|
||||||
|
OpObjectRange,
|
||||||
|
OpObjectHash,
|
||||||
|
}
|
||||||
|
|
||||||
|
mOrder = make(map[Op]uint8, len(orderedOps))
|
||||||
|
|
||||||
|
for i := range orderedOps {
|
||||||
|
mOrder[orderedOps[i]] = uint8(i)
|
||||||
|
}
|
||||||
|
}
|
37
container/acl/util.go
Normal file
37
container/acl/util.go
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
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)
|
||||||
|
}
|
130
container/acl/util_test.go
Normal file
130
container/acl/util_test.go
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,77 +0,0 @@
|
||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/nspcc-dev/neofs-api-go/v2/container"
|
|
||||||
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
|
|
||||||
)
|
|
||||||
|
|
||||||
// UsedSpaceAnnouncement is an announcement message used by storage nodes to
|
|
||||||
// estimate actual container sizes.
|
|
||||||
type UsedSpaceAnnouncement container.UsedSpaceAnnouncement
|
|
||||||
|
|
||||||
// NewAnnouncement initialize empty UsedSpaceAnnouncement message.
|
|
||||||
//
|
|
||||||
// Defaults:
|
|
||||||
// - epoch: 0;
|
|
||||||
// - usedSpace: 0;
|
|
||||||
// - cid: nil.
|
|
||||||
func NewAnnouncement() *UsedSpaceAnnouncement {
|
|
||||||
return NewAnnouncementFromV2(new(container.UsedSpaceAnnouncement))
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAnnouncementFromV2 wraps protocol dependent version of
|
|
||||||
// UsedSpaceAnnouncement message.
|
|
||||||
//
|
|
||||||
// Nil container.UsedSpaceAnnouncement converts to nil.
|
|
||||||
func NewAnnouncementFromV2(v *container.UsedSpaceAnnouncement) *UsedSpaceAnnouncement {
|
|
||||||
return (*UsedSpaceAnnouncement)(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Epoch of the announcement.
|
|
||||||
func (a *UsedSpaceAnnouncement) Epoch() uint64 {
|
|
||||||
return (*container.UsedSpaceAnnouncement)(a).GetEpoch()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetEpoch sets announcement epoch value.
|
|
||||||
func (a *UsedSpaceAnnouncement) SetEpoch(epoch uint64) {
|
|
||||||
(*container.UsedSpaceAnnouncement)(a).SetEpoch(epoch)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContainerID of the announcement.
|
|
||||||
func (a *UsedSpaceAnnouncement) ContainerID() *cid.ID {
|
|
||||||
return cid.NewFromV2(
|
|
||||||
(*container.UsedSpaceAnnouncement)(a).GetContainerID(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetContainerID sets announcement container value.
|
|
||||||
func (a *UsedSpaceAnnouncement) SetContainerID(cid *cid.ID) {
|
|
||||||
(*container.UsedSpaceAnnouncement)(a).SetContainerID(cid.ToV2())
|
|
||||||
}
|
|
||||||
|
|
||||||
// UsedSpace in container.
|
|
||||||
func (a *UsedSpaceAnnouncement) UsedSpace() uint64 {
|
|
||||||
return (*container.UsedSpaceAnnouncement)(a).GetUsedSpace()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetUsedSpace sets used space value by specified container.
|
|
||||||
func (a *UsedSpaceAnnouncement) SetUsedSpace(value uint64) {
|
|
||||||
(*container.UsedSpaceAnnouncement)(a).SetUsedSpace(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToV2 returns protocol dependent version of UsedSpaceAnnouncement message.
|
|
||||||
//
|
|
||||||
// Nil UsedSpaceAnnouncement converts to nil.
|
|
||||||
func (a *UsedSpaceAnnouncement) ToV2() *container.UsedSpaceAnnouncement {
|
|
||||||
return (*container.UsedSpaceAnnouncement)(a)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Marshal marshals UsedSpaceAnnouncement into a protobuf binary form.
|
|
||||||
func (a *UsedSpaceAnnouncement) Marshal() ([]byte, error) {
|
|
||||||
return a.ToV2().StableMarshal(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal unmarshals protobuf binary representation of UsedSpaceAnnouncement.
|
|
||||||
func (a *UsedSpaceAnnouncement) Unmarshal(data []byte) error {
|
|
||||||
return a.ToV2().Unmarshal(data)
|
|
||||||
}
|
|
|
@ -1,99 +0,0 @@
|
||||||
package container_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/sha256"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
containerv2 "github.com/nspcc-dev/neofs-api-go/v2/container"
|
|
||||||
"github.com/nspcc-dev/neofs-api-go/v2/refs"
|
|
||||||
"github.com/nspcc-dev/neofs-sdk-go/container"
|
|
||||||
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
|
|
||||||
cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test"
|
|
||||||
containertest "github.com/nspcc-dev/neofs-sdk-go/container/test"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestAnnouncement(t *testing.T) {
|
|
||||||
const epoch, usedSpace uint64 = 10, 100
|
|
||||||
|
|
||||||
cidValue := [sha256.Size]byte{1, 2, 3}
|
|
||||||
id := cidtest.IDWithChecksum(cidValue)
|
|
||||||
|
|
||||||
a := container.NewAnnouncement()
|
|
||||||
a.SetEpoch(epoch)
|
|
||||||
a.SetContainerID(id)
|
|
||||||
a.SetUsedSpace(usedSpace)
|
|
||||||
|
|
||||||
require.Equal(t, epoch, a.Epoch())
|
|
||||||
require.Equal(t, usedSpace, a.UsedSpace())
|
|
||||||
require.Equal(t, id, a.ContainerID())
|
|
||||||
|
|
||||||
t.Run("test v2", func(t *testing.T) {
|
|
||||||
const newEpoch, newUsedSpace uint64 = 20, 200
|
|
||||||
|
|
||||||
newCidValue := [32]byte{4, 5, 6}
|
|
||||||
newCID := new(refs.ContainerID)
|
|
||||||
newCID.SetValue(newCidValue[:])
|
|
||||||
|
|
||||||
v2 := a.ToV2()
|
|
||||||
require.Equal(t, usedSpace, v2.GetUsedSpace())
|
|
||||||
require.Equal(t, epoch, v2.GetEpoch())
|
|
||||||
require.Equal(t, cidValue[:], v2.GetContainerID().GetValue())
|
|
||||||
|
|
||||||
v2.SetEpoch(newEpoch)
|
|
||||||
v2.SetUsedSpace(newUsedSpace)
|
|
||||||
v2.SetContainerID(newCID)
|
|
||||||
|
|
||||||
newA := container.NewAnnouncementFromV2(v2)
|
|
||||||
|
|
||||||
require.Equal(t, newEpoch, newA.Epoch())
|
|
||||||
require.Equal(t, newUsedSpace, newA.UsedSpace())
|
|
||||||
require.Equal(t, cid.NewFromV2(newCID), newA.ContainerID())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUsedSpaceEncoding(t *testing.T) {
|
|
||||||
a := containertest.UsedSpaceAnnouncement()
|
|
||||||
|
|
||||||
t.Run("binary", func(t *testing.T) {
|
|
||||||
data, err := a.Marshal()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
a2 := container.NewAnnouncement()
|
|
||||||
require.NoError(t, a2.Unmarshal(data))
|
|
||||||
|
|
||||||
require.Equal(t, a, a2)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUsedSpaceAnnouncement_ToV2(t *testing.T) {
|
|
||||||
t.Run("nil", func(t *testing.T) {
|
|
||||||
var x *container.UsedSpaceAnnouncement
|
|
||||||
|
|
||||||
require.Nil(t, x.ToV2())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("default values", func(t *testing.T) {
|
|
||||||
announcement := container.NewAnnouncement()
|
|
||||||
|
|
||||||
// check initial values
|
|
||||||
require.Zero(t, announcement.Epoch())
|
|
||||||
require.Zero(t, announcement.UsedSpace())
|
|
||||||
require.Nil(t, announcement.ContainerID())
|
|
||||||
|
|
||||||
// convert to v2 message
|
|
||||||
announcementV2 := announcement.ToV2()
|
|
||||||
|
|
||||||
require.Zero(t, announcementV2.GetEpoch())
|
|
||||||
require.Zero(t, announcementV2.GetUsedSpace())
|
|
||||||
require.Nil(t, announcementV2.GetContainerID())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewAnnouncementFromV2(t *testing.T) {
|
|
||||||
t.Run("from nil", func(t *testing.T) {
|
|
||||||
var x *containerv2.UsedSpaceAnnouncement
|
|
||||||
|
|
||||||
require.Nil(t, container.NewAnnouncementFromV2(x))
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,139 +0,0 @@
|
||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/nspcc-dev/neofs-api-go/v2/container"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
Attribute container.Attribute
|
|
||||||
Attributes []Attribute
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewAttribute creates and initializes blank Attribute.
|
|
||||||
//
|
|
||||||
// Defaults:
|
|
||||||
// - key: "";
|
|
||||||
// - value: "".
|
|
||||||
func NewAttribute() *Attribute {
|
|
||||||
return NewAttributeFromV2(new(container.Attribute))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Attribute) SetKey(v string) {
|
|
||||||
(*container.Attribute)(a).SetKey(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Attribute) SetValue(v string) {
|
|
||||||
(*container.Attribute)(a).SetValue(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Attribute) Key() string {
|
|
||||||
return (*container.Attribute)(a).GetKey()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Attribute) Value() string {
|
|
||||||
return (*container.Attribute)(a).GetValue()
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAttributeFromV2 wraps protocol dependent version of
|
|
||||||
// Attribute message.
|
|
||||||
//
|
|
||||||
// Nil container.Attribute converts to nil.
|
|
||||||
func NewAttributeFromV2(v *container.Attribute) *Attribute {
|
|
||||||
return (*Attribute)(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToV2 converts Attribute to v2 Attribute message.
|
|
||||||
//
|
|
||||||
// Nil Attribute converts to nil.
|
|
||||||
func (a *Attribute) ToV2() *container.Attribute {
|
|
||||||
return (*container.Attribute)(a)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAttributesFromV2(v []container.Attribute) Attributes {
|
|
||||||
if v == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
attrs := make(Attributes, len(v))
|
|
||||||
for i := range v {
|
|
||||||
attrs[i] = *NewAttributeFromV2(&v[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
return attrs
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a Attributes) ToV2() []container.Attribute {
|
|
||||||
if a == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
attrs := make([]container.Attribute, len(a))
|
|
||||||
for i := range a {
|
|
||||||
attrs[i] = *a[i].ToV2()
|
|
||||||
}
|
|
||||||
|
|
||||||
return attrs
|
|
||||||
}
|
|
||||||
|
|
||||||
// sets value of the attribute by key.
|
|
||||||
func setAttribute(c *Container, key, value string) {
|
|
||||||
attrs := c.Attributes()
|
|
||||||
found := false
|
|
||||||
|
|
||||||
for i := range attrs {
|
|
||||||
if attrs[i].Key() == key {
|
|
||||||
attrs[i].SetValue(value)
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
index := len(attrs)
|
|
||||||
attrs = append(attrs, Attribute{})
|
|
||||||
attrs[index].SetKey(key)
|
|
||||||
attrs[index].SetValue(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.SetAttributes(attrs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// iterates over container attributes. Stops at f's true return.
|
|
||||||
//
|
|
||||||
// Handler must not be nil.
|
|
||||||
func iterateAttributes(c *Container, f func(*Attribute) bool) {
|
|
||||||
attrs := c.Attributes()
|
|
||||||
for i := range attrs {
|
|
||||||
if f(&attrs[i]) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetNativeNameWithZone sets container native name and its zone.
|
|
||||||
//
|
|
||||||
// Use SetNativeName to set default zone.
|
|
||||||
func SetNativeNameWithZone(c *Container, name, zone string) {
|
|
||||||
setAttribute(c, container.SysAttributeName, name)
|
|
||||||
setAttribute(c, container.SysAttributeZone, zone)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetNativeName sets container native name with default zone (container).
|
|
||||||
func SetNativeName(c *Container, name string) {
|
|
||||||
SetNativeNameWithZone(c, name, container.SysAttributeZoneDefault)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNativeNameWithZone returns container native name and its zone.
|
|
||||||
func GetNativeNameWithZone(c *Container) (name string, zone string) {
|
|
||||||
iterateAttributes(c, func(a *Attribute) bool {
|
|
||||||
if key := a.Key(); key == container.SysAttributeName {
|
|
||||||
name = a.Value()
|
|
||||||
} else if key == container.SysAttributeZone {
|
|
||||||
zone = a.Value()
|
|
||||||
}
|
|
||||||
|
|
||||||
return name != "" && zone != ""
|
|
||||||
})
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
|
@ -1,152 +0,0 @@
|
||||||
package container_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
containerv2 "github.com/nspcc-dev/neofs-api-go/v2/container"
|
|
||||||
"github.com/nspcc-dev/neofs-sdk-go/container"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestAttribute(t *testing.T) {
|
|
||||||
t.Run("nil", func(t *testing.T) {
|
|
||||||
var x *container.Attribute
|
|
||||||
|
|
||||||
require.Nil(t, x.ToV2())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("default values", func(t *testing.T) {
|
|
||||||
attr := container.NewAttribute()
|
|
||||||
|
|
||||||
// check initial values
|
|
||||||
require.Empty(t, attr.Key())
|
|
||||||
require.Empty(t, attr.Value())
|
|
||||||
|
|
||||||
// convert to v2 message
|
|
||||||
attrV2 := attr.ToV2()
|
|
||||||
require.Empty(t, attrV2.GetKey())
|
|
||||||
require.Empty(t, attrV2.GetValue())
|
|
||||||
})
|
|
||||||
|
|
||||||
const (
|
|
||||||
key = "key"
|
|
||||||
value = "value"
|
|
||||||
)
|
|
||||||
|
|
||||||
attr := container.NewAttribute()
|
|
||||||
attr.SetKey(key)
|
|
||||||
attr.SetValue(value)
|
|
||||||
|
|
||||||
require.Equal(t, key, attr.Key())
|
|
||||||
require.Equal(t, value, attr.Value())
|
|
||||||
|
|
||||||
t.Run("test v2", func(t *testing.T) {
|
|
||||||
const (
|
|
||||||
newKey = "newKey"
|
|
||||||
newValue = "newValue"
|
|
||||||
)
|
|
||||||
|
|
||||||
v2 := attr.ToV2()
|
|
||||||
require.Equal(t, key, v2.GetKey())
|
|
||||||
require.Equal(t, value, v2.GetValue())
|
|
||||||
|
|
||||||
v2.SetKey(newKey)
|
|
||||||
v2.SetValue(newValue)
|
|
||||||
|
|
||||||
newAttr := container.NewAttributeFromV2(v2)
|
|
||||||
|
|
||||||
require.Equal(t, newKey, newAttr.Key())
|
|
||||||
require.Equal(t, newValue, newAttr.Value())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAttributes(t *testing.T) {
|
|
||||||
t.Run("nil", func(t *testing.T) {
|
|
||||||
var x container.Attributes
|
|
||||||
|
|
||||||
require.Nil(t, x.ToV2())
|
|
||||||
|
|
||||||
require.Nil(t, container.NewAttributesFromV2(nil))
|
|
||||||
})
|
|
||||||
|
|
||||||
var (
|
|
||||||
keys = []string{"key1", "key2", "key3"}
|
|
||||||
vals = []string{"val1", "val2", "val3"}
|
|
||||||
)
|
|
||||||
|
|
||||||
attrs := make(container.Attributes, len(keys))
|
|
||||||
|
|
||||||
for i := range keys {
|
|
||||||
attrs[i].SetKey(keys[i])
|
|
||||||
attrs[i].SetValue(vals[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("test v2", func(t *testing.T) {
|
|
||||||
const postfix = "x"
|
|
||||||
|
|
||||||
v2 := attrs.ToV2()
|
|
||||||
require.Len(t, v2, len(keys))
|
|
||||||
|
|
||||||
for i := range v2 {
|
|
||||||
k := v2[i].GetKey()
|
|
||||||
v := v2[i].GetValue()
|
|
||||||
|
|
||||||
require.Equal(t, keys[i], k)
|
|
||||||
require.Equal(t, vals[i], v)
|
|
||||||
|
|
||||||
v2[i].SetKey(k + postfix)
|
|
||||||
v2[i].SetValue(v + postfix)
|
|
||||||
}
|
|
||||||
|
|
||||||
newAttrs := container.NewAttributesFromV2(v2)
|
|
||||||
require.Len(t, newAttrs, len(keys))
|
|
||||||
|
|
||||||
for i := range newAttrs {
|
|
||||||
require.Equal(t, keys[i]+postfix, newAttrs[i].Key())
|
|
||||||
require.Equal(t, vals[i]+postfix, newAttrs[i].Value())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewAttributeFromV2(t *testing.T) {
|
|
||||||
t.Run("from nil", func(t *testing.T) {
|
|
||||||
var x *containerv2.Attribute
|
|
||||||
|
|
||||||
require.Nil(t, container.NewAttributeFromV2(x))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetNameWithZone(t *testing.T) {
|
|
||||||
c := container.New()
|
|
||||||
|
|
||||||
for _, item := range [...]struct {
|
|
||||||
name, zone string
|
|
||||||
}{
|
|
||||||
{"name1", ""},
|
|
||||||
{"name1", "zone1"},
|
|
||||||
{"name2", "zone1"},
|
|
||||||
{"name2", "zone2"},
|
|
||||||
{"", "zone2"},
|
|
||||||
{"", ""},
|
|
||||||
} {
|
|
||||||
container.SetNativeNameWithZone(c, item.name, item.zone)
|
|
||||||
|
|
||||||
name, zone := container.GetNativeNameWithZone(c)
|
|
||||||
|
|
||||||
require.Equal(t, item.name, name, item.name)
|
|
||||||
require.Equal(t, item.zone, zone, item.zone)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSetNativeName(t *testing.T) {
|
|
||||||
c := container.New()
|
|
||||||
|
|
||||||
const nameDefZone = "some name"
|
|
||||||
|
|
||||||
container.SetNativeName(c, nameDefZone)
|
|
||||||
|
|
||||||
name, zone := container.GetNativeNameWithZone(c)
|
|
||||||
|
|
||||||
require.Equal(t, nameDefZone, name)
|
|
||||||
require.Equal(t, containerv2.SysAttributeZoneDefault, zone)
|
|
||||||
}
|
|
|
@ -1,193 +1,521 @@
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates, initializes and returns blank Container instance.
|
const (
|
||||||
|
attributeName = "Name"
|
||||||
|
attributeTimestamp = "Timestamp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// reads Container from the container.Container message. If checkFieldPresence is set,
|
||||||
|
// returns an error on absence of any protocol-required field.
|
||||||
|
func (x *Container) readFromV2(m container.Container, checkFieldPresence bool) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
ownerV2 := m.GetOwnerID()
|
||||||
|
if ownerV2 != nil {
|
||||||
|
var owner user.ID
|
||||||
|
|
||||||
|
err = owner.ReadFromV2(*ownerV2)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid owner: %w", err)
|
||||||
|
}
|
||||||
|
} else if checkFieldPresence {
|
||||||
|
return errors.New("missing owner")
|
||||||
|
}
|
||||||
|
|
||||||
|
binNonce := m.GetNonce()
|
||||||
|
if len(binNonce) > 0 {
|
||||||
|
var nonce uuid.UUID
|
||||||
|
|
||||||
|
err = nonce.UnmarshalBinary(binNonce)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid nonce: %w", err)
|
||||||
|
} else if ver := nonce.Version(); ver != 4 {
|
||||||
|
return fmt.Errorf("invalid nonce UUID version %d", ver)
|
||||||
|
}
|
||||||
|
} else if checkFieldPresence {
|
||||||
|
return errors.New("missing nonce")
|
||||||
|
}
|
||||||
|
|
||||||
|
ver := m.GetVersion()
|
||||||
|
if checkFieldPresence && ver == nil {
|
||||||
|
return errors.New("missing version")
|
||||||
|
}
|
||||||
|
|
||||||
|
policyV2 := m.GetPlacementPolicy()
|
||||||
|
if policyV2 != nil {
|
||||||
|
var policy netmap.PlacementPolicy
|
||||||
|
|
||||||
|
err = policy.ReadFromV2(*policyV2)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid placement policy: %w", err)
|
||||||
|
}
|
||||||
|
} else if checkFieldPresence {
|
||||||
|
return errors.New("missing placement policy")
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
//
|
//
|
||||||
// Defaults:
|
// See also WriteToV2.
|
||||||
// - token: nil;
|
func (x *Container) ReadFromV2(m container.Container) error {
|
||||||
// - sig: nil;
|
return x.readFromV2(m, true)
|
||||||
// - 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToV2 returns the v2 Container message.
|
// WriteToV2 writes Container into the container.Container message.
|
||||||
|
// The message MUST NOT be nil.
|
||||||
//
|
//
|
||||||
// Nil Container converts to nil.
|
// See also ReadFromV2.
|
||||||
func (c *Container) ToV2() *container.Container {
|
func (x Container) WriteToV2(m *container.Container) {
|
||||||
if c == nil {
|
*m = x.v2
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return &c.v2
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewVerifiedFromV2 constructs Container from NeoFS API V2 Container message.
|
// Marshal encodes Container into a binary format of the FrostFS API protocol
|
||||||
|
// (Protocol Buffers with direct field order).
|
||||||
//
|
//
|
||||||
// Does not perform if message meets NeoFS API V2 specification. To do this
|
// See also Unmarshal.
|
||||||
// use NewVerifiedFromV2 constructor.
|
func (x Container) Marshal() []byte {
|
||||||
func NewContainerFromV2(c *container.Container) *Container {
|
return x.v2.StableMarshal(nil)
|
||||||
cnr := new(Container)
|
|
||||||
|
|
||||||
if c != nil {
|
|
||||||
cnr.v2 = *c
|
|
||||||
}
|
|
||||||
|
|
||||||
return cnr
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CalculateID calculates container identifier
|
// Unmarshal decodes FrostFS API protocol binary format into the Container
|
||||||
// based on its structure.
|
// (Protocol Buffers with direct field order). Returns an error describing
|
||||||
func CalculateID(c *Container) *cid.ID {
|
// a format violation.
|
||||||
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 {
|
||||||
panic(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
id := cid.New()
|
return x.readFromV2(m, false)
|
||||||
id.SetSHA256(sha256.Sum256(data))
|
|
||||||
|
|
||||||
return id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) Version() *version.Version {
|
// MarshalJSON encodes Container into a JSON format of the FrostFS API protocol
|
||||||
return version.NewFromV2(c.v2.GetVersion())
|
// (Protocol Buffers JSON).
|
||||||
}
|
|
||||||
|
|
||||||
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.
|
|
||||||
//
|
//
|
||||||
// Returns error if container nonce is not a valid UUID.
|
// See also UnmarshalJSON.
|
||||||
func (c *Container) NonceUUID() (uuid.UUID, error) {
|
func (x Container) MarshalJSON() ([]byte, error) {
|
||||||
return uuid.FromBytes(c.v2.GetNonce())
|
return x.v2.MarshalJSON()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetNonceUUID sets container nonce as UUID.
|
// UnmarshalJSON decodes FrostFS API protocol JSON format into the Container
|
||||||
func (c *Container) SetNonceUUID(v uuid.UUID) {
|
// (Protocol Buffers JSON). Returns an error describing a format violation.
|
||||||
data, _ := v.MarshalBinary()
|
//
|
||||||
c.v2.SetNonce(data)
|
// See also MarshalJSON.
|
||||||
|
func (x *Container) UnmarshalJSON(data []byte) error {
|
||||||
|
return x.v2.UnmarshalJSON(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) BasicACL() uint32 {
|
// Init initializes all internal data of the Container required by FrostFS API
|
||||||
return c.v2.GetBasicACL()
|
// protocol. Init MUST be called when creating a new container. Init SHOULD NOT
|
||||||
|
// be called multiple times. Init SHOULD NOT be called if the Container instance
|
||||||
|
// is used for decoding only.
|
||||||
|
func (x *Container) Init() {
|
||||||
|
var ver refs.Version
|
||||||
|
version.Current().WriteToV2(&ver)
|
||||||
|
|
||||||
|
x.v2.SetVersion(&ver)
|
||||||
|
|
||||||
|
nonce, err := uuid.New().MarshalBinary()
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("unexpected error from UUID.MarshalBinary: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
x.v2.SetNonce(nonce)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) SetBasicACL(v acl.BasicACL) {
|
// SetOwner specifies the owner of the Container. Each Container has exactly
|
||||||
c.v2.SetBasicACL(uint32(v))
|
// one owner, so SetOwner MUST be called for instances to be saved in the
|
||||||
|
// FrostFS.
|
||||||
|
//
|
||||||
|
// See also Owner.
|
||||||
|
func (x *Container) SetOwner(owner user.ID) {
|
||||||
|
var m refs.OwnerID
|
||||||
|
owner.WriteToV2(&m)
|
||||||
|
|
||||||
|
x.v2.SetOwnerID(&m)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) Attributes() Attributes {
|
// Owner returns owner of the Container set using SetOwner.
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) SetAttributes(v Attributes) {
|
// SetBasicACL specifies basic part of the Container ACL. Basic ACL is used
|
||||||
c.v2.SetAttributes(v.ToV2())
|
// to control access inside container storage.
|
||||||
|
//
|
||||||
|
// See also BasicACL.
|
||||||
|
func (x *Container) SetBasicACL(basicACL acl.Basic) {
|
||||||
|
x.v2.SetBasicACL(basicACL.Bits())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) PlacementPolicy() *netmap.PlacementPolicy {
|
// BasicACL returns basic ACL set using SetBasicACL.
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) SetPlacementPolicy(v *netmap.PlacementPolicy) {
|
// SetPlacementPolicy sets placement policy for the objects within the Container.
|
||||||
c.v2.SetPlacementPolicy(v.ToV2())
|
// FrostFS storage layer strives to follow the specified policy.
|
||||||
|
//
|
||||||
|
// See also PlacementPolicy.
|
||||||
|
func (x *Container) SetPlacementPolicy(policy netmap.PlacementPolicy) {
|
||||||
|
var m v2netmap.PlacementPolicy
|
||||||
|
policy.WriteToV2(&m)
|
||||||
|
|
||||||
|
x.v2.SetPlacementPolicy(&m)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SessionToken returns token of the session within
|
// PlacementPolicy returns placement policy set using SetPlacementPolicy.
|
||||||
// which container was created.
|
//
|
||||||
func (c Container) SessionToken() *session.Token {
|
// Zero Container has no placement policy which is incorrect according to
|
||||||
return c.token
|
// FrostFS API protocol.
|
||||||
|
func (x Container) PlacementPolicy() (res netmap.PlacementPolicy) {
|
||||||
|
m := x.v2.GetPlacementPolicy()
|
||||||
|
if m != nil {
|
||||||
|
err := res.ReadFromV2(*m)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("unexpected error from PlacementPolicy.ReadFromV2: %v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetSessionToken sets token of the session within
|
// SetAttribute sets Container attribute value by key. Both key and value
|
||||||
// which container was created.
|
// MUST NOT be empty. Attributes set by the creator (owner) are most commonly
|
||||||
func (c *Container) SetSessionToken(t *session.Token) {
|
// ignored by the FrostFS system and used for application layer. Some attributes
|
||||||
c.token = t
|
// are so-called system or well-known attributes: they are reserved for system
|
||||||
|
// needs. System attributes SHOULD NOT be modified using SetAttribute, use
|
||||||
|
// corresponding methods/functions. List of the reserved keys is documented
|
||||||
|
// in the particular protocol version.
|
||||||
|
//
|
||||||
|
// SetAttribute overwrites existing attribute value.
|
||||||
|
//
|
||||||
|
// See also Attribute, IterateAttributes, IterateUserAttributes.
|
||||||
|
func (x *Container) SetAttribute(key, value string) {
|
||||||
|
if key == "" {
|
||||||
|
panic("empty attribute key")
|
||||||
|
} else if value == "" {
|
||||||
|
panic("empty attribute value")
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs := x.v2.GetAttributes()
|
||||||
|
ln := len(attrs)
|
||||||
|
|
||||||
|
for i := range ln {
|
||||||
|
if attrs[i].GetKey() == key {
|
||||||
|
attrs[i].SetValue(value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs = append(attrs, container.Attribute{})
|
||||||
|
attrs[ln].SetKey(key)
|
||||||
|
attrs[ln].SetValue(value)
|
||||||
|
|
||||||
|
x.v2.SetAttributes(attrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Signature returns signature of the marshaled container.
|
// Attribute reads value of the Container attribute by key. Empty result means
|
||||||
func (c Container) Signature() *signature.Signature {
|
// attribute absence.
|
||||||
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 ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetSignature sets signature of the marshaled container.
|
// IterateAttributes iterates over all Container attributes and passes them
|
||||||
func (c *Container) SetSignature(sig *signature.Signature) {
|
// into f. The handler MUST NOT be nil.
|
||||||
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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Marshal marshals Container into a protobuf binary form.
|
// IterateUserAttributes iterates over user Container attributes and passes them
|
||||||
func (c *Container) Marshal() ([]byte, error) {
|
// into f. The handler MUST NOT be nil.
|
||||||
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unmarshal unmarshals protobuf binary representation of Container.
|
// SetName sets human-readable name of the Container. Name MUST NOT be empty.
|
||||||
func (c *Container) Unmarshal(data []byte) error {
|
//
|
||||||
return c.v2.Unmarshal(data)
|
// See also Name.
|
||||||
|
func SetName(cnr *Container, name string) {
|
||||||
|
cnr.SetAttribute(attributeName, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalJSON encodes Container to protobuf JSON format.
|
// Name returns container name set using SetName.
|
||||||
func (c *Container) MarshalJSON() ([]byte, error) {
|
//
|
||||||
return c.v2.MarshalJSON()
|
// Zero Container has no name.
|
||||||
|
func Name(cnr Container) string {
|
||||||
|
return cnr.Attribute(attributeName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalJSON decodes Container from protobuf JSON format.
|
// SetCreationTime writes container's creation time in Unix Timestamp format.
|
||||||
func (c *Container) UnmarshalJSON(data []byte) error {
|
//
|
||||||
return c.v2.UnmarshalJSON(data)
|
// See also CreatedAt.
|
||||||
|
func SetCreationTime(cnr *Container, t time.Time) {
|
||||||
|
cnr.SetAttribute(attributeTimestamp, strconv.FormatInt(t.Unix(), 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatedAt returns container's creation time set using SetCreationTime.
|
||||||
|
//
|
||||||
|
// Zero Container has zero timestamp (in seconds).
|
||||||
|
func CreatedAt(cnr Container) time.Time {
|
||||||
|
var sec int64
|
||||||
|
|
||||||
|
attr := cnr.Attribute(attributeTimestamp)
|
||||||
|
if attr != "" {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
sec, err = strconv.ParseInt(cnr.Attribute(attributeTimestamp), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("parse container timestamp: %v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Unix(sec, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const attributeHomoHashEnabled = "true"
|
||||||
|
|
||||||
|
// DisableHomomorphicHashing sets flag to disable homomorphic hashing of the
|
||||||
|
// Container data.
|
||||||
|
//
|
||||||
|
// See also IsHomomorphicHashingDisabled.
|
||||||
|
func DisableHomomorphicHashing(cnr *Container) {
|
||||||
|
cnr.SetAttribute(container.SysAttributeHomomorphicHashing, attributeHomoHashEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsHomomorphicHashingDisabled checks if DisableHomomorphicHashing was called.
|
||||||
|
//
|
||||||
|
// Zero Container has enabled hashing.
|
||||||
|
func IsHomomorphicHashingDisabled(cnr Container) bool {
|
||||||
|
return cnr.Attribute(container.SysAttributeHomomorphicHashing) == attributeHomoHashEnabled ||
|
||||||
|
cnr.Attribute(container.SysAttributeHomomorphicHashingNeoFS) == attributeHomoHashEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// Domain represents information about container domain registered in the NNS
|
||||||
|
// contract deployed in the FrostFS network.
|
||||||
|
type Domain struct {
|
||||||
|
name, zone string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetName sets human-friendly container domain name.
|
||||||
|
func (x *Domain) SetName(name string) {
|
||||||
|
x.name = name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns name set using SetName.
|
||||||
|
//
|
||||||
|
// Zero Domain has zero name.
|
||||||
|
func (x Domain) Name() string {
|
||||||
|
return x.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetZone sets zone which is used as a TLD of a domain name in NNS contract.
|
||||||
|
func (x *Domain) SetZone(zone string) {
|
||||||
|
x.zone = zone
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zone returns domain zone set using SetZone.
|
||||||
|
//
|
||||||
|
// Zero Domain has "container" zone.
|
||||||
|
func (x Domain) Zone() string {
|
||||||
|
if x.zone != "" {
|
||||||
|
return x.zone
|
||||||
|
}
|
||||||
|
|
||||||
|
return "container"
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteDomain writes Domain into the Container. Name MUST NOT be empty.
|
||||||
|
func WriteDomain(cnr *Container, domain Domain) {
|
||||||
|
cnr.SetAttribute(container.SysAttributeName, domain.Name())
|
||||||
|
cnr.SetAttribute(container.SysAttributeZone, domain.Zone())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadDomain reads Domain from the Container. Returns value with empty name
|
||||||
|
// if domain is not specified.
|
||||||
|
func ReadDomain(cnr Container) (res Domain) {
|
||||||
|
if name := cnr.Attribute(container.SysAttributeName); name != "" {
|
||||||
|
res.SetName(name)
|
||||||
|
res.SetZone(cnr.Attribute(container.SysAttributeZone))
|
||||||
|
} else if name = cnr.Attribute(container.SysAttributeNameNeoFS); name != "" {
|
||||||
|
res.SetName(name)
|
||||||
|
res.SetZone(cnr.Attribute(container.SysAttributeZoneNeoFS))
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalculateSignature calculates signature of the Container using provided signer
|
||||||
|
// and writes it into dst. Signature instance MUST NOT be nil. CalculateSignature
|
||||||
|
// is expected to be called after all the Container data is filled and before
|
||||||
|
// saving the Container in the FrostFS network. Note that мany subsequent change
|
||||||
|
// will most likely break the signature.
|
||||||
|
//
|
||||||
|
// See also VerifySignature.
|
||||||
|
func CalculateSignature(dst *frostfscrypto.Signature, cnr Container, signer ecdsa.PrivateKey) error {
|
||||||
|
return dst.Calculate(frostfsecdsa.SignerRFC6979(signer), cnr.Marshal())
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifySignature verifies Container signature calculated using CalculateSignature.
|
||||||
|
// Result means signature correctness.
|
||||||
|
func VerifySignature(sig frostfscrypto.Signature, cnr Container) bool {
|
||||||
|
return sig.Verify(cnr.Marshal())
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalculateIDFromBinary calculates identifier of the binary-encoded container
|
||||||
|
// in CAS of the FrostFS containers and writes it into dst. ID instance MUST NOT
|
||||||
|
// be nil.
|
||||||
|
//
|
||||||
|
// See also CalculateID, AssertID.
|
||||||
|
func CalculateIDFromBinary(dst *cid.ID, cnr []byte) {
|
||||||
|
dst.SetSHA256(sha256.Sum256(cnr))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalculateID encodes the given Container and passes the result into
|
||||||
|
// CalculateIDFromBinary.
|
||||||
|
//
|
||||||
|
// See also Container.Marshal, AssertID.
|
||||||
|
func CalculateID(dst *cid.ID, cnr Container) {
|
||||||
|
CalculateIDFromBinary(dst, cnr.Marshal())
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertID checks if the given Container matches its identifier in CAS of the
|
||||||
|
// FrostFS containers.
|
||||||
|
//
|
||||||
|
// See also CalculateID.
|
||||||
|
func AssertID(id cid.ID, cnr Container) bool {
|
||||||
|
var id2 cid.ID
|
||||||
|
CalculateID(&id2, cnr)
|
||||||
|
|
||||||
|
return id2.Equals(id)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,136 +1,333 @@
|
||||||
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/neofs-sdk-go/acl"
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||||
"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 TestNewContainer(t *testing.T) {
|
func TestPlacementPolicyEncoding(t *testing.T) {
|
||||||
c := container.New()
|
v := containertest.Container()
|
||||||
|
|
||||||
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) {
|
||||||
data, err := c.Marshal()
|
var v2 container.Container
|
||||||
require.NoError(t, err)
|
require.NoError(t, v2.Unmarshal(v.Marshal()))
|
||||||
|
|
||||||
c2 := container.New()
|
require.Equal(t, v, v2)
|
||||||
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 := c.MarshalJSON()
|
data, err := v.MarshalJSON()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
c2 := container.New()
|
var v2 container.Container
|
||||||
require.NoError(t, c2.UnmarshalJSON(data))
|
require.NoError(t, v2.UnmarshalJSON(data))
|
||||||
|
|
||||||
require.Equal(t, c, c2)
|
require.Equal(t, v, v2)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestContainer_SessionToken(t *testing.T) {
|
func TestContainer_Init(t *testing.T) {
|
||||||
tok := sessiontest.Token()
|
val := containertest.Container()
|
||||||
|
|
||||||
cnr := container.New()
|
val.Init()
|
||||||
|
|
||||||
cnr.SetSessionToken(tok)
|
var msg v2container.Container
|
||||||
|
val.WriteToV2(&msg)
|
||||||
|
|
||||||
require.Equal(t, tok, cnr.SessionToken())
|
binNonce := msg.GetNonce()
|
||||||
|
|
||||||
|
var nonce uuid.UUID
|
||||||
|
require.NoError(t, nonce.UnmarshalBinary(binNonce))
|
||||||
|
require.EqualValues(t, 4, nonce.Version())
|
||||||
|
|
||||||
|
verV2 := msg.GetVersion()
|
||||||
|
require.NotNil(t, verV2)
|
||||||
|
|
||||||
|
var ver version.Version
|
||||||
|
require.NoError(t, ver.ReadFromV2(*verV2))
|
||||||
|
|
||||||
|
require.Equal(t, version.Current(), ver)
|
||||||
|
|
||||||
|
var val2 container.Container
|
||||||
|
require.NoError(t, val2.ReadFromV2(msg))
|
||||||
|
|
||||||
|
require.Equal(t, val, val2)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestContainer_Signature(t *testing.T) {
|
func TestContainer_Owner(t *testing.T) {
|
||||||
sig := sigtest.Signature()
|
var val container.Container
|
||||||
|
|
||||||
cnr := container.New()
|
require.Zero(t, val.Owner())
|
||||||
cnr.SetSignature(sig)
|
|
||||||
|
|
||||||
require.Equal(t, sig, cnr.Signature())
|
val = containertest.Container()
|
||||||
|
|
||||||
|
owner := usertest.ID()
|
||||||
|
|
||||||
|
val.SetOwner(owner)
|
||||||
|
|
||||||
|
var msg v2container.Container
|
||||||
|
val.WriteToV2(&msg)
|
||||||
|
|
||||||
|
var msgOwner refs.OwnerID
|
||||||
|
owner.WriteToV2(&msgOwner)
|
||||||
|
|
||||||
|
require.Equal(t, &msgOwner, msg.GetOwnerID())
|
||||||
|
|
||||||
|
var val2 container.Container
|
||||||
|
require.NoError(t, val2.ReadFromV2(msg))
|
||||||
|
|
||||||
|
require.True(t, val2.Owner().Equals(owner))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestContainer_ToV2(t *testing.T) {
|
func TestContainer_BasicACL(t *testing.T) {
|
||||||
t.Run("nil", func(t *testing.T) {
|
var val container.Container
|
||||||
var x *container.Container
|
|
||||||
|
|
||||||
require.Nil(t, x.ToV2())
|
require.Zero(t, val.BasicACL())
|
||||||
|
|
||||||
|
val = containertest.Container()
|
||||||
|
|
||||||
|
basicACL := containertest.BasicACL()
|
||||||
|
val.SetBasicACL(basicACL)
|
||||||
|
|
||||||
|
var msg v2container.Container
|
||||||
|
val.WriteToV2(&msg)
|
||||||
|
|
||||||
|
require.EqualValues(t, basicACL.Bits(), msg.GetBasicACL())
|
||||||
|
|
||||||
|
var val2 container.Container
|
||||||
|
require.NoError(t, val2.ReadFromV2(msg))
|
||||||
|
|
||||||
|
require.Equal(t, basicACL, val2.BasicACL())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContainer_PlacementPolicy(t *testing.T) {
|
||||||
|
var val container.Container
|
||||||
|
|
||||||
|
require.Zero(t, val.PlacementPolicy())
|
||||||
|
|
||||||
|
val = containertest.Container()
|
||||||
|
|
||||||
|
pp := netmaptest.PlacementPolicy()
|
||||||
|
val.SetPlacementPolicy(pp)
|
||||||
|
|
||||||
|
var msgPolicy v2netmap.PlacementPolicy
|
||||||
|
pp.WriteToV2(&msgPolicy)
|
||||||
|
|
||||||
|
var msg v2container.Container
|
||||||
|
val.WriteToV2(&msg)
|
||||||
|
|
||||||
|
require.Equal(t, &msgPolicy, msg.GetPlacementPolicy())
|
||||||
|
|
||||||
|
var val2 container.Container
|
||||||
|
require.NoError(t, val2.ReadFromV2(msg))
|
||||||
|
|
||||||
|
require.Equal(t, pp, val2.PlacementPolicy())
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertContainsAttribute(t *testing.T, m v2container.Container, key, val string) {
|
||||||
|
var msgAttr v2container.Attribute
|
||||||
|
|
||||||
|
msgAttr.SetKey(key)
|
||||||
|
msgAttr.SetValue(val)
|
||||||
|
require.Contains(t, m.GetAttributes(), msgAttr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContainer_Attribute(t *testing.T) {
|
||||||
|
const attrKey1, attrKey2 = 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
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("default values", func(t *testing.T) {
|
require.GreaterOrEqual(t, len(m), 2)
|
||||||
cnt := container.New()
|
require.Equal(t, attrVal1, m[attrKey1])
|
||||||
|
require.Equal(t, attrVal2, m[attrKey2])
|
||||||
|
|
||||||
// check initial values
|
val2.SetAttribute(attrKey1, attrVal1+"_")
|
||||||
require.Nil(t, cnt.SessionToken())
|
require.Equal(t, attrVal1+"_", val2.Attribute(attrKey1))
|
||||||
require.Nil(t, cnt.Signature())
|
}
|
||||||
require.Nil(t, cnt.Attributes())
|
|
||||||
require.Nil(t, cnt.PlacementPolicy())
|
func TestSetName(t *testing.T) {
|
||||||
require.Nil(t, cnt.OwnerID())
|
var val container.Container
|
||||||
|
|
||||||
require.EqualValues(t, acl.PrivateBasicRule, cnt.BasicACL())
|
require.Panics(t, func() {
|
||||||
require.Equal(t, version.Current(), cnt.Version())
|
container.SetName(&val, "")
|
||||||
|
})
|
||||||
nonce, err := cnt.NonceUUID()
|
|
||||||
require.NoError(t, err)
|
val = containertest.Container()
|
||||||
require.NotNil(t, nonce)
|
|
||||||
|
const name = "some name"
|
||||||
// convert to v2 message
|
|
||||||
cntV2 := cnt.ToV2()
|
container.SetName(&val, name)
|
||||||
|
|
||||||
nonceV2, err := uuid.FromBytes(cntV2.GetNonce())
|
var msg v2container.Container
|
||||||
require.NoError(t, err)
|
val.WriteToV2(&msg)
|
||||||
|
|
||||||
require.Equal(t, nonce.String(), nonceV2.String())
|
assertContainsAttribute(t, msg, "Name", name)
|
||||||
|
|
||||||
require.Nil(t, cntV2.GetAttributes())
|
var val2 container.Container
|
||||||
require.Nil(t, cntV2.GetPlacementPolicy())
|
require.NoError(t, val2.ReadFromV2(msg))
|
||||||
require.Nil(t, cntV2.GetOwnerID())
|
|
||||||
|
require.Equal(t, name, container.Name(val2))
|
||||||
require.Equal(t, uint32(acl.PrivateBasicRule), cntV2.GetBasicACL())
|
}
|
||||||
require.Equal(t, version.Current().ToV2(), cntV2.GetVersion())
|
|
||||||
})
|
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))
|
||||||
}
|
}
|
||||||
|
|
49
container/doc.go
Normal file
49
container/doc.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
Package container provides functionality related to the FrostFS containers.
|
||||||
|
|
||||||
|
The base type is Container. To create new container in the FrostFS network
|
||||||
|
Container instance should be initialized
|
||||||
|
|
||||||
|
var cnr Container
|
||||||
|
cnr.Init()
|
||||||
|
// fill all the fields
|
||||||
|
|
||||||
|
// encode cnr and send
|
||||||
|
|
||||||
|
After the container is persisted in the FrostFS network, applications can process
|
||||||
|
it using the instance of Container types
|
||||||
|
|
||||||
|
// recv binary container
|
||||||
|
|
||||||
|
var cnr Container
|
||||||
|
|
||||||
|
err := cnr.Unmarshal(bin)
|
||||||
|
// ...
|
||||||
|
|
||||||
|
// process the container data
|
||||||
|
|
||||||
|
Instances can be also used to process FrostFS API V2 protocol messages
|
||||||
|
(see neo.fs.v2.container package in https://git.frostfs.info/TrueCloudLab/frostfs-api).
|
||||||
|
|
||||||
|
On client side:
|
||||||
|
|
||||||
|
import "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
|
||||||
|
|
||||||
|
var msg container.Container
|
||||||
|
cnr.WriteToV2(&msg)
|
||||||
|
|
||||||
|
// send msg
|
||||||
|
|
||||||
|
On server side:
|
||||||
|
|
||||||
|
// recv msg
|
||||||
|
|
||||||
|
var cnr Container
|
||||||
|
cnr.ReadFromV2(msg)
|
||||||
|
|
||||||
|
// process cnr
|
||||||
|
|
||||||
|
Using package types in an application is recommended to potentially work with
|
||||||
|
different protocol versions with which these types are compatible.
|
||||||
|
*/
|
||||||
|
package container
|
7
container/id/doc.go
Normal file
7
container/id/doc.go
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
/*
|
||||||
|
Package cid provides primitives to work with container identification in FrostFS.
|
||||||
|
|
||||||
|
Using package types in an application is recommended to potentially work with
|
||||||
|
different protocol versions with which these types are compatible.
|
||||||
|
*/
|
||||||
|
package cid
|
|
@ -1,90 +1,115 @@
|
||||||
package cid
|
package cid
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"errors"
|
"fmt"
|
||||||
|
|
||||||
|
"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 v2-compatible container identifier.
|
// ID represents FrostFS container identifier.
|
||||||
type ID refs.ContainerID
|
|
||||||
|
|
||||||
// NewFromV2 wraps v2 ContainerID message to ID.
|
|
||||||
//
|
//
|
||||||
// Nil refs.ContainerID converts to nil.
|
// ID is mutually compatible with git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs.ContainerID
|
||||||
func NewFromV2(idV2 *refs.ContainerID) *ID {
|
// message. See ReadFromV2 / WriteToV2 methods.
|
||||||
return (*ID)(idV2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates and initializes blank ID.
|
|
||||||
//
|
//
|
||||||
// Defaults:
|
// Instances can be created using built-in var declaration.
|
||||||
// - value: nil.
|
|
||||||
func New() *ID {
|
|
||||||
return NewFromV2(new(refs.ContainerID))
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetSHA256 sets container identifier value to SHA256 checksum of container body.
|
|
||||||
func (id *ID) SetSHA256(v [sha256.Size]byte) {
|
|
||||||
(*refs.ContainerID)(id).SetValue(v[:])
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToV2 returns the v2 container ID message.
|
|
||||||
//
|
//
|
||||||
// Nil ID converts to nil.
|
// Note that direct typecast is not safe and may result in loss of compatibility:
|
||||||
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.
|
|
||||||
//
|
//
|
||||||
// Returns error if s is not a base58 encoded
|
// _ = ID([32]byte) // not recommended
|
||||||
// ID data.
|
type ID [sha256.Size]byte
|
||||||
func (id *ID) Parse(s string) error {
|
|
||||||
data, err := base58.Decode(s)
|
// ReadFromV2 reads ID from the refs.ContainerID message.
|
||||||
if err != nil {
|
// Returns an error if the message is malformed according
|
||||||
return err
|
// to the FrostFS API V2 protocol.
|
||||||
} else if len(data) != sha256.Size {
|
//
|
||||||
return errors.New("incorrect format of the string container ID")
|
// See also WriteToV2.
|
||||||
|
func (id *ID) ReadFromV2(m refs.ContainerID) error {
|
||||||
|
return id.Decode(m.GetValue())
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteToV2 writes ID to the refs.ContainerID message.
|
||||||
|
// The message must not be nil.
|
||||||
|
//
|
||||||
|
// See also ReadFromV2.
|
||||||
|
func (id ID) WriteToV2(m *refs.ContainerID) {
|
||||||
|
m.SetValue(id[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode encodes ID into 32 bytes of dst. Panics if
|
||||||
|
// dst length is less than 32.
|
||||||
|
//
|
||||||
|
// Zero ID is all zeros.
|
||||||
|
//
|
||||||
|
// See also Decode.
|
||||||
|
func (id ID) Encode(dst []byte) {
|
||||||
|
if l := len(dst); l < sha256.Size {
|
||||||
|
panic(fmt.Sprintf("destination length is less than %d bytes: %d", sha256.Size, l))
|
||||||
}
|
}
|
||||||
|
|
||||||
(*refs.ContainerID)(id).SetValue(data)
|
copy(dst, id[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode decodes src bytes into ID.
|
||||||
|
//
|
||||||
|
// Decode expects that src has 32 bytes length. If the input is malformed,
|
||||||
|
// Decode returns an error describing format violation. In this case ID
|
||||||
|
// remains unchanged.
|
||||||
|
//
|
||||||
|
// Decode doesn't mutate src.
|
||||||
|
//
|
||||||
|
// See also Encode.
|
||||||
|
func (id *ID) Decode(src []byte) error {
|
||||||
|
if len(src) != sha256.Size {
|
||||||
|
return fmt.Errorf("invalid length %d", len(src))
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(id[:], src)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// String returns base58 string representation of ID.
|
// SetSHA256 sets container identifier value to SHA256 checksum of container structure.
|
||||||
func (id *ID) String() string {
|
func (id *ID) SetSHA256(v [sha256.Size]byte) {
|
||||||
return base58.Encode((*refs.ContainerID)(id).GetValue())
|
copy(id[:], v[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Marshal marshals ID into a protobuf binary form.
|
// Equals defines a comparison relation between two ID instances.
|
||||||
func (id *ID) Marshal() ([]byte, error) {
|
//
|
||||||
return (*refs.ContainerID)(id).StableMarshal(nil)
|
// Note that comparison using '==' operator is not recommended since it MAY result
|
||||||
|
// in loss of compatibility.
|
||||||
|
func (id ID) Equals(id2 ID) bool {
|
||||||
|
return id == id2
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unmarshal unmarshals protobuf binary representation of ID.
|
// EncodeToString encodes ID into FrostFS API protocol string.
|
||||||
func (id *ID) Unmarshal(data []byte) error {
|
//
|
||||||
return (*refs.ContainerID)(id).Unmarshal(data)
|
// Zero ID is base58 encoding of 32 zeros.
|
||||||
|
//
|
||||||
|
// See also DecodeString.
|
||||||
|
func (id ID) EncodeToString() string {
|
||||||
|
return base58.Encode(id[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalJSON encodes ID to protobuf JSON format.
|
// DecodeString decodes string into ID according to FrostFS API protocol. Returns
|
||||||
func (id *ID) MarshalJSON() ([]byte, error) {
|
// an error if s is malformed.
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalJSON decodes ID from protobuf JSON format.
|
// String implements fmt.Stringer.
|
||||||
func (id *ID) UnmarshalJSON(data []byte) error {
|
//
|
||||||
return (*refs.ContainerID)(id).UnmarshalJSON(data)
|
// String is designed to be human-readable, and its format MAY differ between
|
||||||
|
// SDK versions. String MAY return same result as EncodeToString. String MUST NOT
|
||||||
|
// be used to encode ID into FrostFS protocol string.
|
||||||
|
func (id ID) String() string {
|
||||||
|
return id.EncodeToString()
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue