googlephotos: implement optional features UserInfo and Disconnect
As part of rclone's UX review it was required that rclone had a means of disconnecting from google photos and showing which user is connected.
This commit is contained in:
parent
a808e98fe1
commit
cd7ca2a320
1 changed files with 107 additions and 17 deletions
|
@ -27,6 +27,7 @@ import (
|
||||||
"github.com/rclone/rclone/fs/config/obscure"
|
"github.com/rclone/rclone/fs/config/obscure"
|
||||||
"github.com/rclone/rclone/fs/dirtree"
|
"github.com/rclone/rclone/fs/dirtree"
|
||||||
"github.com/rclone/rclone/fs/fserrors"
|
"github.com/rclone/rclone/fs/fserrors"
|
||||||
|
"github.com/rclone/rclone/fs/fshttp"
|
||||||
"github.com/rclone/rclone/fs/hash"
|
"github.com/rclone/rclone/fs/hash"
|
||||||
"github.com/rclone/rclone/fs/log"
|
"github.com/rclone/rclone/fs/log"
|
||||||
"github.com/rclone/rclone/lib/oauthutil"
|
"github.com/rclone/rclone/lib/oauthutil"
|
||||||
|
@ -60,6 +61,8 @@ var (
|
||||||
// Description of how to auth for this app
|
// Description of how to auth for this app
|
||||||
oauthConfig = &oauth2.Config{
|
oauthConfig = &oauth2.Config{
|
||||||
Scopes: []string{
|
Scopes: []string{
|
||||||
|
"openid",
|
||||||
|
"profile",
|
||||||
scopeReadWrite,
|
scopeReadWrite,
|
||||||
},
|
},
|
||||||
Endpoint: google.Endpoint,
|
Endpoint: google.Endpoint,
|
||||||
|
@ -143,18 +146,20 @@ type Options struct {
|
||||||
|
|
||||||
// Fs represents a remote storage server
|
// Fs represents a remote storage server
|
||||||
type Fs struct {
|
type Fs struct {
|
||||||
name string // name of this remote
|
name string // name of this remote
|
||||||
root string // the path we are working on if any
|
root string // the path we are working on if any
|
||||||
opt Options // parsed options
|
opt Options // parsed options
|
||||||
features *fs.Features // optional features
|
features *fs.Features // optional features
|
||||||
srv *rest.Client // the connection to the one drive server
|
unAuth *rest.Client // unauthenticated http client
|
||||||
pacer *fs.Pacer // To pace the API calls
|
srv *rest.Client // the connection to the one drive server
|
||||||
startTime time.Time // time Fs was started - used for datestamps
|
ts *oauthutil.TokenSource // token source for oauth2
|
||||||
albumsMu sync.Mutex // protect albums (but not contents)
|
pacer *fs.Pacer // To pace the API calls
|
||||||
albums map[bool]*albums // albums, shared or not
|
startTime time.Time // time Fs was started - used for datestamps
|
||||||
uploadedMu sync.Mutex // to protect the below
|
albumsMu sync.Mutex // protect albums (but not contents)
|
||||||
uploaded dirtree.DirTree // record of uploaded items
|
albums map[bool]*albums // albums, shared or not
|
||||||
createMu sync.Mutex // held when creating albums to prevent dupes
|
uploadedMu sync.Mutex // to protect the below
|
||||||
|
uploaded dirtree.DirTree // record of uploaded items
|
||||||
|
createMu sync.Mutex // held when creating albums to prevent dupes
|
||||||
}
|
}
|
||||||
|
|
||||||
// Object describes a storage object
|
// Object describes a storage object
|
||||||
|
@ -241,7 +246,8 @@ func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
oAuthClient, _, err := oauthutil.NewClient(name, m, oauthConfig)
|
baseClient := fshttp.NewClient(fs.Config)
|
||||||
|
oAuthClient, ts, err := oauthutil.NewClientWithBaseClient(name, m, oauthConfig, baseClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to configure Box")
|
return nil, errors.Wrap(err, "failed to configure Box")
|
||||||
}
|
}
|
||||||
|
@ -250,11 +256,14 @@ func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
|
||||||
if root == "." || root == "/" {
|
if root == "." || root == "/" {
|
||||||
root = ""
|
root = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
f := &Fs{
|
f := &Fs{
|
||||||
name: name,
|
name: name,
|
||||||
root: root,
|
root: root,
|
||||||
opt: *opt,
|
opt: *opt,
|
||||||
|
unAuth: rest.NewClient(baseClient),
|
||||||
srv: rest.NewClient(oAuthClient).SetRoot(rootURL),
|
srv: rest.NewClient(oAuthClient).SetRoot(rootURL),
|
||||||
|
ts: ts,
|
||||||
pacer: fs.NewPacer(pacer.NewGoogleDrive(pacer.MinSleep(minSleep))),
|
pacer: fs.NewPacer(pacer.NewGoogleDrive(pacer.MinSleep(minSleep))),
|
||||||
startTime: time.Now(),
|
startTime: time.Now(),
|
||||||
albums: map[bool]*albums{},
|
albums: map[bool]*albums{},
|
||||||
|
@ -280,6 +289,85 @@ func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
|
||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fetchEndpoint gets the openid endpoint named from the Google config
|
||||||
|
func (f *Fs) fetchEndpoint(name string) (endpoint string, err error) {
|
||||||
|
// Get openID config without auth
|
||||||
|
opts := rest.Opts{
|
||||||
|
Method: "GET",
|
||||||
|
RootURL: "https://accounts.google.com/.well-known/openid-configuration",
|
||||||
|
}
|
||||||
|
var openIDconfig map[string]interface{}
|
||||||
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
|
resp, err := f.unAuth.CallJSON(&opts, nil, &openIDconfig)
|
||||||
|
return shouldRetry(resp, err)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "couldn't read openID config")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find userinfo endpoint
|
||||||
|
endpoint, ok := openIDconfig[name].(string)
|
||||||
|
if !ok {
|
||||||
|
return "", errors.Errorf("couldn't find %q from openID config", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return endpoint, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserInfo fetches info about the current user with oauth2
|
||||||
|
func (f *Fs) UserInfo(ctx context.Context) (userInfo map[string]string, err error) {
|
||||||
|
endpoint, err := f.fetchEndpoint("userinfo_endpoint")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the user info with auth
|
||||||
|
opts := rest.Opts{
|
||||||
|
Method: "GET",
|
||||||
|
RootURL: endpoint,
|
||||||
|
}
|
||||||
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
|
resp, err := f.srv.CallJSON(&opts, nil, &userInfo)
|
||||||
|
return shouldRetry(resp, err)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "couldn't read user info")
|
||||||
|
}
|
||||||
|
return userInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disconnect kills the token and refresh token
|
||||||
|
func (f *Fs) Disconnect(ctx context.Context) (err error) {
|
||||||
|
endpoint, err := f.fetchEndpoint("revocation_endpoint")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
token, err := f.ts.Token()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Revoke the token and the refresh token
|
||||||
|
opts := rest.Opts{
|
||||||
|
Method: "POST",
|
||||||
|
RootURL: endpoint,
|
||||||
|
MultipartParams: url.Values{
|
||||||
|
"token": []string{token.AccessToken},
|
||||||
|
"token_type_hint": []string{"access_token"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
var res interface{}
|
||||||
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
|
resp, err := f.srv.CallJSON(&opts, nil, &res)
|
||||||
|
return shouldRetry(resp, err)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "couldn't revoke token")
|
||||||
|
}
|
||||||
|
fs.Infof(f, "res = %+v", res)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Return an Object from a path
|
// Return an Object from a path
|
||||||
//
|
//
|
||||||
// If it can't be found it returns the error fs.ErrorObjectNotFound.
|
// If it can't be found it returns the error fs.ErrorObjectNotFound.
|
||||||
|
@ -963,8 +1051,10 @@ func (o *Object) ID() string {
|
||||||
|
|
||||||
// Check the interfaces are satisfied
|
// Check the interfaces are satisfied
|
||||||
var (
|
var (
|
||||||
_ fs.Fs = &Fs{}
|
_ fs.Fs = &Fs{}
|
||||||
_ fs.Object = &Object{}
|
_ fs.UserInfoer = &Fs{}
|
||||||
_ fs.MimeTyper = &Object{}
|
_ fs.Disconnecter = &Fs{}
|
||||||
_ fs.IDer = &Object{}
|
_ fs.Object = &Object{}
|
||||||
|
_ fs.MimeTyper = &Object{}
|
||||||
|
_ fs.IDer = &Object{}
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue