diff --git a/CHANGELOG.md b/CHANGELOG.md index 548701d641..f222602631 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,14 +39,17 @@ Changelog for NeoFS Node - Optimized netmap caching in node (#3966) - Store in metabase associated object ID in bytes instead of Base58 (#3971) - Optimized local RANGE request execution (#3967) +- GET now supports payload ranges (#3991) ### Removed - `policer.max_workers` configuration (#3920) - Deprecated `Search` method support from storage nodes (#3931) - Internal conversions of node addresses received from network map (#3981) +- `GetRangeHash` method support from storage nodes and related `object hash` CLI command (#3991) ### Updated - NeoGo dependency to 0.119.0 (#3993) +- `github.com/nspcc-dev/neofs-sdk-go` module to `v1.0.0-rc.18.0.20260513135441-5c10a9626760` (#3991) ### Updating from v0.52.0 Drop `policer.max_workers` configuration, it's no-op since 0.52.0. @@ -60,6 +63,12 @@ migrate to `Searchv2` if needed. Metabase will migrate to version 11 with this release to update object counters, this can take a while for shards with high object numbers. +Storage nodes no longer implement deprecated `GetRangeHash` method and +`neofs-cli object hash` command has been removed. + +GET now supports payload ranges. Migrate from deprecated RANGE requests to GET +with range parameters. + ## [0.52.0] - 2026-03-27 - Woodo Delivering performance optimizations and initial placement feature this diff --git a/cmd/neofs-cli/modules/acl/extended/create.go b/cmd/neofs-cli/modules/acl/extended/create.go index b007b5574b..07c3daf552 100644 --- a/cmd/neofs-cli/modules/acl/extended/create.go +++ b/cmd/neofs-cli/modules/acl/extended/create.go @@ -24,7 +24,7 @@ Rule consist of these blocks: [ ...] [ .. Action is 'allow' or 'deny'. -Operation is an object service verb: 'get', 'head', 'put', 'search', 'delete', 'getrange', or 'getrangehash'. +Operation is an object service verb: 'get', 'head', 'put', 'search', 'delete', or 'getrange'. Filter consists of : Typ is 'obj' for object applied filter or 'req' for request applied filter. diff --git a/cmd/neofs-cli/modules/object/get.go b/cmd/neofs-cli/modules/object/get.go index 497d3ae548..1ea066f63d 100644 --- a/cmd/neofs-cli/modules/object/get.go +++ b/cmd/neofs-cli/modules/object/get.go @@ -12,11 +12,14 @@ import ( "github.com/nspcc-dev/neofs-node/internal/object" "github.com/nspcc-dev/neofs-sdk-go/client" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + sdkobject "github.com/nspcc-dev/neofs-sdk-go/object" oid "github.com/nspcc-dev/neofs-sdk-go/object/id" "github.com/nspcc-dev/neofs-sdk-go/user" "github.com/spf13/cobra" ) +const withHeaderFlag = "with-header" + var objectGetCmd = &cobra.Command{ Use: "get", Short: "Get object from NeoFS", @@ -38,9 +41,11 @@ func initObjectGetCmd() { _ = objectGetCmd.MarkFlagRequired(commonflags.OIDFlag) flags.String(fileFlag, "", "File to write object payload to(with -b together with signature and header). Default: stdout.") + flags.String(rangeFlag, "", rangeFlagUsage) flags.Bool(rawFlag, false, rawFlagDesc) flags.Bool(noProgressFlag, false, "Do not show progress bar") flags.Bool(binaryFlag, false, "Serialize whole object structure into given file(id + signature + header + payload).") + flags.Bool(withHeaderFlag, false, "Request and print object header together with a payload range") } func getObject(cmd *cobra.Command, _ []string) error { @@ -67,6 +72,14 @@ func getObject(cmd *cobra.Command, _ []string) error { out = f } + ranges, err := getRangeList(cmd) + if err != nil { + return err + } + if len(ranges) > 1 { + return fmt.Errorf("at most one range can be specified, got: %d", len(ranges)) + } + pk, err := key.GetOrGenerate(cmd) if err != nil { return err @@ -100,6 +113,23 @@ func getObject(cmd *cobra.Command, _ []string) error { noProgress, _ := cmd.Flags().GetBool(noProgressFlag) binary, _ := cmd.Flags().GetBool(binaryFlag) + withHeader, _ := cmd.Flags().GetBool(withHeaderFlag) + if len(ranges) == 0 { + if withHeader { + return fmt.Errorf("--%s requires --%s", withHeaderFlag, rangeFlag) + } + } else { + if binary { + return fmt.Errorf("--%s cannot be used with --%s", binaryFlag, rangeFlag) + } + if withHeader && filename == "" { + return fmt.Errorf("--%s with --%s requires --%s", withHeaderFlag, rangeFlag, fileFlag) + } + prm.SetRange(ranges[0].GetOffset(), ranges[0].GetLength()) + if !withHeader { + prm.MarkPayloadOnly() + } + } hdr, rdr, err := cli.ObjectGetInit(ctx, cnr, obj, user.NewAutoIDSigner(*pk), prm) if err == nil { @@ -111,7 +141,7 @@ func getObject(cmd *cobra.Command, _ []string) error { } if filename != "" && !noProgress { - p = pb.New64(int64(hdr.PayloadSize())) + p = pb.New64(payloadReadSize(hdr.PayloadSize(), ranges)) p.Output = cmd.OutOrStdout() p.Start() @@ -141,7 +171,7 @@ func getObject(cmd *cobra.Command, _ []string) error { } // Print header only if file is not streamed to stdout. - if filename != "" { + if filename != "" && (len(ranges) == 0 || withHeader) { err = printHeader(cmd, &hdr) if err != nil { return err @@ -155,3 +185,18 @@ func strictOutput(cmd *cobra.Command) bool { toProto, _ := cmd.Flags().GetBool("proto") return toJSON || toProto } + +func payloadReadSize(payloadSize uint64, ranges []*sdkobject.Range) int64 { + if len(ranges) == 0 { + return int64(payloadSize) + } + + rng := ranges[0] + if ln := rng.GetLength(); ln != 0 { + return int64(ln) + } + if off := rng.GetOffset(); off < payloadSize { + return int64(payloadSize - off) + } + return 0 +} diff --git a/cmd/neofs-cli/modules/object/hash.go b/cmd/neofs-cli/modules/object/hash.go deleted file mode 100644 index 8c11d643de..0000000000 --- a/cmd/neofs-cli/modules/object/hash.go +++ /dev/null @@ -1,154 +0,0 @@ -package object - -import ( - "encoding/hex" - "fmt" - "strings" - - internalclient "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/client" - "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/common" - "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/commonflags" - "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/key" - "github.com/nspcc-dev/neofs-sdk-go/client" - 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/user" - "github.com/spf13/cobra" -) - -const getRangeHashSaltFlag = "salt" - -const ( - hashSha256 = "sha256" - rangeSep = ":" -) - -var objectHashCmd = &cobra.Command{ - Use: "hash", - Short: "Get object hash", - Long: "Get object hash", - Args: cobra.NoArgs, - RunE: getObjectHash, -} - -func initObjectHashCmd() { - commonflags.Init(objectHashCmd) - initFlagSession(objectHashCmd, "RANGEHASH") - - flags := objectHashCmd.Flags() - - flags.String(commonflags.CIDFlag, "", commonflags.CIDFlagUsage) - _ = objectHashCmd.MarkFlagRequired(commonflags.CIDFlag) - - flags.String(commonflags.OIDFlag, "", commonflags.OIDFlagUsage) - _ = objectHashCmd.MarkFlagRequired(commonflags.OIDFlag) - - flags.String("range", "", "Range to take hash from in the form offset1:length1,... Full object payload length if not specified") - flags.String("type", hashSha256, "Hash type. Deprecated: can be omitted, only 'sha256' is currently supported") - flags.String(getRangeHashSaltFlag, "", "Salt in hex format") -} - -func getObjectHash(cmd *cobra.Command, _ []string) error { - var cnr cid.ID - var obj oid.ID - - _, err := readObjectAddress(cmd, &cnr, &obj) - if err != nil { - return err - } - - ranges, err := getRangeList(cmd) - if err != nil { - return err - } - // only validation of deprecated no-op flag - _, err = getHashType(cmd) - if err != nil { - return err - } - - strSalt := strings.TrimPrefix(cmd.Flag(getRangeHashSaltFlag).Value.String(), "0x") - - salt, err := hex.DecodeString(strSalt) - if err != nil { - return fmt.Errorf("could not decode salt: %w", err) - } - - pk, err := key.GetOrGenerate(cmd) - if err != nil { - return err - } - - ctx, cancel := commonflags.GetCommandContext(cmd) - defer cancel() - - cli, err := internalclient.GetSDKClientByFlag(ctx, commonflags.RPC) - if err != nil { - return err - } - defer cli.Close() - - fullHash := len(ranges) == 0 - if fullHash { - common.PrintVerbose(cmd, "Get the hash of the full object payload.") - var headPrm client.PrmObjectHead - err = Prepare(cmd, &headPrm) - if err != nil { - return err - } - - // get hash of full payload through HEAD (may be user can do it through dedicated command?) - hdr, err := cli.ObjectHead(ctx, cnr, obj, user.NewAutoIDSigner(*pk), headPrm) - if err != nil { - return fmt.Errorf("rpc error: read object header via client: %w", err) - } - - cs, csSet := hdr.PayloadChecksum() - if csSet { - cmd.Println(hex.EncodeToString(cs.Value())) - } else { - cmd.Println("Missing checksum in object header.") - } - - return nil - } - - var hashPrm client.PrmObjectHash - err = Prepare(cmd, &hashPrm) - if err != nil { - return err - } - err = readSession(cmd, &hashPrm, pk, cnr, obj) - if err != nil { - return err - } - hashPrm.UseSalt(salt) - - rngs := make([]uint64, 2*len(ranges)) - for i := range ranges { - rngs[2*i] = ranges[i].GetOffset() - rngs[2*i+1] = ranges[i].GetLength() - } - hashPrm.SetRangeList(rngs...) - - hs, err := cli.ObjectHash(ctx, cnr, obj, user.NewAutoIDSigner(*pk), hashPrm) - if err != nil { - return fmt.Errorf("rpc error: read payload hashes via client: %w", err) - } - - for i := range hs { - cmd.Printf("Offset=%d (Length=%d)\t: %s\n", ranges[i].GetOffset(), ranges[i].GetLength(), - hex.EncodeToString(hs[i])) - } - return nil -} - -func getHashType(cmd *cobra.Command) (string, error) { - rawType := cmd.Flag("type").Value.String() - switch typ := strings.ToLower(rawType); typ { - case hashSha256: - return typ, nil - default: - return "", fmt.Errorf("invalid hash type: %s", typ) - } -} diff --git a/cmd/neofs-cli/modules/object/range.go b/cmd/neofs-cli/modules/object/range.go index 0d2b0507be..327b4bad9b 100644 --- a/cmd/neofs-cli/modules/object/range.go +++ b/cmd/neofs-cli/modules/object/range.go @@ -20,6 +20,12 @@ import ( "github.com/spf13/cobra" ) +const ( + rangeSep = ":" + rangeFlag = "range" + rangeFlagUsage = "Range to take data from in the form offset:length" +) + var objectRangeCmd = &cobra.Command{ Use: "range", Short: "Get payload range data of an object", @@ -108,6 +114,7 @@ func getObjectRange(cmd *cobra.Command, _ []string) error { prm.MarkRaw() } + //nolint:staticcheck rdr, err := cli.ObjectRangeInit(ctx, cnr, obj, ranges[0].GetOffset(), ranges[0].GetLength(), user.NewAutoIDSigner(*pk), prm) if err != nil { err = fmt.Errorf("init payload reading: %w", err) diff --git a/cmd/neofs-cli/modules/object/root.go b/cmd/neofs-cli/modules/object/root.go index 4864453097..d49f333241 100644 --- a/cmd/neofs-cli/modules/object/root.go +++ b/cmd/neofs-cli/modules/object/root.go @@ -26,7 +26,6 @@ func init() { objectSearchCmd, searchV2Cmd, objectHeadCmd, - objectHashCmd, objectRangeCmd, objectLockCmd} @@ -43,7 +42,6 @@ func init() { initObjectGetCmd() initObjectSearchCmd() initObjectHeadCmd() - initObjectHashCmd() initObjectRangeCmd() initCommandObjectLock() initObjectNodesCmd() diff --git a/cmd/neofs-cli/modules/object/util.go b/cmd/neofs-cli/modules/object/util.go index 910e0190d8..c72151c2bd 100644 --- a/cmd/neofs-cli/modules/object/util.go +++ b/cmd/neofs-cli/modules/object/util.go @@ -224,8 +224,6 @@ func _readVerifiedSession(cmd *cobra.Command, dst SessionPrm, key *ecdsa.Private cmdVerb = session.VerbObjectSearch case *client.PrmObjectRange: cmdVerb = session.VerbObjectRange - case *client.PrmObjectHash: - cmdVerb = session.VerbObjectRangeHash } tok, err := getVerifiedSession(cmd, cmdVerb, key, cnr) diff --git a/cmd/neofs-cli/modules/object/util_session_v2.go b/cmd/neofs-cli/modules/object/util_session_v2.go index 1b87c5bd4f..f570b1256e 100644 --- a/cmd/neofs-cli/modules/object/util_session_v2.go +++ b/cmd/neofs-cli/modules/object/util_session_v2.go @@ -79,8 +79,6 @@ func attachVerifiedSessionV2(cmd *cobra.Command, tok *session.Token, dst Session cmdVerb = session.VerbObjectSearch case *client.PrmObjectRange: cmdVerb = session.VerbObjectRange - case *client.PrmObjectHash: - cmdVerb = session.VerbObjectRangeHash } err := verifySessionV2(cmd, tok, cmdVerb, key, cnr) diff --git a/cmd/neofs-cli/modules/session/create_v2.go b/cmd/neofs-cli/modules/session/create_v2.go index 4593a60b3e..2c709c19d4 100644 --- a/cmd/neofs-cli/modules/session/create_v2.go +++ b/cmd/neofs-cli/modules/session/create_v2.go @@ -368,8 +368,6 @@ func parseVerbs(verbsStr string) ([]session.Verb, error) { verb = session.VerbObjectDelete case "RANGE", "OBJECTRANGE": verb = session.VerbObjectRange - case "RANGEHASH", "OBJECTRANGEHASH", "RANGE_HASH", "OBJECT_RANGE_HASH": - verb = session.VerbObjectRangeHash case "CONTAINERSET", "CONTAINERSETACL", "CONTAINER_SET", "CONTAINER_SET_ACL": verb = session.VerbContainerSetEACL case "CONTAINERPUT", "CONTAINER_PUT": @@ -377,7 +375,7 @@ func parseVerbs(verbsStr string) ([]session.Verb, error) { case "CONTAINERDELETE", "CONTAINER_DELETE": verb = session.VerbContainerDelete default: - return nil, fmt.Errorf("unknown verb: %s (supported: GET,PUT,HEAD,SEARCH,DELETE,RANGE,RANGEHASH,CONTAINERSET,CONTAINERPUT,CONTAINERDELETE)", verbStr) + return nil, fmt.Errorf("unknown verb: %s (supported: GET,PUT,HEAD,SEARCH,DELETE,RANGE,CONTAINERSET,CONTAINERPUT,CONTAINERDELETE)", verbStr) } verbs = append(verbs, verb) diff --git a/cmd/neofs-cli/modules/util/acl.go b/cmd/neofs-cli/modules/util/acl.go index 04d0383715..6c3d7b506b 100644 --- a/cmd/neofs-cli/modules/util/acl.go +++ b/cmd/neofs-cli/modules/util/acl.go @@ -19,11 +19,11 @@ import ( func PrettyPrintTableBACL(cmd *cobra.Command, bacl *acl.Basic) { // Header w := tabwriter.NewWriter(cmd.OutOrStdout(), 1, 4, 4, ' ', 0) - fmt.Fprintln(w, "\tRangeHASH\tRange\tSearch\tDelete\tPut\tHead\tGet") + fmt.Fprintln(w, "\tRange\tSearch\tDelete\tPut\tHead\tGet") // Bits bits := []string{ boolToString(bacl.Sticky()) + " " + boolToString(!bacl.Extendable()), - getRoleBitsForOperation(bacl, acl.OpObjectHash), getRoleBitsForOperation(bacl, acl.OpObjectRange), + getRoleBitsForOperation(bacl, acl.OpObjectRange), getRoleBitsForOperation(bacl, acl.OpObjectSearch), getRoleBitsForOperation(bacl, acl.OpObjectDelete), getRoleBitsForOperation(bacl, acl.OpObjectPut), getRoleBitsForOperation(bacl, acl.OpObjectHead), getRoleBitsForOperation(bacl, acl.OpObjectGet), @@ -31,7 +31,7 @@ func PrettyPrintTableBACL(cmd *cobra.Command, bacl *acl.Basic) { fmt.Fprintln(w, strings.Join(bits, "\t")) // Footer footer := []string{"X F"} - for range 7 { + for range 6 { footer = append(footer, "U S O B") } fmt.Fprintln(w, strings.Join(footer, "\t")) diff --git a/cmd/neofs-node/object.go b/cmd/neofs-node/object.go index 6f5a0e00f6..8e70c94131 100644 --- a/cmd/neofs-node/object.go +++ b/cmd/neofs-node/object.go @@ -130,10 +130,6 @@ func (s *objectSvc) GetRange(ctx context.Context, prm getsvc.RangePrm) error { return s.get.GetRange(ctx, prm) } -func (s *objectSvc) GetRangeHash(ctx context.Context, prm getsvc.RangeHashPrm) (*getsvc.RangeHashRes, error) { - return s.get.GetRangeHash(ctx, prm) -} - type delNetInfo struct { netmap.State tsLifetime uint64 @@ -443,14 +439,6 @@ func (c *reputationClient) ObjectHead(ctx context.Context, containerID cid.ID, o return res, err } -func (c *reputationClient) ObjectHash(ctx context.Context, containerID cid.ID, objectID oid.ID, signer user.Signer, prm client.PrmObjectHash) ([][]byte, error) { - res, err := c.MultiAddressClient.ObjectHash(ctx, containerID, objectID, signer, prm) - - c.submitResult(err) - - return res, err -} - func (c *reputationClient) ObjectSearchInit(ctx context.Context, containerID cid.ID, signer user.Signer, prm client.PrmObjectSearch) (*client.ObjectListReader, error) { res, err := c.MultiAddressClient.ObjectSearchInit(ctx, containerID, signer, prm) diff --git a/docs/cli-commands/neofs-cli_acl_extended_create.md b/docs/cli-commands/neofs-cli_acl_extended_create.md index 760dcf4161..d00520eb6c 100644 --- a/docs/cli-commands/neofs-cli_acl_extended_create.md +++ b/docs/cli-commands/neofs-cli_acl_extended_create.md @@ -10,7 +10,7 @@ Rule consist of these blocks: [ ...] [ .. Action is 'allow' or 'deny'. -Operation is an object service verb: 'get', 'head', 'put', 'search', 'delete', 'getrange', or 'getrangehash'. +Operation is an object service verb: 'get', 'head', 'put', 'search', 'delete', or 'getrange'. Filter consists of : Typ is 'obj' for object applied filter or 'req' for request applied filter. diff --git a/docs/cli-commands/neofs-cli_object.md b/docs/cli-commands/neofs-cli_object.md index 6c1d1b7a96..a12b28650a 100644 --- a/docs/cli-commands/neofs-cli_object.md +++ b/docs/cli-commands/neofs-cli_object.md @@ -24,7 +24,6 @@ Operations with Objects * [neofs-cli](neofs-cli.md) - Command Line Tool to work with NeoFS * [neofs-cli object delete](neofs-cli_object_delete.md) - Delete object from NeoFS * [neofs-cli object get](neofs-cli_object_get.md) - Get object from NeoFS -* [neofs-cli object hash](neofs-cli_object_hash.md) - Get object hash * [neofs-cli object head](neofs-cli_object_head.md) - Get object header * [neofs-cli object lock](neofs-cli_object_lock.md) - Lock object in container * [neofs-cli object nodes](neofs-cli_object_nodes.md) - Show nodes for an object diff --git a/docs/cli-commands/neofs-cli_object_get.md b/docs/cli-commands/neofs-cli_object_get.md index 1a5c7c01a9..95e32d3c5b 100644 --- a/docs/cli-commands/neofs-cli_object_get.md +++ b/docs/cli-commands/neofs-cli_object_get.md @@ -22,12 +22,14 @@ neofs-cli object get [flags] -h, --help help for get --no-progress Do not show progress bar --oid string Object ID. + --range string Range to take data from in the form offset:length --raw Set raw request option -r, --rpc-endpoint string Remote node address (as 'multiaddr' or ':') --session string Filepath to a JSON- or binary-encoded token of the object GET session -t, --timeout duration Timeout for the operation (default 15s) --ttl uint32 TTL value in request meta header (default 2) -w, --wallet string Path to the wallet + --with-header Request and print object header together with a payload range -x, --xhdr strings Request X-Headers in form of Key=Value ``` diff --git a/docs/cli-commands/neofs-cli_object_hash.md b/docs/cli-commands/neofs-cli_object_hash.md deleted file mode 100644 index ce94ad00f6..0000000000 --- a/docs/cli-commands/neofs-cli_object_hash.md +++ /dev/null @@ -1,43 +0,0 @@ -## neofs-cli object hash - -Get object hash - -### Synopsis - -Get object hash - -``` -neofs-cli object hash [flags] -``` - -### Options - -``` - --address string Address of wallet account - --bearer string File with signed JSON or binary encoded bearer token - --cid string Container ID. - -g, --generate-key Generate new private key - -h, --help help for hash - --oid string Object ID. - --range string Range to take hash from in the form offset1:length1,... Full object payload length if not specified - -r, --rpc-endpoint string Remote node address (as 'multiaddr' or ':') - --salt string Salt in hex format - --session string Filepath to a JSON- or binary-encoded token of the object RANGEHASH session - -t, --timeout duration Timeout for the operation (default 15s) - --ttl uint32 TTL value in request meta header (default 2) - --type string Hash type. Deprecated: can be omitted, only 'sha256' is currently supported (default "sha256") - -w, --wallet string Path to the wallet - -x, --xhdr strings Request X-Headers in form of Key=Value -``` - -### Options inherited from parent commands - -``` - -c, --config string Config file (default is $HOME/.config/neofs-cli/config.yaml) - -v, --verbose Verbose output -``` - -### SEE ALSO - -* [neofs-cli object](neofs-cli_object.md) - Operations with Objects - diff --git a/go.mod b/go.mod index 49ca085635..ec1178985b 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/nspcc-dev/neo-go v0.119.0 github.com/nspcc-dev/neofs-api-go/v2 v2.14.1-0.20240827150555-5ce597aa14ea github.com/nspcc-dev/neofs-contract v0.26.1 - github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.18.0.20260417090255-d6b86c01a5af + github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.18.0.20260513135441-5c10a9626760 github.com/nspcc-dev/tzhash v1.8.4 github.com/panjf2000/ants/v2 v2.11.5 github.com/prometheus/client_golang v1.23.2 diff --git a/go.sum b/go.sum index d15264d946..16b3dbdead 100644 --- a/go.sum +++ b/go.sum @@ -191,8 +191,8 @@ github.com/nspcc-dev/neofs-api-go/v2 v2.14.1-0.20240827150555-5ce597aa14ea h1:mK github.com/nspcc-dev/neofs-api-go/v2 v2.14.1-0.20240827150555-5ce597aa14ea/go.mod h1:YzhD4EZmC9Z/PNyd7ysC7WXgIgURc9uCG1UWDeV027Y= github.com/nspcc-dev/neofs-contract v0.26.1 h1:7Ii7Q4L3au408LOsIWKiSgfnT1g8G9jo3W7381d41T8= github.com/nspcc-dev/neofs-contract v0.26.1/go.mod h1:pevVF9OWdEN5bweKxOu6ryZv9muCEtS1ppzYM4RfBIo= -github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.18.0.20260417090255-d6b86c01a5af h1:ZoeUmDyZU3z4GQl5xfbM/brvYgV+9xNkcE1A92aQx4A= -github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.18.0.20260417090255-d6b86c01a5af/go.mod h1:aUnzNCaipQZUQaai9lcapG90OwYx7g9s4UsrvcVkBoo= +github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.18.0.20260513135441-5c10a9626760 h1:Bskhno+qkReNu5LZSV4JgCxgIplpd/iqhJGcxt4bv9M= +github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.18.0.20260513135441-5c10a9626760/go.mod h1:KtAAolqjEdTNZ8T57FvpWsE/i9/Bpj6wuRPeUs/U85I= github.com/nspcc-dev/rfc6979 v0.2.4 h1:NBgsdCjhLpEPJZqmC9rciMZDcSY297po2smeaRjw57k= github.com/nspcc-dev/rfc6979 v0.2.4/go.mod h1:86ylDw6Kss+P6v4QAJqo1Sp3mC0/Zr9G97xSjQ9TuFg= github.com/nspcc-dev/tzhash v1.8.4 h1:lvuPGWsqEo9dVEvo/kdNLKv/Cy0yxRs9z5hJp8VcBuo= @@ -282,16 +282,16 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= -go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= -go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= -go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= -go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= +go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= +go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= +go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= -go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= -go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= +go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= diff --git a/pkg/core/client/client.go b/pkg/core/client/client.go index ee66a20a3c..eb44a377c0 100644 --- a/pkg/core/client/client.go +++ b/pkg/core/client/client.go @@ -25,7 +25,6 @@ type Client interface { ObjectSearchInit(ctx context.Context, containerID cid.ID, signer user.Signer, prm client.PrmObjectSearch) (*client.ObjectListReader, error) SearchObjects(context.Context, cid.ID, object.SearchFilters, []string, string, neofscrypto.Signer, client.SearchObjectsOptions) ([]client.SearchResultItem, string, error) ObjectRangeInit(ctx context.Context, containerID cid.ID, objectID oid.ID, offset, length uint64, signer user.Signer, prm client.PrmObjectRange) (*client.ObjectRangeReader, error) - ObjectHash(ctx context.Context, containerID cid.ID, objectID oid.ID, signer user.Signer, prm client.PrmObjectHash) ([][]byte, error) AnnounceLocalTrust(ctx context.Context, epoch uint64, trusts []reputationSDK.Trust, prm client.PrmAnnounceLocalTrust) error AnnounceIntermediateTrust(ctx context.Context, epoch uint64, trust reputationSDK.PeerToPeerTrust, prm client.PrmAnnounceIntermediateTrust) error } diff --git a/pkg/metrics/object.go b/pkg/metrics/object.go index ac8cde6ff9..ea834c1d89 100644 --- a/pkg/metrics/object.go +++ b/pkg/metrics/object.go @@ -17,21 +17,19 @@ type ( } objectServiceMetrics struct { - getCounter methodCount - putCounter methodCount - headCounter methodCount - searchCounter methodCount - deleteCounter methodCount - rangeCounter methodCount - rangeHashCounter methodCount - - getDuration prometheus.Histogram - putDuration prometheus.Histogram - headDuration prometheus.Histogram - searchDuration prometheus.Histogram - deleteDuration prometheus.Histogram - rangeDuration prometheus.Histogram - rangeHashDuration prometheus.Histogram + getCounter methodCount + putCounter methodCount + headCounter methodCount + searchCounter methodCount + deleteCounter methodCount + rangeCounter methodCount + + getDuration prometheus.Histogram + putDuration prometheus.Histogram + headDuration prometheus.Histogram + searchDuration prometheus.Histogram + deleteDuration prometheus.Histogram + rangeDuration prometheus.Histogram putPayload prometheus.Counter getPayload prometheus.Counter @@ -78,13 +76,12 @@ func (m methodCount) inc(success bool) { func newObjectServiceMetrics() objectServiceMetrics { var ( // Request counter metrics. - getCounter = newMethodCallCounter("get") - putCounter = newMethodCallCounter("put") - headCounter = newMethodCallCounter("head") - searchCounter = newMethodCallCounter("search") - deleteCounter = newMethodCallCounter("delete") - rangeCounter = newMethodCallCounter("range") - rangeHashCounter = newMethodCallCounter("range_hash") + getCounter = newMethodCallCounter("get") + putCounter = newMethodCallCounter("put") + headCounter = newMethodCallCounter("head") + searchCounter = newMethodCallCounter("search") + deleteCounter = newMethodCallCounter("delete") + rangeCounter = newMethodCallCounter("range") ) var ( // Request duration metrics. @@ -129,13 +126,6 @@ func newObjectServiceMetrics() objectServiceMetrics { Name: "rpc_range_time", Help: "RPC 'range request' handling time", }) - - rangeHashDuration = prometheus.NewHistogram(prometheus.HistogramOpts{ - Namespace: storageNodeNameSpace, - Subsystem: objectSubsystem, - Name: "rpc_range_hash_time", - Help: "RPC 'range hash' handling time", - }) ) var ( // Object payload metrics. @@ -173,24 +163,22 @@ func newObjectServiceMetrics() objectServiceMetrics { ) return objectServiceMetrics{ - getCounter: getCounter, - putCounter: putCounter, - headCounter: headCounter, - searchCounter: searchCounter, - deleteCounter: deleteCounter, - rangeCounter: rangeCounter, - rangeHashCounter: rangeHashCounter, - getDuration: getDuration, - putDuration: putDuration, - headDuration: headDuration, - searchDuration: searchDuration, - deleteDuration: deleteDuration, - rangeDuration: rangeDuration, - rangeHashDuration: rangeHashDuration, - putPayload: putPayload, - getPayload: getPayload, - shardMetrics: shardsMetrics, - shardsReadonly: shardsReadonly, + getCounter: getCounter, + putCounter: putCounter, + headCounter: headCounter, + searchCounter: searchCounter, + deleteCounter: deleteCounter, + rangeCounter: rangeCounter, + getDuration: getDuration, + putDuration: putDuration, + headDuration: headDuration, + searchDuration: searchDuration, + deleteDuration: deleteDuration, + rangeDuration: rangeDuration, + putPayload: putPayload, + getPayload: getPayload, + shardMetrics: shardsMetrics, + shardsReadonly: shardsReadonly, } } @@ -201,7 +189,6 @@ func (m objectServiceMetrics) register() { m.searchCounter.mustRegister() m.deleteCounter.mustRegister() m.rangeCounter.mustRegister() - m.rangeHashCounter.mustRegister() prometheus.MustRegister(m.getDuration) prometheus.MustRegister(m.putDuration) @@ -209,7 +196,6 @@ func (m objectServiceMetrics) register() { prometheus.MustRegister(m.searchDuration) prometheus.MustRegister(m.deleteDuration) prometheus.MustRegister(m.rangeDuration) - prometheus.MustRegister(m.rangeHashDuration) prometheus.MustRegister(m.putPayload) prometheus.MustRegister(m.getPayload) @@ -240,9 +226,6 @@ func (m objectServiceMetrics) HandleOpExecResult(op stat.Method, success bool, d case stat.MethodObjectRange: m.rangeCounter.inc(success) m.rangeDuration.Observe(d.Seconds()) - case stat.MethodObjectHash: - m.rangeHashCounter.inc(success) - m.rangeHashDuration.Observe(d.Seconds()) } } diff --git a/pkg/network/cache/clients.go b/pkg/network/cache/clients.go index 9864045180..f17afdcac0 100644 --- a/pkg/network/cache/clients.go +++ b/pkg/network/cache/clients.go @@ -372,20 +372,12 @@ func (x *connections) ObjectRangeInit(ctx context.Context, cnr cid.ID, id oid.ID var res *client.ObjectRangeReader return res, x.forAny(ctx, func(ctx context.Context, c *client.Client) error { var err error + //nolint:staticcheck res, err = c.ObjectRangeInit(ctx, cnr, id, off, ln, signer, opts) return err }) } -func (x *connections) ObjectHash(ctx context.Context, cnr cid.ID, id oid.ID, signer user.Signer, opts client.PrmObjectHash) ([][]byte, error) { - var res [][]byte - return res, x.forAny(ctx, func(ctx context.Context, c *client.Client) error { - var err error - res, err = c.ObjectHash(ctx, cnr, id, signer, opts) - return err - }) -} - func (x *connections) AnnounceLocalTrust(ctx context.Context, epoch uint64, ts []apireputation.Trust, opts client.PrmAnnounceLocalTrust) error { return x.forAny(ctx, func(ctx context.Context, c *client.Client) error { return c.AnnounceLocalTrust(ctx, epoch, ts, opts) diff --git a/pkg/services/object/acl/eacl/v2/headers.go b/pkg/services/object/acl/eacl/v2/headers.go index dd1d418eee..cd49c288d5 100644 --- a/pkg/services/object/acl/eacl/v2/headers.go +++ b/pkg/services/object/acl/eacl/v2/headers.go @@ -140,7 +140,6 @@ func (h *cfg) readObjectHeaders(dst *headerSource) error { dst.incompleteObjectHeaders = !completed case *protoobject.GetRangeRequest, - *protoobject.GetRangeHashRequest, *protoobject.DeleteRequest: if h.obj == nil { return errMissingOID diff --git a/pkg/services/object/acl/v2/service.go b/pkg/services/object/acl/v2/service.go index 6310775322..e657f07d25 100644 --- a/pkg/services/object/acl/v2/service.go +++ b/pkg/services/object/acl/v2/service.go @@ -506,22 +506,6 @@ func (b Service) RangeRequestToInfo(request *protoobject.GetRangeRequest) (Reque return b.findRequestInfo(request, cnr, acl.OpObjectRange, sessionSDK.VerbObjectRange, sessionv2.VerbObjectRange, *obj) } -// HashRequestToInfo resolves RequestInfo from the request to check it using -// [ACLChecker]. -func (b Service) HashRequestToInfo(request *protoobject.GetRangeHashRequest) (RequestInfo, error) { - cnr, err := getContainerIDFromRequest(request) - if err != nil { - return RequestInfo{}, err - } - - obj, err := getObjectIDFromRequestBody(request.GetBody()) - if err != nil { - return RequestInfo{}, err - } - - return b.findRequestInfo(request, cnr, acl.OpObjectHash, sessionSDK.VerbObjectRangeHash, sessionv2.VerbObjectRangeHash, *obj) -} - var ErrSkipRequest = errors.New("skip request") // PutRequestToInfo resolves RequestInfo from the request to check it using diff --git a/pkg/services/object/acl/v2/service_test.go b/pkg/services/object/acl/v2/service_test.go index 95ff2f9c93..f764644167 100644 --- a/pkg/services/object/acl/v2/service_test.go +++ b/pkg/services/object/acl/v2/service_test.go @@ -255,26 +255,6 @@ func TestService_RangeRequestToInfo_BearerTokenIssuer(t *testing.T) { }) } -func TestService_HashRequestToInfo_BearerTokenIssuer(t *testing.T) { - testBearerTokenIssuer(t, (*aclsvc.Service).HashRequestToInfo, func(t *testing.T, signer neofscrypto.Signer, cnrID cid.ID, meta *protosession.RequestMetaHeader) *protoobject.GetRangeHashRequest { - req := &protoobject.GetRangeHashRequest{ - Body: &protoobject.GetRangeHashRequest_Body{ - Address: &refs.Address{ - ContainerId: cnrID.ProtoMessage(), - ObjectId: oidtest.ID().ProtoMessage(), - }, - }, - MetaHeader: meta, - } - - var err error - req.VerifyHeader, err = neofscrypto.SignRequestWithBuffer(signer, req, nil) - require.NoError(t, err) - - return req - }) -} - func TestService_PutRequestToInfo_BearerTokenIssuer(t *testing.T) { testBearerTokenIssuer(t, func(svc *aclsvc.Service, req *protoobject.PutRequest) (aclsvc.RequestInfo, error) { res, _, err := svc.PutRequestToInfo(req) diff --git a/pkg/services/object/acl/v2/util.go b/pkg/services/object/acl/v2/util.go index 7c05ff99e5..e9aef86b9a 100644 --- a/pkg/services/object/acl/v2/util.go +++ b/pkg/services/object/acl/v2/util.go @@ -38,8 +38,6 @@ func getContainerIDFromRequest(req any) (cid.ID, error) { mID = v.GetBody().GetAddress().GetContainerId() case *protoobject.GetRangeRequest: mID = v.GetBody().GetAddress().GetContainerId() - case *protoobject.GetRangeHashRequest: - mID = v.GetBody().GetAddress().GetContainerId() default: return cid.ID{}, errors.New("unknown request type") } @@ -78,12 +76,11 @@ func assertVerb(tok sessionSDK.Object, reqVerb sessionSDK.ObjectVerb) bool { sessionSDK.VerbObjectHead, sessionSDK.VerbObjectGet, sessionSDK.VerbObjectDelete, - sessionSDK.VerbObjectRange, - sessionSDK.VerbObjectRangeHash) + sessionSDK.VerbObjectRange) case sessionSDK.VerbObjectSearch: return tok.AssertVerb(sessionSDK.VerbObjectSearch, sessionSDK.VerbObjectDelete) case sessionSDK.VerbObjectRange: - return tok.AssertVerb(sessionSDK.VerbObjectRange, sessionSDK.VerbObjectRangeHash) + return tok.AssertVerb(sessionSDK.VerbObjectRange) } } diff --git a/pkg/services/object/acl/v2/util_test.go b/pkg/services/object/acl/v2/util_test.go index 34de23e055..172fbfe658 100644 --- a/pkg/services/object/acl/v2/util_test.go +++ b/pkg/services/object/acl/v2/util_test.go @@ -22,11 +22,9 @@ func TestIsVerbCompatible(t *testing.T) { sessionSDK.VerbObjectGet, sessionSDK.VerbObjectDelete, sessionSDK.VerbObjectRange, - sessionSDK.VerbObjectRangeHash, }, - sessionSDK.VerbObjectRange: {sessionSDK.VerbObjectRange, sessionSDK.VerbObjectRangeHash}, - sessionSDK.VerbObjectRangeHash: {sessionSDK.VerbObjectRangeHash}, - sessionSDK.VerbObjectSearch: {sessionSDK.VerbObjectSearch, sessionSDK.VerbObjectDelete}, + sessionSDK.VerbObjectRange: {sessionSDK.VerbObjectRange}, + sessionSDK.VerbObjectSearch: {sessionSDK.VerbObjectSearch, sessionSDK.VerbObjectDelete}, } verbs := []sessionSDK.ObjectVerb{ @@ -34,7 +32,6 @@ func TestIsVerbCompatible(t *testing.T) { sessionSDK.VerbObjectDelete, sessionSDK.VerbObjectHead, sessionSDK.VerbObjectRange, - sessionSDK.VerbObjectRangeHash, sessionSDK.VerbObjectGet, sessionSDK.VerbObjectSearch, } diff --git a/pkg/services/object/get.go b/pkg/services/object/get.go index 85f3169d4d..9f8b054bc4 100644 --- a/pkg/services/object/get.go +++ b/pkg/services/object/get.go @@ -51,7 +51,7 @@ func (x *getProxyContext) continueWithConn(ctx context.Context, conn *grpc.Clien var respBuf mem.BufferSlice if err = stream.RecvMsg(&respBuf); err != nil { if errors.Is(err, io.EOF) { - if !prog.headWas { + if !prog.headWas && !x.req.GetBody().GetPayloadOnly() { return io.ErrUnexpectedEOF } return nil @@ -125,7 +125,10 @@ func (x *getProxyContext) handleResponseBody(streamProg *getStreamProgress, resp opts.InterceptBytes = func(num protowire.Number, buffers iprotobuf.BuffersSlice) error { if num == protoobject.FieldGetResponseBodyChunk { if !streamProg.headWas { - return errors.New("incorrect message sequence") + if !x.req.GetBody().GetPayloadOnly() { + return errors.New("incorrect message sequence") + } + streamProg.headWas = true } oneofNum, oneofFld = num, buffers } diff --git a/pkg/services/object/get/assemble.go b/pkg/services/object/get/assemble.go index 4a05aef3d8..8058508a80 100644 --- a/pkg/services/object/get/assemble.go +++ b/pkg/services/object/get/assemble.go @@ -60,13 +60,21 @@ func (exec *execCtx) assemble() { exec.overtakePayloadDirectly(children, nil, true) } } else { - if ok := exec.overtakePayloadInReverse(children[len(children)-1]); ok { + ok := true + if !exec.payloadOnly { + ok = exec.writeCollectedHeader() + } + if ok && exec.overtakePayloadInReverse(children[len(children)-1]) { // payload of all children except the last are written, write last payload exec.copyChild(exec.lastChildID, &exec.lastChildRange, false) } } } else if prev != nil { - if ok := exec.writeCollectedHeader(); ok { + ok := true + if exec.ctxRange() == nil || !exec.payloadOnly { + ok = exec.writeCollectedHeader() + } + if ok { if ok := exec.overtakePayloadInReverse(*prev); ok { var rng *object.Range if exec.ctxRange() != nil { diff --git a/pkg/services/object/get/assembly_v2.go b/pkg/services/object/get/assembly_v2.go index 787d4872df..10f81f71b9 100644 --- a/pkg/services/object/get/assembly_v2.go +++ b/pkg/services/object/get/assembly_v2.go @@ -123,6 +123,11 @@ func (exec *execCtx) processV2Link(linkID oid.ID) bool { return true } + if !exec.writeCollectedHeader() { + exec.log.Debug("failed to write parent header") + return true + } + return exec.rangeFromLink(link) } diff --git a/pkg/services/object/get/exec.go b/pkg/services/object/get/exec.go index d801354975..250a9540a4 100644 --- a/pkg/services/object/get/exec.go +++ b/pkg/services/object/get/exec.go @@ -33,8 +33,7 @@ type execCtx struct { ctx context.Context - prm RangePrm - prmRangeHash *RangeHashPrm + prm RangePrm statusError @@ -57,10 +56,10 @@ type execCtx struct { nodeLists [][]netmap.NodeInfo repRules []uint - // headerWritten is set to true after WriteHeader is successfully called. - // If an error occurs after that, the stream is already corrupted and - // no retry should be attempted. - headerWritten bool + // responseStarted is set to true after the first response bytes are + // successfully written. If an error occurs after that, the stream is + // already corrupted and no retry should be attempted. + responseStarted bool localGetBuffer []byte submitLocalGetStreamFn SubmitStreamFunc @@ -74,6 +73,9 @@ type execCtx struct { localRangeBuffer []byte submitLocalRangeStreamFn SubmitDataStreamFunc + + payloadOnly bool + legacyRange bool } type execOption func(*execCtx) @@ -100,9 +102,15 @@ func withPayloadRange(r *object.Range) execOption { } } -func withHash(p *RangeHashPrm) execOption { - return func(ctx *execCtx) { - ctx.prmRangeHash = p +func withPayloadOnly(v bool) execOption { + return func(c *execCtx) { + c.payloadOnly = v + } +} + +func withLegacyRange(v bool) execOption { + return func(c *execCtx) { + c.legacyRange = v } } @@ -355,16 +363,29 @@ func mergeSplitInfo(dst, src *object.SplitInfo) { } func (exec *execCtx) writeCollectedHeader() bool { - if exec.ctxRange() != nil { + hdr := exec.collectedHeader.CutPayload() + if exec.payloadOnly { + if v, ok := exec.prm.objWriter.(HeaderValidator); ok { + err := v.ValidateHeader(hdr) + if err != nil { + exec.status = statusUndefined + exec.err = err + + exec.log.Debug("could not validate header", + zap.Error(err), + ) + return false + } + } + exec.status = statusOK + exec.err = nil return true } - err := exec.prm.objWriter.WriteHeader( - exec.collectedHeader.CutPayload(), - ) + err := exec.prm.objWriter.WriteHeader(hdr) if err == nil { - exec.headerWritten = true + exec.responseStarted = true exec.status = statusOK exec.err = nil } else { @@ -392,9 +413,17 @@ func (exec *execCtx) writeObjectPayload(obj *object.Object, reader io.ReadCloser exec.log.Debug("error while closing payload reader", zap.Error(err)) } }() - err = copyPayloadStream(exec.prm.objWriter, reader) + err = copyPayloadStream(chunkWriteObserver{ + ChunkWriter: exec.prm.objWriter, + onWrite: func() { + exec.responseStarted = true + }, + }, reader) } else { err = exec.prm.objWriter.WriteChunk(obj.Payload()) + if err == nil && len(obj.Payload()) > 0 { + exec.responseStarted = true + } } if err == nil { @@ -441,6 +470,21 @@ func copyPayloadStream(w ChunkWriter, r io.Reader) error { return err } +type chunkWriteObserver struct { + ChunkWriter + onWrite func() +} + +func (w chunkWriteObserver) WriteChunk(p []byte) error { + if err := w.ChunkWriter.WriteChunk(p); err != nil { + return err + } + if len(p) > 0 && w.onWrite != nil { + w.onWrite() + } + return nil +} + // returns number of written bytes. Returns errResponseStreamFailure on w failure. func copyPayloadStreamBuffer(w ChunkWriter, r io.Reader, buf []byte) (uint64, error) { for done := uint64(0); ; { @@ -467,16 +511,9 @@ func (exec *execCtx) writeCollectedObject() { } } -// isRangeHashForwardingEnabled returns true if common execution -// parameters has GETRANGEHASH request forwarding closure set. -func (exec execCtx) isRangeHashForwardingEnabled() bool { - return exec.prm.rangeForwarder != nil -} - // disableForwarding removes request forwarding closure from common // parameters, so it won't be inherited in new execution contexts. func (exec *execCtx) disableForwarding() { exec.prm.SetRequestForwarder(nil) - exec.prm.SetRangeHashRequestForwarder(nil) exec.forwardGetRequestFn = nil } diff --git a/pkg/services/object/get/get.go b/pkg/services/object/get/get.go index 12b1ce342e..cadf6e7ded 100644 --- a/pkg/services/object/get/get.go +++ b/pkg/services/object/get/get.go @@ -4,10 +4,8 @@ import ( "context" "errors" "fmt" - "slices" iec "github.com/nspcc-dev/neofs-node/internal/ec" - "github.com/nspcc-dev/neofs-node/pkg/util" apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" "github.com/nspcc-dev/neofs-sdk-go/netmap" "github.com/nspcc-dev/neofs-sdk-go/object" @@ -25,6 +23,17 @@ func (s *Service) Get(ctx context.Context, prm Prm) error { if pi.RuleIndex >= 0 { // TODO: deny if node is not in the container? + if prm.rng != nil { + if prm.payloadOnly && prm.localGetBuffer != nil { + stream, err := s.localObjects.ReadECPartRange(prm.addr.Container(), prm.addr.Object(), pi, prm.rng.GetOffset(), prm.rng.GetLength(), prm.localGetBuffer) + if err == nil { + prm.submitLocalGetStreamFn(0, stream) + } + return err + } + return s.copyLocalECPartRange(prm.objWriter, prm.addr.Container(), prm.addr.Object(), pi, prm.rng.GetOffset(), prm.rng.GetLength()) + } + if prm.localGetBuffer != nil { n, stream, err := s.localObjects.ReadECPart(prm.addr.Container(), prm.addr.Object(), pi, prm.localGetBuffer) if err == nil { @@ -39,8 +48,11 @@ func (s *Service) Get(ctx context.Context, prm Prm) error { if prm.common.LocalOnly() && len(prm.container.PlacementPolicy().ECRules()) == 0 && // EC breaks TTL requirements currently. len(prm.container.PlacementPolicy().Replicas()) != 0 { - bufOpt := withLocalGetBuffer(prm.localGetBuffer, prm.submitLocalGetStreamFn) - return s.get(ctx, prm.commonPrm, bufOpt).err // It handles locality internally. + opts := []execOption{withPayloadRange(prm.rng), withPayloadOnly(prm.payloadOnly)} + if prm.rng == nil && !prm.payloadOnly { + opts = append(opts, withLocalGetBuffer(prm.localGetBuffer, prm.submitLocalGetStreamFn)) + } + return s.get(ctx, prm.commonPrm, opts...).err // It handles locality internally. } nodeLists, repRules, ecRules, err := s.neoFSNet.GetNodesForObject(prm.addr) @@ -49,9 +61,16 @@ func (s *Service) Get(ctx context.Context, prm Prm) error { } if len(repRules) > 0 { // REP format does not require encoding - bufOpt := withLocalGetBuffer(prm.localGetBuffer, prm.submitLocalGetStreamFn) - forwardOpt := withForwardGetRequestFunc(prm.forwardRequestFn) - err := s.get(ctx, prm.commonPrm, withPreSortedContainerNodes(nodeLists[:len(repRules)], repRules), bufOpt, forwardOpt).err + opts := []execOption{ + withPreSortedContainerNodes(nodeLists[:len(repRules)], repRules), + withPayloadRange(prm.rng), + withPayloadOnly(prm.payloadOnly), + withForwardGetRequestFunc(prm.forwardRequestFn), + } + if prm.rng == nil && !prm.payloadOnly { + opts = append(opts, withLocalGetBuffer(prm.localGetBuffer, prm.submitLocalGetStreamFn)) + } + err := s.get(ctx, prm.commonPrm, opts...).err if len(ecRules) == 0 || !errors.Is(err, apistatus.ErrObjectNotFound) { return err } @@ -67,7 +86,17 @@ func (s *Service) Get(ctx context.Context, prm Prm) error { for i := range ecRules { repRules[i] = uint(ecRules[i].DataPartNum + ecRules[i].ParityPartNum) } - return s.get(ctx, prm.commonPrm, withPreSortedContainerNodes(ecNodeLists, repRules)).err + return s.get(ctx, prm.commonPrm, withPreSortedContainerNodes(ecNodeLists, repRules), withPayloadRange(prm.rng), withPayloadOnly(prm.payloadOnly)).err + } + + if prm.rng != nil { + if !prm.payloadOnly { + if err := s.copyECObjectHeader(ctx, prm.objWriter, prm.addr.Container(), prm.addr.Object(), prm.common.SessionToken(), ecRules, ecNodeLists, nil, nil); err != nil { + return err + } + } + return s.copyECObjectRange(ctx, prm.objWriter, prm.addr.Container(), prm.addr.Object(), prm.common.SessionToken(), + ecRules, ecNodeLists, prm.rng.GetOffset(), prm.rng.GetLength()) } return s.copyECObject(ctx, prm.addr.Container(), prm.addr.Object(), prm.common.SessionToken(), @@ -101,7 +130,7 @@ func (s *Service) GetRange(ctx context.Context, prm RangePrm) error { len(prm.container.PlacementPolicy().Replicas()) != 0 { // It handles locality internally. bufOpt := withLocalRangeBuffer(prm.localBuffer, prm.submitLocalStreamFn) - return s.get(ctx, prm.commonPrm, withPayloadRange(prm.rng), bufOpt).err + return s.get(ctx, prm.commonPrm, withPayloadRange(prm.rng), withPayloadOnly(true), withLegacyRange(true), bufOpt).err } nodeLists, repRules, ecRules, err := s.neoFSNet.GetNodesForObject(prm.addr) @@ -109,29 +138,20 @@ func (s *Service) GetRange(ctx context.Context, prm RangePrm) error { return fmt.Errorf("get nodes for object: %w", err) } - return s.getRange(ctx, prm, nodeLists, repRules, ecRules, nil) + return s.getRange(ctx, prm, nodeLists, repRules, ecRules) } -func (s *Service) getRange(ctx context.Context, prm RangePrm, nodeLists [][]netmap.NodeInfo, repRules []uint, ecRules []iec.Rule, - hashPrm *RangeHashPrm) error { +func (s *Service) getRange(ctx context.Context, prm RangePrm, nodeLists [][]netmap.NodeInfo, repRules []uint, ecRules []iec.Rule) error { if len(repRules) > 0 { // REP format does not require encoding bufOpt := withLocalRangeBuffer(prm.localBuffer, prm.submitLocalStreamFn) forwardOpt := withForwardRangeRequestFunc(prm.forwardRequestFn) - err := s.get(ctx, prm.commonPrm, withPreSortedContainerNodes(nodeLists[:len(repRules)], repRules), withPayloadRange(prm.rng), withHash(hashPrm), bufOpt, forwardOpt).err + err := s.get(ctx, prm.commonPrm, withPreSortedContainerNodes(nodeLists[:len(repRules)], repRules), withPayloadRange(prm.rng), withPayloadOnly(true), withLegacyRange(true), bufOpt, forwardOpt).err if len(ecRules) == 0 || !errors.Is(err, apistatus.ErrObjectNotFound) { return err } } ecNodeLists := nodeLists[len(repRules):] - if hashPrm != nil && prm.rangeForwarder != nil && !localNodeInSets(s.neoFSNet, nodeLists) { - hashes, err := s.proxyHashRequest(ctx, ecNodeLists, prm.rangeForwarder) - if err == nil { - hashPrm.forwardedRangeHashResponse = hashes - } - return err - } - if prm.forwardRequestFn != nil && !localNodeInSets(s.neoFSNet, ecNodeLists) { return s.forwardRangeRequest(ctx, ecNodeLists, prm.forwardRequestFn) } @@ -141,84 +161,13 @@ func (s *Service) getRange(ctx context.Context, prm RangePrm, nodeLists [][]netm for i := range ecRules { repRules[i] = uint(ecRules[i].DataPartNum + ecRules[i].ParityPartNum) } - return s.get(ctx, prm.commonPrm, withPreSortedContainerNodes(ecNodeLists, repRules), withPayloadRange(prm.rng)).err + return s.get(ctx, prm.commonPrm, withPreSortedContainerNodes(ecNodeLists, repRules), withPayloadRange(prm.rng), withPayloadOnly(true), withLegacyRange(true)).err } return s.copyECObjectRange(ctx, prm.objWriter, prm.addr.Container(), prm.addr.Object(), prm.common.SessionToken(), ecRules, ecNodeLists, prm.rng.GetOffset(), prm.rng.GetLength()) } -func (s *Service) GetRangeHash(ctx context.Context, prm RangeHashPrm) (*RangeHashRes, error) { - nodeLists, repRules, ecRules, err := s.neoFSNet.GetNodesForObject(prm.addr) - if err != nil { - return nil, fmt.Errorf("get nodes for object: %w", err) - } - - hashes := make([][]byte, 0, len(prm.rngs)) - - for _, rng := range prm.rngs { - h := prm.hashGen() - - // For big ranges we could fetch range-hashes from different nodes and concatenate them locally. - // However, - // 1. Potential gains are insignificant when operating in the Internet given typical latencies and losses. - // 2. Parallel solution is more complex in terms of code. - // 3. TZ-hash is likely to be disabled in private installations. - rngPrm := RangePrm{ - commonPrm: prm.commonPrm, - } - - rngPrm.SetRange(&rng) - rngPrm.SetChunkWriter(&hasherWrapper{ - hash: util.NewSaltingWriter(h, prm.salt), - }) - - if err := s.getRange(ctx, rngPrm, nodeLists, repRules, ecRules, &prm); err != nil { - return nil, err - } - - if prm.forwardedRangeHashResponse != nil { - // forwarder request case; no need to collect the other - // parts, the whole response has already been received - hashes = prm.forwardedRangeHashResponse - break - } - - hashes = append(hashes, h.Sum(nil)) - } - - return &RangeHashRes{ - hashes: hashes, - }, nil -} - -func (s *Service) proxyHashRequest(ctx context.Context, sortedNodeLists [][]netmap.NodeInfo, proxyFn RangeRequestForwarder) ([][]byte, error) { - for i := range sortedNodeLists { - for j := range sortedNodeLists[i] { - conn, err := s.conns.(*clientCacheWrapper).connect(ctx, sortedNodeLists[i][j]) - if err != nil { - s.log.Debug("get conn to remote node", - zap.Strings("addresses", slices.Collect(sortedNodeLists[i][j].NetworkEndpoints())), zap.Error(err)) - continue - } - - hashes, err := proxyFn(ctx, conn) - if err == nil { - return hashes, nil - } - - if errors.Is(err, apistatus.ErrObjectAlreadyRemoved) || errors.Is(err, apistatus.ErrObjectAccessDenied) || - errors.Is(err, apistatus.ErrObjectOutOfRange) || errors.Is(err, ctx.Err()) { - return nil, err - } - - s.log.Info("request proxy failed", zap.String("request", "HASH"), zap.Error(err)) - } - } - - return nil, apistatus.ErrObjectNotFound -} - // Head reads object header from container. // // Returns ErrNotFound if the header was not received for the call. @@ -340,7 +289,7 @@ func (exec *execCtx) analyzeStatus(execCnr bool) { zap.Error(exec.err), ) - if execCnr && !exec.headerWritten { + if execCnr && !exec.responseStarted { exec.executeOnContainer() exec.analyzeStatus(false) } diff --git a/pkg/services/object/get/get_test.go b/pkg/services/object/get/get_test.go index 4eafe6568e..8900117b50 100644 --- a/pkg/services/object/get/get_test.go +++ b/pkg/services/object/get/get_test.go @@ -412,6 +412,76 @@ func TestGetLocalOnly(t *testing.T) { testSplit(addr, splitInfo) }) }) + + t.Run("RANGE split GET writes header", func(t *testing.T) { + storage := newTestStorage() + splitInfo := object.NewSplitInfo() + splitInfo.SetLink(oidtest.ID()) + + srcObj := generateObject(addr, nil, nil) + children, childIDs, payload := generateChain(2, addr.Container()) + srcObj.SetPayload(payload) + srcObj.SetPayloadSize(uint64(len(payload))) + splitInfo.SetFirstPart(childIDs[0]) + + var linkChildren []object.MeasuredObject + for i := range children { + children[i].SetParentID(addr.Object()) + children[i].SetParent(srcObj) + var child object.MeasuredObject + child.SetObjectID(children[i].GetID()) + child.SetObjectSize(uint32(children[i].PayloadSize())) + linkChildren = append(linkChildren, child) + } + + linkAddr := oid.NewAddress(addr.Container(), splitInfo.GetLink()) + linkObj := generateObject(linkAddr, nil, nil) + linkObj.SetParentID(addr.Object()) + linkObj.SetParent(srcObj) + linkObj.SetFirstID(childIDs[0]) + var link object.Link + link.SetObjects(linkChildren) + linkObj.WriteLink(link) + + storage.addVirtual(addr, splitInfo) + ns, as := testNodeMatrix(t, []int{1}) + vectors := map[oid.Address][][]netmap.NodeInfo{ + addr: ns, + linkAddr: ns, + } + c := newTestClient() + c.addResult(linkAddr, linkObj, nil) + for i := range children { + childAddr := children[i].Address() + vectors[childAddr] = ns + c.addResult(childAddr, children[i], nil) + } + + svc := &Service{cfg: new(cfg)} + svc.log = zaptest.NewLogger(t) + svc.localObjects = storage + svc.localStorage = storage + svc.neoFSNet = &testNeoFS{vectors: vectors} + svc.clientCache = &testClientCache{ + clients: map[string]*testClient{ + as[0][0]: c, + }, + } + + w := NewSimpleObjectWriter() + p := newPrm(false, w) + p.WithAddress(addr) + + r := object.NewRange() + r.SetOffset(1) + r.SetLength(1) + p.SetRange(r) + + err := svc.Get(ctx, p) + require.NoError(t, err) + require.Equal(t, srcObj.CutPayload(), w.Object().CutPayload()) + require.Equal(t, payload[1:2], w.Object().Payload()) + }) } func testNodeMatrix(t testing.TB, dim []int) ([][]netmap.NodeInfo, [][]string) { @@ -900,6 +970,20 @@ func TestGetRemoteSmall(t *testing.T) { err = svc.GetRange(ctx, rngPrm) require.NoError(t, err) require.Equal(t, payload[off:off+ln], w.Object().Payload()) + + w = NewSimpleObjectWriter() + p = newPrm(false, w) + p.WithAddress(addr) + + r := object.NewRange() + r.SetOffset(off) + r.SetLength(ln) + p.SetRange(r) + + err = svc.Get(ctx, p) + require.NoError(t, err) + require.Equal(t, srcObj.CutPayload(), w.Object().CutPayload()) + require.Equal(t, payload[off:off+ln], w.Object().Payload()) }) }) @@ -1099,6 +1183,20 @@ func TestGetRemoteSmall(t *testing.T) { require.NoError(t, err) require.Equal(t, payload[off:off+ln], w.Object().Payload()) + w = NewSimpleObjectWriter() + p = newPrm(false, w) + p.WithAddress(addr) + + r := object.NewRange() + r.SetOffset(off) + r.SetLength(ln) + p.SetRange(r) + + err = svc.Get(ctx, p) + require.NoError(t, err) + require.Equal(t, srcObj.CutPayload(), w.Object().CutPayload()) + require.Equal(t, payload[off:off+ln], w.Object().Payload()) + w = NewSimpleObjectWriter() off = payloadSz - 2 ln = 1 @@ -1130,6 +1228,84 @@ func parameterizeXHeaders(t testing.TB, p *Prm, ss []string) { p.SetCommonParameters(cp) } +func TestWriteCollectedHeaderPayloadOnlyDoesNotStartResponse(t *testing.T) { + addr := oidtest.Address() + exec := &execCtx{ + prm: RangePrm{ + commonPrm: commonPrm{ + objWriter: &trackingWriter{}, + }, + }, + payloadOnly: true, + collectedHeader: generateObject(addr, nil, []byte("payload")), + log: zaptest.NewLogger(t), + } + + ok := exec.writeCollectedHeader() + require.True(t, ok) + require.False(t, exec.responseStarted) + require.Equal(t, statusOK, exec.status) + require.NoError(t, exec.err) + + w := exec.prm.objWriter.(*trackingWriter) + require.Zero(t, w.writeHeaderCount.Load()) +} + +func TestFallbackRangeReader(t *testing.T) { + t.Run("fallback get exec clears range and flags", func(t *testing.T) { + rng := object.NewRange() + rng.SetOffset(10) + rng.SetLength(20) + + exec := execCtx{ + prm: RangePrm{ + rng: rng, + }, + payloadOnly: true, + legacyRange: true, + } + + fallback := exec.fallbackGetExec() + require.Nil(t, fallback.ctxRange()) + require.False(t, fallback.payloadOnly) + require.False(t, fallback.legacyRange) + }) + + t.Run("partial chunk is returned before fallback", func(t *testing.T) { + buf := make([]byte, 16) + fr := &fallbackRangeReader{ + ReadCloser: &partialErrorReader{ + data: []byte("payload"), + err: apistatus.ErrObjectAccessDenied, + }, + } + + n, err := fr.Read(buf) + require.NoError(t, err) + require.Equal(t, len("payload"), n) + require.Equal(t, []byte("payload"), buf[:n]) + require.EqualValues(t, len("payload"), fr.delivered) + require.True(t, fr.fallbackPending) + require.False(t, fr.fallbackDone) + }) + + t.Run("fallback resumes after already delivered bytes", func(t *testing.T) { + rng := object.NewRange() + rng.SetOffset(10) + rng.SetLength(20) + + fr := &fallbackRangeReader{ + rng: rng, + delivered: 7, + } + + from, to, err := fr.fallbackBounds(100) + require.NoError(t, err) + require.EqualValues(t, 17, from) + require.EqualValues(t, 30, to) + }) +} + type failingReader struct { data []byte pos int @@ -1155,6 +1331,36 @@ func (r *failingReader) Close() error { return nil } +type errorReader struct { + err error +} + +func (r *errorReader) Read([]byte) (int, error) { + return 0, r.err +} + +func (r *errorReader) Close() error { + return nil +} + +type partialErrorReader struct { + data []byte + err error + read bool +} + +func (r *partialErrorReader) Read(p []byte) (int, error) { + if r.read { + return 0, io.EOF + } + r.read = true + return copy(p, r.data), r.err +} + +func (r *partialErrorReader) Close() error { + return nil +} + type trackingWriter struct { writeHeaderCount atomic.Int32 writeChunkCount atomic.Int32 @@ -1207,6 +1413,29 @@ func (s *testStorageWithFailingReader) Head(oid.Address, bool) (*object.Object, return s.obj.CutPayload(), nil } +type testStorageWithImmediateReadError struct { + unimplementedLocalStorage + obj *object.Object + err error +} + +func (s *testStorageWithImmediateReadError) get(*execCtx) (*object.Object, io.ReadCloser, error) { + if s.obj == nil { + return nil, nil, errors.New("object not found") + } + + objWithoutPayload := s.obj.CutPayload() + objWithoutPayload.SetPayloadSize(s.obj.PayloadSize()) + return objWithoutPayload, &errorReader{err: s.err}, nil +} + +func (s *testStorageWithImmediateReadError) Head(oid.Address, bool) (*object.Object, error) { + if s.obj == nil { + return nil, errors.New("object not found") + } + return s.obj.CutPayload(), nil +} + func TestDoubleWriteHeaderOnPayloadReadFailure(t *testing.T) { ctx := context.Background() addr := oidtest.Address() @@ -1310,3 +1539,54 @@ func TestDoubleWriteHeaderOnChunkWriteFailure(t *testing.T) { t.Logf("WriteChunk called: %d times", writer.writeChunkCount.Load()) require.EqualValues(t, 1, writer.writeHeaderCount.Load()) } + +func TestPayloadOnlyRangeReadFailureBeforeFirstChunkFallsBackRemote(t *testing.T) { + ctx := context.Background() + addr := oidtest.Address() + + payload := []byte("payload data") + obj := generateObject(addr, nil, payload) + + readErr := errors.New("simulated payload read error") + storage := &testStorageWithImmediateReadError{ + obj: obj, + err: readErr, + } + + anyNodeLists, nodeStrs := testNodeMatrix(t, []int{1}) + + clientCache := &testClientCache{ + clients: make(map[string]*testClient), + } + remoteClient := newTestClient() + remoteClient.addResult(addr, obj, nil) + clientCache.clients[nodeStrs[0][0]] = remoteClient + + svc := &Service{cfg: new(cfg)} + svc.log = zaptest.NewLogger(t) + svc.localObjects = storage + svc.localStorage = storage + svc.clientCache = clientCache + svc.neoFSNet = &testNeoFS{ + vectors: map[oid.Address][][]netmap.NodeInfo{ + addr: anyNodeLists, + }, + } + + writer := NewSimpleObjectWriter() + + var prm Prm + prm.SetObjectWriter(writer) + prm.WithAddress(addr) + prm.common = new(util.CommonPrm) + prm.MarkPayloadOnly() + + rng := object.NewRange() + rng.SetOffset(2) + rng.SetLength(5) + prm.SetRange(rng) + + err := svc.Get(ctx, prm) + require.NoError(t, err) + require.Equal(t, payload[2:7], writer.Object().Payload()) +} diff --git a/pkg/services/object/get/prm.go b/pkg/services/object/get/prm.go index 9c08858667..7354125ad4 100644 --- a/pkg/services/object/get/prm.go +++ b/pkg/services/object/get/prm.go @@ -3,7 +3,6 @@ package getsvc import ( "context" "crypto/ecdsa" - "hash" "io" iprotobuf "github.com/nspcc-dev/neofs-node/internal/protobuf" @@ -26,6 +25,9 @@ type SubmitDataStreamFunc = func(io.ReadCloser) type Prm struct { commonPrm + rng *object.Range + payloadOnly bool + localGetBuffer []byte submitLocalGetStreamFn SubmitStreamFunc @@ -44,21 +46,7 @@ type RangePrm struct { forwardRequestFn ForwardRangeRequestFunc } -// RangeHashPrm groups parameters of GetRange service call. -type RangeHashPrm struct { - commonPrm - - hashGen func() hash.Hash - - rngs []object.Range - - salt []byte - - forwardedRangeHashResponse [][]byte -} - type RequestForwarder func(context.Context, coreclient.MultiAddressClient) (*object.Object, error) -type RangeRequestForwarder func(context.Context, coreclient.MultiAddressClient) ([][]byte, error) // ForwardHeadRequestFunc sends currently served HEAD request to remote node // through passed connection and returns buffered response with requested @@ -98,8 +86,6 @@ type commonPrm struct { raw bool - rangeForwarder RangeRequestForwarder - // signerKey is a cached key that should be used for spawned // requests (if any), could be nil if incoming request handling // routine does not include any key fetching operations @@ -118,11 +104,27 @@ type ObjectWriter interface { ChunkWriter } +// HeaderValidator is an optional interface for validating object headers +// before suppressing them from the response. +type HeaderValidator interface { + ValidateHeader(*object.Object) error +} + // SetObjectWriter sets target component to write the object. func (p *Prm) SetObjectWriter(w ObjectWriter) { p.objWriter = w } +// SetRange sets range of the requested payload data. +func (p *Prm) SetRange(rng *object.Range) { + p.rng = rng +} + +// MarkPayloadOnly requests payload without an object header. +func (p *Prm) MarkPayloadOnly() { + p.payloadOnly = true +} + // SetChunkWriter sets target component to write the object payload range. func (p *RangePrm) SetChunkWriter(w ChunkWriter) { p.objWriter = &partWriter{ @@ -135,30 +137,11 @@ func (p *RangePrm) SetRange(rng *object.Range) { p.rng = rng } -// SetRangeList sets a list of object payload ranges. -func (p *RangeHashPrm) SetRangeList(rngs []object.Range) { - p.rngs = rngs -} - -// SetHashGenerator sets constructor of hashing algorithm. -func (p *RangeHashPrm) SetHashGenerator(v func() hash.Hash) { - p.hashGen = v -} - -// SetSalt sets binary salt to XOR object's payload ranges before hash calculation. -func (p *RangeHashPrm) SetSalt(salt []byte) { - p.salt = salt -} - // SetCommonParameters sets common parameters of the operation. func (p *commonPrm) SetCommonParameters(common *util.CommonPrm) { p.common = common } -func (p *commonPrm) SetRangeHashRequestForwarder(f RangeRequestForwarder) { - p.rangeForwarder = f -} - // WithAddress sets object address to be read. func (p *commonPrm) WithAddress(addr oid.Address) { p.addr = addr @@ -209,6 +192,16 @@ func (p Prm) GetBuffer() ([]byte, SubmitStreamFunc) { return p.localGetBuffer, p.submitLocalGetStreamFn } +// Range returns payload range settings. +func (p Prm) Range() *object.Range { + return p.rng +} + +// PayloadOnly reports whether only payload was requested. +func (p Prm) PayloadOnly() bool { + return p.payloadOnly +} + // SetRequestForwarder specifies request transport callback to use for receiving // response from remote node. // diff --git a/pkg/services/object/get/res.go b/pkg/services/object/get/res.go deleted file mode 100644 index 75a5aaedde..0000000000 --- a/pkg/services/object/get/res.go +++ /dev/null @@ -1,9 +0,0 @@ -package getsvc - -type RangeHashRes struct { - hashes [][]byte -} - -func (r *RangeHashRes) Hashes() [][]byte { - return r.hashes -} diff --git a/pkg/services/object/get/util.go b/pkg/services/object/get/util.go index 890acd6f00..969a956af8 100644 --- a/pkg/services/object/get/util.go +++ b/pkg/services/object/get/util.go @@ -62,10 +62,6 @@ type partWriter struct { chunkWriter ChunkWriter } -type hasherWrapper struct { - hash io.Writer -} - // fallbackRangeReader wraps a range reader obtained via ObjectRangeInit and // falls back to a full GET in case apistatus.ErrObjectAccessDenied is // returned while reading. @@ -76,7 +72,17 @@ type fallbackRangeReader struct { key *ecdsa.PrivateKey rng *object.Range - fallbackDone bool + delivered uint64 + fallbackPending bool + fallbackDone bool +} + +func (exec execCtx) fallbackGetExec() execCtx { + exec.prm.rng = nil + exec.payloadOnly = false + exec.legacyRange = false + + return exec } func newFallbackRangeReader(exec *execCtx, c *clientWrapper, key *ecdsa.PrivateKey, rng *object.Range, rdr io.ReadCloser) io.ReadCloser { @@ -90,39 +96,70 @@ func newFallbackRangeReader(exec *execCtx, c *clientWrapper, key *ecdsa.PrivateK } func (f *fallbackRangeReader) Read(p []byte) (int, error) { + if f.fallbackPending && !f.fallbackDone { + return f.fallbackRead(p) + } + n, err := f.ReadCloser.Read(p) + if n > 0 { + f.delivered += uint64(n) + } if err == nil || !errors.Is(err, apistatus.ErrObjectAccessDenied) || f.fallbackDone { return n, err } + if n > 0 { + f.fallbackPending = true + return n, nil + } + return f.fallbackRead(p) +} + +func (f *fallbackRangeReader) fallbackBounds(payloadSize uint64) (from, to uint64, err error) { + base := f.rng.GetOffset() + from = base + f.delivered + if from < base { + return 0, 0, apistatus.ErrObjectOutOfRange + } + + if ln := f.rng.GetLength(); ln != 0 { + to = base + ln + if to < base || to < from { + return 0, 0, apistatus.ErrObjectOutOfRange + } + } else { + to = payloadSize + } + + if payloadSize < from || payloadSize < to { + return 0, 0, apistatus.ErrObjectOutOfRange + } + + return from, to, nil +} + +func (f *fallbackRangeReader) fallbackRead(p []byte) (int, error) { f.exec.log.Debug("range read access denied, falling back to full GET") + f.fallbackPending = false f.fallbackDone = true - hdr, rdr, getErr := f.client.get(f.exec, f.key) + fallbackExec := f.exec.fallbackGetExec() + hdr, rdr, getErr := f.client.get(&fallbackExec, f.key) if getErr != nil { return 0, fmt.Errorf("fallback GET after access denial failed: %w", getErr) } - pLen := hdr.PayloadSize() - from := f.rng.GetOffset() - ln := f.rng.GetLength() - var to uint64 - if ln != 0 { - to = from + ln - } else { - to = pLen - } - - if to < from || pLen < from || pLen < to { + from, to, err := f.fallbackBounds(hdr.PayloadSize()) + if err != nil { _ = rdr.Close() - return 0, apistatus.ErrObjectOutOfRange + return 0, err } if from > 0 { _, err = io.CopyN(io.Discard, rdr, int64(from)) if err != nil { _ = rdr.Close() - return n, fmt.Errorf("discard %d bytes in stream: %w", from, err) + return 0, fmt.Errorf("discard %d bytes in stream: %w", from, err) } } @@ -229,24 +266,47 @@ func (c *clientWrapper) getObject(exec *execCtx) (*object.Object, io.ReadCloser, return hdr, nil, nil } - if rngH := exec.prmRangeHash; rngH != nil && exec.isRangeHashForwardingEnabled() { - exec.prmRangeHash.forwardedRangeHashResponse, err = exec.prm.rangeForwarder(exec.ctx, c.client) - return nil, nil, err - } - // we don't specify payload writer because we accumulate // the object locally (even huge). if rng := exec.ctxRange(); rng != nil { addr := exec.address() id := addr.Object() - ln := rng.GetLength() - var opts client.PrmObjectRange + if exec.legacyRange { + ln := rng.GetLength() + + var opts client.PrmObjectRange + if exec.prm.common.TTL() < 2 { + opts.MarkLocal() + } + if stV2 := exec.prm.common.SessionTokenV2(); stV2 != nil { + if stV2.AssertVerb(sessionv2.VerbObjectRange, addr.Container()) { + opts.WithinSessionV2(*stV2) + } + } else if st := exec.prm.common.SessionToken(); st != nil && st.AssertObject(id) { + opts.WithinSession(*st) + } + if bt := exec.prm.common.BearerToken(); bt != nil { + opts.WithBearerToken(*bt) + } + opts.WithXHeaders(exec.prm.common.XHeaders()...) + if exec.isRaw() { + opts.MarkRaw() + } + + rdr, err := c.client.ObjectRangeInit(exec.context(), addr.Container(), id, rng.GetOffset(), ln, user.NewAutoIDSigner(*key), opts) + if err != nil { + return nil, nil, fmt.Errorf("init payload reading: %w", err) + } + return nil, newFallbackRangeReader(exec, c, key, rng, rdr), nil + } + + var opts client.PrmObjectGet if exec.prm.common.TTL() < 2 { opts.MarkLocal() } if stV2 := exec.prm.common.SessionTokenV2(); stV2 != nil { - if stV2.AssertVerb(sessionv2.VerbObjectRange, addr.Container()) { + if stV2.AssertVerb(sessionv2.VerbObjectGet, addr.Container()) { opts.WithinSessionV2(*stV2) } } else if st := exec.prm.common.SessionToken(); st != nil && st.AssertObject(id) { @@ -259,13 +319,19 @@ func (c *clientWrapper) getObject(exec *execCtx) (*object.Object, io.ReadCloser, if exec.isRaw() { opts.MarkRaw() } + opts.SetRange(rng.GetOffset(), rng.GetLength()) + if exec.payloadOnly { + opts.MarkPayloadOnly() + } - rdr, err := c.client.ObjectRangeInit(exec.context(), addr.Container(), id, rng.GetOffset(), ln, user.NewAutoIDSigner(*key), opts) + hdr, rdr, err := c.client.ObjectGetInit(exec.context(), addr.Container(), id, user.NewAutoIDSigner(*key), opts) if err != nil { return nil, nil, fmt.Errorf("init payload reading: %w", err) } - // fallback to full GET in case of access denial error. - return nil, newFallbackRangeReader(exec, c, key, rng, rdr), nil + if exec.payloadOnly { + return nil, newFallbackRangeReader(exec, c, key, rng, rdr), nil + } + return &hdr, newFallbackRangeReader(exec, c, key, rng, rdr), nil } return c.get(exec, key) @@ -293,6 +359,9 @@ func (c *clientWrapper) get(exec *execCtx, key *ecdsa.PrivateKey) (*object.Objec if exec.isRaw() { opts.MarkRaw() } + if exec.payloadOnly && exec.ctxRange() == nil { + opts.MarkPayloadOnly() + } hdr, rdr, err := c.client.ObjectGetInit(exec.context(), addr.Container(), id, user.NewAutoIDSigner(*key), opts) if err != nil { @@ -320,7 +389,17 @@ func (e *storageEngineWrapper) get(exec *execCtx) (*object.Object, io.ReadCloser return nil, nil, err } r, err := e.engine.GetRangeStream(exec.address(), rng.GetOffset(), rng.GetLength()) - return nil, r, err + if err != nil || exec.payloadOnly { + return nil, r, err + } + h, hErr := e.engine.Head(exec.address(), exec.isRaw()) + if hErr != nil { + if r != nil { + _ = r.Close() + } + return nil, nil, hErr + } + return h, r, nil } if exec.localGetBuffer != nil { @@ -342,11 +421,6 @@ func (w *partWriter) WriteHeader(o *object.Object) error { return w.headWriter.WriteHeader(o) } -func (h *hasherWrapper) WriteChunk(p []byte) error { - _, err := h.hash.Write(p) - return err -} - func prettyRange(rng *object.Range) string { return fmt.Sprintf("[%d:%d]", rng.GetOffset(), rng.GetLength()) } diff --git a/pkg/services/object/put/service_test.go b/pkg/services/object/put/service_test.go index 6aeb7c06ec..708a53feda 100644 --- a/pkg/services/object/put/service_test.go +++ b/pkg/services/object/put/service_test.go @@ -1112,10 +1112,6 @@ func (m *serviceClient) ObjectRangeInit(context.Context, cid.ID, oid.ID, uint64, panic("unimplemented") } -func (m *serviceClient) ObjectHash(context.Context, cid.ID, oid.ID, user.Signer, client.PrmObjectHash) ([][]byte, error) { - panic("unimplemented") -} - func (m *serviceClient) AnnounceLocalTrust(context.Context, uint64, []apireputation.Trust, client.PrmAnnounceLocalTrust) error { // TODO: interfaces are oversaturated. This will never be needed to server object PUT. Refactor this. panic("unimplemented") diff --git a/pkg/services/object/server.go b/pkg/services/object/server.go index 619da94f57..200b0c813a 100644 --- a/pkg/services/object/server.go +++ b/pkg/services/object/server.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "crypto/ecdsa" - "crypto/sha256" "encoding/base64" "encoding/binary" "errors" @@ -48,7 +47,6 @@ import ( "github.com/nspcc-dev/neofs-sdk-go/stat" "github.com/nspcc-dev/neofs-sdk-go/user" "github.com/nspcc-dev/neofs-sdk-go/version" - "github.com/nspcc-dev/tzhash/tz" "github.com/panjf2000/ants/v2" "google.golang.org/grpc" grpccodes "google.golang.org/grpc/codes" @@ -65,7 +63,6 @@ type Handlers interface { Head(context.Context, getsvc.HeadPrm) error Delete(context.Context, deletesvc.Prm) error GetRange(context.Context, getsvc.RangePrm) error - GetRangeHash(context.Context, getsvc.RangeHashPrm) (*getsvc.RangeHashRes, error) } // Various NeoFS protocol status codes. @@ -83,7 +80,6 @@ const ( // - [stat.MethodObjectDelete] // - [stat.MethodObjectSearch] // - [stat.MethodObjectRange] -// - [stat.MethodObjectHash] type MetricCollector interface { // HandleOpExecResult handles measured execution results of the given op. HandleOpExecResult(_ stat.Method, success bool, _ time.Duration) @@ -160,7 +156,6 @@ type ACLInfoExtractor interface { PutRequestToInfo(*protoobject.PutRequest) (aclsvc.RequestInfo, user.ID, error) DeleteRequestToInfo(*protoobject.DeleteRequest) (aclsvc.RequestInfo, error) HeadRequestToInfo(*protoobject.HeadRequest) (aclsvc.RequestInfo, error) - HashRequestToInfo(*protoobject.GetRangeHashRequest) (aclsvc.RequestInfo, error) GetRequestToInfo(*protoobject.GetRequest) (aclsvc.RequestInfo, error) RangeRequestToInfo(*protoobject.GetRangeRequest) (aclsvc.RequestInfo, error) SearchV2RequestToInfo(*protoobject.SearchV2Request) (aclsvc.RequestInfo, error) @@ -825,185 +820,9 @@ func convertHeadPrm(signer ecdsa.PrivateKey, cnr container.Container, req *proto return p, nil } -func (s *Server) signHashResponse(resp *protoobject.GetRangeHashResponse, req *protoobject.GetRangeHashRequest) *protoobject.GetRangeHashResponse { - resp.VerifyHeader = util.SignResponseIfNeeded(&s.signer, resp, req) - return resp -} - -func (s *Server) makeStatusHashResponse(err error, req *protoobject.GetRangeHashRequest) *protoobject.GetRangeHashResponse { - return s.signHashResponse(&protoobject.GetRangeHashResponse{ - MetaHeader: s.makeResponseMetaHeader(util.ToStatus(err)), - }, req) -} - -// GetRangeHash converts gRPC GetRangeHashRequest message and passes it to internal Object service. -func (s *Server) GetRangeHash(ctx context.Context, req *protoobject.GetRangeHashRequest) (*protoobject.GetRangeHashResponse, error) { - var ( - err error - t = time.Now() - ) - defer func() { s.pushOpExecResult(stat.MethodObjectHash, err, t) }() - if err = icrypto.VerifyRequestSignaturesN3(req, s.fsChain); err != nil { - return s.makeStatusHashResponse(err, req), nil - } - - if s.fsChain.LocalNodeUnderMaintenance() { - return s.makeStatusHashResponse(apistatus.ErrNodeUnderMaintenance, req), nil - } - - reqInfo, err := s.reqInfoProc.HashRequestToInfo(req) - if err != nil { - if !errors.Is(err, apistatus.Error) { - var bad = new(apistatus.BadRequest) - bad.SetMessage(err.Error()) - err = bad // defer - } - return s.makeStatusHashResponse(err, req), nil - } - if !s.aclChecker.CheckBasicACL(reqInfo) { - err = basicACLErr(reqInfo) // needed for defer - return s.makeStatusHashResponse(err, req), nil - } - err = s.aclChecker.CheckEACL(req, reqInfo) - if err != nil && !errors.Is(err, aclsvc.ErrNotMatched) { // Not matched -> follow basic ACL. - err = eACLErr(reqInfo, err) // needed for defer - return s.makeStatusHashResponse(err, req), nil - } - - p, err := convertHashPrm(s.signer, reqInfo.Container, s.storage, req) - if err != nil { - if !errors.Is(err, apistatus.Error) { - var bad = new(apistatus.BadRequest) - bad.SetMessage(err.Error()) - err = bad // defer - } - return s.makeStatusHashResponse(err, req), nil - } - res, err := s.handlers.GetRangeHash(ctx, p) - if err != nil { - return s.makeStatusHashResponse(err, req), nil - } - - return s.signHashResponse(&protoobject.GetRangeHashResponse{ - Body: &protoobject.GetRangeHashResponse_Body{ - Type: req.Body.Type, - HashList: res.Hashes(), - }}, req), nil -} - -// converts original request into parameters accepted by the internal handler. -func convertHashPrm(signer ecdsa.PrivateKey, cnr container.Container, ss sessions, req *protoobject.GetRangeHashRequest) (getsvc.RangeHashPrm, error) { - body := req.GetBody() - ma := body.GetAddress() - if ma == nil { // includes nil body - return getsvc.RangeHashPrm{}, errors.New("missing object address") - } - - var addr oid.Address - if err := addr.FromProtoMessage(ma); err != nil { - return getsvc.RangeHashPrm{}, fmt.Errorf("invalid object address: %w", err) - } - - cp, err := objutil.CommonPrmFromRequest(req) - if err != nil { - return getsvc.RangeHashPrm{}, err - } - - var p getsvc.RangeHashPrm - - switch t := body.GetType(); t { - default: - return getsvc.RangeHashPrm{}, fmt.Errorf("unknown checksum type %v", t) - case refs.ChecksumType_SHA256: - p.SetHashGenerator(sha256.New) - case refs.ChecksumType_TZ: - p.SetHashGenerator(tz.New) - } - - if tokV2 := cp.SessionTokenV2(); tokV2 != nil { - signerKey, err := ss.GetSessionV2PrivateKey(tokV2.Subjects()) - if err != nil { - if !errors.Is(err, apistatus.ErrSessionTokenNotFound) { - return getsvc.RangeHashPrm{}, fmt.Errorf("fetching session v2 key: %w", err) - } - cp.ForgetTokens() - signerKey = signer - } - p.WithCachedSignerKey(&signerKey) - } else if tok := cp.SessionToken(); tok != nil { - authUser, err := tok.AuthUser() - if err != nil { - return getsvc.RangeHashPrm{}, fmt.Errorf("getting auth user from token: %w", err) - } - signerKey, err := ss.GetSessionPrivateKey(authUser) - if err != nil { - if !errors.Is(err, apistatus.ErrSessionTokenNotFound) { - return getsvc.RangeHashPrm{}, fmt.Errorf("fetching session key: %w", err) - } - cp.ForgetTokens() - signerKey = signer - } - p.WithCachedSignerKey(&signerKey) - } - - mr := body.GetRanges() - rngs := make([]object.Range, len(mr)) - for i := range mr { - rngs[i].SetOffset(mr[i].Offset) - rngs[i].SetLength(mr[i].Length) - } - - p.SetCommonParameters(cp) - p.WithAddress(addr) - p.WithContainer(cnr) - p.SetRangeList(rngs) - p.SetSalt(body.GetSalt()) - - if cp.LocalOnly() { - return p, nil - } - - var onceResign sync.Once - meta := req.GetMetaHeader() - if meta == nil { - return getsvc.RangeHashPrm{}, errors.New("missing meta header") - } - p.SetRangeHashRequestForwarder(func(ctx context.Context, c client.MultiAddressClient) ([][]byte, error) { - var err error - onceResign.Do(func() { - req.MetaHeader = &protosession.RequestMetaHeader{ - // TODO: #1165 think how to set the other fields - Version: newCurrentProtoVersionMessage(), - Ttl: meta.GetTtl() - 1, - Origin: meta, - } - req.VerifyHeader, err = neofscrypto.SignRequestWithBuffer(neofsecdsa.Signer(signer), req, nil) - }) - if err != nil { - return nil, err - } - - var hs [][]byte - return hs, c.ForAnyGRPCConn(ctx, func(ctx context.Context, conn *grpc.ClientConn) error { - var err error - hs, err = getHashesFromRemoteNode(ctx, conn, req) - return err // TODO: log error - }) - }) - return p, nil -} - -func getHashesFromRemoteNode(ctx context.Context, conn *grpc.ClientConn, req *protoobject.GetRangeHashRequest) ([][]byte, error) { - resp, err := protoobject.NewObjectServiceClient(conn).GetRangeHash(ctx, req) - if err != nil { - return nil, fmt.Errorf("GetRangeHash rpc failure: %w", err) - } - - if err := checkStatus(resp.GetMetaHeader().GetStatus()); err != nil { - return nil, err - } - // TODO: verify number of hashes - return resp.GetBody().GetHashList(), nil +// GetRangeHash is deprecated and no longer supported by the node. +func (s *Server) GetRangeHash(_ context.Context, _ *protoobject.GetRangeHashRequest) (*protoobject.GetRangeHashResponse, error) { + return nil, grpcstatus.Error(grpccodes.Unimplemented, "no longer supported") } func (s *Server) sendGetResponse(stream protoobject.ObjectService_GetServer, resp *protoobject.GetResponse, sign bool) error { @@ -1036,9 +855,14 @@ type getStream struct { recheckEACL bool signResponse bool + payloadOnly bool } -func (s *getStream) WriteHeader(hdr *object.Object) error { +func (s *getStream) ValidateHeader(hdr *object.Object) error { + if !s.recheckEACL { + return nil + } + mo := hdr.ProtoMessage() resp := &protoobject.GetResponse{ Body: &protoobject.GetResponse_Body{ @@ -1049,11 +873,31 @@ func (s *getStream) WriteHeader(hdr *object.Object) error { }}, }, } - if s.recheckEACL { - err := s.srv.aclChecker.CheckEACL(resp, s.reqInfo) - if err != nil && !errors.Is(err, aclsvc.ErrNotMatched) { // Not matched -> follow basic ACL. - return eACLErr(s.reqInfo, err) - } + + err := s.srv.aclChecker.CheckEACL(resp, s.reqInfo) + if err != nil && !errors.Is(err, aclsvc.ErrNotMatched) { // Not matched -> follow basic ACL. + return eACLErr(s.reqInfo, err) + } + return nil +} + +func (s *getStream) WriteHeader(hdr *object.Object) error { + if err := s.ValidateHeader(hdr); err != nil { + return err + } + if s.payloadOnly { + return nil + } + + mo := hdr.ProtoMessage() + resp := &protoobject.GetResponse{ + Body: &protoobject.GetResponse_Body{ + ObjectPart: &protoobject.GetResponse_Body_Init_{Init: &protoobject.GetResponse_Body_Init{ + ObjectId: mo.ObjectId, + Signature: mo.Signature, + Header: mo.Header, + }}, + }, } return s.srv.sendGetResponse(s.base, resp, s.signResponse) } @@ -1121,6 +965,7 @@ func (s *Server) Get(req *protoobject.GetRequest, gStream protoobject.ObjectServ reqInfo: reqInfo, recheckEACL: recheckEACL, signResponse: needSignResp, + payloadOnly: req.GetBody().GetPayloadOnly(), }) if err != nil { if !errors.Is(err, apistatus.Error) { @@ -1135,8 +980,12 @@ func (s *Server) Get(req *protoobject.GetRequest, gStream protoobject.ObjectServ // We could acquire ~256K buffer (like for chunks) if storage would try to read it full. // Then small objects would fit into a single buffer, and for large ones it'd be possible to // encode the first chunk response using the heading buffer. - hdrRespBuf, hdrBuf := getBufferForHeadResponse() - defer hdrRespBuf.Free() + var hdrRespBuf *iprotobuf.MemBuffer + var hdrBuf []byte + if p.Range() == nil && !p.PayloadOnly() { + hdrRespBuf, hdrBuf = getBufferForHeadResponse() + defer hdrRespBuf.Free() + } hdrLen := -1 var stream io.ReadCloser @@ -1146,7 +995,9 @@ func (s *Server) Get(req *protoobject.GetRequest, gStream protoobject.ObjectServ } }() - p.WithBuffer(hdrBuf, func(ln int, s io.ReadCloser) { hdrLen, stream = ln, s }) + if hdrBuf != nil { + p.WithBuffer(hdrBuf, func(ln int, s io.ReadCloser) { hdrLen, stream = ln, s }) + } err = s.handlers.Get(gStream.Context(), p) if err != nil { @@ -1296,6 +1147,18 @@ func convertGetPrm(signer ecdsa.PrivateKey, cnr container.Container, req *protoo return getsvc.Prm{}, fmt.Errorf("invalid object address: %w", err) } + rng := body.GetRange() + if rng != nil { + rln := rng.GetLength() + if rln == 0 { + if rng.GetOffset() != 0 { + return getsvc.Prm{}, errors.New("zero range length") + } + } else if rng.GetOffset()+rln <= rng.GetOffset() { + return getsvc.Prm{}, errors.New("range overflow") + } + } + cp, err := objutil.CommonPrmFromRequest(req) if err != nil { return getsvc.Prm{}, err @@ -1307,6 +1170,15 @@ func convertGetPrm(signer ecdsa.PrivateKey, cnr container.Container, req *protoo p.WithContainer(cnr) p.WithRawFlag(body.Raw) p.SetObjectWriter(stream) + if rng != nil { + var objRng object.Range + objRng.SetOffset(rng.GetOffset()) + objRng.SetLength(rng.GetLength()) + p.SetRange(&objRng) + } + if body.GetPayloadOnly() { + p.MarkPayloadOnly() + } if cp.LocalOnly() { return p, nil } diff --git a/pkg/services/object/server_test.go b/pkg/services/object/server_test.go index 0ef9805384..9c66ae890d 100644 --- a/pkg/services/object/server_test.go +++ b/pkg/services/object/server_test.go @@ -85,10 +85,6 @@ func (x noCallObjectService) GetRange(context.Context, getsvc.RangePrm) error { panic("must not be called") } -func (x noCallObjectService) GetRangeHash(context.Context, getsvc.RangeHashPrm) (*getsvc.RangeHashRes, error) { - panic("must not be called") -} - type noCallTestFSChain struct{} func (*noCallTestFSChain) ForEachContainerNodePublicKeyInLastTwoEpochs(cid.ID, func([]byte) bool) error { @@ -141,9 +137,6 @@ func (noCallTestReqInfoExtractor) DeleteRequestToInfo(*protoobject.DeleteRequest func (noCallTestReqInfoExtractor) HeadRequestToInfo(*protoobject.HeadRequest) (v2.RequestInfo, error) { panic("must not be called") } -func (noCallTestReqInfoExtractor) HashRequestToInfo(*protoobject.GetRangeHashRequest) (v2.RequestInfo, error) { - panic("must not be called") -} func (noCallTestReqInfoExtractor) GetRequestToInfo(*protoobject.GetRequest) (v2.RequestInfo, error) { panic("must not be called") } @@ -180,9 +173,6 @@ func (nopReqInfoExtractor) DeleteRequestToInfo(*protoobject.DeleteRequest) (v2.R func (nopReqInfoExtractor) HeadRequestToInfo(*protoobject.HeadRequest) (v2.RequestInfo, error) { return v2.RequestInfo{}, nil } -func (nopReqInfoExtractor) HashRequestToInfo(*protoobject.GetRangeHashRequest) (v2.RequestInfo, error) { - return v2.RequestInfo{}, nil -} func (nopReqInfoExtractor) GetRequestToInfo(*protoobject.GetRequest) (v2.RequestInfo, error) { return v2.RequestInfo{}, nil } @@ -805,10 +795,6 @@ func (unimplementedConn) ObjectRangeInit(context.Context, cid.ID, oid.ID, uint64 panic("unimplemented") } -func (unimplementedConn) ObjectHash(context.Context, cid.ID, oid.ID, user.Signer, client.PrmObjectHash) ([][]byte, error) { - panic("unimplemented") -} - func (unimplementedConn) AnnounceLocalTrust(context.Context, uint64, []reputation.Trust, client.PrmAnnounceLocalTrust) error { panic("unimplemented") }