倍率问题和图片问题调整
This commit is contained in:
@@ -295,9 +295,14 @@ func (h *assetsHandler) download(w http.ResponseWriter, r *http.Request, public
|
||||
return
|
||||
}
|
||||
defer blob.Body.Close()
|
||||
disposition := contentDisposition(a.Name)
|
||||
name, blob, err := assets.NormalizeImageDownload(a.Name, blob)
|
||||
if err != nil {
|
||||
writeAssetError(w, err, public, "")
|
||||
return
|
||||
}
|
||||
disposition := contentDisposition(name)
|
||||
if inline {
|
||||
disposition = inlineContentDisposition(a.Name)
|
||||
disposition = inlineContentDisposition(name)
|
||||
}
|
||||
writeBlob(w, blob, "private, no-store", disposition)
|
||||
}
|
||||
|
||||
177
backend/internal/httpapi/assets_image_names_test.go
Normal file
177
backend/internal/httpapi/assets_image_names_test.go
Normal file
@@ -0,0 +1,177 @@
|
||||
package httpapi_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/assets"
|
||||
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/httpapi"
|
||||
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/publicapi"
|
||||
)
|
||||
|
||||
type formatDownloadBlobs struct {
|
||||
*assetBlobs
|
||||
contentTypes map[string]string
|
||||
reads int
|
||||
}
|
||||
|
||||
type failingImageBody struct {
|
||||
prefix []byte
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (b *failingImageBody) Read(p []byte) (int, error) {
|
||||
if len(b.prefix) > 0 {
|
||||
n := copy(p, b.prefix)
|
||||
b.prefix = b.prefix[n:]
|
||||
return n, nil
|
||||
}
|
||||
return 0, errors.New("storage stream failed")
|
||||
}
|
||||
|
||||
func (b *failingImageBody) Close() error {
|
||||
b.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
type failingDownloadBlobs struct {
|
||||
*assetBlobs
|
||||
body *failingImageBody
|
||||
}
|
||||
|
||||
func (b *failingDownloadBlobs) Read(context.Context, string) (assets.Blob, error) {
|
||||
return assets.Blob{Body: b.body, ContentType: "application/octet-stream", Size: 1000}, nil
|
||||
}
|
||||
|
||||
func (b *formatDownloadBlobs) Read(ctx context.Context, key string) (assets.Blob, error) {
|
||||
blob, err := b.assetBlobs.Read(ctx, key)
|
||||
if err == nil {
|
||||
b.reads++
|
||||
blob.ContentType = b.contentTypes[key]
|
||||
}
|
||||
return blob, err
|
||||
}
|
||||
|
||||
func TestLegacyImageDownloadUsesBytesForResponseNameAndType(t *testing.T) {
|
||||
tests := []struct {
|
||||
label, id, name, mediaType, suffix string
|
||||
data []byte
|
||||
}{
|
||||
{"png", "legacy-png", "生成图片.image", "image/png", ".png", append([]byte("\x89PNG\r\n\x1a\n"), bytes.Repeat([]byte("p"), 650)...)},
|
||||
{"jpeg", "legacy-jpeg", "生成图片.image", "image/jpeg", ".jpg", append([]byte("\xff\xd8\xff\xe0"), bytes.Repeat([]byte("j"), 650)...)},
|
||||
{"webp", "legacy-webp", "生成图片.image", "image/webp", ".webp", append([]byte("RIFF\x12\x00\x00\x00WEBPVP8 "), bytes.Repeat([]byte("w"), 650)...)},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.label, func(t *testing.T) {
|
||||
key := "uploads/" + tc.id + ".image"
|
||||
original := assets.Asset{ID: tc.id, OwnerID: "demo-merchant", Name: tc.name, Kind: assets.KindImage, StoragePath: key, URL: "https://cdn.test/" + key}
|
||||
catalog := &assetCatalog{values: []assets.Asset{original}}
|
||||
blobs := &formatDownloadBlobs{
|
||||
assetBlobs: &assetBlobs{values: map[string][]byte{key: bytes.Clone(tc.data)}},
|
||||
contentTypes: map[string]string{key: "application/octet-stream"},
|
||||
}
|
||||
h := newImageDownloadHandler(t, catalog, blobs)
|
||||
for _, inline := range []bool{false, true} {
|
||||
path := "/api/assets/" + tc.id + "/download"
|
||||
kind := "attachment"
|
||||
if inline {
|
||||
path += "?inline=1"
|
||||
kind = "inline"
|
||||
}
|
||||
response := request(t, h, http.MethodGet, path, nil, nil)
|
||||
wantName := strings.TrimSuffix(tc.name, ".image") + tc.suffix
|
||||
if response.Code != http.StatusOK || !bytes.Equal(response.Body.Bytes(), tc.data) {
|
||||
t.Fatalf("inline=%t status=%d body=%q", inline, response.Code, response.Body.Bytes())
|
||||
}
|
||||
if got := response.Header().Get("Content-Type"); got != tc.mediaType {
|
||||
t.Errorf("Content-Type=%q, want %q", got, tc.mediaType)
|
||||
}
|
||||
if got := response.Header().Get("Content-Length"); got != strconv.Itoa(len(tc.data)) {
|
||||
t.Errorf("Content-Length=%q, want %d", got, len(tc.data))
|
||||
}
|
||||
if got := response.Header().Get("Content-Disposition"); !strings.HasPrefix(got, kind+"; ") || !strings.Contains(got, "filename*=UTF-8''"+url.PathEscape(wantName)) {
|
||||
t.Errorf("Content-Disposition=%q, want %s and UTF-8 filename %q", got, kind, wantName)
|
||||
}
|
||||
}
|
||||
if !reflect.DeepEqual(catalog.values, []assets.Asset{original}) || !bytes.Equal(blobs.values[key], tc.data) || len(blobs.values) != 1 {
|
||||
t.Fatal("download changed catalog metadata or stored bytes")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicLegacyImageDownloadRequiresClientReadAccess(t *testing.T) {
|
||||
data := append([]byte("\x89PNG\r\n\x1a\n"), bytes.Repeat([]byte("x"), 600)...)
|
||||
key := "uploads/public.image"
|
||||
asset := assets.Asset{ID: "public-image", OwnerID: publicapi.OwnerID("agent-a"), Name: "公开图.image", Kind: assets.KindImage, StoragePath: key, Tags: []string{assets.ClientTag("agent-a")}}
|
||||
catalog := &assetCatalog{values: []assets.Asset{asset}}
|
||||
blobs := &formatDownloadBlobs{assetBlobs: &assetBlobs{values: map[string][]byte{key: data}}, contentTypes: map[string]string{key: "application/octet-stream"}}
|
||||
h := newImageDownloadHandler(t, catalog, blobs)
|
||||
path := "/api/v1/assets/public-image/download"
|
||||
|
||||
for _, headers := range []map[string]string{nil, {"Authorization": "Bearer key-b"}} {
|
||||
response := request(t, h, http.MethodGet, path, nil, headers)
|
||||
if response.Code != http.StatusUnauthorized && response.Code != http.StatusNotFound {
|
||||
t.Fatalf("unreadable public download status=%d body=%s", response.Code, response.Body.String())
|
||||
}
|
||||
}
|
||||
if blobs.reads != 0 {
|
||||
t.Fatalf("blob was read without access: %d reads", blobs.reads)
|
||||
}
|
||||
response := request(t, h, http.MethodGet, path, nil, map[string]string{"Authorization": "Bearer key-a"})
|
||||
if response.Code != http.StatusOK || !bytes.Equal(response.Body.Bytes(), data) || response.Header().Get("Content-Type") != "image/png" || !strings.Contains(response.Header().Get("Content-Disposition"), "filename*=UTF-8''"+url.PathEscape("公开图.png")) {
|
||||
t.Fatalf("public download status=%d headers=%v body=%q", response.Code, response.Header(), response.Body.Bytes())
|
||||
}
|
||||
if blobs.reads != 1 {
|
||||
t.Fatalf("authorized blob reads=%d, want 1", blobs.reads)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVideoDownloadKeepsOriginalNameAndType(t *testing.T) {
|
||||
key := "uploads/video.mp4"
|
||||
data := []byte("\x00\x00\x00\x18ftypmp42-video-content")
|
||||
catalog := &assetCatalog{values: []assets.Asset{{ID: "video", OwnerID: "demo-merchant", Kind: assets.KindVideo, Name: "视频.image", StoragePath: key}}}
|
||||
blobs := &formatDownloadBlobs{assetBlobs: &assetBlobs{values: map[string][]byte{key: data}}, contentTypes: map[string]string{key: "video/mp4"}}
|
||||
h := newImageDownloadHandler(t, catalog, blobs)
|
||||
response := request(t, h, http.MethodGet, "/api/assets/video/download", nil, nil)
|
||||
if response.Code != http.StatusOK || !bytes.Equal(response.Body.Bytes(), data) || response.Header().Get("Content-Type") != "video/mp4" || !strings.Contains(response.Header().Get("Content-Disposition"), "filename*=UTF-8''"+url.PathEscape("视频.image")) {
|
||||
t.Fatalf("video download status=%d headers=%v body=%q", response.Code, response.Header(), response.Body.Bytes())
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageDownloadPrefixFailureDoesNotSendSuccessHeaders(t *testing.T) {
|
||||
key := "uploads/failing.image"
|
||||
catalog := &assetCatalog{values: []assets.Asset{{ID: "failing", OwnerID: "demo-merchant", Kind: assets.KindImage, Name: "失败.image", StoragePath: key}}}
|
||||
body := &failingImageBody{prefix: []byte("\x89PNG\r\n\x1a\n")}
|
||||
blobs := &failingDownloadBlobs{assetBlobs: &assetBlobs{values: map[string][]byte{}}, body: body}
|
||||
h := newImageDownloadHandler(t, catalog, blobs)
|
||||
response := request(t, h, http.MethodGet, "/api/assets/failing/download", nil, nil)
|
||||
if response.Code != http.StatusInternalServerError || response.Header().Get("Content-Disposition") != "" || response.Header().Get("Content-Type") == "image/png" || !body.closed {
|
||||
t.Fatalf("failed download status=%d headers=%v closed=%t body=%q", response.Code, response.Header(), body.closed, response.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func newImageDownloadHandler(t *testing.T, catalog *assetCatalog, blobs assets.BlobStore) http.Handler {
|
||||
t.Helper()
|
||||
service := assets.NewService(catalog, blobs, nil, time.Now, nil)
|
||||
platform, err := httpapi.NewPlatformAuthorizer(httpapi.AuthState{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h, err := httpapi.NewAssetsHandler(service, platform, publicapi.NewAuthenticator(publicapi.Config{APIKeys: "agent-a:key-a,agent-b:key-b"}), httpapi.AssetsConfig{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
var _ io.ReadCloser = (*failingImageBody)(nil)
|
||||
Reference in New Issue
Block a user