diff --git a/models/group.go b/models/group.go index a082a1d..9f4af46 100644 --- a/models/group.go +++ b/models/group.go @@ -25,10 +25,11 @@ type Group struct { // GroupOption 用户组其他配置 type GroupOption struct { - ArchiveDownloadEnabled bool `json:"archive_download"` - ArchiveTaskEnabled bool `json:"archive_task"` - OneTimeDownloadEnabled bool `json:"one_time_download"` - ShareDownloadEnabled bool `json:"share_download"` + ArchiveDownloadEnabled bool `json:"archive_download,omitempty"` + ArchiveTaskEnabled bool `json:"archive_task,omitempty"` + OneTimeDownloadEnabled bool `json:"one_time_download,omitempty"` + ShareDownloadEnabled bool `json:"share_download,omitempty"` + ShareFreeEnabled bool `json:"share_free,omitempty"` } // GetAria2Option 获取用户离线下载设备 diff --git a/models/migration.go b/models/migration.go index db2597a..5bef34f 100644 --- a/models/migration.go +++ b/models/migration.go @@ -108,6 +108,7 @@ solid #e9e9e9;"bgcolor="#fff"> 0 && user.IsAnonymous() { + return errors.New("未登录用户无法下载") + } + + return nil +} + +// WasDownloadedBy 返回分享是否已被用户下载过 +func (share *Share) WasDownloadedBy(user *User) bool { + _, exist := cache.Get(fmt.Sprintf("share_%d_%d", share.ID, user.ID)) + return exist +} + +// DownloadBy 增加下载次数、检查积分等,匿名用户不会缓存 +func (share *Share) DownloadBy(user *User) error { + if !share.WasDownloadedBy(user) { + if err := share.Purchase(user); err != nil { + return err + } + share.Downloaded() + if !user.IsAnonymous() { + cache.Set(fmt.Sprintf("share_%d_%d", share.ID, user.ID), true, + GetIntSetting("share_download_session_timeout", 2073600)) + } + } + return nil +} + +// Purchase 使用积分购买分享 +func (share *Share) Purchase(user *User) error { + // 不需要付积分 + if share.Score == 0 || user.Group.OptionsSerialized.ShareFreeEnabled { + return nil + } + + ok := user.PayScore(share.Score) + if !ok { + return errors.New("积分不足") + } + + return nil +} + +// Viewed 增加访问次数 +func (share *Share) Viewed() { + share.Views++ + DB.Model(share).UpdateColumn("views", gorm.Expr("views + ?", 1)) +} + +// Downloaded 增加下载次数 +func (share *Share) Downloaded() { + share.Downloads++ + DB.Model(share).UpdateColumn("downloads", gorm.Expr("downloads + ?", 1)) +} diff --git a/models/user.go b/models/user.go index 08a9618..f60d6ac 100644 --- a/models/user.go +++ b/models/user.go @@ -39,6 +39,7 @@ type User struct { Avatar string Options string `json:"-",gorm:"size:4096"` Authn string `gorm:"size:8192"` + Score int // 关联模型 Group Group `gorm:"association_autoupdate:false"` @@ -93,6 +94,20 @@ func (user *User) IncreaseStorage(size uint64) bool { return false } +// PayScore 扣除积分,返回是否成功 +// todo 测试 +func (user *User) PayScore(score int) bool { + if score == 0 { + return true + } + if score <= user.Score { + user.Score -= score + DB.Model(user).UpdateColumn("score", gorm.Expr("score - ?", score)) + return true + } + return false +} + // IncreaseStorageWithoutCheck 忽略可用容量,增加用户已用容量 func (user *User) IncreaseStorageWithoutCheck(size uint64) { if size == 0 { diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index 13a2246..a0f89b7 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -14,7 +14,7 @@ import ( ) var ( - ErrAuthFailed = serializer.NewError(serializer.CodeNoRightErr, "鉴权失败", nil) + ErrAuthFailed = serializer.NewError(serializer.CodeNoPermissionErr, "鉴权失败", nil) ErrExpired = serializer.NewError(serializer.CodeSignExpired, "签名已过期", nil) ) diff --git a/pkg/filesystem/filesystem.go b/pkg/filesystem/filesystem.go index a9ceb3e..bf59502 100644 --- a/pkg/filesystem/filesystem.go +++ b/pkg/filesystem/filesystem.go @@ -280,6 +280,21 @@ func (fs *FileSystem) SetTargetFileByIDs(ids []uint) error { return nil } +// SetTargetByInterface 根据 model.File 或者 model.Folder 设置目标对象 +// TODO 测试 +func (fs *FileSystem) SetTargetByInterface(target interface{}) error { + if file, ok := target.(*model.File); ok { + fs.SetTargetFile(&[]model.File{*file}) + return nil + } + if folder, ok := target.(*model.Folder); ok { + fs.SetTargetDir(&[]model.Folder{*folder}) + return nil + } + + return ErrObjectNotExist +} + // CleanTargets 清空目标 func (fs *FileSystem) CleanTargets() { fs.FileTarget = fs.FileTarget[:0] diff --git a/pkg/serializer/error.go b/pkg/serializer/error.go index 9a23860..3af265e 100644 --- a/pkg/serializer/error.go +++ b/pkg/serializer/error.go @@ -46,8 +46,8 @@ const ( CodeNotFullySuccess = 203 // CodeCheckLogin 未登录 CodeCheckLogin = 401 - // CodeNoRightErr 未授权访问 - CodeNoRightErr = 403 + // CodeNoPermissionErr 未授权访问 + CodeNoPermissionErr = 403 // CodeNotFound 资源未找到 CodeNotFound = 404 // CodeUploadFailed 上传出错 diff --git a/pkg/serializer/share.go b/pkg/serializer/share.go index 20c9265..0735ba8 100644 --- a/pkg/serializer/share.go +++ b/pkg/serializer/share.go @@ -8,6 +8,7 @@ import ( // Share 分享序列化 type Share struct { + Key string `json:"key"` Locked bool `json:"locked"` IsDir bool `json:"is_dir"` Score int `json:"score"` @@ -34,6 +35,7 @@ type shareSource struct { func BuildShareResponse(share *model.Share, unlocked bool) Share { creator := share.GetCreator() resp := Share{ + Key: hashid.HashID(share.ID, hashid.ShareID), Locked: !unlocked, Creator: &shareCreator{ Key: hashid.HashID(creator.ID, hashid.UserID), diff --git a/pkg/serializer/user.go b/pkg/serializer/user.go index 8908de5..c4b899b 100644 --- a/pkg/serializer/user.go +++ b/pkg/serializer/user.go @@ -22,6 +22,7 @@ type User struct { Avatar string `json:"avatar"` CreatedAt int64 `json:"created_at"` PreferredTheme string `json:"preferred_theme"` + Score int `json:"score"` Policy policy `json:"policy"` Group group `json:"group"` } @@ -41,6 +42,7 @@ type group struct { AllowRemoteDownload bool `json:"allowRemoteDownload"` AllowTorrentDownload bool `json:"allowTorrentDownload"` AllowArchiveDownload bool `json:"allowArchiveDownload"` + ShareFreeEnabled bool `json:"shareFree"` } type storage struct { @@ -60,6 +62,7 @@ func BuildUser(user model.User) User { Avatar: user.Avatar, CreatedAt: user.CreatedAt.Unix(), PreferredTheme: user.OptionsSerialized.PreferredTheme, + Score: user.Score, Policy: policy{ SaveType: user.Policy.Type, MaxSize: fmt.Sprintf("%.2fmb", float64(user.Policy.MaxSize)/(1024*1024)), @@ -74,6 +77,7 @@ func BuildUser(user model.User) User { AllowRemoteDownload: aria2Option[0], AllowTorrentDownload: aria2Option[2], AllowArchiveDownload: user.Group.OptionsSerialized.ArchiveDownloadEnabled, + ShareFreeEnabled: user.Group.OptionsSerialized.ShareFreeEnabled, }, } } diff --git a/routers/controllers/share.go b/routers/controllers/share.go index 6fd700f..6845f5a 100644 --- a/routers/controllers/share.go +++ b/routers/controllers/share.go @@ -26,3 +26,14 @@ func GetShare(c *gin.Context) { c.JSON(200, ErrorResponse(err)) } } + +// GetShareDownload 创建分享下载会话 +func GetShareDownload(c *gin.Context) { + var service share.SingleFileService + if err := c.ShouldBindQuery(&service); err == nil { + res := service.CreateDownloadSession(c) + c.JSON(200, res) + } else { + c.JSON(200, ErrorResponse(err)) + } +} diff --git a/routers/router.go b/routers/router.go index 50f7deb..a2c1d1e 100644 --- a/routers/router.go +++ b/routers/router.go @@ -172,6 +172,8 @@ func InitMasterRouter() *gin.Engine { { // 获取分享 share.GET(":id", controllers.GetShare) + // 创建文件下载会话 + share.POST("download/:id", controllers.GetShareDownload) } // 需要登录保护的 diff --git a/service/share/manage.go b/service/share/manage.go index dda8a34..eb0bd1d 100644 --- a/service/share/manage.go +++ b/service/share/manage.go @@ -25,7 +25,7 @@ func (service *ShareCreateService) Create(c *gin.Context) serializer.Response { // 是否拥有权限 if !user.Group.ShareEnabled { - return serializer.Err(serializer.CodeNoRightErr, "您无权创建分享链接", nil) + return serializer.Err(serializer.CodeNoPermissionErr, "您无权创建分享链接", nil) } // 对象是否存在 diff --git a/service/share/visit.go b/service/share/visit.go index 7514dce..e622fb6 100644 --- a/service/share/visit.go +++ b/service/share/visit.go @@ -1,8 +1,10 @@ package share import ( + "context" "fmt" model "github.com/HFO4/cloudreve/models" + "github.com/HFO4/cloudreve/pkg/filesystem" "github.com/HFO4/cloudreve/pkg/serializer" "github.com/HFO4/cloudreve/pkg/util" "github.com/gin-gonic/gin" @@ -13,8 +15,14 @@ type ShareGetService struct { Password string `form:"password" binding:"max=255"` } +// SingleFileService 对单文件进行操作的服务,path为可选文件完整路径 +type SingleFileService struct { + Path string `form:"path" binding:"max=65535"` +} + // Get 获取分享内容 func (service *ShareGetService) Get(c *gin.Context) serializer.Response { + user := currentUser(c) share := model.GetShareByHashID(c.Param("id")) if share == nil || !share.IsAvailable() { return serializer.Err(serializer.CodeNotFound, "分享不存在或已被取消", nil) @@ -34,8 +42,62 @@ func (service *ShareGetService) Get(c *gin.Context) serializer.Response { } } + if unlocked { + share.Viewed() + } + + // 如果已经下载过,不需要付积分 + if share.WasDownloadedBy(user) { + share.Score = 0 + } + return serializer.Response{ Code: 0, Data: serializer.BuildShareResponse(share, unlocked), } } + +// CreateDownloadSession 创建下载会话 +func (service *SingleFileService) CreateDownloadSession(c *gin.Context) serializer.Response { + user := currentUser(c) + share := model.GetShareByHashID(c.Param("id")) + if share == nil || !share.IsAvailable() { + return serializer.Err(serializer.CodeNotFound, "分享不存在或已被取消", nil) + } + + // 检查用户是否可以下载此分享的文件 + err := share.CanBeDownloadBy(user) + if err != nil { + return serializer.Err(serializer.CodeNoPermissionErr, err.Error(), nil) + } + + // 对积分、下载次数进行更新 + err = share.DownloadBy(user) + if err != nil { + return serializer.Err(serializer.CodeNoPermissionErr, err.Error(), nil) + } + + // 创建文件系统 + fs, err := filesystem.NewFileSystem(user) + if err != nil { + return serializer.Err(serializer.CodePolicyNotAllowed, err.Error(), err) + } + defer fs.Recycle() + + // 重设文件系统处理目标为源文件 + err = fs.SetTargetByInterface(share.GetSource()) + if err != nil { + return serializer.Err(serializer.CodePolicyNotAllowed, "源文件不存在", err) + } + + // 取得下载地址 + downloadURL, err := fs.GetDownloadURL(context.Background(), "", "download_timeout") + if err != nil { + return serializer.Err(serializer.CodeNotSet, err.Error(), err) + } + + return serializer.Response{ + Code: 0, + Data: downloadURL, + } +}