Compare commits

..

399 commits

Author SHA1 Message Date
e523bde976 [#xxx] client: add error node maintenance
Signed-off-by: Viktor Nesterenko <vemeyzer@gmail.com>
2023-10-06 15:10:20 +02:00
ac8fc6d440 [#162] netmap: Allow to parse single unnamed selectors
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-09-11 15:22:24 +03:00
0a0b590df3 [#162] Fix pre-commit warnings
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-09-11 15:21:35 +03:00
4df642e941 [#162] netmap: Fix possible panic
Placement policy is unvalidated external input.
Under no circumstances should we panic here.

Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-09-11 15:19:56 +03:00
8bc64e088e [#161] .golangci.yml: Reenable deprecated usage warnings
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-09-08 17:17:02 +03:00
49ad985cad [#161] *: Do not use math/rand.Read()
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-09-08 17:17:02 +03:00
aa12d8c6a6 [#121] client: Make PrmObjectHash fields public
* Introduce buildRequest for PrmObjectHash

Signed-off-by: Airat Arifullin <a.arifullin@yadro.com>
2023-09-08 13:36:45 +00:00
303508328a [#121] pool: Refactor PrmSessionCreate usage
Signed-off-by: Airat Arifullin <a.arifullin@yadro.com>
2023-09-05 17:32:03 +03:00
55699d1480 [#121] client: Make PrmSessionCreate fields public
Signed-off-by: Airat Arifullin <a.arifullin@yadro.com>
2023-09-05 17:30:42 +03:00
55a1f23e71 [#121] client: Make PrmEndpointInfo, PrmNetworkInfo fields public
Signed-off-by: Airat Arifullin <a.arifullin@yadro.com>
2023-09-04 19:55:23 +03:00
291a71ba84 [#121] client: Make PrmAnnounceSpace fields public
Signed-off-by: Airat Arifullin <a.arifullin@yadro.com>
2023-09-04 19:53:34 +03:00
5a471e5002 [#121] client: Make PrmObjectDelete fields public
* Introduce buildRequest for PrmObjectDelete
* Refactor the usage of these params in pool

Signed-off-by: Airat Arifullin <a.arifullin@yadro.com>
2023-09-04 14:14:22 +00:00
b5fe52d6bd [#150] policy: Check for redundant selectors and filters
Signed-off-by: Anton Nikiforov <an.nikiforov@yadro.com>
2023-08-29 14:16:57 +03:00
84e7e69f98 [#121] client: Make PrmObjectGet/Head/GetRange fields public
* Remove common PrmObjectRead structure
* Introduce buildRequest for PrmObjectGet/Head/GetRange
* Refactor the usage of these params in pool

Signed-off-by: Airat Arifullin <a.arifullin@yadro.com>
2023-08-28 11:26:57 +03:00
46a214d065 [#149] pool: Configure homomorphic hash and buffer size
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-08-25 09:45:15 +03:00
202412230a [#115] pool: Drop part buffer pool
Tests showed that using part buffer pool doesn't save memory a lot.
Especially on big parts.
Probably we can use pool only for small parts
after adding buffer in payloadSizeLimiter

Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-08-21 12:03:03 +03:00
3cb3841073 [#115] pool: Try putSingle if possible
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-08-21 12:03:03 +03:00
faeeeab87a [#114] pool: Don't use part buffers when client cut is off
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-08-21 12:02:40 +03:00
cae215534f [#114] pool: Fix linter errors
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-08-21 12:02:40 +03:00
518fb79bc0 [#114] pool: Support client cut with memory limiter
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-08-21 12:02:40 +03:00
342524159a [#121] pool: Make PrmContainerSetEACL fields public
Signed-off-by: Airat Arifullin <a.arifullin@yadro.com>
2023-08-21 10:33:19 +03:00
22978303f8 [#121] clientt: Make PrmContainerSetEACL fields public
Signed-off-by: Airat Arifullin <a.arifullin@yadro.com>
2023-08-18 18:05:47 +03:00
6fdbe75517 [#121] pool: Make PrmContainerEACL fields public
Signed-off-by: Airat Arifullin <a.arifullin@yadro.com>
2023-08-16 09:53:47 +00:00
3353940554 [#121] client: Make PrmContainerEACL fields public
Signed-off-by: Airat Arifullin <a.arifullin@yadro.com>
2023-08-16 09:53:47 +00:00
a3b5d4d4f5 [#51] Add node addresses as debug information
Signed-off-by: Artem Tataurov <a.tataurov@yadro.com>
2023-08-15 09:51:40 +03:00
0314b326d3 [#51] Add current nodes as external statistics
Signed-off-by: Artem Tataurov <a.tataurov@yadro.com>
2023-08-15 09:51:13 +03:00
0382785763 [#146] .forgejo: Update DCO action
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-08-11 12:34:29 +03:00
548a81d3e6 [#48] client: Refactor accounting.Balance()
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-08-10 11:05:30 +00:00
d48788c7a9 [#144] Bump required go version to go1.20
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-08-09 09:52:35 +03:00
6353df8bca [#142] Fix unwrapErr for go 1.20
Signed-off-by: Alejandro Lopez <a.lopez@yadro.com>
2023-08-05 15:58:25 +00:00
936e6d230b [#121] pool: Add wait params validation for containerPut method
* Add WaitParams.CheckValidity() check because SetWaitParams is deprecated,
  but parameters were checked within this setter with checkForPositive()

Signed-off-by: Airat Arifullin a.arifullin@yadro.com
2023-08-05 15:58:14 +00:00
be28b89312 [#121] pool: Make PrmContainerDelete fields public
* Refactor client PrmContainerDelete usage
* Introduce WaitParams CheckValidity method

Signed-off-by: Airat Arifullin a.arifullin@yadro.com
2023-08-05 15:58:14 +00:00
9e5faaf829 [#121] client: Make PrmContainerDelete fields public
Signed-off-by: Airat Arifullin a.arifullin@yadro.com
2023-08-05 15:58:14 +00:00
3dc8129ed7 [#135] Make all error status receivers pointers
Signed-off-by: Alejandro Lopez <a.lopez@yadro.com>
2023-08-04 08:35:01 +00:00
55c52c8d5d [#121] pool: Make PrmContainerGet fields public
* Also refactor client PrmContainerGet usage

Signed-off-by: Airat Arifullin a.arifullin@yadro.com
2023-08-03 10:54:13 +03:00
d376302a3b [#121] client: Make PrmContainerGet fields public
Signed-off-by: Airat Arifullin a.arifullin@yadro.com
2023-08-02 18:23:32 +03:00
363f153eaf [#136] pool: Set order field to get subtree
With new revision of tree service protocol, getSubTree
requires to use explicit order field.

Signed-off-by: Alex Vanin <a.vanin@yadro.com>
2023-08-02 10:32:37 +00:00
95b987b818 [#137] go.mod: Update api-go
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-08-02 12:04:10 +03:00
13d0b170d2 [#121] client: Make pool PrmContainerPut struct fields public
Signed-off-by: Airat Arifullin a.arifullin@yadro.com
2023-08-01 09:59:57 +00:00
18a9e4bceb [#121] client: Make PrmContainerPut struct fields public
Signed-off-by: Airat Arifullin a.arifullin@yadro.com
2023-08-01 09:59:57 +00:00
0fe0d71678 [#133] .forgejo: Add names to actions
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-07-31 11:33:39 +00:00
78d1439b2c [#133] go.mod: Update api-go
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-07-31 11:33:39 +00:00
0886d80083 [#69] Add close for nns.Dial
Signed-off-by: Marina Biryukova <m.biryukova@yadro.com>
2023-07-31 11:05:26 +03:00
ecb1fef78c [#129] client: Do not override error status WriteObject()
Here is a scenario:
1. `resolveFrostFSErrors` is false.
2. The first object part was not written, which was signified in status.
3. The second part was written correctly.

Client now thinks that the object is written even though it was not.
In theory we could also return only status, but client-side splitting
is not a single RPC, so it makes sense.

Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-07-26 14:38:43 +03:00
5defed4ab4 [#126] go.mod: Update frostfs-api-go package version
Signed-off-by: Airat Arifullin a.arifullin@yadro.com
2023-07-19 16:03:56 +03:00
fb05f7dc5e [#112] Add basic documentation for placement policies
Signed-off-by: Alejandro Lopez <a.lopez@yadro.com>
2023-07-18 10:53:54 +00:00
b91f9d8c79 [#xx] Add support for SELECT-FILTER expressions
Signed-off-by: Alejandro Lopez <a.lopez@yadro.com>
2023-07-17 10:29:48 +00:00
b9afe7a2f9 [#42] Add ResolveContractHash method
Signed-off-by: Pavel Pogodaev <p.pogodaev@yadro.com>
2023-07-12 22:22:58 +03:00
998fe1a7ab [#102] netmap: properly process multiple REP
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-07-11 14:21:35 +00:00
c359a7465a [#64] transformer: Simplify interface
Signed-off-by: Dmitrii Stepanov <d.stepanov@yadro.com>
2023-07-11 07:33:12 +00:00
d70ef2187b [#97] Add a method IterateUserAttributes in Container
Signed-off-by: Anton Nikiforov <an.nikiforov@yadro.com>
2023-07-10 09:05:01 +00:00
ac95b87e7c [#101] Add Equals for Address
Signed-off-by: Anton Nikiforov <an.nikiforov@yadro.com>
2023-07-10 09:05:01 +00:00
863be6034f [#104] Update neo-go/pkg/interop version
neo-go module uses broken commit of interop package.

Signed-off-by: Alex Vanin <a.vanin@yadro.com>
2023-07-10 09:04:10 +00:00
35346a01c9 [#109] Bump neo-go version
Synced version with frostfs-node, see frostfs-node#417

Signed-off-by: Alex Vanin <a.vanin@yadro.com>
2023-07-10 09:04:10 +00:00
fe35373d8f [#107] go.mod: Tidy dependencies
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-07-07 14:57:16 +03:00
388d1ca1de [#107] pool/tree: Support grpc schemas
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-07-07 14:57:13 +03:00
14ed3e177d [#104] nemtap: Escape special symbols in filters
Signed-off-by: Alex Vanin <a.vanin@yadro.com>
2023-07-07 09:33:00 +00:00
fe28c33277 [#104] netmap: Add test with quote escaping
Signed-off-by: Alex Vanin <a.vanin@yadro.com>
2023-07-07 09:33:00 +00:00
98cab7ed61 [#103] object: Add PutSingle method code
Signed-off-by: Dmitrii Stepanov <d.stepanov@yadro.com>
2023-07-06 17:06:17 +03:00
37e22b33ad [#103] sdk-go: Update api-go version
Signed-off-by: Dmitrii Stepanov <d.stepanov@yadro.com>
2023-07-05 18:10:42 +03:00
769f6eec05 [#105] client: Fix revive linter warning
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-07-05 15:52:06 +03:00
5d62cef27e [#98] Fix linter issues
Signed-off-by: Alex Vanin <a.vanin@yadro.com>
2023-06-28 12:13:02 +00:00
c0c0c588b5 [#98] Add forgejo workflows
Signed-off-by: Alex Vanin <a.vanin@yadro.com>
2023-06-28 12:13:02 +00:00
2f88460172 [#83] Allow to split objects in the client
Signed-off-by: Anton Nikiforov <an.nikiforov@yadro.com>
2023-06-28 12:12:37 +00:00
66cb5dcf34 [#83] Update .gitignore
Signed-off-by: Anton Nikiforov <an.nikiforov@yadro.com>
2023-06-28 12:12:37 +00:00
91e80ba743 [#73] pool/tree: Fix index in retry loop
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-06-28 13:06:23 +03:00
c243b443bc [#80] objectwriter: Allow custimize maxChunkLen
Signed-off-by: Dmitrii Stepanov <d.stepanov@yadro.com>
2023-06-26 18:13:04 +03:00
aa8ffebc63 [#95] transformer: Set parent version
Signed-off-by: Dmitrii Stepanov <d.stepanov@yadro.com>
2023-06-23 13:48:02 +03:00
9d40228cec [#73] pool/tree: Fix client healthy status
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-06-08 17:01:55 +03:00
af40dc68f0 [#84] pool/tree: Allow to pass gRPC dial options
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-06-08 15:55:07 +03:00
981d24a493 [#84] pool: Allow to pass gRPC dial options
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-06-08 15:54:48 +03:00
19adb4dffa [#73] pool/tree: Add tests
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-06-08 14:37:57 +03:00
51e022ab8c [#73] pool/tree: Add tree pool
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-06-08 14:37:57 +03:00
0d3dacb515 [#73] pool: Add getters for NodeParam
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-06-08 14:37:57 +03:00
b2e302624d [#73] pool/tree: Add proto tree service client
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-06-08 14:37:57 +03:00
fcbf96add6 [#76] Add UNIQUE keyword
Signed-off-by: Alejandro Lopez <a.lopez@yadro.com>
2023-06-06 13:54:07 +03:00
4f48f6c9e0 [#78] netmap: Add new keywords NOT and UNIQUE
* Add the rule for NOT operation to the policy parser grammar
* Regenerate query parse
* Implement NOT in filter
* Add unit-tests

Signed-off-by: Airat Arifullin a.arifullin@yadro.com
2023-06-02 17:47:20 +03:00
ec59ebfd88 [#87] go.mod: Update hrw
```
                                                              │      old      │                 new                  │
                                                              │    sec/op     │    sec/op     vs base                │
Netmap_ContainerNodes/REP_2-8                                    13.07µ ± 12%   11.03µ ± 12%  -15.63% (p=0.003 n=10)
Netmap_ContainerNodes/REP_2_IN_X_CBF_2_SELECT_2_FROM_*_AS_X-8   11.383µ ±  9%   9.970µ ± 19%  -12.42% (p=0.005 n=10)
geomean                                                          12.20µ         10.49µ        -14.04%

                                                              │      old      │                 new                  │
                                                              │     B/op      │     B/op      vs base                │
Netmap_ContainerNodes/REP_2-8                                   10.203Ki ± 0%   7.711Ki ± 0%  -24.43% (p=0.000 n=10)
Netmap_ContainerNodes/REP_2_IN_X_CBF_2_SELECT_2_FROM_*_AS_X-8    9.641Ki ± 0%   7.148Ki ± 0%  -25.85% (p=0.000 n=10)
geomean                                                          9.918Ki        7.424Ki       -25.14%

                                                              │    old     │                new                 │
                                                              │ allocs/op  │ allocs/op   vs base                │
Netmap_ContainerNodes/REP_2-8                                   216.0 ± 0%   131.0 ± 0%  -39.35% (p=0.000 n=10)
Netmap_ContainerNodes/REP_2_IN_X_CBF_2_SELECT_2_FROM_*_AS_X-8   215.0 ± 0%   130.0 ± 0%  -39.53% (p=0.000 n=10)
geomean                                                         215.5        130.5       -39.44%
```

Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-06-02 14:54:40 +03:00
030ff2f122 [#87] netmap: Add benchmark for ContainerNodes()
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-06-02 14:54:40 +03:00
0f7455ff7a [#75] Update antlr4 version to 4.13.0
Signed-off-by: Anton Nikiforov <an.nikiforov@yadro.com>
2023-06-01 13:15:11 +00:00
e6b662cfa6 [#75] Add make targets policy and docker/%
Signed-off-by: Anton Nikiforov <an.nikiforov@yadro.com>
2023-06-01 13:15:11 +00:00
406c2324d4 [#85] go.mod: Tidy
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-06-01 10:02:23 +03:00
10482ffbed [#82] client: Allow to pass gRPC dial options
Signed-off-by: Dmitrii Stepanov <d.stepanov@yadro.com>
2023-05-30 16:51:22 +03:00
f5b23eb225 [#74] netmap/parser: Update antlr4 generator
Also, add -no-listener option, as we use visitor only.

Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-05-19 17:47:24 +03:00
70f23dd1ea [#74] pre-commit: Exclude auto-generated files
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-05-19 17:47:24 +03:00
57f874048b [#74] go.mod: Update antlr4 dependency
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-05-19 17:47:24 +03:00
a397d1fd15 [#74] go.mod: Update dependencies
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-05-19 17:47:23 +03:00
9803c2816a [#74] pool: move to sync/atomic
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-05-19 14:58:30 +03:00
d04d96b42e [#74] go.mod: Move to go1.19
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-05-19 14:53:47 +03:00
9a072a8f49 [#68] Replace interface{} with any
Signed-off-by: Anton Nikiforov <an.nikiforov@yadro.com>
2023-05-15 17:21:49 +03:00
15b4287092 [#49] bearer: Allow empty eacl if token is impersonated
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-05-05 12:45:39 +03:00
d4fe9a193d [#66] transformer: Accept constructor in NextTarget
The code of frostfs-node is not yet ready to reuse egress target for
multiple objects, let's postpone until #64.

Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-05-03 11:22:09 +03:00
c42a6119ff [#66] transformer: Extend tests
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-05-03 11:22:09 +03:00
29b188db57 [#52] Remove storage groups and audit
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-05-03 08:20:37 +00:00
38b03ff28b [#63] transformer: Resolve funlen linter
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-04-25 13:29:42 +03:00
0fa23a9b14 [#63] transformer: Add context to methods
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-04-25 13:29:42 +03:00
d0762d037d [#44] pool: Add copies number vector when putting object
Signed-off-by: Artem Tataurov <a.tataurov@yadro.com>
2023-04-20 13:41:05 +03:00
db5b89496d [#49] bearer: Consider impersonate in fillBody
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-04-18 14:54:05 +00:00
7c75db2f2d [#59] netmap: Remove unused param from getSelection()
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-04-18 14:11:04 +00:00
dce55a436a [#9999] sdk: Up api-go version
Signed-off-by: Dmitrii Stepanov <d.stepanov@yadro.com>
2023-04-18 09:59:52 +00:00
cae2f37cdd client: rename formRequest() to buildRequest()
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-04-18 09:58:34 +00:00
f60bea4be5 [#48] client: Refactor SessionCreate()
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-04-18 09:58:34 +00:00
a16fc40c39 [#48] client: Refactor EndpointInfo()
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-04-18 09:58:34 +00:00
40d966bec2 [#48] client: Refactor NetworkInfo()
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-04-18 09:58:34 +00:00
Pavel Karpy
d0c5d837d2 [#56] *: Drop reputation system
Signed-off-by: Pavel Karpy <p.karpy@yadro.com>
2023-04-18 07:52:16 +00:00
237b90f744 [#49] bearer: Add impersonate flag
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-04-17 11:51:38 +03:00
c8e620ad24 [#53] sdk-go: Drop subnet
Signed-off-by: Dmitrii Stepanov <d.stepanov@yadro.com>
2023-04-14 16:31:08 +03:00
591dd1247d [#48] client: Refactor ContainerPut()
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-04-14 09:07:18 +03:00
57619fbbe4 [#48] client: Refactor ContainerSetEACL()
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-04-14 09:07:18 +03:00
8bc8f1f365 [#48] client: Refactor ContainerDelete()
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-04-14 09:07:18 +03:00
09ed2863fc [#48] client: Refactor ContainerEACL()
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-04-14 09:07:18 +03:00
8e2f77890f [#48] client: Refactor ContainerGet()
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-04-14 09:07:18 +03:00
8852b262f2 [#48] client: Refactor ContainerList()
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-04-14 09:07:18 +03:00
b2c66cb99e [#48] client: Refactor ContainerAnnounceUsedSpace()
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-04-14 09:07:18 +03:00
6c9b92c9dc [#48] client: Execute metainfo callback in processResponse()
It was lost in 8c5333ea55.

Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-04-14 09:07:18 +03:00
772fa90983 [#48] client/container: Form requests in a separate function
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-04-14 09:06:52 +03:00
1bfa9ecdb0 [#48] client: Remove ctx == nil checks
Much less useful after we made context to be passed explicitly.

Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-04-13 09:04:09 +03:00
55b06cd764 [#48] client: Split container methods by files
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-04-13 09:04:09 +03:00
Pavel Karpy
f41860f9bd [#46] client: Allow set copy_number for every placement vector
Signed-off-by: Pavel Karpy <p.karpy@yadro.com>
2023-04-11 10:06:55 +00:00
Pavel Karpy
423b320f91 [#46] go.mod: Update api-go version
Signed-off-by: Pavel Karpy <p.karpy@yadro.com>
2023-04-11 10:06:55 +00:00
f8c34b45f3 [#47] eacl: Fix duplicate imports
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-04-07 08:59:19 +03:00
fa9573e857 [#47] client: Pass context to Dial() explicitly
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-04-07 08:59:19 +03:00
bc62e2f712 [#47] pool: Resolve contextcheck and containedctx linters
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-04-07 08:59:19 +03:00
31271ad8b1 [#47] netmap: Make PlacementPolicy.WriteStringTo() pass nofunlen, cogognit
REP statement is obligatory, thus insert newline before each statement
after REP.

Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-04-07 08:59:19 +03:00
25e9336d68 [#47] container: Make readFromV2() pass funlen
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-04-07 08:59:19 +03:00
4cd755877c [#47] client: Do not check context in NetmapSnapshot()
It is passed explicitly, non-nil by convention.

Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-04-07 08:59:19 +03:00
1395b282fe [#47] acl: Remove unused resetBit()
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-04-07 08:59:19 +03:00
708d933fe3 [#47] .golangci.yml: Unify with other FrostFS repos
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-04-07 08:59:19 +03:00
4fa52312c7 [#47] reputation: Fix misspelling
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-04-07 08:59:19 +03:00
552219b8e1 [#16] pool: Fix counting context canceled error
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-03-29 15:58:04 +03:00
cfb8a7b914 [#41] .gitlint: Synchronize settings across FrostFS repos
This change allows to use `[#xx]` placeholders for issue number.

Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-03-24 22:07:13 +03:00
4438f115fb [#39] Add Issue Template
Add bug report and feature request templates

Signed-off-by: Liza <e.chichindaeva@yadro.com>
2023-03-23 12:25:57 +03:00
bec77f280a [#37] container: Support legacy sys attributes
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-03-16 11:14:42 +03:00
df2090c2be [#37] netmap: Update tests for new sys attributes
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-03-16 11:14:36 +03:00
7e6592b28e [#37] go.mod: Update api-go for new sys attributes
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-03-16 11:14:27 +03:00
d589d51509 [#19] transformer: Fix dependencies
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-03-16 09:51:03 +03:00
25588ee3be [#19] transformer: Do not allocate intermeate slice for hashers
```
name                 old time/op    new time/op    delta
Transformer/small-8    73.7µs ±15%    72.4µs ±16%    ~     (p=0.604 n=10+9)
Transformer/big-8       1.36s ± 4%     1.36s ± 8%    ~     (p=0.579 n=10+10)

name                 old alloc/op   new alloc/op   delta
Transformer/small-8    7.67kB ± 0%    7.57kB ± 0%  -1.36%  (p=0.000 n=10+10)
Transformer/big-8      49.0kB ± 0%    48.3kB ± 0%  -1.48%  (p=0.000 n=10+10)

name                 old allocs/op  new allocs/op  delta
Transformer/small-8       101 ± 0%        98 ± 0%  -2.97%  (p=0.000 n=10+10)
Transformer/big-8         609 ± 0%       591 ± 1%  -3.00%  (p=0.000 n=10+9)
```

Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-03-15 05:39:09 +00:00
9407f30248 [#19] transformer: Optimize payload hashers a bit
```
name                 old time/op    new time/op    delta
Transformer/small-8    74.8µs ±11%    73.7µs ±15%    ~     (p=0.529 n=10+10)
Transformer/big-8       1.38s ±11%     1.36s ± 4%    ~     (p=0.796 n=10+10)

name                 old alloc/op   new alloc/op   delta
Transformer/small-8    7.69kB ± 0%    7.67kB ± 0%  -0.21%  (p=0.000 n=10+10)
Transformer/big-8      49.2kB ± 0%    49.0kB ± 0%  -0.48%  (p=0.004 n=10+10)

name                 old allocs/op  new allocs/op  delta
Transformer/small-8       102 ± 0%       101 ± 0%  -0.98%  (p=0.000 n=9+10)
Transformer/big-8         620 ± 1%       609 ± 0%  -1.66%  (p=0.000 n=10+10)
```

Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-03-15 05:39:09 +00:00
94c0a607b5 [#19] transformer: Add a target which sends parts to a channel
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-03-15 05:39:09 +00:00
e45647de3c [#19] transformer: Do not reuse memory of sent objects
Slower, but more correct.
```
name                 old time/op    new time/op    delta
Transformer/small-8    72.4µs ± 8%    74.8µs ±11%     ~     (p=0.278 n=9+10)
Transformer/big-8       1.31s ± 8%     1.38s ±11%   +5.50%  (p=0.035 n=10+10)

name                 old alloc/op   new alloc/op   delta
Transformer/small-8    7.39kB ± 0%    7.69kB ± 0%   +4.04%  (p=0.000 n=10+10)
Transformer/big-8      46.9kB ± 0%    49.2kB ± 0%   +4.87%  (p=0.000 n=10+10)

name                 old allocs/op  new allocs/op  delta
Transformer/small-8      94.6 ± 1%     102.0 ± 0%   +7.82%  (p=0.000 n=10+9)
Transformer/big-8         560 ± 0%       620 ± 1%  +10.66%  (p=0.000 n=10+10)
```

Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-03-15 05:39:09 +00:00
611e20587b [#19] transformer/test: Check owner ID and payload hash for parts
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-03-15 05:39:09 +00:00
eba6831125 [#19] transformer/test: Add helper functions
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-03-15 05:39:09 +00:00
7e3810d654 [#19] transformer: Move EpochSource to other types
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-03-15 05:39:09 +00:00
cc0fef2c55 [#19] transformer: Merge formatter and payload splitter
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-03-15 05:39:09 +00:00
b696d3c70e [#19] transformer: Make writeChunk non-recursive
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-03-15 05:39:09 +00:00
1c94309d7a [#19] transformer: Simplify AccessIdentifiers
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-03-15 05:39:09 +00:00
f43f18ecda [#19] transformer: Cover with unit-tests
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-03-15 05:39:09 +00:00
ac8442bf99 [#19] object: Move transformer implementation from node
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-03-15 05:39:09 +00:00
0ad877288e [TrueCloudLab#16] pool: Don't count grpc canceled error
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-03-10 06:58:34 +00:00
0e1999c965 [#23] pre-commit: Add gitlint hook
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-03-09 22:53:16 +03:00
b461aa64b8 [#23] pre-commit: Add golangci-lint hook
Also, fix minor issues. Skip deprecated warning for now.

Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-03-09 22:53:14 +03:00
b761fd8070 [#23] pre-commit: Add initial configuration
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-03-09 22:52:51 +03:00
94476f9055 Rename package name
Due to source code relocation from GitHub.

Signed-off-by: Alex Vanin <a.vanin@yadro.com>
2023-03-07 15:47:21 +03:00
5e759bf089 [#2] Remove panic from RPCs
Signed-off-by: Anton Nikiforov <an.nikiforov@yadro.com>
2023-03-01 10:29:23 +03:00
d4f5bba459 [#2] Update lint config, fix lint errors
Signed-off-by: Anton Nikiforov <an.nikiforov@yadro.com>
2023-03-01 10:29:23 +03:00
e9c1a2ab2b [TrueCloudLab/hrw#2] sdk-go: Optimize node hash
Compute node hash by node initialization

Signed-off-by: Dmitrii Stepanov <d.stepanov@yadro.com>
2023-02-28 18:07:14 +03:00
2cbc585edd [TrueCloudLab/hrw#2] sdk-go: Use typed HRW methods
Update HRW pkg and use typed HRW methods to sort nodes

Signed-off-by: Dmitrii Stepanov <d.stepanov@yadro.com>
2023-02-28 18:07:14 +03:00
e355e5eeba [TrueCloudLab#22] .github: Fix CODEOWNERS
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-02-28 16:35:38 +03:00
f08069ceeb [#20] .github: Update CODEOWNERS
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-02-27 17:47:21 +03:00
dad99bad48 [#20] .github: Drop go1.17, add go1.20
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-02-27 17:47:21 +03:00
0d3a238d9c [#5] pool: Update hashicorp/lru to v2
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-02-24 17:32:40 +03:00
cf64ddfb14 [TrueCloudLab#13] pool: Renew token before it expired
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-01-30 15:06:02 +03:00
cf9a54dcda [TrueCloudLab#11] pool: Fix handling SplitInfoError
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-01-23 11:07:29 +03:00
Denis Kirillov
b2a37543d3 [#362] pool: Don't use default session token for read
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2023-01-13 13:21:29 +03:00
f0ac49b8f0 [TrueCloudLab#6] pool: Decrease rebalanceInterval
Set default rebalance interval to 15s

Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-01-13 13:16:22 +03:00
54696acf48 [TrueCloudLab#6] pool: Log node healthy changing
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2023-01-13 13:16:22 +03:00
dd88a5c5e0 [#4] go.mod: Bump supported go version to 1.18
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2022-12-30 13:40:50 +03:00
4ff9c00de3 [#4] Rename NeoFS mentions in comments and method names
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2022-12-30 13:40:50 +03:00
b204a62da1 [#4] go.mod: Update ANTLR
Current `go:generate` command was, probably, executed only on my laptop.
Replace it with explicit version, because package in the generated code
depends on in.

Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2022-12-30 13:40:50 +03:00
377a5a0517 [#4] go.mod: Update dependencies
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2022-12-30 13:40:50 +03:00
43c046f343 [#4] go.mod: Update neo-go to v0.100.1
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2022-12-30 13:40:50 +03:00
Denis Kirillov
8c0c7789ca [#365] pool: Add request callback
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2022-12-29 16:34:13 +03:00
Evgenii Stratonikov
339e2702f8 [#367] client: Allow to override key in Object.Hash RPC
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2022-12-29 16:34:13 +03:00
Pavel Karpy
a1748ae0e7 [#370] bearer, session: Clarify expiration epoch
The expiration epoch is the _last_ valid epoch for an entity. Also, clarify
the expiration epoch meaning for tombstones and regular objects.

Signed-off-by: Pavel Karpy <p.karpy@yadro.com>
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2022-12-29 16:34:13 +03:00
Pavel Karpy
4c779423f5 Move to frostfs-sdk-go
Signed-off-by: Pavel Karpy <p.karpy@yadro.com>
2022-12-14 09:59:29 +03:00
Pavel Karpy
45a6e7a7c2 [#369] status: Make errors return default messages
Use default messages in `Error` methods like in `ToStatusV2`.

Signed-off-by: Pavel Karpy <p.karpy@yadro.com>
2022-12-14 09:59:29 +03:00
Denis Kirillov
b4b07a3c4e [#364] pool: Adopt client stream timeout param
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-11-15 17:08:20 +03:00
Denis Kirillov
d047289182 [#360] object: Add new package relations
Package relations provides feature to process inner object structure.

Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-11-14 17:46:17 +03:00
Denis Kirillov
1cacf472a3 [#360] pool: Fix removing virtual object
Include phy objects in session token scope

Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-11-14 17:46:17 +03:00
Denis Kirillov
e35f0df1ca [#358] pool: Make ErrPoolClientUnhealthy unexported
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-11-09 10:22:33 +03:00
Denis Kirillov
5f9d846fb4 [#358] pool: Don't init session with dead node
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-11-09 10:22:33 +03:00
Denis Kirillov
2eefdab0e4 [#358] pool: Start even if not all node healthy
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-11-09 10:22:33 +03:00
Denis Kirillov
7a2a76af95 [#356] ns: Use domain instead of name in NNS resolver
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-10-28 12:24:56 +04:00
Evgenii Stratonikov
da4ddcf337 [#353] netmap: Get rid of ioutil
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-10-19 12:05:49 +04:00
Evgenii Stratonikov
231ec66054 [#353] go.mod: Update external dependencies
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-10-19 12:05:49 +04:00
Evgenii Stratonikov
a02a3870a5 [#353] go.mod: Update neofs-api-go to v2.14.0
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-10-19 12:05:49 +04:00
Evgenii Stratonikov
9d4b36a8e6 [#353] go.mod: Update neo-contract to v0.16.0
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-10-19 12:05:49 +04:00
Evgenii Stratonikov
d3a09cbe7f [#353] go.mod: Update neo-go to v0.99.4
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-10-19 12:05:49 +04:00
anastasia prasolova
f21d63bded Add CODEOWNERS file
Signed-off-by: anastasia prasolova <anastasia@nspcc.ru>
2022-10-16 21:06:24 +03:00
21eef1ae7f [#351] Add StringifyPublicKey method for NodeInfo
Signed-off-by: Anton Nikiforov <an.nikiforov@yadro.com>
2022-10-13 10:27:18 +03:00
Leonard Lyubich
d2f3929b51 [#349] client: Use context.Background in Dial by default
Passing `nil` context to `rpc.Balance` leads to panic. To prevent panic,
we need to provide some default context in `Dial`.

Signed-off-by: Leonard Lyubich <ctulhurider@gmail.com>
2022-10-12 17:50:55 +04:00
Leonard Lyubich
8c682641bf [#343] pool: Provide dial context to clients
After recent changes `client.Client` accepts dial context. There is a
need to forward the context passed into `Pool.Dial` to the underlying
`Client` instances.

Define type aliases of different client constructors: context-based and
non-context. Use context-based constructor in `Pool`. Pass `ctx`
parameter of `Pool.Dial` method to the client builder.

Signed-off-by: Leonard Lyubich <ctulhurider@gmail.com>
2022-10-07 13:24:02 +03:00
Leonard Lyubich
452a50e9d5 [#343] client: Accept context parameter in Dial
In previous implementation of `Client.Dial` there was no ability to
specify parent context (e.g. global application context).

Add `PrmDial.SetContext` method which accepts optional base dial
context. Use the context to open client connection or fall back to using
`context.Background()`.

Upgraded version of `github.com/nspcc-dev/neofs-api-go/v2` module
also fixes the problem when dial timeout didn't work properly.

Signed-off-by: Leonard Lyubich <ctulhurider@gmail.com>
2022-10-07 13:24:02 +03:00
Leonard Lyubich
1325b4f272 [#336] session: Support group object sessions
Rename `LimitByObject` method of the `session.Object` type to
`LimitByObjects` and make it to accept variadic parameter.

Signed-off-by: Leonard Lyubich <ctulhurider@gmail.com>
2022-10-05 13:39:51 +04:00
Evgenii Stratonikov
c6576c8112 [#344] netmap: Support ExternalAddr well-known attribute
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-09-26 13:28:39 +03:00
Leonard Lyubich
8e3173eacd [#270] client/netmap: Cover NetMapSnapshot with unit tests
There is a need to test each `Client` operation. In previous
implementation `Client` was based on real socket connection. This didn't
allow to test the `Client` without OS resources. In order to write
convenient and useful unit tests we need to slightly refactor the code.

Introduce `neoFSAPIServer` interface as a provider of `Client` type's
abstraction from the exact NeoFS API server. Add `netMapSnapshot` method
for initial implementation. Define core interface provider used in real
code. Set `coreServer` as an underlying `neoFSAPIServer` in
`Client.Dial`. Cover `Client.NetMapSnapshot` method with unit tests
using the opportunity to override the server.

From now client library can be tested not only with real physical
listeners but with imitations.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-09-22 10:51:07 +04:00
Leonard Lyubich
89124d442d [#312] client/netmap: Simplify NetMapSnapshot implementaiton
Make flat set of instructions similar to `ObjectDelete` operation.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-09-22 10:51:07 +04:00
Leonard Lyubich
664392afc2 [#312] netmap: Support NetmapService.NetmapSnapshot RPC
Extend functionality of `NetMap` type. Add `NetMapSnapshot` operation to
`client` package.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-09-22 10:51:07 +04:00
Leonard Lyubich
f2f97f656d [#315] apistatus: Write default message in NodeUnderMaintenance.Error
Use `node is under maintenance` message in `NodeUnderMaintenance.Error`
is `message` field is unset in the status message.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-09-20 11:23:22 +04:00
Leonard Lyubich
be9a1aca90 [#315] apistatus: Support NodeUnderMaintenance in FromStatusV2
Support decoding `NodeUnderMaintenance` status errors in `FromStatusV2`
function.

From now `NodeUnderMaintenance` instance can be decoded from
`status.Status` message of NeoFS API V2 protocol.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-09-20 11:23:22 +04:00
Evgenii Stratonikov
3d6b5d807b [#315] netmap: Add maintenance node state
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-09-19 16:49:27 +04:00
Evgenii Stratonikov
3dad44232e [#315] netmap: Add maintenance mode network setting
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-09-19 16:49:27 +04:00
Evgenii Stratonikov
4662d39886 [#315] client/status: Add NodeUnderMaintenance status
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-09-19 16:49:27 +04:00
Evgenii Stratonikov
f75a5feba3 [#334] client: Use parameter key in object.Get
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-09-14 11:34:56 +04:00
Evgenii Stratonikov
1929b634a1 [#332] netmap/parser: Disallow trailing garbage
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-09-12 10:45:37 +03:00
Evgenii Stratonikov
402d72e629 [#332] go.mod: Update antlr dependency
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-09-12 10:45:37 +03:00
Evgenii Stratonikov
43a57d42dd [#331] client: Allow to set timeout for streaming operations
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-09-07 11:51:28 +04:00
Pavel Karpy
71891029da [#327] nns: Support Null returned value
Signed-off-by: Pavel Karpy <carpawell@nspcc.ru>
2022-09-06 17:27:07 +04:00
Pavel Karpy
d808f72c38 [#327] Add go 1.19 tests
Signed-off-by: Pavel Karpy <carpawell@nspcc.ru>
2022-09-06 17:27:07 +04:00
Pavel Karpy
511886b8d2 [#327] Update go to 1.17
Signed-off-by: Pavel Karpy <carpawell@nspcc.ru>
2022-09-06 17:27:07 +04:00
Pavel Karpy
01c238ddc0 [#327] nns: Simplify code using neo-go v0.99.2
Signed-off-by: Pavel Karpy <carpawell@nspcc.ru>
2022-09-06 17:27:07 +04:00
Pavel Karpy
2e5c66934c [#327] go.mod: Update neo-go to v0.99.2
Signed-off-by: Pavel Karpy <carpawell@nspcc.ru>
2022-09-06 17:27:07 +04:00
Denis Kirillov
74234623b2 [#215] object: Add well-known FilePath attribute
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-09-06 17:21:37 +04:00
Evgenii Stratonikov
ee92df3203 [#326] client: Sign message in object.GetRange'
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-08-29 14:45:50 +03:00
Evgenii Stratonikov
cf7bee3087 [#326] client: Provide pointers requests for signing
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-08-29 14:45:50 +03:00
Evgenii Stratonikov
456167e777 [#325] go.mod: Update neofs-api-go
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-08-29 11:53:34 +03:00
Evgenii Stratonikov
0e4d07fb06 [#323] client: Remove xheaders length check in a common helper
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-08-26 17:34:27 +03:00
Evgenii Stratonikov
d6d6a41f5d [#323] client: Refactor object.Put
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-08-26 17:34:27 +03:00
Evgenii Stratonikov
724d30db1a [#323] client: Refactor object.Get
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-08-26 17:34:27 +03:00
Evgenii Stratonikov
1f593d0fb2 [#323] client: Refactor object.GetRange
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-08-26 17:34:27 +03:00
Evgenii Stratonikov
f543ba68d3 [#323] client: Refactor object.Delete
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-08-26 17:34:27 +03:00
Evgenii Stratonikov
dd5826d071 [#323] client: Refactor object.Head
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-08-26 17:34:27 +03:00
Evgenii Stratonikov
02bc2bc236 [#323] client: Unify parameter processing for Get* methods
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-08-26 17:34:27 +03:00
Evgenii Stratonikov
992b26a3ff [#323] client: Form meta header during parameter initialization
Unify with other client methods.

Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-08-26 17:34:27 +03:00
Evgenii Stratonikov
6a43accf96 [#323] client: Refactor object.Search
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-08-26 17:34:27 +03:00
Evgenii Stratonikov
5d7650c3e7 [#323] client: Use checksum parameter directly in object.Hash
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-08-26 17:34:27 +03:00
Evgenii Stratonikov
a926e5a1de [#323] client: Replace writeToMetaHeader method with function
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-08-26 17:34:27 +03:00
Evgenii Stratonikov
8c5333ea55 [#323] client: Refactor object.Hash
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-08-26 17:34:27 +03:00
Evgenii Stratonikov
0236b03fa7 [#323] client: Use constant for error message
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-08-26 17:34:27 +03:00
Evgenii Stratonikov
2f843de3ed [#323] client: Remove initCallContextWithoutKey
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-08-26 17:34:27 +03:00
Evgenii Stratonikov
84888854ab [#322] *: Go fmt -s
go1.19 rewrites comments to proper render them in docs.

Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-08-24 18:58:59 +03:00
Evgenii Stratonikov
7537fa0dec [#321] client: Set object ID field in Put
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-08-24 17:04:10 +03:00
Pavel Karpy
7578b54fac [#319] crypto: Fix signing in go v1.19
Signed-off-by: Pavel Karpy <carpawell@nspcc.ru>
2022-08-22 13:19:10 +03:00
Leonard Lyubich
737e690482 [#299] pool: Do not use pointers to the resulting values
In previous implementation `pool` package provided access to resulting
values as pointers to them. This caused clients to handle nil
cases even when the field presence in the response is required.

Avoid returning pointers to values in result getters. This also reduces
reference counter load and allows values from `client.Client` to be
forwarded directly without additional assignment.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-08-16 15:31:57 +04:00
Leonard Lyubich
f8148c954b [#317] client: Fix processing of the Object PUT failed response
In previous implementation `ObjectWrite.Close` called `close` method
which in turn called `result` callback. Thus failed statuses could lead
to false-positive result processing.

Replace calling `close` method with direct `closer` method's call in
`ObjectWrite.Close`.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-08-16 15:31:37 +04:00
Leonard Lyubich
4e31b4f231 [#299] client: Do not use pointers to required response fields
In previous implementation `client` package provided access to nested
response fields as pointers to them. This caused clients to handle nil
cases even when the field presence in the response is required.

Avoid returning pointers to required fields in response getters. This
also reduces reference counter load and allows fields to be decoded
directly without additional assignment.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-08-08 12:55:25 +03:00
Denis Kirillov
30bf79f075 [#302] client: Adopt SetCopiesNumber
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-08-03 15:22:13 +03:00
Pavel Karpy
7a99cc916c [#307] status: Support EACL_NOT_FOUND status code
Signed-off-by: Pavel Karpy <carpawell@nspcc.ru>
2022-08-01 19:57:07 +03:00
Pavel Karpy
a0f7c903d3 [#307] go.mod: Update neofs-api-go to v2.13.1
Signed-off-by: Pavel Karpy <carpawell@nspcc.ru>
2022-08-01 19:57:07 +03:00
Evgenii Stratonikov
7de66159d4 [#304] go.mod: Update neo-go to v0.99.1
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-07-29 11:43:51 +03:00
Denis Kirillov
90255e9efa [#291] pool: Use more robust way to init methods
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-07-26 21:47:06 +03:00
Denis Kirillov
828cfdc5bf [#291] pool: Improve docs
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-07-26 21:47:06 +03:00
Denis Kirillov
90f1cc7a1a [#301] ns: Adopt neo-go int parameter
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-07-25 13:14:11 +03:00
Evgenii Stratonikov
3a95686aab [#300] go.mod: Update neofs-contract
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-07-22 17:02:58 +03:00
Denis Kirillov
f4ac75423c [#283] pool: Store methods status in slice
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-07-22 15:57:37 +03:00
Denis Kirillov
b4f4ee4f79 [#283] pool: Compute avg time for every method
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-07-22 15:57:37 +03:00
Denis Kirillov
54145916a9 [#283] pool: Change batch of atomics to mutex
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-07-22 15:57:37 +03:00
Denis Kirillov
0d54757545 [#283] pool: Add res nil check
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-07-22 15:57:37 +03:00
Denis Kirillov
58fe1768cc [#283] pool: Fix incErrorRate
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-07-22 15:57:37 +03:00
Denis Kirillov
0b8c53ebc9 [#283] pool: Add number of requests to statistic
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-07-22 15:57:37 +03:00
Denis Kirillov
1b30d228da [#283] pool: Expose statistic
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-07-22 15:57:37 +03:00
Denis Kirillov
423804de84 [#283] pool: Add latency calculating
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-07-22 15:57:37 +03:00
Denis Kirillov
99e185690e [#283] pool: Add error threshold
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-07-22 15:57:37 +03:00
Denis Kirillov
9d3a1835d1 [#283] pool: Drop gomock
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-07-22 15:57:37 +03:00
Denis Kirillov
e6cb5f2ee1 [#283] pool: Add counter for errors
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-07-22 15:57:37 +03:00
Leonard Lyubich
7d10b432d1 [#284] *: Return error from all ReadFromV2 methods
Return `error` from all `ReadFromV2` methods in order to support
backward compatibility if message will be extended with some formatted
field.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-07-07 14:20:18 +03:00
Evgenii Stratonikov
0d862d8568 [#294] go.mod: Update tzhash to v1.6.1
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-07-06 18:10:41 +03:00
Pavel Karpy
48ba86f4dd [#293] version: Update API version constants
Signed-off-by: Pavel Karpy <carpawell@nspcc.ru>
2022-07-06 12:05:58 +03:00
Evgenii Stratonikov
f91b1facd5 [#292] go.mod: Update tzhash to v1.6.0
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-07-05 20:44:56 +03:00
Pavel Karpy
c6fee6d01a [#240] pool: Add SyncContainerWithNetwork
It is a helper function that uses a `Pool` to fetch network information and
apply it to the container (change the container if it does not fit the
network). Currently, only applies the homomorphic hashing parameter.

Signed-off-by: Pavel Karpy <carpawell@nspcc.ru>
2022-07-05 15:37:49 +03:00
Pavel Karpy
a6ecf6b881 [#240] client: Add SyncContainerWithNetwork func
It requests network configuration and syncs it (changes if required) with
passed container.

Signed-off-by: Pavel Karpy <carpawell@nspcc.ru>
2022-07-05 15:37:49 +03:00
Pavel Karpy
9996b3be01 [#240] container: Support disabling homomorphic hashing
Signed-off-by: Pavel Karpy <carpawell@nspcc.ru>
2022-07-05 15:37:49 +03:00
Pavel Karpy
0cd790cfe0 [#240] netmap: Support HomomorphicHashingDisabled network config
Signed-off-by: Pavel Karpy <carpawell@nspcc.ru>
2022-07-05 15:37:49 +03:00
Pavel Karpy
df6538c68c [#282] apistatus: Support OUT_OF_RANGE error
Signed-off-by: Pavel Karpy <carpawell@nspcc.ru>
2022-07-05 11:53:29 +03:00
Pavel Karpy
27fe9c19a7 [#273] status: Support signature status
Signed-off-by: Pavel Karpy <carpawell@nspcc.ru>
2022-07-05 11:50:44 +03:00
Leonard Lyubich
6994eb0e55 [#222] client: Fix docs of resolving the failure statuses
Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-07-05 11:43:38 +03:00
Leonard Lyubich
cec2373b50 [#222] reputation: Clarify docs of GlobalTrust formation
Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-07-05 11:42:58 +03:00
Leonard Lyubich
ff09b1cfe1 Upgrade NeoFS API Go to v2.13.0
Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-07-05 11:42:58 +03:00
Leonard Lyubich
041e1ef2b6 [#286] reputation: Refactor and document package functionality
Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-07-05 11:42:58 +03:00
Leonard Lyubich
30d27c3050 [#289] oid: Add JSON encoding of Address type
Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-07-04 14:58:00 +03:00
Leonard Lyubich
2ad89085a3 [#225] container: Clarify docs of Container type initialization
Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-07-04 11:21:16 +03:00
Leonard Lyubich
70845147f6 [#225] container: Refactor and document package functionality
Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-07-04 11:21:16 +03:00
Angira Kekteeva
86a447bc80 [#288] pool: Fix lost err in fmt.Errorf
Signed-off-by: Angira Kekteeva <kira@nspcc.ru>
2022-07-01 15:09:26 +03:00
Leonard Lyubich
ab4d1e34a8 [#280] policy: Use policyVisitor as ErrorListener for QueryLexer
Share `policyVisitor` instance as an `ErrorListener` provider for both
`QueryLexer` and `Query`.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-06-28 14:47:22 +03:00
Leonard Lyubich
dea3d9c419 [#280] policy: Remove error listeners from query lexer
In previous implementation `PlacementPolicy.DecodeString` produced
undesired console logs.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-06-28 14:47:22 +03:00
Leonard Lyubich
d2cd9ebfbd [#278] client: Increase depth of error wrapping in IsErr* tests
Wrap test error twice just to be sure error unwrapping goes deep.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-06-28 09:06:13 +03:00
Leonard Lyubich
1e1139f305 [#278] pool: Avoid using strings.Contains for error assertion
`client` package now provides functions for error checking.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-06-28 09:06:13 +03:00
Leonard Lyubich
dcaf454c1d [#278] client: Add session error checkers
Add `IsErrSessionExpired` and `IsErrSessionNotFound` functions which
assert corresponding session errors.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-06-28 09:06:13 +03:00
Leonard Lyubich
09ed6077f9 [#278] client: Support wrapped errors in error checkers
Client errors are quite often wrapped in context. When checking a
specific error, it is required not to lose the ability to determine it,
regardless of the attached context. In previous implementation `IsErr*`
functions didn't support wrapped errors.

Make `IsErr*` functions to preliminarily unwrap `error` argument before
type assertion. Use `errors.Unwrap` for this instead of `errors.As`
since the latter has the overhead of filling in an error.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-06-28 09:06:13 +03:00
Leonard Lyubich
40942affe9 [#278] client: Add unit test for error checkers
`IsErr*` functions assert `error` argument corresponds to particular
typed error.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-06-28 09:06:13 +03:00
Leonard Lyubich
aa5ee1dcde [#225] container/acl: Make Basic type numeric
Also make well-known values constant not variables.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-06-23 11:05:32 +03:00
Leonard Lyubich
3b15a01327 [#225] container/acl: Fix minor places
Make test value generator to be random as documented. Add `default`
keyword to switch-case to avoid linter comments.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-06-23 11:05:32 +03:00
Leonard Lyubich
c4ebe8d854 [#225] container: Replace basic ACL code to a separate package
Create `acl` package inside `container` path. Replace basic ACL
functionality into it.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-06-23 11:05:32 +03:00
Leonard Lyubich
e82a2d86ef [#225] container: Refactor and document basic ACL
Replace basic ACL functionality from `acl` package to the `container`
one. Create `BasicACL` type and provide convenient interface to work
with it.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-06-23 11:05:32 +03:00
Evgenii Stratonikov
af7e20073b [#277] netmap: Allow more uint64 config values
NEO VM uses little-endian format for integers,
however the resulting byte slice can contain less than 8 bytes.

Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-06-22 15:51:36 +03:00
Evgenii Stratonikov
596f43a540 [#274] crypto: Add WalletConnect API support
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-06-22 11:20:54 +03:00
Leonard Lyubich
721df386c5 [#276] container: Remove session token and signature from Container/eACL
Session token and signature isn't presented in `Container` and
`EACLTable` messages of NeoFS API V2 protocol. These entities are needed
for access control and doesn't carry payload of these messages.

Remove `SetSessionToken` / `SessionToken` methods of
`container.Container` and `eacl.Table` types. Provide methods to specify
these components in corresponding `Client` operations.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-06-21 20:03:07 +03:00
Leonard Lyubich
e986f47807 [#227] netmap: Add more encoding methods for PlacementPolicy
Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-06-16 11:23:21 +03:00
Leonard Lyubich
e999fb00c3 [#227] netmap: Remove unnecessary unit tests
Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-06-16 11:23:21 +03:00
Leonard Lyubich
d51a324147 [#227] netmap: Clarify some fields of PlacementPolicy
Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-06-15 20:50:32 +03:00
Leonard Lyubich
d648b86776 [#227] netmap: Fix minor blots in documentation/code
Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-06-15 20:50:32 +03:00
Leonard Lyubich
ca523f1ff1 [#227] netmap: Refactor and document package functionality
Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-06-15 20:50:32 +03:00
Leonard Lyubich
5bfdb64251 [#227] netmap: Do not export bucket weight calculator
`GetBucketWeight` is not usable outside placement context.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-06-15 20:50:32 +03:00
Leonard Lyubich
86bdc670d5 [#227] netmap: Do not export default weightFunc initializer
`GetDefaultWeightFunc` is not usable outside placement context.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-06-15 20:50:32 +03:00
Leonard Lyubich
6796b4a29a [#227] netmap: Remove unused aggregators and normalizers
Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-06-15 20:50:32 +03:00
Leonard Lyubich
723ba5ee45 [#227] netmap: Do not use intermediate types for placement
Support preprocessing within the `NodeInfo` type. Provide methods for
placement directly from the `NodeInfo` type. Returns slice of slices of
`NodeInfo` from placement methods of `Netmap`. Remove no longer needed
`Node` and `Nodes` types.

```
name            old time/op    new time/op    delta
ManySelects-12    19.7µs ±14%    15.8µs ±15%  -19.70%  (p=0.000 n=20+20)

name            old alloc/op   new alloc/op   delta
ManySelects-12    8.65kB ± 0%    6.22kB ± 0%  -28.03%  (p=0.000 n=20+20)

name            old allocs/op  new allocs/op  delta
ManySelects-12      82.0 ± 0%      81.0 ± 0%   -1.22%  (p=0.000 n=20+20)
```

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-06-15 20:50:32 +03:00
Leonard Lyubich
2b21146185 [#227] netmap/test: Assert source placement builder is pure for nodes
`GetContainerNodes` / `GetPlacementVectors` must not mutate source slice
of nodes.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-06-15 20:50:32 +03:00
Leonard Lyubich
9c47fca7c2 [#227] netmap: Pre-calculate resulting slice capacity in flattenNodes
```
name           old time/op  new time/op  delta
ManySelects-12  17.5µs ±27%   15.5µs ±24%  -11.57%  (p=0.024 n=20+20)

name           old alloc/op  new alloc/op  delta
ManySelects-12  9.03kB ± 0%   8.65kB ± 0%  -4.25%  (p=0.000 n=20+20)

name           old allocs/op  new allocs/op  delta
ManySelects-12  86.0 ± 0%     82.0 ± 0%     -4.65%  (p=0.000 n=20+20)
```

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-06-15 20:50:32 +03:00
Leonard Lyubich
a8fd2ef954 [#227] netmap: Benchmark JSON tests
Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-06-15 20:50:32 +03:00
Leonard Lyubich
ea21cdf731 [#269] client: Fix response processing of Object.Put rpc
After 85e3c7b087 `processResponse` method
sets `err` field or sets status in `statusRes` field of `contextCall`.
In previous implementation `ObjectWriter.Close` method returned
`ctxCall.err` on `false` return of `processResponse` method. This could
cause NPE-panic if status failure resolving was disabled.

Make `ObjectWriter.Close` to return internal `err` field only if it is
set, otherwise return status response.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-06-15 15:30:39 +03:00
Leonard Lyubich
eb3b990812 [#210] subnet: Refactor and document package functionality
Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-06-15 11:52:07 +03:00
Pavel Karpy
6709b00c89 [#245] storagegroup: Add Object <-> SG converters
Signed-off-by: Pavel Karpy <carpawell@nspcc.ru>
2022-06-15 10:33:47 +03:00
Evgenii Stratonikov
517d7a1e4a [#266] eacl: Allow lossless conversion of reserved filters
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-06-09 09:45:32 +03:00
Pavel Karpy
6ac9deabb8 [#264] storagegroup: Fix out of range
It panicked when the previous members slice had capacity more than a new one
because of incorrect slicing that led to `out of range`.

Signed-off-by: Pavel Karpy <carpawell@nspcc.ru>
2022-06-09 09:38:02 +03:00
Evgenii Stratonikov
5518b63432 [#243] eacl: Return success flag in CalculateAction
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-06-09 09:35:32 +03:00
Leonard Lyubich
031eac2f48 [#265] session: Implement method to verify session data signature
There is a need to verify session data signatures calculated using
private session key. `Container` token encapsulates public session key,
so we need to provide method for signature check.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-06-07 21:53:39 +03:00
Leonard Lyubich
67ff996dc3 Revert "[#261] session: Make failed Sign pure on failed signature calculation"
This reverts commit 458c882ff4.

If `issuerSet` is set after signature calculation then issuer ID isn't
written to signed data, in other words not signed.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-06-06 10:02:17 +03:00
Leonard Lyubich
bf8312a547 [#263] session: Add Sign / VerifySignature test
Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-06-06 10:02:17 +03:00
Leonard Lyubich
0ef49cf851 Upgrade NeoFS API Go to v2.12.2
Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-06-03 17:07:45 +03:00
Leonard Lyubich
458c882ff4 [#261] session: Make failed Sign pure on failed signature calculation
Make `Sign` method implementation to not modify `issuerSet` state
variable after signature calculation's failures.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-06-03 14:46:18 +03:00
Leonard Lyubich
0bb40b3245 [#261] session: Share common code between Container and Object
`Container` and `Object` types are transmitted in single `session.Token`
message. They differ only by session context.

Share common parts of the message in `commonData` struct. Embed struct
into `Container` and `Object`. Make `ReadFromV2` methods to check
protocol compliance. Make `Unmarshal`/`UmarshalJSON` to check field
format in case of presence only.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-06-03 14:46:18 +03:00
Pavel Karpy
d3b998d672 [#250] eacl: Do not require CID in eACL table
Container ID of extended ACL table can be omitted in bearer token according
to API.

Signed-off-by: Pavel Karpy <carpawell@nspcc.ru>
2022-06-03 14:19:57 +03:00
Leonard Lyubich
c65be6d469 [#259] bearer: Align with the protocol
Sync lifetime names with `session` package. Add `AssertContainer` and
`AssertUser` methods which cover specific cases describe in protocol.
Increase test coverage.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-06-01 18:32:49 +03:00
Leonard Lyubich
82d762f536 [#258] Fix string encoding of identifiers
Use `EncodeToString` method to get protocol string according to type
docs.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-05-31 12:14:04 +03:00
Leonard Lyubich
6cb513c976 [#257] Upgrade NeoFS API Go module
New version contains fix for `object.GetRangeResponse` message type.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-05-31 09:47:55 +03:00
Denis Kirillov
3953c2166e [#255] Create session token using arbitrary key
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-05-30 20:09:43 +03:00
Leonard Lyubich
85e3c7b087 [#254] client: Fix incorrect response processing case
In previous implementation `processResponse` returned `true` if
`resolveAPIFailures` is set and status failure is received. This led to
post-processing of the results, which no longer pays attention to the
status. For example, `ObjectHead` returned `unexpected header type`
error due to empty body.

Make `contextCall.processResponse` to return success flag regardless of
`resolveAPIFailures` setting. Make `contextCall.processCall` to return
`err` field presence flag on `processResponse` false return.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-05-30 17:23:56 +03:00
Leonard Lyubich
3bbf7ee15d [#252] object/id: Fix typos in docs
Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-05-27 12:41:35 +03:00
Leonard Lyubich
85affc3c93 object: Remove debug printline
Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-05-27 12:41:35 +03:00
Leonard Lyubich
f0a5eb6dbc [#251] object/address: Refactor and document package functionality
Merge `address` package into `oid` one. Bring `session.Object`
implementation into conformity with the NeoFS API protocol.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-05-27 12:41:35 +03:00
Leonard Lyubich
bef4618cd6 [#252] session: Add Object.Issuer method
Add method to get `Object` session's issuer similar to `Container`.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-05-26 09:54:57 +03:00
Evgenii Stratonikov
1f7fe6864d [#229] netmap: make context private
It isn't used anywhere outside of the package.

Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-05-25 11:02:51 +03:00
Leonard Lyubich
60ef026923 [#248] session: Add Issuer method
There is a need to duplicate session token owner, e.g. in container
created within the session. For such cases we need to have the ability
to receive session issuer.

Add `Container.Issuer` method. Transform `IssuedBy` to helper function.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-05-25 10:57:53 +03:00
Evgenii Stratonikov
c976332e20 [#242] object: Require only 1 field to be set for SplitInfo
Also write more tests.

Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-05-23 19:09:53 +03:00
Alex Vanin
12ea1e8d74 [#238] session: Add container.IssuedBy method
Signed-off-by: Alex Vanin <alexey@nspcc.ru>
2022-05-04 22:24:02 +03:00
Alex Vanin
539ac9915e [#237] client: Sign container ID value at container removal
Container contract expects signature of container ID value,
which is SHA256 of container body. Not the signature of stable
marshaled container.ID structure.

Signed-off-by: Alex Vanin <alexey@nspcc.ru>
2022-05-04 15:22:48 +03:00
Leonard Lyubich
526b45e207 [#197] session: Link issues for further TODO resolving
Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-04-27 11:09:29 +03:00
Leonard Lyubich
5fe6d96bf1 [#197] session: Fix Container.AppliedTo method docs
Method checks propagation, not limit.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-04-27 11:09:29 +03:00
Leonard Lyubich
4cbbbdd3e2 [#197] pool: Change function signature
Make `createSessionTokenForDuration` to accept a pointer to
`session.Object` and write the response on session creation request
through the pointer. Rename function to `initSessionForDuration`. As a
consequence, problem in `openDefaultSession` with uncached sessions is
fixed.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-04-27 11:09:29 +03:00
Leonard Lyubich
f0134ef26e [#197] pool: Remove no longer needed test func
`TestCopySessionTokenWithoutSignatureAndContext` was based on already
removed function.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-04-27 11:09:29 +03:00
Leonard Lyubich
e468f409d7 [#197] pool: Remove unused function parameter
`owner.ID` parameter of `createSessionTokenForDuration` function is no
longer used since session owner is set automatically during the sign
operation. As a consequence, remove `Pool.sessionOwner` field and its
getter.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-04-27 11:09:29 +03:00
Leonard Lyubich
4dc3a7669e [#197] client: Link FIXME with an issue
Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-04-27 11:09:29 +03:00
Leonard Lyubich
48434d9999 [#197] client: Remove no longer used type
Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-04-27 11:09:29 +03:00
Leonard Lyubich
552c7875bf [#197] session: Refactor and document the package
Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-04-27 11:09:29 +03:00
Denis Kirillov
497053c785 [#228] ns: use neofs-contract for nns constants
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-04-24 14:11:16 +03:00
Alex Vanin
6e81e13e1b [#218] client: Decode status at io.EOF in the client-side stream
Object service of NeoFS API contains one client-side stream method:
object.Put. In client-side streams, server can return an error after
processing stream message. In this case write method returns `io.EOF`
and actual error reason is encoded in response status, which is
obtained after `Close()`. Client library should process such case.

Signed-off-by: Alex Vanin <alexey@nspcc.ru>
2022-04-21 15:57:37 +03:00
Leonard Lyubich
1ed426b8a6 [#199] owner: Rename to user, refactor and doc
Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-04-19 18:44:02 +03:00
Leonard Lyubich
d20999113a [#190] crypto/ecdsa: Actualize package docs
Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-04-19 12:55:11 +03:00
Leonard Lyubich
6554c681c8 [#190] crypto: Doc RegisterScheme isn't thread-unsafe
Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-04-19 12:55:11 +03:00
Leonard Lyubich
9f20d74d76 [#190] crypto/ecdsa: Use separate types for RFC-6979 signature algo
Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-04-19 12:55:11 +03:00
Leonard Lyubich
7fe75d2cd9 [#190] crypto: Replace public key encoding methods to PublicKey
This make `neofscrypto.Signer` interface more similar to the one from
standard `crypto` package.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-04-19 12:55:11 +03:00
Leonard Lyubich
bcbffd516a [#190] crypto: Provide fmt.Stringer of the Scheme type
Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-04-19 12:55:11 +03:00
Leonard Lyubich
2f9cc50fec [#190] crypto: Start Scheme enum from zero
Enumerate signature schemes of `Scheme` type from 0 in order to sync
with NeoFS API protocol by values.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-04-19 12:55:11 +03:00
Leonard Lyubich
ea043f4ca3 [#190] Refactor cryptographic functionality
Remove `signature` and `util/signature` packages. Re-implement their
functionality in new `crypto` package. Generalize the approach of
digital signature computation and verification by adding `Signer` and
`PublicKey` primitives similar to standard `crypto` package. Support
already exising in protocol signature schemes.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-04-19 12:55:11 +03:00
Pavel Karpy
2deaaeef05 [#170] object: Fix comments about checksums
Delete "must not be nil" where there are no pointers.

Signed-off-by: Pavel Karpy <carpawell@nspcc.ru>
2022-04-13 18:33:33 +03:00
Pavel Karpy
9b63c07c59 [#170] storagegroup: Refactor and document package functionality
Signed-off-by: Pavel Karpy <carpawell@nspcc.ru>
2022-04-13 18:33:33 +03:00
Pavel Karpy
1186f2f703 [#170] oid, cid: Add marshal format checks
Also add checking presence of the `oid`, `cid` fields via `set` flag.

Signed-off-by: Pavel Karpy <carpawell@nspcc.ru>
2022-04-13 17:32:25 +03:00
Pavel Karpy
f7172adf18 [#170] oid, cid: Refactor and document package functionality
Signed-off-by: Pavel Karpy <carpawell@nspcc.ru>
2022-04-13 17:32:25 +03:00
Alex Vanin
24d6c2221f [#170] bearer: Return presence flag in getters of optional fields
Signed-off-by: Alex Vanin <alexey@nspcc.ru>
2022-04-13 11:24:15 +03:00
Alex Vanin
27cd721422 [#170] bearer: Add docs, refactor
Signed-off-by: Alex Vanin <alexey@nspcc.ru>
2022-04-13 11:24:15 +03:00
Pavel Karpy
96892d7bc4 [#170] checksum: Drop Empty method
Signed-off-by: Pavel Karpy <carpawell@nspcc.ru>
2022-04-13 10:53:57 +03:00
Pavel Karpy
168b3ee7a4 [#170] checksum: Add examples
Signed-off-by: Pavel Karpy <carpawell@nspcc.ru>
2022-04-13 10:53:57 +03:00
Pavel Karpy
caa055236b [#170] checksum: Do not use pointers
Do not return pointers from getters. Do not pass pointers to the methods
that does not modify the checksum. Add `Empty` method.

Signed-off-by: Pavel Karpy <carpawell@nspcc.ru>
2022-04-13 10:53:57 +03:00
Pavel Karpy
fd13e61266 [#170] checksum: Drop Empty method
Signed-off-by: Pavel Karpy <carpawell@nspcc.ru>
2022-04-13 10:53:57 +03:00
Leonard Lyubich
9c502a9cae [#179] resolver: Support wss connection scheme in NNS.Dial
WebSocket Secure scheme should also lead to using `neoclient.WSClient`
by `NNS`.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-04-13 10:46:27 +03:00
Leonard Lyubich
d51d18d5f3 [#179] ns: Select Neo connection scheme dynamically in NNS.Dial
WebSocket protocol is optionally supported by Neo servers.

Make `NNS.Dial` to select between HTTP and WebSocket connections
according to URL address scheme.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-04-13 10:46:27 +03:00
Leonard Lyubich
55283d3c91 [#179] Rename resolver package to ns
`resolver` isn't suitable name for the package with NeoFS name system
functionality.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-04-13 10:46:27 +03:00
Leonard Lyubich
75a5b6588d [#179] resolver: Use Client with WSClient in NNS
WebSocket client (`WSClient` type) from Neo Go library shows better
performance than HTTP one (`Client` type).

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-04-13 10:46:27 +03:00
Leonard Lyubich
146fc4f07a [#179] resolver: Refactor and document the package
Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-04-13 10:46:27 +03:00
Alex Vanin
c961aea144 [#170] version: Add specification string encoder
Signed-off-by: Alex Vanin <alexey@nspcc.ru>
2022-04-13 10:28:12 +03:00
Alex Vanin
ade8822a2f [#170] version: Add docs, refactor
Signed-off-by: Alex Vanin <alexey@nspcc.ru>
2022-04-13 10:28:12 +03:00
Denis Kirillov
3e75660802 [#205] pool: fix using default key for requests
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-04-12 18:12:50 +03:00
Denis Kirillov
53e064e556 [#206] pool: fix waitForContainerRemoved
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-04-12 18:12:39 +03:00
Denis Kirillov
385f1b10f9 [#203] pool: fix PrmContainerDelete
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-04-12 14:26:08 +03:00
Denis Kirillov
1e8aa6a99f [#85] pool: update tests
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-04-12 10:30:28 +03:00
Denis Kirillov
97c8274dc0 [#85] pool: use non-pointer client inside wrapper
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-04-12 10:30:28 +03:00
Denis Kirillov
9e8e12f6ae [#85] pool: make client interface methods private
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-04-12 10:30:28 +03:00
Denis Kirillov
aa6a4950d5 [#85] pool: fix skipped tests
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-04-12 10:30:28 +03:00
Denis Kirillov
e56eef495d [#85] pool: refactor client interface
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-04-12 10:30:28 +03:00
Denis Kirillov
e50e6d2828 [#137] pool: simplify session creation
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-04-07 13:33:16 +03:00
Denis Kirillov
df0573d521 [#137] pool: drop sessionTokenThreshold
Description

Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-04-07 13:33:16 +03:00
Denis Kirillov
c4adb03f8e [#137] pool: drop retry for object operations
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-04-07 13:33:16 +03:00
Denis Kirillov
fcfae4a249 [#137] Use callback response to update epoch
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-04-07 13:33:16 +03:00
Denis Kirillov
9814748958 [#137] pool: fix tests
Description

Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-04-07 13:33:16 +03:00
Denis Kirillov
7df00fb0eb [#137] Add exp to ResSessionCreate
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-04-07 13:33:16 +03:00
Denis Kirillov
11a25bb413 [#137] Expire session tokens locally
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-04-07 13:33:16 +03:00
Denis Kirillov
b8d2158acd [#180] eacl: make equal methods functions
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-04-07 09:50:02 +03:00
Denis Kirillov
a709cf5444 [#180] pool: make some operations synchronous
Operations: PutContainer, DeleteContainer, SetEACL

Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-04-07 09:50:02 +03:00
Denis Kirillov
0dbea5452a [#180] pool: add WaitForContainerRemoved
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-04-07 09:50:02 +03:00
Denis Kirillov
47345a33da [#180] pool: add WaitForEACLPresence
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-04-07 09:50:02 +03:00
Denis Kirillov
d568458fab [#180] eacl: add EqualTo for table
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-04-07 09:50:02 +03:00
Alex Vanin
2104945f9e [#193] pool: Return copy of session token in cache
To avoid side effects after token re-sign.

Signed-off-by: Alex Vanin <alexey@nspcc.ru>
2022-04-06 12:06:24 +03:00
Alex Vanin
f38a24e8b5 [#193] pool: Add test to check side effects in token cache
Signed-off-by: Alex Vanin <alexey@nspcc.ru>
2022-04-06 12:06:24 +03:00
Denis Kirillov
e0281c3b34 [#191] pool: use bearer token
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-04-06 10:27:58 +04:00
Evgenii Stratonikov
f18b4a2a75 [#177] *: Remove logger package
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
2022-03-29 09:54:46 +03:00
Pavel Karpy
58d4f4a55f [#170] acl: Document package functionality
Signed-off-by: Pavel Karpy <carpawell@nspcc.ru>
2022-03-25 19:15:22 +03:00
Denis Kirillov
030bbce2cf [#165] pool: drop prmCommon from container params
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-03-21 12:41:08 +03:00
Denis Kirillov
59b49dd7e6 [#165] pool: update WaitForContainerPresence
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-03-21 12:41:08 +03:00
Denis Kirillov
191d85e607 [#165] pool: change exported fields to setters
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-03-21 12:41:08 +03:00
Denis Kirillov
9be9697856 [#165] pool: make client interface unexported
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-03-21 12:41:08 +03:00
Denis Kirillov
52548fe176 [#165] pool: add docs
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-03-21 12:41:08 +03:00
Denis Kirillov
7811d8eefc [#165] pool: distinguish init step and run
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-03-21 12:41:08 +03:00
Denis Kirillov
d03523a3bc [#165] pool: replace options with parameters
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-03-21 12:41:08 +03:00
Denis Kirillov
ec5c223f29 [#165] pool: make private inner structs
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-03-21 12:41:08 +03:00
Denis Kirillov
f5cabe26cb [#165] pool: drop builder
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
2022-03-21 12:41:08 +03:00
Leonard Lyubich
aeb4ac638a [#170] accounting: Remove message sub-string from V2-methods
Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-03-21 12:37:53 +03:00
Leonard Lyubich
7d31de57ec [#170] audit: Refactor and document package functionality
Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-03-21 12:37:53 +03:00
Leonard Lyubich
48150852f3 [#170] accounting: Refactor and document package functionality
Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
2022-03-21 12:37:53 +03:00
Alex Vanin
a55ffa4796 [#176] status: Do not lose built-in error text message
Signed-off-by: Alex Vanin <alexey@nspcc.ru>
2022-03-16 17:16:20 +03:00
334 changed files with 28056 additions and 17001 deletions

View 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.21'
- name: Run commit format checker
uses: https://git.frostfs.info/TrueCloudLab/dco-go@v2
with:
from: 'origin/${{ github.event.pull_request.base.ref }}'

View file

@ -0,0 +1,32 @@
name: Tests and linters
on: [pull_request]
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: golangci-lint
uses: https://github.com/golangci/golangci-lint-action@v2
with:
version: latest
tests:
name: Tests
runs-on: ubuntu-latest
strategy:
matrix:
go_versions: [ '1.19', '1.20' ]
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
View file

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

45
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View 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
View file

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

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

View file

@ -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 }}

View file

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

6
.gitignore vendored
View file

@ -21,3 +21,9 @@ vendor/
# coverage # coverage
coverage.txt coverage.txt
coverage.html coverage.html
# antlr tool jar
antlr-*.jar
# tempfiles
.cache

10
.gitlint Normal file
View file

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

View file

@ -4,10 +4,10 @@
# 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:
@ -24,6 +24,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 +39,28 @@ linters:
- revive - revive
# some default golangci-lint linters # some default golangci-lint linters
- deadcode
- errcheck - errcheck
- gosimple - gosimple
- godot
- ineffassign - ineffassign
- staticcheck - staticcheck
- structcheck
- typecheck - typecheck
- unused - unused
- varcheck
# extra linters # extra linters
- bidichk
- durationcheck
- exhaustive - exhaustive
- godot - exportloopref
- gofmt - gofmt
- whitespace
- goimports - goimports
- misspell
- predeclared
- reassign
- whitespace
- containedctx
- funlen
- gocognit
- contextcheck
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

30
.pre-commit-config.yaml Normal file
View file

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

4
Dockerfile Normal file
View file

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

19
Makefile Normal file → Executable file
View file

@ -1,5 +1,7 @@
#!/usr/bin/make -f #!/usr/bin/make -f
ANTLR_VERSION="4.13.0"
# Run tests # Run tests
test: test:
@go test ./... -cover @go test ./... -cover
@ -29,6 +31,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:'

View file

@ -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,75 @@ 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 prmInit.ResolveFrostFSFailures() // enable erroneous status parsing
client.WithNeoFSErrorHandling(), // enable erroneous status parsing
client.WithTLSConfig(&tls.Config{})) // custom TLS configuration
ctx, cancel := context.WithTimeout(context.Background(), 5 * time.Second) var c client.Client
defer cancel() c.Init(prmInit)
res, err := c.BalanceGet(ctx, owner) 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). There is also
a `client.WithNeoFSErrorHandling()` to seamlessly convert erroneous statuses into Go error type. a `client.PrmInit.ResolveFrostFSFailures()` 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
@ -90,12 +102,12 @@ outdated in some details.
```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).
@ -110,13 +122,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.

View file

@ -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)
}

View file

@ -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
View 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

View file

@ -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
View 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

View file

@ -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
}
}

View file

@ -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())
}

View file

@ -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)
}

View file

@ -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))
})
}

View file

@ -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
}

387
bearer/bearer.go Normal file
View file

@ -0,0 +1,387 @@
package bearer
import (
"crypto/ecdsa"
"errors"
"fmt"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl"
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto"
frostfsecdsa "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto/ecdsa"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
)
// Token represents bearer token for object service operations.
//
// Token is mutually compatible with git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl.BearerToken
// message. See ReadFromV2 / WriteToV2 methods.
//
// Instances can be created using built-in var declaration.
type Token struct {
targetUserSet bool
targetUser user.ID
eaclTableSet bool
eaclTable eacl.Table
lifetimeSet bool
iat, nbf, exp uint64
sigSet bool
sig refs.Signature
impersonate bool
}
// 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()
eaclTable := body.GetEACL()
if b.eaclTableSet = eaclTable != nil; b.eaclTableSet {
b.eaclTable = *eacl.NewTableFromV2(eaclTable)
} else if checkFieldPresence && !b.impersonate {
return errors.New("missing eACL table")
}
targetUser := body.GetOwnerID()
if b.targetUserSet = targetUser != nil; b.targetUserSet {
err = b.targetUser.ReadFromV2(*targetUser)
if err != nil {
return fmt.Errorf("invalid target user: %w", err)
}
}
lifetime := body.GetLifetime()
if b.lifetimeSet = lifetime != nil; b.lifetimeSet {
b.iat = lifetime.GetIat()
b.nbf = lifetime.GetNbf()
b.exp = lifetime.GetExp()
} else if checkFieldPresence {
return errors.New("missing token lifetime")
}
sig := m.GetSignature()
if b.sigSet = sig != nil; sig != nil {
b.sig = *sig
} else if checkFieldPresence {
return errors.New("missing body signature")
}
return nil
}
// ReadFromV2 reads Token from the acl.BearerToken message.
//
// See also WriteToV2.
func (b *Token) ReadFromV2(m acl.BearerToken) error {
return b.readFromV2(m, true)
}
func (b Token) fillBody() *acl.BearerTokenBody {
if !b.eaclTableSet && !b.targetUserSet && !b.lifetimeSet && !b.impersonate {
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)
}
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{}
}
// 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
}

394
bearer/bearer_test.go Normal file
View file

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

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

20
bearer/test/generate.go Normal file
View file

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

View file

@ -1,15 +1,25 @@
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
@ -23,29 +33,46 @@ const (
// SHA256 is a SHA256 checksum type. // SHA256 is a SHA256 checksum type.
SHA256 SHA256
// TZ is a Tillich-Zemor checksum type. // TZ is a Tillich-Zémor checksum type.
TZ TZ
) )
// NewFromV2 wraps v2 Checksum message to Checksum. // ReadFromV2 reads Checksum from the refs.Checksum message. Checks if the
// message conforms to FrostFS API V2 protocol.
// //
// Nil refs.Checksum converts to nil. // 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
}

View file

@ -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
View 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
View 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
View 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

View file

@ -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)

View file

@ -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 prmCommonMeta
ownerSet bool accountSet bool
ownerID owner.ID account user.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) { func (x *PrmBalanceGet) SetAccount(id user.ID) {
x.ownerID = id x.account = id
x.ownerSet = true x.accountSet = true
}
func (x *PrmBalanceGet) buildRequest(c *Client) (*v2accounting.BalanceRequest, error) {
if !x.accountSet {
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.ResolveFrostFSFailures has been called, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included // FrostFS status codes are returned as `error`, otherwise, are included
// in the returned result structure. // in the returned result structure.
// //
// Immediately panics if parameters are set incorrectly (see PrmBalanceGet docs). // Returns an error if parameters are set incorrectly (see PrmBalanceGet docs).
// Context is required and must not be nil. It is used for network communication. // 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
} }

35
client/api.go Normal file
View 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
}

View file

@ -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`
@ -35,6 +38,7 @@ import (
// All possible responses are documented in methods, however, some may be // All possible responses are documented in methods, however, some may be
// returned from all of them (pay attention to the presence of the pointer sign): // returned from all of them (pay attention to the presence of the pointer sign):
// - *apistatus.ServerInternal on internal server error; // - *apistatus.ServerInternal on internal server error;
// - *apistatus.NodeUnderMaintenance if a server is under maintenance;
// - *apistatus.SuccessDefaultV2 on default success. // - *apistatus.SuccessDefaultV2 on default success.
// //
// Client MUST NOT be copied by value: use pointer to Client instead. // Client MUST NOT be copied by value: use pointer to Client instead.
@ -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,72 @@ 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.timeoutDialSet {
if prm.timeoutDial <= 0 { if prm.timeoutDial <= 0 {
panic("non-positive timeout") return errorNonPositiveTimeout
} }
} else { } else {
prm.timeoutDial = 5 * time.Second prm.timeoutDial = 5 * time.Second
} }
if prm.streamTimeoutSet {
if prm.streamTimeout <= 0 {
return errorNonPositiveTimeout
}
} else {
prm.streamTimeout = 10 * time.Second
}
c.c = *client.New(append( c.c = *client.New(append(
client.WithNetworkURIAddress(prm.endpoint, prm.tlsConfig), client.WithNetworkURIAddress(prm.endpoint, prm.tlsConfig),
client.WithDialTimeout(prm.timeoutDial), client.WithDialTimeout(prm.timeoutDial),
client.WithRWTimeout(prm.streamTimeout),
client.WithGRPCDialOptions(prm.dialOptions),
)...) )...)
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,7 +144,7 @@ func (c *Client) Close() error {
// //
// See also Init. // See also Init.
type PrmInit struct { type PrmInit struct {
resolveNeoFSErrors bool resolveFrostFSErrors bool
key ecdsa.PrivateKey key ecdsa.PrivateKey
@ -125,16 +161,16 @@ func (x *PrmInit) SetDefaultPrivateKey(key ecdsa.PrivateKey) {
x.key = key x.key = key
} }
// ResolveNeoFSFailures makes the Client to resolve failure statuses of the // ResolveFrostFSFailures makes the Client to resolve failure statuses of the
// NeoFS protocol into Go built-in errors. These errors are returned from // FrostFS protocol into Go built-in errors. These errors are returned from
// each protocol operation. By default, statuses aren't resolved and written // each protocol operation. By default, statuses aren't resolved and written
// to the resulting structure (see corresponding Res* docs). // to the resulting structure (see corresponding Res* docs).
func (x *PrmInit) ResolveNeoFSFailures() { func (x *PrmInit) ResolveFrostFSFailures() {
x.resolveNeoFSErrors = true x.resolveFrostFSErrors = 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.
func (x *PrmInit) SetResponseInfoCallback(f func(ResponseMetaInfo) error) { func (x *PrmInit) SetResponseInfoCallback(f func(ResponseMetaInfo) error) {
x.cbRespInfo = f x.cbRespInfo = f
} }
@ -149,15 +185,22 @@ type PrmDial struct {
timeoutDialSet bool timeoutDialSet bool
timeoutDial time.Duration timeoutDial time.Duration
streamTimeoutSet bool
streamTimeout time.Duration
dialOptions []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 // grpc
// grpcs // grpcs
// //
@ -167,7 +210,7 @@ func (x *PrmDial) SetServerURI(endpoint string) {
} }
// SetTLSConfig sets tls.Config to open TLS client connection // SetTLSConfig sets tls.Config to open TLS client connection
// to the NeoFS server. Nil (default) means insecure connection. // to the FrostFS server. Nil (default) means insecure connection.
// //
// See also SetServerURI. // See also SetServerURI.
func (x *PrmDial) SetTLSConfig(tlsConfig *tls.Config) { func (x *PrmDial) SetTLSConfig(tlsConfig *tls.Config) {
@ -180,3 +223,15 @@ func (x *PrmDial) SetTimeout(timeout time.Duration) {
x.timeoutDialSet = true x.timeoutDialSet = true
x.timeoutDial = timeout x.timeoutDial = timeout
} }
// SetStreamTimeout sets the timeout for individual operations in streaming RPC.
// MUST BE positive. If not called, 10s timeout will be used by default.
func (x *PrmDial) SetStreamTimeout(timeout time.Duration) {
x.streamTimeoutSet = true
x.streamTimeout = timeout
}
// SetGRPCDialOptions sets the gRPC dial options for new gRPC client connection.
func (x *PrmDial) SetGRPCDialOptions(opts ...grpc.DialOption) {
x.dialOptions = opts
}

65
client/client_test.go Normal file
View file

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

View file

@ -1,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,26 +24,9 @@ func (x statusRes) Status() apistatus.Status {
return x.st return x.st
} }
type prmSession struct {
tokenSessionSet bool
tokenSession session.Token
}
// SetSessionToken sets token of the session within which request should be sent.
func (x *prmSession) SetSessionToken(tok session.Token) {
x.tokenSession = tok
x.tokenSessionSet = true
}
func (x prmSession) writeToMetaHeader(meta *v2session.RequestMetaHeader) {
if x.tokenSessionSet {
meta.SetSessionToken(x.tokenSession.ToV2())
}
}
// groups meta parameters shared between all Client operations. // groups meta parameters shared between all Client operations.
type prmCommonMeta struct { type prmCommonMeta struct {
// NeoFS request X-Headers // FrostFS request X-Headers
xHeaders []string xHeaders []string
} }
@ -69,252 +42,92 @@ func (x *prmCommonMeta) WithXHeaders(hs ...string) {
x.xHeaders = hs x.xHeaders = hs
} }
func (x prmCommonMeta) writeToMetaHeader(h *v2session.RequestMetaHeader) { func writeXHeadersToMeta(xHeaders []string, h *v2session.RequestMetaHeader) {
if len(x.xHeaders) > 0 { if len(xHeaders) == 0 {
hs := make([]v2session.XHeader, len(x.xHeaders)/2) return
}
for i := 0; i < len(x.xHeaders); i += 2 { // TODO (aarifullin): remove the panic when all client parameters will check XHeaders
hs[i].SetKey(x.xHeaders[i]) // within buildRequest invocation.
hs[i].SetValue(x.xHeaders[i+1]) if len(xHeaders)%2 != 0 {
panic("slice of X-Headers with odd length")
}
hs := make([]v2session.XHeader, len(xHeaders)/2)
for i := 0; i < len(xHeaders); i += 2 {
hs[i].SetKey(xHeaders[i])
hs[i].SetValue(xHeaders[i+1])
} }
h.SetXHeaders(hs) 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")
errorNonPositiveTimeout = errors.New("non-positive timeout")
errorEACLTableNotSet = errors.New("eACL table not set")
errorMissingAnnouncements = errors.New("missing announcements")
errorZeroRangeLength = errors.New("zero range length")
errorMissingRanges = errors.New("missing ranges")
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 {
// ==================================================
// state vars that do not require explicit initialization
// 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 GetMetaHeader() *v2session.RequestMetaHeader
SetMetaHeader(*v2session.RequestMetaHeader) SetMetaHeader(*v2session.RequestMetaHeader)
SetVerificationHeader(*v2session.RequestVerificationHeader) 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.cbRespInfo != 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.cbRespInfo(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() err := signature.VerifyServiceMessage(resp)
if x.err != nil { if err != nil {
x.err = fmt.Errorf("write request: %w", x.err) return nil, fmt.Errorf("invalid response signature: %w", err)
return false
} }
return true st := apistatus.FromStatusV2(resp.GetMetaHeader().GetStatus())
if c.prm.resolveFrostFSErrors {
return st, apistatus.ErrFromStatus(st)
}
return st, nil
} }
// performs common actions of response processing and writes any problem as a result status or client error // ExecRaw executes f with underlying git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client.Client
// (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
// while verification needs marshaling
// verify response signature
x.err = signature.VerifyServiceMessage(x.resp)
if x.err != nil {
x.err = fmt.Errorf("invalid response signature: %w", x.err)
return false
}
// get result status
st := apistatus.FromStatusV2(x.resp.GetMetaHeader().GetStatus())
// unwrap unsuccessful status and return it
// as error if client has been configured so
successfulStatus := apistatus.IsSuccessful(st)
if !successfulStatus && x.resolveAPIFailures {
x.err = apistatus.ErrFromStatus(st)
return false
}
x.statusRes.setStatus(st)
return successfulStatus || !x.resolveAPIFailures
}
// reads response (if rResp is set) and processes it. Result means success.
// If failed, contextCall.err contains the reason.
func (x *contextCall) readResponse() bool {
if x.rResp != nil {
x.err = x.rResp()
if x.err != nil {
x.err = fmt.Errorf("read response: %w", x.err)
return false
}
}
return x.processResponse()
}
// closes the message stream (if closer is set) and writes the results (if result is set).
// Return means success. If failed, contextCall.err contains the reason.
func (x *contextCall) close() bool {
if x.closer != nil {
x.err = x.closer()
if x.err != nil {
x.err = fmt.Errorf("close RPC: %w", x.err)
return false
}
}
// write response to resulting structure
if x.result != nil {
x.result(x.resp)
}
return true
}
// goes through all stages of sending a request and processing a response. Returns true if successful.
// If failed, contextCall.err contains the reason.
func (x *contextCall) processCall() bool {
// set request writer
x.wReq = func() error {
var err error
x.resp, err = x.call()
return err
}
// write request
ok := x.writeRequest()
if !ok {
return false
}
// read response
ok = x.readResponse()
if !ok {
return false
}
// close and write response to resulting structure
ok = x.close()
if !ok {
return false
}
return x.err == nil
}
// initializes static cross-call parameters inherited from client.
func (c *Client) initCallContext(ctx *contextCall) {
ctx.key = c.prm.key
c.initCallContextWithoutKey(ctx)
}
// initializes static cross-call parameters inherited from client except private key.
func (c *Client) initCallContextWithoutKey(ctx *contextCall) {
ctx.resolveAPIFailures = c.prm.resolveNeoFSErrors
ctx.callbackResp = c.prm.cbRespInfo
ctx.netMagic = c.prm.netMagic
}
// ExecRaw executes f with underlying github.com/nspcc-dev/neofs-api-go/v2/rpc/client.Client
// instance. Communicate over the Protocol Buffers protocol in a more flexible way: // instance. Communicate over the Protocol Buffers protocol in a more flexible way:
// most often used to transmit data over a fixed version of the 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 +136,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)
} }

View file

@ -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
View 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.ResolveFrostFSFailures has been called, unsuccessful
// FrostFS status codes are returned as `error`, otherwise, are included
// in the returned result structure.
//
// Operation is asynchronous and no guaranteed even in the absence of errors.
// 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
}

110
client/container_eacl.go Normal file
View file

@ -0,0 +1,110 @@
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/eacl"
)
// PrmContainerEACL groups parameters of ContainerEACL operation.
type PrmContainerEACL struct {
// FrostFS request X-Headers.
XHeaders []string
ContainerID *cid.ID
}
// SetContainer sets identifier of the FrostFS container to read the eACL table.
// Required parameter.
//
// Deprecated: Use PrmContainerEACL.ContainerID instead.
func (x *PrmContainerEACL) SetContainer(id cid.ID) {
x.ContainerID = &id
}
func (x *PrmContainerEACL) buildRequest(c *Client) (*v2container.GetExtendedACLRequest, error) {
if x.ContainerID == nil {
return nil, errorMissingContainer
}
if len(x.XHeaders)%2 != 0 {
return nil, errorInvalidXHeaders
}
var cidV2 refs.ContainerID
x.ContainerID.WriteToV2(&cidV2)
reqBody := new(v2container.GetExtendedACLRequestBody)
reqBody.SetContainerID(&cidV2)
var req v2container.GetExtendedACLRequest
req.SetBody(reqBody)
c.prepareRequest(&req, new(v2session.RequestMetaHeader))
return &req, nil
}
// ResContainerEACL groups resulting values of ContainerEACL operation.
type ResContainerEACL struct {
statusRes
table eacl.Table
}
// Table returns eACL table of the requested container.
func (x ResContainerEACL) Table() eacl.Table {
return x.table
}
// ContainerEACL reads eACL table of the FrostFS container.
//
// Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as `error`.
// If PrmInit.ResolveFrostFSFailures has been called, unsuccessful
// FrostFS status codes are returned as `error`, otherwise, are included
// in the returned result structure.
//
// Returns an error if parameters are set incorrectly (see PrmContainerEACL docs).
// Context is required and must not be nil. It is used for network communication.
//
// Return statuses:
// - global (see Client docs);
// - *apistatus.ContainerNotFound;
// - *apistatus.EACLNotFound.
func (c *Client) ContainerEACL(ctx context.Context, prm PrmContainerEACL) (*ResContainerEACL, 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.GetEACL(&c.c, req, client.WithContext(ctx))
if err != nil {
return nil, err
}
var res ResContainerEACL
res.st, err = c.processResponse(resp)
if err != nil || !apistatus.IsSuccessful(res.st) {
return &res, err
}
eACL := resp.GetBody().GetEACL()
if eACL == nil {
return &res, newErrMissingResponseField("eACL")
}
res.table = *eacl.NewTableFromV2(eACL)
return &res, nil
}

113
client/container_get.go Normal file
View file

@ -0,0 +1,113 @@
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"
)
// PrmContainerGet groups parameters of ContainerGet operation.
type PrmContainerGet struct {
// FrostFS request X-Headers.
XHeaders []string
ContainerID *cid.ID
}
// 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 req v2container.GetRequest
req.SetBody(reqBody)
c.prepareRequest(&req, new(v2session.RequestMetaHeader))
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.ResolveFrostFSFailures has been called, unsuccessful
// FrostFS status codes are returned as `error`, otherwise, are included
// in the returned result structure.
//
// Returns an error if parameters are set incorrectly (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
}

106
client/container_list.go Normal file
View file

@ -0,0 +1,106 @@
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/user"
)
// PrmContainerList groups parameters of ContainerList operation.
type PrmContainerList struct {
prmCommonMeta
ownerSet bool
ownerID user.ID
}
// SetAccount sets identifier of the FrostFS account to list the containers.
// Required parameter.
func (x *PrmContainerList) SetAccount(id user.ID) {
x.ownerID = id
x.ownerSet = true
}
func (x *PrmContainerList) buildRequest(c *Client) (*v2container.ListRequest, error) {
if !x.ownerSet {
return nil, errorAccountNotSet
}
var ownerV2 refs.OwnerID
x.ownerID.WriteToV2(&ownerV2)
reqBody := new(v2container.ListRequestBody)
reqBody.SetOwnerID(&ownerV2)
var req v2container.ListRequest
req.SetBody(reqBody)
c.prepareRequest(&req, new(v2session.RequestMetaHeader))
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.ResolveFrostFSFailures has been called, unsuccessful
// FrostFS status codes are returned as `error`, otherwise, are included
// in the returned result structure.
//
// Returns an error if parameters are set incorrectly (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
View 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.ResolveFrostFSFailures has been called, unsuccessful
// FrostFS status codes are returned as `error`, otherwise, are included
// in the returned result structure.
//
// Operation is asynchronous and no guaranteed even in the absence of errors.
// The required time is also not predictable.
//
// Success can be verified by reading by identifier (see ResContainerPut.ID).
//
// Returns an error if parameters are set incorrectly (see PrmContainerPut docs).
// Context is required and must not be nil. It is used for network communication.
//
// Return statuses:
// - global (see Client docs).
//
// nolint: funlen
func (c *Client) ContainerPut(ctx context.Context, prm PrmContainerPut) (*ResContainerPut, error) {
req, err := prm.buildRequest(c)
if err != nil {
return nil, err
}
if err := signature.SignServiceMessage(&c.prm.key, req); err != nil {
return nil, fmt.Errorf("sign request: %w", err)
}
resp, err := rpcapi.PutContainer(&c.c, req, client.WithContext(ctx))
if err != nil {
return nil, err
}
var res ResContainerPut
res.st, err = c.processResponse(resp)
if err != nil || !apistatus.IsSuccessful(res.st) {
return &res, err
}
const fieldCnrID = "container ID"
cidV2 := resp.GetBody().GetContainerID()
if cidV2 == nil {
return &res, newErrMissingResponseField(fieldCnrID)
}
if err := res.id.ReadFromV2(*cidV2); err != nil {
return &res, newErrInvalidResponseField(fieldCnrID, err)
}
return &res, nil
}

View file

@ -0,0 +1,136 @@
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"
frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto"
frostfsecdsa "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto/ecdsa"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
)
// PrmContainerSetEACL groups parameters of ContainerSetEACL operation.
type PrmContainerSetEACL struct {
// FrostFS request X-Headers.
XHeaders []string
Table *eacl.Table
Session *session.Container
}
// SetTable sets eACL table structure to be set for the container.
// Required parameter.
//
// Deprecated: Use PrmContainerSetEACL.Table instead.
func (x *PrmContainerSetEACL) SetTable(table eacl.Table) {
x.Table = &table
}
// WithinSession specifies session within which extended ACL of the container
// should be saved.
//
// Creator of the session acquires the authorship of the request. This affects
// the execution of an operation (e.g. access control).
//
// Session is optional, if set the following requirements apply:
// - if particular container is specified (ApplyOnlyTo), it MUST equal the container
// for which extended ACL is going to be set
// - session operation MUST be session.VerbContainerSetEACL (ForVerb)
// - token MUST be signed using private key of the owner of the container to be saved
//
// Deprecated: Use PrmContainerSetEACL.Session instead.
func (x *PrmContainerSetEACL) WithinSession(s session.Container) {
x.Session = &s
}
func (x *PrmContainerSetEACL) buildRequest(c *Client) (*v2container.SetExtendedACLRequest, error) {
if x.Table == nil {
return nil, errorEACLTableNotSet
}
if len(x.XHeaders)%2 != 0 {
return nil, errorInvalidXHeaders
}
eaclV2 := x.Table.ToV2()
var sig frostfscrypto.Signature
err := sig.Calculate(frostfsecdsa.SignerRFC6979(c.prm.key), eaclV2.StableMarshal(nil))
if err != nil {
return nil, fmt.Errorf("calculate signature: %w", err)
}
var sigv2 refs.Signature
sig.WriteToV2(&sigv2)
reqBody := new(v2container.SetExtendedACLRequestBody)
reqBody.SetEACL(eaclV2)
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.SetExtendedACLRequest
req.SetBody(reqBody)
c.prepareRequest(&req, &meta)
return &req, nil
}
// ResContainerSetEACL groups resulting values of ContainerSetEACL operation.
type ResContainerSetEACL struct {
statusRes
}
// ContainerSetEACL sends request to update eACL table of the FrostFS container.
//
// Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as `error`.
// If PrmInit.ResolveFrostFSFailures has been called, unsuccessful
// FrostFS status codes are returned as `error`, otherwise, are included
// in the returned result structure.
//
// Operation is asynchronous and no guaranteed even in the absence of errors.
// The required time is also not predictable.
//
// Success can be verified by reading by identifier (see EACL).
//
// Returns an error if parameters are set incorrectly (see PrmContainerSetEACL docs).
// Context is required and must not be nil. It is used for network communication.
//
// Return statuses:
// - global (see Client docs).
func (c *Client) ContainerSetEACL(ctx context.Context, prm PrmContainerSetEACL) (*ResContainerSetEACL, 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.SetEACL(&c.c, req, client.WithContext(ctx))
if err != nil {
return nil, err
}
var res ResContainerSetEACL
res.st, err = c.processResponse(resp)
return &res, err
}

92
client/container_space.go Normal file
View file

@ -0,0 +1,92 @@
package client
import (
"context"
"fmt"
v2container "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
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/container"
)
// PrmAnnounceSpace groups parameters of ContainerAnnounceUsedSpace operation.
type PrmAnnounceSpace struct {
XHeaders []string
Announcements []container.SizeEstimation
}
// SetValues sets values describing volume of space that is used for the container objects.
// Required parameter. Must not be empty.
//
// Must not be mutated before the end of the operation.
//
// Deprecated: Use PrmAnnounceSpace.Announcements instead.
func (x *PrmAnnounceSpace) SetValues(vs []container.SizeEstimation) {
x.Announcements = vs
}
func (x *PrmAnnounceSpace) buildRequest(c *Client) (*v2container.AnnounceUsedSpaceRequest, error) {
if len(x.Announcements) == 0 {
return nil, errorMissingAnnouncements
}
v2announce := make([]v2container.UsedSpaceAnnouncement, len(x.Announcements))
for i := range x.Announcements {
x.Announcements[i].WriteToV2(&v2announce[i])
}
reqBody := new(v2container.AnnounceUsedSpaceRequestBody)
reqBody.SetAnnouncements(v2announce)
var req v2container.AnnounceUsedSpaceRequest
req.SetBody(reqBody)
c.prepareRequest(&req, new(v2session.RequestMetaHeader))
return &req, nil
}
// 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 PrmInit.ResolveFrostFSFailures has been called, unsuccessful
// FrostFS status codes are returned as `error`, otherwise, are included
// in the returned result structure.
//
// Operation is asynchronous and no guaranteed even in the absence of errors.
// The required time is also not predictable.
//
// At this moment success can not be checked.
//
// Returns an error if parameters are set incorrectly (see PrmAnnounceSpace docs).
// Context is required and must not be nil. It is used for network communication.
//
// Return statuses:
// - global (see Client docs).
func (c *Client) ContainerAnnounceUsedSpace(ctx context.Context, prm PrmAnnounceSpace) (*ResAnnounceSpace, 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.AnnounceUsedSpace(&c.c, req, client.WithContext(ctx))
if err != nil {
return nil, err
}
var res ResAnnounceSpace
res.st, err = c.processResponse(resp)
return &res, err
}

View file

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

View file

@ -1,42 +1,80 @@
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)
}
// IsErrMaintenance checks if err corresponds to FrostFS status return
// which tells that node is currently under maintenance. Supports wrapped
// errors.
func IsErrMaintenance(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)
} }

52
client/errors_test.go Normal file
View file

@ -0,0 +1,52 @@
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.IsErrMaintenance,
err: new(apistatus.NodeUnderMaintenance),
},
}
for i := range errs {
for j := range errs {
nestedErr := fmt.Errorf("top-level context: :%w", fmt.Errorf("inner context: %w", errs[j].err))
require.Equal(t, i == j, errs[i].check(errs[j].err))
require.Equal(t, i == j, errs[i].check(nestedErr))
}
}
}

View file

@ -2,60 +2,62 @@ 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.ResolveFrostFSFailures has been called, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included // FrostFS status codes are returned as `error`, otherwise, are included
// in the returned result structure. // in the returned result structure.
// //
// Immediately panics if parameters are set incorrectly (see PrmEndpointInfo docs). // Returns an error if parameters are set incorrectly (see PrmEndpointInfo docs).
// Context is required and must not be nil. It is used for network communication. // 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.
@ -64,76 +66,85 @@ func (x *ResEndpointInfo) setNodeInfo(info *netmap.NodeInfo) {
// 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
resp, err := rpcapi.LocalNodeInfo(&c.c, req, client.WithContext(ctx))
var ( if err != nil {
cc contextCall return nil, err
res ResEndpointInfo }
)
var res ResEndpointInfo
c.initCallContext(&cc) res.st, err = c.processResponse(resp)
cc.meta = prm.prmCommonMeta if err != nil || !apistatus.IsSuccessful(res.st) {
cc.req = &req return &res, err
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() body := resp.GetBody()
res.setLatestVersion(version.NewFromV2(body.GetVersion())) const fieldVersion = "version"
res.setNodeInfo(netmap.NewNodeInfoFromV2(body.GetNodeInfo()))
verV2 := body.GetVersion()
if verV2 == nil {
return nil, newErrMissingResponseField(fieldVersion)
}
if err := res.version.ReadFromV2(*verV2); err != nil {
return nil, newErrInvalidResponseField(fieldVersion, err)
} }
// process call const fieldNodeInfo = "node info"
if !cc.processCall() {
return nil, cc.err
}
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.ResolveFrostFSFailures has been called, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included // FrostFS status codes are returned as `error`, otherwise, are included
// in the returned result structure. // in the returned result structure.
// //
// Immediately panics if parameters are set incorrectly (see PrmNetworkInfo docs). // Returns an error if parameters are set incorrectly (see PrmNetworkInfo docs).
// Context is required and must not be nil. It is used for network communication. // 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.
@ -142,37 +153,111 @@ func (x *ResNetworkInfo) setInfo(info *netmap.NetworkInfo) {
// 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.ResolveFrostFSFailures has been called, unsuccessful
// FrostFS status codes are returned as `error`, otherwise, are included
// in the returned result structure.
//
// Returns an error if parameters are set incorrectly.
// Context is required and MUST NOT be nil. It is used for network communication.
//
// Exactly one return value is non-nil. Server status return is returned in ResNetMapSnapshot.
// Reflects all internal errors in second return value (transport problems, response processing, etc.).
//
// Return statuses:
// - global (see Client docs).
func (c *Client) NetMapSnapshot(ctx context.Context, _ PrmNetMapSnapshot) (*ResNetMapSnapshot, error) {
// 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

131
client/netmap_test.go Normal file
View file

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

View file

@ -3,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.ResolveFrostFSFailures has been called, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included // FrostFS status codes are returned as `error`, otherwise, are included
// in the returned result structure. // in the returned result structure.
// //
// Immediately panics if parameters are set incorrectly (see PrmObjectDelete docs). // Returns an error if parameters are set incorrectly (see PrmObjectDelete docs).
// Context is required and must not be nil. It is used for network communication. // 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

View file

@ -7,104 +7,91 @@ 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"
) )
// shared parameters of GET/HEAD/RANGE.
type prmObjectRead struct {
prmCommonMeta
raw bool
local bool
sessionSet bool
session session.Token
bearerSet bool
bearer token.BearerToken
cnrSet bool
cnr cid.ID
objSet bool
obj oid.ID
}
func (x prmObjectRead) writeToMetaHeader(h *v2session.RequestMetaHeader) {
if x.local {
h.SetTTL(1)
}
if x.bearerSet {
h.SetBearerToken(x.bearer.ToV2())
}
if x.sessionSet {
h.SetSessionToken(x.session.ToV2())
}
x.prmCommonMeta.writeToMetaHeader(h)
}
// MarkRaw marks an intent to read physically stored object.
func (x *prmObjectRead) MarkRaw() {
x.raw = true
}
// MarkLocal tells the server to execute the operation locally.
func (x *prmObjectRead) MarkLocal() {
x.local = true
}
// WithinSession specifies session within which object should be read.
//
// Creator of the session acquires the authorship of the request.
// This may affect the execution of an operation (e.g. access control).
//
// Must be signed.
func (x *prmObjectRead) WithinSession(t session.Token) {
x.session = t
x.sessionSet = true
}
// 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 *prmObjectRead) WithBearerToken(t token.BearerToken) {
x.bearer = t
x.bearerSet = true
}
// FromContainer specifies NeoFS container of the object.
// Required parameter.
func (x *prmObjectRead) FromContainer(id cid.ID) {
x.cnr = id
x.cnrSet = true
}
// ByID specifies identifier of the requested object.
// Required parameter.
func (x *prmObjectRead) ByID(id oid.ID) {
x.obj = id
x.objSet = true
}
// PrmObjectGet groups parameters of ObjectGetInit operation. // PrmObjectGet groups parameters of ObjectGetInit operation.
type PrmObjectGet struct { type PrmObjectGet struct {
prmObjectRead XHeaders []string
BearerToken *bearer.Token
Session *session.Object
Raw bool
Local bool
ContainerID *cid.ID
ObjectID *oid.ID
Key *ecdsa.PrivateKey
}
func (prm *PrmObjectGet) buildRequest(c *Client) (*v2object.GetRequest, error) {
if prm.ContainerID == nil {
return nil, errorMissingContainer
}
if prm.ObjectID == nil {
return nil, errorMissingObject
}
if len(prm.XHeaders)%2 != 0 {
return nil, errorInvalidXHeaders
}
meta := new(v2session.RequestMetaHeader)
writeXHeadersToMeta(prm.XHeaders, meta)
if prm.BearerToken != nil {
v2BearerToken := new(acl.BearerToken)
prm.BearerToken.WriteToV2(v2BearerToken)
meta.SetBearerToken(v2BearerToken)
}
if prm.Session != nil {
v2SessionToken := new(v2session.Token)
prm.Session.WriteToV2(v2SessionToken)
meta.SetSessionToken(v2SessionToken)
}
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)
body := new(v2object.GetRequestBody)
body.SetRaw(prm.Raw)
body.SetAddress(addr)
req := new(v2object.GetRequest)
req.SetBody(body)
c.prepareRequest(req, meta)
return req, nil
} }
// ResObjectGet groups the final result values of ObjectGetInit operation. // ResObjectGet groups the final result values of ObjectGetInit operation.
@ -112,17 +99,20 @@ type ResObjectGet struct {
statusRes statusRes
} }
// ObjectReader is designed to read one object from NeoFS system. // ObjectReader is designed to read one object from FrostFS system.
// //
// Must be initialized using Client.ObjectGetInit, any other // Must be initialized using Client.ObjectGetInit, any other
// usage is unsafe. // usage is unsafe.
type ObjectReader struct { type ObjectReader struct {
cancelCtxStream context.CancelFunc cancelCtxStream context.CancelFunc
ctxCall contextCall client *Client
stream interface {
Read(resp *v2object.GetResponse) error
}
// initially bound to contextCall res ResObjectGet
bodyResp v2object.GetResponseBody err error
tailPayload []byte tailPayload []byte
@ -131,29 +121,34 @@ type ObjectReader struct {
// UseKey specifies private key to sign the requests. // UseKey specifies private key to sign the requests.
// If key is not provided, then Client default key is used. // If key is not provided, then Client default key is used.
func (x *ObjectReader) UseKey(key ecdsa.PrivateKey) { //
x.ctxCall.key = key // Deprecated: Use PrmObjectGet.Key instead.
} func (prm *PrmObjectGet) UseKey(key ecdsa.PrivateKey) {
prm.Key = &key
func handleSplitInfo(ctx *contextCall, i *v2object.SplitInfo) {
ctx.err = object.NewSplitInfoError(object.NewSplitInfoFromV2(i))
} }
// ReadHeader reads header of the object. Result means success. // ReadHeader reads header of the object. Result means success.
// Failure reason can be received via Close. // Failure reason can be received via Close.
func (x *ObjectReader) ReadHeader(dst *object.Object) bool { func (x *ObjectReader) ReadHeader(dst *object.Object) bool {
if !x.ctxCall.writeRequest() || !x.ctxCall.readResponse() { var resp v2object.GetResponse
x.err = x.stream.Read(&resp)
if x.err != nil {
return false
}
x.res.st, x.err = x.client.processResponse(&resp)
if x.err != nil || !apistatus.IsSuccessful(x.res.st) {
return false return false
} }
var partInit *v2object.GetObjectPartInit var partInit *v2object.GetObjectPartInit
switch v := x.bodyResp.GetObjectPart().(type) { switch v := resp.GetBody().GetObjectPart().(type) {
default: default:
x.ctxCall.err = fmt.Errorf("unexpected message instead of heading part: %T", v) x.err = fmt.Errorf("unexpected message instead of heading part: %T", v)
return false return false
case *v2object.SplitInfo: case *v2object.SplitInfo:
handleSplitInfo(&x.ctxCall, v) x.err = object.NewSplitInfoError(object.NewSplitInfoFromV2(v))
return false return false
case *v2object.GetObjectPartInit: case *v2object.GetObjectPartInit:
partInit = v partInit = v
@ -184,26 +179,25 @@ func (x *ObjectReader) readChunk(buf []byte) (int, bool) {
return read, true return read, true
} }
var ok bool
var part v2object.GetObjectPart
var chunk []byte var chunk []byte
var lastRead int var lastRead int
for { for {
// receive next message var resp v2object.GetResponse
ok = x.ctxCall.readResponse() x.err = x.stream.Read(&resp)
if !ok { if x.err != nil {
return read, false return read, false
} }
// get chunk part message x.res.st, x.err = x.client.processResponse(&resp)
part = x.bodyResp.GetObjectPart() if x.err != nil || !apistatus.IsSuccessful(x.res.st) {
return read, false
}
var partChunk *v2object.GetObjectPartChunk part := resp.GetBody().GetObjectPart()
partChunk, ok := part.(*v2object.GetObjectPartChunk)
partChunk, ok = part.(*v2object.GetObjectPartChunk)
if !ok { if !ok {
x.ctxCall.err = fmt.Errorf("unexpected message instead of chunk part: %T", part) x.err = fmt.Errorf("unexpected message instead of chunk part: %T", part)
return read, false return read, false
} }
@ -238,9 +232,9 @@ func (x *ObjectReader) ReadChunk(buf []byte) (int, bool) {
func (x *ObjectReader) close(ignoreEOF bool) (*ResObjectGet, error) { func (x *ObjectReader) close(ignoreEOF bool) (*ResObjectGet, error) {
defer x.cancelCtxStream() defer x.cancelCtxStream()
if x.ctxCall.err != nil { if x.err != nil {
if !errors.Is(x.ctxCall.err, io.EOF) { if !errors.Is(x.err, io.EOF) {
return nil, x.ctxCall.err return nil, x.err
} else if !ignoreEOF { } else if !ignoreEOF {
if x.remainingPayloadLen > 0 { if x.remainingPayloadLen > 0 {
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
@ -250,7 +244,7 @@ func (x *ObjectReader) close(ignoreEOF bool) (*ResObjectGet, error) {
} }
} }
return x.ctxCall.statusRes.(*ResObjectGet), nil return &x.res, nil
} }
// Close ends reading the object and returns the result of the operation // Close ends reading the object and returns the result of the operation
@ -258,10 +252,11 @@ func (x *ObjectReader) close(ignoreEOF bool) (*ResObjectGet, error) {
// //
// Exactly one return value is non-nil. By default, server status is returned in res structure. // Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as Go built-in error. // Any client's internal or transport errors are returned as Go built-in error.
// If Client is tuned to resolve 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 errors: // Return errors:
//
// *object.SplitInfoError (returned on virtual objects with PrmObjectGet.MakeRaw). // *object.SplitInfoError (returned on virtual objects with PrmObjectGet.MakeRaw).
// //
// Return statuses: // Return statuses:
@ -297,92 +292,70 @@ func (x *ObjectReader) Read(p []byte) (int, error) {
return n, nil return n, nil
} }
// ObjectGetInit initiates reading an object through a remote server using NeoFS API protocol. // ObjectGetInit initiates reading an object through a remote server using FrostFS API protocol.
// //
// The call only opens the transmission channel, explicit fetching is done using the ObjectReader. // The call only opens the transmission channel, explicit fetching is done using the ObjectReader.
// Exactly one return value is non-nil. Resulting reader must be finally closed. // Exactly one return value is non-nil. Resulting reader must be finally closed.
// //
// Immediately panics if parameters are set incorrectly (see PrmObjectGet docs). // Returns an error if parameters are set incorrectly (see PrmObjectGet docs).
// Context is required and must not be nil. It is used for network communication. // Context is required and must not be nil. It is used for network communication.
func (c *Client) ObjectGetInit(ctx context.Context, prm PrmObjectGet) (*ObjectReader, error) { func (c *Client) ObjectGetInit(ctx context.Context, prm PrmObjectGet) (*ObjectReader, error) {
// check parameters req, err := prm.buildRequest(c)
switch {
case ctx == nil:
panic(panicMsgMissingContext)
case !prm.cnrSet:
panic(panicMsgMissingContainer)
case !prm.objSet:
panic("missing object")
}
var addr v2refs.Address
addr.SetContainerID(prm.cnr.ToV2())
addr.SetObjectID(prm.obj.ToV2())
// form request body
var body v2object.GetRequestBody
body.SetRaw(prm.raw)
body.SetAddress(&addr)
// form meta header
var meta v2session.RequestMetaHeader
prm.prmObjectRead.writeToMetaHeader(&meta)
// form request
var req v2object.GetRequest
req.SetBody(&body)
req.SetMetaHeader(&meta)
// init reader
var (
r ObjectReader
resp v2object.GetResponse
stream *rpcapi.GetResponseReader
)
ctx, r.cancelCtxStream = context.WithCancel(ctx)
resp.SetBody(&r.bodyResp)
// init call context
c.initCallContext(&r.ctxCall)
r.ctxCall.req = &req
r.ctxCall.statusRes = new(ResObjectGet)
r.ctxCall.resp = &resp
r.ctxCall.wReq = func() error {
var err error
stream, err = rpcapi.GetObject(&c.c, &req, client.WithContext(ctx))
if err != nil { if err != nil {
return fmt.Errorf("open stream: %w", err) return nil, err
} }
return nil key := prm.Key
if key == nil {
key = &c.prm.key
} }
r.ctxCall.rResp = func() error {
return stream.Read(&resp) err = signature.SignServiceMessage(key, req)
if err != nil {
return nil, fmt.Errorf("sign request: %w", err)
} }
ctx, cancel := context.WithCancel(ctx)
stream, err := rpcapi.GetObject(&c.c, req, client.WithContext(ctx))
if err != nil {
cancel()
return nil, fmt.Errorf("open stream: %w", err)
}
var r ObjectReader
r.cancelCtxStream = cancel
r.stream = stream
r.client = c
return &r, nil return &r, nil
} }
// PrmObjectHead groups parameters of ObjectHead operation. // PrmObjectHead groups parameters of ObjectHead operation.
type PrmObjectHead struct { type PrmObjectHead struct {
prmObjectRead XHeaders []string
keySet bool BearerToken *bearer.Token
key ecdsa.PrivateKey
Session *session.Object
Raw bool
Local bool
ContainerID *cid.ID
ObjectID *oid.ID
Key *ecdsa.PrivateKey
} }
// 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 *PrmObjectHead) UseKey(key ecdsa.PrivateKey) { //
x.keySet = true // Deprecated: Use PrmObjectHead.Key instead.
x.key = key func (prm *PrmObjectHead) UseKey(key ecdsa.PrivateKey) {
prm.Key = &key
} }
// ResObjectHead groups resulting values of ObjectHead operation. // ResObjectHead groups resulting values of ObjectHead operation.
@ -408,25 +381,78 @@ func (x *ResObjectHead) ReadHeader(dst *object.Object) bool {
objv2.SetSignature(x.hdr.GetSignature()) objv2.SetSignature(x.hdr.GetSignature())
obj := object.NewFromV2(&objv2) obj := object.NewFromV2(&objv2)
obj.SetID(&x.idObj) obj.SetID(x.idObj)
*dst = *obj *dst = *obj
return true return true
} }
// ObjectHead reads object header through a remote server using NeoFS API protocol. func (prm *PrmObjectHead) buildRequest(c *Client) (*v2object.HeadRequest, error) {
if prm.ContainerID == nil {
return nil, errorMissingContainer
}
if prm.ObjectID == nil {
return nil, errorMissingObject
}
if len(prm.XHeaders)%2 != 0 {
return nil, errorInvalidXHeaders
}
meta := new(v2session.RequestMetaHeader)
writeXHeadersToMeta(prm.XHeaders, meta)
if prm.BearerToken != nil {
v2BearerToken := new(acl.BearerToken)
prm.BearerToken.WriteToV2(v2BearerToken)
meta.SetBearerToken(v2BearerToken)
}
if prm.Session != nil {
v2SessionToken := new(v2session.Token)
prm.Session.WriteToV2(v2SessionToken)
meta.SetSessionToken(v2SessionToken)
}
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)
body := new(v2object.HeadRequestBody)
body.SetRaw(prm.Raw)
body.SetAddress(addr)
req := new(v2object.HeadRequest)
req.SetBody(body)
c.prepareRequest(req, meta)
return req, nil
}
// ObjectHead reads object header through a remote server using FrostFS API protocol.
// //
// Exactly one return value is non-nil. By default, server status is returned in res structure. // 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.ResolveFrostFSFailures has been called, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included // FrostFS status codes are returned as `error`, otherwise, are included
// in the returned result structure. // in the returned result structure.
// //
// Immediately panics if parameters are set incorrectly (see PrmObjectHead docs). // Returns an error if parameters are set incorrectly (see PrmObjectHead docs).
// Context is required and must not be nil. It is used for network communication. // Context is required and must not be nil. It is used for network communication.
// //
// Return errors: // Return errors:
//
// *object.SplitInfoError (returned on virtual objects with PrmObjectHead.MakeRaw). // *object.SplitInfoError (returned on virtual objects with PrmObjectHead.MakeRaw).
// //
// Return statuses: // Return statuses:
@ -437,94 +463,143 @@ func (x *ResObjectHead) ReadHeader(dst *object.Object) bool {
// - *apistatus.ObjectAlreadyRemoved; // - *apistatus.ObjectAlreadyRemoved;
// - *apistatus.SessionTokenExpired. // - *apistatus.SessionTokenExpired.
func (c *Client) ObjectHead(ctx context.Context, prm PrmObjectHead) (*ResObjectHead, error) { func (c *Client) ObjectHead(ctx context.Context, prm PrmObjectHead) (*ResObjectHead, error) {
switch { req, err := prm.buildRequest(c)
case ctx == nil: if err != nil {
panic(panicMsgMissingContext) return nil, err
case !prm.cnrSet:
panic(panicMsgMissingContainer)
case !prm.objSet:
panic("missing object")
} }
var addr v2refs.Address key := c.prm.key
if prm.Key != nil {
addr.SetContainerID(prm.cnr.ToV2()) key = *prm.Key
addr.SetObjectID(prm.obj.ToV2())
// form request body
var body v2object.HeadRequestBody
body.SetRaw(prm.raw)
body.SetAddress(&addr)
// form meta header
var meta v2session.RequestMetaHeader
prm.prmObjectRead.writeToMetaHeader(&meta)
// form request
var req v2object.HeadRequest
req.SetBody(&body)
req.SetMetaHeader(&meta)
// init call context
var (
cc contextCall
res ResObjectHead
)
res.idObj = prm.obj
if prm.keySet {
c.initCallContextWithoutKey(&cc)
cc.key = prm.key
} else {
c.initCallContext(&cc)
} }
cc.req = &req // sign the request
cc.statusRes = &res
cc.call = func() (responseV2, error) { err = signature.SignServiceMessage(&key, req)
return rpcapi.HeadObject(&c.c, &req, client.WithContext(ctx)) if err != nil {
return nil, fmt.Errorf("sign request: %w", err)
} }
cc.result = func(r responseV2) {
switch v := r.(*v2object.HeadResponse).GetBody().GetHeaderPart().(type) { resp, err := rpcapi.HeadObject(&c.c, req, client.WithContext(ctx))
if err != nil {
return nil, fmt.Errorf("write request: %w", err)
}
var res ResObjectHead
res.st, err = c.processResponse(resp)
if err != nil {
return nil, err
}
if !apistatus.IsSuccessful(res.st) {
return &res, nil
}
res.idObj = *prm.ObjectID
switch v := resp.GetBody().GetHeaderPart().(type) {
default: default:
cc.err = fmt.Errorf("unexpected header type %T", v) return nil, fmt.Errorf("unexpected header type %T", v)
case *v2object.SplitInfo: case *v2object.SplitInfo:
handleSplitInfo(&cc, v) return nil, object.NewSplitInfoError(object.NewSplitInfoFromV2(v))
case *v2object.HeaderWithSignature: case *v2object.HeaderWithSignature:
res.hdr = v res.hdr = v
} }
}
// process call
if !cc.processCall() {
return nil, cc.err
}
return &res, nil return &res, nil
} }
// PrmObjectRange groups parameters of ObjectRange operation. // PrmObjectRange groups parameters of ObjectRange operation.
type PrmObjectRange struct { type PrmObjectRange struct {
prmObjectRead XHeaders []string
off, ln uint64 BearerToken *bearer.Token
Session *session.Object
Raw bool
Local bool
ContainerID *cid.ID
ObjectID *oid.ID
Key *ecdsa.PrivateKey
Offset uint64
Length uint64
} }
// SetOffset sets offset of the payload range to be read. func (prm *PrmObjectRange) buildRequest(c *Client) (*v2object.GetRangeRequest, error) {
// Zero by default. if prm.Length == 0 {
func (x *PrmObjectRange) SetOffset(off uint64) { return nil, errorZeroRangeLength
x.off = off }
if prm.ContainerID == nil {
return nil, errorMissingContainer
}
if prm.ObjectID == nil {
return nil, errorMissingObject
}
if len(prm.XHeaders)%2 != 0 {
return nil, errorInvalidXHeaders
}
meta := new(v2session.RequestMetaHeader)
writeXHeadersToMeta(prm.XHeaders, meta)
if prm.BearerToken != nil {
v2BearerToken := new(acl.BearerToken)
prm.BearerToken.WriteToV2(v2BearerToken)
meta.SetBearerToken(v2BearerToken)
}
if prm.Session != nil {
v2SessionToken := new(v2session.Token)
prm.Session.WriteToV2(v2SessionToken)
meta.SetSessionToken(v2SessionToken)
}
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)
rng := new(v2object.Range)
rng.SetLength(prm.Length)
rng.SetOffset(prm.Offset)
body := new(v2object.GetRangeRequestBody)
body.SetRaw(prm.Raw)
body.SetAddress(addr)
body.SetRange(rng)
req := new(v2object.GetRangeRequest)
req.SetBody(body)
c.prepareRequest(req, meta)
return req, nil
} }
// SetLength sets length of the payload range to be read. // UseKey specifies private key to sign the requests.
// Must be positive. // If key is not provided, then Client default key is used.
func (x *PrmObjectRange) SetLength(ln uint64) { //
x.ln = ln // Deprecated: Use PrmObjectRange.Key instead.
func (prm *PrmObjectRange) UseKey(key ecdsa.PrivateKey) {
prm.Key = &key
} }
// ResObjectRange groups the final result values of ObjectRange operation. // ResObjectRange groups the final result values of ObjectRange operation.
@ -533,40 +608,28 @@ type ResObjectRange struct {
} }
// ObjectRangeReader is designed to read payload range of one object // ObjectRangeReader is designed to read payload range of one object
// from NeoFS system. // from FrostFS system.
// //
// Must be initialized using Client.ObjectRangeInit, any other // Must be initialized using Client.ObjectRangeInit, any other
// usage is unsafe. // usage is unsafe.
type ObjectRangeReader struct { type ObjectRangeReader struct {
cancelCtxStream context.CancelFunc cancelCtxStream context.CancelFunc
ctxCall contextCall client *Client
reqWritten bool res ResObjectRange
err error
// initially bound to contextCall stream interface {
bodyResp v2object.GetRangeResponseBody Read(resp *v2object.GetRangeResponse) error
}
tailPayload []byte tailPayload []byte
remainingPayloadLen int remainingPayloadLen int
} }
// UseKey specifies private key to sign the requests.
// If key is not provided, then Client default key is used.
func (x *ObjectRangeReader) UseKey(key ecdsa.PrivateKey) {
x.ctxCall.key = key
}
func (x *ObjectRangeReader) readChunk(buf []byte) (int, bool) { func (x *ObjectRangeReader) readChunk(buf []byte) (int, bool) {
if !x.reqWritten {
if !x.ctxCall.writeRequest() {
return 0, false
}
x.reqWritten = true
}
var read int var read int
// read remaining tail // read remaining tail
@ -578,25 +641,29 @@ func (x *ObjectRangeReader) readChunk(buf []byte) (int, bool) {
return read, true return read, true
} }
var ok bool
var partChunk *v2object.GetRangePartChunk var partChunk *v2object.GetRangePartChunk
var chunk []byte var chunk []byte
var lastRead int var lastRead int
for { for {
// receive next message var resp v2object.GetRangeResponse
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
} }
// get chunk message // get chunk message
switch v := x.bodyResp.GetRangePart().(type) { switch v := resp.GetBody().GetRangePart().(type) {
default: default:
x.ctxCall.err = fmt.Errorf("unexpected message received: %T", v) x.err = fmt.Errorf("unexpected message received: %T", v)
return read, false return read, false
case *v2object.SplitInfo: case *v2object.SplitInfo:
handleSplitInfo(&x.ctxCall, v) x.err = object.NewSplitInfoError(object.NewSplitInfoFromV2(v))
return read, false return read, false
case *v2object.GetRangePartChunk: case *v2object.GetRangePartChunk:
partChunk = v partChunk = v
@ -632,9 +699,9 @@ func (x *ObjectRangeReader) ReadChunk(buf []byte) (int, bool) {
func (x *ObjectRangeReader) close(ignoreEOF bool) (*ResObjectRange, error) { func (x *ObjectRangeReader) close(ignoreEOF bool) (*ResObjectRange, error) {
defer x.cancelCtxStream() defer x.cancelCtxStream()
if x.ctxCall.err != nil { if x.err != nil {
if !errors.Is(x.ctxCall.err, io.EOF) { if !errors.Is(x.err, io.EOF) {
return nil, x.ctxCall.err return nil, x.err
} else if !ignoreEOF { } else if !ignoreEOF {
if x.remainingPayloadLen > 0 { if x.remainingPayloadLen > 0 {
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
@ -644,7 +711,7 @@ func (x *ObjectRangeReader) close(ignoreEOF bool) (*ResObjectRange, error) {
} }
} }
return x.ctxCall.statusRes.(*ResObjectRange), nil return &x.res, nil
} }
// Close ends reading the payload range and returns the result of the operation // Close ends reading the payload range and returns the result of the operation
@ -652,10 +719,11 @@ func (x *ObjectRangeReader) close(ignoreEOF bool) (*ResObjectRange, error) {
// //
// Exactly one return value is non-nil. By default, server status is returned in res structure. // Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as Go built-in error. // Any client's internal or transport errors are returned as Go built-in error.
// If Client is tuned to resolve 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 errors: // Return errors:
//
// *object.SplitInfoError (returned on virtual objects with PrmObjectRange.MakeRaw). // *object.SplitInfoError (returned on virtual objects with PrmObjectRange.MakeRaw).
// //
// Return statuses: // Return statuses:
@ -664,6 +732,7 @@ func (x *ObjectRangeReader) close(ignoreEOF bool) (*ResObjectRange, error) {
// - *apistatus.ObjectNotFound; // - *apistatus.ObjectNotFound;
// - *apistatus.ObjectAccessDenied; // - *apistatus.ObjectAccessDenied;
// - *apistatus.ObjectAlreadyRemoved; // - *apistatus.ObjectAlreadyRemoved;
// - *apistatus.ObjectOutOfRange;
// - *apistatus.SessionTokenExpired. // - *apistatus.SessionTokenExpired.
func (x *ObjectRangeReader) Close() (*ResObjectRange, error) { func (x *ObjectRangeReader) Close() (*ResObjectRange, error) {
return x.close(true) return x.close(true)
@ -692,85 +761,42 @@ func (x *ObjectRangeReader) Read(p []byte) (int, error) {
} }
// ObjectRangeInit initiates reading an object's payload range through a remote // ObjectRangeInit initiates reading an object's payload range through a remote
// server using NeoFS API protocol. // server using FrostFS API protocol.
// //
// The call only opens the transmission channel, explicit fetching is done using the ObjectRangeReader. // The call only opens the transmission channel, explicit fetching is done using the ObjectRangeReader.
// Exactly one return value is non-nil. Resulting reader must be finally closed. // Exactly one return value is non-nil. Resulting reader must be finally closed.
// //
// Immediately panics if parameters are set incorrectly (see PrmObjectRange docs). // Returns an error if parameters are set incorrectly (see PrmObjectRange docs).
// Context is required and must not be nil. It is used for network communication. // Context is required and must not be nil. It is used for network communication.
func (c *Client) ObjectRangeInit(ctx context.Context, prm PrmObjectRange) (*ObjectRangeReader, error) { func (c *Client) ObjectRangeInit(ctx context.Context, prm PrmObjectRange) (*ObjectRangeReader, error) {
// check parameters req, err := prm.buildRequest(c)
switch {
case ctx == nil:
panic(panicMsgMissingContext)
case !prm.cnrSet:
panic(panicMsgMissingContainer)
case !prm.objSet:
panic("missing object")
case prm.ln == 0:
panic("zero range length")
}
var addr v2refs.Address
addr.SetContainerID(prm.cnr.ToV2())
addr.SetObjectID(prm.obj.ToV2())
var rng v2object.Range
rng.SetOffset(prm.off)
rng.SetLength(prm.ln)
// form request body
var body v2object.GetRangeRequestBody
body.SetRaw(prm.raw)
body.SetAddress(&addr)
body.SetRange(&rng)
// form meta header
var meta v2session.RequestMetaHeader
prm.prmObjectRead.writeToMetaHeader(&meta)
// form request
var req v2object.GetRangeRequest
req.SetBody(&body)
req.SetMetaHeader(&meta)
// init reader
var (
r ObjectRangeReader
resp v2object.GetRangeResponse
stream *rpcapi.ObjectRangeResponseReader
)
r.remainingPayloadLen = int(prm.ln)
ctx, r.cancelCtxStream = context.WithCancel(ctx)
resp.SetBody(&r.bodyResp)
// init call context
c.initCallContext(&r.ctxCall)
r.ctxCall.req = &req
r.ctxCall.statusRes = new(ResObjectRange)
r.ctxCall.resp = &resp
r.ctxCall.wReq = func() error {
var err error
stream, err = rpcapi.GetObjectRange(&c.c, &req, client.WithContext(ctx))
if err != nil { if err != nil {
return fmt.Errorf("open stream: %w", err) return nil, err
} }
return nil key := prm.Key
if key == nil {
key = &c.prm.key
} }
r.ctxCall.rResp = func() error {
return stream.Read(&resp) err = signature.SignServiceMessage(key, req)
if err != nil {
return nil, fmt.Errorf("sign request: %w", err)
} }
ctx, cancel := context.WithCancel(ctx)
stream, err := rpcapi.GetObjectRange(&c.c, req, client.WithContext(ctx))
if err != nil {
cancel()
return nil, fmt.Errorf("open stream: %w", err)
}
var r ObjectRangeReader
r.remainingPayloadLen = int(prm.Length)
r.cancelCtxStream = cancel
r.stream = stream
r.client = c
return &r, nil return &r, nil
} }

View file

@ -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.ResolveFrostFSFailures has been called, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included // FrostFS status codes are returned as `error`, otherwise, are included
// in the returned result structure. // in the returned result structure.
// //
// Immediately panics if parameters are set incorrectly (see PrmObjectHash docs). // Returns an error if parameters are set incorrectly (see PrmObjectHash docs).
// Context is required and must not be nil. It is used for network communication. // 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

View file

@ -3,231 +3,158 @@ 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"
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc" v2session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
v2session "github.com/nspcc-dev/neofs-api-go/v2/session" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
"github.com/nspcc-dev/neofs-sdk-go/object" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
oid "github.com/nspcc-dev/neofs-sdk-go/object/id" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/transformer"
"github.com/nspcc-dev/neofs-sdk-go/session" "git.frostfs.info/TrueCloudLab/frostfs-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 {
// At the moment the operation is not parameterized, however, copyNum []uint32
// the structure is still declared for backward compatibility. key *ecdsa.PrivateKey
type PrmObjectPutInit struct{} meta v2session.RequestMetaHeader
maxChunkLen int
maxSize uint64
epochSource transformer.EpochSource
withoutHomomorphicHash bool
}
// SetCopiesNumber sets number of object copies that is enough to consider put successful.
func (x *PrmObjectPutInit) SetCopiesNumber(copiesNumber uint32) {
x.copyNum = []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.
func (x *PrmObjectPutInit) SetCopiesNumberByVectors(copiesNumbers []uint32) {
x.copyNum = 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.
func (x *PrmObjectPutInit) SetGRPCPayloadChunkLen(v int) {
x.maxChunkLen = 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
} }
// 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. // 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) { func (x *PrmObjectPutInit) UseKey(key ecdsa.PrivateKey) {
x.ctxCall.key = key 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) { func (x *PrmObjectPutInit) WithBearerToken(t bearer.Token) {
x.metaHdr.SetBearerToken(t.ToV2()) var v2token acl.BearerToken
t.WriteToV2(&v2token)
x.meta.SetBearerToken(&v2token)
} }
// WithinSession specifies session within which object should be stored. // WithinSession specifies session within which object should be stored.
// Should be called once before any writing steps. // Should be called once before any writing steps.
func (x *ObjectWriter) WithinSession(t session.Token) { func (x *PrmObjectPutInit) WithinSession(t session.Object) {
x.metaHdr.SetSessionToken(t.ToV2()) var tv2 v2session.Token
t.WriteToV2(&tv2)
x.meta.SetSessionToken(&tv2)
} }
// MarkLocal tells the server to execute the operation locally. // MarkLocal tells the server to execute the operation locally.
func (x *ObjectWriter) MarkLocal() { func (x *PrmObjectPutInit) MarkLocal() {
x.metaHdr.SetTTL(1) x.meta.SetTTL(1)
} }
// WithXHeaders specifies list of extended headers (string key-value pairs) // WithXHeaders specifies list of extended headers (string key-value pairs)
// to be attached to the request. Must have an even length. // to be attached to the request. Must have an even length.
// //
// Slice must not be mutated until the operation completes. // Slice must not be mutated until the operation completes.
func (x *ObjectWriter) WithXHeaders(hs ...string) { func (x *PrmObjectPutInit) WithXHeaders(hs ...string) {
if len(hs)%2 != 0 { writeXHeadersToMeta(hs, &x.meta)
panic("slice of X-Headers with odd length")
}
prmCommonMeta{xHeaders: hs}.writeToMetaHeader(&x.metaHdr)
} }
// WriteHeader writes header of the object. Result means success. // WithObjectMaxSize specifies max object size value and use it during object splitting.
// Failure reason can be received via Close. // When specified, start writing to the stream only after the object is formed.
func (x *ObjectWriter) WriteHeader(hdr object.Object) bool { // Continue processing the input only when the previous formed object has been successfully written.
v2Hdr := hdr.ToV2() func (x *PrmObjectPutInit) WithObjectMaxSize(maxSize uint64) {
x.maxSize = maxSize
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. // WithoutHomomorphicHash if set to true do not use Tillich-Zémor hash for payload.
// Failure reason can be received via Close. func (x *PrmObjectPutInit) WithoutHomomorphicHash(v bool) {
func (x *ObjectWriter) WritePayloadChunk(chunk []byte) bool { x.withoutHomomorphicHash = v
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 // WithEpochSource specifies epoch for object when split it on client side.
// along with the final results. Must be called after using the ObjectWriter. func (x *PrmObjectPutInit) WithEpochSource(es transformer.EpochSource) {
// x.epochSource = es
// Exactly one return value is non-nil. By default, server status is returned in res structure.
// Any client's internal or transport errors are returned as Go built-in error.
// If Client is tuned to resolve NeoFS API statuses, then NeoFS failures
// codes are returned as error.
//
// Return statuses:
// - global (see Client docs);
// - *apistatus.ContainerNotFound;
// - *apistatus.ObjectAccessDenied;
// - *apistatus.ObjectLocked;
// - *apistatus.LockNonRegularObject;
// - *apistatus.SessionTokenNotFound;
// - *apistatus.SessionTokenExpired.
func (x *ObjectWriter) Close() (*ResObjectPut, error) {
defer x.cancelCtxStream()
if x.ctxCall.err != nil {
return nil, x.ctxCall.err
}
if !x.ctxCall.close() {
return nil, x.ctxCall.err
}
if !x.ctxCall.processResponse() {
return nil, x.ctxCall.err
}
return x.ctxCall.statusRes.(*ResObjectPut), nil
} }
// ObjectPutInit initiates writing an object through a remote server using NeoFS API protocol. // ObjectPutInit initiates writing an object through a remote server using FrostFS API protocol.
// //
// The call only opens the transmission channel, explicit recording is done using the ObjectWriter. // 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
} }

154
client/object_put_raw.go Normal file
View file

@ -0,0 +1,154 @@
package client
import (
"context"
"crypto/ecdsa"
"errors"
"fmt"
"io"
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"
"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) {
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.copyNum)
w.req.SetBody(new(v2object.PutRequestBody))
if prm.maxChunkLen > 0 {
w.maxChunkLen = prm.maxChunkLen
} else {
w.maxChunkLen = defaultGRPCPayloadChunkLen
}
c.prepareRequest(&w.req, &prm.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)
}
return &x.res, nil
}

116
client/object_put_single.go Normal file
View file

@ -0,0 +1,116 @@
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/session"
)
// PrmObjectPutSingle groups parameters of PutSingle operation.
type PrmObjectPutSingle struct {
copyNum []uint32
meta v2session.RequestMetaHeader
object *v2object.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.
func (x *PrmObjectPutSingle) SetCopiesNumber(v []uint32) {
x.copyNum = v
}
// UseKey specifies private key to sign the requests.
// If key is not provided, then Client default key is used.
func (x *PrmObjectPutSingle) UseKey(key *ecdsa.PrivateKey) {
x.key = key
}
// WithBearerToken attaches bearer token to be used for the operation.
// Should be called once before any writing steps.
func (x *PrmObjectPutSingle) WithBearerToken(t bearer.Token) {
v2token := &acl.BearerToken{}
t.WriteToV2(v2token)
x.meta.SetBearerToken(v2token)
}
// WithinSession specifies session within which object should be stored.
// Should be called once before any writing steps.
func (x *PrmObjectPutSingle) WithinSession(t session.Object) {
tv2 := &v2session.Token{}
t.WriteToV2(tv2)
x.meta.SetSessionToken(tv2)
}
// ExecuteLocal tells the server to execute the operation locally.
func (x *PrmObjectPutSingle) ExecuteLocal() {
x.meta.SetTTL(1)
}
// WithXHeaders specifies list of extended headers (string key-value pairs)
// to be attached to the request. Must have an even length.
//
// Slice must not be mutated until the operation completes.
func (x *PrmObjectPutSingle) WithXHeaders(hs ...string) {
writeXHeadersToMeta(hs, &x.meta)
}
// SetObject specifies prepared object to put.
func (x *PrmObjectPutSingle) SetObject(o *v2object.Object) {
x.object = o
}
// ResObjectPutSingle groups resulting values of PutSingle operation.
type ResObjectPutSingle struct {
statusRes
}
// 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) {
body := &v2object.PutSingleRequestBody{}
body.SetCopiesNumber(prm.copyNum)
body.SetObject(prm.object)
req := &v2object.PutSingleRequest{}
req.SetBody(body)
c.prepareRequest(req, &prm.meta)
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
}
return &res, nil
}

View file

@ -0,0 +1,119 @@
package client
import (
"context"
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.withoutHomomorphicHash,
NetworkState: prm.epochSource,
})
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) {
ai, err := x.ot.Close(ctx)
if err != nil {
return nil, err
}
if ai != nil && 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.resolveFrostFSErrors && !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
}
var prm PrmObjectPutSingle
prm.SetCopiesNumber(it.prm.copyNum)
prm.SetObject(o.ToV2())
prm.UseKey(prm.key)
prm.meta = it.prm.meta
res, err := it.client.ObjectPutSingle(ctx, prm)
if err != nil && status.Code(err) == codes.Unimplemented {
return false, err
}
if err == nil {
id, _ := o.ID()
it.res = &ResObjectPut{
statusRes: res.statusRes,
obj: id,
}
if !it.client.prm.resolveFrostFSErrors && !apistatus.IsSuccessful(it.res.st) {
return true, apistatus.ErrFromStatus(it.res.st)
}
return true, nil
}
return true, err
}

View file

@ -7,40 +7,36 @@ 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 meta v2session.RequestMetaHeader
local bool key *ecdsa.PrivateKey
sessionSet bool
session session.Token
bearerSet bool
bearer token.BearerToken
cnrSet bool cnrSet bool
cnr cid.ID cnrID cid.ID
filters object.SearchFilters filters object.SearchFilters
} }
// MarkLocal tells the server to execute the operation locally. // MarkLocal tells the server to execute the operation locally.
func (x *PrmObjectSearch) MarkLocal() { func (x *PrmObjectSearch) MarkLocal() {
x.local = true x.meta.SetTTL(1)
} }
// WithinSession specifies session within which the search query must be executed. // WithinSession specifies session within which the search query must be executed.
@ -49,9 +45,10 @@ func (x *PrmObjectSearch) MarkLocal() {
// This may affect the execution of an operation (e.g. access control). // 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) { func (x *PrmObjectSearch) WithinSession(t session.Object) {
x.session = t var tokv2 v2session.Token
x.sessionSet = true t.WriteToV2(&tokv2)
x.meta.SetSessionToken(&tokv2)
} }
// WithBearerToken attaches bearer token to be used for the operation. // WithBearerToken attaches bearer token to be used for the operation.
@ -59,15 +56,30 @@ func (x *PrmObjectSearch) WithinSession(t session.Token) {
// If set, underlying eACL rules will be used in access control. // If set, underlying eACL rules will be used in access control.
// //
// Must be signed. // Must be signed.
func (x *PrmObjectSearch) WithBearerToken(t token.BearerToken) { func (x *PrmObjectSearch) WithBearerToken(t bearer.Token) {
x.bearer = t var v2token acl.BearerToken
x.bearerSet = true t.WriteToV2(&v2token)
x.meta.SetBearerToken(&v2token)
}
// WithXHeaders specifies list of extended headers (string key-value pairs)
// to be attached to the request. Must have an even length.
//
// Slice must not be mutated until the operation completes.
func (x *PrmObjectSearch) WithXHeaders(hs ...string) {
writeXHeadersToMeta(hs, &x.meta)
}
// UseKey specifies private key to sign the requests.
// If key is not provided, then Client default key is used.
func (x *PrmObjectSearch) UseKey(key ecdsa.PrivateKey) {
x.key = &key
} }
// InContainer specifies the container in which to look for objects. // InContainer specifies the container in which to look for objects.
// Required parameter. // Required parameter.
func (x *PrmObjectSearch) InContainer(id cid.ID) { func (x *PrmObjectSearch) InContainer(id cid.ID) {
x.cnr = id x.cnrID = id
x.cnrSet = true x.cnrSet = true
} }
@ -82,28 +94,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 +119,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 +157,14 @@ func (x *ObjectListReader) Read(buf []oid.ID) (int, bool) {
} }
} }
func copyIDBuffers(dst []oid.ID, src []v2refs.ObjectID) int {
var i int
for ; i < len(dst) && i < len(src); i++ {
_ = dst[i].ReadFromV2(src[i])
}
return i
}
// Iterate iterates over the list of found object identifiers. // 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 +194,7 @@ func (x *ObjectListReader) Iterate(f func(oid.ID) bool) error {
// //
// Exactly one return value is non-nil. By default, server status is returned in res structure. // 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 +205,58 @@ 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. // 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 // check parameters
switch { if !prm.cnrSet {
case ctx == nil: return nil, errorMissingContainer
panic(panicMsgMissingContext)
case !prm.cnrSet:
panic(panicMsgMissingContainer)
} }
// form request body var cidV2 v2refs.ContainerID
var body v2object.SearchRequestBody prm.cnrID.WriteToV2(&cidV2)
var body v2object.SearchRequestBody
body.SetVersion(1) body.SetVersion(1)
body.SetContainerID(prm.cnr.ToV2()) body.SetContainerID(&cidV2)
body.SetFilters(prm.filters.ToV2()) body.SetFilters(prm.filters.ToV2())
// form meta header
var meta v2session.RequestMetaHeader
if prm.local {
meta.SetTTL(1)
}
if prm.bearerSet {
meta.SetBearerToken(prm.bearer.ToV2())
}
if prm.sessionSet {
meta.SetSessionToken(prm.session.ToV2())
}
prm.prmCommonMeta.writeToMetaHeader(&meta)
// form request
var req v2object.SearchRequest
req.SetBody(&body)
req.SetMetaHeader(&meta)
// init reader // init reader
var ( var req v2object.SearchRequest
r ObjectListReader req.SetBody(&body)
resp v2object.SearchResponse c.prepareRequest(&req, &prm.meta)
stream *rpcapi.SearchResponseReader
)
key := prm.key
if key == nil {
key = &c.prm.key
}
err := signature.SignServiceMessage(key, &req)
if err != nil {
return nil, fmt.Errorf("sign request: %w", err)
}
var r ObjectListReader
ctx, r.cancelCtxStream = context.WithCancel(ctx) ctx, r.cancelCtxStream = context.WithCancel(ctx)
resp.SetBody(&r.bodyResp) r.stream, err = rpcapi.SearchObjects(&c.c, &req, client.WithContext(ctx))
// init call context
c.initCallContext(&r.ctxCall)
r.ctxCall.req = &req
r.ctxCall.statusRes = new(ResObjectSearch)
r.ctxCall.resp = &resp
r.ctxCall.wReq = func() error {
var err error
stream, err = rpcapi.SearchObjects(&c.c, &req, client.WithContext(ctx))
if err != nil { if err != nil {
return fmt.Errorf("open stream: %w", err) return nil, fmt.Errorf("open stream: %w", err)
}
return nil
}
r.ctxCall.rResp = func() error {
return stream.Read(&resp)
} }
r.client = c
return &r, nil return &r, nil
} }

View file

@ -1,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 },
result: func(v2 responseV2) {},
statusRes: new(ResObjectSearch),
},
reqWritten: true,
bodyResp: object.SearchResponseBody{},
tail: nil, tail: nil,
} }
}
return obj, func(id []oid.ID) *object.SearchResponse { func newSearchStream(key *ecdsa.PrivateKey, endError error, idList ...[]oid.ID) *singleStreamResponder {
resp := new(object.SearchResponse) return &singleStreamResponder{
resp.SetBody(new(object.SearchResponseBody)) key: key,
endError: endError,
v2id := make([]refs.ObjectID, len(id)) idList: idList,
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
} }
} }
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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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.ResolveFrostFSFailures has been called, unsuccessful
// NeoFS status codes are returned as `error`, otherwise, are included // FrostFS status codes are returned as `error`, otherwise, are included
// in the returned result structure. // in the returned result structure.
// //
// Immediately panics if parameters are set incorrectly (see PrmSessionCreate docs). // Returns an error if parameters are set incorrectly (see PrmSessionCreate docs).
// Context is required and must not be nil. It is used for network communication. // 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()) resp, err := rpcapi.CreateSession(&c.c, req, client.WithContext(ctx))
reqBody.SetExpiration(prm.exp) if err != nil {
return nil, err
// for request }
var req v2session.CreateRequest
var res ResSessionCreate
req.SetBody(reqBody) res.st, err = c.processResponse(resp)
if err != nil || !apistatus.IsSuccessful(res.st) {
// init call context return &res, err
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() body := resp.GetBody()
res.id = body.GetID()
res.setID(body.GetID()) res.sessionKey = body.GetSessionKey()
res.setSessionKey(body.GetSessionKey())
}
// process call
if !cc.processCall() {
return nil, cc.err
}
return &res, nil return &res, nil
} }

View file

@ -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()
}

View file

@ -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())
})
}

View file

@ -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
} }

View file

@ -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
} }

View file

@ -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"
) )

View file

@ -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
} }

View file

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

View file

@ -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"
) )

View file

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

View file

@ -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())
} }

View file

@ -3,19 +3,19 @@ package apistatus
import ( import (
"fmt" "fmt"
"github.com/nspcc-dev/neofs-api-go/v2/container" "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
"github.com/nspcc-dev/neofs-api-go/v2/object" "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
"github.com/nspcc-dev/neofs-api-go/v2/session" "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
"github.com/nspcc-dev/neofs-api-go/v2/status" "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status"
) )
// StatusV2 defines a variety of Status instances compatible with 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 +28,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 +56,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 +73,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
@ -97,7 +106,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 +117,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"

View file

@ -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,12 @@ func TestToStatusV2(t *testing.T) {
}), }),
codeV2: 4097, codeV2: 4097,
}, },
{
status: (statusConstructor)(func() apistatus.Status {
return new(apistatus.NodeUnderMaintenance)
}),
codeV2: 1027,
},
} { } {
var st apistatus.Status var st apistatus.Status
@ -124,6 +144,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 +165,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 +248,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 +278,12 @@ func TestFromStatusV2(t *testing.T) {
}), }),
codeV2: 4097, codeV2: 4097,
}, },
{
status: (statusConstructor)(func() apistatus.Status {
return new(apistatus.NodeUnderMaintenance)
}),
codeV2: 1027,
},
} { } {
var st apistatus.Status var st apistatus.Status
@ -254,6 +297,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
View 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
View 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
}

View 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
View 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
View 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
View 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
View 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)
}
}
}

View file

@ -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)
}

View file

@ -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))
})
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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"
// Defaults: attributeTimestamp = "Timestamp"
// - token: nil; )
// - sig: nil;
// - basicACL: acl.PrivateBasicRule;
// - version: version.Current;
// - nonce: random UUID;
// - attr: nil;
// - policy: nil;
// - ownerID: nil.
func New(opts ...Option) *Container {
cnrOptions := defaultContainerOptions()
for i := range opts { // reads Container from the container.Container message. If checkFieldPresence is set,
opts[i](&cnrOptions) // returns an error on absence of any protocol-required field.
} func (x *Container) readFromV2(m container.Container, checkFieldPresence bool) error {
var err error
cnr := new(Container) ownerV2 := m.GetOwnerID()
cnr.SetNonceUUID(cnrOptions.nonce) if ownerV2 != nil {
cnr.SetBasicACL(cnrOptions.acl) var owner user.ID
if cnrOptions.owner != nil { err = owner.ReadFromV2(*ownerV2)
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.
//
// Nil Container converts to nil.
func (c *Container) ToV2() *container.Container {
if c == nil {
return nil
}
return &c.v2
}
// NewVerifiedFromV2 constructs Container from NeoFS API V2 Container message.
//
// Does not perform if message meets NeoFS API V2 specification. To do this
// use NewVerifiedFromV2 constructor.
func NewContainerFromV2(c *container.Container) *Container {
cnr := new(Container)
if c != nil {
cnr.v2 = *c
}
return cnr
}
// CalculateID calculates container identifier
// based on its structure.
func CalculateID(c *Container) *cid.ID {
data, err := c.ToV2().StableMarshal(nil)
if err != nil { if err != nil {
panic(err) return fmt.Errorf("invalid owner: %w", err)
}
} else if checkFieldPresence {
return errors.New("missing owner")
} }
id := cid.New() binNonce := m.GetNonce()
id.SetSHA256(sha256.Sum256(data)) if len(binNonce) > 0 {
var nonce uuid.UUID
return id 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 (c *Container) Version() *version.Version { func checkAttributes(m container.Container) error {
return version.NewFromV2(c.v2.GetVersion()) 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
} }
func (c *Container) SetVersion(v *version.Version) { // ReadFromV2 reads Container from the container.Container message. Checks if the
c.v2.SetVersion(v.ToV2()) // message conforms to FrostFS API V2 protocol.
}
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 WriteToV2.
func (c *Container) NonceUUID() (uuid.UUID, error) { func (x *Container) ReadFromV2(m container.Container) error {
return uuid.FromBytes(c.v2.GetNonce()) return x.readFromV2(m, true)
} }
// SetNonceUUID sets container nonce as UUID. // WriteToV2 writes Container into the container.Container message.
func (c *Container) SetNonceUUID(v uuid.UUID) { // The message MUST NOT be nil.
data, _ := v.MarshalBinary() //
c.v2.SetNonce(data) // See also ReadFromV2.
func (x Container) WriteToV2(m *container.Container) {
*m = x.v2
} }
func (c *Container) BasicACL() uint32 { // Marshal encodes Container into a binary format of the FrostFS API protocol
return c.v2.GetBasicACL() // (Protocol Buffers with direct field order).
//
// See also Unmarshal.
func (x Container) Marshal() []byte {
return x.v2.StableMarshal(nil)
} }
func (c *Container) SetBasicACL(v acl.BasicACL) { // Unmarshal decodes FrostFS API protocol binary format into the Container
c.v2.SetBasicACL(uint32(v)) // (Protocol Buffers with direct field order). Returns an error describing
// a format violation.
//
// See also Marshal.
func (x *Container) Unmarshal(data []byte) error {
var m container.Container
err := m.Unmarshal(data)
if err != nil {
return err
}
return x.readFromV2(m, false)
} }
func (c *Container) Attributes() Attributes { // MarshalJSON encodes Container into a JSON format of the FrostFS API protocol
return NewAttributesFromV2(c.v2.GetAttributes()) // (Protocol Buffers JSON).
//
// See also UnmarshalJSON.
func (x Container) MarshalJSON() ([]byte, error) {
return x.v2.MarshalJSON()
} }
func (c *Container) SetAttributes(v Attributes) { // UnmarshalJSON decodes FrostFS API protocol JSON format into the Container
c.v2.SetAttributes(v.ToV2()) // (Protocol Buffers JSON). Returns an error describing a format violation.
//
// See also MarshalJSON.
func (x *Container) UnmarshalJSON(data []byte) error {
return x.v2.UnmarshalJSON(data)
} }
func (c *Container) PlacementPolicy() *netmap.PlacementPolicy { // Init initializes all internal data of the Container required by FrostFS API
return netmap.NewPlacementPolicyFromV2(c.v2.GetPlacementPolicy()) // 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) SetPlacementPolicy(v *netmap.PlacementPolicy) { // SetOwner specifies the owner of the Container. Each Container has exactly
c.v2.SetPlacementPolicy(v.ToV2()) // 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)
} }
// SessionToken returns token of the session within // Owner returns owner of the Container set using SetOwner.
// which container was created. //
func (c Container) SessionToken() *session.Token { // Zero Container has no owner which is incorrect according to FrostFS API
return c.token // 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
} }
// SetSessionToken sets token of the session within // SetBasicACL specifies basic part of the Container ACL. Basic ACL is used
// which container was created. // to control access inside container storage.
func (c *Container) SetSessionToken(t *session.Token) { //
c.token = t // See also BasicACL.
func (x *Container) SetBasicACL(basicACL acl.Basic) {
x.v2.SetBasicACL(basicACL.Bits())
} }
// Signature returns signature of the marshaled container. // BasicACL returns basic ACL set using SetBasicACL.
func (c Container) Signature() *signature.Signature { //
return c.sig // 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
} }
// SetSignature sets signature of the marshaled container. // SetPlacementPolicy sets placement policy for the objects within the Container.
func (c *Container) SetSignature(sig *signature.Signature) { // FrostFS storage layer strives to follow the specified policy.
c.sig = sig //
// See also PlacementPolicy.
func (x *Container) SetPlacementPolicy(policy netmap.PlacementPolicy) {
var m v2netmap.PlacementPolicy
policy.WriteToV2(&m)
x.v2.SetPlacementPolicy(&m)
} }
// Marshal marshals Container into a protobuf binary form. // PlacementPolicy returns placement policy set using SetPlacementPolicy.
func (c *Container) Marshal() ([]byte, error) { //
return c.v2.StableMarshal(nil) // Zero Container has no placement policy which is incorrect according to
// FrostFS API protocol.
func (x Container) PlacementPolicy() (res netmap.PlacementPolicy) {
m := x.v2.GetPlacementPolicy()
if m != nil {
err := res.ReadFromV2(*m)
if err != nil {
panic(fmt.Sprintf("unexpected error from PlacementPolicy.ReadFromV2: %v", err))
}
}
return
} }
// Unmarshal unmarshals protobuf binary representation of Container. // SetAttribute sets Container attribute value by key. Both key and value
func (c *Container) Unmarshal(data []byte) error { // MUST NOT be empty. Attributes set by the creator (owner) are most commonly
return c.v2.Unmarshal(data) // ignored by the FrostFS system and used for application layer. Some attributes
// are so-called system or well-known attributes: they are reserved for system
// needs. System attributes SHOULD NOT be modified using SetAttribute, use
// corresponding methods/functions. List of the reserved keys is documented
// in the particular protocol version.
//
// SetAttribute overwrites existing attribute value.
//
// See also Attribute, IterateAttributes, IterateUserAttributes.
func (x *Container) SetAttribute(key, value string) {
if key == "" {
panic("empty attribute key")
} else if value == "" {
panic("empty attribute value")
}
attrs := x.v2.GetAttributes()
ln := len(attrs)
for i := 0; i < ln; i++ {
if attrs[i].GetKey() == key {
attrs[i].SetValue(value)
return
}
}
attrs = append(attrs, container.Attribute{})
attrs[ln].SetKey(key)
attrs[ln].SetValue(value)
x.v2.SetAttributes(attrs)
} }
// MarshalJSON encodes Container to protobuf JSON format. // Attribute reads value of the Container attribute by key. Empty result means
func (c *Container) MarshalJSON() ([]byte, error) { // attribute absence.
return c.v2.MarshalJSON() //
// 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 ""
} }
// UnmarshalJSON decodes Container from protobuf JSON format. // IterateAttributes iterates over all Container attributes and passes them
func (c *Container) UnmarshalJSON(data []byte) error { // into f. The handler MUST NOT be nil.
return c.v2.UnmarshalJSON(data) //
// See also SetAttribute, Attribute.
func (x Container) IterateAttributes(f func(key, val string)) {
attrs := x.v2.GetAttributes()
for i := range attrs {
f(attrs[i].GetKey(), attrs[i].GetValue())
}
}
// IterateUserAttributes iterates over user Container attributes and passes them
// into f. The handler MUST NOT be nil.
//
// See also SetAttribute, Attribute.
func (x Container) IterateUserAttributes(f func(key, val string)) {
attrs := x.v2.GetAttributes()
for _, attr := range attrs {
var key = attr.GetKey()
if !strings.HasPrefix(key, container.SysAttributePrefix) &&
!strings.HasPrefix(key, container.SysAttributePrefixNeoFS) {
f(key, attr.GetValue())
}
}
}
// SetName sets human-readable name of the Container. Name MUST NOT be empty.
//
// See also Name.
func SetName(cnr *Container, name string) {
cnr.SetAttribute(attributeName, name)
}
// Name returns container name set using SetName.
//
// Zero Container has no name.
func Name(cnr Container) string {
return cnr.Attribute(attributeName)
}
// SetCreationTime writes container's creation time in Unix Timestamp format.
//
// See also CreatedAt.
func SetCreationTime(cnr *Container, t time.Time) {
cnr.SetAttribute(attributeTimestamp, strconv.FormatInt(t.Unix(), 10))
}
// CreatedAt returns container's creation time set using SetCreationTime.
//
// Zero Container has zero timestamp (in seconds).
func CreatedAt(cnr Container) time.Time {
var sec int64
attr := cnr.Attribute(attributeTimestamp)
if attr != "" {
var err error
sec, err = strconv.ParseInt(cnr.Attribute(attributeTimestamp), 10, 64)
if err != nil {
panic(fmt.Sprintf("parse container timestamp: %v", err))
}
}
return time.Unix(sec, 0)
}
const attributeHomoHashEnabled = "true"
// DisableHomomorphicHashing sets flag to disable homomorphic hashing of the
// Container data.
//
// See also IsHomomorphicHashingDisabled.
func DisableHomomorphicHashing(cnr *Container) {
cnr.SetAttribute(container.SysAttributeHomomorphicHashing, attributeHomoHashEnabled)
}
// IsHomomorphicHashingDisabled checks if DisableHomomorphicHashing was called.
//
// Zero Container has enabled hashing.
func IsHomomorphicHashingDisabled(cnr Container) bool {
return cnr.Attribute(container.SysAttributeHomomorphicHashing) == attributeHomoHashEnabled ||
cnr.Attribute(container.SysAttributeHomomorphicHashingNeoFS) == attributeHomoHashEnabled
}
// Domain represents information about container domain registered in the NNS
// contract deployed in the FrostFS network.
type Domain struct {
name, zone string
}
// SetName sets human-friendly container domain name.
func (x *Domain) SetName(name string) {
x.name = name
}
// Name returns name set using SetName.
//
// Zero Domain has zero name.
func (x Domain) Name() string {
return x.name
}
// SetZone sets zone which is used as a TLD of a domain name in NNS contract.
func (x *Domain) SetZone(zone string) {
x.zone = zone
}
// Zone returns domain zone set using SetZone.
//
// Zero Domain has "container" zone.
func (x Domain) Zone() string {
if x.zone != "" {
return x.zone
}
return "container"
}
// WriteDomain writes Domain into the Container. Name MUST NOT be empty.
func WriteDomain(cnr *Container, domain Domain) {
cnr.SetAttribute(container.SysAttributeName, domain.Name())
cnr.SetAttribute(container.SysAttributeZone, domain.Zone())
}
// ReadDomain reads Domain from the Container. Returns value with empty name
// if domain is not specified.
func ReadDomain(cnr Container) (res Domain) {
if name := cnr.Attribute(container.SysAttributeName); name != "" {
res.SetName(name)
res.SetZone(cnr.Attribute(container.SysAttributeZone))
} else if name = cnr.Attribute(container.SysAttributeNameNeoFS); name != "" {
res.SetName(name)
res.SetZone(cnr.Attribute(container.SysAttributeZoneNeoFS))
}
return
}
// CalculateSignature calculates signature of the Container using provided signer
// and writes it into dst. Signature instance MUST NOT be nil. CalculateSignature
// is expected to be called after all the Container data is filled and before
// saving the Container in the FrostFS network. Note that мany subsequent change
// will most likely break the signature.
//
// See also VerifySignature.
func CalculateSignature(dst *frostfscrypto.Signature, cnr Container, signer ecdsa.PrivateKey) error {
return dst.Calculate(frostfsecdsa.SignerRFC6979(signer), cnr.Marshal())
}
// VerifySignature verifies Container signature calculated using CalculateSignature.
// Result means signature correctness.
func VerifySignature(sig frostfscrypto.Signature, cnr Container) bool {
return sig.Verify(cnr.Marshal())
}
// CalculateIDFromBinary calculates identifier of the binary-encoded container
// in CAS of the FrostFS containers and writes it into dst. ID instance MUST NOT
// be nil.
//
// See also CalculateID, AssertID.
func CalculateIDFromBinary(dst *cid.ID, cnr []byte) {
dst.SetSHA256(sha256.Sum256(cnr))
}
// CalculateID encodes the given Container and passes the result into
// CalculateIDFromBinary.
//
// See also Container.Marshal, AssertID.
func CalculateID(dst *cid.ID, cnr Container) {
CalculateIDFromBinary(dst, cnr.Marshal())
}
// AssertID checks if the given Container matches its identifier in CAS of the
// FrostFS containers.
//
// See also CalculateID.
func AssertID(id cid.ID, cnr Container) bool {
var id2 cid.ID
CalculateID(&id2, cnr)
return id2.Equals(id)
} }

View file

@ -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
View 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
View 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

View file

@ -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()
} }

View file

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

13
container/id/test/doc.go Normal file
View file

@ -0,0 +1,13 @@
/*
Package cidtest provides functions for convenient testing of cid package API.
Note that importing the package into source files is highly discouraged.
Random instance generation functions can be useful when testing expects any value, e.g.:
import cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test"
cid := cidtest.ID()
// test the value
*/
package cidtest

View file

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

20
container/network.go Normal file
View file

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

33
container/network_test.go Normal file
View file

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

View file

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

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