github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/admin-cluster-bucket-import.go (about)

     1  // Copyright (c) 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  	"io"
    25  	"os"
    26  	"path/filepath"
    27  	"strings"
    28  
    29  	"github.com/fatih/color"
    30  	"github.com/klauspost/compress/zip"
    31  	"github.com/minio/cli"
    32  	json "github.com/minio/colorjson"
    33  	"github.com/minio/madmin-go/v3"
    34  	"github.com/minio/mc/pkg/probe"
    35  	"github.com/minio/pkg/v2/console"
    36  )
    37  
    38  var adminClusterBucketImportCmd = cli.Command{
    39  	Name:            "import",
    40  	Usage:           "restore bucket metadata from a zip file",
    41  	Action:          mainClusterBucketImport,
    42  	OnUsageError:    onUsageError,
    43  	Before:          setGlobalsFromContext,
    44  	Flags:           globalFlags,
    45  	HideHelpCommand: true,
    46  	CustomHelpTemplate: `NAME:
    47    {{.HelpName}} - {{.Usage}}
    48  
    49  USAGE:
    50    {{.HelpName}} [FLAGS] TARGET/[BUCKET] /path/to/backups/bucket-metadata.zip
    51  
    52  FLAGS:
    53    {{range .VisibleFlags}}{{.}}
    54    {{end}}
    55  EXAMPLES:
    56    1. Recover bucket metadata for all buckets from previously saved bucket metadata backup.
    57       {{.Prompt}} {{.HelpName}} myminio /backups/myminio-bucket-metadata.zip
    58  `,
    59  }
    60  
    61  func checkBucketImportSyntax(ctx *cli.Context) {
    62  	if len(ctx.Args()) != 2 {
    63  		showCommandHelpAndExit(ctx, 1) // last argument is exit code
    64  	}
    65  }
    66  
    67  // mainClusterBucketImport - bucket metadata import command
    68  func mainClusterBucketImport(ctx *cli.Context) error {
    69  	// Check for command syntax
    70  	checkBucketImportSyntax(ctx)
    71  	console.SetColor("Name", color.New(color.Bold, color.FgCyan))
    72  	console.SetColor("success", color.New(color.Bold, color.FgGreen))
    73  	console.SetColor("warning", color.New(color.Bold, color.FgYellow))
    74  	console.SetColor("errors", color.New(color.Bold, color.FgRed))
    75  	console.SetColor("statusMsg", color.New(color.Bold, color.FgHiWhite))
    76  	console.SetColor("failCell", color.New(color.FgRed))
    77  	console.SetColor("passCell", color.New(color.FgGreen))
    78  
    79  	// Get the alias parameter from cli
    80  	args := ctx.Args()
    81  	aliasedURL := args.Get(0)
    82  	var r io.Reader
    83  	var sz int64
    84  	f, e := os.Open(args.Get(1))
    85  	if e != nil {
    86  		fatalIf(probe.NewError(e).Trace(args...), "Unable to get bucket metadata")
    87  	}
    88  	if st, e := f.Stat(); e == nil {
    89  		sz = st.Size()
    90  	}
    91  	defer f.Close()
    92  	r = f
    93  
    94  	_, e = zip.NewReader(r.(io.ReaderAt), sz)
    95  	fatalIf(probe.NewError(e).Trace(args...), fmt.Sprintf("Unable to read zip file %s", args.Get(1)))
    96  
    97  	f, e = os.Open(args.Get(1))
    98  	fatalIf(probe.NewError(e).Trace(args...), "Unable to get bucket metadata")
    99  
   100  	// Create a new MinIO Admin Client
   101  	client, err := newAdminClient(aliasedURL)
   102  	if err != nil {
   103  		fatalIf(err.Trace(aliasedURL), "Unable to initialize admin client.")
   104  		return nil
   105  	}
   106  
   107  	// Compute bucket and object from the aliased URL
   108  	aliasedURL = filepath.ToSlash(aliasedURL)
   109  	aliasedURL = filepath.Clean(aliasedURL)
   110  	_, bucket := url2Alias(aliasedURL)
   111  
   112  	rpt, e := client.ImportBucketMetadata(context.Background(), bucket, f)
   113  	fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to import bucket metadata.")
   114  
   115  	printMsg(importMetaMsg{
   116  		BucketMetaImportErrs: rpt,
   117  		Status:               "success",
   118  		URL:                  aliasedURL,
   119  		Op:                   ctx.Command.Name,
   120  	})
   121  
   122  	return nil
   123  }
   124  
   125  type importMetaMsg struct {
   126  	madmin.BucketMetaImportErrs
   127  	Op     string
   128  	URL    string `json:"url"`
   129  	Status string `json:"status"`
   130  }
   131  
   132  func statusTick(s madmin.MetaStatus) string {
   133  	switch {
   134  	case s.Err != "":
   135  		return console.Colorize("failCell", crossTickCell)
   136  	case !s.IsSet:
   137  		return blankCell
   138  	default:
   139  		return console.Colorize("passCell", tickCell)
   140  	}
   141  }
   142  
   143  func (i importMetaMsg) String() string {
   144  	m := i.BucketMetaImportErrs.Buckets
   145  	totBuckets := len(m)
   146  	totErrs := 0
   147  	for _, st := range m {
   148  		if st.ObjectLock.Err != "" || st.Versioning.Err != "" ||
   149  			st.SSEConfig.Err != "" || st.Tagging.Err != "" ||
   150  			st.Lifecycle.Err != "" || st.Quota.Err != "" ||
   151  			st.Policy.Err != "" || st.Notification.Err != "" {
   152  			totErrs++
   153  		}
   154  	}
   155  	var b strings.Builder
   156  	numSch := "success"
   157  	if totErrs > 0 {
   158  		numSch = "warning"
   159  	}
   160  	msg := "\n" + console.Colorize(numSch, totBuckets-totErrs) +
   161  		console.Colorize("statusMsg", "/") +
   162  		console.Colorize("success", totBuckets) +
   163  		console.Colorize("statusMsg", " buckets were imported successfully.")
   164  	fmt.Fprintln(&b, msg)
   165  	if totErrs > 0 {
   166  		fmt.Fprintln(&b, console.Colorize("errors", "Errors: \n"))
   167  		for bucket, st := range m {
   168  			if st.ObjectLock.Err != "" || st.Versioning.Err != "" ||
   169  				st.SSEConfig.Err != "" || st.Tagging.Err != "" ||
   170  				st.Lifecycle.Err != "" || st.Quota.Err != "" ||
   171  				st.Policy.Err != "" || st.Notification.Err != "" {
   172  				fmt.Fprintln(&b, printImportErrs(bucket, st))
   173  			}
   174  		}
   175  	}
   176  	return b.String()
   177  }
   178  
   179  func (i importMetaMsg) JSON() string {
   180  	buf := &bytes.Buffer{}
   181  	enc := json.NewEncoder(buf)
   182  	enc.SetIndent("", " ")
   183  	// Disable escaping special chars to display XML tags correctly
   184  	enc.SetEscapeHTML(false)
   185  
   186  	fatalIf(probe.NewError(enc.Encode(i.BucketMetaImportErrs.Buckets)), "Unable to marshal into JSON.")
   187  	return buf.String()
   188  }
   189  
   190  // pretty print import errors
   191  func printImportErrs(bucket string, r madmin.BucketStatus) string {
   192  	var b strings.Builder
   193  	placeHolder := ""
   194  	key := fmt.Sprintf("%-10s: %s", "Name", bucket)
   195  	fmt.Fprintln(&b, console.Colorize("Name", key))
   196  
   197  	if r.ObjectLock.IsSet {
   198  		fmt.Fprintf(&b, "%2s%s %s", placeHolder, "Object lock: ", statusTick(r.ObjectLock))
   199  		fmt.Fprintln(&b)
   200  	}
   201  	if r.Versioning.IsSet {
   202  		fmt.Fprintf(&b, "%2s%s %s", placeHolder, "Versioning: ", statusTick(r.Versioning))
   203  		fmt.Fprintln(&b)
   204  	}
   205  
   206  	if r.SSEConfig.IsSet {
   207  		fmt.Fprintf(&b, "%2s%s %s", placeHolder, "Encryption: ", statusTick(r.SSEConfig))
   208  		fmt.Fprintln(&b)
   209  	}
   210  	if r.Lifecycle.IsSet {
   211  		fmt.Fprintf(&b, "%2s%s %s", placeHolder, "Lifecycle: ", statusTick(r.Lifecycle))
   212  		fmt.Fprintln(&b)
   213  	}
   214  	if r.Notification.IsSet {
   215  		fmt.Fprintf(&b, "%2s%s %s", placeHolder, "Notification: ", statusTick(r.Notification))
   216  		fmt.Fprintln(&b)
   217  	}
   218  	if r.Quota.IsSet {
   219  		fmt.Fprintf(&b, "%2s%s %s", placeHolder, "Quota: ", statusTick(r.Quota))
   220  		fmt.Fprintln(&b)
   221  	}
   222  	if r.Policy.IsSet {
   223  		fmt.Fprintf(&b, "%2s%s %s", placeHolder, "Policy: ", statusTick(r.Policy))
   224  		fmt.Fprintln(&b)
   225  	}
   226  	if r.Tagging.IsSet {
   227  		fmt.Fprintf(&b, "%2s%s %s", placeHolder, "Tagging: ", statusTick(r.Tagging))
   228  		fmt.Fprintln(&b)
   229  	}
   230  	return b.String()
   231  }