Monorepo for Tangled
1package repo
2
3import (
4 "fmt"
5 "io"
6 "net/http"
7 "net/url"
8 "strings"
9
10 "github.com/go-chi/chi/v5"
11)
12
13func (rp *Repo) DownloadArchive(w http.ResponseWriter, r *http.Request) {
14 l := rp.logger.With("handler", "DownloadArchive")
15 ref := chi.URLParam(r, "ref")
16 ref, _ = url.PathUnescape(ref)
17 ref = strings.TrimSuffix(ref, ".tar.gz")
18 f, err := rp.repoResolver.Resolve(r)
19 if err != nil {
20 l.Error("failed to get repo and knot", "err", err)
21 return
22 }
23 scheme := "http"
24 if !rp.config.Core.Dev {
25 scheme = "https"
26 }
27 host := fmt.Sprintf("%s://%s", scheme, f.Knot)
28 didSlashRepo := f.DidSlashRepo()
29
30 // build the xrpc url
31 u, err := url.Parse(host)
32 if err != nil {
33 l.Error("failed to parse host URL", "err", err)
34 rp.pages.Error503(w)
35 return
36 }
37
38 u.Path = "/xrpc/sh.tangled.repo.archive"
39 query := url.Values{}
40 query.Set("format", "tar.gz")
41 query.Set("prefix", r.URL.Query().Get("prefix"))
42 query.Set("ref", ref)
43 query.Set("repo", didSlashRepo)
44 u.RawQuery = query.Encode()
45
46 xrpcURL := u.String()
47
48 // make the get request
49 resp, err := http.Get(xrpcURL)
50 if err != nil {
51 l.Error("failed to call XRPC repo.archive", "err", err)
52 rp.pages.Error503(w)
53 return
54 }
55
56 // pass through headers from upstream response
57 if contentDisposition := resp.Header.Get("Content-Disposition"); contentDisposition != "" {
58 w.Header().Set("Content-Disposition", contentDisposition)
59 }
60 if contentType := resp.Header.Get("Content-Type"); contentType != "" {
61 w.Header().Set("Content-Type", contentType)
62 }
63 if contentLength := resp.Header.Get("Content-Length"); contentLength != "" {
64 w.Header().Set("Content-Length", contentLength)
65 }
66 if link := resp.Header.Get("Link"); link != "" {
67 if resolvedRef, err := extractImmutableLink(link); err == nil {
68 newLink := fmt.Sprintf("<%s/%s/archive/%s.tar.gz>; rel=\"immutable\"",
69 rp.config.Core.BaseUrl(), f.DidSlashRepo(), resolvedRef)
70 w.Header().Set("Link", newLink)
71 }
72 }
73
74 // stream the archive data directly
75 if _, err := io.Copy(w, resp.Body); err != nil {
76 l.Error("failed to write response", "err", err)
77 }
78}
79
80func extractImmutableLink(linkHeader string) (string, error) {
81 trimmed := strings.TrimPrefix(linkHeader, "<")
82 trimmed = strings.TrimSuffix(trimmed, ">; rel=\"immutable\"")
83
84 parsedLink, err := url.Parse(trimmed)
85 if err != nil {
86 return "", err
87 }
88
89 resolvedRef := parsedLink.Query().Get("ref")
90 if resolvedRef == "" {
91 return "", fmt.Errorf("no ref found in link header")
92 }
93
94 return resolvedRef, nil
95}