缩略图懒生成 + 评论登录限流:时间线不再拉原图
- 新增 /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,94 @@
|
||||
package thumbs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func makePNG(t *testing.T, w, h int, transparent bool) []byte {
|
||||
t.Helper()
|
||||
img := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||
for y := 0; y < h; y++ {
|
||||
for x := 0; x < w; x++ {
|
||||
// 左红右绿两半,方便验证盒均值没把通道混错
|
||||
c := color.RGBA{255, 0, 0, 255}
|
||||
if x >= w/2 {
|
||||
c = color.RGBA{0, 255, 0, 255}
|
||||
}
|
||||
if transparent {
|
||||
c = color.RGBA{0, 0, 0, 0}
|
||||
}
|
||||
img.Set(x, y, c)
|
||||
}
|
||||
}
|
||||
var b bytes.Buffer
|
||||
if err := png.Encode(&b, img); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
func TestGenerateDownscale(t *testing.T) {
|
||||
src := makePNG(t, 800, 400, false)
|
||||
out, mime, pass, err := Generate(src, "image/png", 400)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if pass {
|
||||
t.Fatal("800px 图在 w=400 不应直通")
|
||||
}
|
||||
if mime != "image/jpeg" {
|
||||
t.Fatalf("mime=%s", mime)
|
||||
}
|
||||
cfg, _, err := image.DecodeConfig(bytes.NewReader(out))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.Width != 400 || cfg.Height != 200 {
|
||||
t.Fatalf("got %dx%d, want 400x200", cfg.Width, cfg.Height)
|
||||
}
|
||||
img, err := jpeg.Decode(bytes.NewReader(out))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// 盒均值后左右两半的中心仍是红/绿(容差给 JPEG 质量 82 留余量)
|
||||
r, g, _, _ := img.At(100, 100).RGBA()
|
||||
if r>>8 < 200 || g>>8 > 60 {
|
||||
t.Fatalf("左半应偏红: r=%d g=%d", r>>8, g>>8)
|
||||
}
|
||||
r, g, _, _ = img.At(300, 100).RGBA()
|
||||
if g>>8 < 200 || r>>8 > 60 {
|
||||
t.Fatalf("右半应偏绿: r=%d g=%d", r>>8, g>>8)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratePassThrough(t *testing.T) {
|
||||
src := makePNG(t, 300, 150, false)
|
||||
out, mime, pass, err := Generate(src, "image/png", 960)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !pass || mime != "image/png" || !bytes.Equal(out, src) {
|
||||
t.Fatalf("小图应原样直通 pass=%v mime=%s", pass, mime)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateRejectsGarbage(t *testing.T) {
|
||||
if _, _, _, err := Generate([]byte("not an image"), "image/jpeg", 400); err == nil {
|
||||
t.Fatal("垃圾字节应报错")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlattenAlphaUnpremultiply(t *testing.T) {
|
||||
// 50% 透明的纯红:alpha=128 预乘 R=128,还原应为 R≈255
|
||||
img := image.NewNRGBA(image.Rect(0, 0, 1, 1))
|
||||
img.Set(0, 0, color.NRGBA{255, 0, 0, 128})
|
||||
flat := flatten(img, img.Bounds())
|
||||
if flat[0] < 240 || flat[3] != 128 {
|
||||
t.Fatalf("unpremultiply wrong: r=%d a=%d", flat[0], flat[3])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user