appview,knotserver: immutable nix flakeref link header #741

open
opened by boltless.me targeting master from push-ptrrwwvnkmxq
Changed files
+194 -329
api
tangled
appview
knotserver
lexicons
-41
api/tangled/repoarchive.go
···
-
// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
-
-
package tangled
-
-
// schema: sh.tangled.repo.archive
-
-
import (
-
"bytes"
-
"context"
-
-
"github.com/bluesky-social/indigo/lex/util"
-
)
-
-
const (
-
RepoArchiveNSID = "sh.tangled.repo.archive"
-
)
-
-
// RepoArchive calls the XRPC method "sh.tangled.repo.archive".
-
//
-
// format: Archive format
-
// prefix: Prefix for files in the archive
-
// ref: Git reference (branch, tag, or commit SHA)
-
// repo: Repository identifier in format 'did:plc:.../repoName'
-
func RepoArchive(ctx context.Context, c util.LexClient, format string, prefix string, ref string, repo string) ([]byte, error) {
-
buf := new(bytes.Buffer)
-
-
params := map[string]interface{}{}
-
if format != "" {
-
params["format"] = format
-
}
-
if prefix != "" {
-
params["prefix"] = prefix
-
}
-
params["ref"] = ref
-
params["repo"] = repo
-
if err := c.LexDo(ctx, util.Query, "", "sh.tangled.repo.archive", params, nil, buf); err != nil {
-
return nil, err
-
}
-
-
return buf.Bytes(), nil
-
}
-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)
-97
appview/state/git_http.go
···
-
package state
-
-
import (
-
"fmt"
-
"io"
-
"maps"
-
"net/http"
-
-
"github.com/bluesky-social/indigo/atproto/identity"
-
"github.com/go-chi/chi/v5"
-
"tangled.org/core/appview/models"
-
)
-
-
func (s *State) InfoRefs(w http.ResponseWriter, r *http.Request) {
-
user := r.Context().Value("resolvedId").(identity.Identity)
-
repo := r.Context().Value("repo").(*models.Repo)
-
-
scheme := "https"
-
if s.config.Core.Dev {
-
scheme = "http"
-
}
-
-
targetURL := fmt.Sprintf("%s://%s/%s/%s/info/refs?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery)
-
s.proxyRequest(w, r, targetURL)
-
-
}
-
-
func (s *State) UploadPack(w http.ResponseWriter, r *http.Request) {
-
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/git-upload-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery)
-
s.proxyRequest(w, r, targetURL)
-
}
-
-
func (s *State) ReceivePack(w http.ResponseWriter, r *http.Request) {
-
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/git-receive-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery)
-
s.proxyRequest(w, r, targetURL)
-
}
-
-
func (s *State) proxyRequest(w http.ResponseWriter, r *http.Request, targetURL string) {
-
client := &http.Client{}
-
-
// Create new request
-
proxyReq, err := http.NewRequest(r.Method, targetURL, r.Body)
-
if err != nil {
-
http.Error(w, err.Error(), http.StatusInternalServerError)
-
return
-
}
-
-
// Copy original headers
-
proxyReq.Header = r.Header
-
-
repoOwnerHandle := chi.URLParam(r, "user")
-
proxyReq.Header.Add("x-tangled-repo-owner-handle", repoOwnerHandle)
-
-
// Execute request
-
resp, err := client.Do(proxyReq)
-
if err != nil {
-
http.Error(w, err.Error(), http.StatusInternalServerError)
-
return
-
}
-
defer resp.Body.Close()
-
-
// Copy response headers
-
maps.Copy(w.Header(), resp.Header)
-
-
// Set response status code
-
w.WriteHeader(resp.StatusCode)
-
-
// Copy response body
-
if _, err := io.Copy(w, resp.Body); err != nil {
-
http.Error(w, err.Error(), http.StatusInternalServerError)
-
return
-
}
-
}
+116
appview/state/proxy_knot.go
···
+
package state
+
+
import (
+
"fmt"
+
"io"
+
"maps"
+
"net/http"
+
+
"github.com/bluesky-social/indigo/atproto/identity"
+
"github.com/go-chi/chi/v5"
+
"tangled.org/core/appview/models"
+
)
+
+
func (s *State) InfoRefs(w http.ResponseWriter, r *http.Request) {
+
user := r.Context().Value("resolvedId").(identity.Identity)
+
repo := r.Context().Value("repo").(*models.Repo)
+
+
scheme := "https"
+
if s.config.Core.Dev {
+
scheme = "http"
+
}
+
+
targetURL := fmt.Sprintf("%s://%s/%s/%s/info/refs?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery)
+
s.proxyRequest(w, r, targetURL)
+
+
}
+
+
func (s *State) UploadPack(w http.ResponseWriter, r *http.Request) {
+
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/git-upload-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery)
+
s.proxyRequest(w, r, targetURL)
+
}
+
+
func (s *State) ReceivePack(w http.ResponseWriter, r *http.Request) {
+
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/git-receive-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery)
+
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{}
+
+
// Create new request
+
proxyReq, err := http.NewRequest(r.Method, targetURL, r.Body)
+
if err != nil {
+
http.Error(w, err.Error(), http.StatusInternalServerError)
+
return
+
}
+
+
// Copy original headers
+
proxyReq.Header = r.Header
+
+
repoOwnerHandle := chi.URLParam(r, "user")
+
proxyReq.Header.Add("x-tangled-repo-owner-handle", repoOwnerHandle)
+
+
// Execute request
+
resp, err := client.Do(proxyReq)
+
if err != nil {
+
http.Error(w, err.Error(), http.StatusInternalServerError)
+
return
+
}
+
defer resp.Body.Close()
+
+
// Copy response headers
+
maps.Copy(w.Header(), resp.Header)
+
+
// Set response status code
+
w.WriteHeader(resp.StatusCode)
+
+
// Copy response body
+
if _, err := io.Copy(w, resp.Body); err != nil {
+
http.Error(w, err.Error(), http.StatusInternalServerError)
+
return
+
}
+
}
+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)
})
})
+69
knotserver/archive.go
···
+
package knotserver
+
+
import (
+
"compress/gzip"
+
"fmt"
+
"net/http"
+
"strings"
+
+
securejoin "github.com/cyphar/filepath-securejoin"
+
"github.com/go-chi/chi/v5"
+
"github.com/go-git/go-git/v5/plumbing"
+
"tangled.org/core/knotserver/git"
+
)
+
+
func (h *Knot) Archive(w http.ResponseWriter, r *http.Request) {
+
var (
+
did = chi.URLParam(r, "did")
+
name = chi.URLParam(r, "name")
+
ref = chi.URLParam(r, "ref")
+
)
+
repo, err := securejoin.SecureJoin(did, name)
+
if err != nil {
+
gitError(w, "repository not found", http.StatusNotFound)
+
h.l.Error("git: failed to secure join repo path", "handler", "InfoRefs", "error", err)
+
return
+
}
+
+
repoPath, err := securejoin.SecureJoin(h.c.Repo.ScanPath, repo)
+
if err != nil {
+
gitError(w, "repository not found", http.StatusNotFound)
+
h.l.Error("git: failed to secure join repo path", "handler", "InfoRefs", "error", err)
+
return
+
}
+
+
gr, err := git.Open(repoPath, ref)
+
+
immutableLink := fmt.Sprintf(
+
"https://%s/%s/%s/archive/%s",
+
h.c.Server.Hostname,
+
did,
+
name,
+
gr.Hash(),
+
)
+
+
safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-")
+
filename := fmt.Sprintf("%s-%s.tar.gz", name, safeRefFilename)
+
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
+
w.Header().Set("Content-Type", "application/gzip")
+
w.Header().Set("Link", fmt.Sprintf("<%s>; rel=\"immutable\"", immutableLink))
+
+
gw := gzip.NewWriter(w)
+
defer gw.Close()
+
+
err = gr.WriteTar(gw, "")
+
if err != nil {
+
// once we start writing to the body we can't report error anymore
+
// so we are only left with logging the error
+
h.l.Error("writing tar file", "error", err)
+
return
+
}
+
+
err = gw.Flush()
+
if err != nil {
+
// once we start writing to the body we can't report error anymore
+
// so we are only left with logging the error
+
h.l.Error("flushing", "error", err.Error())
+
return
+
}
+
}
+4
knotserver/git/git.go
···
return &g, nil
}
+
func (g *GitRepo) Hash() plumbing.Hash {
+
return g.h
+
}
+
// re-open a repository and update references
func (g *GitRepo) Refresh() error {
refreshed, err := PlainOpen(g.path)
+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)
})
})
-81
knotserver/xrpc/repo_archive.go
···
-
package xrpc
-
-
import (
-
"compress/gzip"
-
"fmt"
-
"net/http"
-
"strings"
-
-
"github.com/go-git/go-git/v5/plumbing"
-
-
"tangled.org/core/knotserver/git"
-
xrpcerr "tangled.org/core/xrpc/errors"
-
)
-
-
func (x *Xrpc) RepoArchive(w http.ResponseWriter, r *http.Request) {
-
repo := r.URL.Query().Get("repo")
-
repoPath, err := x.parseRepoParam(repo)
-
if err != nil {
-
writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest)
-
return
-
}
-
-
ref := r.URL.Query().Get("ref")
-
// ref can be empty (git.Open handles this)
-
-
format := r.URL.Query().Get("format")
-
if format == "" {
-
format = "tar.gz" // default
-
}
-
-
prefix := r.URL.Query().Get("prefix")
-
-
if format != "tar.gz" {
-
writeError(w, xrpcerr.NewXrpcError(
-
xrpcerr.WithTag("InvalidRequest"),
-
xrpcerr.WithMessage("only tar.gz format is supported"),
-
), http.StatusBadRequest)
-
return
-
}
-
-
gr, err := git.Open(repoPath, ref)
-
if err != nil {
-
writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound)
-
return
-
}
-
-
repoParts := strings.Split(repo, "/")
-
repoName := repoParts[len(repoParts)-1]
-
-
safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-")
-
-
var archivePrefix string
-
if prefix != "" {
-
archivePrefix = prefix
-
} else {
-
archivePrefix = fmt.Sprintf("%s-%s", repoName, safeRefFilename)
-
}
-
-
filename := fmt.Sprintf("%s-%s.tar.gz", repoName, safeRefFilename)
-
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
-
w.Header().Set("Content-Type", "application/gzip")
-
-
gw := gzip.NewWriter(w)
-
defer gw.Close()
-
-
err = gr.WriteTar(gw, archivePrefix)
-
if err != nil {
-
// once we start writing to the body we can't report error anymore
-
// so we are only left with logging the error
-
x.Logger.Error("writing tar file", "error", err.Error())
-
return
-
}
-
-
err = gw.Flush()
-
if err != nil {
-
// once we start writing to the body we can't report error anymore
-
// so we are only left with logging the error
-
x.Logger.Error("flushing", "error", err.Error())
-
return
-
}
-
}
-1
knotserver/xrpc/xrpc.go
···
r.Get("/"+tangled.RepoCompareNSID, x.RepoCompare)
r.Get("/"+tangled.RepoGetDefaultBranchNSID, x.RepoGetDefaultBranch)
r.Get("/"+tangled.RepoBranchNSID, x.RepoBranch)
-
r.Get("/"+tangled.RepoArchiveNSID, x.RepoArchive)
r.Get("/"+tangled.RepoLanguagesNSID, x.RepoLanguages)
// knot query endpoints (no auth required)
-55
lexicons/repo/archive.json
···
-
{
-
"lexicon": 1,
-
"id": "sh.tangled.repo.archive",
-
"defs": {
-
"main": {
-
"type": "query",
-
"parameters": {
-
"type": "params",
-
"required": ["repo", "ref"],
-
"properties": {
-
"repo": {
-
"type": "string",
-
"description": "Repository identifier in format 'did:plc:.../repoName'"
-
},
-
"ref": {
-
"type": "string",
-
"description": "Git reference (branch, tag, or commit SHA)"
-
},
-
"format": {
-
"type": "string",
-
"description": "Archive format",
-
"enum": ["tar", "zip", "tar.gz", "tar.bz2", "tar.xz"],
-
"default": "tar.gz"
-
},
-
"prefix": {
-
"type": "string",
-
"description": "Prefix for files in the archive"
-
}
-
}
-
},
-
"output": {
-
"encoding": "*/*",
-
"description": "Binary archive data"
-
},
-
"errors": [
-
{
-
"name": "RepoNotFound",
-
"description": "Repository not found or access denied"
-
},
-
{
-
"name": "RefNotFound",
-
"description": "Git reference not found"
-
},
-
{
-
"name": "InvalidRequest",
-
"description": "Invalid request parameters"
-
},
-
{
-
"name": "ArchiveError",
-
"description": "Failed to create archive"
-
}
-
]
-
}
-
}
-
}