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 }