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  )