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= // - Header: Authorization: Bearer // // 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= // - 头部: Authorization: Bearer // // 响应: // - 成功: 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 // // Response: // - Success: 200 OK {"code": 0, "message": "success", "data": {"files": ["string"]}} // - Unauthorized: 401 {"code": 401, "message": "未提供认证令牌"} // // 请求: // - 方法: GET // - 路径: /api/v1/files // - 头部: Authorization: Bearer // // 响应: // - 成功: 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 // // 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 // // 响应: // - 成功: 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 // // 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 // // 响应: // - 成功: 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 // // Response: // - Success: 200 OK {"code": 0, "message": "已移到回收站"} // - Unauthorized: 401 {"code": 401, "message": "未提供认证令牌"} // // 请求: // - 方法: POST // - 路径: /api/v1/files/soft-delete/:name // - 头部: Authorization: Bearer // // 响应: // - 成功: 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 // // 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 // // 响应: // - 成功: 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 // // 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 // // 响应: // - 成功: 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 // // Response: // - Success: 200 OK {"code": 0, "message": "success", "data": {"favorites": [...]}} // - Unauthorized: 401 {"code": 401, "message": "未提供认证令牌"} // // 请求: // - 方法: GET // - 路径: /api/v1/files/favorites // - 头部: Authorization: Bearer // // 响应: // - 成功: 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 // // Response: // - Success: 200 OK {"code": 0, "message": "success", "data": {"files": [...]}} // - Unauthorized: 401 {"code": 401, "message": "未提供认证令牌"} // // 请求: // - 方法: GET // - 路径: /api/v1/files/recycle-bin // - 头部: Authorization: Bearer // // 响应: // - 成功: 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 // // 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 // // 响应: // - 成功: 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 // // 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 // // 响应: // - 成功: 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 // // 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 // // 响应: // - 成功: 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 // // 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 // // 响应: // - 成功: 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=&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=&hash=&chunk_index=&chunk_count=&filename=&file_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, "上传完成") }