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 }