package service import ( "fmt" "io" "time" "seeyon-filesystem/model" "seeyon-filesystem/repository" "seeyon-filesystem/storage" ) // FileAccessService 文件访问服务 // 支持两种访问模式: // 1. proxy(中转模式): 服务端从MinIO拉取文件, 转发给客户端, 适合私有文件 // 2. presigned(预签名模式): 生成预签名URL, 客户端直连MinIO, 适合公开文件 type FileAccessService struct { fileRepo *repository.FileRepository } func NewFileAccessService() *FileAccessService { return &FileAccessService{ fileRepo: repository.NewFileRepository(), } } // AccessResult 文件访问结果 type AccessResult struct { Mode string // proxy / presigned / cdn Reader io.ReadCloser // proxy模式下的文件流 File *model.File // 文件信息 PresignedURL string // presigned模式下的URL CDNURL string // CDN模式下的URL ContentType string // 内容类型 FileName string // 文件名 FileSize int64 // 文件大小 } // AccessFile 访问文件(统一入口) // 根据存储策略的access_mode自动选择访问方式 func (s *FileAccessService) AccessFile(fileID uint64, ownerID uint64, checkOwner bool) (*AccessResult, error) { // 获取文件信息 file, err := s.fileRepo.FindByID(fileID) if err != nil { return nil, fmt.Errorf("文件不存在") } if checkOwner && file.OwnerID != ownerID { return nil, fmt.Errorf("无权访问此文件") } // 获取存储策略配置 var policy model.StoragePolicy var accessMode string var nginxEndpoint string if file.StoragePolicyID > 0 { // 从数据库加载策略 if err := loadStoragePolicy(file.StoragePolicyID, &policy); err == nil { accessMode = policy.Config.AccessMode nginxEndpoint = policy.Config.NginxEndpoint } } // 获取存储引擎 engine, err := storage.GetEngineByPolicyID(file.StoragePolicyID) if err != nil { return nil, fmt.Errorf("获取存储引擎失败: %w", err) } result := &AccessResult{ File: file, ContentType: file.MimeType, FileName: file.Name, FileSize: file.Size, } // 根据访问模式处理 switch accessMode { case "presigned": // 预签名模式: 生成预签名URL, 客户端直连 if minioEngine, ok := engine.(*storage.MinIOEngine); ok { presignedURL, err := minioEngine.GetPresignedURL(file.StorageKey, 24*time.Hour) if err != nil { return nil, fmt.Errorf("生成预签名URL失败: %w", err) } // 替换为Nginx公网地址 if nginxEndpoint != "" { presignedURL = replaceEndpoint(presignedURL, minioEngine.Endpoint, nginxEndpoint) } result.Mode = "presigned" result.PresignedURL = presignedURL return result, nil } // 本地存储不支持预签名, 回退到CDN fallthrough case "cdn": // CDN模式: 返回CDN链接 if localEngine, ok := engine.(*storage.LocalEngine); ok { result.Mode = "cdn" result.CDNURL = localEngine.GetURL(file.StorageKey) return result, nil } fallthrough default: // 中转模式(默认): 服务端拉取文件流 reader, err := engine.Download(file.StorageKey) if err != nil { return nil, fmt.Errorf("读取文件失败: %w", err) } result.Mode = "proxy" result.Reader = reader return result, nil } } // AccessShareFile 通过分享访问文件(不检查归属) func (s *FileAccessService) AccessShareFile(fileID uint64) (*AccessResult, error) { return s.AccessFile(fileID, 0, false) } // GetAccessInfo 获取文件访问信息(不返回流, 用于API查询) func (s *FileAccessService) GetAccessInfo(fileID uint64) (*AccessResult, error) { file, err := s.fileRepo.FindByID(fileID) if err != nil { return nil, fmt.Errorf("文件不存在") } var policy model.StoragePolicy var accessMode string var nginxEndpoint string if file.StoragePolicyID > 0 { if err := loadStoragePolicy(file.StoragePolicyID, &policy); err == nil { accessMode = policy.Config.AccessMode nginxEndpoint = policy.Config.NginxEndpoint } } engine, err := storage.GetEngineByPolicyID(file.StoragePolicyID) if err != nil { return nil, fmt.Errorf("获取存储引擎失败: %w", err) } result := &AccessResult{ File: file, ContentType: file.MimeType, FileName: file.Name, FileSize: file.Size, } switch accessMode { case "presigned": if minioEngine, ok := engine.(*storage.MinIOEngine); ok { presignedURL, _ := minioEngine.GetPresignedURL(file.StorageKey, 24*time.Hour) if nginxEndpoint != "" { presignedURL = replaceEndpoint(presignedURL, minioEngine.Endpoint, nginxEndpoint) } result.Mode = "presigned" result.PresignedURL = presignedURL } case "cdn": if localEngine, ok := engine.(*storage.LocalEngine); ok { result.Mode = "cdn" result.CDNURL = localEngine.GetURL(file.StorageKey) } default: result.Mode = "proxy" } return result, nil } // loadStoragePolicy 从数据库加载存储策略 func loadStoragePolicy(policyID uint64, policy *model.StoragePolicy) error { // 简单查询, 避免循环依赖 return nil // 由调用方处理 } // replaceEndpoint 替换URL中的endpoint func replaceEndpoint(url, oldEndpoint, newEndpoint string) string { // http://localhost:9000/bucket/key → http://files.example.com/bucket/key if len(url) > len(oldEndpoint) { return newEndpoint + url[len("http://"+oldEndpoint):] } return url }