github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/ls.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 "fmt" 23 "path/filepath" 24 "sort" 25 "strings" 26 "time" 27 28 "github.com/dustin/go-humanize" 29 json "github.com/minio/colorjson" 30 "github.com/minio/mc/pkg/probe" 31 "github.com/minio/pkg/v2/console" 32 ) 33 34 // printDate - human friendly formatted date. 35 const ( 36 printDate = "2006-01-02 15:04:05 MST" 37 ) 38 39 // contentMessage container for content message structure. 40 type contentMessage struct { 41 Status string `json:"status"` 42 Filetype string `json:"type"` 43 Time time.Time `json:"lastModified"` 44 Size int64 `json:"size"` 45 Key string `json:"key"` 46 ETag string `json:"etag"` 47 URL string `json:"url,omitempty"` 48 49 VersionID string `json:"versionId,omitempty"` 50 VersionOrd int `json:"versionOrdinal,omitempty"` 51 VersionIndex int `json:"versionIndex,omitempty"` 52 IsDeleteMarker bool `json:"isDeleteMarker,omitempty"` 53 StorageClass string `json:"storageClass,omitempty"` 54 55 Metadata map[string]string `json:"metadata,omitempty"` 56 Tags map[string]string `json:"tags,omitempty"` 57 } 58 59 // String colorized string message. 60 func (c contentMessage) String() string { 61 message := console.Colorize("Time", fmt.Sprintf("[%s]", c.Time.Format(printDate))) 62 message += console.Colorize("Size", fmt.Sprintf("%7s", strings.Join(strings.Fields(humanize.IBytes(uint64(c.Size))), ""))) 63 fileDesc := "" 64 65 if c.StorageClass != "" { 66 message += " " + console.Colorize("SC", c.StorageClass) 67 } 68 69 if c.VersionID != "" { 70 fileDesc += console.Colorize("VersionID", " "+c.VersionID) + console.Colorize("VersionOrd", fmt.Sprintf(" v%d", c.VersionOrd)) 71 if c.IsDeleteMarker { 72 fileDesc += console.Colorize("DEL", " DEL") 73 } else { 74 fileDesc += console.Colorize("PUT", " PUT") 75 } 76 } 77 78 fileDesc += " " + c.Key 79 80 if c.Filetype == "folder" { 81 message += console.Colorize("Dir", fileDesc) 82 } else { 83 message += console.Colorize("File", fileDesc) 84 } 85 return message 86 } 87 88 // JSON jsonified content message. 89 func (c contentMessage) JSON() string { 90 c.Status = "success" 91 jsonMessageBytes, e := json.MarshalIndent(c, "", " ") 92 fatalIf(probe.NewError(e), "Unable to marshal into JSON.") 93 94 return string(jsonMessageBytes) 95 } 96 97 // Use OS separator and adds a trailing separator if it is a dir 98 func getOSDependantKey(path string, isDir bool) string { 99 sep := "/" 100 101 if isDir && !strings.HasSuffix(path, sep) { 102 return fmt.Sprintf("%s%s", path, sep) 103 } 104 return path 105 } 106 107 // get content key 108 func getKey(c *ClientContent) string { 109 return getOSDependantKey(c.URL.Path, c.Type.IsDir()) 110 } 111 112 // Generate printable listing from a list of sorted client 113 // contents, the latest created content comes first. 114 func generateContentMessages(clntURL ClientURL, ctnts []*ClientContent, printAllVersions bool) (msgs []contentMessage) { 115 prefixPath := clntURL.Path 116 prefixPath = filepath.ToSlash(prefixPath) 117 if !strings.HasSuffix(prefixPath, "/") { 118 prefixPath = prefixPath[:strings.LastIndex(prefixPath, "/")+1] 119 } 120 prefixPath = strings.TrimPrefix(prefixPath, "./") 121 122 nrVersions := len(ctnts) 123 124 for i, c := range ctnts { 125 // Convert any os specific delimiters to "/". 126 contentURL := filepath.ToSlash(c.URL.Path) 127 // Trim prefix path from the content path. 128 c.URL.Path = strings.TrimPrefix(contentURL, prefixPath) 129 130 contentMsg := contentMessage{} 131 contentMsg.Time = c.Time.Local() 132 133 // guess file type. 134 contentMsg.Filetype = func() string { 135 if c.Type.IsDir() { 136 return "folder" 137 } 138 return "file" 139 }() 140 141 contentMsg.Size = c.Size 142 contentMsg.StorageClass = c.StorageClass 143 contentMsg.Metadata = c.Metadata 144 contentMsg.Tags = c.Tags 145 146 md5sum := strings.TrimPrefix(c.ETag, "\"") 147 md5sum = strings.TrimSuffix(md5sum, "\"") 148 contentMsg.ETag = md5sum 149 // Convert OS Type to match console file printing style. 150 contentMsg.Key = getKey(c) 151 contentMsg.VersionID = c.VersionID 152 contentMsg.IsDeleteMarker = c.IsDeleteMarker 153 contentMsg.VersionOrd = nrVersions - i 154 // URL is empty by default 155 // Set it to either relative dir (host) or public url (remote) 156 contentMsg.URL = clntURL.String() 157 158 msgs = append(msgs, contentMsg) 159 160 if !printAllVersions { 161 break 162 } 163 } 164 return 165 } 166 167 func sortObjectVersions(ctntVersions []*ClientContent) { 168 // Sort versions 169 sort.Slice(ctntVersions, func(i, j int) bool { 170 if ctntVersions[i].IsLatest { 171 return true 172 } 173 if ctntVersions[j].IsLatest { 174 return false 175 } 176 return ctntVersions[i].Time.After(ctntVersions[j].Time) 177 }) 178 } 179 180 // summaryMessage container for summary message structure 181 type summaryMessage struct { 182 TotalObjects int64 `json:"totalObjects"` 183 TotalSize int64 `json:"totalSize"` 184 } 185 186 // String colorized string message 187 func (s summaryMessage) String() string { 188 msg := console.Colorize("Summarize", fmt.Sprintf("\nTotal Size: %s", humanize.IBytes(uint64(s.TotalSize)))) 189 msg += "\n" + console.Colorize("Summarize", fmt.Sprintf("Total Objects: %d", s.TotalObjects)) 190 return msg 191 } 192 193 // JSON jsonified summary message 194 func (s summaryMessage) JSON() string { 195 jsonMessageBytes, e := json.MarshalIndent(s, "", "") 196 fatalIf(probe.NewError(e), "Unable to marshal into JSON") 197 return string(jsonMessageBytes) 198 } 199 200 // Pretty print the list of versions belonging to one object 201 func printObjectVersions(clntURL ClientURL, ctntVersions []*ClientContent, printAllVersions bool) { 202 sortObjectVersions(ctntVersions) 203 msgs := generateContentMessages(clntURL, ctntVersions, printAllVersions) 204 for _, msg := range msgs { 205 printMsg(msg) 206 } 207 } 208 209 type doListOptions struct { 210 timeRef time.Time 211 isRecursive bool 212 isIncomplete bool 213 isSummary bool 214 withOlderVersions bool 215 listZip bool 216 filter string 217 } 218 219 // doList - list all entities inside a folder. 220 func doList(ctx context.Context, clnt Client, o doListOptions) error { 221 var ( 222 lastPath string 223 perObjectVersions []*ClientContent 224 cErr error 225 totalSize int64 226 totalObjects int64 227 ) 228 229 for content := range clnt.List(ctx, ListOptions{ 230 Recursive: o.isRecursive, 231 Incomplete: o.isIncomplete, 232 TimeRef: o.timeRef, 233 WithOlderVersions: o.withOlderVersions || !o.timeRef.IsZero(), 234 WithDeleteMarkers: true, 235 ShowDir: DirNone, 236 ListZip: o.listZip, 237 }) { 238 if content.Err != nil { 239 errorIf(content.Err.Trace(clnt.GetURL().String()), "Unable to list folder.") 240 cErr = exitStatus(globalErrorExitStatus) // Set the exit status. 241 continue 242 } 243 244 if content.StorageClass != "" && o.filter != "" && o.filter != "*" && content.StorageClass != o.filter { 245 continue 246 } 247 248 if lastPath != content.URL.Path { 249 // Print any object in the current list before reinitializing it 250 printObjectVersions(clnt.GetURL(), perObjectVersions, o.withOlderVersions) 251 lastPath = content.URL.Path 252 perObjectVersions = []*ClientContent{} 253 } 254 255 perObjectVersions = append(perObjectVersions, content) 256 totalSize += content.Size 257 totalObjects++ 258 } 259 260 printObjectVersions(clnt.GetURL(), perObjectVersions, o.withOlderVersions) 261 262 if o.isSummary { 263 printMsg(summaryMessage{ 264 TotalObjects: totalObjects, 265 TotalSize: totalSize, 266 }) 267 } 268 269 return cErr 270 }