tangled
alpha
login
or
join now
back
round
3
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
+251
-353
api
tangled
repoarchive.go
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
-41
api/tangled/repoarchive.go
···
1
1
-
// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
2
2
-
3
3
-
package tangled
4
4
-
5
5
-
// schema: sh.tangled.repo.archive
6
6
-
7
7
-
import (
8
8
-
"bytes"
9
9
-
"context"
10
10
-
11
11
-
"github.com/bluesky-social/indigo/lex/util"
12
12
-
)
13
13
-
14
14
-
const (
15
15
-
RepoArchiveNSID = "sh.tangled.repo.archive"
16
16
-
)
17
17
-
18
18
-
// RepoArchive calls the XRPC method "sh.tangled.repo.archive".
19
19
-
//
20
20
-
// format: Archive format
21
21
-
// prefix: Prefix for files in the archive
22
22
-
// ref: Git reference (branch, tag, or commit SHA)
23
23
-
// repo: Repository identifier in format 'did:plc:.../repoName'
24
24
-
func RepoArchive(ctx context.Context, c util.LexClient, format string, prefix string, ref string, repo string) ([]byte, error) {
25
25
-
buf := new(bytes.Buffer)
26
26
-
27
27
-
params := map[string]interface{}{}
28
28
-
if format != "" {
29
29
-
params["format"] = format
30
30
-
}
31
31
-
if prefix != "" {
32
32
-
params["prefix"] = prefix
33
33
-
}
34
34
-
params["ref"] = ref
35
35
-
params["repo"] = repo
36
36
-
if err := c.LexDo(ctx, util.Query, "", "sh.tangled.repo.archive", params, nil, buf); err != nil {
37
37
-
return nil, err
38
38
-
}
39
39
-
40
40
-
return buf.Bytes(), nil
41
41
-
}
-49
appview/repo/archive.go
···
1
1
-
package repo
2
2
-
3
3
-
import (
4
4
-
"fmt"
5
5
-
"net/http"
6
6
-
"net/url"
7
7
-
"strings"
8
8
-
9
9
-
"tangled.org/core/api/tangled"
10
10
-
xrpcclient "tangled.org/core/appview/xrpcclient"
11
11
-
12
12
-
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
13
13
-
"github.com/go-chi/chi/v5"
14
14
-
"github.com/go-git/go-git/v5/plumbing"
15
15
-
)
16
16
-
17
17
-
func (rp *Repo) DownloadArchive(w http.ResponseWriter, r *http.Request) {
18
18
-
l := rp.logger.With("handler", "DownloadArchive")
19
19
-
ref := chi.URLParam(r, "ref")
20
20
-
ref, _ = url.PathUnescape(ref)
21
21
-
f, err := rp.repoResolver.Resolve(r)
22
22
-
if err != nil {
23
23
-
l.Error("failed to get repo and knot", "err", err)
24
24
-
return
25
25
-
}
26
26
-
scheme := "http"
27
27
-
if !rp.config.Core.Dev {
28
28
-
scheme = "https"
29
29
-
}
30
30
-
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
31
31
-
xrpcc := &indigoxrpc.Client{
32
32
-
Host: host,
33
33
-
}
34
34
-
didSlashRepo := f.DidSlashRepo()
35
35
-
archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", ref, didSlashRepo)
36
36
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
37
37
-
l.Error("failed to call XRPC repo.archive", "err", xrpcerr)
38
38
-
rp.pages.Error503(w)
39
39
-
return
40
40
-
}
41
41
-
// Set headers for file download, just pass along whatever the knot specifies
42
42
-
safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-")
43
43
-
filename := fmt.Sprintf("%s-%s.tar.gz", f.Name, safeRefFilename)
44
44
-
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
45
45
-
w.Header().Set("Content-Type", "application/gzip")
46
46
-
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(archiveBytes)))
47
47
-
// Write the archive data directly
48
48
-
w.Write(archiveBytes)
49
49
-
}
-4
appview/repo/router.go
···
40
40
r.Get("/blob/{ref}/*", rp.Blob)
41
41
r.Get("/raw/{ref}/*", rp.RepoBlobRaw)
42
42
43
43
-
// intentionally doesn't use /* as this isn't
44
44
-
// a file path
45
45
-
r.Get("/archive/{ref}", rp.DownloadArchive)
46
46
-
47
43
r.Route("/fork", func(r chi.Router) {
48
44
r.Use(middleware.AuthMiddleware(rp.oauth))
49
45
r.Get("/", rp.ForkRepo)
-97
appview/state/git_http.go
···
1
1
-
package state
2
2
-
3
3
-
import (
4
4
-
"fmt"
5
5
-
"io"
6
6
-
"maps"
7
7
-
"net/http"
8
8
-
9
9
-
"github.com/bluesky-social/indigo/atproto/identity"
10
10
-
"github.com/go-chi/chi/v5"
11
11
-
"tangled.org/core/appview/models"
12
12
-
)
13
13
-
14
14
-
func (s *State) InfoRefs(w http.ResponseWriter, r *http.Request) {
15
15
-
user := r.Context().Value("resolvedId").(identity.Identity)
16
16
-
repo := r.Context().Value("repo").(*models.Repo)
17
17
-
18
18
-
scheme := "https"
19
19
-
if s.config.Core.Dev {
20
20
-
scheme = "http"
21
21
-
}
22
22
-
23
23
-
targetURL := fmt.Sprintf("%s://%s/%s/%s/info/refs?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery)
24
24
-
s.proxyRequest(w, r, targetURL)
25
25
-
26
26
-
}
27
27
-
28
28
-
func (s *State) UploadPack(w http.ResponseWriter, r *http.Request) {
29
29
-
user, ok := r.Context().Value("resolvedId").(identity.Identity)
30
30
-
if !ok {
31
31
-
http.Error(w, "failed to resolve user", http.StatusInternalServerError)
32
32
-
return
33
33
-
}
34
34
-
repo := r.Context().Value("repo").(*models.Repo)
35
35
-
36
36
-
scheme := "https"
37
37
-
if s.config.Core.Dev {
38
38
-
scheme = "http"
39
39
-
}
40
40
-
41
41
-
targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery)
42
42
-
s.proxyRequest(w, r, targetURL)
43
43
-
}
44
44
-
45
45
-
func (s *State) ReceivePack(w http.ResponseWriter, r *http.Request) {
46
46
-
user, ok := r.Context().Value("resolvedId").(identity.Identity)
47
47
-
if !ok {
48
48
-
http.Error(w, "failed to resolve user", http.StatusInternalServerError)
49
49
-
return
50
50
-
}
51
51
-
repo := r.Context().Value("repo").(*models.Repo)
52
52
-
53
53
-
scheme := "https"
54
54
-
if s.config.Core.Dev {
55
55
-
scheme = "http"
56
56
-
}
57
57
-
58
58
-
targetURL := fmt.Sprintf("%s://%s/%s/%s/git-receive-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery)
59
59
-
s.proxyRequest(w, r, targetURL)
60
60
-
}
61
61
-
62
62
-
func (s *State) proxyRequest(w http.ResponseWriter, r *http.Request, targetURL string) {
63
63
-
client := &http.Client{}
64
64
-
65
65
-
// Create new request
66
66
-
proxyReq, err := http.NewRequest(r.Method, targetURL, r.Body)
67
67
-
if err != nil {
68
68
-
http.Error(w, err.Error(), http.StatusInternalServerError)
69
69
-
return
70
70
-
}
71
71
-
72
72
-
// Copy original headers
73
73
-
proxyReq.Header = r.Header
74
74
-
75
75
-
repoOwnerHandle := chi.URLParam(r, "user")
76
76
-
proxyReq.Header.Add("x-tangled-repo-owner-handle", repoOwnerHandle)
77
77
-
78
78
-
// Execute request
79
79
-
resp, err := client.Do(proxyReq)
80
80
-
if err != nil {
81
81
-
http.Error(w, err.Error(), http.StatusInternalServerError)
82
82
-
return
83
83
-
}
84
84
-
defer resp.Body.Close()
85
85
-
86
86
-
// Copy response headers
87
87
-
maps.Copy(w.Header(), resp.Header)
88
88
-
89
89
-
// Set response status code
90
90
-
w.WriteHeader(resp.StatusCode)
91
91
-
92
92
-
// Copy response body
93
93
-
if _, err := io.Copy(w, resp.Body); err != nil {
94
94
-
http.Error(w, err.Error(), http.StatusInternalServerError)
95
95
-
return
96
96
-
}
97
97
-
}
+167
appview/state/proxy_knot.go
···
1
1
+
package state
2
2
+
3
3
+
import (
4
4
+
"fmt"
5
5
+
"io"
6
6
+
"maps"
7
7
+
"net/http"
8
8
+
"strings"
9
9
+
10
10
+
"github.com/bluesky-social/indigo/atproto/identity"
11
11
+
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
12
12
+
"github.com/go-chi/chi/v5"
13
13
+
"github.com/go-git/go-git/v5/plumbing"
14
14
+
"github.com/hashicorp/go-version"
15
15
+
"tangled.org/core/api/tangled"
16
16
+
"tangled.org/core/appview/models"
17
17
+
xrpcclient "tangled.org/core/appview/xrpcclient"
18
18
+
)
19
19
+
20
20
+
func (s *State) InfoRefs(w http.ResponseWriter, r *http.Request) {
21
21
+
user := r.Context().Value("resolvedId").(identity.Identity)
22
22
+
repo := r.Context().Value("repo").(*models.Repo)
23
23
+
24
24
+
scheme := "https"
25
25
+
if s.config.Core.Dev {
26
26
+
scheme = "http"
27
27
+
}
28
28
+
29
29
+
targetURL := fmt.Sprintf("%s://%s/%s/%s/info/refs?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery)
30
30
+
s.proxyRequest(w, r, targetURL)
31
31
+
32
32
+
}
33
33
+
34
34
+
func (s *State) UploadPack(w http.ResponseWriter, r *http.Request) {
35
35
+
user, ok := r.Context().Value("resolvedId").(identity.Identity)
36
36
+
if !ok {
37
37
+
http.Error(w, "failed to resolve user", http.StatusInternalServerError)
38
38
+
return
39
39
+
}
40
40
+
repo := r.Context().Value("repo").(*models.Repo)
41
41
+
42
42
+
scheme := "https"
43
43
+
if s.config.Core.Dev {
44
44
+
scheme = "http"
45
45
+
}
46
46
+
47
47
+
targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery)
48
48
+
s.proxyRequest(w, r, targetURL)
49
49
+
}
50
50
+
51
51
+
func (s *State) ReceivePack(w http.ResponseWriter, r *http.Request) {
52
52
+
user, ok := r.Context().Value("resolvedId").(identity.Identity)
53
53
+
if !ok {
54
54
+
http.Error(w, "failed to resolve user", http.StatusInternalServerError)
55
55
+
return
56
56
+
}
57
57
+
repo := r.Context().Value("repo").(*models.Repo)
58
58
+
59
59
+
scheme := "https"
60
60
+
if s.config.Core.Dev {
61
61
+
scheme = "http"
62
62
+
}
63
63
+
64
64
+
targetURL := fmt.Sprintf("%s://%s/%s/%s/git-receive-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery)
65
65
+
s.proxyRequest(w, r, targetURL)
66
66
+
}
67
67
+
68
68
+
var knotVersionDownloadArchiveConstraint = version.MustConstraints(version.NewConstraint(">= 1.12"))
69
69
+
70
70
+
func (s *State) DownloadArchive(w http.ResponseWriter, r *http.Request) {
71
71
+
l := s.logger.With("handler", "DownloadArchive")
72
72
+
ref := chi.URLParam(r, "ref")
73
73
+
74
74
+
user, ok := r.Context().Value("resolvedId").(identity.Identity)
75
75
+
if !ok {
76
76
+
l.Error("failed to resolve user")
77
77
+
http.Error(w, "failed to resolve user", http.StatusInternalServerError)
78
78
+
return
79
79
+
}
80
80
+
repo := r.Context().Value("repo").(*models.Repo)
81
81
+
82
82
+
scheme := "https"
83
83
+
if s.config.Core.Dev {
84
84
+
scheme = "http"
85
85
+
}
86
86
+
87
87
+
xrpcc := &indigoxrpc.Client{
88
88
+
Host: repo.Knot,
89
89
+
}
90
90
+
l = l.With("knot", repo.Knot)
91
91
+
92
92
+
isCompatible := func() bool {
93
93
+
out, err := tangled.KnotVersion(r.Context(), xrpcc)
94
94
+
if err != nil {
95
95
+
l.Warn("failed to get knot version", "err", err)
96
96
+
return false
97
97
+
}
98
98
+
99
99
+
v, err := version.NewVersion(out.Version)
100
100
+
if err != nil {
101
101
+
l.Warn("failed to parse knot version", "version", out.Version, "err", err)
102
102
+
return false
103
103
+
}
104
104
+
105
105
+
if !knotVersionDownloadArchiveConstraint.Check(v) {
106
106
+
l.Warn("knot version incompatible.", "version", v)
107
107
+
return false
108
108
+
}
109
109
+
return true
110
110
+
}()
111
111
+
l.Debug("knot compatibility check", "isCompatible", isCompatible)
112
112
+
if isCompatible {
113
113
+
targetURL := fmt.Sprintf("%s://%s/%s/%s/archive/%s", scheme, repo.Knot, user.DID, repo.Name, ref)
114
114
+
s.proxyRequest(w, r, targetURL)
115
115
+
} else {
116
116
+
l.Debug("requesting xrpc/sh.tangled.repo.archive")
117
117
+
archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", ref, repo.DidSlashRepo())
118
118
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
119
119
+
l.Error("failed to call XRPC repo.archive", "err", xrpcerr)
120
120
+
s.pages.Error503(w)
121
121
+
return
122
122
+
}
123
123
+
safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-")
124
124
+
filename := fmt.Sprintf("%s-%s.tar.gz", repo.Name, safeRefFilename)
125
125
+
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
126
126
+
w.Header().Set("Content-Type", "application/gzip")
127
127
+
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(archiveBytes)))
128
128
+
w.Write(archiveBytes)
129
129
+
}
130
130
+
}
131
131
+
132
132
+
func (s *State) proxyRequest(w http.ResponseWriter, r *http.Request, targetURL string) {
133
133
+
client := &http.Client{}
134
134
+
135
135
+
// Create new request
136
136
+
proxyReq, err := http.NewRequest(r.Method, targetURL, r.Body)
137
137
+
if err != nil {
138
138
+
http.Error(w, err.Error(), http.StatusInternalServerError)
139
139
+
return
140
140
+
}
141
141
+
142
142
+
// Copy original headers
143
143
+
proxyReq.Header = r.Header
144
144
+
145
145
+
repoOwnerHandle := chi.URLParam(r, "user")
146
146
+
proxyReq.Header.Add("x-tangled-repo-owner-handle", repoOwnerHandle)
147
147
+
148
148
+
// Execute request
149
149
+
resp, err := client.Do(proxyReq)
150
150
+
if err != nil {
151
151
+
http.Error(w, err.Error(), http.StatusInternalServerError)
152
152
+
return
153
153
+
}
154
154
+
defer resp.Body.Close()
155
155
+
156
156
+
// Copy response headers
157
157
+
maps.Copy(w.Header(), resp.Header)
158
158
+
159
159
+
// Set response status code
160
160
+
w.WriteHeader(resp.StatusCode)
161
161
+
162
162
+
// Copy response body
163
163
+
if _, err := io.Copy(w, resp.Body); err != nil {
164
164
+
http.Error(w, err.Error(), http.StatusInternalServerError)
165
165
+
return
166
166
+
}
167
167
+
}
+3
-1
appview/state/router.go
···
103
103
r.Get("/info/refs", s.InfoRefs)
104
104
r.Post("/git-upload-pack", s.UploadPack)
105
105
r.Post("/git-receive-pack", s.ReceivePack)
106
106
-
106
106
+
// intentionally doesn't use /* as this isn't
107
107
+
// a file path
108
108
+
r.Get("/archive/{ref}", s.DownloadArchive)
107
109
})
108
110
})
109
111
+1
go.mod
···
132
132
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect
133
133
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
134
134
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
135
135
+
github.com/hashicorp/go-version v1.8.0 // indirect
135
136
github.com/hashicorp/golang-lru v1.0.2 // indirect
136
137
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
137
138
github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
+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
267
+
github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
268
268
+
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
267
269
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
268
270
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
269
271
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
+69
knotserver/archive.go
···
1
1
+
package knotserver
2
2
+
3
3
+
import (
4
4
+
"compress/gzip"
5
5
+
"fmt"
6
6
+
"net/http"
7
7
+
"strings"
8
8
+
9
9
+
securejoin "github.com/cyphar/filepath-securejoin"
10
10
+
"github.com/go-chi/chi/v5"
11
11
+
"github.com/go-git/go-git/v5/plumbing"
12
12
+
"tangled.org/core/knotserver/git"
13
13
+
)
14
14
+
15
15
+
func (h *Knot) Archive(w http.ResponseWriter, r *http.Request) {
16
16
+
var (
17
17
+
did = chi.URLParam(r, "did")
18
18
+
name = chi.URLParam(r, "name")
19
19
+
ref = chi.URLParam(r, "ref")
20
20
+
)
21
21
+
repo, err := securejoin.SecureJoin(did, name)
22
22
+
if err != nil {
23
23
+
gitError(w, "repository not found", http.StatusNotFound)
24
24
+
h.l.Error("git: failed to secure join repo path", "handler", "InfoRefs", "error", err)
25
25
+
return
26
26
+
}
27
27
+
28
28
+
repoPath, err := securejoin.SecureJoin(h.c.Repo.ScanPath, repo)
29
29
+
if err != nil {
30
30
+
gitError(w, "repository not found", http.StatusNotFound)
31
31
+
h.l.Error("git: failed to secure join repo path", "handler", "InfoRefs", "error", err)
32
32
+
return
33
33
+
}
34
34
+
35
35
+
gr, err := git.Open(repoPath, ref)
36
36
+
37
37
+
immutableLink := fmt.Sprintf(
38
38
+
"https://%s/%s/%s/archive/%s",
39
39
+
h.c.Server.Hostname,
40
40
+
did,
41
41
+
name,
42
42
+
gr.Hash(),
43
43
+
)
44
44
+
45
45
+
safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-")
46
46
+
filename := fmt.Sprintf("%s-%s.tar.gz", name, safeRefFilename)
47
47
+
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
48
48
+
w.Header().Set("Content-Type", "application/gzip")
49
49
+
w.Header().Set("Link", fmt.Sprintf("<%s>; rel=\"immutable\"", immutableLink))
50
50
+
51
51
+
gw := gzip.NewWriter(w)
52
52
+
defer gw.Close()
53
53
+
54
54
+
err = gr.WriteTar(gw, "")
55
55
+
if err != nil {
56
56
+
// once we start writing to the body we can't report error anymore
57
57
+
// so we are only left with logging the error
58
58
+
h.l.Error("writing tar file", "error", err)
59
59
+
return
60
60
+
}
61
61
+
62
62
+
err = gw.Flush()
63
63
+
if err != nil {
64
64
+
// once we start writing to the body we can't report error anymore
65
65
+
// so we are only left with logging the error
66
66
+
h.l.Error("flushing", "error", err.Error())
67
67
+
return
68
68
+
}
69
69
+
}
+4
knotserver/git/git.go
···
76
76
return &g, nil
77
77
}
78
78
79
79
+
func (g *GitRepo) Hash() plumbing.Hash {
80
80
+
return g.h
81
81
+
}
82
82
+
79
83
// re-open a repository and update references
80
84
func (g *GitRepo) Refresh() error {
81
85
refreshed, err := PlainOpen(g.path)
+2
knotserver/router.go
···
84
84
r.Get("/info/refs", h.InfoRefs)
85
85
r.Post("/git-upload-pack", h.UploadPack)
86
86
r.Post("/git-receive-pack", h.ReceivePack)
87
87
+
// convenience routes
88
88
+
r.Get("/archive/{ref}", h.Archive)
87
89
})
88
90
})
89
91
-81
knotserver/xrpc/repo_archive.go
···
1
1
-
package xrpc
2
2
-
3
3
-
import (
4
4
-
"compress/gzip"
5
5
-
"fmt"
6
6
-
"net/http"
7
7
-
"strings"
8
8
-
9
9
-
"github.com/go-git/go-git/v5/plumbing"
10
10
-
11
11
-
"tangled.org/core/knotserver/git"
12
12
-
xrpcerr "tangled.org/core/xrpc/errors"
13
13
-
)
14
14
-
15
15
-
func (x *Xrpc) RepoArchive(w http.ResponseWriter, r *http.Request) {
16
16
-
repo := r.URL.Query().Get("repo")
17
17
-
repoPath, err := x.parseRepoParam(repo)
18
18
-
if err != nil {
19
19
-
writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest)
20
20
-
return
21
21
-
}
22
22
-
23
23
-
ref := r.URL.Query().Get("ref")
24
24
-
// ref can be empty (git.Open handles this)
25
25
-
26
26
-
format := r.URL.Query().Get("format")
27
27
-
if format == "" {
28
28
-
format = "tar.gz" // default
29
29
-
}
30
30
-
31
31
-
prefix := r.URL.Query().Get("prefix")
32
32
-
33
33
-
if format != "tar.gz" {
34
34
-
writeError(w, xrpcerr.NewXrpcError(
35
35
-
xrpcerr.WithTag("InvalidRequest"),
36
36
-
xrpcerr.WithMessage("only tar.gz format is supported"),
37
37
-
), http.StatusBadRequest)
38
38
-
return
39
39
-
}
40
40
-
41
41
-
gr, err := git.Open(repoPath, ref)
42
42
-
if err != nil {
43
43
-
writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound)
44
44
-
return
45
45
-
}
46
46
-
47
47
-
repoParts := strings.Split(repo, "/")
48
48
-
repoName := repoParts[len(repoParts)-1]
49
49
-
50
50
-
safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-")
51
51
-
52
52
-
var archivePrefix string
53
53
-
if prefix != "" {
54
54
-
archivePrefix = prefix
55
55
-
} else {
56
56
-
archivePrefix = fmt.Sprintf("%s-%s", repoName, safeRefFilename)
57
57
-
}
58
58
-
59
59
-
filename := fmt.Sprintf("%s-%s.tar.gz", repoName, safeRefFilename)
60
60
-
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
61
61
-
w.Header().Set("Content-Type", "application/gzip")
62
62
-
63
63
-
gw := gzip.NewWriter(w)
64
64
-
defer gw.Close()
65
65
-
66
66
-
err = gr.WriteTar(gw, archivePrefix)
67
67
-
if err != nil {
68
68
-
// once we start writing to the body we can't report error anymore
69
69
-
// so we are only left with logging the error
70
70
-
x.Logger.Error("writing tar file", "error", err.Error())
71
71
-
return
72
72
-
}
73
73
-
74
74
-
err = gw.Flush()
75
75
-
if err != nil {
76
76
-
// once we start writing to the body we can't report error anymore
77
77
-
// so we are only left with logging the error
78
78
-
x.Logger.Error("flushing", "error", err.Error())
79
79
-
return
80
80
-
}
81
81
-
}
-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
67
-
r.Get("/"+tangled.RepoArchiveNSID, x.RepoArchive)
68
67
r.Get("/"+tangled.RepoLanguagesNSID, x.RepoLanguages)
69
68
70
69
// knot query endpoints (no auth required)
-55
lexicons/repo/archive.json
···
1
1
-
{
2
2
-
"lexicon": 1,
3
3
-
"id": "sh.tangled.repo.archive",
4
4
-
"defs": {
5
5
-
"main": {
6
6
-
"type": "query",
7
7
-
"parameters": {
8
8
-
"type": "params",
9
9
-
"required": ["repo", "ref"],
10
10
-
"properties": {
11
11
-
"repo": {
12
12
-
"type": "string",
13
13
-
"description": "Repository identifier in format 'did:plc:.../repoName'"
14
14
-
},
15
15
-
"ref": {
16
16
-
"type": "string",
17
17
-
"description": "Git reference (branch, tag, or commit SHA)"
18
18
-
},
19
19
-
"format": {
20
20
-
"type": "string",
21
21
-
"description": "Archive format",
22
22
-
"enum": ["tar", "zip", "tar.gz", "tar.bz2", "tar.xz"],
23
23
-
"default": "tar.gz"
24
24
-
},
25
25
-
"prefix": {
26
26
-
"type": "string",
27
27
-
"description": "Prefix for files in the archive"
28
28
-
}
29
29
-
}
30
30
-
},
31
31
-
"output": {
32
32
-
"encoding": "*/*",
33
33
-
"description": "Binary archive data"
34
34
-
},
35
35
-
"errors": [
36
36
-
{
37
37
-
"name": "RepoNotFound",
38
38
-
"description": "Repository not found or access denied"
39
39
-
},
40
40
-
{
41
41
-
"name": "RefNotFound",
42
42
-
"description": "Git reference not found"
43
43
-
},
44
44
-
{
45
45
-
"name": "InvalidRequest",
46
46
-
"description": "Invalid request parameters"
47
47
-
},
48
48
-
{
49
49
-
"name": "ArchiveError",
50
50
-
"description": "Failed to create archive"
51
51
-
}
52
52
-
]
53
53
-
}
54
54
-
}
55
55
-
}
+3
-24
nix/gomod2nix.toml
···
165
165
[mod."github.com/davecgh/go-spew"]
166
166
version = "v1.1.2-0.20180830191138-d8f796af33cc"
167
167
hash = "sha256-fV9oI51xjHdOmEx6+dlq7Ku2Ag+m/bmbzPo6A4Y74qc="
168
168
-
[mod."github.com/decred/dcrd/dcrec/secp256k1/v4"]
169
169
-
version = "v4.4.0"
170
170
-
hash = "sha256-qrhEIwhDll3cxoVpMbm1NQ9/HTI42S7ms8Buzlo5HCg="
171
168
[mod."github.com/dgraph-io/ristretto"]
172
169
version = "v0.2.0"
173
170
hash = "sha256-bnpxX+oO/Qf7IJevA0gsbloVoqRx+5bh7RQ9d9eLNYw="
···
307
304
[mod."github.com/hashicorp/go-sockaddr"]
308
305
version = "v1.0.7"
309
306
hash = "sha256-p6eDOrGzN1jMmT/F/f/VJMq0cKNFhUcEuVVwTE6vSrs="
307
307
+
[mod."github.com/hashicorp/go-version"]
308
308
+
version = "v1.8.0"
309
309
+
hash = "sha256-KXtqERmYrWdpqPCViWcHbe6jnuH7k16bvBIcuJuevj8="
310
310
[mod."github.com/hashicorp/golang-lru"]
311
311
version = "v1.0.2"
312
312
hash = "sha256-yy+5botc6T5wXgOe2mfNXJP3wr+MkVlUZ2JBkmmrA48="
···
373
373
[mod."github.com/klauspost/cpuid/v2"]
374
374
version = "v2.3.0"
375
375
hash = "sha256-50JhbQyT67BK38HIdJihPtjV7orYp96HknI2VP7A9Yc="
376
376
-
[mod."github.com/lestrrat-go/blackmagic"]
377
377
-
version = "v1.0.4"
378
378
-
hash = "sha256-HmWOpwoPDNMwLdOi7onNn3Sb+ZsAa3Ai3gVBbXmQ0e8="
379
379
-
[mod."github.com/lestrrat-go/httpcc"]
380
380
-
version = "v1.0.1"
381
381
-
hash = "sha256-SMRSwJpqDIs/xL0l2e8vP0W65qtCHX2wigcOeqPJmos="
382
382
-
[mod."github.com/lestrrat-go/httprc"]
383
383
-
version = "v1.0.6"
384
384
-
hash = "sha256-mfZzePEhrmyyu/avEBd2MsDXyto8dq5+fyu5lA8GUWM="
385
385
-
[mod."github.com/lestrrat-go/iter"]
386
386
-
version = "v1.0.2"
387
387
-
hash = "sha256-30tErRf7Qu/NOAt1YURXY/XJSA6sCr6hYQfO8QqHrtw="
388
388
-
[mod."github.com/lestrrat-go/jwx/v2"]
389
389
-
version = "v2.1.6"
390
390
-
hash = "sha256-0LszXRZIba+X8AOrs3T4uanAUafBdlVB8/MpUNEFpbc="
391
391
-
[mod."github.com/lestrrat-go/option"]
392
392
-
version = "v1.0.1"
393
393
-
hash = "sha256-jVcIYYVsxElIS/l2akEw32vdEPR8+anR6oeT1FoYULI="
394
376
[mod."github.com/lucasb-eyer/go-colorful"]
395
377
version = "v1.2.0"
396
378
hash = "sha256-Gg9dDJFCTaHrKHRR1SrJgZ8fWieJkybljybkI9x0gyE="
···
511
493
[mod."github.com/ryanuber/go-glob"]
512
494
version = "v1.0.0"
513
495
hash = "sha256-YkMl1utwUhi3E0sHK23ISpAsPyj4+KeXyXKoFYGXGVY="
514
514
-
[mod."github.com/segmentio/asm"]
515
515
-
version = "v1.2.0"
516
516
-
hash = "sha256-zbNuKxNrUDUc6IlmRQNuJQzVe5Ol/mqp7srDg9IMMqs="
517
496
[mod."github.com/sergi/go-diff"]
518
497
version = "v1.1.0"
519
498
hash = "sha256-8NJMabldpf40uwQN20T6QXx5KORDibCBJL02KD661xY="