From 99f4008a6ce2d909b4a89229479f21d71d07b608 Mon Sep 17 00:00:00 2001 From: Seongmin Lee Date: Sat, 18 Oct 2025 14:45:24 +0900 Subject: [PATCH] appview,knotserver: immutable nix flakeref link header Change-Id: ptrrwwvnkmxqwyvqlklqlmwpzwnkmuqx Close: #231 Signed-off-by: Seongmin Lee --- api/tangled/reporesolveRef.go | 33 +++++++++++++++++++++++ appview/repo/repo.go | 19 ++++++++++++- knotserver/git/git.go | 4 +++ knotserver/xrpc/repo_resolve_ref.go | 31 +++++++++++++++++++++ knotserver/xrpc/xrpc.go | 1 + lexicons/repo/resolveRef.json | 42 +++++++++++++++++++++++++++++ 6 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 api/tangled/reporesolveRef.go create mode 100644 knotserver/xrpc/repo_resolve_ref.go create mode 100644 lexicons/repo/resolveRef.json diff --git a/api/tangled/reporesolveRef.go b/api/tangled/reporesolveRef.go new file mode 100644 index 00000000..a1ea364f --- /dev/null +++ b/api/tangled/reporesolveRef.go @@ -0,0 +1,33 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +package tangled + +// schema: sh.tangled.repo.resolveRef + +import ( + "bytes" + "context" + + "github.com/bluesky-social/indigo/lex/util" +) + +const ( + RepoResolveRefNSID = "sh.tangled.repo.resolveRef" +) + +// RepoResolveRef calls the XRPC method "sh.tangled.repo.resolveRef". +// +// ref: Reference name (branch, tag or other references) +// repo: Repository identifier in format 'did:plc:.../repoName' +func RepoResolveRef(ctx context.Context, c util.LexClient, ref string, repo string) ([]byte, error) { + buf := new(bytes.Buffer) + + params := map[string]interface{}{} + params["ref"] = ref + params["repo"] = repo + if err := c.LexDo(ctx, util.Query, "", "sh.tangled.repo.resolveRef", params, nil, buf); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} diff --git a/appview/repo/repo.go b/appview/repo/repo.go index 7ddd6357..f64e0238 100644 --- a/appview/repo/repo.go +++ b/appview/repo/repo.go @@ -110,7 +110,23 @@ func (rp *Repo) DownloadArchive(w http.ResponseWriter, r *http.Request) { } repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) - archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", ref, repo) + // TODO: we are requesting the knot twice here to get permanent commit-hash. + // This should purely handled from knot instead. + rawHash, err := tangled.RepoResolveRef(r.Context(), xrpcc, ref, repo) + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { + l.Error("failed to call XRPC repo.archive", "err", xrpcerr) + rp.pages.Error503(w) + return + } + hash := string(rawHash) + immutableLink := fmt.Sprintf( + "%s/%s/archive/%s", + rp.config.Core.AppviewHost, + repo, + hash, + ) + + archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", hash, repo) if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { l.Error("failed to call XRPC repo.archive", "err", xrpcerr) rp.pages.Error503(w) @@ -123,6 +139,7 @@ func (rp *Repo) DownloadArchive(w http.ResponseWriter, r *http.Request) { 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))) + w.Header().Set("Link", fmt.Sprintf("<%s>; rel=\"immutable\"", immutableLink)) // Write the archive data directly w.Write(archiveBytes) diff --git a/knotserver/git/git.go b/knotserver/git/git.go index 2fe31c70..d9e32535 100644 --- a/knotserver/git/git.go +++ b/knotserver/git/git.go @@ -71,6 +71,10 @@ func PlainOpen(path string) (*GitRepo, error) { 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) diff --git a/knotserver/xrpc/repo_resolve_ref.go b/knotserver/xrpc/repo_resolve_ref.go new file mode 100644 index 00000000..512192ae --- /dev/null +++ b/knotserver/xrpc/repo_resolve_ref.go @@ -0,0 +1,31 @@ +package xrpc + +import ( + "fmt" + "net/http" + + "tangled.org/core/knotserver/git" + xrpcerr "tangled.org/core/xrpc/errors" +) + +func (x *Xrpc) RepoResolveRef(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) + + gr, err := git.Open(repoPath, ref) + if err != nil { + x.Logger.Error("failed to open", "error", err) + writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound) + return + } + + w.Header().Set("Content-Type", "text/plain") + fmt.Fprint(w, gr.Hash().String()) +} diff --git a/knotserver/xrpc/xrpc.go b/knotserver/xrpc/xrpc.go index c110356f..025b030b 100644 --- a/knotserver/xrpc/xrpc.go +++ b/knotserver/xrpc/xrpc.go @@ -66,6 +66,7 @@ func (x *Xrpc) Router() http.Handler { r.Get("/"+tangled.RepoBranchNSID, x.RepoBranch) r.Get("/"+tangled.RepoArchiveNSID, x.RepoArchive) r.Get("/"+tangled.RepoLanguagesNSID, x.RepoLanguages) + r.Get("/"+tangled.RepoResolveRefNSID, x.RepoResolveRef) // knot query endpoints (no auth required) r.Get("/"+tangled.KnotListKeysNSID, x.ListKeys) diff --git a/lexicons/repo/resolveRef.json b/lexicons/repo/resolveRef.json new file mode 100644 index 00000000..8e817669 --- /dev/null +++ b/lexicons/repo/resolveRef.json @@ -0,0 +1,42 @@ +{ + "lexicon": 1, + "id": "sh.tangled.repo.resolveRef", + "defs": { + "main": { + "type": "query", + "description": "Resolve a ref to its corresponding commit hash", + "parameters": { + "type": "params", + "required": ["repo", "ref"], + "properties": { + "repo": { + "type": "string", + "description": "Repository identifier in format 'did:plc:.../repoName'" + }, + "ref": { + "type": "string", + "description": "Reference name (branch, tag or other references)" + } + } + }, + "output": { + "encoding": "*/*", + "description": "Resolved hash" + }, + "errors": [ + { + "name": "RepoNotFound", + "description": "Repository not found or access denied" + }, + { + "name": "RefNotFound", + "description": "Ref not found" + }, + { + "name": "InvalidRequest", + "description": "Invalid request parameters" + } + ] + } + } +} -- 2.43.0