Feat: download file from single file share
This commit is contained in:
parent
0977b36f8b
commit
7f0feebf42
13 changed files with 199 additions and 8 deletions
|
@ -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 获取用户离线下载设备
|
||||
|
|
|
@ -108,6 +108,7 @@ solid #e9e9e9;"bgcolor="#fff"><tbody><tr style="font-family: 'Helvetica Neue',He
|
|||
{Name: "upload_session_timeout", Value: `86400`, Type: "timeout"},
|
||||
{Name: "slave_api_timeout", Value: `60`, Type: "timeout"},
|
||||
{Name: "onedrive_monitor_timeout", Value: `600`, Type: "timeout"},
|
||||
{Name: "share_download_session_timeout", Value: `2073600`, Type: "timeout"},
|
||||
{Name: "onedrive_callback_check", Value: `20`, Type: "timeout"},
|
||||
{Name: "onedrive_chunk_retries", Value: `1`, Type: "retry"},
|
||||
{Name: "allowdVisitorDownload", Value: `false`, Type: "share"},
|
||||
|
@ -185,6 +186,7 @@ func addDefaultGroups() {
|
|||
OptionsSerialized: GroupOption{
|
||||
ArchiveDownloadEnabled: true,
|
||||
ArchiveTaskEnabled: true,
|
||||
ShareDownloadEnabled: true,
|
||||
},
|
||||
}
|
||||
if err := DB.Create(&defaultAdminGroup).Error; err != nil {
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/HFO4/cloudreve/pkg/cache"
|
||||
"github.com/HFO4/cloudreve/pkg/hashid"
|
||||
"github.com/HFO4/cloudreve/pkg/util"
|
||||
"github.com/jinzhu/gorm"
|
||||
|
@ -86,6 +89,14 @@ func (share *Share) GetCreator() *User {
|
|||
return &share.User
|
||||
}
|
||||
|
||||
// GetSource 返回源对象
|
||||
func (share *Share) GetSource() interface{} {
|
||||
if share.IsDir {
|
||||
return share.GetSourceFolder()
|
||||
}
|
||||
return share.GetSourceFile()
|
||||
}
|
||||
|
||||
// GetSourceFolder 获取源目录
|
||||
func (share *Share) GetSourceFolder() *Folder {
|
||||
if share.Folder.ID == 0 {
|
||||
|
@ -107,3 +118,69 @@ func (share *Share) GetSourceFile() *File {
|
|||
}
|
||||
return &share.File
|
||||
}
|
||||
|
||||
// CanBeDownloadBy 返回此分享是否可以被给定用户下载
|
||||
func (share *Share) CanBeDownloadBy(user *User) error {
|
||||
// 用户组权限
|
||||
if !user.Group.OptionsSerialized.ShareDownloadEnabled {
|
||||
if user.IsAnonymous() {
|
||||
return errors.New("未登录用户无法下载")
|
||||
}
|
||||
return errors.New("您当前的用户组无权下载")
|
||||
}
|
||||
|
||||
// 需要积分但未登录
|
||||
if share.Score > 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))
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -14,7 +14,7 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
ErrAuthFailed = serializer.NewError(serializer.CodeNoRightErr, "鉴权失败", nil)
|
||||
ErrAuthFailed = serializer.NewError(serializer.CodeNoPermissionErr, "鉴权失败", nil)
|
||||
ErrExpired = serializer.NewError(serializer.CodeSignExpired, "签名已过期", nil)
|
||||
)
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -46,8 +46,8 @@ const (
|
|||
CodeNotFullySuccess = 203
|
||||
// CodeCheckLogin 未登录
|
||||
CodeCheckLogin = 401
|
||||
// CodeNoRightErr 未授权访问
|
||||
CodeNoRightErr = 403
|
||||
// CodeNoPermissionErr 未授权访问
|
||||
CodeNoPermissionErr = 403
|
||||
// CodeNotFound 资源未找到
|
||||
CodeNotFound = 404
|
||||
// CodeUploadFailed 上传出错
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -172,6 +172,8 @@ func InitMasterRouter() *gin.Engine {
|
|||
{
|
||||
// 获取分享
|
||||
share.GET(":id", controllers.GetShare)
|
||||
// 创建文件下载会话
|
||||
share.POST("download/:id", controllers.GetShareDownload)
|
||||
}
|
||||
|
||||
// 需要登录保护的
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
// 对象是否存在
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue