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