drive: add team drive support - fixes #885

This commit is contained in:
Nick Craig-Wood 2017-06-01 20:12:11 +01:00
parent bdc19b7c8a
commit a5cfdfd233
3 changed files with 163 additions and 40 deletions

View File

@ -22,10 +22,13 @@ Here is an example of how to make a remote called `remote`. First run:
This will guide you through an interactive setup process: This will guide you through an interactive setup process:
``` ```
No remotes found - make a new one
n) New remote n) New remote
d) Delete remote r) Rename remote
c) Copy remote
s) Set configuration password
q) Quit config q) Quit config
e/n/d/q> n n/r/c/s/q> n
name> remote name> remote
Type of storage to configure. Type of storage to configure.
Choose a number from below, or type in your own value Choose a number from below, or type in your own value
@ -39,23 +42,25 @@ Choose a number from below, or type in your own value
\ "dropbox" \ "dropbox"
5 / Encrypt/Decrypt a remote 5 / Encrypt/Decrypt a remote
\ "crypt" \ "crypt"
6 / Google Cloud Storage (this is not Google Drive) 6 / FTP Connection
\ "ftp"
7 / Google Cloud Storage (this is not Google Drive)
\ "google cloud storage" \ "google cloud storage"
7 / Google Drive 8 / Google Drive
\ "drive" \ "drive"
8 / Hubic 9 / Hubic
\ "hubic" \ "hubic"
9 / Local Disk 10 / Local Disk
\ "local" \ "local"
10 / Microsoft OneDrive 11 / Microsoft OneDrive
\ "onedrive" \ "onedrive"
11 / Openstack Swift (Rackspace Cloud Files, Memset Memstore, OVH) 12 / Openstack Swift (Rackspace Cloud Files, Memset Memstore, OVH)
\ "swift" \ "swift"
12 / SSH/SFTP Connection 13 / SSH/SFTP Connection
\ "sftp" \ "sftp"
13 / Yandex Disk 14 / Yandex Disk
\ "yandex" \ "yandex"
Storage> 7 Storage> 8
Google Application Client Id - leave blank normally. Google Application Client Id - leave blank normally.
client_id> client_id>
Google Application Client Secret - leave blank normally. Google Application Client Secret - leave blank normally.
@ -71,6 +76,10 @@ If your browser doesn't open automatically go to the following link: http://127.
Log in and authorize rclone for access Log in and authorize rclone for access
Waiting for code... Waiting for code...
Got code Got code
Configure this as a team drive?
y) Yes
n) No
y/n> n
-------------------- --------------------
[remote] [remote]
client_id = client_id =
@ -104,6 +113,44 @@ To copy a local directory to a drive directory called backup
rclone copy /home/source remote:backup rclone copy /home/source remote:backup
### Team drives ###
If you want to configure the remote to point to a Google Team Drive
then answer `y` to the question `Configure this as a team drive?`.
This will fetch the list of Team Drives from google and allow you to
configure which one you want to use. You can also type in a team
drive ID if you prefer.
For example:
```
Configure this as a team drive?
y) Yes
n) No
y/n> y
Fetching team drive list...
Choose a number from below, or type in your own value
1 / Rclone Test
\ "xxxxxxxxxxxxxxxxxxxx"
2 / Rclone Test 2
\ "yyyyyyyyyyyyyyyyyyyy"
3 / Rclone Test 3
\ "zzzzzzzzzzzzzzzzzzzz"
Enter a Team Drive ID> 1
--------------------
[remote]
client_id =
client_secret =
token = {"AccessToken":"xxxx.x.xxxxx_xxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","RefreshToken":"1/xxxxxxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxx","Expiry":"2014-03-16T13:57:58.955387075Z","Extra":null}
team_drive = xxxxxxxxxxxxxxxxxxxx
--------------------
y) Yes this is OK
e) Edit this remote
d) Delete this remote
y/e/d> y
```
### Modified time ### ### Modified time ###
Google drive stores modification times accurate to 1 ms. Google drive stores modification times accurate to 1 ms.

View File

@ -99,6 +99,10 @@ func init() {
if err != nil { if err != nil {
log.Fatalf("Failed to configure token: %v", err) log.Fatalf("Failed to configure token: %v", err)
} }
err = configTeamDrive(name)
if err != nil {
log.Fatalf("Failed to configure team drive: %v", err)
}
}, },
Options: []fs.Option{{ Options: []fs.Option{{
Name: fs.ConfigClientID, Name: fs.ConfigClientID,
@ -129,6 +133,8 @@ type Fs struct {
dirCache *dircache.DirCache // Map of directory path to directory id dirCache *dircache.DirCache // Map of directory path to directory id
pacer *pacer.Pacer // To pace the API calls pacer *pacer.Pacer // To pace the API calls
extensions []string // preferred extensions to download docs extensions []string // preferred extensions to download docs
teamDriveID string // team drive ID, may be ""
isTeamDrive bool // true if this is a team drive
} }
// Object describes a drive object // Object describes a drive object
@ -241,6 +247,12 @@ func (f *Fs) listAll(dirID string, title string, directoriesOnly bool, filesOnly
if *driveListChunk > 0 { if *driveListChunk > 0 {
list = list.MaxResults(*driveListChunk) list = list.MaxResults(*driveListChunk)
} }
if f.isTeamDrive {
list.TeamDriveId(f.teamDriveID)
list.SupportsTeamDrives(true)
list.IncludeTeamDriveItems(true)
list.Corpora("teamDrive")
}
var fields = partialFields var fields = partialFields
@ -307,6 +319,61 @@ func (f *Fs) parseExtensions(extensions string) error {
return nil return nil
} }
// Figure out if the user wants to use a team drive
func configTeamDrive(name string) error {
teamDrive := fs.ConfigFileGet(name, "team_drive")
if teamDrive == "" {
fmt.Printf("Configure this as a team drive?\n")
} else {
fmt.Printf("Change current team drive ID %q?\n", teamDrive)
}
if !fs.Confirm() {
return nil
}
client, _, err := oauthutil.NewClient(name, driveConfig)
if err != nil {
return errors.Wrap(err, "config team drive failed to make oauth client")
}
svc, err := drive.New(client)
if err != nil {
return errors.Wrap(err, "config team drive failed to make drive client")
}
fmt.Printf("Fetching team drive list...\n")
var driveIDs, driveNames []string
listTeamDrives := svc.Teamdrives.List().MaxResults(100)
for {
var teamDrives *drive.TeamDriveList
err = newPacer().Call(func() (bool, error) {
teamDrives, err = listTeamDrives.Do()
return shouldRetry(err)
})
if err != nil {
return errors.Wrap(err, "list team drives failed")
}
for _, drive := range teamDrives.Items {
driveIDs = append(driveIDs, drive.Id)
driveNames = append(driveNames, drive.Name)
}
if teamDrives.NextPageToken == "" {
break
}
listTeamDrives.PageToken(teamDrives.NextPageToken)
}
var driveID string
if len(driveIDs) == 0 {
fmt.Printf("No team drives found in your account")
} else {
driveID = fs.Choose("Enter a Team Drive ID", driveIDs, driveNames, true)
}
fs.ConfigFileSet(name, "team_drive", driveID)
return nil
}
// newPacer makes a pacer configured for drive
func newPacer() *pacer.Pacer {
return pacer.New().SetMinSleep(minSleep).SetPacer(pacer.GoogleDrivePacer)
}
// NewFs contstructs an Fs from the path, container:path // NewFs contstructs an Fs from the path, container:path
func NewFs(name, path string) (fs.Fs, error) { func NewFs(name, path string) (fs.Fs, error) {
if !isPowerOfTwo(int64(chunkSize)) { if !isPowerOfTwo(int64(chunkSize)) {
@ -329,8 +396,10 @@ func NewFs(name, path string) (fs.Fs, error) {
f := &Fs{ f := &Fs{
name: name, name: name,
root: root, root: root,
pacer: pacer.New().SetMinSleep(minSleep).SetPacer(pacer.GoogleDrivePacer), pacer: newPacer(),
} }
f.teamDriveID = fs.ConfigFileGet(name, "team_drive")
f.isTeamDrive = f.teamDriveID != ""
f.features = (&fs.Features{DuplicateFiles: true, ReadMimeType: true, WriteMimeType: true}).Fill(f) f.features = (&fs.Features{DuplicateFiles: true, ReadMimeType: true, WriteMimeType: true}).Fill(f)
// Create a new authorized Drive client. // Create a new authorized Drive client.
@ -348,6 +417,10 @@ func NewFs(name, path string) (fs.Fs, error) {
if err != nil { if err != nil {
return nil, errors.Wrap(err, "couldn't read info about Drive") return nil, errors.Wrap(err, "couldn't read info about Drive")
} }
// override root folder for a team drive
if f.isTeamDrive {
f.about.RootFolderId = f.teamDriveID
}
f.dirCache = dircache.New(root, f.about.RootFolderId, f) f.dirCache = dircache.New(root, f.about.RootFolderId, f)
@ -437,7 +510,7 @@ func (f *Fs) CreateDir(pathID, leaf string) (newID string, err error) {
} }
var info *drive.File var info *drive.File
err = f.pacer.Call(func() (bool, error) { err = f.pacer.Call(func() (bool, error) {
info, err = f.svc.Files.Insert(createInfo).Fields(googleapi.Field(partialFields)).Do() info, err = f.svc.Files.Insert(createInfo).Fields(googleapi.Field(partialFields)).SupportsTeamDrives(f.isTeamDrive).Do()
return shouldRetry(err) return shouldRetry(err)
}) })
if err != nil { if err != nil {
@ -616,7 +689,7 @@ func (f *Fs) PutUnchecked(in io.Reader, src fs.ObjectInfo, options ...fs.OpenOpt
// Make the API request to upload metadata and file data. // Make the API request to upload metadata and file data.
// Don't retry, return a retry error instead // Don't retry, return a retry error instead
err = f.pacer.CallNoRetry(func() (bool, error) { err = f.pacer.CallNoRetry(func() (bool, error) {
info, err = f.svc.Files.Insert(createInfo).Media(in, googleapi.ContentType("")).Fields(googleapi.Field(partialFields)).Do() info, err = f.svc.Files.Insert(createInfo).Media(in, googleapi.ContentType("")).Fields(googleapi.Field(partialFields)).SupportsTeamDrives(f.isTeamDrive).Do()
return shouldRetry(err) return shouldRetry(err)
}) })
if err != nil { if err != nil {
@ -678,9 +751,9 @@ func (f *Fs) Rmdir(dir string) error {
// in or the user wants to trash, otherwise // in or the user wants to trash, otherwise
// delete it. // delete it.
if trashedFiles || *driveUseTrash { if trashedFiles || *driveUseTrash {
_, err = f.svc.Files.Trash(directoryID).Fields(googleapi.Field(partialFields)).Do() _, err = f.svc.Files.Trash(directoryID).Fields(googleapi.Field(partialFields)).SupportsTeamDrives(f.isTeamDrive).Do()
} else { } else {
err = f.svc.Files.Delete(directoryID).Fields(googleapi.Field(partialFields)).Do() err = f.svc.Files.Delete(directoryID).Fields(googleapi.Field(partialFields)).SupportsTeamDrives(f.isTeamDrive).Do()
} }
return shouldRetry(err) return shouldRetry(err)
}) })
@ -726,7 +799,7 @@ func (f *Fs) Copy(src fs.Object, remote string) (fs.Object, error) {
var info *drive.File var info *drive.File
err = o.fs.pacer.Call(func() (bool, error) { err = o.fs.pacer.Call(func() (bool, error) {
info, err = o.fs.svc.Files.Copy(srcObj.id, createInfo).Fields(googleapi.Field(partialFields)).Do() info, err = o.fs.svc.Files.Copy(srcObj.id, createInfo).Fields(googleapi.Field(partialFields)).SupportsTeamDrives(f.isTeamDrive).Do()
return shouldRetry(err) return shouldRetry(err)
}) })
if err != nil { if err != nil {
@ -752,9 +825,9 @@ func (f *Fs) Purge() error {
} }
err = f.pacer.Call(func() (bool, error) { err = f.pacer.Call(func() (bool, error) {
if *driveUseTrash { if *driveUseTrash {
_, err = f.svc.Files.Trash(f.dirCache.RootID()).Fields(googleapi.Field(partialFields)).Do() _, err = f.svc.Files.Trash(f.dirCache.RootID()).Fields(googleapi.Field(partialFields)).SupportsTeamDrives(f.isTeamDrive).Do()
} else { } else {
err = f.svc.Files.Delete(f.dirCache.RootID()).Fields(googleapi.Field(partialFields)).Do() err = f.svc.Files.Delete(f.dirCache.RootID()).Fields(googleapi.Field(partialFields)).SupportsTeamDrives(f.isTeamDrive).Do()
} }
return shouldRetry(err) return shouldRetry(err)
}) })
@ -793,7 +866,7 @@ func (f *Fs) Move(src fs.Object, remote string) (fs.Object, error) {
// Do the move // Do the move
var info *drive.File var info *drive.File
err = f.pacer.Call(func() (bool, error) { err = f.pacer.Call(func() (bool, error) {
info, err = f.svc.Files.Patch(srcObj.id, dstInfo).SetModifiedDate(true).Fields(googleapi.Field(partialFields)).Do() info, err = f.svc.Files.Patch(srcObj.id, dstInfo).SetModifiedDate(true).Fields(googleapi.Field(partialFields)).SupportsTeamDrives(f.isTeamDrive).Do()
return shouldRetry(err) return shouldRetry(err)
}) })
if err != nil { if err != nil {
@ -880,7 +953,7 @@ func (f *Fs) DirMove(src fs.Fs, srcRemote, dstRemote string) error {
Parents: []*drive.ParentReference{{Id: directoryID}}, Parents: []*drive.ParentReference{{Id: directoryID}},
} }
err = f.pacer.Call(func() (bool, error) { err = f.pacer.Call(func() (bool, error) {
_, err = f.svc.Files.Patch(srcID, &patch).Fields(googleapi.Field(partialFields)).Do() _, err = f.svc.Files.Patch(srcID, &patch).Fields(googleapi.Field(partialFields)).SupportsTeamDrives(f.isTeamDrive).Do()
return shouldRetry(err) return shouldRetry(err)
}) })
if err != nil { if err != nil {
@ -922,7 +995,7 @@ func (f *Fs) dirchangeNotifyRunner(notifyFunc func(string), pollInterval time.Du
var startPageToken *drive.StartPageToken var startPageToken *drive.StartPageToken
err = f.pacer.Call(func() (bool, error) { err = f.pacer.Call(func() (bool, error) {
startPageToken, err = f.svc.Changes.GetStartPageToken().Do() startPageToken, err = f.svc.Changes.GetStartPageToken().SupportsTeamDrives(f.isTeamDrive).Do()
return shouldRetry(err) return shouldRetry(err)
}) })
if err != nil { if err != nil {
@ -941,7 +1014,7 @@ func (f *Fs) dirchangeNotifyRunner(notifyFunc func(string), pollInterval time.Du
if *driveListChunk > 0 { if *driveListChunk > 0 {
changesCall = changesCall.MaxResults(*driveListChunk) changesCall = changesCall.MaxResults(*driveListChunk)
} }
changeList, err = changesCall.Do() changeList, err = changesCall.SupportsTeamDrives(f.isTeamDrive).Do()
return shouldRetry(err) return shouldRetry(err)
}) })
if err != nil { if err != nil {
@ -1118,7 +1191,7 @@ func (o *Object) SetModTime(modTime time.Time) error {
// Set modified date // Set modified date
var info *drive.File var info *drive.File
err = o.fs.pacer.Call(func() (bool, error) { err = o.fs.pacer.Call(func() (bool, error) {
info, err = o.fs.svc.Files.Update(o.id, updateInfo).SetModifiedDate(true).Fields(googleapi.Field(partialFields)).Do() info, err = o.fs.svc.Files.Update(o.id, updateInfo).SetModifiedDate(true).Fields(googleapi.Field(partialFields)).SupportsTeamDrives(o.fs.isTeamDrive).Do()
return shouldRetry(err) return shouldRetry(err)
}) })
if err != nil { if err != nil {
@ -1230,7 +1303,7 @@ func (o *Object) Update(in io.Reader, src fs.ObjectInfo, options ...fs.OpenOptio
if size == 0 || size < int64(driveUploadCutoff) { if size == 0 || size < int64(driveUploadCutoff) {
// Don't retry, return a retry error instead // Don't retry, return a retry error instead
err = o.fs.pacer.CallNoRetry(func() (bool, error) { err = o.fs.pacer.CallNoRetry(func() (bool, error) {
info, err = o.fs.svc.Files.Update(updateInfo.Id, updateInfo).SetModifiedDate(true).Media(in, googleapi.ContentType("")).Fields(googleapi.Field(partialFields)).Do() info, err = o.fs.svc.Files.Update(updateInfo.Id, updateInfo).SetModifiedDate(true).Media(in, googleapi.ContentType("")).Fields(googleapi.Field(partialFields)).SupportsTeamDrives(o.fs.isTeamDrive).Do()
return shouldRetry(err) return shouldRetry(err)
}) })
if err != nil { if err != nil {
@ -1255,9 +1328,9 @@ func (o *Object) Remove() error {
var err error var err error
err = o.fs.pacer.Call(func() (bool, error) { err = o.fs.pacer.Call(func() (bool, error) {
if *driveUseTrash { if *driveUseTrash {
_, err = o.fs.svc.Files.Trash(o.id).Fields(googleapi.Field(partialFields)).Do() _, err = o.fs.svc.Files.Trash(o.id).Fields(googleapi.Field(partialFields)).SupportsTeamDrives(o.fs.isTeamDrive).Do()
} else { } else {
err = o.fs.svc.Files.Delete(o.id).Fields(googleapi.Field(partialFields)).Do() err = o.fs.svc.Files.Delete(o.id).Fields(googleapi.Field(partialFields)).SupportsTeamDrives(o.fs.isTeamDrive).Do()
} }
return shouldRetry(err) return shouldRetry(err)
}) })

View File

@ -57,6 +57,9 @@ func (f *Fs) Upload(in io.Reader, size int64, contentType string, info *drive.Fi
params := make(url.Values) params := make(url.Values)
params.Set("alt", "json") params.Set("alt", "json")
params.Set("uploadType", "resumable") params.Set("uploadType", "resumable")
if f.isTeamDrive {
params.Set("supportsTeamDrives", "true")
}
urls := "https://www.googleapis.com/upload/drive/v2/files" urls := "https://www.googleapis.com/upload/drive/v2/files"
method := "POST" method := "POST"
if fileID != "" { if fileID != "" {