123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330 |
- /*
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
- package main
- import (
- "bytes"
- "context"
- "crypto/tls"
- "encoding/json"
- "fmt"
- "io"
- "net/http"
- "net/url"
- "strings"
- "time"
- dp "idio.link/go/depager/v2"
- )
- func NewGogsClient(
- ctx context.Context,
- accessToken string,
- ) *GogsClient {
- t := &http.Transport{
- TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
- TLSHandshakeTimeout: 15 * time.Second,
- IdleConnTimeout: 30 * time.Minute,
- ResponseHeaderTimeout: 180 * time.Second,
- ExpectContinueTimeout: 10 * time.Second,
- }
- c := &http.Client{
- Transport: t,
- Timeout: 30 * time.Minute,
- }
- baseURI, _ := url.Parse("https://git.idiolink.net/api/v1/")
- return &GogsClient{
- baseURI: baseURI,
- ctx: ctx,
- httpClient: c,
- retries: 3,
- retryWait: 10 * time.Second,
- token: accessToken,
- }
- }
- type GogsClient struct {
- ctx context.Context
- httpClient *http.Client
- baseURI *url.URL
- blockReqsBefore time.Time
- retries int
- retryWait time.Duration
- token string
- }
- func (c *GogsClient) expandResource(
- resource *url.URL,
- ) *url.URL {
- ex, err :=
- url.JoinPath(c.baseURI.String(), resource.EscapedPath())
- if err != nil {
- panic(fmt.Sprintf("BUG: gogs client: expand resource: failed to join path: '%s' + '%s': %v", c.baseURI, resource.RequestURI(), err))
- }
- next, err := url.Parse(ex)
- if err != nil {
- panic(fmt.Sprintf("BUG: gogs client: expand resource: failed to parse expanded resource '%s': %v", ex, err))
- }
- return next
- }
- func (c *GogsClient) sendRequestWithRetry(
- req *http.Request,
- ) (resp *http.Response, err error) {
- for i := 0; i < c.retries+1; i++ {
- resp, err = c.httpClient.Do(req)
- if err != nil {
- err = fmt.Errorf("send request with retry: %w", err)
- return
- }
- }
- if resp == nil {
- err = fmt.Errorf("send request with retry: unknown failure")
- return
- }
- return
- }
- func (c *GogsClient) request(
- method string,
- resource *url.URL,
- body io.Reader,
- ) (respHead http.Header, respBody []byte, err error) {
- req, err := http.NewRequestWithContext(
- c.ctx,
- method,
- resource.String(),
- body,
- )
- if err != nil {
- err = fmt.Errorf("request %v: %w", resource, err)
- return
- }
- req.Header.
- Add("Authorization", fmt.Sprintf("token %s", c.token))
- resp, err := c.sendRequestWithRetry(req)
- if err != nil {
- err = fmt.Errorf("request '%s': %+v: %w", req.URL, req, err)
- return
- }
- defer resp.Body.Close()
- respHead = resp.Header
- respBody, err = io.ReadAll(resp.Body)
- if err != nil {
- err = fmt.Errorf("request %v: failed to read response body: %w", resource, err)
- return
- }
- // Success response
- if 200 <= resp.StatusCode && resp.StatusCode <= 299 {
- return
- }
- err = fmt.Errorf("request %v: %s", resource, http.StatusText(resp.StatusCode))
- return
- }
- func (c *GogsClient) get(
- resource *url.URL,
- ) (http.Header, []byte, error) {
- return c.request(http.MethodGet, resource, nil)
- }
- func (c *GogsClient) post(
- resource *url.URL,
- body io.Reader,
- ) (http.Header, []byte, error) {
- return c.request(http.MethodPost, resource, body)
- }
- func newGogsSubclient[T any](
- c *GogsClient,
- resource *url.URL,
- ) *GogsSubclient[T] {
- expanded := c.expandResource(resource)
- return &GogsSubclient[T]{
- GogsClient: *c,
- uri: expanded,
- }
- }
- type GogsSubclient[T any] struct {
- GogsClient
- uri *url.URL
- }
- func (c *GogsSubclient[T]) NextPage(
- offset uint64,
- ) (page dp.Page[T], _ uint64, err error) {
- aggr := make([]T, 0, 32)
- head, body, err := c.get(c.uri)
- if err != nil {
- err = fmt.Errorf("gogs client: next page: %w", err)
- return
- }
- err = json.Unmarshal(body, &aggr)
- if err != nil {
- err = fmt.Errorf("gogs client: next page: unmarshal response: %w", err)
- return
- }
- if next := getLinkNext(head.Get("link")); next != "" {
- c.uri, err = url.Parse(next)
- if err != nil {
- err = fmt.Errorf("gogs client: next page: unable to parse next link '%s': %w", next, err)
- return
- }
- }
- page = GogsAggregate[T](aggr)
- return
- }
- func getLinkNext(link string) (next string) {
- // this could be made faster, but doesn't seem necessary
- // See https://www.w3.org/wiki/LinkHeader for details.
- // basic format: `<meta.rdf>; rel=meta, ...`
- before, _, found := strings.Cut(link, `rel="next"`)
- if !found {
- return
- }
- idx := strings.LastIndex(before, "<")
- if idx == -1 {
- return
- }
- r := strings.NewReader(before)
- _, err := r.ReadAt([]byte(next), int64(idx+1))
- if err != nil {
- return
- }
- parts := strings.Split(next, ">")
- if len(parts) != 2 {
- return
- }
- next = parts[0]
- return
- }
- type GogsAggregate[T any] []T
- func (a GogsAggregate[T]) Elems() []T {
- return a
- }
- type GogsRepo struct {
- Id int `json:"id"`
- Owner struct {
- Id int `json:"id"`
- Username string `json:"username"`
- FullName string `json:"full_name"`
- EMail string `json:"email"`
- AvatarURL string `json:"avatar_url"`
- } `json:"owner"`
- FullName string `json:"full_name"`
- CloneURL string `json:"clone_url"`
- HTMLURL string `json:"html_url"`
- SSHURL string `json:"ssh_url"`
- DefaultBranch string `json:"default_branch"`
- Private string `json:"private"`
- Fork string `json:"fork"`
- Permissions struct {
- Admin bool `json:"admin"`
- Push bool `json:"push"`
- Pull bool `json:"pull"`
- } `json:"permissions"`
- }
- func FetchGogsOrganizationRepos[T *GogsRepo](
- c *GogsClient,
- group string,
- ) dp.Pager[T] {
- resStrFmt := "/orgs/%s/repos"
- escaped := url.PathEscape(group)
- resStr := fmt.Sprintf(resStrFmt, escaped)
- resource, err := url.Parse(resStr)
- if err != nil { // should only occur in case of bugs
- panic(err)
- }
- return dp.NewPager[T](
- newGogsSubclient[T](c, resource),
- 100,
- )
- }
- type MigrateToGogsRepoInput struct {
- SrcURI string `query:"clone_addr"` // Required Remote Git address (HTTP/HTTPS URL or local path)
- SrcUsername string `query:"auth_username"` // Authorization username [for remote]
- SrcPassword string `query:"auth_password"` // Authorization password [for remote]
- TgtUID int `query:"uid"` // Required User ID who takes ownership of this repository
- TgtRepoName string `query:"repo_name"` // Required Repository name
- TgtRepoMirror bool `query:"mirror"` // Repository will be a mirror. Default is false
- TgtRepoPrivate bool `query:"private"` // Repository will be private. Default is false
- TgtRepoDescr string `query:"description"` // Repository description
- }
- func MigrateToGogsRepo(
- c *GogsClient,
- input *MigrateToGogsRepoInput,
- repo *GogsRepo,
- ) (err error) {
- errBase := "migrate to gogs repo"
- resource, err := url.Parse("/repos/migrate")
- if err != nil { // should only occur in case of bugs
- panic(err)
- }
- expanded := c.expandResource(resource)
- req, err := json.Marshal(input)
- if err != nil {
- err = fmt.Errorf("%s: marshal input '%#v': %w", errBase, string(req), err)
- return
- }
- _, resp, err := c.post(expanded, bytes.NewReader(req))
- if err != nil {
- err = fmt.Errorf("%s: post request '%s': %w", errBase, string(req), err)
- return
- }
- err = json.Unmarshal(resp, repo)
- if err != nil {
- err = fmt.Errorf("%s: parse response '%s': %w", errBase, string(resp), err)
- return
- }
- return
- }
- type GogsUser struct {
- Id int `json:"id"`
- Username string `json:"username"`
- FullName string `json:"full_name"`
- EMail string `json:"email"`
- AvatarURL string `json:"avatar_url"`
- }
- func FetchGogsUserAuthenticated(
- c *GogsClient,
- user *GogsUser,
- ) (err error) {
- errBase := "fetch gogs user authenticated"
- resource, err := url.Parse("/user")
- if err != nil { // should only occur in case of bugs
- err = fmt.Errorf("%s: %w", errBase, err)
- panic(err)
- }
- expanded := c.expandResource(resource)
- _, resp, err := c.get(expanded)
- if err != nil {
- err = fmt.Errorf("%s: get request '%s': %w", errBase, expanded, err)
- return
- }
- err = json.Unmarshal(resp, user)
- if err != nil {
- err = fmt.Errorf("%s: parse response '%s': %w", errBase, string(resp), err)
- return
- }
- return
- }
|