feat(storage): 统一二进制对象存储与公开错误
新增 Aliyun OSS 与 S3 协议、通道内重试和按优先级跨通道切换,保留 server-main 兼容与环境 OSS 内存通道。 将请求及结果中的 Base64、Data URI、Buffer、multipart 和内联二进制统一对象化,生产路径不再写入本机静态目录,历史本地资源仅保留只读兼容。 引入 PublicErrorV1 并统一 API、异步查询、兼容协议和失败回调的安全错误输出,同时补充迁移、管理端、指标、OpenAPI 与本地模拟验收。 验证:go test ./... -count=1;go vet ./...;pnpm lint;pnpm test;pnpm build;pnpm openapi;tests/ci/migrations-test.sh。
This commit is contained in:
@@ -12,13 +12,12 @@ import (
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/config"
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
|
||||
)
|
||||
|
||||
func TestDirectOSSUploadSignsRequestAndReturnsPublicURL(t *testing.T) {
|
||||
func TestDirectOSSUploadSignsRequestAndReturnsPrivateSignedURL(t *testing.T) {
|
||||
payload := []byte("production-isomorphic-image")
|
||||
var calls atomic.Int64
|
||||
var uploadedPath string
|
||||
@@ -60,9 +59,6 @@ func TestDirectOSSUploadSignsRequestAndReturnsPublicURL(t *testing.T) {
|
||||
MediaOSSObjectPrefix: "easyai-ai-gateway/production/media",
|
||||
}
|
||||
uploader := newDirectOSSUploader(cfg)
|
||||
uploader.now = func() time.Time {
|
||||
return time.Date(2026, time.July, 30, 12, 0, 0, 0, time.UTC)
|
||||
}
|
||||
service := &Service{cfg: cfg, directOSS: uploader}
|
||||
uploaded, err := service.UploadFile(context.Background(), FileUploadPayload{
|
||||
Bytes: payload,
|
||||
@@ -77,29 +73,30 @@ func TestDirectOSSUploadSignsRequestAndReturnsPublicURL(t *testing.T) {
|
||||
if calls.Load() != 2 {
|
||||
t.Fatalf("upload calls=%d, want one retry", calls.Load())
|
||||
}
|
||||
if !strings.HasPrefix(uploadedPath, "/easyai-ai-gateway/production/media/request_asset/2026/07/30/input-") ||
|
||||
!strings.HasSuffix(uploadedPath, ".png") {
|
||||
if !strings.Contains(uploadedPath, "/easyai-ai-gateway/production/media/request_asset/") ||
|
||||
!strings.HasSuffix(uploadedPath, "/"+sha256Hex(payload)+".png") {
|
||||
t.Fatalf("unexpected object path=%q", uploadedPath)
|
||||
}
|
||||
if got := stringFromAny(uploaded["url"]); got != "https://cdn.example.com"+uploadedPath {
|
||||
t.Fatalf("public URL=%q", got)
|
||||
if got := stringFromAny(uploaded["url"]); !strings.HasPrefix(got, server.URL+uploadedPath+"?") || !strings.Contains(got, "Signature=") {
|
||||
t.Fatalf("private signed URL=%q", got)
|
||||
}
|
||||
if uploaded["accessScope"] != "private" {
|
||||
t.Fatalf("request asset access scope=%v", uploaded["accessScope"])
|
||||
}
|
||||
channel, _ := uploaded["storageChannel"].(map[string]any)
|
||||
if stringFromAny(channel["provider"]) != "aliyun_oss_direct" {
|
||||
if stringFromAny(channel["provider"]) != "aliyun_oss" {
|
||||
t.Fatalf("storage channel=%+v", channel)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirectOSSDoesNotReplaceGeneralUploadScene(t *testing.T) {
|
||||
func TestDirectOSSPersistsGeneralUploadScene(t *testing.T) {
|
||||
var calls atomic.Int64
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
calls.Add(1)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
storageDir := t.TempDir()
|
||||
cfg := config.Config{
|
||||
LocalUploadedStorageDir: storageDir,
|
||||
MediaOSSDirectEnabled: true,
|
||||
MediaOSSEndpoint: server.URL,
|
||||
MediaOSSBucket: "media-bucket",
|
||||
@@ -118,11 +115,11 @@ func TestDirectOSSDoesNotReplaceGeneralUploadScene(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("general upload: %v", err)
|
||||
}
|
||||
if calls.Load() != 0 {
|
||||
if calls.Load() != 1 {
|
||||
t.Fatalf("direct OSS calls=%d for general upload scene", calls.Load())
|
||||
}
|
||||
channel, _ := uploaded["storageChannel"].(map[string]any)
|
||||
if stringFromAny(channel["provider"]) != "local_static" {
|
||||
if stringFromAny(channel["provider"]) != "aliyun_oss" {
|
||||
t.Fatalf("general upload channel=%+v", channel)
|
||||
}
|
||||
}
|
||||
@@ -158,7 +155,7 @@ func TestGeneratedInlineAssetUsesDirectOSS(t *testing.T) {
|
||||
SourceKey: "b64_json",
|
||||
},
|
||||
0,
|
||||
[]store.FileStorageChannel{{ID: "unused-channel"}},
|
||||
[]store.FileStorageChannel{service.directOSS.fileStorageChannel()},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("direct generated upload: %v", err)
|
||||
@@ -166,11 +163,11 @@ func TestGeneratedInlineAssetUsesDirectOSS(t *testing.T) {
|
||||
if calls.Load() != 1 {
|
||||
t.Fatalf("direct OSS calls=%d, want 1", calls.Load())
|
||||
}
|
||||
if contentType != "image/png" || kind != "image" || strategy != "direct_aliyun_oss" {
|
||||
if contentType != "image/png" || kind != "image" || strategy != "upload_inline_media" {
|
||||
t.Fatalf("generated metadata=%s/%s/%s", contentType, kind, strategy)
|
||||
}
|
||||
channel, _ := upload["storageChannel"].(map[string]any)
|
||||
if stringFromAny(channel["provider"]) != "aliyun_oss_direct" {
|
||||
if stringFromAny(channel["provider"]) != "aliyun_oss" {
|
||||
t.Fatalf("storage channel=%+v", channel)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user