配置(#98): 扩展文件上传 Content-Type 白名单支持图片
- 在 etc/ppanel.yaml 与 internal/config/config.go 的 S3.AllowedContentTypes 新增 image/jpeg,image/jpg,image/png,image/webp,image/gif,image/heic,image/heif,image/bmp - 保留原 zip/gzip/text/json/octet-stream - validateInitRequest 在拒绝时携带 content_type is not allowed 业务消息 - 新增 internal/logic/public/file/common_test.go,覆盖允许/拒绝及无 Content-Type 嗅探 - doc/tapi-file-upload-zh.md 同步允许类型列表与错误码说明 Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -58,7 +58,7 @@ type S3Config struct {
|
||||
UsePathStyle bool `yaml:"UsePathStyle" default:"false"`
|
||||
PresignExpireSeconds int64 `yaml:"PresignExpireSeconds" default:"300"`
|
||||
MaxUploadSize int64 `yaml:"MaxUploadSize" default:"104857600"`
|
||||
AllowedContentTypes string `yaml:"AllowedContentTypes" default:"application/zip,application/x-zip-compressed,application/gzip,application/x-gzip,application/octet-stream,text/plain,application/json"`
|
||||
AllowedContentTypes string `yaml:"AllowedContentTypes" default:"application/zip,application/x-zip-compressed,application/gzip,application/x-gzip,application/octet-stream,text/plain,application/json,image/jpeg,image/jpg,image/png,image/webp,image/gif,image/heic,image/heif,image/bmp"`
|
||||
}
|
||||
|
||||
type RedisConfig struct {
|
||||
|
||||
@@ -80,7 +80,7 @@ func validateInitRequest(svcCtx *svc.ServiceContext, bizType, fileName, contentT
|
||||
allowed := allowedContentTypeSet(svcCtx.Config.S3.AllowedContentTypes)
|
||||
if len(allowed) > 0 {
|
||||
if _, ok := allowed[strings.ToLower(strings.TrimSpace(contentType))]; !ok {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "content_type is not allowed")
|
||||
return errors.Wrapf(xerr.NewErrCodeMsg(xerr.InvalidParams, "content_type is not allowed"), "content_type is not allowed")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"mime/multipart"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/perfect-panel/server/internal/config"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/pkg/storage"
|
||||
)
|
||||
|
||||
const testAllowedContentTypes = "application/zip,application/x-zip-compressed,application/gzip,application/x-gzip,application/octet-stream,text/plain,application/json,image/jpeg,image/jpg,image/png,image/webp,image/gif,image/heic,image/heif,image/bmp"
|
||||
|
||||
type testMultipartFile struct {
|
||||
*strings.Reader
|
||||
}
|
||||
|
||||
func (testMultipartFile) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestValidateInitRequestAllowedContentTypes(t *testing.T) {
|
||||
svcCtx := &svc.ServiceContext{
|
||||
Config: config.Config{
|
||||
S3: config.S3Config{
|
||||
Enable: true,
|
||||
MaxUploadSize: 1024,
|
||||
AllowedContentTypes: testAllowedContentTypes,
|
||||
},
|
||||
},
|
||||
S3Store: &storage.S3Store{},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
contentType string
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "allow jpeg", contentType: "image/jpeg"},
|
||||
{name: "allow png", contentType: "image/png"},
|
||||
{name: "allow webp", contentType: "image/webp"},
|
||||
{name: "reject unknown", contentType: "application/x-sh", wantErr: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validateInitRequest(svcCtx, "app-package", "demo.bin", tt.contentType, 10)
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "content_type is not allowed") {
|
||||
t.Fatalf("expected content type error, got %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil error, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSniffContentTypeDetectsCommonImages(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data string
|
||||
want string
|
||||
}{
|
||||
{name: "jpeg", data: "\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01", want: "image/jpeg"},
|
||||
{name: "png", data: "\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR", want: "image/png"},
|
||||
{name: "webp", data: "RIFF\x1a\x00\x00\x00WEBPVP8 \x0e\x00\x00\x00", want: "image/webp"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
file := testMultipartFile{Reader: strings.NewReader(tt.data)}
|
||||
got, err := sniffContentType(&multipart.FileHeader{}, file)
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil error, got %v", err)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Fatalf("expected %q, got %q", tt.want, got)
|
||||
}
|
||||
if pos, err := file.Seek(0, 1); err != nil || pos != 0 {
|
||||
t.Fatalf("expected reader reset to start, pos=%d err=%v", pos, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user