github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/stat.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  	"bytes"
    22  	"context"
    23  	"fmt"
    24  	"os"
    25  	"path/filepath"
    26  	"sort"
    27  	"strings"
    28  	"time"
    29  
    30  	"github.com/dustin/go-humanize"
    31  	json "github.com/minio/colorjson"
    32  	"github.com/minio/madmin-go/v3"
    33  	"github.com/minio/mc/pkg/probe"
    34  	"github.com/minio/minio-go/v7"
    35  	"github.com/minio/minio-go/v7/pkg/lifecycle"
    36  	"github.com/minio/minio-go/v7/pkg/notification"
    37  	"github.com/minio/minio-go/v7/pkg/replication"
    38  	"github.com/minio/pkg/v2/console"
    39  )
    40  
    41  // contentMessage container for content message structure.
    42  type statMessage struct {
    43  	Status            string             `json:"status"`
    44  	Key               string             `json:"name"`
    45  	Date              time.Time          `json:"lastModified"`
    46  	Size              int64              `json:"size"`
    47  	ETag              string             `json:"etag"`
    48  	Type              string             `json:"type,omitempty"`
    49  	Expires           *time.Time         `json:"expires,omitempty"`
    50  	Expiration        *time.Time         `json:"expiration,omitempty"`
    51  	ExpirationRuleID  string             `json:"expirationRuleID,omitempty"`
    52  	ReplicationStatus string             `json:"replicationStatus,omitempty"`
    53  	Metadata          map[string]string  `json:"metadata,omitempty"`
    54  	VersionID         string             `json:"versionID,omitempty"`
    55  	DeleteMarker      bool               `json:"deleteMarker,omitempty"`
    56  	Restore           *minio.RestoreInfo `json:"restore,omitempty"`
    57  }
    58  
    59  func (stat statMessage) String() (msg string) {
    60  	var msgBuilder strings.Builder
    61  	// Format properly for alignment based on maxKey leng
    62  	stat.Key = fmt.Sprintf("%-10s: %s", "Name", stat.Key)
    63  	msgBuilder.WriteString(console.Colorize("Name", stat.Key) + "\n")
    64  	if !stat.Date.IsZero() {
    65  		msgBuilder.WriteString(fmt.Sprintf("%-10s: %s ", "Date", stat.Date.Format(printDate)) + "\n")
    66  	}
    67  	if stat.Type != "folder" {
    68  		msgBuilder.WriteString(fmt.Sprintf("%-10s: %-6s ", "Size", humanize.IBytes(uint64(stat.Size))) + "\n")
    69  	}
    70  
    71  	if stat.ETag != "" {
    72  		msgBuilder.WriteString(fmt.Sprintf("%-10s: %s ", "ETag", stat.ETag) + "\n")
    73  	}
    74  	if stat.VersionID != "" {
    75  		versionIDField := stat.VersionID
    76  		if stat.DeleteMarker {
    77  			versionIDField += " (delete-marker)"
    78  		}
    79  		msgBuilder.WriteString(fmt.Sprintf("%-10s: %s ", "VersionID", versionIDField) + "\n")
    80  	}
    81  	msgBuilder.WriteString(fmt.Sprintf("%-10s: %s ", "Type", stat.Type) + "\n")
    82  	if stat.Expires != nil {
    83  		msgBuilder.WriteString(fmt.Sprintf("%-10s: %s ", "Expires", stat.Expires.Format(printDate)) + "\n")
    84  	}
    85  	if stat.Expiration != nil {
    86  		msgBuilder.WriteString(fmt.Sprintf("%-10s: %s (lifecycle-rule-id: %s) ", "Expiration",
    87  			stat.Expiration.Local().Format(printDate), stat.ExpirationRuleID) + "\n")
    88  	}
    89  	if stat.Restore != nil {
    90  		msgBuilder.WriteString(fmt.Sprintf("%-10s:", "Restore") + "\n")
    91  		msgBuilder.WriteString(fmt.Sprintf("  %-10s: %s", "ExpiryTime",
    92  			stat.Restore.ExpiryTime.Local().Format(printDate)) + "\n")
    93  		msgBuilder.WriteString(fmt.Sprintf("  %-10s: %t", "Ongoing",
    94  			stat.Restore.OngoingRestore) + "\n")
    95  	}
    96  	maxKeyMetadata := 0
    97  	maxKeyEncrypted := 0
    98  	for k := range stat.Metadata {
    99  		// Skip encryption headers, we print them later.
   100  		if !strings.HasPrefix(strings.ToLower(k), serverEncryptionKeyPrefix) {
   101  			if len(k) > maxKeyMetadata {
   102  				maxKeyMetadata = len(k)
   103  			}
   104  		} else if strings.HasPrefix(strings.ToLower(k), serverEncryptionKeyPrefix) {
   105  			if len(k) > maxKeyEncrypted {
   106  				maxKeyEncrypted = len(k)
   107  			}
   108  		}
   109  	}
   110  
   111  	if maxKeyEncrypted > 0 {
   112  		if keyID, ok := stat.Metadata["X-Amz-Server-Side-Encryption-Aws-Kms-Key-Id"]; ok {
   113  			msgBuilder.WriteString(fmt.Sprintf("%-10s: SSE-%s (%s)\n", "Encryption", "KMS", keyID))
   114  		} else if _, ok := stat.Metadata["X-Amz-Server-Side-Encryption-Customer-Key-Md5"]; ok {
   115  			msgBuilder.WriteString(fmt.Sprintf("%-10s: SSE-%s\n", "Encryption", "C"))
   116  		} else {
   117  			msgBuilder.WriteString(fmt.Sprintf("%-10s: SSE-%s\n", "Encryption", "S3"))
   118  		}
   119  	}
   120  
   121  	if maxKeyMetadata > 0 {
   122  		msgBuilder.WriteString(fmt.Sprintf("%-10s:", "Metadata") + "\n")
   123  		for k, v := range stat.Metadata {
   124  			// Skip encryption headers, we print them later.
   125  			if !strings.HasPrefix(strings.ToLower(k), serverEncryptionKeyPrefix) {
   126  				msgBuilder.WriteString(fmt.Sprintf("  %-*.*s: %s ", maxKeyMetadata, maxKeyMetadata, k, v) + "\n")
   127  			}
   128  		}
   129  	}
   130  
   131  	if stat.ReplicationStatus != "" {
   132  		msgBuilder.WriteString(fmt.Sprintf("%-10s: %s ", "Replication Status", stat.ReplicationStatus))
   133  	}
   134  
   135  	msgBuilder.WriteString("\n")
   136  
   137  	return msgBuilder.String()
   138  }
   139  
   140  // JSON jsonified content message.
   141  func (stat statMessage) JSON() string {
   142  	stat.Status = "success"
   143  	jsonMessageBytes, e := json.MarshalIndent(stat, "", " ")
   144  	fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
   145  
   146  	return string(jsonMessageBytes)
   147  }
   148  
   149  // parseStat parses client Content container into statMessage struct.
   150  func parseStat(c *ClientContent) statMessage {
   151  	content := statMessage{}
   152  	content.Date = c.Time.Local()
   153  	// guess file type.
   154  	content.Type = func() string {
   155  		if c.Type.IsDir() {
   156  			return "folder"
   157  		}
   158  		return "file"
   159  	}()
   160  	content.Size = c.Size
   161  	content.VersionID = c.VersionID
   162  	content.Key = getKey(c)
   163  	content.Metadata = c.Metadata
   164  	content.ETag = strings.TrimPrefix(c.ETag, "\"")
   165  	content.ETag = strings.TrimSuffix(content.ETag, "\"")
   166  	if !c.Expires.IsZero() {
   167  		content.Expires = &c.Expires
   168  	}
   169  	if !c.Expiration.IsZero() {
   170  		content.Expiration = &c.Expiration
   171  	}
   172  	content.ExpirationRuleID = c.ExpirationRuleID
   173  	content.ReplicationStatus = c.ReplicationStatus
   174  	content.Restore = c.Restore
   175  	return content
   176  }
   177  
   178  // Return standardized URL to be used to compare later.
   179  func getStandardizedURL(targetURL string) string {
   180  	return filepath.FromSlash(targetURL)
   181  }
   182  
   183  // statURL - uses combination of GET listing and HEAD to fetch information of one or more objects
   184  // HEAD can fail with 400 with an SSE-C encrypted object but we still return information gathered
   185  // from GET listing.
   186  func statURL(ctx context.Context, targetURL, versionID string, timeRef time.Time, includeOlderVersions, isIncomplete, isRecursive bool, encKeyDB map[string][]prefixSSEPair) *probe.Error {
   187  	clnt, err := newClient(targetURL)
   188  	if err != nil {
   189  		return err
   190  	}
   191  
   192  	targetAlias, _, _ := mustExpandAlias(targetURL)
   193  	prefixPath := clnt.GetURL().Path
   194  	separator := string(clnt.GetURL().Separator)
   195  
   196  	hasTrailingSlash := strings.HasSuffix(prefixPath, separator)
   197  
   198  	if !hasTrailingSlash {
   199  		prefixPath = prefixPath[:strings.LastIndex(prefixPath, separator)+1]
   200  	}
   201  
   202  	// if stat is on a bucket and non-recursive mode, serve the bucket metadata
   203  	if !isRecursive && !hasTrailingSlash {
   204  		bstat, err := clnt.GetBucketInfo(ctx)
   205  		if err == nil {
   206  			// Convert any os specific delimiters to "/".
   207  			contentURL := filepath.ToSlash(bstat.URL.Path)
   208  			prefixPath = filepath.ToSlash(prefixPath)
   209  			// Trim prefix path from the content path.
   210  			contentURL = strings.TrimPrefix(contentURL, prefixPath)
   211  			bstat.URL.Path = contentURL
   212  
   213  			if bstat.Date.IsZero() || bstat.Date.Equal(timeSentinel) {
   214  				bstat.Date = time.Now()
   215  			}
   216  
   217  			var bu madmin.BucketUsageInfo
   218  
   219  			adminClient, _ := newAdminClient(targetURL)
   220  			if adminClient != nil {
   221  				// Create a new MinIO Admin Client
   222  				duinfo, e := adminClient.DataUsageInfo(ctx)
   223  				if e == nil {
   224  					bu = duinfo.BucketsUsage[bstat.Key]
   225  				}
   226  			}
   227  
   228  			if prefixPath != "/" {
   229  				bstat.Prefix = true
   230  			}
   231  
   232  			printMsg(bucketInfoMessage{
   233  				Status:     "success",
   234  				BucketInfo: bstat,
   235  				Usage:      bu,
   236  			})
   237  
   238  			return nil
   239  		}
   240  	}
   241  
   242  	lstOptions := ListOptions{Recursive: isRecursive, Incomplete: isIncomplete, ShowDir: DirNone}
   243  	switch {
   244  	case versionID != "":
   245  		lstOptions.WithOlderVersions = true
   246  		lstOptions.WithDeleteMarkers = true
   247  	case !timeRef.IsZero(), includeOlderVersions:
   248  		lstOptions.WithOlderVersions = includeOlderVersions
   249  		lstOptions.WithDeleteMarkers = true
   250  		lstOptions.TimeRef = timeRef
   251  	}
   252  
   253  	var e error
   254  	for content := range clnt.List(ctx, lstOptions) {
   255  		if content.Err != nil {
   256  			switch content.Err.ToGoError().(type) {
   257  			// handle this specifically for filesystem related errors.
   258  			case BrokenSymlink:
   259  				errorIf(content.Err.Trace(clnt.GetURL().String()), "Unable to list broken link.")
   260  				continue
   261  			case TooManyLevelsSymlink:
   262  				errorIf(content.Err.Trace(clnt.GetURL().String()), "Unable to list too many levels link.")
   263  				continue
   264  			case PathNotFound:
   265  				errorIf(content.Err.Trace(clnt.GetURL().String()), "Unable to list folder.")
   266  				continue
   267  			case PathInsufficientPermission:
   268  				errorIf(content.Err.Trace(clnt.GetURL().String()), "Unable to list folder.")
   269  				continue
   270  			}
   271  			errorIf(content.Err.Trace(clnt.GetURL().String()), "Unable to list folder.")
   272  			e = exitStatus(globalErrorExitStatus) // Set the exit status.
   273  			continue
   274  		}
   275  
   276  		if content.StorageClass == s3StorageClassGlacier {
   277  			continue
   278  		}
   279  
   280  		url := targetAlias + getKey(content)
   281  		standardizedURL := getStandardizedURL(targetURL)
   282  
   283  		if !isRecursive && !strings.HasPrefix(filepath.FromSlash(url), standardizedURL) && !filepath.IsAbs(url) {
   284  			return errTargetNotFound(targetURL).Trace(url, standardizedURL)
   285  		}
   286  
   287  		if versionID != "" {
   288  			if versionID != content.VersionID {
   289  				continue
   290  			}
   291  		}
   292  		_, stat, err := url2Stat(ctx, url2StatOptions{urlStr: url, versionID: content.VersionID, fileAttr: true, encKeyDB: encKeyDB, timeRef: timeRef, isZip: false, ignoreBucketExistsCheck: false})
   293  		if err != nil {
   294  			continue
   295  		}
   296  
   297  		// Convert any os specific delimiters to "/".
   298  		contentURL := filepath.ToSlash(stat.URL.Path)
   299  		prefixPath = filepath.ToSlash(prefixPath)
   300  		// Trim prefix path from the content path.
   301  		contentURL = strings.TrimPrefix(contentURL, prefixPath)
   302  		stat.URL.Path = contentURL
   303  
   304  		printMsg(parseStat(stat))
   305  	}
   306  
   307  	return probe.NewError(e)
   308  }
   309  
   310  // BucketInfo holds info about a bucket
   311  type BucketInfo struct {
   312  	URL        ClientURL   `json:"-"`
   313  	Key        string      `json:"name"`
   314  	Date       time.Time   `json:"lastModified"`
   315  	Size       int64       `json:"size"`
   316  	Type       os.FileMode `json:"-"`
   317  	Prefix     bool        `json:"-"`
   318  	Versioning struct {
   319  		Status    string `json:"status"`
   320  		MFADelete string `json:"MFADelete"`
   321  	} `json:"Versioning,omitempty"`
   322  	Encryption struct {
   323  		Algorithm string `json:"algorithm,omitempty"`
   324  		KeyID     string `json:"keyId,omitempty"`
   325  	} `json:"Encryption,omitempty"`
   326  	Locking struct {
   327  		Enabled  string              `json:"enabled"`
   328  		Mode     minio.RetentionMode `json:"mode"`
   329  		Validity string              `json:"validity"`
   330  	} `json:"ObjectLock,omitempty"`
   331  	Replication struct {
   332  		Enabled bool               `json:"enabled"`
   333  		Config  replication.Config `json:"config,omitempty"`
   334  	} `json:"Replication"`
   335  	Policy struct {
   336  		Type string `json:"type"`
   337  		Text string `json:"policy,omitempty"`
   338  	} `json:"Policy,omitempty"`
   339  	Location string            `json:"location"`
   340  	Tagging  map[string]string `json:"tagging,omitempty"`
   341  	ILM      struct {
   342  		Config *lifecycle.Configuration `json:"config,omitempty"`
   343  	} `json:"ilm,omitempty"`
   344  	Notification struct {
   345  		Config notification.Configuration `json:"config,omitempty"`
   346  	} `json:"notification,omitempty"`
   347  }
   348  
   349  // Tags returns stringified tag list.
   350  func (i BucketInfo) Tags() string {
   351  	keys := []string{}
   352  	for key := range i.Tagging {
   353  		keys = append(keys, key)
   354  	}
   355  	sort.Strings(keys)
   356  
   357  	strs := []string{}
   358  	for _, key := range keys {
   359  		strs = append(
   360  			strs,
   361  			fmt.Sprintf("%v:%v", console.Colorize("Key", key), console.Colorize("Value", i.Tagging[key])),
   362  		)
   363  	}
   364  
   365  	return strings.Join(strs, ", ")
   366  }
   367  
   368  type bucketInfoMessage struct {
   369  	Status string `json:"status"`
   370  	BucketInfo
   371  	Usage madmin.BucketUsageInfo
   372  }
   373  
   374  func (v bucketInfoMessage) JSON() string {
   375  	v.Status = "success"
   376  	v.Key = getKey(&ClientContent{URL: v.URL, Type: v.Type})
   377  	var buf bytes.Buffer
   378  	enc := json.NewEncoder(&buf)
   379  	enc.SetIndent("", " ")
   380  	// Disable escaping special chars to display XML tags correctly
   381  	enc.SetEscapeHTML(false)
   382  
   383  	fatalIf(probe.NewError(enc.Encode(v)), "Unable to marshal into JSON.")
   384  	return buf.String()
   385  }
   386  
   387  type histogramDef struct {
   388  	start, end uint64
   389  	text       string
   390  }
   391  
   392  var histogramTagsDesc = map[string]histogramDef{
   393  	"LESS_THAN_1024_B":          {0, 1024, "less than 1024 bytes"},
   394  	"BETWEEN_1024_B_AND_1_MB":   {1024, 1024 * 1024, "between 1024 bytes and 1 MB"},
   395  	"BETWEEN_1_MB_AND_10_MB":    {1024 * 1024, 10 * 1024 * 1024, "between 1 MB and 10 MB"},
   396  	"BETWEEN_10_MB_AND_64_MB":   {10 * 1024 * 1024, 64 * 1024 * 1024, "between 10 MB and 64 MB"},
   397  	"BETWEEN_64_MB_AND_128_MB":  {64 * 1024 * 1024, 128 * 1024 * 1024, "between 64 MB and 128 MB"},
   398  	"BETWEEN_128_MB_AND_512_MB": {128 * 1024 * 1024, 512 * 1024 * 1024, "between 128 MB and 512 MB"},
   399  	"GREATER_THAN_512_MB":       {512 * 1024 * 1024, 0, "greater than 512 MB"},
   400  }
   401  
   402  // Return a sorted list of histograms
   403  func sortHistogramTags() (orderedTags []string) {
   404  	orderedTags = make([]string, 0, len(histogramTagsDesc))
   405  	for tag := range histogramTagsDesc {
   406  		orderedTags = append(orderedTags, tag)
   407  	}
   408  	sort.Slice(orderedTags, func(i, j int) bool {
   409  		return histogramTagsDesc[orderedTags[i]].start < histogramTagsDesc[orderedTags[j]].start
   410  	})
   411  	return
   412  }
   413  
   414  func countDigits(num uint64) (count uint) {
   415  	for num > 0 {
   416  		num /= 10
   417  		count++
   418  	}
   419  	return
   420  }
   421  
   422  func (v bucketInfoMessage) String() string {
   423  	var b strings.Builder
   424  
   425  	keyStr := getKey(&ClientContent{URL: v.URL, Type: v.Type})
   426  	keyStr = strings.TrimSuffix(keyStr, slashSeperator)
   427  	key := fmt.Sprintf("%-10s: %s", "Name", keyStr)
   428  	b.WriteString(console.Colorize("Title", key) + "\n")
   429  	if !v.Date.IsZero() && !v.Date.Equal(timeSentinel) {
   430  		b.WriteString(fmt.Sprintf("%-10s: %s ", "Date", v.Date.Format(printDate)) + "\n")
   431  	}
   432  	b.WriteString(fmt.Sprintf("%-10s: %-6s \n", "Size", "N/A"))
   433  
   434  	fType := func() string {
   435  		if v.Prefix {
   436  			return "prefix"
   437  		}
   438  		if v.Type.IsDir() {
   439  			return "folder"
   440  		}
   441  		return "file"
   442  	}()
   443  	b.WriteString(fmt.Sprintf("%-10s: %s \n", "Type", fType))
   444  	fmt.Fprintf(&b, "\n")
   445  
   446  	if !v.Prefix {
   447  		fmt.Fprint(&b, console.Colorize("Title", "Properties:\n"))
   448  		fmt.Fprint(&b, prettyPrintBucketMetadata(v.BucketInfo))
   449  		fmt.Fprintf(&b, "\n")
   450  	}
   451  
   452  	fmt.Fprint(&b, console.Colorize("Title", "Usage:\n"))
   453  
   454  	fmt.Fprintf(&b, "%16s: %s\n", "Total size", console.Colorize("Count", humanize.IBytes(v.Usage.Size)))
   455  	fmt.Fprintf(&b, "%16s: %s\n", "Objects count", console.Colorize("Count", humanize.Comma(int64(v.Usage.ObjectsCount))))
   456  	fmt.Fprintf(&b, "%16s: %s\n", "Versions count", console.Colorize("Count", humanize.Comma(int64(v.Usage.VersionsCount))))
   457  	fmt.Fprintf(&b, "\n")
   458  
   459  	if len(v.Usage.ObjectSizesHistogram) > 0 {
   460  		fmt.Fprint(&b, console.Colorize("Title", "Object sizes histogram:\n"))
   461  
   462  		var maxDigits uint
   463  		for _, val := range v.Usage.ObjectSizesHistogram {
   464  			if d := countDigits(val); d > maxDigits {
   465  				maxDigits = d
   466  			}
   467  		}
   468  
   469  		sortedTags := sortHistogramTags()
   470  		for _, tagName := range sortedTags {
   471  			val, ok := v.Usage.ObjectSizesHistogram[tagName]
   472  			if ok {
   473  				fmt.Fprintf(&b, "   %*d object(s) %s\n", maxDigits, val, histogramTagsDesc[tagName].text)
   474  			}
   475  		}
   476  	}
   477  
   478  	return b.String()
   479  }
   480  
   481  // Pretty print bucket configuration - used by stat and admin bucket info as well
   482  func prettyPrintBucketMetadata(info BucketInfo) string {
   483  	var b strings.Builder
   484  	placeHolder := ""
   485  	if info.Encryption.Algorithm != "" {
   486  		fmt.Fprintf(&b, "%2s%s", placeHolder, "Encryption: ")
   487  		if info.Encryption.Algorithm == "aws:kms" {
   488  			fmt.Fprint(&b, console.Colorize("Key", "\n\tKey Type: "))
   489  			fmt.Fprint(&b, console.Colorize("Value", "SSE-KMS"))
   490  			fmt.Fprint(&b, console.Colorize("Key", "\n\tKey ID: "))
   491  			fmt.Fprint(&b, console.Colorize("Value", info.Encryption.KeyID))
   492  		} else {
   493  			fmt.Fprint(&b, console.Colorize("Key", "\n\tKey Type: "))
   494  			fmt.Fprint(&b, console.Colorize("Value", strings.ToUpper(info.Encryption.Algorithm)))
   495  		}
   496  		fmt.Fprintln(&b)
   497  	}
   498  	fmt.Fprintf(&b, "%2s%s", placeHolder, "Versioning: ")
   499  	if info.Versioning.Status == "" {
   500  		fmt.Fprint(&b, console.Colorize("Unset", "Un-versioned"))
   501  	} else {
   502  		fmt.Fprint(&b, console.Colorize("Set", info.Versioning.Status))
   503  	}
   504  	fmt.Fprintln(&b)
   505  
   506  	if info.Locking.Mode != "" {
   507  		fmt.Fprintf(&b, "%2s%s\n", placeHolder, "LockConfiguration: ")
   508  		fmt.Fprintf(&b, "%4s%s", placeHolder, "RetentionMode: ")
   509  		fmt.Fprint(&b, console.Colorize("Value", info.Locking.Mode))
   510  		fmt.Fprintln(&b)
   511  		fmt.Fprintf(&b, "%4s%s", placeHolder, "Retention Until Date: ")
   512  		fmt.Fprint(&b, console.Colorize("Value", info.Locking.Validity))
   513  		fmt.Fprintln(&b)
   514  	}
   515  	if len(info.Notification.Config.TopicConfigs) > 0 {
   516  		fmt.Fprintf(&b, "%2s%s", placeHolder, "Notification: ")
   517  		fmt.Fprint(&b, console.Colorize("Set", "Set"))
   518  		fmt.Fprintln(&b)
   519  	}
   520  	if info.Replication.Enabled {
   521  		fmt.Fprintf(&b, "%2s%s", placeHolder, "Replication: ")
   522  		fmt.Fprint(&b, console.Colorize("Set", "Enabled"))
   523  		fmt.Fprintln(&b)
   524  	}
   525  	fmt.Fprintf(&b, "%2s%s", placeHolder, "Location: ")
   526  	fmt.Fprint(&b, console.Colorize("Generic", info.Location))
   527  	fmt.Fprintln(&b)
   528  	fmt.Fprintf(&b, "%2s%s", placeHolder, "Anonymous: ")
   529  	if info.Policy.Type == "none" {
   530  		fmt.Fprint(&b, console.Colorize("UnSet", "Disabled"))
   531  	} else {
   532  		fmt.Fprint(&b, console.Colorize("Set", "Enabled"))
   533  	}
   534  	fmt.Fprintln(&b)
   535  	if info.Tags() != "" {
   536  		fmt.Fprintf(&b, "%2s%s", placeHolder, "Tagging: ")
   537  		fmt.Fprint(&b, console.Colorize("Generic", info.Tags()))
   538  		fmt.Fprintln(&b)
   539  	}
   540  	fmt.Fprintf(&b, "%2s%s", placeHolder, "ILM: ")
   541  	if info.ILM.Config != nil {
   542  		fmt.Fprint(&b, console.Colorize("Set", "Enabled"))
   543  	} else {
   544  		fmt.Fprint(&b, console.Colorize("UnSet", "Disabled"))
   545  	}
   546  	fmt.Fprintln(&b)
   547  
   548  	return b.String()
   549  }