Mirror of @tangled.org/core. Running on a Raspberry Pi Zero 2 (Please be gentle).
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

appview,knotserver: support lockable http tarball protocol

the lockable http tarball protocol is meant to serve tarball flakes, by
emitting a stable `Link` header:

Link: <flakeref>; rel="immutable"

this patch now supports the new header in two places, on the appview, at
the `/archive/<ref>.tar.gz` endpoint:

λ nix flake metadata -v --refresh --no-write-lock-file 'http://127.0.0.1:3000/oppi.li/repo-19-01-26-08-04-14/archive/main.tar.gz'
unpacking 'http://127.0.0.1:3000/oppi.li/repo-19-01-26-08-04-14/archive/main.tar.gz' into the Git cache...
warning: not writing modified lock file of flake 'http://127.0.0.1:3000/oppi.li/repo-19-01-26-08-04-14/archive/main.tar.gz':
• Added input 'nixpkgs':
'github:nixos/nixpkgs/bde09022887110deb780067364a0818e89258968?narHash=sha256-tLj4KcRDLakrlpvboTJDKsrp6z2XLwyQ4Zmo%2Bw8KsY4%3D' (2026-01-19)
Resolved URL: http://127.0.0.1:3000/oppi.li/repo-19-01-26-08-04-14/archive/main.tar.gz
Locked URL: http://127.0.0.1:3000/did:plc:qfpnj4og54vl56wngdriaxug/repo-19-01-26-08-04-14/archive/a63d945ae97b84812e394207f3cc80f6525c2082.tar.gz?narHash=sha256-IdKT88RIWvWrgQFx6c%2BX3cC7JFene%2BQI9yo2rKSGoA4%3D
Path: /nix/store/0k9pv83f0qn5cm0qy82j51plryk7szx7-source
Fingerprint: 9512ee4857b31a76c1112f05161bda5280d8596b866c4f78986c6c01c1d2f419
Inputs:
└───nixpkgs: github:nixos/nixpkgs/bde09022887110deb780067364a0818e89258968?narHash=sha256-tLj4KcRDLakrlpvboTJDKsrp6z2XLwyQ4Zmo%2Bw8KsY4%3D (2026-01-19 00:39:23)

and on the knotserver, when using the `/xrpc/sh.tangled.repo.archive`
endpoint:

λ nix flake metadata -v --refresh --no-write-lock-file "http://localhost:5555/xrpc/sh.tangled.repo.archive?format=tar.gz&prefix=&ref=main&repo=did%3Aplc%3Aqfpnj4og54vl56wngdriaxug%2Frepo-19-01-26-08-04-14"
unpacking 'http://localhost:5555/xrpc/sh.tangled.repo.archive?format=tar.gz&prefix=&ref=main&repo=did:plc:qfpnj4og54vl56wngdriaxug/repo-19-01-26-08-04-14' into the Git cache...
warning: not writing modified lock file of flake 'http://localhost:5555/xrpc/sh.tangled.repo.archive?format=tar.gz&prefix=&ref=main&repo=did:plc:qfpnj4og54vl56wngdriaxug/repo-19-01-26-08-04-14':
• Added input 'nixpkgs':
'github:nixos/nixpkgs/bde09022887110deb780067364a0818e89258968?narHash=sha256-tLj4KcRDLakrlpvboTJDKsrp6z2XLwyQ4Zmo%2Bw8KsY4%3D' (2026-01-19)
Resolved URL: http://localhost:5555/xrpc/sh.tangled.repo.archive?format=tar.gz&prefix=&ref=main&repo=did:plc:qfpnj4og54vl56wngdriaxug/repo-19-01-26-08-04-14
Locked URL: http://localhost:5555/xrpc/sh.tangled.repo.archive?format=tar.gz&narHash=sha256-IdKT88RIWvWrgQFx6c%2BX3cC7JFene%2BQI9yo2rKSGoA4%3D&prefix=&ref=a63d945ae97b84812e394207f3cc80f6525c2082&repo=did:plc:qfpnj4og54vl56wngdriaxug/repo-19-01-26-08-04-14
Path: /nix/store/0k9pv83f0qn5cm0qy82j51plryk7szx7-source
Fingerprint: 9512ee4857b31a76c1112f05161bda5280d8596b866c4f78986c6c01c1d2f419
Inputs:
└───nixpkgs: github:nixos/nixpkgs/bde09022887110deb780067364a0818e89258968?narHash=sha256-tLj4KcRDLakrlpvboTJDKsrp6z2XLwyQ4Zmo%2Bw8KsY4%3D (2026-01-19 00:39:23)

note that the "Resolved URL" includes a hash of the commit.

Co-authored-by: Seongmin Lee <git@boltless.me>
Signed-off-by: oppiliappan <me@oppi.li>

authored by

oppiliappan
Seongmin Lee
and committed by tangled.org cde47050 1abafd05

+103 -19
+64 -19
appview/repo/archive.go
··· 2 2 3 3 import ( 4 4 "fmt" 5 + "io" 5 6 "net/http" 6 7 "net/url" 7 8 "strings" 8 9 9 - "tangled.org/core/api/tangled" 10 - xrpcclient "tangled.org/core/appview/xrpcclient" 11 - 12 - indigoxrpc "github.com/bluesky-social/indigo/xrpc" 13 10 "github.com/go-chi/chi/v5" 14 - "github.com/go-git/go-git/v5/plumbing" 15 11 ) 16 12 17 13 func (rp *Repo) DownloadArchive(w http.ResponseWriter, r *http.Request) { ··· 25 29 scheme = "https" 26 30 } 27 31 host := fmt.Sprintf("%s://%s", scheme, f.Knot) 28 - xrpcc := &indigoxrpc.Client{ 29 - Host: host, 30 - } 31 32 didSlashRepo := f.DidSlashRepo() 32 - archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", ref, didSlashRepo) 33 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 34 - l.Error("failed to call XRPC repo.archive", "err", xrpcerr) 33 + 34 + // build the xrpc url 35 + u, err := url.Parse(host) 36 + if err != nil { 37 + l.Error("failed to parse host URL", "err", err) 35 38 rp.pages.Error503(w) 36 39 return 37 40 } 38 - // Set headers for file download, just pass along whatever the knot specifies 39 - safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-") 40 - filename := fmt.Sprintf("%s-%s.tar.gz", f.Name, safeRefFilename) 41 - w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) 42 - w.Header().Set("Content-Type", "application/gzip") 43 - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(archiveBytes))) 44 - // Write the archive data directly 45 - w.Write(archiveBytes) 41 + 42 + u.Path = "/xrpc/sh.tangled.repo.archive" 43 + query := url.Values{} 44 + query.Set("format", "tar.gz") 45 + query.Set("prefix", r.URL.Query().Get("prefix")) 46 + query.Set("ref", ref) 47 + query.Set("repo", didSlashRepo) 48 + u.RawQuery = query.Encode() 49 + 50 + xrpcURL := u.String() 51 + 52 + // make the get request 53 + resp, err := http.Get(xrpcURL) 54 + if err != nil { 55 + l.Error("failed to call XRPC repo.archive", "err", err) 56 + rp.pages.Error503(w) 57 + return 58 + } 59 + 60 + // pass through headers from upstream response 61 + if contentDisposition := resp.Header.Get("Content-Disposition"); contentDisposition != "" { 62 + w.Header().Set("Content-Disposition", contentDisposition) 63 + } 64 + if contentType := resp.Header.Get("Content-Type"); contentType != "" { 65 + w.Header().Set("Content-Type", contentType) 66 + } 67 + if contentLength := resp.Header.Get("Content-Length"); contentLength != "" { 68 + w.Header().Set("Content-Length", contentLength) 69 + } 70 + if link := resp.Header.Get("Link"); link != "" { 71 + if resolvedRef, err := extractImmutableLink(link); err == nil { 72 + newLink := fmt.Sprintf("<%s/%s/archive/%s.tar.gz>; rel=\"immutable\"", 73 + rp.config.Core.AppviewHost, f.DidSlashRepo(), resolvedRef) 74 + w.Header().Set("Link", newLink) 75 + } 76 + } 77 + 78 + // stream the archive data directly 79 + if _, err := io.Copy(w, resp.Body); err != nil { 80 + l.Error("failed to write response", "err", err) 81 + } 82 + } 83 + 84 + func extractImmutableLink(linkHeader string) (string, error) { 85 + trimmed := strings.TrimPrefix(linkHeader, "<") 86 + trimmed = strings.TrimSuffix(trimmed, ">; rel=\"immutable\"") 87 + 88 + parsedLink, err := url.Parse(trimmed) 89 + if err != nil { 90 + return "", err 91 + } 92 + 93 + resolvedRef := parsedLink.Query().Get("ref") 94 + if resolvedRef == "" { 95 + return "", fmt.Errorf("no ref found in link header") 96 + } 97 + 98 + return resolvedRef, nil 46 99 }
+4
knotserver/git/git.go
··· 76 76 return &g, nil 77 77 } 78 78 79 + func (g *GitRepo) Hash() plumbing.Hash { 80 + return g.h 81 + } 82 + 79 83 // re-open a repository and update references 80 84 func (g *GitRepo) Refresh() error { 81 85 refreshed, err := PlainOpen(g.path)
+35
knotserver/xrpc/repo_archive.go
··· 4 4 "compress/gzip" 5 5 "fmt" 6 6 "net/http" 7 + "net/url" 7 8 "strings" 8 9 9 10 "github.com/go-git/go-git/v5/plumbing" 10 11 12 + "tangled.org/core/api/tangled" 11 13 "tangled.org/core/knotserver/git" 12 14 xrpcerr "tangled.org/core/xrpc/errors" 13 15 ) ··· 49 47 repoParts := strings.Split(repo, "/") 50 48 repoName := repoParts[len(repoParts)-1] 51 49 50 + immutableLink, err := x.buildImmutableLink(repo, format, gr.Hash().String(), prefix) 51 + if err != nil { 52 + x.Logger.Error( 53 + "failed to build immutable link", 54 + "err", err.Error(), 55 + "repo", repo, 56 + "format", format, 57 + "ref", gr.Hash().String(), 58 + "prefix", prefix, 59 + ) 60 + } 61 + 52 62 safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-") 53 63 54 64 var archivePrefix string ··· 73 59 filename := fmt.Sprintf("%s-%s.tar.gz", repoName, safeRefFilename) 74 60 w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) 75 61 w.Header().Set("Content-Type", "application/gzip") 62 + w.Header().Set("Link", fmt.Sprintf("<%s>; rel=\"immutable\"", immutableLink)) 76 63 77 64 gw := gzip.NewWriter(w) 78 65 defer gw.Close() ··· 93 78 x.Logger.Error("flushing", "error", err.Error()) 94 79 return 95 80 } 81 + } 82 + 83 + func (x *Xrpc) buildImmutableLink(repo string, format string, ref string, prefix string) (string, error) { 84 + scheme := "https" 85 + if x.Config.Server.Dev { 86 + scheme = "http" 87 + } 88 + 89 + u, err := url.Parse(scheme + "://" + x.Config.Server.Hostname + "/xrpc/" + tangled.RepoArchiveNSID) 90 + if err != nil { 91 + return "", err 92 + } 93 + 94 + params := url.Values{} 95 + params.Set("repo", repo) 96 + params.Set("format", format) 97 + params.Set("ref", ref) 98 + params.Set("prefix", prefix) 99 + 100 + return fmt.Sprintf("%s?%s", u.String(), params.Encode()), nil 96 101 }