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 }