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