Monorepo for Tangled
tangled.org
1package xrpc
2
3import (
4 "bytes"
5 "context"
6 "fmt"
7 "io"
8 "net/http"
9 "net/url"
10 "os/exec"
11 "strings"
12
13 "github.com/bluesky-social/indigo/atproto/atclient"
14 "github.com/bluesky-social/indigo/atproto/syntax"
15 "github.com/go-git/go-git/v5/plumbing"
16 "tangled.org/core/api/tangled"
17 "tangled.org/core/knotmirror/db"
18 "tangled.org/core/knotmirror/xrpc/gitea"
19)
20
21func (x *Xrpc) GetArchive(w http.ResponseWriter, r *http.Request) {
22 var (
23 repoQuery = r.URL.Query().Get("repo")
24 ref = r.URL.Query().Get("ref")
25 format = r.URL.Query().Get("format")
26 prefix = r.URL.Query().Get("prefix")
27 )
28
29 repo, err := syntax.ParseDID(repoQuery)
30 if err != nil {
31 writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo parameter invalid: %s", repoQuery)})
32 return
33 }
34
35 if format == "" {
36 format = "tar.gz"
37 }
38 if format != "tar.gz" && format != "zip" {
39 writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: "only tar.gz and zip formats are supported"})
40 return
41 }
42
43 l := x.logger.With("repo", repo, "ref", ref, "format", format, "prefix", prefix)
44 l.Debug("request")
45
46 ctx := r.Context()
47
48 repoPath, err := x.makeRepoPath(ctx, repo)
49 if err != nil {
50 l.Warn("local mirror failed, trying proxy", "err", err)
51 if x.proxyToKnot(w, r, repo) {
52 return
53 }
54 writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to resolve repo"})
55 return
56 }
57
58 rev := ref
59 if rev == "" {
60 rev = "HEAD"
61 }
62 commit, err := gitea.GetCommit(ctx, repoPath, rev)
63 if err != nil {
64 l.Warn("local mirror failed, trying proxy", "err", err)
65 if x.proxyToKnot(w, r, repo) {
66 return
67 }
68 writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to resolve ref"})
69 return
70 }
71
72 repoName, err := func() (string, error) {
73 r, err := db.GetRepoByRepoDid(ctx, x.db, repo)
74 if err != nil {
75 return "", err
76 }
77 if r == nil {
78 return "", fmt.Errorf("repo not found: %s", repo)
79 }
80 return r.Name, nil
81 }()
82 if err != nil {
83 l.Warn("local mirror failed, trying proxy", "err", err)
84 if x.proxyToKnot(w, r, repo) {
85 return
86 }
87 writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to retrieve repo name"})
88 return
89 }
90
91 safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-")
92 if safeRefFilename == "" {
93 safeRefFilename = commit.Hash.String()
94 }
95 immutableLink := func() string {
96 params := url.Values{}
97 params.Set("repo", repo.String())
98 params.Set("ref", commit.Hash.String())
99 params.Set("format", format)
100 params.Set("prefix", prefix)
101 return fmt.Sprintf("%s/xrpc/%s?%s", x.cfg.BaseUrl(), tangled.GitTempGetArchiveNSID, params.Encode())
102 }()
103
104 var archivePrefix string
105 if prefix != "" {
106 archivePrefix = prefix
107 } else {
108 archivePrefix = fmt.Sprintf("%s-%s", repoName, safeRefFilename)
109 }
110
111 filename := fmt.Sprintf("%s-%s.%s", repoName, safeRefFilename, format)
112 w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
113 w.Header().Set("Content-Type", archiveContentType(format))
114 w.Header().Set("Link", fmt.Sprintf("<%s>; rel=\"immutable\"", immutableLink))
115
116 if err := writeLocalArchive(ctx, w, repoPath, commit.Hash.String(), format, archivePrefix); err != nil {
117 l.Error("writing archive", "err", err.Error(), "format", format)
118 w.WriteHeader(http.StatusInternalServerError)
119 }
120}
121
122func archiveContentType(format string) string {
123 if format == "zip" {
124 return "application/zip"
125 }
126 return "application/gzip"
127}
128
129func writeLocalArchive(ctx context.Context, w io.Writer, repoPath, rev, format, prefix string) error {
130 args := []string{"-C", repoPath, "archive", "--format=" + format}
131 if prefix != "" {
132 args = append(args, "--prefix="+strings.TrimRight(prefix, "/")+"/")
133 }
134 args = append(args, rev)
135
136 cmd := exec.CommandContext(ctx, "git", args...)
137 cmd.Stdout = w
138 stderr := new(bytes.Buffer)
139 cmd.Stderr = stderr
140
141 if err := cmd.Run(); err != nil {
142 return fmt.Errorf("%w, stderr: %s", err, stderr.String())
143 }
144 return nil
145}