github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/tag-list.go (about)

     1  // Copyright (c) 2015-2022 MinIO, Inc.
     2  //
     3  // This file is part of MinIO Object Storage stack
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package cmd
    19  
    20  import (
    21  	"context"
    22  	"errors"
    23  	"fmt"
    24  	"sort"
    25  	"strings"
    26  	"time"
    27  
    28  	"github.com/fatih/color"
    29  	"github.com/minio/cli"
    30  	json "github.com/minio/colorjson"
    31  	"github.com/minio/mc/pkg/probe"
    32  	"github.com/minio/minio-go/v7"
    33  	"github.com/minio/pkg/v2/console"
    34  )
    35  
    36  var tagListFlags = []cli.Flag{
    37  	cli.StringFlag{
    38  		Name:  "version-id, vid",
    39  		Usage: "list tags of particular object version",
    40  	},
    41  	cli.StringFlag{
    42  		Name:  "rewind",
    43  		Usage: "list tags of particular object version at specified time",
    44  	},
    45  	cli.BoolFlag{
    46  		Name:  "versions",
    47  		Usage: "list tags on all versions for an object",
    48  	},
    49  	cli.BoolFlag{
    50  		Name:  "recursive, r",
    51  		Usage: "recursivley show tags for all objects",
    52  	},
    53  }
    54  
    55  var tagListCmd = cli.Command{
    56  	Name:         "list",
    57  	Usage:        "list tags of a bucket or an object",
    58  	Action:       mainListTag,
    59  	OnUsageError: onUsageError,
    60  	Before:       setGlobalsFromContext,
    61  	Flags:        append(tagListFlags, globalFlags...),
    62  	CustomHelpTemplate: `NAME:
    63    {{.HelpName}} - {{.Usage}}
    64  
    65  USAGE:
    66    {{.HelpName}} [COMMAND FLAGS] TARGET
    67  
    68  FLAGS:
    69    {{range .VisibleFlags}}{{.}}
    70    {{end}}
    71  DESCRIPTION:
    72     List tags assigned to a bucket or an object
    73  
    74  EXAMPLES:
    75    1. List the tags assigned to an object.
    76       {{.Prompt}} {{.HelpName}} myminio/testbucket/testobject
    77  
    78    2. List the tags assigned to particular version of an object.
    79       {{.Prompt}} {{.HelpName}} --version-id "ieQq7aXsyhlhDt47YURGlrucYY3GxWHa" myminio/testbucket/testobject
    80  
    81    3. List the tags assigned to an object versions that are older than one week.
    82       {{.Prompt}} {{.HelpName}} --versions --rewind 7d myminio/testbucket/testobject
    83  
    84    4. List the tags assigned to an object in JSON format.
    85       {{.Prompt}} {{.HelpName}} --json myminio/testbucket/testobject
    86  
    87    5. List the tags assigned to a bucket.
    88       {{.Prompt}} {{.HelpName}} myminio/testbucket
    89  
    90    6. List the tags assigned to a bucket in JSON format.
    91       {{.Prompt}} {{.HelpName}} --json s3/testbucket
    92  
    93    7. List the tags recursively for all the objects of subdirs of bucket.
    94       {{.Prompt}} {{.HelpName}} --recursive myminio/testbucket
    95  
    96    8. Show the tags recursively for all versions of all objects of subdirs of bucket.
    97       {{.Prompt}} {{.HelpName}} --recursive --versions myminio/testbucket
    98  `,
    99  }
   100  
   101  // tagListMessage structure for displaying tag
   102  type tagListMessage struct {
   103  	Tags      map[string]string `json:"tagset,omitempty"`
   104  	Status    string            `json:"status"`
   105  	URL       string            `json:"url"`
   106  	VersionID string            `json:"versionID"`
   107  }
   108  
   109  func (t tagListMessage) JSON() string {
   110  	tagJSONbytes, e := json.MarshalIndent(t, "", "  ")
   111  	fatalIf(probe.NewError(e), "Unable to marshal into JSON for "+t.URL)
   112  	return string(tagJSONbytes)
   113  }
   114  
   115  func (t tagListMessage) String() string {
   116  	keys := []string{}
   117  	maxKeyLen := 4 // len("Name")
   118  	for key := range t.Tags {
   119  		keys = append(keys, key)
   120  		if len(key) > maxKeyLen {
   121  			maxKeyLen = len(key)
   122  		}
   123  	}
   124  	sort.Strings(keys)
   125  
   126  	maxKeyLen += 2 // add len(" :")
   127  	strName := t.URL
   128  	if strings.TrimSpace(t.VersionID) != "" {
   129  		strName += " (" + t.VersionID + ")"
   130  	}
   131  	strs := []string{
   132  		fmt.Sprintf("%v%*v %v", console.Colorize("Name", "Name"), maxKeyLen-4, ":", console.Colorize("Name", strName)),
   133  	}
   134  
   135  	for _, key := range keys {
   136  		strs = append(
   137  			strs,
   138  			fmt.Sprintf("%v%*v %v", console.Colorize("Key", key), maxKeyLen-len(key), ":", console.Colorize("Value", t.Tags[key])),
   139  		)
   140  	}
   141  
   142  	if len(keys) == 0 {
   143  		strs = append(strs, console.Colorize("NoTags", "No tags found"))
   144  	}
   145  
   146  	return strings.Join(strs, "\n")
   147  }
   148  
   149  // parseTagListSyntax performs command-line input validation for tag list command.
   150  func parseTagListSyntax(ctx *cli.Context) (targetURL, versionID string, timeRef time.Time, withOlderVersions, recursive bool) {
   151  	if len(ctx.Args()) != 1 {
   152  		showCommandHelpAndExit(ctx, globalErrorExitStatus)
   153  	}
   154  
   155  	targetURL = ctx.Args().Get(0)
   156  	versionID = ctx.String("version-id")
   157  	withOlderVersions = ctx.Bool("versions")
   158  	rewind := ctx.String("rewind")
   159  	recursive = ctx.Bool("recursive")
   160  
   161  	if versionID != "" && rewind != "" {
   162  		fatalIf(errDummy().Trace(), "You cannot specify both --version-id and --rewind flags at the same time")
   163  	}
   164  
   165  	timeRef = parseRewindFlag(rewind)
   166  	return
   167  }
   168  
   169  // showTags pretty prints tags of a bucket or a specified object/version
   170  func showTags(ctx context.Context, clnt Client, versionID string) {
   171  	targetName := clnt.GetURL().String()
   172  	if versionID != "" {
   173  		targetName += " (" + versionID + ")"
   174  	}
   175  
   176  	tagsMap, err := clnt.GetTags(ctx, versionID)
   177  	if err != nil {
   178  		if minio.ToErrorResponse(err.ToGoError()).Code == "NoSuchTagSet" {
   179  			fatalIf(probe.NewError(errors.New("check 'mc tag set --help' on how to set tags")), "No tags found  for "+targetName)
   180  		}
   181  		fatalIf(err, "Unable to fetch tags for "+targetName)
   182  		return
   183  	}
   184  
   185  	printMsg(tagListMessage{
   186  		Tags:      tagsMap,
   187  		Status:    "success",
   188  		URL:       clnt.GetURL().String(),
   189  		VersionID: versionID,
   190  	})
   191  }
   192  
   193  func showTagsSingle(ctx context.Context, alias, url, versionID string) *probe.Error {
   194  	newClnt, err := newClientFromAlias(alias, url)
   195  	if err != nil {
   196  		return err
   197  	}
   198  
   199  	showTags(ctx, newClnt, versionID)
   200  	return nil
   201  }
   202  
   203  func mainListTag(cliCtx *cli.Context) error {
   204  	ctx, cancelListTag := context.WithCancel(globalContext)
   205  	defer cancelListTag()
   206  
   207  	console.SetColor("Name", color.New(color.Bold, color.FgCyan))
   208  	console.SetColor("Key", color.New(color.FgGreen))
   209  	console.SetColor("Value", color.New(color.FgYellow))
   210  	console.SetColor("NoTags", color.New(color.FgRed))
   211  
   212  	targetURL, versionID, timeRef, withVersions, recursive := parseTagListSyntax(cliCtx)
   213  	if timeRef.IsZero() && withVersions {
   214  		timeRef = time.Now().UTC()
   215  	}
   216  
   217  	clnt, err := newClient(targetURL)
   218  	fatalIf(err, "Unable to initialize target "+targetURL)
   219  
   220  	alias, urlStr, _ := mustExpandAlias(targetURL)
   221  	if timeRef.IsZero() && !withVersions && !recursive {
   222  		err := showTagsSingle(ctx, alias, urlStr, versionID)
   223  		fatalIf(err.Trace(), "Unable to show tags on `%s`", targetURL)
   224  		return nil
   225  	}
   226  
   227  	for content := range clnt.List(ctx, ListOptions{TimeRef: timeRef, WithOlderVersions: withVersions, Recursive: recursive}) {
   228  		if content.Err != nil {
   229  			fatalIf(content.Err.Trace(), "Unable to list target "+targetURL)
   230  			continue
   231  		}
   232  
   233  		// Skip if its delete marker
   234  		if content.IsDeleteMarker {
   235  			continue
   236  		}
   237  
   238  		if !recursive && alias+getKey(content) != getStandardizedURL(targetURL) {
   239  			break
   240  		}
   241  
   242  		err := showTagsSingle(ctx, alias, content.URL.String(), content.VersionID)
   243  		if err != nil {
   244  			errorIf(err.Trace(clnt.GetURL().String()), "Invalid URL")
   245  			continue
   246  		}
   247  	}
   248  
   249  	return nil
   250  }