From dc611bcb0d004764063c3cdbd2e9ba3914dad27c Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Thu, 26 Jun 2025 18:45:54 +0800 Subject: [PATCH] feat(explorer): manage created direct links / option to enable unique redirected direct links --- assets | 2 +- inventory/direct_link.go | 9 +++++++++ inventory/file.go | 18 ++++++++++-------- inventory/types/types.go | 1 + pkg/filemanager/fs/dbfs/dbfs.go | 9 +++++++++ pkg/filemanager/fs/fs.go | 1 + pkg/filemanager/manager/entity.go | 3 ++- routers/controllers/file.go | 10 ++++++++++ routers/router.go | 16 ++++++++++++---- service/explorer/file.go | 25 +++++++++++++++++++++++++ service/explorer/response.go | 25 +++++++++++++++++++++++-- 11 files changed, 103 insertions(+), 16 deletions(-) diff --git a/assets b/assets index 0fa5754..672b001 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 0fa57541d5b1018251674ef01a7e799f6899564e +Subproject commit 672b00192cb89575b56805d338eb0fdc0ca7ed22 diff --git a/inventory/direct_link.go b/inventory/direct_link.go index dfed618..f6bc356 100644 --- a/inventory/direct_link.go +++ b/inventory/direct_link.go @@ -5,6 +5,7 @@ import ( "github.com/cloudreve/Cloudreve/v4/ent" "github.com/cloudreve/Cloudreve/v4/ent/directlink" + "github.com/cloudreve/Cloudreve/v4/ent/schema" "github.com/cloudreve/Cloudreve/v4/pkg/conf" "github.com/cloudreve/Cloudreve/v4/pkg/hashid" ) @@ -16,6 +17,8 @@ type ( GetByNameID(ctx context.Context, id int, name string) (*ent.DirectLink, error) // GetByID get direct link by id GetByID(ctx context.Context, id int) (*ent.DirectLink, error) + // Delete delete direct link by id + Delete(ctx context.Context, id int) error } LoadDirectLinkFile struct{} ) @@ -60,6 +63,12 @@ func (d *directLinkClient) GetByNameID(ctx context.Context, id int, name string) return res, nil } +func (d *directLinkClient) Delete(ctx context.Context, id int) error { + ctx = schema.SkipSoftDelete(ctx) + _, err := d.client.DirectLink.Delete().Where(directlink.ID(id)).Exec(ctx) + return err +} + func withDirectLinkEagerLoading(ctx context.Context, q *ent.DirectLinkQuery) *ent.DirectLinkQuery { if v, ok := ctx.Value(LoadDirectLinkFile{}).(bool); ok && v { q.WithFile(func(m *ent.FileQuery) { diff --git a/inventory/file.go b/inventory/file.go index a99d40f..b94d57f 100644 --- a/inventory/file.go +++ b/inventory/file.go @@ -192,7 +192,7 @@ type FileClient interface { // UnlinkEntity unlinks an entity from a file UnlinkEntity(ctx context.Context, entity *ent.Entity, file *ent.File, owner *ent.User) (StorageDiff, error) // CreateDirectLink creates a direct link for a file - CreateDirectLink(ctx context.Context, fileID int, name string, speed int) (*ent.DirectLink, error) + CreateDirectLink(ctx context.Context, fileID int, name string, speed int, reuse bool) (*ent.DirectLink, error) // CountByTimeRange counts files created in a given time range CountByTimeRange(ctx context.Context, start, end *time.Time) (int, error) // CountEntityByTimeRange counts entities created in a given time range @@ -322,13 +322,15 @@ func (f *fileClient) CountEntityByStoragePolicyID(ctx context.Context, storagePo return v[0].Count, v[0].Sum, nil } -func (f *fileClient) CreateDirectLink(ctx context.Context, file int, name string, speed int) (*ent.DirectLink, error) { - // Find existed - existed, err := f.client.DirectLink. - Query(). - Where(directlink.FileID(file), directlink.Name(name), directlink.Speed(speed)).First(ctx) - if err == nil { - return existed, nil +func (f *fileClient) CreateDirectLink(ctx context.Context, file int, name string, speed int, reuse bool) (*ent.DirectLink, error) { + if reuse { + // Find existed + existed, err := f.client.DirectLink. + Query(). + Where(directlink.FileID(file), directlink.Name(name), directlink.Speed(speed)).First(ctx) + if err == nil { + return existed, nil + } } return f.client.DirectLink. diff --git a/inventory/types/types.go b/inventory/types/types.go index 2e6b9df..95a3676 100644 --- a/inventory/types/types.go +++ b/inventory/types/types.go @@ -206,6 +206,7 @@ const ( GroupPermission_CommunityPlaceholder4 GroupPermissionSetExplicitUser_placeholder GroupPermissionIgnoreFileOwnership // not used + GroupPermissionUniqueRedirectDirectLink ) const ( diff --git a/pkg/filemanager/fs/dbfs/dbfs.go b/pkg/filemanager/fs/dbfs/dbfs.go index cb7d3dc..e12b24f 100644 --- a/pkg/filemanager/fs/dbfs/dbfs.go +++ b/pkg/filemanager/fs/dbfs/dbfs.go @@ -384,6 +384,10 @@ func (f *DBFS) Get(ctx context.Context, path *fs.URI, opts ...fs.Option) (fs.Fil ctx = context.WithValue(ctx, inventory.LoadFileEntity{}, true) } + if o.extendedInfo { + ctx = context.WithValue(ctx, inventory.LoadFileDirectLink{}, true) + } + if o.loadFileShareIfOwned { ctx = context.WithValue(ctx, inventory.LoadFileShare{}, true) } @@ -407,6 +411,11 @@ func (f *DBFS) Get(ctx context.Context, path *fs.URI, opts ...fs.Option) (fs.Fil StorageUsed: target.SizeUsed(), EntityStoragePolicies: make(map[int]*ent.StoragePolicy), } + + if f.user.ID == target.OwnerID() { + extendedInfo.DirectLinks = target.Model.Edges.DirectLinks + } + policyID := target.PolicyID() if policyID > 0 { policy, err := f.storagePolicyClient.GetPolicyByID(ctx, policyID) diff --git a/pkg/filemanager/fs/fs.go b/pkg/filemanager/fs/fs.go index e6b1bde..b081340 100644 --- a/pkg/filemanager/fs/fs.go +++ b/pkg/filemanager/fs/fs.go @@ -191,6 +191,7 @@ type ( Shares []*ent.Share EntityStoragePolicies map[int]*ent.StoragePolicy View *types.ExplorerView + DirectLinks []*ent.DirectLink } FolderSummary struct { diff --git a/pkg/filemanager/manager/entity.go b/pkg/filemanager/manager/entity.go index 7198688..a8954bc 100644 --- a/pkg/filemanager/manager/entity.go +++ b/pkg/filemanager/manager/entity.go @@ -98,8 +98,9 @@ func (m *manager) GetDirectLink(ctx context.Context, urls ...*fs.URI) ([]DirectL } if useRedirect { + reuseExisting := !m.user.Edges.Group.Permissions.Enabled(int(types.GroupPermissionUniqueRedirectDirectLink)) // Use redirect source - link, err := fileClient.CreateDirectLink(ctx, file.ID(), file.Name(), m.user.Edges.Group.SpeedLimit) + link, err := fileClient.CreateDirectLink(ctx, file.ID(), file.Name(), m.user.Edges.Group.SpeedLimit, reuseExisting) if err != nil { ae.Add(url.String(), err) continue diff --git a/routers/controllers/file.go b/routers/controllers/file.go index 2f3d049..dab661a 100644 --- a/routers/controllers/file.go +++ b/routers/controllers/file.go @@ -116,6 +116,16 @@ func GetSource(c *gin.Context) { c.JSON(200, serializer.Response{Data: res}) } +func DeleteDirectLink(c *gin.Context) { + err := explorer.DeleteDirectLink(c) + if err != nil { + c.JSON(200, serializer.Err(c, err)) + return + } + + c.JSON(200, serializer.Response{}) +} + // Thumb 获取文件缩略图 func Thumb(c *gin.Context) { service := ParametersFromContext[*explorer.FileThumbService](c, explorer.FileThumbParameterCtx{}) diff --git a/routers/router.go b/routers/router.go index 3af6e4e..af16223 100644 --- a/routers/router.go +++ b/routers/router.go @@ -697,10 +697,18 @@ func initMasterRouter(dep dependency.Dep) *gin.Engine { ) // 取得文件外链 - file.PUT("source", - controllers.FromJSON[explorer.GetDirectLinkService](explorer.GetDirectLinkParamCtx{}), - middleware.ValidateBatchFileCount(dep, explorer.GetDirectLinkParamCtx{}), - controllers.GetSource) + source := file.Group("source") + { + source.PUT("", + controllers.FromJSON[explorer.GetDirectLinkService](explorer.GetDirectLinkParamCtx{}), + middleware.ValidateBatchFileCount(dep, explorer.GetDirectLinkParamCtx{}), + controllers.GetSource, + ) + source.DELETE(":id", + middleware.HashID(hashid.SourceLinkID), + controllers.DeleteDirectLink, + ) + } // Patch view file.PATCH("view", controllers.FromJSON[explorer.PatchViewService](explorer.PatchViewParameterCtx{}), diff --git a/service/explorer/file.go b/service/explorer/file.go index 83aa835..b207582 100644 --- a/service/explorer/file.go +++ b/service/explorer/file.go @@ -120,6 +120,31 @@ func (s *GetDirectLinkService) Get(c *gin.Context) ([]DirectLinkResponse, error) return BuildDirectLinkResponse(res), err } +func DeleteDirectLink(c *gin.Context) error { + dep := dependency.FromContext(c) + user := inventory.UserFromContext(c) + m := manager.NewFileManager(dep, user) + defer m.Recycle() + + linkId := hashid.FromContext(c) + linkClient := dep.DirectLinkClient() + ctx := context.WithValue(c, inventory.LoadDirectLinkFile{}, true) + link, err := linkClient.GetByID(ctx, linkId) + if err != nil || link.Edges.File == nil { + return serializer.NewError(serializer.CodeNotFound, "Direct link not found", err) + } + + if link.Edges.File.OwnerID != user.ID { + return serializer.NewError(serializer.CodeNotFound, "Direct link not found", err) + } + + if err := linkClient.Delete(c, link.ID); err != nil { + return serializer.NewError(serializer.CodeDBError, "Failed to delete direct link", err) + } + + return nil +} + type ( // ListFileParameterCtx define key fore ListFileService ListFileParameterCtx struct{} diff --git a/service/explorer/response.go b/service/explorer/response.go index bcf0208..b0251cb 100644 --- a/service/explorer/response.go +++ b/service/explorer/response.go @@ -239,6 +239,14 @@ type ExtendedInfo struct { Shares []Share `json:"shares,omitempty"` Entities []Entity `json:"entities,omitempty"` View *types.ExplorerView `json:"view,omitempty"` + DirectLinks []DirectLink `json:"direct_links,omitempty"` +} + +type DirectLink struct { + ID string `json:"id"` + URL string `json:"url"` + Downloaded int `json:"downloaded"` + CreatedAt time.Time `json:"created_at"` } type StoragePolicy struct { @@ -372,16 +380,20 @@ func BuildExtendedInfo(ctx context.Context, u *ent.User, f fs.File, hasher hashi return nil } + dep := dependency.FromContext(ctx) + base := dep.SettingProvider().SiteURL(ctx) + ext := &ExtendedInfo{ StoragePolicy: BuildStoragePolicy(extendedInfo.StoragePolicy, hasher), StorageUsed: extendedInfo.StorageUsed, Entities: lo.Map(f.Entities(), func(e fs.Entity, index int) Entity { return BuildEntity(extendedInfo, e, hasher) }), + DirectLinks: lo.Map(extendedInfo.DirectLinks, func(d *ent.DirectLink, index int) DirectLink { + return BuildDirectLink(d, hasher, base) + }), } - dep := dependency.FromContext(ctx) - base := dep.SettingProvider().SiteURL(ctx) if u.ID == f.OwnerID() { // Only owner can see the shares settings. ext.Shares = lo.Map(extendedInfo.Shares, func(s *ent.Share, index int) Share { @@ -393,6 +405,15 @@ func BuildExtendedInfo(ctx context.Context, u *ent.User, f fs.File, hasher hashi return ext } +func BuildDirectLink(d *ent.DirectLink, hasher hashid.Encoder, base *url.URL) DirectLink { + return DirectLink{ + ID: hashid.EncodeSourceLinkID(hasher, d.ID), + URL: routes.MasterDirectLink(base, hashid.EncodeSourceLinkID(hasher, d.ID), d.Name).String(), + Downloaded: d.Downloads, + CreatedAt: d.CreatedAt, + } +} + func BuildEntity(extendedInfo *fs.FileExtendedInfo, e fs.Entity, hasher hashid.Encoder) Entity { var u *user.User createdBy := e.CreatedBy()