713 lines
22 KiB
Go
713 lines
22 KiB
Go
package handler
|
|
|
|
import (
|
|
"io"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"cloudnest/internal/application/auth"
|
|
"cloudnest/internal/application/file"
|
|
"cloudnest/internal/pkg/errors"
|
|
"cloudnest/internal/pkg/response"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
type FileHandler struct {
|
|
service *file.FileService
|
|
authService *auth.AuthService
|
|
}
|
|
|
|
func NewFileHandler(service *file.FileService, authService *auth.AuthService) *FileHandler {
|
|
return &FileHandler{service: service, authService: authService}
|
|
}
|
|
|
|
// Upload handles file upload requests.
|
|
// Upload 处理文件上传请求。
|
|
//
|
|
// Request:
|
|
// - Method: POST
|
|
// - Path: /api/v1/files/upload
|
|
// - Content-Type: multipart/form-data
|
|
// - Body: file=<file>
|
|
// - Header: Authorization: Bearer <token>
|
|
//
|
|
// Response:
|
|
// - Success: 200 OK {"code": 0, "message": "success", "data": {"message": "上传成功", "file": "string", "size": int}}
|
|
// - Bad Request: 400 {"code": 400, "message": "文件上传失败"}
|
|
// - Unauthorized: 401 {"code": 401, "message": "未提供认证令牌"}
|
|
//
|
|
// 请求:
|
|
// - 方法: POST
|
|
// - 路径: /api/v1/files/upload
|
|
// - 内容类型: multipart/form-data
|
|
// - 请求体: file=<file>
|
|
// - 头部: Authorization: Bearer <token>
|
|
//
|
|
// 响应:
|
|
// - 成功: 200 OK {"code": 0, "message": "success", "data": {"message": "上传成功", "file": "string", "size": int}}
|
|
// - 请求错误: 400 {"code": 400, "message": "文件上传失败"}
|
|
// - 未授权: 401 {"code": 401, "message": "未提供认证令牌"}
|
|
func (h *FileHandler) Upload(c *gin.Context) {
|
|
f, header, err := c.Request.FormFile("file")
|
|
if err != nil {
|
|
response.BadRequest(c, "文件上传失败")
|
|
return
|
|
}
|
|
defer f.Close()
|
|
|
|
userCode := c.GetString("user_code")
|
|
|
|
if err := h.authService.CheckStorageLimit(userCode, header.Size); err != nil {
|
|
if appErr, ok := errors.AsAppError(err); ok {
|
|
response.Error(c, appErr.Code, appErr.Message)
|
|
} else {
|
|
response.InternalError(c, err.Error())
|
|
}
|
|
return
|
|
}
|
|
|
|
if err := h.service.Upload(userCode, header.Filename, f, header.Size, header.Header.Get("Content-Type")); err != nil {
|
|
if appErr, ok := errors.AsAppError(err); ok {
|
|
response.Error(c, appErr.Code, appErr.Message)
|
|
} else {
|
|
response.InternalError(c, err.Error())
|
|
}
|
|
return
|
|
}
|
|
|
|
if err := h.authService.AddStorageUsed(userCode, header.Size); err != nil {
|
|
response.InternalError(c, "更新存储空间失败")
|
|
return
|
|
}
|
|
|
|
response.Success(c, file.UploadResponse{
|
|
Message: "上传成功",
|
|
File: userCode + "/" + header.Filename,
|
|
Size: header.Size,
|
|
})
|
|
}
|
|
|
|
// List handles file listing requests.
|
|
// List 处理文件列表请求。
|
|
//
|
|
// Request:
|
|
// - Method: GET
|
|
// - Path: /api/v1/files
|
|
// - Header: Authorization: Bearer <token>
|
|
//
|
|
// Response:
|
|
// - Success: 200 OK {"code": 0, "message": "success", "data": {"files": ["string"]}}
|
|
// - Unauthorized: 401 {"code": 401, "message": "未提供认证令牌"}
|
|
//
|
|
// 请求:
|
|
// - 方法: GET
|
|
// - 路径: /api/v1/files
|
|
// - 头部: Authorization: Bearer <token>
|
|
//
|
|
// 响应:
|
|
// - 成功: 200 OK {"code": 0, "message": "success", "data": {"files": ["string"]}}
|
|
// - 未授权: 401 {"code": 401, "message": "未提供认证令牌"}
|
|
func (h *FileHandler) List(c *gin.Context) {
|
|
userCode := c.GetString("user_code")
|
|
fileInfos, err := h.service.List(userCode)
|
|
if err != nil {
|
|
if appErr, ok := errors.AsAppError(err); ok {
|
|
response.Error(c, appErr.Code, appErr.Message)
|
|
} else {
|
|
response.InternalError(c, err.Error())
|
|
}
|
|
return
|
|
}
|
|
|
|
items := make([]file.FileItem, len(fileInfos))
|
|
for i, info := range fileInfos {
|
|
items[i] = file.FileItem{
|
|
Key: info.Key,
|
|
Size: info.Size,
|
|
LastModified: info.LastModified,
|
|
}
|
|
}
|
|
|
|
response.Success(c, file.ListResponse{Files: items})
|
|
}
|
|
|
|
// Download handles file download requests by streaming the file through the backend.
|
|
// Download 处理文件下载请求,通过后端流式传输文件。
|
|
//
|
|
// Request:
|
|
// - Method: GET
|
|
// - Path: /api/v1/files/download/:name
|
|
// - Header: Authorization: Bearer <token>
|
|
//
|
|
// Response:
|
|
// - Success: 200 OK (file content streamed with Content-Disposition header)
|
|
// - Not Found: 404 {"code": 404, "message": "文件不存在"}
|
|
// - Unauthorized: 401 {"code": 401, "message": "未提供认证令牌"}
|
|
//
|
|
// 请求:
|
|
// - 方法: GET
|
|
// - 路径: /api/v1/files/download/:name
|
|
// - 头部: Authorization: Bearer <token>
|
|
//
|
|
// 响应:
|
|
// - 成功: 200 OK(文件内容流式传输,带 Content-Disposition 头部)
|
|
// - 未找到: 404 {"code": 404, "message": "文件不存在"}
|
|
// - 未授权: 401 {"code": 401, "message": "未提供认证令牌"}
|
|
func (h *FileHandler) Download(c *gin.Context) {
|
|
userCode := c.GetString("user_code")
|
|
filename := c.Param("name")
|
|
|
|
reader, size, contentType, err := h.service.GetFileStream(userCode, filename)
|
|
if err != nil {
|
|
if appErr, ok := errors.AsAppError(err); ok {
|
|
response.Error(c, appErr.Code, appErr.Message)
|
|
} else {
|
|
response.InternalError(c, err.Error())
|
|
}
|
|
return
|
|
}
|
|
defer reader.Close()
|
|
|
|
// Set headers for file download / 设置文件下载头部
|
|
encodedName := url.PathEscape(filename)
|
|
c.Header("Content-Disposition", "attachment; filename=\""+filename+"\"; filename*=UTF-8''"+encodedName)
|
|
c.Header("Content-Type", contentType)
|
|
c.Header("Content-Length", strconv.FormatInt(size, 10))
|
|
|
|
// Stream the file content to the response / 将文件内容流式传输到响应
|
|
c.Status(200)
|
|
io.Copy(c.Writer, reader)
|
|
}
|
|
|
|
// Delete handles file deletion requests.
|
|
// Delete 处理文件删除请求。
|
|
//
|
|
// Request:
|
|
// - Method: DELETE
|
|
// - Path: /api/v1/files/:name
|
|
// - Header: Authorization: Bearer <token>
|
|
//
|
|
// Response:
|
|
// - Success: 200 OK {"code": 0, "message": "删除成功"}
|
|
// - Not Found: 404 {"code": 404, "message": "文件不存在"}
|
|
// - Unauthorized: 401 {"code": 401, "message": "未提供认证令牌"}
|
|
//
|
|
// 请求:
|
|
// - 方法: DELETE
|
|
// - 路径: /api/v1/files/:name
|
|
// - 头部: Authorization: Bearer <token>
|
|
//
|
|
// 响应:
|
|
// - 成功: 200 OK {"code": 0, "message": "删除成功"}
|
|
// - 未找到: 404 {"code": 404, "message": "文件不存在"}
|
|
// - 未授权: 401 {"code": 401, "message": "未提供认证令牌"}
|
|
func (h *FileHandler) Delete(c *gin.Context) {
|
|
userCode := c.GetString("user_code")
|
|
if err := h.service.Delete(userCode, c.Param("name")); err != nil {
|
|
if appErr, ok := errors.AsAppError(err); ok {
|
|
response.Error(c, appErr.Code, appErr.Message)
|
|
} else {
|
|
response.InternalError(c, err.Error())
|
|
}
|
|
return
|
|
}
|
|
|
|
response.SuccessMsg(c, "删除成功")
|
|
}
|
|
|
|
// SoftDelete handles soft delete requests, moving file to recycle bin.
|
|
// SoftDelete 处理软删除请求,将文件移动到回收站。
|
|
//
|
|
// Request:
|
|
// - Method: POST
|
|
// - Path: /api/v1/files/soft-delete/:name
|
|
// - Header: Authorization: Bearer <token>
|
|
//
|
|
// Response:
|
|
// - Success: 200 OK {"code": 0, "message": "已移到回收站"}
|
|
// - Unauthorized: 401 {"code": 401, "message": "未提供认证令牌"}
|
|
//
|
|
// 请求:
|
|
// - 方法: POST
|
|
// - 路径: /api/v1/files/soft-delete/:name
|
|
// - 头部: Authorization: Bearer <token>
|
|
//
|
|
// 响应:
|
|
// - 成功: 200 OK {"code": 0, "message": "已移到回收站"}
|
|
// - 未授权: 401 {"code": 401, "message": "未提供认证令牌"}
|
|
func (h *FileHandler) SoftDelete(c *gin.Context) {
|
|
userCode := c.GetString("user_code")
|
|
userID := c.GetUint("user_id")
|
|
filename := c.Param("name")
|
|
var req struct {
|
|
Size int64 `json:"size"`
|
|
}
|
|
c.ShouldBindJSON(&req)
|
|
|
|
if err := h.service.SoftDelete(userID, userCode, filename, req.Size); err != nil {
|
|
if appErr, ok := errors.AsAppError(err); ok {
|
|
response.Error(c, appErr.Code, appErr.Message)
|
|
} else {
|
|
response.InternalError(c, err.Error())
|
|
}
|
|
return
|
|
}
|
|
|
|
response.SuccessMsg(c, "已移到回收站")
|
|
}
|
|
|
|
// Favorite handles file favorite requests.
|
|
// Favorite 处理文件收藏请求。
|
|
//
|
|
// Request:
|
|
// - Method: POST
|
|
// - Path: /api/v1/files/favorite/:name
|
|
// - Header: Authorization: Bearer <token>
|
|
//
|
|
// Response:
|
|
// - Success: 200 OK {"code": 0, "message": "收藏成功"}
|
|
// - Bad Request: 400 {"code": 400, "message": "文件已在收藏夹中"}
|
|
// - Unauthorized: 401 {"code": 401, "message": "未提供认证令牌"}
|
|
//
|
|
// 请求:
|
|
// - 方法: POST
|
|
// - 路径: /api/v1/files/favorite/:name
|
|
// - 头部: Authorization: Bearer <token>
|
|
//
|
|
// 响应:
|
|
// - 成功: 200 OK {"code": 0, "message": "收藏成功"}
|
|
// - 请求错误: 400 {"code": 400, "message": "文件已在收藏夹中"}
|
|
// - 未授权: 401 {"code": 401, "message": "未提供认证令牌"}
|
|
func (h *FileHandler) Favorite(c *gin.Context) {
|
|
userCode := c.GetString("user_code")
|
|
userID := c.GetUint("user_id")
|
|
if err := h.service.Favorite(userID, userCode, c.Param("name")); err != nil {
|
|
if appErr, ok := errors.AsAppError(err); ok {
|
|
response.Error(c, appErr.Code, appErr.Message)
|
|
} else {
|
|
response.InternalError(c, err.Error())
|
|
}
|
|
return
|
|
}
|
|
|
|
response.SuccessMsg(c, "收藏成功")
|
|
}
|
|
|
|
// Unfavorite handles file unfavorite requests.
|
|
// Unfavorite 处理文件取消收藏请求。
|
|
//
|
|
// Request:
|
|
// - Method: DELETE
|
|
// - Path: /api/v1/files/favorite/:name
|
|
// - Header: Authorization: Bearer <token>
|
|
//
|
|
// Response:
|
|
// - Success: 200 OK {"code": 0, "message": "取消收藏成功"}
|
|
// - Not Found: 404 {"code": 404, "message": "收藏记录不存在"}
|
|
// - Unauthorized: 401 {"code": 401, "message": "未提供认证令牌"}
|
|
//
|
|
// 请求:
|
|
// - 方法: DELETE
|
|
// - 路径: /api/v1/files/favorite/:name
|
|
// - 头部: Authorization: Bearer <token>
|
|
//
|
|
// 响应:
|
|
// - 成功: 200 OK {"code": 0, "message": "取消收藏成功"}
|
|
// - 未找到: 404 {"code": 404, "message": "收藏记录不存在"}
|
|
// - 未授权: 401 {"code": 401, "message": "未提供认证令牌"}
|
|
func (h *FileHandler) Unfavorite(c *gin.Context) {
|
|
userCode := c.GetString("user_code")
|
|
if err := h.service.Unfavorite(userCode, c.Param("name")); err != nil {
|
|
if appErr, ok := errors.AsAppError(err); ok {
|
|
response.Error(c, appErr.Code, appErr.Message)
|
|
} else {
|
|
response.InternalError(c, err.Error())
|
|
}
|
|
return
|
|
}
|
|
|
|
response.SuccessMsg(c, "取消收藏成功")
|
|
}
|
|
|
|
// ListFavorites handles favorite listing requests.
|
|
// ListFavorites 处理收藏列表请求。
|
|
//
|
|
// Request:
|
|
// - Method: GET
|
|
// - Path: /api/v1/files/favorites
|
|
// - Header: Authorization: Bearer <token>
|
|
//
|
|
// Response:
|
|
// - Success: 200 OK {"code": 0, "message": "success", "data": {"favorites": [...]}}
|
|
// - Unauthorized: 401 {"code": 401, "message": "未提供认证令牌"}
|
|
//
|
|
// 请求:
|
|
// - 方法: GET
|
|
// - 路径: /api/v1/files/favorites
|
|
// - 头部: Authorization: Bearer <token>
|
|
//
|
|
// 响应:
|
|
// - 成功: 200 OK {"code": 0, "message": "success", "data": {"favorites": [...]}}
|
|
// - 未授权: 401 {"code": 401, "message": "未提供认证令牌"}
|
|
func (h *FileHandler) ListFavorites(c *gin.Context) {
|
|
userCode := c.GetString("user_code")
|
|
favorites, err := h.service.GetFavorites(userCode)
|
|
if err != nil {
|
|
if appErr, ok := errors.AsAppError(err); ok {
|
|
response.Error(c, appErr.Code, appErr.Message)
|
|
} else {
|
|
response.InternalError(c, err.Error())
|
|
}
|
|
return
|
|
}
|
|
|
|
items := make([]file.FavoriteItem, len(favorites))
|
|
for i, f := range favorites {
|
|
items[i] = file.FavoriteItem{
|
|
FileKey: f.FileKey,
|
|
FileName: strings.TrimPrefix(f.FileKey, userCode+"/"),
|
|
Size: f.Size,
|
|
CreatedAt: f.CreatedAt.Format("2006-01-02 15:04:05"),
|
|
}
|
|
}
|
|
|
|
response.Success(c, file.FavoriteListResponse{Favorites: items})
|
|
}
|
|
|
|
// RecycleBin handles recycle bin listing requests.
|
|
// RecycleBin 处理回收站列表请求。
|
|
//
|
|
// Request:
|
|
// - Method: GET
|
|
// - Path: /api/v1/files/recycle-bin
|
|
// - Header: Authorization: Bearer <token>
|
|
//
|
|
// Response:
|
|
// - Success: 200 OK {"code": 0, "message": "success", "data": {"files": [...]}}
|
|
// - Unauthorized: 401 {"code": 401, "message": "未提供认证令牌"}
|
|
//
|
|
// 请求:
|
|
// - 方法: GET
|
|
// - 路径: /api/v1/files/recycle-bin
|
|
// - 头部: Authorization: Bearer <token>
|
|
//
|
|
// 响应:
|
|
// - 成功: 200 OK {"code": 0, "message": "success", "data": {"files": [...]}}
|
|
// - 未授权: 401 {"code": 401, "message": "未提供认证令牌"}
|
|
func (h *FileHandler) RecycleBin(c *gin.Context) {
|
|
userCode := c.GetString("user_code")
|
|
deletedFiles, err := h.service.GetRecycleBin(userCode)
|
|
if err != nil {
|
|
if appErr, ok := errors.AsAppError(err); ok {
|
|
response.Error(c, appErr.Code, appErr.Message)
|
|
} else {
|
|
response.InternalError(c, err.Error())
|
|
}
|
|
return
|
|
}
|
|
|
|
items := make([]file.DeletedFileItem, len(deletedFiles))
|
|
for i, df := range deletedFiles {
|
|
items[i] = file.DeletedFileItem{
|
|
FileKey: df.FileKey,
|
|
FileName: df.FileName,
|
|
Size: df.Size,
|
|
DeletedAt: df.DeletedAt.Format("2006-01-02 15:04:05"),
|
|
}
|
|
}
|
|
|
|
response.Success(c, file.RecycleBinResponse{Files: items})
|
|
}
|
|
|
|
// Restore handles file restore requests from recycle bin.
|
|
// Restore 处理从回收站恢复文件的请求。
|
|
//
|
|
// Request:
|
|
// - Method: POST
|
|
// - Path: /api/v1/files/restore/:name
|
|
// - Header: Authorization: Bearer <token>
|
|
//
|
|
// Response:
|
|
// - Success: 200 OK {"code": 0, "message": "恢复成功"}
|
|
// - Not Found: 404 {"code": 404, "message": "回收站中不存在该文件"}
|
|
// - Unauthorized: 401 {"code": 401, "message": "未提供认证令牌"}
|
|
//
|
|
// 请求:
|
|
// - 方法: POST
|
|
// - 路径: /api/v1/files/restore/:name
|
|
// - 头部: Authorization: Bearer <token>
|
|
//
|
|
// 响应:
|
|
// - 成功: 200 OK {"code": 0, "message": "恢复成功"}
|
|
// - 未找到: 404 {"code": 404, "message": "回收站中不存在该文件"}
|
|
// - 未授权: 401 {"code": 401, "message": "未提供认证令牌"}
|
|
func (h *FileHandler) Restore(c *gin.Context) {
|
|
userCode := c.GetString("user_code")
|
|
if _, err := h.service.RestoreFromRecycleBin(userCode, c.Param("name")); err != nil {
|
|
if appErr, ok := errors.AsAppError(err); ok {
|
|
response.Error(c, appErr.Code, appErr.Message)
|
|
} else {
|
|
response.InternalError(c, err.Error())
|
|
}
|
|
return
|
|
}
|
|
|
|
response.SuccessMsg(c, "恢复成功")
|
|
}
|
|
|
|
// DeleteFromRecycleBin handles permanent deletion requests from recycle bin.
|
|
// DeleteFromRecycleBin 处理从回收站永久删除文件的请求。
|
|
//
|
|
// Request:
|
|
// - Method: DELETE
|
|
// - Path: /api/v1/files/recycle-bin/:name
|
|
// - Header: Authorization: Bearer <token>
|
|
//
|
|
// Response:
|
|
// - Success: 200 OK {"code": 0, "message": "永久删除成功"}
|
|
// - Not Found: 404 {"code": 404, "message": "回收站中不存在该文件"}
|
|
// - Unauthorized: 401 {"code": 401, "message": "未提供认证令牌"}
|
|
//
|
|
// 请求:
|
|
// - 方法: DELETE
|
|
// - 路径: /api/v1/files/recycle-bin/:name
|
|
// - 头部: Authorization: Bearer <token>
|
|
//
|
|
// 响应:
|
|
// - 成功: 200 OK {"code": 0, "message": "永久删除成功"}
|
|
// - 未找到: 404 {"code": 404, "message": "回收站中不存在该文件"}
|
|
// - 未授权: 401 {"code": 401, "message": "未提供认证令牌"}
|
|
func (h *FileHandler) DeleteFromRecycleBin(c *gin.Context) {
|
|
userCode := c.GetString("user_code")
|
|
if err := h.service.DeleteFromRecycleBin(userCode, c.Param("name")); err != nil {
|
|
if appErr, ok := errors.AsAppError(err); ok {
|
|
response.Error(c, appErr.Code, appErr.Message)
|
|
} else {
|
|
response.InternalError(c, err.Error())
|
|
}
|
|
return
|
|
}
|
|
|
|
response.SuccessMsg(c, "永久删除成功")
|
|
}
|
|
|
|
// Preview handles file preview requests.
|
|
// Preview 处理文件预览请求。
|
|
//
|
|
// Request:
|
|
// - Method: GET
|
|
// - Path: /api/v1/files/preview/:name
|
|
// - Header: Authorization: Bearer <token>
|
|
//
|
|
// Response:
|
|
// - Success: 200 OK {"code": 0, "message": "success", "data": {"file_type": "string"}}
|
|
// - Not Found: 404 {"code": 404, "message": "文件不存在"}
|
|
// - Unauthorized: 401 {"code": 401, "message": "未提供认证令牌"}
|
|
//
|
|
// 请求:
|
|
// - 方法: GET
|
|
// - 路径: /api/v1/files/preview/:name
|
|
// - 头部: Authorization: Bearer <token>
|
|
//
|
|
// 响应:
|
|
// - 成功: 200 OK {"code": 0, "message": "success", "data": {"file_type": "string"}}
|
|
// - 未找到: 404 {"code": 404, "message": "文件不存在"}
|
|
// - 未授权: 401 {"code": 401, "message": "未提供认证令牌"}
|
|
func (h *FileHandler) Preview(c *gin.Context) {
|
|
filename := c.Param("name")
|
|
|
|
fileType := "other"
|
|
if h.service.IsImageFile(filename) {
|
|
fileType = "image"
|
|
} else if h.service.IsTextFile(filename) {
|
|
fileType = "text"
|
|
} else if h.service.IsPDFFile(filename) {
|
|
fileType = "pdf"
|
|
}
|
|
|
|
response.Success(c, file.PreviewResponse{FileType: fileType})
|
|
}
|
|
|
|
// Content handles file content requests by streaming the file inline through the backend.
|
|
// Content 处理文件内容请求,通过后端内联流式传输文件。
|
|
//
|
|
// This endpoint is used for file preview (images, PDFs, text) by streaming
|
|
// the file content directly from MinIO through the Go backend, avoiding
|
|
// CORS issues and keeping MinIO internal.
|
|
//
|
|
// Request:
|
|
// - Method: GET
|
|
// - Path: /api/v1/files/content/:name
|
|
// - Header: Authorization: Bearer <token>
|
|
//
|
|
// Response:
|
|
// - Success: 200 OK (file content streamed inline)
|
|
// - Not Found: 404 {"code": 404, "message": "文件不存在"}
|
|
// - Unauthorized: 401 {"code": 401, "message": "未提供认证令牌"}
|
|
//
|
|
// 此端点用于文件预览(图片、PDF、文本),通过 Go 后端直接从 MinIO
|
|
// 流式传输文件内容,避免 CORS 问题并保持 MinIO 内部可见。
|
|
//
|
|
// 请求:
|
|
// - 方法: GET
|
|
// - 路径: /api/v1/files/content/:name
|
|
// - 头部: Authorization: Bearer <token>
|
|
//
|
|
// 响应:
|
|
// - 成功: 200 OK(文件内容内联流式传输)
|
|
// - 未找到: 404 {"code": 404, "message": "文件不存在"}
|
|
// - 未授权: 401 {"code": 401, "message": "未提供认证令牌"}
|
|
func (h *FileHandler) Content(c *gin.Context) {
|
|
userCode := c.GetString("user_code")
|
|
filename := c.Param("name")
|
|
|
|
reader, size, contentType, err := h.service.GetFileStream(userCode, filename)
|
|
if err != nil {
|
|
if appErr, ok := errors.AsAppError(err); ok {
|
|
response.Error(c, appErr.Code, appErr.Message)
|
|
} else {
|
|
response.InternalError(c, err.Error())
|
|
}
|
|
return
|
|
}
|
|
defer reader.Close()
|
|
|
|
// Set headers for inline preview / 设置内联预览头部
|
|
c.Header("Content-Disposition", "inline")
|
|
c.Header("Content-Type", contentType)
|
|
c.Header("Content-Length", strconv.FormatInt(size, 10))
|
|
|
|
// Stream the file content to the response / 将文件内容流式传输到响应
|
|
c.Status(200)
|
|
io.Copy(c.Writer, reader)
|
|
}
|
|
|
|
// CheckUpload handles upload check requests for resumable uploads.
|
|
// CheckUpload 处理断点续传的上传检查请求。
|
|
//
|
|
// Request:
|
|
// - Method: GET
|
|
// - Path: /api/v1/files/upload/check
|
|
// - Query: hash=<file_hash>&filename=<filename>
|
|
//
|
|
// Response:
|
|
// - Success: 200 OK {"code": 0, "message": "success", "data": {"uploaded_chunks": [0, 1, 2]}}
|
|
func (h *FileHandler) CheckUpload(c *gin.Context) {
|
|
userCode := c.GetString("user_code")
|
|
hash := c.Query("hash")
|
|
if hash == "" {
|
|
response.BadRequest(c, "缺少文件哈希")
|
|
return
|
|
}
|
|
|
|
chunks, err := h.service.GetUploadedChunks(userCode, hash)
|
|
if err != nil {
|
|
response.InternalError(c, err.Error())
|
|
return
|
|
}
|
|
|
|
response.Success(c, map[string]interface{}{"uploaded_chunks": chunks})
|
|
}
|
|
|
|
// UploadChunk handles chunk upload requests for resumable uploads.
|
|
// UploadChunk 处理断点续传的分片上传请求。
|
|
//
|
|
// Request:
|
|
// - Method: POST
|
|
// - Path: /api/v1/files/upload/chunk
|
|
// - Content-Type: multipart/form-data
|
|
// - Body: file=<chunk_data>&hash=<file_hash>&chunk_index=<index>&chunk_count=<count>&filename=<filename>&file_size=<size>
|
|
func (h *FileHandler) UploadChunk(c *gin.Context) {
|
|
userCode := c.GetString("user_code")
|
|
|
|
f, _, err := c.Request.FormFile("file")
|
|
if err != nil {
|
|
response.BadRequest(c, "分片上传失败")
|
|
return
|
|
}
|
|
defer f.Close()
|
|
|
|
hash := c.PostForm("hash")
|
|
chunkIndexStr := c.PostForm("chunk_index")
|
|
fileSizeStr := c.PostForm("file_size")
|
|
|
|
if hash == "" || chunkIndexStr == "" || fileSizeStr == "" {
|
|
response.BadRequest(c, "缺少必要参数")
|
|
return
|
|
}
|
|
|
|
chunkIndex, err := strconv.Atoi(chunkIndexStr)
|
|
if err != nil {
|
|
response.BadRequest(c, "分片索引无效")
|
|
return
|
|
}
|
|
|
|
fileSize, err := strconv.ParseInt(fileSizeStr, 10, 64)
|
|
if err != nil {
|
|
response.BadRequest(c, "文件大小无效")
|
|
return
|
|
}
|
|
|
|
if err := h.authService.CheckStorageLimit(userCode, fileSize); err != nil {
|
|
if appErr, ok := errors.AsAppError(err); ok {
|
|
response.Error(c, appErr.Code, appErr.Message)
|
|
} else {
|
|
response.InternalError(c, err.Error())
|
|
}
|
|
return
|
|
}
|
|
|
|
data, err := io.ReadAll(f)
|
|
if err != nil {
|
|
response.BadRequest(c, "读取分片数据失败")
|
|
return
|
|
}
|
|
|
|
if err := h.service.SaveChunk(userCode, hash, chunkIndex, data); err != nil {
|
|
response.InternalError(c, err.Error())
|
|
return
|
|
}
|
|
|
|
response.SuccessMsg(c, "分片上传成功")
|
|
}
|
|
|
|
// CompleteUpload handles upload completion requests for resumable uploads.
|
|
// CompleteUpload 处理断点续传的上传完成请求。
|
|
//
|
|
// Request:
|
|
// - Method: POST
|
|
// - Path: /api/v1/files/upload/complete
|
|
// - Body: {"hash": "string", "filename": "string", "chunk_count": int, "file_size": int}
|
|
func (h *FileHandler) CompleteUpload(c *gin.Context) {
|
|
userCode := c.GetString("user_code")
|
|
|
|
var req struct {
|
|
Hash string `json:"hash"`
|
|
Filename string `json:"filename"`
|
|
ChunkCount int `json:"chunk_count"`
|
|
FileSize int64 `json:"file_size"`
|
|
}
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, "参数错误")
|
|
return
|
|
}
|
|
|
|
if req.Hash == "" || req.Filename == "" || req.ChunkCount <= 0 {
|
|
response.BadRequest(c, "缺少必要参数")
|
|
return
|
|
}
|
|
|
|
if err := h.service.MergeChunks(userCode, req.Hash, req.Filename, req.ChunkCount, req.FileSize); err != nil {
|
|
response.InternalError(c, err.Error())
|
|
return
|
|
}
|
|
|
|
if err := h.authService.AddStorageUsed(userCode, req.FileSize); err != nil {
|
|
response.InternalError(c, "更新存储空间失败")
|
|
return
|
|
}
|
|
|
|
response.SuccessMsg(c, "上传完成")
|
|
} |