- 新增媒体库:media_library/media_thumbs 服务(WebP 缩略图)、管理端 media 页面 - 移动端底部导航 MobileTabBar 替换 MobilePostBar - 旧数据导入增强与测试、路由与登录会话/板块侧边栏测试补强 - site-doc 组件精简(移除 Breadcrumb),文档新增迁移公告说明
173 lines
4.6 KiB
Go
173 lines
4.6 KiB
Go
package service
|
|
|
|
import (
|
|
"bytes"
|
|
"image"
|
|
"image/color"
|
|
"image/gif"
|
|
"image/jpeg"
|
|
"image/png"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/freefire/jiang13-bbs/model"
|
|
webpenc "github.com/gen2brain/webp"
|
|
"github.com/glebarez/sqlite"
|
|
xwebp "golang.org/x/image/webp"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func newUploadTestService(t *testing.T) (*UploadService, string) {
|
|
t.Helper()
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatalf("open sqlite: %v", err)
|
|
}
|
|
if err := db.AutoMigrate(&model.Attachment{}); err != nil {
|
|
t.Fatalf("migrate: %v", err)
|
|
}
|
|
dir := t.TempDir()
|
|
s := NewUploadService(db, dir)
|
|
if err := s.EnsureDir(); err != nil {
|
|
t.Fatalf("ensure dir: %v", err)
|
|
}
|
|
return s, dir
|
|
}
|
|
|
|
func storedUploadPath(t *testing.T, dir, url string) []byte {
|
|
t.Helper()
|
|
rel := strings.TrimPrefix(url, "/uploads/")
|
|
data, err := os.ReadFile(filepath.Join(dir, filepath.FromSlash(rel)))
|
|
if err != nil {
|
|
t.Fatalf("read stored file %s: %v", url, err)
|
|
}
|
|
return data
|
|
}
|
|
|
|
func TestSaveImageTranscodesJPEGToWebP(t *testing.T) {
|
|
s, dir := newUploadTestService(t)
|
|
|
|
src := image.NewRGBA(image.Rect(0, 0, 48, 36))
|
|
for y := 0; y < 36; y++ {
|
|
for x := 0; x < 48; x++ {
|
|
src.Set(x, y, color.RGBA{uint8(x * 5), uint8(y * 7), 128, 255})
|
|
}
|
|
}
|
|
var in bytes.Buffer
|
|
if err := jpeg.Encode(&in, src, nil); err != nil {
|
|
t.Fatalf("encode jpeg: %v", err)
|
|
}
|
|
|
|
att, err := s.SaveImage(1, bytes.NewReader(in.Bytes()))
|
|
if err != nil {
|
|
t.Fatalf("SaveImage: %v", err)
|
|
}
|
|
if !strings.HasSuffix(att.URL, ".webp") || att.MIME != "image/webp" {
|
|
t.Fatalf("want webp attachment, got url=%s mime=%s", att.URL, att.MIME)
|
|
}
|
|
if att.Width != 48 || att.Height != 36 {
|
|
t.Fatalf("dims changed: %dx%d", att.Width, att.Height)
|
|
}
|
|
|
|
img, err := xwebp.Decode(bytes.NewReader(storedUploadPath(t, dir, att.URL)))
|
|
if err != nil {
|
|
t.Fatalf("decode stored webp: %v", err)
|
|
}
|
|
if got := img.Bounds(); got.Dx() != 48 || got.Dy() != 36 {
|
|
t.Fatalf("stored webp dims: %v", got)
|
|
}
|
|
}
|
|
|
|
func TestSaveImageTranscodesPNGLosslessKeepsAlpha(t *testing.T) {
|
|
s, dir := newUploadTestService(t)
|
|
|
|
src := image.NewNRGBA(image.Rect(0, 0, 24, 24))
|
|
for y := 0; y < 24; y++ {
|
|
for x := 0; x < 24; x++ {
|
|
if y < 12 {
|
|
src.SetNRGBA(x, y, color.NRGBA{0, 0, 0, 0}) // 上半透明
|
|
} else {
|
|
src.SetNRGBA(x, y, color.NRGBA{12, 34, 56, 255})
|
|
}
|
|
}
|
|
}
|
|
var in bytes.Buffer
|
|
if err := png.Encode(&in, src); err != nil {
|
|
t.Fatalf("encode png: %v", err)
|
|
}
|
|
|
|
att, err := s.SaveImage(1, bytes.NewReader(in.Bytes()))
|
|
if err != nil {
|
|
t.Fatalf("SaveImage: %v", err)
|
|
}
|
|
if !strings.HasSuffix(att.URL, ".webp") || att.MIME != "image/webp" {
|
|
t.Fatalf("want webp attachment, got url=%s mime=%s", att.URL, att.MIME)
|
|
}
|
|
|
|
img, err := xwebp.Decode(bytes.NewReader(storedUploadPath(t, dir, att.URL)))
|
|
if err != nil {
|
|
t.Fatalf("decode stored webp: %v", err)
|
|
}
|
|
if _, _, _, a := img.At(0, 0).RGBA(); a != 0 {
|
|
t.Fatalf("alpha lost at transparent pixel: %d", a)
|
|
}
|
|
r, g, b, _ := img.At(5, 20).RGBA()
|
|
if r>>8 != 12 || g>>8 != 34 || b>>8 != 56 {
|
|
t.Fatalf("opaque color changed: %d,%d,%d", r>>8, g>>8, b>>8)
|
|
}
|
|
}
|
|
|
|
func TestSaveImageKeepsGIFAsIs(t *testing.T) {
|
|
s, dir := newUploadTestService(t)
|
|
|
|
src := image.NewRGBA(image.Rect(0, 0, 16, 16))
|
|
for y := 0; y < 16; y++ {
|
|
for x := 0; x < 16; x++ {
|
|
src.Set(x, y, color.RGBA{uint8(x * 16), uint8(y * 16), 0, 255})
|
|
}
|
|
}
|
|
var in bytes.Buffer
|
|
if err := gif.Encode(&in, src, nil); err != nil {
|
|
t.Fatalf("encode gif: %v", err)
|
|
}
|
|
|
|
att, err := s.SaveImage(1, bytes.NewReader(in.Bytes()))
|
|
if err != nil {
|
|
t.Fatalf("SaveImage: %v", err)
|
|
}
|
|
if !strings.HasSuffix(att.URL, ".gif") || att.MIME != "image/gif" {
|
|
t.Fatalf("gif should be kept as-is, got url=%s mime=%s", att.URL, att.MIME)
|
|
}
|
|
data := storedUploadPath(t, dir, att.URL)
|
|
magic := string(data[:6])
|
|
if magic != "GIF89a" && magic != "GIF87a" {
|
|
t.Fatalf("stored gif magic invalid: %q", magic)
|
|
}
|
|
}
|
|
|
|
func TestSaveImageKeepsWebPAsIs(t *testing.T) {
|
|
s, dir := newUploadTestService(t)
|
|
|
|
src := image.NewRGBA(image.Rect(0, 0, 20, 10))
|
|
var in bytes.Buffer
|
|
if err := webpenc.Encode(&in, src, webpenc.Options{Quality: ImageWebPQuality}); err != nil {
|
|
t.Fatalf("encode webp: %v", err)
|
|
}
|
|
|
|
att, err := s.SaveImage(1, bytes.NewReader(in.Bytes()))
|
|
if err != nil {
|
|
t.Fatalf("SaveImage: %v", err)
|
|
}
|
|
if !strings.HasSuffix(att.URL, ".webp") || att.MIME != "image/webp" {
|
|
t.Fatalf("want webp attachment, got url=%s mime=%s", att.URL, att.MIME)
|
|
}
|
|
if !bytes.Equal(storedUploadPath(t, dir, att.URL), in.Bytes()) {
|
|
t.Fatal("webp input should be stored byte-for-byte")
|
|
}
|
|
if att.Size != in.Len() {
|
|
t.Fatalf("size mismatch: att=%d input=%d", att.Size, in.Len())
|
|
}
|
|
}
|