github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/head-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  	"bufio"
    22  	"compress/bzip2"
    23  	"compress/gzip"
    24  	"context"
    25  	"io"
    26  	"os"
    27  	"strings"
    28  	"syscall"
    29  	"time"
    30  
    31  	"github.com/minio/cli"
    32  	"github.com/minio/mc/pkg/probe"
    33  )
    34  
    35  var headFlags = []cli.Flag{
    36  	cli.Int64Flag{
    37  		Name:  "n,lines",
    38  		Usage: "print the first 'n' lines",
    39  		Value: 10,
    40  	},
    41  	cli.StringFlag{
    42  		Name:  "rewind",
    43  		Usage: "select an object version at specified time",
    44  	},
    45  	cli.StringFlag{
    46  		Name:  "version-id, vid",
    47  		Usage: "select an object version to display",
    48  	},
    49  	cli.BoolFlag{
    50  		Name:  "zip",
    51  		Usage: "extract from remote zip file (MinIO server source only)",
    52  	},
    53  }
    54  
    55  // Display contents of a file.
    56  var headCmd = cli.Command{
    57  	Name:         "head",
    58  	Usage:        "display first 'n' lines of an object",
    59  	Action:       mainHead,
    60  	OnUsageError: onUsageError,
    61  	Before:       setGlobalsFromContext,
    62  	Flags:        append(append(headFlags, encCFlag), globalFlags...),
    63  	CustomHelpTemplate: `NAME:
    64    {{.HelpName}} - {{.Usage}}
    65  
    66  USAGE:
    67    {{.HelpName}} [FLAGS] TARGET [TARGET...]
    68  
    69  FLAGS:
    70    {{range .VisibleFlags}}{{.}}
    71    {{end}}
    72  
    73  NOTE:
    74    '{{.HelpName}}' automatically decompresses 'gzip', 'bzip2' compressed objects.
    75  
    76  EXAMPLES:
    77    1. Display only first line from a 'gzip' compressed object on Amazon S3.
    78       {{.Prompt}} {{.HelpName}} -n 1 s3/csv-data/population.csv.gz
    79  
    80    2. Display only first line from server encrypted object on Amazon S3.
    81       {{.Prompt}} {{.HelpName}} -n 1 --enc-c 's3/csv-data=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA' s3/csv-data/population.csv
    82  
    83    3. Display only first line from server encrypted object on Amazon S3.
    84       {{.Prompt}} {{.HelpName}} --enc-c "s3/json-data=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA"  s3/json-data/population.json
    85  
    86    4. Display the first lines of a specific object version.
    87       {{.Prompt}} {{.HelpName}} --version-id "3ddac055-89a7-40fa-8cd3-530a5581b6b8" s3/json-data/population.json
    88  `,
    89  }
    90  
    91  // headURL displays contents of a URL to stdout.
    92  func headURL(sourceURL, sourceVersion string, timeRef time.Time, encKeyDB map[string][]prefixSSEPair, nlines int64, zip bool) *probe.Error {
    93  	var reader io.ReadCloser
    94  	switch sourceURL {
    95  	case "-":
    96  		reader = os.Stdin
    97  	default:
    98  		var err *probe.Error
    99  		var content *ClientContent
   100  		if reader, content, err = getSourceStreamMetadataFromURL(context.Background(), sourceURL, sourceVersion, timeRef, encKeyDB, zip); err != nil {
   101  			return err.Trace(sourceURL)
   102  		}
   103  
   104  		ctype := content.Metadata["Content-Type"]
   105  		if strings.Contains(ctype, "gzip") {
   106  			var e error
   107  			reader, e = gzip.NewReader(reader)
   108  			if e != nil {
   109  				return probe.NewError(e)
   110  			}
   111  			defer reader.Close()
   112  		} else if strings.Contains(ctype, "bzip") {
   113  			defer reader.Close()
   114  			reader = io.NopCloser(bzip2.NewReader(reader))
   115  		} else {
   116  			defer reader.Close()
   117  		}
   118  	}
   119  	return headOut(reader, nlines).Trace(sourceURL)
   120  }
   121  
   122  // headOut reads from reader stream and writes to stdout. Also check the length of the
   123  // read bytes against size parameter (if not -1) and return the appropriate error
   124  func headOut(r io.Reader, nlines int64) *probe.Error {
   125  	var stdout io.Writer
   126  
   127  	// In case of a user showing the object content in a terminal,
   128  	// avoid printing control and other bad characters to avoid
   129  	// terminal session corruption
   130  	if isTerminal() {
   131  		stdout = newPrettyStdout(os.Stdout)
   132  	} else {
   133  		stdout = os.Stdout
   134  	}
   135  
   136  	// Initialize a new scanner.
   137  	br := bufio.NewReader(r)
   138  
   139  	// Negative number of lines means default number of lines.
   140  	if nlines < 0 {
   141  		nlines = 10
   142  	}
   143  
   144  	for nlines > 0 {
   145  		line, _, e := br.ReadLine()
   146  		if e != nil {
   147  			return probe.NewError(e)
   148  		}
   149  		if _, e := stdout.Write(line); e != nil {
   150  			switch e := e.(type) {
   151  			case *os.PathError:
   152  				if e.Err == syscall.EPIPE {
   153  					// stdout closed by the user. Gracefully exit.
   154  					return nil
   155  				}
   156  				return probe.NewError(e)
   157  			default:
   158  				return probe.NewError(e)
   159  			}
   160  		}
   161  		stdout.Write([]byte("\n"))
   162  		nlines--
   163  	}
   164  	return nil
   165  }
   166  
   167  // parseHeadSyntax performs command-line input validation for head command.
   168  func parseHeadSyntax(ctx *cli.Context) (args []string, versionID string, timeRef time.Time) {
   169  	args = ctx.Args()
   170  
   171  	versionID = ctx.String("version-id")
   172  	rewind := ctx.String("rewind")
   173  
   174  	if versionID != "" && rewind != "" {
   175  		fatalIf(errInvalidArgument().Trace(), "You cannot specify --version-id and --rewind at the same time")
   176  	}
   177  
   178  	if versionID != "" && len(args) != 1 {
   179  		fatalIf(errInvalidArgument().Trace(), "You need to pass at least one argument if --version-id is specified")
   180  	}
   181  
   182  	timeRef = parseRewindFlag(rewind)
   183  	return
   184  }
   185  
   186  // mainHead is the main entry point for head command.
   187  func mainHead(ctx *cli.Context) error {
   188  	// Parse encryption keys per command.
   189  	encryptionKeys, err := validateAndCreateEncryptionKeys(ctx)
   190  	fatalIf(err, "Unable to parse encryption keys.")
   191  
   192  	args, versionID, timeRef := parseHeadSyntax(ctx)
   193  
   194  	stdinMode := len(args) == 0
   195  
   196  	// handle std input data.
   197  	if stdinMode {
   198  		fatalIf(headOut(os.Stdin, ctx.Int64("lines")).Trace(), "Unable to read from standard input.")
   199  		return nil
   200  	}
   201  
   202  	// Convert arguments to URLs: expand alias, fix format.
   203  	for _, url := range ctx.Args() {
   204  		err = headURL(
   205  			url,
   206  			versionID,
   207  			timeRef,
   208  			encryptionKeys,
   209  			ctx.Int64("lines"),
   210  			ctx.Bool("zip"),
   211  		)
   212  		fatalIf(err.Trace(url), "Unable to read from `"+url+"`.")
   213  	}
   214  
   215  	return nil
   216  }