tangled
alpha
login
or
join now
back
round
6
view raw
appview,knotserver: immutable nix flakeref link header
#741
open
opened by
boltless.me
2 months ago
targeting
master
from
push-ptrrwwvnkmxq
Close: #231
Signed-off-by: Seongmin Lee
git@boltless.me
options
unified
split
Changed files
+270
-250
appview
repo
archive.go
router.go
state
git_http.go
proxy_knot.go
router.go
go.mod
go.sum
knotserver
archive.go
git
git.go
router.go
xrpc
repo_archive.go
xrpc.go
lexicons
repo
archive.json
nix
gomod2nix.toml
-49
appview/repo/archive.go
···
1
-
package repo
2
-
3
-
import (
4
-
"fmt"
5
-
"net/http"
6
-
"net/url"
7
-
"strings"
8
-
9
-
"tangled.org/core/api/tangled"
10
-
xrpcclient "tangled.org/core/appview/xrpcclient"
11
-
12
-
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
13
-
"github.com/go-chi/chi/v5"
14
-
"github.com/go-git/go-git/v5/plumbing"
15
-
)
16
-
17
-
func (rp *Repo) DownloadArchive(w http.ResponseWriter, r *http.Request) {
18
-
l := rp.logger.With("handler", "DownloadArchive")
19
-
ref := chi.URLParam(r, "ref")
20
-
ref, _ = url.PathUnescape(ref)
21
-
f, err := rp.repoResolver.Resolve(r)
22
-
if err != nil {
23
-
l.Error("failed to get repo and knot", "err", err)
24
-
return
25
-
}
26
-
scheme := "http"
27
-
if !rp.config.Core.Dev {
28
-
scheme = "https"
29
-
}
30
-
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
31
-
xrpcc := &indigoxrpc.Client{
32
-
Host: host,
33
-
}
34
-
didSlashRepo := f.DidSlashRepo()
35
-
archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", ref, didSlashRepo)
36
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
37
-
l.Error("failed to call XRPC repo.archive", "err", xrpcerr)
38
-
rp.pages.Error503(w)
39
-
return
40
-
}
41
-
// Set headers for file download, just pass along whatever the knot specifies
42
-
safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-")
43
-
filename := fmt.Sprintf("%s-%s.tar.gz", f.Name, safeRefFilename)
44
-
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
45
-
w.Header().Set("Content-Type", "application/gzip")
46
-
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(archiveBytes)))
47
-
// Write the archive data directly
48
-
w.Write(archiveBytes)
49
-
}
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
-4
appview/repo/router.go
···
40
r.Get("/blob/{ref}/*", rp.Blob)
41
r.Get("/raw/{ref}/*", rp.RepoBlobRaw)
42
43
-
// intentionally doesn't use /* as this isn't
44
-
// a file path
45
-
r.Get("/archive/{ref}", rp.DownloadArchive)
46
-
47
r.Route("/fork", func(r chi.Router) {
48
r.Use(middleware.AuthMiddleware(rp.oauth))
49
r.Get("/", rp.ForkRepo)
···
40
r.Get("/blob/{ref}/*", rp.Blob)
41
r.Get("/raw/{ref}/*", rp.RepoBlobRaw)
42
0
0
0
0
43
r.Route("/fork", func(r chi.Router) {
44
r.Use(middleware.AuthMiddleware(rp.oauth))
45
r.Get("/", rp.ForkRepo)
-114
appview/state/git_http.go
···
1
-
package state
2
-
3
-
import (
4
-
"fmt"
5
-
"io"
6
-
"maps"
7
-
"net/http"
8
-
9
-
"github.com/bluesky-social/indigo/atproto/identity"
10
-
"github.com/go-chi/chi/v5"
11
-
"tangled.org/core/appview/models"
12
-
)
13
-
14
-
func (s *State) InfoRefs(w http.ResponseWriter, r *http.Request) {
15
-
user := r.Context().Value("resolvedId").(identity.Identity)
16
-
repo := r.Context().Value("repo").(*models.Repo)
17
-
18
-
scheme := "https"
19
-
if s.config.Core.Dev {
20
-
scheme = "http"
21
-
}
22
-
23
-
targetURL := fmt.Sprintf("%s://%s/%s/%s/info/refs?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery)
24
-
s.proxyRequest(w, r, targetURL)
25
-
26
-
}
27
-
28
-
func (s *State) UploadArchive(w http.ResponseWriter, r *http.Request) {
29
-
user, ok := r.Context().Value("resolvedId").(identity.Identity)
30
-
if !ok {
31
-
http.Error(w, "failed to resolve user", http.StatusInternalServerError)
32
-
return
33
-
}
34
-
repo := r.Context().Value("repo").(*models.Repo)
35
-
36
-
scheme := "https"
37
-
if s.config.Core.Dev {
38
-
scheme = "http"
39
-
}
40
-
41
-
targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-archive?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery)
42
-
s.proxyRequest(w, r, targetURL)
43
-
}
44
-
45
-
func (s *State) UploadPack(w http.ResponseWriter, r *http.Request) {
46
-
user, ok := r.Context().Value("resolvedId").(identity.Identity)
47
-
if !ok {
48
-
http.Error(w, "failed to resolve user", http.StatusInternalServerError)
49
-
return
50
-
}
51
-
repo := r.Context().Value("repo").(*models.Repo)
52
-
53
-
scheme := "https"
54
-
if s.config.Core.Dev {
55
-
scheme = "http"
56
-
}
57
-
58
-
targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery)
59
-
s.proxyRequest(w, r, targetURL)
60
-
}
61
-
62
-
func (s *State) ReceivePack(w http.ResponseWriter, r *http.Request) {
63
-
user, ok := r.Context().Value("resolvedId").(identity.Identity)
64
-
if !ok {
65
-
http.Error(w, "failed to resolve user", http.StatusInternalServerError)
66
-
return
67
-
}
68
-
repo := r.Context().Value("repo").(*models.Repo)
69
-
70
-
scheme := "https"
71
-
if s.config.Core.Dev {
72
-
scheme = "http"
73
-
}
74
-
75
-
targetURL := fmt.Sprintf("%s://%s/%s/%s/git-receive-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery)
76
-
s.proxyRequest(w, r, targetURL)
77
-
}
78
-
79
-
func (s *State) proxyRequest(w http.ResponseWriter, r *http.Request, targetURL string) {
80
-
client := &http.Client{}
81
-
82
-
// Create new request
83
-
proxyReq, err := http.NewRequest(r.Method, targetURL, r.Body)
84
-
if err != nil {
85
-
http.Error(w, err.Error(), http.StatusInternalServerError)
86
-
return
87
-
}
88
-
89
-
// Copy original headers
90
-
proxyReq.Header = r.Header
91
-
92
-
repoOwnerHandle := chi.URLParam(r, "user")
93
-
proxyReq.Header.Add("x-tangled-repo-owner-handle", repoOwnerHandle)
94
-
95
-
// Execute request
96
-
resp, err := client.Do(proxyReq)
97
-
if err != nil {
98
-
http.Error(w, err.Error(), http.StatusInternalServerError)
99
-
return
100
-
}
101
-
defer resp.Body.Close()
102
-
103
-
// Copy response headers
104
-
maps.Copy(w.Header(), resp.Header)
105
-
106
-
// Set response status code
107
-
w.WriteHeader(resp.StatusCode)
108
-
109
-
// Copy response body
110
-
if _, err := io.Copy(w, resp.Body); err != nil {
111
-
http.Error(w, err.Error(), http.StatusInternalServerError)
112
-
return
113
-
}
114
-
}
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
+185
appview/state/proxy_knot.go
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
package state
2
+
3
+
import (
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
+
20
+
func (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
+
34
+
func (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
+
51
+
func (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
+
68
+
func (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
+
85
+
var knotVersionDownloadArchiveConstraint = version.MustConstraints(version.NewConstraint(">= 1.12.0-alpha"))
86
+
87
+
func (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
+
150
+
func (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
+
}
+3
-1
appview/state/router.go
···
104
r.Post("/git-upload-archive", s.UploadArchive)
105
r.Post("/git-upload-pack", s.UploadPack)
106
r.Post("/git-receive-pack", s.ReceivePack)
107
-
0
0
108
})
109
})
110
···
104
r.Post("/git-upload-archive", s.UploadArchive)
105
r.Post("/git-upload-pack", s.UploadPack)
106
r.Post("/git-receive-pack", s.ReceivePack)
107
+
// intentionally doesn't use /* as this isn't
108
+
// a file path
109
+
r.Get("/archive/{ref}", s.DownloadArchive)
110
})
111
})
112
+1
go.mod
···
131
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect
132
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
133
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
0
134
github.com/hashicorp/golang-lru v1.0.2 // indirect
135
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
136
github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
···
131
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect
132
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
133
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
134
+
github.com/hashicorp/go-version v1.8.0 // indirect
135
github.com/hashicorp/golang-lru v1.0.2 // indirect
136
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
137
github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
+2
go.sum
···
264
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
265
github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw=
266
github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw=
0
0
267
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
268
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
269
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
···
264
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
265
github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw=
266
github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw=
267
+
github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
268
+
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
269
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
270
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
271
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
+69
knotserver/archive.go
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
package knotserver
2
+
3
+
import (
4
+
"compress/gzip"
5
+
"fmt"
6
+
"net/http"
7
+
"strings"
8
+
9
+
securejoin "github.com/cyphar/filepath-securejoin"
10
+
"github.com/go-chi/chi/v5"
11
+
"github.com/go-git/go-git/v5/plumbing"
12
+
"tangled.org/core/knotserver/git"
13
+
)
14
+
15
+
func (h *Knot) Archive(w http.ResponseWriter, r *http.Request) {
16
+
var (
17
+
did = chi.URLParam(r, "did")
18
+
name = chi.URLParam(r, "name")
19
+
ref = chi.URLParam(r, "ref")
20
+
)
21
+
repo, err := securejoin.SecureJoin(did, name)
22
+
if err != nil {
23
+
gitError(w, "repository not found", http.StatusNotFound)
24
+
h.l.Error("git: failed to secure join repo path", "handler", "InfoRefs", "error", err)
25
+
return
26
+
}
27
+
28
+
repoPath, err := securejoin.SecureJoin(h.c.Repo.ScanPath, repo)
29
+
if err != nil {
30
+
gitError(w, "repository not found", http.StatusNotFound)
31
+
h.l.Error("git: failed to secure join repo path", "handler", "InfoRefs", "error", err)
32
+
return
33
+
}
34
+
35
+
gr, err := git.Open(repoPath, ref)
36
+
37
+
immutableLink := fmt.Sprintf(
38
+
"https://%s/%s/%s/archive/%s",
39
+
h.c.Server.Hostname,
40
+
did,
41
+
name,
42
+
gr.Hash(),
43
+
)
44
+
45
+
safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-")
46
+
filename := fmt.Sprintf("%s-%s.tar.gz", name, safeRefFilename)
47
+
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
48
+
w.Header().Set("Content-Type", "application/gzip")
49
+
w.Header().Set("Link", fmt.Sprintf("<%s>; rel=\"immutable\"", immutableLink))
50
+
51
+
gw := gzip.NewWriter(w)
52
+
defer gw.Close()
53
+
54
+
err = gr.WriteTar(gw, "")
55
+
if err != nil {
56
+
// once we start writing to the body we can't report error anymore
57
+
// so we are only left with logging the error
58
+
h.l.Error("writing tar file", "error", err)
59
+
return
60
+
}
61
+
62
+
err = gw.Flush()
63
+
if err != nil {
64
+
// once we start writing to the body we can't report error anymore
65
+
// so we are only left with logging the error
66
+
h.l.Error("flushing", "error", err.Error())
67
+
return
68
+
}
69
+
}
+4
knotserver/git/git.go
···
76
return &g, nil
77
}
78
0
0
0
0
79
// re-open a repository and update references
80
func (g *GitRepo) Refresh() error {
81
refreshed, err := PlainOpen(g.path)
···
76
return &g, nil
77
}
78
79
+
func (g *GitRepo) Hash() plumbing.Hash {
80
+
return g.h
81
+
}
82
+
83
// re-open a repository and update references
84
func (g *GitRepo) Refresh() error {
85
refreshed, err := PlainOpen(g.path)
+2
knotserver/router.go
···
85
r.Post("/git-upload-archive", h.UploadArchive)
86
r.Post("/git-upload-pack", h.UploadPack)
87
r.Post("/git-receive-pack", h.ReceivePack)
0
0
88
})
89
})
90
···
85
r.Post("/git-upload-archive", h.UploadArchive)
86
r.Post("/git-upload-pack", h.UploadPack)
87
r.Post("/git-receive-pack", h.ReceivePack)
88
+
// convenience routes
89
+
r.Get("/archive/{ref}", h.Archive)
90
})
91
})
92
-81
knotserver/xrpc/repo_archive.go
···
1
-
package xrpc
2
-
3
-
import (
4
-
"compress/gzip"
5
-
"fmt"
6
-
"net/http"
7
-
"strings"
8
-
9
-
"github.com/go-git/go-git/v5/plumbing"
10
-
11
-
"tangled.org/core/knotserver/git"
12
-
xrpcerr "tangled.org/core/xrpc/errors"
13
-
)
14
-
15
-
func (x *Xrpc) RepoArchive(w http.ResponseWriter, r *http.Request) {
16
-
repo := r.URL.Query().Get("repo")
17
-
repoPath, err := x.parseRepoParam(repo)
18
-
if err != nil {
19
-
writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest)
20
-
return
21
-
}
22
-
23
-
ref := r.URL.Query().Get("ref")
24
-
// ref can be empty (git.Open handles this)
25
-
26
-
format := r.URL.Query().Get("format")
27
-
if format == "" {
28
-
format = "tar.gz" // default
29
-
}
30
-
31
-
prefix := r.URL.Query().Get("prefix")
32
-
33
-
if format != "tar.gz" {
34
-
writeError(w, xrpcerr.NewXrpcError(
35
-
xrpcerr.WithTag("InvalidRequest"),
36
-
xrpcerr.WithMessage("only tar.gz format is supported"),
37
-
), http.StatusBadRequest)
38
-
return
39
-
}
40
-
41
-
gr, err := git.Open(repoPath, ref)
42
-
if err != nil {
43
-
writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound)
44
-
return
45
-
}
46
-
47
-
repoParts := strings.Split(repo, "/")
48
-
repoName := repoParts[len(repoParts)-1]
49
-
50
-
safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-")
51
-
52
-
var archivePrefix string
53
-
if prefix != "" {
54
-
archivePrefix = prefix
55
-
} else {
56
-
archivePrefix = fmt.Sprintf("%s-%s", repoName, safeRefFilename)
57
-
}
58
-
59
-
filename := fmt.Sprintf("%s-%s.tar.gz", repoName, safeRefFilename)
60
-
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
61
-
w.Header().Set("Content-Type", "application/gzip")
62
-
63
-
gw := gzip.NewWriter(w)
64
-
defer gw.Close()
65
-
66
-
err = gr.WriteTar(gw, archivePrefix)
67
-
if err != nil {
68
-
// once we start writing to the body we can't report error anymore
69
-
// so we are only left with logging the error
70
-
x.Logger.Error("writing tar file", "error", err.Error())
71
-
return
72
-
}
73
-
74
-
err = gw.Flush()
75
-
if err != nil {
76
-
// once we start writing to the body we can't report error anymore
77
-
// so we are only left with logging the error
78
-
x.Logger.Error("flushing", "error", err.Error())
79
-
return
80
-
}
81
-
}
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
-1
knotserver/xrpc/xrpc.go
···
64
r.Get("/"+tangled.RepoCompareNSID, x.RepoCompare)
65
r.Get("/"+tangled.RepoGetDefaultBranchNSID, x.RepoGetDefaultBranch)
66
r.Get("/"+tangled.RepoBranchNSID, x.RepoBranch)
67
-
r.Get("/"+tangled.RepoArchiveNSID, x.RepoArchive)
68
r.Get("/"+tangled.RepoLanguagesNSID, x.RepoLanguages)
69
70
// knot query endpoints (no auth required)
···
64
r.Get("/"+tangled.RepoCompareNSID, x.RepoCompare)
65
r.Get("/"+tangled.RepoGetDefaultBranchNSID, x.RepoGetDefaultBranch)
66
r.Get("/"+tangled.RepoBranchNSID, x.RepoBranch)
0
67
r.Get("/"+tangled.RepoLanguagesNSID, x.RepoLanguages)
68
69
// knot query endpoints (no auth required)
+1
lexicons/repo/archive.json
···
4
"defs": {
5
"main": {
6
"type": "query",
0
7
"parameters": {
8
"type": "params",
9
"required": ["repo", "ref"],
···
4
"defs": {
5
"main": {
6
"type": "query",
7
+
"description": "deprecated. use `/{did}/{reponame}/archive/{ref} endpoint instead",
8
"parameters": {
9
"type": "params",
10
"required": ["repo", "ref"],
+3
nix/gomod2nix.toml
···
304
[mod."github.com/hashicorp/go-sockaddr"]
305
version = "v1.0.7"
306
hash = "sha256-p6eDOrGzN1jMmT/F/f/VJMq0cKNFhUcEuVVwTE6vSrs="
0
0
0
307
[mod."github.com/hashicorp/golang-lru"]
308
version = "v1.0.2"
309
hash = "sha256-yy+5botc6T5wXgOe2mfNXJP3wr+MkVlUZ2JBkmmrA48="
···
304
[mod."github.com/hashicorp/go-sockaddr"]
305
version = "v1.0.7"
306
hash = "sha256-p6eDOrGzN1jMmT/F/f/VJMq0cKNFhUcEuVVwTE6vSrs="
307
+
[mod."github.com/hashicorp/go-version"]
308
+
version = "v1.8.0"
309
+
hash = "sha256-KXtqERmYrWdpqPCViWcHbe6jnuH7k16bvBIcuJuevj8="
310
[mod."github.com/hashicorp/golang-lru"]
311
version = "v1.0.2"
312
hash = "sha256-yy+5botc6T5wXgOe2mfNXJP3wr+MkVlUZ2JBkmmrA48="