Skip to content

Commit

Permalink
Refactor all the upload related bits
Browse files Browse the repository at this point in the history
  • Loading branch information
svanharmelen committed Dec 29, 2021
1 parent fe1d2ba commit 979f1c9
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 121 deletions.
88 changes: 83 additions & 5 deletions gitlab.go
Expand Up @@ -18,12 +18,14 @@
package gitlab

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"math/rand"
"mime/multipart"
"net/http"
"net/url"
"sort"
Expand Down Expand Up @@ -520,11 +522,11 @@ func (c *Client) setBaseURL(urlStr string) error {
return nil
}

// NewRequest creates an API request. An optional relative URL path can be
// provided in path, in which case it is resolved relative to the base URL
// of the Client.
// Paths should always be specified without a preceding slash. If specified,
// the value pointed to by body is JSON encoded and included as request body.
// NewRequest creates a new API request. The method expects a relative URL
// path that will be resolved relative to the base URL of the Client.
// Relative URL paths should always be specified without a preceding slash.
// If specified, the value pointed to by body is JSON encoded and included
// as the request body.
func (c *Client) NewRequest(method, path string, opt interface{}, options []RequestOptionFunc) (*retryablehttp.Request, error) {
u := *c.baseURL
unescaped, err := url.PathUnescape(path)
Expand Down Expand Up @@ -585,6 +587,82 @@ func (c *Client) NewRequest(method, path string, opt interface{}, options []Requ
return req, nil
}

// UploadRequest creates an API request for uploading a file. The method
// expects a relative URL path that will be resolved relative to the base
// URL of the Client. Relative URL paths should always be specified without
// a preceding slash. If specified, the value pointed to by body is JSON
// encoded and included as the request body.
func (c *Client) UploadRequest(method, path string, content io.Reader, filename string, uploadType UploadType, opt interface{}, options []RequestOptionFunc) (*retryablehttp.Request, error) {
u := *c.baseURL
unescaped, err := url.PathUnescape(path)
if err != nil {
return nil, err
}

// Set the encoded path data
u.RawPath = c.baseURL.Path + path
u.Path = c.baseURL.Path + unescaped

// Create a request specific headers map.
reqHeaders := make(http.Header)
reqHeaders.Set("Accept", "application/json")

if c.UserAgent != "" {
reqHeaders.Set("User-Agent", c.UserAgent)
}

b := new(bytes.Buffer)
w := multipart.NewWriter(b)

fw, err := w.CreateFormFile(string(uploadType), filename)
if err != nil {
return nil, err
}

if _, err := io.Copy(fw, content); err != nil {
return nil, err
}

if opt != nil {
fields, err := query.Values(opt)
if err != nil {
return nil, err
}
for name := range fields {
if err = w.WriteField(name, fmt.Sprintf("%v", fields.Get(name))); err != nil {
return nil, err
}
}
}

if err = w.Close(); err != nil {
return nil, err
}

reqHeaders.Set("Content-Type", w.FormDataContentType())

req, err := retryablehttp.NewRequest(method, u.String(), b)
if err != nil {
return nil, err
}

for _, fn := range options {
if fn == nil {
continue
}
if err := fn(req); err != nil {
return nil, err
}
}

// Set the request specific headers.
for k, v := range reqHeaders {
req.Header[k] = v
}

return req, nil
}

// Response is a GitLab API response. This wraps the standard http.Response
// returned from GitLab and provides convenient access to things like
// pagination links.
Expand Down
17 changes: 13 additions & 4 deletions project_import_export.go
Expand Up @@ -19,6 +19,7 @@ package gitlab
import (
"bytes"
"fmt"
"io"
"net/http"
"time"
)
Expand Down Expand Up @@ -162,18 +163,26 @@ func (s *ProjectImportExportService) ExportDownload(pid interface{}, options ...
// https://docs.gitlab.com/ce/api/project_import_export.html#import-a-file
type ImportFileOptions struct {
Namespace *string `url:"namespace,omitempty" json:"namespace,omitempty"`
File *string `url:"file,omitempty" json:"file,omitempty"`
Name *string `url:"name,omitempty" json:"name,omitempty"`
Path *string `url:"path,omitempty" json:"path,omitempty"`
Overwrite *bool `url:"overwrite,omitempty" json:"overwrite,omitempty"`
OverrideParams *CreateProjectOptions `url:"override_params,omitempty" json:"override_params,omitempty"`
}

// ImportFile import a file.
// Import a project from an archive file.
//
// GitLab API docs:
// https://docs.gitlab.com/ce/api/project_import_export.html#import-a-file
func (s *ProjectImportExportService) ImportFile(opt *ImportFileOptions, options ...RequestOptionFunc) (*ImportStatus, *Response, error) {
req, err := s.client.NewRequest(http.MethodPost, "projects/import", opt, options)
func (s *ProjectImportExportService) ImportFromFile(archive io.Reader, opt *ImportFileOptions, options ...RequestOptionFunc) (*ImportStatus, *Response, error) {
req, err := s.client.UploadRequest(
http.MethodPost,
"projects/import",
archive,
"archive.tar.gz",
UploadFile,
opt,
options,
)
if err != nil {
return nil, nil, err
}
Expand Down
21 changes: 4 additions & 17 deletions project_import_export_test.go
@@ -1,6 +1,7 @@
package gitlab

import (
"bytes"
"fmt"
"net/http"
"testing"
Expand Down Expand Up @@ -159,32 +160,18 @@ func TestProjectImportExportService_ImportFile(t *testing.T) {
ImportStatus: "scheduled",
}

es, resp, err := client.ProjectImportExport.ImportFile(nil, nil)
file := bytes.NewBufferString("dummy")
es, resp, err := client.ProjectImportExport.ImportFromFile(file, nil, nil)
require.NoError(t, err)
require.NotNil(t, resp)
require.Equal(t, want, es)

es, resp, err = client.ProjectImportExport.ImportFile(nil, errorOption)
es, resp, err = client.ProjectImportExport.ImportFromFile(file, nil, errorOption)
require.EqualError(t, err, "RequestOptionFunc returns an error")
require.Nil(t, resp)
require.Nil(t, es)
}

func TestProjectImportExportService_ImportFile_NotFound(t *testing.T) {
mux, server, client := setup(t)
defer teardown(server)

mux.HandleFunc("/api/v4/projects/import", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodPost)
w.WriteHeader(http.StatusNotFound)
})

es, resp, err := client.ProjectImportExport.ImportFile(nil, nil)
require.Error(t, err)
require.Nil(t, es)
require.Equal(t, http.StatusNotFound, resp.StatusCode)
}

func TestProjectImportExportService_ImportStatus(t *testing.T) {
mux, server, client := setup(t)
defer teardown(server)
Expand Down
113 changes: 37 additions & 76 deletions projects.go
Expand Up @@ -17,13 +17,9 @@
package gitlab

import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"time"
)

Expand Down Expand Up @@ -1278,7 +1274,7 @@ func (s *ProjectsService) DeleteProjectForkRelation(pid interface{}, options ...
return s.client.Do(req, nil)
}

// ProjectFile represents an uploaded project file
// ProjectFile represents an uploaded project file.
//
// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#upload-a-file
type ProjectFile struct {
Expand All @@ -1287,57 +1283,69 @@ type ProjectFile struct {
Markdown string `json:"markdown"`
}

// UploadFile upload a file from disk
// UploadFile uploads a file.
//
// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#upload-a-file
func (s *ProjectsService) UploadFile(pid interface{}, file string, options ...RequestOptionFunc) (*ProjectFile, *Response, error) {
func (s *ProjectsService) UploadFile(pid interface{}, content io.Reader, filename string, options ...RequestOptionFunc) (*ProjectFile, *Response, error) {
project, err := parseID(pid)
if err != nil {
return nil, nil, err
}
u := fmt.Sprintf("projects/%s/uploads", PathEscape(project))

f, err := os.Open(file)
req, err := s.client.UploadRequest(
http.MethodPost,
u,
content,
filename,
UploadFile,
nil,
options,
)
if err != nil {
return nil, nil, err
}
defer f.Close()

b := &bytes.Buffer{}
w := multipart.NewWriter(b)

_, filename := filepath.Split(file)
fw, err := w.CreateFormFile("file", filename)
pf := new(ProjectFile)
resp, err := s.client.Do(req, pf)
if err != nil {
return nil, nil, err
return nil, resp, err
}

_, err = io.Copy(fw, f)
if err != nil {
return nil, nil, err
}
w.Close()
return pf, resp, nil
}

req, err := s.client.NewRequest(http.MethodPost, u, nil, options)
// UploadAvatar uploads an avatar.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/projects.html#upload-a-project-avatar
func (s *ProjectsService) UploadAvatar(pid interface{}, avatar io.Reader, options ...RequestOptionFunc) (*Project, *Response, error) {
project, err := parseID(pid)
if err != nil {
return nil, nil, err
}
u := fmt.Sprintf("projects/%s", PathEscape(project))

// Set the buffer as the request body.
if err = req.SetBody(b); err != nil {
req, err := s.client.UploadRequest(
http.MethodPut,
u,
avatar,
"avatar.png",
UploadAvatar,
nil,
options,
)
if err != nil {
return nil, nil, err
}

// Overwrite the default content type.
req.Header.Set("Content-Type", w.FormDataContentType())

uf := &ProjectFile{}
resp, err := s.client.Do(req, uf)
p := new(Project)
resp, err := s.client.Do(req, p)
if err != nil {
return nil, resp, err
}

return uf, resp, nil
return p, resp, err
}

// ListProjectForks gets a list of project forks.
Expand Down Expand Up @@ -1808,50 +1816,3 @@ func (s *ProjectsService) TransferProject(pid interface{}, opt *TransferProjectO

return p, resp, err
}

// UploadAvatar uploads an avatar for the project
//
// GitLab API docs: https://docs.gitlab.com/ee/api/projects.html#upload-a-project-avatar
func (s *ProjectsService) UploadAvatar(pid interface{}, avatar io.Reader, filename string, options ...RequestOptionFunc) (*Project, *Response, error) {
project, err := parseID(pid)
if err != nil {
return nil, nil, err
}
u := fmt.Sprintf("projects/%s", PathEscape(project))

b := &bytes.Buffer{}
w := multipart.NewWriter(b)

_, filename = filepath.Split(filename)
fw, err := w.CreateFormFile("avatar", filename)
if err != nil {
return nil, nil, err
}

_, err = io.Copy(fw, avatar)
if err != nil {
return nil, nil, err
}
w.Close()

req, err := s.client.NewRequest(http.MethodPut, u, nil, options)
if err != nil {
return nil, nil, err
}

// Set the buffer as the request body.
if err = req.SetBody(b); err != nil {
return nil, nil, err
}

// Overwrite the default content type.
req.Header.Set("Content-Type", w.FormDataContentType())

p := new(Project)
resp, err := s.client.Do(req, p)
if err != nil {
return nil, resp, err
}

return p, resp, err
}

0 comments on commit 979f1c9

Please sign in to comment.