缩略图懒生成 + 评论登录限流:时间线不再拉原图
- 新增 /uploads/thumb/{key}?w=:首访从存储端读一次原图,标准库盒均值缩放后
落盘 data/.thumbnail_cache(按内容哈希命名,天然失效);失败写 .failed 冷却
一小时,非图片/超大一律 302 回原图,前端无感。原图路由改用 ServeContent,
补上视频拖动进度条所需的 Range 支持
- 前端 thumbURL/thumbifyHtml 接入两套 UI 的时间线配图、长文封面与短文正文,
并补 loading=lazy;详情页与灯箱仍用原图。/api/site 改为
{settings, uploads_public_base},让前端识别哪些直链属于自家存储
- 后台登录的滑动窗口限速器抽成 internal/ratelimit 共享包(调用面不变),
新增:读者登录失败按 IP 20 次/10 分钟、评论写入按读者 5 条/分钟
(站主豁免,且只计成功写入)
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
// 缩略图懒生成:GET /uploads/thumb/{key}?w=960
|
||||
//
|
||||
// 首次请求从存储端读一次原图,缩放编码后落盘缓存(DataDir/.thumbnail_cache),
|
||||
// 之后直接供缓存——时间线首屏不再拉原图。非图片 / 解码失败 / 冷却期内
|
||||
// 一律 302 回原图:前端 <img> 拿 302 是无感的。
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"oneblog/internal/httpx"
|
||||
"oneblog/internal/model"
|
||||
"oneblog/internal/storage"
|
||||
"oneblog/internal/store"
|
||||
"oneblog/internal/thumbs"
|
||||
)
|
||||
|
||||
const (
|
||||
thumbDefaultW = 960
|
||||
thumbMinW = 64
|
||||
thumbMaxW = 1600
|
||||
)
|
||||
|
||||
func (a *API) ThumbHandler() http.Handler {
|
||||
return http.HandlerFunc(a.thumb)
|
||||
}
|
||||
|
||||
func (a *API) thumb(w http.ResponseWriter, r *http.Request) {
|
||||
if a.Thumbs == nil {
|
||||
httpx.NotFound(w)
|
||||
return
|
||||
}
|
||||
key := strings.TrimPrefix(r.URL.Path, "/uploads/thumb/")
|
||||
if key == "" || strings.Contains(key, "..") {
|
||||
httpx.NotFound(w)
|
||||
return
|
||||
}
|
||||
width := thumbDefaultW
|
||||
if s := r.URL.Query().Get("w"); s != "" {
|
||||
if n, err := strconv.Atoi(s); err == nil && n >= thumbMinW && n <= thumbMaxW {
|
||||
width = n
|
||||
}
|
||||
}
|
||||
|
||||
f, err := a.Store.GetFileByKey(key)
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
httpx.NotFound(w)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpx.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !thumbs.Supported(f.Mime) || f.SHA256 == "" {
|
||||
a.thumbFallback(w, r, f)
|
||||
return
|
||||
}
|
||||
|
||||
// 快路径:缓存命中直接供(锁外)
|
||||
if p := a.Thumbs.FindCached(f.SHA256, width); p != "" {
|
||||
a.serveThumbFile(w, r, p, f)
|
||||
return
|
||||
}
|
||||
// 冷却期:近期失败过,不再尝试
|
||||
if a.Thumbs.FailedRecently(f.SHA256) {
|
||||
a.thumbFallback(w, r, f)
|
||||
return
|
||||
}
|
||||
|
||||
// 慢路径:同 key+宽度并发只生成一次,后来者等锁后读缓存
|
||||
lk := a.Thumbs.Lock(f.SHA256, width)
|
||||
lk.Lock()
|
||||
defer lk.Unlock()
|
||||
if p := a.Thumbs.FindCached(f.SHA256, width); p != "" {
|
||||
a.serveThumbFile(w, r, p, f)
|
||||
return
|
||||
}
|
||||
|
||||
rc, _, err := a.Blobs.Open(r.Context(), f.Key)
|
||||
if err != nil {
|
||||
a.Thumbs.MarkFailed(f.SHA256)
|
||||
a.thumbFallback(w, r, f)
|
||||
return
|
||||
}
|
||||
src, err := thumbs.ReadAllLimited(rc, thumbs.MaxSrcBytes)
|
||||
rc.Close()
|
||||
if err != nil {
|
||||
a.Thumbs.MarkFailed(f.SHA256)
|
||||
a.thumbFallback(w, r, f)
|
||||
return
|
||||
}
|
||||
out, outMime, _, err := thumbs.Generate(src, f.Mime, width)
|
||||
if err != nil {
|
||||
a.Thumbs.MarkFailed(f.SHA256)
|
||||
a.thumbFallback(w, r, f)
|
||||
return
|
||||
}
|
||||
p, err := a.Thumbs.Put(f.SHA256, width, out, outMime)
|
||||
if err != nil {
|
||||
// 写缓存失败不拖累本次响应:产物就在内存里
|
||||
a.serveThumbBytes(w, outMime, out, f)
|
||||
return
|
||||
}
|
||||
a.serveThumbFile(w, r, p, f)
|
||||
}
|
||||
|
||||
// thumbFallback 回原图:R2 + 公开域名是 302 直链,本地/未配域名回 /uploads/ 路由。
|
||||
func (a *API) thumbFallback(w http.ResponseWriter, r *http.Request, f model.File) {
|
||||
http.Redirect(w, r, storage.FileURL(f.Store, f.Key, a.Cfg.UploadsPublicBase), http.StatusFound)
|
||||
}
|
||||
|
||||
// serveThumbFile 用 ServeContent 供缓存文件:自带 Range/Last-Modified/304。
|
||||
func (a *API) serveThumbFile(w http.ResponseWriter, r *http.Request, path string, f model.File) {
|
||||
fp, err := os.Open(path)
|
||||
if err != nil {
|
||||
a.thumbFallback(w, r, f)
|
||||
return
|
||||
}
|
||||
defer fp.Close()
|
||||
fi, err := fp.Stat()
|
||||
if err != nil {
|
||||
httpx.NotFound(w)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
|
||||
http.ServeContent(w, r, path, fi.ModTime(), fp)
|
||||
}
|
||||
|
||||
func (a *API) serveThumbBytes(w http.ResponseWriter, mime string, data []byte, f model.File) {
|
||||
w.Header().Set("Content-Type", mime)
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
|
||||
w.Header().Set("ETag", `"`+f.SHA256+`"`)
|
||||
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(data)
|
||||
}
|
||||
Reference in New Issue
Block a user