go4.org@v0.0.0-20230225012048-214862532bf5/cloud/google/gcsutil/storage.go (about) 1 /* 2 Copyright 2015 The Go4 Authors 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 gcsutil provides tools for accessing Google Cloud Storage until they can be 18 // completely replaced by cloud.google.com/go/storage. 19 package gcsutil // import "go4.org/cloud/google/gcsutil" 20 21 import ( 22 "encoding/xml" 23 "errors" 24 "fmt" 25 "io" 26 "net/http" 27 "net/url" 28 "os" 29 "strings" 30 31 "cloud.google.com/go/storage" 32 "go4.org/ctxutil" 33 "golang.org/x/net/context" 34 ) 35 36 const gsAccessURL = "https://storage.googleapis.com" 37 38 // An Object holds the name of an object (its bucket and key) within 39 // Google Cloud Storage. 40 type Object struct { 41 Bucket string 42 Key string 43 } 44 45 func (o *Object) valid() error { 46 if o == nil { 47 return errors.New("invalid nil Object") 48 } 49 if o.Bucket == "" { 50 return errors.New("missing required Bucket field in Object") 51 } 52 if o.Key == "" { 53 return errors.New("missing required Key field in Object") 54 } 55 return nil 56 } 57 58 // A SizedObject holds the bucket, key, and size of an object. 59 type SizedObject struct { 60 Object 61 Size int64 62 } 63 64 func (o *Object) String() string { 65 if o == nil { 66 return "<nil *Object>" 67 } 68 return fmt.Sprintf("%v/%v", o.Bucket, o.Key) 69 } 70 71 func (so SizedObject) String() string { 72 return fmt.Sprintf("%v/%v (%vB)", so.Bucket, so.Key, so.Size) 73 } 74 75 // Makes a simple body-less google storage request 76 func simpleRequest(method, url_ string) (*http.Request, error) { 77 req, err := http.NewRequest(method, url_, nil) 78 if err != nil { 79 return nil, err 80 } 81 req.Header.Set("x-goog-api-version", "2") 82 return req, err 83 } 84 85 // ErrInvalidRange is used when the server has returned http.StatusRequestedRangeNotSatisfiable. 86 var ErrInvalidRange = errors.New("gcsutil: requested range not satisfiable") 87 88 // GetPartialObject fetches part of a Google Cloud Storage object. 89 // This function relies on the ctx ctxutil.HTTPClient value being set to an OAuth2 90 // authorized and authenticated HTTP client. 91 // If length is negative, the rest of the object is returned. 92 // It returns ErrInvalidRange if the server replies with http.StatusRequestedRangeNotSatisfiable. 93 // The caller must call Close on the returned value. 94 func GetPartialObject(ctx context.Context, obj Object, offset, length int64) (io.ReadCloser, error) { 95 if offset < 0 { 96 return nil, errors.New("invalid negative offset") 97 } 98 if err := obj.valid(); err != nil { 99 return nil, err 100 } 101 102 req, err := simpleRequest("GET", gsAccessURL+"/"+obj.Bucket+"/"+obj.Key) 103 if err != nil { 104 return nil, err 105 } 106 if length >= 0 { 107 req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", offset, offset+length-1)) 108 } else { 109 req.Header.Set("Range", fmt.Sprintf("bytes=%d-", offset)) 110 } 111 req.Cancel = ctx.Done() 112 res, err := ctxutil.Client(ctx).Do(req) 113 if err != nil { 114 return nil, fmt.Errorf("GET (offset=%d, length=%d) failed: %v\n", offset, length, err) 115 } 116 if res.StatusCode == http.StatusNotFound { 117 res.Body.Close() 118 return nil, os.ErrNotExist 119 } 120 if !(res.StatusCode == http.StatusPartialContent || (offset == 0 && res.StatusCode == http.StatusOK)) { 121 res.Body.Close() 122 if res.StatusCode == http.StatusRequestedRangeNotSatisfiable { 123 return nil, ErrInvalidRange 124 } 125 return nil, fmt.Errorf("GET (offset=%d, length=%d) got failed status: %v\n", offset, length, res.Status) 126 } 127 128 return res.Body, nil 129 } 130 131 // EnumerateObjects lists the objects in a bucket. 132 // This function relies on the ctx oauth2.HTTPClient value being set to an OAuth2 133 // authorized and authenticated HTTP client. 134 // If after is non-empty, listing will begin with lexically greater object names. 135 // If limit is non-zero, the length of the list will be limited to that number. 136 func EnumerateObjects(ctx context.Context, bucket, after string, limit int) ([]*storage.ObjectAttrs, error) { 137 // Build url, with query params 138 var params []string 139 if after != "" { 140 params = append(params, "marker="+url.QueryEscape(after)) 141 } 142 if limit > 0 { 143 params = append(params, fmt.Sprintf("max-keys=%v", limit)) 144 } 145 query := "" 146 if len(params) > 0 { 147 query = "?" + strings.Join(params, "&") 148 } 149 150 req, err := simpleRequest("GET", gsAccessURL+"/"+bucket+"/"+query) 151 if err != nil { 152 return nil, err 153 } 154 req.Cancel = ctx.Done() 155 res, err := ctxutil.Client(ctx).Do(req) 156 if err != nil { 157 return nil, err 158 } 159 defer res.Body.Close() 160 if res.StatusCode != http.StatusOK { 161 return nil, fmt.Errorf("gcsutil: bad enumerate response code: %v", res.Status) 162 } 163 164 var xres struct { 165 Contents []SizedObject 166 } 167 if err = xml.NewDecoder(res.Body).Decode(&xres); err != nil { 168 return nil, err 169 } 170 171 objAttrs := make([]*storage.ObjectAttrs, len(xres.Contents)) 172 for k, o := range xres.Contents { 173 objAttrs[k] = &storage.ObjectAttrs{ 174 Name: o.Key, 175 Size: o.Size, 176 } 177 } 178 179 return objAttrs, nil 180 }