diff --git a/cmd/neofs-cli/internal/common/verbose.go b/cmd/neofs-cli/internal/common/verbose.go index 847349d67..48399a123 100644 --- a/cmd/neofs-cli/internal/common/verbose.go +++ b/cmd/neofs-cli/internal/common/verbose.go @@ -1,11 +1,14 @@ package common import ( + "encoding/hex" "fmt" "strconv" "time" "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/commonflags" + "github.com/nspcc-dev/neofs-sdk-go/checksum" + "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -28,3 +31,17 @@ func PrettyPrintUnixTime(s string) string { return timestamp.String() } + +// PrintChecksum prints checksum. +func PrintChecksum(cmd *cobra.Command, name string, recv func() (checksum.Checksum, bool)) { + var strVal string + + cs, csSet := recv() + if csSet { + strVal = hex.EncodeToString(cs.Value()) + } else { + strVal = "" + } + + cmd.Printf("%s: %s\n", name, strVal) +} diff --git a/cmd/neofs-cli/modules/container.go b/cmd/neofs-cli/modules/container.go index 0acc2ea04..260d05ead 100644 --- a/cmd/neofs-cli/modules/container.go +++ b/cmd/neofs-cli/modules/container.go @@ -16,6 +16,8 @@ import ( "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" + objectCli "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/object" + sessionCli "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/session" "github.com/nspcc-dev/neofs-sdk-go/acl" "github.com/nspcc-dev/neofs-sdk-go/container" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" @@ -286,10 +288,11 @@ var listContainerObjectsCmd = &cobra.Command{ filters := new(object.SearchFilters) filters.AddRootFilter() // search only user created objects - var prm internalclient.SearchObjectsPrm + pk := key.GetOrGenerate(cmd) - prepareSessionPrm(cmd, *id, nil, &prm) - prepareObjectPrm(cmd, &prm) + var prm internalclient.SearchObjectsPrm + sessionCli.Prepare(cmd, *id, nil, pk, &prm) + objectCli.Prepare(cmd, &prm) prm.SetContainerID(*id) prm.SetFilters(*filters) diff --git a/cmd/neofs-cli/modules/object.go b/cmd/neofs-cli/modules/object.go deleted file mode 100644 index 313f27317..000000000 --- a/cmd/neofs-cli/modules/object.go +++ /dev/null @@ -1,1137 +0,0 @@ -package cmd - -import ( - "bytes" - "crypto/ecdsa" - "encoding/hex" - "errors" - "fmt" - "io" - "os" - "path/filepath" - "strconv" - "strings" - "time" - - "github.com/cheggaaa/pb" - objectV2 "github.com/nspcc-dev/neofs-api-go/v2/object" - "github.com/nspcc-dev/neofs-api-go/v2/refs" - 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" - sessionCli "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/session" - "github.com/nspcc-dev/neofs-sdk-go/bearer" - "github.com/nspcc-dev/neofs-sdk-go/checksum" - cid "github.com/nspcc-dev/neofs-sdk-go/container/id" - "github.com/nspcc-dev/neofs-sdk-go/object" - oid "github.com/nspcc-dev/neofs-sdk-go/object/id" - "github.com/nspcc-dev/neofs-sdk-go/session" - "github.com/nspcc-dev/neofs-sdk-go/user" - "github.com/spf13/cobra" -) - -const ( - getRangeCmdUse = "range" - - getRangeCmdShortDesc = "Get payload range data of an object" - - getRangeCmdLongDesc = "Get payload range data of an object" -) - -const ( - getRangeHashSaltFlag = "salt" -) - -const bearerTokenFlag = "bearer" - -var ( - // objectCmd represents the object command - objectCmd = &cobra.Command{ - Use: "object", - Short: "Operations with Objects", - Long: `Operations with Objects`, - PersistentPreRun: func(cmd *cobra.Command, args []string) { - // bind exactly that cmd's flags to - // the viper before execution - commonflags.Bind(cmd) - commonflags.BindAPI(cmd) - }, - } - - objectPutCmd = &cobra.Command{ - Use: "put", - Short: "Put object to NeoFS", - Long: "Put object to NeoFS", - Run: putObject, - } - - objectGetCmd = &cobra.Command{ - Use: "get", - Short: "Get object from NeoFS", - Long: "Get object from NeoFS", - Run: getObject, - } - - objectDelCmd = &cobra.Command{ - Use: "delete", - Aliases: []string{"del"}, - Short: "Delete object from NeoFS", - Long: "Delete object from NeoFS", - Run: deleteObject, - } - - searchFilters []string - - objectSearchCmd = &cobra.Command{ - Use: "search", - Short: "Search object", - Long: "Search object", - Run: searchObject, - } - - objectHeadCmd = &cobra.Command{ - Use: "head", - Short: "Get object header", - Long: "Get object header", - Run: getObjectHeader, - } - - objectHashCmd = &cobra.Command{ - Use: "hash", - Short: "Get object hash", - Long: "Get object hash", - Run: getObjectHash, - } - - objectRangeCmd = &cobra.Command{ - Use: getRangeCmdUse, - Short: getRangeCmdShortDesc, - Long: getRangeCmdLongDesc, - Run: getObjectRange, - } -) - -const notificationFlag = "notify" - -const ( - hashSha256 = "sha256" - hashTz = "tz" - rangeSep = ":" -) - -const searchOIDFlag = "oid" - -const ( - rawFlag = "raw" - rawFlagDesc = "Set raw request option" -) - -const putExpiresOnFlag = "expires-on" - -const noProgressFlag = "no-progress" - -var putExpiredOn uint64 - -func initObjectPutCmd() { - commonflags.Init(objectPutCmd) - - flags := objectPutCmd.Flags() - - flags.String("file", "", "File with object payload") - _ = objectPutCmd.MarkFlagFilename("file") - _ = objectPutCmd.MarkFlagRequired("file") - - flags.String("cid", "", "Container ID") - _ = objectPutCmd.MarkFlagRequired("cid") - - flags.String("attributes", "", "User attributes in form of Key1=Value1,Key2=Value2") - flags.Bool("disable-filename", false, "Do not set well-known filename attribute") - flags.Bool("disable-timestamp", false, "Do not set well-known timestamp attribute") - flags.Uint64VarP(&putExpiredOn, putExpiresOnFlag, "e", 0, "Last epoch in the life of the object") - flags.Bool(noProgressFlag, false, "Do not show progress bar") - - flags.String(notificationFlag, "", "Object notification in the form of *epoch*:*topic*; '-' topic means using default") -} - -func initObjectDeleteCmd() { - commonflags.Init(objectDelCmd) - - flags := objectDelCmd.Flags() - - flags.String("cid", "", "Container ID") - _ = objectDelCmd.MarkFlagRequired("cid") - - flags.String("oid", "", "Object ID") - _ = objectDelCmd.MarkFlagRequired("oid") -} - -func initObjectGetCmd() { - commonflags.Init(objectGetCmd) - - flags := objectGetCmd.Flags() - - flags.String("cid", "", "Container ID") - _ = objectGetCmd.MarkFlagRequired("cid") - - flags.String("oid", "", "Object ID") - _ = objectGetCmd.MarkFlagRequired("oid") - - flags.String("file", "", "File to write object payload to. Default: stdout.") - flags.String("header", "", "File to write header to. Default: stdout.") - flags.Bool(rawFlag, false, rawFlagDesc) - flags.Bool(noProgressFlag, false, "Do not show progress bar") -} - -func initObjectSearchCmd() { - commonflags.Init(objectSearchCmd) - - flags := objectSearchCmd.Flags() - - flags.String("cid", "", "Container ID") - _ = objectSearchCmd.MarkFlagRequired("cid") - - flags.StringSliceVarP(&searchFilters, "filters", "f", nil, - "Repeated filter expressions or files with protobuf JSON") - - flags.Bool("root", false, "Search for user objects") - flags.Bool("phy", false, "Search physically stored objects") - flags.String(searchOIDFlag, "", "Search object by identifier") -} - -func initObjectHeadCmd() { - commonflags.Init(objectHeadCmd) - - flags := objectHeadCmd.Flags() - - flags.String("cid", "", "Container ID") - _ = objectHeadCmd.MarkFlagRequired("cid") - - flags.String("oid", "", "Object ID") - _ = objectHeadCmd.MarkFlagRequired("oid") - - flags.String("file", "", "File to write header to. Default: stdout.") - flags.Bool("main-only", false, "Return only main fields") - flags.Bool("json", false, "Marshal output in JSON") - flags.Bool("proto", false, "Marshal output in Protobuf") - flags.Bool(rawFlag, false, rawFlagDesc) -} - -func initObjectHashCmd() { - commonflags.Init(objectHashCmd) - - flags := objectHashCmd.Flags() - - flags.String("cid", "", "Container ID") - _ = objectHashCmd.MarkFlagRequired("cid") - - flags.String("oid", "", "Object ID") - _ = objectHashCmd.MarkFlagRequired("oid") - - flags.String("range", "", "Range to take hash from in the form offset1:length1,...") - flags.String("type", hashSha256, "Hash type. Either 'sha256' or 'tz'") - flags.String(getRangeHashSaltFlag, "", "Salt in hex format") -} - -func initObjectRangeCmd() { - commonflags.Init(objectRangeCmd) - - flags := objectRangeCmd.Flags() - - flags.String("cid", "", "Container ID") - _ = objectRangeCmd.MarkFlagRequired("cid") - - flags.String("oid", "", "Object ID") - _ = objectRangeCmd.MarkFlagRequired("oid") - - flags.String("range", "", "Range to take data from in the form offset:length") - flags.String("file", "", "File to write object payload to. Default: stdout.") - flags.Bool(rawFlag, false, rawFlagDesc) -} - -func init() { - objectChildCommands := []*cobra.Command{ - objectPutCmd, - objectDelCmd, - objectGetCmd, - objectSearchCmd, - objectHeadCmd, - objectHashCmd, - objectRangeCmd, - cmdObjectLock, - } - - rootCmd.AddCommand(objectCmd) - objectCmd.AddCommand(objectChildCommands...) - - for _, objCommand := range objectChildCommands { - flags := objCommand.Flags() - - flags.String(bearerTokenFlag, "", "File with signed JSON or binary encoded bearer token") - commonflags.InitAPI(objCommand) - } - - // Here you will define your flags and configuration settings. - - // Cobra supports Persistent Flags which will work for this command - // and all subcommands, e.g.: - // objectCmd.PersistentFlags().String("foo", "", "A help for foo") - - // Cobra supports local flags which will only run when this command - // is called directly, e.g.: - // objectCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") - - initObjectPutCmd() - initObjectDeleteCmd() - initObjectGetCmd() - initObjectSearchCmd() - initObjectHeadCmd() - initObjectHashCmd() - initObjectRangeCmd() - initCommandObjectLock() - - for _, cmd := range []*cobra.Command{ - objectPutCmd, - objectDelCmd, - objectGetCmd, - objectSearchCmd, - objectHeadCmd, - objectRangeCmd, - cmdObjectLock, - } { - commonflags.InitSession(cmd) - } -} - -type clientKeySession interface { - clientWithKey - SetSessionToken(*session.Object) -} - -func prepareSessionPrm(cmd *cobra.Command, cnr cid.ID, obj *oid.ID, prms ...sessionCli.RPCParameters) { - pk := key.GetOrGenerate(cmd) - sessionCli.Prepare(cmd, cnr, obj, pk, prms...) -} - -type objectPrm interface { - SetBearerToken(prm *bearer.Token) - SetTTL(uint32) - SetXHeaders([]string) -} - -func prepareObjectPrm(cmd *cobra.Command, prms ...objectPrm) { - for i := range prms { - btok := common.ReadBearerToken(cmd, bearerTokenFlag) - - prms[i].SetBearerToken(btok) - prms[i].SetTTL(getTTL()) - prms[i].SetXHeaders(parseXHeaders(cmd)) - } -} - -func prepareObjectPrmRaw(cmd *cobra.Command, prm interface { - objectPrm - SetRawFlag(bool) -}) { - prepareObjectPrm(cmd, prm) - - raw, _ := cmd.Flags().GetBool(rawFlag) - prm.SetRawFlag(raw) -} - -func putObject(cmd *cobra.Command, _ []string) { - pk := key.GetOrGenerate(cmd) - - ownerID, err := getOwnerID(pk) - common.ExitOnErr(cmd, "", err) - - var cnr cid.ID - common.ExitOnErr(cmd, "", readCID(cmd, &cnr)) - - filename := cmd.Flag("file").Value.String() - f, err := os.OpenFile(filename, os.O_RDONLY, os.ModePerm) - if err != nil { - common.ExitOnErr(cmd, "", fmt.Errorf("can't open file '%s': %w", filename, err)) - } - - attrs, err := parseObjectAttrs(cmd) - common.ExitOnErr(cmd, "can't parse object attributes: %w", err) - - expiresOn, _ := cmd.Flags().GetUint64(putExpiresOnFlag) - if expiresOn > 0 { - var expAttrFound bool - expAttrValue := strconv.FormatUint(expiresOn, 10) - - for i := range attrs { - if attrs[i].Key() == objectV2.SysAttributeExpEpoch { - attrs[i].SetValue(expAttrValue) - expAttrFound = true - break - } - } - - if !expAttrFound { - index := len(attrs) - attrs = append(attrs, object.Attribute{}) - attrs[index].SetKey(objectV2.SysAttributeExpEpoch) - attrs[index].SetValue(expAttrValue) - } - } - - obj := object.New() - obj.SetContainerID(cnr) - obj.SetOwnerID(ownerID) - obj.SetAttributes(attrs...) - - notificationInfo, err := parseObjectNotifications(cmd) - common.ExitOnErr(cmd, "can't parse object notification information: %w", err) - - if notificationInfo != nil { - obj.SetNotification(*notificationInfo) - } - - var prm internalclient.PutObjectPrm - - sessionCli.Prepare(cmd, cnr, nil, pk, &prm) - prepareObjectPrm(cmd, &prm) - prm.SetHeader(obj) - - var p *pb.ProgressBar - - noProgress, _ := cmd.Flags().GetBool(noProgressFlag) - if noProgress { - prm.SetPayloadReader(f) - } else { - fi, err := f.Stat() - if err != nil { - cmd.PrintErrf("Failed to get file size, progress bar is disabled: %v\n", err) - prm.SetPayloadReader(f) - } else { - p = pb.New64(fi.Size()) - p.Output = cmd.OutOrStdout() - prm.SetPayloadReader(p.NewProxyReader(f)) - p.Start() - } - } - - res, err := internalclient.PutObject(prm) - common.ExitOnErr(cmd, "rpc error: %w", err) - - if p != nil { - p.Finish() - } - cmd.Printf("[%s] Object successfully stored\n", filename) - cmd.Printf(" ID: %s\n CID: %s\n", res.ID(), cnr) -} - -func deleteObject(cmd *cobra.Command, _ []string) { - var cnr cid.ID - var obj oid.ID - - err := readObjectAddress(cmd, &cnr, &obj) - common.ExitOnErr(cmd, "", err) - - var prm internalclient.DeleteObjectPrm - - prepareSessionPrm(cmd, cnr, &obj, &prm) - prepareObjectPrm(cmd, &prm) - - var addr oid.Address - addr.SetContainer(cnr) - addr.SetObject(obj) - - prm.SetAddress(addr) - - res, err := internalclient.DeleteObject(prm) - common.ExitOnErr(cmd, "rpc error: %w", err) - - tomb := res.Tombstone() - - cmd.Println("Object removed successfully.") - cmd.Printf(" ID: %s\n CID: %s\n", tomb, cnr) -} - -func getObject(cmd *cobra.Command, _ []string) { - var cnr cid.ID - var obj oid.ID - - err := readObjectAddress(cmd, &cnr, &obj) - common.ExitOnErr(cmd, "", err) - - var out io.Writer - filename := cmd.Flag("file").Value.String() - if filename == "" { - out = os.Stdout - } else { - f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, os.ModePerm) - if err != nil { - common.ExitOnErr(cmd, "", fmt.Errorf("can't open file '%s': %w", filename, err)) - } - - defer f.Close() - - out = f - } - - var prm internalclient.GetObjectPrm - - prepareSessionPrm(cmd, cnr, &obj, &prm) - prepareObjectPrmRaw(cmd, &prm) - - var objAddr oid.Address - objAddr.SetContainer(cnr) - objAddr.SetObject(obj) - - prm.SetAddress(objAddr) - - var p *pb.ProgressBar - noProgress, _ := cmd.Flags().GetBool(noProgressFlag) - - if filename == "" || noProgress { - prm.SetPayloadWriter(out) - } else { - p = pb.New64(0) - p.Output = cmd.OutOrStdout() - prm.SetPayloadWriter(p.NewProxyWriter(out)) - prm.SetHeaderCallback(func(o *object.Object) { - p.SetTotal64(int64(o.PayloadSize())) - p.Start() - }) - } - - res, err := internalclient.GetObject(prm) - if err != nil { - if ok := printSplitInfoErr(cmd, err); ok { - return - } - - common.ExitOnErr(cmd, "rpc error: %w", err) - } - - hdrFile := cmd.Flag("header").Value.String() - if filename != "" { - if p != nil { - p.Finish() - } - if hdrFile != "" || !strictOutput(cmd) { - cmd.Printf("[%s] Object successfully saved\n", filename) - } - } - - // Print header only if file is not streamed to stdout. - if filename != "" || hdrFile != "" { - err = saveAndPrintHeader(cmd, res.Header(), hdrFile) - common.ExitOnErr(cmd, "", err) - } -} - -func getObjectHeader(cmd *cobra.Command, _ []string) { - var cnr cid.ID - var obj oid.ID - - err := readObjectAddress(cmd, &cnr, &obj) - common.ExitOnErr(cmd, "", err) - - mainOnly, _ := cmd.Flags().GetBool("main-only") - - var prm internalclient.HeadObjectPrm - - prepareSessionPrm(cmd, cnr, &obj, &prm) - prepareObjectPrmRaw(cmd, &prm) - - var objAddr oid.Address - objAddr.SetContainer(cnr) - objAddr.SetObject(obj) - - prm.SetAddress(objAddr) - prm.SetMainOnlyFlag(mainOnly) - - res, err := internalclient.HeadObject(prm) - if err != nil { - if ok := printSplitInfoErr(cmd, err); ok { - return - } - - common.ExitOnErr(cmd, "rpc error: %w", err) - } - - err = saveAndPrintHeader(cmd, res.Header(), cmd.Flag("file").Value.String()) - common.ExitOnErr(cmd, "", err) -} - -func searchObject(cmd *cobra.Command, _ []string) { - var cnr cid.ID - - err := readCID(cmd, &cnr) - common.ExitOnErr(cmd, "", err) - - sf, err := parseSearchFilters(cmd) - common.ExitOnErr(cmd, "", err) - - var prm internalclient.SearchObjectsPrm - - prepareSessionPrm(cmd, cnr, nil, &prm) - prepareObjectPrm(cmd, &prm) - prm.SetContainerID(cnr) - prm.SetFilters(sf) - - res, err := internalclient.SearchObjects(prm) - common.ExitOnErr(cmd, "rpc error: %w", err) - - ids := res.IDList() - - cmd.Printf("Found %d objects.\n", len(ids)) - for i := range ids { - cmd.Println(ids[i]) - } -} - -func getObjectHash(cmd *cobra.Command, _ []string) { - var cnr cid.ID - var obj oid.ID - - err := readObjectAddress(cmd, &cnr, &obj) - common.ExitOnErr(cmd, "", err) - ranges, err := getRangeList(cmd) - common.ExitOnErr(cmd, "", err) - typ, err := getHashType(cmd) - common.ExitOnErr(cmd, "", err) - - strSalt := strings.TrimPrefix(cmd.Flag(getRangeHashSaltFlag).Value.String(), "0x") - - salt, err := hex.DecodeString(strSalt) - common.ExitOnErr(cmd, "could not decode salt: %w", err) - - var ( - hashPrm internalclient.HashPayloadRangesPrm - headPrm internalclient.HeadObjectPrm - - sesPrms = []sessionCli.RPCParameters{&hashPrm} - objPrms = []objectPrm{&hashPrm} - ) - - fullHash := len(ranges) == 0 - if fullHash { - sesPrms = append(sesPrms, &headPrm) - objPrms = append(objPrms, &headPrm) - } - - prepareSessionPrm(cmd, cnr, &obj, sesPrms...) - prepareObjectPrm(cmd, objPrms...) - - var objAddr oid.Address - objAddr.SetContainer(cnr) - objAddr.SetObject(obj) - - tz := typ == hashTz - - if fullHash { - headPrm.SetAddress(objAddr) - - // get hash of full payload through HEAD (may be user can do it through dedicated command?) - res, err := internalclient.HeadObject(headPrm) - common.ExitOnErr(cmd, "rpc error: %w", err) - - var cs checksum.Checksum - var csSet bool - - if tz { - cs, csSet = res.Header().PayloadHomomorphicHash() - } else { - cs, csSet = res.Header().PayloadChecksum() - } - - if csSet { - cmd.Println(hex.EncodeToString(cs.Value())) - } else { - cmd.Println("Missing checksum in object header.") - } - - return - } - - hashPrm.SetAddress(objAddr) - hashPrm.SetSalt(salt) - hashPrm.SetRanges(ranges) - - if tz { - hashPrm.TZ() - } - - res, err := internalclient.HashPayloadRanges(hashPrm) - common.ExitOnErr(cmd, "rpc error: %w", err) - - hs := res.HashList() - - for i := range hs { - cmd.Printf("Offset=%d (Length=%d)\t: %s\n", ranges[i].GetOffset(), ranges[i].GetLength(), - hex.EncodeToString(hs[i])) - } -} - -func getOwnerID(key *ecdsa.PrivateKey) (*user.ID, error) { - var res user.ID - user.IDFromKey(&res, key.PublicKey) - - return &res, nil -} - -var searchUnaryOpVocabulary = map[string]object.SearchMatchType{ - "NOPRESENT": object.MatchNotPresent, -} - -var searchBinaryOpVocabulary = map[string]object.SearchMatchType{ - "EQ": object.MatchStringEqual, - "NE": object.MatchStringNotEqual, - "COMMON_PREFIX": object.MatchCommonPrefix, -} - -func parseSearchFilters(cmd *cobra.Command) (object.SearchFilters, error) { - var fs object.SearchFilters - - for i := range searchFilters { - words := strings.Fields(searchFilters[i]) - - switch len(words) { - default: - return nil, fmt.Errorf("invalid field number: %d", len(words)) - case 1: - data, err := os.ReadFile(words[0]) - if err != nil { - return nil, fmt.Errorf("could not read attributes filter from file: %w", err) - } - - subFs := object.NewSearchFilters() - - if err := subFs.UnmarshalJSON(data); err != nil { - return nil, fmt.Errorf("could not unmarshal attributes filter from file: %w", err) - } - - fs = append(fs, subFs...) - case 2: - m, ok := searchUnaryOpVocabulary[words[1]] - if !ok { - return nil, fmt.Errorf("unsupported unary op: %s", words[1]) - } - - fs.AddFilter(words[0], "", m) - case 3: - m, ok := searchBinaryOpVocabulary[words[1]] - if !ok { - return nil, fmt.Errorf("unsupported binary op: %s", words[1]) - } - - fs.AddFilter(words[0], words[2], m) - } - } - - root, _ := cmd.Flags().GetBool("root") - if root { - fs.AddRootFilter() - } - - phy, _ := cmd.Flags().GetBool("phy") - if phy { - fs.AddPhyFilter() - } - - strObj, _ := cmd.Flags().GetString(searchOIDFlag) - if strObj != "" { - var id oid.ID - if err := id.DecodeString(strObj); err != nil { - return nil, fmt.Errorf("could not parse object ID: %w", err) - } - - fs.AddObjectIDFilter(object.MatchStringEqual, id) - } - - return fs, nil -} - -func parseObjectAttrs(cmd *cobra.Command) ([]object.Attribute, error) { - var rawAttrs []string - - raw := cmd.Flag("attributes").Value.String() - if len(raw) != 0 { - rawAttrs = strings.Split(raw, ",") - } - - attrs := make([]object.Attribute, len(rawAttrs), len(rawAttrs)+2) // name + timestamp attributes - for i := range rawAttrs { - kv := strings.SplitN(rawAttrs[i], "=", 2) - if len(kv) != 2 { - return nil, fmt.Errorf("invalid attribute format: %s", rawAttrs[i]) - } - attrs[i].SetKey(kv[0]) - attrs[i].SetValue(kv[1]) - } - - disableFilename, _ := cmd.Flags().GetBool("disable-filename") - if !disableFilename { - filename := filepath.Base(cmd.Flag("file").Value.String()) - index := len(attrs) - attrs = append(attrs, object.Attribute{}) - attrs[index].SetKey(object.AttributeFileName) - attrs[index].SetValue(filename) - } - - disableTime, _ := cmd.Flags().GetBool("disable-timestamp") - if !disableTime { - index := len(attrs) - attrs = append(attrs, object.Attribute{}) - attrs[index].SetKey(object.AttributeTimestamp) - attrs[index].SetValue(strconv.FormatInt(time.Now().Unix(), 10)) - } - - return attrs, nil -} - -func parseObjectNotifications(cmd *cobra.Command) (*object.NotificationInfo, error) { - const ( - separator = ":" - useDefaultTopic = "-" - ) - - raw := cmd.Flag(notificationFlag).Value.String() - if raw == "" { - return nil, nil - } - - rawSlice := strings.SplitN(raw, separator, 2) - if len(rawSlice) != 2 { - return nil, fmt.Errorf("notification must be in the form of: *epoch*%s*topic*, got %s", separator, raw) - } - - ni := new(object.NotificationInfo) - - epoch, err := strconv.ParseUint(rawSlice[0], 10, 64) - if err != nil { - return nil, fmt.Errorf("could not parse notification epoch %s: %w", rawSlice[0], err) - } - - ni.SetEpoch(epoch) - - if rawSlice[1] == "" { - return nil, fmt.Errorf("incorrect empty topic: use %s to force using default topic", useDefaultTopic) - } - - if rawSlice[1] != useDefaultTopic { - ni.SetTopic(rawSlice[1]) - } - - return ni, nil -} - -func readCID(cmd *cobra.Command, cnr *cid.ID) error { - err := cnr.DecodeString(cmd.Flag("cid").Value.String()) - if err != nil { - return fmt.Errorf("decode container ID string: %w", err) - } - - return nil -} - -func readOID(cmd *cobra.Command, obj *oid.ID) error { - err := obj.DecodeString(cmd.Flag("oid").Value.String()) - if err != nil { - return fmt.Errorf("decode container ID string: %w", err) - } - - return nil -} - -func readObjectAddress(cmd *cobra.Command, cnr *cid.ID, obj *oid.ID) error { - err := readCID(cmd, cnr) - if err != nil { - return err - } - - err = readOID(cmd, obj) - if err != nil { - return err - } - - return nil -} - -func getRangeList(cmd *cobra.Command) ([]*object.Range, error) { - v := cmd.Flag("range").Value.String() - if len(v) == 0 { - return nil, nil - } - vs := strings.Split(v, ",") - rs := make([]*object.Range, len(vs)) - for i := range vs { - r := strings.Split(vs[i], rangeSep) - if len(r) != 2 { - return nil, fmt.Errorf("invalid range specifier: %s", vs[i]) - } - - offset, err := strconv.ParseUint(r[0], 10, 64) - if err != nil { - return nil, fmt.Errorf("invalid range specifier: %s", vs[i]) - } - length, err := strconv.ParseUint(r[1], 10, 64) - if err != nil { - return nil, fmt.Errorf("invalid range specifier: %s", vs[i]) - } - rs[i] = object.NewRange() - rs[i].SetOffset(offset) - rs[i].SetLength(length) - } - return rs, nil -} - -func getHashType(cmd *cobra.Command) (string, error) { - rawType := cmd.Flag("type").Value.String() - switch typ := strings.ToLower(rawType); typ { - case hashSha256, hashTz: - return typ, nil - default: - return "", fmt.Errorf("invalid hash type: %s", typ) - } -} - -func saveAndPrintHeader(cmd *cobra.Command, obj *object.Object, filename string) error { - bs, err := marshalHeader(cmd, obj) - if err != nil { - return fmt.Errorf("could not marshal header: %w", err) - } - if len(bs) != 0 { - if filename == "" { - cmd.Println(string(bs)) - return nil - } - err = os.WriteFile(filename, bs, os.ModePerm) - if err != nil { - return fmt.Errorf("could not write header to file: %w", err) - } - cmd.Printf("[%s] Header successfully saved.", filename) - } - - return printHeader(cmd, obj) -} - -func printChecksum(cmd *cobra.Command, name string, recv func() (checksum.Checksum, bool)) { - var strVal string - - cs, csSet := recv() - if csSet { - strVal = hex.EncodeToString(cs.Value()) - } else { - strVal = "" - } - - cmd.Printf("%s: %s\n", name, strVal) -} - -func printObjectID(cmd *cobra.Command, recv func() (oid.ID, bool)) { - var strID string - - id, ok := recv() - if ok { - strID = id.String() - } else { - strID = "" - } - - cmd.Printf("ID: %s\n", strID) -} - -func printContainerID(cmd *cobra.Command, recv func() (cid.ID, bool)) { - var strID string - - id, ok := recv() - if ok { - strID = id.String() - } else { - strID = "" - } - - cmd.Printf("CID: %s\n", strID) -} - -func printHeader(cmd *cobra.Command, obj *object.Object) error { - printObjectID(cmd, obj.ID) - printContainerID(cmd, obj.ContainerID) - cmd.Printf("Owner: %s\n", obj.OwnerID()) - cmd.Printf("CreatedAt: %d\n", obj.CreationEpoch()) - cmd.Printf("Size: %d\n", obj.PayloadSize()) - printChecksum(cmd, "HomoHash", obj.PayloadHomomorphicHash) - printChecksum(cmd, "Checksum", obj.PayloadChecksum) - cmd.Printf("Type: %s\n", obj.Type()) - - cmd.Println("Attributes:") - for _, attr := range obj.Attributes() { - if attr.Key() == object.AttributeTimestamp { - cmd.Printf(" %s=%s (%s)\n", - attr.Key(), - attr.Value(), - common.PrettyPrintUnixTime(attr.Value())) - continue - } - cmd.Printf(" %s=%s\n", attr.Key(), attr.Value()) - } - - return printSplitHeader(cmd, obj) -} - -func printSplitHeader(cmd *cobra.Command, obj *object.Object) error { - if splitID := obj.SplitID(); splitID != nil { - cmd.Printf("Split ID: %s\n", splitID) - } - - if par, ok := obj.ParentID(); ok { - cmd.Printf("Split ParentID: %s\n", par) - } - - if prev, ok := obj.PreviousID(); ok { - cmd.Printf("Split PreviousID: %s\n", prev) - } - - for _, child := range obj.Children() { - cmd.Printf("Split ChildID: %s\n", child) - } - - if signature := obj.Signature(); signature != nil { - cmd.Print("Split Header Signature:\n") - - // TODO(@cthulhu-rider): #1387 implement and use another approach to avoid conversion - var sigV2 refs.Signature - signature.WriteToV2(&sigV2) - - cmd.Printf(" public key: %s\n", hex.EncodeToString(sigV2.GetKey())) - cmd.Printf(" signature: %s\n", hex.EncodeToString(sigV2.GetSign())) - } - - parent := obj.Parent() - if parent != nil { - cmd.Print("\nSplit Parent Header:\n") - - return printHeader(cmd, parent) - } - - return nil -} - -func strictOutput(cmd *cobra.Command) bool { - toJSON, _ := cmd.Flags().GetBool("json") - toProto, _ := cmd.Flags().GetBool("proto") - return toJSON || toProto -} - -func marshalHeader(cmd *cobra.Command, hdr *object.Object) ([]byte, error) { - toJSON, _ := cmd.Flags().GetBool("json") - toProto, _ := cmd.Flags().GetBool("proto") - switch { - case toJSON && toProto: - return nil, errors.New("'--json' and '--proto' flags are mutually exclusive") - case toJSON: - return hdr.MarshalJSON() - case toProto: - return hdr.Marshal() - default: - return nil, nil - } -} - -func getObjectRange(cmd *cobra.Command, _ []string) { - var cnr cid.ID - var obj oid.ID - - common.ExitOnErr(cmd, "", readObjectAddress(cmd, &cnr, &obj)) - - ranges, err := getRangeList(cmd) - common.ExitOnErr(cmd, "", err) - - if len(ranges) != 1 { - common.ExitOnErr(cmd, "", fmt.Errorf("exactly one range must be specified, got: %d", len(ranges))) - } - - var out io.Writer - - filename := cmd.Flag("file").Value.String() - if filename == "" { - out = os.Stdout - } else { - f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, os.ModePerm) - if err != nil { - common.ExitOnErr(cmd, "", fmt.Errorf("can't open file '%s': %w", filename, err)) - } - - defer f.Close() - - out = f - } - - var prm internalclient.PayloadRangePrm - - prepareSessionPrm(cmd, cnr, &obj, &prm) - prepareObjectPrmRaw(cmd, &prm) - - var addr oid.Address - addr.SetContainer(cnr) - addr.SetObject(obj) - - prm.SetAddress(addr) - prm.SetRange(ranges[0]) - prm.SetPayloadWriter(out) - - _, err = internalclient.PayloadRange(prm) - if err != nil { - if ok := printSplitInfoErr(cmd, err); ok { - return - } - - common.ExitOnErr(cmd, "can't get object payload range: %w", err) - } - - if filename != "" { - cmd.Printf("[%s] Payload successfully saved\n", filename) - } -} - -func printSplitInfoErr(cmd *cobra.Command, err error) bool { - var errSplitInfo *object.SplitInfoError - - ok := errors.As(err, &errSplitInfo) - - if ok { - cmd.PrintErrln("Object is complex, split information received.") - printSplitInfo(cmd, errSplitInfo.SplitInfo()) - } - - return ok -} - -func printSplitInfo(cmd *cobra.Command, info *object.SplitInfo) { - bs, err := marshalSplitInfo(cmd, info) - common.ExitOnErr(cmd, "can't marshal split info: %w", err) - - cmd.Println(string(bs)) -} - -func marshalSplitInfo(cmd *cobra.Command, info *object.SplitInfo) ([]byte, error) { - toJSON, _ := cmd.Flags().GetBool("json") - toProto, _ := cmd.Flags().GetBool("proto") - switch { - case toJSON && toProto: - return nil, errors.New("'--json' and '--proto' flags are mutually exclusive") - case toJSON: - return info.MarshalJSON() - case toProto: - return info.Marshal() - default: - b := bytes.NewBuffer(nil) - if splitID := info.SplitID(); splitID != nil { - b.WriteString("Split ID: " + splitID.String() + "\n") - } - if link, ok := info.Link(); ok { - b.WriteString(fmt.Sprintf("Linking object: %s\n", link)) - } - if last, ok := info.LastPart(); ok { - b.WriteString(fmt.Sprintf("Last object: %s\n", last)) - } - return b.Bytes(), nil - } -} diff --git a/cmd/neofs-cli/modules/object/delete.go b/cmd/neofs-cli/modules/object/delete.go new file mode 100644 index 000000000..a21c7d2ee --- /dev/null +++ b/cmd/neofs-cli/modules/object/delete.go @@ -0,0 +1,54 @@ +package object + +import ( + 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" + sessionCli "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/session" + cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + oid "github.com/nspcc-dev/neofs-sdk-go/object/id" + "github.com/spf13/cobra" +) + +var objectDelCmd = &cobra.Command{ + Use: "delete", + Aliases: []string{"del"}, + Short: "Delete object from NeoFS", + Long: "Delete object from NeoFS", + Run: deleteObject, +} + +func initObjectDeleteCmd() { + commonflags.Init(objectDelCmd) + commonflags.InitSession(objectDelCmd) + + flags := objectDelCmd.Flags() + + flags.String("cid", "", "Container ID") + _ = objectDelCmd.MarkFlagRequired("cid") + + flags.String("oid", "", "Object ID") + _ = objectDelCmd.MarkFlagRequired("oid") +} + +func deleteObject(cmd *cobra.Command, _ []string) { + var cnr cid.ID + var obj oid.ID + + objAddr := readObjectAddress(cmd, &cnr, &obj) + pk := key.GetOrGenerate(cmd) + + var prm internalclient.DeleteObjectPrm + sessionCli.Prepare(cmd, cnr, &obj, pk, &prm) + Prepare(cmd, &prm) + prm.SetAddress(objAddr) + + res, err := internalclient.DeleteObject(prm) + common.ExitOnErr(cmd, "rpc error: %w", err) + + tomb := res.Tombstone() + + cmd.Println("Object removed successfully.") + cmd.Printf(" ID: %s\n CID: %s\n", tomb, cnr) +} diff --git a/cmd/neofs-cli/modules/object/get.go b/cmd/neofs-cli/modules/object/get.go new file mode 100644 index 000000000..bc8a6b3f9 --- /dev/null +++ b/cmd/neofs-cli/modules/object/get.go @@ -0,0 +1,121 @@ +package object + +import ( + "fmt" + "io" + "os" + + "github.com/cheggaaa/pb" + 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" + sessionCli "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/session" + cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + "github.com/nspcc-dev/neofs-sdk-go/object" + oid "github.com/nspcc-dev/neofs-sdk-go/object/id" + "github.com/spf13/cobra" +) + +var objectGetCmd = &cobra.Command{ + Use: "get", + Short: "Get object from NeoFS", + Long: "Get object from NeoFS", + Run: getObject, +} + +func initObjectGetCmd() { + commonflags.Init(objectGetCmd) + commonflags.InitSession(objectGetCmd) + + flags := objectGetCmd.Flags() + + flags.String("cid", "", "Container ID") + _ = objectGetCmd.MarkFlagRequired("cid") + + flags.String("oid", "", "Object ID") + _ = objectGetCmd.MarkFlagRequired("oid") + + flags.String("file", "", "File to write object payload to. Default: stdout.") + flags.String("header", "", "File to write header to. Default: stdout.") + flags.Bool(rawFlag, false, rawFlagDesc) + flags.Bool(noProgressFlag, false, "Do not show progress bar") +} + +func getObject(cmd *cobra.Command, _ []string) { + var cnr cid.ID + var obj oid.ID + + objAddr := readObjectAddress(cmd, &cnr, &obj) + + var out io.Writer + filename := cmd.Flag("file").Value.String() + if filename == "" { + out = os.Stdout + } else { + f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, os.ModePerm) + if err != nil { + common.ExitOnErr(cmd, "", fmt.Errorf("can't open file '%s': %w", filename, err)) + } + + defer f.Close() + + out = f + } + + pk := key.GetOrGenerate(cmd) + + var prm internalclient.GetObjectPrm + sessionCli.Prepare(cmd, cnr, &obj, pk, &prm) + Prepare(cmd, &prm) + + raw, _ := cmd.Flags().GetBool(rawFlag) + prm.SetRawFlag(raw) + prm.SetAddress(objAddr) + + var p *pb.ProgressBar + noProgress, _ := cmd.Flags().GetBool(noProgressFlag) + + if filename == "" || noProgress { + prm.SetPayloadWriter(out) + } else { + p = pb.New64(0) + p.Output = cmd.OutOrStdout() + prm.SetPayloadWriter(p.NewProxyWriter(out)) + prm.SetHeaderCallback(func(o *object.Object) { + p.SetTotal64(int64(o.PayloadSize())) + p.Start() + }) + } + + res, err := internalclient.GetObject(prm) + if err != nil { + if ok := printSplitInfoErr(cmd, err); ok { + return + } + + common.ExitOnErr(cmd, "rpc error: %w", err) + } + + hdrFile := cmd.Flag("header").Value.String() + if filename != "" { + if p != nil { + p.Finish() + } + if hdrFile != "" || !strictOutput(cmd) { + cmd.Printf("[%s] Object successfully saved\n", filename) + } + } + + // Print header only if file is not streamed to stdout. + if filename != "" || hdrFile != "" { + err = saveAndPrintHeader(cmd, res.Header(), hdrFile) + common.ExitOnErr(cmd, "", err) + } +} + +func strictOutput(cmd *cobra.Command) bool { + toJSON, _ := cmd.Flags().GetBool("json") + toProto, _ := cmd.Flags().GetBool("proto") + return toJSON || toProto +} diff --git a/cmd/neofs-cli/modules/object/hash.go b/cmd/neofs-cli/modules/object/hash.go new file mode 100644 index 000000000..8a1f19edd --- /dev/null +++ b/cmd/neofs-cli/modules/object/hash.go @@ -0,0 +1,128 @@ +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" + sessionCli "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/session" + "github.com/nspcc-dev/neofs-sdk-go/checksum" + cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + oid "github.com/nspcc-dev/neofs-sdk-go/object/id" + "github.com/spf13/cobra" +) + +const getRangeHashSaltFlag = "salt" + +const ( + hashSha256 = "sha256" + hashTz = "tz" + rangeSep = ":" +) + +var objectHashCmd = &cobra.Command{ + Use: "hash", + Short: "Get object hash", + Long: "Get object hash", + Run: getObjectHash, +} + +func initObjectHashCmd() { + commonflags.Init(objectHashCmd) + + flags := objectHashCmd.Flags() + + flags.String("cid", "", "Container ID") + _ = objectHashCmd.MarkFlagRequired("cid") + + flags.String("oid", "", "Object ID") + _ = objectHashCmd.MarkFlagRequired("oid") + + flags.String("range", "", "Range to take hash from in the form offset1:length1,...") + flags.String("type", hashSha256, "Hash type. Either 'sha256' or 'tz'") + flags.String(getRangeHashSaltFlag, "", "Salt in hex format") +} + +func getObjectHash(cmd *cobra.Command, _ []string) { + var cnr cid.ID + var obj oid.ID + + objAddr := readObjectAddress(cmd, &cnr, &obj) + + ranges, err := getRangeList(cmd) + common.ExitOnErr(cmd, "", err) + typ, err := getHashType(cmd) + common.ExitOnErr(cmd, "", err) + + strSalt := strings.TrimPrefix(cmd.Flag(getRangeHashSaltFlag).Value.String(), "0x") + + salt, err := hex.DecodeString(strSalt) + common.ExitOnErr(cmd, "could not decode salt: %w", err) + + pk := key.GetOrGenerate(cmd) + + tz := typ == hashTz + fullHash := len(ranges) == 0 + if fullHash { + var headPrm internalclient.HeadObjectPrm + sessionCli.Prepare(cmd, cnr, &obj, pk, &headPrm) + Prepare(cmd, &headPrm) + headPrm.SetAddress(objAddr) + + // get hash of full payload through HEAD (may be user can do it through dedicated command?) + res, err := internalclient.HeadObject(headPrm) + common.ExitOnErr(cmd, "rpc error: %w", err) + + var cs checksum.Checksum + var csSet bool + + if tz { + cs, csSet = res.Header().PayloadHomomorphicHash() + } else { + cs, csSet = res.Header().PayloadChecksum() + } + + if csSet { + cmd.Println(hex.EncodeToString(cs.Value())) + } else { + cmd.Println("Missing checksum in object header.") + } + + return + } + + var hashPrm internalclient.HashPayloadRangesPrm + sessionCli.Prepare(cmd, cnr, &obj, pk, &hashPrm) + Prepare(cmd, &hashPrm) + hashPrm.SetAddress(objAddr) + hashPrm.SetSalt(salt) + hashPrm.SetRanges(ranges) + + if tz { + hashPrm.TZ() + } + + res, err := internalclient.HashPayloadRanges(hashPrm) + common.ExitOnErr(cmd, "rpc error: %w", err) + + hs := res.HashList() + + for i := range hs { + cmd.Printf("Offset=%d (Length=%d)\t: %s\n", ranges[i].GetOffset(), ranges[i].GetLength(), + hex.EncodeToString(hs[i])) + } +} + +func getHashType(cmd *cobra.Command) (string, error) { + rawType := cmd.Flag("type").Value.String() + switch typ := strings.ToLower(rawType); typ { + case hashSha256, hashTz: + return typ, nil + default: + return "", fmt.Errorf("invalid hash type: %s", typ) + } +} diff --git a/cmd/neofs-cli/modules/object/head.go b/cmd/neofs-cli/modules/object/head.go new file mode 100644 index 000000000..fc50fab0f --- /dev/null +++ b/cmd/neofs-cli/modules/object/head.go @@ -0,0 +1,200 @@ +package object + +import ( + "encoding/hex" + "errors" + "fmt" + "os" + + "github.com/nspcc-dev/neofs-api-go/v2/refs" + 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" + sessionCli "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/session" + cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + "github.com/nspcc-dev/neofs-sdk-go/object" + oid "github.com/nspcc-dev/neofs-sdk-go/object/id" + oidSDK "github.com/nspcc-dev/neofs-sdk-go/object/id" + "github.com/spf13/cobra" +) + +var objectHeadCmd = &cobra.Command{ + Use: "head", + Short: "Get object header", + Long: "Get object header", + Run: getObjectHeader, +} + +func initObjectHeadCmd() { + commonflags.Init(objectHeadCmd) + commonflags.InitSession(objectHeadCmd) + + flags := objectHeadCmd.Flags() + + flags.String("cid", "", "Container ID") + _ = objectHeadCmd.MarkFlagRequired("cid") + + flags.String("oid", "", "Object ID") + _ = objectHeadCmd.MarkFlagRequired("oid") + + flags.String("file", "", "File to write header to. Default: stdout.") + flags.Bool("main-only", false, "Return only main fields") + flags.Bool("json", false, "Marshal output in JSON") + flags.Bool("proto", false, "Marshal output in Protobuf") + flags.Bool(rawFlag, false, rawFlagDesc) +} + +func getObjectHeader(cmd *cobra.Command, _ []string) { + var cnr cid.ID + var obj oid.ID + + objAddr := readObjectAddress(cmd, &cnr, &obj) + mainOnly, _ := cmd.Flags().GetBool("main-only") + pk := key.GetOrGenerate(cmd) + + var prm internalclient.HeadObjectPrm + sessionCli.Prepare(cmd, cnr, &obj, pk, &prm) + Prepare(cmd, &prm) + + raw, _ := cmd.Flags().GetBool(rawFlag) + prm.SetRawFlag(raw) + prm.SetAddress(objAddr) + prm.SetMainOnlyFlag(mainOnly) + + res, err := internalclient.HeadObject(prm) + if err != nil { + if ok := printSplitInfoErr(cmd, err); ok { + return + } + + common.ExitOnErr(cmd, "rpc error: %w", err) + } + + err = saveAndPrintHeader(cmd, res.Header(), cmd.Flag("file").Value.String()) + common.ExitOnErr(cmd, "", err) +} + +func saveAndPrintHeader(cmd *cobra.Command, obj *object.Object, filename string) error { + bs, err := marshalHeader(cmd, obj) + if err != nil { + return fmt.Errorf("could not marshal header: %w", err) + } + if len(bs) != 0 { + if filename == "" { + cmd.Println(string(bs)) + return nil + } + err = os.WriteFile(filename, bs, os.ModePerm) + if err != nil { + return fmt.Errorf("could not write header to file: %w", err) + } + cmd.Printf("[%s] Header successfully saved.", filename) + } + + return printHeader(cmd, obj) +} + +func marshalHeader(cmd *cobra.Command, hdr *object.Object) ([]byte, error) { + toJSON, _ := cmd.Flags().GetBool("json") + toProto, _ := cmd.Flags().GetBool("proto") + switch { + case toJSON && toProto: + return nil, errors.New("'--json' and '--proto' flags are mutually exclusive") + case toJSON: + return hdr.MarshalJSON() + case toProto: + return hdr.Marshal() + default: + return nil, nil + } +} + +func printObjectID(cmd *cobra.Command, recv func() (oidSDK.ID, bool)) { + var strID string + + id, ok := recv() + if ok { + strID = id.String() + } else { + strID = "" + } + + cmd.Printf("ID: %s\n", strID) +} + +func printContainerID(cmd *cobra.Command, recv func() (cid.ID, bool)) { + var strID string + + id, ok := recv() + if ok { + strID = id.String() + } else { + strID = "" + } + + cmd.Printf("CID: %s\n", strID) +} + +func printHeader(cmd *cobra.Command, obj *object.Object) error { + printObjectID(cmd, obj.ID) + printContainerID(cmd, obj.ContainerID) + cmd.Printf("Owner: %s\n", obj.OwnerID()) + cmd.Printf("CreatedAt: %d\n", obj.CreationEpoch()) + cmd.Printf("Size: %d\n", obj.PayloadSize()) + common.PrintChecksum(cmd, "HomoHash", obj.PayloadHomomorphicHash) + common.PrintChecksum(cmd, "Checksum", obj.PayloadChecksum) + cmd.Printf("Type: %s\n", obj.Type()) + + cmd.Println("Attributes:") + for _, attr := range obj.Attributes() { + if attr.Key() == object.AttributeTimestamp { + cmd.Printf(" %s=%s (%s)\n", + attr.Key(), + attr.Value(), + common.PrettyPrintUnixTime(attr.Value())) + continue + } + cmd.Printf(" %s=%s\n", attr.Key(), attr.Value()) + } + + return printSplitHeader(cmd, obj) +} + +func printSplitHeader(cmd *cobra.Command, obj *object.Object) error { + if splitID := obj.SplitID(); splitID != nil { + cmd.Printf("Split ID: %s\n", splitID) + } + + if oid, ok := obj.ParentID(); ok { + cmd.Printf("Split ParentID: %s\n", oid) + } + + if prev, ok := obj.PreviousID(); ok { + cmd.Printf("Split PreviousID: %s\n", prev) + } + + for _, child := range obj.Children() { + cmd.Printf("Split ChildID: %s\n", child.String()) + } + + if signature := obj.Signature(); signature != nil { + cmd.Print("Split Header Signature:\n") + + // TODO(@cthulhu-rider): #1387 implement and use another approach to avoid conversion + var sigV2 refs.Signature + signature.WriteToV2(&sigV2) + + cmd.Printf(" public key: %s\n", hex.EncodeToString(sigV2.GetKey())) + cmd.Printf(" signature: %s\n", hex.EncodeToString(sigV2.GetSign())) + } + + parent := obj.Parent() + if parent != nil { + cmd.Print("\nSplit Parent Header:\n") + + return printHeader(cmd, parent) + } + + return nil +} diff --git a/cmd/neofs-cli/modules/lock.go b/cmd/neofs-cli/modules/object/lock.go similarity index 85% rename from cmd/neofs-cli/modules/lock.go rename to cmd/neofs-cli/modules/object/lock.go index 220b5c293..40ec98376 100644 --- a/cmd/neofs-cli/modules/lock.go +++ b/cmd/neofs-cli/modules/object/lock.go @@ -1,4 +1,4 @@ -package cmd +package object import ( "fmt" @@ -11,11 +11,12 @@ import ( cid "github.com/nspcc-dev/neofs-sdk-go/container/id" "github.com/nspcc-dev/neofs-sdk-go/object" oid "github.com/nspcc-dev/neofs-sdk-go/object/id" + "github.com/nspcc-dev/neofs-sdk-go/user" "github.com/spf13/cobra" ) // object lock command. -var cmdObjectLock = &cobra.Command{ +var objectLockCmd = &cobra.Command{ Use: "lock CONTAINER OBJECT...", Short: "Lock object in container", Long: "Lock object in container", @@ -37,22 +38,22 @@ var cmdObjectLock = &cobra.Command{ key := key.GetOrGenerate(cmd) - idOwner, err := getOwnerID(key) - common.ExitOnErr(cmd, "", err) + var idOwner user.ID + user.IDFromKey(&idOwner, key.PublicKey) var lock object.Lock lock.WriteMembers(lockList) obj := object.New() obj.SetContainerID(cnr) - obj.SetOwnerID(idOwner) + obj.SetOwnerID(&idOwner) obj.SetType(object.TypeLock) obj.SetPayload(lock.Marshal()) var prm internalclient.PutObjectPrm sessionCli.Prepare(cmd, cnr, nil, key, &prm) - prepareObjectPrm(cmd, &prm) + Prepare(cmd, &prm) prm.SetHeader(obj) _, err = internalclient.PutObject(prm) @@ -63,5 +64,6 @@ var cmdObjectLock = &cobra.Command{ } func initCommandObjectLock() { - commonflags.Init(cmdObjectLock) + commonflags.Init(objectLockCmd) + commonflags.InitSession(objectLockCmd) } diff --git a/cmd/neofs-cli/modules/object/put.go b/cmd/neofs-cli/modules/object/put.go new file mode 100644 index 000000000..9022080ed --- /dev/null +++ b/cmd/neofs-cli/modules/object/put.go @@ -0,0 +1,217 @@ +package object + +import ( + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/cheggaaa/pb" + objectV2 "github.com/nspcc-dev/neofs-api-go/v2/object" + 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" + sessionCli "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/session" + cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + "github.com/nspcc-dev/neofs-sdk-go/object" + "github.com/nspcc-dev/neofs-sdk-go/user" + "github.com/spf13/cobra" +) + +const ( + putExpiresOnFlag = "expires-on" + noProgressFlag = "no-progress" + notificationFlag = "notify" +) + +var putExpiredOn uint64 + +var objectPutCmd = &cobra.Command{ + Use: "put", + Short: "Put object to NeoFS", + Long: "Put object to NeoFS", + Run: putObject, +} + +func initObjectPutCmd() { + commonflags.Init(objectPutCmd) + commonflags.InitSession(objectPutCmd) + + flags := objectPutCmd.Flags() + + flags.String("file", "", "File with object payload") + _ = objectPutCmd.MarkFlagFilename("file") + _ = objectPutCmd.MarkFlagRequired("file") + + flags.String("cid", "", "Container ID") + _ = objectPutCmd.MarkFlagRequired("cid") + + flags.String("attributes", "", "User attributes in form of Key1=Value1,Key2=Value2") + flags.Bool("disable-filename", false, "Do not set well-known filename attribute") + flags.Bool("disable-timestamp", false, "Do not set well-known timestamp attribute") + flags.Uint64VarP(&putExpiredOn, putExpiresOnFlag, "e", 0, "Last epoch in the life of the object") + flags.Bool(noProgressFlag, false, "Do not show progress bar") + + flags.String(notificationFlag, "", "Object notification in the form of *epoch*:*topic*; '-' topic means using default") +} + +func putObject(cmd *cobra.Command, _ []string) { + pk := key.GetOrGenerate(cmd) + + var ownerID user.ID + user.IDFromKey(&ownerID, pk.PublicKey) + + var cnr cid.ID + readCID(cmd, &cnr) + + filename := cmd.Flag("file").Value.String() + f, err := os.OpenFile(filename, os.O_RDONLY, os.ModePerm) + if err != nil { + common.ExitOnErr(cmd, "", fmt.Errorf("can't open file '%s': %w", filename, err)) + } + + attrs, err := parseObjectAttrs(cmd) + common.ExitOnErr(cmd, "can't parse object attributes: %w", err) + + expiresOn, _ := cmd.Flags().GetUint64(putExpiresOnFlag) + if expiresOn > 0 { + var expAttrFound bool + expAttrValue := strconv.FormatUint(expiresOn, 10) + + for i := range attrs { + if attrs[i].Key() == objectV2.SysAttributeExpEpoch { + attrs[i].SetValue(expAttrValue) + expAttrFound = true + break + } + } + + if !expAttrFound { + index := len(attrs) + attrs = append(attrs, object.Attribute{}) + attrs[index].SetKey(objectV2.SysAttributeExpEpoch) + attrs[index].SetValue(expAttrValue) + } + } + + obj := object.New() + obj.SetContainerID(cnr) + obj.SetOwnerID(&ownerID) + obj.SetAttributes(attrs...) + + notificationInfo, err := parseObjectNotifications(cmd) + common.ExitOnErr(cmd, "can't parse object notification information: %w", err) + + if notificationInfo != nil { + obj.SetNotification(*notificationInfo) + } + + var prm internalclient.PutObjectPrm + sessionCli.Prepare(cmd, cnr, nil, pk, &prm) + Prepare(cmd, &prm) + prm.SetHeader(obj) + + var p *pb.ProgressBar + + noProgress, _ := cmd.Flags().GetBool(noProgressFlag) + if noProgress { + prm.SetPayloadReader(f) + } else { + fi, err := f.Stat() + if err != nil { + cmd.PrintErrf("Failed to get file size, progress bar is disabled: %v\n", err) + prm.SetPayloadReader(f) + } else { + p = pb.New64(fi.Size()) + p.Output = cmd.OutOrStdout() + prm.SetPayloadReader(p.NewProxyReader(f)) + p.Start() + } + } + + res, err := internalclient.PutObject(prm) + common.ExitOnErr(cmd, "rpc error: %w", err) + + if p != nil { + p.Finish() + } + cmd.Printf("[%s] Object successfully stored\n", filename) + cmd.Printf(" ID: %s\n CID: %s\n", res.ID(), cnr) +} + +func parseObjectAttrs(cmd *cobra.Command) ([]object.Attribute, error) { + var rawAttrs []string + + raw := cmd.Flag("attributes").Value.String() + if len(raw) != 0 { + rawAttrs = strings.Split(raw, ",") + } + + attrs := make([]object.Attribute, len(rawAttrs), len(rawAttrs)+2) // name + timestamp attributes + for i := range rawAttrs { + kv := strings.SplitN(rawAttrs[i], "=", 2) + if len(kv) != 2 { + return nil, fmt.Errorf("invalid attribute format: %s", rawAttrs[i]) + } + attrs[i].SetKey(kv[0]) + attrs[i].SetValue(kv[1]) + } + + disableFilename, _ := cmd.Flags().GetBool("disable-filename") + if !disableFilename { + filename := filepath.Base(cmd.Flag("file").Value.String()) + index := len(attrs) + attrs = append(attrs, object.Attribute{}) + attrs[index].SetKey(object.AttributeFileName) + attrs[index].SetValue(filename) + } + + disableTime, _ := cmd.Flags().GetBool("disable-timestamp") + if !disableTime { + index := len(attrs) + attrs = append(attrs, object.Attribute{}) + attrs[index].SetKey(object.AttributeTimestamp) + attrs[index].SetValue(strconv.FormatInt(time.Now().Unix(), 10)) + } + + return attrs, nil +} + +func parseObjectNotifications(cmd *cobra.Command) (*object.NotificationInfo, error) { + const ( + separator = ":" + useDefaultTopic = "-" + ) + + raw := cmd.Flag(notificationFlag).Value.String() + if raw == "" { + return nil, nil + } + + rawSlice := strings.SplitN(raw, separator, 2) + if len(rawSlice) != 2 { + return nil, fmt.Errorf("notification must be in the form of: *epoch*%s*topic*, got %s", separator, raw) + } + + ni := new(object.NotificationInfo) + + epoch, err := strconv.ParseUint(rawSlice[0], 10, 64) + if err != nil { + return nil, fmt.Errorf("could not parse notification epoch %s: %w", rawSlice[0], err) + } + + ni.SetEpoch(epoch) + + if rawSlice[1] == "" { + return nil, fmt.Errorf("incorrect empty topic: use %s to force using default topic", useDefaultTopic) + } + + if rawSlice[1] != useDefaultTopic { + ni.SetTopic(rawSlice[1]) + } + + return ni, nil +} diff --git a/cmd/neofs-cli/modules/object/range.go b/cmd/neofs-cli/modules/object/range.go new file mode 100644 index 000000000..d85a9fa74 --- /dev/null +++ b/cmd/neofs-cli/modules/object/range.go @@ -0,0 +1,173 @@ +package object + +import ( + "bytes" + "errors" + "fmt" + "io" + "os" + "strconv" + "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" + sessionCli "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/session" + cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + "github.com/nspcc-dev/neofs-sdk-go/object" + oid "github.com/nspcc-dev/neofs-sdk-go/object/id" + "github.com/spf13/cobra" +) + +var objectRangeCmd = &cobra.Command{ + Use: "range", + Short: "Get payload range data of an object", + Long: "Get payload range data of an object", + Run: getObjectRange, +} + +func initObjectRangeCmd() { + commonflags.Init(objectRangeCmd) + commonflags.InitSession(objectRangeCmd) + + flags := objectRangeCmd.Flags() + + flags.String("cid", "", "Container ID") + _ = objectRangeCmd.MarkFlagRequired("cid") + + flags.String("oid", "", "Object ID") + _ = objectRangeCmd.MarkFlagRequired("oid") + + flags.String("range", "", "Range to take data from in the form offset:length") + flags.String("file", "", "File to write object payload to. Default: stdout.") + flags.Bool(rawFlag, false, rawFlagDesc) +} + +func getObjectRange(cmd *cobra.Command, _ []string) { + var cnr cid.ID + var obj oid.ID + + objAddr := readObjectAddress(cmd, &cnr, &obj) + + ranges, err := getRangeList(cmd) + common.ExitOnErr(cmd, "", err) + + if len(ranges) != 1 { + common.ExitOnErr(cmd, "", fmt.Errorf("exactly one range must be specified, got: %d", len(ranges))) + } + + var out io.Writer + + filename := cmd.Flag("file").Value.String() + if filename == "" { + out = os.Stdout + } else { + f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, os.ModePerm) + if err != nil { + common.ExitOnErr(cmd, "", fmt.Errorf("can't open file '%s': %w", filename, err)) + } + + defer f.Close() + + out = f + } + + pk := key.GetOrGenerate(cmd) + + var prm internalclient.PayloadRangePrm + sessionCli.Prepare(cmd, cnr, &obj, pk, &prm) + Prepare(cmd, &prm) + + raw, _ := cmd.Flags().GetBool(rawFlag) + prm.SetRawFlag(raw) + prm.SetAddress(objAddr) + prm.SetRange(ranges[0]) + prm.SetPayloadWriter(out) + + _, err = internalclient.PayloadRange(prm) + if err != nil { + if ok := printSplitInfoErr(cmd, err); ok { + return + } + + common.ExitOnErr(cmd, "can't get object payload range: %w", err) + } + + if filename != "" { + cmd.Printf("[%s] Payload successfully saved\n", filename) + } +} + +func printSplitInfoErr(cmd *cobra.Command, err error) bool { + var errSplitInfo *object.SplitInfoError + + ok := errors.As(err, &errSplitInfo) + + if ok { + cmd.PrintErrln("Object is complex, split information received.") + printSplitInfo(cmd, errSplitInfo.SplitInfo()) + } + + return ok +} + +func printSplitInfo(cmd *cobra.Command, info *object.SplitInfo) { + bs, err := marshalSplitInfo(cmd, info) + common.ExitOnErr(cmd, "can't marshal split info: %w", err) + + cmd.Println(string(bs)) +} + +func marshalSplitInfo(cmd *cobra.Command, info *object.SplitInfo) ([]byte, error) { + toJSON, _ := cmd.Flags().GetBool("json") + toProto, _ := cmd.Flags().GetBool("proto") + switch { + case toJSON && toProto: + return nil, errors.New("'--json' and '--proto' flags are mutually exclusive") + case toJSON: + return info.MarshalJSON() + case toProto: + return info.Marshal() + default: + b := bytes.NewBuffer(nil) + if splitID := info.SplitID(); splitID != nil { + b.WriteString("Split ID: " + splitID.String() + "\n") + } + if link, ok := info.Link(); ok { + b.WriteString("Linking object: " + link.String() + "\n") + } + if last, ok := info.LastPart(); ok { + b.WriteString("Last object: " + last.String() + "\n") + } + return b.Bytes(), nil + } +} + +func getRangeList(cmd *cobra.Command) ([]*object.Range, error) { + v := cmd.Flag("range").Value.String() + if len(v) == 0 { + return nil, nil + } + vs := strings.Split(v, ",") + rs := make([]*object.Range, len(vs)) + for i := range vs { + r := strings.Split(vs[i], rangeSep) + if len(r) != 2 { + return nil, fmt.Errorf("invalid range specifier: %s", vs[i]) + } + + offset, err := strconv.ParseUint(r[0], 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid range specifier: %s", vs[i]) + } + length, err := strconv.ParseUint(r[1], 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid range specifier: %s", vs[i]) + } + rs[i] = object.NewRange() + rs[i].SetOffset(offset) + rs[i].SetLength(length) + } + return rs, nil +} diff --git a/cmd/neofs-cli/modules/object/root.go b/cmd/neofs-cli/modules/object/root.go new file mode 100644 index 000000000..0c076f254 --- /dev/null +++ b/cmd/neofs-cli/modules/object/root.go @@ -0,0 +1,47 @@ +package object + +import ( + "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/commonflags" + "github.com/spf13/cobra" +) + +// Cmd represents the object command +var Cmd = &cobra.Command{ + Use: "object", + Short: "Operations with Objects", + Long: `Operations with Objects`, + PersistentPreRun: func(cmd *cobra.Command, args []string) { + // bind exactly that cmd's flags to + // the viper before execution + commonflags.Bind(cmd) + commonflags.BindAPI(cmd) + }, +} + +func init() { + objectChildCommands := []*cobra.Command{ + objectPutCmd, + objectDelCmd, + objectGetCmd, + objectSearchCmd, + objectHeadCmd, + objectHashCmd, + objectRangeCmd, + objectLockCmd} + + Cmd.AddCommand(objectChildCommands...) + + for _, objCommand := range objectChildCommands { + InitBearer(objCommand) + commonflags.InitAPI(objCommand) + } + + initObjectPutCmd() + initObjectDeleteCmd() + initObjectGetCmd() + initObjectSearchCmd() + initObjectHeadCmd() + initObjectHashCmd() + initObjectRangeCmd() + initCommandObjectLock() +} diff --git a/cmd/neofs-cli/modules/object/search.go b/cmd/neofs-cli/modules/object/search.go new file mode 100644 index 000000000..8136f897f --- /dev/null +++ b/cmd/neofs-cli/modules/object/search.go @@ -0,0 +1,145 @@ +package object + +import ( + "fmt" + "os" + "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" + sessionCli "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/session" + cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + "github.com/nspcc-dev/neofs-sdk-go/object" + oidSDK "github.com/nspcc-dev/neofs-sdk-go/object/id" + "github.com/spf13/cobra" +) + +const searchOIDFlag = "oid" + +var ( + searchFilters []string + + objectSearchCmd = &cobra.Command{ + Use: "search", + Short: "Search object", + Long: "Search object", + Run: searchObject, + } +) + +func initObjectSearchCmd() { + commonflags.Init(objectSearchCmd) + commonflags.InitSession(objectSearchCmd) + + flags := objectSearchCmd.Flags() + + flags.String("cid", "", "Container ID") + _ = objectSearchCmd.MarkFlagRequired("cid") + + flags.StringSliceVarP(&searchFilters, "filters", "f", nil, + "Repeated filter expressions or files with protobuf JSON") + + flags.Bool("root", false, "Search for user objects") + flags.Bool("phy", false, "Search physically stored objects") + flags.String(searchOIDFlag, "", "Search object by identifier") +} + +func searchObject(cmd *cobra.Command, _ []string) { + var cnr cid.ID + readCID(cmd, &cnr) + + sf, err := parseSearchFilters(cmd) + common.ExitOnErr(cmd, "", err) + + pk := key.GetOrGenerate(cmd) + + var prm internalclient.SearchObjectsPrm + sessionCli.Prepare(cmd, cnr, nil, pk, &prm) + Prepare(cmd, &prm) + prm.SetContainerID(cnr) + prm.SetFilters(sf) + + res, err := internalclient.SearchObjects(prm) + common.ExitOnErr(cmd, "rpc error: %w", err) + + ids := res.IDList() + + cmd.Printf("Found %d objects.\n", len(ids)) + for i := range ids { + cmd.Println(ids[i].String()) + } +} + +var searchUnaryOpVocabulary = map[string]object.SearchMatchType{ + "NOPRESENT": object.MatchNotPresent, +} + +var searchBinaryOpVocabulary = map[string]object.SearchMatchType{ + "EQ": object.MatchStringEqual, + "NE": object.MatchStringNotEqual, + "COMMON_PREFIX": object.MatchCommonPrefix, +} + +func parseSearchFilters(cmd *cobra.Command) (object.SearchFilters, error) { + var fs object.SearchFilters + + for i := range searchFilters { + words := strings.Fields(searchFilters[i]) + + switch len(words) { + default: + return nil, fmt.Errorf("invalid field number: %d", len(words)) + case 1: + data, err := os.ReadFile(words[0]) + if err != nil { + return nil, fmt.Errorf("could not read attributes filter from file: %w", err) + } + + subFs := object.NewSearchFilters() + + if err := subFs.UnmarshalJSON(data); err != nil { + return nil, fmt.Errorf("could not unmarshal attributes filter from file: %w", err) + } + + fs = append(fs, subFs...) + case 2: + m, ok := searchUnaryOpVocabulary[words[1]] + if !ok { + return nil, fmt.Errorf("unsupported unary op: %s", words[1]) + } + + fs.AddFilter(words[0], "", m) + case 3: + m, ok := searchBinaryOpVocabulary[words[1]] + if !ok { + return nil, fmt.Errorf("unsupported binary op: %s", words[1]) + } + + fs.AddFilter(words[0], words[2], m) + } + } + + root, _ := cmd.Flags().GetBool("root") + if root { + fs.AddRootFilter() + } + + phy, _ := cmd.Flags().GetBool("phy") + if phy { + fs.AddPhyFilter() + } + + oid, _ := cmd.Flags().GetString(searchOIDFlag) + if oid != "" { + var id oidSDK.ID + if err := id.DecodeString(oid); err != nil { + return nil, fmt.Errorf("could not parse object ID: %w", err) + } + + fs.AddObjectIDFilter(object.MatchStringEqual, id) + } + + return fs, nil +} diff --git a/cmd/neofs-cli/modules/object/util.go b/cmd/neofs-cli/modules/object/util.go new file mode 100644 index 000000000..1e20614fa --- /dev/null +++ b/cmd/neofs-cli/modules/object/util.go @@ -0,0 +1,83 @@ +package object + +import ( + "fmt" + "strings" + + "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-sdk-go/bearer" + cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + oid "github.com/nspcc-dev/neofs-sdk-go/object/id" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +const ( + bearerTokenFlag = "bearer" + + rawFlag = "raw" + rawFlagDesc = "Set raw request option" +) + +type RPCParameters interface { + SetBearerToken(prm *bearer.Token) + SetTTL(uint32) + SetXHeaders([]string) +} + +// InitBearer adds bearer token flag to a command. +func InitBearer(cmd *cobra.Command) { + flags := cmd.Flags() + flags.String(bearerTokenFlag, "", "File with signed JSON or binary encoded bearer token") +} + +// Prepare prepares object-related parameters for a command. +func Prepare(cmd *cobra.Command, prms ...RPCParameters) { + ttl := viper.GetUint32(commonflags.TTL) + common.PrintVerbose("TTL: %d", ttl) + + for i := range prms { + btok := common.ReadBearerToken(cmd, bearerTokenFlag) + + prms[i].SetBearerToken(btok) + prms[i].SetTTL(ttl) + prms[i].SetXHeaders(parseXHeaders(cmd)) + } +} + +func parseXHeaders(cmd *cobra.Command) []string { + xHeaders, _ := cmd.Flags().GetStringSlice(commonflags.XHeadersKey) + xs := make([]string, 0, 2*len(xHeaders)) + + for i := range xHeaders { + kv := strings.SplitN(xHeaders[i], "=", 2) + if len(kv) != 2 { + panic(fmt.Errorf("invalid X-Header format: %s", xHeaders[i])) + } + + xs = append(xs, kv[0], kv[1]) + } + + return xs +} + +func readObjectAddress(cmd *cobra.Command, cnr *cid.ID, obj *oid.ID) oid.Address { + readCID(cmd, cnr) + readOID(cmd, obj) + + var addr oid.Address + addr.SetContainer(*cnr) + addr.SetObject(*obj) + return addr +} + +func readCID(cmd *cobra.Command, id *cid.ID) { + err := id.DecodeString(cmd.Flag("cid").Value.String()) + common.ExitOnErr(cmd, "decode container ID string: %w", err) +} + +func readOID(cmd *cobra.Command, id *oid.ID) { + err := id.DecodeString(cmd.Flag("oid").Value.String()) + common.ExitOnErr(cmd, "decode object ID string: %w", err) +} diff --git a/cmd/neofs-cli/modules/root.go b/cmd/neofs-cli/modules/root.go index ac047e3f4..545a625d0 100644 --- a/cmd/neofs-cli/modules/root.go +++ b/cmd/neofs-cli/modules/root.go @@ -1,10 +1,8 @@ package cmd import ( - "fmt" "os" "path/filepath" - "strings" "github.com/mitchellh/go-homedir" internalclient "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/client" @@ -16,6 +14,7 @@ import ( bearerCli "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/bearer" controlCli "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/control" netmapCli "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/netmap" + objectCli "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/object" sessionCli "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/session" utilCli "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/util" "github.com/nspcc-dev/neofs-node/misc" @@ -80,6 +79,7 @@ func init() { rootCmd.AddCommand(controlCli.Cmd) rootCmd.AddCommand(utilCli.Cmd) rootCmd.AddCommand(netmapCli.Cmd) + rootCmd.AddCommand(objectCli.Cmd) rootCmd.AddCommand(gendoc.Command(rootCmd)) } @@ -137,26 +137,3 @@ func prepareAPIClient(cmd *cobra.Command, dst ...clientWithKey) { d.SetClient(cli) } } - -func getTTL() uint32 { - ttl := viper.GetUint32(commonflags.TTL) - common.PrintVerbose("TTL: %d", ttl) - - return ttl -} - -func parseXHeaders(cmd *cobra.Command) []string { - xHeaders, _ := cmd.Flags().GetStringSlice(commonflags.XHeadersKey) - xs := make([]string, 0, 2*len(xHeaders)) - - for i := range xHeaders { - kv := strings.SplitN(xHeaders[i], "=", 2) - if len(kv) != 2 { - panic(fmt.Errorf("invalid X-Header format: %s", xHeaders[i])) - } - - xs = append(xs, kv[0], kv[1]) - } - - return xs -} diff --git a/cmd/neofs-cli/modules/storagegroup.go b/cmd/neofs-cli/modules/storagegroup.go index 6cb83d0b9..a72626f3f 100644 --- a/cmd/neofs-cli/modules/storagegroup.go +++ b/cmd/neofs-cli/modules/storagegroup.go @@ -10,6 +10,7 @@ import ( "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" + objectCli "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/object" sessionCli "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/session" "github.com/nspcc-dev/neofs-node/pkg/services/object_manager/storagegroup" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" @@ -64,6 +65,7 @@ var sgDelCmd = &cobra.Command{ const ( sgMembersFlag = "members" sgIDFlag = "id" + sgRawFlag = "raw" ) var ( @@ -93,6 +95,8 @@ func initSGGetCmd() { flags.StringVarP(&sgID, sgIDFlag, "", "", "storage group identifier") _ = sgGetCmd.MarkFlagRequired(sgIDFlag) + + flags.Bool(sgRawFlag, false, "Set raw request option") } func initSGListCmd() { @@ -126,9 +130,7 @@ func init() { storagegroupCmd.AddCommand(storageGroupChildCommands...) for _, sgCommand := range storageGroupChildCommands { - flags := sgCommand.Flags() - - flags.String(bearerTokenFlag, "", "File with signed JSON or binary encoded bearer token") + objectCli.InitBearer(sgCommand) commonflags.InitAPI(sgCommand) } @@ -168,12 +170,12 @@ func (c sgHeadReceiver) Head(addr oid.Address) (interface{}, error) { func putSG(cmd *cobra.Command, _ []string) { pk := key.GetOrGenerate(cmd) - ownerID, err := getOwnerID(pk) - common.ExitOnErr(cmd, "", err) + var ownerID user.ID + user.IDFromKey(&ownerID, pk.PublicKey) var cnr cid.ID - err = readCID(cmd, &cnr) + err := readCID(cmd, &cnr) common.ExitOnErr(cmd, "", err) members := make([]oid.ID, len(sgMembers)) @@ -189,14 +191,14 @@ func putSG(cmd *cobra.Command, _ []string) { ) sessionCli.Prepare(cmd, cnr, nil, pk, &putPrm) - prepareObjectPrm(cmd, &headPrm, &putPrm) + objectCli.Prepare(cmd, &headPrm, &putPrm) headPrm.SetRawFlag(true) sg, err := storagegroup.CollectMembers(sgHeadReceiver{ cmd: cmd, key: pk, - ownerID: *ownerID, + ownerID: ownerID, prm: headPrm, }, cnr, members) common.ExitOnErr(cmd, "could not collect storage group members: %w", err) @@ -206,7 +208,7 @@ func putSG(cmd *cobra.Command, _ []string) { obj := object.New() obj.SetContainerID(cnr) - obj.SetOwnerID(ownerID) + obj.SetOwnerID(&ownerID) obj.SetType(object.TypeStorageGroup) putPrm.SetHeader(obj) @@ -244,10 +246,14 @@ func getSG(cmd *cobra.Command, _ []string) { buf := bytes.NewBuffer(nil) - var prm internalclient.GetObjectPrm + pk := key.GetOrGenerate(cmd) - prepareSessionPrm(cmd, cnr, id, &prm) - prepareObjectPrmRaw(cmd, &prm) + var prm internalclient.GetObjectPrm + sessionCli.Prepare(cmd, cnr, id, pk, &prm) + objectCli.Prepare(cmd, &prm) + + raw, _ := cmd.Flags().GetBool(sgRawFlag) + prm.SetRawFlag(raw) prm.SetAddress(addr) prm.SetPayloadWriter(buf) @@ -261,7 +267,7 @@ func getSG(cmd *cobra.Command, _ []string) { cmd.Printf("Expiration epoch: %d\n", sg.ExpirationEpoch()) cmd.Printf("Group size: %d\n", sg.ValidationDataSize()) - printChecksum(cmd, "Group hash", sg.ValidationDataHash) + common.PrintChecksum(cmd, "Group hash", sg.ValidationDataHash) if members := sg.Members(); len(members) > 0 { cmd.Println("Members:") @@ -278,10 +284,11 @@ func listSG(cmd *cobra.Command, _ []string) { err := readCID(cmd, &cnr) common.ExitOnErr(cmd, "", err) - var prm internalclient.SearchObjectsPrm + pk := key.GetOrGenerate(cmd) - prepareSessionPrm(cmd, cnr, nil, &prm) - prepareObjectPrm(cmd, &prm) + var prm internalclient.SearchObjectsPrm + sessionCli.Prepare(cmd, cnr, nil, pk, &prm) + objectCli.Prepare(cmd, &prm) prm.SetContainerID(cnr) prm.SetFilters(storagegroup.SearchQuery()) @@ -312,8 +319,9 @@ func delSG(cmd *cobra.Command, _ []string) { var prm internalclient.DeleteObjectPrm - prepareSessionPrm(cmd, cnr, id, &prm) - prepareObjectPrm(cmd, &prm) + pk := key.GetOrGenerate(cmd) + sessionCli.Prepare(cmd, cnr, id, pk, &prm) + objectCli.Prepare(cmd, &prm) prm.SetAddress(addr) res, err := internalclient.DeleteObject(prm) @@ -324,3 +332,12 @@ func delSG(cmd *cobra.Command, _ []string) { cmd.Println("Storage group removed successfully.") cmd.Printf(" Tombstone: %s\n", tomb) } + +func readCID(cmd *cobra.Command, cnr *cid.ID) error { + err := cnr.DecodeString(cmd.Flag("cid").Value.String()) + if err != nil { + return fmt.Errorf("decode container ID string: %w", err) + } + + return nil +}