Monorepo for Tangled tangled.org
5

Configure Feed

Select the types of activity you want to include in your feed.

at icy/kyxspm 3.2 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 "tangled.org/core/api/tangled" 12) 13 14func (rp *Repo) DownloadArchive(w http.ResponseWriter, r *http.Request) { 15 l := rp.logger.With("handler", "DownloadArchive") 16 ref := chi.URLParam(r, "ref") 17 ref, _ = url.PathUnescape(ref) 18 format := r.URL.Query().Get("format") 19 ref, format = archiveRefAndFormat(ref, format, r.UserAgent()) 20 f, err := rp.repoResolver.Resolve(r) 21 if err != nil { 22 l.Error("failed to get repo and knot", "err", err) 23 return 24 } 25 26 // build the xrpc url 27 query := url.Values{} 28 query.Set("repo", f.RepoDid) 29 query.Set("ref", ref) 30 query.Set("format", format) 31 query.Set("prefix", r.URL.Query().Get("prefix")) 32 xrpcURL := fmt.Sprintf( 33 "%s/xrpc/%s?%s", 34 rp.config.KnotMirror.Url, 35 tangled.GitTempGetArchiveNSID, 36 query.Encode(), 37 ) 38 39 // make the get request 40 resp, err := http.Get(xrpcURL) 41 if err != nil { 42 l.Error("failed to call XRPC repo.archive", "err", err) 43 rp.pages.Error503(w) 44 return 45 } 46 defer resp.Body.Close() 47 48 w.Header().Set("Content-Type", archiveContentType(format)) 49 50 filename := "" 51 if cd := resp.Header.Get("Content-Disposition"); strings.HasPrefix(cd, "attachment;") { 52 filename = cd // knot has already set the attachment CD 53 } 54 if filename == "" { 55 filename = fmt.Sprintf("attachment; filename=\"%s-%s.%s\"", f.Name, ref, format) 56 } 57 w.Header().Set("Content-Disposition", filename) 58 w.Header().Set("X-Content-Type-Options", "nosniff") 59 60 if link := resp.Header.Get("Link"); link != "" { 61 if resolvedRef, err := extractImmutableLink(link); err == nil { 62 newLink := fmt.Sprintf("<%s/%s/archive/%s.%s>; rel=\"immutable\"", 63 rp.config.Core.BaseUrl(), f.RepoIdentifier(), resolvedRef, format) 64 w.Header().Set("Link", newLink) 65 } 66 } 67 68 // stream the archive data directly 69 if _, err := io.Copy(w, resp.Body); err != nil { 70 l.Error("failed to write response", "err", err) 71 } 72} 73 74func archiveRefAndFormat(ref string, requestedFormat string, userAgent string) (string, string) { 75 switch { 76 case strings.HasSuffix(ref, ".tar.gz"): 77 ref = strings.TrimSuffix(ref, ".tar.gz") 78 if requestedFormat == "" { 79 requestedFormat = "tar.gz" 80 } 81 case strings.HasSuffix(ref, ".zip"): 82 ref = strings.TrimSuffix(ref, ".zip") 83 if requestedFormat == "" { 84 requestedFormat = "zip" 85 } 86 } 87 88 switch requestedFormat { 89 case "zip", "tar.gz": 90 return ref, requestedFormat 91 default: 92 if prefersZipArchive(userAgent) { 93 return ref, "zip" 94 } 95 return ref, "tar.gz" 96 } 97} 98 99func prefersZipArchive(userAgent string) bool { 100 ua := strings.ToLower(userAgent) 101 return strings.Contains(ua, "windows") || strings.Contains(ua, "win64") || strings.Contains(ua, "win32") 102} 103 104func archiveContentType(format string) string { 105 if format == "zip" { 106 return "application/zip" 107 } 108 return "application/gzip" 109} 110 111func extractImmutableLink(linkHeader string) (string, error) { 112 trimmed := strings.TrimPrefix(linkHeader, "<") 113 trimmed = strings.TrimSuffix(trimmed, ">; rel=\"immutable\"") 114 115 parsedLink, err := url.Parse(trimmed) 116 if err != nil { 117 return "", err 118 } 119 120 resolvedRef := parsedLink.Query().Get("ref") 121 if resolvedRef == "" { 122 return "", fmt.Errorf("no ref found in link header") 123 } 124 125 return resolvedRef, nil 126}