Skip to content

Commit

Permalink
Fully support project, group and user avatars
Browse files Browse the repository at this point in the history
This change set adds support for project, group and user avatars in the
same fashion this is already implemented for topics.

Currently it's not possible to remove avatars from those API - I'm
exploring possible solutions
[here](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/92604).

Nonetheless, I think this PR can already be merged.
  • Loading branch information
timofurrer committed Jul 16, 2022
1 parent cf40536 commit 7916da7
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 53 deletions.
47 changes: 45 additions & 2 deletions groups.go
Expand Up @@ -18,10 +18,13 @@ package gitlab

import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"time"

retryablehttp "github.com/hashicorp/go-retryablehttp"
)

// GroupsService handles communication with the group related methods of
Expand Down Expand Up @@ -90,6 +93,15 @@ type GroupAvatar struct {
Image io.Reader
}

// MarshalJSON implements the json.Marshaler interface.
func (a *GroupAvatar) MarshalJSON() ([]byte, error) {
if a.Filename == "" && a.Image == nil {
return []byte(`""`), nil
}
type alias GroupAvatar
return json.Marshal((*alias)(a))
}

// LDAPGroupLink represents a GitLab LDAP group link.
//
// GitLab API docs: https://docs.gitlab.com/ce/api/groups.html#ldap-group-links
Expand Down Expand Up @@ -312,6 +324,7 @@ func (s *GroupsService) DownloadAvatar(gid interface{}, options ...RequestOption
type CreateGroupOptions struct {
Name *string `url:"name,omitempty" json:"name,omitempty"`
Path *string `url:"path,omitempty" json:"path,omitempty"`
Avatar *GroupAvatar `url:"-" json:"-"`
Description *string `url:"description,omitempty" json:"description,omitempty"`
MembershipLock *bool `url:"membership_lock,omitempty" json:"membership_lock,omitempty"`
Visibility *VisibilityValue `url:"visibility,omitempty" json:"visibility,omitempty"`
Expand All @@ -336,7 +349,22 @@ type CreateGroupOptions struct {
//
// GitLab API docs: https://docs.gitlab.com/ce/api/groups.html#new-group
func (s *GroupsService) CreateGroup(opt *CreateGroupOptions, options ...RequestOptionFunc) (*Group, *Response, error) {
req, err := s.client.NewRequest(http.MethodPost, "groups", opt, options)
var err error
var req *retryablehttp.Request

if opt.Avatar == nil {
req, err = s.client.NewRequest(http.MethodPost, "groups", opt, options)
} else {
req, err = s.client.UploadRequest(
http.MethodPost,
"groups",
opt.Avatar.Image,
opt.Avatar.Filename,
UploadAvatar,
opt,
options,
)
}
if err != nil {
return nil, nil, err
}
Expand Down Expand Up @@ -420,6 +448,7 @@ func (s *GroupsService) TransferSubGroup(gid interface{}, opt *TransferSubGroupO
type UpdateGroupOptions struct {
Name *string `url:"name,omitempty" json:"name,omitempty"`
Path *string `url:"path,omitempty" json:"path,omitempty"`
Avatar *GroupAvatar `url:"-" json:"avatar,omitempty"`
Description *string `url:"description,omitempty" json:"description,omitempty"`
MembershipLock *bool `url:"membership_lock,omitempty" json:"membership_lock,omitempty"`
Visibility *VisibilityValue `url:"visibility,omitempty" json:"visibility,omitempty"`
Expand Down Expand Up @@ -453,7 +482,21 @@ func (s *GroupsService) UpdateGroup(gid interface{}, opt *UpdateGroupOptions, op
}
u := fmt.Sprintf("groups/%s", PathEscape(group))

req, err := s.client.NewRequest(http.MethodPut, u, opt, options)
var req *retryablehttp.Request

if opt.Avatar == nil || (opt.Avatar.Filename == "" && opt.Avatar.Image == nil) {
req, err = s.client.NewRequest(http.MethodPut, u, opt, options)
} else {
req, err = s.client.UploadRequest(
http.MethodPut,
u,
opt.Avatar.Image,
opt.Avatar.Filename,
UploadAvatar,
opt,
options,
)
}
if err != nil {
return nil, nil, err
}
Expand Down
14 changes: 12 additions & 2 deletions projects.go
Expand Up @@ -17,6 +17,7 @@
package gitlab

import (
"encoding/json"
"fmt"
"io"
"net/http"
Expand Down Expand Up @@ -704,6 +705,15 @@ type ProjectAvatar struct {
Image io.Reader
}

// MarshalJSON implements the json.Marshaler interface.
func (a *ProjectAvatar) MarshalJSON() ([]byte, error) {
if a.Filename == "" && a.Image == nil {
return []byte(`""`), nil
}
type alias ProjectAvatar
return json.Marshal((*alias)(a))
}

// CreateProject creates a new project owned by the authenticated user.
//
// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#create-project
Expand Down Expand Up @@ -805,7 +815,7 @@ type EditProjectOptions struct {
AutoDevopsDeployStrategy *string `url:"auto_devops_deploy_strategy,omitempty" json:"auto_devops_deploy_strategy,omitempty"`
AutoDevopsEnabled *bool `url:"auto_devops_enabled,omitempty" json:"auto_devops_enabled,omitempty"`
AutocloseReferencedIssues *bool `url:"autoclose_referenced_issues,omitempty" json:"autoclose_referenced_issues,omitempty"`
Avatar *ProjectAvatar `url:"-" json:"-"`
Avatar *ProjectAvatar `url:"-" json:"avatar,omitempty"`
BuildCoverageRegex *string `url:"build_coverage_regex,omitempty" json:"build_coverage_regex,omitempty"`
BuildGitStrategy *string `url:"build_git_strategy,omitempty" json:"build_git_strategy,omitempty"`
BuildTimeout *int `url:"build_timeout,omitempty" json:"build_timeout,omitempty"`
Expand Down Expand Up @@ -893,7 +903,7 @@ func (s *ProjectsService) EditProject(pid interface{}, opt *EditProjectOptions,

var req *retryablehttp.Request

if opt.Avatar == nil {
if opt.Avatar == nil || (opt.Avatar.Filename == "" && opt.Avatar.Image == nil) {
req, err = s.client.NewRequest(http.MethodPut, u, opt, options)
} else {
req, err = s.client.UploadRequest(
Expand Down
151 changes: 102 additions & 49 deletions users.go
Expand Up @@ -17,11 +17,15 @@
package gitlab

import (
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"time"

retryablehttp "github.com/hashicorp/go-retryablehttp"
)

// List a couple of standard errors.
Expand Down Expand Up @@ -109,6 +113,23 @@ type UserIdentity struct {
ExternUID string `json:"extern_uid"`
}

// UserAvatar represents a GitLab user avatar.
//
// GitLab API docs: https://docs.gitlab.com/ce/api/users.html
type UserAvatar struct {
Filename string
Image io.Reader
}

// MarshalJSON implements the json.Marshaler interface.
func (a *UserAvatar) MarshalJSON() ([]byte, error) {
if a.Filename == "" && a.Image == nil {
return []byte(`""`), nil
}
type alias UserAvatar
return json.Marshal((*alias)(a))
}

// ListUsersOptions represents the available ListUsers() options.
//
// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#list-users
Expand Down Expand Up @@ -184,37 +205,53 @@ func (s *UsersService) GetUser(user int, opt GetUsersOptions, options ...Request
//
// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#user-creation
type CreateUserOptions struct {
Email *string `url:"email,omitempty" json:"email,omitempty"`
Password *string `url:"password,omitempty" json:"password,omitempty"`
ResetPassword *bool `url:"reset_password,omitempty" json:"reset_password,omitempty"`
ForceRandomPassword *bool `url:"force_random_password,omitempty" json:"force_random_password,omitempty"`
Username *string `url:"username,omitempty" json:"username,omitempty"`
Name *string `url:"name,omitempty" json:"name,omitempty"`
Skype *string `url:"skype,omitempty" json:"skype,omitempty"`
Linkedin *string `url:"linkedin,omitempty" json:"linkedin,omitempty"`
Twitter *string `url:"twitter,omitempty" json:"twitter,omitempty"`
WebsiteURL *string `url:"website_url,omitempty" json:"website_url,omitempty"`
Organization *string `url:"organization,omitempty" json:"organization,omitempty"`
JobTitle *string `url:"job_title,omitempty" json:"job_title,omitempty"`
ProjectsLimit *int `url:"projects_limit,omitempty" json:"projects_limit,omitempty"`
ExternUID *string `url:"extern_uid,omitempty" json:"extern_uid,omitempty"`
Provider *string `url:"provider,omitempty" json:"provider,omitempty"`
Bio *string `url:"bio,omitempty" json:"bio,omitempty"`
Location *string `url:"location,omitempty" json:"location,omitempty"`
Admin *bool `url:"admin,omitempty" json:"admin,omitempty"`
CanCreateGroup *bool `url:"can_create_group,omitempty" json:"can_create_group,omitempty"`
SkipConfirmation *bool `url:"skip_confirmation,omitempty" json:"skip_confirmation,omitempty"`
External *bool `url:"external,omitempty" json:"external,omitempty"`
PrivateProfile *bool `url:"private_profile,omitempty" json:"private_profile,omitempty"`
Note *string `url:"note,omitempty" json:"note,omitempty"`
ThemeID *int `url:"theme_id,omitempty" json:"theme_id,omitempty"`
Avatar *UserAvatar `url:"-" json:"-"`
Email *string `url:"email,omitempty" json:"email,omitempty"`
Password *string `url:"password,omitempty" json:"password,omitempty"`
ResetPassword *bool `url:"reset_password,omitempty" json:"reset_password,omitempty"`
ForceRandomPassword *bool `url:"force_random_password,omitempty" json:"force_random_password,omitempty"`
Username *string `url:"username,omitempty" json:"username,omitempty"`
Name *string `url:"name,omitempty" json:"name,omitempty"`
Skype *string `url:"skype,omitempty" json:"skype,omitempty"`
Linkedin *string `url:"linkedin,omitempty" json:"linkedin,omitempty"`
Twitter *string `url:"twitter,omitempty" json:"twitter,omitempty"`
WebsiteURL *string `url:"website_url,omitempty" json:"website_url,omitempty"`
Organization *string `url:"organization,omitempty" json:"organization,omitempty"`
JobTitle *string `url:"job_title,omitempty" json:"job_title,omitempty"`
ProjectsLimit *int `url:"projects_limit,omitempty" json:"projects_limit,omitempty"`
ExternUID *string `url:"extern_uid,omitempty" json:"extern_uid,omitempty"`
Provider *string `url:"provider,omitempty" json:"provider,omitempty"`
Bio *string `url:"bio,omitempty" json:"bio,omitempty"`
Location *string `url:"location,omitempty" json:"location,omitempty"`
Admin *bool `url:"admin,omitempty" json:"admin,omitempty"`
CanCreateGroup *bool `url:"can_create_group,omitempty" json:"can_create_group,omitempty"`
SkipConfirmation *bool `url:"skip_confirmation,omitempty" json:"skip_confirmation,omitempty"`
External *bool `url:"external,omitempty" json:"external,omitempty"`
PrivateProfile *bool `url:"private_profile,omitempty" json:"private_profile,omitempty"`
Note *string `url:"note,omitempty" json:"note,omitempty"`
ThemeID *int `url:"theme_id,omitempty" json:"theme_id,omitempty"`
}

// CreateUser creates a new user. Note only administrators can create new users.
//
// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#user-creation
func (s *UsersService) CreateUser(opt *CreateUserOptions, options ...RequestOptionFunc) (*User, *Response, error) {
req, err := s.client.NewRequest(http.MethodPost, "users", opt, options)
var err error
var req *retryablehttp.Request

if opt.Avatar == nil {
req, err = s.client.NewRequest(http.MethodPost, "users", opt, options)
} else {
req, err = s.client.UploadRequest(
http.MethodPost,
"users",
opt.Avatar.Image,
opt.Avatar.Filename,
UploadAvatar,
opt,
options,
)
}
if err != nil {
return nil, nil, err
}
Expand All @@ -232,29 +269,30 @@ func (s *UsersService) CreateUser(opt *CreateUserOptions, options ...RequestOpti
//
// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#user-modification
type ModifyUserOptions struct {
Email *string `url:"email,omitempty" json:"email,omitempty"`
Password *string `url:"password,omitempty" json:"password,omitempty"`
Username *string `url:"username,omitempty" json:"username,omitempty"`
Name *string `url:"name,omitempty" json:"name,omitempty"`
Skype *string `url:"skype,omitempty" json:"skype,omitempty"`
Linkedin *string `url:"linkedin,omitempty" json:"linkedin,omitempty"`
Twitter *string `url:"twitter,omitempty" json:"twitter,omitempty"`
WebsiteURL *string `url:"website_url,omitempty" json:"website_url,omitempty"`
Organization *string `url:"organization,omitempty" json:"organization,omitempty"`
JobTitle *string `url:"job_title,omitempty" json:"job_title,omitempty"`
ProjectsLimit *int `url:"projects_limit,omitempty" json:"projects_limit,omitempty"`
ExternUID *string `url:"extern_uid,omitempty" json:"extern_uid,omitempty"`
Provider *string `url:"provider,omitempty" json:"provider,omitempty"`
Bio *string `url:"bio,omitempty" json:"bio,omitempty"`
Location *string `url:"location,omitempty" json:"location,omitempty"`
Admin *bool `url:"admin,omitempty" json:"admin,omitempty"`
CanCreateGroup *bool `url:"can_create_group,omitempty" json:"can_create_group,omitempty"`
SkipReconfirmation *bool `url:"skip_reconfirmation,omitempty" json:"skip_reconfirmation,omitempty"`
External *bool `url:"external,omitempty" json:"external,omitempty"`
PrivateProfile *bool `url:"private_profile,omitempty" json:"private_profile,omitempty"`
Note *string `url:"note,omitempty" json:"note,omitempty"`
ThemeID *int `url:"theme_id,omitempty" json:"theme_id,omitempty"`
PublicEmail *string `url:"public_email,omitempty" json:"public_email,omitempty"`
Avatar *UserAvatar `url:"-" json:"avatar,omitempty"`
Email *string `url:"email,omitempty" json:"email,omitempty"`
Password *string `url:"password,omitempty" json:"password,omitempty"`
Username *string `url:"username,omitempty" json:"username,omitempty"`
Name *string `url:"name,omitempty" json:"name,omitempty"`
Skype *string `url:"skype,omitempty" json:"skype,omitempty"`
Linkedin *string `url:"linkedin,omitempty" json:"linkedin,omitempty"`
Twitter *string `url:"twitter,omitempty" json:"twitter,omitempty"`
WebsiteURL *string `url:"website_url,omitempty" json:"website_url,omitempty"`
Organization *string `url:"organization,omitempty" json:"organization,omitempty"`
JobTitle *string `url:"job_title,omitempty" json:"job_title,omitempty"`
ProjectsLimit *int `url:"projects_limit,omitempty" json:"projects_limit,omitempty"`
ExternUID *string `url:"extern_uid,omitempty" json:"extern_uid,omitempty"`
Provider *string `url:"provider,omitempty" json:"provider,omitempty"`
Bio *string `url:"bio,omitempty" json:"bio,omitempty"`
Location *string `url:"location,omitempty" json:"location,omitempty"`
Admin *bool `url:"admin,omitempty" json:"admin,omitempty"`
CanCreateGroup *bool `url:"can_create_group,omitempty" json:"can_create_group,omitempty"`
SkipReconfirmation *bool `url:"skip_reconfirmation,omitempty" json:"skip_reconfirmation,omitempty"`
External *bool `url:"external,omitempty" json:"external,omitempty"`
PrivateProfile *bool `url:"private_profile,omitempty" json:"private_profile,omitempty"`
Note *string `url:"note,omitempty" json:"note,omitempty"`
ThemeID *int `url:"theme_id,omitempty" json:"theme_id,omitempty"`
PublicEmail *string `url:"public_email,omitempty" json:"public_email,omitempty"`
}

// ModifyUser modifies an existing user. Only administrators can change attributes
Expand All @@ -264,7 +302,22 @@ type ModifyUserOptions struct {
func (s *UsersService) ModifyUser(user int, opt *ModifyUserOptions, options ...RequestOptionFunc) (*User, *Response, error) {
u := fmt.Sprintf("users/%d", user)

req, err := s.client.NewRequest(http.MethodPut, u, opt, options)
var err error
var req *retryablehttp.Request

if opt.Avatar == nil || (opt.Avatar.Filename == "" && opt.Avatar.Image == nil) {
req, err = s.client.NewRequest(http.MethodPut, u, opt, options)
} else {
req, err = s.client.UploadRequest(
http.MethodPut,
u,
opt.Avatar.Image,
opt.Avatar.Filename,
UploadAvatar,
opt,
options,
)
}
if err != nil {
return nil, nil, err
}
Expand Down

0 comments on commit 7916da7

Please sign in to comment.