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>
This commit is contained in:
2026-05-21 01:01:50 +01:00
parent 01e862a238
commit 715f54e39d
+7 -53
View File
@@ -8,7 +8,6 @@ import (
"io" "io"
"net/http" "net/http"
"os" "os"
"sync"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
@@ -18,19 +17,8 @@ import (
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
) )
// previewCache tracks known blob SHAs on S3 to skip Stat calls. // HTMLPreview serves an HTML file via S3.
// Key: S3 object path, Value: blob SHA that was uploaded. // Always uploads the current blob (overwrite), then redirects to presigned S3 URL.
var previewCache sync.Map
// HTMLPreview serves an HTML file via S3 with stale-while-revalidate caching.
//
// Flow:
// 1. Resolve the git blob for the requested file path
// 2. Build the S3 object key: preview/{owner}/{repo}/{path}
// 3. Check in-memory cache for the blob SHA
// 4. HIT+fresh (SHA matches current blob) → redirect to S3 presigned URL
// 5. HIT+stale (SHA mismatch) → redirect to S3 immediately, revalidate async
// 6. MISS → upload synchronously, then redirect
func HTMLPreview(ctx *context.Context) { func HTMLPreview(ctx *context.Context) {
if !setting.HTMLPreview.Enabled { if !setting.HTMLPreview.Enabled {
ctx.NotFound(fmt.Errorf("HTML preview is not enabled")) ctx.NotFound(fmt.Errorf("HTML preview is not enabled"))
@@ -43,7 +31,6 @@ func HTMLPreview(ctx *context.Context) {
return return
} }
// Get the blob
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treePath) entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treePath)
if err != nil { if err != nil {
if git.IsErrNotExist(err) { if git.IsErrNotExist(err) {
@@ -59,34 +46,9 @@ func HTMLPreview(ctx *context.Context) {
} }
blob := entry.Blob() blob := entry.Blob()
blobSHA := blob.ID.String()
s3Key := buildPreviewS3Key(ctx) s3Key := buildPreviewS3Key(ctx)
// Check in-memory cache if err := uploadPreview(s3Key, blob); err != nil {
if cachedSHA, ok := previewCache.Load(s3Key); ok {
if cachedSHA.(string) == blobSHA {
// Cache HIT + fresh — redirect directly
redirectToS3(ctx, s3Key, treePath)
return
}
// Cache HIT + stale — redirect to old version, revalidate in background
go revalidatePreview(s3Key, blob, blobSHA)
redirectToS3(ctx, s3Key, treePath)
return
}
// Cache MISS — check if S3 has it (cold start)
_, err = storage.HTMLPreview.Stat(s3Key)
if err == nil {
// S3 has a version — serve it and revalidate in background
previewCache.Store(s3Key, "") // mark as known but unknown SHA
go revalidatePreview(s3Key, blob, blobSHA)
redirectToS3(ctx, s3Key, treePath)
return
}
// No cached version — upload synchronously
if err := uploadPreview(s3Key, blob, blobSHA); err != nil {
ctx.ServerError("uploadPreview", err) ctx.ServerError("uploadPreview", err)
return return
} }
@@ -111,16 +73,16 @@ func HTMLPreviewByID(ctx *context.Context) {
} }
blobSHA := blob.ID.String() blobSHA := blob.ID.String()
// For blob-by-SHA, use a content-addressed key (immutable)
s3Key := fmt.Sprintf("blobs/%s", blobSHA) s3Key := fmt.Sprintf("blobs/%s", blobSHA)
// Content-addressed: same SHA = same content, skip if exists
_, err = storage.HTMLPreview.Stat(s3Key) _, err = storage.HTMLPreview.Stat(s3Key)
if err == nil { if err == nil {
redirectToS3(ctx, s3Key, blobSHA+".html") redirectToS3(ctx, s3Key, blobSHA+".html")
return return
} }
if err := uploadPreview(s3Key, blob, blobSHA); err != nil { if err := uploadPreview(s3Key, blob); err != nil {
ctx.ServerError("uploadPreview", err) ctx.ServerError("uploadPreview", err)
return return
} }
@@ -140,7 +102,6 @@ func redirectToS3(ctx *context.Context, s3Key, name string) {
ContentType: "text/html; charset=utf-8", ContentType: "text/html; charset=utf-8",
}) })
if err != nil { if err != nil {
// Fallback: serve directly through Gitea
servePreviewDirect(ctx, s3Key) servePreviewDirect(ctx, s3Key)
return return
} }
@@ -171,7 +132,7 @@ func servePreviewDirect(ctx *context.Context, s3Key string) {
_, _ = io.Copy(ctx.Resp, obj) _, _ = io.Copy(ctx.Resp, obj)
} }
func uploadPreview(s3Key string, blob *git.Blob, blobSHA string) error { func uploadPreview(s3Key string, blob *git.Blob) error {
dataRc, err := blob.DataAsync() dataRc, err := blob.DataAsync()
if err != nil { if err != nil {
return fmt.Errorf("blob DataAsync: %w", err) return fmt.Errorf("blob DataAsync: %w", err)
@@ -183,15 +144,8 @@ func uploadPreview(s3Key string, blob *git.Blob, blobSHA string) error {
return fmt.Errorf("S3 save: %w", err) return fmt.Errorf("S3 save: %w", err)
} }
previewCache.Store(s3Key, blobSHA) log.Debug("HTMLPreview: uploaded %s (%d bytes)", s3Key, blob.Size())
log.Debug("HTMLPreview: uploaded %s (blob %s)", s3Key, blobSHA[:8])
return nil return nil
} }
func revalidatePreview(s3Key string, blob *git.Blob, blobSHA string) {
if err := uploadPreview(s3Key, blob, blobSHA); err != nil {
log.Error("HTMLPreview revalidate failed for %s: %v", s3Key, err)
}
}
// isHTMLTreePath is defined in view_file.go // isHTMLTreePath is defined in view_file.go