From 7148ba2aa997bb88c947098d67316888407e9f81 Mon Sep 17 00:00:00 2001 From: previ Date: Fri, 2 Apr 2021 22:38:13 +0200 Subject: [PATCH] add Group import export Signed-off-by: previ --- .github/workflows/lint_and_test.yml | 4 +- go.mod | 4 +- go.sum | 16 ++- groups.go | 163 ++++++++++++++++++++++++++++ groups_test.go | 89 +++++++++++++++ 5 files changed, 268 insertions(+), 8 deletions(-) diff --git a/.github/workflows/lint_and_test.yml b/.github/workflows/lint_and_test.yml index c6dd610f0..6a26448d8 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.13.x, 1.14.x, 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/go.mod b/go.mod index eb13bcef7..e363c2737 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 74ea228e9..671236d70 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/groups.go b/groups.go index 863235704..676dd7831 100644 --- a/groups.go +++ b/groups.go @@ -17,8 +17,12 @@ package gitlab import ( + "bytes" "fmt" + "io" + "mime/multipart" "net/http" + "os" "time" ) @@ -770,3 +774,162 @@ func (s *GroupsService) DeleteGroupPushRule(gid interface{}, options ...RequestO return s.client.Do(req, nil) } + +// ImportExportGroupStatus represent Group Export / Import return status +// +// GitLab API docs: https://docs.gitlab.com/ee/api/group_import_export.html +type ImportExportGroupStatus struct { + Message *string `json:"message,omitempty"` +} + +// GroupExportRequest gets all details of a group. +// +// GitLab API docs: https://docs.gitlab.com/ee/api/group_import_export.html +func (s *GroupsService) GroupExportRequest(gid interface{}, options ...RequestOptionFunc) (*ImportExportGroupStatus, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, 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, nil, err + } + + status := new(ImportExportGroupStatus) + resp, err := s.client.Do(req, status) + if err != nil { + return nil, resp, err + } + + return status, resp, err +} + +// GroupExportDownload gets all details of a group. +// +// GitLab API docs: https://docs.gitlab.com/ee/api/group_import_export.html +func (s *GroupsService) GroupExportDownload(gid interface{}, options ...RequestOptionFunc) ([]byte, *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 + } + + var b bytes.Buffer + resp, err := s.client.Do(req, &b) + if err != nil { + return nil, resp, err + } + + return b.Bytes(), resp, err +} + +// GroupImportOptions represents parameters for GroupImport. +// +// GitLab API docs: https://docs.gitlab.com/ee/api/group_import_export.html +type GroupImportOptions struct { + Name *string `url:"name,omitempty" json:"name,omitempty"` + File *string `url:"file,omitempty" json:"file,omitempty"` + Path *string `url:"path,omitempty" json:"path,omitempty"` + ParentID *string `url:"parent_id,omitempty" json:"parent_id,omitempty"` +} + +// GroupImport gets all details of a group. +// +// GitLab API docs: https://docs.gitlab.com/ee/api/group_import_export.html +func (s *GroupsService) GroupImport(opt *GroupImportOptions, options ...RequestOptionFunc) (*ImportExportGroupStatus, *Response, error) { + // Open the file + file, err := os.Open(*opt.File) + if err != nil { + fmt.Println(err) + } + // Close the file later + defer file.Close() + + // Buffer to store our request body as bytes + var requestBody bytes.Buffer + + // Create a multipart writer + multiPartWriter := multipart.NewWriter(&requestBody) + + // Initialize the file field + fileWriter, err := multiPartWriter.CreateFormFile("file", "grpup..tar.gz") + if err != nil { + fmt.Println(err) + return nil, nil, err + } + + // Copy the actual file content to the field field's writer + _, err = io.Copy(fileWriter, file) + if err != nil { + fmt.Println(err) + return nil, nil, err + } + + // Populate other fields + fw, err := multiPartWriter.CreateFormField("name") + if err != nil { + fmt.Println(err) + return nil, nil, err + } + + _, err = fw.Write([]byte(*opt.Name)) + if err != nil { + fmt.Println(err) + return nil, nil, err + } + + fw, err = multiPartWriter.CreateFormField("path") + if err != nil { + fmt.Println(err) + return nil, nil, err + } + + _, err = fw.Write([]byte(*opt.Path)) + if err != nil { + fmt.Println(err) + return nil, nil, err + } + + fw, err = multiPartWriter.CreateFormField("parent_id") + if err != nil { + fmt.Println(err) + } + + _, err = fw.Write([]byte(*opt.ParentID)) + if err != nil { + fmt.Println(err) + } + + // We completed adding the file and the fields, let's close the multipart writer + // So it writes the ending boundary + multiPartWriter.Close() + + req, err := s.client.NewRequest(http.MethodPost, "groups/import", nil, options) + if err != nil { + return nil, nil, err + } + + // Set the buffer as the request body. + if err = req.SetBody(&requestBody); err != nil { + return nil, nil, err + } + + // We need to set the content type from the writer, it includes necessary boundary as well + req.Header.Set("Content-Type", multiPartWriter.FormDataContentType()) + + // Do the request + var status = new(ImportExportGroupStatus) + resp, err := s.client.Do(req, status) + if err != nil { + return nil, resp, err + } + + return status, resp, err +} diff --git a/groups_test.go b/groups_test.go index 515dc81c4..7dbce730a 100644 --- a/groups_test.go +++ b/groups_test.go @@ -2,7 +2,10 @@ package gitlab import ( "fmt" + "io/ioutil" + "log" "net/http" + "os" "reflect" "testing" ) @@ -376,3 +379,89 @@ func TestUnshareGroupFromGroup(t *testing.T) { t.Errorf("Groups.UnshareGroupFromGroup returned status code %d", r.StatusCode) } } + +func TestGroupExportRequest(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"}`) + }) + + status, _, err := client.Groups.GroupExportRequest(1) + if err != nil { + t.Errorf("Groups.GroupExportRequest returned error: %v", err) + } + + want := &ImportExportGroupStatus{Message: String("202 Accepted")} + if !reflect.DeepEqual(want, status) { + t.Errorf("Groups.GroupExportRequest returned %+v, want %+v", status, want) + } +} + +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) + }) + + data, _, err := client.Groups.GroupExportDownload(1) + if err != nil { + t.Errorf("Groups.GroupExportDownload returned error: %v", err) + } + + want := []byte("fake content") + if !reflect.DeepEqual(want, data) { + t.Errorf("Groups.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 := GroupImportOptions{ + Name: String("test"), + Path: String("path"), + ParentID: String("1"), + File: String(tmpfile.Name()), + } + + r, _, err := client.Groups.GroupImport(&opt) + if err != nil { + t.Errorf("Groups.GroupExportImport returned error: %v", err) + } + + want := &ImportExportGroupStatus{Message: String("202 Accepted")} + if !reflect.DeepEqual(want, r) { + t.Errorf("Groups.GroupExportDownload returned %+v, want %+v", r, want) + } +}