refactor(download): handle stream saver download outside of driver implementation (fix #2366)

pull/2224/merge
Aaron Liu 2025-05-16 13:52:31 +08:00
parent bdaf091aca
commit 0a28bf1689
8 changed files with 73 additions and 59 deletions

View File

@ -105,6 +105,8 @@ type (
ThumbMaxSize int64 ThumbMaxSize int64
// ThumbProxy indicates whether to generate thumbnails using local generators. // ThumbProxy indicates whether to generate thumbnails using local generators.
ThumbProxy bool ThumbProxy bool
// BrowserRelayedDownload indicates whether to relay download via stream-saver.
BrowserRelayedDownload bool
} }
) )

View File

@ -4,6 +4,10 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"os"
"strings"
"time"
"github.com/cloudreve/Cloudreve/v4/ent" "github.com/cloudreve/Cloudreve/v4/ent"
"github.com/cloudreve/Cloudreve/v4/inventory/types" "github.com/cloudreve/Cloudreve/v4/inventory/types"
"github.com/cloudreve/Cloudreve/v4/pkg/boolset" "github.com/cloudreve/Cloudreve/v4/pkg/boolset"
@ -16,10 +20,6 @@ import (
"github.com/cloudreve/Cloudreve/v4/pkg/request" "github.com/cloudreve/Cloudreve/v4/pkg/request"
"github.com/cloudreve/Cloudreve/v4/pkg/serializer" "github.com/cloudreve/Cloudreve/v4/pkg/serializer"
"github.com/cloudreve/Cloudreve/v4/pkg/setting" "github.com/cloudreve/Cloudreve/v4/pkg/setting"
"net/url"
"os"
"strings"
"time"
) )
// Driver OneDrive 适配器 // Driver OneDrive 适配器
@ -150,11 +150,6 @@ func (handler *Driver) Source(ctx context.Context, e fs.Entity, args *driver.Get
return "", err return "", err
} }
if args.IsDownload && handler.policy.Settings.StreamSaver {
downloadUrl := res.DownloadURL + "&" + streamSaverParam + "=" + url.QueryEscape(args.DisplayName)
return downloadUrl, nil
}
return res.DownloadURL, nil return res.DownloadURL, nil
} }
@ -229,12 +224,13 @@ func (handler *Driver) CompleteUpload(ctx context.Context, session *fs.UploadSes
func (handler *Driver) Capabilities() *driver.Capabilities { func (handler *Driver) Capabilities() *driver.Capabilities {
return &driver.Capabilities{ return &driver.Capabilities{
StaticFeatures: features, StaticFeatures: features,
ThumbSupportedExts: handler.policy.Settings.ThumbExts, ThumbSupportedExts: handler.policy.Settings.ThumbExts,
ThumbSupportAllExts: handler.policy.Settings.ThumbSupportAllExts, ThumbSupportAllExts: handler.policy.Settings.ThumbSupportAllExts,
ThumbMaxSize: handler.policy.Settings.ThumbMaxSize, ThumbMaxSize: handler.policy.Settings.ThumbMaxSize,
ThumbProxy: handler.policy.Settings.ThumbGeneratorProxy, ThumbProxy: handler.policy.Settings.ThumbGeneratorProxy,
MediaMetaProxy: handler.policy.Settings.MediaMetaGeneratorProxy, MediaMetaProxy: handler.policy.Settings.MediaMetaGeneratorProxy,
BrowserRelayedDownload: handler.policy.Settings.StreamSaver,
} }
} }

View File

@ -22,7 +22,7 @@ const (
const ( const (
QuerySearchName = "name" QuerySearchName = "name"
QuerySearchNameOpOr = "use_or" QuerySearchNameOpOr = "name_op_or"
QuerySearchMetadataPrefix = "meta_" QuerySearchMetadataPrefix = "meta_"
QuerySearchCaseFolding = "case_folding" QuerySearchCaseFolding = "case_folding"
QuerySearchType = "type" QuerySearchType = "type"

View File

@ -19,33 +19,39 @@ import (
"github.com/samber/lo" "github.com/samber/lo"
) )
type EntityManagement interface { type (
// GetEntityUrls gets download urls of given entities, return URLs and the earliest expiry time EntityManagement interface {
GetEntityUrls(ctx context.Context, args []GetEntityUrlArgs, opts ...fs.Option) ([]string, *time.Time, error) // GetEntityUrls gets download urls of given entities, return URLs and the earliest expiry time
// GetUrlForRedirectedDirectLink gets redirected direct download link of given direct link GetEntityUrls(ctx context.Context, args []GetEntityUrlArgs, opts ...fs.Option) ([]EntityUrl, *time.Time, error)
GetUrlForRedirectedDirectLink(ctx context.Context, dl *ent.DirectLink, opts ...fs.Option) (string, *time.Time, error) // GetUrlForRedirectedDirectLink gets redirected direct download link of given direct link
// GetDirectLink gets permanent direct download link of given files GetUrlForRedirectedDirectLink(ctx context.Context, dl *ent.DirectLink, opts ...fs.Option) (string, *time.Time, error)
GetDirectLink(ctx context.Context, urls ...*fs.URI) ([]DirectLink, error) // GetDirectLink gets permanent direct download link of given files
// GetEntitySource gets source of given entity GetDirectLink(ctx context.Context, urls ...*fs.URI) ([]DirectLink, error)
GetEntitySource(ctx context.Context, entityID int, opts ...fs.Option) (entitysource.EntitySource, error) // GetEntitySource gets source of given entity
// Thumbnail gets thumbnail entity of given file GetEntitySource(ctx context.Context, entityID int, opts ...fs.Option) (entitysource.EntitySource, error)
Thumbnail(ctx context.Context, uri *fs.URI) (entitysource.EntitySource, error) // Thumbnail gets thumbnail entity of given file
// SubmitAndAwaitThumbnailTask submits a thumbnail task and waits for result Thumbnail(ctx context.Context, uri *fs.URI) (entitysource.EntitySource, error)
SubmitAndAwaitThumbnailTask(ctx context.Context, uri *fs.URI, ext string, entity fs.Entity) (fs.Entity, error) // SubmitAndAwaitThumbnailTask submits a thumbnail task and waits for result
// SetCurrentVersion sets current version of given file SubmitAndAwaitThumbnailTask(ctx context.Context, uri *fs.URI, ext string, entity fs.Entity) (fs.Entity, error)
SetCurrentVersion(ctx context.Context, path *fs.URI, version int) error // SetCurrentVersion sets current version of given file
// DeleteVersion deletes a version of given file SetCurrentVersion(ctx context.Context, path *fs.URI, version int) error
DeleteVersion(ctx context.Context, path *fs.URI, version int) error // DeleteVersion deletes a version of given file
// ExtractAndSaveMediaMeta extracts and saves media meta into file metadata of given file. DeleteVersion(ctx context.Context, path *fs.URI, version int) error
ExtractAndSaveMediaMeta(ctx context.Context, uri *fs.URI, entityID int) error // ExtractAndSaveMediaMeta extracts and saves media meta into file metadata of given file.
// RecycleEntities recycles a group of entities ExtractAndSaveMediaMeta(ctx context.Context, uri *fs.URI, entityID int) error
RecycleEntities(ctx context.Context, force bool, entityIDs ...int) error // RecycleEntities recycles a group of entities
} RecycleEntities(ctx context.Context, force bool, entityIDs ...int) error
}
DirectLink struct {
File fs.File
Url string
}
type DirectLink struct { EntityUrl struct {
File fs.File Url string `json:"url"`
Url string BrowserDownloadDisplayName string `json:"stream_saver_display_name,omitempty"`
} }
)
func (m *manager) GetDirectLink(ctx context.Context, urls ...*fs.URI) ([]DirectLink, error) { func (m *manager) GetDirectLink(ctx context.Context, urls ...*fs.URI) ([]DirectLink, error) {
ae := serializer.NewAggregateError() ae := serializer.NewAggregateError()
@ -212,14 +218,14 @@ func (m *manager) GetUrlForRedirectedDirectLink(ctx context.Context, dl *ent.Dir
return res, expire, nil return res, expire, nil
} }
func (m *manager) GetEntityUrls(ctx context.Context, args []GetEntityUrlArgs, opts ...fs.Option) ([]string, *time.Time, error) { func (m *manager) GetEntityUrls(ctx context.Context, args []GetEntityUrlArgs, opts ...fs.Option) ([]EntityUrl, *time.Time, error) {
o := newOption() o := newOption()
for _, opt := range opts { for _, opt := range opts {
opt.Apply(o) opt.Apply(o)
} }
var earliestExpireAt *time.Time var earliestExpireAt *time.Time
res := make([]string, len(args)) res := make([]EntityUrl, len(args))
ae := serializer.NewAggregateError() ae := serializer.NewAggregateError()
for i, arg := range args { for i, arg := range args {
file, err := m.fs.Get( file, err := m.fs.Get(
@ -261,6 +267,12 @@ func (m *manager) GetEntityUrls(ctx context.Context, args []GetEntityUrlArgs, op
m.l.Warning("Failed to execute navigator hooks: %s", err) m.l.Warning("Failed to execute navigator hooks: %s", err)
} }
policy, d, err := m.getEntityPolicyDriver(ctx, target, nil)
if err != nil {
ae.Add(arg.URI.String(), err)
continue
}
// Try to read from cache. // Try to read from cache.
cacheKey := entityUrlCacheKey(target.ID(), o.DownloadSpeed, getEntityDisplayName(file, target), o.IsDownload, cacheKey := entityUrlCacheKey(target.ID(), o.DownloadSpeed, getEntityDisplayName(file, target), o.IsDownload,
m.settings.SiteURL(ctx).String()) m.settings.SiteURL(ctx).String())
@ -270,17 +282,14 @@ func (m *manager) GetEntityUrls(ctx context.Context, args []GetEntityUrlArgs, op
if cachedItem.ExpireAt != nil && (earliestExpireAt == nil || cachedItem.ExpireAt.Before(*earliestExpireAt)) { if cachedItem.ExpireAt != nil && (earliestExpireAt == nil || cachedItem.ExpireAt.Before(*earliestExpireAt)) {
earliestExpireAt = cachedItem.ExpireAt earliestExpireAt = cachedItem.ExpireAt
} }
res[i] = cachedItem.Url res[i] = EntityUrl{
Url: cachedItem.Url,
BrowserDownloadDisplayName: cachedItem.BrowserDownloadDisplayName,
}
continue continue
} }
// Cache miss, Generate new url // Cache miss, Generate new url
policy, d, err := m.getEntityPolicyDriver(ctx, target, nil)
if err != nil {
ae.Add(arg.URI.String(), err)
continue
}
source := entitysource.NewEntitySource(target, d, policy, m.auth, m.settings, m.hasher, m.dep.RequestClient(), source := entitysource.NewEntitySource(target, d, policy, m.auth, m.settings, m.hasher, m.dep.RequestClient(),
m.l, m.config, m.dep.MimeDetector(ctx)) m.l, m.config, m.dep.MimeDetector(ctx))
downloadUrl, err := source.Url(ctx, downloadUrl, err := source.Url(ctx,
@ -308,7 +317,12 @@ func (m *manager) GetEntityUrls(ctx context.Context, args []GetEntityUrlArgs, op
}, cacheValidDuration) }, cacheValidDuration)
} }
res[i] = downloadUrl.Url res[i] = EntityUrl{
Url: downloadUrl.Url,
}
if d.Capabilities().BrowserRelayedDownload {
res[i].BrowserDownloadDisplayName = getEntityDisplayName(file, target)
}
} }
return res, earliestExpireAt, ae.Aggregate() return res, earliestExpireAt, ae.Aggregate()

View File

@ -36,8 +36,9 @@ type (
} }
EntityUrlCache struct { EntityUrlCache struct {
Url string Url string
ExpireAt *time.Time BrowserDownloadDisplayName string
ExpireAt *time.Time
} }
) )

View File

@ -194,7 +194,7 @@ func (m *RemoteDownloadTask) createDownloadTask(ctx context.Context, dep depende
return task.StatusError, fmt.Errorf("no torrent urls found") return task.StatusError, fmt.Errorf("no torrent urls found")
} }
torrentUrl = torrentUrls[0] torrentUrl = torrentUrls[0].Url
} }
// Create download task // Create download task

View File

@ -367,8 +367,8 @@ type (
NoCache bool `json:"no_cache"` NoCache bool `json:"no_cache"`
} }
FileURLResponse struct { FileURLResponse struct {
Urls []string `json:"urls"` Urls []manager.EntityUrl `json:"urls"`
Expires *time.Time `json:"expires"` Expires *time.Time `json:"expires"`
} }
ArchiveDownloadSession struct { ArchiveDownloadSession struct {
Uris []*fs.URI `json:"uris"` Uris []*fs.URI `json:"uris"`
@ -419,7 +419,7 @@ func (s *FileURLService) GetArchiveDownloadSession(c *gin.Context) (*FileURLResp
} }
return &FileURLResponse{ return &FileURLResponse{
Urls: []string{finalUrl.String()}, Urls: []manager.EntityUrl{{Url: finalUrl.String()}},
Expires: &expire, Expires: &expire,
}, nil }, nil
} }
@ -473,7 +473,7 @@ func (s *FileURLService) Get(c *gin.Context) (*FileURLResponse, error) {
//} //}
if s.Redirect && len(uris) == 1 { if s.Redirect && len(uris) == 1 {
c.Redirect(http.StatusFound, res[0]) c.Redirect(http.StatusFound, res[0].Url)
return nil, nil return nil, nil
} }

View File

@ -71,6 +71,7 @@ type (
// UserResetEmailService 发送密码重设邮件服务 // UserResetEmailService 发送密码重设邮件服务
UserResetEmailService struct { UserResetEmailService struct {
UserName string `form:"email" json:"email" binding:"required,email"` UserName string `form:"email" json:"email" binding:"required,email"`
Language string `form:"language" json:"language"`
} }
UserResetEmailParameterCtx struct{} UserResetEmailParameterCtx struct{}
) )