github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/cat-main.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  	"errors"
    24  	"fmt"
    25  	"io"
    26  	"os"
    27  	"strings"
    28  	"syscall"
    29  	"time"
    30  	"unicode"
    31  	"unicode/utf8"
    32  
    33  	"github.com/minio/cli"
    34  	"github.com/minio/mc/pkg/probe"
    35  )
    36  
    37  var catFlags = []cli.Flag{
    38  	cli.StringFlag{
    39  		Name:  "rewind",
    40  		Usage: "display an earlier object version",
    41  	},
    42  	cli.StringFlag{
    43  		Name:  "version-id, vid",
    44  		Usage: "display a specific version of an object",
    45  	},
    46  	cli.BoolFlag{
    47  		Name:  "zip",
    48  		Usage: "extract from remote zip file (MinIO server source only)",
    49  	},
    50  	cli.Int64Flag{
    51  		Name:  "offset",
    52  		Usage: "start offset",
    53  	},
    54  	cli.Int64Flag{
    55  		Name:  "tail",
    56  		Usage: "tail number of bytes at ending of file",
    57  	},
    58  }
    59  
    60  // Display contents of a file.
    61  var catCmd = cli.Command{
    62  	Name:         "cat",
    63  	Usage:        "display object contents",
    64  	Action:       mainCat,
    65  	OnUsageError: onUsageError,
    66  	Before:       setGlobalsFromContext,
    67  	Flags:        append(append(catFlags, encCFlag), globalFlags...),
    68  	CustomHelpTemplate: `NAME:
    69    {{.HelpName}} - {{.Usage}}
    70  
    71  USAGE:
    72    {{.HelpName}} [FLAGS] TARGET [TARGET...]
    73  
    74  FLAGS:
    75    {{range .VisibleFlags}}{{.}}
    76    {{end}}
    77  
    78  EXAMPLES:
    79    1. Stream an object from Amazon S3 cloud storage to mplayer standard input.
    80       {{.Prompt}} {{.HelpName}} s3/mysql-backups/kubecon-mysql-operator.mpv | mplayer -
    81  
    82    2. Concatenate contents of file1.txt and stdin to standard output.
    83       {{.Prompt}} {{.HelpName}} file1.txt - > file.txt
    84  
    85    3. Concatenate multiple files to one.
    86       {{.Prompt}} {{.HelpName}} part.* > complete.img
    87  
    88    4. Save an encrypted object from Amazon S3 cloud storage to a local file.
    89       {{.Prompt}} {{.HelpName}} --enc-c "play/my-bucket/=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" s3/mysql-backups/backups-201810.gz > /mnt/data/recent.gz
    90  
    91    5. Display the content of encrypted object. In case the encryption key contains non-printable character like tab, pass the
    92       base64 encoded string as key.
    93       {{.Prompt}} {{.HelpName}} --enc-c "play/my-bucket/=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" play/my-bucket/my-object
    94  
    95    6. Display the content of an object 10 days earlier
    96       {{.Prompt}} {{.HelpName}} --rewind 10d play/my-bucket/my-object
    97  
    98    7. Display the content of a particular object version
    99       {{.Prompt}} {{.HelpName}} --vid "3ddac055-89a7-40fa-8cd3-530a5581b6b8" play/my-bucket/my-object
   100  `,
   101  }
   102  
   103  // checkCatSyntax - validate all the passed arguments
   104  func checkCatSyntax(ctx *cli.Context) {
   105  	if len(ctx.Args()) == 0 {
   106  		showCommandHelpAndExit(ctx, 1) // last argument is exit code
   107  	}
   108  }
   109  
   110  // prettyStdout replaces some non printable characters
   111  // with <hex> format to be better viewable by the user
   112  type prettyStdout struct {
   113  	// Internal data to pretty-print
   114  	writer io.Writer
   115  	// Internal buffer which contains pretty printed
   116  	// form of binary (no printable) characters
   117  	buffer *bytes.Buffer
   118  }
   119  
   120  // newPrettyStdout returns an initialized prettyStdout struct
   121  func newPrettyStdout(w io.Writer) *prettyStdout {
   122  	return &prettyStdout{
   123  		writer: w,
   124  		buffer: bytes.NewBuffer([]byte{}),
   125  	}
   126  }
   127  
   128  // Read() returns pretty printed binary characters
   129  func (s prettyStdout) Write(input []byte) (int, error) {
   130  	inputLen := len(input)
   131  
   132  	// Convert no printable characters to '^?'
   133  	// and fill into s.buffer
   134  	for len(input) > 0 {
   135  		r, size := utf8.DecodeRune(input)
   136  		if unicode.IsPrint(r) || unicode.IsSpace(r) {
   137  			s.buffer.WriteRune(r)
   138  		} else {
   139  			s.buffer.WriteString("^?")
   140  		}
   141  		input = input[size:]
   142  	}
   143  
   144  	bufLen := s.buffer.Len()
   145  
   146  	// Copy all buffer content to the writer (stdout)
   147  	n, e := s.buffer.WriteTo(s.writer)
   148  	if e != nil {
   149  		return 0, e
   150  	}
   151  
   152  	if int(n) != bufLen {
   153  		return 0, errors.New("error when writing to stdout")
   154  	}
   155  
   156  	return inputLen, nil
   157  }
   158  
   159  type catOpts struct {
   160  	args      []string
   161  	versionID string
   162  	timeRef   time.Time
   163  	startO    int64
   164  	tailO     int64
   165  	isZip     bool
   166  	stdinMode bool
   167  }
   168  
   169  // parseCatSyntax performs command-line input validation for cat command.
   170  func parseCatSyntax(ctx *cli.Context) catOpts {
   171  	// Validate command-line arguments.
   172  	checkCatSyntax(ctx)
   173  
   174  	var o catOpts
   175  	o.args = ctx.Args()
   176  
   177  	o.versionID = ctx.String("version-id")
   178  	rewind := ctx.String("rewind")
   179  
   180  	if o.versionID != "" && rewind != "" {
   181  		fatalIf(errInvalidArgument().Trace(), "You cannot specify --version-id and --rewind at the same time")
   182  	}
   183  
   184  	if o.versionID != "" && len(o.args) != 1 {
   185  		fatalIf(errInvalidArgument().Trace(), "You need to pass at least one argument if --version-id is specified")
   186  	}
   187  
   188  	for _, arg := range o.args {
   189  		if strings.HasPrefix(arg, "-") && len(arg) > 1 {
   190  			fatalIf(probe.NewError(errors.New("")), fmt.Sprintf("Unknown flag `%s` passed.", arg))
   191  		}
   192  	}
   193  
   194  	o.stdinMode = len(o.args) == 0
   195  
   196  	o.timeRef = parseRewindFlag(rewind)
   197  	o.isZip = ctx.Bool("zip")
   198  	o.startO = ctx.Int64("offset")
   199  	o.tailO = ctx.Int64("tail")
   200  	if o.tailO != 0 && o.startO != 0 {
   201  		fatalIf(errInvalidArgument().Trace(), "You cannot specify both --tail and --offset")
   202  	}
   203  	if o.tailO < 0 || o.startO < 0 {
   204  		fatalIf(errInvalidArgument().Trace(), "You cannot specify negative --tail or --offset")
   205  	}
   206  	if o.isZip && (o.tailO != 0 || o.startO != 0) {
   207  		fatalIf(errInvalidArgument().Trace(), "You cannot combine --zip with --tail or --offset")
   208  	}
   209  	if o.stdinMode && (o.isZip || o.startO != 0 || o.tailO != 0) {
   210  		fatalIf(errInvalidArgument().Trace(), "You cannot use --zip --tail or --offset with stdin")
   211  	}
   212  
   213  	return o
   214  }
   215  
   216  // catURL displays contents of a URL to stdout.
   217  func catURL(ctx context.Context, sourceURL string, encKeyDB map[string][]prefixSSEPair, o catOpts) *probe.Error {
   218  	var reader io.ReadCloser
   219  	size := int64(-1)
   220  	switch sourceURL {
   221  	case "-":
   222  		reader = os.Stdin
   223  	default:
   224  		versionID := o.versionID
   225  		var err *probe.Error
   226  		// Try to stat the object, the purpose is to:
   227  		// 1. extract the size of S3 object so we can check if the size of the
   228  		// downloaded object is equal to the original one. FS files
   229  		// are ignored since some of them have zero size though they
   230  		// have contents like files under /proc.
   231  		// 2. extract the version ID if rewind flag is passed
   232  		if client, content, err := url2Stat(ctx, url2StatOptions{
   233  			urlStr:                  sourceURL,
   234  			versionID:               o.versionID,
   235  			fileAttr:                false,
   236  			encKeyDB:                encKeyDB,
   237  			timeRef:                 o.timeRef,
   238  			isZip:                   o.isZip,
   239  			ignoreBucketExistsCheck: false,
   240  		}); err == nil {
   241  			if o.versionID == "" {
   242  				versionID = content.VersionID
   243  			}
   244  			if o.tailO > 0 && content.Size > 0 {
   245  				o.startO = content.Size - o.tailO
   246  				if o.startO < 0 {
   247  					// Return all.
   248  					o.startO = 0
   249  				}
   250  			}
   251  
   252  			if client.GetURL().Type == objectStorage {
   253  				size = content.Size - o.startO
   254  				if size < 0 {
   255  					err := probe.NewError(fmt.Errorf("specified offset (%d) bigger than file (%d)", o.startO, content.Size))
   256  					return err.Trace(sourceURL)
   257  				}
   258  			}
   259  		} else {
   260  			return err.Trace(sourceURL)
   261  		}
   262  		gopts := GetOptions{VersionID: versionID, Zip: o.isZip, RangeStart: o.startO}
   263  		if reader, err = getSourceStreamFromURL(ctx, sourceURL, encKeyDB, getSourceOpts{
   264  			GetOptions: gopts,
   265  			preserve:   false,
   266  		}); err != nil {
   267  			return err.Trace(sourceURL)
   268  		}
   269  		defer reader.Close()
   270  	}
   271  	return catOut(reader, size).Trace(sourceURL)
   272  }
   273  
   274  // catOut reads from reader stream and writes to stdout. Also check the length of the
   275  // read bytes against size parameter (if not -1) and return the appropriate error
   276  func catOut(r io.Reader, size int64) *probe.Error {
   277  	var n int64
   278  	var e error
   279  
   280  	var stdout io.Writer
   281  
   282  	// In case of a user showing the object content in a terminal,
   283  	// avoid printing control and other bad characters to avoid
   284  	// terminal session corruption
   285  	if isTerminal() {
   286  		stdout = newPrettyStdout(os.Stdout)
   287  	} else {
   288  		stdout = os.Stdout
   289  	}
   290  
   291  	// Read till EOF.
   292  	if n, e = io.Copy(stdout, r); e != nil {
   293  		switch e := e.(type) {
   294  		case *os.PathError:
   295  			if e.Err == syscall.EPIPE {
   296  				// stdout closed by the user. Gracefully exit.
   297  				return nil
   298  			}
   299  			return probe.NewError(e)
   300  		default:
   301  			return probe.NewError(e)
   302  		}
   303  	}
   304  	if size != -1 && n < size {
   305  		return probe.NewError(UnexpectedEOF{
   306  			TotalSize:    size,
   307  			TotalWritten: n,
   308  		})
   309  	}
   310  	if size != -1 && n > size {
   311  		return probe.NewError(UnexpectedEOF{
   312  			TotalSize:    size,
   313  			TotalWritten: n,
   314  		})
   315  	}
   316  	return nil
   317  }
   318  
   319  // mainCat is the main entry point for cat command.
   320  func mainCat(cliCtx *cli.Context) error {
   321  	ctx, cancelCat := context.WithCancel(globalContext)
   322  	defer cancelCat()
   323  
   324  	encKeyDB, err := validateAndCreateEncryptionKeys(cliCtx)
   325  	fatalIf(err, "Unable to parse encryption keys.")
   326  
   327  	// check 'cat' cli arguments.
   328  	o := parseCatSyntax(cliCtx)
   329  
   330  	// handle std input data.
   331  	if o.stdinMode {
   332  		fatalIf(catOut(os.Stdin, -1).Trace(), "Unable to read from standard input.")
   333  		return nil
   334  	}
   335  
   336  	// if Args contain `-`, we need to preserve its order specially.
   337  	if len(o.args) > 0 && o.args[0] == "-" {
   338  		for i, arg := range os.Args {
   339  			if arg == "cat" {
   340  				// Overwrite cliCtx.Args with os.Args.
   341  				o.args = os.Args[i+1:]
   342  				break
   343  			}
   344  		}
   345  	}
   346  
   347  	// Convert arguments to URLs: expand alias, fix format.
   348  	for _, url := range o.args {
   349  		fatalIf(catURL(ctx, url, encKeyDB, o).Trace(url), "Unable to read from `"+url+"`.")
   350  	}
   351  
   352  	return nil
   353  }