github.com/viant/toolbox@v0.34.5/storage/gs/service.go (about)

     1  package gs
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"crypto/md5"
     7  	"errors"
     8  	"fmt"
     9  	"hash/crc32"
    10  	"io"
    11  	"io/ioutil"
    12  	"net/url"
    13  
    14  	"cloud.google.com/go/storage"
    15  	"github.com/viant/toolbox"
    16  	tstorage "github.com/viant/toolbox/storage"
    17  	"google.golang.org/api/option"
    18  	"os"
    19  	"strings"
    20  	"time"
    21  )
    22  
    23  type service struct {
    24  	projectID string
    25  	options   []option.ClientOption
    26  }
    27  
    28  func (s *service) NewClient() (*storage.Client, context.Context, error) {
    29  	if s.projectID == "" {
    30  		return nil, nil, fmt.Errorf("project ID was empty, consider setting GOOGLE_CLOUD_PROJECT")
    31  	}
    32  	ctx := context.Background()
    33  	deadline, _ := ctx.Deadline()
    34  	deadline.Add(time.Minute * 30)
    35  	client, err := storage.NewClient(ctx, s.options...)
    36  	if err != nil {
    37  		err = fmt.Errorf("failed to create google storage client:%v", err)
    38  	}
    39  	return client, ctx, err
    40  }
    41  
    42  //List returns a list of object for supplied url
    43  func (s *service) List(URL string) ([]tstorage.Object, error) {
    44  	parsedUrl, err := url.Parse(URL)
    45  	if err != nil {
    46  		return nil, err
    47  	}
    48  	client, ctx, err := s.NewClient()
    49  	if err != nil {
    50  		return nil, err
    51  	}
    52  	defer client.Close()
    53  
    54  	var query = &storage.Query{
    55  		Delimiter: "/",
    56  	}
    57  	if len(parsedUrl.Path) > 0 {
    58  		query.Prefix = parsedUrl.Path[1:]
    59  	}
    60  	responseIterator := client.Bucket(parsedUrl.Host).Objects(ctx, query)
    61  	var result = make([]tstorage.Object, 0)
    62  	for obj, err := responseIterator.Next(); err == nil; obj, err = responseIterator.Next() {
    63  		objectURL := "gs://" + parsedUrl.Host + "/" + obj.Prefix + obj.Name
    64  		var fileMode, _ = tstorage.NewFileMode("-rw-rw-rw-")
    65  		if obj.Prefix != "" {
    66  			fileMode, _ = tstorage.NewFileMode("drw-rw-rw-")
    67  		}
    68  		var _, name = toolbox.URLSplit(objectURL)
    69  		var fileInfo = tstorage.NewFileInfo(name, obj.Size, fileMode, obj.Updated, fileMode.IsDir())
    70  		var object = newStorageObject(objectURL, obj, fileInfo)
    71  		result = append(result, object)
    72  	}
    73  	return result, err
    74  }
    75  
    76  func (s *service) Exists(URL string) (bool, error) {
    77  	objects, err := s.List(URL)
    78  	if err != nil {
    79  		return false, err
    80  	}
    81  	return len(objects) > 0, nil
    82  }
    83  
    84  func (s *service) StorageObject(URL string) (tstorage.Object, error) {
    85  	objects, err := s.List(URL)
    86  	if err != nil {
    87  		return nil, err
    88  	}
    89  	if len(objects) == 0 {
    90  		return nil, fmt.Errorf("Not found %v", URL)
    91  	}
    92  	return objects[0], nil
    93  }
    94  
    95  //Download returns reader for downloaded storage object
    96  func (s *service) Download(object tstorage.Object) (io.ReadCloser, error) {
    97  	client, ctx, err := s.NewClient()
    98  	if err != nil {
    99  		return nil, err
   100  	}
   101  	defer client.Close()
   102  
   103  	objectInfo := &storage.ObjectAttrs{}
   104  	err = object.Unwrap(&objectInfo)
   105  	if err != nil {
   106  		return nil, err
   107  	}
   108  	return client.Bucket(objectInfo.Bucket).
   109  		Object(objectInfo.Name).
   110  		NewReader(ctx)
   111  }
   112  
   113  func (s *service) Upload(URL string, reader io.Reader) error {
   114  	return s.UploadWithMode(URL, tstorage.DefaultFileMode, reader)
   115  }
   116  
   117  func (s *service) UploadWithMode(URL string, mode os.FileMode, reader io.Reader) error {
   118  	parserURL, err := url.Parse(URL)
   119  	if err != nil {
   120  		return fmt.Errorf("failed to parse URL for uploading: %v, %v", URL, err)
   121  	}
   122  	client, ctx, err := s.NewClient()
   123  	if err != nil {
   124  		return err
   125  	}
   126  	defer client.Close()
   127  	name := parserURL.Path
   128  	if len(parserURL.Path) > 0 {
   129  		name = parserURL.Path[1:]
   130  	}
   131  
   132  	err = s.uploadContent(ctx, client, parserURL, name, reader)
   133  	if toolbox.IsNotFoundError(err) {
   134  		err := client.Bucket(parserURL.Host).Create(ctx, s.projectID, &storage.BucketAttrs{})
   135  		if err != nil {
   136  			return fmt.Errorf("failed to create bucket: %v, %v", parserURL.Host, err)
   137  		}
   138  		//_, _ = client.Bucket(parserURL.Host).DefaultObjectACL().List(ctx)
   139  		return s.uploadContent(ctx, client, parserURL, name, reader)
   140  	}
   141  	if err != nil {
   142  		return fmt.Errorf("unable upload: %v", err)
   143  	}
   144  	return nil
   145  }
   146  
   147  func (s *service) uploadContent(ctx context.Context, client *storage.Client, parserURL *url.URL, name string, reader io.Reader) error {
   148  	writer := client.Bucket(parserURL.Host).
   149  		Object(name).
   150  		NewWriter(ctx)
   151  	expiry := parserURL.Query().Get("expiry")
   152  	if expiry != "" {
   153  		writer.Metadata = map[string]string{
   154  			"Cache-Control": "private, max-age=" + expiry,
   155  		}
   156  	}
   157  	reader, err := updateUploadChecksum(parserURL, reader, writer)
   158  	if _, err = io.Copy(writer, reader); err != nil {
   159  		return fmt.Errorf("failed to copy to writer during upload:%v", err)
   160  	}
   161  	if err = writer.Close(); err != nil {
   162  		return toolbox.ReclassifyNotFoundIfMatched(err, parserURL.String())
   163  	}
   164  	return nil
   165  }
   166  
   167  func updateUploadChecksum(parserURL *url.URL, reader io.Reader, writer *storage.Writer) (io.Reader, error) {
   168  	checksumDisabled := parserURL.Query().Get("disableChecksum") != ""
   169  	updateMD5 := parserURL.Query().Get("disableMD5") == ""
   170  	updateCRC := parserURL.Query().Get("disableCRC32") == ""
   171  	if !(updateCRC || updateMD5) || checksumDisabled {
   172  		return reader, nil
   173  	}
   174  
   175  	var err error
   176  	bufferReader, ok := reader.(*bytes.Buffer)
   177  	if !ok {
   178  		content, err := ioutil.ReadAll(reader)
   179  		if err != nil {
   180  			return nil, fmt.Errorf("failed to read all during upload:%v", err)
   181  		}
   182  		bufferReader = bytes.NewBuffer(content)
   183  	}
   184  	if parserURL.Query().Get("disableMD5") == "" {
   185  		hashReader := bytes.NewBuffer(bufferReader.Bytes())
   186  		h := md5.New()
   187  		_, _ = io.Copy(h, hashReader)
   188  		writer.MD5 = h.Sum(nil)
   189  		hashReader.Reset()
   190  	} else if parserURL.Query().Get("disableCRC32") == "" {
   191  		crc32HashReader := bytes.NewBuffer(bufferReader.Bytes())
   192  		crc32Hash := crc32.New(crc32.MakeTable(crc32.Castagnoli))
   193  		_, _ = io.Copy(crc32Hash, crc32HashReader)
   194  		writer.CRC32C = crc32Hash.Sum32()
   195  		crc32HashReader.Reset()
   196  	}
   197  	return bufferReader, err
   198  }
   199  
   200  func (s *service) Register(schema string, service tstorage.Service) error {
   201  	return errors.New("unsupported")
   202  }
   203  
   204  func (s *service) Close() error {
   205  	return nil
   206  }
   207  
   208  func (s *service) listAll(URL string, result *[]tstorage.Object) error {
   209  	if !strings.HasSuffix(URL, "/") {
   210  		URL += "/"
   211  	}
   212  	objects, err := s.List(URL)
   213  	if err != nil {
   214  		return err
   215  	}
   216  	for _, object := range objects {
   217  		if !object.IsFolder() {
   218  			*result = append(*result, object)
   219  			continue
   220  		}
   221  		if err = s.listAll(object.URL(), result); err != nil {
   222  			return err
   223  		}
   224  	}
   225  	return nil
   226  }
   227  
   228  //Delete removes passed in storage object
   229  func (s *service) Delete(object tstorage.Object) error {
   230  	client, ctx, err := s.NewClient()
   231  	if err != nil {
   232  		return err
   233  	}
   234  	defer client.Close()
   235  	objectInfo := &storage.ObjectAttrs{}
   236  	err = object.Unwrap(&objectInfo)
   237  	if err != nil {
   238  		return err
   239  	}
   240  	if object.IsFolder() {
   241  		var objects = []tstorage.Object{}
   242  		err := s.listAll(object.URL(), &objects)
   243  		if err != nil {
   244  			return err
   245  		}
   246  		for _, object := range objects {
   247  			if err := s.Delete(object); err != nil {
   248  				return err
   249  			}
   250  		}
   251  		return nil
   252  	}
   253  	return client.Bucket(objectInfo.Bucket).
   254  		Object(objectInfo.Name).Delete(ctx)
   255  }
   256  
   257  //DownloadWithURL downloads content for passed in object URL
   258  func (s *service) DownloadWithURL(URL string) (io.ReadCloser, error) {
   259  	object, err := s.StorageObject(URL)
   260  	if err != nil {
   261  		return nil, err
   262  	}
   263  	return s.Download(object)
   264  }
   265  
   266  //NewService create a new gc storage service
   267  func NewService(projectId string, options ...option.ClientOption) *service {
   268  	return &service{
   269  		projectID: projectId,
   270  		options:   options,
   271  	}
   272  }