Feat: generate share URL

pull/247/head
HFO4 4 years ago
parent 4abd5b2346
commit 0ee0ac5e89

@ -26,6 +26,7 @@ require (
github.com/qiniu/api.v7/v7 v7.4.0
github.com/rafaeljusto/redigomock v0.0.0-20191117212112-00b2509252a1
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/tencentyun/cos-go-sdk-v5 v0.0.0-20200120023323-87ff3bc489ac
github.com/upyun/go-sdk v2.1.0+incompatible

@ -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/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/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.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=

@ -28,6 +28,7 @@ type GroupOption struct {
ArchiveDownloadEnabled bool `json:"archive_download"`
ArchiveTaskEnabled bool `json:"archive_task"`
OneTimeDownloadEnabled bool `json:"one_time_download"`
ShareDownloadEnabled bool `json:"share_download"`
}
// GetAria2Option 获取用户离线下载设备

@ -29,7 +29,7 @@ func migration() {
if conf.DatabaseConfig.Type == "mysql" {
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()
@ -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: "secret_key", Value: util.RandStringRunes(256), Type: "auth"},
{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 {

@ -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"`
Debug bool
SessionSecret string
HashIDSalt string `validate:"required"`
}
// 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
// CodeNoRightErr 未授权访问
CodeNoRightErr = 403
// CodeNotFound 资源未找到
CodeNotFound = 404
// CodeUploadFailed 上传出错
CodeUploadFailed = 40002
// CodeCreateFolderFailed 目录创建失败

@ -4,15 +4,17 @@ import model "github.com/HFO4/cloudreve/models"
// SiteConfig 站点全局设置序列
type SiteConfig struct {
SiteName string `json:"title"`
LoginCaptcha bool `json:"loginCaptcha"`
RegCaptcha bool `json:"regCaptcha"`
ForgetCaptcha bool `json:"forgetCaptcha"`
EmailActive bool `json:"emailActive"`
QQLogin bool `json:"QQLogin"`
Themes string `json:"themes"`
DefaultTheme string `json:"defaultTheme"`
User User `json:"user"`
SiteName string `json:"title"`
LoginCaptcha bool `json:"loginCaptcha"`
RegCaptcha bool `json:"regCaptcha"`
ForgetCaptcha bool `json:"forgetCaptcha"`
EmailActive bool `json:"emailActive"`
QQLogin bool `json:"QQLogin"`
Themes string `json:"themes"`
DefaultTheme string `json:"defaultTheme"`
ScoreEnabled bool `json:"score_enabled"`
ShareScoreRate string `json:"share_score_rate"`
User User `json:"user"`
}
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{
Data: SiteConfig{
SiteName: checkSettingValue(settings, "siteName"),
LoginCaptcha: model.IsTrueVal(checkSettingValue(settings, "login_captcha")),
RegCaptcha: model.IsTrueVal(checkSettingValue(settings, "reg_captcha")),
ForgetCaptcha: model.IsTrueVal(checkSettingValue(settings, "forget_captcha")),
EmailActive: model.IsTrueVal(checkSettingValue(settings, "email_active")),
QQLogin: model.IsTrueVal(checkSettingValue(settings, "qq_login")),
Themes: checkSettingValue(settings, "themes"),
DefaultTheme: checkSettingValue(settings, "defaultTheme"),
User: userRes,
SiteName: checkSettingValue(settings, "siteName"),
LoginCaptcha: model.IsTrueVal(checkSettingValue(settings, "login_captcha")),
RegCaptcha: model.IsTrueVal(checkSettingValue(settings, "reg_captcha")),
ForgetCaptcha: model.IsTrueVal(checkSettingValue(settings, "forget_captcha")),
EmailActive: model.IsTrueVal(checkSettingValue(settings, "email_active")),
QQLogin: model.IsTrueVal(checkSettingValue(settings, "qq_login")),
Themes: checkSettingValue(settings, "themes"),
DefaultTheme: checkSettingValue(settings, "defaultTheme"),
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": "邮箱",
"Password": "密码",
"Path": "路径",
"SourceID": "原始资源",
}
// 未通过的规则与中文对应
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",
"themes",
"defaultTheme",
"score_enabled",
"share_score_rate",
})
// 如果已登录,则同时返回用户信息

@ -233,6 +233,13 @@ func InitMasterRouter() *gin.Engine {
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…
Cancel
Save