Repository: xanzy/go-gitlab
Pull request #1180 (2021-07-22 13:24:06)

Add GenericPackagesServices (#1100)
This PR continues the work in #1101, rebasing it to master, updating its interface (returning download URL), and some real-world testing (fixed the URL endpoint and request type).

matejvelikonja: @svanharmelen  any chance for merging this?
dewey: Thanks for working on it @neomantra! Hope this gets merged soon to unblock the goreleaser fix.
neomantra: I just rebased this branch onto latest master and resolved the minor conflicts.
svanharmelen: I'll have a look at this one tomorrow!
neomantra: Thanks for the review and improvements.  I'm adding `select` and am testing this end-to-end  with `goreleaser` patches.

Patch:
diff --git a/README.md b/README.md
index 1df039a..568fffc 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 0000000..ba875fa
--- /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/gitlab.go b/gitlab.go
index 5f585cb..ddfee94 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 9b95aad..679cb9e 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
 


Test patch:
diff --git a/generic_packages_test.go b/generic_packages_test.go
new file mode 100644
index 0000000..e227f26
--- /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)
+	}
+}