From 11093f75353dd7bc369188434f8be4726d8417ca Mon Sep 17 00:00:00 2001 From: "Gustavo L de Mello (Guz)" Date: Tue, 7 Jan 2025 18:13:00 -0300 Subject: [PATCH] feat(blogo,forgejo): add Forgejo/Gitea client-methods --- blogo/forgejo/client.go | 171 +++++++++++++++++++++++++++++++++++++++ blogo/forgejo/forgejo.go | 34 ++++++++ 2 files changed, 205 insertions(+) create mode 100644 blogo/forgejo/client.go diff --git a/blogo/forgejo/client.go b/blogo/forgejo/client.go new file mode 100644 index 0000000..b3c1bb2 --- /dev/null +++ b/blogo/forgejo/client.go @@ -0,0 +1,171 @@ +package forgejo + +// Contents of this file are heavily sourced from the official Gitea SDK for Go, +// (available at https://gitea.com/gitea/go-sdk). These contents are licensed under +// the terms of the MIT license, which a copy can be found at https://opensource.org/license/mit, +// https://spdx.org/licenses/MIT.html, and stated below: +// +// --- Start of MIT license --- +// +// Copyright (c) 2025 Gustavo "Guz" L. de Mello +// Copyright (c) 2025 Lored.dev +// Copyright (c) 2016 The Gitea Authors +// Copyright (c) 2014 The Gogs Authors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// +// --- End of MIT license --- +// +// By contributing to this file, you agree with the terms listed in this file's +// License. + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" +) + +type client struct { + endpoint string + http *http.Client +} + +func newClient(endpoint string, http *http.Client) *client { + return &client{endpoint, http} +} + +func (c *client) GetContents(owner, repo, ref, filepath string) (contentsResponse, error) { + data, _, err := c.get( + fmt.Sprintf("/repos/%s/%s/contents/%s?ref=%s", owner, repo, url.QueryEscape(ref), filepath), + ) + if err != nil { + return contentsResponse{}, err + } + + var file contentsResponse + if err := json.Unmarshal(data, &file); err != nil { + return contentsResponse{}, errors.Join( + errors.New("failed to parse JSON response from API"), + err, + ) + } + + return file, nil +} + +func (c *client) ListContents(owner, repo, ref, filepath string) ([]contentsResponse, error) { + data, _, err := c.get( + fmt.Sprintf("/repos/%s/%s/contents/%s?ref=%s", owner, repo, url.QueryEscape(ref), filepath), + ) + if err != nil { + return []contentsResponse{}, err + } + + var directory []contentsResponse + if err := json.Unmarshal(data, &directory); err != nil { + return []contentsResponse{}, errors.Join( + errors.New("failed to parse JSON response from API"), + err, + ) + } + + return directory, nil +} + +func (c *client) get(path string) (body []byte, res *http.Response, err error) { + res, err = c.http.Get(c.endpoint + path) + if err != nil { + return nil, nil, errors.Join(errors.New("failed to request"), err) + } + + data, err := statusCodeToErr(res) + if err != nil { + return data, res, err + } + + data, err = io.ReadAll(res.Body) + if err != nil { + return nil, res, err + } + + return data, res, err +} + +func statusCodeToErr(resp *http.Response) (body []byte, err error) { + if resp.StatusCode/100 == 2 { + return nil, nil + } + + defer resp.Body.Close() + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("body read on HTTP error %d: %v", resp.StatusCode, err) + } + + errMap := make(map[string]interface{}) + if err = json.Unmarshal(data, &errMap); err != nil { + return data, fmt.Errorf( + "Unknown API Error: %d Request Path: '%s'\nResponse body: '%s'", + resp.StatusCode, + resp.Request.URL.Path, + string(data), + ) + } + + if msg, ok := errMap["message"]; ok { + return data, fmt.Errorf("%v", msg) + } + + return data, fmt.Errorf("%s: %s", resp.Status, string(data)) +} + +type contentsResponse struct { + Name string `json:"name"` + Path string `json:"path"` + SHA string `json:"sha"` + + // NOTE: can be "file", "dir", "symlink" or "submodule" + Type string `json:"type"` + Size int64 `json:"size"` + // NOTE: populated just when `type` is "contentsResponseTypeFile" + Encoding *string `json:"encoding"` + // NOTE: populated just when `type` is "contentsResponseTypeFile" + Content *string `json:"content"` + // NOTE: populated just when `type` is "contentsResponseTypeSymlink" + Target *string `json:"target"` + + URL *string `json:"url"` + HTMLURL *string `json:"html_url"` + GitURL *string `json:"git_url"` + DownloadURL *string `json:"download_url"` + + // NOTE: populated just when `type` is "contentsResponseTypeSubmodule" + SubmoduleGitURL *string `json:"submodule_giit_url"` + + Links *fileLinksResponse `json:"_links"` +} + +type fileLinksResponse struct { + Self *string `json:"self"` + GitURL *string `json:"git"` + HTMLURL *string `json:"html"` +} diff --git a/blogo/forgejo/forgejo.go b/blogo/forgejo/forgejo.go index 2da9f1f..c0debcf 100644 --- a/blogo/forgejo/forgejo.go +++ b/blogo/forgejo/forgejo.go @@ -1,16 +1,25 @@ package forgejo import ( + "fmt" + "net/http" + "net/url" + "strings" + "forge.capytal.company/loreddev/x/blogo" ) const pluginName = "blogo-forgejo" type plugin struct { + client *client + owner string repo string } + type Opts struct { + HTTPClient *http.Client Ref string } @@ -20,7 +29,32 @@ func New(owner, repo, apiUrl string, opts ...Opts) blogo.Plugin { opt = opts[0] } + if opt.HTTPClient == nil { + opt.HTTPClient = http.DefaultClient + } + + u, err := url.Parse(apiUrl) + if err != nil { + panic( + fmt.Sprintf( + "%s: %q is not a valid URL. Err: %q", + pluginName, + apiUrl, + err.Error(), + ), + ) + } + + if u.Path == "" || u.Path == "/" { + u.Path = "/api/v1" + } else { + u.Path = strings.TrimSuffix(u.Path, "/api/v1") + } + + client := newClient(u.String(), opt.HTTPClient) + return &plugin{ + client: client, } }