yao/openapi/file/file.go
Max 070ff59225 Enhance attachment management with file storage improvements
- Refactored the attachment manager to support file uploads with a new storage path and improved metadata handling.
- Implemented chunked uploads and direct content retrieval, enhancing performance and flexibility.
- Updated the file management API to include comprehensive operations for file uploads, downloads, and metadata management.
- Added support for multiple storage backends, including local and S3, with improved error handling and validation.
- Enhanced test coverage for file operations, ensuring reliability and consistency across different storage implementations.
2025-07-26 19:25:41 +08:00

470 lines
13 KiB
Go

package file
import (
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"github.com/yaoapp/yao/attachment"
"github.com/yaoapp/yao/openapi/oauth/types"
"github.com/yaoapp/yao/openapi/response"
)
// Attach attaches the file management handlers to the router
func Attach(group *gin.RouterGroup, oauth types.OAuth) {
// https://api.openai.com/v1/files
// Protect all endpoints with OAuth
group.Use(oauth.Guard)
// Upload a file (supports chunked upload)
group.POST("/files/:uploaderID", upload)
// List files
group.GET("/files/:uploaderID", list)
// Retrieve file
group.GET("/files/:uploaderID/:fileID", retrieve)
// Delete file
group.DELETE("/files/:uploaderID/:fileID", delete)
// Retrieve file content
group.GET("/files/:uploaderID/:fileID/content", content)
// Check if file exists
group.GET("/files/:uploaderID/:fileID/exists", exists)
}
// upload handles file upload
func upload(c *gin.Context) {
// Get the uploader ID from the URL path
uploaderID := c.Param("uploaderID")
if uploaderID == "" {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "Uploader ID is required",
}
response.RespondWithError(c, response.StatusBadRequest, errorResp)
return
}
// Get the attachment manager
manager, exists := attachment.Managers[uploaderID]
if !exists {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "Uploader not found: " + uploaderID,
}
response.RespondWithError(c, response.StatusNotFound, errorResp)
return
}
// Parse multipart form
err := c.Request.ParseMultipartForm(32 << 20) // 32 MB max memory
if err != nil {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "Failed to parse multipart form: " + err.Error(),
}
response.RespondWithError(c, response.StatusBadRequest, errorResp)
return
}
// Get the file from the form
file, fileHeader, err := c.Request.FormFile("file")
if err != nil {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "File is required",
}
response.RespondWithError(c, response.StatusBadRequest, errorResp)
return
}
defer file.Close()
// Get original filename from form data
originalFilename := c.PostForm("original_filename")
if originalFilename == "" {
originalFilename = fileHeader.Filename
}
// Get path from form data for user_path
userPath := c.PostForm("path")
if userPath == "" {
userPath = originalFilename
}
// Parse groups from form data
var groups []string
groupsStr := c.PostForm("groups")
if groupsStr != "" {
groups = strings.Split(groupsStr, ",")
// Trim spaces
for i, group := range groups {
groups[i] = strings.TrimSpace(group)
}
}
// Create upload header from request
header := attachment.GetHeader(c.Request.Header, fileHeader.Header, fileHeader.Size)
// Parse gzip option
gzip := false
if gzipStr := c.PostForm("gzip"); gzipStr == "true" {
gzip = true
}
// Parse compress image options
compressImage := false
if compressImageStr := c.PostForm("compress_image"); compressImageStr == "true" {
compressImage = true
}
compressSize := 0
if compressSizeStr := c.PostForm("compress_size"); compressSizeStr != "" {
if size, err := strconv.Atoi(compressSizeStr); err == nil && size > 0 {
compressSize = size
}
}
// Create upload options
uploadOption := attachment.UploadOption{
OriginalFilename: originalFilename, // Use original filename from form data
Groups: groups, // Groups for directory structure
ClientID: c.PostForm("client_id"),
OpenID: c.PostForm("openid"),
Gzip: gzip, // Gzip compression
CompressImage: compressImage, // Image compression
CompressSize: compressSize, // Compression size
}
// Upload the file
uploadedFile, err := manager.Upload(c.Request.Context(), header, file, uploadOption)
if err != nil {
errorResp := &response.ErrorResponse{
Code: response.ErrServerError.Code,
ErrorDescription: "Failed to upload file: " + err.Error(),
}
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
return
}
// Return the uploaded file info
response.RespondWithSuccess(c, response.StatusOK, uploadedFile)
}
// list handles file listing with pagination and filtering
func list(c *gin.Context) {
uploaderID := c.Param("uploaderID")
if uploaderID == "" {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "Uploader ID is required",
}
response.RespondWithError(c, response.StatusBadRequest, errorResp)
return
}
// Get the attachment manager
manager, ok := attachment.Managers[uploaderID]
if !ok {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "Uploader not found: " + uploaderID,
}
response.RespondWithError(c, response.StatusNotFound, errorResp)
return
}
// Parse query parameters
page := 1
if pageStr := c.Query("page"); pageStr != "" {
if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
page = p
}
}
pageSize := 20
if pageSizeStr := c.Query("page_size"); pageSizeStr != "" {
if ps, err := strconv.Atoi(pageSizeStr); err == nil && ps > 0 && ps <= 100 {
pageSize = ps
}
}
// Parse filters
filters := make(map[string]interface{})
filters["uploader"] = uploaderID // Always filter by current uploader
if status := c.Query("status"); status != "" {
filters["status"] = status
}
if contentType := c.Query("content_type"); contentType != "" {
filters["content_type"] = contentType
}
if name := c.Query("name"); name != "" {
filters["name"] = name + "*" // Wildcard search
}
// Parse order by
orderBy := c.Query("order_by")
if orderBy == "" {
orderBy = "created_at desc"
}
// Parse select fields
var selectFields []string
if selectStr := c.Query("select"); selectStr != "" {
selectFields = strings.Split(selectStr, ",")
for i, field := range selectFields {
selectFields[i] = strings.TrimSpace(field)
}
}
// Create list option
listOption := attachment.ListOption{
Page: page,
PageSize: pageSize,
Filters: filters,
OrderBy: orderBy,
Select: selectFields,
}
// Get file list
result, err := manager.List(c.Request.Context(), listOption)
if err != nil {
errorResp := &response.ErrorResponse{
Code: response.ErrServerError.Code,
ErrorDescription: "Failed to list files: " + err.Error(),
}
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
return
}
// Return the list result
response.RespondWithSuccess(c, response.StatusOK, result)
}
// retrieve handles file metadata retrieval
func retrieve(c *gin.Context) {
uploaderID := c.Param("uploaderID")
fileID, _ := url.QueryUnescape(c.Param("fileID"))
if uploaderID == "" {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "Uploader ID is required",
}
response.RespondWithError(c, response.StatusBadRequest, errorResp)
return
}
if fileID == "" {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "File ID is required",
}
response.RespondWithError(c, response.StatusBadRequest, errorResp)
return
}
// Get the attachment manager
manager, ok := attachment.Managers[uploaderID]
if !ok {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "Uploader not found: " + uploaderID,
}
response.RespondWithError(c, response.StatusNotFound, errorResp)
return
}
// Get file info using the new Info method
fileInfo, err := manager.Info(c.Request.Context(), fileID)
if err != nil {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "File not found: " + err.Error(),
}
response.RespondWithError(c, response.StatusNotFound, errorResp)
return
}
// Return the file info
response.RespondWithSuccess(c, response.StatusOK, fileInfo)
}
// delete handles file deletion
func delete(c *gin.Context) {
uploaderID := c.Param("uploaderID")
fileID, _ := url.QueryUnescape(c.Param("fileID"))
if uploaderID == "" {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "Uploader ID is required",
}
response.RespondWithError(c, response.StatusBadRequest, errorResp)
return
}
if fileID == "" {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "File ID is required",
}
response.RespondWithError(c, response.StatusBadRequest, errorResp)
return
}
// Get the attachment manager
manager, ok := attachment.Managers[uploaderID]
if !ok {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "Uploader not found: " + uploaderID,
}
response.RespondWithError(c, response.StatusNotFound, errorResp)
return
}
// Check if file exists first
if !manager.Exists(c.Request.Context(), fileID) {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "File not found",
}
response.RespondWithError(c, response.StatusNotFound, errorResp)
return
}
// Delete the file
err := manager.Delete(c.Request.Context(), fileID)
if err != nil {
errorResp := &response.ErrorResponse{
Code: response.ErrServerError.Code,
ErrorDescription: "Failed to delete file: " + err.Error(),
}
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
return
}
successData := gin.H{
"message": "File deleted successfully",
"file_id": fileID,
}
response.RespondWithSuccess(c, response.StatusOK, successData)
}
// content handles file content retrieval
func content(c *gin.Context) {
uploaderID := c.Param("uploaderID")
fileID, _ := url.QueryUnescape(c.Param("fileID"))
if uploaderID == "" {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "Uploader ID is required",
}
response.RespondWithError(c, response.StatusBadRequest, errorResp)
return
}
if fileID == "" {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "File ID is required",
}
response.RespondWithError(c, response.StatusBadRequest, errorResp)
return
}
// Get the attachment manager
manager, ok := attachment.Managers[uploaderID]
if !ok {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "Uploader not found: " + uploaderID,
}
response.RespondWithError(c, response.StatusNotFound, errorResp)
return
}
// Get file info first to obtain metadata
fileInfo, err := manager.Info(c.Request.Context(), fileID)
if err != nil {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "File not found: " + err.Error(),
}
response.RespondWithError(c, response.StatusNotFound, errorResp)
return
}
// Read the file content
content, err := manager.Read(c.Request.Context(), fileID)
if err != nil {
errorResp := &response.ErrorResponse{
Code: response.ErrServerError.Code,
ErrorDescription: "Failed to read file: " + err.Error(),
}
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
return
}
// Set headers based on file info
c.Header("Content-Type", fileInfo.ContentType)
if fileInfo.Filename != "" {
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", fileInfo.Filename))
}
c.Header("Content-Length", fmt.Sprintf("%d", len(content)))
// Return file content directly
c.Data(http.StatusOK, fileInfo.ContentType, content)
}
// exists checks if a file exists
func exists(c *gin.Context) {
uploaderID := c.Param("uploaderID")
fileID, _ := url.QueryUnescape(c.Param("fileID"))
if uploaderID == "" {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "Uploader ID is required",
}
response.RespondWithError(c, response.StatusBadRequest, errorResp)
return
}
if fileID == "" {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "File ID is required",
}
response.RespondWithError(c, response.StatusBadRequest, errorResp)
return
}
// Get the attachment manager
manager, ok := attachment.Managers[uploaderID]
if !ok {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: "Uploader not found: " + uploaderID,
}
response.RespondWithError(c, response.StatusNotFound, errorResp)
return
}
// Check if file exists
exists := manager.Exists(c.Request.Context(), fileID)
successData := gin.H{
"exists": exists,
"file_id": fileID,
}
response.RespondWithSuccess(c, response.StatusOK, successData)
}