Repository: xanzy/go-gitlab
Pull request #1122 (2021-04-03 16:33:53)

add Group import export
Implements Group Import and Export API as defined in [GitLab docs](https://docs.gitlab.com/ee/api/group_import_export.html).

Methods are added to the [Group](https://github.com/previ/go-gitlab/blob/master/groups.go) type:

- [GroupExportRequest](https://github.com/previ/go-gitlab/blob/master/groups.go#L788)
- [GroupExportDownload](https://github.com/previ/go-gitlab/blob/master/groups.go#L812)
- [GroupImport](https://github.com/previ/go-gitlab/blob/master/groups.go#L846)


Patch:
diff --git a/gitlab.go b/gitlab.go
index 265aaf2..ca7bdf2 100644
--- a/gitlab.go
+++ b/gitlab.go
@@ -122,6 +122,7 @@ type Client struct {
 	GitIgnoreTemplates    *GitIgnoreTemplatesService
 	GroupBadges           *GroupBadgesService
 	GroupCluster          *GroupClustersService
+	GroupImportExport     *GroupImportExportService
 	GroupIssueBoards      *GroupIssueBoardsService
 	GroupLabels           *GroupLabelsService
 	GroupMembers          *GroupMembersService
@@ -293,6 +294,7 @@ func newClient(options ...ClientOptionFunc) (*Client, error) {
 	c.GitIgnoreTemplates = &GitIgnoreTemplatesService{client: c}
 	c.GroupBadges = &GroupBadgesService{client: c}
 	c.GroupCluster = &GroupClustersService{client: c}
+	c.GroupImportExport = &GroupImportExportService{client: c}
 	c.GroupIssueBoards = &GroupIssueBoardsService{client: c}
 	c.GroupLabels = &GroupLabelsService{client: c}
 	c.GroupMembers = &GroupMembersService{client: c}
diff --git a/go.mod b/go.mod
index eb13bce..e363c27 100644
--- a/go.mod
+++ b/go.mod
@@ -5,9 +5,9 @@ require (
 	github.com/hashicorp/go-cleanhttp v0.5.1
 	github.com/hashicorp/go-retryablehttp v0.6.8
 	github.com/stretchr/testify v1.4.0
-	golang.org/x/net v0.0.0-20181108082009-03003ca0c849 // indirect
+	golang.org/x/net v0.0.0-20201021035429-f5854403a974 // indirect
 	golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288
-	golang.org/x/sync v0.0.0-20181108010431-42b317875d0f // indirect
+	golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 // indirect
 	golang.org/x/time v0.0.0-20191024005414-555d28b269f0
 	google.golang.org/appengine v1.3.0 // indirect
 )
diff --git a/go.sum b/go.sum
index 74ea228..671236d 100644
--- a/go.sum
+++ b/go.sum
@@ -17,16 +17,24 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181108082009-03003ca0c849 h1:FSqE2GGG7wzsYUsWiQ8MZrvEd1EOyU3NCF0AW3Wtltg=
-golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288 h1:JIqe8uIcRBHXDQVvZtHwp80ai3Lw3IJAeJEs55Dc1W0=
 golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
-golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 google.golang.org/appengine v1.3.0 h1:FBSsiFRMz3LBeXIomRnVzrQwSDj4ibvcRexLG0LZGQk=
 google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
diff --git a/group_import_export.go b/group_import_export.go
new file mode 100644
index 0000000..c578805
--- /dev/null
+++ b/group_import_export.go
@@ -0,0 +1,180 @@
+//
+// Copyright 2021, Sander van Harmelen
+//
+// 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"
+	"mime/multipart"
+	"net/http"
+	"os"
+	"path/filepath"
+	"strconv"
+)
+
+// GroupImportExportService handles communication with the group import export
+// related methods of the GitLab API.
+//
+// GitLab API docs: https://docs.gitlab.com/ce/api/group_import_export.html
+type GroupImportExportService struct {
+	client *Client
+}
+
+// ScheduleExport starts a new group export.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ce/api/group_import_export.html#schedule-new-export
+func (s *GroupImportExportService) ScheduleExport(gid interface{}, options ...RequestOptionFunc) (*Response, error) {
+	group, err := parseID(gid)
+	if err != nil {
+		return nil, err
+	}
+	u := fmt.Sprintf("groups/%s/export", pathEscape(group))
+
+	req, err := s.client.NewRequest(http.MethodPost, u, nil, options)
+	if err != nil {
+		return nil, err
+	}
+
+	return s.client.Do(req, nil)
+}
+
+// ExportDownload downloads the finished export.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ce/api/group_import_export.html#export-download
+func (s *GroupImportExportService) ExportDownload(gid interface{}, options ...RequestOptionFunc) (*bytes.Reader, *Response, error) {
+	group, err := parseID(gid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("groups/%s/export/download", pathEscape(group))
+
+	req, err := s.client.NewRequest(http.MethodGet, u, nil, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	exportDownload := new(bytes.Buffer)
+	resp, err := s.client.Do(req, exportDownload)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return bytes.NewReader(exportDownload.Bytes()), resp, err
+}
+
+// GroupImportFileOptions represents the available ImportFile() options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ce/api/group_import_export.html#import-a-file
+type GroupImportFileOptions struct {
+	Name     *string `url:"name,omitempty" json:"name,omitempty"`
+	Path     *string `url:"path,omitempty" json:"path,omitempty"`
+	File     *string `url:"file,omitempty" json:"file,omitempty"`
+	ParentID *int    `url:"parent_id,omitempty" json:"parent_id,omitempty"`
+}
+
+// ImportFile imports a file.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ce/api/group_import_export.html#import-a-file
+func (s *GroupImportExportService) ImportFile(opt *GroupImportFileOptions, options ...RequestOptionFunc) (*Response, error) {
+	// First check if we got all required options.
+	if opt.Name == nil || *opt.Name == "" {
+		return nil, fmt.Errorf("Missing required option: Name")
+	}
+	if opt.Path == nil || *opt.Path == "" {
+		return nil, fmt.Errorf("Missing required option: Path")
+	}
+	if opt.File == nil || *opt.File == "" {
+		return nil, fmt.Errorf("Missing required option: File")
+	}
+
+	f, err := os.Open(*opt.File)
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+
+	b := &bytes.Buffer{}
+	w := multipart.NewWriter(b)
+
+	_, filename := filepath.Split(*opt.File)
+	fw, err := w.CreateFormFile("file", filename)
+	if err != nil {
+		return nil, err
+	}
+
+	_, err = io.Copy(fw, f)
+	if err != nil {
+		return nil, err
+	}
+
+	// Populate the additional fields.
+	fw, err = w.CreateFormField("name")
+	if err != nil {
+		return nil, err
+	}
+
+	_, err = fw.Write([]byte(*opt.Name))
+	if err != nil {
+		return nil, err
+	}
+
+	fw, err = w.CreateFormField("path")
+	if err != nil {
+		return nil, err
+	}
+
+	_, err = fw.Write([]byte(*opt.Path))
+	if err != nil {
+		return nil, err
+	}
+
+	if opt.ParentID != nil {
+		fw, err = w.CreateFormField("parent_id")
+		if err != nil {
+			return nil, err
+		}
+
+		_, err = fw.Write([]byte(strconv.Itoa(*opt.ParentID)))
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	if err = w.Close(); err != nil {
+		return nil, err
+	}
+
+	req, err := s.client.NewRequest(http.MethodPost, "groups/import", nil, options)
+	if err != nil {
+		return nil, err
+	}
+
+	// Set the buffer as the request body.
+	if err = req.SetBody(b); err != nil {
+		return nil, err
+	}
+
+	// Overwrite the default content type.
+	req.Header.Set("Content-Type", w.FormDataContentType())
+
+	return s.client.Do(req, nil)
+}
diff --git a/project_access_tokens.go b/project_access_tokens.go
index 9bca815..ea54587 100644
--- a/project_access_tokens.go
+++ b/project_access_tokens.go
@@ -118,7 +118,7 @@ func (s *ProjectAccessTokensService) CreateProjectAccessToken(pid interface{}, o
 	return pat, resp, err
 }
 
-// RemoveProjectAccessToken deletes a Project Access Token.
+// DeleteProjectAccessToken deletes a Project Access Token.
 //
 // GitLab API docs:
 // https://docs.gitlab.com/ee/api/resource_access_tokens.html#revoke-a-project-access-token


Test patch:
diff --git a/.github/workflows/lint_and_test.yml b/.github/workflows/lint_and_test.yml
index c6dd610..8bc2359 100644
--- a/.github/workflows/lint_and_test.yml
+++ b/.github/workflows/lint_and_test.yml
@@ -10,7 +10,7 @@ jobs:
     name: Lint and Test - ${{ matrix.go-version }}
     strategy:
       matrix:
-        go-version: [1.14.x, 1.15.x, 1.x]
+        go-version: [1.15.x, 1.16.x, 1.x]
         platform: [ubuntu-latest]
     runs-on: ${{ matrix.platform }}
     steps:
@@ -25,7 +25,7 @@ jobs:
       - name: Lint package
         uses: golangci/golangci-lint-action@v2
         with:
-          version: v1.35
+          version: v1.39.0
 
       - name: Test package
         run: go test -v ./...
diff --git a/group_import_export_test.go b/group_import_export_test.go
new file mode 100644
index 0000000..d1c5db0
--- /dev/null
+++ b/group_import_export_test.go
@@ -0,0 +1,92 @@
+package gitlab
+
+import (
+	"fmt"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"os"
+	"reflect"
+	"testing"
+)
+
+func TestGroupScheduleExport(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/groups/1/export",
+		func(w http.ResponseWriter, r *http.Request) {
+			testMethod(t, r, http.MethodPost)
+			fmt.Fprint(w, `{"message": "202 Accepted"}`)
+		})
+
+	_, err := client.GroupImportExport.ScheduleExport(1)
+	if err != nil {
+		t.Errorf("GroupImportExport.ScheduleExport returned error: %v", err)
+	}
+}
+
+func TestGroupExportDownload(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+	content := []byte("fake content")
+
+	mux.HandleFunc("/api/v4/groups/1/export/download",
+		func(w http.ResponseWriter, r *http.Request) {
+			testMethod(t, r, http.MethodGet)
+			w.Write(content)
+		})
+
+	export, _, err := client.GroupImportExport.ExportDownload(1)
+	if err != nil {
+		t.Errorf("GroupImportExport.ExportDownload returned error: %v", err)
+	}
+
+	data, err := ioutil.ReadAll(export)
+	if err != nil {
+		t.Errorf("Error reading export: %v", err)
+	}
+
+	want := []byte("fake content")
+	if !reflect.DeepEqual(want, data) {
+		t.Errorf("GroupImportExport.GroupExportDownload returned %+v, want %+v", data, want)
+	}
+}
+
+func TestGroupImport(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	content := []byte("temporary file's content")
+	tmpfile, err := ioutil.TempFile("", "example.*.tar.gz")
+	if err != nil {
+		tmpfile.Close()
+		log.Fatal(err)
+	}
+	if _, err := tmpfile.Write(content); err != nil {
+		tmpfile.Close()
+		log.Fatal(err)
+	}
+	if err := tmpfile.Close(); err != nil {
+		log.Fatal(err)
+	}
+	defer os.Remove(tmpfile.Name()) // clean up
+
+	mux.HandleFunc("/api/v4/groups/import",
+		func(w http.ResponseWriter, r *http.Request) {
+			testMethod(t, r, http.MethodPost)
+			fmt.Fprint(w, `{"message": "202 Accepted"}`)
+		})
+
+	opt := &GroupImportFileOptions{
+		Name:     String("test"),
+		Path:     String("path"),
+		File:     String(tmpfile.Name()),
+		ParentID: Int(1),
+	}
+
+	_, err = client.GroupImportExport.ImportFile(opt)
+	if err != nil {
+		t.Errorf("GroupImportExport.ImportFile returned error: %v", err)
+	}
+}