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,
+ }
+}