From c15dfcdca57cb91d6ecb38031a1a139986a40da6 Mon Sep 17 00:00:00 2001 From: Anirudh Oppiliappan Date: Fri, 1 Aug 2025 16:58:42 +0300 Subject: [PATCH] knotserver: also serve text/plain in BlobRaw Change-Id: qwknlvxtkwzvltqqtrxoxpxtnszywyyn Signed-off-by: Anirudh Oppiliappan --- knotserver/routes.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/knotserver/routes.go b/knotserver/routes.go index 8b37982..ba0afa3 100644 --- a/knotserver/routes.go +++ b/knotserver/routes.go @@ -286,9 +286,17 @@ func (h *Handle) BlobRaw(w http.ResponseWriter, r *http.Request) { mimeType = "image/svg+xml" } - if !strings.HasPrefix(mimeType, "image/") && !strings.HasPrefix(mimeType, "video/") { - l.Error("attempted to serve non-image/video file", "mimetype", mimeType) - writeError(w, "only image and video files can be accessed directly", http.StatusForbidden) + // allow image, video, and text/plain files to be served directly + switch { + case strings.HasPrefix(mimeType, "image/"): + // allowed + case strings.HasPrefix(mimeType, "video/"): + // allowed + case strings.HasPrefix(mimeType, "text/plain"): + // allowed + default: + l.Error("attempted to serve disallowed file type", "mimetype", mimeType) + writeError(w, "only image, video, and text files can be accessed directly", http.StatusForbidden) return } -- 2.43.0 From 48062a520538e9df2003ed8c4da4b83245f86b49 Mon Sep 17 00:00:00 2001 From: Anirudh Oppiliappan Date: Fri, 1 Aug 2025 16:58:42 +0300 Subject: [PATCH] appview/{pages,repo}: show previews for image/ and video/ mimetypes Change-Id: svtkmrzmrwkyxpvlzszltptlmnslyqxt Signed-off-by: Anirudh Oppiliappan --- appview/pages/markup/camo.go | 4 +- appview/pages/pages.go | 4 ++ appview/pages/templates/repo/blob.html | 25 +++++++--- appview/repo/repo.go | 65 ++++++++++++++++++++------ 4 files changed, 77 insertions(+), 21 deletions(-) diff --git a/appview/pages/markup/camo.go b/appview/pages/markup/camo.go index 9995f72..65d108e 100644 --- a/appview/pages/markup/camo.go +++ b/appview/pages/markup/camo.go @@ -9,7 +9,7 @@ import ( "github.com/yuin/goldmark/ast" ) -func generateCamoURL(baseURL, secret, imageURL string) string { +func GenerateCamoURL(baseURL, secret, imageURL string) string { h := hmac.New(sha256.New, []byte(secret)) h.Write([]byte(imageURL)) signature := hex.EncodeToString(h.Sum(nil)) @@ -24,7 +24,7 @@ func (rctx *RenderContext) camoImageLinkTransformer(dst string) string { } if rctx.CamoUrl != "" && rctx.CamoSecret != "" { - return generateCamoURL(rctx.CamoUrl, rctx.CamoSecret, dst) + return GenerateCamoURL(rctx.CamoUrl, rctx.CamoSecret, dst) } return dst diff --git a/appview/pages/pages.go b/appview/pages/pages.go index 88c13b6..c8f6e4e 100644 --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -624,6 +624,10 @@ type RepoBlobParams struct { LoggedInUser *oauth.User RepoInfo repoinfo.RepoInfo Active string + Unsupported bool + IsImage bool + IsVideo bool + ContentSrc string BreadCrumbs [][]string ShowRendered bool RenderToggle bool diff --git a/appview/pages/templates/repo/blob.html b/appview/pages/templates/repo/blob.html index ff4fce5..89280c3 100644 --- a/appview/pages/templates/repo/blob.html +++ b/appview/pages/templates/repo/blob.html @@ -5,9 +5,9 @@ {{ $title := printf "%s at %s · %s" .Path .Ref .RepoInfo.FullName }} {{ $url := printf "https://tangled.sh/%s/blob/%s/%s" .RepoInfo.FullName .Ref .Path }} - + {{ template "repo/fragments/og" (dict "RepoInfo" .RepoInfo "Title" $title "Url" $url) }} - + {{ end }} {{ define "repoContent" }} @@ -44,18 +44,31 @@ view raw {{ if .RenderToggle }} - view {{ if .ShowRendered }}code{{ else }}rendered{{ end }} {{ end }} - {{ if .IsBinary }} + {{ if and .IsBinary .Unsupported }}

- This is a binary file and will not be displayed. + Previews are not supported for this file type.

+ {{ else if .IsBinary }} +
+ {{ if .IsImage }} + {{ .Path }} + {{ else if .IsVideo }} + + {{ end }} +
{{ else }}
{{ if .ShowRendered }} diff --git a/appview/repo/repo.go b/appview/repo/repo.go index 3cf93b8..b8810fd 100644 --- a/appview/repo/repo.go +++ b/appview/repo/repo.go @@ -10,6 +10,7 @@ import ( "log" "net/http" "net/url" + "path/filepath" "slices" "strconv" "strings" @@ -532,6 +533,31 @@ func (rp *Repo) RepoBlob(w http.ResponseWriter, r *http.Request) { showRendered = r.URL.Query().Get("code") != "true" } + var unsupported bool + var isImage bool + var isVideo bool + var contentSrc string + + if result.IsBinary { + ext := strings.ToLower(filepath.Ext(result.Path)) + switch ext { + case ".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp": + isImage = true + case ".mp4", ".webm", ".ogg", ".mov", ".avi": + isVideo = true + default: + unsupported = true + } + + // fetch the actual binary content like in RepoBlobRaw + + blobURL := fmt.Sprintf("%s://%s/%s/%s/raw/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath) + contentSrc = blobURL + if !rp.config.Core.Dev { + contentSrc = markup.GenerateCamoURL(rp.config.Camo.Host, rp.config.Camo.SharedSecret, blobURL) + } + } + user := rp.oauth.GetUser(r) rp.pages.RepoBlob(w, pages.RepoBlobParams{ LoggedInUser: user, @@ -540,6 +566,10 @@ func (rp *Repo) RepoBlob(w http.ResponseWriter, r *http.Request) { BreadCrumbs: breadcrumbs, ShowRendered: showRendered, RenderToggle: renderToggle, + Unsupported: unsupported, + IsImage: isImage, + IsVideo: isVideo, + ContentSrc: contentSrc, }) } @@ -547,6 +577,7 @@ func (rp *Repo) RepoBlobRaw(w http.ResponseWriter, r *http.Request) { f, err := rp.repoResolver.Resolve(r) if err != nil { log.Println("failed to get repo and knot", err) + w.WriteHeader(http.StatusBadRequest) return } @@ -557,33 +588,41 @@ func (rp *Repo) RepoBlobRaw(w http.ResponseWriter, r *http.Request) { if !rp.config.Core.Dev { protocol = "https" } - resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)) + blobURL := fmt.Sprintf("%s://%s/%s/%s/raw/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath) + resp, err := http.Get(blobURL) if err != nil { - log.Println("failed to reach knotserver", err) + log.Println("failed to reach knotserver:", err) + rp.pages.Error503(w) return } + defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) - if err != nil { - log.Printf("Error reading response body: %v", err) + if resp.StatusCode != http.StatusOK { + log.Printf("knotserver returned non-OK status for raw blob %s: %d", blobURL, resp.StatusCode) + w.WriteHeader(resp.StatusCode) + _, _ = io.Copy(w, resp.Body) return } - var result types.RepoBlobResponse - err = json.Unmarshal(body, &result) + contentType := resp.Header.Get("Content-Type") + body, err := io.ReadAll(resp.Body) if err != nil { - log.Println("failed to parse response:", err) + log.Printf("error reading response body from knotserver: %v", err) + w.WriteHeader(http.StatusInternalServerError) return } - if result.IsBinary { - w.Header().Set("Content-Type", "application/octet-stream") + if strings.Contains(contentType, "text/plain") { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Write(body) + } else if strings.HasPrefix(contentType, "image/") || strings.HasPrefix(contentType, "video/") { + w.Header().Set("Content-Type", contentType) + w.Write(body) + } else { + w.WriteHeader(http.StatusUnsupportedMediaType) + w.Write([]byte("unsupported content type")) return } - - w.Header().Set("Content-Type", "text/plain; charset=utf-8") - w.Write([]byte(result.Contents)) } // modify the spindle configured for this repo -- 2.43.0