forked from tangled.org/core
Monorepo for Tangled — https://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
···
-
package repo
-
-
import (
-
"fmt"
-
"net/http"
-
"net/url"
-
"strings"
-
-
"tangled.org/core/api/tangled"
-
xrpcclient "tangled.org/core/appview/xrpcclient"
-
-
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
-
"github.com/go-chi/chi/v5"
-
"github.com/go-git/go-git/v5/plumbing"
-
)
-
-
func (rp *Repo) DownloadArchive(w http.ResponseWriter, r *http.Request) {
-
l := rp.logger.With("handler", "DownloadArchive")
-
ref := chi.URLParam(r, "ref")
-
ref, _ = url.PathUnescape(ref)
-
f, err := rp.repoResolver.Resolve(r)
-
if err != nil {
-
l.Error("failed to get repo and knot", "err", err)
-
return
-
}
-
scheme := "http"
-
if !rp.config.Core.Dev {
-
scheme = "https"
-
}
-
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
-
xrpcc := &indigoxrpc.Client{
-
Host: host,
-
}
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
-
archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", ref, repo)
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
-
l.Error("failed to call XRPC repo.archive", "err", xrpcerr)
-
rp.pages.Error503(w)
-
return
-
}
-
// Set headers for file download, just pass along whatever the knot specifies
-
safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-")
-
filename := fmt.Sprintf("%s-%s.tar.gz", f.Name, safeRefFilename)
-
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
-
w.Header().Set("Content-Type", "application/gzip")
-
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(archiveBytes)))
-
// Write the archive data directly
-
w.Write(archiveBytes)
-
}
-4
appview/repo/router.go
···
r.Get("/blob/{ref}/*", rp.Blob)
r.Get("/raw/{ref}/*", rp.RepoBlobRaw)
-
// intentionally doesn't use /* as this isn't
-
// a file path
-
r.Get("/archive/{ref}", rp.DownloadArchive)
-
r.Route("/fork", func(r chi.Router) {
r.Use(middleware.AuthMiddleware(rp.oauth))
r.Get("/", rp.ForkRepo)
+19
appview/state/git_http.go appview/state/proxy_knot.go
···
s.proxyRequest(w, r, targetURL)
}
+
func (s *State) DownloadArchive(w http.ResponseWriter, r *http.Request) {
+
ref := chi.URLParam(r, "ref")
+
+
user, ok := r.Context().Value("resolvedId").(identity.Identity)
+
if !ok {
+
http.Error(w, "failed to resolve user", http.StatusInternalServerError)
+
return
+
}
+
repo := r.Context().Value("repo").(*models.Repo)
+
+
scheme := "https"
+
if s.config.Core.Dev {
+
scheme = "http"
+
}
+
+
targetURL := fmt.Sprintf("%s://%s/%s/%s/archive/%s", scheme, repo.Knot, user.DID, repo.Name, ref)
+
s.proxyRequest(w, r, targetURL)
+
}
+
func (s *State) proxyRequest(w http.ResponseWriter, r *http.Request, targetURL string) {
client := &http.Client{}
+3 -1
appview/state/router.go
···
r.Get("/info/refs", s.InfoRefs)
r.Post("/git-upload-pack", s.UploadPack)
r.Post("/git-receive-pack", s.ReceivePack)
-
+
// intentionally doesn't use /* as this isn't
+
// a file path
+
r.Get("/archive/{ref}", s.DownloadArchive)
})
})
+2
knotserver/router.go
···
r.Get("/info/refs", h.InfoRefs)
r.Post("/git-upload-pack", h.UploadPack)
r.Post("/git-receive-pack", h.ReceivePack)
+
// convenience routes
+
r.Get("/archive/{ref}", h.Archive)
})
})