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:
@@ -315,6 +315,86 @@ func TestVolcesProtocolAcceptsThreeSixAndNineReferenceImages(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestObjectStorageEmulationSupportsRetryLifecycleAndFaults(t *testing.T) {
|
||||
server := httptest.NewServer(New(Config{}).Handler())
|
||||
defer server.Close()
|
||||
objectURL := server.URL + "/storage/s3-transient/bucket/media/result.png"
|
||||
|
||||
request, _ := http.NewRequest(http.MethodPut, objectURL, strings.NewReader("image-bytes"))
|
||||
request.Header.Set("Content-Type", "image/png")
|
||||
response, err := http.DefaultClient.Do(request)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = response.Body.Close()
|
||||
if response.StatusCode != http.StatusServiceUnavailable {
|
||||
t.Fatalf("first transient PUT status=%d", response.StatusCode)
|
||||
}
|
||||
request, _ = http.NewRequest(http.MethodPut, objectURL, strings.NewReader("image-bytes"))
|
||||
request.Header.Set("Content-Type", "image/png")
|
||||
response, err = http.DefaultClient.Do(request)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
t.Fatalf("retried PUT status=%d", response.StatusCode)
|
||||
}
|
||||
|
||||
response, err = http.Get(objectURL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
payload, _ := io.ReadAll(response.Body)
|
||||
_ = response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK || string(payload) != "image-bytes" {
|
||||
t.Fatalf("GET status=%d payload=%q", response.StatusCode, payload)
|
||||
}
|
||||
request, _ = http.NewRequest(http.MethodHead, objectURL, nil)
|
||||
response, err = http.DefaultClient.Do(request)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
t.Fatalf("HEAD status=%d", response.StatusCode)
|
||||
}
|
||||
request, _ = http.NewRequest(http.MethodDelete, objectURL, nil)
|
||||
response, err = http.DefaultClient.Do(request)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = response.Body.Close()
|
||||
if response.StatusCode != http.StatusNoContent {
|
||||
t.Fatalf("DELETE status=%d", response.StatusCode)
|
||||
}
|
||||
|
||||
for profile, wantStatus := range map[string]int{"oss-auth": http.StatusForbidden, "s3-fail": http.StatusServiceUnavailable} {
|
||||
request, _ = http.NewRequest(http.MethodPut, server.URL+"/storage/"+profile+"/probe.bin", strings.NewReader("probe"))
|
||||
response, err = http.DefaultClient.Do(request)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = response.Body.Close()
|
||||
if response.StatusCode != wantStatus {
|
||||
t.Fatalf("%s status=%d, want %d", profile, response.StatusCode, wantStatus)
|
||||
}
|
||||
}
|
||||
|
||||
response, err = http.Get(server.URL + "/report")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
var report Report
|
||||
if err := json.NewDecoder(response.Body).Decode(&report); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if report.StoragePuts != 1 || report.StorageGets != 1 || report.StorageHeads != 1 || report.StorageDeletes != 1 || report.StorageFailures != 3 {
|
||||
t.Fatalf("unexpected storage report: %+v", report)
|
||||
}
|
||||
}
|
||||
|
||||
func postIdempotent(url string, body []byte, key string) (*http.Response, error) {
|
||||
request, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user