zoho: add support for private spaces

This commit is contained in:
buengese 2024-09-02 01:10:31 +02:00 committed by Nick Craig-Wood
parent eceb390152
commit 48543d38e8
2 changed files with 144 additions and 25 deletions

View File

@ -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"`
} }

View File

@ -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, &currentTeamInfo)
if err != nil {
return nil, err
}
return &currentTeamInfo, 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