fix: align WebP uploads with confirmed prototype (#11)
This commit is contained in:
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Architecture-and-Code-Map
|
||||
wiki_url: https://git.ilapage.cn/OPC/chorus/wiki/Architecture-and-Code-Map.-
|
||||
wiki_revision: f9f5d67d06d86694948f5812ce8a565e30b4f266
|
||||
synchronized_at: 2026-08-20T16:41:50Z
|
||||
wiki_revision: 04391bbce2fcc59da2078265c020a8a845ca4c16
|
||||
synchronized_at: 2026-08-20T16:52:38Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 架构与代码地图
|
||||
@@ -101,7 +101,7 @@ MVP-0 第一张上传图自动作为 `primary`,其余为 `reference`;不提
|
||||
- `portal/session` 使用服务端内存会话和 HMAC 不透明 Cookie;登录成功与退出都会轮换 session ID 和 CSRF token。Cookie 为 HttpOnly/SameSite=Lax,生产启用 Secure;写请求只接受 `X-CSRF-Token`,避免 multipart 在正文限流前被隐式解析。
|
||||
- MVP-0 会话仅支持单 portal 进程,重启后统一失效;会话数量有内存上限并在创建时清理过期项。多实例共享会话不在 MVP-0 范围。
|
||||
- `internal/platform/password` 固定 `bcrypt:v1:<hash>` 版本化编码;未知版本失败关闭。登录对未知账号、错误密码和禁用账号执行同类密码校验并返回同一错误,按远端地址做内存窗口节流。
|
||||
- `portal/service` 统一处理 prompt 渲染、用户作用域幂等、上传数量/单文件/总量、声明与实际 MIME、扩展名、解码和像素校验;第一张图为 primary,其余为 reference。
|
||||
- `portal/service` 统一处理 prompt 渲染、用户作用域幂等、上传数量/单文件/总量、JPG/PNG/WebP 的声明与实际 MIME、扩展名、解码和像素校验;第一张图为 primary,其余为 reference。
|
||||
- `CreateIdempotentPrepared` 在事务插入 generation 后才执行 inputs 回调,因而文件 metadata 可使用真实 generation ID;幂等重放不执行回调,回调或数据库失败只补偿删除本次保存文件。
|
||||
- 同步提交只创建 pending、inputs 和 rendered_prompt,不 import 或调用 Provider/worker,也不读取点数。JSON 与 HTMX 查询共享同一用户作用域服务,认证过期返回 401;HTMX 额外返回登录跳转提示,不修改 generation 状态。
|
||||
- 任务、原图、生成图和缩略图查询先用 generation.user_id 过滤,再核对文件 metadata 的 owner/generation;响应只给受控 URL、内容和安全文件名,不暴露 storage key 或文件系统路径。
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
|
||||
corestorage "git.ilapage.cn/OPC/chorus/internal/core/storage"
|
||||
"github.com/disintegration/imaging"
|
||||
_ "golang.org/x/image/webp"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -381,6 +382,8 @@ func imageMIME(format string) string {
|
||||
return "image/png"
|
||||
case "gif":
|
||||
return "image/gif"
|
||||
case "webp":
|
||||
return "image/webp"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package storage
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"image"
|
||||
"image/color"
|
||||
@@ -19,7 +20,7 @@ func newTestStore(t *testing.T, maxBytes int64, maxPixels uint64) *Local {
|
||||
t.Helper()
|
||||
store, err := NewLocal(Config{
|
||||
Root: t.TempDir(), MaxObjectBytes: maxBytes, MaxImagePixels: maxPixels, ThumbnailMaxSide: 256,
|
||||
AllowedImageMIME: map[string]bool{"image/png": true, "image/jpeg": true},
|
||||
AllowedImageMIME: map[string]bool{"image/png": true, "image/jpeg": true, "image/webp": true},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -27,6 +28,21 @@ func newTestStore(t *testing.T, maxBytes int64, maxPixels uint64) *Local {
|
||||
return store
|
||||
}
|
||||
|
||||
func TestPutWebPCreatesPNGThumbnail(t *testing.T) {
|
||||
data, err := base64.StdEncoding.DecodeString("UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
store := newTestStore(t, 1024, 100)
|
||||
objects, err := store.PutImage(context.Background(), ImageRequest{Key: "webp/original", ThumbnailKey: "webp/thumbnail", OwnerID: 1, GenerationID: 1, ContentType: "image/webp", Source: bytes.NewReader(data)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if objects.Original.ContentType != "image/webp" || objects.Thumbnail.ContentType != "image/png" {
|
||||
t.Fatalf("objects=%#v", objects)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalPutOpenAtomicMetadataAndTraversal(t *testing.T) {
|
||||
store := newTestStore(t, 1024, 1_000_000)
|
||||
object, err := store.Put(context.Background(), corestorage.PutRequest{
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ func run() error {
|
||||
return errors.New("configure portal database")
|
||||
}
|
||||
defer sqlDB.Close()
|
||||
storage, err := platformstorage.NewLocal(platformstorage.Config{Root: cfg.StorageRoot, MaxObjectBytes: cfg.MaxImageBytes, MaxImagePixels: cfg.MaxImagePixels, ThumbnailMaxSide: 256, AllowedImageMIME: map[string]bool{"image/png": true, "image/jpeg": true}})
|
||||
storage, err := platformstorage.NewLocal(platformstorage.Config{Root: cfg.StorageRoot, MaxObjectBytes: cfg.MaxImageBytes, MaxImagePixels: cfg.MaxImagePixels, ThumbnailMaxSide: 256, AllowedImageMIME: map[string]bool{"image/png": true, "image/jpeg": true, "image/webp": true}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"git.ilapage.cn/OPC/chorus/internal/core/model"
|
||||
"git.ilapage.cn/OPC/chorus/internal/core/queue"
|
||||
corestorage "git.ilapage.cn/OPC/chorus/internal/core/storage"
|
||||
_ "golang.org/x/image/webp"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -153,11 +154,11 @@ func (s *Service) validateImages(uploads []Upload) ([]validatedImage, error) {
|
||||
detected = detected[:separator]
|
||||
}
|
||||
declared := strings.TrimSpace(strings.Split(upload.DeclaredMIME, ";")[0])
|
||||
if (detected != "image/png" && detected != "image/jpeg") || declared != detected {
|
||||
if (detected != "image/png" && detected != "image/jpeg" && detected != "image/webp") || declared != detected {
|
||||
return nil, &FileError{index, name, ErrImageMIME}
|
||||
}
|
||||
extension := strings.ToLower(path.Ext(name))
|
||||
if (detected == "image/png" && extension != ".png") || (detected == "image/jpeg" && extension != ".jpg" && extension != ".jpeg") {
|
||||
if (detected == "image/png" && extension != ".png") || (detected == "image/jpeg" && extension != ".jpg" && extension != ".jpeg") || (detected == "image/webp" && extension != ".webp") {
|
||||
return nil, &FileError{index, name, ErrImageExtension}
|
||||
}
|
||||
config, _, err := image.DecodeConfig(bytes.NewReader(upload.Content))
|
||||
|
||||
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"image"
|
||||
"image/color"
|
||||
@@ -53,6 +54,18 @@ func TestImageCountAndTotalSize(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebPValidationMatchesConfirmedPrototype(t *testing.T) {
|
||||
data, err := base64.StdEncoding.DecodeString("UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
service := &Service{config: Config{MaxImages: 1, MaxImageBytes: 1000, MaxUploadBytes: 1000, MaxImagePixels: 100}}
|
||||
images, err := service.validateImages([]Upload{{Name: "pixel.webp", DeclaredMIME: "image/webp", Content: data}})
|
||||
if err != nil || len(images) != 1 || images[0].MIME != "image/webp" {
|
||||
t.Fatalf("webp validation=%#v error=%v", images, err)
|
||||
}
|
||||
}
|
||||
|
||||
func smallPNG(width, height int) []byte {
|
||||
img := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||
for y := 0; y < height; y++ {
|
||||
|
||||
Reference in New Issue
Block a user