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  }