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