From 24995abdaa5e604b509fc743d7e464613bac454b Mon Sep 17 00:00:00 2001 From: Heidi Berry Date: Sat, 9 Nov 2024 18:03:46 +0000 Subject: [PATCH] Add support for the project markdown uploads API --- gitlab.go | 2 + project_markdown_uploads.go | 210 +++++++++++++++++++++++++++++++ project_markdown_uploads_test.go | 163 ++++++++++++++++++++++++ 3 files changed, 375 insertions(+) create mode 100644 project_markdown_uploads.go create mode 100644 project_markdown_uploads_test.go diff --git a/gitlab.go b/gitlab.go index 19ed3eadb..be3e80515 100644 --- a/gitlab.go +++ b/gitlab.go @@ -195,6 +195,7 @@ type Client struct { ProjectFeatureFlags *ProjectFeatureFlagService ProjectImportExport *ProjectImportExportService ProjectIterations *ProjectIterationsService + ProjectMarkdownUploads *ProjectMarkdownUploadsService ProjectMembers *ProjectMembersService ProjectMirrors *ProjectMirrorService ProjectRepositoryStorageMove *ProjectRepositoryStorageMoveService @@ -433,6 +434,7 @@ func newClient(options ...ClientOptionFunc) (*Client, error) { c.ProjectFeatureFlags = &ProjectFeatureFlagService{client: c} c.ProjectImportExport = &ProjectImportExportService{client: c} c.ProjectIterations = &ProjectIterationsService{client: c} + c.ProjectMarkdownUploads = &ProjectMarkdownUploadsService{client: c} c.ProjectMembers = &ProjectMembersService{client: c} c.ProjectMirrors = &ProjectMirrorService{client: c} c.ProjectRepositoryStorageMove = &ProjectRepositoryStorageMoveService{client: c} diff --git a/project_markdown_uploads.go b/project_markdown_uploads.go new file mode 100644 index 000000000..a8998e07f --- /dev/null +++ b/project_markdown_uploads.go @@ -0,0 +1,210 @@ +// +// Copyright 2024, 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" + "net/http" + "time" +) + +// ProjectMarkdownUploadsService handles communication with the project markdown uploads +// related methods of the GitLab API. +// +// Gitlab API docs: https://docs.gitlab.com/ee/api/project_markdown_uploads.html +type ProjectMarkdownUploadsService struct { + client *Client +} + +// ProjectMarkdownUploadedFile represents a single project markdown uploaded file. +// +// Gitlab API docs: https://docs.gitlab.com/ee/api/project_markdown_uploads.html +type ProjectMarkdownUploadedFile struct { + ID int `json:"id"` + Alt string `json:"alt"` + URL string `json:"url"` + FullPath string `json:"full_path"` + Markdown string `json:"markdown"` +} + +// ProjectMarkdownUpload represents a single project markdown upload. +// +// Gitlab API docs: https://docs.gitlab.com/ee/api/project_markdown_uploads.html +type ProjectMarkdownUpload struct { + ID int `json:"id"` + Size int `json:"size"` + Filename string `json:"filename"` + CreatedAt *time.Time `json:"created_at"` + UploadedBy *User `json:"uploaded_by"` +} + +// Gets a string representation of a ProjectMarkdownUpload. +// +// GitLab API docs: https://docs.gitlab.com/ee/api/project_markdown_uploads.html +func (m ProjectMarkdownUpload) String() string { + return Stringify(m) +} + +// UploadProjectMarkdown uploads a markdown file to a project. +// +// GitLab docs: +// https://docs.gitlab.com/ee/api/project_markdown_uploads.html#upload-a-file +func (s *ProjectMarkdownUploadsService) UploadProjectMarkdown(pid interface{}, content io.Reader, options ...RequestOptionFunc) (*ProjectMarkdownUploadedFile, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/uploads", PathEscape(project)) + + // 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.MethodPost, u, nil, options) + if err != nil { + return nil, nil, err + } + + // Overwrite the method and body. + req.Method = http.MethodPost + req.SetBody(content) + + f := new(ProjectMarkdownUploadedFile) + resp, err := s.client.Do(req, f) + if err != nil { + return nil, resp, err + } + + return f, resp, nil +} + +// ListProjectMarkdownUploads gets all markdown uploads for a project. +// +// GitLab API Docs: +// https://docs.gitlab.com/ee/api/project_markdown_uploads.html#list-uploads +func (s *ProjectMarkdownUploadsService) ListProjectMarkdownUploads(pid interface{}, options ...RequestOptionFunc) ([]*ProjectMarkdownUpload, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/uploads", PathEscape(project)) + + req, err := s.client.NewRequest(http.MethodGet, u, nil, options) + if err != nil { + return nil, nil, err + } + + var uploads []*ProjectMarkdownUpload + resp, err := s.client.Do(req, &uploads) + if err != nil { + return nil, resp, err + } + + return uploads, resp, err +} + +// DownloadProjectMarkdownUploadByID downloads a specific upload by ID. +// +// GitLab API Docs: +// https://docs.gitlab.com/ee/api/project_markdown_uploads.html#download-an-uploaded-file-by-id +func (s *ProjectMarkdownUploadsService) DownloadProjectMarkdownUploadByID(pid interface{}, uploadID int, options ...RequestOptionFunc) ([]byte, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/uploads/%d", PathEscape(project), uploadID) + + 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 +} + +// DownloadProjectMarkdownUploadBySecretAndFilename downloads a specific upload +// by secret and filename. +// +// GitLab API Docs: +// https://docs.gitlab.com/ee/api/project_markdown_uploads.html#download-an-uploaded-file-by-secret-and-filename +func (s *ProjectMarkdownUploadsService) DownloadProjectMarkdownUploadBySecretAndFilename(pid interface{}, secret string, filename string, options ...RequestOptionFunc) ([]byte, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/uploads/%s/%s", PathEscape(project), PathEscape(secret), 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 +} + +// DeleteProjectMarkdownUploadByID deletes an upload by ID. +// +// GitLab API Docs: +// https://docs.gitlab.com/ee/api/project_markdown_uploads.html#delete-an-uploaded-file-by-id +func (s *ProjectMarkdownUploadsService) DeleteProjectMarkdownUploadByID(pid interface{}, uploadID int, options ...RequestOptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/uploads/%d", PathEscape(project), uploadID) + + req, err := s.client.NewRequest(http.MethodDelete, u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// DeleteProjectMarkdownUploadBySecretAndFilename deletes an upload +// by secret and filename. +// +// GitLab API Docs: +// https://docs.gitlab.com/ee/api/project_markdown_uploads.html#delete-an-uploaded-file-by-secret-and-filename +func (s *ProjectMarkdownUploadsService) DeleteProjectMarkdownUploadBySecretAndFilename(pid interface{}, secret string, filename string, options ...RequestOptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/uploads/%s/%s", + PathEscape(project), PathEscape(secret), PathEscape(filename)) + + req, err := s.client.NewRequest(http.MethodDelete, u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} diff --git a/project_markdown_uploads_test.go b/project_markdown_uploads_test.go new file mode 100644 index 000000000..b83305a49 --- /dev/null +++ b/project_markdown_uploads_test.go @@ -0,0 +1,163 @@ +package gitlab + +import ( + "fmt" + "net/http" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestProjectMarkdownUploads_UploadProjectMarkdown(t *testing.T) { + mux, client := setup(t) + + mux.HandleFunc("/api/v4/projects/1/uploads", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodPost) + fmt.Fprint(w, ` + { + "id": 5, + "alt": "dk", + "url": "/uploads/66dbcd21ec5d24ed6ea225176098d52b/dk.png", + "full_path": "/-/project/1234/uploads/66dbcd21ec5d24ed6ea225176098d52b/dk.png", + "markdown": "![dk](/uploads/66dbcd21ec5d24ed6ea225176098d52b/dk.png)" + } + `) + }) + + want := &ProjectMarkdownUploadedFile{ + ID: 5, + Alt: "dk", + URL: "/uploads/66dbcd21ec5d24ed6ea225176098d52b/dk.png", + FullPath: "/-/project/1234/uploads/66dbcd21ec5d24ed6ea225176098d52b/dk.png", + Markdown: "![dk](/uploads/66dbcd21ec5d24ed6ea225176098d52b/dk.png)", + } + + content := strings.NewReader("bar = baz") + upload, resp, err := client.ProjectMarkdownUploads.UploadProjectMarkdown(1, content) + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, want, upload) +} + +func TestProjectMarkdownUploads_ListProjectMarkdownUploads(t *testing.T) { + mux, client := setup(t) + + mux.HandleFunc("/api/v4/projects/1/uploads", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + fmt.Fprint(w, ` + [ + { + "id": 1, + "size": 1024, + "filename": "image.png", + "created_at":"2024-06-20T15:53:03.000Z", + "uploaded_by": { + "id": 18, + "name" : "Alexandra Bashirian", + "username" : "eileen.lowe" + } + }, + { + "id": 2, + "size": 512, + "filename": "other-image.png", + "created_at":"2024-06-19T15:53:03.000Z", + "uploaded_by": null + } + ] + `) + }) + + created1 := time.Date(2024, 6, 20, 15, 53, 3, 0, time.UTC) + created2 := time.Date(2024, 6, 19, 15, 53, 3, 0, time.UTC) + want := []*ProjectMarkdownUpload{ + { + ID: 1, + Size: 1024, + Filename: "image.png", + CreatedAt: &created1, + UploadedBy: &User{ + ID: 18, + Name: "Alexandra Bashirian", + Username: "eileen.lowe", + }, + }, + { + ID: 2, + Size: 512, + Filename: "other-image.png", + CreatedAt: &created2, + }, + } + + uploads, resp, err := client.ProjectMarkdownUploads.ListProjectMarkdownUploads(1) + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, want, uploads) +} + +func TestProjectMarkdownUploads_DownloadProjectMarkdownUploadByID(t *testing.T) { + mux, client := setup(t) + + mux.HandleFunc("/api/v4/projects/1/uploads/2", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + fmt.Fprint(w, strings.TrimSpace(` + bar = baz + `)) + }) + + want := []byte("bar = baz") + + bytes, resp, err := client.ProjectMarkdownUploads.DownloadProjectMarkdownUploadByID(1, 2) + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, want, bytes) +} + +func TestProjectMarkdownUploads_DownloadProjectMarkdownUploadBySecretAndFilename(t *testing.T) { + mux, client := setup(t) + + mux.HandleFunc("/api/v4/projects/1/uploads/secret/filename", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + fmt.Fprint(w, strings.TrimSpace(` + bar = baz + `)) + }) + + want := []byte("bar = baz") + + bytes, resp, err := client.ProjectMarkdownUploads.DownloadProjectMarkdownUploadBySecretAndFilename(1, "secret", "filename") + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, want, bytes) +} + +func TestProjectMarkdownUploads_DeleteProjectMarkdownUploadByID(t *testing.T) { + mux, client := setup(t) + + mux.HandleFunc("/api/v4/projects/1/uploads/2", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodDelete) + w.WriteHeader(204) + }) + + resp, err := client.ProjectMarkdownUploads.DeleteProjectMarkdownUploadByID(1, 2) + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, 204, resp.StatusCode) +} + +func TestProjectMarkdownUploads_DeleteProjectMarkdownUploadBySecretAndFilename(t *testing.T) { + mux, client := setup(t) + + mux.HandleFunc("/api/v4/projects/1/uploads/secret/filename", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodDelete) + w.WriteHeader(204) + }) + + resp, err := client.ProjectMarkdownUploads.DeleteProjectMarkdownUploadBySecretAndFilename(1, "secret", "filename") + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, 204, resp.StatusCode) +}