Files
studio/routers/web/repo/preview.go
T
ozan 715f54e39d simplify: drop SWR cache, always upload fresh to S3
Always overwrites S3 on every preview request — no stale content.
Removed sync.Map cache and background goroutines.
Blob-by-SHA paths still skip upload if already exists (immutable).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 01:01:50 +01:00

152 lines
3.6 KiB
Go

// Copyright 2026 The Tinqs Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"fmt"
"io"
"net/http"
"os"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
)
// HTMLPreview serves an HTML file via S3.
// Always uploads the current blob (overwrite), then redirects to presigned S3 URL.
func HTMLPreview(ctx *context.Context) {
if !setting.HTMLPreview.Enabled {
ctx.NotFound(fmt.Errorf("HTML preview is not enabled"))
return
}
treePath := ctx.Repo.TreePath
if !isHTMLTreePath(treePath) {
ctx.NotFound(fmt.Errorf("not an HTML file"))
return
}
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treePath)
if err != nil {
if git.IsErrNotExist(err) {
ctx.NotFound(err)
} else {
ctx.ServerError("GetTreeEntryByPath", err)
}
return
}
if entry.IsDir() || entry.IsSubModule() {
ctx.NotFound(nil)
return
}
blob := entry.Blob()
s3Key := buildPreviewS3Key(ctx)
if err := uploadPreview(s3Key, blob); err != nil {
ctx.ServerError("uploadPreview", err)
return
}
redirectToS3(ctx, s3Key, treePath)
}
// HTMLPreviewByID serves an HTML preview by blob SHA (for diff views).
func HTMLPreviewByID(ctx *context.Context) {
if !setting.HTMLPreview.Enabled {
ctx.NotFound(fmt.Errorf("HTML preview is not enabled"))
return
}
blob, err := ctx.Repo.GitRepo.GetBlob(ctx.PathParam("sha"))
if err != nil {
if git.IsErrNotExist(err) {
ctx.NotFound(nil)
} else {
ctx.ServerError("GetBlob", err)
}
return
}
blobSHA := blob.ID.String()
s3Key := fmt.Sprintf("blobs/%s", blobSHA)
// Content-addressed: same SHA = same content, skip if exists
_, err = storage.HTMLPreview.Stat(s3Key)
if err == nil {
redirectToS3(ctx, s3Key, blobSHA+".html")
return
}
if err := uploadPreview(s3Key, blob); err != nil {
ctx.ServerError("uploadPreview", err)
return
}
redirectToS3(ctx, s3Key, blobSHA+".html")
}
func buildPreviewS3Key(ctx *context.Context) string {
owner := ctx.Repo.Repository.OwnerName
repo := ctx.Repo.Repository.Name
ref := ctx.Repo.RefFullName.ShortName()
path := util.PathEscapeSegments(ctx.Repo.TreePath)
return fmt.Sprintf("%s/%s/%s/%s", owner, repo, ref, path)
}
func redirectToS3(ctx *context.Context, s3Key, name string) {
u, err := storage.HTMLPreview.ServeDirectURL(s3Key, name, http.MethodGet, &storage.ServeDirectOptions{
ContentType: "text/html; charset=utf-8",
})
if err != nil {
servePreviewDirect(ctx, s3Key)
return
}
ctx.Redirect(u.String())
}
func servePreviewDirect(ctx *context.Context, s3Key string) {
obj, err := storage.HTMLPreview.Open(s3Key)
if err != nil {
if os.IsNotExist(err) {
ctx.NotFound(err)
} else {
ctx.ServerError("Open preview", err)
}
return
}
defer obj.Close()
info, err := obj.Stat()
if err != nil {
ctx.ServerError("Stat preview", err)
return
}
ctx.Resp.Header().Set("Content-Type", "text/html; charset=utf-8")
ctx.Resp.Header().Set("Content-Length", fmt.Sprintf("%d", info.Size()))
ctx.Resp.Header().Set("X-Content-Type-Options", "nosniff")
_, _ = io.Copy(ctx.Resp, obj)
}
func uploadPreview(s3Key string, blob *git.Blob) error {
dataRc, err := blob.DataAsync()
if err != nil {
return fmt.Errorf("blob DataAsync: %w", err)
}
defer dataRc.Close()
_, err = storage.HTMLPreview.Save(s3Key, dataRc, blob.Size())
if err != nil {
return fmt.Errorf("S3 save: %w", err)
}
log.Debug("HTMLPreview: uploaded %s (%d bytes)", s3Key, blob.Size())
return nil
}
// isHTMLTreePath is defined in view_file.go