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 }