package controller import ( "fmt" "io" "net/http" "strconv" "time" "seeyon-filesystem/config" "seeyon-filesystem/middleware" "seeyon-filesystem/model" "seeyon-filesystem/storage" "seeyon-filesystem/utils" "github.com/gin-gonic/gin" ) // FileAccessController 文件访问控制器 // 统一处理文件下载/预览, 支持中转和预签名两种模式 type FileAccessController struct{} func NewFileAccessController() *FileAccessController { return &FileAccessController{} } // Access 统一文件访问入口 // GET /api/file/:id/access // 根据存储策略自动选择: 中转(proxy) / 预签名(presigned) / CDN func (c *FileAccessController) Access(ctx *gin.Context) { ownerID := middleware.GetCurrentUserID(ctx) fileID, err := strconv.ParseUint(ctx.Param("id"), 10, 64) if err != nil { utils.BadRequest(ctx, "无效的文件ID") return } file, err := getFileByID(fileID) if err != nil { utils.NotFound(ctx, "文件不存在") return } if file.OwnerID != ownerID { utils.Forbidden(ctx, "无权访问") return } c.serveFile(ctx, file) } // AccessByShare 通过分享访问文件(不检查归属) // GET /api/s/:code/access func (c *FileAccessController) AccessByShare(ctx *gin.Context) { code := ctx.Param("code") // 验证分享 share, err := getShareByCode(code) if err != nil { utils.NotFound(ctx, "分享不存在或已失效") return } // 检查密码 if share.Password != "" { password := ctx.Query("password") if !verifySharePassword(share, password) { utils.Unauthorized(ctx, "需要密码") return } } // 检查过期 if share.ExpireAt != nil && share.ExpireAt.Before(time.Now()) { utils.NotFound(ctx, "分享已过期") return } file, err := getFileByID(share.FileID) if err != nil { utils.NotFound(ctx, "文件不存在") return } c.serveFile(ctx, file) } // serveFile 根据存储策略配置的访问模式提供文件 func (c *FileAccessController) serveFile(ctx *gin.Context, file *model.File) { // 获取存储策略 policy := getStoragePolicy(file.StoragePolicyID) // 获取存储引擎 engine, err := storage.GetEngineByPolicyID(file.StoragePolicyID) if err != nil { utils.ServerError(ctx, "存储引擎不可用") return } accessMode := policy.Config.AccessMode switch accessMode { case "presigned": // 预签名模式: 返回URL, 客户端直连MinIO c.servePresigned(ctx, file, engine, policy) case "cdn": // CDN模式: 返回CDN链接 c.serveCDN(ctx, file, engine) default: // 中转模式(默认): 服务端拉取文件流转发给客户端 c.serveProxy(ctx, file, engine, policy) } } // serveProxy 中转模式: 服务端从存储拉取文件, 转发给客户端 func (c *FileAccessController) serveProxy(ctx *gin.Context, file *model.File, engine storage.Engine, policy *model.StoragePolicy) { reader, err := engine.Download(file.StorageKey) if err != nil { utils.ServerError(ctx, "读取文件失败: "+err.Error()) return } defer reader.Close() // 设置响应头 ctx.Header("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", file.Name)) ctx.Header("Content-Type", file.MimeType) ctx.Header("Content-Length", strconv.FormatInt(file.Size, 10)) ctx.Header("Cache-Control", "private, max-age=3600") // X-Sendfile模式: 返回路径让Nginx直接发送文件 if policy.Config.UseXSendfile && policy.Config.XSendfilePath != "" { // Nginx会拦截这个头, 直接从本地路径发送文件 localPath := fmt.Sprintf("%s/%s", policy.Config.XSendfilePath, file.StorageKey) ctx.Header("X-Accel-Redirect", localPath) ctx.Status(200) return } // 普通中转: 流式传输 ctx.Status(200) io.Copy(ctx.Writer, reader) } // servePresigned 预签名模式: 返回MinIO预签名URL func (c *FileAccessController) servePresigned(ctx *gin.Context, file *model.File, engine storage.Engine, policy *model.StoragePolicy) { minioEngine, ok := engine.(*storage.MinIOEngine) if !ok { // 非MinIO引擎, 回退到中转模式 c.serveProxy(ctx, file, engine, policy) return } presignedURL, err := minioEngine.GetPresignedURL(file.StorageKey, 24*time.Hour) if err != nil { utils.ServerError(ctx, "生成预签名URL失败") return } // 替换为Nginx公网地址 if policy.Config.NginxEndpoint != "" { presignedURL = replaceMinIOEndpoint(presignedURL, minioEngine.Endpoint, policy.Config.NginxEndpoint) } // 返回302重定向到预签名URL ctx.Redirect(http.StatusFound, presignedURL) } // serveCDN CDN模式: 返回CDN链接 func (c *FileAccessController) serveCDN(ctx *gin.Context, file *model.File, engine storage.Engine) { if localEngine, ok := engine.(*storage.LocalEngine); ok { cdnURL := localEngine.GetURL(file.StorageKey) ctx.Redirect(http.StatusFound, cdnURL) return } // 回退到中转 c.serveProxy(ctx, file, engine, &model.StoragePolicy{}) } // GetAccessURL 获取文件访问URL(不重定向, 返回JSON) // GET /api/file/:id/access-url func (c *FileAccessController) GetAccessURL(ctx *gin.Context) { ownerID := middleware.GetCurrentUserID(ctx) fileID, err := strconv.ParseUint(ctx.Param("id"), 10, 64) if err != nil { utils.BadRequest(ctx, "无效的文件ID") return } file, err := getFileByID(fileID) if err != nil { utils.NotFound(ctx, "文件不存在") return } if file.OwnerID != ownerID { utils.Forbidden(ctx, "无权访问") return } policy := getStoragePolicy(file.StoragePolicyID) engine, err := storage.GetEngineByPolicyID(file.StoragePolicyID) if err != nil { utils.ServerError(ctx, "存储引擎不可用") return } result := gin.H{ "mode": policy.Config.AccessMode, "file_name": file.Name, "file_size": file.Size, "content_type": file.MimeType, } switch policy.Config.AccessMode { case "presigned": if minioEngine, ok := engine.(*storage.MinIOEngine); ok { presignedURL, _ := minioEngine.GetPresignedURL(file.StorageKey, 24*time.Hour) if policy.Config.NginxEndpoint != "" { presignedURL = replaceMinIOEndpoint(presignedURL, minioEngine.Endpoint, policy.Config.NginxEndpoint) } result["url"] = presignedURL } case "cdn": if localEngine, ok := engine.(*storage.LocalEngine); ok { result["url"] = localEngine.GetURL(file.StorageKey) } default: result["url"] = fmt.Sprintf("/api/file/%d/access", fileID) result["mode"] = "proxy" } utils.Success(ctx, result) } // ========== 辅助函数 ========== func getFileByID(id uint64) (*model.File, error) { var file model.File if err := config.DB.First(&file, id).Error; err != nil { return nil, err } return &file, nil } func getShareByCode(code string) (*model.Share, error) { var share model.Share if err := config.DB.Where("share_code = ? AND status = 1", code).First(&share).Error; err != nil { return nil, err } return &share, nil } func verifySharePassword(share *model.Share, password string) bool { return utils.CheckPassword(password, share.Password) } func getStoragePolicy(policyID uint64) *model.StoragePolicy { var policy model.StoragePolicy if policyID > 0 { config.DB.First(&policy, policyID) } if policy.Config.AccessMode == "" { policy.Config.AccessMode = "proxy" } return &policy } // replaceMinIOEndpoint 替换预签名URL中的MinIO地址为Nginx公网地址 func replaceMinIOEndpoint(url, minioEndpoint, nginxEndpoint string) string { old := "http://" + minioEndpoint if url[:len(old)] == old { return nginxEndpoint + url[len(old):] } old = "https://" + minioEndpoint if len(url) > len(old) && url[:len(old)] == old { return nginxEndpoint + url[len(old):] } return url }