mirror of https://github.com/cloudreve/Cloudreve
Feat: generate share URL
parent
4abd5b2346
commit
0ee0ac5e89
1
go.mod
1
go.mod
|
@ -26,6 +26,7 @@ require (
|
||||||
github.com/qiniu/api.v7/v7 v7.4.0
|
github.com/qiniu/api.v7/v7 v7.4.0
|
||||||
github.com/rafaeljusto/redigomock v0.0.0-20191117212112-00b2509252a1
|
github.com/rafaeljusto/redigomock v0.0.0-20191117212112-00b2509252a1
|
||||||
github.com/smartystreets/goconvey v1.6.4 // indirect
|
github.com/smartystreets/goconvey v1.6.4 // indirect
|
||||||
|
github.com/speps/go-hashids v2.0.0+incompatible
|
||||||
github.com/stretchr/testify v1.4.0
|
github.com/stretchr/testify v1.4.0
|
||||||
github.com/tencentyun/cos-go-sdk-v5 v0.0.0-20200120023323-87ff3bc489ac
|
github.com/tencentyun/cos-go-sdk-v5 v0.0.0-20200120023323-87ff3bc489ac
|
||||||
github.com/upyun/go-sdk v2.1.0+incompatible
|
github.com/upyun/go-sdk v2.1.0+incompatible
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -163,6 +163,8 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh
|
||||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||||
|
github.com/speps/go-hashids v2.0.0+incompatible h1:kSfxGfESueJKTx0mpER9Y/1XHl+FVQjtCqRyYcviFbw=
|
||||||
|
github.com/speps/go-hashids v2.0.0+incompatible/go.mod h1:P7hqPzMdnZOfyIk+xrlG1QaSMw+gCBdHKsBDnhpaZvc=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
|
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
|
||||||
|
|
|
@ -28,6 +28,7 @@ type GroupOption struct {
|
||||||
ArchiveDownloadEnabled bool `json:"archive_download"`
|
ArchiveDownloadEnabled bool `json:"archive_download"`
|
||||||
ArchiveTaskEnabled bool `json:"archive_task"`
|
ArchiveTaskEnabled bool `json:"archive_task"`
|
||||||
OneTimeDownloadEnabled bool `json:"one_time_download"`
|
OneTimeDownloadEnabled bool `json:"one_time_download"`
|
||||||
|
ShareDownloadEnabled bool `json:"share_download"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAria2Option 获取用户离线下载设备
|
// GetAria2Option 获取用户离线下载设备
|
||||||
|
|
|
@ -29,7 +29,7 @@ func migration() {
|
||||||
if conf.DatabaseConfig.Type == "mysql" {
|
if conf.DatabaseConfig.Type == "mysql" {
|
||||||
DB = DB.Set("gorm:table_options", "ENGINE=InnoDB")
|
DB = DB.Set("gorm:table_options", "ENGINE=InnoDB")
|
||||||
}
|
}
|
||||||
DB.AutoMigrate(&User{}, &Setting{}, &Group{}, &Policy{}, &Folder{}, &File{}, &StoragePack{})
|
DB.AutoMigrate(&User{}, &Setting{}, &Group{}, &Policy{}, &Folder{}, &File{}, &StoragePack{}, &Share{})
|
||||||
|
|
||||||
// 创建初始存储策略
|
// 创建初始存储策略
|
||||||
addDefaultPolicy()
|
addDefaultPolicy()
|
||||||
|
@ -161,6 +161,8 @@ Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; verti
|
||||||
{Name: "task_queue_token", Value: ``, Type: "task"},
|
{Name: "task_queue_token", Value: ``, Type: "task"},
|
||||||
{Name: "secret_key", Value: util.RandStringRunes(256), Type: "auth"},
|
{Name: "secret_key", Value: util.RandStringRunes(256), Type: "auth"},
|
||||||
{Name: "temp_path", Value: "temp", Type: "path"},
|
{Name: "temp_path", Value: "temp", Type: "path"},
|
||||||
|
{Name: "score_enabled", Value: "1", Type: "score"},
|
||||||
|
{Name: "share_score_rate", Value: "80", Type: "score"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, value := range defaultSettings {
|
for _, value := range defaultSettings {
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/HFO4/cloudreve/pkg/util"
|
||||||
|
"github.com/jinzhu/gorm"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Share 分享模型
|
||||||
|
type Share struct {
|
||||||
|
gorm.Model
|
||||||
|
Password string // 分享密码,空值为非加密分享
|
||||||
|
IsDir bool // 原始资源是否为目录
|
||||||
|
UserID uint // 创建用户ID
|
||||||
|
SourceID uint // 原始资源ID
|
||||||
|
Views int // 浏览数
|
||||||
|
Downloads int // 下载数
|
||||||
|
RemainDownloads int // 剩余下载配额,负值标识无限制
|
||||||
|
Expires *time.Time // 过期时间,空值表示无过期时间
|
||||||
|
Score int // 每人次下载扣除积分
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create 创建分享
|
||||||
|
// TODO 测试
|
||||||
|
func (share *Share) Create() (uint, error) {
|
||||||
|
if err := DB.Create(share).Error; err != nil {
|
||||||
|
util.Log().Warning("无法插入数据库记录, %s", err)
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return share.ID, nil
|
||||||
|
}
|
|
@ -22,6 +22,7 @@ type system struct {
|
||||||
Listen string `validate:"required"`
|
Listen string `validate:"required"`
|
||||||
Debug bool
|
Debug bool
|
||||||
SessionSecret string
|
SessionSecret string
|
||||||
|
HashIDSalt string `validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// slave 作为slave存储端配置
|
// slave 作为slave存储端配置
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
package hashid
|
||||||
|
|
||||||
|
import "github.com/HFO4/cloudreve/pkg/conf"
|
||||||
|
import "github.com/speps/go-hashids"
|
||||||
|
|
||||||
|
// ID类型
|
||||||
|
const (
|
||||||
|
ShareID = iota // 分享
|
||||||
|
UserID // 用户
|
||||||
|
)
|
||||||
|
|
||||||
|
// HashEncode 对给定数据计算HashID
|
||||||
|
func HashEncode(v []int) (string, error) {
|
||||||
|
hd := hashids.NewData()
|
||||||
|
hd.Salt = conf.SystemConfig.HashIDSalt
|
||||||
|
|
||||||
|
h, err := hashids.NewWithData(hd)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := h.Encode(v)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HashID 计算数据库内主键对应的HashID
|
||||||
|
func HashID(id uint, t int) string {
|
||||||
|
v, _ := HashEncode([]int{int(id), t})
|
||||||
|
return v
|
||||||
|
}
|
|
@ -48,6 +48,8 @@ const (
|
||||||
CodeCheckLogin = 401
|
CodeCheckLogin = 401
|
||||||
// CodeNoRightErr 未授权访问
|
// CodeNoRightErr 未授权访问
|
||||||
CodeNoRightErr = 403
|
CodeNoRightErr = 403
|
||||||
|
// CodeNotFound 资源未找到
|
||||||
|
CodeNotFound = 404
|
||||||
// CodeUploadFailed 上传出错
|
// CodeUploadFailed 上传出错
|
||||||
CodeUploadFailed = 40002
|
CodeUploadFailed = 40002
|
||||||
// CodeCreateFolderFailed 目录创建失败
|
// CodeCreateFolderFailed 目录创建失败
|
||||||
|
|
|
@ -4,15 +4,17 @@ import model "github.com/HFO4/cloudreve/models"
|
||||||
|
|
||||||
// SiteConfig 站点全局设置序列
|
// SiteConfig 站点全局设置序列
|
||||||
type SiteConfig struct {
|
type SiteConfig struct {
|
||||||
SiteName string `json:"title"`
|
SiteName string `json:"title"`
|
||||||
LoginCaptcha bool `json:"loginCaptcha"`
|
LoginCaptcha bool `json:"loginCaptcha"`
|
||||||
RegCaptcha bool `json:"regCaptcha"`
|
RegCaptcha bool `json:"regCaptcha"`
|
||||||
ForgetCaptcha bool `json:"forgetCaptcha"`
|
ForgetCaptcha bool `json:"forgetCaptcha"`
|
||||||
EmailActive bool `json:"emailActive"`
|
EmailActive bool `json:"emailActive"`
|
||||||
QQLogin bool `json:"QQLogin"`
|
QQLogin bool `json:"QQLogin"`
|
||||||
Themes string `json:"themes"`
|
Themes string `json:"themes"`
|
||||||
DefaultTheme string `json:"defaultTheme"`
|
DefaultTheme string `json:"defaultTheme"`
|
||||||
User User `json:"user"`
|
ScoreEnabled bool `json:"score_enabled"`
|
||||||
|
ShareScoreRate string `json:"share_score_rate"`
|
||||||
|
User User `json:"user"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkSettingValue(setting map[string]string, key string) string {
|
func checkSettingValue(setting map[string]string, key string) string {
|
||||||
|
@ -30,14 +32,16 @@ func BuildSiteConfig(settings map[string]string, user *model.User) Response {
|
||||||
}
|
}
|
||||||
return Response{
|
return Response{
|
||||||
Data: SiteConfig{
|
Data: SiteConfig{
|
||||||
SiteName: checkSettingValue(settings, "siteName"),
|
SiteName: checkSettingValue(settings, "siteName"),
|
||||||
LoginCaptcha: model.IsTrueVal(checkSettingValue(settings, "login_captcha")),
|
LoginCaptcha: model.IsTrueVal(checkSettingValue(settings, "login_captcha")),
|
||||||
RegCaptcha: model.IsTrueVal(checkSettingValue(settings, "reg_captcha")),
|
RegCaptcha: model.IsTrueVal(checkSettingValue(settings, "reg_captcha")),
|
||||||
ForgetCaptcha: model.IsTrueVal(checkSettingValue(settings, "forget_captcha")),
|
ForgetCaptcha: model.IsTrueVal(checkSettingValue(settings, "forget_captcha")),
|
||||||
EmailActive: model.IsTrueVal(checkSettingValue(settings, "email_active")),
|
EmailActive: model.IsTrueVal(checkSettingValue(settings, "email_active")),
|
||||||
QQLogin: model.IsTrueVal(checkSettingValue(settings, "qq_login")),
|
QQLogin: model.IsTrueVal(checkSettingValue(settings, "qq_login")),
|
||||||
Themes: checkSettingValue(settings, "themes"),
|
Themes: checkSettingValue(settings, "themes"),
|
||||||
DefaultTheme: checkSettingValue(settings, "defaultTheme"),
|
DefaultTheme: checkSettingValue(settings, "defaultTheme"),
|
||||||
User: userRes,
|
ScoreEnabled: model.IsTrueVal(checkSettingValue(settings, "score_enabled")),
|
||||||
|
ShareScoreRate: checkSettingValue(settings, "share_score_rate"),
|
||||||
|
User: userRes,
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ func ParamErrorMsg(filed string, tag string) string {
|
||||||
"UserName": "邮箱",
|
"UserName": "邮箱",
|
||||||
"Password": "密码",
|
"Password": "密码",
|
||||||
"Path": "路径",
|
"Path": "路径",
|
||||||
|
"SourceID": "原始资源",
|
||||||
}
|
}
|
||||||
// 未通过的规则与中文对应
|
// 未通过的规则与中文对应
|
||||||
tagMap := map[string]string{
|
tagMap := map[string]string{
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/HFO4/cloudreve/service/share"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateShare 创建分享
|
||||||
|
func CreateShare(c *gin.Context) {
|
||||||
|
var service share.ShareCreateService
|
||||||
|
if err := c.ShouldBindJSON(&service); err == nil {
|
||||||
|
res := service.Create(c)
|
||||||
|
c.JSON(200, res)
|
||||||
|
} else {
|
||||||
|
c.JSON(200, ErrorResponse(err))
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,8 @@ func SiteConfig(c *gin.Context) {
|
||||||
"email_active",
|
"email_active",
|
||||||
"themes",
|
"themes",
|
||||||
"defaultTheme",
|
"defaultTheme",
|
||||||
|
"score_enabled",
|
||||||
|
"share_score_rate",
|
||||||
})
|
})
|
||||||
|
|
||||||
// 如果已登录,则同时返回用户信息
|
// 如果已登录,则同时返回用户信息
|
||||||
|
|
|
@ -233,6 +233,13 @@ func InitMasterRouter() *gin.Engine {
|
||||||
object.POST("rename", controllers.Rename)
|
object.POST("rename", controllers.Rename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 分享
|
||||||
|
share := auth.Group("share")
|
||||||
|
{
|
||||||
|
// 创建新分享
|
||||||
|
share.POST("", controllers.CreateShare)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
package share
|
||||||
|
|
||||||
|
import (
|
||||||
|
model "github.com/HFO4/cloudreve/models"
|
||||||
|
"github.com/HFO4/cloudreve/pkg/hashid"
|
||||||
|
"github.com/HFO4/cloudreve/pkg/serializer"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ShareCreateService 创建新分享服务
|
||||||
|
type ShareCreateService struct {
|
||||||
|
SourceID uint `json:"id" binding:"required"`
|
||||||
|
IsDir bool `json:"is_dir"`
|
||||||
|
Password string `json:"password" binding:"max=255"`
|
||||||
|
RemainDownloads int `json:"downloads"`
|
||||||
|
Expire int `json:"expire"`
|
||||||
|
Score int `json:"score" binding:"gte=0"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create 创建新分享
|
||||||
|
func (service *ShareCreateService) Create(c *gin.Context) serializer.Response {
|
||||||
|
userCtx, _ := c.Get("user")
|
||||||
|
user := userCtx.(*model.User)
|
||||||
|
|
||||||
|
// 是否拥有权限
|
||||||
|
if !user.Group.ShareEnabled {
|
||||||
|
return serializer.Err(serializer.CodeNoRightErr, "您无权创建分享链接", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对象是否存在
|
||||||
|
exist := true
|
||||||
|
if service.IsDir {
|
||||||
|
folder, err := model.GetFoldersByIDs([]uint{service.SourceID}, user.ID)
|
||||||
|
if err != nil || len(folder) == 0 {
|
||||||
|
exist = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
file, err := model.GetFilesByIDs([]uint{service.SourceID}, user.ID)
|
||||||
|
if err != nil || len(file) == 0 {
|
||||||
|
exist = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !exist {
|
||||||
|
return serializer.Err(serializer.CodeNotFound, "原始资源不存在", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
newShare := model.Share{
|
||||||
|
Password: service.Password,
|
||||||
|
IsDir: service.IsDir,
|
||||||
|
UserID: user.ID,
|
||||||
|
SourceID: service.SourceID,
|
||||||
|
Score: service.RemainDownloads,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果开启了自动过期
|
||||||
|
if service.RemainDownloads > 0 {
|
||||||
|
expires := time.Now().Add(time.Duration(service.Expire) * time.Second)
|
||||||
|
newShare.RemainDownloads = service.RemainDownloads
|
||||||
|
newShare.Expires = &expires
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建分享
|
||||||
|
id, err := newShare.Create()
|
||||||
|
if err != nil {
|
||||||
|
return serializer.Err(serializer.CodeDBError, "分享链接创建失败", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取分享的唯一id
|
||||||
|
uid := hashid.HashID(id, hashid.ShareID)
|
||||||
|
// 最终得到分享链接
|
||||||
|
siteURL := model.GetSiteURL()
|
||||||
|
sharePath, _ := url.Parse("/#/s/" + uid)
|
||||||
|
shareURL := siteURL.ResolveReference(sharePath)
|
||||||
|
|
||||||
|
return serializer.Response{
|
||||||
|
Code: 0,
|
||||||
|
Data: shareURL.String(),
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue