+63
-22
appview/pages/templates/layouts/fragments/topbar.html
+63
-22
appview/pages/templates/layouts/fragments/topbar.html
···
47
47
{{ end }}
48
48
49
49
{{ define "profileDropdown" }}
50
-
<details class="relative inline-block text-left nav-dropdown">
51
-
<summary class="cursor-pointer list-none flex items-center gap-1">
52
-
{{ $user := .Did }}
53
-
<img
54
-
src="{{ tinyAvatar $user }}"
55
-
alt=""
56
-
class="rounded-full h-6 w-6 border border-gray-300 dark:border-gray-700"
57
-
/>
58
-
<span class="hidden md:inline">{{ $user | resolve | truncateAt30 }}</span>
59
-
</summary>
60
-
<div class="absolute flex flex-col right-0 mt-4 p-4 rounded w-48 bg-white dark:bg-gray-800 dark:text-white border border-gray-200 dark:border-gray-700">
61
-
<a href="/{{ $user }}">profile</a>
62
-
<a href="/{{ $user }}?tab=repos">repositories</a>
63
-
<a href="/{{ $user }}?tab=strings">strings</a>
64
-
<a href="/settings">settings</a>
50
+
{{ $user := .Did }}
51
+
<button type="button" popovertarget="navigation-popover" class="site-navigation-dropdown-trigger" aria-label="Open site navigation dropdown">
52
+
<img
53
+
src="{{ tinyAvatar $user }}"
54
+
alt=""
55
+
class="rounded-full h-6 w-6 border border-gray-300 dark:border-gray-700"
56
+
/>
57
+
</button>
58
+
<div popover="auto" id="navigation-popover" class="site-navigation-popover shadow-md border border-gray-200 rounded p-2 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700">
59
+
<div class="flex gap-2 py-2">
60
+
<img
61
+
src="{{ tinyAvatar $user }}"
62
+
alt=""
63
+
class="rounded-full h-6 w-6 border border-gray-300 dark:border-gray-700"
64
+
/>
65
+
<p>{{ $user | resolve | truncateAt30 }}</p>
66
+
</div>
67
+
<hr class="h-1 w-full mb-1 mt-2 dark:border-gray-700" />
68
+
<ul id="navigation-menu-popover">
69
+
<li>
70
+
<a href="/{{ $user }}">
71
+
{{ i "user" "w-4 h-4" }}
72
+
<span>profile</span>
73
+
</a>
74
+
</li>
75
+
<li>
76
+
<a href="/{{ $user }}?tab=repos">
77
+
{{ i "book-marked" "w-4 h-4" }}
78
+
<span>repositories</span>
79
+
</a>
80
+
</li>
81
+
<li>
82
+
<a href="/{{ $user }}?tab=strings">
83
+
{{ i "spool" "w-4 h-4" }}
84
+
<span>strings</span>
85
+
</a>
86
+
</li>
87
+
<li>
88
+
<a href="/settings">
89
+
{{ i "settings" "w-4 h-4" }}
90
+
<span>settings</span>
91
+
</a>
92
+
</li>
93
+
<hr class="h-1 w-full mb-1 mt-2 dark:border-gray-700" />
94
+
<li>
65
95
<a href="#"
66
-
hx-post="/logout"
67
-
hx-swap="none"
68
-
class="text-red-400 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300">
69
-
logout
70
-
</a>
71
-
</div>
72
-
</details>
96
+
hx-post="/logout"
97
+
hx-swap="none"
98
+
class="text-red-400 flex gap-2 items-center hover:bg-red-50 hover:text-red-700 px-2 py-2 rounded-sm dark:hover:bg-red-700 dark:hover:text-red-50">
99
+
{{ i "arrow-right-from-line" "w-4 h-4" }}
100
+
<span>logout</span>
101
+
</a>
102
+
</li>
103
+
</ul>
104
+
</div>
73
105
74
106
<script>
75
107
document.addEventListener('click', function(event) {
···
80
112
}
81
113
});
82
114
});
115
+
116
+
const navigationPopoverLinks = document.querySelectorAll("#navigation-menu-popover li a");
117
+
const currentPageURL = window.location.href
118
+
navigationPopoverLinks.forEach(link => {
119
+
const navigationPopoverLinkURL = link.href
120
+
if (navigationPopoverLinkURL === currentPageURL) {
121
+
link.ariaCurrent = "page"
122
+
}
123
+
})
83
124
</script>
84
125
{{ end }}
+49
appview/repo/archive.go
+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
+
}
+4
appview/repo/router.go
+4
appview/repo/router.go
···
40
40
r.Get("/blob/{ref}/*", rp.Blob)
41
41
r.Get("/raw/{ref}/*", rp.RepoBlobRaw)
42
42
43
+
// intentionally doesn't use /* as this isn't
44
+
// a file path
45
+
r.Get("/archive/{ref}", rp.DownloadArchive)
46
+
43
47
r.Route("/fork", func(r chi.Router) {
44
48
r.Use(middleware.AuthMiddleware(rp.oauth))
45
49
r.Get("/", rp.ForkRepo)
+114
appview/state/git_http.go
+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
+
}
-185
appview/state/proxy_knot.go
-185
appview/state/proxy_knot.go
···
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
-
}
+1
-3
appview/state/router.go
+1
-3
appview/state/router.go
···
104
104
r.Post("/git-upload-archive", s.UploadArchive)
105
105
r.Post("/git-upload-pack", s.UploadPack)
106
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)
107
+
110
108
})
111
109
})
112
110
-1
go.mod
-1
go.mod
···
131
131
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect
132
132
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
133
133
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
134
-
github.com/hashicorp/go-version v1.8.0 // indirect
135
134
github.com/hashicorp/golang-lru v1.0.2 // indirect
136
135
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
137
136
github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
-2
go.sum
-2
go.sum
···
264
264
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
265
265
github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw=
266
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
267
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
270
268
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
271
269
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
+19
input.css
+19
input.css
···
89
89
@apply no-underline text-black hover:underline hover:text-gray-800 dark:text-white dark:hover:text-gray-300;
90
90
}
91
91
92
+
#navigation-menu-popover li:not(:last-of-type) a {
93
+
@apply flex gap-2 items-center px-2 pb-2 pt-1.5 rounded-sm hover:bg-green-50 hover:text-green-700 no-underline dark:hover:text-green-50 dark:hover:bg-green-700;
94
+
}
95
+
96
+
a[hx-post="/logout"] {
97
+
@apply no-underline;
98
+
}
99
+
92
100
label {
93
101
@apply block text-gray-900 text-sm font-bold py-2 uppercase dark:text-gray-100;
94
102
}
···
962
970
color: #f9fafb;
963
971
}
964
972
}
973
+
974
+
.site-navigation-dropdown-trigger {
975
+
anchor-name: --dropdown-trigger;
976
+
}
977
+
978
+
.site-navigation-popover {
979
+
margin: 0;
980
+
inset: auto;
981
+
position-anchor: --dropdown-trigger;
982
+
position-area: bottom left;
983
+
}
-69
knotserver/archive.go
-69
knotserver/archive.go
···
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
-4
knotserver/git/git.go
-2
knotserver/router.go
-2
knotserver/router.go
+81
knotserver/xrpc/repo_archive.go
+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
+
}
+1
knotserver/xrpc/xrpc.go
+1
knotserver/xrpc/xrpc.go
···
64
64
r.Get("/"+tangled.RepoCompareNSID, x.RepoCompare)
65
65
r.Get("/"+tangled.RepoGetDefaultBranchNSID, x.RepoGetDefaultBranch)
66
66
r.Get("/"+tangled.RepoBranchNSID, x.RepoBranch)
67
+
r.Get("/"+tangled.RepoArchiveNSID, x.RepoArchive)
67
68
r.Get("/"+tangled.RepoLanguagesNSID, x.RepoLanguages)
68
69
69
70
// knot query endpoints (no auth required)
-1
lexicons/repo/archive.json
-1
lexicons/repo/archive.json
-3
nix/gomod2nix.toml
-3
nix/gomod2nix.toml
···
304
304
[mod."github.com/hashicorp/go-sockaddr"]
305
305
version = "v1.0.7"
306
306
hash = "sha256-p6eDOrGzN1jMmT/F/f/VJMq0cKNFhUcEuVVwTE6vSrs="
307
-
[mod."github.com/hashicorp/go-version"]
308
-
version = "v1.8.0"
309
-
hash = "sha256-KXtqERmYrWdpqPCViWcHbe6jnuH7k16bvBIcuJuevj8="
310
307
[mod."github.com/hashicorp/golang-lru"]
311
308
version = "v1.0.2"
312
309
hash = "sha256-yy+5botc6T5wXgOe2mfNXJP3wr+MkVlUZ2JBkmmrA48="