From 8d7ecedf47e7063f3a80dd7f3952ac94e11f324f Mon Sep 17 00:00:00 2001 From: HFO4 <912394456@qq.com> Date: Sat, 29 Oct 2022 11:06:07 +0800 Subject: [PATCH] feat(source link): create perm source link with shorter url --- middleware/file.go | 29 ++++++++++++++ middleware/frontend.go | 6 ++- models/file.go | 20 +++++++++ models/group.go | 23 ++++++----- models/migration.go | 24 ++++++----- models/source_link.go | 41 +++++++++++++++++++ pkg/filesystem/driver/onedrive/handler.go | 14 ------- pkg/hashid/hash.go | 1 + routers/controllers/file.go | 37 ++++++++++++++++- routers/router.go | 11 ++++- service/explorer/file.go | 49 +++++++++++++++++------ 11 files changed, 203 insertions(+), 52 deletions(-) create mode 100644 middleware/file.go create mode 100644 models/source_link.go diff --git a/middleware/file.go b/middleware/file.go new file mode 100644 index 0000000..69165a8 --- /dev/null +++ b/middleware/file.go @@ -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() + } +} diff --git a/middleware/frontend.go b/middleware/frontend.go index 95e4609..f07d9b6 100644 --- a/middleware/frontend.go +++ b/middleware/frontend.go @@ -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 } diff --git a/models/file.go b/models/file.go index 0c7268a..161bbbb 100644 --- a/models/file.go +++ b/models/file.go @@ -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 接口 */ diff --git a/models/group.go b/models/group.go index 78f7bfd..490fc38 100644 --- a/models/group.go +++ b/models/group.go @@ -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) diff --git a/models/migration.go b/models/migration.go index 63ae60d..17a08ce 100644 --- a/models/migration.go +++ b/models/migration.go @@ -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 { diff --git a/models/source_link.go b/models/source_link.go new file mode 100644 index 0000000..c1c1812 --- /dev/null +++ b/models/source_link.go @@ -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 +} diff --git a/pkg/filesystem/driver/onedrive/handler.go b/pkg/filesystem/driver/onedrive/handler.go index 98b2ba7..389ede2 100644 --- a/pkg/filesystem/driver/onedrive/handler.go +++ b/pkg/filesystem/driver/onedrive/handler.go @@ -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 - } - } // 尝试从缓存中查找 diff --git a/pkg/hashid/hash.go b/pkg/hashid/hash.go index a236d50..ffe5944 100644 --- a/pkg/hashid/hash.go +++ b/pkg/hashid/hash.go @@ -15,6 +15,7 @@ const ( FolderID // 目录ID TagID // 标签ID PolicyID // 存储策略ID + SourceLinkID ) var ( diff --git a/routers/controllers/file.go b/routers/controllers/file.go index c660f88..8caadc2 100644 --- a/routers/controllers/file.go +++ b/routers/controllers/file.go @@ -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()) diff --git a/routers/router.go b/routers/router.go index 07f40e3..5c50f3e 100644 --- a/routers/router.go +++ b/routers/router.go @@ -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) // 打包并下载文件 diff --git a/service/explorer/file.go b/service/explorer/file.go index aea0fbf..246d4a7 100644 --- a/service/explorer/file.go +++ b/service/explorer/file.go @@ -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{