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  }