Files
SeeyonFileSystem/server/controller/file_access_controller.go

278 lines
7.4 KiB
Go
Raw Permalink Normal View History

2026-07-03 16:01:08 +08:00
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
}