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  }