初始化2
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"seeyon-filesystem/config"
|
||||
@@ -36,9 +37,14 @@ func NewFileService() *FileService {
|
||||
}
|
||||
|
||||
// Upload 上传文件
|
||||
// 流程: 计算MD5 -> 检查秒传 -> 存储文件 -> 保存元数据
|
||||
// storagePolicyID: 存储策略ID, 0=使用默认策略
|
||||
// 流程: 校验大小 -> 计算MD5 -> 检查秒传 -> 存储文件 -> 保存元数据
|
||||
func (s *FileService) Upload(fileHeader *multipart.FileHeader, folderID uint64, ownerID uint64, storagePolicyID uint64) (*dto.FileVO, error) {
|
||||
// 校验文件大小
|
||||
maxSize := s.getMaxFileSize(ownerID)
|
||||
if maxSize > 0 && fileHeader.Size > maxSize {
|
||||
return nil, fmt.Errorf("文件大小 %s 超过限制 %s", formatBytes(fileHeader.Size), formatBytes(maxSize))
|
||||
}
|
||||
|
||||
// 验证文件夹
|
||||
if folderID > 0 {
|
||||
folder, err := s.folderRepo.FindByID(folderID)
|
||||
@@ -99,6 +105,7 @@ func (s *FileService) Upload(fileHeader *multipart.FileHeader, folderID uint64,
|
||||
existing, _ := s.fileRepo.FindByMD5(md5)
|
||||
if existing != nil {
|
||||
newFile := &model.File{
|
||||
ID: utils.GenID(),
|
||||
Name: fileHeader.Filename,
|
||||
Extension: utils.GetExtension(fileHeader.Filename),
|
||||
FolderID: folderID,
|
||||
@@ -132,6 +139,7 @@ func (s *FileService) Upload(fileHeader *multipart.FileHeader, folderID uint64,
|
||||
}
|
||||
|
||||
newFile := &model.File{
|
||||
ID: utils.GenID(),
|
||||
Name: fileHeader.Filename,
|
||||
Extension: ext,
|
||||
FolderID: folderID,
|
||||
@@ -159,7 +167,7 @@ func (s *FileService) GetByID(id uint64, ownerID uint64) (*dto.FileVO, error) {
|
||||
if err != nil {
|
||||
return nil, errors.New("文件不存在")
|
||||
}
|
||||
if file.OwnerID != ownerID {
|
||||
if !s.canAccessFile(file.ID, file.OwnerID, ownerID) {
|
||||
return nil, errors.New("无权访问此文件")
|
||||
}
|
||||
return s.toFileVO(file), nil
|
||||
@@ -171,7 +179,7 @@ func (s *FileService) Download(id uint64, ownerID uint64) (io.ReadCloser, *model
|
||||
if err != nil {
|
||||
return nil, nil, errors.New("文件不存在")
|
||||
}
|
||||
if file.OwnerID != ownerID {
|
||||
if !s.canAccessFile(file.ID, file.OwnerID, ownerID) {
|
||||
return nil, nil, errors.New("无权下载此文件")
|
||||
}
|
||||
return s.downloadFile(file, id)
|
||||
@@ -210,7 +218,7 @@ func (s *FileService) Rename(id uint64, newName string, ownerID uint64, version
|
||||
if err != nil {
|
||||
return errors.New("文件不存在")
|
||||
}
|
||||
if file.OwnerID != ownerID {
|
||||
if !s.canAccessFile(file.ID, file.OwnerID, ownerID) {
|
||||
return errors.New("无权操作此文件")
|
||||
}
|
||||
|
||||
@@ -243,7 +251,7 @@ func (s *FileService) Move(id uint64, targetFolderID uint64, ownerID uint64) err
|
||||
if err != nil {
|
||||
return errors.New("文件不存在")
|
||||
}
|
||||
if file.OwnerID != ownerID {
|
||||
if !s.canAccessFile(file.ID, file.OwnerID, ownerID) {
|
||||
return errors.New("无权操作此文件")
|
||||
}
|
||||
|
||||
@@ -267,7 +275,7 @@ func (s *FileService) SoftDelete(id uint64, ownerID uint64) error {
|
||||
if err != nil {
|
||||
return errors.New("文件不存在")
|
||||
}
|
||||
if file.OwnerID != ownerID {
|
||||
if !s.canAccessFile(file.ID, file.OwnerID, ownerID) {
|
||||
return errors.New("无权操作此文件")
|
||||
}
|
||||
|
||||
@@ -280,7 +288,7 @@ func (s *FileService) Restore(id uint64, ownerID uint64) error {
|
||||
if err != nil {
|
||||
return errors.New("文件不存在")
|
||||
}
|
||||
if file.OwnerID != ownerID {
|
||||
if !s.canAccessFile(file.ID, file.OwnerID, ownerID) {
|
||||
return errors.New("无权操作此文件")
|
||||
}
|
||||
|
||||
@@ -293,7 +301,7 @@ func (s *FileService) HardDelete(id uint64, ownerID uint64) error {
|
||||
if err != nil {
|
||||
return errors.New("文件不存在")
|
||||
}
|
||||
if file.OwnerID != ownerID {
|
||||
if !s.canAccessFile(file.ID, file.OwnerID, ownerID) {
|
||||
return errors.New("无权操作此文件")
|
||||
}
|
||||
|
||||
@@ -308,53 +316,102 @@ func (s *FileService) HardDelete(id uint64, ownerID uint64) error {
|
||||
}
|
||||
|
||||
// GetDownloadURL 生成文件下载链接
|
||||
func (s *FileService) GetDownloadURL(id uint64) (string, error) {
|
||||
func (s *FileService) GetDownloadURL(id uint64, token ...string) (string, error) {
|
||||
file, err := s.fileRepo.FindByID(id)
|
||||
if err != nil {
|
||||
return "", errors.New("文件不存在")
|
||||
}
|
||||
return s.generateFileURL(file, "download")
|
||||
return GenerateFileURL(file, "download", token...)
|
||||
}
|
||||
|
||||
// GetPreviewURL 生成文件预览链接
|
||||
func (s *FileService) GetPreviewURL(id uint64) (string, error) {
|
||||
func (s *FileService) GetPreviewURL(id uint64, token ...string) (string, error) {
|
||||
file, err := s.fileRepo.FindByID(id)
|
||||
if err != nil {
|
||||
return "", errors.New("文件不存在")
|
||||
}
|
||||
return s.generateFileURL(file, "preview")
|
||||
return GenerateFileURL(file, "preview", token...)
|
||||
}
|
||||
|
||||
// generateFileURL 统一生成文件URL
|
||||
func (s *FileService) generateFileURL(file *model.File, urlType string) (string, error) {
|
||||
engine, err := storage.GetEngineByPolicyID(file.StoragePolicyID)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("/api/file/%d/%s", file.ID, urlType), nil
|
||||
return GenerateFileURL(file, urlType)
|
||||
}
|
||||
|
||||
// GenerateFileURL 统一生成文件URL(包级别, 供其他service调用)
|
||||
// download: 返回直接下载链接
|
||||
// preview: 返回在线预览链接(Content-Disposition: inline)
|
||||
// token: 可选, 用于本地存储时拼接到URL中供浏览器直接访问
|
||||
func GenerateFileURL(file *model.File, urlType string, token ...string) (string, error) {
|
||||
var tk string
|
||||
if len(token) > 0 {
|
||||
tk = token[0]
|
||||
}
|
||||
|
||||
// 本地存储: 使用CDN地址
|
||||
if localEngine, ok := engine.(*storage.LocalEngine); ok {
|
||||
if localEngine.CDNHost != "" {
|
||||
return localEngine.GetURL(file.StorageKey), nil
|
||||
}
|
||||
return fmt.Sprintf("/api/file/%d/%s", file.ID, urlType), nil
|
||||
}
|
||||
// 根据存储策略的实际类型决定URL生成方式
|
||||
policyType := getStoragePolicyType(file.StoragePolicyID)
|
||||
|
||||
// MinIO存储: 生成预签名URL并替换为Nginx代理地址
|
||||
if minioEngine, ok := engine.(*storage.MinIOEngine); ok {
|
||||
presignedURL, err := minioEngine.GetPresignedURL(file.StorageKey, 24*time.Hour)
|
||||
switch policyType {
|
||||
case "minio":
|
||||
engine, err := storage.GetEngineByPolicyID(file.StoragePolicyID)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("/api/file/%d/%s", file.ID, urlType), nil
|
||||
// MinIO不可用, 回退到API代理
|
||||
return buildAPIURL(file.ID, urlType, tk), nil
|
||||
}
|
||||
minioEngine := engine.(*storage.MinIOEngine)
|
||||
var presignedURL string
|
||||
var genErr error
|
||||
if urlType == "preview" {
|
||||
presignedURL, genErr = minioEngine.GetPresignedPreviewURL(file.StorageKey, file.MimeType, 24*time.Hour)
|
||||
} else {
|
||||
presignedURL, genErr = minioEngine.GetPresignedURL(file.StorageKey, 24*time.Hour)
|
||||
}
|
||||
if genErr != nil {
|
||||
return buildAPIURL(file.ID, urlType, tk), nil
|
||||
}
|
||||
// 替换为Nginx代理地址
|
||||
policy := getStoragePolicyConfig(file.StoragePolicyID)
|
||||
if policy != nil && policy.NginxEndpoint != "" {
|
||||
presignedURL = replaceMinIOEndpoint(presignedURL, minioEngine.Endpoint, policy.NginxEndpoint)
|
||||
}
|
||||
return presignedURL, nil
|
||||
}
|
||||
|
||||
return fmt.Sprintf("/api/file/%d/%s", file.ID, urlType), nil
|
||||
case "local":
|
||||
// 本地存储: 有CDN就用CDN, 否则用API路径
|
||||
cfg := config.AppConfig.Storage
|
||||
if cfg.CDNHost != "" {
|
||||
prefix := strings.TrimRight(cfg.CDNPathPrefix, "/")
|
||||
key := strings.TrimLeft(file.StorageKey, "/")
|
||||
return fmt.Sprintf("%s%s/%s", cfg.CDNHost, prefix, key), nil
|
||||
}
|
||||
return buildAPIURL(file.ID, urlType, tk), nil
|
||||
|
||||
default:
|
||||
// 未知策略或策略不存在, 回退到API代理
|
||||
return buildAPIURL(file.ID, urlType, tk), nil
|
||||
}
|
||||
}
|
||||
|
||||
// getStoragePolicyType 获取存储策略类型
|
||||
func getStoragePolicyType(policyID uint64) string {
|
||||
var policy model.StoragePolicy
|
||||
if config.DB.Select("type").First(&policy, policyID).Error != nil {
|
||||
return ""
|
||||
}
|
||||
return policy.Type
|
||||
}
|
||||
|
||||
// buildAPIURL 构建完整的API文件访问地址
|
||||
func buildAPIURL(fileID uint64, urlType string, token ...string) string {
|
||||
publicURL := config.AppConfig.Server.PublicURL
|
||||
if publicURL == "" {
|
||||
publicURL = fmt.Sprintf("http://localhost:%d", config.AppConfig.Server.Port)
|
||||
}
|
||||
publicURL = strings.TrimRight(publicURL, "/")
|
||||
base := fmt.Sprintf("%s/api/file/%d/%s", publicURL, fileID, urlType)
|
||||
if len(token) > 0 && token[0] != "" {
|
||||
base += "?token=" + token[0]
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
// getStoragePolicyConfig 获取存储策略配置
|
||||
@@ -441,10 +498,12 @@ func (s *FileService) GetDownloadPath(storageKey string) string {
|
||||
// toFileVO 转换为视图对象
|
||||
func (s *FileService) toFileVO(file *model.File) *dto.FileVO {
|
||||
vo := &dto.FileVO{
|
||||
ID: file.ID,
|
||||
ID: dto.StringID(file.ID),
|
||||
Name: file.Name,
|
||||
Extension: file.Extension,
|
||||
FolderID: file.FolderID,
|
||||
FolderID: dto.StringID(file.FolderID),
|
||||
OwnerID: dto.StringID(file.OwnerID),
|
||||
OwnerName: getUserName(file.OwnerID),
|
||||
Size: file.Size,
|
||||
StorageKey: file.StorageKey,
|
||||
StoragePolicyID: file.StoragePolicyID,
|
||||
@@ -477,3 +536,227 @@ func (s *FileService) getUserRoleIDs(userID uint64) []uint64 {
|
||||
}
|
||||
return roleIDs
|
||||
}
|
||||
|
||||
// canAccessFile 检查用户是否有权操作该文件
|
||||
// 规则: owner → 有权限 | admin → 有权限 | fs_file_permission中被授权 → 有权限
|
||||
func (s *FileService) canAccessFile(fileID, fileOwnerID, userID uint64) bool {
|
||||
// 1. 文件所有者
|
||||
if fileOwnerID == userID {
|
||||
return true
|
||||
}
|
||||
// 2. 管理员
|
||||
if s.isAdmin(userID) {
|
||||
return true
|
||||
}
|
||||
// 3. 文件级授权
|
||||
var count int64
|
||||
config.DB.Model(&model.FilePermission{}).
|
||||
Where("file_id = ? AND (user_id = ? OR role_id IN (?))",
|
||||
fileID, userID,
|
||||
config.DB.Model(&model.UserRole{}).Select("role_id").Where("user_id = ?", userID),
|
||||
).Count(&count)
|
||||
return count > 0
|
||||
}
|
||||
|
||||
// isAdmin 检查用户是否为管理员
|
||||
func (s *FileService) isAdmin(userID uint64) bool {
|
||||
var userRoles []model.UserRole
|
||||
config.DB.Where("user_id = ?", userID).Find(&userRoles)
|
||||
for _, ur := range userRoles {
|
||||
var role model.Role
|
||||
if config.DB.First(&role, ur.RoleID).Error == nil && role.Name == "admin" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// getMaxFileSize 根据用户角色返回最大文件大小(字节), 0=不限制
|
||||
func (s *FileService) getMaxFileSize(userID uint64) int64 {
|
||||
var userRoles []model.UserRole
|
||||
config.DB.Where("user_id = ?", userID).Find(&userRoles)
|
||||
|
||||
roleIDs := make([]uint64, 0, len(userRoles))
|
||||
for _, ur := range userRoles {
|
||||
roleIDs = append(roleIDs, ur.RoleID)
|
||||
}
|
||||
|
||||
var roles []model.Role
|
||||
if len(roleIDs) > 0 {
|
||||
config.DB.Where("id IN ?", roleIDs).Find(&roles)
|
||||
}
|
||||
|
||||
for _, r := range roles {
|
||||
switch r.Name {
|
||||
case "admin":
|
||||
return 0 // 不限制
|
||||
case "staff":
|
||||
return 500 * 1024 * 1024 // 500MB
|
||||
case "user":
|
||||
return 100 * 1024 * 1024 // 100MB
|
||||
case "guest":
|
||||
return 0 // 不允许上传
|
||||
}
|
||||
}
|
||||
return 100 * 1024 * 1024 // 默认100MB
|
||||
}
|
||||
|
||||
// formatBytes 格式化字节数
|
||||
func formatBytes(bytes int64) string {
|
||||
const (
|
||||
KB = 1024
|
||||
MB = KB * 1024
|
||||
GB = MB * 1024
|
||||
)
|
||||
switch {
|
||||
case bytes >= GB:
|
||||
return fmt.Sprintf("%.1fGB", float64(bytes)/float64(GB))
|
||||
case bytes >= MB:
|
||||
return fmt.Sprintf("%.1fMB", float64(bytes)/float64(MB))
|
||||
case bytes >= KB:
|
||||
return fmt.Sprintf("%.1fKB", float64(bytes)/float64(KB))
|
||||
default:
|
||||
return fmt.Sprintf("%dB", bytes)
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 文件权限管理 ==========
|
||||
|
||||
// GrantPermission 授权文件权限给用户或角色
|
||||
func (s *FileService) GrantPermission(fileID uint64, grantBy uint64, req *dto.GrantPermissionRequest) error {
|
||||
// 验证文件存在且属于授权者
|
||||
file, err := s.fileRepo.FindByID(fileID)
|
||||
if err != nil {
|
||||
return errors.New("文件不存在")
|
||||
}
|
||||
if !s.canAccessFile(file.ID, file.OwnerID, grantBy) {
|
||||
return errors.New("只有文件所有者或管理员才能授权")
|
||||
}
|
||||
|
||||
// user_id 和 role_id 二选一
|
||||
userID := uint64(req.UserID)
|
||||
roleID := uint64(req.RoleID)
|
||||
if userID == 0 && roleID == 0 {
|
||||
return errors.New("请指定 user_id 或 role_id")
|
||||
}
|
||||
if userID > 0 && roleID > 0 {
|
||||
return errors.New("user_id 和 role_id 只能选一个")
|
||||
}
|
||||
|
||||
// 验证权限级别
|
||||
switch req.PermLevel {
|
||||
case "read", "write", "admin":
|
||||
default:
|
||||
return errors.New("perm_level 必须是 read / write / admin")
|
||||
}
|
||||
|
||||
// 去重: 同一文件+同一用户/角色只保留一条
|
||||
var existing model.FilePermission
|
||||
query := config.DB.Where("file_id = ?", fileID)
|
||||
if userID > 0 {
|
||||
query = query.Where("user_id = ?", userID)
|
||||
} else {
|
||||
query = query.Where("role_id = ?", roleID)
|
||||
}
|
||||
if query.First(&existing).Error == nil {
|
||||
// 已存在则更新
|
||||
existing.PermLevel = req.PermLevel
|
||||
existing.GrantBy = grantBy
|
||||
return config.DB.Save(&existing).Error
|
||||
}
|
||||
|
||||
return config.DB.Create(&model.FilePermission{
|
||||
FileID: fileID,
|
||||
UserID: userID,
|
||||
RoleID: roleID,
|
||||
PermLevel: req.PermLevel,
|
||||
GrantBy: grantBy,
|
||||
}).Error
|
||||
}
|
||||
|
||||
// RevokePermission 撤销文件权限
|
||||
func (s *FileService) RevokePermission(fileID uint64, permID uint64, userID uint64) error {
|
||||
// 验证文件归属
|
||||
file, err := s.fileRepo.FindByID(fileID)
|
||||
if err != nil {
|
||||
return errors.New("文件不存在")
|
||||
}
|
||||
if !s.canAccessFile(file.ID, file.OwnerID, userID) {
|
||||
return errors.New("只有文件所有者或管理员才能撤销权限")
|
||||
}
|
||||
|
||||
result := config.DB.Where("id = ? AND file_id = ?", permID, fileID).Delete(&model.FilePermission{})
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New("权限记录不存在")
|
||||
}
|
||||
return result.Error
|
||||
}
|
||||
|
||||
// ListPermissions 查看文件的授权列表
|
||||
func (s *FileService) ListPermissions(fileID uint64, userID uint64) ([]dto.FilePermissionVO, error) {
|
||||
file, err := s.fileRepo.FindByID(fileID)
|
||||
if err != nil {
|
||||
return nil, errors.New("文件不存在")
|
||||
}
|
||||
if !s.canAccessFile(file.ID, file.OwnerID, userID) {
|
||||
return nil, errors.New("只有文件所有者或管理员才能查看权限列表")
|
||||
}
|
||||
|
||||
var perms []model.FilePermission
|
||||
config.DB.Where("file_id = ?", fileID).Find(&perms)
|
||||
|
||||
vos := make([]dto.FilePermissionVO, 0, len(perms))
|
||||
for _, p := range perms {
|
||||
vo := dto.FilePermissionVO{
|
||||
ID: p.ID,
|
||||
FileID: p.FileID,
|
||||
UserID: p.UserID,
|
||||
RoleID: p.RoleID,
|
||||
PermLevel: p.PermLevel,
|
||||
GrantBy: p.GrantBy,
|
||||
CreatedAt: p.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
// 填充用户名
|
||||
if p.UserID > 0 {
|
||||
var user model.User
|
||||
if config.DB.Select("nickname", "username").First(&user, p.UserID).Error == nil {
|
||||
vo.UserName = user.Nickname
|
||||
if vo.UserName == "" {
|
||||
vo.UserName = user.Username
|
||||
}
|
||||
}
|
||||
}
|
||||
// 填充角色名
|
||||
if p.RoleID > 0 {
|
||||
var role model.Role
|
||||
if config.DB.Select("display_name", "name").First(&role, p.RoleID).Error == nil {
|
||||
vo.RoleName = role.DisplayName
|
||||
if vo.RoleName == "" {
|
||||
vo.RoleName = role.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
// 填充授权人
|
||||
var granter model.User
|
||||
if config.DB.Select("nickname", "username").First(&granter, p.GrantBy).Error == nil {
|
||||
vo.GrantByName = granter.Nickname
|
||||
if vo.GrantByName == "" {
|
||||
vo.GrantByName = granter.Username
|
||||
}
|
||||
}
|
||||
vos = append(vos, vo)
|
||||
}
|
||||
return vos, nil
|
||||
}
|
||||
|
||||
// getUserName 根据用户ID获取显示名称
|
||||
func getUserName(userID uint64) string {
|
||||
var user model.User
|
||||
if config.DB.Select("nickname", "username").First(&user, userID).Error != nil {
|
||||
return ""
|
||||
}
|
||||
if user.Nickname != "" {
|
||||
return user.Nickname
|
||||
}
|
||||
return user.Username
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user