This repository has no description
1package repo
2
3import (
4 "fmt"
5 "io"
6 "net/http"
7 "net/url"
8 "strings"
9 "time"
10
11 "github.com/go-chi/chi/v5"
12 "github.com/samber/lo"
13 "tangled.org/core/api/tangled"
14 "tangled.org/core/appview/models"
15 "tangled.org/core/gitutil"
16)
17
18const archiveRoute = "/archive/*"
19
20func newArchiveClient(headerTimeout time.Duration) *http.Client {
21 transport := http.DefaultTransport.(*http.Transport).Clone()
22 transport.ResponseHeaderTimeout = headerTimeout
23 return &http.Client{Transport: transport}
24}
25
26func (rp *Repo) DownloadArchive(w http.ResponseWriter, r *http.Request) {
27 l := rp.logger.With("handler", "DownloadArchive")
28 fail := func(status int) {
29 w.WriteHeader(status)
30 lo.Ternary(status == http.StatusServiceUnavailable, rp.pages.Error503, rp.pages.Error404)(w)
31 }
32
33 params, err := parseArchiveRequest(r)
34 if err != nil {
35 l.Warn("rejecting archive request", "err", err)
36 fail(http.StatusNotFound)
37 return
38 }
39
40 f, err := rp.repoResolver.Resolve(r)
41 if err != nil {
42 l.Error("failed to get repo and knot", "err", err)
43 fail(http.StatusNotFound)
44 return
45 }
46
47 name := gitutil.RepoName(f.Slug())
48 params.Prefix = params.Prefix.OrDefault(name, params.Rev)
49
50 // build the xrpc url
51 xrpcURL := fmt.Sprintf("%s/xrpc/%s?%s",
52 rp.config.KnotMirror.Url, tangled.GitTempGetArchiveNSID, params.Query(f.RepoDid).Encode())
53
54 // make the get request
55 req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, xrpcURL, nil)
56 if err != nil {
57 l.Error("failed to build XRPC repo.archive request", "err", err)
58 fail(http.StatusServiceUnavailable)
59 return
60 }
61 resp, err := rp.archiveClient.Do(req)
62 if err != nil {
63 l.Error("failed to call XRPC repo.archive", "err", err)
64 fail(http.StatusServiceUnavailable)
65 return
66 }
67 defer resp.Body.Close()
68
69 if resp.StatusCode != http.StatusOK {
70 l.Error("XRPC repo.archive failed", "status", resp.StatusCode, "ref", params.Rev)
71 overloaded := resp.StatusCode >= http.StatusInternalServerError || resp.StatusCode == http.StatusTooManyRequests
72 fail(lo.Ternary(overloaded, http.StatusServiceUnavailable, http.StatusNotFound))
73 return
74 }
75
76 params.SetHeaders(w.Header(), name)
77
78 if resolvedRev, err := gitutil.ParseImmutableLink(resp.Header.Get("Link")); err == nil {
79 w.Header().Set("Link", gitutil.ImmutableLink(rp.immutableArchiveURL(f, params.WithRev(resolvedRev))))
80 }
81
82 // stream the archive data directly
83 if _, err := io.Copy(w, resp.Body); err != nil {
84 l.Error("failed to write response", "err", err)
85 }
86}
87
88func parseArchiveRequest(r *http.Request) (gitutil.ArchiveParams, error) {
89 ref := chi.URLParam(r, "*")
90 if unescaped, err := url.PathUnescape(ref); err == nil && r.URL.RawPath != "" {
91 ref = unescaped
92 }
93
94 suffix, found := lo.Find(gitutil.ArchiveFormats, func(f gitutil.ArchiveFormat) bool {
95 return strings.HasSuffix(ref, "."+f.String())
96 })
97 if found {
98 ref = strings.TrimSuffix(ref, "."+suffix.String())
99 }
100
101 rev, err := gitutil.ParseRev(ref)
102 if err != nil {
103 return gitutil.ArchiveParams{}, err
104 }
105
106 query := r.URL.Query()
107 query.Del("ref")
108 query.Set("format", archiveFormat(query.Get("format"), suffix, r.UserAgent()).String())
109 params, err := gitutil.ParseArchiveParams(query)
110 if err != nil {
111 return gitutil.ArchiveParams{}, err
112 }
113 return params.WithRev(rev), nil
114}
115
116func archiveFormat(requested string, suffix gitutil.ArchiveFormat, userAgent string) gitutil.ArchiveFormat {
117 if format, err := gitutil.ParseArchiveFormat(requested); err == nil {
118 return format
119 }
120 if suffix != "" {
121 return suffix
122 }
123 ua := strings.ToLower(userAgent)
124 windows := lo.SomeBy([]string{"windows", "win64", "win32"}, func(s string) bool { return strings.Contains(ua, s) })
125 return lo.Ternary(windows, gitutil.ArchiveZip, gitutil.ArchiveTarGz)
126}
127
128func (rp *Repo) immutableArchiveURL(f *models.Repo, params gitutil.ArchiveParams) string {
129 return fmt.Sprintf("%s/%s/archive/%s.%s?%s",
130 rp.config.Core.BaseUrl(), f.RepoIdentifier(),
131 url.PathEscape(params.Rev.String()), params.Format,
132 url.Values{"prefix": {params.Prefix.String()}}.Encode())
133}