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