- Introduced a comprehensive Signin API for user authentication, supporting multiple OAuth providers (Google, GitHub, Microsoft, Apple). - Updated the file management API endpoints to use a singular `/file` path instead of `/files`, enhancing consistency across the API. - Revised README documentation to include detailed descriptions of the new Signin API and updated file management endpoints. - Enhanced test cases to reflect the changes in endpoint structure and ensure robust coverage for the new Signin functionality.
470 lines
13 KiB
Go
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("/:uploaderID", upload)
|
|
|
|
// List files
|
|
group.GET("/:uploaderID", list)
|
|
|
|
// Retrieve file
|
|
group.GET("/:uploaderID/:fileID", retrieve)
|
|
|
|
// Delete file
|
|
group.DELETE("/:uploaderID/:fileID", delete)
|
|
|
|
// Retrieve file content
|
|
group.GET("/:uploaderID/:fileID/content", content)
|
|
|
|
// Check if file exists
|
|
group.GET("/: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)
|
|
}
|