gogs_client.go 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. /*
  2. * This Source Code Form is subject to the terms of the Mozilla Public
  3. * License, v. 2.0. If a copy of the MPL was not distributed with this
  4. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
  5. */
  6. package main
  7. import (
  8. "context"
  9. "crypto/tls"
  10. "encoding/json"
  11. "fmt"
  12. "io"
  13. "net/http"
  14. "net/url"
  15. "time"
  16. dp "idio.link/go/depager/v2"
  17. )
  18. func NewGogsClient(
  19. ctx context.Context,
  20. accessToken string,
  21. ) *GogsClient {
  22. t := &http.Transport{
  23. TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
  24. TLSHandshakeTimeout: 15 * time.Second,
  25. IdleConnTimeout: 30 * time.Minute,
  26. ResponseHeaderTimeout: 180 * time.Second,
  27. ExpectContinueTimeout: 10 * time.Second,
  28. }
  29. c := &http.Client{
  30. Transport: t,
  31. Timeout: 30 * time.Minute,
  32. }
  33. baseURI, _ := url.Parse("https://git.idiolink.net/api/v1/")
  34. return &GogsClient{
  35. baseURI: baseURI,
  36. ctx: ctx,
  37. httpClient: c,
  38. retries: 3,
  39. retryWait: 10 * time.Second,
  40. token: accessToken,
  41. }
  42. }
  43. type GogsClient struct {
  44. ctx context.Context
  45. httpClient *http.Client
  46. baseURI *url.URL
  47. blockReqsBefore time.Time
  48. retries int
  49. retryWait time.Duration
  50. token string
  51. }
  52. func (c *GogsClient) expandResource(
  53. resource *url.URL,
  54. ) *url.URL {
  55. ex, err :=
  56. url.JoinPath(c.baseURI.String(), resource.EscapedPath())
  57. if err != nil {
  58. panic(fmt.Sprintf("BUG: gogs client: expand resource: failed to join path: '%s' + '%s': %v", c.baseURI, resource.RequestURI(), err))
  59. }
  60. next, err := url.Parse(ex)
  61. if err != nil {
  62. panic(fmt.Sprintf("BUG: gogs client: expand resource: failed to parse expanded resource '%s': %v", ex, err))
  63. }
  64. return next
  65. }
  66. func (c *GogsClient) sendRequestWithRetry(
  67. req *http.Request,
  68. ) (resp *http.Response, err error) {
  69. for i := 0; i < c.retries+1; i++ {
  70. resp, err = c.httpClient.Do(req)
  71. if err != nil {
  72. err = fmt.Errorf("send request with retry: %w", err)
  73. return
  74. }
  75. }
  76. if resp == nil {
  77. err = fmt.Errorf("send request with retry: unknown failure")
  78. return
  79. }
  80. return
  81. }
  82. func (c *GogsClient) request(
  83. method string,
  84. resource *url.URL,
  85. body io.Reader,
  86. ) (respHead http.Header, respBody []byte, err error) {
  87. req, err := http.NewRequestWithContext(
  88. c.ctx,
  89. method,
  90. resource.String(),
  91. body,
  92. )
  93. if err != nil {
  94. err = fmt.Errorf("request %#v: %w", resource.String(), err)
  95. return
  96. }
  97. req.Header.
  98. Add("Authorization", fmt.Sprintf("token %s", c.token))
  99. resp, err := c.sendRequestWithRetry(req)
  100. if err != nil {
  101. err = fmt.Errorf("request %#v: %+v: %w", req.URL.String(), req, err)
  102. return
  103. }
  104. defer resp.Body.Close()
  105. respHead = resp.Header
  106. respBody, err = io.ReadAll(resp.Body)
  107. if err != nil {
  108. err = fmt.Errorf("request %v: failed to read response body: %w", resource, err)
  109. return
  110. }
  111. // Success response
  112. if 200 <= resp.StatusCode && resp.StatusCode <= 299 {
  113. return
  114. }
  115. err = fmt.Errorf("request %v: %s", resource, http.StatusText(resp.StatusCode))
  116. return
  117. }
  118. func (c *GogsClient) get(
  119. resource *url.URL,
  120. ) (http.Header, []byte, error) {
  121. return c.request(http.MethodGet, resource, nil)
  122. }
  123. func newGogsSubclient[T any](
  124. c *GogsClient,
  125. resource *url.URL,
  126. ) *GogsSubclient[T] {
  127. expanded := c.expandResource(resource)
  128. return &GogsSubclient[T]{
  129. GogsClient: *c,
  130. uri: expanded,
  131. }
  132. }
  133. type GogsSubclient[T any] struct {
  134. GogsClient
  135. uri *url.URL
  136. }
  137. func (c *GogsSubclient[T]) NextPage(
  138. offset uint64,
  139. ) (page dp.Page[T], _ uint64, err error) {
  140. aggr := make([]T, 0, 32)
  141. head, body, err := c.get(c.uri)
  142. if err != nil {
  143. err = fmt.Errorf("gogs client: next page: %w", err)
  144. return
  145. }
  146. err = json.Unmarshal(body, &aggr)
  147. if err != nil {
  148. err = fmt.Errorf("gogs client: next page: unmarshal response: %w", err)
  149. return
  150. }
  151. if next := getLinkNext(head.Get("link")); next != "" {
  152. c.uri, err = url.Parse(next)
  153. if err != nil {
  154. err = fmt.Errorf("gogs client: next page: unable to parse next link '%s': %w", next, err)
  155. return
  156. }
  157. }
  158. page = GogsAggregate[T](aggr)
  159. return
  160. }
  161. type GogsAggregate[T any] []T
  162. func (a GogsAggregate[T]) Elems() []T {
  163. return a
  164. }
  165. type GogsRepo struct {
  166. FullName string `json:"full_name"`
  167. CloneURL string `json:"clone_url"`
  168. HTMLURL string `json:"html_url"`
  169. DefBranch string `json:"default_branch"`
  170. }
  171. func (r *GogsRepo) Path() string {
  172. return r.FullName
  173. }
  174. func (r *GogsRepo) GitURI() string {
  175. return r.CloneURL
  176. }
  177. func (r *GogsRepo) WebURI() string {
  178. return r.HTMLURL
  179. }
  180. func (r *GogsRepo) DefaultBranch() string {
  181. return r.DefBranch
  182. }
  183. func FetchOrgRepos[T *GogsRepo](
  184. c *GogsClient,
  185. group string,
  186. ) dp.Pager[T] {
  187. resStrFmt := "/orgs/%s/repos"
  188. escaped := url.PathEscape(group)
  189. resStr := fmt.Sprintf(resStrFmt, escaped)
  190. resource, err := url.Parse(resStr)
  191. if err != nil { // should only occur in case of bugs
  192. panic(err)
  193. }
  194. return dp.NewPager[T](
  195. newGogsSubclient[T](c, resource),
  196. 100,
  197. )
  198. }
  199. type GogsTag struct {
  200. Name string `json:"name"`
  201. Commit struct {
  202. Timestamp string `json:"timestamp"`
  203. } `json:"commit"`
  204. }
  205. func ListTags[T *GogsTag](
  206. c *GogsClient,
  207. owner,
  208. repo string,
  209. ) dp.Pager[T] {
  210. resStrFmt := "/repos/%s/%s/tags"
  211. resStr := fmt.Sprintf(
  212. resStrFmt,
  213. url.PathEscape(owner),
  214. url.PathEscape(repo),
  215. )
  216. resource, err := url.Parse(resStr)
  217. if err != nil {
  218. panic(err)
  219. }
  220. return dp.NewPager[T](
  221. newGogsSubclient[T](c, resource),
  222. 100,
  223. )
  224. }
  225. type GogsArchive struct {
  226. Contents []byte
  227. }
  228. func DownloadArchive[T *GogsArchive](
  229. c *GogsClient,
  230. owner,
  231. repo,
  232. ref string,
  233. ) (*GogsArchive, error) {
  234. resStrFmt := "/repos/%s/%s/archive/%s%s"
  235. resStr := fmt.Sprintf(
  236. resStrFmt,
  237. url.PathEscape(owner),
  238. url.PathEscape(repo),
  239. url.PathEscape(ref),
  240. url.PathEscape(".zip"),
  241. )
  242. resource, err := url.Parse(resStr)
  243. if err != nil {
  244. panic(err)
  245. }
  246. expanded := c.expandResource(resource)
  247. _, body, err := c.get(expanded)
  248. if err != nil {
  249. return nil, fmt.Errorf("download gogs archive: %v", err)
  250. }
  251. return &GogsArchive{Contents: body}, nil
  252. }