Monorepo for Tangled tangled.org

appview,knotserver: support knot http endpoint for archive

This will allow users to directly request archive from the knot without
using xrpc.
Xrpc doesn't fit here because it strips out the http headers which might
include valuable metadata like download filename or immutable link.

- remove xrpc method `sh.tangled.repo.archive`
- reimplement archive on knot as `/{owner}/{repo}/archive/{ref}`
endpoint
- appview will just proxy the request to knot on `/archive` like it is
doing for git http endpoints
- rename the `git_http.go` file to generalized `proxy_knot.go` filaname

Signed-off-by: Seongmin Lee <git@boltless.me>

boltless.me ace30b75 428c002e

verified
Changed files
+24 -54
appview
knotserver
-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 - repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 35 - archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", ref, repo) 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
··· 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 - 47 43 r.Route("/fork", func(r chi.Router) { 48 44 r.Use(middleware.AuthMiddleware(rp.oauth)) 49 45 r.Get("/", rp.ForkRepo)
+19
appview/state/git_http.go appview/state/proxy_knot.go
··· 59 59 s.proxyRequest(w, r, targetURL) 60 60 } 61 61 62 + func (s *State) DownloadArchive(w http.ResponseWriter, r *http.Request) { 63 + ref := chi.URLParam(r, "ref") 64 + 65 + user, ok := r.Context().Value("resolvedId").(identity.Identity) 66 + if !ok { 67 + http.Error(w, "failed to resolve user", http.StatusInternalServerError) 68 + return 69 + } 70 + repo := r.Context().Value("repo").(*models.Repo) 71 + 72 + scheme := "https" 73 + if s.config.Core.Dev { 74 + scheme = "http" 75 + } 76 + 77 + targetURL := fmt.Sprintf("%s://%s/%s/%s/archive/%s", scheme, repo.Knot, user.DID, repo.Name, ref) 78 + s.proxyRequest(w, r, targetURL) 79 + } 80 + 62 81 func (s *State) proxyRequest(w http.ResponseWriter, r *http.Request, targetURL string) { 63 82 client := &http.Client{} 64 83
+3 -1
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 + // intentionally doesn't use /* as this isn't 104 + // a file path 105 + r.Get("/archive/{ref}", s.DownloadArchive) 104 106 }) 105 107 }) 106 108
+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 + // convenience routes 88 + r.Get("/archive/{ref}", h.Archive) 87 89 }) 88 90 }) 89 91