新增 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。
71 lines
2.2 KiB
Go
71 lines
2.2 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/easyai/easyai-ai-gateway/apps/api/internal/clients"
|
|
"github.com/easyai/easyai-ai-gateway/apps/api/internal/runner"
|
|
)
|
|
|
|
const maxGatewayUploadBytes = 256 << 20
|
|
|
|
// uploadFile godoc
|
|
// @Summary 上传文件
|
|
// @Description 上传文件到配置的对象存储通道;所有通道失败时返回标准化存储错误,不写入本机静态目录。单文件最大 256MiB。
|
|
// @Tags files
|
|
// @Accept multipart/form-data
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Param file formData file true "要上传的文件"
|
|
// @Param source formData string false "上传来源标识" default(ai-gateway-openapi)
|
|
// @Success 200 {object} FileUploadResponse
|
|
// @Failure 400 {object} ErrorEnvelope
|
|
// @Failure 401 {object} ErrorEnvelope
|
|
// @Failure 502 {object} ErrorEnvelope
|
|
// @Failure 503 {object} ErrorEnvelope
|
|
// @Router /api/v1/files/upload [post]
|
|
func (s *Server) uploadFile(w http.ResponseWriter, r *http.Request) {
|
|
r.Body = http.MaxBytesReader(w, r.Body, maxGatewayUploadBytes)
|
|
if err := r.ParseMultipartForm(32 << 20); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid multipart upload")
|
|
return
|
|
}
|
|
file, header, err := r.FormFile("file")
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "file is required")
|
|
return
|
|
}
|
|
defer file.Close()
|
|
payload, err := io.ReadAll(file)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "read upload file failed")
|
|
return
|
|
}
|
|
contentType := strings.TrimSpace(header.Header.Get("Content-Type"))
|
|
if contentType == "" && len(payload) > 0 {
|
|
contentType = http.DetectContentType(payload)
|
|
}
|
|
upload, err := s.runner.UploadFile(r.Context(), runner.FileUploadPayload{
|
|
Bytes: payload,
|
|
ContentType: contentType,
|
|
FileName: header.Filename,
|
|
Source: firstNonEmptyFormValue(r, "source", "ai-gateway-openapi"),
|
|
})
|
|
if err != nil {
|
|
s.logger.Error("upload file failed", "error", err)
|
|
status := statusFromRunError(err)
|
|
writeError(w, status, err.Error(), clients.ErrorCode(err))
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, easyAIFileUploadResponse(upload))
|
|
}
|
|
|
|
func firstNonEmptyFormValue(r *http.Request, key string, fallback string) string {
|
|
if value := strings.TrimSpace(r.FormValue(key)); value != "" {
|
|
return value
|
|
}
|
|
return fallback
|
|
}
|