diff --git a/groups.go b/groups.go index 4b7713912..dda5c306e 100644 --- a/groups.go +++ b/groups.go @@ -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 @@ -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 @@ -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"` @@ -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 } @@ -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"` @@ -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 } diff --git a/projects.go b/projects.go index 1aadf902b..1bef75f98 100644 --- a/projects.go +++ b/projects.go @@ -17,6 +17,7 @@ package gitlab import ( + "encoding/json" "fmt" "io" "net/http" @@ -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 @@ -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"` @@ -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( diff --git a/users.go b/users.go index bbe535a10..6fc6b2303 100644 --- a/users.go +++ b/users.go @@ -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. @@ -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 @@ -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 } @@ -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 @@ -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 }