github.com/xhghs/rclone@v1.51.1-0.20200430155106-e186a28cced8/backend/googlephotos/googlephotos.go (about) 1 // Package googlephotos provides an interface to Google Photos 2 package googlephotos 3 4 // FIXME Resumable uploads not implemented - rclone can't resume uploads in general 5 6 import ( 7 "context" 8 "encoding/json" 9 "fmt" 10 "io" 11 golog "log" 12 "net/http" 13 "net/url" 14 "path" 15 "regexp" 16 "strconv" 17 "strings" 18 "sync" 19 "time" 20 21 "github.com/pkg/errors" 22 "github.com/rclone/rclone/backend/googlephotos/api" 23 "github.com/rclone/rclone/fs" 24 "github.com/rclone/rclone/fs/config" 25 "github.com/rclone/rclone/fs/config/configmap" 26 "github.com/rclone/rclone/fs/config/configstruct" 27 "github.com/rclone/rclone/fs/config/obscure" 28 "github.com/rclone/rclone/fs/dirtree" 29 "github.com/rclone/rclone/fs/fserrors" 30 "github.com/rclone/rclone/fs/fshttp" 31 "github.com/rclone/rclone/fs/hash" 32 "github.com/rclone/rclone/fs/log" 33 "github.com/rclone/rclone/lib/oauthutil" 34 "github.com/rclone/rclone/lib/pacer" 35 "github.com/rclone/rclone/lib/rest" 36 "golang.org/x/oauth2" 37 "golang.org/x/oauth2/google" 38 ) 39 40 var ( 41 errCantUpload = errors.New("can't upload files here") 42 errCantMkdir = errors.New("can't make directories here") 43 errCantRmdir = errors.New("can't remove this directory") 44 errAlbumDelete = errors.New("google photos API does not implement deleting albums") 45 errRemove = errors.New("google photos API only implements removing files from albums") 46 errOwnAlbums = errors.New("google photos API only allows uploading to albums rclone created") 47 ) 48 49 const ( 50 rcloneClientID = "202264815644-rt1o1c9evjaotbpbab10m83i8cnjk077.apps.googleusercontent.com" 51 rcloneEncryptedClientSecret = "kLJLretPefBgrDHosdml_nlF64HZ9mUcO85X5rdjYBPP8ChA-jr3Ow" 52 rootURL = "https://photoslibrary.googleapis.com/v1" 53 listChunks = 100 // chunk size to read directory listings 54 albumChunks = 50 // chunk size to read album listings 55 minSleep = 10 * time.Millisecond 56 scopeReadOnly = "https://www.googleapis.com/auth/photoslibrary.readonly" 57 scopeReadWrite = "https://www.googleapis.com/auth/photoslibrary" 58 ) 59 60 var ( 61 // Description of how to auth for this app 62 oauthConfig = &oauth2.Config{ 63 Scopes: []string{ 64 "openid", 65 "profile", 66 scopeReadWrite, 67 }, 68 Endpoint: google.Endpoint, 69 ClientID: rcloneClientID, 70 ClientSecret: obscure.MustReveal(rcloneEncryptedClientSecret), 71 RedirectURL: oauthutil.TitleBarRedirectURL, 72 } 73 ) 74 75 // Register with Fs 76 func init() { 77 fs.Register(&fs.RegInfo{ 78 Name: "google photos", 79 Prefix: "gphotos", 80 Description: "Google Photos", 81 NewFs: NewFs, 82 Config: func(name string, m configmap.Mapper) { 83 // Parse config into Options struct 84 opt := new(Options) 85 err := configstruct.Set(m, opt) 86 if err != nil { 87 fs.Errorf(nil, "Couldn't parse config into struct: %v", err) 88 return 89 } 90 91 // Fill in the scopes 92 if opt.ReadOnly { 93 oauthConfig.Scopes[0] = scopeReadOnly 94 } else { 95 oauthConfig.Scopes[0] = scopeReadWrite 96 } 97 98 // Do the oauth 99 err = oauthutil.Config("google photos", name, m, oauthConfig) 100 if err != nil { 101 golog.Fatalf("Failed to configure token: %v", err) 102 } 103 104 // Warn the user 105 fmt.Print(` 106 *** IMPORTANT: All media items uploaded to Google Photos with rclone 107 *** are stored in full resolution at original quality. These uploads 108 *** will count towards storage in your Google Account. 109 110 `) 111 112 }, 113 Options: []fs.Option{{ 114 Name: config.ConfigClientID, 115 Help: "Google Application Client Id\nLeave blank normally.", 116 }, { 117 Name: config.ConfigClientSecret, 118 Help: "Google Application Client Secret\nLeave blank normally.", 119 }, { 120 Name: "read_only", 121 Default: false, 122 Help: `Set to make the Google Photos backend read only. 123 124 If you choose read only then rclone will only request read only access 125 to your photos, otherwise rclone will request full access.`, 126 }, { 127 Name: "read_size", 128 Default: false, 129 Help: `Set to read the size of media items. 130 131 Normally rclone does not read the size of media items since this takes 132 another transaction. This isn't necessary for syncing. However 133 rclone mount needs to know the size of files in advance of reading 134 them, so setting this flag when using rclone mount is recommended if 135 you want to read the media.`, 136 Advanced: true, 137 }}, 138 }) 139 } 140 141 // Options defines the configuration for this backend 142 type Options struct { 143 ReadOnly bool `config:"read_only"` 144 ReadSize bool `config:"read_size"` 145 } 146 147 // Fs represents a remote storage server 148 type Fs struct { 149 name string // name of this remote 150 root string // the path we are working on if any 151 opt Options // parsed options 152 features *fs.Features // optional features 153 unAuth *rest.Client // unauthenticated http client 154 srv *rest.Client // the connection to the one drive server 155 ts *oauthutil.TokenSource // token source for oauth2 156 pacer *fs.Pacer // To pace the API calls 157 startTime time.Time // time Fs was started - used for datestamps 158 albumsMu sync.Mutex // protect albums (but not contents) 159 albums map[bool]*albums // albums, shared or not 160 uploadedMu sync.Mutex // to protect the below 161 uploaded dirtree.DirTree // record of uploaded items 162 createMu sync.Mutex // held when creating albums to prevent dupes 163 } 164 165 // Object describes a storage object 166 // 167 // Will definitely have info but maybe not meta 168 type Object struct { 169 fs *Fs // what this object is part of 170 remote string // The remote path 171 url string // download path 172 id string // ID of this object 173 bytes int64 // Bytes in the object 174 modTime time.Time // Modified time of the object 175 mimeType string 176 } 177 178 // ------------------------------------------------------------ 179 180 // Name of the remote (as passed into NewFs) 181 func (f *Fs) Name() string { 182 return f.name 183 } 184 185 // Root of the remote (as passed into NewFs) 186 func (f *Fs) Root() string { 187 return f.root 188 } 189 190 // String converts this Fs to a string 191 func (f *Fs) String() string { 192 return fmt.Sprintf("Google Photos path %q", f.root) 193 } 194 195 // Features returns the optional features of this Fs 196 func (f *Fs) Features() *fs.Features { 197 return f.features 198 } 199 200 // dirTime returns the time to set a directory to 201 func (f *Fs) dirTime() time.Time { 202 return f.startTime 203 } 204 205 // retryErrorCodes is a slice of error codes that we will retry 206 var retryErrorCodes = []int{ 207 429, // Too Many Requests. 208 500, // Internal Server Error 209 502, // Bad Gateway 210 503, // Service Unavailable 211 504, // Gateway Timeout 212 509, // Bandwidth Limit Exceeded 213 } 214 215 // shouldRetry returns a boolean as to whether this resp and err 216 // deserve to be retried. It returns the err as a convenience 217 func shouldRetry(resp *http.Response, err error) (bool, error) { 218 return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err 219 } 220 221 // errorHandler parses a non 2xx error response into an error 222 func errorHandler(resp *http.Response) error { 223 body, err := rest.ReadBody(resp) 224 if err != nil { 225 body = nil 226 } 227 var e = api.Error{ 228 Details: api.ErrorDetails{ 229 Code: resp.StatusCode, 230 Message: string(body), 231 Status: resp.Status, 232 }, 233 } 234 if body != nil { 235 _ = json.Unmarshal(body, &e) 236 } 237 return &e 238 } 239 240 // NewFs constructs an Fs from the path, bucket:path 241 func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) { 242 // Parse config into Options struct 243 opt := new(Options) 244 err := configstruct.Set(m, opt) 245 if err != nil { 246 return nil, err 247 } 248 249 baseClient := fshttp.NewClient(fs.Config) 250 oAuthClient, ts, err := oauthutil.NewClientWithBaseClient(name, m, oauthConfig, baseClient) 251 if err != nil { 252 return nil, errors.Wrap(err, "failed to configure Box") 253 } 254 255 root = strings.Trim(path.Clean(root), "/") 256 if root == "." || root == "/" { 257 root = "" 258 } 259 260 f := &Fs{ 261 name: name, 262 root: root, 263 opt: *opt, 264 unAuth: rest.NewClient(baseClient), 265 srv: rest.NewClient(oAuthClient).SetRoot(rootURL), 266 ts: ts, 267 pacer: fs.NewPacer(pacer.NewGoogleDrive(pacer.MinSleep(minSleep))), 268 startTime: time.Now(), 269 albums: map[bool]*albums{}, 270 uploaded: dirtree.New(), 271 } 272 f.features = (&fs.Features{ 273 ReadMimeType: true, 274 }).Fill(f) 275 f.srv.SetErrorHandler(errorHandler) 276 277 _, _, pattern := patterns.match(f.root, "", true) 278 if pattern != nil && pattern.isFile { 279 oldRoot := f.root 280 var leaf string 281 f.root, leaf = path.Split(f.root) 282 f.root = strings.TrimRight(f.root, "/") 283 _, err := f.NewObject(context.TODO(), leaf) 284 if err == nil { 285 return f, fs.ErrorIsFile 286 } 287 f.root = oldRoot 288 } 289 return f, nil 290 } 291 292 // fetchEndpoint gets the openid endpoint named from the Google config 293 func (f *Fs) fetchEndpoint(ctx context.Context, name string) (endpoint string, err error) { 294 // Get openID config without auth 295 opts := rest.Opts{ 296 Method: "GET", 297 RootURL: "https://accounts.google.com/.well-known/openid-configuration", 298 } 299 var openIDconfig map[string]interface{} 300 err = f.pacer.Call(func() (bool, error) { 301 resp, err := f.unAuth.CallJSON(ctx, &opts, nil, &openIDconfig) 302 return shouldRetry(resp, err) 303 }) 304 if err != nil { 305 return "", errors.Wrap(err, "couldn't read openID config") 306 } 307 308 // Find userinfo endpoint 309 endpoint, ok := openIDconfig[name].(string) 310 if !ok { 311 return "", errors.Errorf("couldn't find %q from openID config", name) 312 } 313 314 return endpoint, nil 315 } 316 317 // UserInfo fetches info about the current user with oauth2 318 func (f *Fs) UserInfo(ctx context.Context) (userInfo map[string]string, err error) { 319 endpoint, err := f.fetchEndpoint(ctx, "userinfo_endpoint") 320 if err != nil { 321 return nil, err 322 } 323 324 // Fetch the user info with auth 325 opts := rest.Opts{ 326 Method: "GET", 327 RootURL: endpoint, 328 } 329 err = f.pacer.Call(func() (bool, error) { 330 resp, err := f.srv.CallJSON(ctx, &opts, nil, &userInfo) 331 return shouldRetry(resp, err) 332 }) 333 if err != nil { 334 return nil, errors.Wrap(err, "couldn't read user info") 335 } 336 return userInfo, nil 337 } 338 339 // Disconnect kills the token and refresh token 340 func (f *Fs) Disconnect(ctx context.Context) (err error) { 341 endpoint, err := f.fetchEndpoint(ctx, "revocation_endpoint") 342 if err != nil { 343 return err 344 } 345 token, err := f.ts.Token() 346 if err != nil { 347 return err 348 } 349 350 // Revoke the token and the refresh token 351 opts := rest.Opts{ 352 Method: "POST", 353 RootURL: endpoint, 354 MultipartParams: url.Values{ 355 "token": []string{token.AccessToken}, 356 "token_type_hint": []string{"access_token"}, 357 }, 358 } 359 var res interface{} 360 err = f.pacer.Call(func() (bool, error) { 361 resp, err := f.srv.CallJSON(ctx, &opts, nil, &res) 362 return shouldRetry(resp, err) 363 }) 364 if err != nil { 365 return errors.Wrap(err, "couldn't revoke token") 366 } 367 fs.Infof(f, "res = %+v", res) 368 return nil 369 } 370 371 // Return an Object from a path 372 // 373 // If it can't be found it returns the error fs.ErrorObjectNotFound. 374 func (f *Fs) newObjectWithInfo(ctx context.Context, remote string, info *api.MediaItem) (fs.Object, error) { 375 o := &Object{ 376 fs: f, 377 remote: remote, 378 } 379 if info != nil { 380 o.setMetaData(info) 381 } else { 382 err := o.readMetaData(ctx) // reads info and meta, returning an error 383 if err != nil { 384 return nil, err 385 } 386 } 387 return o, nil 388 } 389 390 // NewObject finds the Object at remote. If it can't be found 391 // it returns the error fs.ErrorObjectNotFound. 392 func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) { 393 defer log.Trace(f, "remote=%q", remote)("") 394 return f.newObjectWithInfo(ctx, remote, nil) 395 } 396 397 // addID adds the ID to name 398 func addID(name string, ID string) string { 399 idStr := "{" + ID + "}" 400 if name == "" { 401 return idStr 402 } 403 return name + " " + idStr 404 } 405 406 // addFileID adds the ID to the fileName passed in 407 func addFileID(fileName string, ID string) string { 408 ext := path.Ext(fileName) 409 base := fileName[:len(fileName)-len(ext)] 410 return addID(base, ID) + ext 411 } 412 413 var idRe = regexp.MustCompile(`\{([A-Za-z0-9_-]{55,})\}`) 414 415 // findID finds an ID in string if one is there or "" 416 func findID(name string) string { 417 match := idRe.FindStringSubmatch(name) 418 if match == nil { 419 return "" 420 } 421 return match[1] 422 } 423 424 // list the albums into an internal cache 425 // FIXME cache invalidation 426 func (f *Fs) listAlbums(ctx context.Context, shared bool) (all *albums, err error) { 427 f.albumsMu.Lock() 428 defer f.albumsMu.Unlock() 429 all, ok := f.albums[shared] 430 if ok && all != nil { 431 return all, nil 432 } 433 opts := rest.Opts{ 434 Method: "GET", 435 Path: "/albums", 436 Parameters: url.Values{}, 437 } 438 if shared { 439 opts.Path = "/sharedAlbums" 440 } 441 all = newAlbums() 442 opts.Parameters.Set("pageSize", strconv.Itoa(albumChunks)) 443 lastID := "" 444 for { 445 var result api.ListAlbums 446 var resp *http.Response 447 err = f.pacer.Call(func() (bool, error) { 448 resp, err = f.srv.CallJSON(ctx, &opts, nil, &result) 449 return shouldRetry(resp, err) 450 }) 451 if err != nil { 452 return nil, errors.Wrap(err, "couldn't list albums") 453 } 454 newAlbums := result.Albums 455 if shared { 456 newAlbums = result.SharedAlbums 457 } 458 if len(newAlbums) > 0 && newAlbums[0].ID == lastID { 459 // skip first if ID duplicated from last page 460 newAlbums = newAlbums[1:] 461 } 462 if len(newAlbums) > 0 { 463 lastID = newAlbums[len(newAlbums)-1].ID 464 } 465 for i := range newAlbums { 466 all.add(&newAlbums[i]) 467 } 468 if result.NextPageToken == "" { 469 break 470 } 471 opts.Parameters.Set("pageToken", result.NextPageToken) 472 } 473 f.albums[shared] = all 474 return all, nil 475 } 476 477 // listFn is called from list to handle an object. 478 type listFn func(remote string, object *api.MediaItem, isDirectory bool) error 479 480 // list the objects into the function supplied 481 // 482 // dir is the starting directory, "" for root 483 // 484 // Set recurse to read sub directories 485 func (f *Fs) list(ctx context.Context, filter api.SearchFilter, fn listFn) (err error) { 486 opts := rest.Opts{ 487 Method: "POST", 488 Path: "/mediaItems:search", 489 } 490 filter.PageSize = listChunks 491 filter.PageToken = "" 492 lastID := "" 493 for { 494 var result api.MediaItems 495 var resp *http.Response 496 err = f.pacer.Call(func() (bool, error) { 497 resp, err = f.srv.CallJSON(ctx, &opts, &filter, &result) 498 return shouldRetry(resp, err) 499 }) 500 if err != nil { 501 return errors.Wrap(err, "couldn't list files") 502 } 503 items := result.MediaItems 504 if len(items) > 0 && items[0].ID == lastID { 505 // skip first if ID duplicated from last page 506 items = items[1:] 507 } 508 if len(items) > 0 { 509 lastID = items[len(items)-1].ID 510 } 511 for i := range items { 512 item := &result.MediaItems[i] 513 remote := item.Filename 514 remote = strings.Replace(remote, "/", "/", -1) 515 err = fn(remote, item, false) 516 if err != nil { 517 return err 518 } 519 } 520 if result.NextPageToken == "" { 521 break 522 } 523 filter.PageToken = result.NextPageToken 524 } 525 526 return nil 527 } 528 529 // Convert a list item into a DirEntry 530 func (f *Fs) itemToDirEntry(ctx context.Context, remote string, item *api.MediaItem, isDirectory bool) (fs.DirEntry, error) { 531 if isDirectory { 532 d := fs.NewDir(remote, f.dirTime()) 533 return d, nil 534 } 535 o := &Object{ 536 fs: f, 537 remote: remote, 538 } 539 o.setMetaData(item) 540 return o, nil 541 } 542 543 // listDir lists a single directory 544 func (f *Fs) listDir(ctx context.Context, prefix string, filter api.SearchFilter) (entries fs.DirEntries, err error) { 545 // List the objects 546 err = f.list(ctx, filter, func(remote string, item *api.MediaItem, isDirectory bool) error { 547 entry, err := f.itemToDirEntry(ctx, prefix+remote, item, isDirectory) 548 if err != nil { 549 return err 550 } 551 if entry != nil { 552 entries = append(entries, entry) 553 } 554 return nil 555 }) 556 if err != nil { 557 return nil, err 558 } 559 // Dedupe the file names 560 dupes := map[string]int{} 561 for _, entry := range entries { 562 o, ok := entry.(*Object) 563 if ok { 564 dupes[o.remote]++ 565 } 566 } 567 for _, entry := range entries { 568 o, ok := entry.(*Object) 569 if ok { 570 duplicated := dupes[o.remote] > 1 571 if duplicated || o.remote == "" { 572 o.remote = addFileID(o.remote, o.id) 573 } 574 } 575 } 576 return entries, err 577 } 578 579 // listUploads lists a single directory from the uploads 580 func (f *Fs) listUploads(ctx context.Context, dir string) (entries fs.DirEntries, err error) { 581 f.uploadedMu.Lock() 582 entries, ok := f.uploaded[dir] 583 f.uploadedMu.Unlock() 584 if !ok && dir != "" { 585 return nil, fs.ErrorDirNotFound 586 } 587 return entries, nil 588 } 589 590 // List the objects and directories in dir into entries. The 591 // entries can be returned in any order but should be for a 592 // complete directory. 593 // 594 // dir should be "" to list the root, and should not have 595 // trailing slashes. 596 // 597 // This should return ErrDirNotFound if the directory isn't 598 // found. 599 func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) { 600 defer log.Trace(f, "dir=%q", dir)("err=%v", &err) 601 match, prefix, pattern := patterns.match(f.root, dir, false) 602 if pattern == nil || pattern.isFile { 603 return nil, fs.ErrorDirNotFound 604 } 605 if pattern.toEntries != nil { 606 return pattern.toEntries(ctx, f, prefix, match) 607 } 608 return nil, fs.ErrorDirNotFound 609 } 610 611 // Put the object into the bucket 612 // 613 // Copy the reader in to the new object which is returned 614 // 615 // The new object may have been created if an error is returned 616 func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) { 617 defer log.Trace(f, "src=%+v", src)("") 618 // Temporary Object under construction 619 o := &Object{ 620 fs: f, 621 remote: src.Remote(), 622 } 623 return o, o.Update(ctx, in, src, options...) 624 } 625 626 // createAlbum creates the album 627 func (f *Fs) createAlbum(ctx context.Context, albumTitle string) (album *api.Album, err error) { 628 opts := rest.Opts{ 629 Method: "POST", 630 Path: "/albums", 631 Parameters: url.Values{}, 632 } 633 var request = api.CreateAlbum{ 634 Album: &api.Album{ 635 Title: albumTitle, 636 }, 637 } 638 var result api.Album 639 var resp *http.Response 640 err = f.pacer.Call(func() (bool, error) { 641 resp, err = f.srv.CallJSON(ctx, &opts, request, &result) 642 return shouldRetry(resp, err) 643 }) 644 if err != nil { 645 return nil, errors.Wrap(err, "couldn't create album") 646 } 647 f.albums[false].add(&result) 648 return &result, nil 649 } 650 651 // getOrCreateAlbum gets an existing album or creates a new one 652 // 653 // It does the creation with the lock held to avoid duplicates 654 func (f *Fs) getOrCreateAlbum(ctx context.Context, albumTitle string) (album *api.Album, err error) { 655 f.createMu.Lock() 656 defer f.createMu.Unlock() 657 albums, err := f.listAlbums(ctx, false) 658 if err != nil { 659 return nil, err 660 } 661 album, ok := albums.get(albumTitle) 662 if ok { 663 return album, nil 664 } 665 return f.createAlbum(ctx, albumTitle) 666 } 667 668 // Mkdir creates the album if it doesn't exist 669 func (f *Fs) Mkdir(ctx context.Context, dir string) (err error) { 670 defer log.Trace(f, "dir=%q", dir)("err=%v", &err) 671 match, prefix, pattern := patterns.match(f.root, dir, false) 672 if pattern == nil { 673 return fs.ErrorDirNotFound 674 } 675 if !pattern.canMkdir { 676 return errCantMkdir 677 } 678 if pattern.isUpload { 679 f.uploadedMu.Lock() 680 d := fs.NewDir(strings.Trim(prefix, "/"), f.dirTime()) 681 f.uploaded.AddEntry(d) 682 f.uploadedMu.Unlock() 683 return nil 684 } 685 albumTitle := match[1] 686 _, err = f.getOrCreateAlbum(ctx, albumTitle) 687 return err 688 } 689 690 // Rmdir deletes the bucket if the fs is at the root 691 // 692 // Returns an error if it isn't empty 693 func (f *Fs) Rmdir(ctx context.Context, dir string) (err error) { 694 defer log.Trace(f, "dir=%q")("err=%v", &err) 695 match, _, pattern := patterns.match(f.root, dir, false) 696 if pattern == nil { 697 return fs.ErrorDirNotFound 698 } 699 if !pattern.canMkdir { 700 return errCantRmdir 701 } 702 if pattern.isUpload { 703 f.uploadedMu.Lock() 704 err = f.uploaded.Prune(map[string]bool{ 705 dir: true, 706 }) 707 f.uploadedMu.Unlock() 708 return err 709 } 710 albumTitle := match[1] 711 allAlbums, err := f.listAlbums(ctx, false) 712 if err != nil { 713 return err 714 } 715 album, ok := allAlbums.get(albumTitle) 716 if !ok { 717 return fs.ErrorDirNotFound 718 } 719 _ = album 720 return errAlbumDelete 721 } 722 723 // Precision returns the precision 724 func (f *Fs) Precision() time.Duration { 725 return fs.ModTimeNotSupported 726 } 727 728 // Hashes returns the supported hash sets. 729 func (f *Fs) Hashes() hash.Set { 730 return hash.Set(hash.None) 731 } 732 733 // ------------------------------------------------------------ 734 735 // Fs returns the parent Fs 736 func (o *Object) Fs() fs.Info { 737 return o.fs 738 } 739 740 // Return a string version 741 func (o *Object) String() string { 742 if o == nil { 743 return "<nil>" 744 } 745 return o.remote 746 } 747 748 // Remote returns the remote path 749 func (o *Object) Remote() string { 750 return o.remote 751 } 752 753 // Hash returns the Md5sum of an object returning a lowercase hex string 754 func (o *Object) Hash(ctx context.Context, t hash.Type) (string, error) { 755 return "", hash.ErrUnsupported 756 } 757 758 // Size returns the size of an object in bytes 759 func (o *Object) Size() int64 { 760 defer log.Trace(o, "")("") 761 if !o.fs.opt.ReadSize || o.bytes >= 0 { 762 return o.bytes 763 } 764 ctx := context.TODO() 765 err := o.readMetaData(ctx) 766 if err != nil { 767 fs.Debugf(o, "Size: Failed to read metadata: %v", err) 768 return -1 769 } 770 var resp *http.Response 771 opts := rest.Opts{ 772 Method: "HEAD", 773 RootURL: o.downloadURL(), 774 } 775 err = o.fs.pacer.Call(func() (bool, error) { 776 resp, err = o.fs.srv.Call(ctx, &opts) 777 return shouldRetry(resp, err) 778 }) 779 if err != nil { 780 fs.Debugf(o, "Reading size failed: %v", err) 781 } else { 782 lengthStr := resp.Header.Get("Content-Length") 783 length, err := strconv.ParseInt(lengthStr, 10, 64) 784 if err != nil { 785 fs.Debugf(o, "Reading size failed to parse Content_length %q: %v", lengthStr, err) 786 } else { 787 o.bytes = length 788 } 789 } 790 return o.bytes 791 } 792 793 // setMetaData sets the fs data from a storage.Object 794 func (o *Object) setMetaData(info *api.MediaItem) { 795 o.url = info.BaseURL 796 o.id = info.ID 797 o.bytes = -1 // FIXME 798 o.mimeType = info.MimeType 799 o.modTime = info.MediaMetadata.CreationTime 800 } 801 802 // readMetaData gets the metadata if it hasn't already been fetched 803 // 804 // it also sets the info 805 func (o *Object) readMetaData(ctx context.Context) (err error) { 806 if !o.modTime.IsZero() && o.url != "" { 807 return nil 808 } 809 dir, fileName := path.Split(o.remote) 810 dir = strings.Trim(dir, "/") 811 _, _, pattern := patterns.match(o.fs.root, o.remote, true) 812 if pattern == nil { 813 return fs.ErrorObjectNotFound 814 } 815 if !pattern.isFile { 816 return fs.ErrorNotAFile 817 } 818 // If have ID fetch it directly 819 if id := findID(fileName); id != "" { 820 opts := rest.Opts{ 821 Method: "GET", 822 Path: "/mediaItems/" + id, 823 } 824 var item api.MediaItem 825 var resp *http.Response 826 err = o.fs.pacer.Call(func() (bool, error) { 827 resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &item) 828 return shouldRetry(resp, err) 829 }) 830 if err != nil { 831 return errors.Wrap(err, "couldn't get media item") 832 } 833 o.setMetaData(&item) 834 return nil 835 } 836 // Otherwise list the directory the file is in 837 entries, err := o.fs.List(ctx, dir) 838 if err != nil { 839 if err == fs.ErrorDirNotFound { 840 return fs.ErrorObjectNotFound 841 } 842 return err 843 } 844 // and find the file in the directory 845 for _, entry := range entries { 846 if entry.Remote() == o.remote { 847 if newO, ok := entry.(*Object); ok { 848 *o = *newO 849 return nil 850 } 851 } 852 } 853 return fs.ErrorObjectNotFound 854 } 855 856 // ModTime returns the modification time of the object 857 // 858 // It attempts to read the objects mtime and if that isn't present the 859 // LastModified returned in the http headers 860 func (o *Object) ModTime(ctx context.Context) time.Time { 861 defer log.Trace(o, "")("") 862 err := o.readMetaData(ctx) 863 if err != nil { 864 fs.Debugf(o, "ModTime: Failed to read metadata: %v", err) 865 return time.Now() 866 } 867 return o.modTime 868 } 869 870 // SetModTime sets the modification time of the local fs object 871 func (o *Object) SetModTime(ctx context.Context, modTime time.Time) (err error) { 872 return fs.ErrorCantSetModTime 873 } 874 875 // Storable returns a boolean as to whether this object is storable 876 func (o *Object) Storable() bool { 877 return true 878 } 879 880 // downloadURL returns the URL for a full bytes download for the object 881 func (o *Object) downloadURL() string { 882 url := o.url + "=d" 883 if strings.HasPrefix(o.mimeType, "video/") { 884 url += "v" 885 } 886 return url 887 } 888 889 // Open an object for read 890 func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.ReadCloser, err error) { 891 defer log.Trace(o, "")("") 892 err = o.readMetaData(ctx) 893 if err != nil { 894 fs.Debugf(o, "Open: Failed to read metadata: %v", err) 895 return nil, err 896 } 897 var resp *http.Response 898 opts := rest.Opts{ 899 Method: "GET", 900 RootURL: o.downloadURL(), 901 Options: options, 902 } 903 err = o.fs.pacer.Call(func() (bool, error) { 904 resp, err = o.fs.srv.Call(ctx, &opts) 905 return shouldRetry(resp, err) 906 }) 907 if err != nil { 908 return nil, err 909 } 910 return resp.Body, err 911 } 912 913 // Update the object with the contents of the io.Reader, modTime and size 914 // 915 // The new object may have been created if an error is returned 916 func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (err error) { 917 defer log.Trace(o, "src=%+v", src)("err=%v", &err) 918 match, _, pattern := patterns.match(o.fs.root, o.remote, true) 919 if pattern == nil || !pattern.isFile || !pattern.canUpload { 920 return errCantUpload 921 } 922 var ( 923 albumID string 924 fileName string 925 ) 926 if pattern.isUpload { 927 fileName = match[1] 928 } else { 929 var albumTitle string 930 albumTitle, fileName = match[1], match[2] 931 932 album, err := o.fs.getOrCreateAlbum(ctx, albumTitle) 933 if err != nil { 934 return err 935 } 936 937 if !album.IsWriteable { 938 return errOwnAlbums 939 } 940 941 albumID = album.ID 942 } 943 944 // Upload the media item in exchange for an UploadToken 945 opts := rest.Opts{ 946 Method: "POST", 947 Path: "/uploads", 948 ExtraHeaders: map[string]string{ 949 "X-Goog-Upload-File-Name": fileName, 950 "X-Goog-Upload-Protocol": "raw", 951 }, 952 Body: in, 953 } 954 var token []byte 955 var resp *http.Response 956 err = o.fs.pacer.CallNoRetry(func() (bool, error) { 957 resp, err = o.fs.srv.Call(ctx, &opts) 958 if err != nil { 959 return shouldRetry(resp, err) 960 } 961 token, err = rest.ReadBody(resp) 962 return shouldRetry(resp, err) 963 }) 964 if err != nil { 965 return errors.Wrap(err, "couldn't upload file") 966 } 967 uploadToken := strings.TrimSpace(string(token)) 968 if uploadToken == "" { 969 return errors.New("empty upload token") 970 } 971 972 // Create the media item from an UploadToken, optionally adding to an album 973 opts = rest.Opts{ 974 Method: "POST", 975 Path: "/mediaItems:batchCreate", 976 } 977 var request = api.BatchCreateRequest{ 978 AlbumID: albumID, 979 NewMediaItems: []api.NewMediaItem{ 980 { 981 SimpleMediaItem: api.SimpleMediaItem{ 982 UploadToken: uploadToken, 983 }, 984 }, 985 }, 986 } 987 var result api.BatchCreateResponse 988 err = o.fs.pacer.Call(func() (bool, error) { 989 resp, err = o.fs.srv.CallJSON(ctx, &opts, request, &result) 990 return shouldRetry(resp, err) 991 }) 992 if err != nil { 993 return errors.Wrap(err, "failed to create media item") 994 } 995 if len(result.NewMediaItemResults) != 1 { 996 return errors.New("bad response to BatchCreate wrong number of items") 997 } 998 mediaItemResult := result.NewMediaItemResults[0] 999 if mediaItemResult.Status.Code != 0 { 1000 return errors.Errorf("upload failed: %s (%d)", mediaItemResult.Status.Message, mediaItemResult.Status.Code) 1001 } 1002 o.setMetaData(&mediaItemResult.MediaItem) 1003 1004 // Add upload to internal storage 1005 if pattern.isUpload { 1006 o.fs.uploaded.AddEntry(o) 1007 } 1008 return nil 1009 } 1010 1011 // Remove an object 1012 func (o *Object) Remove(ctx context.Context) (err error) { 1013 match, _, pattern := patterns.match(o.fs.root, o.remote, true) 1014 if pattern == nil || !pattern.isFile || !pattern.canUpload || pattern.isUpload { 1015 return errRemove 1016 } 1017 albumTitle, fileName := match[1], match[2] 1018 album, ok := o.fs.albums[false].get(albumTitle) 1019 if !ok { 1020 return errors.Errorf("couldn't file %q in album %q for delete", fileName, albumTitle) 1021 } 1022 opts := rest.Opts{ 1023 Method: "POST", 1024 Path: "/albums/" + album.ID + ":batchRemoveMediaItems", 1025 NoResponse: true, 1026 } 1027 var request = api.BatchRemoveItems{ 1028 MediaItemIds: []string{o.id}, 1029 } 1030 var resp *http.Response 1031 err = o.fs.pacer.Call(func() (bool, error) { 1032 resp, err = o.fs.srv.CallJSON(ctx, &opts, &request, nil) 1033 return shouldRetry(resp, err) 1034 }) 1035 if err != nil { 1036 return errors.Wrap(err, "couldn't delete item from album") 1037 } 1038 return nil 1039 } 1040 1041 // MimeType of an Object if known, "" otherwise 1042 func (o *Object) MimeType(ctx context.Context) string { 1043 return o.mimeType 1044 } 1045 1046 // ID of an Object if known, "" otherwise 1047 func (o *Object) ID() string { 1048 return o.id 1049 } 1050 1051 // Check the interfaces are satisfied 1052 var ( 1053 _ fs.Fs = &Fs{} 1054 _ fs.UserInfoer = &Fs{} 1055 _ fs.Disconnecter = &Fs{} 1056 _ fs.Object = &Object{} 1057 _ fs.MimeTyper = &Object{} 1058 _ fs.IDer = &Object{} 1059 )