github.com/aavshr/aws-sdk-go@v1.41.3/example/service/s3/presignURL/client/client.go (about)

     1  //go:build example
     2  // +build example
     3  
     4  package main
     5  
     6  import (
     7  	"bytes"
     8  	"encoding/json"
     9  	"flag"
    10  	"fmt"
    11  	"io"
    12  	"io/ioutil"
    13  	"net/http"
    14  	"os"
    15  	"strconv"
    16  	"strings"
    17  )
    18  
    19  // client.go is an example of a client that will request URLs from a service that
    20  // the client will use to upload and download content with.
    21  //
    22  // The server must be started before the client is run.
    23  //
    24  // Use "--help" command line argument flag to see all options and defaults. If
    25  // filename is not provided the client will read from stdin for uploads and
    26  // write to stdout for downloads.
    27  //
    28  // Usage:
    29  //    go run -tags example client.go -get myObjectKey -f filename
    30  func main() {
    31  	method, filename, key, serverURL := loadConfig()
    32  
    33  	var err error
    34  
    35  	switch method {
    36  	case GetMethod:
    37  		// Requests the URL from the server that the client will use to download
    38  		// the content from. The content will be written to the file pointed to
    39  		// by filename. Creating it if the file does not exist. If filename is
    40  		// not set the contents will be written to stdout.
    41  		err = downloadFile(serverURL, key, filename)
    42  	case PutMethod:
    43  		// Requests the URL from the service that the client will use to upload
    44  		// content to. The content will be read from the file pointed to by the
    45  		// filename. If the filename is not set, content will be read from stdin.
    46  		err = uploadFile(serverURL, key, filename)
    47  	}
    48  
    49  	if err != nil {
    50  		exitError(err)
    51  	}
    52  }
    53  
    54  // loadConfig configures the client based on the command line arguments used.
    55  func loadConfig() (method Method, serverURL, key, filename string) {
    56  	var getKey, putKey string
    57  	flag.StringVar(&getKey, "get", "",
    58  		"Downloads the object from S3 by the `key`. Writes the object to a file the filename is provided, otherwise writes to stdout.")
    59  	flag.StringVar(&putKey, "put", "",
    60  		"Uploads data to S3 at the `key` provided. Uploads the file if filename is provided, otherwise reads from stdin.")
    61  	flag.StringVar(&serverURL, "s", "http://127.0.0.1:8080", "Required `URL` the client will request presigned S3 operation from.")
    62  	flag.StringVar(&filename, "f", "", "The `filename` of the file to upload and get from S3.")
    63  	flag.Parse()
    64  
    65  	var errs Errors
    66  
    67  	if len(serverURL) == 0 {
    68  		errs = append(errs, fmt.Errorf("server URL required"))
    69  	}
    70  
    71  	if !((len(getKey) != 0) != (len(putKey) != 0)) {
    72  		errs = append(errs, fmt.Errorf("either `get` or `put` can be provided, and one of the two is required."))
    73  	}
    74  
    75  	if len(getKey) > 0 {
    76  		method = GetMethod
    77  		key = getKey
    78  	} else {
    79  		method = PutMethod
    80  		key = putKey
    81  	}
    82  
    83  	if len(errs) > 0 {
    84  		fmt.Fprintf(os.Stderr, "Failed to load configuration:%v\n", errs)
    85  		flag.PrintDefaults()
    86  		os.Exit(1)
    87  	}
    88  
    89  	return method, filename, key, serverURL
    90  }
    91  
    92  // downloadFile will request a URL from the server that the client can download
    93  // the content pointed to by "key". The content will be written to the file
    94  // pointed to by filename, creating the file if it doesn't exist. If filename
    95  // is not set the content will be written to stdout.
    96  func downloadFile(serverURL, key, filename string) error {
    97  	var w *os.File
    98  	if len(filename) > 0 {
    99  		f, err := os.Create(filename)
   100  		if err != nil {
   101  			return fmt.Errorf("failed to create download file %s, %v", filename, err)
   102  		}
   103  		w = f
   104  	} else {
   105  		w = os.Stdout
   106  	}
   107  	defer w.Close()
   108  
   109  	// Get the presigned URL from the remote service.
   110  	req, err := getPresignedRequest(serverURL, "GET", key, 0)
   111  	if err != nil {
   112  		return fmt.Errorf("failed to get get presigned request, %v", err)
   113  	}
   114  
   115  	// Gets the file contents with the URL provided by the service.
   116  	resp, err := http.DefaultClient.Do(req)
   117  	if err != nil {
   118  		return fmt.Errorf("failed to do GET request, %v", err)
   119  	}
   120  	defer resp.Body.Close()
   121  
   122  	if resp.StatusCode != http.StatusOK {
   123  		return fmt.Errorf("failed to get S3 object, %d:%s",
   124  			resp.StatusCode, resp.Status)
   125  	}
   126  
   127  	if _, err = io.Copy(w, resp.Body); err != nil {
   128  		return fmt.Errorf("failed to write S3 object, %v", err)
   129  	}
   130  
   131  	return nil
   132  }
   133  
   134  // uploadFile will request a URL from the service that the client can use to
   135  // upload content to. The content will be read from the file pointed to by filename.
   136  // If filename is not set the content will be read from stdin.
   137  func uploadFile(serverURL, key, filename string) error {
   138  	var r io.ReadCloser
   139  	var size int64
   140  	if len(filename) > 0 {
   141  		f, err := os.Open(filename)
   142  		if err != nil {
   143  			return fmt.Errorf("failed to open upload file %s, %v", filename, err)
   144  		}
   145  
   146  		// Get the size of the file so that the constraint of Content-Length
   147  		// can be included with the presigned URL. This can be used by the
   148  		// server or client to ensure the content uploaded is of a certain size.
   149  		//
   150  		// These constraints can further be expanded to include things like
   151  		// Content-Type. Additionally constraints such as X-Amz-Content-Sha256
   152  		// header set restricting the content of the file to only the content
   153  		// the client initially made the request with. This prevents the object
   154  		// from being overwritten or used to upload other unintended content.
   155  		stat, err := f.Stat()
   156  		if err != nil {
   157  			return fmt.Errorf("failed to stat file, %s, %v", filename, err)
   158  		}
   159  
   160  		size = stat.Size()
   161  		r = f
   162  	} else {
   163  		buf := &bytes.Buffer{}
   164  		io.Copy(buf, os.Stdin)
   165  		size = int64(buf.Len())
   166  
   167  		r = ioutil.NopCloser(buf)
   168  	}
   169  	defer r.Close()
   170  
   171  	// Get the Presigned URL from the remote service. Pass in the file's
   172  	// size if it is known so that the presigned URL returned will be required
   173  	// to be used with the size of content requested.
   174  	req, err := getPresignedRequest(serverURL, "PUT", key, size)
   175  	if err != nil {
   176  		return fmt.Errorf("failed to get put presigned request, %v", err)
   177  	}
   178  	req.Body = r
   179  
   180  	// Upload the file contents to S3.
   181  	resp, err := http.DefaultClient.Do(req)
   182  	if err != nil {
   183  		return fmt.Errorf("failed to do PUT request, %v", err)
   184  	}
   185  
   186  	defer resp.Body.Close()
   187  
   188  	if resp.StatusCode != http.StatusOK {
   189  		return fmt.Errorf("failed to put S3 object, %d:%s",
   190  			resp.StatusCode, resp.Status)
   191  	}
   192  
   193  	return nil
   194  }
   195  
   196  // getPresignRequest will request a URL from the service for the content specified
   197  // by the key and method. Returns a constructed Request that can be used to
   198  // upload or download content with based on the method used.
   199  //
   200  // If the PUT method is used the request's Body will need to be set on the returned
   201  // request value.
   202  func getPresignedRequest(serverURL, method, key string, contentLen int64) (*http.Request, error) {
   203  	u := fmt.Sprintf("%s/presign/%s?method=%s&contentLength=%d",
   204  		serverURL, key, method, contentLen,
   205  	)
   206  
   207  	resp, err := http.Get(u)
   208  	if err != nil {
   209  		return nil, fmt.Errorf("failed to make request for presigned URL, %v", err)
   210  	}
   211  	defer resp.Body.Close()
   212  
   213  	if resp.StatusCode != http.StatusOK {
   214  		return nil, fmt.Errorf("failed to get valid presign response, %s", resp.Status)
   215  	}
   216  
   217  	p := PresignResp{}
   218  	if err := json.NewDecoder(resp.Body).Decode(&p); err != nil {
   219  		return nil, fmt.Errorf("failed to decode response body, %v", err)
   220  	}
   221  
   222  	req, err := http.NewRequest(p.Method, p.URL, nil)
   223  	if err != nil {
   224  		return nil, fmt.Errorf("failed to build presigned request, %v", err)
   225  	}
   226  
   227  	for k, vs := range p.Header {
   228  		for _, v := range vs {
   229  			req.Header.Add(k, v)
   230  		}
   231  	}
   232  	// Need to ensure that the content length member is set of the HTTP Request
   233  	// or the request will not be transmitted correctly with a content length
   234  	// value across the wire.
   235  	if contLen := req.Header.Get("Content-Length"); len(contLen) > 0 {
   236  		req.ContentLength, _ = strconv.ParseInt(contLen, 10, 64)
   237  	}
   238  
   239  	return req, nil
   240  }
   241  
   242  type Method int
   243  
   244  const (
   245  	PutMethod Method = iota
   246  	GetMethod
   247  )
   248  
   249  type Errors []error
   250  
   251  func (es Errors) Error() string {
   252  	out := make([]string, len(es))
   253  	for _, e := range es {
   254  		out = append(out, e.Error())
   255  	}
   256  	return strings.Join(out, "\n")
   257  }
   258  
   259  type PresignResp struct {
   260  	Method, URL string
   261  	Header      http.Header
   262  }
   263  
   264  func exitError(err error) {
   265  	fmt.Fprintln(os.Stderr, err.Error())
   266  	os.Exit(1)
   267  }