Monorepo for Tangled tangled.org

appview/{pages,repo}: show previews for image/ and video/ mimetypes

Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.sh>

anirudh.fi 48062a52 c15dfcdc

verified
Changed files
+77 -21
appview
pages
markup
templates
repo
repo
+2 -2
appview/pages/markup/camo.go
··· 9 "github.com/yuin/goldmark/ast" 10 ) 11 12 - func generateCamoURL(baseURL, secret, imageURL string) string { 13 h := hmac.New(sha256.New, []byte(secret)) 14 h.Write([]byte(imageURL)) 15 signature := hex.EncodeToString(h.Sum(nil)) ··· 24 } 25 26 if rctx.CamoUrl != "" && rctx.CamoSecret != "" { 27 - return generateCamoURL(rctx.CamoUrl, rctx.CamoSecret, dst) 28 } 29 30 return dst
··· 9 "github.com/yuin/goldmark/ast" 10 ) 11 12 + func GenerateCamoURL(baseURL, secret, imageURL string) string { 13 h := hmac.New(sha256.New, []byte(secret)) 14 h.Write([]byte(imageURL)) 15 signature := hex.EncodeToString(h.Sum(nil)) ··· 24 } 25 26 if rctx.CamoUrl != "" && rctx.CamoSecret != "" { 27 + return GenerateCamoURL(rctx.CamoUrl, rctx.CamoSecret, dst) 28 } 29 30 return dst
+4
appview/pages/pages.go
··· 624 LoggedInUser *oauth.User 625 RepoInfo repoinfo.RepoInfo 626 Active string 627 BreadCrumbs [][]string 628 ShowRendered bool 629 RenderToggle bool
··· 624 LoggedInUser *oauth.User 625 RepoInfo repoinfo.RepoInfo 626 Active string 627 + Unsupported bool 628 + IsImage bool 629 + IsVideo bool 630 + ContentSrc string 631 BreadCrumbs [][]string 632 ShowRendered bool 633 RenderToggle bool
+19 -6
appview/pages/templates/repo/blob.html
··· 5 6 {{ $title := printf "%s at %s &middot; %s" .Path .Ref .RepoInfo.FullName }} 7 {{ $url := printf "https://tangled.sh/%s/blob/%s/%s" .RepoInfo.FullName .Ref .Path }} 8 - 9 {{ template "repo/fragments/og" (dict "RepoInfo" .RepoInfo "Title" $title "Url" $url) }} 10 - 11 {{ end }} 12 13 {{ define "repoContent" }} ··· 44 <a href="/{{ .RepoInfo.FullName }}/raw/{{ .Ref }}/{{ .Path }}">view raw</a> 45 {{ if .RenderToggle }} 46 <span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span> 47 - <a 48 - href="/{{ .RepoInfo.FullName }}/blob/{{ .Ref }}/{{ .Path }}?code={{ .ShowRendered }}" 49 hx-boost="true" 50 >view {{ if .ShowRendered }}code{{ else }}rendered{{ end }}</a> 51 {{ end }} 52 </div> 53 </div> 54 </div> 55 - {{ if .IsBinary }} 56 <p class="text-center text-gray-400 dark:text-gray-500"> 57 - This is a binary file and will not be displayed. 58 </p> 59 {{ else }} 60 <div class="overflow-auto relative"> 61 {{ if .ShowRendered }}
··· 5 6 {{ $title := printf "%s at %s &middot; %s" .Path .Ref .RepoInfo.FullName }} 7 {{ $url := printf "https://tangled.sh/%s/blob/%s/%s" .RepoInfo.FullName .Ref .Path }} 8 + 9 {{ template "repo/fragments/og" (dict "RepoInfo" .RepoInfo "Title" $title "Url" $url) }} 10 + 11 {{ end }} 12 13 {{ define "repoContent" }} ··· 44 <a href="/{{ .RepoInfo.FullName }}/raw/{{ .Ref }}/{{ .Path }}">view raw</a> 45 {{ if .RenderToggle }} 46 <span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span> 47 + <a 48 + href="/{{ .RepoInfo.FullName }}/blob/{{ .Ref }}/{{ .Path }}?code={{ .ShowRendered }}" 49 hx-boost="true" 50 >view {{ if .ShowRendered }}code{{ else }}rendered{{ end }}</a> 51 {{ end }} 52 </div> 53 </div> 54 </div> 55 + {{ if and .IsBinary .Unsupported }} 56 <p class="text-center text-gray-400 dark:text-gray-500"> 57 + Previews are not supported for this file type. 58 </p> 59 + {{ else if .IsBinary }} 60 + <div class="text-center"> 61 + {{ if .IsImage }} 62 + <img src="{{ .ContentSrc }}" 63 + alt="{{ .Path }}" 64 + class="max-w-full h-auto mx-auto border border-gray-200 dark:border-gray-700 rounded" /> 65 + {{ else if .IsVideo }} 66 + <video controls class="max-w-full h-auto mx-auto border border-gray-200 dark:border-gray-700 rounded"> 67 + <source src="{{ .ContentSrc }}"> 68 + Your browser does not support the video tag. 69 + </video> 70 + {{ end }} 71 + </div> 72 {{ else }} 73 <div class="overflow-auto relative"> 74 {{ if .ShowRendered }}
+52 -13
appview/repo/repo.go
··· 10 "log" 11 "net/http" 12 "net/url" 13 "slices" 14 "strconv" 15 "strings" ··· 532 showRendered = r.URL.Query().Get("code") != "true" 533 } 534 535 user := rp.oauth.GetUser(r) 536 rp.pages.RepoBlob(w, pages.RepoBlobParams{ 537 LoggedInUser: user, ··· 540 BreadCrumbs: breadcrumbs, 541 ShowRendered: showRendered, 542 RenderToggle: renderToggle, 543 }) 544 } 545 ··· 547 f, err := rp.repoResolver.Resolve(r) 548 if err != nil { 549 log.Println("failed to get repo and knot", err) 550 return 551 } 552 ··· 557 if !rp.config.Core.Dev { 558 protocol = "https" 559 } 560 - resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)) 561 if err != nil { 562 - log.Println("failed to reach knotserver", err) 563 return 564 } 565 566 - body, err := io.ReadAll(resp.Body) 567 - if err != nil { 568 - log.Printf("Error reading response body: %v", err) 569 return 570 } 571 572 - var result types.RepoBlobResponse 573 - err = json.Unmarshal(body, &result) 574 if err != nil { 575 - log.Println("failed to parse response:", err) 576 return 577 } 578 579 - if result.IsBinary { 580 - w.Header().Set("Content-Type", "application/octet-stream") 581 w.Write(body) 582 return 583 } 584 - 585 - w.Header().Set("Content-Type", "text/plain; charset=utf-8") 586 - w.Write([]byte(result.Contents)) 587 } 588 589 // modify the spindle configured for this repo
··· 10 "log" 11 "net/http" 12 "net/url" 13 + "path/filepath" 14 "slices" 15 "strconv" 16 "strings" ··· 533 showRendered = r.URL.Query().Get("code") != "true" 534 } 535 536 + var unsupported bool 537 + var isImage bool 538 + var isVideo bool 539 + var contentSrc string 540 + 541 + if result.IsBinary { 542 + ext := strings.ToLower(filepath.Ext(result.Path)) 543 + switch ext { 544 + case ".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp": 545 + isImage = true 546 + case ".mp4", ".webm", ".ogg", ".mov", ".avi": 547 + isVideo = true 548 + default: 549 + unsupported = true 550 + } 551 + 552 + // fetch the actual binary content like in RepoBlobRaw 553 + 554 + blobURL := fmt.Sprintf("%s://%s/%s/%s/raw/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath) 555 + contentSrc = blobURL 556 + if !rp.config.Core.Dev { 557 + contentSrc = markup.GenerateCamoURL(rp.config.Camo.Host, rp.config.Camo.SharedSecret, blobURL) 558 + } 559 + } 560 + 561 user := rp.oauth.GetUser(r) 562 rp.pages.RepoBlob(w, pages.RepoBlobParams{ 563 LoggedInUser: user, ··· 566 BreadCrumbs: breadcrumbs, 567 ShowRendered: showRendered, 568 RenderToggle: renderToggle, 569 + Unsupported: unsupported, 570 + IsImage: isImage, 571 + IsVideo: isVideo, 572 + ContentSrc: contentSrc, 573 }) 574 } 575 ··· 577 f, err := rp.repoResolver.Resolve(r) 578 if err != nil { 579 log.Println("failed to get repo and knot", err) 580 + w.WriteHeader(http.StatusBadRequest) 581 return 582 } 583 ··· 588 if !rp.config.Core.Dev { 589 protocol = "https" 590 } 591 + blobURL := fmt.Sprintf("%s://%s/%s/%s/raw/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath) 592 + resp, err := http.Get(blobURL) 593 if err != nil { 594 + log.Println("failed to reach knotserver:", err) 595 + rp.pages.Error503(w) 596 return 597 } 598 + defer resp.Body.Close() 599 600 + if resp.StatusCode != http.StatusOK { 601 + log.Printf("knotserver returned non-OK status for raw blob %s: %d", blobURL, resp.StatusCode) 602 + w.WriteHeader(resp.StatusCode) 603 + _, _ = io.Copy(w, resp.Body) 604 return 605 } 606 607 + contentType := resp.Header.Get("Content-Type") 608 + body, err := io.ReadAll(resp.Body) 609 if err != nil { 610 + log.Printf("error reading response body from knotserver: %v", err) 611 + w.WriteHeader(http.StatusInternalServerError) 612 return 613 } 614 615 + if strings.Contains(contentType, "text/plain") { 616 + w.Header().Set("Content-Type", "text/plain; charset=utf-8") 617 w.Write(body) 618 + } else if strings.HasPrefix(contentType, "image/") || strings.HasPrefix(contentType, "video/") { 619 + w.Header().Set("Content-Type", contentType) 620 + w.Write(body) 621 + } else { 622 + w.WriteHeader(http.StatusUnsupportedMediaType) 623 + w.Write([]byte("unsupported content type")) 624 return 625 } 626 } 627 628 // modify the spindle configured for this repo