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  }