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  )