github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/ilm-restore.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 "time" 25 26 "github.com/minio/cli" 27 json "github.com/minio/colorjson" 28 "github.com/minio/mc/pkg/probe" 29 ) 30 31 // ilm restore specific flags. 32 var ( 33 ilmRestoreFlags = []cli.Flag{ 34 cli.IntFlag{ 35 Name: "days", 36 Value: 1, 37 Usage: "keep the restored copy for N days", 38 }, 39 cli.BoolFlag{ 40 Name: "recursive, r", 41 Usage: "apply recursively", 42 }, 43 cli.BoolFlag{ 44 Name: "versions", 45 Usage: "apply on versions", 46 }, 47 cli.StringFlag{ 48 Name: "version-id, vid", 49 Usage: "select a specific version id", 50 }, 51 } 52 ) 53 54 var ilmRestoreCmd = cli.Command{ 55 Name: "restore", 56 Usage: "restore archived objects", 57 Action: mainILMRestore, 58 OnUsageError: onUsageError, 59 Before: setGlobalsFromContext, 60 Flags: append(append(ilmRestoreFlags, encCFlag), globalFlags...), 61 CustomHelpTemplate: `NAME: 62 {{.HelpName}} - {{.Usage}} 63 64 USAGE: 65 {{.HelpName}} TARGET 66 67 DESCRIPTION: 68 Restore a copy of one or more objects from its remote tier. This copy automatically expires 69 after the specified number of days (Default 1 day). 70 71 FLAGS: 72 {{range .VisibleFlags}}{{.}} 73 {{end}} 74 75 EXAMPLES: 76 1. Restore one specific object 77 {{.Prompt}} {{.HelpName}} myminio/mybucket/path/to/object 78 79 2. Restore a specific object version 80 {{.Prompt}} {{.HelpName}} --vid "CL3sWgdSN2pNntSf6UnZAuh2kcu8E8si" myminio/mybucket/path/to/object 81 82 3. Restore all objects under a specific prefix 83 {{.Prompt}} {{.HelpName}} --recursive myminio/mybucket/dir/ 84 85 4. Restore all objects with all versions under a specific prefix 86 {{.Prompt}} {{.HelpName}} --recursive --versions myminio/mybucket/dir/ 87 88 5. Restore an SSE-C encrypted object. 89 {{.Prompt}} {{.HelpName}} --enc-c "myminio/mybucket/=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" myminio/mybucket/myobject.txt 90 `, 91 } 92 93 // checkILMRestoreSyntax - validate arguments passed by user 94 func checkILMRestoreSyntax(ctx *cli.Context) { 95 if len(ctx.Args()) != 1 { 96 showCommandHelpAndExit(ctx, globalErrorExitStatus) 97 } 98 99 if ctx.Int("days") <= 0 { 100 fatalIf(errDummy().Trace(), "--days should be equal or greater than 1") 101 } 102 103 if ctx.Bool("version-id") && (ctx.Bool("recursive") || ctx.Bool("versions")) { 104 fatalIf(errDummy().Trace(), "You cannot combine --version-id with --recursive or --versions flags.") 105 } 106 } 107 108 // Send Restore S3 API 109 func restoreObject(ctx context.Context, targetAlias, targetURL, versionID string, days int) *probe.Error { 110 clnt, err := newClientFromAlias(targetAlias, targetURL) 111 if err != nil { 112 return err 113 } 114 115 return clnt.Restore(ctx, versionID, days) 116 } 117 118 // Send restore S3 API request to one or more objects depending on the arguments 119 func sendRestoreRequests(ctx context.Context, targetAlias, targetURL, targetVersionID string, recursive, applyOnVersions bool, days int, restoreSentReq chan *probe.Error) { 120 defer close(restoreSentReq) 121 122 client, err := newClientFromAlias(targetAlias, targetURL) 123 if err != nil { 124 restoreSentReq <- err 125 return 126 } 127 128 if !recursive { 129 err := restoreObject(ctx, targetAlias, targetURL, targetVersionID, days) 130 restoreSentReq <- err 131 return 132 } 133 134 prev := "" 135 for content := range client.List(ctx, ListOptions{ 136 Recursive: true, 137 WithOlderVersions: applyOnVersions, 138 ShowDir: DirNone, 139 }) { 140 if content.Err != nil { 141 errorIf(content.Err.Trace(client.GetURL().String()), "Unable to list folder.") 142 continue 143 } 144 err := restoreObject(ctx, targetAlias, content.URL.String(), content.VersionID, days) 145 if err != nil { 146 restoreSentReq <- err 147 continue 148 } 149 // Avoid sending the status of each separate version 150 // of the same object name. 151 if prev != content.URL.String() { 152 prev = content.URL.String() 153 restoreSentReq <- nil 154 } 155 } 156 } 157 158 // Wait until an object which receives restore request is completely restored in the fast tier 159 func waitRestoreObject(ctx context.Context, targetAlias, targetURL, versionID string, encKeyDB map[string][]prefixSSEPair) *probe.Error { 160 clnt, err := newClientFromAlias(targetAlias, targetURL) 161 if err != nil { 162 return err 163 } 164 165 for { 166 opts := StatOptions{ 167 versionID: versionID, 168 sse: getSSE(targetAlias+clnt.GetURL().Path, encKeyDB[targetAlias]), 169 } 170 st, err := clnt.Stat(ctx, opts) 171 if err != nil { 172 return err 173 } 174 if st.Restore == nil { 175 return probe.NewError(fmt.Errorf("`%s` did not receive restore request", targetURL)) 176 } 177 if st.Restore != nil && !st.Restore.OngoingRestore { 178 return nil 179 } 180 // Restore still going on, wait for 5 seconds before checking again 181 time.Sleep(5 * time.Second) 182 } 183 } 184 185 // Check and wait the restore status of one or more objects one by one. 186 func checkRestoreStatus(ctx context.Context, targetAlias, targetURL, targetVersionID string, recursive, applyOnVersions bool, encKeyDB map[string][]prefixSSEPair, restoreStatus chan *probe.Error) { 187 defer close(restoreStatus) 188 189 client, err := newClientFromAlias(targetAlias, targetURL) 190 if err != nil { 191 restoreStatus <- err 192 return 193 } 194 195 if !recursive { 196 restoreStatus <- waitRestoreObject(ctx, targetAlias, targetURL, targetVersionID, encKeyDB) 197 return 198 } 199 200 prev := "" 201 for content := range client.List(ctx, ListOptions{ 202 Recursive: true, 203 WithOlderVersions: applyOnVersions, 204 ShowDir: DirNone, 205 }) { 206 if content.Err != nil { 207 restoreStatus <- content.Err 208 continue 209 } 210 211 err := waitRestoreObject(ctx, targetAlias, content.URL.String(), content.VersionID, encKeyDB) 212 if err != nil { 213 restoreStatus <- err 214 continue 215 } 216 217 if prev != content.URL.String() { 218 prev = content.URL.String() 219 restoreStatus <- nil 220 } 221 } 222 } 223 224 var dotCycle = 0 225 226 // Clear and print text in the same line 227 func printStatus(msg string, args ...interface{}) { 228 if globalJSON { 229 return 230 } 231 232 dotCycle++ 233 dots := bytes.Repeat([]byte{'.'}, dotCycle%3+1) 234 fmt.Print("\n\033[1A\033[K") 235 fmt.Printf(msg+string(dots), args...) 236 } 237 238 // Receive restore request & restore finished status and print in the console 239 func showRestoreStatus(restoreReqStatus, restoreFinishedStatus chan *probe.Error, doneCh chan struct{}) { 240 var sent, finished int 241 var done bool 242 243 ticker := time.NewTicker(1 * time.Second) 244 defer ticker.Stop() 245 246 for !done { 247 select { 248 case err, ok := <-restoreReqStatus: 249 if !ok { 250 done = true 251 break 252 } 253 errorIf(err.Trace(), "Unable to send restore request.") 254 if err == nil { 255 sent++ 256 } 257 case <-ticker.C: 258 } 259 260 printStatus("Sent restore requests to %d object(s)", sent) 261 } 262 263 if !globalJSON { 264 fmt.Println("") 265 } 266 267 done = false 268 269 for !done { 270 select { 271 case err, ok := <-restoreFinishedStatus: 272 if !ok { 273 done = true 274 break 275 } 276 errorIf(err.Trace(), "Unable to check for restore status") 277 if err == nil { 278 finished++ 279 } 280 case <-ticker.C: 281 } 282 printStatus("%d/%d object(s) successfully restored", finished, sent) 283 } 284 285 if !globalJSON { 286 fmt.Println("") 287 } else { 288 type ilmRestore struct { 289 Status string `json:"status"` 290 Restored int `json:"restored"` 291 } 292 293 msgBytes, _ := json.Marshal(ilmRestore{Status: "success", Restored: sent}) 294 fmt.Println(string(msgBytes)) 295 } 296 297 close(doneCh) 298 } 299 300 func mainILMRestore(cliCtx *cli.Context) (cErr error) { 301 ctx, cancelILMRestore := context.WithCancel(globalContext) 302 defer cancelILMRestore() 303 304 checkILMRestoreSyntax(cliCtx) 305 306 args := cliCtx.Args() 307 aliasedURL := args.Get(0) 308 309 versionID := cliCtx.String("version-id") 310 recursive := cliCtx.Bool("recursive") 311 includeVersions := cliCtx.Bool("versions") 312 days := cliCtx.Int("days") 313 314 encKeyDB, err := validateAndCreateEncryptionKeys(cliCtx) 315 fatalIf(err, "Unable to parse encryption keys.") 316 317 targetAlias, targetURL, _ := mustExpandAlias(aliasedURL) 318 if targetAlias == "" { 319 fatalIf(errDummy().Trace(), "Unable to restore the given URL") 320 } 321 322 restoreReqStatus := make(chan *probe.Error) 323 restoreStatus := make(chan *probe.Error) 324 325 done := make(chan struct{}) 326 327 go func() { 328 showRestoreStatus(restoreReqStatus, restoreStatus, done) 329 }() 330 331 sendRestoreRequests(ctx, targetAlias, targetURL, versionID, recursive, includeVersions, days, restoreReqStatus) 332 checkRestoreStatus(ctx, targetAlias, targetURL, versionID, recursive, includeVersions, encKeyDB, restoreStatus) 333 334 // Wait until the UI printed all the status 335 <-done 336 337 return nil 338 }