初始化
This commit is contained in:
277
server/controller/file_access_controller.go
Normal file
277
server/controller/file_access_controller.go
Normal file
@@ -0,0 +1,277 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user