gogs_client.go 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  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. "bytes"
  9. "context"
  10. "crypto/tls"
  11. "encoding/json"
  12. "fmt"
  13. "io"
  14. "net/http"
  15. "net/url"
  16. "strings"
  17. "time"
  18. dp "idio.link/go/depager/v2"
  19. )
  20. func NewGogsClient(
  21. ctx context.Context,
  22. accessToken string,
  23. ) *GogsClient {
  24. t := &http.Transport{
  25. TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
  26. TLSHandshakeTimeout: 15 * time.Second,
  27. IdleConnTimeout: 30 * time.Minute,
  28. ResponseHeaderTimeout: 180 * time.Second,
  29. ExpectContinueTimeout: 10 * time.Second,
  30. }
  31. c := &http.Client{
  32. Transport: t,
  33. Timeout: 30 * time.Minute,
  34. }
  35. baseURI, _ := url.Parse("https://git.idiolink.net/api/v1/")
  36. return &GogsClient{
  37. baseURI: baseURI,
  38. ctx: ctx,
  39. httpClient: c,
  40. retries: 3,
  41. retryWait: 10 * time.Second,
  42. token: accessToken,
  43. }
  44. }
  45. type GogsClient struct {
  46. ctx context.Context
  47. httpClient *http.Client
  48. baseURI *url.URL
  49. blockReqsBefore time.Time
  50. retries int
  51. retryWait time.Duration
  52. token string
  53. }
  54. func (c *GogsClient) expandResource(
  55. resource *url.URL,
  56. ) *url.URL {
  57. ex, err :=
  58. url.JoinPath(c.baseURI.String(), resource.EscapedPath())
  59. if err != nil {
  60. panic(fmt.Sprintf("BUG: gogs client: expand resource: failed to join path: '%s' + '%s': %v", c.baseURI, resource.RequestURI(), err))
  61. }
  62. next, err := url.Parse(ex)
  63. if err != nil {
  64. panic(fmt.Sprintf("BUG: gogs client: expand resource: failed to parse expanded resource '%s': %v", ex, err))
  65. }
  66. return next
  67. }
  68. func (c *GogsClient) sendRequestWithRetry(
  69. req *http.Request,
  70. ) (resp *http.Response, err error) {
  71. for i := 0; i < c.retries+1; i++ {
  72. resp, err = c.httpClient.Do(req)
  73. if err != nil {
  74. err = fmt.Errorf("send request with retry: %w", err)
  75. return
  76. }
  77. }
  78. if resp == nil {
  79. err = fmt.Errorf("send request with retry: unknown failure")
  80. return
  81. }
  82. return
  83. }
  84. func (c *GogsClient) request(
  85. method string,
  86. resource *url.URL,
  87. body io.Reader,
  88. ) (respHead http.Header, respBody []byte, err error) {
  89. req, err := http.NewRequestWithContext(
  90. c.ctx,
  91. method,
  92. resource.String(),
  93. body,
  94. )
  95. if err != nil {
  96. err = fmt.Errorf("request %v: %w", resource, err)
  97. return
  98. }
  99. req.Header.
  100. Add("Authorization", fmt.Sprintf("token %s", c.token))
  101. resp, err := c.sendRequestWithRetry(req)
  102. if err != nil {
  103. err = fmt.Errorf("request '%s': %+v: %w", req.URL, req, err)
  104. return
  105. }
  106. defer resp.Body.Close()
  107. respHead = resp.Header
  108. respBody, err = io.ReadAll(resp.Body)
  109. if err != nil {
  110. err = fmt.Errorf("request %v: failed to read response body: %w", resource, err)
  111. return
  112. }
  113. // Success response
  114. if 200 <= resp.StatusCode && resp.StatusCode <= 299 {
  115. return
  116. }
  117. err = fmt.Errorf("request %v: %s", resource, http.StatusText(resp.StatusCode))
  118. return
  119. }
  120. func (c *GogsClient) get(
  121. resource *url.URL,
  122. ) (http.Header, []byte, error) {
  123. return c.request(http.MethodGet, resource, nil)
  124. }
  125. func (c *GogsClient) post(
  126. resource *url.URL,
  127. body io.Reader,
  128. ) (http.Header, []byte, error) {
  129. return c.request(http.MethodPost, resource, body)
  130. }
  131. func newGogsSubclient[T any](
  132. c *GogsClient,
  133. resource *url.URL,
  134. ) *GogsSubclient[T] {
  135. expanded := c.expandResource(resource)
  136. return &GogsSubclient[T]{
  137. GogsClient: *c,
  138. uri: expanded,
  139. }
  140. }
  141. type GogsSubclient[T any] struct {
  142. GogsClient
  143. uri *url.URL
  144. }
  145. func (c *GogsSubclient[T]) NextPage(
  146. offset uint64,
  147. ) (page dp.Page[T], _ uint64, err error) {
  148. aggr := make([]T, 0, 32)
  149. head, body, err := c.get(c.uri)
  150. if err != nil {
  151. err = fmt.Errorf("gogs client: next page: %w", err)
  152. return
  153. }
  154. err = json.Unmarshal(body, &aggr)
  155. if err != nil {
  156. err = fmt.Errorf("gogs client: next page: unmarshal response: %w", err)
  157. return
  158. }
  159. if next := getLinkNext(head.Get("link")); next != "" {
  160. c.uri, err = url.Parse(next)
  161. if err != nil {
  162. err = fmt.Errorf("gogs client: next page: unable to parse next link '%s': %w", next, err)
  163. return
  164. }
  165. }
  166. page = GogsAggregate[T](aggr)
  167. return
  168. }
  169. func getLinkNext(link string) (next string) {
  170. // this could be made faster, but doesn't seem necessary
  171. // See https://www.w3.org/wiki/LinkHeader for details.
  172. // basic format: `<meta.rdf>; rel=meta, ...`
  173. before, _, found := strings.Cut(link, `rel="next"`)
  174. if !found {
  175. return
  176. }
  177. idx := strings.LastIndex(before, "<")
  178. if idx == -1 {
  179. return
  180. }
  181. r := strings.NewReader(before)
  182. _, err := r.ReadAt([]byte(next), int64(idx+1))
  183. if err != nil {
  184. return
  185. }
  186. parts := strings.Split(next, ">")
  187. if len(parts) != 2 {
  188. return
  189. }
  190. next = parts[0]
  191. return
  192. }
  193. type GogsAggregate[T any] []T
  194. func (a GogsAggregate[T]) Elems() []T {
  195. return a
  196. }
  197. type GogsRepo struct {
  198. Id int `json:"id"`
  199. Owner struct {
  200. Id int `json:"id"`
  201. Username string `json:"username"`
  202. FullName string `json:"full_name"`
  203. EMail string `json:"email"`
  204. AvatarURL string `json:"avatar_url"`
  205. } `json:"owner"`
  206. FullName string `json:"full_name"`
  207. CloneURL string `json:"clone_url"`
  208. HTMLURL string `json:"html_url"`
  209. SSHURL string `json:"ssh_url"`
  210. DefaultBranch string `json:"default_branch"`
  211. Private string `json:"private"`
  212. Fork string `json:"fork"`
  213. Permissions struct {
  214. Admin bool `json:"admin"`
  215. Push bool `json:"push"`
  216. Pull bool `json:"pull"`
  217. } `json:"permissions"`
  218. }
  219. func FetchGogsOrganizationRepos[T *GogsRepo](
  220. c *GogsClient,
  221. group string,
  222. ) dp.Pager[T] {
  223. resStrFmt := "/orgs/%s/repos"
  224. escaped := url.PathEscape(group)
  225. resStr := fmt.Sprintf(resStrFmt, escaped)
  226. resource, err := url.Parse(resStr)
  227. if err != nil { // should only occur in case of bugs
  228. panic(err)
  229. }
  230. return dp.NewPager[T](
  231. newGogsSubclient[T](c, resource),
  232. 100,
  233. )
  234. }
  235. type MigrateToGogsRepoInput struct {
  236. SrcURI string `query:"clone_addr"` // Required Remote Git address (HTTP/HTTPS URL or local path)
  237. SrcUsername string `query:"auth_username"` // Authorization username [for remote]
  238. SrcPassword string `query:"auth_password"` // Authorization password [for remote]
  239. TgtUID int `query:"uid"` // Required User ID who takes ownership of this repository
  240. TgtRepoName string `query:"repo_name"` // Required Repository name
  241. TgtRepoMirror bool `query:"mirror"` // Repository will be a mirror. Default is false
  242. TgtRepoPrivate bool `query:"private"` // Repository will be private. Default is false
  243. TgtRepoDescr string `query:"description"` // Repository description
  244. }
  245. func MigrateToGogsRepo(
  246. c *GogsClient,
  247. input *MigrateToGogsRepoInput,
  248. repo *GogsRepo,
  249. ) (err error) {
  250. errBase := "migrate to gogs repo"
  251. resource, err := url.Parse("/repos/migrate")
  252. if err != nil { // should only occur in case of bugs
  253. panic(err)
  254. }
  255. expanded := c.expandResource(resource)
  256. req, err := json.Marshal(input)
  257. if err != nil {
  258. err = fmt.Errorf("%s: marshal input '%#v': %w", errBase, string(req), err)
  259. return
  260. }
  261. _, resp, err := c.post(expanded, bytes.NewReader(req))
  262. if err != nil {
  263. err = fmt.Errorf("%s: post request '%s': %w", errBase, string(req), err)
  264. return
  265. }
  266. err = json.Unmarshal(resp, repo)
  267. if err != nil {
  268. err = fmt.Errorf("%s: parse response '%s': %w", errBase, string(resp), err)
  269. return
  270. }
  271. return
  272. }
  273. type GogsUser struct {
  274. Id int `json:"id"`
  275. Username string `json:"username"`
  276. FullName string `json:"full_name"`
  277. EMail string `json:"email"`
  278. AvatarURL string `json:"avatar_url"`
  279. }
  280. func FetchGogsUserAuthenticated(
  281. c *GogsClient,
  282. user *GogsUser,
  283. ) (err error) {
  284. errBase := "fetch gogs user authenticated"
  285. resource, err := url.Parse("/user")
  286. if err != nil { // should only occur in case of bugs
  287. err = fmt.Errorf("%s: %w", errBase, err)
  288. panic(err)
  289. }
  290. expanded := c.expandResource(resource)
  291. _, resp, err := c.get(expanded)
  292. if err != nil {
  293. err = fmt.Errorf("%s: get request '%s': %w", errBase, expanded, err)
  294. return
  295. }
  296. err = json.Unmarshal(resp, user)
  297. if err != nil {
  298. err = fmt.Errorf("%s: parse response '%s': %w", errBase, string(resp), err)
  299. return
  300. }
  301. return
  302. }