1//go:build example
2// +build example
3
4package main
5
6import (
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
30func 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.
55func 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.
96func 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.
137func 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.
202func 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
242type Method int
243
244const (
245	PutMethod Method = iota
246	GetMethod
247)
248
249type Errors []error
250
251func (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
259type PresignResp struct {
260	Method, URL string
261	Header      http.Header
262}
263
264func exitError(err error) {
265	fmt.Fprintln(os.Stderr, err.Error())
266	os.Exit(1)
267}
268