Files
ds2api/internal/httpapi/openai/files/handler_files.go
2026-04-26 06:58:20 +08:00

114 lines
3.2 KiB
Go

package files
import (
"io"
"net/http"
"strings"
"time"
"ds2api/internal/auth"
"ds2api/internal/chathistory"
dsclient "ds2api/internal/deepseek/client"
"ds2api/internal/httpapi/openai/shared"
)
const openAIUploadMaxMemory = 32 << 20
type Handler struct {
Store shared.ConfigReader
Auth shared.AuthResolver
DS shared.DeepSeekCaller
ChatHistory *chathistory.Store
}
func (h *Handler) UploadFile(w http.ResponseWriter, r *http.Request) {
a, err := h.Auth.Determine(r)
if err != nil {
status := http.StatusUnauthorized
detail := err.Error()
if err == auth.ErrNoAccount {
status = http.StatusTooManyRequests
}
shared.WriteOpenAIError(w, status, detail)
return
}
defer h.Auth.Release(a)
if !strings.HasPrefix(strings.ToLower(strings.TrimSpace(r.Header.Get("Content-Type"))), "multipart/form-data") {
shared.WriteOpenAIError(w, http.StatusBadRequest, "content-type must be multipart/form-data")
return
}
// Enforce a hard cap on the total request body size to prevent OOM
r.Body = http.MaxBytesReader(w, r.Body, shared.UploadMaxSize)
if err := r.ParseMultipartForm(openAIUploadMaxMemory); err != nil {
if strings.Contains(strings.ToLower(err.Error()), "too large") {
shared.WriteOpenAIError(w, http.StatusRequestEntityTooLarge, "file size exceeds limit")
return
}
shared.WriteOpenAIError(w, http.StatusBadRequest, "invalid multipart form")
return
}
if r.MultipartForm != nil {
defer func() { _ = r.MultipartForm.RemoveAll() }()
}
r = r.WithContext(auth.WithAuth(r.Context(), a))
file, header, err := r.FormFile("file")
if err != nil {
shared.WriteOpenAIError(w, http.StatusBadRequest, "file is required")
return
}
defer func() { _ = file.Close() }()
data, err := io.ReadAll(file)
if err != nil {
shared.WriteOpenAIError(w, http.StatusBadRequest, "failed to read uploaded file")
return
}
contentType := strings.TrimSpace(header.Header.Get("Content-Type"))
if contentType == "" && len(data) > 0 {
contentType = http.DetectContentType(data)
}
result, err := h.DS.UploadFile(r.Context(), a, dsclient.UploadFileRequest{
Filename: header.Filename,
ContentType: contentType,
Purpose: strings.TrimSpace(r.FormValue("purpose")),
Data: data,
}, 3)
if err != nil {
shared.WriteOpenAIError(w, http.StatusInternalServerError, "Failed to upload file.")
return
}
if result != nil && result.AccountID == "" {
result.AccountID = a.AccountID
}
shared.WriteJSON(w, http.StatusOK, buildOpenAIFileObject(result))
}
func buildOpenAIFileObject(result *dsclient.UploadFileResult) map[string]any {
if result == nil {
obj := map[string]any{
"id": "",
"object": "file",
"bytes": 0,
"created_at": time.Now().Unix(),
"filename": "",
"purpose": "",
"status": "uploaded",
"status_details": nil,
}
return obj
}
obj := map[string]any{
"id": result.ID,
"object": "file",
"bytes": result.Bytes,
"created_at": time.Now().Unix(),
"filename": result.Filename,
"purpose": result.Purpose,
"status": result.Status,
"status_details": nil,
}
if result.AccountID != "" {
obj["account_id"] = result.AccountID
}
return obj
}