Monorepo for Tangled
at f6ac2d9789063b566b9b50795c2edf1a72f33172 95 lines 2.5 kB view raw
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}