mirror of
https://github.com/rclone/rclone.git
synced 2024-11-24 17:34:57 +01:00
zoho: add support for private spaces
This commit is contained in:
parent
eceb390152
commit
48543d38e8
@ -27,8 +27,8 @@ func (t *Time) UnmarshalJSON(data []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// User is a Zoho user we are only interested in the ZUID here
|
// OAuthUser is a Zoho user we are only interested in the ZUID here
|
||||||
type User struct {
|
type OAuthUser struct {
|
||||||
FirstName string `json:"First_Name"`
|
FirstName string `json:"First_Name"`
|
||||||
Email string `json:"Email"`
|
Email string `json:"Email"`
|
||||||
LastName string `json:"Last_Name"`
|
LastName string `json:"Last_Name"`
|
||||||
@ -36,12 +36,41 @@ type User struct {
|
|||||||
ZUID int64 `json:"ZUID"`
|
ZUID int64 `json:"ZUID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TeamWorkspace represents a Zoho Team or workspace
|
// UserInfoResponse is returned by the user info API.
|
||||||
|
type UserInfoResponse struct {
|
||||||
|
Data struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"users"`
|
||||||
|
Attributes struct {
|
||||||
|
EmailID string `json:"email_id"`
|
||||||
|
Edition string `json:"edition"`
|
||||||
|
} `json:"attributes"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrivateSpaceInfo gives basic information about a users private folder.
|
||||||
|
type PrivateSpaceInfo struct {
|
||||||
|
Data struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"string"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CurrentTeamInfo gives information about the current user in a team.
|
||||||
|
type CurrentTeamInfo struct {
|
||||||
|
Data struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"string"`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TeamWorkspace represents a Zoho Team, Workspace or Private Space
|
||||||
// It's actually a VERY large json object that differs between
|
// It's actually a VERY large json object that differs between
|
||||||
// Team and Workspace but we are only interested in some fields
|
// Team and Workspace and Private Space but we are only interested in some fields
|
||||||
// that both of them have so we can use the same struct for both
|
// that all of them have so we can use the same struct.
|
||||||
type TeamWorkspace struct {
|
type TeamWorkspace struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
Attributes struct {
|
Attributes struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Created Time `json:"created_time_in_millisecond"`
|
Created Time `json:"created_time_in_millisecond"`
|
||||||
@ -49,7 +78,8 @@ type TeamWorkspace struct {
|
|||||||
} `json:"attributes"`
|
} `json:"attributes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TeamWorkspaceResponse is the response by the list teams api
|
// TeamWorkspaceResponse is the response by the list teams API, list workspace API
|
||||||
|
// or list team private spaces API.
|
||||||
type TeamWorkspaceResponse struct {
|
type TeamWorkspaceResponse struct {
|
||||||
TeamWorkspace []TeamWorkspace `json:"data"`
|
TeamWorkspace []TeamWorkspace `json:"data"`
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,8 @@ const (
|
|||||||
maxSleep = 60 * time.Second
|
maxSleep = 60 * time.Second
|
||||||
decayConstant = 2 // bigger for slower decay, exponential
|
decayConstant = 2 // bigger for slower decay, exponential
|
||||||
configRootID = "root_folder_id"
|
configRootID = "root_folder_id"
|
||||||
|
|
||||||
|
largeFileTheshold = 10 * 1024 * 1024 // 10 MiB
|
||||||
)
|
)
|
||||||
|
|
||||||
// Globals
|
// Globals
|
||||||
@ -92,12 +94,12 @@ func init() {
|
|||||||
|
|
||||||
switch config.State {
|
switch config.State {
|
||||||
case "":
|
case "":
|
||||||
return oauthutil.ConfigOut("teams", &oauthutil.Options{
|
return oauthutil.ConfigOut("type", &oauthutil.Options{
|
||||||
OAuth2Config: oauthConfig,
|
OAuth2Config: oauthConfig,
|
||||||
// No refresh token unless ApprovalForce is set
|
// No refresh token unless ApprovalForce is set
|
||||||
OAuth2Opts: []oauth2.AuthCodeOption{oauth2.ApprovalForce},
|
OAuth2Opts: []oauth2.AuthCodeOption{oauth2.ApprovalForce},
|
||||||
})
|
})
|
||||||
case "teams":
|
case "type":
|
||||||
// We need to rewrite the token type to "Zoho-oauthtoken" because Zoho wants
|
// We need to rewrite the token type to "Zoho-oauthtoken" because Zoho wants
|
||||||
// it's own custom type
|
// it's own custom type
|
||||||
token, err := oauthutil.GetToken(name, m)
|
token, err := oauthutil.GetToken(name, m)
|
||||||
@ -112,24 +114,43 @@ func init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
authSrv, apiSrv, err := getSrvs()
|
_, apiSrv, err := getSrvs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the user Info
|
userInfo, err := getUserInfo(ctx, apiSrv)
|
||||||
opts := rest.Opts{
|
if err != nil {
|
||||||
Method: "GET",
|
return nil, err
|
||||||
Path: "/oauth/user/info",
|
|
||||||
}
|
}
|
||||||
var user api.User
|
// If personal Edition only one private Space is available. Directly configure that.
|
||||||
_, err = authSrv.CallJSON(ctx, &opts, nil, &user)
|
if userInfo.Data.Attributes.Edition == "PERSONAL" {
|
||||||
|
return fs.ConfigResult("private_space", userInfo.Data.ID)
|
||||||
|
}
|
||||||
|
// Otherwise go to team selection
|
||||||
|
return fs.ConfigResult("team", userInfo.Data.ID)
|
||||||
|
case "private_space":
|
||||||
|
_, apiSrv, err := getSrvs()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
workspaces, err := getPrivateSpaces(ctx, config.Result, apiSrv)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return fs.ConfigChoose("workspace_end", "config_workspace", "Workspace ID", len(workspaces), func(i int) (string, string) {
|
||||||
|
workspace := workspaces[i]
|
||||||
|
return workspace.ID, workspace.Name
|
||||||
|
})
|
||||||
|
case "team":
|
||||||
|
_, apiSrv, err := getSrvs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the teams
|
// Get the teams
|
||||||
teams, err := listTeams(ctx, user.ZUID, apiSrv)
|
teams, err := listTeams(ctx, config.Result, apiSrv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -147,9 +168,19 @@ func init() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
currentTeamInfo, err := getCurrentTeamInfo(ctx, teamID, apiSrv)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
privateSpaces, err := getPrivateSpaces(ctx, currentTeamInfo.Data.ID, apiSrv)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
workspaces = append(workspaces, privateSpaces...)
|
||||||
|
|
||||||
return fs.ConfigChoose("workspace_end", "config_workspace", "Workspace ID", len(workspaces), func(i int) (string, string) {
|
return fs.ConfigChoose("workspace_end", "config_workspace", "Workspace ID", len(workspaces), func(i int) (string, string) {
|
||||||
workspace := workspaces[i]
|
workspace := workspaces[i]
|
||||||
return workspace.ID, workspace.Attributes.Name
|
return workspace.ID, workspace.Name
|
||||||
})
|
})
|
||||||
case "workspace_end":
|
case "workspace_end":
|
||||||
workspaceID := config.Result
|
workspaceID := config.Result
|
||||||
@ -245,11 +276,63 @@ func setupRegion(m configmap.Mapper) error {
|
|||||||
|
|
||||||
// ------------------------------------------------------------
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
func listTeams(ctx context.Context, uid int64, srv *rest.Client) ([]api.TeamWorkspace, error) {
|
type workspaceInfo struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUserInfo(ctx context.Context, srv *rest.Client) (*api.UserInfoResponse, error) {
|
||||||
|
var userInfo api.UserInfoResponse
|
||||||
|
opts := rest.Opts{
|
||||||
|
Method: "GET",
|
||||||
|
Path: "/users/me",
|
||||||
|
ExtraHeaders: map[string]string{"Accept": "application/vnd.api+json"},
|
||||||
|
}
|
||||||
|
_, err := srv.CallJSON(ctx, &opts, nil, &userInfo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &userInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCurrentTeamInfo(ctx context.Context, teamID string, srv *rest.Client) (*api.CurrentTeamInfo, error) {
|
||||||
|
var currentTeamInfo api.CurrentTeamInfo
|
||||||
|
opts := rest.Opts{
|
||||||
|
Method: "GET",
|
||||||
|
Path: "/teams/" + teamID + "/currentuser",
|
||||||
|
ExtraHeaders: map[string]string{"Accept": "application/vnd.api+json"},
|
||||||
|
}
|
||||||
|
_, err := srv.CallJSON(ctx, &opts, nil, ¤tTeamInfo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ¤tTeamInfo, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPrivateSpaces(ctx context.Context, teamUserID string, srv *rest.Client) ([]workspaceInfo, error) {
|
||||||
|
var privateSpaceListResponse api.TeamWorkspaceResponse
|
||||||
|
opts := rest.Opts{
|
||||||
|
Method: "GET",
|
||||||
|
Path: "/users/" + teamUserID + "/privatespace",
|
||||||
|
ExtraHeaders: map[string]string{"Accept": "application/vnd.api+json"},
|
||||||
|
}
|
||||||
|
_, err := srv.CallJSON(ctx, &opts, nil, &privateSpaceListResponse)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
workspaceList := make([]workspaceInfo, 0, len(privateSpaceListResponse.TeamWorkspace))
|
||||||
|
for _, workspace := range privateSpaceListResponse.TeamWorkspace {
|
||||||
|
workspaceList = append(workspaceList, workspaceInfo{ID: workspace.ID, Name: "My Space"})
|
||||||
|
}
|
||||||
|
return workspaceList, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func listTeams(ctx context.Context, zuid string, srv *rest.Client) ([]api.TeamWorkspace, error) {
|
||||||
var teamList api.TeamWorkspaceResponse
|
var teamList api.TeamWorkspaceResponse
|
||||||
opts := rest.Opts{
|
opts := rest.Opts{
|
||||||
Method: "GET",
|
Method: "GET",
|
||||||
Path: "/users/" + strconv.FormatInt(uid, 10) + "/teams",
|
Path: "/users/" + zuid + "/teams",
|
||||||
ExtraHeaders: map[string]string{"Accept": "application/vnd.api+json"},
|
ExtraHeaders: map[string]string{"Accept": "application/vnd.api+json"},
|
||||||
}
|
}
|
||||||
_, err := srv.CallJSON(ctx, &opts, nil, &teamList)
|
_, err := srv.CallJSON(ctx, &opts, nil, &teamList)
|
||||||
@ -259,18 +342,24 @@ func listTeams(ctx context.Context, uid int64, srv *rest.Client) ([]api.TeamWork
|
|||||||
return teamList.TeamWorkspace, nil
|
return teamList.TeamWorkspace, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func listWorkspaces(ctx context.Context, teamID string, srv *rest.Client) ([]api.TeamWorkspace, error) {
|
func listWorkspaces(ctx context.Context, teamID string, srv *rest.Client) ([]workspaceInfo, error) {
|
||||||
var workspaceList api.TeamWorkspaceResponse
|
var workspaceListResponse api.TeamWorkspaceResponse
|
||||||
opts := rest.Opts{
|
opts := rest.Opts{
|
||||||
Method: "GET",
|
Method: "GET",
|
||||||
Path: "/teams/" + teamID + "/workspaces",
|
Path: "/teams/" + teamID + "/workspaces",
|
||||||
ExtraHeaders: map[string]string{"Accept": "application/vnd.api+json"},
|
ExtraHeaders: map[string]string{"Accept": "application/vnd.api+json"},
|
||||||
}
|
}
|
||||||
_, err := srv.CallJSON(ctx, &opts, nil, &workspaceList)
|
_, err := srv.CallJSON(ctx, &opts, nil, &workspaceListResponse)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return workspaceList.TeamWorkspace, nil
|
|
||||||
|
workspaceList := make([]workspaceInfo, 0, len(workspaceListResponse.TeamWorkspace))
|
||||||
|
for _, workspace := range workspaceListResponse.TeamWorkspace {
|
||||||
|
workspaceList = append(workspaceList, workspaceInfo{ID: workspace.ID, Name: workspace.Attributes.Name})
|
||||||
|
}
|
||||||
|
|
||||||
|
return workspaceList, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------------
|
// --------------------------------------------------------------
|
||||||
@ -789,7 +878,7 @@ func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options .
|
|||||||
}
|
}
|
||||||
|
|
||||||
// use normal upload API for small sizes (<10MiB)
|
// use normal upload API for small sizes (<10MiB)
|
||||||
if size < 10*1024*1024 {
|
if size < largeFileTheshold {
|
||||||
info, err := f.upload(ctx, f.opt.Enc.FromStandardName(leaf), directoryID, size, in, options...)
|
info, err := f.upload(ctx, f.opt.Enc.FromStandardName(leaf), directoryID, size, in, options...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -1272,7 +1361,7 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op
|
|||||||
}
|
}
|
||||||
|
|
||||||
// use normal upload API for small sizes (<10MiB)
|
// use normal upload API for small sizes (<10MiB)
|
||||||
if size < 10*1024*1024 {
|
if size < largeFileTheshold {
|
||||||
info, err := o.fs.upload(ctx, o.fs.opt.Enc.FromStandardName(leaf), directoryID, size, in, options...)
|
info, err := o.fs.upload(ctx, o.fs.opt.Enc.FromStandardName(leaf), directoryID, size, in, options...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
Loading…
Reference in New Issue
Block a user