feat(source link): create perm source link with shorter url

pull/1519/head
HFO4 2 years ago
parent 1f836a4b8b
commit 8d7ecedf47

@ -0,0 +1,29 @@
package middleware
import (
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/gin-gonic/gin"
)
// ValidateSourceLink validates if the perm source link is a valid redirect link
func ValidateSourceLink() gin.HandlerFunc {
return func(c *gin.Context) {
linkID, ok := c.Get("object_id")
if !ok {
c.JSON(200, serializer.Err(serializer.CodeFileNotFound, "", nil))
c.Abort()
return
}
sourceLink, err := model.GetSourceLinkByID(linkID)
if err != nil || sourceLink.File.ID == 0 || sourceLink.File.Name != c.Param("name") {
c.JSON(200, serializer.Err(serializer.CodeFileNotFound, "", nil))
c.Abort()
return
}
c.Set("source_link", sourceLink)
c.Next()
}
}

@ -39,7 +39,11 @@ func FrontendFileHandler() gin.HandlerFunc {
path := c.Request.URL.Path
// API 跳过
if strings.HasPrefix(path, "/api") || strings.HasPrefix(path, "/custom") || strings.HasPrefix(path, "/dav") || path == "/manifest.json" {
if strings.HasPrefix(path, "/api") ||
strings.HasPrefix(path, "/custom") ||
strings.HasPrefix(path, "/dav") ||
strings.HasPrefix(path, "/f") ||
path == "/manifest.json" {
c.Next()
return
}

@ -4,6 +4,7 @@ import (
"encoding/gob"
"encoding/json"
"errors"
"fmt"
"path"
"time"
@ -339,6 +340,25 @@ func (file *File) CanCopy() bool {
return file.UploadSessionID == nil
}
// CreateOrGetSourceLink creates a SourceLink model. If the given model exists, the existing
// model will be returned.
func (file *File) CreateOrGetSourceLink() (*SourceLink, error) {
res := &SourceLink{}
err := DB.Set("gorm:auto_preload", true).Where("file_id = ?", file.ID).Find(&res).Error
if err == nil && res.ID > 0 {
return res, nil
}
res.FileID = file.ID
res.Name = file.Name
if err := DB.Save(res).Error; err != nil {
return nil, fmt.Errorf("failed to insert SourceLink: %w", err)
}
res.File = *file
return res, nil
}
/*
webdav.FileInfo
*/

@ -23,16 +23,17 @@ type Group struct {
// GroupOption 用户组其他配置
type GroupOption struct {
ArchiveDownload bool `json:"archive_download,omitempty"` // 打包下载
ArchiveTask bool `json:"archive_task,omitempty"` // 在线压缩
CompressSize uint64 `json:"compress_size,omitempty"` // 可压缩大小
DecompressSize uint64 `json:"decompress_size,omitempty"`
OneTimeDownload bool `json:"one_time_download,omitempty"`
ShareDownload bool `json:"share_download,omitempty"`
Aria2 bool `json:"aria2,omitempty"` // 离线下载
Aria2Options map[string]interface{} `json:"aria2_options,omitempty"` // 离线下载用户组配置
SourceBatchSize int `json:"source_batch,omitempty"`
Aria2BatchSize int `json:"aria2_batch,omitempty"`
ArchiveDownload bool `json:"archive_download,omitempty"` // 打包下载
ArchiveTask bool `json:"archive_task,omitempty"` // 在线压缩
CompressSize uint64 `json:"compress_size,omitempty"` // 可压缩大小
DecompressSize uint64 `json:"decompress_size,omitempty"`
OneTimeDownload bool `json:"one_time_download,omitempty"`
ShareDownload bool `json:"share_download,omitempty"`
Aria2 bool `json:"aria2,omitempty"` // 离线下载
Aria2Options map[string]interface{} `json:"aria2_options,omitempty"` // 离线下载用户组配置
SourceBatchSize int `json:"source_batch,omitempty"`
RedirectedSource bool `json:"redirected_source,omitempty"`
Aria2BatchSize int `json:"aria2_batch,omitempty"`
}
// GetGroupByID 用ID获取用户组
@ -66,7 +67,7 @@ func (group *Group) BeforeSave() (err error) {
return err
}
//SerializePolicyList 将序列后的可选策略列表、配置写入数据库字段
// SerializePolicyList 将序列后的可选策略列表、配置写入数据库字段
// TODO 完善测试
func (group *Group) SerializePolicyList() (err error) {
policies, err := json.Marshal(&group.PolicyList)

@ -19,7 +19,7 @@ func needMigration() bool {
return DB.Where("name = ?", "db_version_"+conf.RequiredDBVersion).First(&setting).Error != nil
}
//执行数据迁移
// 执行数据迁移
func migration() {
// 确认是否需要执行迁移
if !needMigration() {
@ -41,7 +41,7 @@ func migration() {
}
DB.AutoMigrate(&User{}, &Setting{}, &Group{}, &Policy{}, &Folder{}, &File{}, &Share{},
&Task{}, &Download{}, &Tag{}, &Webdav{}, &Node{})
&Task{}, &Download{}, &Tag{}, &Webdav{}, &Node{}, &SourceLink{})
// 创建初始存储策略
addDefaultPolicy()
@ -104,12 +104,13 @@ func addDefaultGroups() {
ShareEnabled: true,
WebDAVEnabled: true,
OptionsSerialized: GroupOption{
ArchiveDownload: true,
ArchiveTask: true,
ShareDownload: true,
Aria2: true,
SourceBatchSize: 1000,
Aria2BatchSize: 50,
ArchiveDownload: true,
ArchiveTask: true,
ShareDownload: true,
Aria2: true,
SourceBatchSize: 1000,
Aria2BatchSize: 50,
RedirectedSource: true,
},
}
if err := DB.Create(&defaultAdminGroup).Error; err != nil {
@ -128,9 +129,10 @@ func addDefaultGroups() {
ShareEnabled: true,
WebDAVEnabled: true,
OptionsSerialized: GroupOption{
ShareDownload: true,
SourceBatchSize: 10,
Aria2BatchSize: 1,
ShareDownload: true,
SourceBatchSize: 10,
Aria2BatchSize: 1,
RedirectedSource: true,
},
}
if err := DB.Create(&defaultAdminGroup).Error; err != nil {

@ -0,0 +1,41 @@
package model
import (
"fmt"
"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
"github.com/jinzhu/gorm"
"net/url"
)
// SourceLink represent a shared file source link
type SourceLink struct {
gorm.Model
FileID uint // corresponding file ID
Name string // name of the file while creating the source link, for annotation
Downloads int // 下载数
// 关联模型
File File `gorm:"save_associations:false:false"`
}
// Link gets the URL of a SourceLink
func (s *SourceLink) Link() (string, error) {
baseURL := GetSiteURL()
linkPath, err := url.Parse(fmt.Sprintf("/f/%s/%s", hashid.HashID(s.ID, hashid.SourceLinkID), s.File.Name))
if err != nil {
return "", err
}
return baseURL.ResolveReference(linkPath).String(), nil
}
// GetTasksByID queries source link based on ID
func GetSourceLinkByID(id interface{}) (*SourceLink, error) {
link := &SourceLink{}
result := DB.Where("id = ?", id).First(link)
files, _ := GetFilesByIDs([]uint{link.FileID}, 0)
if len(files) > 0 {
link.File = files[0]
}
return link, result.Error
}

@ -11,7 +11,6 @@ import (
"time"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/auth"
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
@ -171,19 +170,6 @@ func (handler Driver) Source(
cacheKey := fmt.Sprintf("onedrive_source_%d_%s", handler.Policy.ID, path)
if file, ok := ctx.Value(fsctx.FileModelCtx).(model.File); ok {
cacheKey = fmt.Sprintf("onedrive_source_file_%d_%d", file.UpdatedAt.Unix(), file.ID)
// 如果是永久链接,则返回签名后的中转外链
if ttl == 0 {
signedURI, err := auth.SignURI(
auth.General,
fmt.Sprintf("/api/v3/file/source/%d/%s", file.ID, file.Name),
ttl,
)
if err != nil {
return "", err
}
return baseURL.ResolveReference(signedURI).String(), nil
}
}
// 尝试从缓存中查找

@ -15,6 +15,7 @@ const (
FolderID // 目录ID
TagID // 标签ID
PolicyID // 存储策略ID
SourceLinkID
)
var (

@ -79,8 +79,8 @@ func AnonymousGetContent(c *gin.Context) {
}
}
// AnonymousPermLink 文件签名后的永久链接
func AnonymousPermLink(c *gin.Context) {
// AnonymousPermLink Deprecated 文件签名后的永久链接
func AnonymousPermLinkDeprecated(c *gin.Context) {
// 创建上下文
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
@ -102,6 +102,39 @@ func AnonymousPermLink(c *gin.Context) {
}
}
// AnonymousPermLink 文件中转后的永久直链接
func AnonymousPermLink(c *gin.Context) {
// 创建上下文
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sourceLinkRaw, ok := c.Get("source_link")
if !ok {
c.JSON(200, serializer.Err(serializer.CodeFileNotFound, "", nil))
return
}
sourceLink := sourceLinkRaw.(*model.SourceLink)
service := &explorer.FileAnonymousGetService{
ID: sourceLink.FileID,
Name: sourceLink.File.Name,
}
res := service.Source(ctx, c)
// 是否需要重定向
if res.Code == -302 {
c.Redirect(302, res.Data.(string))
return
}
// 是否有错误发生
if res.Code != 0 {
c.JSON(200, res)
}
}
func GetSource(c *gin.Context) {
// 创建上下文
ctx, cancel := context.WithCancel(context.Background())

@ -145,6 +145,15 @@ func InitMasterRouter() *gin.Engine {
*/
{
// Redirect file source link
source := r.Group("f")
{
source.GET(":id/:name",
middleware.HashID(hashid.SourceLinkID),
middleware.ValidateSourceLink(),
controllers.AnonymousPermLink)
}
// 全局设置相关
site := v3.Group("site")
{
@ -210,7 +219,7 @@ func InitMasterRouter() *gin.Engine {
// 文件外链(直接输出文件数据)
file.GET("get/:id/:name", controllers.AnonymousGetContent)
// 文件外链(301跳转)
file.GET("source/:id/:name", controllers.AnonymousPermLink)
file.GET("source/:id/:name", controllers.AnonymousPermLinkDeprecated)
// 下载文件
file.GET("download/:id", controllers.Download)
// 打包并下载文件

@ -178,12 +178,13 @@ func (service *FileAnonymousGetService) Source(ctx context.Context, c *gin.Conte
}
// 获取文件流
res, err := fs.SignURL(ctx, &fs.FileTarget[0],
int64(model.GetIntSetting("preview_timeout", 60)), false)
ttl := int64(model.GetIntSetting("preview_timeout", 60))
res, err := fs.SignURL(ctx, &fs.FileTarget[0], ttl, false)
if err != nil {
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
}
c.Header("Cache-Control", fmt.Sprintf("max-age=%d", ttl))
return serializer.Response{
Code: -302,
Data: res,
@ -442,22 +443,46 @@ func (s *ItemIDService) Sources(ctx context.Context, c *gin.Context) serializer.
}
res := make([]serializer.Sources, 0, len(s.Raw().Items))
for _, id := range s.Raw().Items {
fs.FileTarget = []model.File{}
sourceURL, err := fs.GetSource(ctx, id)
if len(fs.FileTarget) > 0 {
current := serializer.Sources{
URL: sourceURL,
Name: fs.FileTarget[0].Name,
Parent: fs.FileTarget[0].FolderID,
files, err := model.GetFilesByIDs(s.Raw().Items, fs.User.ID)
if err != nil || len(files) == 0 {
return serializer.Err(serializer.CodeFileNotFound, "", err)
}
getSourceFunc := func(file model.File) (string, error) {
fs.FileTarget = []model.File{file}
return fs.GetSource(ctx, file.ID)
}
// Create redirected source link if needed
if fs.User.Group.OptionsSerialized.RedirectedSource {
getSourceFunc = func(file model.File) (string, error) {
source, err := file.CreateOrGetSourceLink()
if err != nil {
return "", err
}
sourceLinkURL, err := source.Link()
if err != nil {
current.Error = err.Error()
return "", err
}
res = append(res, current)
return sourceLinkURL, nil
}
}
for _, file := range files {
sourceURL, err := getSourceFunc(file)
current := serializer.Sources{
URL: sourceURL,
Name: file.Name,
Parent: file.FolderID,
}
if err != nil {
current.Error = err.Error()
}
res = append(res, current)
}
return serializer.Response{

Loading…
Cancel
Save