mirror of https://github.com/cloudreve/Cloudreve
feat(source link): create perm source link with shorter url
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
|
path := c.Request.URL.Path
|
||||||
|
|
||||||
// API 跳过
|
// 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()
|
c.Next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"path"
|
"path"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -339,6 +340,25 @@ func (file *File) CanCopy() bool {
|
||||||
return file.UploadSessionID == nil
|
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 接口
|
实现 webdav.FileInfo 接口
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -23,16 +23,17 @@ type Group struct {
|
||||||
|
|
||||||
// GroupOption 用户组其他配置
|
// GroupOption 用户组其他配置
|
||||||
type GroupOption struct {
|
type GroupOption struct {
|
||||||
ArchiveDownload bool `json:"archive_download,omitempty"` // 打包下载
|
ArchiveDownload bool `json:"archive_download,omitempty"` // 打包下载
|
||||||
ArchiveTask bool `json:"archive_task,omitempty"` // 在线压缩
|
ArchiveTask bool `json:"archive_task,omitempty"` // 在线压缩
|
||||||
CompressSize uint64 `json:"compress_size,omitempty"` // 可压缩大小
|
CompressSize uint64 `json:"compress_size,omitempty"` // 可压缩大小
|
||||||
DecompressSize uint64 `json:"decompress_size,omitempty"`
|
DecompressSize uint64 `json:"decompress_size,omitempty"`
|
||||||
OneTimeDownload bool `json:"one_time_download,omitempty"`
|
OneTimeDownload bool `json:"one_time_download,omitempty"`
|
||||||
ShareDownload bool `json:"share_download,omitempty"`
|
ShareDownload bool `json:"share_download,omitempty"`
|
||||||
Aria2 bool `json:"aria2,omitempty"` // 离线下载
|
Aria2 bool `json:"aria2,omitempty"` // 离线下载
|
||||||
Aria2Options map[string]interface{} `json:"aria2_options,omitempty"` // 离线下载用户组配置
|
Aria2Options map[string]interface{} `json:"aria2_options,omitempty"` // 离线下载用户组配置
|
||||||
SourceBatchSize int `json:"source_batch,omitempty"`
|
SourceBatchSize int `json:"source_batch,omitempty"`
|
||||||
Aria2BatchSize int `json:"aria2_batch,omitempty"`
|
RedirectedSource bool `json:"redirected_source,omitempty"`
|
||||||
|
Aria2BatchSize int `json:"aria2_batch,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetGroupByID 用ID获取用户组
|
// GetGroupByID 用ID获取用户组
|
||||||
|
@ -66,7 +67,7 @@ func (group *Group) BeforeSave() (err error) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
//SerializePolicyList 将序列后的可选策略列表、配置写入数据库字段
|
// SerializePolicyList 将序列后的可选策略列表、配置写入数据库字段
|
||||||
// TODO 完善测试
|
// TODO 完善测试
|
||||||
func (group *Group) SerializePolicyList() (err error) {
|
func (group *Group) SerializePolicyList() (err error) {
|
||||||
policies, err := json.Marshal(&group.PolicyList)
|
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
|
return DB.Where("name = ?", "db_version_"+conf.RequiredDBVersion).First(&setting).Error != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//执行数据迁移
|
// 执行数据迁移
|
||||||
func migration() {
|
func migration() {
|
||||||
// 确认是否需要执行迁移
|
// 确认是否需要执行迁移
|
||||||
if !needMigration() {
|
if !needMigration() {
|
||||||
|
@ -41,7 +41,7 @@ func migration() {
|
||||||
}
|
}
|
||||||
|
|
||||||
DB.AutoMigrate(&User{}, &Setting{}, &Group{}, &Policy{}, &Folder{}, &File{}, &Share{},
|
DB.AutoMigrate(&User{}, &Setting{}, &Group{}, &Policy{}, &Folder{}, &File{}, &Share{},
|
||||||
&Task{}, &Download{}, &Tag{}, &Webdav{}, &Node{})
|
&Task{}, &Download{}, &Tag{}, &Webdav{}, &Node{}, &SourceLink{})
|
||||||
|
|
||||||
// 创建初始存储策略
|
// 创建初始存储策略
|
||||||
addDefaultPolicy()
|
addDefaultPolicy()
|
||||||
|
@ -104,12 +104,13 @@ func addDefaultGroups() {
|
||||||
ShareEnabled: true,
|
ShareEnabled: true,
|
||||||
WebDAVEnabled: true,
|
WebDAVEnabled: true,
|
||||||
OptionsSerialized: GroupOption{
|
OptionsSerialized: GroupOption{
|
||||||
ArchiveDownload: true,
|
ArchiveDownload: true,
|
||||||
ArchiveTask: true,
|
ArchiveTask: true,
|
||||||
ShareDownload: true,
|
ShareDownload: true,
|
||||||
Aria2: true,
|
Aria2: true,
|
||||||
SourceBatchSize: 1000,
|
SourceBatchSize: 1000,
|
||||||
Aria2BatchSize: 50,
|
Aria2BatchSize: 50,
|
||||||
|
RedirectedSource: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if err := DB.Create(&defaultAdminGroup).Error; err != nil {
|
if err := DB.Create(&defaultAdminGroup).Error; err != nil {
|
||||||
|
@ -128,9 +129,10 @@ func addDefaultGroups() {
|
||||||
ShareEnabled: true,
|
ShareEnabled: true,
|
||||||
WebDAVEnabled: true,
|
WebDAVEnabled: true,
|
||||||
OptionsSerialized: GroupOption{
|
OptionsSerialized: GroupOption{
|
||||||
ShareDownload: true,
|
ShareDownload: true,
|
||||||
SourceBatchSize: 10,
|
SourceBatchSize: 10,
|
||||||
Aria2BatchSize: 1,
|
Aria2BatchSize: 1,
|
||||||
|
RedirectedSource: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if err := DB.Create(&defaultAdminGroup).Error; err != nil {
|
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"
|
"time"
|
||||||
|
|
||||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
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/cache"
|
||||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver"
|
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver"
|
||||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
|
"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)
|
cacheKey := fmt.Sprintf("onedrive_source_%d_%s", handler.Policy.ID, path)
|
||||||
if file, ok := ctx.Value(fsctx.FileModelCtx).(model.File); ok {
|
if file, ok := ctx.Value(fsctx.FileModelCtx).(model.File); ok {
|
||||||
cacheKey = fmt.Sprintf("onedrive_source_file_%d_%d", file.UpdatedAt.Unix(), file.ID)
|
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
|
FolderID // 目录ID
|
||||||
TagID // 标签ID
|
TagID // 标签ID
|
||||||
PolicyID // 存储策略ID
|
PolicyID // 存储策略ID
|
||||||
|
SourceLinkID
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
|
@ -79,8 +79,8 @@ func AnonymousGetContent(c *gin.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AnonymousPermLink 文件签名后的永久链接
|
// AnonymousPermLink Deprecated 文件签名后的永久链接
|
||||||
func AnonymousPermLink(c *gin.Context) {
|
func AnonymousPermLinkDeprecated(c *gin.Context) {
|
||||||
// 创建上下文
|
// 创建上下文
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
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) {
|
func GetSource(c *gin.Context) {
|
||||||
// 创建上下文
|
// 创建上下文
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
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")
|
site := v3.Group("site")
|
||||||
{
|
{
|
||||||
|
@ -210,7 +219,7 @@ func InitMasterRouter() *gin.Engine {
|
||||||
// 文件外链(直接输出文件数据)
|
// 文件外链(直接输出文件数据)
|
||||||
file.GET("get/:id/:name", controllers.AnonymousGetContent)
|
file.GET("get/:id/:name", controllers.AnonymousGetContent)
|
||||||
// 文件外链(301跳转)
|
// 文件外链(301跳转)
|
||||||
file.GET("source/:id/:name", controllers.AnonymousPermLink)
|
file.GET("source/:id/:name", controllers.AnonymousPermLinkDeprecated)
|
||||||
// 下载文件
|
// 下载文件
|
||||||
file.GET("download/:id", controllers.Download)
|
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],
|
ttl := int64(model.GetIntSetting("preview_timeout", 60))
|
||||||
int64(model.GetIntSetting("preview_timeout", 60)), false)
|
res, err := fs.SignURL(ctx, &fs.FileTarget[0], ttl, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
|
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.Header("Cache-Control", fmt.Sprintf("max-age=%d", ttl))
|
||||||
return serializer.Response{
|
return serializer.Response{
|
||||||
Code: -302,
|
Code: -302,
|
||||||
Data: res,
|
Data: res,
|
||||||
|
@ -442,24 +443,48 @@ func (s *ItemIDService) Sources(ctx context.Context, c *gin.Context) serializer.
|
||||||
}
|
}
|
||||||
|
|
||||||
res := make([]serializer.Sources, 0, len(s.Raw().Items))
|
res := make([]serializer.Sources, 0, len(s.Raw().Items))
|
||||||
for _, id := range s.Raw().Items {
|
files, err := model.GetFilesByIDs(s.Raw().Items, fs.User.ID)
|
||||||
fs.FileTarget = []model.File{}
|
if err != nil || len(files) == 0 {
|
||||||
sourceURL, err := fs.GetSource(ctx, id)
|
return serializer.Err(serializer.CodeFileNotFound, "", err)
|
||||||
if len(fs.FileTarget) > 0 {
|
}
|
||||||
current := serializer.Sources{
|
|
||||||
URL: sourceURL,
|
|
||||||
Name: fs.FileTarget[0].Name,
|
|
||||||
Parent: fs.FileTarget[0].FolderID,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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 {
|
if err != nil {
|
||||||
current.Error = err.Error()
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
res = append(res, current)
|
sourceLinkURL, err := source.Link()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
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{
|
return serializer.Response{
|
||||||
Code: 0,
|
Code: 0,
|
||||||
Data: res,
|
Data: res,
|
||||||
|
|
Loading…
Reference in New Issue