github.com/olivere/camlistore@v0.0.0-20140121221811-1b7ac2da0199/pkg/googlestorage/googlestorage.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 googlestorage implements a generic Google Storage API
    18  // client. It does not include any Camlistore-specific logic.
    19  package googlestorage
    20  
    21  import (
    22  	"encoding/xml"
    23  	"fmt"
    24  	"io"
    25  	"net/http"
    26  	"net/url"
    27  	"strconv"
    28  	"strings"
    29  
    30  	"camlistore.org/third_party/code.google.com/p/goauth2/oauth"
    31  )
    32  
    33  const (
    34  	gsAccessURL = "https://storage.googleapis.com"
    35  )
    36  
    37  type Client struct {
    38  	transport *oauth.Transport
    39  	client    *http.Client
    40  }
    41  
    42  type Object struct {
    43  	Bucket string
    44  	Key    string
    45  }
    46  
    47  type SizedObject struct {
    48  	Object
    49  	Size int64
    50  }
    51  
    52  func NewClient(transport *oauth.Transport) *Client {
    53  	return &Client{transport, transport.Client()}
    54  }
    55  
    56  func (gso Object) String() string {
    57  	return fmt.Sprintf("%v/%v", gso.Bucket, gso.Key)
    58  }
    59  
    60  func (sgso SizedObject) String() string {
    61  	return fmt.Sprintf("%v/%v (%vB)", sgso.Bucket, sgso.Key, sgso.Size)
    62  }
    63  
    64  // A close relative to http.Client.Do(), helping with token refresh logic.
    65  // If canResend is true and the initial request's response is an auth error
    66  // (401 or 403), oauth credentials will be refreshed and the request sent
    67  // again.  This should only be done for requests with empty bodies, since the
    68  // Body will be consumed on the first attempt if it exists.
    69  // If canResend is false, and req would have been resent if canResend were
    70  // true, then shouldRetry will be true.
    71  // One of resp or err will always be nil.
    72  func (gsa *Client) doRequest(req *http.Request, canResend bool) (resp *http.Response, err error, shouldRetry bool) {
    73  	if resp, err = gsa.client.Do(req); err != nil {
    74  		return
    75  	}
    76  
    77  	if resp.StatusCode == 401 || resp.StatusCode == 403 {
    78  		// Unauth.  Perhaps tokens need refreshing?
    79  		if err = gsa.transport.Refresh(); err != nil {
    80  			return
    81  		}
    82  		// Refresh succeeded.  req should be resent
    83  		if !canResend {
    84  			return resp, nil, true
    85  		}
    86  		// Resend req.  First, need to close the soon-overwritten response Body
    87  		resp.Body.Close()
    88  		resp, err = gsa.client.Do(req)
    89  	}
    90  
    91  	return
    92  }
    93  
    94  // Makes a simple body-less google storage request
    95  func (gsa *Client) simpleRequest(method, url_ string) (resp *http.Response, err error) {
    96  	// Construct the request
    97  	req, err := http.NewRequest(method, url_, nil)
    98  	if err != nil {
    99  		return
   100  	}
   101  	req.Header.Set("x-goog-api-version", "2")
   102  
   103  	resp, err, _ = gsa.doRequest(req, true)
   104  	return
   105  }
   106  
   107  // Fetch a GS object.
   108  // Bucket and Key fields are trusted to be valid.
   109  // Returns (object reader, object size, err).  Reader must be closed.
   110  func (gsa *Client) GetObject(obj *Object) (io.ReadCloser, int64, error) {
   111  
   112  	resp, err := gsa.simpleRequest("GET", gsAccessURL+"/"+obj.Bucket+"/"+obj.Key)
   113  	if err != nil {
   114  		return nil, 0, fmt.Errorf("GS GET request failed: %v\n", err)
   115  	}
   116  
   117  	if resp.StatusCode != http.StatusOK {
   118  		return nil, 0, fmt.Errorf("GS GET request failed status: %v\n", resp.Status)
   119  	}
   120  
   121  	return resp.Body, resp.ContentLength, nil
   122  }
   123  
   124  // Check for size / existence of a GS object.
   125  // Bucket and Key fields are trusted to be valid.
   126  // err signals io / authz errors, a nonexistant file is not an error.
   127  func (gsa *Client) StatObject(obj *Object) (size int64, exists bool, err error) {
   128  	resp, err := gsa.simpleRequest("HEAD", gsAccessURL+"/"+obj.Bucket+"/"+obj.Key)
   129  	if err != nil {
   130  		return
   131  	}
   132  	resp.Body.Close() // should be empty
   133  
   134  	if resp.StatusCode == http.StatusNotFound {
   135  		return
   136  	}
   137  	if resp.StatusCode == http.StatusOK {
   138  		if size, err = strconv.ParseInt(resp.Header["Content-Length"][0], 10, 64); err != nil {
   139  			return
   140  		}
   141  		return size, true, nil
   142  	}
   143  
   144  	// Any response other than 404 or 200 is erroneous
   145  	return 0, false, fmt.Errorf("Bad head response code: %v", resp.Status)
   146  }
   147  
   148  // Upload a GS object.  Bucket and Key are trusted to be valid.
   149  // shouldRetry will be true if the put failed due to authorization, but
   150  // credentials have been refreshed and another attempt is likely to succeed.
   151  // In this case, content will have been consumed.
   152  func (gsa *Client) PutObject(obj *Object, content io.ReadCloser) (shouldRetry bool, err error) {
   153  	objURL := gsAccessURL + "/" + obj.Bucket + "/" + obj.Key
   154  	var req *http.Request
   155  	if req, err = http.NewRequest("PUT", objURL, content); err != nil {
   156  		return
   157  	}
   158  	req.Header.Set("x-goog-api-version", "2")
   159  
   160  	var resp *http.Response
   161  	if resp, err, shouldRetry = gsa.doRequest(req, false); err != nil {
   162  		return
   163  	}
   164  	resp.Body.Close() // should be empty
   165  
   166  	if resp.StatusCode != http.StatusOK {
   167  		return shouldRetry, fmt.Errorf("Bad put response code: %v", resp.Status)
   168  	}
   169  	return
   170  }
   171  
   172  // Removes a GS object.
   173  // Bucket and Key values are trusted to be valid.
   174  func (gsa *Client) DeleteObject(obj *Object) (err error) {
   175  	//	bucketURL := gsAccessURL + "/" + obj.Bucket + "/" + obj.Key
   176  	resp, err := gsa.simpleRequest("DELETE", gsAccessURL+"/"+obj.Bucket+"/"+obj.Key)
   177  	if err != nil {
   178  		return
   179  	}
   180  	if resp.StatusCode != http.StatusNoContent {
   181  		err = fmt.Errorf("Bad delete response code: %v", resp.Status)
   182  	}
   183  	return
   184  }
   185  
   186  // Used for unmarshalling XML returned by enumerate request
   187  type gsListResult struct {
   188  	Contents []SizedObject
   189  }
   190  
   191  // List the objects in a GS bucket.
   192  // If after is nonempty, listing will begin with lexically greater object names
   193  // If limit is nonzero, the length of the list will be limited to that number.
   194  func (gsa *Client) EnumerateObjects(bucket, after string, limit int) ([]SizedObject, error) {
   195  	// Build url, with query params
   196  	params := make([]string, 0, 2)
   197  	if after != "" {
   198  		params = append(params, "marker="+url.QueryEscape(after))
   199  	}
   200  	if limit > 0 {
   201  		params = append(params, fmt.Sprintf("max-keys=%v", limit))
   202  	}
   203  	query := ""
   204  	if len(params) > 0 {
   205  		query = "?" + strings.Join(params, "&")
   206  	}
   207  
   208  	// Make the request
   209  	resp, err := gsa.simpleRequest("GET", gsAccessURL+"/"+bucket+"/"+query)
   210  	if err != nil {
   211  		return nil, err
   212  	}
   213  	defer resp.Body.Close()
   214  	if resp.StatusCode != http.StatusOK {
   215  		return nil, fmt.Errorf("Bad enumerate response code: %v", resp.Status)
   216  	}
   217  
   218  	// Parse the XML response
   219  	result := &gsListResult{make([]SizedObject, 0, limit)}
   220  	if err = xml.NewDecoder(resp.Body).Decode(result); err != nil {
   221  		return nil, err
   222  	}
   223  	// Fill in the Bucket on all the SizedObjects
   224  	for i, _ := range result.Contents {
   225  		result.Contents[i].Bucket = bucket
   226  	}
   227  
   228  	return result.Contents, nil
   229  }