github.com/olivere/camlistore@v0.0.0-20140121221811-1b7ac2da0199/pkg/misc/amazon/s3/client.go (about) 1 /* 2 Copyright 2011 Google Inc. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 // Package s3 implements a generic Amazon S3 client, not specific 18 // to Camlistore. 19 package s3 20 21 import ( 22 "bytes" 23 "encoding/base64" 24 "encoding/xml" 25 "errors" 26 "fmt" 27 "hash" 28 "io" 29 "io/ioutil" 30 "net/http" 31 "net/url" 32 "os" 33 "strconv" 34 ) 35 36 // Client is an Amazon S3 client. 37 type Client struct { 38 *Auth 39 HTTPClient *http.Client // or nil for default client 40 } 41 42 type Bucket struct { 43 Name string 44 CreationDate string // 2006-02-03T16:45:09.000Z 45 } 46 47 func (c *Client) httpClient() *http.Client { 48 if c.HTTPClient != nil { 49 return c.HTTPClient 50 } 51 return http.DefaultClient 52 } 53 54 func newReq(url_ string) *http.Request { 55 req, err := http.NewRequest("GET", url_, nil) 56 if err != nil { 57 panic(fmt.Sprintf("s3 client; invalid URL: %v", err)) 58 } 59 req.Header.Set("User-Agent", "go-camlistore-s3") 60 return req 61 } 62 63 func (c *Client) Buckets() ([]*Bucket, error) { 64 req := newReq("https://" + c.hostname() + "/") 65 c.Auth.SignRequest(req) 66 res, err := c.httpClient().Do(req) 67 if err != nil { 68 return nil, err 69 } 70 defer res.Body.Close() 71 if res.StatusCode != 200 { 72 return nil, fmt.Errorf("s3: Unexpected status code %d fetching bucket list", res.StatusCode) 73 } 74 return parseListAllMyBuckets(res.Body) 75 } 76 77 func parseListAllMyBuckets(r io.Reader) ([]*Bucket, error) { 78 type allMyBuckets struct { 79 Buckets struct { 80 Bucket []*Bucket 81 } 82 } 83 var res allMyBuckets 84 if err := xml.NewDecoder(r).Decode(&res); err != nil { 85 return nil, err 86 } 87 return res.Buckets.Bucket, nil 88 } 89 90 // Returns 0, os.ErrNotExist if not on S3, otherwise reterr is real. 91 func (c *Client) Stat(name, bucket string) (size int64, reterr error) { 92 req := newReq("http://" + bucket + "." + c.hostname() + "/" + name) 93 req.Method = "HEAD" 94 c.Auth.SignRequest(req) 95 res, err := c.httpClient().Do(req) 96 if err != nil { 97 return 0, err 98 } 99 if res.Body != nil { 100 defer res.Body.Close() 101 } 102 if res.StatusCode == http.StatusNotFound { 103 return 0, os.ErrNotExist 104 } 105 return strconv.ParseInt(res.Header.Get("Content-Length"), 10, 64) 106 } 107 108 func (c *Client) PutObject(name, bucket string, md5 hash.Hash, size int64, body io.Reader) error { 109 req := newReq("http://" + bucket + "." + c.hostname() + "/" + name) 110 req.Method = "PUT" 111 req.ContentLength = size 112 if md5 != nil { 113 b64 := new(bytes.Buffer) 114 encoder := base64.NewEncoder(base64.StdEncoding, b64) 115 encoder.Write(md5.Sum(nil)) 116 encoder.Close() 117 req.Header.Set("Content-MD5", b64.String()) 118 } 119 c.Auth.SignRequest(req) 120 req.Body = ioutil.NopCloser(body) 121 122 res, err := c.httpClient().Do(req) 123 if res != nil && res.Body != nil { 124 defer res.Body.Close() 125 } 126 if err != nil { 127 return err 128 } 129 if res.StatusCode != 200 { 130 res.Write(os.Stderr) 131 return fmt.Errorf("Got response code %d from s3", res.StatusCode) 132 } 133 return nil 134 } 135 136 type Item struct { 137 Key string 138 Size int64 139 } 140 141 type listBucketResults struct { 142 Contents []*Item 143 IsTruncated bool 144 } 145 146 // marker returns the string lexically greater than the provided s, 147 // if s is not empty. 148 func marker(s string) string { 149 if s == "" { 150 return s 151 } 152 b := []byte(s) 153 i := len(b) 154 for i > 0 { 155 i-- 156 b[i]++ 157 if b[i] != 0 { 158 break 159 } 160 } 161 return string(b) 162 } 163 164 // ListBucket returns 0 to maxKeys (inclusive) items from the provided 165 // bucket. The items will have keys greater than the provided after, which 166 // may be empty. (Note: this is not greater than or equal to, like the S3 167 // API's 'marker' parameter). If the length of the returned items is equal 168 // to maxKeys, there is no indication whether or not the returned list is 169 // truncated. 170 func (c *Client) ListBucket(bucket string, after string, maxKeys int) (items []*Item, err error) { 171 if maxKeys < 0 { 172 return nil, errors.New("invalid negative maxKeys") 173 } 174 const s3APIMaxFetch = 1000 175 for len(items) < maxKeys { 176 fetchN := maxKeys - len(items) 177 if fetchN > s3APIMaxFetch { 178 fetchN = s3APIMaxFetch 179 } 180 var bres listBucketResults 181 url_ := fmt.Sprintf("http://%s.%s/?marker=%s&max-keys=%d", 182 bucket, c.hostname(), url.QueryEscape(marker(after)), fetchN) 183 req := newReq(url_) 184 c.Auth.SignRequest(req) 185 res, err := c.httpClient().Do(req) 186 if err != nil { 187 return nil, err 188 } 189 if err := xml.NewDecoder(res.Body).Decode(&bres); err != nil { 190 return nil, err 191 } 192 res.Body.Close() 193 for _, it := range bres.Contents { 194 if it.Key <= after { 195 return nil, fmt.Errorf("Unexpected response from Amazon: item key %q but wanted greater than %q", it.Key, after) 196 } 197 items = append(items, it) 198 after = it.Key 199 } 200 if !bres.IsTruncated { 201 break 202 } 203 } 204 return items, nil 205 } 206 207 func (c *Client) Get(bucket, key string) (body io.ReadCloser, size int64, err error) { 208 url_ := fmt.Sprintf("http://%s.%s/%s", bucket, c.hostname(), key) 209 req := newReq(url_) 210 c.Auth.SignRequest(req) 211 var res *http.Response 212 res, err = c.httpClient().Do(req) 213 if err != nil { 214 return 215 } 216 if res.StatusCode != http.StatusOK && res != nil && res.Body != nil { 217 defer func() { 218 io.Copy(os.Stderr, res.Body) 219 }() 220 } 221 if res.StatusCode == http.StatusNotFound { 222 err = os.ErrNotExist 223 return 224 } 225 if res.StatusCode != http.StatusOK { 226 err = fmt.Errorf("Amazon HTTP error on GET: %d", res.StatusCode) 227 return 228 } 229 return res.Body, res.ContentLength, nil 230 } 231 232 func (c *Client) Delete(bucket, key string) error { 233 url_ := fmt.Sprintf("http://%s.%s/%s", bucket, c.hostname(), key) 234 req := newReq(url_) 235 req.Method = "DELETE" 236 c.Auth.SignRequest(req) 237 res, err := c.httpClient().Do(req) 238 if err != nil { 239 return err 240 } 241 if res != nil && res.Body != nil { 242 defer res.Body.Close() 243 } 244 if res.StatusCode == http.StatusNotFound || res.StatusCode == http.StatusNoContent || 245 res.StatusCode == http.StatusOK { 246 return nil 247 } 248 return fmt.Errorf("Amazon HTTP error on DELETE: %d", res.StatusCode) 249 }