diff --git a/README.md b/README.md index 1df039a19..568fffcab 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ to add new and/or missing endpoints. Currently the following services are suppor - [x] Events - [x] Feature Flags - [ ] Geo Nodes +- [x] Generic Packages - [x] GitLab CI Config Templates - [x] Gitignores Templates - [x] Group Access Requests @@ -57,6 +58,7 @@ to add new and/or missing endpoints. Currently the following services are suppor - [x] Notes (comments) - [x] Notification Settings - [x] Open Source License Templates +- [x] Packages - [x] Pages - [x] Pages Domains - [x] Personal Access Tokens diff --git a/generic_packages.go b/generic_packages.go new file mode 100644 index 000000000..ba875fa35 --- /dev/null +++ b/generic_packages.go @@ -0,0 +1,158 @@ +// +// Copyright 2021, Sune Keller +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package gitlab + +import ( + "bytes" + "fmt" + "io" + "net/http" + "time" +) + +// GenericPackagesService handles communication with the packages related +// methods of the GitLab API. +// +// GitLab docs: +// https://docs.gitlab.com/ee/user/packages/generic_packages/index.html +type GenericPackagesService struct { + client *Client +} + +// GenericPackagesFile represents a GitLab generic package file. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/user/packages/generic_packages/index.html#publish-a-package-file +type GenericPackagesFile struct { + ID int `json:"id"` + PackageID int `json:"package_id"` + CreatedAt *time.Time `json:"created_at"` + UpdatedAt *time.Time `json:"updated_at"` + Size int `json:"size"` + FileStore int `json:"file_store"` + FileMD5 string `json:"file_md5"` + FileSHA1 string `json:"file_sha1"` + FileName string `json:"file_name"` + File struct { + URL string `json:"url"` + } `json:"file"` + FileSHA256 string `json:"file_sha256"` + VerificationRetryAt *time.Time `json:"verification_retry_at"` + VerifiedAt *time.Time `json:"verified_at"` + VerificationFailure bool `json:"verification_failure"` + VerificationRetryCount int `json:"verification_retry_count"` + VerificationChecksum string `json:"verification_checksum"` + VerificationState int `json:"verification_state"` + VerificationStartedAt *time.Time `json:"verification_started_at"` + NewFilePath string `json:"new_file_path"` +} + +// FormatPackageURL returns the GitLab Package Registry URL for the given artifact metadata, without the BaseURL. +// This does not make a GitLab API request, but rather computes it based on their documentation. +func (s *GenericPackagesService) FormatPackageURL(pid interface{}, packageName, packageVersion, fileName string) (string, error) { + project, err := parseID(pid) + if err != nil { + return "", err + } + u := fmt.Sprintf( + "projects/%s/packages/generic/%s/%s/%s", + pathEscape(project), + pathEscape(packageName), + pathEscape(packageVersion), + pathEscape(fileName), + ) + return u, nil +} + +// PublishPackageFileOptions represents the available PublishPackageFile() +// options. +// +// GitLab docs: +// https://docs.gitlab.com/ee/user/packages/generic_packages/index.html#publish-a-package-file +type PublishPackageFileOptions struct { + Status *GenericPackageStatusValue `url:"status,omitempty" json:"status,omitempty"` + Select *GenericPackageSelectValue `url:"select,omitempty" json:"select,omitempty"` +} + +// PublishPackageFile uploads a file to a project's package registry. +// +// GitLab docs: +// https://docs.gitlab.com/ee/user/packages/generic_packages/index.html#publish-a-package-file +func (s *GenericPackagesService) PublishPackageFile(pid interface{}, packageName, packageVersion, fileName string, content io.Reader, opt *PublishPackageFileOptions, options ...RequestOptionFunc) (*GenericPackagesFile, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf( + "projects/%s/packages/generic/%s/%s/%s", + pathEscape(project), + pathEscape(packageName), + pathEscape(packageVersion), + pathEscape(fileName), + ) + + // We need to create the request as a GET request to make sure the options + // are set correctly. After the request is created we will overwrite both + // the method and the body. + req, err := s.client.NewRequest(http.MethodGet, u, opt, options) + if err != nil { + return nil, nil, err + } + + // Overwrite the method and body. + req.Method = http.MethodPut + req.SetBody(content) + + f := new(GenericPackagesFile) + resp, err := s.client.Do(req, f) + if err != nil { + return nil, resp, err + } + + return f, resp, err +} + +// DownloadPackageFile allows you to download the package file. +// +// GitLab docs: +// https://docs.gitlab.com/ee/user/packages/generic_packages/index.html#download-package-file +func (s *GenericPackagesService) DownloadPackageFile(pid interface{}, packageName, packageVersion, fileName string, options ...RequestOptionFunc) ([]byte, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf( + "projects/%s/packages/generic/%s/%s/%s", + pathEscape(project), + pathEscape(packageName), + pathEscape(packageVersion), + pathEscape(fileName), + ) + + req, err := s.client.NewRequest(http.MethodGet, u, nil, options) + if err != nil { + return nil, nil, err + } + + var f bytes.Buffer + resp, err := s.client.Do(req, &f) + if err != nil { + return nil, resp, err + } + + return f.Bytes(), resp, err +} diff --git a/generic_packages_test.go b/generic_packages_test.go new file mode 100644 index 000000000..e227f268f --- /dev/null +++ b/generic_packages_test.go @@ -0,0 +1,66 @@ +// +// Copyright 2021, Sune Keller +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package gitlab + +import ( + "fmt" + "net/http" + "reflect" + "strings" + "testing" +) + +func TestPublishPackageFile(t *testing.T) { + mux, server, client := setup(t) + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1234/packages/generic/foo/0.1.2/bar-baz.txt", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodPut) + fmt.Fprint(w, ` + { + "message": "201 Created" + } + `) + }) + + _, _, err := client.GenericPackages.PublishPackageFile(1234, "foo", "0.1.2", "bar-baz.txt", strings.NewReader("bar = baz"), &PublishPackageFileOptions{}) + if err != nil { + t.Errorf("GenericPackages.PublishPackageFile returned error: %v", err) + } +} + +func TestDownloadPackageFile(t *testing.T) { + mux, server, client := setup(t) + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1234/packages/generic/foo/0.1.2/bar-baz.txt", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + fmt.Fprint(w, strings.TrimSpace(` + bar = baz + `)) + }) + + packageBytes, _, err := client.GenericPackages.DownloadPackageFile(1234, "foo", "0.1.2", "bar-baz.txt") + if err != nil { + t.Errorf("GenericPackages.DownloadPackageFile returned error: %v", err) + } + + want := []byte("bar = baz") + if !reflect.DeepEqual(want, packageBytes) { + t.Errorf("GenericPackages.DownloadPackageFile returned %+v, want %+v", packageBytes, want) + } +} diff --git a/gitlab.go b/gitlab.go index 5f585cb87..ddfee949f 100644 --- a/gitlab.go +++ b/gitlab.go @@ -122,6 +122,7 @@ type Client struct { ExternalStatusChecks *ExternalStatusChecksService Features *FeaturesService FreezePeriods *FreezePeriodsService + GenericPackages *GenericPackagesService GeoNodes *GeoNodesService GitIgnoreTemplates *GitIgnoreTemplatesService GroupBadges *GroupBadgesService @@ -312,6 +313,7 @@ func newClient(options ...ClientOptionFunc) (*Client, error) { c.ExternalStatusChecks = &ExternalStatusChecksService{client: c} c.Features = &FeaturesService{client: c} c.FreezePeriods = &FreezePeriodsService{client: c} + c.GenericPackages = &GenericPackagesService{client: c} c.GeoNodes = &GeoNodesService{client: c} c.GitIgnoreTemplates = &GitIgnoreTemplatesService{client: c} c.GroupBadges = &GroupBadgesService{client: c} diff --git a/types.go b/types.go index 9b95aadd3..679cb9ef1 100644 --- a/types.go +++ b/types.go @@ -199,6 +199,39 @@ func FileAction(v FileActionValue) *FileActionValue { return p } +// GenericPackageSelectValue represents a generic package select value. +type GenericPackageSelectValue string + +// The available generic package select values. +const ( + SelectPackageFile GenericPackageSelectValue = "package_file" +) + +// GenericPackageSelect is a helper routine that allocates a new +// GenericPackageSelectValue value to store v and returns a pointer to it. +func GenericPackageSelect(v GenericPackageSelectValue) *GenericPackageSelectValue { + p := new(GenericPackageSelectValue) + *p = v + return p +} + +// GenericPackageStatusValue represents a generic package status. +type GenericPackageStatusValue string + +// The available generic package statuses. +const ( + PackageDefault GenericPackageStatusValue = "default" + PackageHidden GenericPackageStatusValue = "hidden" +) + +// GenericPackageStatus is a helper routine that allocates a new +// GenericPackageStatusValue value to store v and returns a pointer to it. +func GenericPackageStatus(v GenericPackageStatusValue) *GenericPackageStatusValue { + p := new(GenericPackageStatusValue) + *p = v + return p +} + // ISOTime represents an ISO 8601 formatted date type ISOTime time.Time