github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/pipe-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  	"io"
    22  	"os"
    23  	"runtime/debug"
    24  	"syscall"
    25  
    26  	"github.com/dustin/go-humanize"
    27  	"github.com/minio/cli"
    28  	"github.com/minio/mc/pkg/probe"
    29  	"github.com/minio/minio-go/v7"
    30  )
    31  
    32  func defaultPartSize() string {
    33  	_, partSize, _, _ := minio.OptimalPartInfo(-1, 0)
    34  	return humanize.IBytes(uint64(partSize))
    35  }
    36  
    37  var pipeFlags = []cli.Flag{
    38  	cli.StringFlag{
    39  		Name:  "storage-class, sc",
    40  		Usage: "set storage class for new object(s) on target",
    41  	},
    42  	cli.StringFlag{
    43  		Name:  "attr",
    44  		Usage: "add custom metadata for the object",
    45  	},
    46  	cli.StringFlag{
    47  		Name:  "tags",
    48  		Usage: "apply one or more tags to the uploaded objects",
    49  	},
    50  	cli.IntFlag{
    51  		Name:  "concurrent",
    52  		Value: 1,
    53  		Usage: "allow N concurrent uploads [WARNING: will use more memory use it with caution]",
    54  	},
    55  	cli.StringFlag{
    56  		Name:  "part-size",
    57  		Value: defaultPartSize(),
    58  		Usage: "customize chunk size for each concurrent upload",
    59  	},
    60  	cli.IntFlag{
    61  		Name:   "pipe-max-size",
    62  		Usage:  "increase the pipe buffer size to a custom value",
    63  		Hidden: true,
    64  	},
    65  }
    66  
    67  // Display contents of a file.
    68  var pipeCmd = cli.Command{
    69  	Name:         "pipe",
    70  	Usage:        "stream STDIN to an object",
    71  	Action:       mainPipe,
    72  	OnUsageError: onUsageError,
    73  	Before:       setGlobalsFromContext,
    74  	Flags:        append(append(pipeFlags, encFlags...), globalFlags...),
    75  	CustomHelpTemplate: `NAME:
    76    {{.HelpName}} - {{.Usage}}
    77  
    78  USAGE:
    79    {{.HelpName}} [FLAGS] [TARGET]
    80  {{if .VisibleFlags}}
    81  FLAGS:
    82    {{range .VisibleFlags}}{{.}}
    83    {{end}}{{end}}
    84  
    85  ENVIRONMENT VARIABLES:
    86    MC_ENC_KMS: KMS encryption key in the form of (alias/prefix=key).
    87    MC_ENC_S3: S3 encryption key in the form of (alias/prefix=key).
    88  
    89  EXAMPLES:
    90    1. Write contents of stdin to a file on local filesystem.
    91       {{.Prompt}} {{.HelpName}} /tmp/hello-world.go
    92  
    93    2. Write contents of stdin to an object on Amazon S3 cloud storage.
    94       {{.Prompt}} {{.HelpName}} s3/personalbuck/meeting-notes.txt
    95  
    96    3. Copy an ISO image to an object on Amazon S3 cloud storage.
    97       {{.Prompt}} cat debian-8.2.iso | {{.HelpName}} s3/opensource-isos/gnuos.iso
    98  
    99    4. Copy an ISO image to an object on minio storage using KMS encryption.
   100       {{.Prompt}} cat debian-8.2.iso | {{.HelpName}} --enc-kms="minio/opensource-isos=my-key-name" minio/opensource-isos/gnuos.iso
   101  
   102    5. Stream MySQL database dump to Amazon S3 directly.
   103       {{.Prompt}} mysqldump -u root -p ******* accountsdb | {{.HelpName}} s3/sql-backups/backups/accountsdb-oct-9-2015.sql
   104  
   105    6. Write contents of stdin to an object on Amazon S3 cloud storage and assign REDUCED_REDUNDANCY storage-class to the uploaded object.
   106       {{.Prompt}} {{.HelpName}} --storage-class REDUCED_REDUNDANCY s3/personalbuck/meeting-notes.txt
   107  
   108    7. Copy to MinIO cloud storage with specified metadata, separated by ";"
   109        {{.Prompt}} cat music.mp3 | {{.HelpName}} --attr "Cache-Control=max-age=90000,min-fresh=9000;Artist=Unknown" play/mybucket/music.mp3
   110  
   111    8. Set tags to the uploaded objects
   112        {{.Prompt}} tar cvf - . | {{.HelpName}} --tags "category=prod&type=backup" play/mybucket/backup.tar
   113  `,
   114  }
   115  
   116  func pipe(ctx *cli.Context, targetURL string, encKeyDB map[string][]prefixSSEPair, meta map[string]string, quiet bool) *probe.Error {
   117  	// If possible increase the pipe buffer size
   118  	if e := increasePipeBufferSize(os.Stdin, ctx.Int("pipe-max-size")); e != nil {
   119  		fatalIf(probe.NewError(e), "Unable to increase custom pipe-max-size")
   120  	}
   121  
   122  	if targetURL == "" {
   123  		// When no target is specified, pipe cat's stdin to stdout.
   124  		return catOut(os.Stdin, -1).Trace()
   125  	}
   126  
   127  	storageClass := ctx.String("storage-class")
   128  	alias, _ := url2Alias(targetURL)
   129  	sseKey := getSSE(targetURL, encKeyDB[alias])
   130  
   131  	multipartThreads := ctx.Int("concurrent")
   132  	if multipartThreads > 1 {
   133  		// We will be allocating large buffers, reduce default GC overhead
   134  		debug.SetGCPercent(20)
   135  	}
   136  
   137  	var multipartSize uint64
   138  	var e error
   139  	if partSizeStr := ctx.String("part-size"); partSizeStr != "" {
   140  		multipartSize, e = humanize.ParseBytes(partSizeStr)
   141  		if e != nil {
   142  			return probe.NewError(e)
   143  		}
   144  	}
   145  
   146  	// Stream from stdin to multiple objects until EOF.
   147  	// Ignore size, since os.Stat() would not return proper size all the time
   148  	// for local filesystem for example /proc files.
   149  	opts := PutOptions{
   150  		sse:              sseKey,
   151  		storageClass:     storageClass,
   152  		metadata:         meta,
   153  		multipartSize:    multipartSize,
   154  		multipartThreads: uint(multipartThreads),
   155  		concurrentStream: ctx.IsSet("concurrent"),
   156  	}
   157  
   158  	var reader io.Reader
   159  	if !quiet {
   160  		pg := newProgressBar(0)
   161  		reader = io.TeeReader(os.Stdin, pg)
   162  	} else {
   163  		reader = os.Stdin
   164  	}
   165  
   166  	_, err := putTargetStreamWithURL(targetURL, reader, -1, opts)
   167  	// TODO: See if this check is necessary.
   168  	switch e := err.ToGoError().(type) {
   169  	case *os.PathError:
   170  		if e.Err == syscall.EPIPE {
   171  			// stdin closed by the user. Gracefully exit.
   172  			return nil
   173  		}
   174  	}
   175  	return err.Trace(targetURL)
   176  }
   177  
   178  // checkPipeSyntax - validate arguments passed by user
   179  func checkPipeSyntax(ctx *cli.Context) {
   180  	if len(ctx.Args()) != 1 {
   181  		showCommandHelpAndExit(ctx, 1) // last argument is exit code.
   182  	}
   183  }
   184  
   185  // mainPipe is the main entry point for pipe command.
   186  func mainPipe(ctx *cli.Context) error {
   187  	// validate pipe input arguments.
   188  	checkPipeSyntax(ctx)
   189  
   190  	encKeyDB, err := validateAndCreateEncryptionKeys(ctx)
   191  	fatalIf(err, "Unable to parse encryption keys.")
   192  
   193  	// globalQuiet is true for no window size to get. We just need --quiet here.
   194  	quiet := ctx.IsSet("quiet")
   195  
   196  	meta := map[string]string{}
   197  	if attr := ctx.String("attr"); attr != "" {
   198  		meta, err = getMetaDataEntry(attr)
   199  		fatalIf(err.Trace(attr), "Unable to parse --attr value")
   200  	}
   201  	if tags := ctx.String("tags"); tags != "" {
   202  		meta["X-Amz-Tagging"] = tags
   203  	}
   204  	if len(ctx.Args()) == 0 {
   205  		err = pipe(ctx, "", nil, meta, quiet)
   206  		fatalIf(err.Trace("stdout"), "Unable to write to one or more targets.")
   207  	} else {
   208  		// extract URLs.
   209  		URLs := ctx.Args()
   210  		err = pipe(ctx, URLs[0], encKeyDB, meta, quiet)
   211  		fatalIf(err.Trace(URLs[0]), "Unable to write to one or more targets.")
   212  	}
   213  
   214  	// Done.
   215  	return nil
   216  }