715f54e39d
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>
152 lines
3.6 KiB
Go
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
|