1
0
mirror of https://github.com/minio/mc.git synced 2025-04-19 21:02:15 +03:00
mc/cmd/undo-main.go
Klaus Post 4d498deb35
Fix non-recursive compares against alias+key on Windows (#4983)
Many listings would stop after the first version had been processed on Windows, since

For example, `mytls/testbucket/trace.json` != `mytls\testbucket\trace.json` would cause `mc undo` to only see one version since this would fail.

Use `getStandardizedURL` for both sides of these compares.
2024-07-11 11:01:28 -07:00

309 lines
8.7 KiB
Go

// Copyright (c) 2015-2022 MinIO, Inc.
//
// This file is part of MinIO Object Storage stack
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package cmd
import (
"context"
"errors"
"fmt"
"path/filepath"
"strings"
"github.com/fatih/color"
"github.com/minio/cli"
json "github.com/minio/colorjson"
"github.com/minio/mc/pkg/probe"
"github.com/minio/pkg/v3/console"
)
const (
actionPut = "PUT"
actionDelete = "DELETE"
)
var undoFlags = []cli.Flag{
cli.IntFlag{
Name: "last",
Usage: "undo N last changes",
Value: 1,
},
cli.BoolFlag{
Name: "recursive, r",
Usage: "undo last S3 PUT/DELETE operations recursively",
},
cli.BoolFlag{
Name: "force",
Usage: "force recursive operation",
},
cli.BoolFlag{
Name: "dry-run",
Usage: "fake an undo operation",
},
cli.StringFlag{
Name: "action",
Usage: "undo only if the latest version is of the following type [PUT/DELETE]",
},
}
var undoCmd = cli.Command{
Name: "undo",
Usage: "undo PUT/DELETE operations",
Action: mainUndo,
OnUsageError: onUsageError,
Before: setGlobalsFromContext,
Flags: append(undoFlags, globalFlags...),
CustomHelpTemplate: `NAME:
{{.HelpName}} - {{.Usage}}
USAGE:
{{.HelpName}} [FLAGS] TARGET
FLAGS:
{{range .VisibleFlags}}{{.}}
{{end}}
EXAMPLES:
1. Undo the last 3 uploads and/or removals of a particular object
{{.Prompt}} {{.HelpName}} s3/backups/file.zip --last 3
2. Undo the last upload/removal change of all objects under a prefix
{{.Prompt}} {{.HelpName}} s3/backups/prefix/ --recursive --force
`,
}
// undoMessage container for undo message structure.
type undoMessage struct {
Status string `json:"status"`
URL string `json:"url,omitempty"`
Key string `json:"key,omitempty"`
VersionID string `json:"versionId,omitempty"`
IsDeleteMarker bool `json:"isDeleteMarker,omitempty"`
}
// String colorized string message.
func (c undoMessage) String() string {
var msg string
fmt.Print(color.GreenString("\u2713 "))
yellow := color.New(color.FgYellow).SprintFunc()
if c.IsDeleteMarker {
msg += "Last " + color.RedString("delete") + " of `" + yellow(c.Key) + "` is reverted"
} else {
msg += "Last " + color.BlueString("upload") + " of `" + yellow(c.Key) + "` (vid=" + c.VersionID + ") is reverted"
}
msg += "."
return msg
}
// JSON jsonified content message.
func (c undoMessage) JSON() string {
c.Status = "success"
jsonMessageBytes, e := json.MarshalIndent(c, "", " ")
fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
return string(jsonMessageBytes)
}
// parseUndoSyntax performs command-line input validation for cat command.
func parseUndoSyntax(ctx *cli.Context) (targetAliasedURL string, last int, recursive, dryRun bool, action string) {
targetAliasedURL = ctx.Args().Get(0)
if targetAliasedURL == "" {
fatalIf(errInvalidArgument().Trace(), "The argument should not be empty")
}
last = ctx.Int("last")
if last < 1 {
fatalIf(errInvalidArgument().Trace(), "--last value should be a positive integer")
}
recursive = ctx.Bool("recursive")
force := ctx.Bool("force")
if recursive && !force {
fatalIf(errInvalidArgument().Trace(), "This is a dangerous operation, you need to provide --force flag as well")
}
dryRun = ctx.Bool("dry-run")
action = strings.ToUpper(ctx.String("action"))
if action != actionPut && action != actionDelete && action != "" {
fatalIf(errInvalidArgument().Trace(), "unsupported action specified, supported actions are PUT, DELETE or empty (default)")
}
if (action == actionPut || action == actionDelete) && last != 1 {
fatalIf(errInvalidArgument().Trace(), "--action if specified requires that you must specify --last=1")
}
return
}
func undoLastNOperations(ctx context.Context, clnt Client, objectVersions []*ClientContent, last int, dryRun bool) (exitErr error) {
if last == 0 {
return
}
sortObjectVersions(objectVersions)
if len(objectVersions) > last {
objectVersions = objectVersions[:last]
}
contentCh := make(chan *ClientContent)
resultCh := clnt.Remove(ctx, false, false, false, false, contentCh)
prefixPath := clnt.GetURL().Path
prefixPath = filepath.ToSlash(prefixPath)
if !strings.HasSuffix(prefixPath, "/") {
prefixPath = prefixPath[:strings.LastIndex(prefixPath, "/")+1]
}
prefixPath = strings.TrimPrefix(prefixPath, "./")
go func() {
for _, objectVersion := range objectVersions {
if !dryRun {
contentCh <- objectVersion
}
// Convert any os specific delimiters to "/".
contentURL := filepath.ToSlash(objectVersion.URL.Path)
// Trim prefix path from the content path.
keyName := strings.TrimPrefix(contentURL, prefixPath)
printMsg(undoMessage{
Status: "success",
Key: getOSDependantKey(keyName, objectVersion.Type.IsDir()),
URL: objectVersion.URL.String(),
VersionID: objectVersion.VersionID,
IsDeleteMarker: objectVersion.IsDeleteMarker,
})
}
close(contentCh)
}()
for result := range resultCh {
if result.Err != nil {
errorIf(result.Err.Trace(), "Unable to undo")
exitErr = exitStatus(globalErrorExitStatus) // Set the exit status.
}
}
return
}
func undoURL(ctx context.Context, aliasedURL string, last int, recursive, dryRun bool, action string) (exitErr error) {
clnt, err := newClient(aliasedURL)
fatalIf(err.Trace(aliasedURL), "Unable to initialize target `"+aliasedURL+"`.")
alias, _, _ := mustExpandAlias(aliasedURL)
var (
lastObjectPath string
perObjectVersions []*ClientContent
atLeastOneUndoApplied bool
)
remove := true
for content := range clnt.List(ctx, ListOptions{
Recursive: recursive,
WithOlderVersions: true,
WithDeleteMarkers: true,
ShowDir: DirNone,
}) {
if content.Err != nil {
fatalIf(content.Err.Trace(clnt.GetURL().String()), "Unable to list folder.")
}
if content.StorageClass == s3StorageClassGlacier {
continue
}
if !recursive {
if getStandardizedURL(alias+getKey(content)) != getStandardizedURL(aliasedURL) {
break
}
}
if lastObjectPath != content.URL.Path {
// Print any object in the current list before reinitializing it
if remove {
exitErr = undoLastNOperations(ctx, clnt, perObjectVersions, last, dryRun)
}
remove = true
lastObjectPath = content.URL.Path
perObjectVersions = []*ClientContent{}
}
if !remove {
continue
}
if (content.IsLatest && action == actionDelete && !content.IsDeleteMarker) || (content.IsLatest && action == actionPut && content.IsDeleteMarker) {
remove = false
continue
}
perObjectVersions = append(perObjectVersions, content)
atLeastOneUndoApplied = true
}
// Undo the remaining versions found if any
if len(perObjectVersions) > 0 && remove {
exitErr = undoLastNOperations(ctx, clnt, perObjectVersions, last, dryRun)
}
if !atLeastOneUndoApplied {
errorIf(errDummy().Trace(clnt.GetURL().String()), "Unable to find any object version to undo.")
exitErr = exitStatus(globalErrorExitStatus) // Set the exit status.
}
return
}
func checkIfBucketIsVersioned(ctx context.Context, aliasedURL string) (versioned bool) {
client, err := newClient(aliasedURL)
fatalIf(err, "Unable to parse `%s`", aliasedURL)
versioningConfig, err := client.GetVersion(ctx)
if err != nil {
if errors.As(err.ToGoError(), &APINotImplemented{}) {
return false
}
fatalIf(err.Trace(), "Unable to get bucket versioning info")
}
if versioningConfig.Status == "Enabled" {
return true
}
return false
}
func checkUndoSyntax(cliCtx *cli.Context) {
if !cliCtx.Args().Present() {
showCommandHelpAndExit(cliCtx, 1)
}
}
// mainUndo is the main entry point for undo command.
func mainUndo(cliCtx *cli.Context) error {
checkUndoSyntax(cliCtx)
ctx, cancelCat := context.WithCancel(globalContext)
defer cancelCat()
console.SetColor("Success", color.New(color.FgGreen, color.Bold))
// check 'undo' cli arguments.
targetAliasedURL, last, recursive, dryRun, action := parseUndoSyntax(cliCtx)
if !checkIfBucketIsVersioned(ctx, targetAliasedURL) {
fatalIf(errDummy().Trace(), "Undo command works only with S3 versioned-enabled buckets.")
}
return undoURL(ctx, targetAliasedURL, last, recursive, dryRun, action)
}