diff --git a/go.mod b/go.mod index 995487a..a0c456a 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index f773e66..48acde0 100644 --- a/go.sum +++ b/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/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= diff --git a/models/group.go b/models/group.go index b1b5ffe..a082a1d 100644 --- a/models/group.go +++ b/models/group.go @@ -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 获取用户离线下载设备 diff --git a/models/migration.go b/models/migration.go index 5ca26a4..db2597a 100644 --- a/models/migration.go +++ b/models/migration.go @@ -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 { diff --git a/models/share.go b/models/share.go new file mode 100644 index 0000000..31caf4e --- /dev/null +++ b/models/share.go @@ -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 +} diff --git a/pkg/conf/conf.go b/pkg/conf/conf.go index 9a77174..5e7282d 100644 --- a/pkg/conf/conf.go +++ b/pkg/conf/conf.go @@ -22,6 +22,7 @@ type system struct { Listen string `validate:"required"` Debug bool SessionSecret string + HashIDSalt string `validate:"required"` } // slave 作为slave存储端配置 diff --git a/pkg/hashid/hash.go b/pkg/hashid/hash.go new file mode 100644 index 0000000..f249ef5 --- /dev/null +++ b/pkg/hashid/hash.go @@ -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 +} diff --git a/pkg/serializer/error.go b/pkg/serializer/error.go index 484dcdf..9a23860 100644 --- a/pkg/serializer/error.go +++ b/pkg/serializer/error.go @@ -48,6 +48,8 @@ const ( CodeCheckLogin = 401 // CodeNoRightErr 未授权访问 CodeNoRightErr = 403 + // CodeNotFound 资源未找到 + CodeNotFound = 404 // CodeUploadFailed 上传出错 CodeUploadFailed = 40002 // CodeCreateFolderFailed 目录创建失败 diff --git a/pkg/serializer/setting.go b/pkg/serializer/setting.go index 7e4fd6d..46be7f6 100644 --- a/pkg/serializer/setting.go +++ b/pkg/serializer/setting.go @@ -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, }} } diff --git a/routers/controllers/main.go b/routers/controllers/main.go index 79cf271..062c12d 100644 --- a/routers/controllers/main.go +++ b/routers/controllers/main.go @@ -15,6 +15,7 @@ func ParamErrorMsg(filed string, tag string) string { "UserName": "邮箱", "Password": "密码", "Path": "路径", + "SourceID": "原始资源", } // 未通过的规则与中文对应 tagMap := map[string]string{ diff --git a/routers/controllers/share.go b/routers/controllers/share.go new file mode 100644 index 0000000..7495a71 --- /dev/null +++ b/routers/controllers/share.go @@ -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)) + } +} diff --git a/routers/controllers/site.go b/routers/controllers/site.go index 98e8bd8..cecb533 100644 --- a/routers/controllers/site.go +++ b/routers/controllers/site.go @@ -21,6 +21,8 @@ func SiteConfig(c *gin.Context) { "email_active", "themes", "defaultTheme", + "score_enabled", + "share_score_rate", }) // 如果已登录,则同时返回用户信息 diff --git a/routers/router.go b/routers/router.go index bd24bc6..de83217 100644 --- a/routers/router.go +++ b/routers/router.go @@ -233,6 +233,13 @@ func InitMasterRouter() *gin.Engine { object.POST("rename", controllers.Rename) } + // 分享 + share := auth.Group("share") + { + // 创建新分享 + share.POST("", controllers.CreateShare) + } + } } diff --git a/service/share/manage.go b/service/share/manage.go new file mode 100644 index 0000000..ebe483b --- /dev/null +++ b/service/share/manage.go @@ -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(), + } +}