this repo has no description
1package state
2
3import (
4 "fmt"
5 "io"
6 "maps"
7 "net/http"
8 "strings"
9
10 "github.com/bluesky-social/indigo/atproto/identity"
11 indigoxrpc "github.com/bluesky-social/indigo/xrpc"
12 "github.com/go-chi/chi/v5"
13 "github.com/go-git/go-git/v5/plumbing"
14 "github.com/hashicorp/go-version"
15 "tangled.org/core/api/tangled"
16 "tangled.org/core/appview/models"
17 xrpcclient "tangled.org/core/appview/xrpcclient"
18)
19
20func (s *State) InfoRefs(w http.ResponseWriter, r *http.Request) {
21 user := r.Context().Value("resolvedId").(identity.Identity)
22 repo := r.Context().Value("repo").(*models.Repo)
23
24 scheme := "https"
25 if s.config.Core.Dev {
26 scheme = "http"
27 }
28
29 targetURL := fmt.Sprintf("%s://%s/%s/%s/info/refs?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery)
30 s.proxyRequest(w, r, targetURL)
31
32}
33
34func (s *State) UploadArchive(w http.ResponseWriter, r *http.Request) {
35 user, ok := r.Context().Value("resolvedId").(identity.Identity)
36 if !ok {
37 http.Error(w, "failed to resolve user", http.StatusInternalServerError)
38 return
39 }
40 repo := r.Context().Value("repo").(*models.Repo)
41
42 scheme := "https"
43 if s.config.Core.Dev {
44 scheme = "http"
45 }
46
47 targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-archive?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery)
48 s.proxyRequest(w, r, targetURL)
49}
50
51func (s *State) UploadPack(w http.ResponseWriter, r *http.Request) {
52 user, ok := r.Context().Value("resolvedId").(identity.Identity)
53 if !ok {
54 http.Error(w, "failed to resolve user", http.StatusInternalServerError)
55 return
56 }
57 repo := r.Context().Value("repo").(*models.Repo)
58
59 scheme := "https"
60 if s.config.Core.Dev {
61 scheme = "http"
62 }
63
64 targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery)
65 s.proxyRequest(w, r, targetURL)
66}
67
68func (s *State) ReceivePack(w http.ResponseWriter, r *http.Request) {
69 user, ok := r.Context().Value("resolvedId").(identity.Identity)
70 if !ok {
71 http.Error(w, "failed to resolve user", http.StatusInternalServerError)
72 return
73 }
74 repo := r.Context().Value("repo").(*models.Repo)
75
76 scheme := "https"
77 if s.config.Core.Dev {
78 scheme = "http"
79 }
80
81 targetURL := fmt.Sprintf("%s://%s/%s/%s/git-receive-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery)
82 s.proxyRequest(w, r, targetURL)
83}
84
85var knotVersionDownloadArchiveConstraint = version.MustConstraints(version.NewConstraint(">= 1.12.0-alpha"))
86
87func (s *State) DownloadArchive(w http.ResponseWriter, r *http.Request) {
88 l := s.logger.With("handler", "DownloadArchive")
89 ref := chi.URLParam(r, "ref")
90
91 user, ok := r.Context().Value("resolvedId").(identity.Identity)
92 if !ok {
93 l.Error("failed to resolve user")
94 http.Error(w, "failed to resolve user", http.StatusInternalServerError)
95 return
96 }
97 repo := r.Context().Value("repo").(*models.Repo)
98
99 scheme := "https"
100 if s.config.Core.Dev {
101 scheme = "http"
102 }
103
104 host := fmt.Sprintf("%s://%s", scheme, repo.Knot)
105 xrpcc := &indigoxrpc.Client{
106 Host: host,
107 }
108 l = l.With("knot", repo.Knot)
109
110 isCompatible := func() bool {
111 out, err := tangled.KnotVersion(r.Context(), xrpcc)
112 if err != nil {
113 l.Warn("failed to get knot version", "err", err)
114 return false
115 }
116
117 v, err := version.NewVersion(out.Version)
118 if err != nil {
119 l.Warn("failed to parse knot version", "version", out.Version, "err", err)
120 return false
121 }
122
123 if !knotVersionDownloadArchiveConstraint.Check(v) {
124 l.Warn("knot version incompatible.", "version", v)
125 return false
126 }
127 return true
128 }()
129 l.Debug("knot compatibility check", "isCompatible", isCompatible)
130 if isCompatible {
131 targetURL := fmt.Sprintf("%s://%s/%s/%s/archive/%s", scheme, repo.Knot, user.DID, repo.Name, ref)
132 s.proxyRequest(w, r, targetURL)
133 } else {
134 l.Debug("requesting xrpc/sh.tangled.repo.archive")
135 archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", ref, repo.DidSlashRepo())
136 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
137 l.Error("failed to call XRPC repo.archive", "err", xrpcerr)
138 s.pages.Error503(w)
139 return
140 }
141 safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-")
142 filename := fmt.Sprintf("%s-%s.tar.gz", repo.Name, safeRefFilename)
143 w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
144 w.Header().Set("Content-Type", "application/gzip")
145 w.Header().Set("Content-Length", fmt.Sprintf("%d", len(archiveBytes)))
146 w.Write(archiveBytes)
147 }
148}
149
150func (s *State) proxyRequest(w http.ResponseWriter, r *http.Request, targetURL string) {
151 client := &http.Client{}
152
153 // Create new request
154 proxyReq, err := http.NewRequest(r.Method, targetURL, r.Body)
155 if err != nil {
156 http.Error(w, err.Error(), http.StatusInternalServerError)
157 return
158 }
159
160 // Copy original headers
161 proxyReq.Header = r.Header
162
163 repoOwnerHandle := chi.URLParam(r, "user")
164 proxyReq.Header.Add("x-tangled-repo-owner-handle", repoOwnerHandle)
165
166 // Execute request
167 resp, err := client.Do(proxyReq)
168 if err != nil {
169 http.Error(w, err.Error(), http.StatusInternalServerError)
170 return
171 }
172 defer resp.Body.Close()
173
174 // Copy response headers
175 maps.Copy(w.Header(), resp.Header)
176
177 // Set response status code
178 w.WriteHeader(resp.StatusCode)
179
180 // Copy response body
181 if _, err := io.Copy(w, resp.Body); err != nil {
182 http.Error(w, err.Error(), http.StatusInternalServerError)
183 return
184 }
185}