tangled
alpha
login
or
join now
back
interdiff of round #2 and #1
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
files
api
tangled
repoarchive.go
reporesolveRef.go
appview
repo
archive.go
repo.go
router.go
state
git_http.go
proxy_knot.go
router.go
knotserver
archive.go
git
git.go
router.go
xrpc
repo_archive.go
repo_resolve_ref.go
xrpc.go
lexicons
repo
archive.json
resolveRef.json
REVERTED
api/tangled/reporesolveRef.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.resolveRef
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
-
RepoResolveRefNSID = "sh.tangled.repo.resolveRef"
16
16
-
)
17
17
-
18
18
-
// RepoResolveRef calls the XRPC method "sh.tangled.repo.resolveRef".
19
19
-
//
20
20
-
// ref: Reference name (branch, tag or other references)
21
21
-
// repo: Repository identifier in format 'did:plc:.../repoName'
22
22
-
func RepoResolveRef(ctx context.Context, c util.LexClient, ref string, repo string) ([]byte, error) {
23
23
-
buf := new(bytes.Buffer)
24
24
-
25
25
-
params := map[string]interface{}{}
26
26
-
params["ref"] = ref
27
27
-
params["repo"] = repo
28
28
-
if err := c.LexDo(ctx, util.Query, "", "sh.tangled.repo.resolveRef", params, nil, buf); err != nil {
29
29
-
return nil, err
30
30
-
}
31
31
-
32
32
-
return buf.Bytes(), nil
33
33
-
}
REVERTED
appview/repo/repo.go
···
110
110
}
111
111
112
112
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
113
113
+
archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", ref, repo)
113
113
-
// TODO: we are requesting the knot twice here to get permanent commit-hash.
114
114
-
// This should purely handled from knot instead.
115
115
-
rawHash, err := tangled.RepoResolveRef(r.Context(), xrpcc, ref, repo)
116
116
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
117
117
-
l.Error("failed to call XRPC repo.archive", "err", xrpcerr)
118
118
-
rp.pages.Error503(w)
119
119
-
return
120
120
-
}
121
121
-
hash := string(rawHash)
122
122
-
immutableLink := fmt.Sprintf(
123
123
-
"%s/%s/archive/%s",
124
124
-
rp.config.Core.AppviewHost,
125
125
-
repo,
126
126
-
hash,
127
127
-
)
128
128
-
129
129
-
archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", hash, repo)
130
114
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
131
115
l.Error("failed to call XRPC repo.archive", "err", xrpcerr)
132
116
rp.pages.Error503(w)
···
139
123
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
140
124
w.Header().Set("Content-Type", "application/gzip")
141
125
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(archiveBytes)))
142
142
-
w.Header().Set("Link", fmt.Sprintf("<%s>; rel=\"immutable\"", immutableLink))
143
126
144
127
// Write the archive data directly
145
128
w.Write(archiveBytes)
ERROR
knotserver/git/git.go
Failed to calculate interdiff for this file.
REVERTED
knotserver/xrpc/repo_resolve_ref.go
···
1
1
-
package xrpc
2
2
-
3
3
-
import (
4
4
-
"fmt"
5
5
-
"net/http"
6
6
-
7
7
-
"tangled.org/core/knotserver/git"
8
8
-
xrpcerr "tangled.org/core/xrpc/errors"
9
9
-
)
10
10
-
11
11
-
func (x *Xrpc) RepoResolveRef(w http.ResponseWriter, r *http.Request) {
12
12
-
repo := r.URL.Query().Get("repo")
13
13
-
repoPath, err := x.parseRepoParam(repo)
14
14
-
if err != nil {
15
15
-
writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest)
16
16
-
return
17
17
-
}
18
18
-
19
19
-
ref := r.URL.Query().Get("ref")
20
20
-
// ref can be empty (git.Open handles this)
21
21
-
22
22
-
gr, err := git.Open(repoPath, ref)
23
23
-
if err != nil {
24
24
-
x.Logger.Error("failed to open", "error", err)
25
25
-
writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound)
26
26
-
return
27
27
-
}
28
28
-
29
29
-
w.Header().Set("Content-Type", "text/plain")
30
30
-
fmt.Fprint(w, gr.Hash().String())
31
31
-
}
ERROR
knotserver/xrpc/xrpc.go
Failed to calculate interdiff for this file.
REVERTED
lexicons/repo/resolveRef.json
···
1
1
-
{
2
2
-
"lexicon": 1,
3
3
-
"id": "sh.tangled.repo.resolveRef",
4
4
-
"defs": {
5
5
-
"main": {
6
6
-
"type": "query",
7
7
-
"description": "Resolve a ref to its corresponding commit hash",
8
8
-
"parameters": {
9
9
-
"type": "params",
10
10
-
"required": ["repo", "ref"],
11
11
-
"properties": {
12
12
-
"repo": {
13
13
-
"type": "string",
14
14
-
"description": "Repository identifier in format 'did:plc:.../repoName'"
15
15
-
},
16
16
-
"ref": {
17
17
-
"type": "string",
18
18
-
"description": "Reference name (branch, tag or other references)"
19
19
-
}
20
20
-
}
21
21
-
},
22
22
-
"output": {
23
23
-
"encoding": "*/*",
24
24
-
"description": "Resolved hash"
25
25
-
},
26
26
-
"errors": [
27
27
-
{
28
28
-
"name": "RepoNotFound",
29
29
-
"description": "Repository not found or access denied"
30
30
-
},
31
31
-
{
32
32
-
"name": "RefNotFound",
33
33
-
"description": "Ref not found"
34
34
-
},
35
35
-
{
36
36
-
"name": "InvalidRequest",
37
37
-
"description": "Invalid request parameters"
38
38
-
}
39
39
-
]
40
40
-
}
41
41
-
}
42
42
-
}
NEW
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
-
}
NEW
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
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
35
35
-
archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", ref, repo)
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
-
}
NEW
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)
NEW
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
-
}
NEW
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
+
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) DownloadArchive(w http.ResponseWriter, r *http.Request) {
63
63
+
ref := chi.URLParam(r, "ref")
64
64
+
65
65
+
user, ok := r.Context().Value("resolvedId").(identity.Identity)
66
66
+
if !ok {
67
67
+
http.Error(w, "failed to resolve user", http.StatusInternalServerError)
68
68
+
return
69
69
+
}
70
70
+
repo := r.Context().Value("repo").(*models.Repo)
71
71
+
72
72
+
scheme := "https"
73
73
+
if s.config.Core.Dev {
74
74
+
scheme = "http"
75
75
+
}
76
76
+
77
77
+
targetURL := fmt.Sprintf("%s://%s/%s/%s/archive/%s", scheme, repo.Knot, user.DID, repo.Name, ref)
78
78
+
s.proxyRequest(w, r, targetURL)
79
79
+
}
80
80
+
81
81
+
func (s *State) proxyRequest(w http.ResponseWriter, r *http.Request, targetURL string) {
82
82
+
client := &http.Client{}
83
83
+
84
84
+
// Create new request
85
85
+
proxyReq, err := http.NewRequest(r.Method, targetURL, r.Body)
86
86
+
if err != nil {
87
87
+
http.Error(w, err.Error(), http.StatusInternalServerError)
88
88
+
return
89
89
+
}
90
90
+
91
91
+
// Copy original headers
92
92
+
proxyReq.Header = r.Header
93
93
+
94
94
+
repoOwnerHandle := chi.URLParam(r, "user")
95
95
+
proxyReq.Header.Add("x-tangled-repo-owner-handle", repoOwnerHandle)
96
96
+
97
97
+
// Execute request
98
98
+
resp, err := client.Do(proxyReq)
99
99
+
if err != nil {
100
100
+
http.Error(w, err.Error(), http.StatusInternalServerError)
101
101
+
return
102
102
+
}
103
103
+
defer resp.Body.Close()
104
104
+
105
105
+
// Copy response headers
106
106
+
maps.Copy(w.Header(), resp.Header)
107
107
+
108
108
+
// Set response status code
109
109
+
w.WriteHeader(resp.StatusCode)
110
110
+
111
111
+
// Copy response body
112
112
+
if _, err := io.Copy(w, resp.Body); err != nil {
113
113
+
http.Error(w, err.Error(), http.StatusInternalServerError)
114
114
+
return
115
115
+
}
116
116
+
}
NEW
appview/state/router.go
···
100
100
r.Get("/info/refs", s.InfoRefs)
101
101
r.Post("/git-upload-pack", s.UploadPack)
102
102
r.Post("/git-receive-pack", s.ReceivePack)
103
103
-
103
103
+
// intentionally doesn't use /* as this isn't
104
104
+
// a file path
105
105
+
r.Get("/archive/{ref}", s.DownloadArchive)
104
106
})
105
107
})
106
108
NEW
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
+
}
NEW
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
NEW
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
-
}
NEW
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
-
}