倍率问题和图片问题调整

This commit is contained in:
andy committed 2026-09-23 13:33:47 +08:00
1 parent f1993eb388
commit 3f29ba716a
20 files changed
+1210 -24

No files matched your search

+2
View File
@@ -245,6 +245,7 @@ func (s *Service) Upload(ctx context.Context, scope Scope, cmd UploadCommand) (A
if s.blobs == nil {
return Asset{}, errors.New("blob store is unavailable")
}
cmd.FileName, cmd.ContentType = normalizeImageFile(cmd.FileName, cmd.ContentType, cmd.Bytes)
name := sanitizeFileName(cmd.FileName)
key := path.Join("uploads", s.now().UTC().Format("2006-01-02"), s.id("file")+"-"+name)
stored, err := s.blobs.Put(ctx, key, bytes.NewReader(cmd.Bytes), int64(len(cmd.Bytes)), cmd.ContentType)
@@ -296,6 +297,7 @@ func (s *Service) ImportGenerated(ctx context.Context, scope Scope, cmd ImportGe
if strings.TrimSpace(name) == "" {
name = path.Base(strings.SplitN(cmd.URL, "?", 2)[0])
}
name, blob.ContentType = normalizeImageFile(name, blob.ContentType, content)
if s.blobs == nil {
return Asset{}, errors.New("blob store is unavailable")
}
+71
View File
@@ -0,0 +1,71 @@
package assets
import (
"bufio"
"errors"
"io"
"net/http"
"path"
"strings"
)
// normalizeImageFile uses the bytes, not a provider URL suffix or an untrusted
// Content-Type, as evidence of a recognized raster format. It does not decode
// or transcode the image. Unrecognized data retains its original metadata.
func normalizeImageFile(name, contentType string, prefix []byte) (string, string) {
if len(prefix) > 512 {
prefix = prefix[:512]
}
detected := http.DetectContentType(prefix)
var extension string
switch detected {
case "image/png":
extension = ".png"
case "image/jpeg":
extension = ".jpg"
case "image/webp":
extension = ".webp"
case "image/gif":
extension = ".gif"
case "image/bmp":
extension = ".bmp"
case "image/x-icon":
extension = ".ico"
default:
return name, contentType
}
name = strings.TrimSpace(name)
existing := path.Ext(name)
lower := strings.ToLower(existing)
if lower == extension || detected == "image/jpeg" && (lower == ".jpeg" || lower == ".jpe") {
return name, detected
}
base := strings.TrimSuffix(name, existing)
if base == "" || base == "." || base == "/" {
base = "image"
}
return base + extension, detected
}
// NormalizeImageDownload fixes legacy filenames and response MIME types from
// a bounded prefix. The returned body streams every original byte, and Close
// still closes the underlying OSS/local/remote stream. No catalog/object write
// is performed, including for historical .image objects.
func NormalizeImageDownload(name string, blob Blob) (string, Blob, error) {
if blob.Body == nil {
return name, blob, ErrBlobNotFound
}
reader := bufio.NewReaderSize(blob.Body, 512)
prefix, err := reader.Peek(512)
if err != nil && !errors.Is(err, io.EOF) {
return name, blob, err
}
name, blob.ContentType = normalizeImageFile(name, blob.ContentType, prefix)
blob.Body = &imageReadCloser{Reader: reader, Closer: blob.Body}
return name, blob, nil
}
type imageReadCloser struct {
io.Reader
io.Closer
}
@@ -0,0 +1,170 @@
package assets
import (
"bytes"
"context"
"encoding/base64"
"errors"
"image"
"image/jpeg"
"image/png"
"io"
"strings"
"testing"
)
func imageFormatFixtures(t *testing.T) (pngBytes, jpegBytes, webpBytes []byte) {
t.Helper()
img := image.NewRGBA(image.Rect(0, 0, 2, 2))
var p, j bytes.Buffer
if err := png.Encode(&p, img); err != nil {
t.Fatal(err)
}
if err := jpeg.Encode(&j, img, nil); err != nil {
t.Fatal(err)
}
w, err := base64.StdEncoding.DecodeString("UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA")
if err != nil {
t.Fatal(err)
}
return p.Bytes(), j.Bytes(), w
}
func TestNormalizeImageFileUsesBytesAndPreservesCompatibleNames(t *testing.T) {
pngData, jpegData, webpData := imageFormatFixtures(t)
for _, test := range []struct {
label, name, mime, wantName, wantMIME string
data []byte
}{
{"opaque PNG", "result.image", "application/octet-stream", "result.png", "image/png", pngData},
{"JPEG", "result.image", "", "result.jpg", "image/jpeg", jpegData},
{"WebP despite bad MIME", "result.image", "image/png", "result.webp", "image/webp", webpData},
{"wrong extension", "result.png", "image/png", "result.jpg", "image/jpeg", jpegData},
{"no suffix", "result", "", "result.png", "image/png", pngData},
{"uppercase", "result.PNG", "image/png; charset=binary", "result.PNG", "image/png", pngData},
{"JPEG alias", "result.JPEG", "application/octet-stream", "result.JPEG", "image/jpeg", jpegData},
{"Chinese filename", "葫芦娃和爷爷.image", "", "葫芦娃和爷爷.png", "image/png", pngData},
{"multi-dot filename", "result.v1.image", "", "result.v1.png", "image/png", pngData},
{"empty filename", "", "", "image.png", "image/png", pngData},
{"video", "result.mp4", "video/mp4", "result.mp4", "video/mp4", []byte("video content")},
{"unknown does not trust MIME", "result.image", "image/png", "result.image", "image/png", []byte(`{"error":"not an image"}`)},
{"empty content", "result.image", "", "result.image", "", nil},
} {
t.Run(test.label, func(t *testing.T) {
name, mime := normalizeImageFile(test.name, test.mime, test.data)
if name != test.wantName || mime != test.wantMIME {
t.Fatalf("got %q / %q, want %q / %q", name, mime, test.wantName, test.wantMIME)
}
})
}
}
func TestImportGeneratedNormalizesImageNameStorageAndContentType(t *testing.T) {
pngData, jpegData, webpData := imageFormatFixtures(t)
for _, test := range []struct {
extension, contentType string
data []byte
}{{"png", "image/png", pngData}, {"jpg", "image/jpeg", jpegData}, {"webp", "image/webp", webpData}} {
t.Run(test.extension, func(t *testing.T) {
cat, blobs := &memoryCatalog{}, &memoryBlobs{}
remote := &memoryRemote{blob: Blob{Body: io.NopCloser(bytes.NewReader(test.data)), ContentType: "application/octet-stream", Size: int64(len(test.data))}}
svc := NewService(cat, blobs, remote, nil, func(prefix string) string { return prefix + "-format" })
url := "https://provider.test/result.image?signature=fixture"
asset, err := svc.ImportGenerated(context.Background(), PlatformScope("owner-a"), ImportGeneratedCommand{URL: url, Capability: "image.generate"})
if err != nil {
t.Fatal(err)
}
if asset.Name != "result."+test.extension || !strings.HasSuffix(asset.StoragePath, "-result."+test.extension) || asset.Kind != KindImage {
t.Fatalf("asset=%#v", asset)
}
if asset.Metadata["contentType"] != test.contentType || asset.Metadata["importedFrom"] != url || blobs.putContentType != test.contentType {
t.Fatalf("metadata=%#v stored MIME=%q", asset.Metadata, blobs.putContentType)
}
if !bytes.Equal(blobs.putBody, test.data) {
t.Fatal("renaming changed the original image bytes")
}
})
}
}
func TestUploadRecognizesReuploadedLegacyImage(t *testing.T) {
pngData, _, _ := imageFormatFixtures(t)
cat, blobs := &memoryCatalog{}, &memoryBlobs{}
svc := NewService(cat, blobs, nil, nil, nil)
asset, err := svc.Upload(context.Background(), PlatformScope("owner-a"), UploadCommand{FileName: "download.image", ContentType: "application/octet-stream", Bytes: pngData})
if err != nil {
t.Fatal(err)
}
if asset.Name != "download.png" || asset.Kind != KindImage || asset.Metadata["contentType"] != "image/png" || !strings.HasSuffix(asset.StoragePath, "-download.png") || !bytes.Equal(blobs.putBody, pngData) {
t.Fatalf("upload=%#v", asset)
}
}
func TestNormalizeImageDownloadStreamsAllBytesAndClosesOriginal(t *testing.T) {
pngData, _, _ := imageFormatFixtures(t)
for _, data := range [][]byte{pngData, append(append([]byte(nil), pngData...), bytes.Repeat([]byte{7}, 4096)...)} {
body := &imageTrackedBody{Reader: bytes.NewReader(data)}
name, blob, err := NormalizeImageDownload("legacy.image", Blob{Body: body, ContentType: "application/octet-stream", Size: int64(len(data))})
if err != nil || name != "legacy.png" || blob.ContentType != "image/png" || blob.Size != int64(len(data)) {
t.Fatalf("name=%q blob=%#v err=%v", name, blob, err)
}
if body.readBytes > 512 || body.closed {
t.Fatalf("prefix read consumed %d bytes, closed=%v", body.readBytes, body.closed)
}
got, err := io.ReadAll(blob.Body)
if err != nil || !bytes.Equal(got, data) {
t.Fatalf("stream mismatch; error=%v", err)
}
if err := blob.Body.Close(); err != nil || !body.closed {
t.Fatalf("original body not closed; error=%v", err)
}
}
}
func TestNormalizeImageDownloadUnknownDataIsNotRenamed(t *testing.T) {
for _, data := range [][]byte{[]byte("video bytes"), nil} {
name, blob, err := NormalizeImageDownload("legacy.image", Blob{Body: io.NopCloser(bytes.NewReader(data)), ContentType: "application/octet-stream", Size: int64(len(data))})
if err != nil || name != "legacy.image" || blob.ContentType != "application/octet-stream" {
t.Fatalf("name=%q blob=%#v err=%v", name, blob, err)
}
got, _ := io.ReadAll(blob.Body)
_ = blob.Body.Close()
if !bytes.Equal(got, data) {
t.Fatal("unknown bytes changed")
}
}
}
func TestNormalizeImageDownloadReadError(t *testing.T) {
failure := errors.New("read failed")
body := &imageTrackedBody{Reader: imageErrorReader{err: failure}}
_, _, err := NormalizeImageDownload("legacy.image", Blob{Body: body})
if !errors.Is(err, failure) || body.closed {
t.Fatalf("error=%v closed=%v", err, body.closed)
}
_ = body.Close()
if _, _, err := NormalizeImageDownload("legacy.image", Blob{}); !errors.Is(err, ErrBlobNotFound) {
t.Fatalf("nil body error=%v", err)
}
}
type imageTrackedBody struct {
io.Reader
readBytes int
closed bool
}
func (b *imageTrackedBody) Read(p []byte) (int, error) {
n, err := b.Reader.Read(p)
b.readBytes += n
return n, err
}
func (b *imageTrackedBody) Close() error {
b.closed = true
return nil
}
type imageErrorReader struct{ err error }
func (r imageErrorReader) Read([]byte) (int, error) { return 0, r.err }