Monorepo for Tangled
tangled.org
1package xrpc
2
3import (
4 "fmt"
5 "net/http"
6 "net/url"
7 "strings"
8
9 "github.com/go-git/go-git/v5/plumbing"
10
11 "tangled.org/core/api/tangled"
12 "tangled.org/core/knotserver/git"
13 xrpcerr "tangled.org/core/xrpc/errors"
14)
15
16func (x *Xrpc) RepoArchive(w http.ResponseWriter, r *http.Request) {
17 repo := r.URL.Query().Get("repo")
18 repoPath, err := x.parseRepoParam(repo)
19 if err != nil {
20 writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest)
21 return
22 }
23
24 ref := r.URL.Query().Get("ref")
25 // ref can be empty (git.Open handles this)
26
27 format := r.URL.Query().Get("format")
28 if format == "" {
29 format = "tar.gz" // default
30 }
31
32 prefix := r.URL.Query().Get("prefix")
33
34 if format != "tar.gz" && format != "zip" {
35 writeError(w, xrpcerr.NewXrpcError(
36 xrpcerr.WithTag("InvalidRequest"),
37 xrpcerr.WithMessage("only tar.gz and zip formats are supported"),
38 ), http.StatusBadRequest)
39 return
40 }
41
42 gr, err := git.Open(repoPath, ref)
43 if err != nil {
44 writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound)
45 return
46 }
47
48 repoParts := strings.Split(repo, "/")
49 repoName := repoParts[len(repoParts)-1]
50
51 immutableLink, err := x.buildImmutableLink(repo, format, gr.Hash().String(), prefix)
52 if err != nil {
53 x.Logger.Error(
54 "failed to build immutable link",
55 "err", err.Error(),
56 "repo", repo,
57 "format", format,
58 "ref", gr.Hash().String(),
59 "prefix", prefix,
60 )
61 }
62
63 safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-")
64
65 var archivePrefix string
66 if prefix != "" {
67 archivePrefix = prefix
68 } else {
69 archivePrefix = fmt.Sprintf("%s-%s", repoName, safeRefFilename)
70 }
71
72 filename := fmt.Sprintf("%s-%s.%s", repoName, safeRefFilename, format)
73 w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
74 w.Header().Set("Content-Type", archiveContentType(format))
75 w.Header().Set("Link", fmt.Sprintf("<%s>; rel=\"immutable\"", immutableLink))
76
77 err = gr.WriteArchive(w, format, archivePrefix)
78 if err != nil {
79 // once we start writing to the body we can't report error anymore
80 // so we are only left with logging the error
81 x.Logger.Error("writing archive", "error", err.Error(), "format", format)
82 return
83 }
84}
85
86func archiveContentType(format string) string {
87 if format == "zip" {
88 return "application/zip"
89 }
90 return "application/gzip"
91}
92
93func (x *Xrpc) buildImmutableLink(repo string, format string, ref string, prefix string) (string, error) {
94 scheme := "https"
95 if x.Config.Server.Dev {
96 scheme = "http"
97 }
98
99 u, err := url.Parse(scheme + "://" + x.Config.Server.Hostname + "/xrpc/" + tangled.RepoArchiveNSID)
100 if err != nil {
101 return "", err
102 }
103
104 params := url.Values{}
105 params.Set("repo", repo)
106 params.Set("format", format)
107 params.Set("ref", ref)
108 params.Set("prefix", prefix)
109
110 return fmt.Sprintf("%s?%s", u.String(), params.Encode()), nil
111}