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