forked from tangled.org/core
Monorepo for Tangled

Compare changes

Choose any two refs to compare.

Changed files
+332 -292
appview
pages
templates
layouts
fragments
repo
state
knotserver
lexicons
nix
+63 -22
appview/pages/templates/layouts/fragments/topbar.html
··· 47 47 {{ end }} 48 48 49 49 {{ define "profileDropdown" }} 50 - <details class="relative inline-block text-left nav-dropdown"> 51 - <summary class="cursor-pointer list-none flex items-center gap-1"> 52 - {{ $user := .Did }} 53 - <img 54 - src="{{ tinyAvatar $user }}" 55 - alt="" 56 - class="rounded-full h-6 w-6 border border-gray-300 dark:border-gray-700" 57 - /> 58 - <span class="hidden md:inline">{{ $user | resolve | truncateAt30 }}</span> 59 - </summary> 60 - <div class="absolute flex flex-col right-0 mt-4 p-4 rounded w-48 bg-white dark:bg-gray-800 dark:text-white border border-gray-200 dark:border-gray-700"> 61 - <a href="/{{ $user }}">profile</a> 62 - <a href="/{{ $user }}?tab=repos">repositories</a> 63 - <a href="/{{ $user }}?tab=strings">strings</a> 64 - <a href="/settings">settings</a> 50 + {{ $user := .Did }} 51 + <button type="button" popovertarget="navigation-popover" class="site-navigation-dropdown-trigger" aria-label="Open site navigation dropdown"> 52 + <img 53 + src="{{ tinyAvatar $user }}" 54 + alt="" 55 + class="rounded-full h-6 w-6 border border-gray-300 dark:border-gray-700" 56 + /> 57 + </button> 58 + <div popover="auto" id="navigation-popover" class="site-navigation-popover shadow-md border border-gray-200 rounded p-2 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700"> 59 + <div class="flex gap-2 py-2"> 60 + <img 61 + src="{{ tinyAvatar $user }}" 62 + alt="" 63 + class="rounded-full h-6 w-6 border border-gray-300 dark:border-gray-700" 64 + /> 65 + <p>{{ $user | resolve | truncateAt30 }}</p> 66 + </div> 67 + <hr class="h-1 w-full mb-1 mt-2 dark:border-gray-700" /> 68 + <ul id="navigation-menu-popover"> 69 + <li> 70 + <a href="/{{ $user }}"> 71 + {{ i "user" "w-4 h-4" }} 72 + <span>profile</span> 73 + </a> 74 + </li> 75 + <li> 76 + <a href="/{{ $user }}?tab=repos"> 77 + {{ i "book-marked" "w-4 h-4" }} 78 + <span>repositories</span> 79 + </a> 80 + </li> 81 + <li> 82 + <a href="/{{ $user }}?tab=strings"> 83 + {{ i "spool" "w-4 h-4" }} 84 + <span>strings</span> 85 + </a> 86 + </li> 87 + <li> 88 + <a href="/settings"> 89 + {{ i "settings" "w-4 h-4" }} 90 + <span>settings</span> 91 + </a> 92 + </li> 93 + <hr class="h-1 w-full mb-1 mt-2 dark:border-gray-700" /> 94 + <li> 65 95 <a href="#" 66 - hx-post="/logout" 67 - hx-swap="none" 68 - class="text-red-400 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"> 69 - logout 70 - </a> 71 - </div> 72 - </details> 96 + hx-post="/logout" 97 + hx-swap="none" 98 + class="text-red-400 flex gap-2 items-center hover:bg-red-50 hover:text-red-700 px-2 py-2 rounded-sm dark:hover:bg-red-700 dark:hover:text-red-50"> 99 + {{ i "arrow-right-from-line" "w-4 h-4" }} 100 + <span>logout</span> 101 + </a> 102 + </li> 103 + </ul> 104 + </div> 73 105 74 106 <script> 75 107 document.addEventListener('click', function(event) { ··· 80 112 } 81 113 }); 82 114 }); 115 + 116 + const navigationPopoverLinks = document.querySelectorAll("#navigation-menu-popover li a"); 117 + const currentPageURL = window.location.href 118 + navigationPopoverLinks.forEach(link => { 119 + const navigationPopoverLinkURL = link.href 120 + if (navigationPopoverLinkURL === currentPageURL) { 121 + link.ariaCurrent = "page" 122 + } 123 + }) 83 124 </script> 84 125 {{ end }}
+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 + didSlashRepo := f.DidSlashRepo() 35 + archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", ref, didSlashRepo) 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 + 43 47 r.Route("/fork", func(r chi.Router) { 44 48 r.Use(middleware.AuthMiddleware(rp.oauth)) 45 49 r.Get("/", rp.ForkRepo)
+114
appview/state/git_http.go
··· 1 + package state 2 + 3 + import ( 4 + "fmt" 5 + "io" 6 + "maps" 7 + "net/http" 8 + 9 + "github.com/bluesky-social/indigo/atproto/identity" 10 + "github.com/go-chi/chi/v5" 11 + "tangled.org/core/appview/models" 12 + ) 13 + 14 + func (s *State) InfoRefs(w http.ResponseWriter, r *http.Request) { 15 + user := r.Context().Value("resolvedId").(identity.Identity) 16 + repo := r.Context().Value("repo").(*models.Repo) 17 + 18 + scheme := "https" 19 + if s.config.Core.Dev { 20 + scheme = "http" 21 + } 22 + 23 + targetURL := fmt.Sprintf("%s://%s/%s/%s/info/refs?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 24 + s.proxyRequest(w, r, targetURL) 25 + 26 + } 27 + 28 + func (s *State) UploadArchive(w http.ResponseWriter, r *http.Request) { 29 + user, ok := r.Context().Value("resolvedId").(identity.Identity) 30 + if !ok { 31 + http.Error(w, "failed to resolve user", http.StatusInternalServerError) 32 + return 33 + } 34 + repo := r.Context().Value("repo").(*models.Repo) 35 + 36 + scheme := "https" 37 + if s.config.Core.Dev { 38 + scheme = "http" 39 + } 40 + 41 + targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-archive?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 42 + s.proxyRequest(w, r, targetURL) 43 + } 44 + 45 + func (s *State) UploadPack(w http.ResponseWriter, r *http.Request) { 46 + user, ok := r.Context().Value("resolvedId").(identity.Identity) 47 + if !ok { 48 + http.Error(w, "failed to resolve user", http.StatusInternalServerError) 49 + return 50 + } 51 + repo := r.Context().Value("repo").(*models.Repo) 52 + 53 + scheme := "https" 54 + if s.config.Core.Dev { 55 + scheme = "http" 56 + } 57 + 58 + targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 59 + s.proxyRequest(w, r, targetURL) 60 + } 61 + 62 + func (s *State) ReceivePack(w http.ResponseWriter, r *http.Request) { 63 + user, ok := r.Context().Value("resolvedId").(identity.Identity) 64 + if !ok { 65 + http.Error(w, "failed to resolve user", http.StatusInternalServerError) 66 + return 67 + } 68 + repo := r.Context().Value("repo").(*models.Repo) 69 + 70 + scheme := "https" 71 + if s.config.Core.Dev { 72 + scheme = "http" 73 + } 74 + 75 + targetURL := fmt.Sprintf("%s://%s/%s/%s/git-receive-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 76 + s.proxyRequest(w, r, targetURL) 77 + } 78 + 79 + func (s *State) proxyRequest(w http.ResponseWriter, r *http.Request, targetURL string) { 80 + client := &http.Client{} 81 + 82 + // Create new request 83 + proxyReq, err := http.NewRequest(r.Method, targetURL, r.Body) 84 + if err != nil { 85 + http.Error(w, err.Error(), http.StatusInternalServerError) 86 + return 87 + } 88 + 89 + // Copy original headers 90 + proxyReq.Header = r.Header 91 + 92 + repoOwnerHandle := chi.URLParam(r, "user") 93 + proxyReq.Header.Add("x-tangled-repo-owner-handle", repoOwnerHandle) 94 + 95 + // Execute request 96 + resp, err := client.Do(proxyReq) 97 + if err != nil { 98 + http.Error(w, err.Error(), http.StatusInternalServerError) 99 + return 100 + } 101 + defer resp.Body.Close() 102 + 103 + // Copy response headers 104 + maps.Copy(w.Header(), resp.Header) 105 + 106 + // Set response status code 107 + w.WriteHeader(resp.StatusCode) 108 + 109 + // Copy response body 110 + if _, err := io.Copy(w, resp.Body); err != nil { 111 + http.Error(w, err.Error(), http.StatusInternalServerError) 112 + return 113 + } 114 + }
-185
appview/state/proxy_knot.go
··· 1 - package state 2 - 3 - import ( 4 - "fmt" 5 - "io" 6 - "maps" 7 - "net/http" 8 - "strings" 9 - 10 - "github.com/bluesky-social/indigo/atproto/identity" 11 - indigoxrpc "github.com/bluesky-social/indigo/xrpc" 12 - "github.com/go-chi/chi/v5" 13 - "github.com/go-git/go-git/v5/plumbing" 14 - "github.com/hashicorp/go-version" 15 - "tangled.org/core/api/tangled" 16 - "tangled.org/core/appview/models" 17 - xrpcclient "tangled.org/core/appview/xrpcclient" 18 - ) 19 - 20 - func (s *State) InfoRefs(w http.ResponseWriter, r *http.Request) { 21 - user := r.Context().Value("resolvedId").(identity.Identity) 22 - repo := r.Context().Value("repo").(*models.Repo) 23 - 24 - scheme := "https" 25 - if s.config.Core.Dev { 26 - scheme = "http" 27 - } 28 - 29 - targetURL := fmt.Sprintf("%s://%s/%s/%s/info/refs?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 30 - s.proxyRequest(w, r, targetURL) 31 - 32 - } 33 - 34 - func (s *State) UploadArchive(w http.ResponseWriter, r *http.Request) { 35 - user, ok := r.Context().Value("resolvedId").(identity.Identity) 36 - if !ok { 37 - http.Error(w, "failed to resolve user", http.StatusInternalServerError) 38 - return 39 - } 40 - repo := r.Context().Value("repo").(*models.Repo) 41 - 42 - scheme := "https" 43 - if s.config.Core.Dev { 44 - scheme = "http" 45 - } 46 - 47 - targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-archive?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 48 - s.proxyRequest(w, r, targetURL) 49 - } 50 - 51 - func (s *State) UploadPack(w http.ResponseWriter, r *http.Request) { 52 - user, ok := r.Context().Value("resolvedId").(identity.Identity) 53 - if !ok { 54 - http.Error(w, "failed to resolve user", http.StatusInternalServerError) 55 - return 56 - } 57 - repo := r.Context().Value("repo").(*models.Repo) 58 - 59 - scheme := "https" 60 - if s.config.Core.Dev { 61 - scheme = "http" 62 - } 63 - 64 - targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 65 - s.proxyRequest(w, r, targetURL) 66 - } 67 - 68 - func (s *State) ReceivePack(w http.ResponseWriter, r *http.Request) { 69 - user, ok := r.Context().Value("resolvedId").(identity.Identity) 70 - if !ok { 71 - http.Error(w, "failed to resolve user", http.StatusInternalServerError) 72 - return 73 - } 74 - repo := r.Context().Value("repo").(*models.Repo) 75 - 76 - scheme := "https" 77 - if s.config.Core.Dev { 78 - scheme = "http" 79 - } 80 - 81 - targetURL := fmt.Sprintf("%s://%s/%s/%s/git-receive-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 82 - s.proxyRequest(w, r, targetURL) 83 - } 84 - 85 - var knotVersionDownloadArchiveConstraint = version.MustConstraints(version.NewConstraint(">= 1.12.0-alpha")) 86 - 87 - func (s *State) DownloadArchive(w http.ResponseWriter, r *http.Request) { 88 - l := s.logger.With("handler", "DownloadArchive") 89 - ref := chi.URLParam(r, "ref") 90 - 91 - user, ok := r.Context().Value("resolvedId").(identity.Identity) 92 - if !ok { 93 - l.Error("failed to resolve user") 94 - http.Error(w, "failed to resolve user", http.StatusInternalServerError) 95 - return 96 - } 97 - repo := r.Context().Value("repo").(*models.Repo) 98 - 99 - scheme := "https" 100 - if s.config.Core.Dev { 101 - scheme = "http" 102 - } 103 - 104 - host := fmt.Sprintf("%s://%s", scheme, repo.Knot) 105 - xrpcc := &indigoxrpc.Client{ 106 - Host: host, 107 - } 108 - l = l.With("knot", repo.Knot) 109 - 110 - isCompatible := func() bool { 111 - out, err := tangled.KnotVersion(r.Context(), xrpcc) 112 - if err != nil { 113 - l.Warn("failed to get knot version", "err", err) 114 - return false 115 - } 116 - 117 - v, err := version.NewVersion(out.Version) 118 - if err != nil { 119 - l.Warn("failed to parse knot version", "version", out.Version, "err", err) 120 - return false 121 - } 122 - 123 - if !knotVersionDownloadArchiveConstraint.Check(v) { 124 - l.Warn("knot version incompatible.", "version", v) 125 - return false 126 - } 127 - return true 128 - }() 129 - l.Debug("knot compatibility check", "isCompatible", isCompatible) 130 - if isCompatible { 131 - targetURL := fmt.Sprintf("%s://%s/%s/%s/archive/%s", scheme, repo.Knot, user.DID, repo.Name, ref) 132 - s.proxyRequest(w, r, targetURL) 133 - } else { 134 - l.Debug("requesting xrpc/sh.tangled.repo.archive") 135 - archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", ref, repo.DidSlashRepo()) 136 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 137 - l.Error("failed to call XRPC repo.archive", "err", xrpcerr) 138 - s.pages.Error503(w) 139 - return 140 - } 141 - safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-") 142 - filename := fmt.Sprintf("%s-%s.tar.gz", repo.Name, safeRefFilename) 143 - w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) 144 - w.Header().Set("Content-Type", "application/gzip") 145 - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(archiveBytes))) 146 - w.Write(archiveBytes) 147 - } 148 - } 149 - 150 - func (s *State) proxyRequest(w http.ResponseWriter, r *http.Request, targetURL string) { 151 - client := &http.Client{} 152 - 153 - // Create new request 154 - proxyReq, err := http.NewRequest(r.Method, targetURL, r.Body) 155 - if err != nil { 156 - http.Error(w, err.Error(), http.StatusInternalServerError) 157 - return 158 - } 159 - 160 - // Copy original headers 161 - proxyReq.Header = r.Header 162 - 163 - repoOwnerHandle := chi.URLParam(r, "user") 164 - proxyReq.Header.Add("x-tangled-repo-owner-handle", repoOwnerHandle) 165 - 166 - // Execute request 167 - resp, err := client.Do(proxyReq) 168 - if err != nil { 169 - http.Error(w, err.Error(), http.StatusInternalServerError) 170 - return 171 - } 172 - defer resp.Body.Close() 173 - 174 - // Copy response headers 175 - maps.Copy(w.Header(), resp.Header) 176 - 177 - // Set response status code 178 - w.WriteHeader(resp.StatusCode) 179 - 180 - // Copy response body 181 - if _, err := io.Copy(w, resp.Body); err != nil { 182 - http.Error(w, err.Error(), http.StatusInternalServerError) 183 - return 184 - } 185 - }
+1 -3
appview/state/router.go
··· 104 104 r.Post("/git-upload-archive", s.UploadArchive) 105 105 r.Post("/git-upload-pack", s.UploadPack) 106 106 r.Post("/git-receive-pack", s.ReceivePack) 107 - // intentionally doesn't use /* as this isn't 108 - // a file path 109 - r.Get("/archive/{ref}", s.DownloadArchive) 107 + 110 108 }) 111 109 }) 112 110
-1
go.mod
··· 131 131 github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect 132 132 github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect 133 133 github.com/hashicorp/go-sockaddr v1.0.7 // indirect 134 - github.com/hashicorp/go-version v1.8.0 // indirect 135 134 github.com/hashicorp/golang-lru v1.0.2 // indirect 136 135 github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 137 136 github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
-2
go.sum
··· 264 264 github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= 265 265 github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw= 266 266 github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw= 267 - github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= 268 - github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 269 267 github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= 270 268 github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 271 269 github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
+19
input.css
··· 89 89 @apply no-underline text-black hover:underline hover:text-gray-800 dark:text-white dark:hover:text-gray-300; 90 90 } 91 91 92 + #navigation-menu-popover li:not(:last-of-type) a { 93 + @apply flex gap-2 items-center px-2 pb-2 pt-1.5 rounded-sm hover:bg-green-50 hover:text-green-700 no-underline dark:hover:text-green-50 dark:hover:bg-green-700; 94 + } 95 + 96 + a[hx-post="/logout"] { 97 + @apply no-underline; 98 + } 99 + 92 100 label { 93 101 @apply block text-gray-900 text-sm font-bold py-2 uppercase dark:text-gray-100; 94 102 } ··· 962 970 color: #f9fafb; 963 971 } 964 972 } 973 + 974 + .site-navigation-dropdown-trigger { 975 + anchor-name: --dropdown-trigger; 976 + } 977 + 978 + .site-navigation-popover { 979 + margin: 0; 980 + inset: auto; 981 + position-anchor: --dropdown-trigger; 982 + position-area: bottom left; 983 + }
-69
knotserver/archive.go
··· 1 - package knotserver 2 - 3 - import ( 4 - "compress/gzip" 5 - "fmt" 6 - "net/http" 7 - "strings" 8 - 9 - securejoin "github.com/cyphar/filepath-securejoin" 10 - "github.com/go-chi/chi/v5" 11 - "github.com/go-git/go-git/v5/plumbing" 12 - "tangled.org/core/knotserver/git" 13 - ) 14 - 15 - func (h *Knot) Archive(w http.ResponseWriter, r *http.Request) { 16 - var ( 17 - did = chi.URLParam(r, "did") 18 - name = chi.URLParam(r, "name") 19 - ref = chi.URLParam(r, "ref") 20 - ) 21 - repo, err := securejoin.SecureJoin(did, name) 22 - if err != nil { 23 - gitError(w, "repository not found", http.StatusNotFound) 24 - h.l.Error("git: failed to secure join repo path", "handler", "InfoRefs", "error", err) 25 - return 26 - } 27 - 28 - repoPath, err := securejoin.SecureJoin(h.c.Repo.ScanPath, repo) 29 - if err != nil { 30 - gitError(w, "repository not found", http.StatusNotFound) 31 - h.l.Error("git: failed to secure join repo path", "handler", "InfoRefs", "error", err) 32 - return 33 - } 34 - 35 - gr, err := git.Open(repoPath, ref) 36 - 37 - immutableLink := fmt.Sprintf( 38 - "https://%s/%s/%s/archive/%s", 39 - h.c.Server.Hostname, 40 - did, 41 - name, 42 - gr.Hash(), 43 - ) 44 - 45 - safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-") 46 - filename := fmt.Sprintf("%s-%s.tar.gz", name, safeRefFilename) 47 - w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) 48 - w.Header().Set("Content-Type", "application/gzip") 49 - w.Header().Set("Link", fmt.Sprintf("<%s>; rel=\"immutable\"", immutableLink)) 50 - 51 - gw := gzip.NewWriter(w) 52 - defer gw.Close() 53 - 54 - err = gr.WriteTar(gw, "") 55 - if err != nil { 56 - // once we start writing to the body we can't report error anymore 57 - // so we are only left with logging the error 58 - h.l.Error("writing tar file", "error", err) 59 - return 60 - } 61 - 62 - err = gw.Flush() 63 - if err != nil { 64 - // once we start writing to the body we can't report error anymore 65 - // so we are only left with logging the error 66 - h.l.Error("flushing", "error", err.Error()) 67 - return 68 - } 69 - }
-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 - 83 79 // re-open a repository and update references 84 80 func (g *GitRepo) Refresh() error { 85 81 refreshed, err := PlainOpen(g.path)
-2
knotserver/router.go
··· 85 85 r.Post("/git-upload-archive", h.UploadArchive) 86 86 r.Post("/git-upload-pack", h.UploadPack) 87 87 r.Post("/git-receive-pack", h.ReceivePack) 88 - // convenience routes 89 - r.Get("/archive/{ref}", h.Archive) 90 88 }) 91 89 }) 92 90
+81
knotserver/xrpc/repo_archive.go
··· 1 + package xrpc 2 + 3 + import ( 4 + "compress/gzip" 5 + "fmt" 6 + "net/http" 7 + "strings" 8 + 9 + "github.com/go-git/go-git/v5/plumbing" 10 + 11 + "tangled.org/core/knotserver/git" 12 + xrpcerr "tangled.org/core/xrpc/errors" 13 + ) 14 + 15 + func (x *Xrpc) RepoArchive(w http.ResponseWriter, r *http.Request) { 16 + repo := r.URL.Query().Get("repo") 17 + repoPath, err := x.parseRepoParam(repo) 18 + if err != nil { 19 + writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest) 20 + return 21 + } 22 + 23 + ref := r.URL.Query().Get("ref") 24 + // ref can be empty (git.Open handles this) 25 + 26 + format := r.URL.Query().Get("format") 27 + if format == "" { 28 + format = "tar.gz" // default 29 + } 30 + 31 + prefix := r.URL.Query().Get("prefix") 32 + 33 + if format != "tar.gz" { 34 + writeError(w, xrpcerr.NewXrpcError( 35 + xrpcerr.WithTag("InvalidRequest"), 36 + xrpcerr.WithMessage("only tar.gz format is supported"), 37 + ), http.StatusBadRequest) 38 + return 39 + } 40 + 41 + gr, err := git.Open(repoPath, ref) 42 + if err != nil { 43 + writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound) 44 + return 45 + } 46 + 47 + repoParts := strings.Split(repo, "/") 48 + repoName := repoParts[len(repoParts)-1] 49 + 50 + safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-") 51 + 52 + var archivePrefix string 53 + if prefix != "" { 54 + archivePrefix = prefix 55 + } else { 56 + archivePrefix = fmt.Sprintf("%s-%s", repoName, safeRefFilename) 57 + } 58 + 59 + filename := fmt.Sprintf("%s-%s.tar.gz", repoName, safeRefFilename) 60 + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) 61 + w.Header().Set("Content-Type", "application/gzip") 62 + 63 + gw := gzip.NewWriter(w) 64 + defer gw.Close() 65 + 66 + err = gr.WriteTar(gw, archivePrefix) 67 + if err != nil { 68 + // once we start writing to the body we can't report error anymore 69 + // so we are only left with logging the error 70 + x.Logger.Error("writing tar file", "error", err.Error()) 71 + return 72 + } 73 + 74 + err = gw.Flush() 75 + if err != nil { 76 + // once we start writing to the body we can't report error anymore 77 + // so we are only left with logging the error 78 + x.Logger.Error("flushing", "error", err.Error()) 79 + return 80 + } 81 + }
+1
knotserver/xrpc/xrpc.go
··· 64 64 r.Get("/"+tangled.RepoCompareNSID, x.RepoCompare) 65 65 r.Get("/"+tangled.RepoGetDefaultBranchNSID, x.RepoGetDefaultBranch) 66 66 r.Get("/"+tangled.RepoBranchNSID, x.RepoBranch) 67 + r.Get("/"+tangled.RepoArchiveNSID, x.RepoArchive) 67 68 r.Get("/"+tangled.RepoLanguagesNSID, x.RepoLanguages) 68 69 69 70 // knot query endpoints (no auth required)
-1
lexicons/repo/archive.json
··· 4 4 "defs": { 5 5 "main": { 6 6 "type": "query", 7 - "description": "deprecated. use `/{did}/{reponame}/archive/{ref} endpoint instead", 8 7 "parameters": { 9 8 "type": "params", 10 9 "required": ["repo", "ref"],
-3
nix/gomod2nix.toml
··· 304 304 [mod."github.com/hashicorp/go-sockaddr"] 305 305 version = "v1.0.7" 306 306 hash = "sha256-p6eDOrGzN1jMmT/F/f/VJMq0cKNFhUcEuVVwTE6vSrs=" 307 - [mod."github.com/hashicorp/go-version"] 308 - version = "v1.8.0" 309 - hash = "sha256-KXtqERmYrWdpqPCViWcHbe6jnuH7k16bvBIcuJuevj8=" 310 307 [mod."github.com/hashicorp/golang-lru"] 311 308 version = "v1.0.2" 312 309 hash = "sha256-yy+5botc6T5wXgOe2mfNXJP3wr+MkVlUZ2JBkmmrA48="