From 3cda4d1ef74645511234b6ac29cd61bc5f6b83cd Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Sat, 12 Jul 2025 11:15:33 +0800 Subject: [PATCH 01/74] feat(fs): custom properties for files (#2407) --- assets | 2 +- inventory/file.go | 8 +- inventory/file_utils.go | 18 +++- inventory/setting.go | 22 +++++ inventory/types/types.go | 70 +++++++++----- pkg/filemanager/fs/uri.go | 16 +++- pkg/filemanager/manager/metadata.go | 141 ++++++++++++++++++++++++++-- pkg/setting/provider.go | 11 +++ service/basic/site.go | 3 + 9 files changed, 252 insertions(+), 39 deletions(-) diff --git a/assets b/assets index e9b91c4e..ada49fd2 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit e9b91c4e03654d5968f8a676a13fc4badf530b5d +Subproject commit ada49fd21d159563b21d29d7d3499a45c5ab1503 diff --git a/inventory/file.go b/inventory/file.go index b94d57f4..ac88eac6 100644 --- a/inventory/file.go +++ b/inventory/file.go @@ -59,11 +59,17 @@ type ( StoragePolicyID int } + MetadataFilter struct { + Key string + Value string + Exact bool + } + SearchFileParameters struct { Name []string // NameOperatorOr is true if the name should match any of the given names, false if all of them NameOperatorOr bool - Metadata map[string]string + Metadata []MetadataFilter Type *types.FileType UseFullText bool CaseFolding bool diff --git a/inventory/file_utils.go b/inventory/file_utils.go index f141c975..890d4191 100644 --- a/inventory/file_utils.go +++ b/inventory/file_utils.go @@ -16,6 +16,10 @@ import ( "github.com/samber/lo" ) +const ( + metadataExactMatchPrefix = "!exact:" +) + func (f *fileClient) searchQuery(q *ent.FileQuery, args *SearchFileParameters, parents []*ent.File, ownerId int) *ent.FileQuery { if len(parents) == 1 && parents[0] == nil { q = q.Where(file.OwnerID(ownerId)) @@ -69,13 +73,17 @@ func (f *fileClient) searchQuery(q *ent.FileQuery, args *SearchFileParameters, p } if len(args.Metadata) > 0 { - metaPredicates := lo.MapToSlice(args.Metadata, func(name string, value string) predicate.Metadata { - nameEq := metadata.NameEQ(value) - if name == "" { + metaPredicates := lo.Map(args.Metadata, func(item MetadataFilter, index int) predicate.Metadata { + if item.Exact { + return metadata.And(metadata.NameEQ(item.Key), metadata.ValueEQ(item.Value)) + } + + nameEq := metadata.NameEQ(item.Key) + if item.Value == "" { return nameEq } else { - valueContain := metadata.ValueContainsFold(value) - return metadata.And(metadata.NameEQ(name), valueContain) + valueContain := metadata.ValueContainsFold(item.Value) + return metadata.And(nameEq, valueContain) } }) metaPredicates = append(metaPredicates, metadata.IsPublic(true)) diff --git a/inventory/setting.go b/inventory/setting.go index d8dd0b59..cf5c2dde 100644 --- a/inventory/setting.go +++ b/inventory/setting.go @@ -324,6 +324,22 @@ var ( }, }, } + + defaultFileProps = []types.CustomProps{ + { + ID: "description", + Type: types.CustomPropsTypeText, + Name: "fileManager.description", + Icon: "fluent:slide-text-24-filled", + }, + { + ID: "rating", + Type: types.CustomPropsTypeRating, + Name: "fileManager.rating", + Icon: "fluent:data-bar-vertical-star-24-filled", + Max: 5, + }, + } ) var DefaultSettings = map[string]string{ @@ -516,4 +532,10 @@ func init() { } DefaultSettings["file_viewers"] = string(viewers) + + customProps, err := json.Marshal(defaultFileProps) + if err != nil { + panic(err) + } + DefaultSettings["custom_props"] = string(customProps) } diff --git a/inventory/types/types.go b/inventory/types/types.go index 8bc383a8..2aaf51da 100644 --- a/inventory/types/types.go +++ b/inventory/types/types.go @@ -173,7 +173,8 @@ type ( } ColumTypeProps struct { - MetadataKey string `json:"metadata_key,omitempty" binding:"max=255"` + MetadataKey string `json:"metadata_key,omitempty" binding:"max=255"` + CustomPropsID string `json:"custom_props_id,omitempty" binding:"max=255"` } ShareProps struct { @@ -278,26 +279,51 @@ const ( ViewerTypeCustom = "custom" ) -type Viewer struct { - ID string `json:"id"` - Type ViewerType `json:"type"` - DisplayName string `json:"display_name"` - Exts []string `json:"exts"` - Url string `json:"url,omitempty"` - Icon string `json:"icon,omitempty"` - WopiActions map[string]map[ViewerAction]string `json:"wopi_actions,omitempty"` - Props map[string]string `json:"props,omitempty"` - MaxSize int64 `json:"max_size,omitempty"` - Disabled bool `json:"disabled,omitempty"` - Templates []NewFileTemplate `json:"templates,omitempty"` - Platform string `json:"platform,omitempty"` -} +type ( + Viewer struct { + ID string `json:"id"` + Type ViewerType `json:"type"` + DisplayName string `json:"display_name"` + Exts []string `json:"exts"` + Url string `json:"url,omitempty"` + Icon string `json:"icon,omitempty"` + WopiActions map[string]map[ViewerAction]string `json:"wopi_actions,omitempty"` + Props map[string]string `json:"props,omitempty"` + MaxSize int64 `json:"max_size,omitempty"` + Disabled bool `json:"disabled,omitempty"` + Templates []NewFileTemplate `json:"templates,omitempty"` + Platform string `json:"platform,omitempty"` + } + ViewerGroup struct { + Viewers []Viewer `json:"viewers"` + } -type ViewerGroup struct { - Viewers []Viewer `json:"viewers"` -} + NewFileTemplate struct { + Ext string `json:"ext"` + DisplayName string `json:"display_name"` + } +) -type NewFileTemplate struct { - Ext string `json:"ext"` - DisplayName string `json:"display_name"` -} +type ( + CustomPropsType string + CustomProps struct { + ID string `json:"id"` + Name string `json:"name"` + Type CustomPropsType `json:"type"` + Max int `json:"max,omitempty"` + Min int `json:"min,omitempty"` + Default string `json:"default,omitempty"` + Options []string `json:"options,omitempty"` + Icon string `json:"icon,omitempty"` + } +) + +const ( + CustomPropsTypeText = "text" + CustomPropsTypeNumber = "number" + CustomPropsTypeBoolean = "boolean" + CustomPropsTypeSelect = "select" + CustomPropsTypeMultiSelect = "multi_select" + CustomPropsTypeLink = "link" + CustomPropsTypeRating = "rating" +) diff --git a/pkg/filemanager/fs/uri.go b/pkg/filemanager/fs/uri.go index 9e1e3a48..3f5bb147 100644 --- a/pkg/filemanager/fs/uri.go +++ b/pkg/filemanager/fs/uri.go @@ -25,6 +25,7 @@ const ( QuerySearchNameOpOr = "name_op_or" QuerySearchUseOr = "use_or" QuerySearchMetadataPrefix = "meta_" + QuerySearchMetadataExact = "exact_meta_" QuerySearchCaseFolding = "case_folding" QuerySearchType = "type" QuerySearchTypeCategory = "category" @@ -218,7 +219,7 @@ func (u *URI) FileSystem() constants.FileSystemType { func (u *URI) SearchParameters() *inventory.SearchFileParameters { q := u.U.Query() res := &inventory.SearchFileParameters{ - Metadata: make(map[string]string), + Metadata: make([]inventory.MetadataFilter, 0), } withSearch := false @@ -252,7 +253,18 @@ func (u *URI) SearchParameters() *inventory.SearchFileParameters { for k, v := range q { if strings.HasPrefix(k, QuerySearchMetadataPrefix) { - res.Metadata[strings.TrimPrefix(k, QuerySearchMetadataPrefix)] = v[0] + res.Metadata = append(res.Metadata, inventory.MetadataFilter{ + Key: strings.TrimPrefix(k, QuerySearchMetadataPrefix), + Value: v[0], + Exact: false, + }) + withSearch = true + } else if strings.HasPrefix(k, QuerySearchMetadataExact) { + res.Metadata = append(res.Metadata, inventory.MetadataFilter{ + Key: strings.TrimPrefix(k, QuerySearchMetadataExact), + Value: v[0], + Exact: true, + }) withSearch = true } } diff --git a/pkg/filemanager/manager/metadata.go b/pkg/filemanager/manager/metadata.go index b6ae4600..9f6cb397 100644 --- a/pkg/filemanager/manager/metadata.go +++ b/pkg/filemanager/manager/metadata.go @@ -5,14 +5,18 @@ import ( "crypto/sha1" "encoding/json" "fmt" + "strconv" + "strings" + "github.com/cloudreve/Cloudreve/v4/application/constants" "github.com/cloudreve/Cloudreve/v4/application/dependency" + "github.com/cloudreve/Cloudreve/v4/inventory/types" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs/dbfs" "github.com/cloudreve/Cloudreve/v4/pkg/hashid" "github.com/cloudreve/Cloudreve/v4/pkg/serializer" "github.com/go-playground/validator/v10" - "strings" + "github.com/samber/lo" ) type ( @@ -20,13 +24,14 @@ type ( ) const ( - wildcardMetadataKey = "*" - customizeMetadataSuffix = "customize" - tagMetadataSuffix = "tag" - iconColorMetadataKey = customizeMetadataSuffix + ":icon_color" - emojiIconMetadataKey = customizeMetadataSuffix + ":emoji" - shareOwnerMetadataKey = dbfs.MetadataSysPrefix + "shared_owner" - shareRedirectMetadataKey = dbfs.MetadataSysPrefix + "shared_redirect" + wildcardMetadataKey = "*" + customizeMetadataSuffix = "customize" + tagMetadataSuffix = "tag" + customPropsMetadataSuffix = "props" + iconColorMetadataKey = customizeMetadataSuffix + ":icon_color" + emojiIconMetadataKey = customizeMetadataSuffix + ":emoji" + shareOwnerMetadataKey = dbfs.MetadataSysPrefix + "shared_owner" + shareRedirectMetadataKey = dbfs.MetadataSysPrefix + "shared_redirect" ) var ( @@ -131,6 +136,126 @@ var ( return nil }, }, + customPropsMetadataSuffix: { + wildcardMetadataKey: func(ctx context.Context, m *manager, patch *fs.MetadataPatch) error { + if patch.Remove { + return nil + } + + customProps := m.settings.CustomProps(ctx) + propId := strings.TrimPrefix(patch.Key, customPropsMetadataSuffix+":") + for _, prop := range customProps { + if prop.ID == propId { + switch prop.Type { + case types.CustomPropsTypeText: + if prop.Min > 0 && prop.Min > len(patch.Value) { + return fmt.Errorf("value is too short") + } + if prop.Max > 0 && prop.Max < len(patch.Value) { + return fmt.Errorf("value is too long") + } + + return nil + case types.CustomPropsTypeRating: + if patch.Value == "" { + return nil + } + + // validate the value is a number + rating, err := strconv.Atoi(patch.Value) + if err != nil { + return fmt.Errorf("value is not a number") + } + + if prop.Max < rating { + return fmt.Errorf("value is too large") + } + + return nil + + case types.CustomPropsTypeNumber: + if patch.Value == "" { + return nil + } + + value, err := strconv.Atoi(patch.Value) + if err != nil { + return fmt.Errorf("value is not a number") + } + + if prop.Min > value { + return fmt.Errorf("value is too small") + } + if prop.Max > 0 && prop.Max < value { + return fmt.Errorf("value is too large") + } + + return nil + + case types.CustomPropsTypeBoolean: + if patch.Value == "" { + return nil + } + + if patch.Value != "true" && patch.Value != "false" { + return fmt.Errorf("value is not a boolean") + } + + return nil + case types.CustomPropsTypeSelect: + if patch.Value == "" { + return nil + } + + for _, option := range prop.Options { + if option == patch.Value { + return nil + } + } + + return fmt.Errorf("invalid option") + case types.CustomPropsTypeMultiSelect: + if patch.Value == "" { + return nil + } + + var values []string + if err := json.Unmarshal([]byte(patch.Value), &values); err != nil { + return fmt.Errorf("invalid multi select value: %w", err) + } + + // make sure all values are in the options + for _, value := range values { + if !lo.Contains(prop.Options, value) { + return fmt.Errorf("invalid option") + } + } + + return nil + + case types.CustomPropsTypeLink: + if patch.Value == "" { + return nil + } + + if prop.Min > 0 && len(patch.Value) < prop.Min { + return fmt.Errorf("value is too small") + } + + if prop.Max > 0 && len(patch.Value) > prop.Max { + return fmt.Errorf("value is too large") + } + + return nil + default: + return nil + } + } + } + + return fmt.Errorf("unkown custom props") + }, + }, } ) diff --git a/pkg/setting/provider.go b/pkg/setting/provider.go index 09aacbea..b234c3de 100644 --- a/pkg/setting/provider.go +++ b/pkg/setting/provider.go @@ -196,6 +196,8 @@ type ( LibRawThumbExts(ctx context.Context) []string // LibRawThumbPath returns the path of libraw executable. LibRawThumbPath(ctx context.Context) string + // CustomProps returns the custom props settings. + CustomProps(ctx context.Context) []types.CustomProps } UseFirstSiteUrlCtxKey = struct{} ) @@ -223,6 +225,15 @@ type ( } ) +func (s *settingProvider) CustomProps(ctx context.Context) []types.CustomProps { + raw := s.getString(ctx, "custom_props", "[]") + var props []types.CustomProps + if err := json.Unmarshal([]byte(raw), &props); err != nil { + return []types.CustomProps{} + } + return props +} + func (s *settingProvider) License(ctx context.Context) string { return s.getString(ctx, "license", "") } diff --git a/service/basic/site.go b/service/basic/site.go index aface24c..5af8fae8 100644 --- a/service/basic/site.go +++ b/service/basic/site.go @@ -45,6 +45,7 @@ type SiteConfig struct { MaxBatchSize int `json:"max_batch_size,omitempty"` ThumbnailWidth int `json:"thumbnail_width,omitempty"` ThumbnailHeight int `json:"thumbnail_height,omitempty"` + CustomProps []types.CustomProps `json:"custom_props,omitempty"` // App settings AppPromotion bool `json:"app_promotion,omitempty"` @@ -87,6 +88,7 @@ func (s *GetSettingService) GetSiteConfig(c *gin.Context) (*SiteConfig, error) { explorerSettings := settings.ExplorerFrontendSettings(c) mapSettings := settings.MapSetting(c) fileViewers := settings.FileViewers(c) + customProps := settings.CustomProps(c) maxBatchSize := settings.MaxBatchedFile(c) w, h := settings.ThumbSize(c) for i := range fileViewers { @@ -102,6 +104,7 @@ func (s *GetSettingService) GetSiteConfig(c *gin.Context) (*SiteConfig, error) { GoogleMapTileType: mapSettings.GoogleTileType, ThumbnailWidth: w, ThumbnailHeight: h, + CustomProps: customProps, }, nil case "emojis": emojis := settings.EmojiPresets(c) From ca57ca1ba061d1312964638d39fdba30ada40cdb Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Tue, 15 Jul 2025 10:41:13 +0800 Subject: [PATCH 02/74] feat(custom): custom sidebar items --- assets | 2 +- inventory/setting.go | 1 + pkg/setting/provider.go | 10 ++++++++++ pkg/setting/types.go | 6 ++++++ service/basic/site.go | 18 ++++++++++-------- 5 files changed, 28 insertions(+), 9 deletions(-) diff --git a/assets b/assets index ada49fd2..d4819eea 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit ada49fd21d159563b21d29d7d3499a45c5ab1503 +Subproject commit d4819eeaf4ed3d443165def69cb89267d31b81da diff --git a/inventory/setting.go b/inventory/setting.go index cf5c2dde..1a31699e 100644 --- a/inventory/setting.go +++ b/inventory/setting.go @@ -517,6 +517,7 @@ var DefaultSettings = map[string]string{ "qq_login": `0`, "qq_login_config": `{"direct_sign_in":false}`, "license": "", + "custom_nav_items": "[]", } func init() { diff --git a/pkg/setting/provider.go b/pkg/setting/provider.go index b234c3de..4b00c664 100644 --- a/pkg/setting/provider.go +++ b/pkg/setting/provider.go @@ -198,6 +198,8 @@ type ( LibRawThumbPath(ctx context.Context) string // CustomProps returns the custom props settings. CustomProps(ctx context.Context) []types.CustomProps + // CustomNavItems returns the custom nav items settings. + CustomNavItems(ctx context.Context) []CustomNavItem } UseFirstSiteUrlCtxKey = struct{} ) @@ -225,6 +227,14 @@ type ( } ) +func (s *settingProvider) CustomNavItems(ctx context.Context) []CustomNavItem { + raw := s.getString(ctx, "custom_nav_items", "[]") + var items []CustomNavItem + if err := json.Unmarshal([]byte(raw), &items); err != nil { + return []CustomNavItem{} + } + return items +} func (s *settingProvider) CustomProps(ctx context.Context) []types.CustomProps { raw := s.getString(ctx, "custom_props", "[]") var props []types.CustomProps diff --git a/pkg/setting/types.go b/pkg/setting/types.go index 6c389c23..759dea13 100644 --- a/pkg/setting/types.go +++ b/pkg/setting/types.go @@ -209,3 +209,9 @@ type AvatarProcess struct { MaxFileSize int64 `json:"max_file_size"` MaxWidth int `json:"max_width"` } + +type CustomNavItem struct { + Icon string `json:"icon"` + Name string `json:"name"` + URL string `json:"url"` +} diff --git a/service/basic/site.go b/service/basic/site.go index 5af8fae8..ec295d18 100644 --- a/service/basic/site.go +++ b/service/basic/site.go @@ -13,13 +13,14 @@ import ( // SiteConfig 站点全局设置序列 type SiteConfig struct { // Basic Section - InstanceID string `json:"instance_id,omitempty"` - SiteName string `json:"title,omitempty"` - Themes string `json:"themes,omitempty"` - DefaultTheme string `json:"default_theme,omitempty"` - User *user.User `json:"user,omitempty"` - Logo string `json:"logo,omitempty"` - LogoLight string `json:"logo_light,omitempty"` + InstanceID string `json:"instance_id,omitempty"` + SiteName string `json:"title,omitempty"` + Themes string `json:"themes,omitempty"` + DefaultTheme string `json:"default_theme,omitempty"` + User *user.User `json:"user,omitempty"` + Logo string `json:"logo,omitempty"` + LogoLight string `json:"logo_light,omitempty"` + CustomNavItems []setting.CustomNavItem `json:"custom_nav_items,omitempty"` // Login Section LoginCaptcha bool `json:"login_captcha,omitempty"` @@ -128,7 +129,7 @@ func (s *GetSettingService) GetSiteConfig(c *gin.Context) (*SiteConfig, error) { reCaptcha := settings.ReCaptcha(c) capCaptcha := settings.CapCaptcha(c) appSetting := settings.AppSetting(c) - + customNavItems := settings.CustomNavItems(c) return &SiteConfig{ InstanceID: siteBasic.ID, SiteName: siteBasic.Name, @@ -144,6 +145,7 @@ func (s *GetSettingService) GetSiteConfig(c *gin.Context) (*SiteConfig, error) { CapSiteKey: capCaptcha.SiteKey, CapAssetServer: capCaptcha.AssetServer, AppPromotion: appSetting.Promotion, + CustomNavItems: customNavItems, }, nil } From 000124f6c7603774c4e0691d596865315def81f8 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Tue, 15 Jul 2025 10:44:51 +0800 Subject: [PATCH 03/74] feat(ui): custom HTML content in predefined locations (#2621) --- assets | 2 +- inventory/setting.go | 3 +++ pkg/setting/provider.go | 9 +++++++++ pkg/setting/types.go | 6 ++++++ service/basic/site.go | 3 +++ 5 files changed, 22 insertions(+), 1 deletion(-) diff --git a/assets b/assets index d4819eea..aa80ca5b 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit d4819eeaf4ed3d443165def69cb89267d31b81da +Subproject commit aa80ca5bb2609d75fc9706d12edd5c6e9a0c76b7 diff --git a/inventory/setting.go b/inventory/setting.go index 1a31699e..4bbf3e9f 100644 --- a/inventory/setting.go +++ b/inventory/setting.go @@ -518,6 +518,9 @@ var DefaultSettings = map[string]string{ "qq_login_config": `{"direct_sign_in":false}`, "license": "", "custom_nav_items": "[]", + "headless_footer_html": "", + "headless_bottom_html": "", + "sidebar_bottom_html": "", } func init() { diff --git a/pkg/setting/provider.go b/pkg/setting/provider.go index 4b00c664..0fc9534e 100644 --- a/pkg/setting/provider.go +++ b/pkg/setting/provider.go @@ -200,6 +200,8 @@ type ( CustomProps(ctx context.Context) []types.CustomProps // CustomNavItems returns the custom nav items settings. CustomNavItems(ctx context.Context) []CustomNavItem + // CustomHTML returns the custom HTML settings. + CustomHTML(ctx context.Context) *CustomHTML } UseFirstSiteUrlCtxKey = struct{} ) @@ -227,6 +229,13 @@ type ( } ) +func (s *settingProvider) CustomHTML(ctx context.Context) *CustomHTML { + return &CustomHTML{ + HeadlessFooter: s.getString(ctx, "headless_footer_html", ""), + HeadlessBody: s.getString(ctx, "headless_bottom_html", ""), + SidebarBottom: s.getString(ctx, "sidebar_bottom_html", ""), + } +} func (s *settingProvider) CustomNavItems(ctx context.Context) []CustomNavItem { raw := s.getString(ctx, "custom_nav_items", "[]") var items []CustomNavItem diff --git a/pkg/setting/types.go b/pkg/setting/types.go index 759dea13..28344ab7 100644 --- a/pkg/setting/types.go +++ b/pkg/setting/types.go @@ -215,3 +215,9 @@ type CustomNavItem struct { Name string `json:"name"` URL string `json:"url"` } + +type CustomHTML struct { + HeadlessFooter string `json:"headless_footer,omitempty"` + HeadlessBody string `json:"headless_bottom,omitempty"` + SidebarBottom string `json:"sidebar_bottom,omitempty"` +} diff --git a/service/basic/site.go b/service/basic/site.go index ec295d18..287c0ed1 100644 --- a/service/basic/site.go +++ b/service/basic/site.go @@ -21,6 +21,7 @@ type SiteConfig struct { Logo string `json:"logo,omitempty"` LogoLight string `json:"logo_light,omitempty"` CustomNavItems []setting.CustomNavItem `json:"custom_nav_items,omitempty"` + CustomHTML *setting.CustomHTML `json:"custom_html,omitempty"` // Login Section LoginCaptcha bool `json:"login_captcha,omitempty"` @@ -130,6 +131,7 @@ func (s *GetSettingService) GetSiteConfig(c *gin.Context) (*SiteConfig, error) { capCaptcha := settings.CapCaptcha(c) appSetting := settings.AppSetting(c) customNavItems := settings.CustomNavItems(c) + customHTML := settings.CustomHTML(c) return &SiteConfig{ InstanceID: siteBasic.ID, SiteName: siteBasic.Name, @@ -146,6 +148,7 @@ func (s *GetSettingService) GetSiteConfig(c *gin.Context) (*SiteConfig, error) { CapAssetServer: capCaptcha.AssetServer, AppPromotion: appSetting.Promotion, CustomNavItems: customNavItems, + CustomHTML: customHTML, }, nil } From 195d68c535cda59397f3a78859fbccaf547908a3 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Tue, 15 Jul 2025 11:01:44 +0800 Subject: [PATCH 04/74] chore(docker): add LibRAW into docker image (#2645) --- Dockerfile | 5 +++-- docker-compose.yml | 2 +- go.sum | 3 --- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index f14a98de..139e276d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ FROM alpine:latest WORKDIR /cloudreve RUN apk update \ - && apk add --no-cache tzdata vips-tools ffmpeg libreoffice aria2 supervisor font-noto font-noto-cjk libheif\ + && apk add --no-cache tzdata vips-tools ffmpeg libreoffice aria2 supervisor font-noto font-noto-cjk libheif libraw-tools\ && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ && echo "Asia/Shanghai" > /etc/timezone \ && mkdir -p ./data/temp/aria2 \ @@ -13,7 +13,8 @@ ENV CR_ENABLE_ARIA2=1 \ CR_SETTING_DEFAULT_thumb_ffmpeg_enabled=1 \ CR_SETTING_DEFAULT_thumb_vips_enabled=1 \ CR_SETTING_DEFAULT_thumb_libreoffice_enabled=1 \ - CR_SETTING_DEFAULT_media_meta_ffprobe=1 + CR_SETTING_DEFAULT_media_meta_ffprobe=1 \ + CR_SETTING_DEFAULT_thumb_libraw_enabled=1 COPY .build/aria2.supervisor.conf .build/entrypoint.sh ./ COPY cloudreve ./cloudreve diff --git a/docker-compose.yml b/docker-compose.yml index 36fb1218..c21dcb64 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,5 @@ services: - pro: + cloudreve: image: cloudreve/cloudreve:latest container_name: cloudreve-backend depends_on: diff --git a/go.sum b/go.sum index 6020f0e7..d2603d1f 100644 --- a/go.sum +++ b/go.sum @@ -274,7 +274,6 @@ github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DP github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/etcd-io/gofail v0.0.0-20190801230047-ad7f989257ca/go.mod h1:49H/RkXP8pKaZy4h0d+NW16rSLhyVBt4o6VLJbmOqDE= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= @@ -660,8 +659,6 @@ github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaO github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc= From d19fc0e75c224ecb72692068947fe122cc1067a8 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Tue, 15 Jul 2025 12:00:39 +0800 Subject: [PATCH 05/74] feat(remote download): sanitize file names with special characters (#2648) --- assets | 2 +- pkg/filemanager/workflows/remote_download.go | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/assets b/assets index aa80ca5b..3a6a22bb 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit aa80ca5bb2609d75fc9706d12edd5c6e9a0c76b7 +Subproject commit 3a6a22bb458783fcfd32f3b35f5fe6afc5414e25 diff --git a/pkg/filemanager/workflows/remote_download.go b/pkg/filemanager/workflows/remote_download.go index e884391d..efe9bfdc 100644 --- a/pkg/filemanager/workflows/remote_download.go +++ b/pkg/filemanager/workflows/remote_download.go @@ -8,6 +8,7 @@ import ( "os" "path" "path/filepath" + "strings" "sync" "sync/atomic" "time" @@ -318,7 +319,7 @@ func (m *RemoteDownloadTask) slaveTransfer(ctx context.Context, dep dependency.D continue } - dst := dstUri.JoinRaw(f.Name) + dst := dstUri.JoinRaw(sanitizeFileName(f.Name)) src := path.Join(m.state.Status.SavePath, f.Name) payload.Files = append(payload.Files, SlaveUploadEntity{ Src: src, @@ -437,9 +438,10 @@ func (m *RemoteDownloadTask) masterTransfer(ctx context.Context, dep dependency. wg.Done() }() - dst := dstUri.JoinRaw(file.Name) + sanitizedName := sanitizeFileName(file.Name) + dst := dstUri.JoinRaw(sanitizedName) src := filepath.FromSlash(path.Join(m.state.Status.SavePath, file.Name)) - m.l.Info("Uploading file %s to %s...", src, file.Name, dst) + m.l.Info("Uploading file %s to %s...", src, sanitizedName, dst) progressKey := fmt.Sprintf("%s%d", ProgressTypeUploadSinglePrefix, workerId) m.Lock() @@ -538,7 +540,7 @@ func (m *RemoteDownloadTask) validateFiles(ctx context.Context, dep dependency.D validateArgs := lo.Map(selectedFiles, func(f downloader.TaskFile, _ int) fs.PreValidateFile { return fs.PreValidateFile{ - Name: f.Name, + Name: sanitizeFileName(f.Name), Size: f.Size, OmitName: f.Name == "", } @@ -637,3 +639,8 @@ func (m *RemoteDownloadTask) Progress(ctx context.Context) queue.Progresses { } return m.progress } + +func sanitizeFileName(name string) string { + r := strings.NewReplacer("\\", "_", ":", "_", "*", "_", "?", "_", "\"", "_", "<", "_", ">", "_", "|", "_") + return r.Replace(name) +} From e96b5956226ad348a83b0fe0da30c48c941a0573 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Tue, 15 Jul 2025 13:22:04 +0800 Subject: [PATCH 06/74] feat(direct link): add option to get direct link with download enforced (#2651) --- assets | 2 +- pkg/filemanager/manager/entity.go | 4 ++-- routers/controllers/file.go | 14 ++++++++------ routers/router.go | 5 ++++- service/explorer/file.go | 3 ++- 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/assets b/assets index 3a6a22bb..e47a708f 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 3a6a22bb458783fcfd32f3b35f5fe6afc5414e25 +Subproject commit e47a708f727bf5243512ddd6d9c6eabd5bf61751 diff --git a/pkg/filemanager/manager/entity.go b/pkg/filemanager/manager/entity.go index a8954bca..bf88ef70 100644 --- a/pkg/filemanager/manager/entity.go +++ b/pkg/filemanager/manager/entity.go @@ -168,7 +168,7 @@ func (m *manager) GetUrlForRedirectedDirectLink(ctx context.Context, dl *ent.Dir ) // Try to read from cache. - cacheKey := entityUrlCacheKey(primaryEntity.ID(), int64(dl.Speed), dl.Name, false, + cacheKey := entityUrlCacheKey(primaryEntity.ID(), int64(dl.Speed), dl.Name, o.IsDownload, m.settings.SiteURL(ctx).String()) if cached, ok := m.kv.Get(cacheKey); ok { cachedItem := cached.(EntityUrlCache) @@ -185,7 +185,7 @@ func (m *manager) GetUrlForRedirectedDirectLink(ctx context.Context, dl *ent.Dir m.l, m.config, m.dep.MimeDetector(ctx)) downloadUrl, err := source.Url(ctx, entitysource.WithExpire(o.Expire), - entitysource.WithDownload(false), + entitysource.WithDownload(o.IsDownload), entitysource.WithSpeedLimit(int64(dl.Speed)), entitysource.WithDisplayName(dl.Name), ) diff --git a/routers/controllers/file.go b/routers/controllers/file.go index dab661ad..e09a95f1 100644 --- a/routers/controllers/file.go +++ b/routers/controllers/file.go @@ -86,12 +86,14 @@ func ExtractArchive(c *gin.Context) { } // AnonymousPermLink 文件中转后的永久直链接 -func AnonymousPermLink(c *gin.Context) { - name := c.Param("name") - if err := explorer.RedirectDirectLink(c, name); err != nil { - c.JSON(404, serializer.Err(c, err)) - c.Abort() - return +func AnonymousPermLink(download bool) gin.HandlerFunc { + return func(c *gin.Context) { + name := c.Param("name") + if err := explorer.RedirectDirectLink(c, name, download); err != nil { + c.JSON(404, serializer.Err(c, err)) + c.Abort() + return + } } } diff --git a/routers/router.go b/routers/router.go index f5897207..27b39ccc 100644 --- a/routers/router.go +++ b/routers/router.go @@ -245,7 +245,10 @@ func initMasterRouter(dep dependency.Dep) *gin.Engine { { source.GET(":id/:name", middleware.HashID(hashid.SourceLinkID), - controllers.AnonymousPermLink) + controllers.AnonymousPermLink(false)) + source.GET("d/:id/:name", + middleware.HashID(hashid.SourceLinkID), + controllers.AnonymousPermLink(true)) } shareShort := r.Group("s") diff --git a/service/explorer/file.go b/service/explorer/file.go index b2075828..02ca5642 100644 --- a/service/explorer/file.go +++ b/service/explorer/file.go @@ -659,7 +659,7 @@ func (s *GetFileInfoService) Get(c *gin.Context) (*FileResponse, error) { return BuildFileResponse(c, user, file, dep.HashIDEncoder(), nil), nil } -func RedirectDirectLink(c *gin.Context, name string) error { +func RedirectDirectLink(c *gin.Context, name string, download bool) error { dep := dependency.FromContext(c) settings := dep.SettingProvider() @@ -680,6 +680,7 @@ func RedirectDirectLink(c *gin.Context, name string) error { expire := time.Now().Add(settings.EntityUrlValidDuration(c)) res, earliestExpire, err := m.GetUrlForRedirectedDirectLink(c, dl, fs.WithUrlExpire(&expire), + fs.WithIsDownload(download), ) if err != nil { return err From 15762cb393b5eb651e57237e18f9d968443fd4af Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Tue, 15 Jul 2025 13:51:23 +0800 Subject: [PATCH 07/74] =?UTF-8?q?feat(thumb):=20support=20output=20webp=20?= =?UTF-8?q?thumbnails=20for=20vips=20generator=20=EF=BC=88#2657=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets | 2 +- pkg/thumb/ffmpeg.go | 2 +- pkg/thumb/libreoffice.go | 5 ++--- pkg/thumb/vips.go | 4 ++-- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/assets b/assets index e47a708f..a827cc60 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit e47a708f727bf5243512ddd6d9c6eabd5bf61751 +Subproject commit a827cc60f232ed8fb95afa6d3d434ef1c41ed251 diff --git a/pkg/thumb/ffmpeg.go b/pkg/thumb/ffmpeg.go index c94a8297..c89371cb 100644 --- a/pkg/thumb/ffmpeg.go +++ b/pkg/thumb/ffmpeg.go @@ -41,7 +41,7 @@ func (f *FfmpegGenerator) Generate(ctx context.Context, es entitysource.EntitySo tempOutputPath := filepath.Join( util.DataPath(f.settings.TempPath(ctx)), thumbTempFolder, - fmt.Sprintf("thumb_%s.%s", uuid.Must(uuid.NewV4()).String(), f.settings.ThumbEncode(ctx).Format), + fmt.Sprintf("thumb_%s.png", uuid.Must(uuid.NewV4()).String()), ) if err := util.CreatNestedFolder(filepath.Dir(tempOutputPath)); err != nil { diff --git a/pkg/thumb/libreoffice.go b/pkg/thumb/libreoffice.go index dfdfe18f..e0626269 100644 --- a/pkg/thumb/libreoffice.go +++ b/pkg/thumb/libreoffice.go @@ -69,10 +69,9 @@ func (l *LibreOfficeGenerator) Generate(ctx context.Context, es entitysource.Ent } // Convert the document to an image - encode := l.settings.ThumbEncode(ctx) cmd := exec.CommandContext(ctx, l.settings.LibreOfficePath(ctx), "--headless", "--nologo", "--nofirststartwizard", "--invisible", "--norestore", "--convert-to", - encode.Format, "--outdir", tempOutputPath, tempInputPath) + "png", "--outdir", tempOutputPath, tempInputPath) // Redirect IO var stdErr bytes.Buffer @@ -86,7 +85,7 @@ func (l *LibreOfficeGenerator) Generate(ctx context.Context, es entitysource.Ent return &Result{ Path: filepath.Join( tempOutputPath, - strings.TrimSuffix(filepath.Base(tempInputPath), filepath.Ext(tempInputPath))+"."+encode.Format, + strings.TrimSuffix(filepath.Base(tempInputPath), filepath.Ext(tempInputPath))+".png", ), Continue: true, Cleanup: []func(){func() { _ = os.RemoveAll(tempOutputPath) }}, diff --git a/pkg/thumb/vips.go b/pkg/thumb/vips.go index 2e1001ef..ee854d61 100644 --- a/pkg/thumb/vips.go +++ b/pkg/thumb/vips.go @@ -38,8 +38,8 @@ func (v *VipsGenerator) Generate(ctx context.Context, es entitysource.EntitySour outputOpt := ".png" encode := v.settings.ThumbEncode(ctx) - if encode.Format == "jpg" { - outputOpt = fmt.Sprintf(".jpg[Q=%d]", encode.Quality) + if encode.Format == "jpg" || encode.Format == "webp" { + outputOpt = fmt.Sprintf(".%s[Q=%d]", encode.Format, encode.Quality) } input := "[descriptor=0]" From 1cdccf5fc95aa7be432666e7f93ae95e02b61297 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Tue, 15 Jul 2025 14:11:42 +0800 Subject: [PATCH 08/74] feat(thumb): adding option to define custom input argument for FFmpeg (#2657) --- assets | 2 +- inventory/setting.go | 1 + pkg/setting/provider.go | 6 ++++++ pkg/thumb/ffmpeg.go | 20 +++++++++++++++++--- 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/assets b/assets index a827cc60..0b49582a 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit a827cc60f232ed8fb95afa6d3d434ef1c41ed251 +Subproject commit 0b49582a07eccfd63896dd18f7d944f7e96ed47d diff --git a/inventory/setting.go b/inventory/setting.go index 4bbf3e9f..fa09f590 100644 --- a/inventory/setting.go +++ b/inventory/setting.go @@ -430,6 +430,7 @@ var DefaultSettings = map[string]string{ "thumb_ffmpeg_max_size": "10737418240", // 10 GB "thumb_ffmpeg_exts": "3g2,3gp,asf,asx,avi,divx,flv,m2ts,m2v,m4v,mkv,mov,mp4,mpeg,mpg,mts,mxf,ogv,rm,swf,webm,wmv", "thumb_ffmpeg_seek": "00:00:01.00", + "thumb_ffmpeg_extra_args": "-hwaccel auto", "thumb_libreoffice_path": "soffice", "thumb_libreoffice_max_size": "78643200", // 75 MB "thumb_libreoffice_enabled": "0", diff --git a/pkg/setting/provider.go b/pkg/setting/provider.go index 0fc9534e..1959433a 100644 --- a/pkg/setting/provider.go +++ b/pkg/setting/provider.go @@ -202,6 +202,8 @@ type ( CustomNavItems(ctx context.Context) []CustomNavItem // CustomHTML returns the custom HTML settings. CustomHTML(ctx context.Context) *CustomHTML + // FFMpegExtraArgs returns the extra arguments of ffmpeg thumb generator. + FFMpegExtraArgs(ctx context.Context) string } UseFirstSiteUrlCtxKey = struct{} ) @@ -406,6 +408,10 @@ func (s *settingProvider) FFMpegThumbSeek(ctx context.Context) string { return s.getString(ctx, "thumb_ffmpeg_seek", "00:00:01.00") } +func (s *settingProvider) FFMpegExtraArgs(ctx context.Context) string { + return s.getString(ctx, "thumb_ffmpeg_extra_args", "") +} + func (s *settingProvider) FFMpegThumbMaxSize(ctx context.Context) int64 { return s.getInt64(ctx, "thumb_ffmpeg_max_size", 10737418240) } diff --git a/pkg/thumb/ffmpeg.go b/pkg/thumb/ffmpeg.go index c89371cb..f016b3af 100644 --- a/pkg/thumb/ffmpeg.go +++ b/pkg/thumb/ffmpeg.go @@ -6,6 +6,7 @@ import ( "fmt" "os/exec" "path/filepath" + "strings" "time" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver" @@ -64,9 +65,22 @@ func (f *FfmpegGenerator) Generate(ctx context.Context, es entitysource.EntitySo // Invoke ffmpeg w, h := f.settings.ThumbSize(ctx) scaleOpt := fmt.Sprintf("scale=%d:%d:force_original_aspect_ratio=decrease", w, h) - cmd := exec.CommandContext(ctx, - f.settings.FFMpegPath(ctx), "-ss", f.settings.FFMpegThumbSeek(ctx), "-i", input, - "-vf", scaleOpt, "-vframes", "1", tempOutputPath) + args := []string{ + "-ss", f.settings.FFMpegThumbSeek(ctx), + } + + extraArgs := f.settings.FFMpegExtraArgs(ctx) + if extraArgs != "" { + args = append(args, strings.Split(extraArgs, " ")...) + } + + args = append(args, []string{ + "-i", input, + "-vf", scaleOpt, + "-vframes", "1", + tempOutputPath, + }...) + cmd := exec.CommandContext(ctx, f.settings.FFMpegPath(ctx), args...) // Redirect IO var stdErr bytes.Buffer From 488f32512d3b1b602a58644ed41ff3e1ca3c5d09 Mon Sep 17 00:00:00 2001 From: omiku <47567246+omiku@users.noreply.github.com> Date: Mon, 21 Jul 2025 16:08:22 +0800 Subject: [PATCH 09/74] Add Kingsoft Cloud object storage policy to solve the cross-domain and friendly file name incompatibility problem of s3 compatible storage policy. (#2665) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 新增金山云对象存储策略,解决s3兼容存储策略的跨域及友好文件名不兼容问题 * fix bug&add download Expire time args * Handling of expiration times when they may be empty --- go.mod | 1 + go.sum | 2 + inventory/types/types.go | 1 + pkg/filemanager/driver/ks3/ks3.go | 551 ++++++++++++++++++++++++++++++ pkg/filemanager/manager/fs.go | 3 + routers/router.go | 6 + service/admin/policy.go | 12 + 7 files changed, 576 insertions(+) create mode 100644 pkg/filemanager/driver/ks3/ks3.go diff --git a/go.mod b/go.mod index 895da74e..ab2486fe 100644 --- a/go.mod +++ b/go.mod @@ -38,6 +38,7 @@ require ( github.com/jinzhu/gorm v1.9.11 github.com/jpillora/backoff v1.0.0 github.com/juju/ratelimit v1.0.1 + github.com/ks3sdklib/aws-sdk-go v1.6.2 github.com/lib/pq v1.10.9 github.com/mholt/archiver/v4 v4.0.0-alpha.6 github.com/mojocn/base64Captcha v0.0.0-20190801020520-752b1cd608b2 diff --git a/go.sum b/go.sum index d2603d1f..509d1aa7 100644 --- a/go.sum +++ b/go.sum @@ -635,6 +635,8 @@ github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/ks3sdklib/aws-sdk-go v1.6.2 h1:nxtaaU3hDD5x6gmoxs/qijSJqZrjFapYYuTiVCEgobA= +github.com/ks3sdklib/aws-sdk-go v1.6.2/go.mod h1:jGcsV0dJgMmStAyqjkKVUu6F167pAXYZAS3LqoZMmtM= github.com/kylelemons/go-gypsy v1.0.0/go.mod h1:chkXM0zjdpXOiqkCW1XcCHDfjfk14PH2KKkQWxfJUcU= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= diff --git a/inventory/types/types.go b/inventory/types/types.go index 2aaf51da..144e77eb 100644 --- a/inventory/types/types.go +++ b/inventory/types/types.go @@ -255,6 +255,7 @@ const ( PolicyTypeOss = "oss" PolicyTypeCos = "cos" PolicyTypeS3 = "s3" + PolicyTypeKs3 = "ks3" PolicyTypeOd = "onedrive" PolicyTypeRemote = "remote" PolicyTypeObs = "obs" diff --git a/pkg/filemanager/driver/ks3/ks3.go b/pkg/filemanager/driver/ks3/ks3.go new file mode 100644 index 00000000..e1ffdf40 --- /dev/null +++ b/pkg/filemanager/driver/ks3/ks3.go @@ -0,0 +1,551 @@ +package ks3 + +import ( + "context" + "errors" + "fmt" + + "github.com/aws/aws-sdk-go/aws/request" + + "io" + "net/url" + "os" + "path" + "path/filepath" + "strings" + "time" + + "strconv" + + "github.com/cloudreve/Cloudreve/v4/ent" + "github.com/cloudreve/Cloudreve/v4/inventory/types" + "github.com/cloudreve/Cloudreve/v4/pkg/boolset" + "github.com/cloudreve/Cloudreve/v4/pkg/cluster/routes" + "github.com/cloudreve/Cloudreve/v4/pkg/conf" + "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/chunk" + "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/chunk/backoff" + "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver" + "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs" + "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs/mime" + "github.com/cloudreve/Cloudreve/v4/pkg/logging" + "github.com/cloudreve/Cloudreve/v4/pkg/serializer" + "github.com/cloudreve/Cloudreve/v4/pkg/setting" + "github.com/ks3sdklib/aws-sdk-go/aws/awserr" + "github.com/ks3sdklib/aws-sdk-go/service/s3/s3manager" + "github.com/samber/lo" + + "github.com/ks3sdklib/aws-sdk-go/aws" + "github.com/ks3sdklib/aws-sdk-go/aws/credentials" + + "github.com/ks3sdklib/aws-sdk-go/service/s3" +) + +// Driver KS3 compatible driver +type Driver struct { + policy *ent.StoragePolicy + chunkSize int64 + + settings setting.Provider + l logging.Logger + config conf.ConfigProvider + mime mime.MimeDetector + + sess *aws.Config + svc *s3.S3 +} + +// UploadPolicy KS3上传策略 +type UploadPolicy struct { + Expiration string `json:"expiration"` + Conditions []interface{} `json:"conditions"` +} + +type Session struct { + Config *aws.Config + Handlers request.Handlers +} + +// MetaData 文件信息 +type MetaData struct { + Size int64 + Etag string +} + +var ( + features = &boolset.BooleanSet{} +) + +func init() { + boolset.Sets(map[driver.HandlerCapability]bool{ + driver.HandlerCapabilityUploadSentinelRequired: true, + }, features) +} + +func Int64(v int64) *int64 { + return &v +} + +func New(ctx context.Context, policy *ent.StoragePolicy, settings setting.Provider, + config conf.ConfigProvider, l logging.Logger, mime mime.MimeDetector) (*Driver, error) { + chunkSize := policy.Settings.ChunkSize + if policy.Settings.ChunkSize == 0 { + chunkSize = 25 << 20 // 25 MB + } + + driver := &Driver{ + policy: policy, + settings: settings, + chunkSize: chunkSize, + config: config, + l: l, + mime: mime, + } + + sess := aws.Config{ + Credentials: credentials.NewStaticCredentials(policy.AccessKey, policy.SecretKey, ""), + Endpoint: policy.Server, + Region: policy.Settings.Region, + S3ForcePathStyle: policy.Settings.S3ForcePathStyle, + } + driver.sess = &sess + driver.svc = s3.New(&sess) + + return driver, nil +} + +// List 列出给定路径下的文件 +func (handler *Driver) List(ctx context.Context, base string, onProgress driver.ListProgressFunc, recursive bool) ([]fs.PhysicalObject, error) { + // 初始化列目录参数 + base = strings.TrimPrefix(base, "/") + if base != "" { + base += "/" + } + + opt := &s3.ListObjectsInput{ + Bucket: &handler.policy.BucketName, + Prefix: &base, + MaxKeys: Int64(1000), + } + + // 是否为递归列出 + if !recursive { + opt.Delimiter = aws.String("/") + } + + var ( + objects []*s3.Object + commons []*s3.CommonPrefix + ) + + for { + res, err := handler.svc.ListObjectsWithContext(ctx, opt) + if err != nil { + return nil, err + } + objects = append(objects, res.Contents...) + commons = append(commons, res.CommonPrefixes...) + + // 如果本次未列取完,则继续使用marker获取结果 + if *res.IsTruncated { + opt.Marker = res.NextMarker + } else { + break + } + } + + // 处理列取结果 + res := make([]fs.PhysicalObject, 0, len(objects)+len(commons)) + + // 处理目录 + for _, object := range commons { + rel, err := filepath.Rel(*opt.Prefix, *object.Prefix) + if err != nil { + continue + } + res = append(res, fs.PhysicalObject{ + Name: path.Base(*object.Prefix), + RelativePath: filepath.ToSlash(rel), + Size: 0, + IsDir: true, + LastModify: time.Now(), + }) + } + onProgress(len(commons)) + + // 处理文件 + for _, object := range objects { + rel, err := filepath.Rel(*opt.Prefix, *object.Key) + if err != nil { + continue + } + res = append(res, fs.PhysicalObject{ + Name: path.Base(*object.Key), + Source: *object.Key, + RelativePath: filepath.ToSlash(rel), + Size: *object.Size, + IsDir: false, + LastModify: time.Now(), + }) + } + onProgress(len(objects)) + + return res, nil + +} + +// Open 打开文件 +func (handler *Driver) Open(ctx context.Context, path string) (*os.File, error) { + return nil, errors.New("not implemented") +} + +// Put 将文件流保存到指定目录 +func (handler *Driver) Put(ctx context.Context, file *fs.UploadRequest) error { + defer file.Close() + + // 是否允许覆盖 + overwrite := file.Mode&fs.ModeOverwrite == fs.ModeOverwrite + if !overwrite { + // Check for duplicated file + if _, err := handler.Meta(ctx, file.Props.SavePath); err == nil { + return fs.ErrFileExisted + } + } + + // 初始化配置 + uploader := s3manager.NewUploader(&s3manager.UploadOptions{ + S3: handler.svc, // S3Client实例,必填 + PartSize: handler.chunkSize, // 分块大小,默认为5MB,非必填 + }) + + mimeType := file.Props.MimeType + if mimeType == "" { + handler.mime.TypeByName(file.Props.Uri.Name()) + } + + _, err := uploader.UploadWithContext(ctx, &s3manager.UploadInput{ + Bucket: &handler.policy.BucketName, + Key: &file.Props.SavePath, + Body: io.LimitReader(file, file.Props.Size), + ContentType: aws.String(mimeType), + }) + + if err != nil { + return err + } + + return nil +} + +// Delete 删除文件 +func (handler *Driver) Delete(ctx context.Context, files ...string) ([]string, error) { + failed := make([]string, 0, len(files)) + batchSize := handler.policy.Settings.S3DeleteBatchSize + if batchSize == 0 { + // https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObjects.html + // The request can contain a list of up to 1000 keys that you want to delete. + batchSize = 1000 + } + + var lastErr error + + groups := lo.Chunk(files, batchSize) + for _, group := range groups { + if len(group) == 1 { + // Invoke single file delete API + _, err := handler.svc.DeleteObjectWithContext(ctx, &s3.DeleteObjectInput{ + Bucket: &handler.policy.BucketName, + Key: &group[0], + }) + + if err != nil { + if aerr, ok := err.(awserr.Error); ok { + // Ignore NoSuchKey error + if aerr.Code() == s3.ErrCodeNoSuchKey { + continue + } + } + failed = append(failed, group[0]) + lastErr = err + } + } else { + // Invoke batch delete API + res, err := handler.svc.DeleteObjects( + &s3.DeleteObjectsInput{ + Bucket: &handler.policy.BucketName, + Delete: &s3.Delete{ + Objects: lo.Map(group, func(s string, i int) *s3.ObjectIdentifier { + return &s3.ObjectIdentifier{Key: &s} + }), + }, + }) + + if err != nil { + failed = append(failed, group...) + lastErr = err + continue + } + + for _, v := range res.Errors { + handler.l.Debug("Failed to delete file: %s, Code:%s, Message:%s", v.Key, v.Code, v.Key) + failed = append(failed, *v.Key) + } + } + } + + return failed, lastErr + +} + +// Thumb 获取缩略图URL +func (handler *Driver) Thumb(ctx context.Context, expire *time.Time, ext string, e fs.Entity) (string, error) { + return "", errors.New("not implemented") +} + +// Source 获取文件外链 +func (handler *Driver) Source(ctx context.Context, e fs.Entity, args *driver.GetSourceArgs) (string, error) { + var contentDescription *string + if args.IsDownload { + encodedFilename := url.PathEscape(args.DisplayName) + contentDescription = aws.String(fmt.Sprintf(`attachment; filename="%s"`, encodedFilename)) + } + + // 确保过期时间不小于 0 ,如果小于则设置为 7 天 + var ttl int64 + if args.Expire != nil { + ttl = int64(time.Until(*args.Expire).Seconds()) + } else { + ttl = 604800 + } + + downloadUrl, err := handler.svc.GeneratePresignedUrl(&s3.GeneratePresignedUrlInput{ + HTTPMethod: s3.GET, // 请求方法 + Bucket: &handler.policy.BucketName, // 存储空间名称 + Key: aws.String(e.Source()), // 对象的key + Expires: ttl, // 过期时间,转换为秒数 + ResponseContentDisposition: contentDescription, // 设置响应头部 Content-Disposition + }) + + if err != nil { + return "", err + } + + // 将最终生成的签名URL域名换成用户自定义的加速域名(如果有) + finalURL, err := url.Parse(downloadUrl) + if err != nil { + return "", err + } + + // 公有空间替换掉Key及不支持的头 + if !handler.policy.IsPrivate { + finalURL.RawQuery = "" + } + + return finalURL.String(), nil +} + +// Token 获取上传凭证 +func (handler *Driver) Token(ctx context.Context, uploadSession *fs.UploadSession, file *fs.UploadRequest) (*fs.UploadCredential, error) { + // Check for duplicated file + if _, err := handler.Meta(ctx, file.Props.SavePath); err == nil { + return nil, fs.ErrFileExisted + } + + // 生成回调地址 + siteURL := handler.settings.SiteURL(setting.UseFirstSiteUrl(ctx)) + // 在从机端创建上传会话 + uploadSession.ChunkSize = handler.chunkSize + uploadSession.Callback = routes.MasterSlaveCallbackUrl(siteURL, types.PolicyTypeKs3, uploadSession.Props.UploadSessionID, uploadSession.CallbackSecret).String() + + mimeType := file.Props.MimeType + if mimeType == "" { + handler.mime.TypeByName(file.Props.Uri.Name()) + } + + // 创建分片上传 + res, err := handler.svc.CreateMultipartUploadWithContext(ctx, &s3.CreateMultipartUploadInput{ + Bucket: &handler.policy.BucketName, + Key: &uploadSession.Props.SavePath, + Expires: &uploadSession.Props.ExpireAt, + ContentType: aws.String(mimeType), + }) + if err != nil { + return nil, fmt.Errorf("failed to create multipart upload: %w", err) + } + + uploadSession.UploadID = *res.UploadID + + // 为每个分片签名上传 URL + chunks := chunk.NewChunkGroup(file, handler.chunkSize, &backoff.ConstantBackoff{}, false, handler.l, "") + urls := make([]string, chunks.Num()) + for chunks.Next() { + err := chunks.Process(func(c *chunk.ChunkGroup, chunk io.Reader) error { + // 计算过期时间(秒) + expireSeconds := int(time.Until(uploadSession.Props.ExpireAt).Seconds()) + partNumber := c.Index() + 1 + + // 生成预签名URL + signedURL, err := handler.svc.GeneratePresignedUrl(&s3.GeneratePresignedUrlInput{ + HTTPMethod: s3.PUT, + Bucket: &handler.policy.BucketName, + Key: &uploadSession.Props.SavePath, + Expires: int64(expireSeconds), + Parameters: map[string]*string{ + "partNumber": aws.String(strconv.Itoa(partNumber)), + "uploadId": res.UploadID, + }, + ContentType: aws.String("application/octet-stream"), + }) + if err != nil { + return fmt.Errorf("failed to generate presigned upload url for chunk %d: %w", partNumber, err) + } + urls[c.Index()] = signedURL + return nil + }) + if err != nil { + return nil, err + } + } + + // 签名完成分片上传的请求URL + expireSeconds := int(time.Until(uploadSession.Props.ExpireAt).Seconds()) + signedURL, err := handler.svc.GeneratePresignedUrl(&s3.GeneratePresignedUrlInput{ + HTTPMethod: s3.POST, + Bucket: &handler.policy.BucketName, + Key: &file.Props.SavePath, + Expires: int64(expireSeconds), + Parameters: map[string]*string{ + "uploadId": res.UploadID, + }, + ContentType: aws.String("application/octet-stream"), + }) + if err != nil { + return nil, err + } + + // 生成上传凭证 + return &fs.UploadCredential{ + UploadID: *res.UploadID, + UploadURLs: urls, + CompleteURL: signedURL, + SessionID: uploadSession.Props.UploadSessionID, + ChunkSize: handler.chunkSize, + }, nil +} + +// CancelToken 取消上传凭证 +func (handler *Driver) CancelToken(ctx context.Context, uploadSession *fs.UploadSession) error { + _, err := handler.svc.AbortMultipartUploadWithContext(ctx, &s3.AbortMultipartUploadInput{ + UploadID: &uploadSession.UploadID, + Bucket: &handler.policy.BucketName, + Key: &uploadSession.Props.SavePath, + }) + return err +} + +// cancelUpload 取消分片上传 +func (handler *Driver) cancelUpload(key, id *string) { + if _, err := handler.svc.AbortMultipartUpload(&s3.AbortMultipartUploadInput{ + Bucket: &handler.policy.BucketName, + UploadID: id, + Key: key, + }); err != nil { + handler.l.Warning("failed to abort multipart upload: %s", err) + } +} + +// Capabilities 获取存储能力 +func (handler *Driver) Capabilities() *driver.Capabilities { + return &driver.Capabilities{ + StaticFeatures: features, + MediaMetaProxy: handler.policy.Settings.MediaMetaGeneratorProxy, + ThumbProxy: handler.policy.Settings.ThumbGeneratorProxy, + MaxSourceExpire: time.Duration(604800) * time.Second, + } +} + +// MediaMeta 获取媒体元信息 +func (handler *Driver) MediaMeta(ctx context.Context, path, ext string) ([]driver.MediaMeta, error) { + return nil, errors.New("not implemented") +} + +// LocalPath 获取本地路径 +func (handler *Driver) LocalPath(ctx context.Context, path string) string { + return "" +} + +// CompleteUpload 完成上传 +func (handler *Driver) CompleteUpload(ctx context.Context, session *fs.UploadSession) error { + if session.SentinelTaskID == 0 { + return nil + } + + // Make sure uploaded file size is correct + res, err := handler.Meta(ctx, session.Props.SavePath) + if err != nil { + return fmt.Errorf("failed to get uploaded file size: %w", err) + } + + if res.Size != session.Props.Size { + return serializer.NewError( + serializer.CodeMetaMismatch, + fmt.Sprintf("File size not match, expected: %d, actual: %d", session.Props.Size, res.Size), + nil, + ) + } + return nil +} + +// Meta 获取文件元信息 +func (handler *Driver) Meta(ctx context.Context, path string) (*MetaData, error) { + res, err := handler.svc.HeadObjectWithContext(ctx, + &s3.HeadObjectInput{ + Bucket: &handler.policy.BucketName, + Key: &path, + }) + + if err != nil { + return nil, err + } + + return &MetaData{ + Size: *res.ContentLength, + Etag: *res.ETag, + }, nil + +} + +// CORS 设置CORS规则 +func (handler *Driver) CORS() error { + rule := s3.CORSRule{ + AllowedMethod: []string{ + "GET", + "POST", + "PUT", + "DELETE", + "HEAD", + }, + AllowedOrigin: []string{"*"}, + AllowedHeader: []string{"*"}, + ExposeHeader: []string{"ETag"}, + MaxAgeSeconds: 3600, + } + + _, err := handler.svc.PutBucketCORS(&s3.PutBucketCORSInput{ + Bucket: &handler.policy.BucketName, + CORSConfiguration: &s3.CORSConfiguration{ + Rules: []*s3.CORSRule{&rule}, + }, + }) + + return err +} + +// Reader 读取器 +type Reader struct { + r io.Reader +} + +// Read 读取数据 +func (r Reader) Read(p []byte) (int, error) { + return r.r.Read(p) +} diff --git a/pkg/filemanager/manager/fs.go b/pkg/filemanager/manager/fs.go index b0e4791b..ac0aec1d 100644 --- a/pkg/filemanager/manager/fs.go +++ b/pkg/filemanager/manager/fs.go @@ -8,6 +8,7 @@ import ( "github.com/cloudreve/Cloudreve/v4/pkg/cluster" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver/cos" + "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver/ks3" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver/local" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver/obs" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver/onedrive" @@ -73,6 +74,8 @@ func (m *manager) GetStorageDriver(ctx context.Context, policy *ent.StoragePolic return cos.New(ctx, policy, m.settings, m.config, m.l, m.dep.MimeDetector(ctx)) case types.PolicyTypeS3: return s3.New(ctx, policy, m.settings, m.config, m.l, m.dep.MimeDetector(ctx)) + case types.PolicyTypeKs3: + return ks3.New(ctx, policy, m.settings, m.config, m.l, m.dep.MimeDetector(ctx)) case types.PolicyTypeObs: return obs.New(ctx, policy, m.settings, m.config, m.l, m.dep.MimeDetector(ctx)) case types.PolicyTypeQiniu: diff --git a/routers/router.go b/routers/router.go index 27b39ccc..a866d880 100644 --- a/routers/router.go +++ b/routers/router.go @@ -492,6 +492,12 @@ func initMasterRouter(dep dependency.Dep) *gin.Engine { middleware.UseUploadSession(types.PolicyTypeS3), controllers.ProcessCallback(http.StatusBadRequest, false), ) + // 金山 ks3策略上传回调 + callback.GET( + "ks3/:sessionID/:key", + middleware.UseUploadSession(types.PolicyTypeKs3), + controllers.ProcessCallback(http.StatusBadRequest, false), + ) // Huawei OBS upload callback callback.POST( "obs/:sessionID/:key", diff --git a/service/admin/policy.go b/service/admin/policy.go index aa6625cf..0d323622 100644 --- a/service/admin/policy.go +++ b/service/admin/policy.go @@ -17,6 +17,7 @@ import ( "github.com/cloudreve/Cloudreve/v4/pkg/cluster/routes" "github.com/cloudreve/Cloudreve/v4/pkg/credmanager" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver/cos" + "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver/ks3" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver/obs" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver/onedrive" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver/oss" @@ -362,6 +363,17 @@ func (service *CreateStoragePolicyCorsService) Create(c *gin.Context) error { return nil + case types.PolicyTypeKs3: + handler, err := ks3.New(c, service.Policy, dep.SettingProvider(), dep.ConfigProvider(), dep.Logger(), dep.MimeDetector(c)) + if err != nil { + return serializer.NewError(serializer.CodeDBError, "Failed to create ks3 driver", err) + } + + if err := handler.CORS(); err != nil { + return serializer.NewError(serializer.CodeInternalSetting, "Failed to create cors: "+err.Error(), err) + } + + return nil case types.PolicyTypeObs: handler, err := obs.New(c, service.Policy, dep.SettingProvider(), dep.ConfigProvider(), dep.Logger(), dep.MimeDetector(c)) if err != nil { From 60bf0e02b3acb92f702cef0eed58618778fc097f Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Fri, 25 Jul 2025 10:15:55 +0800 Subject: [PATCH 10/74] fix(qbittorrent): download task option not working (#2666) --- pkg/downloader/qbittorrent/qbittorrent.go | 32 +++++++++++------------ 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/pkg/downloader/qbittorrent/qbittorrent.go b/pkg/downloader/qbittorrent/qbittorrent.go index ae0d6f1d..110cf2cd 100644 --- a/pkg/downloader/qbittorrent/qbittorrent.go +++ b/pkg/downloader/qbittorrent/qbittorrent.go @@ -32,18 +32,18 @@ const ( ) var ( - supportDownloadOptions = map[string]bool{ - "cookie": true, - "skip_checking": true, - "root_folder": true, - "rename": true, - "upLimit": true, - "dlLimit": true, - "ratioLimit": true, - "seedingTimeLimit": true, - "autoTMM": true, - "sequentialDownload": true, - "firstLastPiecePrio": true, + downloadOptionFormatTypes = map[string]string{ + "cookie": "%s", + "skip_checking": "%s", + "root_folder": "%s", + "rename": "%s", + "upLimit": "%.0f", + "dlLimit": "%.0f", + "ratioLimit": "%f", + "seedingTimeLimit": "%.0f", + "autoTMM": "%t", + "sequentialDownload": "%s", + "firstLastPiecePrio": "%t", } ) @@ -271,15 +271,15 @@ func (c *qbittorrentClient) CreateTask(ctx context.Context, url string, options // Apply global options for k, v := range c.options.Options { - if _, ok := supportDownloadOptions[k]; ok { - _ = formWriter.WriteField(k, fmt.Sprintf("%s", v)) + if _, ok := downloadOptionFormatTypes[k]; ok { + _ = formWriter.WriteField(k, fmt.Sprintf(downloadOptionFormatTypes[k], v)) } } // Apply group options for k, v := range options { - if _, ok := supportDownloadOptions[k]; ok { - _ = formWriter.WriteField(k, fmt.Sprintf("%s", v)) + if _, ok := downloadOptionFormatTypes[k]; ok { + _ = formWriter.WriteField(k, fmt.Sprintf(downloadOptionFormatTypes[k], v)) } } From c8c2a60adb6c65601baba930170ccbc27ddc718a Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Fri, 25 Jul 2025 11:32:04 +0800 Subject: [PATCH 11/74] =?UTF-8?q?feat(storage=20policy):=20set=20deny/allo?= =?UTF-8?q?w=20list=20for=20file=20extension=20and=20custom=20regexp=20?= =?UTF-8?q?=EF=BC=88#2695=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- inventory/types/types.go | 6 ++++ pkg/filemanager/fs/dbfs/manage.go | 18 +++++++++++ pkg/filemanager/fs/dbfs/validator.go | 33 ++++++++++++++++---- service/explorer/response.go | 45 ++++++++++++++++++++-------- 4 files changed, 84 insertions(+), 18 deletions(-) diff --git a/inventory/types/types.go b/inventory/types/types.go index 144e77eb..479574f5 100644 --- a/inventory/types/types.go +++ b/inventory/types/types.go @@ -41,6 +41,12 @@ type ( Token string `json:"token"` // 允许的文件扩展名 FileType []string `json:"file_type"` + // IsFileTypeDenyList Whether above list is a deny list. + IsFileTypeDenyList bool `json:"is_file_type_deny_list,omitempty"` + // FileRegexp 文件扩展名正则表达式 + NameRegexp string `json:"file_regexp,omitempty"` + // IsNameRegexp Whether above regexp is a deny list. + IsNameRegexpDenyList bool `json:"is_name_regexp_deny_list,omitempty"` // OauthRedirect Oauth 重定向地址 OauthRedirect string `json:"od_redirect,omitempty"` // CustomProxy whether to use custom-proxy to get file content diff --git a/pkg/filemanager/fs/dbfs/manage.go b/pkg/filemanager/fs/dbfs/manage.go index 00d5542f..914b1f0b 100644 --- a/pkg/filemanager/fs/dbfs/manage.go +++ b/pkg/filemanager/fs/dbfs/manage.go @@ -120,6 +120,20 @@ func (f *DBFS) Create(ctx context.Context, path *fs.URI, fileType types.FileType ancestor = newFile(ancestor, newFolder) } else { + // valide file name + policy, err := f.getPreferredPolicy(ctx, ancestor, 0) + if err != nil { + return nil, err + } + + if err := validateExtension(desired[i], policy); err != nil { + return nil, fs.ErrIllegalObjectName.WithError(err) + } + + if err := validateFileNameRegexp(desired[i], policy); err != nil { + return nil, fs.ErrIllegalObjectName.WithError(err) + } + file, err := f.createFile(ctx, ancestor, desired[i], fileType, o) if err != nil { return nil, err @@ -170,6 +184,10 @@ func (f *DBFS) Rename(ctx context.Context, path *fs.URI, newName string) (fs.Fil if err := validateExtension(newName, policy); err != nil { return nil, fs.ErrIllegalObjectName.WithError(err) } + + if err := validateFileNameRegexp(newName, policy); err != nil { + return nil, fs.ErrIllegalObjectName.WithError(err) + } } // Lock target diff --git a/pkg/filemanager/fs/dbfs/validator.go b/pkg/filemanager/fs/dbfs/validator.go index 71337499..34fde54d 100644 --- a/pkg/filemanager/fs/dbfs/validator.go +++ b/pkg/filemanager/fs/dbfs/validator.go @@ -3,10 +3,12 @@ package dbfs import ( "context" "fmt" + "regexp" + "strings" + "github.com/cloudreve/Cloudreve/v4/ent" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs" "github.com/cloudreve/Cloudreve/v4/pkg/util" - "strings" ) const MaxFileNameLength = 256 @@ -30,18 +32,35 @@ func validateFileName(name string) error { // validateExtension validates the file extension. func validateExtension(name string, policy *ent.StoragePolicy) error { - // 不需要验证 if len(policy.Settings.FileType) == 0 { return nil } - if !util.IsInExtensionList(policy.Settings.FileType, name) { + inList := util.IsInExtensionList(policy.Settings.FileType, name) + if (policy.Settings.IsFileTypeDenyList && inList) || (!policy.Settings.IsFileTypeDenyList && !inList) { return fmt.Errorf("file extension is not allowed") } return nil } +func validateFileNameRegexp(name string, policy *ent.StoragePolicy) error { + if policy.Settings.NameRegexp == "" { + return nil + } + + match, err := regexp.MatchString(policy.Settings.NameRegexp, name) + if err != nil { + return fmt.Errorf("invalid file name regexp: %s", err) + } + + if (policy.Settings.IsNameRegexpDenyList && match) || (!policy.Settings.IsNameRegexpDenyList && !match) { + return fmt.Errorf("file name is not allowed by regexp") + } + + return nil +} + // validateFileSize validates the file size. func validateFileSize(size int64, policy *ent.StoragePolicy) error { if policy.MaxSize == 0 { @@ -56,11 +75,15 @@ func validateFileSize(size int64, policy *ent.StoragePolicy) error { // validateNewFile validates the upload request. func validateNewFile(fileName string, size int64, policy *ent.StoragePolicy) error { if err := validateFileName(fileName); err != nil { - return err + return fs.ErrIllegalObjectName.WithError(err) } if err := validateExtension(fileName, policy); err != nil { - return err + return fs.ErrIllegalObjectName.WithError(err) + } + + if err := validateFileNameRegexp(fileName, policy); err != nil { + return fs.ErrIllegalObjectName.WithError(err) } if err := validateFileSize(size, policy); err != nil { diff --git a/service/explorer/response.go b/service/explorer/response.go index 10afdb05..755e96e2 100644 --- a/service/explorer/response.go +++ b/service/explorer/response.go @@ -250,12 +250,15 @@ type DirectLink struct { } type StoragePolicy struct { - ID string `json:"id"` - Name string `json:"name"` - AllowedSuffix []string `json:"allowed_suffix,omitempty"` - Type types.PolicyType `json:"type"` - MaxSize int64 `json:"max_size"` - Relay bool `json:"relay,omitempty"` + ID string `json:"id"` + Name string `json:"name"` + AllowedSuffix []string `json:"allowed_suffix,omitempty"` + DeniedSuffix []string `json:"denied_suffix,omitempty"` + AllowedNameRegexp string `json:"allowed_name_regexp,omitempty"` + DeniedNameRegexp string `json:"denied_name_regexp,omitempty"` + Type types.PolicyType `json:"type"` + MaxSize int64 `json:"max_size"` + Relay bool `json:"relay,omitempty"` } type Entity struct { @@ -442,14 +445,30 @@ func BuildStoragePolicy(sp *ent.StoragePolicy, hasher hashid.Encoder) *StoragePo if sp == nil { return nil } - return &StoragePolicy{ - ID: hashid.EncodePolicyID(hasher, sp.ID), - Name: sp.Name, - Type: types.PolicyType(sp.Type), - MaxSize: sp.MaxSize, - AllowedSuffix: sp.Settings.FileType, - Relay: sp.Settings.Relay, + + res := &StoragePolicy{ + ID: hashid.EncodePolicyID(hasher, sp.ID), + Name: sp.Name, + Type: types.PolicyType(sp.Type), + MaxSize: sp.MaxSize, + Relay: sp.Settings.Relay, + } + + if sp.Settings.IsFileTypeDenyList { + res.DeniedSuffix = sp.Settings.FileType + } else { + res.AllowedSuffix = sp.Settings.FileType + } + + if sp.Settings.NameRegexp != "" { + if sp.Settings.IsNameRegexpDenyList { + res.DeniedNameRegexp = sp.Settings.NameRegexp + } else { + res.AllowedNameRegexp = sp.Settings.NameRegexp + } } + + return res } func WriteEventSourceHeader(c *gin.Context) { From 36be9b7a19279d7ed469faca205f01ebaa3d242a Mon Sep 17 00:00:00 2001 From: Git'Fellow <12234510+solracsf@users.noreply.github.com> Date: Thu, 31 Jul 2025 05:18:48 +0200 Subject: [PATCH 12/74] Fix typos on README (#2693) --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5b7e67bd..aaee56bf 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Cloudreve
-

Self-hosted file management system with muilt-cloud support.

+

Self-hosted file management system with multi-cloud support.

@@ -43,13 +43,13 @@ - 💾 Integrate with Aria2/qBittorrent to download files in background, use multiple download nodes to share the load. - 📚 Compress/Extract files, download files in batch. - 💻 WebDAV support covering all storage providers. -- :zap:Drag&Drop to upload files or folders, with resumeable upload support. +- :zap:Drag&Drop to upload files or folders, with resumable upload support. - :card_file_box: Extract media metadata from files, search files by metadata or tags. - :family_woman_girl_boy: Multi-users with multi-groups. - :link: Create share links for files and folders with expiration date. - :eye_speech_bubble: Preview videos, images, audios, ePub files online; edit texts, diagrams, Markdown, images, Office documents online. - :art: Customize theme colors, dark mode, PWA application, SPA, i18n. -- :rocket: All-In-One packing, with all features out-of-the-box. +- :rocket: All-in-one packaging, with all features out of the box. - 🌈 ... ... ## :hammer_and_wrench: Deploy From 51d9e06f2131b59777ab91cb7342e2afd4c1addb Mon Sep 17 00:00:00 2001 From: Curious <13582788510@139.com> Date: Mon, 4 Aug 2025 14:52:21 +0800 Subject: [PATCH 13/74] chore(docker compose): pin postgres to major version (#2723) --- docker-compose.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index c21dcb64..78ff7b57 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,7 +19,10 @@ services: - backend_data:/cloudreve/data postgresql: - image: postgres:latest + # Best practice: Pin to major version. + # NOTE: For major version jumps: + # backup & consult https://www.postgresql.org/docs/current/pgupgrade.html + image: postgres:17 container_name: postgresql environment: - POSTGRES_USER=cloudreve From e31a6cbcb31fd4fb6787f34a160320b979eb1e80 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Tue, 5 Aug 2025 12:02:17 +0800 Subject: [PATCH 14/74] fix(workflow): concurrent read&write to progress map while transfer files in batch (#2737) --- assets | 2 +- pkg/filemanager/fs/dbfs/manage.go | 2 +- pkg/filemanager/workflows/remote_download.go | 40 +++++++++++--------- pkg/filemanager/workflows/upload.go | 35 ++++++++++------- 4 files changed, 46 insertions(+), 33 deletions(-) diff --git a/assets b/assets index 0b49582a..c4a65939 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 0b49582a07eccfd63896dd18f7d944f7e96ed47d +Subproject commit c4a6593921d34ec47d78d5288c2d1c0865d435b6 diff --git a/pkg/filemanager/fs/dbfs/manage.go b/pkg/filemanager/fs/dbfs/manage.go index 914b1f0b..18657e47 100644 --- a/pkg/filemanager/fs/dbfs/manage.go +++ b/pkg/filemanager/fs/dbfs/manage.go @@ -121,7 +121,7 @@ func (f *DBFS) Create(ctx context.Context, path *fs.URI, fileType types.FileType ancestor = newFile(ancestor, newFolder) } else { // valide file name - policy, err := f.getPreferredPolicy(ctx, ancestor, 0) + policy, err := f.getPreferredPolicy(ctx, ancestor) if err != nil { return nil, err } diff --git a/pkg/filemanager/workflows/remote_download.go b/pkg/filemanager/workflows/remote_download.go index efe9bfdc..4425c4d9 100644 --- a/pkg/filemanager/workflows/remote_download.go +++ b/pkg/filemanager/workflows/remote_download.go @@ -432,12 +432,6 @@ func (m *RemoteDownloadTask) masterTransfer(ctx context.Context, dep dependency. ae := serializer.NewAggregateError() transferFunc := func(workerId int, file downloader.TaskFile) { - defer func() { - atomic.AddInt64(&m.progress[ProgressTypeUploadCount].Current, 1) - worker <- workerId - wg.Done() - }() - sanitizedName := sanitizeFileName(file.Name) dst := dstUri.JoinRaw(sanitizedName) src := filepath.FromSlash(path.Join(m.state.Status.SavePath, file.Name)) @@ -446,12 +440,21 @@ func (m *RemoteDownloadTask) masterTransfer(ctx context.Context, dep dependency. progressKey := fmt.Sprintf("%s%d", ProgressTypeUploadSinglePrefix, workerId) m.Lock() m.progress[progressKey] = &queue.Progress{Identifier: dst.String(), Total: file.Size} + fileProgress := m.progress[progressKey] + uploadProgress := m.progress[ProgressTypeUpload] + uploadCountProgress := m.progress[ProgressTypeUploadCount] m.Unlock() + defer func() { + atomic.AddInt64(&uploadCountProgress.Current, 1) + worker <- workerId + wg.Done() + }() + fileStream, err := os.Open(src) if err != nil { m.l.Warning("Failed to open file %s: %s", src, err.Error()) - atomic.AddInt64(&m.progress[ProgressTypeUpload].Current, file.Size) + atomic.AddInt64(&uploadProgress.Current, file.Size) atomic.AddInt64(&failed, 1) ae.Add(file.Name, fmt.Errorf("failed to open file: %w", err)) return @@ -465,8 +468,8 @@ func (m *RemoteDownloadTask) masterTransfer(ctx context.Context, dep dependency. Size: file.Size, }, ProgressFunc: func(current, diff int64, total int64) { - atomic.AddInt64(&m.progress[progressKey].Current, diff) - atomic.AddInt64(&m.progress[ProgressTypeUpload].Current, diff) + atomic.AddInt64(&fileProgress.Current, diff) + atomic.AddInt64(&uploadProgress.Current, diff) }, File: fileStream, } @@ -475,7 +478,7 @@ func (m *RemoteDownloadTask) masterTransfer(ctx context.Context, dep dependency. if err != nil { m.l.Warning("Failed to upload file %s: %s", src, err.Error()) atomic.AddInt64(&failed, 1) - atomic.AddInt64(&m.progress[ProgressTypeUpload].Current, file.Size) + atomic.AddInt64(&uploadProgress.Current, file.Size) ae.Add(file.Name, fmt.Errorf("failed to upload file: %w", err)) return } @@ -490,8 +493,10 @@ func (m *RemoteDownloadTask) masterTransfer(ctx context.Context, dep dependency. // Check if file is already transferred if _, ok := m.state.Transferred[file.Index]; ok { m.l.Info("File %s already transferred, skipping...", file.Name) + m.Lock() atomic.AddInt64(&m.progress[ProgressTypeUpload].Current, file.Size) atomic.AddInt64(&m.progress[ProgressTypeUploadCount].Current, 1) + m.Unlock() continue } @@ -625,19 +630,18 @@ func (m *RemoteDownloadTask) Progress(ctx context.Context) queue.Progresses { m.Lock() defer m.Unlock() - if m.state.NodeState.progress != nil { - merged := make(queue.Progresses) - for k, v := range m.progress { - merged[k] = v - } + merged := make(queue.Progresses) + for k, v := range m.progress { + merged[k] = v + } + if m.state.NodeState.progress != nil { for k, v := range m.state.NodeState.progress { merged[k] = v } - - return merged } - return m.progress + + return merged } func sanitizeFileName(name string) string { diff --git a/pkg/filemanager/workflows/upload.go b/pkg/filemanager/workflows/upload.go index 65b36ee9..b50a0358 100644 --- a/pkg/filemanager/workflows/upload.go +++ b/pkg/filemanager/workflows/upload.go @@ -115,23 +115,26 @@ func (t *SlaveUploadTask) Do(ctx context.Context) (task.Status, error) { atomic.StoreInt64(&t.progress[ProgressTypeUpload].Total, totalSize) ae := serializer.NewAggregateError() transferFunc := func(workerId, fileId int, file SlaveUploadEntity) { - defer func() { - atomic.AddInt64(&t.progress[ProgressTypeUploadCount].Current, 1) - worker <- workerId - wg.Done() - }() - t.l.Info("Uploading file %s to %s...", file.Src, file.Uri.String()) progressKey := fmt.Sprintf("%s%d", ProgressTypeUploadSinglePrefix, workerId) t.Lock() t.progress[progressKey] = &queue.Progress{Identifier: file.Uri.String(), Total: file.Size} + fileProgress := t.progress[progressKey] + uploadProgress := t.progress[ProgressTypeUpload] + uploadCountProgress := t.progress[ProgressTypeUploadCount] t.Unlock() + defer func() { + atomic.AddInt64(&uploadCountProgress.Current, 1) + worker <- workerId + wg.Done() + }() + handle, err := os.Open(filepath.FromSlash(file.Src)) if err != nil { t.l.Warning("Failed to open file %s: %s", file.Src, err.Error()) - atomic.AddInt64(&t.progress[ProgressTypeUpload].Current, file.Size) + atomic.AddInt64(&fileProgress.Current, file.Size) ae.Add(path.Base(file.Src), fmt.Errorf("failed to open file: %w", err)) return } @@ -140,7 +143,7 @@ func (t *SlaveUploadTask) Do(ctx context.Context) (task.Status, error) { if err != nil { t.l.Warning("Failed to get file stat for %s: %s", file.Src, err.Error()) handle.Close() - atomic.AddInt64(&t.progress[ProgressTypeUpload].Current, file.Size) + atomic.AddInt64(&fileProgress.Current, file.Size) ae.Add(path.Base(file.Src), fmt.Errorf("failed to get file stat: %w", err)) return } @@ -151,9 +154,9 @@ func (t *SlaveUploadTask) Do(ctx context.Context) (task.Status, error) { Size: stat.Size(), }, ProgressFunc: func(current, diff int64, total int64) { - atomic.AddInt64(&t.progress[progressKey].Current, diff) - atomic.AddInt64(&t.progress[ProgressTypeUpload].Current, diff) - atomic.StoreInt64(&t.progress[progressKey].Total, total) + atomic.AddInt64(&fileProgress.Current, diff) + atomic.AddInt64(&uploadCountProgress.Current, 1) + atomic.StoreInt64(&fileProgress.Total, total) }, File: handle, Seeker: handle, @@ -163,7 +166,7 @@ func (t *SlaveUploadTask) Do(ctx context.Context) (task.Status, error) { if err != nil { handle.Close() t.l.Warning("Failed to upload file %s: %s", file.Src, err.Error()) - atomic.AddInt64(&t.progress[ProgressTypeUpload].Current, file.Size) + atomic.AddInt64(&uploadProgress.Current, file.Size) ae.Add(path.Base(file.Src), fmt.Errorf("failed to upload file: %w", err)) return } @@ -179,8 +182,10 @@ func (t *SlaveUploadTask) Do(ctx context.Context) (task.Status, error) { // Check if file is already transferred if _, ok := t.state.Transferred[fileId]; ok { t.l.Info("File %s already transferred, skipping...", file.Src) + t.Lock() atomic.AddInt64(&t.progress[ProgressTypeUpload].Current, file.Size) atomic.AddInt64(&t.progress[ProgressTypeUploadCount].Current, 1) + t.Unlock() continue } @@ -221,5 +226,9 @@ func (m *SlaveUploadTask) Progress(ctx context.Context) queue.Progresses { m.Lock() defer m.Unlock() - return m.progress + res := make(queue.Progresses) + for k, v := range m.progress { + res[k] = v + } + return res } From 80b25e88ee13cce4948e8f69002b6e20c49356cc Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Tue, 5 Aug 2025 15:11:32 +0800 Subject: [PATCH 15/74] fix(dbfs): file modified_at should not be updated by ent --- assets | 2 +- ent/client.go | 3 +- ent/file.go | 16 +----- ent/file/file.go | 13 +---- ent/file/where.go | 55 -------------------- ent/file_create.go | 78 ---------------------------- ent/file_update.go | 78 +++------------------------- ent/internal/schema.go | 2 +- ent/migrate/schema.go | 13 +++-- ent/mutation.go | 75 +------------------------- ent/runtime/runtime.go | 19 +++---- ent/schema/file.go | 33 ++++++++++-- inventory/file.go | 17 ++++-- pkg/filemanager/fs/dbfs/props.go | 12 +++++ pkg/filemanager/fs/fs.go | 9 ++-- pkg/filemanager/manager/metadata.go | 28 +++++++--- pkg/filemanager/manager/operation.go | 5 +- pkg/filemanager/manager/upload.go | 2 +- 18 files changed, 110 insertions(+), 350 deletions(-) diff --git a/assets b/assets index c4a65939..09480ffa 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit c4a6593921d34ec47d78d5288c2d1c0865d435b6 +Subproject commit 09480ffa21d859a1d2f9bb2421e6f78f113494c4 diff --git a/ent/client.go b/ent/client.go index 7a157b5d..06129e13 100644 --- a/ent/client.go +++ b/ent/client.go @@ -1034,8 +1034,7 @@ func (c *FileClient) Hooks() []Hook { // Interceptors returns the client interceptors. func (c *FileClient) Interceptors() []Interceptor { - inters := c.inters.File - return append(inters[:len(inters):len(inters)], file.Interceptors[:]...) + return c.inters.File } func (c *FileClient) mutate(ctx context.Context, m *FileMutation) (Value, error) { diff --git a/ent/file.go b/ent/file.go index e92ede44..d65450b0 100644 --- a/ent/file.go +++ b/ent/file.go @@ -25,8 +25,6 @@ type File struct { CreatedAt time.Time `json:"created_at,omitempty"` // UpdatedAt holds the value of the "updated_at" field. UpdatedAt time.Time `json:"updated_at,omitempty"` - // DeletedAt holds the value of the "deleted_at" field. - DeletedAt *time.Time `json:"deleted_at,omitempty"` // Type holds the value of the "type" field. Type int `json:"type,omitempty"` // Name holds the value of the "name" field. @@ -171,7 +169,7 @@ func (*File) scanValues(columns []string) ([]any, error) { values[i] = new(sql.NullInt64) case file.FieldName: values[i] = new(sql.NullString) - case file.FieldCreatedAt, file.FieldUpdatedAt, file.FieldDeletedAt: + case file.FieldCreatedAt, file.FieldUpdatedAt: values[i] = new(sql.NullTime) default: values[i] = new(sql.UnknownType) @@ -206,13 +204,6 @@ func (f *File) assignValues(columns []string, values []any) error { } else if value.Valid { f.UpdatedAt = value.Time } - case file.FieldDeletedAt: - if value, ok := values[i].(*sql.NullTime); !ok { - return fmt.Errorf("unexpected type %T for field deleted_at", values[i]) - } else if value.Valid { - f.DeletedAt = new(time.Time) - *f.DeletedAt = value.Time - } case file.FieldType: if value, ok := values[i].(*sql.NullInt64); !ok { return fmt.Errorf("unexpected type %T for field type", values[i]) @@ -351,11 +342,6 @@ func (f *File) String() string { builder.WriteString("updated_at=") builder.WriteString(f.UpdatedAt.Format(time.ANSIC)) builder.WriteString(", ") - if v := f.DeletedAt; v != nil { - builder.WriteString("deleted_at=") - builder.WriteString(v.Format(time.ANSIC)) - } - builder.WriteString(", ") builder.WriteString("type=") builder.WriteString(fmt.Sprintf("%v", f.Type)) builder.WriteString(", ") diff --git a/ent/file/file.go b/ent/file/file.go index 6adc3dae..61315d4c 100644 --- a/ent/file/file.go +++ b/ent/file/file.go @@ -19,8 +19,6 @@ const ( FieldCreatedAt = "created_at" // FieldUpdatedAt holds the string denoting the updated_at field in the database. FieldUpdatedAt = "updated_at" - // FieldDeletedAt holds the string denoting the deleted_at field in the database. - FieldDeletedAt = "deleted_at" // FieldType holds the string denoting the type field in the database. FieldType = "type" // FieldName holds the string denoting the name field in the database. @@ -112,7 +110,6 @@ var Columns = []string{ FieldID, FieldCreatedAt, FieldUpdatedAt, - FieldDeletedAt, FieldType, FieldName, FieldOwnerID, @@ -146,14 +143,11 @@ func ValidColumn(column string) bool { // // import _ "github.com/cloudreve/Cloudreve/v4/ent/runtime" var ( - Hooks [1]ent.Hook - Interceptors [1]ent.Interceptor + Hooks [1]ent.Hook // DefaultCreatedAt holds the default value on creation for the "created_at" field. DefaultCreatedAt func() time.Time // DefaultUpdatedAt holds the default value on creation for the "updated_at" field. DefaultUpdatedAt func() time.Time - // UpdateDefaultUpdatedAt holds the default value on update for the "updated_at" field. - UpdateDefaultUpdatedAt func() time.Time // DefaultSize holds the default value on creation for the "size" field. DefaultSize int64 // DefaultIsSymbolic holds the default value on creation for the "is_symbolic" field. @@ -178,11 +172,6 @@ func ByUpdatedAt(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldUpdatedAt, opts...).ToFunc() } -// ByDeletedAt orders the results by the deleted_at field. -func ByDeletedAt(opts ...sql.OrderTermOption) OrderOption { - return sql.OrderByField(FieldDeletedAt, opts...).ToFunc() -} - // ByType orders the results by the type field. func ByType(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldType, opts...).ToFunc() diff --git a/ent/file/where.go b/ent/file/where.go index f2a08bbf..1b90793a 100644 --- a/ent/file/where.go +++ b/ent/file/where.go @@ -65,11 +65,6 @@ func UpdatedAt(v time.Time) predicate.File { return predicate.File(sql.FieldEQ(FieldUpdatedAt, v)) } -// DeletedAt applies equality check predicate on the "deleted_at" field. It's identical to DeletedAtEQ. -func DeletedAt(v time.Time) predicate.File { - return predicate.File(sql.FieldEQ(FieldDeletedAt, v)) -} - // Type applies equality check predicate on the "type" field. It's identical to TypeEQ. func Type(v int) predicate.File { return predicate.File(sql.FieldEQ(FieldType, v)) @@ -190,56 +185,6 @@ func UpdatedAtLTE(v time.Time) predicate.File { return predicate.File(sql.FieldLTE(FieldUpdatedAt, v)) } -// DeletedAtEQ applies the EQ predicate on the "deleted_at" field. -func DeletedAtEQ(v time.Time) predicate.File { - return predicate.File(sql.FieldEQ(FieldDeletedAt, v)) -} - -// DeletedAtNEQ applies the NEQ predicate on the "deleted_at" field. -func DeletedAtNEQ(v time.Time) predicate.File { - return predicate.File(sql.FieldNEQ(FieldDeletedAt, v)) -} - -// DeletedAtIn applies the In predicate on the "deleted_at" field. -func DeletedAtIn(vs ...time.Time) predicate.File { - return predicate.File(sql.FieldIn(FieldDeletedAt, vs...)) -} - -// DeletedAtNotIn applies the NotIn predicate on the "deleted_at" field. -func DeletedAtNotIn(vs ...time.Time) predicate.File { - return predicate.File(sql.FieldNotIn(FieldDeletedAt, vs...)) -} - -// DeletedAtGT applies the GT predicate on the "deleted_at" field. -func DeletedAtGT(v time.Time) predicate.File { - return predicate.File(sql.FieldGT(FieldDeletedAt, v)) -} - -// DeletedAtGTE applies the GTE predicate on the "deleted_at" field. -func DeletedAtGTE(v time.Time) predicate.File { - return predicate.File(sql.FieldGTE(FieldDeletedAt, v)) -} - -// DeletedAtLT applies the LT predicate on the "deleted_at" field. -func DeletedAtLT(v time.Time) predicate.File { - return predicate.File(sql.FieldLT(FieldDeletedAt, v)) -} - -// DeletedAtLTE applies the LTE predicate on the "deleted_at" field. -func DeletedAtLTE(v time.Time) predicate.File { - return predicate.File(sql.FieldLTE(FieldDeletedAt, v)) -} - -// DeletedAtIsNil applies the IsNil predicate on the "deleted_at" field. -func DeletedAtIsNil() predicate.File { - return predicate.File(sql.FieldIsNull(FieldDeletedAt)) -} - -// DeletedAtNotNil applies the NotNil predicate on the "deleted_at" field. -func DeletedAtNotNil() predicate.File { - return predicate.File(sql.FieldNotNull(FieldDeletedAt)) -} - // TypeEQ applies the EQ predicate on the "type" field. func TypeEQ(v int) predicate.File { return predicate.File(sql.FieldEQ(FieldType, v)) diff --git a/ent/file_create.go b/ent/file_create.go index a0330c3b..1e3470fd 100644 --- a/ent/file_create.go +++ b/ent/file_create.go @@ -57,20 +57,6 @@ func (fc *FileCreate) SetNillableUpdatedAt(t *time.Time) *FileCreate { return fc } -// SetDeletedAt sets the "deleted_at" field. -func (fc *FileCreate) SetDeletedAt(t time.Time) *FileCreate { - fc.mutation.SetDeletedAt(t) - return fc -} - -// SetNillableDeletedAt sets the "deleted_at" field if the given value is not nil. -func (fc *FileCreate) SetNillableDeletedAt(t *time.Time) *FileCreate { - if t != nil { - fc.SetDeletedAt(*t) - } - return fc -} - // SetType sets the "type" field. func (fc *FileCreate) SetType(i int) *FileCreate { fc.mutation.SetType(i) @@ -413,10 +399,6 @@ func (fc *FileCreate) createSpec() (*File, *sqlgraph.CreateSpec) { _spec.SetField(file.FieldUpdatedAt, field.TypeTime, value) _node.UpdatedAt = value } - if value, ok := fc.mutation.DeletedAt(); ok { - _spec.SetField(file.FieldDeletedAt, field.TypeTime, value) - _node.DeletedAt = &value - } if value, ok := fc.mutation.GetType(); ok { _spec.SetField(file.FieldType, field.TypeInt, value) _node.Type = value @@ -636,24 +618,6 @@ func (u *FileUpsert) UpdateUpdatedAt() *FileUpsert { return u } -// SetDeletedAt sets the "deleted_at" field. -func (u *FileUpsert) SetDeletedAt(v time.Time) *FileUpsert { - u.Set(file.FieldDeletedAt, v) - return u -} - -// UpdateDeletedAt sets the "deleted_at" field to the value that was provided on create. -func (u *FileUpsert) UpdateDeletedAt() *FileUpsert { - u.SetExcluded(file.FieldDeletedAt) - return u -} - -// ClearDeletedAt clears the value of the "deleted_at" field. -func (u *FileUpsert) ClearDeletedAt() *FileUpsert { - u.SetNull(file.FieldDeletedAt) - return u -} - // SetType sets the "type" field. func (u *FileUpsert) SetType(v int) *FileUpsert { u.Set(file.FieldType, v) @@ -863,27 +827,6 @@ func (u *FileUpsertOne) UpdateUpdatedAt() *FileUpsertOne { }) } -// SetDeletedAt sets the "deleted_at" field. -func (u *FileUpsertOne) SetDeletedAt(v time.Time) *FileUpsertOne { - return u.Update(func(s *FileUpsert) { - s.SetDeletedAt(v) - }) -} - -// UpdateDeletedAt sets the "deleted_at" field to the value that was provided on create. -func (u *FileUpsertOne) UpdateDeletedAt() *FileUpsertOne { - return u.Update(func(s *FileUpsert) { - s.UpdateDeletedAt() - }) -} - -// ClearDeletedAt clears the value of the "deleted_at" field. -func (u *FileUpsertOne) ClearDeletedAt() *FileUpsertOne { - return u.Update(func(s *FileUpsert) { - s.ClearDeletedAt() - }) -} - // SetType sets the "type" field. func (u *FileUpsertOne) SetType(v int) *FileUpsertOne { return u.Update(func(s *FileUpsert) { @@ -1289,27 +1232,6 @@ func (u *FileUpsertBulk) UpdateUpdatedAt() *FileUpsertBulk { }) } -// SetDeletedAt sets the "deleted_at" field. -func (u *FileUpsertBulk) SetDeletedAt(v time.Time) *FileUpsertBulk { - return u.Update(func(s *FileUpsert) { - s.SetDeletedAt(v) - }) -} - -// UpdateDeletedAt sets the "deleted_at" field to the value that was provided on create. -func (u *FileUpsertBulk) UpdateDeletedAt() *FileUpsertBulk { - return u.Update(func(s *FileUpsert) { - s.UpdateDeletedAt() - }) -} - -// ClearDeletedAt clears the value of the "deleted_at" field. -func (u *FileUpsertBulk) ClearDeletedAt() *FileUpsertBulk { - return u.Update(func(s *FileUpsert) { - s.ClearDeletedAt() - }) -} - // SetType sets the "type" field. func (u *FileUpsertBulk) SetType(v int) *FileUpsertBulk { return u.Update(func(s *FileUpsert) { diff --git a/ent/file_update.go b/ent/file_update.go index 4f913962..24ee76d7 100644 --- a/ent/file_update.go +++ b/ent/file_update.go @@ -41,26 +41,14 @@ func (fu *FileUpdate) SetUpdatedAt(t time.Time) *FileUpdate { return fu } -// SetDeletedAt sets the "deleted_at" field. -func (fu *FileUpdate) SetDeletedAt(t time.Time) *FileUpdate { - fu.mutation.SetDeletedAt(t) - return fu -} - -// SetNillableDeletedAt sets the "deleted_at" field if the given value is not nil. -func (fu *FileUpdate) SetNillableDeletedAt(t *time.Time) *FileUpdate { +// SetNillableUpdatedAt sets the "updated_at" field if the given value is not nil. +func (fu *FileUpdate) SetNillableUpdatedAt(t *time.Time) *FileUpdate { if t != nil { - fu.SetDeletedAt(*t) + fu.SetUpdatedAt(*t) } return fu } -// ClearDeletedAt clears the value of the "deleted_at" field. -func (fu *FileUpdate) ClearDeletedAt() *FileUpdate { - fu.mutation.ClearDeletedAt() - return fu -} - // SetType sets the "type" field. func (fu *FileUpdate) SetType(i int) *FileUpdate { fu.mutation.ResetType() @@ -472,9 +460,6 @@ func (fu *FileUpdate) RemoveDirectLinks(d ...*DirectLink) *FileUpdate { // Save executes the query and returns the number of nodes affected by the update operation. func (fu *FileUpdate) Save(ctx context.Context) (int, error) { - if err := fu.defaults(); err != nil { - return 0, err - } return withHooks(ctx, fu.sqlSave, fu.mutation, fu.hooks) } @@ -500,18 +485,6 @@ func (fu *FileUpdate) ExecX(ctx context.Context) { } } -// defaults sets the default values of the builder before save. -func (fu *FileUpdate) defaults() error { - if _, ok := fu.mutation.UpdatedAt(); !ok { - if file.UpdateDefaultUpdatedAt == nil { - return fmt.Errorf("ent: uninitialized file.UpdateDefaultUpdatedAt (forgotten import ent/runtime?)") - } - v := file.UpdateDefaultUpdatedAt() - fu.mutation.SetUpdatedAt(v) - } - return nil -} - // check runs all checks and user-defined validators on the builder. func (fu *FileUpdate) check() error { if _, ok := fu.mutation.OwnerID(); fu.mutation.OwnerCleared() && !ok { @@ -535,12 +508,6 @@ func (fu *FileUpdate) sqlSave(ctx context.Context) (n int, err error) { if value, ok := fu.mutation.UpdatedAt(); ok { _spec.SetField(file.FieldUpdatedAt, field.TypeTime, value) } - if value, ok := fu.mutation.DeletedAt(); ok { - _spec.SetField(file.FieldDeletedAt, field.TypeTime, value) - } - if fu.mutation.DeletedAtCleared() { - _spec.ClearField(file.FieldDeletedAt, field.TypeTime) - } if value, ok := fu.mutation.GetType(); ok { _spec.SetField(file.FieldType, field.TypeInt, value) } @@ -912,26 +879,14 @@ func (fuo *FileUpdateOne) SetUpdatedAt(t time.Time) *FileUpdateOne { return fuo } -// SetDeletedAt sets the "deleted_at" field. -func (fuo *FileUpdateOne) SetDeletedAt(t time.Time) *FileUpdateOne { - fuo.mutation.SetDeletedAt(t) - return fuo -} - -// SetNillableDeletedAt sets the "deleted_at" field if the given value is not nil. -func (fuo *FileUpdateOne) SetNillableDeletedAt(t *time.Time) *FileUpdateOne { +// SetNillableUpdatedAt sets the "updated_at" field if the given value is not nil. +func (fuo *FileUpdateOne) SetNillableUpdatedAt(t *time.Time) *FileUpdateOne { if t != nil { - fuo.SetDeletedAt(*t) + fuo.SetUpdatedAt(*t) } return fuo } -// ClearDeletedAt clears the value of the "deleted_at" field. -func (fuo *FileUpdateOne) ClearDeletedAt() *FileUpdateOne { - fuo.mutation.ClearDeletedAt() - return fuo -} - // SetType sets the "type" field. func (fuo *FileUpdateOne) SetType(i int) *FileUpdateOne { fuo.mutation.ResetType() @@ -1356,9 +1311,6 @@ func (fuo *FileUpdateOne) Select(field string, fields ...string) *FileUpdateOne // Save executes the query and returns the updated File entity. func (fuo *FileUpdateOne) Save(ctx context.Context) (*File, error) { - if err := fuo.defaults(); err != nil { - return nil, err - } return withHooks(ctx, fuo.sqlSave, fuo.mutation, fuo.hooks) } @@ -1384,18 +1336,6 @@ func (fuo *FileUpdateOne) ExecX(ctx context.Context) { } } -// defaults sets the default values of the builder before save. -func (fuo *FileUpdateOne) defaults() error { - if _, ok := fuo.mutation.UpdatedAt(); !ok { - if file.UpdateDefaultUpdatedAt == nil { - return fmt.Errorf("ent: uninitialized file.UpdateDefaultUpdatedAt (forgotten import ent/runtime?)") - } - v := file.UpdateDefaultUpdatedAt() - fuo.mutation.SetUpdatedAt(v) - } - return nil -} - // check runs all checks and user-defined validators on the builder. func (fuo *FileUpdateOne) check() error { if _, ok := fuo.mutation.OwnerID(); fuo.mutation.OwnerCleared() && !ok { @@ -1436,12 +1376,6 @@ func (fuo *FileUpdateOne) sqlSave(ctx context.Context) (_node *File, err error) if value, ok := fuo.mutation.UpdatedAt(); ok { _spec.SetField(file.FieldUpdatedAt, field.TypeTime, value) } - if value, ok := fuo.mutation.DeletedAt(); ok { - _spec.SetField(file.FieldDeletedAt, field.TypeTime, value) - } - if fuo.mutation.DeletedAtCleared() { - _spec.ClearField(file.FieldDeletedAt, field.TypeTime) - } if value, ok := fuo.mutation.GetType(); ok { _spec.SetField(file.FieldType, field.TypeInt, value) } diff --git a/ent/internal/schema.go b/ent/internal/schema.go index 160f7aff..6380a862 100644 --- a/ent/internal/schema.go +++ b/ent/internal/schema.go @@ -6,4 +6,4 @@ // Package internal holds a loadable version of the latest schema. package internal -const Schema = "{\"Schema\":\"github.com/cloudreve/Cloudreve/v4/ent/schema\",\"Package\":\"github.com/cloudreve/Cloudreve/v4/ent\",\"Schemas\":[{\"name\":\"DavAccount\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"owner\",\"type\":\"User\",\"field\":\"owner_id\",\"ref_name\":\"dav_accounts\",\"unique\":true,\"inverse\":true,\"required\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"uri\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"password\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0},\"sensitive\":true},{\"name\":\"options\",\"type\":{\"Type\":5,\"Ident\":\"*boolset.BooleanSet\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"PkgName\":\"boolset\",\"Nillable\":true,\"RType\":{\"Name\":\"BooleanSet\",\"Ident\":\"boolset.BooleanSet\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"Methods\":{\"Enabled\":{\"In\":[{\"Name\":\"int\",\"Ident\":\"int\",\"Kind\":2,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"bool\",\"Ident\":\"bool\",\"Kind\":1,\"PkgPath\":\"\",\"Methods\":null}]},\"MarshalBinary\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Scan\":{\"In\":[{\"Name\":\"\",\"Ident\":\"interface {}\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"String\":{\"In\":[],\"Out\":[{\"Name\":\"string\",\"Ident\":\"string\",\"Kind\":24,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalBinary\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Value\":{\"In\":[],\"Out\":[{\"Name\":\"Value\",\"Ident\":\"driver.Value\",\"Kind\":20,\"PkgPath\":\"database/sql/driver\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"props\",\"type\":{\"Type\":3,\"Ident\":\"*types.DavAccountProps\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"DavAccountProps\",\"Ident\":\"types.DavAccountProps\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"owner_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}}],\"indexes\":[{\"unique\":true,\"fields\":[\"owner_id\",\"password\"]}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"DirectLink\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"file\",\"type\":\"File\",\"field\":\"file_id\",\"ref_name\":\"direct_links\",\"unique\":true,\"inverse\":true,\"required\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"downloads\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"file_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"speed\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Entity\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"file\",\"type\":\"File\",\"ref_name\":\"entities\",\"inverse\":true},{\"name\":\"user\",\"type\":\"User\",\"field\":\"created_by\",\"ref_name\":\"entities\",\"unique\":true,\"inverse\":true},{\"name\":\"storage_policy\",\"type\":\"StoragePolicy\",\"field\":\"storage_policy_entities\",\"ref_name\":\"entities\",\"unique\":true,\"inverse\":true,\"required\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"type\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"source\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"size\",\"type\":{\"Type\":13,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"reference_count\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":1,\"default_kind\":2,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"storage_policy_entities\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"created_by\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"upload_session_id\",\"type\":{\"Type\":4,\"Ident\":\"uuid.UUID\",\"PkgPath\":\"github.com/gofrs/uuid\",\"PkgName\":\"uuid\",\"Nillable\":false,\"RType\":{\"Name\":\"UUID\",\"Ident\":\"uuid.UUID\",\"Kind\":17,\"PkgPath\":\"github.com/gofrs/uuid\",\"Methods\":{\"Bytes\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}]},\"Format\":{\"In\":[{\"Name\":\"State\",\"Ident\":\"fmt.State\",\"Kind\":20,\"PkgPath\":\"fmt\",\"Methods\":null},{\"Name\":\"int32\",\"Ident\":\"int32\",\"Kind\":5,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"MarshalBinary\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"MarshalText\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Scan\":{\"In\":[{\"Name\":\"\",\"Ident\":\"interface {}\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"SetVariant\":{\"In\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"SetVersion\":{\"In\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"String\":{\"In\":[],\"Out\":[{\"Name\":\"string\",\"Ident\":\"string\",\"Kind\":24,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalBinary\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalText\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Value\":{\"In\":[],\"Out\":[{\"Name\":\"Value\",\"Ident\":\"driver.Value\",\"Kind\":20,\"PkgPath\":\"database/sql/driver\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Variant\":{\"In\":[],\"Out\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}]},\"Version\":{\"In\":[],\"Out\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":6,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"recycle_options\",\"type\":{\"Type\":3,\"Ident\":\"*types.EntityRecycleOption\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"EntityRecycleOption\",\"Ident\":\"types.EntityRecycleOption\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"position\":{\"Index\":7,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"File\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"owner\",\"type\":\"User\",\"field\":\"owner_id\",\"ref_name\":\"files\",\"unique\":true,\"inverse\":true,\"required\":true},{\"name\":\"storage_policies\",\"type\":\"StoragePolicy\",\"field\":\"storage_policy_files\",\"ref_name\":\"files\",\"unique\":true,\"inverse\":true},{\"name\":\"parent\",\"type\":\"File\",\"field\":\"file_children\",\"ref\":{\"name\":\"children\",\"type\":\"File\"},\"unique\":true,\"inverse\":true},{\"name\":\"metadata\",\"type\":\"Metadata\"},{\"name\":\"entities\",\"type\":\"Entity\"},{\"name\":\"shares\",\"type\":\"Share\"},{\"name\":\"direct_links\",\"type\":\"DirectLink\"}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"type\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"owner_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"size\",\"type\":{\"Type\":13,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":0,\"default_kind\":6,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"primary_entity\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"file_children\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"is_symbolic\",\"type\":{\"Type\":1,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":false,\"default_kind\":1,\"position\":{\"Index\":6,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"props\",\"type\":{\"Type\":3,\"Ident\":\"*types.FileProps\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"FileProps\",\"Ident\":\"types.FileProps\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"position\":{\"Index\":7,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"storage_policy_files\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":8,\"MixedIn\":false,\"MixinIndex\":0}}],\"indexes\":[{\"unique\":true,\"fields\":[\"file_children\",\"name\"]},{\"fields\":[\"file_children\",\"type\",\"updated_at\"]},{\"fields\":[\"file_children\",\"type\",\"size\"]}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Group\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"users\",\"type\":\"User\"},{\"name\":\"storage_policies\",\"type\":\"StoragePolicy\",\"field\":\"storage_policy_id\",\"ref_name\":\"groups\",\"unique\":true,\"inverse\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"max_storage\",\"type\":{\"Type\":13,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"speed_limit\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"permissions\",\"type\":{\"Type\":5,\"Ident\":\"*boolset.BooleanSet\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"PkgName\":\"boolset\",\"Nillable\":true,\"RType\":{\"Name\":\"BooleanSet\",\"Ident\":\"boolset.BooleanSet\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"Methods\":{\"Enabled\":{\"In\":[{\"Name\":\"int\",\"Ident\":\"int\",\"Kind\":2,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"bool\",\"Ident\":\"bool\",\"Kind\":1,\"PkgPath\":\"\",\"Methods\":null}]},\"MarshalBinary\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Scan\":{\"In\":[{\"Name\":\"\",\"Ident\":\"interface {}\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"String\":{\"In\":[],\"Out\":[{\"Name\":\"string\",\"Ident\":\"string\",\"Kind\":24,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalBinary\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Value\":{\"In\":[],\"Out\":[{\"Name\":\"Value\",\"Ident\":\"driver.Value\",\"Kind\":20,\"PkgPath\":\"database/sql/driver\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"settings\",\"type\":{\"Type\":3,\"Ident\":\"*types.GroupSetting\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"GroupSetting\",\"Ident\":\"types.GroupSetting\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"default\":true,\"default_value\":{},\"default_kind\":22,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"storage_policy_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Metadata\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"file\",\"type\":\"File\",\"field\":\"file_id\",\"ref_name\":\"metadata\",\"unique\":true,\"inverse\":true,\"required\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"value\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"file_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"is_public\",\"type\":{\"Type\":1,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":false,\"default_kind\":1,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}}],\"indexes\":[{\"unique\":true,\"fields\":[\"file_id\",\"name\"]}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Node\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"storage_policy\",\"type\":\"StoragePolicy\"}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"status\",\"type\":{\"Type\":6,\"Ident\":\"node.Status\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"enums\":[{\"N\":\"active\",\"V\":\"active\"},{\"N\":\"suspended\",\"V\":\"suspended\"}],\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"type\",\"type\":{\"Type\":6,\"Ident\":\"node.Type\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"enums\":[{\"N\":\"master\",\"V\":\"master\"},{\"N\":\"slave\",\"V\":\"slave\"}],\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"server\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"slave_key\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"capabilities\",\"type\":{\"Type\":5,\"Ident\":\"*boolset.BooleanSet\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"PkgName\":\"boolset\",\"Nillable\":true,\"RType\":{\"Name\":\"BooleanSet\",\"Ident\":\"boolset.BooleanSet\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"Methods\":{\"Enabled\":{\"In\":[{\"Name\":\"int\",\"Ident\":\"int\",\"Kind\":2,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"bool\",\"Ident\":\"bool\",\"Kind\":1,\"PkgPath\":\"\",\"Methods\":null}]},\"MarshalBinary\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Scan\":{\"In\":[{\"Name\":\"\",\"Ident\":\"interface {}\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"String\":{\"In\":[],\"Out\":[{\"Name\":\"string\",\"Ident\":\"string\",\"Kind\":24,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalBinary\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Value\":{\"In\":[],\"Out\":[{\"Name\":\"Value\",\"Ident\":\"driver.Value\",\"Kind\":20,\"PkgPath\":\"database/sql/driver\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"settings\",\"type\":{\"Type\":3,\"Ident\":\"*types.NodeSetting\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"NodeSetting\",\"Ident\":\"types.NodeSetting\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"default\":true,\"default_value\":{},\"default_kind\":22,\"position\":{\"Index\":6,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"weight\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":0,\"default_kind\":2,\"position\":{\"Index\":7,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Passkey\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"user\",\"type\":\"User\",\"field\":\"user_id\",\"ref_name\":\"passkey\",\"unique\":true,\"inverse\":true,\"required\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"user_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"credential_id\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"credential\",\"type\":{\"Type\":3,\"Ident\":\"*webauthn.Credential\",\"PkgPath\":\"github.com/go-webauthn/webauthn/webauthn\",\"PkgName\":\"webauthn\",\"Nillable\":true,\"RType\":{\"Name\":\"Credential\",\"Ident\":\"webauthn.Credential\",\"Kind\":22,\"PkgPath\":\"github.com/go-webauthn/webauthn/webauthn\",\"Methods\":{\"Descriptor\":{\"In\":[],\"Out\":[{\"Name\":\"CredentialDescriptor\",\"Ident\":\"protocol.CredentialDescriptor\",\"Kind\":25,\"PkgPath\":\"github.com/go-webauthn/webauthn/protocol\",\"Methods\":null}]},\"Verify\":{\"In\":[{\"Name\":\"Provider\",\"Ident\":\"metadata.Provider\",\"Kind\":20,\"PkgPath\":\"github.com/go-webauthn/webauthn/metadata\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0},\"sensitive\":true},{\"name\":\"used_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}}],\"indexes\":[{\"unique\":true,\"fields\":[\"user_id\",\"credential_id\"]}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Setting\",\"config\":{\"Table\":\"\"},\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"unique\":true,\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"value\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"optional\":true,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Share\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"user\",\"type\":\"User\",\"ref_name\":\"shares\",\"unique\":true,\"inverse\":true},{\"name\":\"file\",\"type\":\"File\",\"ref_name\":\"shares\",\"unique\":true,\"inverse\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"password\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"views\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":0,\"default_kind\":2,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"downloads\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":0,\"default_kind\":2,\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"expires\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"remain_downloads\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"props\",\"type\":{\"Type\":3,\"Ident\":\"*types.ShareProps\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"ShareProps\",\"Ident\":\"types.ShareProps\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"StoragePolicy\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"groups\",\"type\":\"Group\"},{\"name\":\"files\",\"type\":\"File\"},{\"name\":\"entities\",\"type\":\"Entity\"},{\"name\":\"node\",\"type\":\"Node\",\"field\":\"node_id\",\"ref_name\":\"storage_policy\",\"unique\":true,\"inverse\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"type\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"server\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"bucket_name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"is_private\",\"type\":{\"Type\":1,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"access_key\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"optional\":true,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"secret_key\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"optional\":true,\"position\":{\"Index\":6,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"max_size\",\"type\":{\"Type\":13,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":7,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"dir_name_rule\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":8,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"file_name_rule\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":9,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"settings\",\"type\":{\"Type\":3,\"Ident\":\"*types.PolicySetting\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"PolicySetting\",\"Ident\":\"types.PolicySetting\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"default\":true,\"default_value\":{\"file_type\":null,\"native_media_processing\":false,\"s3_path_style\":false,\"token\":\"\"},\"default_kind\":22,\"position\":{\"Index\":10,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"node_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":11,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Task\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"user\",\"type\":\"User\",\"field\":\"user_tasks\",\"ref_name\":\"tasks\",\"unique\":true,\"inverse\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"type\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"status\",\"type\":{\"Type\":6,\"Ident\":\"task.Status\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"enums\":[{\"N\":\"queued\",\"V\":\"queued\"},{\"N\":\"processing\",\"V\":\"processing\"},{\"N\":\"suspending\",\"V\":\"suspending\"},{\"N\":\"error\",\"V\":\"error\"},{\"N\":\"canceled\",\"V\":\"canceled\"},{\"N\":\"completed\",\"V\":\"completed\"}],\"default\":true,\"default_value\":\"queued\",\"default_kind\":24,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"public_state\",\"type\":{\"Type\":3,\"Ident\":\"*types.TaskPublicState\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"TaskPublicState\",\"Ident\":\"types.TaskPublicState\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"private_state\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"optional\":true,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"correlation_id\",\"type\":{\"Type\":4,\"Ident\":\"uuid.UUID\",\"PkgPath\":\"github.com/gofrs/uuid\",\"PkgName\":\"uuid\",\"Nillable\":false,\"RType\":{\"Name\":\"UUID\",\"Ident\":\"uuid.UUID\",\"Kind\":17,\"PkgPath\":\"github.com/gofrs/uuid\",\"Methods\":{\"Bytes\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}]},\"Format\":{\"In\":[{\"Name\":\"State\",\"Ident\":\"fmt.State\",\"Kind\":20,\"PkgPath\":\"fmt\",\"Methods\":null},{\"Name\":\"int32\",\"Ident\":\"int32\",\"Kind\":5,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"MarshalBinary\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"MarshalText\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Scan\":{\"In\":[{\"Name\":\"\",\"Ident\":\"interface {}\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"SetVariant\":{\"In\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"SetVersion\":{\"In\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"String\":{\"In\":[],\"Out\":[{\"Name\":\"string\",\"Ident\":\"string\",\"Kind\":24,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalBinary\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalText\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Value\":{\"In\":[],\"Out\":[{\"Name\":\"Value\",\"Ident\":\"driver.Value\",\"Kind\":20,\"PkgPath\":\"database/sql/driver\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Variant\":{\"In\":[],\"Out\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}]},\"Version\":{\"In\":[],\"Out\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"optional\":true,\"immutable\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"user_tasks\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"User\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"group\",\"type\":\"Group\",\"field\":\"group_users\",\"ref_name\":\"users\",\"unique\":true,\"inverse\":true,\"required\":true},{\"name\":\"files\",\"type\":\"File\"},{\"name\":\"dav_accounts\",\"type\":\"DavAccount\"},{\"name\":\"shares\",\"type\":\"Share\"},{\"name\":\"passkey\",\"type\":\"Passkey\"},{\"name\":\"tasks\",\"type\":\"Task\"},{\"name\":\"entities\",\"type\":\"Entity\"}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"email\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":100,\"unique\":true,\"validators\":1,\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"nick\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":100,\"validators\":1,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"password\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0},\"sensitive\":true},{\"name\":\"status\",\"type\":{\"Type\":6,\"Ident\":\"user.Status\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"enums\":[{\"N\":\"active\",\"V\":\"active\"},{\"N\":\"inactive\",\"V\":\"inactive\"},{\"N\":\"manual_banned\",\"V\":\"manual_banned\"},{\"N\":\"sys_banned\",\"V\":\"sys_banned\"}],\"default\":true,\"default_value\":\"active\",\"default_kind\":24,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"storage\",\"type\":{\"Type\":13,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":0,\"default_kind\":6,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"two_factor_secret\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0},\"sensitive\":true},{\"name\":\"avatar\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":6,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"settings\",\"type\":{\"Type\":3,\"Ident\":\"*types.UserSetting\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"UserSetting\",\"Ident\":\"types.UserSetting\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"default\":true,\"default_value\":{},\"default_kind\":22,\"position\":{\"Index\":7,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"group_users\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":8,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]}],\"Features\":[\"intercept\",\"schema/snapshot\",\"sql/upsert\",\"sql/upsert\",\"sql/execquery\"]}" +const Schema = "{\"Schema\":\"github.com/cloudreve/Cloudreve/v4/ent/schema\",\"Package\":\"github.com/cloudreve/Cloudreve/v4/ent\",\"Schemas\":[{\"name\":\"DavAccount\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"owner\",\"type\":\"User\",\"field\":\"owner_id\",\"ref_name\":\"dav_accounts\",\"unique\":true,\"inverse\":true,\"required\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"uri\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"password\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0},\"sensitive\":true},{\"name\":\"options\",\"type\":{\"Type\":5,\"Ident\":\"*boolset.BooleanSet\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"PkgName\":\"boolset\",\"Nillable\":true,\"RType\":{\"Name\":\"BooleanSet\",\"Ident\":\"boolset.BooleanSet\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"Methods\":{\"Enabled\":{\"In\":[{\"Name\":\"int\",\"Ident\":\"int\",\"Kind\":2,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"bool\",\"Ident\":\"bool\",\"Kind\":1,\"PkgPath\":\"\",\"Methods\":null}]},\"MarshalBinary\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Scan\":{\"In\":[{\"Name\":\"\",\"Ident\":\"interface {}\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"String\":{\"In\":[],\"Out\":[{\"Name\":\"string\",\"Ident\":\"string\",\"Kind\":24,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalBinary\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Value\":{\"In\":[],\"Out\":[{\"Name\":\"Value\",\"Ident\":\"driver.Value\",\"Kind\":20,\"PkgPath\":\"database/sql/driver\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"props\",\"type\":{\"Type\":3,\"Ident\":\"*types.DavAccountProps\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"DavAccountProps\",\"Ident\":\"types.DavAccountProps\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"owner_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}}],\"indexes\":[{\"unique\":true,\"fields\":[\"owner_id\",\"password\"]}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"DirectLink\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"file\",\"type\":\"File\",\"field\":\"file_id\",\"ref_name\":\"direct_links\",\"unique\":true,\"inverse\":true,\"required\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"downloads\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"file_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"speed\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Entity\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"file\",\"type\":\"File\",\"ref_name\":\"entities\",\"inverse\":true},{\"name\":\"user\",\"type\":\"User\",\"field\":\"created_by\",\"ref_name\":\"entities\",\"unique\":true,\"inverse\":true},{\"name\":\"storage_policy\",\"type\":\"StoragePolicy\",\"field\":\"storage_policy_entities\",\"ref_name\":\"entities\",\"unique\":true,\"inverse\":true,\"required\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"type\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"source\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"size\",\"type\":{\"Type\":13,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"reference_count\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":1,\"default_kind\":2,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"storage_policy_entities\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"created_by\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"upload_session_id\",\"type\":{\"Type\":4,\"Ident\":\"uuid.UUID\",\"PkgPath\":\"github.com/gofrs/uuid\",\"PkgName\":\"uuid\",\"Nillable\":false,\"RType\":{\"Name\":\"UUID\",\"Ident\":\"uuid.UUID\",\"Kind\":17,\"PkgPath\":\"github.com/gofrs/uuid\",\"Methods\":{\"Bytes\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}]},\"Format\":{\"In\":[{\"Name\":\"State\",\"Ident\":\"fmt.State\",\"Kind\":20,\"PkgPath\":\"fmt\",\"Methods\":null},{\"Name\":\"int32\",\"Ident\":\"int32\",\"Kind\":5,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"MarshalBinary\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"MarshalText\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Scan\":{\"In\":[{\"Name\":\"\",\"Ident\":\"interface {}\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"SetVariant\":{\"In\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"SetVersion\":{\"In\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"String\":{\"In\":[],\"Out\":[{\"Name\":\"string\",\"Ident\":\"string\",\"Kind\":24,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalBinary\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalText\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Value\":{\"In\":[],\"Out\":[{\"Name\":\"Value\",\"Ident\":\"driver.Value\",\"Kind\":20,\"PkgPath\":\"database/sql/driver\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Variant\":{\"In\":[],\"Out\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}]},\"Version\":{\"In\":[],\"Out\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":6,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"recycle_options\",\"type\":{\"Type\":3,\"Ident\":\"*types.EntityRecycleOption\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"EntityRecycleOption\",\"Ident\":\"types.EntityRecycleOption\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"position\":{\"Index\":7,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"File\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"owner\",\"type\":\"User\",\"field\":\"owner_id\",\"ref_name\":\"files\",\"unique\":true,\"inverse\":true,\"required\":true},{\"name\":\"storage_policies\",\"type\":\"StoragePolicy\",\"field\":\"storage_policy_files\",\"ref_name\":\"files\",\"unique\":true,\"inverse\":true},{\"name\":\"parent\",\"type\":\"File\",\"field\":\"file_children\",\"ref\":{\"name\":\"children\",\"type\":\"File\"},\"unique\":true,\"inverse\":true},{\"name\":\"metadata\",\"type\":\"Metadata\"},{\"name\":\"entities\",\"type\":\"Entity\"},{\"name\":\"shares\",\"type\":\"Share\"},{\"name\":\"direct_links\",\"type\":\"DirectLink\"}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"type\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"owner_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"size\",\"type\":{\"Type\":13,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":0,\"default_kind\":6,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"primary_entity\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":6,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"file_children\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":7,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"is_symbolic\",\"type\":{\"Type\":1,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":false,\"default_kind\":1,\"position\":{\"Index\":8,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"props\",\"type\":{\"Type\":3,\"Ident\":\"*types.FileProps\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"FileProps\",\"Ident\":\"types.FileProps\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"position\":{\"Index\":9,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"storage_policy_files\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":10,\"MixedIn\":false,\"MixinIndex\":0}}],\"indexes\":[{\"unique\":true,\"fields\":[\"file_children\",\"name\"]},{\"fields\":[\"file_children\",\"type\",\"updated_at\"]},{\"fields\":[\"file_children\",\"type\",\"size\"]}],\"hooks\":[{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}]},{\"name\":\"Group\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"users\",\"type\":\"User\"},{\"name\":\"storage_policies\",\"type\":\"StoragePolicy\",\"field\":\"storage_policy_id\",\"ref_name\":\"groups\",\"unique\":true,\"inverse\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"max_storage\",\"type\":{\"Type\":13,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"speed_limit\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"permissions\",\"type\":{\"Type\":5,\"Ident\":\"*boolset.BooleanSet\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"PkgName\":\"boolset\",\"Nillable\":true,\"RType\":{\"Name\":\"BooleanSet\",\"Ident\":\"boolset.BooleanSet\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"Methods\":{\"Enabled\":{\"In\":[{\"Name\":\"int\",\"Ident\":\"int\",\"Kind\":2,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"bool\",\"Ident\":\"bool\",\"Kind\":1,\"PkgPath\":\"\",\"Methods\":null}]},\"MarshalBinary\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Scan\":{\"In\":[{\"Name\":\"\",\"Ident\":\"interface {}\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"String\":{\"In\":[],\"Out\":[{\"Name\":\"string\",\"Ident\":\"string\",\"Kind\":24,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalBinary\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Value\":{\"In\":[],\"Out\":[{\"Name\":\"Value\",\"Ident\":\"driver.Value\",\"Kind\":20,\"PkgPath\":\"database/sql/driver\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"settings\",\"type\":{\"Type\":3,\"Ident\":\"*types.GroupSetting\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"GroupSetting\",\"Ident\":\"types.GroupSetting\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"default\":true,\"default_value\":{},\"default_kind\":22,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"storage_policy_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Metadata\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"file\",\"type\":\"File\",\"field\":\"file_id\",\"ref_name\":\"metadata\",\"unique\":true,\"inverse\":true,\"required\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"value\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"file_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"is_public\",\"type\":{\"Type\":1,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":false,\"default_kind\":1,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}}],\"indexes\":[{\"unique\":true,\"fields\":[\"file_id\",\"name\"]}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Node\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"storage_policy\",\"type\":\"StoragePolicy\"}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"status\",\"type\":{\"Type\":6,\"Ident\":\"node.Status\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"enums\":[{\"N\":\"active\",\"V\":\"active\"},{\"N\":\"suspended\",\"V\":\"suspended\"}],\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"type\",\"type\":{\"Type\":6,\"Ident\":\"node.Type\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"enums\":[{\"N\":\"master\",\"V\":\"master\"},{\"N\":\"slave\",\"V\":\"slave\"}],\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"server\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"slave_key\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"capabilities\",\"type\":{\"Type\":5,\"Ident\":\"*boolset.BooleanSet\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"PkgName\":\"boolset\",\"Nillable\":true,\"RType\":{\"Name\":\"BooleanSet\",\"Ident\":\"boolset.BooleanSet\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"Methods\":{\"Enabled\":{\"In\":[{\"Name\":\"int\",\"Ident\":\"int\",\"Kind\":2,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"bool\",\"Ident\":\"bool\",\"Kind\":1,\"PkgPath\":\"\",\"Methods\":null}]},\"MarshalBinary\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Scan\":{\"In\":[{\"Name\":\"\",\"Ident\":\"interface {}\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"String\":{\"In\":[],\"Out\":[{\"Name\":\"string\",\"Ident\":\"string\",\"Kind\":24,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalBinary\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Value\":{\"In\":[],\"Out\":[{\"Name\":\"Value\",\"Ident\":\"driver.Value\",\"Kind\":20,\"PkgPath\":\"database/sql/driver\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"settings\",\"type\":{\"Type\":3,\"Ident\":\"*types.NodeSetting\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"NodeSetting\",\"Ident\":\"types.NodeSetting\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"default\":true,\"default_value\":{},\"default_kind\":22,\"position\":{\"Index\":6,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"weight\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":0,\"default_kind\":2,\"position\":{\"Index\":7,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Passkey\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"user\",\"type\":\"User\",\"field\":\"user_id\",\"ref_name\":\"passkey\",\"unique\":true,\"inverse\":true,\"required\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"user_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"credential_id\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"credential\",\"type\":{\"Type\":3,\"Ident\":\"*webauthn.Credential\",\"PkgPath\":\"github.com/go-webauthn/webauthn/webauthn\",\"PkgName\":\"webauthn\",\"Nillable\":true,\"RType\":{\"Name\":\"Credential\",\"Ident\":\"webauthn.Credential\",\"Kind\":22,\"PkgPath\":\"github.com/go-webauthn/webauthn/webauthn\",\"Methods\":{\"Descriptor\":{\"In\":[],\"Out\":[{\"Name\":\"CredentialDescriptor\",\"Ident\":\"protocol.CredentialDescriptor\",\"Kind\":25,\"PkgPath\":\"github.com/go-webauthn/webauthn/protocol\",\"Methods\":null}]},\"Verify\":{\"In\":[{\"Name\":\"Provider\",\"Ident\":\"metadata.Provider\",\"Kind\":20,\"PkgPath\":\"github.com/go-webauthn/webauthn/metadata\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0},\"sensitive\":true},{\"name\":\"used_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}}],\"indexes\":[{\"unique\":true,\"fields\":[\"user_id\",\"credential_id\"]}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Setting\",\"config\":{\"Table\":\"\"},\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"unique\":true,\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"value\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"optional\":true,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Share\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"user\",\"type\":\"User\",\"ref_name\":\"shares\",\"unique\":true,\"inverse\":true},{\"name\":\"file\",\"type\":\"File\",\"ref_name\":\"shares\",\"unique\":true,\"inverse\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"password\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"views\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":0,\"default_kind\":2,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"downloads\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":0,\"default_kind\":2,\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"expires\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"remain_downloads\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"props\",\"type\":{\"Type\":3,\"Ident\":\"*types.ShareProps\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"ShareProps\",\"Ident\":\"types.ShareProps\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"StoragePolicy\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"groups\",\"type\":\"Group\"},{\"name\":\"files\",\"type\":\"File\"},{\"name\":\"entities\",\"type\":\"Entity\"},{\"name\":\"node\",\"type\":\"Node\",\"field\":\"node_id\",\"ref_name\":\"storage_policy\",\"unique\":true,\"inverse\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"type\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"server\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"bucket_name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"is_private\",\"type\":{\"Type\":1,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"access_key\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"optional\":true,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"secret_key\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"optional\":true,\"position\":{\"Index\":6,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"max_size\",\"type\":{\"Type\":13,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":7,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"dir_name_rule\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":8,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"file_name_rule\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":9,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"settings\",\"type\":{\"Type\":3,\"Ident\":\"*types.PolicySetting\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"PolicySetting\",\"Ident\":\"types.PolicySetting\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"default\":true,\"default_value\":{\"file_type\":null,\"native_media_processing\":false,\"s3_path_style\":false,\"token\":\"\"},\"default_kind\":22,\"position\":{\"Index\":10,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"node_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":11,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Task\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"user\",\"type\":\"User\",\"field\":\"user_tasks\",\"ref_name\":\"tasks\",\"unique\":true,\"inverse\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"type\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"status\",\"type\":{\"Type\":6,\"Ident\":\"task.Status\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"enums\":[{\"N\":\"queued\",\"V\":\"queued\"},{\"N\":\"processing\",\"V\":\"processing\"},{\"N\":\"suspending\",\"V\":\"suspending\"},{\"N\":\"error\",\"V\":\"error\"},{\"N\":\"canceled\",\"V\":\"canceled\"},{\"N\":\"completed\",\"V\":\"completed\"}],\"default\":true,\"default_value\":\"queued\",\"default_kind\":24,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"public_state\",\"type\":{\"Type\":3,\"Ident\":\"*types.TaskPublicState\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"TaskPublicState\",\"Ident\":\"types.TaskPublicState\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"private_state\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"optional\":true,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"correlation_id\",\"type\":{\"Type\":4,\"Ident\":\"uuid.UUID\",\"PkgPath\":\"github.com/gofrs/uuid\",\"PkgName\":\"uuid\",\"Nillable\":false,\"RType\":{\"Name\":\"UUID\",\"Ident\":\"uuid.UUID\",\"Kind\":17,\"PkgPath\":\"github.com/gofrs/uuid\",\"Methods\":{\"Bytes\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}]},\"Format\":{\"In\":[{\"Name\":\"State\",\"Ident\":\"fmt.State\",\"Kind\":20,\"PkgPath\":\"fmt\",\"Methods\":null},{\"Name\":\"int32\",\"Ident\":\"int32\",\"Kind\":5,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"MarshalBinary\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"MarshalText\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Scan\":{\"In\":[{\"Name\":\"\",\"Ident\":\"interface {}\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"SetVariant\":{\"In\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"SetVersion\":{\"In\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"String\":{\"In\":[],\"Out\":[{\"Name\":\"string\",\"Ident\":\"string\",\"Kind\":24,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalBinary\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalText\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Value\":{\"In\":[],\"Out\":[{\"Name\":\"Value\",\"Ident\":\"driver.Value\",\"Kind\":20,\"PkgPath\":\"database/sql/driver\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Variant\":{\"In\":[],\"Out\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}]},\"Version\":{\"In\":[],\"Out\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"optional\":true,\"immutable\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"user_tasks\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"User\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"group\",\"type\":\"Group\",\"field\":\"group_users\",\"ref_name\":\"users\",\"unique\":true,\"inverse\":true,\"required\":true},{\"name\":\"files\",\"type\":\"File\"},{\"name\":\"dav_accounts\",\"type\":\"DavAccount\"},{\"name\":\"shares\",\"type\":\"Share\"},{\"name\":\"passkey\",\"type\":\"Passkey\"},{\"name\":\"tasks\",\"type\":\"Task\"},{\"name\":\"entities\",\"type\":\"Entity\"}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"email\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":100,\"unique\":true,\"validators\":1,\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"nick\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":100,\"validators\":1,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"password\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0},\"sensitive\":true},{\"name\":\"status\",\"type\":{\"Type\":6,\"Ident\":\"user.Status\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"enums\":[{\"N\":\"active\",\"V\":\"active\"},{\"N\":\"inactive\",\"V\":\"inactive\"},{\"N\":\"manual_banned\",\"V\":\"manual_banned\"},{\"N\":\"sys_banned\",\"V\":\"sys_banned\"}],\"default\":true,\"default_value\":\"active\",\"default_kind\":24,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"storage\",\"type\":{\"Type\":13,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":0,\"default_kind\":6,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"two_factor_secret\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0},\"sensitive\":true},{\"name\":\"avatar\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":6,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"settings\",\"type\":{\"Type\":3,\"Ident\":\"*types.UserSetting\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"UserSetting\",\"Ident\":\"types.UserSetting\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"default\":true,\"default_value\":{},\"default_kind\":22,\"position\":{\"Index\":7,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"group_users\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":8,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]}],\"Features\":[\"intercept\",\"schema/snapshot\",\"sql/upsert\",\"sql/upsert\",\"sql/execquery\"]}" diff --git a/ent/migrate/schema.go b/ent/migrate/schema.go index 84465d16..8b85556a 100644 --- a/ent/migrate/schema.go +++ b/ent/migrate/schema.go @@ -107,7 +107,6 @@ var ( {Name: "id", Type: field.TypeInt, Increment: true}, {Name: "created_at", Type: field.TypeTime, SchemaType: map[string]string{"mysql": "datetime"}}, {Name: "updated_at", Type: field.TypeTime, SchemaType: map[string]string{"mysql": "datetime"}}, - {Name: "deleted_at", Type: field.TypeTime, Nullable: true, SchemaType: map[string]string{"mysql": "datetime"}}, {Name: "type", Type: field.TypeInt}, {Name: "name", Type: field.TypeString}, {Name: "size", Type: field.TypeInt64, Default: 0}, @@ -126,19 +125,19 @@ var ( ForeignKeys: []*schema.ForeignKey{ { Symbol: "files_files_children", - Columns: []*schema.Column{FilesColumns[10]}, + Columns: []*schema.Column{FilesColumns[9]}, RefColumns: []*schema.Column{FilesColumns[0]}, OnDelete: schema.SetNull, }, { Symbol: "files_storage_policies_files", - Columns: []*schema.Column{FilesColumns[11]}, + Columns: []*schema.Column{FilesColumns[10]}, RefColumns: []*schema.Column{StoragePoliciesColumns[0]}, OnDelete: schema.SetNull, }, { Symbol: "files_users_files", - Columns: []*schema.Column{FilesColumns[12]}, + Columns: []*schema.Column{FilesColumns[11]}, RefColumns: []*schema.Column{UsersColumns[0]}, OnDelete: schema.NoAction, }, @@ -147,17 +146,17 @@ var ( { Name: "file_file_children_name", Unique: true, - Columns: []*schema.Column{FilesColumns[10], FilesColumns[5]}, + Columns: []*schema.Column{FilesColumns[9], FilesColumns[4]}, }, { Name: "file_file_children_type_updated_at", Unique: false, - Columns: []*schema.Column{FilesColumns[10], FilesColumns[4], FilesColumns[2]}, + Columns: []*schema.Column{FilesColumns[9], FilesColumns[3], FilesColumns[2]}, }, { Name: "file_file_children_type_size", Unique: false, - Columns: []*schema.Column{FilesColumns[10], FilesColumns[4], FilesColumns[6]}, + Columns: []*schema.Column{FilesColumns[9], FilesColumns[3], FilesColumns[5]}, }, }, } diff --git a/ent/mutation.go b/ent/mutation.go index 2620a599..5a612a26 100644 --- a/ent/mutation.go +++ b/ent/mutation.go @@ -2972,7 +2972,6 @@ type FileMutation struct { id *int created_at *time.Time updated_at *time.Time - deleted_at *time.Time _type *int add_type *int name *string @@ -3179,55 +3178,6 @@ func (m *FileMutation) ResetUpdatedAt() { m.updated_at = nil } -// SetDeletedAt sets the "deleted_at" field. -func (m *FileMutation) SetDeletedAt(t time.Time) { - m.deleted_at = &t -} - -// DeletedAt returns the value of the "deleted_at" field in the mutation. -func (m *FileMutation) DeletedAt() (r time.Time, exists bool) { - v := m.deleted_at - if v == nil { - return - } - return *v, true -} - -// OldDeletedAt returns the old "deleted_at" field's value of the File entity. -// If the File object wasn't provided to the builder, the object is fetched from the database. -// An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *FileMutation) OldDeletedAt(ctx context.Context) (v *time.Time, err error) { - if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldDeletedAt is only allowed on UpdateOne operations") - } - if m.id == nil || m.oldValue == nil { - return v, errors.New("OldDeletedAt requires an ID field in the mutation") - } - oldValue, err := m.oldValue(ctx) - if err != nil { - return v, fmt.Errorf("querying old value for OldDeletedAt: %w", err) - } - return oldValue.DeletedAt, nil -} - -// ClearDeletedAt clears the value of the "deleted_at" field. -func (m *FileMutation) ClearDeletedAt() { - m.deleted_at = nil - m.clearedFields[file.FieldDeletedAt] = struct{}{} -} - -// DeletedAtCleared returns if the "deleted_at" field was cleared in this mutation. -func (m *FileMutation) DeletedAtCleared() bool { - _, ok := m.clearedFields[file.FieldDeletedAt] - return ok -} - -// ResetDeletedAt resets all changes to the "deleted_at" field. -func (m *FileMutation) ResetDeletedAt() { - m.deleted_at = nil - delete(m.clearedFields, file.FieldDeletedAt) -} - // SetType sets the "type" field. func (m *FileMutation) SetType(i int) { m._type = &i @@ -4076,16 +4026,13 @@ func (m *FileMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *FileMutation) Fields() []string { - fields := make([]string, 0, 12) + fields := make([]string, 0, 11) if m.created_at != nil { fields = append(fields, file.FieldCreatedAt) } if m.updated_at != nil { fields = append(fields, file.FieldUpdatedAt) } - if m.deleted_at != nil { - fields = append(fields, file.FieldDeletedAt) - } if m._type != nil { fields = append(fields, file.FieldType) } @@ -4125,8 +4072,6 @@ func (m *FileMutation) Field(name string) (ent.Value, bool) { return m.CreatedAt() case file.FieldUpdatedAt: return m.UpdatedAt() - case file.FieldDeletedAt: - return m.DeletedAt() case file.FieldType: return m.GetType() case file.FieldName: @@ -4158,8 +4103,6 @@ func (m *FileMutation) OldField(ctx context.Context, name string) (ent.Value, er return m.OldCreatedAt(ctx) case file.FieldUpdatedAt: return m.OldUpdatedAt(ctx) - case file.FieldDeletedAt: - return m.OldDeletedAt(ctx) case file.FieldType: return m.OldType(ctx) case file.FieldName: @@ -4201,13 +4144,6 @@ func (m *FileMutation) SetField(name string, value ent.Value) error { } m.SetUpdatedAt(v) return nil - case file.FieldDeletedAt: - v, ok := value.(time.Time) - if !ok { - return fmt.Errorf("unexpected type %T for field %s", value, name) - } - m.SetDeletedAt(v) - return nil case file.FieldType: v, ok := value.(int) if !ok { @@ -4340,9 +4276,6 @@ func (m *FileMutation) AddField(name string, value ent.Value) error { // mutation. func (m *FileMutation) ClearedFields() []string { var fields []string - if m.FieldCleared(file.FieldDeletedAt) { - fields = append(fields, file.FieldDeletedAt) - } if m.FieldCleared(file.FieldPrimaryEntity) { fields = append(fields, file.FieldPrimaryEntity) } @@ -4369,9 +4302,6 @@ func (m *FileMutation) FieldCleared(name string) bool { // error if the field is not defined in the schema. func (m *FileMutation) ClearField(name string) error { switch name { - case file.FieldDeletedAt: - m.ClearDeletedAt() - return nil case file.FieldPrimaryEntity: m.ClearPrimaryEntity() return nil @@ -4398,9 +4328,6 @@ func (m *FileMutation) ResetField(name string) error { case file.FieldUpdatedAt: m.ResetUpdatedAt() return nil - case file.FieldDeletedAt: - m.ResetDeletedAt() - return nil case file.FieldType: m.ResetType() return nil diff --git a/ent/runtime/runtime.go b/ent/runtime/runtime.go index cdc8fa56..90100484 100644 --- a/ent/runtime/runtime.go +++ b/ent/runtime/runtime.go @@ -87,31 +87,24 @@ func init() { entityDescReferenceCount := entityFields[3].Descriptor() // entity.DefaultReferenceCount holds the default value on creation for the reference_count field. entity.DefaultReferenceCount = entityDescReferenceCount.Default.(int) - fileMixin := schema.File{}.Mixin() - fileMixinHooks0 := fileMixin[0].Hooks() - file.Hooks[0] = fileMixinHooks0[0] - fileMixinInters0 := fileMixin[0].Interceptors() - file.Interceptors[0] = fileMixinInters0[0] - fileMixinFields0 := fileMixin[0].Fields() - _ = fileMixinFields0 + fileHooks := schema.File{}.Hooks() + file.Hooks[0] = fileHooks[0] fileFields := schema.File{}.Fields() _ = fileFields // fileDescCreatedAt is the schema descriptor for created_at field. - fileDescCreatedAt := fileMixinFields0[0].Descriptor() + fileDescCreatedAt := fileFields[0].Descriptor() // file.DefaultCreatedAt holds the default value on creation for the created_at field. file.DefaultCreatedAt = fileDescCreatedAt.Default.(func() time.Time) // fileDescUpdatedAt is the schema descriptor for updated_at field. - fileDescUpdatedAt := fileMixinFields0[1].Descriptor() + fileDescUpdatedAt := fileFields[1].Descriptor() // file.DefaultUpdatedAt holds the default value on creation for the updated_at field. file.DefaultUpdatedAt = fileDescUpdatedAt.Default.(func() time.Time) - // file.UpdateDefaultUpdatedAt holds the default value on update for the updated_at field. - file.UpdateDefaultUpdatedAt = fileDescUpdatedAt.UpdateDefault.(func() time.Time) // fileDescSize is the schema descriptor for size field. - fileDescSize := fileFields[3].Descriptor() + fileDescSize := fileFields[5].Descriptor() // file.DefaultSize holds the default value on creation for the size field. file.DefaultSize = fileDescSize.Default.(int64) // fileDescIsSymbolic is the schema descriptor for is_symbolic field. - fileDescIsSymbolic := fileFields[6].Descriptor() + fileDescIsSymbolic := fileFields[8].Descriptor() // file.DefaultIsSymbolic holds the default value on creation for the is_symbolic field. file.DefaultIsSymbolic = fileDescIsSymbolic.Default.(bool) groupMixin := schema.Group{}.Mixin() diff --git a/ent/schema/file.go b/ent/schema/file.go index 4961fe24..31c92e09 100644 --- a/ent/schema/file.go +++ b/ent/schema/file.go @@ -1,10 +1,15 @@ package schema import ( + "context" + "time" + "entgo.io/ent" + "entgo.io/ent/dialect" "entgo.io/ent/schema/edge" "entgo.io/ent/schema/field" "entgo.io/ent/schema/index" + "github.com/cloudreve/Cloudreve/v4/ent/hook" "github.com/cloudreve/Cloudreve/v4/inventory/types" ) @@ -16,6 +21,17 @@ type File struct { // Fields of the File. func (File) Fields() []ent.Field { return []ent.Field{ + field.Time("created_at"). + Immutable(). + Default(time.Now). + SchemaType(map[string]string{ + dialect.MySQL: "datetime", + }), + field.Time("updated_at"). + Default(time.Now). + SchemaType(map[string]string{ + dialect.MySQL: "datetime", + }), field.Int("type"), field.String("name"), field.Int("owner_id"), @@ -66,8 +82,19 @@ func (File) Indexes() []ent.Index { } } -func (File) Mixin() []ent.Mixin { - return []ent.Mixin{ - CommonMixin{}, +func (f File) Hooks() []ent.Hook { + return []ent.Hook{ + hook.On(func(next ent.Mutator) ent.Mutator { + return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) { + if s, ok := m.(interface{ SetUpdatedAt(time.Time) }); ok { + _, set := m.Field("updated_at") + if !set { + s.SetUpdatedAt(time.Now()) + } + } + v, err := next.Mutate(ctx, m) + return v, err + }) + }, ent.OpUpdate|ent.OpUpdateOne), } } diff --git a/inventory/file.go b/inventory/file.go index ac88eac6..b6d81f73 100644 --- a/inventory/file.go +++ b/inventory/file.go @@ -217,6 +217,8 @@ type FileClient interface { ListEntities(ctx context.Context, args *ListEntityParameters) (*ListEntityResult, error) // UpdateProps updates props of a file UpdateProps(ctx context.Context, file *ent.File, props *types.FileProps) (*ent.File, error) + // UpdateModifiedAt updates modified at of a file + UpdateModifiedAt(ctx context.Context, file *ent.File, modifiedAt time.Time) error } func NewFileClient(client *ent.Client, dbType conf.DBType, hasher hashid.Encoder) FileClient { @@ -646,6 +648,10 @@ func (f *fileClient) Copy(ctx context.Context, files []*ent.File, dstMap map[int return newDstMap, map[int]int64{dstMap[files[0].FileChildren][0].OwnerID: sizeDiff}, nil } +func (f *fileClient) UpdateModifiedAt(ctx context.Context, file *ent.File, modifiedAt time.Time) error { + return f.client.File.UpdateOne(file).SetUpdatedAt(modifiedAt).Exec(ctx) +} + func (f *fileClient) UpsertMetadata(ctx context.Context, file *ent.File, data map[string]string, privateMask map[string]bool) error { // Validate value length for key, value := range data { @@ -718,10 +724,15 @@ func (f *fileClient) UpgradePlaceholder(ctx context.Context, file *ent.File, mod } if entityType == types.EntityTypeVersion { - if err := f.client.File.UpdateOne(file). + stm := f.client.File.UpdateOne(file). SetSize(placeholder.Size). - SetPrimaryEntity(placeholder.ID). - Exec(ctx); err != nil { + SetPrimaryEntity(placeholder.ID) + + if modifiedAt != nil { + stm.SetUpdatedAt(*modifiedAt) + } + + if err := stm.Exec(ctx); err != nil { return fmt.Errorf("failed to upgrade file primary entity: %v", err) } } diff --git a/pkg/filemanager/fs/dbfs/props.go b/pkg/filemanager/fs/dbfs/props.go index 34c29100..04b39646 100644 --- a/pkg/filemanager/fs/dbfs/props.go +++ b/pkg/filemanager/fs/dbfs/props.go @@ -3,6 +3,7 @@ package dbfs import ( "context" "fmt" + "time" "github.com/cloudreve/Cloudreve/v4/inventory" "github.com/cloudreve/Cloudreve/v4/inventory/types" @@ -100,6 +101,7 @@ func (f *DBFS) PatchMetadata(ctx context.Context, path []*fs.URI, metas ...fs.Me metadataMap := make(map[string]string) privateMap := make(map[string]bool) deleted := make([]string, 0) + updateModifiedAt := false for _, meta := range metas { if meta.Remove { deleted = append(deleted, meta.Key) @@ -109,6 +111,9 @@ func (f *DBFS) PatchMetadata(ctx context.Context, path []*fs.URI, metas ...fs.Me if meta.Private { privateMap[meta.Key] = meta.Private } + if meta.UpdateModifiedAt { + updateModifiedAt = true + } } fc, tx, ctx, err := inventory.WithTx(ctx, f.fileClient) @@ -128,6 +133,13 @@ func (f *DBFS) PatchMetadata(ctx context.Context, path []*fs.URI, metas ...fs.Me return fmt.Errorf("failed to remove metadata: %w", err) } } + + if updateModifiedAt { + if err := fc.UpdateModifiedAt(ctx, target.Model, time.Now()); err != nil { + _ = inventory.Rollback(tx) + return fmt.Errorf("failed to update file modified at: %w", err) + } + } } if err := inventory.Commit(tx); err != nil { diff --git a/pkg/filemanager/fs/fs.go b/pkg/filemanager/fs/fs.go index b081340c..0121547c 100644 --- a/pkg/filemanager/fs/fs.go +++ b/pkg/filemanager/fs/fs.go @@ -203,10 +203,11 @@ type ( } MetadataPatch struct { - Key string `json:"key" binding:"required"` - Value string `json:"value"` - Private bool `json:"private" binding:"ne=true"` - Remove bool `json:"remove"` + Key string `json:"key" binding:"required"` + Value string `json:"value"` + Private bool `json:"private" binding:"ne=true"` + Remove bool `json:"remove"` + UpdateModifiedAt bool `json:"-"` } // ListFileResult result of listing files. diff --git a/pkg/filemanager/manager/metadata.go b/pkg/filemanager/manager/metadata.go index 9f6cb397..69797fbf 100644 --- a/pkg/filemanager/manager/metadata.go +++ b/pkg/filemanager/manager/metadata.go @@ -43,6 +43,8 @@ var ( // validateColor validates a color value validateColor = func(optional bool) metadataValidator { return func(ctx context.Context, m *manager, patch *fs.MetadataPatch) error { + patch.UpdateModifiedAt = true + if patch.Remove { return nil } @@ -67,6 +69,8 @@ var ( return fmt.Errorf("cannot remove system metadata") } + patch.UpdateModifiedAt = true + dep := dependency.FromContext(ctx) // Validate share owner is valid hashid if patch.Key == shareOwnerMetadataKey { @@ -96,6 +100,8 @@ var ( customizeMetadataSuffix: { iconColorMetadataKey: validateColor(false), emojiIconMetadataKey: func(ctx context.Context, m *manager, patch *fs.MetadataPatch) error { + patch.UpdateModifiedAt = true + if patch.Remove { return nil } @@ -125,6 +131,8 @@ var ( }, tagMetadataSuffix: { wildcardMetadataKey: func(ctx context.Context, m *manager, patch *fs.MetadataPatch) error { + patch.UpdateModifiedAt = true + if err := validateColor(true)(ctx, m, patch); err != nil { return err } @@ -138,6 +146,8 @@ var ( }, customPropsMetadataSuffix: { wildcardMetadataKey: func(ctx context.Context, m *manager, patch *fs.MetadataPatch) error { + patch.UpdateModifiedAt = true + if patch.Remove { return nil } @@ -260,40 +270,44 @@ var ( ) func (m *manager) PatchMedata(ctx context.Context, path []*fs.URI, data ...fs.MetadataPatch) error { - if err := m.validateMetadata(ctx, data...); err != nil { + data, err := m.validateMetadata(ctx, data...) + if err != nil { return err } return m.fs.PatchMetadata(ctx, path, data...) } -func (m *manager) validateMetadata(ctx context.Context, data ...fs.MetadataPatch) error { +func (m *manager) validateMetadata(ctx context.Context, data ...fs.MetadataPatch) ([]fs.MetadataPatch, error) { + validated := make([]fs.MetadataPatch, 0, len(data)) for _, patch := range data { category := strings.Split(patch.Key, ":") if len(category) < 2 { - return serializer.NewError(serializer.CodeParamErr, "Invalid metadata key", nil) + return validated, serializer.NewError(serializer.CodeParamErr, "Invalid metadata key", nil) } categoryValidators, ok := validators[category[0]] if !ok { - return serializer.NewError(serializer.CodeParamErr, "Invalid metadata key", + return validated, serializer.NewError(serializer.CodeParamErr, "Invalid metadata key", fmt.Errorf("unknown category: %s", category[0])) } // Explicit validators if v, ok := categoryValidators[patch.Key]; ok { if err := v(ctx, m, &patch); err != nil { - return serializer.NewError(serializer.CodeParamErr, "Invalid metadata patch", err) + return validated, serializer.NewError(serializer.CodeParamErr, "Invalid metadata patch", err) } } // Wildcard validators if v, ok := categoryValidators[wildcardMetadataKey]; ok { if err := v(ctx, m, &patch); err != nil { - return serializer.NewError(serializer.CodeParamErr, "Invalid metadata patch", err) + return validated, serializer.NewError(serializer.CodeParamErr, "Invalid metadata patch", err) } } + + validated = append(validated, patch) } - return nil + return validated, nil } diff --git a/pkg/filemanager/manager/operation.go b/pkg/filemanager/manager/operation.go index 9ba859bc..83891f12 100644 --- a/pkg/filemanager/manager/operation.go +++ b/pkg/filemanager/manager/operation.go @@ -115,7 +115,7 @@ func (m *manager) Create(ctx context.Context, path *fs.URI, fileType types.FileT isSymbolic := false if o.Metadata != nil { - if err := m.validateMetadata(ctx, lo.MapToSlice(o.Metadata, func(key string, value string) fs.MetadataPatch { + _, err := m.validateMetadata(ctx, lo.MapToSlice(o.Metadata, func(key string, value string) fs.MetadataPatch { if key == shareRedirectMetadataKey { isSymbolic = true } @@ -124,7 +124,8 @@ func (m *manager) Create(ctx context.Context, path *fs.URI, fileType types.FileT Key: key, Value: value, } - })...); err != nil { + })...) + if err != nil { return nil, err } } diff --git a/pkg/filemanager/manager/upload.go b/pkg/filemanager/manager/upload.go index 0337761a..7d93ed35 100644 --- a/pkg/filemanager/manager/upload.go +++ b/pkg/filemanager/manager/upload.go @@ -55,7 +55,7 @@ func (m *manager) CreateUploadSession(ctx context.Context, req *fs.UploadRequest // Validate metadata if req.Props.Metadata != nil { - if err := m.validateMetadata(ctx, lo.MapToSlice(req.Props.Metadata, func(key string, value string) fs.MetadataPatch { + if _, err := m.validateMetadata(ctx, lo.MapToSlice(req.Props.Metadata, func(key string, value string) fs.MetadataPatch { return fs.MetadataPatch{ Key: key, Value: value, From 7654ce889c4b83e6be1326680741095f13824ff2 Mon Sep 17 00:00:00 2001 From: Darren Yu Date: Tue, 5 Aug 2025 20:29:14 +0800 Subject: [PATCH 16/74] fix(blob path): Random variables in blob save path be wrongly fixed (#2741) * fix(blob path): Random variables in blob save path be wrongly fixed * feat(blob path): Use regex to match all magic variables --- pkg/filemanager/fs/dbfs/dbfs.go | 90 +++++++++++++++++++++------------ 1 file changed, 58 insertions(+), 32 deletions(-) diff --git a/pkg/filemanager/fs/dbfs/dbfs.go b/pkg/filemanager/fs/dbfs/dbfs.go index e12b24fa..e362e23a 100644 --- a/pkg/filemanager/fs/dbfs/dbfs.go +++ b/pkg/filemanager/fs/dbfs/dbfs.go @@ -7,6 +7,7 @@ import ( "math/rand" "path" "path/filepath" + "regexp" "strconv" "strings" "sync" @@ -765,44 +766,69 @@ func (f *DBFS) navigatorId(path *fs.URI) string { // generateSavePath generates the physical save path for the upload request. func generateSavePath(policy *ent.StoragePolicy, req *fs.UploadRequest, user *ent.User) string { - baseTable := map[string]string{ - "{randomkey16}": util.RandStringRunes(16), - "{randomkey8}": util.RandStringRunes(8), - "{timestamp}": strconv.FormatInt(time.Now().Unix(), 10), - "{timestamp_nano}": strconv.FormatInt(time.Now().UnixNano(), 10), - "{randomnum2}": strconv.Itoa(rand.Intn(2)), - "{randomnum3}": strconv.Itoa(rand.Intn(3)), - "{randomnum4}": strconv.Itoa(rand.Intn(4)), - "{randomnum8}": strconv.Itoa(rand.Intn(8)), - "{uid}": strconv.Itoa(user.ID), - "{datetime}": time.Now().Format("20060102150405"), - "{date}": time.Now().Format("20060102"), - "{year}": time.Now().Format("2006"), - "{month}": time.Now().Format("01"), - "{day}": time.Now().Format("02"), - "{hour}": time.Now().Format("15"), - "{minute}": time.Now().Format("04"), - "{second}": time.Now().Format("05"), + currentTime := time.Now() + originName := req.Props.Uri.Name() + + dynamicReplace := func(regPattern string, rule string) string { + re := regexp.MustCompile(regPattern) + return re.ReplaceAllStringFunc(rule, func(match string) string { + switch match { + case "{timestamp}": + return strconv.FormatInt(currentTime.Unix(), 10) + case "{timestamp_nano}": + return strconv.FormatInt(currentTime.UnixNano(), 10) + case "{datetime}": + return currentTime.Format("20060102150405") + case "{date}": + return currentTime.Format("20060102") + case "{year}": + return currentTime.Format("2006") + case "{month}": + return currentTime.Format("01") + case "{day}": + return currentTime.Format("02") + case "{hour}": + return currentTime.Format("15") + case "{minute}": + return currentTime.Format("04") + case "{second}": + return currentTime.Format("05") + case "{uid}": + return strconv.Itoa(user.ID) + case "{randomkey16}": + return util.RandStringRunes(16) + case "{randomkey8}": + return util.RandStringRunes(8) + case "{randomnum8}": + return strconv.Itoa(rand.Intn(8)) + case "{randomnum4}": + return strconv.Itoa(rand.Intn(4)) + case "{randomnum3}": + return strconv.Itoa(rand.Intn(3)) + case "{randomnum2}": + return strconv.Itoa(rand.Intn(2)) + case "{uuid}": + return uuid.Must(uuid.NewV4()).String() + case "{path}": + return req.Props.Uri.Dir() + fs.Separator + case "{originname}": + return originName + case "{ext}": + return filepath.Ext(originName) + case "{originname_without_ext}": + return strings.TrimSuffix(originName, filepath.Ext(originName)) + default: + return match + } + }) } dirRule := policy.DirNameRule dirRule = filepath.ToSlash(dirRule) - dirRule = util.Replace(baseTable, dirRule) - dirRule = util.Replace(map[string]string{ - "{path}": req.Props.Uri.Dir() + fs.Separator, - }, dirRule) - - originName := req.Props.Uri.Name() - nameTable := map[string]string{ - "{originname}": originName, - "{ext}": filepath.Ext(originName), - "{originname_without_ext}": strings.TrimSuffix(originName, filepath.Ext(originName)), - "{uuid}": uuid.Must(uuid.NewV4()).String(), - } + dirRule = dynamicReplace(`\{[^{}]+\}`, dirRule) nameRule := policy.FileNameRule - nameRule = util.Replace(baseTable, nameRule) - nameRule = util.Replace(nameTable, nameRule) + nameRule = dynamicReplace(`\{[^{}]+\}`, nameRule) return path.Join(path.Clean(dirRule), nameRule) } From 48e97193366242f08a8d3231c52e56187e8a043c Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Thu, 7 Aug 2025 10:29:47 +0800 Subject: [PATCH 17/74] fix(dbfs): deadlock in SQLite while creating upload session --- inventory/tx.go | 17 +++++++++++++++++ pkg/filemanager/fs/dbfs/dbfs.go | 3 ++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/inventory/tx.go b/inventory/tx.go index e267047a..f53c4fd9 100644 --- a/inventory/tx.go +++ b/inventory/tx.go @@ -3,6 +3,7 @@ package inventory import ( "context" "fmt" + "github.com/cloudreve/Cloudreve/v4/ent" "github.com/cloudreve/Cloudreve/v4/pkg/logging" ) @@ -60,6 +61,22 @@ func WithTx[T TxOperator](ctx context.Context, c T) (T, *Tx, context.Context, er return c.SetClient(txClient).(T), txWrapper, ctx, nil } +// InheritTx wraps the given inventory client with a transaction. +// If the transaction is already in the context, it will be inherited. +// Otherwise, original client will be returned. +func InheritTx[T TxOperator](ctx context.Context, c T) (T, *Tx) { + var txClient *ent.Client + var txWrapper *Tx + + if txInherited, ok := ctx.Value(TxCtx{}).(*Tx); ok && !txInherited.finished { + txWrapper = &Tx{inherited: true, tx: txInherited.tx, parent: txInherited} + txClient = txWrapper.tx.Client() + return c.SetClient(txClient).(T), txWrapper + } + + return c, nil +} + func Rollback(tx *Tx) error { if !tx.inherited { tx.finished = true diff --git a/pkg/filemanager/fs/dbfs/dbfs.go b/pkg/filemanager/fs/dbfs/dbfs.go index e362e23a..c0600937 100644 --- a/pkg/filemanager/fs/dbfs/dbfs.go +++ b/pkg/filemanager/fs/dbfs/dbfs.go @@ -652,7 +652,8 @@ func (f *DBFS) getPreferredPolicy(ctx context.Context, file *File) (*ent.Storage return nil, fmt.Errorf("owner group not loaded") } - groupPolicy, err := f.storagePolicyClient.GetByGroup(ctx, ownerGroup) + sc, _ := inventory.InheritTx(ctx, f.storagePolicyClient) + groupPolicy, err := sc.GetByGroup(ctx, ownerGroup) if err != nil { return nil, serializer.NewError(serializer.CodeDBError, "Failed to get available storage policies", err) } From b0375f5a24c6b4fb638c9f3760c5001b07c838fd Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Thu, 7 Aug 2025 11:03:02 +0800 Subject: [PATCH 18/74] fix(recycle): nil pointer if failed to found files in trash (#2750) --- pkg/filemanager/manager/recycle.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/filemanager/manager/recycle.go b/pkg/filemanager/manager/recycle.go index 2b1c84e1..ee9cfbe8 100644 --- a/pkg/filemanager/manager/recycle.go +++ b/pkg/filemanager/manager/recycle.go @@ -311,6 +311,7 @@ func CronCollectTrashBin(ctx context.Context) { res, err := fm.fs.AllFilesInTrashBin(ctx, fs.WithPageSize(pageSize)) if err != nil { l.Error("Failed to get files in trash bin: %s", err) + return } expired := lo.Filter(res.Files, func(file fs.File, index int) bool { From 4c976b862734d16a69d0d3095dbd5e882cb4f76d Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Thu, 7 Aug 2025 11:35:28 +0800 Subject: [PATCH 19/74] feat(blob path): diable `{path}` magic var for blob path --- assets | 2 +- pkg/filemanager/fs/dbfs/dbfs.go | 99 +++++++++++++++++---------------- 2 files changed, 52 insertions(+), 49 deletions(-) diff --git a/assets b/assets index 09480ffa..3a23464a 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 09480ffa21d859a1d2f9bb2421e6f78f113494c4 +Subproject commit 3a23464a0f4330e4583e99c1815b84c5240ffc9d diff --git a/pkg/filemanager/fs/dbfs/dbfs.go b/pkg/filemanager/fs/dbfs/dbfs.go index c0600937..7d57c7d6 100644 --- a/pkg/filemanager/fs/dbfs/dbfs.go +++ b/pkg/filemanager/fs/dbfs/dbfs.go @@ -770,66 +770,69 @@ func generateSavePath(policy *ent.StoragePolicy, req *fs.UploadRequest, user *en currentTime := time.Now() originName := req.Props.Uri.Name() - dynamicReplace := func(regPattern string, rule string) string { + dynamicReplace := func(regPattern string, rule string, pathAvailable bool) string { re := regexp.MustCompile(regPattern) return re.ReplaceAllStringFunc(rule, func(match string) string { switch match { - case "{timestamp}": - return strconv.FormatInt(currentTime.Unix(), 10) - case "{timestamp_nano}": - return strconv.FormatInt(currentTime.UnixNano(), 10) - case "{datetime}": - return currentTime.Format("20060102150405") - case "{date}": - return currentTime.Format("20060102") - case "{year}": - return currentTime.Format("2006") - case "{month}": - return currentTime.Format("01") - case "{day}": - return currentTime.Format("02") - case "{hour}": - return currentTime.Format("15") - case "{minute}": - return currentTime.Format("04") - case "{second}": - return currentTime.Format("05") - case "{uid}": - return strconv.Itoa(user.ID) - case "{randomkey16}": - return util.RandStringRunes(16) - case "{randomkey8}": - return util.RandStringRunes(8) - case "{randomnum8}": - return strconv.Itoa(rand.Intn(8)) - case "{randomnum4}": - return strconv.Itoa(rand.Intn(4)) - case "{randomnum3}": - return strconv.Itoa(rand.Intn(3)) - case "{randomnum2}": - return strconv.Itoa(rand.Intn(2)) - case "{uuid}": - return uuid.Must(uuid.NewV4()).String() - case "{path}": + case "{timestamp}": + return strconv.FormatInt(currentTime.Unix(), 10) + case "{timestamp_nano}": + return strconv.FormatInt(currentTime.UnixNano(), 10) + case "{datetime}": + return currentTime.Format("20060102150405") + case "{date}": + return currentTime.Format("20060102") + case "{year}": + return currentTime.Format("2006") + case "{month}": + return currentTime.Format("01") + case "{day}": + return currentTime.Format("02") + case "{hour}": + return currentTime.Format("15") + case "{minute}": + return currentTime.Format("04") + case "{second}": + return currentTime.Format("05") + case "{uid}": + return strconv.Itoa(user.ID) + case "{randomkey16}": + return util.RandStringRunes(16) + case "{randomkey8}": + return util.RandStringRunes(8) + case "{randomnum8}": + return strconv.Itoa(rand.Intn(8)) + case "{randomnum4}": + return strconv.Itoa(rand.Intn(4)) + case "{randomnum3}": + return strconv.Itoa(rand.Intn(3)) + case "{randomnum2}": + return strconv.Itoa(rand.Intn(2)) + case "{uuid}": + return uuid.Must(uuid.NewV4()).String() + case "{path}": + if pathAvailable { return req.Props.Uri.Dir() + fs.Separator - case "{originname}": - return originName - case "{ext}": - return filepath.Ext(originName) - case "{originname_without_ext}": - return strings.TrimSuffix(originName, filepath.Ext(originName)) - default: - return match + } + return match + case "{originname}": + return originName + case "{ext}": + return filepath.Ext(originName) + case "{originname_without_ext}": + return strings.TrimSuffix(originName, filepath.Ext(originName)) + default: + return match } }) } dirRule := policy.DirNameRule dirRule = filepath.ToSlash(dirRule) - dirRule = dynamicReplace(`\{[^{}]+\}`, dirRule) + dirRule = dynamicReplace(`\{[^{}]+\}`, dirRule, true) nameRule := policy.FileNameRule - nameRule = dynamicReplace(`\{[^{}]+\}`, nameRule) + nameRule = dynamicReplace(`\{[^{}]+\}`, nameRule, false) return path.Join(path.Clean(dirRule), nameRule) } From 4c08644b05dd969c2aef494a2192f1f6e899a5b3 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Sun, 10 Aug 2025 09:38:27 +0800 Subject: [PATCH 20/74] fix(dbfs): generate thumbnail blob should not update file modification date --- inventory/file.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inventory/file.go b/inventory/file.go index b6d81f73..56b4d4c9 100644 --- a/inventory/file.go +++ b/inventory/file.go @@ -907,7 +907,7 @@ func (f *fileClient) CreateEntity(ctx context.Context, file *ent.File, args *Ent diff := map[int]int64{file.OwnerID: created.Size} - if err := f.client.File.UpdateOne(file).AddEntities(created).Exec(ctx); err != nil { + if err := f.client.Entity.UpdateOne(created).AddFile(file).Exec(ctx); err != nil { return nil, diff, fmt.Errorf("failed to add file entity: %v", err) } From 8688069fac670095a14192c572e472777ff437e8 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Sun, 10 Aug 2025 10:40:21 +0800 Subject: [PATCH 21/74] refactor(mail): migrate to wneessen/go-mail (#2738) --- assets | 2 +- go.mod | 4 +-- go.sum | 47 ++++++++++++++++++++++++++---- pkg/email/smtp.go | 66 +++++++++++++++++++++++++----------------- service/admin/tools.go | 39 ++++++++++++++----------- 5 files changed, 104 insertions(+), 54 deletions(-) diff --git a/assets b/assets index 3a23464a..8b2c8a7b 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 3a23464a0f4330e4583e99c1815b84c5240ffc9d +Subproject commit 8b2c8a7bdbde43a1f95ddaa4555e4304215c1e7c diff --git a/go.mod b/go.mod index ab2486fe..d12dcdd1 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,6 @@ require ( github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2 github.com/gin-gonic/gin v1.10.0 github.com/go-ini/ini v1.50.0 - github.com/go-mail/mail v2.3.1+incompatible github.com/go-playground/validator/v10 v10.20.0 github.com/go-sql-driver/mysql v1.6.0 github.com/go-webauthn/webauthn v0.11.2 @@ -54,6 +53,7 @@ require ( github.com/tencentyun/cos-go-sdk-v5 v0.7.54 github.com/ua-parser/uap-go v0.0.0-20250213224047-9c035f085b90 github.com/upyun/go-sdk v2.1.0+incompatible + github.com/wneessen/go-mail v0.6.2 golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e golang.org/x/image v0.0.0-20211028202545-6944b10bf410 golang.org/x/text v0.23.0 @@ -139,8 +139,6 @@ require ( golang.org/x/sync v0.12.0 // indirect golang.org/x/sys v0.31.0 // indirect google.golang.org/protobuf v1.34.2 // indirect - gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect - gopkg.in/mail.v2 v2.3.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect diff --git a/go.sum b/go.sum index 509d1aa7..6f70e0b5 100644 --- a/go.sum +++ b/go.sum @@ -325,8 +325,6 @@ github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgO github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-mail/mail v2.3.1+incompatible h1:UzNOn0k5lpfVtO31cK3hn6I4VEVGhe3lX8AJBAxXExM= -github.com/go-mail/mail v2.3.1+incompatible/go.mod h1:VPWjmmNyRsWXQZHVHT3g0YbIINUkSmuKOiLIDkWbL6M= github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4= github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= @@ -967,6 +965,8 @@ github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+ github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= github.com/weppos/publicsuffix-go v0.13.1-0.20210123135404-5fd73613514e/go.mod h1:HYux0V0Zi04bHNwOHy4cXJVz/TQjYonnF6aoYhj+3QE= github.com/weppos/publicsuffix-go v0.15.1-0.20210511084619-b1f36a2d6c0b/go.mod h1:HYux0V0Zi04bHNwOHy4cXJVz/TQjYonnF6aoYhj+3QE= +github.com/wneessen/go-mail v0.6.2 h1:c6V7c8D2mz868z9WJ+8zDKtUyLfZ1++uAZmo2GRFji8= +github.com/wneessen/go-mail v0.6.2/go.mod h1:L/PYjPK3/2ZlNb2/FjEBIn9n1rUWjW+Toy531oVmeb4= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xanzy/go-gitlab v0.31.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug= @@ -1051,6 +1051,10 @@ golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf/go.mod h1:P+XmwS30IXTQdn5 golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1095,6 +1099,10 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1151,6 +1159,11 @@ golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1181,6 +1194,11 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1257,12 +1275,24 @@ golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1273,6 +1303,12 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1348,6 +1384,9 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1484,8 +1523,6 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= -gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1501,8 +1538,6 @@ gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/R gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk= -gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= diff --git a/pkg/email/smtp.go b/pkg/email/smtp.go index 1ea8926a..b9fcda86 100644 --- a/pkg/email/smtp.go +++ b/pkg/email/smtp.go @@ -9,8 +9,7 @@ import ( "github.com/cloudreve/Cloudreve/v4/inventory" "github.com/cloudreve/Cloudreve/v4/pkg/logging" "github.com/cloudreve/Cloudreve/v4/pkg/setting" - "github.com/go-mail/mail" - "github.com/gofrs/uuid" + "github.com/wneessen/go-mail" ) // SMTPPool SMTP协议发送邮件 @@ -38,9 +37,11 @@ type SMTPConfig struct { } type message struct { - msg *mail.Message - cid string - userID int + msg *mail.Msg + to string + subject string + cid string + userID int } // NewSMTPPool initializes a new SMTP based email sending queue. @@ -81,17 +82,21 @@ func (client *SMTPPool) Send(ctx context.Context, to, title, body string) error return nil } - m := mail.NewMessage() - m.SetAddressHeader("From", client.config.From, client.config.FromName) - m.SetAddressHeader("Reply-To", client.config.ReplyTo, client.config.FromName) - m.SetHeader("To", to) - m.SetHeader("Subject", title) - m.SetHeader("Message-ID", fmt.Sprintf("<%s@%s>", uuid.Must(uuid.NewV4()).String(), "cloudreve")) - m.SetBody("text/html", body) + m := mail.NewMsg() + if err := m.FromFormat(client.config.FromName, client.config.From); err != nil { + return err + } + m.ReplyToFormat(client.config.FromName, client.config.ReplyTo) + m.To(to) + m.Subject(title) + m.SetMessageID() + m.SetBodyString(mail.TypeTextHTML, body) client.ch <- &message{ - msg: m, - cid: logging.CorrelationID(ctx).String(), - userID: inventory.UserIDFromContext(ctx), + msg: m, + subject: title, + to: to, + cid: logging.CorrelationID(ctx).String(), + userID: inventory.UserIDFromContext(ctx), } return nil } @@ -116,17 +121,24 @@ func (client *SMTPPool) Init() { } }() - d := mail.NewDialer(client.config.Host, client.config.Port, client.config.User, client.config.Password) - d.Timeout = time.Duration(client.config.Keepalive+5) * time.Second - client.chOpen = true - // 是否启用 SSL - d.SSL = false + tlsPolicy := mail.TLSOpportunistic if client.config.ForceEncryption { - d.SSL = true + tlsPolicy = mail.TLSMandatory + } + + d, diaErr := mail.NewClient(client.config.Host, + mail.WithPort(client.config.Port), + mail.WithTimeout(time.Duration(client.config.Keepalive+5)*time.Second), + mail.WithSMTPAuth(mail.SMTPAuthAutoDiscover), mail.WithTLSPortPolicy(tlsPolicy), + mail.WithUsername(client.config.User), mail.WithPassword(client.config.Password), + ) + if diaErr != nil { + client.l.Panic("Failed to create SMTP client: %s", diaErr) + return } - d.StartTLSPolicy = mail.OpportunisticStartTLS - var s mail.SendCloser + client.chOpen = true + var err error open := false for { @@ -139,22 +151,22 @@ func (client *SMTPPool) Init() { } if !open { - if s, err = d.Dial(); err != nil { + if err = d.DialWithContext(context.Background()); err != nil { panic(err) } open = true } l := client.l.CopyWithPrefix(fmt.Sprintf("[Cid: %s]", m.cid)) - if err := mail.Send(s, m.msg); err != nil { + if err := d.Send(m.msg); err != nil { l.Warning("Failed to send email: %s, Cid=%s", err, m.cid) } else { - l.Info("Email sent to %q, title: %q.", m.msg.GetHeader("To"), m.msg.GetHeader("Subject")) + l.Info("Email sent to %q, title: %q.", m.to, m.subject) } // 长时间没有新邮件,则关闭SMTP连接 case <-time.After(time.Duration(client.config.Keepalive) * time.Second): if open { - if err := s.Close(); err != nil { + if err := d.Close(); err != nil { client.l.Warning("Failed to close SMTP connection: %s", err) } open = false diff --git a/service/admin/tools.go b/service/admin/tools.go index 9409b949..8bea48d1 100644 --- a/service/admin/tools.go +++ b/service/admin/tools.go @@ -5,6 +5,8 @@ import ( "net/http" "strconv" + "github.com/cloudreve/Cloudreve/v4/inventory/types" + "github.com/cloudreve/Cloudreve/v4/application/dependency" "github.com/cloudreve/Cloudreve/v4/pkg/boolset" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager" @@ -12,9 +14,8 @@ import ( "github.com/cloudreve/Cloudreve/v4/pkg/serializer" "github.com/cloudreve/Cloudreve/v4/pkg/setting" "github.com/cloudreve/Cloudreve/v4/pkg/wopi" - "github.com/cloudreve/Cloudreve/v4/inventory/types" "github.com/gin-gonic/gin" - "github.com/go-mail/mail" + "github.com/wneessen/go-mail" ) type ( @@ -138,26 +139,30 @@ func (s *TestSMTPService) Test(c *gin.Context) error { return serializer.NewError(serializer.CodeParamErr, "Invalid SMTP port", err) } - d := mail.NewDialer(s.Settings["smtpHost"], port, s.Settings["smtpUser"], s.Settings["smtpPass"]) - d.SSL = false + tlsPolicy := mail.TLSOpportunistic if setting.IsTrueValue(s.Settings["smtpEncryption"]) { - d.SSL = true + tlsPolicy = mail.TLSMandatory } - d.StartTLSPolicy = mail.OpportunisticStartTLS - - sender, err := d.Dial() - if err != nil { - return serializer.NewError(serializer.CodeInternalSetting, "Failed to connect to SMTP server: "+err.Error(), err) + d, diaErr := mail.NewClient(s.Settings["smtpHost"], + mail.WithPort(port), + mail.WithSMTPAuth(mail.SMTPAuthAutoDiscover), mail.WithTLSPortPolicy(tlsPolicy), + mail.WithUsername(s.Settings["smtpUser"]), mail.WithPassword(s.Settings["smtpPass"]), + ) + if diaErr != nil { + return serializer.NewError(serializer.CodeInternalSetting, "Failed to create SMTP client: "+diaErr.Error(), diaErr) } - m := mail.NewMessage() - m.SetHeader("From", s.Settings["fromAdress"]) - m.SetAddressHeader("Reply-To", s.Settings["replyTo"], s.Settings["fromName"]) - m.SetHeader("To", s.To) - m.SetHeader("Subject", "Cloudreve SMTP Test") - m.SetBody("text/plain", "This is a test email from Cloudreve.") + m := mail.NewMsg() + if err := m.FromFormat(s.Settings["fromName"], s.Settings["fromAdress"]); err != nil { + return serializer.NewError(serializer.CodeInternalSetting, "Failed to set FROM address: "+err.Error(), err) + } + m.ReplyToFormat(s.Settings["fromName"], s.Settings["replyTo"]) + m.To(s.To) + m.Subject("Cloudreve SMTP Test") + m.SetMessageID() + m.SetBodyString(mail.TypeTextHTML, "This is a test email from Cloudreve.") - err = mail.Send(sender, m) + err = d.DialAndSendWithContext(c, m) if err != nil { return serializer.NewError(serializer.CodeInternalSetting, "Failed to send test email: "+err.Error(), err) } From 8deeadb1e53d31ace14c9a0fa4bd4cf2adc4de96 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Sun, 10 Aug 2025 10:47:29 +0800 Subject: [PATCH 22/74] fix(middleware): only select first client IP from X-Forwarded-For (#2748) --- middleware/common.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/middleware/common.go b/middleware/common.go index 21833bfd..a7adab04 100644 --- a/middleware/common.go +++ b/middleware/common.go @@ -3,6 +3,10 @@ package middleware import ( "context" "fmt" + "net/http" + "strings" + "time" + "github.com/cloudreve/Cloudreve/v4/application/constants" "github.com/cloudreve/Cloudreve/v4/application/dependency" "github.com/cloudreve/Cloudreve/v4/pkg/auth/requestinfo" @@ -14,8 +18,6 @@ import ( "github.com/cloudreve/Cloudreve/v4/pkg/util" "github.com/gin-gonic/gin" "github.com/gofrs/uuid" - "net/http" - "time" ) // HashID 将给定对象的HashID转换为真实ID @@ -92,8 +94,13 @@ func MobileRequestOnly() gin.HandlerFunc { // 2. Generate and inject correlation ID for diagnostic. func InitializeHandling(dep dependency.Dep) gin.HandlerFunc { return func(c *gin.Context) { + clientIp := c.ClientIP() + if idx := strings.Index(clientIp, ","); idx > 0 { + clientIp = clientIp[:idx] + } + reqInfo := &requestinfo.RequestInfo{ - IP: c.ClientIP(), + IP: clientIp, Host: c.Request.Host, UserAgent: c.Request.UserAgent(), } From bb3db2e326933f59e73ca5536fc783faed5d51ee Mon Sep 17 00:00:00 2001 From: Darren Yu Date: Tue, 12 Aug 2025 09:35:36 +0800 Subject: [PATCH 23/74] fix(middleware): left deafult `ProxyHeader` config item as blank to reduce risk of fake xff (#2760) --- application/migrator/conf/defaults.go | 2 +- pkg/conf/types.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/application/migrator/conf/defaults.go b/application/migrator/conf/defaults.go index 4ecfd2b2..d9160273 100644 --- a/application/migrator/conf/defaults.go +++ b/application/migrator/conf/defaults.go @@ -22,7 +22,7 @@ var SystemConfig = &system{ Debug: false, Mode: "master", Listen: ":5212", - ProxyHeader: "X-Forwarded-For", + ProxyHeader: "", } // CORSConfig 跨域配置 diff --git a/pkg/conf/types.go b/pkg/conf/types.go index e5a53b4d..509cecaf 100644 --- a/pkg/conf/types.go +++ b/pkg/conf/types.go @@ -114,7 +114,7 @@ var SystemConfig = &System{ Debug: false, Mode: MasterMode, Listen: ":5212", - ProxyHeader: "X-Forwarded-For", + ProxyHeader: "", LogLevel: "info", } From b0057fe92ff7c54c61bbc7a8869dbdb766ceabd5 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Tue, 12 Aug 2025 09:52:47 +0800 Subject: [PATCH 24/74] feat(profile): options to select why kind of share links to show in user's profile (#2453) --- assets | 2 +- inventory/types/types.go | 27 ++++++++++------ service/explorer/response.go | 53 ++++++++++++++++-------------- service/share/manage.go | 2 +- service/share/visit.go | 12 ++++++- service/user/response.go | 63 +++++++++++++++++++----------------- service/user/setting.go | 7 ++++ 7 files changed, 101 insertions(+), 65 deletions(-) diff --git a/assets b/assets index 8b2c8a7b..eb2cfac3 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 8b2c8a7bdbde43a1f95ddaa4555e4304215c1e7c +Subproject commit eb2cfac37d73e5bd3000eb66a3a0062509efe122 diff --git a/inventory/types/types.go b/inventory/types/types.go index 479574f5..bc069859 100644 --- a/inventory/types/types.go +++ b/inventory/types/types.go @@ -7,17 +7,20 @@ import ( // UserSetting 用户其他配置 type ( UserSetting struct { - ProfileOff bool `json:"profile_off,omitempty"` - PreferredTheme string `json:"preferred_theme,omitempty"` - VersionRetention bool `json:"version_retention,omitempty"` - VersionRetentionExt []string `json:"version_retention_ext,omitempty"` - VersionRetentionMax int `json:"version_retention_max,omitempty"` - Pined []PinedFile `json:"pined,omitempty"` - Language string `json:"email_language,omitempty"` - DisableViewSync bool `json:"disable_view_sync,omitempty"` - FsViewMap map[string]ExplorerView `json:"fs_view_map,omitempty"` + ProfileOff bool `json:"profile_off,omitempty"` + PreferredTheme string `json:"preferred_theme,omitempty"` + VersionRetention bool `json:"version_retention,omitempty"` + VersionRetentionExt []string `json:"version_retention_ext,omitempty"` + VersionRetentionMax int `json:"version_retention_max,omitempty"` + Pined []PinedFile `json:"pined,omitempty"` + Language string `json:"email_language,omitempty"` + DisableViewSync bool `json:"disable_view_sync,omitempty"` + FsViewMap map[string]ExplorerView `json:"fs_view_map,omitempty"` + ShareLinksInProfile ShareLinksInProfileLevel `json:"share_links_in_profile,omitempty"` } + ShareLinksInProfileLevel string + PinedFile struct { Uri string `json:"uri"` Name string `json:"name,omitempty"` @@ -334,3 +337,9 @@ const ( CustomPropsTypeLink = "link" CustomPropsTypeRating = "rating" ) + +const ( + ProfilePublicShareOnly = ShareLinksInProfileLevel("") + ProfileAllShare = ShareLinksInProfileLevel("all_share") + ProfileHideShare = ShareLinksInProfileLevel("hide_share") +) diff --git a/service/explorer/response.go b/service/explorer/response.go index 755e96e2..302a2275 100644 --- a/service/explorer/response.go +++ b/service/explorer/response.go @@ -271,19 +271,20 @@ type Entity struct { } type Share struct { - ID string `json:"id"` - Name string `json:"name,omitempty"` - RemainDownloads *int `json:"remain_downloads,omitempty"` - Visited int `json:"visited"` - Downloaded int `json:"downloaded,omitempty"` - Expires *time.Time `json:"expires,omitempty"` - Unlocked bool `json:"unlocked"` - SourceType *types.FileType `json:"source_type,omitempty"` - Owner user.User `json:"owner"` - CreatedAt time.Time `json:"created_at,omitempty"` - Expired bool `json:"expired"` - Url string `json:"url"` - ShowReadMe bool `json:"show_readme,omitempty"` + ID string `json:"id"` + Name string `json:"name,omitempty"` + RemainDownloads *int `json:"remain_downloads,omitempty"` + Visited int `json:"visited"` + Downloaded int `json:"downloaded,omitempty"` + Expires *time.Time `json:"expires,omitempty"` + Unlocked bool `json:"unlocked"` + PasswordProtected bool `json:"password_protected,omitempty"` + SourceType *types.FileType `json:"source_type,omitempty"` + Owner user.User `json:"owner"` + CreatedAt time.Time `json:"created_at,omitempty"` + Expired bool `json:"expired"` + Url string `json:"url"` + ShowReadMe bool `json:"show_readme,omitempty"` // Only viewable by owner IsPrivate bool `json:"is_private,omitempty"` @@ -301,15 +302,16 @@ func BuildShare(s *ent.Share, base *url.URL, hasher hashid.Encoder, requester *e redactLevel = user.RedactLevelUser } res := Share{ - Name: name, - ID: hashid.EncodeShareID(hasher, s.ID), - Unlocked: unlocked, - Owner: user.BuildUserRedacted(owner, redactLevel, hasher), - Expired: inventory.IsShareExpired(s) != nil || expired, - Url: BuildShareLink(s, hasher, base), - CreatedAt: s.CreatedAt, - Visited: s.Views, - SourceType: util.ToPtr(t), + Name: name, + ID: hashid.EncodeShareID(hasher, s.ID), + Unlocked: unlocked, + Owner: user.BuildUserRedacted(owner, redactLevel, hasher), + Expired: inventory.IsShareExpired(s) != nil || expired, + Url: BuildShareLink(s, hasher, base, unlocked), + CreatedAt: s.CreatedAt, + Visited: s.Views, + SourceType: util.ToPtr(t), + PasswordProtected: s.Password != "", } if unlocked { @@ -436,9 +438,12 @@ func BuildEntity(extendedInfo *fs.FileExtendedInfo, e fs.Entity, hasher hashid.E } } -func BuildShareLink(s *ent.Share, hasher hashid.Encoder, base *url.URL) string { +func BuildShareLink(s *ent.Share, hasher hashid.Encoder, base *url.URL, unlocked bool) string { shareId := hashid.EncodeShareID(hasher, s.ID) - return routes.MasterShareUrl(base, shareId, s.Password).String() + if unlocked { + return routes.MasterShareUrl(base, shareId, s.Password).String() + } + return routes.MasterShareUrl(base, shareId, "").String() } func BuildStoragePolicy(sp *ent.StoragePolicy, hasher hashid.Encoder) *StoragePolicy { diff --git a/service/share/manage.go b/service/share/manage.go index 335f3468..b8ad4d1d 100644 --- a/service/share/manage.go +++ b/service/share/manage.go @@ -66,7 +66,7 @@ func (service *ShareCreateService) Upsert(c *gin.Context, existed int) (string, } base := dep.SettingProvider().SiteURL(c) - return explorer.BuildShareLink(share, dep.HashIDEncoder(), base), nil + return explorer.BuildShareLink(share, dep.HashIDEncoder(), base, true), nil } func DeleteShare(c *gin.Context, shareId int) error { diff --git a/service/share/visit.go b/service/share/visit.go index e5a756b9..cf07622a 100644 --- a/service/share/visit.go +++ b/service/share/visit.go @@ -137,6 +137,16 @@ func (s *ListShareService) ListInUserProfile(c *gin.Context, uid int) (*ListShar hasher := dep.HashIDEncoder() shareClient := dep.ShareClient() + targetUser, err := dep.UserClient().GetActiveByID(c, uid) + if err != nil { + return nil, serializer.NewError(serializer.CodeDBError, "Failed to get user", err) + } + + if targetUser.Settings != nil && targetUser.Settings.ShareLinksInProfile == types.ProfileHideShare { + return nil, serializer.NewError(serializer.CodeParamErr, "User has disabled share links in profile", nil) + } + + publicOnly := targetUser.Settings == nil || targetUser.Settings.ShareLinksInProfile == types.ProfilePublicShareOnly args := &inventory.ListShareArgs{ PaginationArgs: &inventory.PaginationArgs{ UseCursorPagination: true, @@ -146,7 +156,7 @@ func (s *ListShareService) ListInUserProfile(c *gin.Context, uid int) (*ListShar OrderBy: s.OrderBy, }, UserID: uid, - PublicOnly: true, + PublicOnly: publicOnly, } ctx := context.WithValue(c, inventory.LoadShareUser{}, true) diff --git a/service/user/response.go b/service/user/response.go index e83e99ed..6c30a2c0 100644 --- a/service/user/response.go +++ b/service/user/response.go @@ -29,6 +29,7 @@ type UserSettings struct { TwoFAEnabled bool `json:"two_fa_enabled"` Passkeys []Passkey `json:"passkeys,omitempty"` DisableViewSync bool `json:"disable_view_sync"` + ShareLinksInProfile string `json:"share_links_in_profile"` } func BuildUserSettings(u *ent.User, passkeys []*ent.Passkey, parser *uaparser.Parser) *UserSettings { @@ -41,7 +42,8 @@ func BuildUserSettings(u *ent.User, passkeys []*ent.Passkey, parser *uaparser.Pa Passkeys: lo.Map(passkeys, func(item *ent.Passkey, index int) Passkey { return BuildPasskey(item) }), - DisableViewSync: u.Settings.DisableViewSync, + DisableViewSync: u.Settings.DisableViewSync, + ShareLinksInProfile: string(u.Settings.ShareLinksInProfile), } } @@ -97,18 +99,19 @@ type BuiltinLoginResponse struct { // User 用户序列化器 type User struct { - ID string `json:"id"` - Email string `json:"email,omitempty"` - Nickname string `json:"nickname"` - Status user.Status `json:"status,omitempty"` - Avatar string `json:"avatar,omitempty"` - CreatedAt time.Time `json:"created_at"` - PreferredTheme string `json:"preferred_theme,omitempty"` - Anonymous bool `json:"anonymous,omitempty"` - Group *Group `json:"group,omitempty"` - Pined []types.PinedFile `json:"pined,omitempty"` - Language string `json:"language,omitempty"` - DisableViewSync bool `json:"disable_view_sync,omitempty"` + ID string `json:"id"` + Email string `json:"email,omitempty"` + Nickname string `json:"nickname"` + Status user.Status `json:"status,omitempty"` + Avatar string `json:"avatar,omitempty"` + CreatedAt time.Time `json:"created_at"` + PreferredTheme string `json:"preferred_theme,omitempty"` + Anonymous bool `json:"anonymous,omitempty"` + Group *Group `json:"group,omitempty"` + Pined []types.PinedFile `json:"pined,omitempty"` + Language string `json:"language,omitempty"` + DisableViewSync bool `json:"disable_view_sync,omitempty"` + ShareLinksInProfile types.ShareLinksInProfileLevel `json:"share_links_in_profile,omitempty"` } type Group struct { @@ -153,18 +156,19 @@ func BuildWebAuthnList(credentials []webauthn.Credential) []WebAuthnCredentials // BuildUser 序列化用户 func BuildUser(user *ent.User, idEncoder hashid.Encoder) User { return User{ - ID: hashid.EncodeUserID(idEncoder, user.ID), - Email: user.Email, - Nickname: user.Nick, - Status: user.Status, - Avatar: user.Avatar, - CreatedAt: user.CreatedAt, - PreferredTheme: user.Settings.PreferredTheme, - Anonymous: user.ID == 0, - Group: BuildGroup(user.Edges.Group, idEncoder), - Pined: user.Settings.Pined, - Language: user.Settings.Language, - DisableViewSync: user.Settings.DisableViewSync, + ID: hashid.EncodeUserID(idEncoder, user.ID), + Email: user.Email, + Nickname: user.Nick, + Status: user.Status, + Avatar: user.Avatar, + CreatedAt: user.CreatedAt, + PreferredTheme: user.Settings.PreferredTheme, + Anonymous: user.ID == 0, + Group: BuildGroup(user.Edges.Group, idEncoder), + Pined: user.Settings.Pined, + Language: user.Settings.Language, + DisableViewSync: user.Settings.DisableViewSync, + ShareLinksInProfile: user.Settings.ShareLinksInProfile, } } @@ -193,10 +197,11 @@ func BuildUserRedacted(u *ent.User, level int, idEncoder hashid.Encoder) User { userRaw := BuildUser(u, idEncoder) user := User{ - ID: userRaw.ID, - Nickname: userRaw.Nickname, - Avatar: userRaw.Avatar, - CreatedAt: userRaw.CreatedAt, + ID: userRaw.ID, + Nickname: userRaw.Nickname, + Avatar: userRaw.Avatar, + CreatedAt: userRaw.CreatedAt, + ShareLinksInProfile: userRaw.ShareLinksInProfile, } if userRaw.Group != nil { diff --git a/service/user/setting.go b/service/user/setting.go index c60085eb..1c2181bc 100644 --- a/service/user/setting.go +++ b/service/user/setting.go @@ -14,6 +14,7 @@ import ( "github.com/cloudreve/Cloudreve/v4/application/dependency" "github.com/cloudreve/Cloudreve/v4/ent" "github.com/cloudreve/Cloudreve/v4/inventory" + "github.com/cloudreve/Cloudreve/v4/inventory/types" "github.com/cloudreve/Cloudreve/v4/pkg/hashid" "github.com/cloudreve/Cloudreve/v4/pkg/request" "github.com/cloudreve/Cloudreve/v4/pkg/serializer" @@ -221,6 +222,7 @@ type ( TwoFAEnabled *bool `json:"two_fa_enabled" binding:"omitempty"` TwoFACode *string `json:"two_fa_code" binding:"omitempty"` DisableViewSync *bool `json:"disable_view_sync" binding:"omitempty"` + ShareLinksInProfile *string `json:"share_links_in_profile" binding:"omitempty"` } PatchUserSettingParamsCtx struct{} ) @@ -267,6 +269,11 @@ func (s *PatchUserSetting) Patch(c *gin.Context) error { saveSetting = true } + if s.ShareLinksInProfile != nil { + u.Settings.ShareLinksInProfile = types.ShareLinksInProfileLevel(*s.ShareLinksInProfile) + saveSetting = true + } + if s.CurrentPassword != nil && s.NewPassword != nil { if err := inventory.CheckPassword(u, *s.CurrentPassword); err != nil { return serializer.NewError(serializer.CodeIncorrectPassword, "Incorrect password", err) From 5f18d277c811455de1db591505d56a02d55c43ac Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Tue, 12 Aug 2025 09:53:15 +0800 Subject: [PATCH 25/74] fix(conf): ProxyHeader should be optional (#2760) --- application/migrator/conf/conf.go | 4 ++-- pkg/conf/types.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/application/migrator/conf/conf.go b/application/migrator/conf/conf.go index e34fc303..45d059d7 100644 --- a/application/migrator/conf/conf.go +++ b/application/migrator/conf/conf.go @@ -27,8 +27,8 @@ type system struct { Debug bool SessionSecret string HashIDSalt string - GracePeriod int `validate:"gte=0"` - ProxyHeader string `validate:"required_with=Listen"` + GracePeriod int `validate:"gte=0"` + ProxyHeader string } type ssl struct { diff --git a/pkg/conf/types.go b/pkg/conf/types.go index 509cecaf..bdf6f80b 100644 --- a/pkg/conf/types.go +++ b/pkg/conf/types.go @@ -46,7 +46,7 @@ type System struct { SessionSecret string HashIDSalt string // deprecated GracePeriod int `validate:"gte=0"` - ProxyHeader string `validate:"required_with=Listen"` + ProxyHeader string LogLevel string `validate:"oneof=debug info warning error"` } From bb9b42eb106358ec86e64a1d72ce2c184719ecf3 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Tue, 12 Aug 2025 13:10:46 +0800 Subject: [PATCH 26/74] feat(audit): flush audit logs into DB in a standalone goroutine --- application/application.go | 8 ++++++++ assets | 2 +- inventory/user.go | 25 +++++++++++++++++++++++-- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/application/application.go b/application/application.go index 20788810..14d8f9ef 100644 --- a/application/application.go +++ b/application/application.go @@ -164,6 +164,14 @@ func (s *server) Start() error { } func (s *server) Close() { + // Close audit recorder first to ensure all logs are persisted + if s.auditRecorder != nil { + s.logger.Info("Closing audit recorder...") + if err := s.auditRecorder.Close(); err != nil { + s.logger.Error("Failed to close audit recorder: %s", err) + } + } + if s.dbClient != nil { s.logger.Info("Shutting down database connection...") if err := s.dbClient.Close(); err != nil { diff --git a/assets b/assets index eb2cfac3..a15906f3 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit eb2cfac37d73e5bd3000eb66a3a0062509efe122 +Subproject commit a15906f3fb29710993e2e83835f92e563113aa18 diff --git a/inventory/user.go b/inventory/user.go index 26470ed9..f394050a 100644 --- a/inventory/user.go +++ b/inventory/user.go @@ -220,8 +220,29 @@ func (c *userClient) Delete(ctx context.Context, uid int) error { func (c *userClient) ApplyStorageDiff(ctx context.Context, diffs StorageDiff) error { ae := serializer.NewAggregateError() for uid, diff := range diffs { - if err := c.client.User.Update().Where(user.ID(uid)).AddStorage(diff).Exec(ctx); err != nil { - ae.Add(fmt.Sprintf("%d", uid), fmt.Errorf("failed to apply storage diff for user %d: %w", uid, err)) + // Retry logic for MySQL deadlock (Error 1213) + // This is a temporary workaround. TODO: optimize storage mutation + maxRetries := 3 + var lastErr error + for attempt := 0; attempt < maxRetries; attempt++ { + if err := c.client.User.Update().Where(user.ID(uid)).AddStorage(diff).Exec(ctx); err != nil { + lastErr = err + // Check if it's a MySQL deadlock error (Error 1213) + if strings.Contains(err.Error(), "Error 1213") && attempt < maxRetries-1 { + // Wait a bit before retrying with exponential backoff + time.Sleep(time.Duration(attempt+1) * 10 * time.Millisecond) + continue + } + ae.Add(fmt.Sprintf("%d", uid), fmt.Errorf("failed to apply storage diff for user %d: %w", uid, err)) + break + } + // Success, break out of retry loop + lastErr = nil + break + } + + if lastErr != nil { + ae.Add(fmt.Sprintf("%d", uid), fmt.Errorf("failed to apply storage diff for user %d: %w", uid, lastErr)) } } From 927c3bff00f82249532ced58174ef3596f3d13ff Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Tue, 12 Aug 2025 13:12:54 +0800 Subject: [PATCH 27/74] fix(dep): remove undefined dependency --- application/application.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/application/application.go b/application/application.go index 14d8f9ef..20788810 100644 --- a/application/application.go +++ b/application/application.go @@ -164,14 +164,6 @@ func (s *server) Start() error { } func (s *server) Close() { - // Close audit recorder first to ensure all logs are persisted - if s.auditRecorder != nil { - s.logger.Info("Closing audit recorder...") - if err := s.auditRecorder.Close(); err != nil { - s.logger.Error("Failed to close audit recorder: %s", err) - } - } - if s.dbClient != nil { s.logger.Info("Shutting down database connection...") if err := s.dbClient.Close(); err != nil { From c0132a10cbcfec49fedbcb9cc15dc6885d4a68f2 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Tue, 12 Aug 2025 13:27:07 +0800 Subject: [PATCH 28/74] feat(dashboard): upgrade promotion --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index a15906f3..ce3c8b62 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit a15906f3fb29710993e2e83835f92e563113aa18 +Subproject commit ce3c8b624a325d104093b15e7dec2bd7d980fb5b From f73583b370cc841546b99b65618d95b5495f9422 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Tue, 12 Aug 2025 13:27:33 +0800 Subject: [PATCH 29/74] update submodule --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index ce3c8b62..f7aa0a09 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit ce3c8b624a325d104093b15e7dec2bd7d980fb5b +Subproject commit f7aa0a09e22792eeeae574b585a499f0f459cede From 872b08e5da244dd94a57f1decdd2c4a72387b172 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Wed, 13 Aug 2025 18:54:56 +0800 Subject: [PATCH 30/74] fix(smtp): force enabling SSL does not work (#2777) --- pkg/email/smtp.go | 16 ++++++++-------- service/admin/tools.go | 15 ++++++++------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/pkg/email/smtp.go b/pkg/email/smtp.go index b9fcda86..b9ce863c 100644 --- a/pkg/email/smtp.go +++ b/pkg/email/smtp.go @@ -121,17 +121,17 @@ func (client *SMTPPool) Init() { } }() - tlsPolicy := mail.TLSOpportunistic + opts := []mail.Option{ + mail.WithPort(client.config.Port), + mail.WithTimeout(time.Duration(client.config.Keepalive+5) * time.Second), + mail.WithSMTPAuth(mail.SMTPAuthAutoDiscover), mail.WithTLSPortPolicy(mail.TLSOpportunistic), + mail.WithUsername(client.config.User), mail.WithPassword(client.config.Password), + } if client.config.ForceEncryption { - tlsPolicy = mail.TLSMandatory + opts = append(opts, mail.WithSSL()) } - d, diaErr := mail.NewClient(client.config.Host, - mail.WithPort(client.config.Port), - mail.WithTimeout(time.Duration(client.config.Keepalive+5)*time.Second), - mail.WithSMTPAuth(mail.SMTPAuthAutoDiscover), mail.WithTLSPortPolicy(tlsPolicy), - mail.WithUsername(client.config.User), mail.WithPassword(client.config.Password), - ) + d, diaErr := mail.NewClient(client.config.Host, opts...) if diaErr != nil { client.l.Panic("Failed to create SMTP client: %s", diaErr) return diff --git a/service/admin/tools.go b/service/admin/tools.go index 8bea48d1..e13e3a7c 100644 --- a/service/admin/tools.go +++ b/service/admin/tools.go @@ -139,15 +139,16 @@ func (s *TestSMTPService) Test(c *gin.Context) error { return serializer.NewError(serializer.CodeParamErr, "Invalid SMTP port", err) } - tlsPolicy := mail.TLSOpportunistic - if setting.IsTrueValue(s.Settings["smtpEncryption"]) { - tlsPolicy = mail.TLSMandatory - } - d, diaErr := mail.NewClient(s.Settings["smtpHost"], + opts := []mail.Option{ mail.WithPort(port), - mail.WithSMTPAuth(mail.SMTPAuthAutoDiscover), mail.WithTLSPortPolicy(tlsPolicy), + mail.WithSMTPAuth(mail.SMTPAuthAutoDiscover), mail.WithTLSPortPolicy(mail.TLSOpportunistic), mail.WithUsername(s.Settings["smtpUser"]), mail.WithPassword(s.Settings["smtpPass"]), - ) + } + if setting.IsTrueValue(s.Settings["smtpEncryption"]) { + opts = append(opts, mail.WithSSL()) + } + + d, diaErr := mail.NewClient(s.Settings["smtpHost"], opts...) if diaErr != nil { return serializer.NewError(serializer.CodeInternalSetting, "Failed to create SMTP client: "+diaErr.Error(), diaErr) } From a1ce16bd5e725e8e76e6ac839537739c9eb66117 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Tue, 19 Aug 2025 09:43:23 +0800 Subject: [PATCH 31/74] fix(smtp): SMTP reset error should be ignored for non-standard SMTP server implementation (#2791) --- assets | 2 +- pkg/email/smtp.go | 11 +++++++++++ service/admin/tools.go | 8 ++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/assets b/assets index f7aa0a09..2c5b89c5 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit f7aa0a09e22792eeeae574b585a499f0f459cede +Subproject commit 2c5b89c59cd69a7690938f4a980468006b49b547 diff --git a/pkg/email/smtp.go b/pkg/email/smtp.go index b9ce863c..ebb5c74f 100644 --- a/pkg/email/smtp.go +++ b/pkg/email/smtp.go @@ -2,6 +2,7 @@ package email import ( "context" + "errors" "fmt" "strings" "time" @@ -159,6 +160,16 @@ func (client *SMTPPool) Init() { l := client.l.CopyWithPrefix(fmt.Sprintf("[Cid: %s]", m.cid)) if err := d.Send(m.msg); err != nil { + // Check if this is an SMTP RESET error after successful delivery + var sendErr *mail.SendError + var errParsed = errors.As(err, &sendErr) + if errParsed && sendErr.Reason == mail.ErrSMTPReset { + open = false + l.Debug("SMTP RESET error, closing connection...") + // https://github.com/wneessen/go-mail/issues/463 + continue // Don't treat this as a delivery failure since mail was sent + } + l.Warning("Failed to send email: %s, Cid=%s", err, m.cid) } else { l.Info("Email sent to %q, title: %q.", m.to, m.subject) diff --git a/service/admin/tools.go b/service/admin/tools.go index e13e3a7c..ac701b44 100644 --- a/service/admin/tools.go +++ b/service/admin/tools.go @@ -2,6 +2,7 @@ package admin import ( "encoding/hex" + "errors" "net/http" "strconv" @@ -165,6 +166,13 @@ func (s *TestSMTPService) Test(c *gin.Context) error { err = d.DialAndSendWithContext(c, m) if err != nil { + // Check if this is an SMTP RESET error after successful delivery + var sendErr *mail.SendError + var errParsed = errors.As(err, &sendErr) + if errParsed && sendErr.Reason == mail.ErrSMTPReset { + return nil // Don't treat this as a delivery failure since mail was sent + } + return serializer.NewError(serializer.CodeInternalSetting, "Failed to send test email: "+err.Error(), err) } From 91717b7c4985853687ce8a16e742c4a54cddfdd2 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Thu, 21 Aug 2025 10:20:13 +0800 Subject: [PATCH 32/74] feat(archive): add support for 7z and bz2 / extract rar and 7zip files protected with password (#2668) --- assets | 2 +- go.mod | 29 +++-- go.sum | 61 +++++---- .../manager/entitysource/entitysource.go | 119 +++++++++++++----- pkg/filemanager/workflows/extract.go | 113 ++++++++++++++--- service/explorer/workflows.go | 3 +- 6 files changed, 244 insertions(+), 83 deletions(-) diff --git a/assets b/assets index 2c5b89c5..5a1665a9 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 2c5b89c59cd69a7690938f4a980468006b49b547 +Subproject commit 5a1665a96a96234fb7ea5fd5131b15ecebe127be diff --git a/go.mod b/go.mod index d12dcdd1..c1834d6f 100644 --- a/go.mod +++ b/go.mod @@ -39,7 +39,7 @@ require ( github.com/juju/ratelimit v1.0.1 github.com/ks3sdklib/aws-sdk-go v1.6.2 github.com/lib/pq v1.10.9 - github.com/mholt/archiver/v4 v4.0.0-alpha.6 + github.com/mholt/archives v0.1.3 github.com/mojocn/base64Captcha v0.0.0-20190801020520-752b1cd608b2 github.com/pquerna/otp v1.2.0 github.com/qiniu/go-sdk/v7 v7.19.0 @@ -65,9 +65,13 @@ require ( require ( ariga.io/atlas v0.19.1-0.20240203083654-5948b60a8e43 // indirect cloud.google.com/go v0.81.0 // indirect + github.com/STARRY-S/zip v0.2.1 // indirect github.com/agext/levenshtein v1.2.1 // indirect - github.com/andybalholm/brotli v1.0.4 // indirect + github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3 // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect + github.com/bodgit/plumbing v1.3.0 // indirect + github.com/bodgit/sevenzip v1.6.0 // indirect + github.com/bodgit/windows v1.0.1 // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/bytedance/sonic v1.11.6 // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect @@ -76,7 +80,7 @@ require ( github.com/cloudwego/iasm v0.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3 // indirect - github.com/dsnet/compress v0.0.1 // indirect + github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect github.com/dsoprea/go-exif/v2 v2.0.0-20200604193436-ca8584a0e1c4 // indirect github.com/dsoprea/go-iptc v0.0.0-20200609062250-162ae6b44feb // indirect github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect @@ -95,10 +99,11 @@ require ( github.com/goccy/go-json v0.10.2 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect - github.com/golang/snappy v0.0.4 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-tpm v0.9.1 // indirect github.com/gorilla/context v1.1.2 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl/v2 v2.13.0 // indirect @@ -106,32 +111,34 @@ require ( github.com/jinzhu/inflection v1.0.0 // indirect github.com/jmespath/go-jmespath v0.3.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.17.7 // indirect + github.com/klauspost/compress v1.17.11 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect - github.com/klauspost/pgzip v1.2.5 // indirect + github.com/klauspost/pgzip v1.2.6 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mikelolasagasti/xz v1.0.1 // indirect + github.com/minio/minlz v1.0.0 // indirect github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mozillazg/go-httpheader v0.4.0 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect - github.com/nwaples/rardecode/v2 v2.0.0-beta.2 // indirect + github.com/nwaples/rardecode/v2 v2.1.0 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect - github.com/pierrec/lz4/v4 v4.1.14 // indirect + github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/sorairolake/lzip-go v0.3.5 // indirect github.com/stretchr/objx v0.5.2 // indirect - github.com/therootcompany/xz v1.0.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect - github.com/ulikunitz/xz v0.5.10 // indirect + github.com/ulikunitz/xz v0.5.12 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/zclconf/go-cty v1.8.0 // indirect - go4.org v0.0.0-20200411211856-f5505b9728dd // indirect + go4.org v0.0.0-20230225012048-214862532bf5 // indirect golang.org/x/arch v0.8.0 // indirect golang.org/x/crypto v0.36.0 // indirect golang.org/x/mod v0.20.0 // indirect diff --git a/go.sum b/go.sum index 6f70e0b5..ed10902b 100644 --- a/go.sum +++ b/go.sum @@ -82,6 +82,8 @@ github.com/Masterminds/sprig v2.15.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuN github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/QcloudApi/qcloud_sign_golang v0.0.0-20141224014652-e4130a326409/go.mod h1:1pk82RBxDY/JZnPQrtqHlUFfCctgdorsd9M06fMynOM= +github.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg= +github.com/STARRY-S/zip v0.2.1/go.mod h1:xNvshLODWtC4EJ702g7cTYn13G53o1+X9BWnPFpcWV4= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= @@ -100,8 +102,8 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F7Y6wUM49Lcha2FMXt4UM8g= github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8= -github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= -github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3 h1:8PmGpDEZl9yDpcdEr6Odf23feCxK3LNUNMxjXg41pZQ= +github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ= @@ -138,6 +140,12 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI= +github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU= +github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs= +github.com/bodgit/sevenzip v1.6.0 h1:a4R0Wu6/P1o1pP/3VV++aEOcyeBxeO/xE2Y9NSTrr6A= +github.com/bodgit/sevenzip v1.6.0/go.mod h1:zOBh9nJUof7tcrlqJFv1koWRrhz3LbDbUNngkuZxLMc= +github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4= +github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= @@ -213,8 +221,8 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8 github.com/dhowden/tag v0.0.0-20230630033851-978a0926ee25 h1:simG0vMYFvNriGhaaat7QVVkaVkXzvqcohaBoLZl9Hg= github.com/dhowden/tag v0.0.0-20230630033851-978a0926ee25/go.mod h1:Z3Lomva4pyMWYezjMAU5QWRh0p1VvO4199OHlFnyKkM= github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= -github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q= -github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo= +github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4= +github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= github.com/dsoprea/go-exif/v2 v2.0.0-20200321225314-640175a69fe4/go.mod h1:Lm2lMM2zx8p4a34ZemkaUV95AnMl4ZvLbCUbwOvLC2E= github.com/dsoprea/go-exif/v2 v2.0.0-20200520183328-015129a9efd5/go.mod h1:9EXlPeHfblFFnwu5UOqmP2eoZfJyAZ2Ri/Vki33ajO0= @@ -414,8 +422,6 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomodule/redigo v1.9.2 h1:HrutZBLhSIU8abiSfW8pj8mPhOyMYjZT/wcA4/L9L9s= github.com/gomodule/redigo v1.9.2/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -519,11 +525,15 @@ github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoP github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.6.4/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= @@ -610,14 +620,14 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= github.com/kisom/goutils v1.4.3/go.mod h1:Lp5qrquG7yhYnWzZCI/68Pa/GpFynw//od6EkGnWpac= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= -github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE= -github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= +github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -686,11 +696,15 @@ github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= -github.com/mholt/archiver/v4 v4.0.0-alpha.6 h1:3wvos9Kn1GpKNBz+MpozinGREPslLo1ds1W16vTkErQ= -github.com/mholt/archiver/v4 v4.0.0-alpha.6/go.mod h1:9PTygYq90FQBWPspdwAng6dNjYiBuTYKqmA6c15KuCo= +github.com/mholt/archives v0.1.3 h1:aEAaOtNra78G+TvV5ohmXrJOAzf++dIlYeDW3N9q458= +github.com/mholt/archives v0.1.3/go.mod h1:LUCGp++/IbV/I0Xq4SzcIR6uwgeh2yjnQWamjRQfLTU= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0= +github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc= +github.com/minio/minlz v1.0.0 h1:Kj7aJZ1//LlTP1DM8Jm7lNKvvJS2m74gyyXXn3+uJWQ= +github.com/minio/minlz v1.0.0/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -736,8 +750,8 @@ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdh github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/nishanths/predeclared v0.0.0-20200524104333-86fad755b4d3/go.mod h1:nt3d53pc1VYcphSCIaYAJtnPYnr3Zyn8fMq2wvPGPso= github.com/nkovacs/streamquote v1.0.0/go.mod h1:BN+NaZ2CmdKqUuTUXUEm9j95B2TRbpOWpxbJYzzgUsc= -github.com/nwaples/rardecode/v2 v2.0.0-beta.2 h1:e3mzJFJs4k83GXBEiTaQ5HgSc/kOK8q0rDaRO0MPaOk= -github.com/nwaples/rardecode/v2 v2.0.0-beta.2/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY= +github.com/nwaples/rardecode/v2 v2.1.0 h1:JQl9ZoBPDy+nIZGb1mx8+anfHp/LV3NE2MjMiv0ct/U= +github.com/nwaples/rardecode/v2 v2.1.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= @@ -774,8 +788,8 @@ github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= -github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE= -github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= +github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -877,6 +891,8 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k github.com/soheilhy/cmux v0.1.5-0.20210205191134-5ec6847320e5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/sorairolake/lzip-go v0.3.5 h1:ms5Xri9o1JBIWvOFAorYtUNik6HI3HgBTkISiqu0Cwg= +github.com/sorairolake/lzip-go v0.3.5/go.mod h1:N0KYq5iWrMXI0ZEXKXaS9hCyOjZUQdBDEIbXfoUwbdk= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/speps/go-hashids v2.0.0+incompatible h1:kSfxGfESueJKTx0mpER9Y/1XHl+FVQjtCqRyYcviFbw= github.com/speps/go-hashids v2.0.0+incompatible/go.mod h1:P7hqPzMdnZOfyIk+xrlG1QaSMw+gCBdHKsBDnhpaZvc= @@ -928,8 +944,6 @@ github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.563/go.mod github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/kms v1.0.563/go.mod h1:uom4Nvi9W+Qkom0exYiJ9VWJjXwyxtPYTkKkaLMlfE0= github.com/tencentyun/cos-go-sdk-v5 v0.7.54 h1:FRamEhNBbSeggyYfWfzFejTLftgbICocSYFk4PKTSV4= github.com/tencentyun/cos-go-sdk-v5 v0.7.54/go.mod h1:UN+VdbCl1hg+kKi5RXqZgaP+Boqfmk+D04GRc4XFk70= -github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= -github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0= github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao= @@ -951,8 +965,9 @@ github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65E github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= -github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= +github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/upyun/go-sdk v2.1.0+incompatible h1:OdjXghQ/TVetWV16Pz3C1/SUpjhGBVPr+cLiqZLLyq0= github.com/upyun/go-sdk v2.1.0+incompatible/go.mod h1:eu3F5Uz4b9ZE5bE5QsCL6mgSNWRwfj0zpJ9J626HEqs= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= @@ -974,6 +989,8 @@ github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0B github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -1023,8 +1040,9 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= -go4.org v0.0.0-20200411211856-f5505b9728dd h1:BNJlw5kRTzdmyfh5U8F93HA2OwkP7ZGwA51eJ/0wKOU= go4.org v0.0.0-20200411211856-f5505b9728dd/go.mod h1:CIiUVy99QCPfoE13bO4EZaz5GZMZXMSBGhxRdsvzbkg= +go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc= +go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU= gocloud.dev v0.19.0/go.mod h1:SmKwiR8YwIMMJvQBKLsC3fHNyMwXLw3PMDO+VVteJMI= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= @@ -1160,6 +1178,7 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= diff --git a/pkg/filemanager/manager/entitysource/entitysource.go b/pkg/filemanager/manager/entitysource/entitysource.go index f664c194..a7632361 100644 --- a/pkg/filemanager/manager/entitysource/entitysource.go +++ b/pkg/filemanager/manager/entitysource/entitysource.go @@ -163,6 +163,10 @@ type ( rsc io.ReadCloser pos int64 o *EntitySourceOptions + + // Cache for resetRequest URL and expiry + cachedUrl string + cachedExpiry time.Time } ) @@ -215,6 +219,10 @@ func NewEntitySource( } func (f *entitySource) Apply(opts ...EntitySourceOption) { + if len(opts) > 0 { + // Clear cache when options are applied as they might affect URL generation + f.clearUrlCache() + } for _, opt := range opts { opt.Apply(f.o) } @@ -247,6 +255,10 @@ func (f *entitySource) LocalPath(ctx context.Context) string { } func (f *entitySource) Serve(w http.ResponseWriter, r *http.Request, opts ...EntitySourceOption) { + if len(opts) > 0 { + // Clear cache when options are applied as they might affect URL generation + f.clearUrlCache() + } for _, opt := range opts { opt.Apply(f.o) } @@ -478,16 +490,22 @@ func (f *entitySource) Read(p []byte) (n int, err error) { } func (f *entitySource) ReadAt(p []byte, off int64) (n int, err error) { - if f.IsLocal() { - if f.rsc == nil { - err = f.resetRequest() - } - if readAt, ok := f.rsc.(io.ReaderAt); ok { - return readAt.ReadAt(p, off) + if f.rsc == nil { + err = f.resetRequest() + if err != nil { + return 0, err } } + if readAt, ok := f.rsc.(io.ReaderAt); ok { + return readAt.ReadAt(p, off) + } - return 0, errors.New("source does not support ReadAt") + // For non-local sources, use HTTP range request to read at specific offset + rsc, err := f.getRsc(off) + if err != nil { + return 0, err + } + return io.ReadFull(rsc, p) } func (f *entitySource) Seek(offset int64, whence int) (int64, error) { @@ -524,6 +542,12 @@ func (f *entitySource) Close() error { return nil } +// clearUrlCache clears the cached URL and expiry +func (f *entitySource) clearUrlCache() { + f.cachedUrl = "" + f.cachedExpiry = time.Time{} +} + func (f *entitySource) ShouldInternalProxy(opts ...EntitySourceOption) bool { for _, opt := range opts { opt.Apply(f.o) @@ -534,6 +558,10 @@ func (f *entitySource) ShouldInternalProxy(opts ...EntitySourceOption) bool { } func (f *entitySource) Url(ctx context.Context, opts ...EntitySourceOption) (*EntityUrl, error) { + if len(opts) > 0 { + // Clear cache when options are applied as they might affect URL generation + f.clearUrlCache() + } for _, opt := range opts { opt.Apply(f.o) } @@ -613,50 +641,75 @@ func (f *entitySource) Url(ctx context.Context, opts ...EntitySourceOption) (*En func (f *entitySource) resetRequest() error { // For inbound files, we can use the handler to open the file directly - if f.IsLocal() { - if f.rsc == nil { - file, err := f.handler.Open(f.o.Ctx, f.e.Source()) - if err != nil { - return fmt.Errorf("failed to open inbound file: %w", err) - } + if f.IsLocal() && f.rsc != nil { + return nil + } - if f.pos > 0 { - _, err = file.Seek(f.pos, io.SeekStart) - if err != nil { - return fmt.Errorf("failed to seek inbound file: %w", err) - } - } + rsc, err := f.getRsc(f.pos) + if err != nil { + return fmt.Errorf("failed to get rsc: %w", err) + } + f.rsc = rsc + return nil +} - f.rsc = file +func (f *entitySource) getRsc(pos int64) (io.ReadCloser, error) { + // For inbound files, we can use the handler to open the file directly + if f.IsLocal() { + file, err := f.handler.Open(f.o.Ctx, f.e.Source()) + if err != nil { + return nil, fmt.Errorf("failed to open inbound file: %w", err) + } - if f.o.SpeedLimit > 0 { - bucket := ratelimit.NewBucketWithRate(float64(f.o.SpeedLimit), f.o.SpeedLimit) - f.rsc = lrs{f.rsc, ratelimit.Reader(f.rsc, bucket)} + if pos > 0 { + _, err = file.Seek(pos, io.SeekStart) + if err != nil { + return nil, fmt.Errorf("failed to seek inbound file: %w", err) } } - return nil + if f.o.SpeedLimit > 0 { + bucket := ratelimit.NewBucketWithRate(float64(f.o.SpeedLimit), f.o.SpeedLimit) + return lrs{f.rsc, ratelimit.Reader(f.rsc, bucket)}, nil + } else { + return file, nil + } + } - expire := time.Now().Add(defaultUrlExpire) - u, err := f.Url(driver.WithForcePublicEndpoint(f.o.Ctx, false), WithNoInternalProxy(), WithExpire(&expire)) - if err != nil { - return fmt.Errorf("failed to generate download url: %w", err) + var urlStr string + now := time.Now() + + // Check if we have a valid cached URL and expiry + if f.cachedUrl != "" && now.Before(f.cachedExpiry.Add(-time.Minute)) { + // Use cached URL if it's still valid (with 1 minute buffer before expiry) + urlStr = f.cachedUrl + } else { + // Generate new URL and cache it + expire := now.Add(defaultUrlExpire) + u, err := f.Url(driver.WithForcePublicEndpoint(f.o.Ctx, false), WithNoInternalProxy(), WithExpire(&expire)) + if err != nil { + return nil, fmt.Errorf("failed to generate download url: %w", err) + } + + // Cache the URL and expiry + f.cachedUrl = u.Url + f.cachedExpiry = expire + urlStr = u.Url } h := http.Header{} - h.Set("Range", fmt.Sprintf("bytes=%d-", f.pos)) - resp := f.c.Request(http.MethodGet, u.Url, nil, + h.Set("Range", fmt.Sprintf("bytes=%d-", pos)) + resp := f.c.Request(http.MethodGet, urlStr, nil, request.WithContext(f.o.Ctx), request.WithLogger(f.l), request.WithHeader(h), ).CheckHTTPResponse(http.StatusOK, http.StatusPartialContent) if resp.Err != nil { - return fmt.Errorf("failed to request download url: %w", resp.Err) + return nil, fmt.Errorf("failed to request download url: %w", resp.Err) } - f.rsc = resp.Response.Body - return nil + return resp.Response.Body, nil } // capExpireTime make sure expire time is not too long or too short (if min or max is set) diff --git a/pkg/filemanager/workflows/extract.go b/pkg/filemanager/workflows/extract.go index 7126e60b..00616f77 100644 --- a/pkg/filemanager/workflows/extract.go +++ b/pkg/filemanager/workflows/extract.go @@ -26,7 +26,14 @@ import ( "github.com/cloudreve/Cloudreve/v4/pkg/queue" "github.com/cloudreve/Cloudreve/v4/pkg/util" "github.com/gofrs/uuid" - "github.com/mholt/archiver/v4" + "github.com/mholt/archives" + "golang.org/x/text/encoding" + "golang.org/x/text/encoding/charmap" + "golang.org/x/text/encoding/japanese" + "golang.org/x/text/encoding/korean" + "golang.org/x/text/encoding/simplifiedchinese" + "golang.org/x/text/encoding/traditionalchinese" + "golang.org/x/text/encoding/unicode" ) type ( @@ -47,6 +54,7 @@ type ( TempZipFilePath string `json:"temp_zip_file_path,omitempty"` ProcessedCursor string `json:"processed_cursor,omitempty"` SlaveTaskID int `json:"slave_task_id,omitempty"` + Password string `json:"password,omitempty"` NodeState `json:",inline"` Phase ExtractArchiveTaskPhase `json:"phase,omitempty"` } @@ -70,13 +78,54 @@ func init() { queue.RegisterResumableTaskFactory(queue.ExtractArchiveTaskType, NewExtractArchiveTaskFromModel) } +var encodings = map[string]encoding.Encoding{ + "ibm866": charmap.CodePage866, + "iso8859_2": charmap.ISO8859_2, + "iso8859_3": charmap.ISO8859_3, + "iso8859_4": charmap.ISO8859_4, + "iso8859_5": charmap.ISO8859_5, + "iso8859_6": charmap.ISO8859_6, + "iso8859_7": charmap.ISO8859_7, + "iso8859_8": charmap.ISO8859_8, + "iso8859_8I": charmap.ISO8859_8I, + "iso8859_10": charmap.ISO8859_10, + "iso8859_13": charmap.ISO8859_13, + "iso8859_14": charmap.ISO8859_14, + "iso8859_15": charmap.ISO8859_15, + "iso8859_16": charmap.ISO8859_16, + "koi8r": charmap.KOI8R, + "koi8u": charmap.KOI8U, + "macintosh": charmap.Macintosh, + "windows874": charmap.Windows874, + "windows1250": charmap.Windows1250, + "windows1251": charmap.Windows1251, + "windows1252": charmap.Windows1252, + "windows1253": charmap.Windows1253, + "windows1254": charmap.Windows1254, + "windows1255": charmap.Windows1255, + "windows1256": charmap.Windows1256, + "windows1257": charmap.Windows1257, + "windows1258": charmap.Windows1258, + "macintoshcyrillic": charmap.MacintoshCyrillic, + "gbk": simplifiedchinese.GBK, + "gb18030": simplifiedchinese.GB18030, + "big5": traditionalchinese.Big5, + "eucjp": japanese.EUCJP, + "iso2022jp": japanese.ISO2022JP, + "shiftjis": japanese.ShiftJIS, + "euckr": korean.EUCKR, + "utf16be": unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM), + "utf16le": unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM), +} + // NewExtractArchiveTask creates a new ExtractArchiveTask -func NewExtractArchiveTask(ctx context.Context, src, dst, encoding string) (queue.Task, error) { +func NewExtractArchiveTask(ctx context.Context, src, dst, encoding, password string) (queue.Task, error) { state := &ExtractArchiveTaskState{ Uri: src, Dst: dst, Encoding: encoding, NodeState: NodeState{}, + Password: password, } stateBytes, err := json.Marshal(state) if err != nil { @@ -197,6 +246,7 @@ func (m *ExtractArchiveTask) createSlaveExtractTask(ctx context.Context, dep dep Encoding: m.state.Encoding, Dst: m.state.Dst, UserID: user.ID, + Password: m.state.Password, } payloadStr, err := json.Marshal(payload) @@ -277,20 +327,21 @@ func (m *ExtractArchiveTask) masterExtractArchive(ctx context.Context, dep depen m.l.Info("Extracting archive %q to %q", uri, m.state.Dst) // Identify file format - format, readStream, err := archiver.Identify(archiveFile.DisplayName(), es) + format, readStream, err := archives.Identify(ctx, archiveFile.DisplayName(), es) if err != nil { return task.StatusError, fmt.Errorf("failed to identify archive format: %w", err) } - m.l.Info("Archive file %q format identified as %q", uri, format.Name()) + m.l.Info("Archive file %q format identified as %q", uri, format.Extension()) - extractor, ok := format.(archiver.Extractor) + extractor, ok := format.(archives.Extractor) if !ok { return task.StatusError, fmt.Errorf("format not an extractor %s") } - if format.Name() == ".zip" { - // Zip extractor requires a Seeker+ReadAt + formatExt := format.Extension() + if formatExt == ".zip" || formatExt == ".7z" { + // Zip/7Z extractor requires a Seeker+ReadAt if m.state.TempZipFilePath == "" && !es.IsLocal() { m.state.Phase = ExtractArchivePhaseDownloadZip m.ResumeAfter(0) @@ -315,11 +366,25 @@ func (m *ExtractArchiveTask) masterExtractArchive(ctx context.Context, dep depen readStream = es } + } + if zipExtractor, ok := extractor.(archives.Zip); ok { if m.state.Encoding != "" { m.l.Info("Using encoding %q for zip archive", m.state.Encoding) - extractor = archiver.Zip{TextEncoding: m.state.Encoding} + encoding, ok := encodings[strings.ToLower(m.state.Encoding)] + if !ok { + m.l.Warning("Unknown encoding %q, fallback to default encoding", m.state.Encoding) + } else { + zipExtractor.TextEncoding = encoding + extractor = zipExtractor + } } + } else if rarExtractor, ok := extractor.(archives.Rar); ok && m.state.Password != "" { + rarExtractor.Password = m.state.Password + extractor = rarExtractor + } else if sevenZipExtractor, ok := extractor.(archives.SevenZip); ok && m.state.Password != "" { + sevenZipExtractor.Password = m.state.Password + extractor = sevenZipExtractor } needSkipToCursor := false @@ -332,7 +397,7 @@ func (m *ExtractArchiveTask) masterExtractArchive(ctx context.Context, dep depen m.Unlock() // extract and upload - err = extractor.Extract(ctx, readStream, nil, func(ctx context.Context, f archiver.File) error { + err = extractor.Extract(ctx, readStream, func(ctx context.Context, f archives.FileInfo) error { if needSkipToCursor && f.NameInArchive != m.state.ProcessedCursor { atomic.AddInt64(&m.progress[ProgressTypeExtractCount].Current, 1) atomic.AddInt64(&m.progress[ProgressTypeExtractSize].Current, f.Size()) @@ -533,6 +598,7 @@ type ( TempPath string `json:"temp_path,omitempty"` TempZipFilePath string `json:"temp_zip_file_path,omitempty"` ProcessedCursor string `json:"processed_cursor,omitempty"` + Password string `json:"password,omitempty"` } ) @@ -602,18 +668,19 @@ func (m *SlaveExtractArchiveTask) Do(ctx context.Context) (task.Status, error) { defer es.Close() // 2. Identify file format - format, readStream, err := archiver.Identify(m.state.FileName, es) + format, readStream, err := archives.Identify(ctx, m.state.FileName, es) if err != nil { return task.StatusError, fmt.Errorf("failed to identify archive format: %w", err) } - m.l.Info("Archive file %q format identified as %q", m.state.FileName, format.Name()) + m.l.Info("Archive file %q format identified as %q", m.state.FileName, format.Extension()) - extractor, ok := format.(archiver.Extractor) + extractor, ok := format.(archives.Extractor) if !ok { - return task.StatusError, fmt.Errorf("format not an extractor %s") + return task.StatusError, fmt.Errorf("format not an extractor %q", format.Extension()) } - if format.Name() == ".zip" { + formatExt := format.Extension() + if formatExt == ".zip" || formatExt == ".7z" { if _, err = es.Seek(0, 0); err != nil { return task.StatusError, fmt.Errorf("failed to seek entity source: %w", err) } @@ -666,11 +733,25 @@ func (m *SlaveExtractArchiveTask) Do(ctx context.Context) (task.Status, error) { if es.IsLocal() { readStream = es } + } + if zipExtractor, ok := extractor.(archives.Zip); ok { if m.state.Encoding != "" { m.l.Info("Using encoding %q for zip archive", m.state.Encoding) - extractor = archiver.Zip{TextEncoding: m.state.Encoding} + encoding, ok := encodings[strings.ToLower(m.state.Encoding)] + if !ok { + m.l.Warning("Unknown encoding %q, fallback to default encoding", m.state.Encoding) + } else { + zipExtractor.TextEncoding = encoding + extractor = zipExtractor + } } + } else if rarExtractor, ok := extractor.(archives.Rar); ok && m.state.Password != "" { + rarExtractor.Password = m.state.Password + extractor = rarExtractor + } else if sevenZipExtractor, ok := extractor.(archives.SevenZip); ok && m.state.Password != "" { + sevenZipExtractor.Password = m.state.Password + extractor = sevenZipExtractor } needSkipToCursor := false @@ -679,7 +760,7 @@ func (m *SlaveExtractArchiveTask) Do(ctx context.Context) (task.Status, error) { } // 3. Extract and upload - err = extractor.Extract(ctx, readStream, nil, func(ctx context.Context, f archiver.File) error { + err = extractor.Extract(ctx, readStream, func(ctx context.Context, f archives.FileInfo) error { if needSkipToCursor && f.NameInArchive != m.state.ProcessedCursor { atomic.AddInt64(&m.progress[ProgressTypeExtractCount].Current, 1) atomic.AddInt64(&m.progress[ProgressTypeExtractSize].Current, f.Size()) diff --git a/service/explorer/workflows.go b/service/explorer/workflows.go index 7d54bfc6..138b2922 100644 --- a/service/explorer/workflows.go +++ b/service/explorer/workflows.go @@ -173,6 +173,7 @@ type ( Src []string `json:"src" binding:"required"` Dst string `json:"dst" binding:"required"` Encoding string `json:"encoding"` + Password string `json:"password"` } CreateArchiveParamCtx struct{} ) @@ -203,7 +204,7 @@ func (service *ArchiveWorkflowService) CreateExtractTask(c *gin.Context) (*TaskR } // Create task - t, err := workflows.NewExtractArchiveTask(c, service.Src[0], service.Dst, service.Encoding) + t, err := workflows.NewExtractArchiveTask(c, service.Src[0], service.Dst, service.Encoding, service.Password) if err != nil { return nil, serializer.NewError(serializer.CodeCreateTaskError, "Failed to create task", err) } From 13e774f27d5bed3c83e7355d3006a1ca9fec92ac Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Thu, 21 Aug 2025 13:14:11 +0800 Subject: [PATCH 33/74] feat(dashboard): filter file by shared link, direct link, uploading status (#2667) --- assets | 2 +- inventory/file.go | 15 +++++++++++++++ service/admin/file.go | 34 ++++++++++++++++++++++++++++------ 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/assets b/assets index 5a1665a9..2827c6bc 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 5a1665a96a96234fb7ea5fd5131b15ecebe127be +Subproject commit 2827c6bc2eddc44597f429d1a35c446c15aa7f30 diff --git a/inventory/file.go b/inventory/file.go index 56b4d4c9..788808ee 100644 --- a/inventory/file.go +++ b/inventory/file.go @@ -57,6 +57,9 @@ type ( UserID int Name string StoragePolicyID int + HasMetadata string + Shared bool + HasDirectLink bool } MetadataFilter struct { @@ -1098,6 +1101,18 @@ func (f *fileClient) FlattenListFiles(ctx context.Context, args *FlattenListFile query = query.Where(file.NameContainsFold(args.Name)) } + if args.HasMetadata != "" { + query = query.Where(file.HasMetadataWith(metadata.Name(args.HasMetadata))) + } + + if args.Shared { + query = query.Where(file.HasSharesWith(share.DeletedAtIsNil())) + } + + if args.HasDirectLink { + query = query.Where(file.HasDirectLinksWith(directlink.DeletedAtIsNil())) + } + query.Order(getFileOrderOption(&ListFileParameters{ PaginationArgs: args.PaginationArgs, })...) diff --git a/service/admin/file.go b/service/admin/file.go index 4e46db08..aeb3fb01 100644 --- a/service/admin/file.go +++ b/service/admin/file.go @@ -16,6 +16,7 @@ import ( "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource" "github.com/cloudreve/Cloudreve/v4/pkg/hashid" "github.com/cloudreve/Cloudreve/v4/pkg/serializer" + "github.com/cloudreve/Cloudreve/v4/pkg/setting" "github.com/gin-gonic/gin" "github.com/samber/lo" ) @@ -150,9 +151,12 @@ func (service *FileBatchService) Delete(c *gin.Context) serializer.Response { } const ( - fileNameCondition = "file_name" - fileUserCondition = "file_user" - filePolicyCondition = "file_policy" + fileNameCondition = "file_name" + fileUserCondition = "file_user" + filePolicyCondition = "file_policy" + fileMetadataCondition = "file_metadata" + fileSharedCondition = "file_shared" + fileDirectLinkCondition = "file_direct_link" ) func (service *AdminListService) Files(c *gin.Context) (*ListFileResponse, error) { @@ -167,9 +171,12 @@ func (service *AdminListService) Files(c *gin.Context) (*ListFileResponse, error ctx = context.WithValue(ctx, inventory.LoadFileDirectLink{}, true) var ( - err error - userID int - policyID int + err error + userID int + policyID int + metadata string + shared bool + directLink bool ) if service.Conditions[fileUserCondition] != "" { @@ -186,6 +193,18 @@ func (service *AdminListService) Files(c *gin.Context) (*ListFileResponse, error } } + if service.Conditions[fileMetadataCondition] != "" { + metadata = service.Conditions[fileMetadataCondition] + } + + if service.Conditions[fileSharedCondition] != "" && setting.IsTrueValue(service.Conditions[fileSharedCondition]) { + shared = true + } + + if service.Conditions[fileDirectLinkCondition] != "" && setting.IsTrueValue(service.Conditions[fileDirectLinkCondition]) { + directLink = true + } + res, err := fileClient.FlattenListFiles(ctx, &inventory.FlattenListFileParameters{ PaginationArgs: &inventory.PaginationArgs{ Page: service.Page - 1, @@ -196,6 +215,9 @@ func (service *AdminListService) Files(c *gin.Context) (*ListFileResponse, error UserID: userID, StoragePolicyID: policyID, Name: service.Conditions[fileNameCondition], + HasMetadata: metadata, + Shared: shared, + HasDirectLink: directLink, }) if err != nil { From a677e23394cdd66e18319738854f044728e09476 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Thu, 21 Aug 2025 14:12:30 +0800 Subject: [PATCH 34/74] feat(dashboard): filter file by shared link, direct link, uploading status (#2782) --- assets | 2 +- pkg/filemanager/fs/dbfs/share_navigator.go | 8 ++++++++ pkg/serializer/error.go | 2 ++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/assets b/assets index 2827c6bc..ad1a0215 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 2827c6bc2eddc44597f429d1a35c446c15aa7f30 +Subproject commit ad1a02152cff89f6ce21f3fcaf345b3878755ccc diff --git a/pkg/filemanager/fs/dbfs/share_navigator.go b/pkg/filemanager/fs/dbfs/share_navigator.go index 374994f3..c03caf83 100644 --- a/pkg/filemanager/fs/dbfs/share_navigator.go +++ b/pkg/filemanager/fs/dbfs/share_navigator.go @@ -157,6 +157,14 @@ func (n *shareNavigator) Root(ctx context.Context, path *fs.URI) (*File, error) } if n.user.ID != n.owner.ID && !n.user.Edges.Group.Permissions.Enabled(int(types.GroupPermissionShareDownload)) { + if inventory.IsAnonymousUser(n.user) { + return nil, serializer.NewError( + serializer.CodeAnonymouseAccessDenied, + fmt.Sprintf("You don't have permission to access share links"), + err, + ) + } + return nil, serializer.NewError( serializer.CodeNoPermissionErr, fmt.Sprintf("You don't have permission to access share links"), diff --git a/pkg/serializer/error.go b/pkg/serializer/error.go index f70a7adf..74c8d9c9 100644 --- a/pkg/serializer/error.go +++ b/pkg/serializer/error.go @@ -253,6 +253,8 @@ const ( CodeNodeUsedByStoragePolicy = 40086 // CodeDomainNotLicensed domain not licensed CodeDomainNotLicensed = 40087 + // CodeAnonymouseAccessDenied 匿名用户无法访问分享 + CodeAnonymouseAccessDenied = 40088 // CodeDBError 数据库操作失败 CodeDBError = 50001 // CodeEncryptError 加密失败 From acc660f112454af5b3a837acee40f90de2129ed8 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Fri, 22 Aug 2025 09:19:35 +0800 Subject: [PATCH 35/74] update submodule --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index ad1a0215..63c7abf2 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit ad1a02152cff89f6ce21f3fcaf345b3878755ccc +Subproject commit 63c7abf214d94995ed02491d412971ae2bf2996b From a095117061ae6fa726dcf13cb72f7ea22f7713bc Mon Sep 17 00:00:00 2001 From: Darren Yu Date: Tue, 26 Aug 2025 11:02:38 +0800 Subject: [PATCH 36/74] feat(email): support magic variables in email title, add init email template for multiple languages (#2814) * feat(email): add init email template for multiple languages * Update setting.go * Update setting.go * feat(email): support magic variables in email title --- inventory/setting.go | 174 +++++++++++++++++++++++++++++++++++++++++- pkg/email/template.go | 40 +++++++--- 2 files changed, 201 insertions(+), 13 deletions(-) diff --git a/inventory/setting.go b/inventory/setting.go index fa09f590..d769ed95 100644 --- a/inventory/setting.go +++ b/inventory/setting.go @@ -340,8 +340,137 @@ var ( Max: 5, }, } + + defaultActiveMailBody = `

` + defaultResetMailBody = `
` ) +type MailTemplateContent struct { + Language string + EmailIsAutoSend string // Translation of `此邮件由系统自动发送。` + + ActiveTitle string // Translation of `激活你的账号` + ActiveDes string // Translation of `请点击下方按钮确认你的电子邮箱并完成账号注册,此链接有效期为 24 小时。` + ActiveButton string // Translation of `确认激活` + + ResetTitle string // Translation of `重设密码` + ResetDes string // Translation of `请点击下方按钮重设你的密码,此链接有效期为 1 小时。` + ResetButton string // Translation of `重设密码` +} + +var mailTemplateContents = []MailTemplateContent{ + { + Language: "en-US", + EmailIsAutoSend: "This email is sent automatically.", + ActiveTitle: "Confirm your account", + ActiveDes: "Please click the button below to confirm your email address and finish setting up your account. This link is valid for 24 hours.", + ActiveButton: "Confirm", + ResetTitle: "Reset your password", + ResetDes: "Please click the button below to reset your password. This link is valid for 1 hour.", + ResetButton: "Reset", + }, + { + Language: "zh-CN", + EmailIsAutoSend: "此邮件由系统自动发送。", + ActiveTitle: "激活你的账号", + ActiveDes: "请点击下方按钮确认你的电子邮箱并完成账号注册,此链接有效期为 24 小时。", + ActiveButton: "确认激活", + ResetTitle: "重设密码", + ResetDes: "请点击下方按钮重设你的密码,此链接有效期为 1 小时。", + ResetButton: "重设密码", + }, + { + Language: "zh-TW", + EmailIsAutoSend: "此郵件由系統自動發送。", + ActiveTitle: "激活你的帳號", + ActiveDes: "請點擊下方按鈕確認你的電子郵箱並完成帳號註冊,此連結有效期為 24 小時。", + ActiveButton: "確認激活", + ResetTitle: "重設密碼", + ResetDes: "請點擊下方按鈕重設你的密碼,此連結有效期為 1 小時。", + ResetButton: "重設密碼", + }, + { + Language: "de-DE", + EmailIsAutoSend: "Diese E-Mail wird automatisch vom System gesendet.", + ActiveTitle: "Bestätigen Sie Ihr Konto", + ActiveDes: "Bitte klicken Sie auf die Schaltfläche unten, um Ihre E-Mail-Adresse zu bestätigen und Ihr Konto einzurichten. Dieser Link ist 24 Stunden lang gültig.", + ActiveButton: "Bestätigen", + ResetTitle: "Passwort zurücksetzen", + ResetDes: "Bitte klicken Sie auf die Schaltfläche unten, um Ihr Passwort zurückzusetzen. Dieser Link ist 1 Stunde lang gültig.", + ResetButton: "Passwort zurücksetzen", + }, + { + Language: "es-ES", + EmailIsAutoSend: "Este correo electrónico se envía automáticamente.", + ActiveTitle: "Confirma tu cuenta", + ActiveDes: "Por favor, haz clic en el botón de abajo para confirmar tu dirección de correo electrónico y completar la configuración de tu cuenta. Este enlace es válido por 24 horas.", + ActiveButton: "Confirmar", + ResetTitle: "Restablecer tu contraseña", + ResetDes: "Por favor, haz clic en el botón de abajo para restablecer tu contraseña. Este enlace es válido por 1 hora.", + ResetButton: "Restablecer", + }, + { + Language: "fr-FR", + EmailIsAutoSend: "Cet e-mail est envoyé automatiquement.", + ActiveTitle: "Confirmer votre compte", + ActiveDes: "Veuillez cliquer sur le bouton ci-dessous pour confirmer votre adresse e-mail et terminer la configuration de votre compte. Ce lien est valable 24 heures.", + ActiveButton: "Confirmer", + ResetTitle: "Réinitialiser votre mot de passe", + ResetDes: "Veuillez cliquer sur le bouton ci-dessous pour réinitialiser votre mot de passe. Ce lien est valable 1 heure.", + ResetButton: "Réinitialiser", + }, + { + Language: "it-IT", + EmailIsAutoSend: "Questa email è inviata automaticamente.", + ActiveTitle: "Conferma il tuo account", + ActiveDes: "Per favore, clicca sul pulsante qui sotto per confermare il tuo indirizzo email e completare la configurazione del tuo account. Questo link è valido per 24 ore.", + ActiveButton: "Conferma", + ResetTitle: "Reimposta la tua password", + ResetDes: "Per favore, clicca sul pulsante qui sotto per reimpostare la tua password. Questo link è valido per 1 ora.", + ResetButton: "Reimposta", + }, + { + Language: "ja-JP", + EmailIsAutoSend: "このメールはシステムによって自動的に送信されました。", + ActiveTitle: "アカウントを確認する", + ActiveDes: "アカウントの設定を完了するために、以下のボタンをクリックしてメールアドレスを確認してください。このリンクは24時間有効です。", + ActiveButton: "確認する", + ResetTitle: "パスワードをリセットする", + ResetDes: "以下のボタンをクリックしてパスワードをリセットしてください。このリンクは1時間有効です。", + ResetButton: "リセットする", + }, + { + Language: "ko-KR", + EmailIsAutoSend: "이 이메일은 시스템에 의해 자동으로 전송됩니다.", + ActiveTitle: "계정 확인", + ActiveDes: "아래 버튼을 클릭하여 이메일 주소를 확인하고 계정을 설정하세요. 이 링크는 24시간 동안 유효합니다.", + ActiveButton: "확인", + ResetTitle: "비밀번호 재설정", + ResetDes: "아래 버튼을 클릭하여 비밀번호를 재설정하세요. 이 링크는 1시간 동안 유효합니다.", + ResetButton: "비밀번호 재설정", + }, + { + Language: "pt-BR", + EmailIsAutoSend: "Este e-mail é enviado automaticamente.", + ActiveTitle: "Confirme sua conta", + ActiveDes: "Por favor, clique no botão abaixo para confirmar seu endereço de e-mail e concluir a configuração da sua conta. Este link é válido por 24 horas.", + ActiveButton: "Confirmar", + ResetTitle: "Redefinir sua senha", + ResetDes: "Por favor, clique no botão abaixo para redefinir sua senha. Este link é válido por 1 hora.", + ResetButton: "Redefinir", + }, + { + Language: "ru-RU", + EmailIsAutoSend: "Это письмо отправлено автоматически.", + ActiveTitle: "Подтвердите вашу учетную запись", + ActiveDes: "Пожалуйста, нажмите кнопку ниже, чтобы подтвердить ваш адрес электронной почты и завершить настройку вашей учетной записи. Эта ссылка действительна в течение 24 часов.", + ActiveButton: "Подтвердить", + ResetTitle: "Сбросить ваш пароль", + ResetDes: "Пожалуйста, нажмите кнопку ниже, чтобы сбросить ваш пароль. Эта ссылка действительна в течение 1 часа.", + ResetButton: "Сбросить пароль", + }, +} + var DefaultSettings = map[string]string{ "siteURL": `http://localhost:5212`, "siteName": `Cloudreve`, @@ -448,8 +577,6 @@ var DefaultSettings = map[string]string{ "public_resource_maxage": "86400", "viewer_session_timeout": "36000", "hash_id_salt": util.RandStringRunes(64), - "mail_activation_template": `[{"language":"en-US","title":"Activate your account","body":"
                                                           
"},{"language":"zh-CN","title":"激活你的账号","body":"
                                                           
"}]`, - "mail_reset_template": `[{"language":"en-US","title":"Reset your password","body":"
                                                           
"},{"language":"zh-CN","title":"重设密码","body":"
                                                           
"}]`, "access_token_ttl": "3600", "refresh_token_ttl": "1209600", // 2 weeks "use_cursor_pagination": "1", @@ -535,7 +662,6 @@ func init() { if err != nil { panic(err) } - DefaultSettings["file_viewers"] = string(viewers) customProps, err := json.Marshal(defaultFileProps) @@ -543,4 +669,44 @@ func init() { panic(err) } DefaultSettings["custom_props"] = string(customProps) -} + + activeMails := []map[string]string{} + for _, langContents := range mailTemplateContents { + activeMails = append(activeMails, map[string]string{ + "language": langContents.Language, + "title": "[{{ .CommonContext.SiteBasic.Name }}] " + langContents.ActiveTitle, + "body": util.Replace(map[string]string{ + "[[ .Language ]]": langContents.Language, + "[[ .ActiveTitle ]]": langContents.ActiveTitle, + "[[ .ActiveDes ]]": langContents.ActiveDes, + "[[ .ActiveButton ]]": langContents.ActiveButton, + "[[ .EmailIsAutoSend ]]": langContents.EmailIsAutoSend, + }, defaultActiveMailBody), + }) + } + mailActivationTemplates, err := json.Marshal(activeMails) + if err != nil { + panic(err) + } + DefaultSettings["mail_activation_template"] = string(mailActivationTemplates) + + resetMails := []map[string]string{} + for _, langContents := range mailTemplateContents { + resetMails = append(resetMails, map[string]string{ + "language": langContents.Language, + "title": "[{{ .CommonContext.SiteBasic.Name }}] " + langContents.ResetTitle, + "body": util.Replace(map[string]string{ + "[[ .Language ]]": langContents.Language, + "[[ .ResetTitle ]]": langContents.ResetTitle, + "[[ .ResetDes ]]": langContents.ResetDes, + "[[ .ResetButton ]]": langContents.ResetButton, + "[[ .EmailIsAutoSend ]]": langContents.EmailIsAutoSend, + }, defaultResetMailBody), + }) + } + mailResetTemplates, err := json.Marshal(resetMails) + if err != nil { + panic(err) + } + DefaultSettings["mail_reset_template"] = string(mailResetTemplates) +} \ No newline at end of file diff --git a/pkg/email/template.go b/pkg/email/template.go index d732eec0..213c092c 100644 --- a/pkg/email/template.go +++ b/pkg/email/template.go @@ -38,18 +38,29 @@ func NewResetEmail(ctx context.Context, settings setting.Provider, user *ent.Use Url: url, } - tmpl, err := template.New("reset").Parse(selected.Body) + tmplTitle, err := template.New("resetTitle").Parse(selected.Title) + if err != nil { + return "", "", fmt.Errorf("failed to parse email title: %w", err) + } + + var resTitle strings.Builder + err = tmplTitle.Execute(&resTitle, resetCtx) + if err != nil { + return "", "", fmt.Errorf("failed to execute email title: %w", err) + } + + tmplBody, err := template.New("resetBody").Parse(selected.Body) if err != nil { return "", "", fmt.Errorf("failed to parse email template: %w", err) } - var res strings.Builder - err = tmpl.Execute(&res, resetCtx) + var resBody strings.Builder + err = tmplBody.Execute(&resBody, resetCtx) if err != nil { return "", "", fmt.Errorf("failed to execute email template: %w", err) } - return fmt.Sprintf("[%s] %s", resetCtx.SiteBasic.Name, selected.Title), res.String(), nil + return resTitle.String(), resBody.String(), nil } // ActivationContext used for variables in activation email @@ -73,18 +84,29 @@ func NewActivationEmail(ctx context.Context, settings setting.Provider, user *en Url: url, } - tmpl, err := template.New("activation").Parse(selected.Body) + tmplTitle, err := template.New("activationTitle").Parse(selected.Title) + if err != nil { + return "", "", fmt.Errorf("failed to parse email title: %w", err) + } + + var resTitle strings.Builder + err = tmplTitle.Execute(&resTitle, activationCtx) + if err != nil { + return "", "", fmt.Errorf("failed to execute email title: %w", err) + } + + tmplBody, err := template.New("activationBody").Parse(selected.Body) if err != nil { return "", "", fmt.Errorf("failed to parse email template: %w", err) } - var res strings.Builder - err = tmpl.Execute(&res, activationCtx) + var resBody strings.Builder + err = tmplBody.Execute(&resBody, activationCtx) if err != nil { return "", "", fmt.Errorf("failed to execute email template: %w", err) } - return fmt.Sprintf("[%s] %s", activationCtx.SiteBasic.Name, selected.Title), res.String(), nil + return resTitle.String(), resBody.String(), nil } func commonContext(ctx context.Context, settings setting.Provider) *CommonContext { @@ -122,4 +144,4 @@ func selectTemplate(templates []setting.EmailTemplate, u *ent.User) setting.Emai } return selected -} +} \ No newline at end of file From a16b491f6528f746cabad8354e0a02f09bc54d96 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Tue, 26 Aug 2025 11:30:55 +0800 Subject: [PATCH 37/74] fix(entitysource): rate limiter applied to nil reader (#2834) --- assets | 2 +- pkg/filemanager/manager/entitysource/entitysource.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/assets b/assets index 63c7abf2..d0540548 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 63c7abf214d94995ed02491d412971ae2bf2996b +Subproject commit d0540548cfb2caeec0c7d44e85819c833fdeda0f diff --git a/pkg/filemanager/manager/entitysource/entitysource.go b/pkg/filemanager/manager/entitysource/entitysource.go index a7632361..2e640edb 100644 --- a/pkg/filemanager/manager/entitysource/entitysource.go +++ b/pkg/filemanager/manager/entitysource/entitysource.go @@ -670,7 +670,7 @@ func (f *entitySource) getRsc(pos int64) (io.ReadCloser, error) { if f.o.SpeedLimit > 0 { bucket := ratelimit.NewBucketWithRate(float64(f.o.SpeedLimit), f.o.SpeedLimit) - return lrs{f.rsc, ratelimit.Reader(f.rsc, bucket)}, nil + return lrs{file, ratelimit.Reader(file, bucket)}, nil } else { return file, nil } From 9b40e0146f45c991600572327eae0e76875d0ee9 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Thu, 28 Aug 2025 11:26:55 +0800 Subject: [PATCH 38/74] fix(dbfs): remove recursive limit for deleting files --- pkg/filemanager/fs/dbfs/manage.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/filemanager/fs/dbfs/manage.go b/pkg/filemanager/fs/dbfs/manage.go index 18657e47..f0471df5 100644 --- a/pkg/filemanager/fs/dbfs/manage.go +++ b/pkg/filemanager/fs/dbfs/manage.go @@ -760,7 +760,6 @@ func (f *DBFS) deleteFiles(ctx context.Context, targets map[Navigator][]*File, f if f.user.Edges.Group == nil { return nil, nil, fmt.Errorf("user group not loaded") } - limit := max(f.user.Edges.Group.Settings.MaxWalkedFiles, 1) allStaleEntities := make([]fs.Entity, 0, len(targets)) storageDiff := make(inventory.StorageDiff) for n, files := range targets { @@ -774,8 +773,7 @@ func (f *DBFS) deleteFiles(ctx context.Context, targets map[Navigator][]*File, f // List all files to be deleted toBeDeletedFiles := make([]*File, 0, len(files)) - if err := n.Walk(ctx, files, limit, intsets.MaxInt, func(targets []*File, level int) error { - limit -= len(targets) + if err := n.Walk(ctx, files, intsets.MaxInt, intsets.MaxInt, func(targets []*File, level int) error { toBeDeletedFiles = append(toBeDeletedFiles, targets...) return nil }); err != nil { From c3ed4f58399b312b6448339b64003ff28fd19da9 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Sat, 30 Aug 2025 10:36:20 +0800 Subject: [PATCH 39/74] feat(uploader): concurrent chunk uploads --- assets | 2 +- inventory/types/types.go | 2 ++ service/explorer/response.go | 12 +++++++----- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/assets b/assets index d0540548..a095f8c6 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit d0540548cfb2caeec0c7d44e85819c833fdeda0f +Subproject commit a095f8c612562536c536f54be04ec84e500d5ca7 diff --git a/inventory/types/types.go b/inventory/types/types.go index bc069859..2306ac40 100644 --- a/inventory/types/types.go +++ b/inventory/types/types.go @@ -101,6 +101,8 @@ type ( SourceAuth bool `json:"source_auth,omitempty"` // QiniuUploadCdn whether to use CDN for Qiniu upload. QiniuUploadCdn bool `json:"qiniu_upload_cdn,omitempty"` + // ChunkConcurrency the number of chunks to upload concurrently. + ChunkConcurrency int `json:"chunk_concurrency,omitempty"` } FileType int diff --git a/service/explorer/response.go b/service/explorer/response.go index 302a2275..991039b9 100644 --- a/service/explorer/response.go +++ b/service/explorer/response.go @@ -259,6 +259,7 @@ type StoragePolicy struct { Type types.PolicyType `json:"type"` MaxSize int64 `json:"max_size"` Relay bool `json:"relay,omitempty"` + ChunkConcurrency int `json:"chunk_concurrency,omitempty"` } type Entity struct { @@ -452,11 +453,12 @@ func BuildStoragePolicy(sp *ent.StoragePolicy, hasher hashid.Encoder) *StoragePo } res := &StoragePolicy{ - ID: hashid.EncodePolicyID(hasher, sp.ID), - Name: sp.Name, - Type: types.PolicyType(sp.Type), - MaxSize: sp.MaxSize, - Relay: sp.Settings.Relay, + ID: hashid.EncodePolicyID(hasher, sp.ID), + Name: sp.Name, + Type: types.PolicyType(sp.Type), + MaxSize: sp.MaxSize, + Relay: sp.Settings.Relay, + ChunkConcurrency: sp.Settings.ChunkConcurrency, } if sp.Settings.IsFileTypeDenyList { From 4acf9401b8dbb17c34e171b434498890d39b58ca Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Sat, 30 Aug 2025 10:37:08 +0800 Subject: [PATCH 40/74] feat(uploader): concurrent chunk uploads for local/remote storage policy --- assets | 2 +- pkg/filemanager/driver/local/local.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/assets b/assets index a095f8c6..35961604 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit a095f8c612562536c536f54be04ec84e500d5ca7 +Subproject commit 35961604a187a49591fa57a50de8c0dad4bb5b78 diff --git a/pkg/filemanager/driver/local/local.go b/pkg/filemanager/driver/local/local.go index 5fb4a1f2..28e2b555 100644 --- a/pkg/filemanager/driver/local/local.go +++ b/pkg/filemanager/driver/local/local.go @@ -140,9 +140,9 @@ func (handler *Driver) Put(ctx context.Context, file *fs.UploadRequest) error { } openMode := os.O_CREATE | os.O_RDWR - if file.Mode&fs.ModeOverwrite == fs.ModeOverwrite && file.Offset == 0 { - openMode |= os.O_TRUNC - } + // if file.Mode&fs.ModeOverwrite == fs.ModeOverwrite && file.Offset == 0 { + // openMode |= os.O_TRUNC + // } out, err := os.OpenFile(dst, openMode, Perm) if err != nil { From 9f1cb52cfb2de2d61d49cf732ab52dc45c159173 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Tue, 2 Sep 2025 11:54:04 +0800 Subject: [PATCH 41/74] feat(explorer): preview archive file content and extract selected files (#2852) --- application/constants/constants.go | 2 +- assets | 2 +- inventory/migration.go | 47 +++++++++++ inventory/setting.go | 73 +++++++++------- inventory/types/types.go | 25 +++--- pkg/filemanager/fs/fs.go | 2 + pkg/filemanager/manager/archive.go | 120 +++++++++++++++++++++++++++ pkg/filemanager/manager/manager.go | 3 + pkg/filemanager/workflows/extract.go | 50 +++++++++-- routers/controllers/file.go | 14 ++++ routers/router.go | 4 + service/explorer/file.go | 30 +++++++ service/explorer/response.go | 10 +++ service/explorer/workflows.go | 3 +- 14 files changed, 329 insertions(+), 56 deletions(-) diff --git a/application/constants/constants.go b/application/constants/constants.go index a50d4153..983d5165 100644 --- a/application/constants/constants.go +++ b/application/constants/constants.go @@ -3,7 +3,7 @@ package constants // These values will be injected at build time, DO NOT EDIT. // BackendVersion 当前后端版本号 -var BackendVersion = "4.1.0" +var BackendVersion = "4.7.0" // IsPro 是否为Pro版本 var IsPro = "false" diff --git a/assets b/assets index 35961604..463794a7 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 35961604a187a49591fa57a50de8c0dad4bb5b78 +Subproject commit 463794a71e6e19b9d4ee35248f00ff64f9485f30 diff --git a/inventory/migration.go b/inventory/migration.go index 9f1e28b5..e2276f2f 100644 --- a/inventory/migration.go +++ b/inventory/migration.go @@ -279,6 +279,53 @@ type ( ) var patches = []Patch{ + { + Name: "apply_default_archive_viewer", + EndVersion: "4.7.0", + Func: func(l logging.Logger, client *ent.Client, ctx context.Context) error { + fileViewersSetting, err := client.Setting.Query().Where(setting.Name("file_viewers")).First(ctx) + if err != nil { + return fmt.Errorf("failed to query file_viewers setting: %w", err) + } + + var fileViewers []types.ViewerGroup + if err := json.Unmarshal([]byte(fileViewersSetting.Value), &fileViewers); err != nil { + return fmt.Errorf("failed to unmarshal file_viewers setting: %w", err) + } + + fileViewerExisted := false + for _, viewer := range fileViewers[0].Viewers { + if viewer.ID == "archive" { + fileViewerExisted = true + break + } + } + + // 2.2 If not existed, add it + if !fileViewerExisted { + // Found existing archive viewer default setting + var defaultArchiveViewer types.Viewer + for _, viewer := range defaultFileViewers[0].Viewers { + if viewer.ID == "archive" { + defaultArchiveViewer = viewer + break + } + } + + fileViewers[0].Viewers = append(fileViewers[0].Viewers, defaultArchiveViewer) + newFileViewersSetting, err := json.Marshal(fileViewers) + if err != nil { + return fmt.Errorf("failed to marshal file_viewers setting: %w", err) + } + + if _, err := client.Setting.UpdateOne(fileViewersSetting).SetValue(string(newFileViewersSetting)).Save(ctx); err != nil { + return fmt.Errorf("failed to update file_viewers setting: %w", err) + } + } + + return nil + }, + }, { Name: "apply_default_excalidraw_viewer", EndVersion: "4.1.0", diff --git a/inventory/setting.go b/inventory/setting.go index d769ed95..19b4aa68 100644 --- a/inventory/setting.go +++ b/inventory/setting.go @@ -321,6 +321,15 @@ var ( }, }, }, + { + ID: "archive", + Type: types.ViewerTypeBuiltin, + DisplayName: "fileManager.archivePreview", + Exts: []string{"zip", "7z"}, + RequiredGroupPermission: []types.GroupPermission{ + types.GroupPermissionArchiveTask, + }, + }, }, }, } @@ -347,19 +356,19 @@ var ( type MailTemplateContent struct { Language string - EmailIsAutoSend string // Translation of `此邮件由系统自动发送。` + EmailIsAutoSend string // Translation of `此邮件由系统自动发送。` - ActiveTitle string // Translation of `激活你的账号` - ActiveDes string // Translation of `请点击下方按钮确认你的电子邮箱并完成账号注册,此链接有效期为 24 小时。` - ActiveButton string // Translation of `确认激活` + ActiveTitle string // Translation of `激活你的账号` + ActiveDes string // Translation of `请点击下方按钮确认你的电子邮箱并完成账号注册,此链接有效期为 24 小时。` + ActiveButton string // Translation of `确认激活` - ResetTitle string // Translation of `重设密码` - ResetDes string // Translation of `请点击下方按钮重设你的密码,此链接有效期为 1 小时。` - ResetButton string // Translation of `重设密码` + ResetTitle string // Translation of `重设密码` + ResetDes string // Translation of `请点击下方按钮重设你的密码,此链接有效期为 1 小时。` + ResetButton string // Translation of `重设密码` } var mailTemplateContents = []MailTemplateContent{ - { + { Language: "en-US", EmailIsAutoSend: "This email is sent automatically.", ActiveTitle: "Confirm your account", @@ -368,8 +377,8 @@ var mailTemplateContents = []MailTemplateContent{ ResetTitle: "Reset your password", ResetDes: "Please click the button below to reset your password. This link is valid for 1 hour.", ResetButton: "Reset", - }, - { + }, + { Language: "zh-CN", EmailIsAutoSend: "此邮件由系统自动发送。", ActiveTitle: "激活你的账号", @@ -378,8 +387,8 @@ var mailTemplateContents = []MailTemplateContent{ ResetTitle: "重设密码", ResetDes: "请点击下方按钮重设你的密码,此链接有效期为 1 小时。", ResetButton: "重设密码", - }, - { + }, + { Language: "zh-TW", EmailIsAutoSend: "此郵件由系統自動發送。", ActiveTitle: "激活你的帳號", @@ -388,8 +397,8 @@ var mailTemplateContents = []MailTemplateContent{ ResetTitle: "重設密碼", ResetDes: "請點擊下方按鈕重設你的密碼,此連結有效期為 1 小時。", ResetButton: "重設密碼", - }, - { + }, + { Language: "de-DE", EmailIsAutoSend: "Diese E-Mail wird automatisch vom System gesendet.", ActiveTitle: "Bestätigen Sie Ihr Konto", @@ -398,8 +407,8 @@ var mailTemplateContents = []MailTemplateContent{ ResetTitle: "Passwort zurücksetzen", ResetDes: "Bitte klicken Sie auf die Schaltfläche unten, um Ihr Passwort zurückzusetzen. Dieser Link ist 1 Stunde lang gültig.", ResetButton: "Passwort zurücksetzen", - }, - { + }, + { Language: "es-ES", EmailIsAutoSend: "Este correo electrónico se envía automáticamente.", ActiveTitle: "Confirma tu cuenta", @@ -408,8 +417,8 @@ var mailTemplateContents = []MailTemplateContent{ ResetTitle: "Restablecer tu contraseña", ResetDes: "Por favor, haz clic en el botón de abajo para restablecer tu contraseña. Este enlace es válido por 1 hora.", ResetButton: "Restablecer", - }, - { + }, + { Language: "fr-FR", EmailIsAutoSend: "Cet e-mail est envoyé automatiquement.", ActiveTitle: "Confirmer votre compte", @@ -418,8 +427,8 @@ var mailTemplateContents = []MailTemplateContent{ ResetTitle: "Réinitialiser votre mot de passe", ResetDes: "Veuillez cliquer sur le bouton ci-dessous pour réinitialiser votre mot de passe. Ce lien est valable 1 heure.", ResetButton: "Réinitialiser", - }, - { + }, + { Language: "it-IT", EmailIsAutoSend: "Questa email è inviata automaticamente.", ActiveTitle: "Conferma il tuo account", @@ -428,8 +437,8 @@ var mailTemplateContents = []MailTemplateContent{ ResetTitle: "Reimposta la tua password", ResetDes: "Per favore, clicca sul pulsante qui sotto per reimpostare la tua password. Questo link è valido per 1 ora.", ResetButton: "Reimposta", - }, - { + }, + { Language: "ja-JP", EmailIsAutoSend: "このメールはシステムによって自動的に送信されました。", ActiveTitle: "アカウントを確認する", @@ -438,8 +447,8 @@ var mailTemplateContents = []MailTemplateContent{ ResetTitle: "パスワードをリセットする", ResetDes: "以下のボタンをクリックしてパスワードをリセットしてください。このリンクは1時間有効です。", ResetButton: "リセットする", - }, - { + }, + { Language: "ko-KR", EmailIsAutoSend: "이 이메일은 시스템에 의해 자동으로 전송됩니다.", ActiveTitle: "계정 확인", @@ -448,8 +457,8 @@ var mailTemplateContents = []MailTemplateContent{ ResetTitle: "비밀번호 재설정", ResetDes: "아래 버튼을 클릭하여 비밀번호를 재설정하세요. 이 링크는 1시간 동안 유효합니다.", ResetButton: "비밀번호 재설정", - }, - { + }, + { Language: "pt-BR", EmailIsAutoSend: "Este e-mail é enviado automaticamente.", ActiveTitle: "Confirme sua conta", @@ -458,8 +467,8 @@ var mailTemplateContents = []MailTemplateContent{ ResetTitle: "Redefinir sua senha", ResetDes: "Por favor, clique no botão abaixo para redefinir sua senha. Este link é válido por 1 hora.", ResetButton: "Redefinir", - }, - { + }, + { Language: "ru-RU", EmailIsAutoSend: "Это письмо отправлено автоматически.", ActiveTitle: "Подтвердите вашу учетную запись", @@ -468,7 +477,7 @@ var mailTemplateContents = []MailTemplateContent{ ResetTitle: "Сбросить ваш пароль", ResetDes: "Пожалуйста, нажмите кнопку ниже, чтобы сбросить ваш пароль. Эта ссылка действительна в течение 1 часа.", ResetButton: "Сбросить пароль", - }, + }, } var DefaultSettings = map[string]string{ @@ -675,7 +684,7 @@ func init() { activeMails = append(activeMails, map[string]string{ "language": langContents.Language, "title": "[{{ .CommonContext.SiteBasic.Name }}] " + langContents.ActiveTitle, - "body": util.Replace(map[string]string{ + "body": util.Replace(map[string]string{ "[[ .Language ]]": langContents.Language, "[[ .ActiveTitle ]]": langContents.ActiveTitle, "[[ .ActiveDes ]]": langContents.ActiveDes, @@ -695,7 +704,7 @@ func init() { resetMails = append(resetMails, map[string]string{ "language": langContents.Language, "title": "[{{ .CommonContext.SiteBasic.Name }}] " + langContents.ResetTitle, - "body": util.Replace(map[string]string{ + "body": util.Replace(map[string]string{ "[[ .Language ]]": langContents.Language, "[[ .ResetTitle ]]": langContents.ResetTitle, "[[ .ResetDes ]]": langContents.ResetDes, @@ -709,4 +718,4 @@ func init() { panic(err) } DefaultSettings["mail_reset_template"] = string(mailResetTemplates) -} \ No newline at end of file +} diff --git a/inventory/types/types.go b/inventory/types/types.go index 2306ac40..9ba07138 100644 --- a/inventory/types/types.go +++ b/inventory/types/types.go @@ -293,18 +293,19 @@ const ( type ( Viewer struct { - ID string `json:"id"` - Type ViewerType `json:"type"` - DisplayName string `json:"display_name"` - Exts []string `json:"exts"` - Url string `json:"url,omitempty"` - Icon string `json:"icon,omitempty"` - WopiActions map[string]map[ViewerAction]string `json:"wopi_actions,omitempty"` - Props map[string]string `json:"props,omitempty"` - MaxSize int64 `json:"max_size,omitempty"` - Disabled bool `json:"disabled,omitempty"` - Templates []NewFileTemplate `json:"templates,omitempty"` - Platform string `json:"platform,omitempty"` + ID string `json:"id"` + Type ViewerType `json:"type"` + DisplayName string `json:"display_name"` + Exts []string `json:"exts"` + Url string `json:"url,omitempty"` + Icon string `json:"icon,omitempty"` + WopiActions map[string]map[ViewerAction]string `json:"wopi_actions,omitempty"` + Props map[string]string `json:"props,omitempty"` + MaxSize int64 `json:"max_size,omitempty"` + Disabled bool `json:"disabled,omitempty"` + Templates []NewFileTemplate `json:"templates,omitempty"` + Platform string `json:"platform,omitempty"` + RequiredGroupPermission []GroupPermission `json:"required_group_permission,omitempty"` } ViewerGroup struct { Viewers []Viewer `json:"viewers"` diff --git a/pkg/filemanager/fs/fs.go b/pkg/filemanager/fs/fs.go index 0121547c..20681d65 100644 --- a/pkg/filemanager/fs/fs.go +++ b/pkg/filemanager/fs/fs.go @@ -699,6 +699,8 @@ func LockSessionToContext(ctx context.Context, session LockSession) context.Cont return context.WithValue(ctx, LockSessionCtxKey{}, session) } +// FindDesiredEntity finds the desired entity from the file. +// entityType is optional, if it is not nil, it will only return the entity with the given type. func FindDesiredEntity(file File, version string, hasher hashid.Encoder, entityType *types.EntityType) (bool, Entity) { if version == "" { return true, file.PrimaryEntity() diff --git a/pkg/filemanager/manager/archive.go b/pkg/filemanager/manager/archive.go index fa2440ab..667ca6f9 100644 --- a/pkg/filemanager/manager/archive.go +++ b/pkg/filemanager/manager/archive.go @@ -3,19 +3,95 @@ package manager import ( "archive/zip" "context" + "encoding/gob" "fmt" "io" "path" "path/filepath" "strings" + "time" + "github.com/bodgit/sevenzip" "github.com/cloudreve/Cloudreve/v4/inventory/types" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs/dbfs" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource" + "github.com/cloudreve/Cloudreve/v4/pkg/util" "golang.org/x/tools/container/intsets" ) +type ( + ArchivedFile struct { + Name string `json:"name"` + Size int64 `json:"size"` + UpdatedAt *time.Time `json:"updated_at"` + IsDirectory bool `json:"is_directory"` + } +) + +const ( + ArchiveListCacheTTL = 3600 // 1 hour +) + +func init() { + gob.Register([]ArchivedFile{}) +} + +func (m *manager) ListArchiveFiles(ctx context.Context, uri *fs.URI, entity string) ([]ArchivedFile, error) { + file, err := m.fs.Get(ctx, uri, dbfs.WithFileEntities(), dbfs.WithRequiredCapabilities(dbfs.NavigatorCapabilityDownloadFile)) + if err != nil { + return nil, fmt.Errorf("failed to get file: %w", err) + } + + if file.Type() != types.FileTypeFile { + return nil, fs.ErrNotSupportedAction.WithError(fmt.Errorf("path %s is not a file", uri)) + } + + // Validate file size + if m.user.Edges.Group.Settings.DecompressSize > 0 && file.Size() > m.user.Edges.Group.Settings.DecompressSize { + return nil, fs.ErrFileSizeTooBig.WithError(fmt.Errorf("file size %d exceeds the limit %d", file.Size(), m.user.Edges.Group.Settings.DecompressSize)) + } + + found, targetEntity := fs.FindDesiredEntity(file, entity, m.hasher, nil) + if !found { + return nil, fs.ErrEntityNotExist + } + + cacheKey := getArchiveListCacheKey(targetEntity.ID()) + kv := m.kv + res, found := kv.Get(cacheKey) + if found { + return res.([]ArchivedFile), nil + } + + es, err := m.GetEntitySource(ctx, 0, fs.WithEntity(targetEntity)) + if err != nil { + return nil, fmt.Errorf("failed to get entity source: %w", err) + } + + es.Apply(entitysource.WithContext(ctx)) + defer es.Close() + + var readerFunc func(ctx context.Context, file io.ReaderAt, size int64) ([]ArchivedFile, error) + switch file.Ext() { + case "zip": + readerFunc = getZipFileList + case "7z": + readerFunc = get7zFileList + default: + return nil, fs.ErrNotSupportedAction.WithError(fmt.Errorf("not supported archive format: %s", file.Ext())) + } + + sr := io.NewSectionReader(es, 0, targetEntity.Size()) + fileList, err := readerFunc(ctx, sr, targetEntity.Size()) + if err != nil { + return nil, fmt.Errorf("failed to read file list: %w", err) + } + + kv.Set(cacheKey, fileList, ArchiveListCacheTTL) + return fileList, nil +} + func (m *manager) CreateArchive(ctx context.Context, uris []*fs.URI, writer io.Writer, opts ...fs.Option) (int, error) { o := newOption() for _, opt := range opts { @@ -122,3 +198,47 @@ func (m *manager) compressFileToArchive(ctx context.Context, parent string, file return err } + +func getZipFileList(ctx context.Context, file io.ReaderAt, size int64) ([]ArchivedFile, error) { + zr, err := zip.NewReader(file, size) + if err != nil { + return nil, fmt.Errorf("failed to create zip reader: %w", err) + } + + fileList := make([]ArchivedFile, 0, len(zr.File)) + for _, f := range zr.File { + info := f.FileInfo() + modTime := info.ModTime() + fileList = append(fileList, ArchivedFile{ + Name: util.FormSlash(f.Name), + Size: info.Size(), + UpdatedAt: &modTime, + IsDirectory: info.IsDir(), + }) + } + return fileList, nil +} + +func get7zFileList(ctx context.Context, file io.ReaderAt, size int64) ([]ArchivedFile, error) { + zr, err := sevenzip.NewReader(file, size) + if err != nil { + return nil, fmt.Errorf("failed to create 7z reader: %w", err) + } + + fileList := make([]ArchivedFile, 0, len(zr.File)) + for _, f := range zr.File { + info := f.FileInfo() + modTime := info.ModTime() + fileList = append(fileList, ArchivedFile{ + Name: util.FormSlash(f.Name), + Size: info.Size(), + UpdatedAt: &modTime, + IsDirectory: info.IsDir(), + }) + } + return fileList, nil +} + +func getArchiveListCacheKey(entity int) string { + return fmt.Sprintf("archive_list_%d", entity) +} diff --git a/pkg/filemanager/manager/manager.go b/pkg/filemanager/manager/manager.go index ad04af5e..98d01b99 100644 --- a/pkg/filemanager/manager/manager.go +++ b/pkg/filemanager/manager/manager.go @@ -85,7 +85,10 @@ type ( } Archiver interface { + // CreateArchive creates an archive CreateArchive(ctx context.Context, uris []*fs.URI, writer io.Writer, opts ...fs.Option) (int, error) + // ListArchiveFiles lists files in an archive + ListArchiveFiles(ctx context.Context, uri *fs.URI, entity string) ([]ArchivedFile, error) } FileManager interface { diff --git a/pkg/filemanager/workflows/extract.go b/pkg/filemanager/workflows/extract.go index 00616f77..f51d226e 100644 --- a/pkg/filemanager/workflows/extract.go +++ b/pkg/filemanager/workflows/extract.go @@ -47,14 +47,15 @@ type ( } ExtractArchiveTaskPhase string ExtractArchiveTaskState struct { - Uri string `json:"uri,omitempty"` - Encoding string `json:"encoding,omitempty"` - Dst string `json:"dst,omitempty"` - TempPath string `json:"temp_path,omitempty"` - TempZipFilePath string `json:"temp_zip_file_path,omitempty"` - ProcessedCursor string `json:"processed_cursor,omitempty"` - SlaveTaskID int `json:"slave_task_id,omitempty"` - Password string `json:"password,omitempty"` + Uri string `json:"uri,omitempty"` + Encoding string `json:"encoding,omitempty"` + Dst string `json:"dst,omitempty"` + TempPath string `json:"temp_path,omitempty"` + TempZipFilePath string `json:"temp_zip_file_path,omitempty"` + ProcessedCursor string `json:"processed_cursor,omitempty"` + SlaveTaskID int `json:"slave_task_id,omitempty"` + Password string `json:"password,omitempty"` + FileMask []string `json:"file_mask,omitempty"` NodeState `json:",inline"` Phase ExtractArchiveTaskPhase `json:"phase,omitempty"` } @@ -119,13 +120,14 @@ var encodings = map[string]encoding.Encoding{ } // NewExtractArchiveTask creates a new ExtractArchiveTask -func NewExtractArchiveTask(ctx context.Context, src, dst, encoding, password string) (queue.Task, error) { +func NewExtractArchiveTask(ctx context.Context, src, dst, encoding, password string, mask []string) (queue.Task, error) { state := &ExtractArchiveTaskState{ Uri: src, Dst: dst, Encoding: encoding, NodeState: NodeState{}, Password: password, + FileMask: mask, } stateBytes, err := json.Marshal(state) if err != nil { @@ -247,6 +249,7 @@ func (m *ExtractArchiveTask) createSlaveExtractTask(ctx context.Context, dep dep Dst: m.state.Dst, UserID: user.ID, Password: m.state.Password, + FileMask: m.state.FileMask, } payloadStr, err := json.Marshal(payload) @@ -416,6 +419,14 @@ func (m *ExtractArchiveTask) masterExtractArchive(ctx context.Context, dep depen rawPath := util.FormSlash(f.NameInArchive) savePath := dst.JoinRaw(rawPath) + // If file mask is not empty, check if the path is in the mask + if len(m.state.FileMask) > 0 && !isFileInMask(rawPath, m.state.FileMask) { + m.l.Warning("File %q is not in the mask, skipping...", f.NameInArchive) + atomic.AddInt64(&m.progress[ProgressTypeExtractCount].Current, 1) + atomic.AddInt64(&m.progress[ProgressTypeExtractSize].Current, f.Size()) + return nil + } + // Check if path is legit if !strings.HasPrefix(savePath.Path(), util.FillSlash(path.Clean(dst.Path()))) { m.l.Warning("Path %q is not legit, skipping...", f.NameInArchive) @@ -599,6 +610,7 @@ type ( TempZipFilePath string `json:"temp_zip_file_path,omitempty"` ProcessedCursor string `json:"processed_cursor,omitempty"` Password string `json:"password,omitempty"` + FileMask []string `json:"file_mask,omitempty"` } ) @@ -779,6 +791,12 @@ func (m *SlaveExtractArchiveTask) Do(ctx context.Context) (task.Status, error) { rawPath := util.FormSlash(f.NameInArchive) savePath := dst.JoinRaw(rawPath) + // If file mask is not empty, check if the path is in the mask + if len(m.state.FileMask) > 0 && !isFileInMask(rawPath, m.state.FileMask) { + m.l.Debug("File %q is not in the mask, skipping...", f.NameInArchive) + return nil + } + // Check if path is legit if !strings.HasPrefix(savePath.Path(), util.FillSlash(path.Clean(dst.Path()))) { atomic.AddInt64(&m.progress[ProgressTypeExtractCount].Current, 1) @@ -846,3 +864,17 @@ func (m *SlaveExtractArchiveTask) Progress(ctx context.Context) queue.Progresses defer m.Unlock() return m.progress } + +func isFileInMask(path string, mask []string) bool { + if len(mask) == 0 { + return true + } + + for _, m := range mask { + if path == m || strings.HasPrefix(path, m+"/") { + return true + } + } + + return false +} diff --git a/routers/controllers/file.go b/routers/controllers/file.go index e09a95f1..1875d178 100644 --- a/routers/controllers/file.go +++ b/routers/controllers/file.go @@ -412,3 +412,17 @@ func PatchView(c *gin.Context) { c.JSON(200, serializer.Response{}) } + +func ListArchiveFiles(c *gin.Context) { + service := ParametersFromContext[*explorer.ArchiveListFilesService](c, explorer.ArchiveListFilesParamCtx{}) + resp, err := service.List(c) + if err != nil { + c.JSON(200, serializer.Err(c, err)) + c.Abort() + return + } + + c.JSON(200, serializer.Response{ + Data: resp, + }) +} diff --git a/routers/router.go b/routers/router.go index a866d880..d474ccf3 100644 --- a/routers/router.go +++ b/routers/router.go @@ -566,6 +566,10 @@ func initMasterRouter(dep dependency.Dep) *gin.Engine { controllers.FromQuery[explorer.ListFileService](explorer.ListFileParameterCtx{}), controllers.ListDirectory, ) + file.GET("archive", + controllers.FromQuery[explorer.ArchiveListFilesService](explorer.ArchiveListFilesParamCtx{}), + controllers.ListArchiveFiles, + ) // Create file file.POST("create", controllers.FromJSON[explorer.CreateFileService](explorer.CreateFileParameterCtx{}), diff --git a/service/explorer/file.go b/service/explorer/file.go index 02ca5642..e82a60c3 100644 --- a/service/explorer/file.go +++ b/service/explorer/file.go @@ -716,3 +716,33 @@ func (s *PatchViewService) Patch(c *gin.Context) error { return nil } + +type ( + ArchiveListFilesParamCtx struct{} + ArchiveListFilesService struct { + Uri string `form:"uri" binding:"required"` + Entity string `form:"entity"` + } +) + +func (s *ArchiveListFilesService) List(c *gin.Context) (*ArchiveListFilesResponse, error) { + dep := dependency.FromContext(c) + user := inventory.UserFromContext(c) + m := manager.NewFileManager(dep, user) + defer m.Recycle() + if !user.Edges.Group.Permissions.Enabled(int(types.GroupPermissionArchiveTask)) { + return nil, serializer.NewError(serializer.CodeGroupNotAllowed, "Group not allowed to extract archive files", nil) + } + + uri, err := fs.NewUriFromString(s.Uri) + if err != nil { + return nil, serializer.NewError(serializer.CodeParamErr, "unknown uri", err) + } + + files, err := m.ListArchiveFiles(c, uri, s.Entity) + if err != nil { + return nil, fmt.Errorf("failed to list archive files: %w", err) + } + + return BuildArchiveListFilesResponse(files), nil +} diff --git a/service/explorer/response.go b/service/explorer/response.go index 991039b9..ee03137b 100644 --- a/service/explorer/response.go +++ b/service/explorer/response.go @@ -26,6 +26,16 @@ import ( "github.com/samber/lo" ) +type ArchiveListFilesResponse struct { + Files []manager.ArchivedFile `json:"files"` +} + +func BuildArchiveListFilesResponse(files []manager.ArchivedFile) *ArchiveListFilesResponse { + return &ArchiveListFilesResponse{ + Files: files, + } +} + type PutRelativeResponse struct { Name string Url string diff --git a/service/explorer/workflows.go b/service/explorer/workflows.go index 138b2922..ad7cf098 100644 --- a/service/explorer/workflows.go +++ b/service/explorer/workflows.go @@ -174,6 +174,7 @@ type ( Dst string `json:"dst" binding:"required"` Encoding string `json:"encoding"` Password string `json:"password"` + FileMask []string `json:"file_mask"` } CreateArchiveParamCtx struct{} ) @@ -204,7 +205,7 @@ func (service *ArchiveWorkflowService) CreateExtractTask(c *gin.Context) (*TaskR } // Create task - t, err := workflows.NewExtractArchiveTask(c, service.Src[0], service.Dst, service.Encoding, service.Password) + t, err := workflows.NewExtractArchiveTask(c, service.Src[0], service.Dst, service.Encoding, service.Password, service.FileMask) if err != nil { return nil, serializer.NewError(serializer.CodeCreateTaskError, "Failed to create task", err) } From af43746ba2157647a4d438d5ed16633741938e61 Mon Sep 17 00:00:00 2001 From: Darren Yu Date: Tue, 2 Sep 2025 11:57:49 +0800 Subject: [PATCH 42/74] feat(email): migrate magic variables to email templates title in patches (#2862) --- inventory/migration.go | 63 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/inventory/migration.go b/inventory/migration.go index e2276f2f..7ba8aea2 100644 --- a/inventory/migration.go +++ b/inventory/migration.go @@ -414,6 +414,69 @@ var patches = []Patch{ } } + return nil + }, + }, + { + Name: "apply_email_title_magic_var", + EndVersion: "4.7.0", + Func: func(l logging.Logger, client *ent.Client, ctx context.Context) error { + // 1. Activate Template + mailActivationTemplateSetting, err := client.Setting.Query().Where(setting.Name("mail_activation_template")).First(ctx) + if err != nil { + return fmt.Errorf("failed to query mail_activation_template setting: %w", err) + } + + var mailActivationTemplate []struct { + Title string `json:"title"` + Body string `json:"body"` + Language string `json:"language"` + } + if err := json.Unmarshal([]byte(mailActivationTemplateSetting.Value), &mailActivationTemplate); err != nil { + return fmt.Errorf("failed to unmarshal mail_activation_template setting: %w", err) + } + + for i, t := range mailActivationTemplate { + mailActivationTemplate[i].Title = fmt.Sprintf("[{{ .CommonContext.SiteBasic.Name }}] %s", t.Title) + } + + newMailActivationTemplate, err := json.Marshal(mailActivationTemplate) + if err != nil { + return fmt.Errorf("failed to marshal mail_activation_template setting: %w", err) + } + + if _, err := client.Setting.UpdateOne(mailActivationTemplateSetting).SetValue(string(newMailActivationTemplate)).Save(ctx); err != nil { + return fmt.Errorf("failed to update mail_activation_template setting: %w", err) + } + + // 2. Reset Password Template + mailResetTemplateSetting, err := client.Setting.Query().Where(setting.Name("mail_reset_template")).First(ctx) + if err != nil { + return fmt.Errorf("failed to query mail_reset_template setting: %w", err) + } + + var mailResetTemplate []struct { + Title string `json:"title"` + Body string `json:"body"` + Language string `json:"language"` + } + if err := json.Unmarshal([]byte(mailResetTemplateSetting.Value), &mailResetTemplate); err != nil { + return fmt.Errorf("failed to unmarshal mail_reset_template setting: %w", err) + } + + for i, t := range mailResetTemplate { + mailResetTemplate[i].Title = fmt.Sprintf("[{{ .CommonContext.SiteBasic.Name }}] %s", t.Title) + } + + newMailResetTemplate, err := json.Marshal(mailResetTemplate) + if err != nil { + return fmt.Errorf("failed to marshal mail_reset_template setting: %w", err) + } + + if _, err := client.Setting.UpdateOne(mailResetTemplateSetting).SetValue(string(newMailResetTemplate)).Save(ctx); err != nil { + return fmt.Errorf("failed to update mail_reset_template setting: %w", err) + } + return nil }, }, From cec2b55e1ed463d4d3285457eb63f55877d55a49 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Tue, 2 Sep 2025 13:06:56 +0800 Subject: [PATCH 43/74] update submodule --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index 463794a7..e53eb86a 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 463794a71e6e19b9d4ee35248f00ff64f9485f30 +Subproject commit e53eb86a326475487eec2dd831307b6fe71f57ae From fe7cf5d0d868e41c3d06856cff721d9f58ce338f Mon Sep 17 00:00:00 2001 From: Darren Yu Date: Fri, 5 Sep 2025 11:40:30 +0800 Subject: [PATCH 44/74] feat(thumb): enhance native thumbnail generater with encoding format and quality (#2868) * feat(thumb): enhance native thumbnail generater with encoding format and quality * Update thumbnail.go * Update obs.go --- pkg/filemanager/driver/cos/cos.go | 8 +++++ pkg/filemanager/driver/ks3/ks3.go | 43 ++++++++++++++++++++++++++- pkg/filemanager/driver/obs/obs.go | 12 +++++++- pkg/filemanager/driver/oss/oss.go | 9 ++++++ pkg/filemanager/driver/qiniu/qiniu.go | 12 +++++++- pkg/filemanager/driver/upyun/upyun.go | 10 ++++++- pkg/filemanager/manager/thumbnail.go | 3 +- 7 files changed, 91 insertions(+), 6 deletions(-) diff --git a/pkg/filemanager/driver/cos/cos.go b/pkg/filemanager/driver/cos/cos.go index 89e83880..b87e3eee 100644 --- a/pkg/filemanager/driver/cos/cos.go +++ b/pkg/filemanager/driver/cos/cos.go @@ -352,6 +352,14 @@ func (handler Driver) Thumb(ctx context.Context, expire *time.Time, ext string, w, h := handler.settings.ThumbSize(ctx) thumbParam := fmt.Sprintf("imageMogr2/thumbnail/%dx%d", w, h) + enco := handler.settings.ThumbEncode(ctx) + switch enco.Format { + case "jpg", "webp": + thumbParam += fmt.Sprintf("/format/%s/rquality/%d", enco.Format, enco.Quality) + case "png": + thumbParam += fmt.Sprintf("/format/%s", enco.Format) + } + source, err := handler.signSourceURL( ctx, e.Source(), diff --git a/pkg/filemanager/driver/ks3/ks3.go b/pkg/filemanager/driver/ks3/ks3.go index e1ffdf40..6e2b008f 100644 --- a/pkg/filemanager/driver/ks3/ks3.go +++ b/pkg/filemanager/driver/ks3/ks3.go @@ -298,7 +298,48 @@ func (handler *Driver) Delete(ctx context.Context, files ...string) ([]string, e // Thumb 获取缩略图URL func (handler *Driver) Thumb(ctx context.Context, expire *time.Time, ext string, e fs.Entity) (string, error) { - return "", errors.New("not implemented") + w, h := handler.settings.ThumbSize(ctx) + thumbParam := fmt.Sprintf("@base@tag=imgScale&m=0&w=%d&h=%d", w, h) + + enco := handler.settings.ThumbEncode(ctx) + switch enco.Format { + case "jpg", "webp": + thumbParam += fmt.Sprintf("&q=%d&F=%s", enco.Quality, enco.Format) + case "png": + thumbParam += fmt.Sprintf("&F=%s", enco.Format) + } + + // 确保过期时间不小于 0 ,如果小于则设置为 7 天 + var ttl int64 + if expire != nil { + ttl = int64(time.Until(*expire).Seconds()) + } else { + ttl = 604800 + } + + thumbUrl, err := handler.svc.GeneratePresignedUrl(&s3.GeneratePresignedUrlInput{ + HTTPMethod: s3.GET, // 请求方法 + Bucket: &handler.policy.BucketName, // 存储空间名称 + Key: aws.String(e.Source()+thumbParam), // 对象的key + Expires: ttl, // 过期时间,转换为秒数 + }) + + if err != nil { + return "", err + } + + // 将最终生成的签名URL域名换成用户自定义的加速域名(如果有) + finalThumbURL, err := url.Parse(thumbUrl) + if err != nil { + return "", err + } + + // 公有空间替换掉Key及不支持的头 + if !handler.policy.IsPrivate { + finalThumbURL.RawQuery = "" + } + + return finalThumbURL.String(), nil } // Source 获取文件外链 diff --git a/pkg/filemanager/driver/obs/obs.go b/pkg/filemanager/driver/obs/obs.go index 4dadfcf7..a9fc9137 100644 --- a/pkg/filemanager/driver/obs/obs.go +++ b/pkg/filemanager/driver/obs/obs.go @@ -335,13 +335,23 @@ func (d *Driver) LocalPath(ctx context.Context, path string) string { func (d *Driver) Thumb(ctx context.Context, expire *time.Time, ext string, e fs.Entity) (string, error) { w, h := d.settings.ThumbSize(ctx) + thumbParam := fmt.Sprintf("image/resize,m_lfit,w_%d,h_%d", w, h) + + enco := d.settings.ThumbEncode(ctx) + switch enco.Format { + case "jpg", "webp": + thumbParam += fmt.Sprintf("/format,%s/quality,q_%d", enco.Format, enco.Quality) + case "png": + thumbParam += fmt.Sprintf("/format,%s", enco.Format) + } + thumbURL, err := d.signSourceURL(&obs.CreateSignedUrlInput{ Method: obs.HttpMethodGet, Bucket: d.policy.BucketName, Key: e.Source(), Expires: int(time.Until(*expire).Seconds()), QueryParams: map[string]string{ - imageProcessHeader: fmt.Sprintf("image/resize,m_lfit,w_%d,h_%d", w, h), + imageProcessHeader: thumbParam, }, }) diff --git a/pkg/filemanager/driver/oss/oss.go b/pkg/filemanager/driver/oss/oss.go index 5575e7be..b2d2c0a7 100644 --- a/pkg/filemanager/driver/oss/oss.go +++ b/pkg/filemanager/driver/oss/oss.go @@ -334,6 +334,15 @@ func (handler *Driver) Thumb(ctx context.Context, expire *time.Time, ext string, w, h := handler.settings.ThumbSize(ctx) thumbParam := fmt.Sprintf("image/resize,m_lfit,h_%d,w_%d", h, w) + + enco := handler.settings.ThumbEncode(ctx) + switch enco.Format { + case "jpg", "webp": + thumbParam += fmt.Sprintf("/format,%s/quality,q_%d", enco.Format, enco.Quality) + case "png": + thumbParam += fmt.Sprintf("/format,%s", enco.Format) + } + thumbOption := []oss.Option{oss.Process(thumbParam)} thumbURL, err := handler.signSourceURL( ctx, diff --git a/pkg/filemanager/driver/qiniu/qiniu.go b/pkg/filemanager/driver/qiniu/qiniu.go index d6dad012..bc185c1f 100644 --- a/pkg/filemanager/driver/qiniu/qiniu.go +++ b/pkg/filemanager/driver/qiniu/qiniu.go @@ -277,10 +277,20 @@ func (handler *Driver) Delete(ctx context.Context, files ...string) ([]string, e // Thumb 获取文件缩略图 func (handler *Driver) Thumb(ctx context.Context, expire *time.Time, ext string, e fs.Entity) (string, error) { w, h := handler.settings.ThumbSize(ctx) + thumbParam := fmt.Sprintf("imageView2/1/w/%d/h/%d", w, h) + + enco := handler.settings.ThumbEncode(ctx) + switch enco.Format { + case "jpg", "webp": + thumbParam += fmt.Sprintf("/format/%s/q/%d", enco.Format, enco.Quality) + case "png": + thumbParam += fmt.Sprintf("/format/%s", enco.Format) + } + return handler.signSourceURL( e.Source(), url.Values{ - fmt.Sprintf("imageView2/1/w/%d/h/%d", w, h): []string{}, + thumbParam: []string{}, }, expire, ), nil diff --git a/pkg/filemanager/driver/upyun/upyun.go b/pkg/filemanager/driver/upyun/upyun.go index 35fe5e7d..f0a5e5b3 100644 --- a/pkg/filemanager/driver/upyun/upyun.go +++ b/pkg/filemanager/driver/upyun/upyun.go @@ -203,8 +203,16 @@ func (handler *Driver) Delete(ctx context.Context, files ...string) ([]string, e // Thumb 获取文件缩略图 func (handler *Driver) Thumb(ctx context.Context, expire *time.Time, ext string, e fs.Entity) (string, error) { w, h := handler.settings.ThumbSize(ctx) - thumbParam := fmt.Sprintf("!/fwfh/%dx%d", w, h) + + enco := handler.settings.ThumbEncode(ctx) + switch enco.Format { + case "jpg", "webp": + thumbParam += fmt.Sprintf("/format/%s/quality/%d", enco.Format, enco.Quality) + case "png": + thumbParam += fmt.Sprintf("/format/%s", enco.Format) + } + thumbURL, err := handler.signURL(ctx, e.Source()+thumbParam, nil, expire) if err != nil { return "", err diff --git a/pkg/filemanager/manager/thumbnail.go b/pkg/filemanager/manager/thumbnail.go index 78e3f3e2..8a4559d8 100644 --- a/pkg/filemanager/manager/thumbnail.go +++ b/pkg/filemanager/manager/thumbnail.go @@ -185,9 +185,8 @@ func (m *manager) generateThumb(ctx context.Context, uri *fs.URI, ext string, es Uri: uri, Size: fileInfo.Size(), SavePath: fmt.Sprintf( - "%s.%s%s", + "%s%s", es.Entity().Source(), - util.RandStringRunes(16), m.settings.ThumbEntitySuffix(ctx), ), MimeType: m.dep.MimeDetector(ctx).TypeByName("thumb.jpg"), From a581851f84843ac13d98b4fc271b41a347de43c3 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Fri, 12 Sep 2025 14:04:51 +0800 Subject: [PATCH 45/74] feat(webdav): option to disable system file uploads (#2871) --- assets | 2 +- inventory/types/types.go | 1 + pkg/webdav/webdav.go | 17 ++++++++++++----- service/setting/webdav.go | 13 +++++++++---- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/assets b/assets index e53eb86a..dcf21d5e 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit e53eb86a326475487eec2dd831307b6fe71f57ae +Subproject commit dcf21d5eb9fbb635e81ab3c13b44e1233db5cac9 diff --git a/inventory/types/types.go b/inventory/types/types.go index 9ba07138..d59806bc 100644 --- a/inventory/types/types.go +++ b/inventory/types/types.go @@ -257,6 +257,7 @@ func FileTypeFromString(s string) FileType { const ( DavAccountReadOnly DavAccountOption = iota DavAccountProxy + DavAccountDisableSysFiles ) const ( diff --git a/pkg/webdav/webdav.go b/pkg/webdav/webdav.go index da20873b..fd630736 100644 --- a/pkg/webdav/webdav.go +++ b/pkg/webdav/webdav.go @@ -9,6 +9,12 @@ import ( "context" "errors" "fmt" + "net/http" + "net/url" + "path" + "strings" + "time" + "github.com/cloudreve/Cloudreve/v4/application/dependency" "github.com/cloudreve/Cloudreve/v4/ent" "github.com/cloudreve/Cloudreve/v4/inventory" @@ -26,11 +32,6 @@ import ( "github.com/gin-gonic/gin" "github.com/samber/lo" "golang.org/x/tools/container/intsets" - "net/http" - "net/url" - "path" - "strings" - "time" ) const ( @@ -228,6 +229,12 @@ func handlePut(c *gin.Context, user *ent.User, fm manager.FileManager) (status i return purposeStatusCodeFromError(err), err } + if user.Edges.DavAccounts[0].Options.Enabled(int(types.DavAccountDisableSysFiles)) { + if strings.HasPrefix(reqPath.Name(), ".") { + return http.StatusMethodNotAllowed, nil + } + } + release, ls, status, err := confirmLock(c, fm, user, ancestor, nil, uri, nil) if err != nil { return status, err diff --git a/service/setting/webdav.go b/service/setting/webdav.go index 640776c7..3ed88001 100644 --- a/service/setting/webdav.go +++ b/service/setting/webdav.go @@ -86,10 +86,11 @@ func (service *ListDavAccountsService) List(c *gin.Context) (*ListDavAccountResp type ( CreateDavAccountService struct { - Uri string `json:"uri" binding:"required"` - Name string `json:"name" binding:"required,min=1,max=255"` - Readonly bool `json:"readonly"` - Proxy bool `json:"proxy"` + Uri string `json:"uri" binding:"required"` + Name string `json:"name" binding:"required,min=1,max=255"` + Readonly bool `json:"readonly"` + Proxy bool `json:"proxy"` + DisableSysFiles bool `json:"disable_sys_files"` } CreateDavAccountParamCtx struct{} ) @@ -173,6 +174,10 @@ func (service *CreateDavAccountService) validateAndGetBs(user *ent.User) (*bools boolset.Set(types.DavAccountReadOnly, true, &bs) } + if service.DisableSysFiles { + boolset.Set(types.DavAccountDisableSysFiles, true, &bs) + } + if service.Proxy && user.Edges.Group.Permissions.Enabled(int(types.GroupPermissionWebDAVProxy)) { boolset.Set(types.DavAccountProxy, true, &bs) } From 7d97237593bf50c6172ca0b663eb43ce86df2813 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Fri, 12 Sep 2025 15:41:43 +0800 Subject: [PATCH 46/74] feat(archive viewer): option to select text encoding for zip files (#2867) --- assets | 2 +- pkg/filemanager/manager/archive.go | 91 +++++++++++++++++++++++++--- pkg/filemanager/manager/manager.go | 2 +- pkg/filemanager/workflows/extract.go | 51 +--------------- service/explorer/file.go | 7 ++- 5 files changed, 90 insertions(+), 63 deletions(-) diff --git a/assets b/assets index dcf21d5e..dece1c70 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit dcf21d5eb9fbb635e81ab3c13b44e1233db5cac9 +Subproject commit dece1c7098de2efe38aaa25d6cafc41a2de568ff diff --git a/pkg/filemanager/manager/archive.go b/pkg/filemanager/manager/archive.go index 667ca6f9..daded9cf 100644 --- a/pkg/filemanager/manager/archive.go +++ b/pkg/filemanager/manager/archive.go @@ -17,6 +17,13 @@ import ( "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs/dbfs" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource" "github.com/cloudreve/Cloudreve/v4/pkg/util" + "golang.org/x/text/encoding" + "golang.org/x/text/encoding/charmap" + "golang.org/x/text/encoding/japanese" + "golang.org/x/text/encoding/korean" + "golang.org/x/text/encoding/simplifiedchinese" + "golang.org/x/text/encoding/traditionalchinese" + "golang.org/x/text/encoding/unicode" "golang.org/x/tools/container/intsets" ) @@ -37,7 +44,47 @@ func init() { gob.Register([]ArchivedFile{}) } -func (m *manager) ListArchiveFiles(ctx context.Context, uri *fs.URI, entity string) ([]ArchivedFile, error) { +var ZipEncodings = map[string]encoding.Encoding{ + "ibm866": charmap.CodePage866, + "iso8859_2": charmap.ISO8859_2, + "iso8859_3": charmap.ISO8859_3, + "iso8859_4": charmap.ISO8859_4, + "iso8859_5": charmap.ISO8859_5, + "iso8859_6": charmap.ISO8859_6, + "iso8859_7": charmap.ISO8859_7, + "iso8859_8": charmap.ISO8859_8, + "iso8859_8I": charmap.ISO8859_8I, + "iso8859_10": charmap.ISO8859_10, + "iso8859_13": charmap.ISO8859_13, + "iso8859_14": charmap.ISO8859_14, + "iso8859_15": charmap.ISO8859_15, + "iso8859_16": charmap.ISO8859_16, + "koi8r": charmap.KOI8R, + "koi8u": charmap.KOI8U, + "macintosh": charmap.Macintosh, + "windows874": charmap.Windows874, + "windows1250": charmap.Windows1250, + "windows1251": charmap.Windows1251, + "windows1252": charmap.Windows1252, + "windows1253": charmap.Windows1253, + "windows1254": charmap.Windows1254, + "windows1255": charmap.Windows1255, + "windows1256": charmap.Windows1256, + "windows1257": charmap.Windows1257, + "windows1258": charmap.Windows1258, + "macintoshcyrillic": charmap.MacintoshCyrillic, + "gbk": simplifiedchinese.GBK, + "gb18030": simplifiedchinese.GB18030, + "big5": traditionalchinese.Big5, + "eucjp": japanese.EUCJP, + "iso2022jp": japanese.ISO2022JP, + "shiftjis": japanese.ShiftJIS, + "euckr": korean.EUCKR, + "utf16be": unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM), + "utf16le": unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM), +} + +func (m *manager) ListArchiveFiles(ctx context.Context, uri *fs.URI, entity, zipEncoding string) ([]ArchivedFile, error) { file, err := m.fs.Get(ctx, uri, dbfs.WithFileEntities(), dbfs.WithRequiredCapabilities(dbfs.NavigatorCapabilityDownloadFile)) if err != nil { return nil, fmt.Errorf("failed to get file: %w", err) @@ -57,7 +104,18 @@ func (m *manager) ListArchiveFiles(ctx context.Context, uri *fs.URI, entity stri return nil, fs.ErrEntityNotExist } - cacheKey := getArchiveListCacheKey(targetEntity.ID()) + var ( + enc encoding.Encoding + ok bool + ) + if zipEncoding != "" { + enc, ok = ZipEncodings[strings.ToLower(zipEncoding)] + if !ok { + return nil, fs.ErrNotSupportedAction.WithError(fmt.Errorf("not supported zip encoding: %s", zipEncoding)) + } + } + + cacheKey := getArchiveListCacheKey(targetEntity.ID(), zipEncoding) kv := m.kv res, found := kv.Get(cacheKey) if found { @@ -72,7 +130,7 @@ func (m *manager) ListArchiveFiles(ctx context.Context, uri *fs.URI, entity stri es.Apply(entitysource.WithContext(ctx)) defer es.Close() - var readerFunc func(ctx context.Context, file io.ReaderAt, size int64) ([]ArchivedFile, error) + var readerFunc func(ctx context.Context, file io.ReaderAt, size int64, textEncoding encoding.Encoding) ([]ArchivedFile, error) switch file.Ext() { case "zip": readerFunc = getZipFileList @@ -83,7 +141,7 @@ func (m *manager) ListArchiveFiles(ctx context.Context, uri *fs.URI, entity stri } sr := io.NewSectionReader(es, 0, targetEntity.Size()) - fileList, err := readerFunc(ctx, sr, targetEntity.Size()) + fileList, err := readerFunc(ctx, sr, targetEntity.Size(), enc) if err != nil { return nil, fmt.Errorf("failed to read file list: %w", err) } @@ -199,7 +257,7 @@ func (m *manager) compressFileToArchive(ctx context.Context, parent string, file } -func getZipFileList(ctx context.Context, file io.ReaderAt, size int64) ([]ArchivedFile, error) { +func getZipFileList(ctx context.Context, file io.ReaderAt, size int64, textEncoding encoding.Encoding) ([]ArchivedFile, error) { zr, err := zip.NewReader(file, size) if err != nil { return nil, fmt.Errorf("failed to create zip reader: %w", err) @@ -207,10 +265,25 @@ func getZipFileList(ctx context.Context, file io.ReaderAt, size int64) ([]Archiv fileList := make([]ArchivedFile, 0, len(zr.File)) for _, f := range zr.File { + hdr := f.FileHeader + if hdr.NonUTF8 && textEncoding != nil { + dec := textEncoding.NewDecoder() + filename, err := dec.String(hdr.Name) + if err == nil { + hdr.Name = filename + } + if hdr.Comment != "" { + comment, err := dec.String(hdr.Comment) + if err == nil { + hdr.Comment = comment + } + } + } + info := f.FileInfo() modTime := info.ModTime() fileList = append(fileList, ArchivedFile{ - Name: util.FormSlash(f.Name), + Name: util.FormSlash(hdr.Name), Size: info.Size(), UpdatedAt: &modTime, IsDirectory: info.IsDir(), @@ -219,7 +292,7 @@ func getZipFileList(ctx context.Context, file io.ReaderAt, size int64) ([]Archiv return fileList, nil } -func get7zFileList(ctx context.Context, file io.ReaderAt, size int64) ([]ArchivedFile, error) { +func get7zFileList(ctx context.Context, file io.ReaderAt, size int64, extEncoding encoding.Encoding) ([]ArchivedFile, error) { zr, err := sevenzip.NewReader(file, size) if err != nil { return nil, fmt.Errorf("failed to create 7z reader: %w", err) @@ -239,6 +312,6 @@ func get7zFileList(ctx context.Context, file io.ReaderAt, size int64) ([]Archive return fileList, nil } -func getArchiveListCacheKey(entity int) string { - return fmt.Sprintf("archive_list_%d", entity) +func getArchiveListCacheKey(entity int, encoding string) string { + return fmt.Sprintf("archive_list_%d_%s", entity, encoding) } diff --git a/pkg/filemanager/manager/manager.go b/pkg/filemanager/manager/manager.go index 98d01b99..bc52b579 100644 --- a/pkg/filemanager/manager/manager.go +++ b/pkg/filemanager/manager/manager.go @@ -88,7 +88,7 @@ type ( // CreateArchive creates an archive CreateArchive(ctx context.Context, uris []*fs.URI, writer io.Writer, opts ...fs.Option) (int, error) // ListArchiveFiles lists files in an archive - ListArchiveFiles(ctx context.Context, uri *fs.URI, entity string) ([]ArchivedFile, error) + ListArchiveFiles(ctx context.Context, uri *fs.URI, entity, zipEncoding string) ([]ArchivedFile, error) } FileManager interface { diff --git a/pkg/filemanager/workflows/extract.go b/pkg/filemanager/workflows/extract.go index f51d226e..181bd034 100644 --- a/pkg/filemanager/workflows/extract.go +++ b/pkg/filemanager/workflows/extract.go @@ -27,13 +27,6 @@ import ( "github.com/cloudreve/Cloudreve/v4/pkg/util" "github.com/gofrs/uuid" "github.com/mholt/archives" - "golang.org/x/text/encoding" - "golang.org/x/text/encoding/charmap" - "golang.org/x/text/encoding/japanese" - "golang.org/x/text/encoding/korean" - "golang.org/x/text/encoding/simplifiedchinese" - "golang.org/x/text/encoding/traditionalchinese" - "golang.org/x/text/encoding/unicode" ) type ( @@ -79,46 +72,6 @@ func init() { queue.RegisterResumableTaskFactory(queue.ExtractArchiveTaskType, NewExtractArchiveTaskFromModel) } -var encodings = map[string]encoding.Encoding{ - "ibm866": charmap.CodePage866, - "iso8859_2": charmap.ISO8859_2, - "iso8859_3": charmap.ISO8859_3, - "iso8859_4": charmap.ISO8859_4, - "iso8859_5": charmap.ISO8859_5, - "iso8859_6": charmap.ISO8859_6, - "iso8859_7": charmap.ISO8859_7, - "iso8859_8": charmap.ISO8859_8, - "iso8859_8I": charmap.ISO8859_8I, - "iso8859_10": charmap.ISO8859_10, - "iso8859_13": charmap.ISO8859_13, - "iso8859_14": charmap.ISO8859_14, - "iso8859_15": charmap.ISO8859_15, - "iso8859_16": charmap.ISO8859_16, - "koi8r": charmap.KOI8R, - "koi8u": charmap.KOI8U, - "macintosh": charmap.Macintosh, - "windows874": charmap.Windows874, - "windows1250": charmap.Windows1250, - "windows1251": charmap.Windows1251, - "windows1252": charmap.Windows1252, - "windows1253": charmap.Windows1253, - "windows1254": charmap.Windows1254, - "windows1255": charmap.Windows1255, - "windows1256": charmap.Windows1256, - "windows1257": charmap.Windows1257, - "windows1258": charmap.Windows1258, - "macintoshcyrillic": charmap.MacintoshCyrillic, - "gbk": simplifiedchinese.GBK, - "gb18030": simplifiedchinese.GB18030, - "big5": traditionalchinese.Big5, - "eucjp": japanese.EUCJP, - "iso2022jp": japanese.ISO2022JP, - "shiftjis": japanese.ShiftJIS, - "euckr": korean.EUCKR, - "utf16be": unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM), - "utf16le": unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM), -} - // NewExtractArchiveTask creates a new ExtractArchiveTask func NewExtractArchiveTask(ctx context.Context, src, dst, encoding, password string, mask []string) (queue.Task, error) { state := &ExtractArchiveTaskState{ @@ -374,7 +327,7 @@ func (m *ExtractArchiveTask) masterExtractArchive(ctx context.Context, dep depen if zipExtractor, ok := extractor.(archives.Zip); ok { if m.state.Encoding != "" { m.l.Info("Using encoding %q for zip archive", m.state.Encoding) - encoding, ok := encodings[strings.ToLower(m.state.Encoding)] + encoding, ok := manager.ZipEncodings[strings.ToLower(m.state.Encoding)] if !ok { m.l.Warning("Unknown encoding %q, fallback to default encoding", m.state.Encoding) } else { @@ -750,7 +703,7 @@ func (m *SlaveExtractArchiveTask) Do(ctx context.Context) (task.Status, error) { if zipExtractor, ok := extractor.(archives.Zip); ok { if m.state.Encoding != "" { m.l.Info("Using encoding %q for zip archive", m.state.Encoding) - encoding, ok := encodings[strings.ToLower(m.state.Encoding)] + encoding, ok := manager.ZipEncodings[strings.ToLower(m.state.Encoding)] if !ok { m.l.Warning("Unknown encoding %q, fallback to default encoding", m.state.Encoding) } else { diff --git a/service/explorer/file.go b/service/explorer/file.go index e82a60c3..05120b84 100644 --- a/service/explorer/file.go +++ b/service/explorer/file.go @@ -720,8 +720,9 @@ func (s *PatchViewService) Patch(c *gin.Context) error { type ( ArchiveListFilesParamCtx struct{} ArchiveListFilesService struct { - Uri string `form:"uri" binding:"required"` - Entity string `form:"entity"` + Uri string `form:"uri" binding:"required"` + Entity string `form:"entity"` + TextEncoding string `form:"text_encoding"` } ) @@ -739,7 +740,7 @@ func (s *ArchiveListFilesService) List(c *gin.Context) (*ArchiveListFilesRespons return nil, serializer.NewError(serializer.CodeParamErr, "unknown uri", err) } - files, err := m.ListArchiveFiles(c, uri, s.Entity) + files, err := m.ListArchiveFiles(c, uri, s.Entity, s.TextEncoding) if err != nil { return nil, fmt.Errorf("failed to list archive files: %w", err) } From 9434c2f29b850860e181624a441fe000c3cc3412 Mon Sep 17 00:00:00 2001 From: Darren Yu Date: Sat, 13 Sep 2025 16:18:18 +0800 Subject: [PATCH 47/74] fix(upgrade v3): validation on unique magic var in either blob name or path (#2890) * fix(upgrade v3): validation on unique magic var in either blob name or path * Update policy.go --- application/migrator/policy.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/application/migrator/policy.go b/application/migrator/policy.go index 83c6c5e0..8ae263f9 100644 --- a/application/migrator/policy.go +++ b/application/migrator/policy.go @@ -103,10 +103,6 @@ func (m *Migrator) migratePolicy() (map[int]bool, error) { settings.ProxyServer = policy.OptionsSerialized.OdProxy } - if policy.DirNameRule == "" { - policy.DirNameRule = "uploads/{uid}/{path}" - } - if policy.Type == types.PolicyTypeCos { settings.ChunkSize = 1024 * 1024 * 25 } @@ -122,8 +118,16 @@ func (m *Migrator) migratePolicy() (map[int]bool, error) { hasRandomElement = true break } + + if strings.Contains(policy.DirNameRule, c) { + hasRandomElement = true + break + } } if !hasRandomElement { + if policy.DirNameRule == "" { + policy.DirNameRule = "uploads/{uid}/{path}" + } policy.FileNameRule = "{uid}_{randomkey8}_{originname}" m.l.Warning("Storage policy %q has no random element in file name rule, using default file name rule.", policy.Name) } From f0c5b084288d55dadee73966247c598bc7ea6370 Mon Sep 17 00:00:00 2001 From: Darren Yu Date: Tue, 16 Sep 2025 10:31:09 +0800 Subject: [PATCH 48/74] feat(extract): preserve last modified when extract archive file (#2897) --- pkg/filemanager/workflows/extract.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pkg/filemanager/workflows/extract.go b/pkg/filemanager/workflows/extract.go index 181bd034..f48d8547 100644 --- a/pkg/filemanager/workflows/extract.go +++ b/pkg/filemanager/workflows/extract.go @@ -292,7 +292,7 @@ func (m *ExtractArchiveTask) masterExtractArchive(ctx context.Context, dep depen extractor, ok := format.(archives.Extractor) if !ok { - return task.StatusError, fmt.Errorf("format not an extractor %s") + return task.StatusError, fmt.Errorf("format not an extractor %s", format.Extension()) } formatExt := format.Extension() @@ -409,6 +409,10 @@ func (m *ExtractArchiveTask) masterExtractArchive(ctx context.Context, dep depen Props: &fs.UploadProps{ Uri: savePath, Size: f.Size(), + LastModified: func() *time.Time { + t := f.FileInfo.ModTime().Local() + return &t + }(), }, ProgressFunc: func(current, diff int64, total int64) { atomic.AddInt64(&m.progress[ProgressTypeExtractSize].Current, diff) @@ -779,6 +783,10 @@ func (m *SlaveExtractArchiveTask) Do(ctx context.Context) (task.Status, error) { Props: &fs.UploadProps{ Uri: savePath, Size: f.Size(), + LastModified: func() *time.Time { + t := f.FileInfo.ModTime().Local() + return &t + }(), }, ProgressFunc: func(current, diff int64, total int64) { atomic.AddInt64(&m.progress[ProgressTypeExtractSize].Current, diff) From 3b8110b648d89a47a69da09e0a215cda9bab952b Mon Sep 17 00:00:00 2001 From: Darren Yu Date: Tue, 16 Sep 2025 10:33:41 +0800 Subject: [PATCH 49/74] fix(cos): traffic limit wrongly given in bytes, should be bits (#2899) --- pkg/filemanager/driver/cos/cos.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/filemanager/driver/cos/cos.go b/pkg/filemanager/driver/cos/cos.go index b87e3eee..88009953 100644 --- a/pkg/filemanager/driver/cos/cos.go +++ b/pkg/filemanager/driver/cos/cos.go @@ -382,7 +382,12 @@ func (handler Driver) Thumb(ctx context.Context, expire *time.Time, ext string, func (handler Driver) Source(ctx context.Context, e fs.Entity, args *driver.GetSourceArgs) (string, error) { // 添加各项设置 options := urlOption{} + if args.Speed > 0 { + // Byte 转换为 bit + args.Speed *= 8 + + // COS对速度值有范围限制 if args.Speed < 819200 { args.Speed = 819200 } @@ -391,6 +396,7 @@ func (handler Driver) Source(ctx context.Context, e fs.Entity, args *driver.GetS } options.Speed = args.Speed } + if args.IsDownload { encodedFilename := url.PathEscape(args.DisplayName) options.ContentDescription = fmt.Sprintf(`attachment; filename="%s"; filename*=UTF-8''%s`, From 58ceae9708e936f429d49785869df0c75a81ed43 Mon Sep 17 00:00:00 2001 From: Darren Yu Date: Tue, 16 Sep 2025 10:35:30 +0800 Subject: [PATCH 50/74] fix(uploader): failed to generate upload token for some file types (#2847) (#2900) * fix(mime): `mimeType` not assigned to new value when is empty * fix(mime): add fallback mime type --- pkg/filemanager/driver/cos/cos.go | 4 ++-- pkg/filemanager/driver/ks3/ks3.go | 4 ++-- pkg/filemanager/driver/oss/oss.go | 4 ++-- pkg/filemanager/driver/qiniu/qiniu.go | 4 ++-- pkg/filemanager/driver/s3/s3.go | 4 ++-- pkg/filemanager/driver/upyun/upyun.go | 4 ++-- pkg/filemanager/fs/mime/mime.go | 8 +++++++- 7 files changed, 19 insertions(+), 13 deletions(-) diff --git a/pkg/filemanager/driver/cos/cos.go b/pkg/filemanager/driver/cos/cos.go index 88009953..ddec60d8 100644 --- a/pkg/filemanager/driver/cos/cos.go +++ b/pkg/filemanager/driver/cos/cos.go @@ -244,7 +244,7 @@ func (handler *Driver) Put(ctx context.Context, file *fs.UploadRequest) error { mimeType := file.Props.MimeType if mimeType == "" { - handler.mime.TypeByName(file.Props.Uri.Name()) + mimeType = handler.mime.TypeByName(file.Props.Uri.Name()) } // 是否允许覆盖 @@ -455,7 +455,7 @@ func (handler Driver) Token(ctx context.Context, uploadSession *fs.UploadSession mimeType := file.Props.MimeType if mimeType == "" { - handler.mime.TypeByName(file.Props.Uri.Name()) + mimeType = handler.mime.TypeByName(file.Props.Uri.Name()) } // 初始化分片上传 diff --git a/pkg/filemanager/driver/ks3/ks3.go b/pkg/filemanager/driver/ks3/ks3.go index 6e2b008f..ddc1c975 100644 --- a/pkg/filemanager/driver/ks3/ks3.go +++ b/pkg/filemanager/driver/ks3/ks3.go @@ -219,7 +219,7 @@ func (handler *Driver) Put(ctx context.Context, file *fs.UploadRequest) error { mimeType := file.Props.MimeType if mimeType == "" { - handler.mime.TypeByName(file.Props.Uri.Name()) + mimeType = handler.mime.TypeByName(file.Props.Uri.Name()) } _, err := uploader.UploadWithContext(ctx, &s3manager.UploadInput{ @@ -399,7 +399,7 @@ func (handler *Driver) Token(ctx context.Context, uploadSession *fs.UploadSessio mimeType := file.Props.MimeType if mimeType == "" { - handler.mime.TypeByName(file.Props.Uri.Name()) + mimeType = handler.mime.TypeByName(file.Props.Uri.Name()) } // 创建分片上传 diff --git a/pkg/filemanager/driver/oss/oss.go b/pkg/filemanager/driver/oss/oss.go index b2d2c0a7..737820ec 100644 --- a/pkg/filemanager/driver/oss/oss.go +++ b/pkg/filemanager/driver/oss/oss.go @@ -240,7 +240,7 @@ func (handler *Driver) Put(ctx context.Context, file *fs.UploadRequest) error { mimeType := file.Props.MimeType if mimeType == "" { - handler.mime.TypeByName(file.Props.Uri.Name()) + mimeType = handler.mime.TypeByName(file.Props.Uri.Name()) } // 是否允许覆盖 @@ -450,7 +450,7 @@ func (handler *Driver) Token(ctx context.Context, uploadSession *fs.UploadSessio mimeType := file.Props.MimeType if mimeType == "" { - handler.mime.TypeByName(file.Props.Uri.Name()) + mimeType = handler.mime.TypeByName(file.Props.Uri.Name()) } // 初始化分片上传 diff --git a/pkg/filemanager/driver/qiniu/qiniu.go b/pkg/filemanager/driver/qiniu/qiniu.go index bc185c1f..06b37ba3 100644 --- a/pkg/filemanager/driver/qiniu/qiniu.go +++ b/pkg/filemanager/driver/qiniu/qiniu.go @@ -223,7 +223,7 @@ func (handler *Driver) Put(ctx context.Context, file *fs.UploadRequest) error { mimeType := file.Props.MimeType if mimeType == "" { - handler.mime.TypeByName(file.Props.Uri.Name()) + mimeType = handler.mime.TypeByName(file.Props.Uri.Name()) } err = resumeUploader.CompleteParts(ctx, upToken, upHost, nil, handler.policy.BucketName, @@ -389,7 +389,7 @@ func (handler *Driver) Token(ctx context.Context, uploadSession *fs.UploadSessio mimeType := file.Props.MimeType if mimeType == "" { - handler.mime.TypeByName(file.Props.Uri.Name()) + mimeType = handler.mime.TypeByName(file.Props.Uri.Name()) } uploadSession.UploadID = ret.UploadID diff --git a/pkg/filemanager/driver/s3/s3.go b/pkg/filemanager/driver/s3/s3.go index c8a6a292..05c41c6e 100644 --- a/pkg/filemanager/driver/s3/s3.go +++ b/pkg/filemanager/driver/s3/s3.go @@ -207,7 +207,7 @@ func (handler *Driver) Put(ctx context.Context, file *fs.UploadRequest) error { mimeType := file.Props.MimeType if mimeType == "" { - handler.mime.TypeByName(file.Props.Uri.Name()) + mimeType = handler.mime.TypeByName(file.Props.Uri.Name()) } _, err := uploader.UploadWithContext(ctx, &s3manager.UploadInput{ @@ -344,7 +344,7 @@ func (handler *Driver) Token(ctx context.Context, uploadSession *fs.UploadSessio mimeType := file.Props.MimeType if mimeType == "" { - handler.mime.TypeByName(file.Props.Uri.Name()) + mimeType = handler.mime.TypeByName(file.Props.Uri.Name()) } // 创建分片上传 diff --git a/pkg/filemanager/driver/upyun/upyun.go b/pkg/filemanager/driver/upyun/upyun.go index f0a5e5b3..895548bf 100644 --- a/pkg/filemanager/driver/upyun/upyun.go +++ b/pkg/filemanager/driver/upyun/upyun.go @@ -161,7 +161,7 @@ func (handler *Driver) Put(ctx context.Context, file *fs.UploadRequest) error { mimeType := file.Props.MimeType if mimeType == "" { - handler.mime.TypeByName(file.Props.Uri.Name()) + mimeType = handler.mime.TypeByName(file.Props.Uri.Name()) } err := handler.up.Put(&upyun.PutObjectConfig{ @@ -309,7 +309,7 @@ func (handler *Driver) Token(ctx context.Context, uploadSession *fs.UploadSessio mimeType := file.Props.MimeType if mimeType == "" { - handler.mime.TypeByName(file.Props.Uri.Name()) + mimeType = handler.mime.TypeByName(file.Props.Uri.Name()) } return &fs.UploadCredential{ diff --git a/pkg/filemanager/fs/mime/mime.go b/pkg/filemanager/fs/mime/mime.go index 751fd5c5..b5482296 100644 --- a/pkg/filemanager/fs/mime/mime.go +++ b/pkg/filemanager/fs/mime/mime.go @@ -36,5 +36,11 @@ func (d *mimeDetector) TypeByName(p string) string { return m } - return mime.TypeByExtension(ext) + m := mime.TypeByExtension(ext) + if m != "" { + return m + } + + // Fallback + return "application/octet-stream" } From 678593f30d1d69681df5eb23d32fb70fdabc1a4c Mon Sep 17 00:00:00 2001 From: Darren Yu Date: Tue, 16 Sep 2025 11:44:22 +0800 Subject: [PATCH 51/74] fix(thumb blob path): remove extra randomkey in thumb blob path (#2893) * fix(thumb blob path): remove extra randomkey in thumb blob path * Update upload.go Refactor SavePath assignment for clarity. * Update thumbnail.go --- pkg/filemanager/fs/dbfs/upload.go | 6 +----- pkg/filemanager/manager/thumbnail.go | 10 +++------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/pkg/filemanager/fs/dbfs/upload.go b/pkg/filemanager/fs/dbfs/upload.go index 6ede4076..c289617f 100644 --- a/pkg/filemanager/fs/dbfs/upload.go +++ b/pkg/filemanager/fs/dbfs/upload.go @@ -146,11 +146,7 @@ func (f *DBFS) PrepareUpload(ctx context.Context, req *fs.UploadRequest, opts .. if req.Props.SavePath == "" || isThumbnailAndPolicyNotAvailable { req.Props.SavePath = generateSavePath(policy, req, f.user) if isThumbnailAndPolicyNotAvailable { - req.Props.SavePath = fmt.Sprintf( - "%s.%s%s", - req.Props.SavePath, - util.RandStringRunes(16), - f.settingClient.ThumbEntitySuffix(ctx)) + req.Props.SavePath = req.Props.SavePath + f.settingClient.ThumbEntitySuffix(ctx) } } diff --git a/pkg/filemanager/manager/thumbnail.go b/pkg/filemanager/manager/thumbnail.go index 8a4559d8..605d47ee 100644 --- a/pkg/filemanager/manager/thumbnail.go +++ b/pkg/filemanager/manager/thumbnail.go @@ -182,13 +182,9 @@ func (m *manager) generateThumb(ctx context.Context, uri *fs.URI, ext string, es entityType := types.EntityTypeThumbnail req := &fs.UploadRequest{ Props: &fs.UploadProps{ - Uri: uri, - Size: fileInfo.Size(), - SavePath: fmt.Sprintf( - "%s%s", - es.Entity().Source(), - m.settings.ThumbEntitySuffix(ctx), - ), + Uri: uri, + Size: fileInfo.Size(), + SavePath: es.Entity().Source() + m.settings.ThumbEntitySuffix(ctx), MimeType: m.dep.MimeDetector(ctx).TypeByName("thumb.jpg"), EntityType: &entityType, }, From 440ab775b8f5faf27954c6fa71d8a6ebf23cc4fd Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Tue, 23 Sep 2025 09:53:31 +0800 Subject: [PATCH 52/74] chore(compose): add aria2 port mapping --- docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 78ff7b57..ff95bc0a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,6 +8,8 @@ services: restart: always ports: - 5212:5212 + - 6888:6888 + - 6888:6888/udp environment: - CR_CONF_Database.Type=postgres - CR_CONF_Database.Host=postgresql From 668b542c5952447892f95af639b9c7267a311198 Mon Sep 17 00:00:00 2001 From: Mason Liu <108563824+MasonDye@users.noreply.github.com> Date: Tue, 23 Sep 2025 11:24:38 +0800 Subject: [PATCH 53/74] feat: update reset thumbnail feature (#2854) * update reset thumbnail feature * consolidate supported thumbnail extensions into site config; remove dedicated API * allow patching thumb ; remove Reset Thumbnail API * fix code formatting --------- Co-authored-by: Aaron Liu --- pkg/filemanager/manager/metadata.go | 9 ++++++ pkg/thumb/builtin.go | 4 +++ routers/router.go | 2 +- service/basic/site.go | 48 +++++++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 1 deletion(-) diff --git a/pkg/filemanager/manager/metadata.go b/pkg/filemanager/manager/metadata.go index 69797fbf..a5293c18 100644 --- a/pkg/filemanager/manager/metadata.go +++ b/pkg/filemanager/manager/metadata.go @@ -97,6 +97,15 @@ var ( }, }, "dav": {}, + // Allow manipulating thumbnail metadata via public PatchMetadata API + "thumb": { + // Only supported thumb metadata currently is thumb:disabled + dbfs.ThumbDisabledKey: func(ctx context.Context, m *manager, patch *fs.MetadataPatch) error { + // Presence of this key disables thumbnails; value is ignored. + // We allow both setting and removing this key. + return nil + }, + }, customizeMetadataSuffix: { iconColorMetadataKey: validateColor(false), emojiIconMetadataKey: func(ctx context.Context, m *manager, patch *fs.MetadataPatch) error { diff --git a/pkg/thumb/builtin.go b/pkg/thumb/builtin.go index 3a9f2ff4..2081a9c2 100644 --- a/pkg/thumb/builtin.go +++ b/pkg/thumb/builtin.go @@ -19,6 +19,10 @@ import ( const thumbTempFolder = "thumb" +// BuiltinSupportedExts lists file extensions supported by the built-in +// thumbnail generator. Extensions are lowercased and do not include the dot. +var BuiltinSupportedExts = []string{"jpg", "jpeg", "png", "gif"} + // Thumb 缩略图 type Thumb struct { src image.Image diff --git a/routers/router.go b/routers/router.go index d474ccf3..7d388ce4 100644 --- a/routers/router.go +++ b/routers/router.go @@ -618,7 +618,7 @@ func initMasterRouter(dep dependency.Dep) *gin.Engine { controllers.ServeEntity, ) } - // 获取缩略图 + // get thumb file.GET("thumb", middleware.ContextHint(), controllers.FromQuery[explorer.FileThumbService](explorer.FileThumbParameterCtx{}), diff --git a/service/basic/site.go b/service/basic/site.go index 287c0ed1..474cd984 100644 --- a/service/basic/site.go +++ b/service/basic/site.go @@ -1,10 +1,14 @@ package basic import ( + "sort" + "strings" + "github.com/cloudreve/Cloudreve/v4/application/dependency" "github.com/cloudreve/Cloudreve/v4/inventory" "github.com/cloudreve/Cloudreve/v4/inventory/types" "github.com/cloudreve/Cloudreve/v4/pkg/setting" + "github.com/cloudreve/Cloudreve/v4/pkg/thumb" "github.com/cloudreve/Cloudreve/v4/service/user" "github.com/gin-gonic/gin" "github.com/mojocn/base64Captcha" @@ -49,6 +53,9 @@ type SiteConfig struct { ThumbnailHeight int `json:"thumbnail_height,omitempty"` CustomProps []types.CustomProps `json:"custom_props,omitempty"` + // Thumbnail section + ThumbExts []string `json:"thumb_exts,omitempty"` + // App settings AppPromotion bool `json:"app_promotion,omitempty"` @@ -118,6 +125,47 @@ func (s *GetSettingService) GetSiteConfig(c *gin.Context) (*SiteConfig, error) { return &SiteConfig{ AppPromotion: appSetting.Promotion, }, nil + case "thumb": + // Return supported thumbnail extensions from enabled generators. + exts := map[string]bool{} + if settings.BuiltinThumbGeneratorEnabled(c) { + for _, e := range thumb.BuiltinSupportedExts { + exts[e] = true + } + } + if settings.FFMpegThumbGeneratorEnabled(c) { + for _, e := range settings.FFMpegThumbExts(c) { + exts[strings.ToLower(e)] = true + } + } + if settings.VipsThumbGeneratorEnabled(c) { + for _, e := range settings.VipsThumbExts(c) { + exts[strings.ToLower(e)] = true + } + } + if settings.LibreOfficeThumbGeneratorEnabled(c) { + for _, e := range settings.LibreOfficeThumbExts(c) { + exts[strings.ToLower(e)] = true + } + } + if settings.MusicCoverThumbGeneratorEnabled(c) { + for _, e := range settings.MusicCoverThumbExts(c) { + exts[strings.ToLower(e)] = true + } + } + if settings.LibRawThumbGeneratorEnabled(c) { + for _, e := range settings.LibRawThumbExts(c) { + exts[strings.ToLower(e)] = true + } + } + + // map -> sorted slice + result := make([]string, 0, len(exts)) + for e := range exts { + result = append(result, e) + } + sort.Strings(result) + return &SiteConfig{ThumbExts: result}, nil default: break } From 5e5dca40c4d74ada4291a012b9d8c9e7cc8197de Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Fri, 26 Sep 2025 11:27:46 +0800 Subject: [PATCH 54/74] feat(media meta): reverse geocoding from mapbox (#2922) --- application/dependency/dependency.go | 2 +- assets | 2 +- inventory/setting.go | 2 + pkg/cluster/routes/routes.go | 4 +- pkg/filemanager/driver/cos/cos.go | 2 +- pkg/filemanager/driver/handler.go | 3 +- pkg/filemanager/driver/ks3/ks3.go | 12 +- pkg/filemanager/driver/local/local.go | 2 +- pkg/filemanager/driver/obs/media.go | 2 +- pkg/filemanager/driver/onedrive/onedrive.go | 2 +- pkg/filemanager/driver/oss/oss.go | 2 +- pkg/filemanager/driver/qiniu/qiniu.go | 2 +- pkg/filemanager/driver/remote/client.go | 6 +- pkg/filemanager/driver/remote/remote.go | 4 +- pkg/filemanager/driver/s3/s3.go | 2 +- pkg/filemanager/driver/upyun/upyun.go | 2 +- pkg/filemanager/manager/mediameta.go | 10 +- pkg/mediameta/exif.go | 7 +- pkg/mediameta/extractor.go | 40 +++- pkg/mediameta/ffprobe.go | 7 +- pkg/mediameta/geocoding.go | 236 ++++++++++++++++++++ pkg/mediameta/music.go | 7 +- pkg/setting/provider.go | 12 + service/explorer/slave.go | 4 +- 24 files changed, 339 insertions(+), 35 deletions(-) create mode 100644 pkg/mediameta/geocoding.go diff --git a/application/dependency/dependency.go b/application/dependency/dependency.go index 70d8514b..308ea10a 100644 --- a/application/dependency/dependency.go +++ b/application/dependency/dependency.go @@ -467,7 +467,7 @@ func (d *dependency) MediaMetaExtractor(ctx context.Context) mediameta.Extractor return d.mediaMeta } - d.mediaMeta = mediameta.NewExtractorManager(ctx, d.SettingProvider(), d.Logger()) + d.mediaMeta = mediameta.NewExtractorManager(ctx, d.SettingProvider(), d.Logger(), d.RequestClient()) return d.mediaMeta } diff --git a/assets b/assets index dece1c70..fc7791cd 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit dece1c7098de2efe38aaa25d6cafc41a2de568ff +Subproject commit fc7791cde1444e1be0935f1fbc32d956fa6eb756 diff --git a/inventory/setting.go b/inventory/setting.go index 19b4aa68..2c3422f1 100644 --- a/inventory/setting.go +++ b/inventory/setting.go @@ -636,6 +636,8 @@ var DefaultSettings = map[string]string{ "media_meta_ffprobe_path": "ffprobe", "media_meta_ffprobe_size_local": "0", "media_meta_ffprobe_size_remote": "0", + "media_meta_geocoding": "0", + "media_meta_geocoding_mapbox_ak": "", "site_logo": "/static/img/logo.svg", "site_logo_light": "/static/img/logo_light.svg", "tos_url": "https://cloudreve.org/privacy-policy", diff --git a/pkg/cluster/routes/routes.go b/pkg/cluster/routes/routes.go index 535db9ae..3e315978 100644 --- a/pkg/cluster/routes/routes.go +++ b/pkg/cluster/routes/routes.go @@ -180,9 +180,9 @@ func SlaveFileContentUrl(base *url.URL, srcPath, name string, download bool, spe return base } -func SlaveMediaMetaRoute(src, ext string) string { +func SlaveMediaMetaRoute(src, ext, language string) string { src = url.PathEscape(base64.URLEncoding.EncodeToString([]byte(src))) - return fmt.Sprintf("file/meta/%s/%s", src, url.PathEscape(ext)) + return fmt.Sprintf("file/meta/%s/%s?language=%s", src, url.PathEscape(ext), language) } func SlaveFileListRoute(srcPath string, recursive bool) string { diff --git a/pkg/filemanager/driver/cos/cos.go b/pkg/filemanager/driver/cos/cos.go index ddec60d8..c9ad912d 100644 --- a/pkg/filemanager/driver/cos/cos.go +++ b/pkg/filemanager/driver/cos/cos.go @@ -594,7 +594,7 @@ func (handler Driver) Meta(ctx context.Context, path string) (*MetaData, error) }, nil } -func (handler *Driver) MediaMeta(ctx context.Context, path, ext string) ([]driver.MediaMeta, error) { +func (handler *Driver) MediaMeta(ctx context.Context, path, ext, language string) ([]driver.MediaMeta, error) { if util.ContainsString(supportedImageExt, ext) { return handler.extractImageMeta(ctx, path) } diff --git a/pkg/filemanager/driver/handler.go b/pkg/filemanager/driver/handler.go index 21b0ec5f..1593ee0d 100644 --- a/pkg/filemanager/driver/handler.go +++ b/pkg/filemanager/driver/handler.go @@ -83,7 +83,7 @@ type ( Capabilities() *Capabilities // MediaMeta extracts media metadata from the given file. - MediaMeta(ctx context.Context, path, ext string) ([]MediaMeta, error) + MediaMeta(ctx context.Context, path, ext, language string) ([]MediaMeta, error) } Capabilities struct { @@ -117,6 +117,7 @@ const ( MetaTypeExif MetaType = "exif" MediaTypeMusic MetaType = "music" MetaTypeStreamMedia MetaType = "stream" + MetaTypeGeocoding MetaType = "geocoding" ) type ForceUsePublicEndpointCtx struct{} diff --git a/pkg/filemanager/driver/ks3/ks3.go b/pkg/filemanager/driver/ks3/ks3.go index ddc1c975..74a4ecc7 100644 --- a/pkg/filemanager/driver/ks3/ks3.go +++ b/pkg/filemanager/driver/ks3/ks3.go @@ -306,7 +306,7 @@ func (handler *Driver) Thumb(ctx context.Context, expire *time.Time, ext string, case "jpg", "webp": thumbParam += fmt.Sprintf("&q=%d&F=%s", enco.Quality, enco.Format) case "png": - thumbParam += fmt.Sprintf("&F=%s", enco.Format) + thumbParam += fmt.Sprintf("&F=%s", enco.Format) } // 确保过期时间不小于 0 ,如果小于则设置为 7 天 @@ -318,10 +318,10 @@ func (handler *Driver) Thumb(ctx context.Context, expire *time.Time, ext string, } thumbUrl, err := handler.svc.GeneratePresignedUrl(&s3.GeneratePresignedUrlInput{ - HTTPMethod: s3.GET, // 请求方法 - Bucket: &handler.policy.BucketName, // 存储空间名称 - Key: aws.String(e.Source()+thumbParam), // 对象的key - Expires: ttl, // 过期时间,转换为秒数 + HTTPMethod: s3.GET, // 请求方法 + Bucket: &handler.policy.BucketName, // 存储空间名称 + Key: aws.String(e.Source() + thumbParam), // 对象的key + Expires: ttl, // 过期时间,转换为秒数 }) if err != nil { @@ -505,7 +505,7 @@ func (handler *Driver) Capabilities() *driver.Capabilities { } // MediaMeta 获取媒体元信息 -func (handler *Driver) MediaMeta(ctx context.Context, path, ext string) ([]driver.MediaMeta, error) { +func (handler *Driver) MediaMeta(ctx context.Context, path, ext, language string) ([]driver.MediaMeta, error) { return nil, errors.New("not implemented") } diff --git a/pkg/filemanager/driver/local/local.go b/pkg/filemanager/driver/local/local.go index 28e2b555..172effb8 100644 --- a/pkg/filemanager/driver/local/local.go +++ b/pkg/filemanager/driver/local/local.go @@ -298,6 +298,6 @@ func (handler *Driver) Capabilities() *driver.Capabilities { return capabilities } -func (handler *Driver) MediaMeta(ctx context.Context, path, ext string) ([]driver.MediaMeta, error) { +func (handler *Driver) MediaMeta(ctx context.Context, path, ext, language string) ([]driver.MediaMeta, error) { return nil, errors.New("not implemented") } diff --git a/pkg/filemanager/driver/obs/media.go b/pkg/filemanager/driver/obs/media.go index b7d75e41..fec894fd 100644 --- a/pkg/filemanager/driver/obs/media.go +++ b/pkg/filemanager/driver/obs/media.go @@ -17,7 +17,7 @@ import ( "github.com/samber/lo" ) -func (d *Driver) MediaMeta(ctx context.Context, path, ext string) ([]driver.MediaMeta, error) { +func (d *Driver) MediaMeta(ctx context.Context, path, ext, language string) ([]driver.MediaMeta, error) { thumbURL, err := d.signSourceURL(&obs.CreateSignedUrlInput{ Method: obs.HttpMethodGet, Bucket: d.policy.BucketName, diff --git a/pkg/filemanager/driver/onedrive/onedrive.go b/pkg/filemanager/driver/onedrive/onedrive.go index 4eeef076..4a3170d1 100644 --- a/pkg/filemanager/driver/onedrive/onedrive.go +++ b/pkg/filemanager/driver/onedrive/onedrive.go @@ -241,7 +241,7 @@ func (handler *Driver) Capabilities() *driver.Capabilities { } } -func (handler *Driver) MediaMeta(ctx context.Context, path, ext string) ([]driver.MediaMeta, error) { +func (handler *Driver) MediaMeta(ctx context.Context, path, ext, language string) ([]driver.MediaMeta, error) { return nil, errors.New("not implemented") } diff --git a/pkg/filemanager/driver/oss/oss.go b/pkg/filemanager/driver/oss/oss.go index 737820ec..9d468078 100644 --- a/pkg/filemanager/driver/oss/oss.go +++ b/pkg/filemanager/driver/oss/oss.go @@ -535,7 +535,7 @@ func (handler *Driver) Capabilities() *driver.Capabilities { } } -func (handler *Driver) MediaMeta(ctx context.Context, path, ext string) ([]driver.MediaMeta, error) { +func (handler *Driver) MediaMeta(ctx context.Context, path, ext, language string) ([]driver.MediaMeta, error) { if util.ContainsString(supportedImageExt, ext) { return handler.extractImageMeta(ctx, path) } diff --git a/pkg/filemanager/driver/qiniu/qiniu.go b/pkg/filemanager/driver/qiniu/qiniu.go index 06b37ba3..40ec09d7 100644 --- a/pkg/filemanager/driver/qiniu/qiniu.go +++ b/pkg/filemanager/driver/qiniu/qiniu.go @@ -433,7 +433,7 @@ func (handler *Driver) Capabilities() *driver.Capabilities { } } -func (handler *Driver) MediaMeta(ctx context.Context, path, ext string) ([]driver.MediaMeta, error) { +func (handler *Driver) MediaMeta(ctx context.Context, path, ext, language string) ([]driver.MediaMeta, error) { if util.ContainsString(supportedImageExt, ext) { return handler.extractImageMeta(ctx, path) } diff --git a/pkg/filemanager/driver/remote/client.go b/pkg/filemanager/driver/remote/client.go index 2c8e6b4f..fe7e095b 100644 --- a/pkg/filemanager/driver/remote/client.go +++ b/pkg/filemanager/driver/remote/client.go @@ -43,7 +43,7 @@ type Client interface { // DeleteUploadSession deletes remote upload session DeleteUploadSession(ctx context.Context, sessionID string) error // MediaMeta gets media meta from remote server - MediaMeta(ctx context.Context, src, ext string) ([]driver.MediaMeta, error) + MediaMeta(ctx context.Context, src, ext, language string) ([]driver.MediaMeta, error) // DeleteFiles deletes files from remote server DeleteFiles(ctx context.Context, files ...string) ([]string, error) // List lists files from remote server @@ -183,10 +183,10 @@ func (c *remoteClient) DeleteFiles(ctx context.Context, files ...string) ([]stri return nil, nil } -func (c *remoteClient) MediaMeta(ctx context.Context, src, ext string) ([]driver.MediaMeta, error) { +func (c *remoteClient) MediaMeta(ctx context.Context, src, ext, language string) ([]driver.MediaMeta, error) { resp, err := c.httpClient.Request( http.MethodGet, - routes.SlaveMediaMetaRoute(src, ext), + routes.SlaveMediaMetaRoute(src, ext, language), nil, request.WithContext(ctx), request.WithLogger(c.l), diff --git a/pkg/filemanager/driver/remote/remote.go b/pkg/filemanager/driver/remote/remote.go index eb2b5efb..42b2b1af 100644 --- a/pkg/filemanager/driver/remote/remote.go +++ b/pkg/filemanager/driver/remote/remote.go @@ -179,6 +179,6 @@ func (handler *Driver) Capabilities() *driver.Capabilities { } } -func (handler *Driver) MediaMeta(ctx context.Context, path, ext string) ([]driver.MediaMeta, error) { - return handler.uploadClient.MediaMeta(ctx, path, ext) +func (handler *Driver) MediaMeta(ctx context.Context, path, ext, language string) ([]driver.MediaMeta, error) { + return handler.uploadClient.MediaMeta(ctx, path, ext, language) } diff --git a/pkg/filemanager/driver/s3/s3.go b/pkg/filemanager/driver/s3/s3.go index 05c41c6e..04c8e8e9 100644 --- a/pkg/filemanager/driver/s3/s3.go +++ b/pkg/filemanager/driver/s3/s3.go @@ -482,7 +482,7 @@ func (handler *Driver) Capabilities() *driver.Capabilities { } } -func (handler *Driver) MediaMeta(ctx context.Context, path, ext string) ([]driver.MediaMeta, error) { +func (handler *Driver) MediaMeta(ctx context.Context, path, ext, language string) ([]driver.MediaMeta, error) { return nil, errors.New("not implemented") } diff --git a/pkg/filemanager/driver/upyun/upyun.go b/pkg/filemanager/driver/upyun/upyun.go index 895548bf..bc1be1fb 100644 --- a/pkg/filemanager/driver/upyun/upyun.go +++ b/pkg/filemanager/driver/upyun/upyun.go @@ -345,7 +345,7 @@ func (handler *Driver) Capabilities() *driver.Capabilities { } } -func (handler *Driver) MediaMeta(ctx context.Context, path, ext string) ([]driver.MediaMeta, error) { +func (handler *Driver) MediaMeta(ctx context.Context, path, ext, language string) ([]driver.MediaMeta, error) { return handler.extractImageMeta(ctx, path) } diff --git a/pkg/filemanager/manager/mediameta.go b/pkg/filemanager/manager/mediameta.go index 4ed3d252..82bf8ed6 100644 --- a/pkg/filemanager/manager/mediameta.go +++ b/pkg/filemanager/manager/mediameta.go @@ -14,6 +14,7 @@ import ( "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs/dbfs" "github.com/cloudreve/Cloudreve/v4/pkg/logging" + "github.com/cloudreve/Cloudreve/v4/pkg/mediameta" "github.com/cloudreve/Cloudreve/v4/pkg/queue" "github.com/cloudreve/Cloudreve/v4/pkg/util" "github.com/samber/lo" @@ -106,6 +107,11 @@ func (m *manager) ExtractAndSaveMediaMeta(ctx context.Context, uri *fs.URI, enti return nil } + language := "" + if file.Owner().Settings != nil { + language = file.Owner().Settings.Language + } + var ( metas []driver.MediaMeta ) @@ -117,7 +123,7 @@ func (m *manager) ExtractAndSaveMediaMeta(ctx context.Context, uri *fs.URI, enti driverCaps := d.Capabilities() if util.IsInExtensionList(driverCaps.MediaMetaSupportedExts, file.Name()) { m.l.Debug("Using native driver to generate media meta.") - metas, err = d.MediaMeta(ctx, targetVersion.Source(), file.Ext()) + metas, err = d.MediaMeta(ctx, targetVersion.Source(), file.Ext(), language) if err != nil { return fmt.Errorf("failed to get media meta using native driver: %w", err) } @@ -130,7 +136,7 @@ func (m *manager) ExtractAndSaveMediaMeta(ctx context.Context, uri *fs.URI, enti return fmt.Errorf("failed to get entity source: %w", err) } - metas, err = extractor.Extract(ctx, file.Ext(), source) + metas, err = extractor.Extract(ctx, file.Ext(), source, mediameta.WithLanguage(language)) if err != nil { return fmt.Errorf("failed to extract media meta using local extractor: %w", err) } diff --git a/pkg/mediameta/exif.go b/pkg/mediameta/exif.go index 27e0d213..df55c885 100644 --- a/pkg/mediameta/exif.go +++ b/pkg/mediameta/exif.go @@ -145,7 +145,12 @@ func (e *exifExtractor) Exts() []string { } // Reference: https://github.com/photoprism/photoprism/blob/602097635f1c84d91f2d919f7aedaef7a07fc458/internal/meta/exif.go -func (e *exifExtractor) Extract(ctx context.Context, ext string, source entitysource.EntitySource) ([]driver.MediaMeta, error) { +func (e *exifExtractor) Extract(ctx context.Context, ext string, source entitysource.EntitySource, opts ...optionFunc) ([]driver.MediaMeta, error) { + option := &option{} + for _, opt := range opts { + opt.apply(option) + } + localLimit, remoteLimit := e.settings.MediaMetaExifSizeLimit(ctx) if err := checkFileSize(localLimit, remoteLimit, source); err != nil { return nil, err diff --git a/pkg/mediameta/extractor.go b/pkg/mediameta/extractor.go index e871189e..90345947 100644 --- a/pkg/mediameta/extractor.go +++ b/pkg/mediameta/extractor.go @@ -4,12 +4,14 @@ import ( "context" "encoding/gob" "errors" + "io" + "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource" "github.com/cloudreve/Cloudreve/v4/pkg/logging" + "github.com/cloudreve/Cloudreve/v4/pkg/request" "github.com/cloudreve/Cloudreve/v4/pkg/setting" "github.com/samber/lo" - "io" ) type ( @@ -17,7 +19,7 @@ type ( // Exts returns the supported file extensions. Exts() []string // Extract extracts the media meta from the given source. - Extract(ctx context.Context, ext string, source entitysource.EntitySource) ([]driver.MediaMeta, error) + Extract(ctx context.Context, ext string, source entitysource.EntitySource, opts ...optionFunc) ([]driver.MediaMeta, error) } ) @@ -29,7 +31,7 @@ func init() { gob.Register([]driver.MediaMeta{}) } -func NewExtractorManager(ctx context.Context, settings setting.Provider, l logging.Logger) Extractor { +func NewExtractorManager(ctx context.Context, settings setting.Provider, l logging.Logger, client request.Client) Extractor { e := &extractorManager{ settings: settings, extMap: make(map[string][]Extractor), @@ -52,6 +54,11 @@ func NewExtractorManager(ctx context.Context, settings setting.Provider, l loggi extractors = append(extractors, ffprobeE) } + if e.settings.MediaMetaGeocodingEnabled(ctx) { + geocodingE := newGeocodingExtractor(settings, l, client) + extractors = append(extractors, geocodingE) + } + for _, extractor := range extractors { for _, ext := range extractor.Exts() { if e.extMap[ext] == nil { @@ -73,12 +80,12 @@ func (e *extractorManager) Exts() []string { return lo.Keys(e.extMap) } -func (e *extractorManager) Extract(ctx context.Context, ext string, source entitysource.EntitySource) ([]driver.MediaMeta, error) { +func (e *extractorManager) Extract(ctx context.Context, ext string, source entitysource.EntitySource, opts ...optionFunc) ([]driver.MediaMeta, error) { if extractor, ok := e.extMap[ext]; ok { res := []driver.MediaMeta{} for _, e := range extractor { _, _ = source.Seek(0, io.SeekStart) - data, err := e.Extract(ctx, ext, source) + data, err := e.Extract(ctx, ext, source, append(opts, WithExtracted(res))...) if err != nil { return nil, err } @@ -92,6 +99,29 @@ func (e *extractorManager) Extract(ctx context.Context, ext string, source entit } } +type option struct { + extracted []driver.MediaMeta + language string +} + +type optionFunc func(*option) + +func (f optionFunc) apply(o *option) { + f(o) +} + +func WithExtracted(extracted []driver.MediaMeta) optionFunc { + return optionFunc(func(o *option) { + o.extracted = extracted + }) +} + +func WithLanguage(language string) optionFunc { + return optionFunc(func(o *option) { + o.language = language + }) +} + // checkFileSize checks if the file size exceeds the limit. func checkFileSize(localLimit, remoteLimit int64, source entitysource.EntitySource) error { if source.IsLocal() && localLimit > 0 && source.Entity().Size() > localLimit { diff --git a/pkg/mediameta/ffprobe.go b/pkg/mediameta/ffprobe.go index 3cc1117f..369985e3 100644 --- a/pkg/mediameta/ffprobe.go +++ b/pkg/mediameta/ffprobe.go @@ -88,7 +88,12 @@ func (f *ffprobeExtractor) Exts() []string { return ffprobeExts } -func (f *ffprobeExtractor) Extract(ctx context.Context, ext string, source entitysource.EntitySource) ([]driver.MediaMeta, error) { +func (f *ffprobeExtractor) Extract(ctx context.Context, ext string, source entitysource.EntitySource, opts ...optionFunc) ([]driver.MediaMeta, error) { + option := &option{} + for _, opt := range opts { + opt.apply(option) + } + localLimit, remoteLimit := f.settings.MediaMetaFFProbeSizeLimit(ctx) if err := checkFileSize(localLimit, remoteLimit, source); err != nil { return nil, err diff --git a/pkg/mediameta/geocoding.go b/pkg/mediameta/geocoding.go new file mode 100644 index 00000000..e2b96423 --- /dev/null +++ b/pkg/mediameta/geocoding.go @@ -0,0 +1,236 @@ +package mediameta + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strconv" + + "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver" + "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource" + "github.com/cloudreve/Cloudreve/v4/pkg/logging" + "github.com/cloudreve/Cloudreve/v4/pkg/request" + "github.com/cloudreve/Cloudreve/v4/pkg/setting" +) + +const mapBoxURL = "https://api.mapbox.com/search/geocode/v6/reverse" + +const ( + Street = "street" + Locality = "locality" + Place = "place" + District = "district" + Region = "region" + Country = "country" +) + +type geocodingExtractor struct { + settings setting.Provider + l logging.Logger + client request.Client +} + +func newGeocodingExtractor(settings setting.Provider, l logging.Logger, client request.Client) *geocodingExtractor { + return &geocodingExtractor{ + settings: settings, + l: l, + client: client, + } +} + +func (e *geocodingExtractor) Exts() []string { + return exifExts +} + +func (e *geocodingExtractor) Extract(ctx context.Context, ext string, source entitysource.EntitySource, opts ...optionFunc) ([]driver.MediaMeta, error) { + option := &option{} + for _, opt := range opts { + opt.apply(option) + } + + // Find GPS info from extracted + var latStr, lngStr string + for _, meta := range option.extracted { + if meta.Key == GpsLat { + latStr = meta.Value + } + if meta.Key == GpsLng { + lngStr = meta.Value + } + } + + if latStr == "" || lngStr == "" { + return nil, nil + } + + lat, err := strconv.ParseFloat(latStr, 64) + if err != nil { + return nil, fmt.Errorf("geocoding: failed to parse latitude: %w", err) + } + + lng, err := strconv.ParseFloat(lngStr, 64) + if err != nil { + return nil, fmt.Errorf("geocoding: failed to parse longitude: %w", err) + } + + metas, err := e.getGeocoding(ctx, lat, lng, option.language) + if err != nil { + return nil, fmt.Errorf("geocoding: failed to get geocoding: %w", err) + } + + for i, _ := range metas { + metas[i].Type = driver.MetaTypeGeocoding + } + + return metas, nil +} + +func (e *geocodingExtractor) getGeocoding(ctx context.Context, lat, lng float64, language string) ([]driver.MediaMeta, error) { + values := url.Values{} + values.Add("longitude", fmt.Sprintf("%f", lng)) + values.Add("latitude", fmt.Sprintf("%f", lat)) + values.Add("limit", "1") + values.Add("access_token", e.settings.MediaMetaGeocodingMapboxAK(ctx)) + if language != "" { + values.Add("language", language) + } + + resp, err := e.client.Request( + "GET", + mapBoxURL+"?"+values.Encode(), + nil, + request.WithContext(ctx), + request.WithLogger(e.l), + ).CheckHTTPResponse(http.StatusOK).GetResponse() + if err != nil { + return nil, fmt.Errorf("failed to get geocoding from mapbox: %w", err) + } + + var geocoding MapboxGeocodingResponse + if err := json.Unmarshal([]byte(resp), &geocoding); err != nil { + return nil, fmt.Errorf("failed to unmarshal geocoding from mapbox: %w", err) + } + + if len(geocoding.Features) == 0 { + return nil, nil + } + + metas := make([]driver.MediaMeta, 0) + contexts := geocoding.Features[0].Properties.Context + if contexts.Street != nil { + metas = append(metas, driver.MediaMeta{ + Key: Street, + Value: contexts.Street.Name, + }) + } + if contexts.Locality != nil { + metas = append(metas, driver.MediaMeta{ + Key: Locality, + Value: contexts.Locality.Name, + }) + } + if contexts.Place != nil { + metas = append(metas, driver.MediaMeta{ + Key: Place, + Value: contexts.Place.Name, + }) + } + if contexts.District != nil { + metas = append(metas, driver.MediaMeta{ + Key: District, + Value: contexts.District.Name, + }) + } + if contexts.Region != nil { + metas = append(metas, driver.MediaMeta{ + Key: Region, + Value: contexts.Region.Name, + }) + } + if contexts.Country != nil { + metas = append(metas, driver.MediaMeta{ + Key: Country, + Value: contexts.Country.Name, + }) + } + + return metas, nil +} + +// MapboxGeocodingResponse represents the response from Mapbox Geocoding API +type MapboxGeocodingResponse struct { + Type string `json:"type"` // "FeatureCollection" + Features []Feature `json:"features"` // Array of feature objects + Attribution string `json:"attribution"` // Attribution to Mapbox +} + +// Feature represents a feature object in the geocoding response +type Feature struct { + ID string `json:"id"` // Feature ID (same as mapbox_id) + Type string `json:"type"` // "Feature" + Geometry Geometry `json:"geometry"` // Spatial geometry of the feature + Properties Properties `json:"properties"` // Feature details +} + +// Geometry represents the spatial geometry of a feature +type Geometry struct { + Type string `json:"type"` // "Point" + Coordinates []float64 `json:"coordinates"` // [longitude, latitude] +} + +// Properties contains the feature's detailed information +type Properties struct { + MapboxID string `json:"mapbox_id"` // Unique feature identifier + FeatureType string `json:"feature_type"` // Type of feature (country, region, etc.) + Name string `json:"name"` // Formatted address string + NamePreferred string `json:"name_preferred"` // Canonical or common alias + PlaceFormatted string `json:"place_formatted"` // Formatted context string + FullAddress string `json:"full_address"` // Full formatted address + Context Context `json:"context"` // Hierarchy of parent features + Coordinates Coordinates `json:"coordinates"` // Geographic position and accuracy + BBox []float64 `json:"bbox,omitempty"` // Bounding box [minLon,minLat,maxLon,maxLat] + MatchCode MatchCode `json:"match_code"` // Metadata about result matching +} + +// Context represents the hierarchy of encompassing parent features +type Context struct { + Country *ContextFeature `json:"country,omitempty"` + Region *ContextFeature `json:"region,omitempty"` + Postcode *ContextFeature `json:"postcode,omitempty"` + District *ContextFeature `json:"district,omitempty"` + Place *ContextFeature `json:"place,omitempty"` + Locality *ContextFeature `json:"locality,omitempty"` + Neighborhood *ContextFeature `json:"neighborhood,omitempty"` + Street *ContextFeature `json:"street,omitempty"` +} + +// ContextFeature represents a feature in the context hierarchy +type ContextFeature struct { + ID string `json:"id"` + Name string `json:"name"` + NamePreferred string `json:"name_preferred,omitempty"` + MapboxID string `json:"mapbox_id"` +} + +// Coordinates represents geographical position and accuracy information +type Coordinates struct { + Longitude float64 `json:"longitude"` // Longitude of result + Latitude float64 `json:"latitude"` // Latitude of result + Accuracy string `json:"accuracy,omitempty"` // Accuracy metric for address results + RoutablePoints []RoutablePoint `json:"routable_points,omitempty"` // Array of routable points +} + +// RoutablePoint represents a routable point for an address feature +type RoutablePoint struct { + Name string `json:"name"` // Name of the routable point + Longitude float64 `json:"longitude"` // Longitude coordinate + Latitude float64 `json:"latitude"` // Latitude coordinate +} + +// MatchCode contains metadata about how result components match the input query +type MatchCode struct { + // Add specific match code fields as needed based on Mapbox documentation + // This structure may vary depending on the specific match codes returned +} diff --git a/pkg/mediameta/music.go b/pkg/mediameta/music.go index ac2ec78c..6d4075c5 100644 --- a/pkg/mediameta/music.go +++ b/pkg/mediameta/music.go @@ -48,7 +48,12 @@ func (a *musicExtractor) Exts() []string { return audioExts } -func (a *musicExtractor) Extract(ctx context.Context, ext string, source entitysource.EntitySource) ([]driver.MediaMeta, error) { +func (a *musicExtractor) Extract(ctx context.Context, ext string, source entitysource.EntitySource, opts ...optionFunc) ([]driver.MediaMeta, error) { + option := &option{} + for _, opt := range opts { + opt.apply(option) + } + localLimit, remoteLimit := a.settings.MediaMetaMusicSizeLimit(ctx) if err := checkFileSize(localLimit, remoteLimit, source); err != nil { return nil, err diff --git a/pkg/setting/provider.go b/pkg/setting/provider.go index 1959433a..017ff2fb 100644 --- a/pkg/setting/provider.go +++ b/pkg/setting/provider.go @@ -102,6 +102,10 @@ type ( MediaMetaFFProbeSizeLimit(ctx context.Context) (int64, int64) // MediaMetaFFProbePath returns the path of ffprobe executable. MediaMetaFFProbePath(ctx context.Context) string + // MediaMetaGeocodingEnabled returns true if media meta geocoding is enabled. + MediaMetaGeocodingEnabled(ctx context.Context) bool + // MediaMetaGeocodingMapboxAK returns the Mapbox access token. + MediaMetaGeocodingMapboxAK(ctx context.Context) string // ThumbSize returns the size limit of thumbnails. ThumbSize(ctx context.Context) (int, int) // ThumbEncode returns the thumbnail encoding settings. @@ -527,6 +531,14 @@ func (s *settingProvider) MediaMetaEnabled(ctx context.Context) bool { return s.getBoolean(ctx, "media_meta", true) } +func (s *settingProvider) MediaMetaGeocodingEnabled(ctx context.Context) bool { + return s.getBoolean(ctx, "media_meta_geocoding", false) +} + +func (s *settingProvider) MediaMetaGeocodingMapboxAK(ctx context.Context) string { + return s.getString(ctx, "media_meta_geocoding_mapbox_ak", "") +} + func (s *settingProvider) PublicResourceMaxAge(ctx context.Context) int { return s.getInt(ctx, "public_resource_maxage", 0) } diff --git a/service/explorer/slave.go b/service/explorer/slave.go index dc356836..c4335aff 100644 --- a/service/explorer/slave.go +++ b/service/explorer/slave.go @@ -13,6 +13,7 @@ import ( "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource" + "github.com/cloudreve/Cloudreve/v4/pkg/mediameta" "github.com/cloudreve/Cloudreve/v4/pkg/serializer" "github.com/gin-gonic/gin" "github.com/samber/lo" @@ -135,8 +136,9 @@ func (s *SlaveMetaService) MediaMeta(c *gin.Context) ([]driver.MediaMeta, error) } defer entitySource.Close() + language := c.Query("language") extractor := dep.MediaMetaExtractor(c) - res, err := extractor.Extract(c, s.Ext, entitySource) + res, err := extractor.Extract(c, s.Ext, entitySource, mediameta.WithLanguage(language)) if err != nil { return nil, fmt.Errorf("failed to extract media meta: %w", err) } From 3d41e00384ec870728a141c9bfa3b9e19975c289 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Sat, 27 Sep 2025 10:19:22 +0800 Subject: [PATCH 55/74] feat(media meta): add Mapbox as a map provider option (#2922) --- assets | 2 +- inventory/setting.go | 1 + pkg/setting/provider.go | 1 + pkg/setting/types.go | 2 ++ service/basic/site.go | 2 ++ 5 files changed, 7 insertions(+), 1 deletion(-) diff --git a/assets b/assets index fc7791cd..0bf85fa0 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit fc7791cde1444e1be0935f1fbc32d956fa6eb756 +Subproject commit 0bf85fa0abdfa25c4bd20305e2013ac307cfc106 diff --git a/inventory/setting.go b/inventory/setting.go index 2c3422f1..12bce190 100644 --- a/inventory/setting.go +++ b/inventory/setting.go @@ -650,6 +650,7 @@ var DefaultSettings = map[string]string{ "emojis": `{"😀":["😀","😃","😄","😁","😆","😅","🤣","😂","🙂","🙃","🫠","😉","😊","😇","🥰","😍","🤩","😘","😗","😚","😙","🥲","😋","😛","😜","🤪","😝","🤑","🤗","🤭","🫢","🫣","🤫","🤔","🫡","🤐","🤨","😐","😑","😶","😶‍🌫️","😏","😒","🙄","😬","😮‍💨","🤥","😌","😔","😪","🤤","😴","😷","🤒","🤕","🤢","🤮","🤧","🥵","🥶","🥴","😵","😵‍💫","🤯","🤠","🥳","🥸","😎","🤓","🧐","😕","🫤","😟","🙁","😮","😯","😲","😳","🥺","🥹","😦","😧","😨","😰","😥","😢","😭","😱","😖","😣","😞","😓","😩","😫","🥱","😤","😡","😠","🤬","😈","👿","💀","☠️","💩","🤡","👹","👺","👻","👽","👾","🤖","😺","😸","😹","😻","😼","😽","🙀","😿","😾","🙈","🙉","🙊","💋","💌","💘","💝","💖","💗","💓","💞","💕","💟","💔","❤️‍🔥","❤️‍🩹","❤️","🧡","💛","💚","💙","💜","🤎","🖤","🤍","💯","💢","💥","💫","💦","💨","🕳️","💣","💬","👁️‍🗨️","🗨️","🗯️","💭","💤"],"👋":["👋","🤚","🖐️","✋","🖖","🫱","🫲","🫳","🫴","👌","🤌","🤏","✌️","🤞","🫰","🤟","🤘","🤙","👈","👉","👆","🖕","👇","☝️","🫵","👍","👎","✊","👊","🤛","🤜","👏","🙌","🫶","👐","🤲","🤝","🙏","✍️","💅","🤳","💪","🦾","🦿","🦵","🦶","👂","🦻","👃","🧠","🫀","🫁","🦷","🦴","👀","👁️","👅","👄","🫦","👶","🧒","👦","👧","🧑","👱","👨","🧔","🧔‍♂️","🧔‍♀️","👨‍🦰","👨‍🦱","👨‍🦳","👨‍🦲","👩","👩‍🦰","🧑‍🦰","👩‍🦱","🧑‍🦱","👩‍🦳","🧑‍🦳","👩‍🦲","🧑‍🦲","👱‍♀️","👱‍♂️","🧓","👴","👵","🙍","🙍‍♂️","🙍‍♀️","🙎","🙎‍♂️","🙎‍♀️","🙅","🙅‍♂️","🙅‍♀️","🙆","🙆‍♂️","🙆‍♀️","💁","💁‍♂️","💁‍♀️","🙋","🙋‍♂️","🙋‍♀️","🧏","🧏‍♂️","🧏‍♀️","🙇","🙇‍♂️","🙇‍♀️","🤦","🤦‍♂️","🤦‍♀️","🤷","🤷‍♂️","🤷‍♀️","🧑‍⚕️","👨‍⚕️","👩‍⚕️","🧑‍🎓","👨‍🎓","👩‍🎓","🧑‍🏫","👨‍🏫","👩‍🏫","🧑‍⚖️","👨‍⚖️","👩‍⚖️","🧑‍🌾","👨‍🌾","👩‍🌾","🧑‍🍳","👨‍🍳","👩‍🍳","🧑‍🔧","👨‍🔧","👩‍🔧","🧑‍🏭","👨‍🏭","👩‍🏭","🧑‍💼","👨‍💼","👩‍💼","🧑‍🔬","👨‍🔬","👩‍🔬","🧑‍💻","👨‍💻","👩‍💻","🧑‍🎤","👨‍🎤","👩‍🎤","🧑‍🎨","👨‍🎨","👩‍🎨","🧑‍✈️","👨‍✈️","👩‍✈️","🧑‍🚀","👨‍🚀","👩‍🚀","🧑‍🚒","👨‍🚒","👩‍🚒","👮","👮‍♂️","👮‍♀️","🕵️","🕵️‍♂️","🕵️‍♀️","💂","💂‍♂️","💂‍♀️","🥷","👷","👷‍♂️","👷‍♀️","🫅","🤴","👸","👳","👳‍♂️","👳‍♀️","👲","🧕","🤵","🤵‍♂️","🤵‍♀️","👰","👰‍♂️","👰‍♀️","🤰","🫃","🫄","🤱","👩‍🍼","👨‍🍼","🧑‍🍼","👼","🎅","🤶","🧑‍🎄","🦸","🦸‍♂️","🦸‍♀️","🦹","🦹‍♂️","🦹‍♀️","🧙","🧙‍♂️","🧙‍♀️","🧚","🧚‍♂️","🧚‍♀️","🧛","🧛‍♂️","🧛‍♀️","🧜","🧜‍♂️","🧜‍♀️","🧝","🧝‍♂️","🧝‍♀️","🧞","🧞‍♂️","🧞‍♀️","🧟","🧟‍♂️","🧟‍♀️","🧌","💆","💆‍♂️","💆‍♀️","💇","💇‍♂️","💇‍♀️","🚶","🚶‍♂️","🚶‍♀️","🧍","🧍‍♂️","🧍‍♀️","🧎","🧎‍♂️","🧎‍♀️","🧑‍🦯","👨‍🦯","👩‍🦯","🧑‍🦼","👨‍🦼","👩‍🦼","🧑‍🦽","👨‍🦽","👩‍🦽","🏃","🏃‍♂️","🏃‍♀️","💃","🕺","🕴️","👯","👯‍♂️","👯‍♀️","🧖","🧖‍♂️","🧖‍♀️","🧗","🧗‍♂️","🧗‍♀️","🤺","🏇","⛷️","🏂","🏌️","🏌️‍♂️","🏌️‍♀️","🏄","🏄‍♂️","🏄‍♀️","🚣","🚣‍♂️","🚣‍♀️","🏊","🏊‍♂️","🏊‍♀️","⛹️","⛹️‍♂️","⛹️‍♀️","🏋️","🏋️‍♂️","🏋️‍♀️","🚴","🚴‍♂️","🚴‍♀️","🚵","🚵‍♂️","🚵‍♀️","🤸","🤸‍♂️","🤸‍♀️","🤼","🤼‍♂️","🤼‍♀️","🤽","🤽‍♂️","🤽‍♀️","🤾","🤾‍♂️","🤾‍♀️","🤹","🤹‍♂️","🤹‍♀️","🧘","🧘‍♂️","🧘‍♀️","🛀","🛌","🧑‍🤝‍🧑","👭","👫","👬","💏","👩‍❤️‍💋‍👨","👨‍❤️‍💋‍👨","👩‍❤️‍💋‍👩","💑","👩‍❤️‍👨","👨‍❤️‍👨","👩‍❤️‍👩","👪","👨‍👩‍👦","👨‍👩‍👧","👨‍👩‍👧‍👦","👨‍👩‍👦‍👦","👨‍👩‍👧‍👧","👨‍👨‍👦","👨‍👨‍👧","👨‍👨‍👧‍👦","👨‍👨‍👦‍👦","👨‍👨‍👧‍👧","👩‍👩‍👦","👩‍👩‍👧","👩‍👩‍👧‍👦","👩‍👩‍👦‍👦","👩‍👩‍👧‍👧","👨‍👦","👨‍👦‍👦","👨‍👧","👨‍👧‍👦","👨‍👧‍👧","👩‍👦","👩‍👦‍👦","👩‍👧","👩‍👧‍👦","👩‍👧‍👧","🗣️","👤","👥","🫂","👣","🦰","🦱","🦳","🦲"],"🐵":["🐵","🐒","🦍","🦧","🐶","🐕","🦮","🐕‍🦺","🐩","🐺","🦊","🦝","🐱","🐈","🐈‍⬛","🦁","🐯","🐅","🐆","🐴","🐎","🦄","🦓","🦌","🦬","🐮","🐂","🐃","🐄","🐷","🐖","🐗","🐽","🐏","🐑","🐐","🐪","🐫","🦙","🦒","🐘","🦣","🦏","🦛","🐭","🐁","🐀","🐹","🐰","🐇","🐿️","🦫","🦔","🦇","🐻","🐻‍❄️","🐨","🐼","🦥","🦦","🦨","🦘","🦡","🐾","🦃","🐔","🐓","🐣","🐤","🐥","🐦","🐧","🕊️","🦅","🦆","🦢","🦉","🦤","🪶","🦩","🦚","🦜","🐸","🐊","🐢","🦎","🐍","🐲","🐉","🦕","🦖","🐳","🐋","🐬","🦭","🐟","🐠","🐡","🦈","🐙","🐚","🪸","🐌","🦋","🐛","🐜","🐝","🪲","🐞","🦗","🪳","🕷️","🕸️","🦂","🦟","🪰","🪱","🦠","💐","🌸","💮","🪷","🏵️","🌹","🥀","🌺","🌻","🌼","🌷","🌱","🪴","🌲","🌳","🌴","🌵","🌾","🌿","☘️","🍀","🍁","🍂","🍃","🪹","🪺"],"🍇":["🍇","🍈","🍉","🍊","🍋","🍌","🍍","🥭","🍎","🍏","🍐","🍑","🍒","🍓","🫐","🥝","🍅","🫒","🥥","🥑","🍆","🥔","🥕","🌽","🌶️","🫑","🥒","🥬","🥦","🧄","🧅","🍄","🥜","🫘","🌰","🍞","🥐","🥖","🫓","🥨","🥯","🥞","🧇","🧀","🍖","🍗","🥩","🥓","🍔","🍟","🍕","🌭","🥪","🌮","🌯","🫔","🥙","🧆","🥚","🍳","🥘","🍲","🫕","🥣","🥗","🍿","🧈","🧂","🥫","🍱","🍘","🍙","🍚","🍛","🍜","🍝","🍠","🍢","🍣","🍤","🍥","🥮","🍡","🥟","🥠","🥡","🦀","🦞","🦐","🦑","🦪","🍦","🍧","🍨","🍩","🍪","🎂","🍰","🧁","🥧","🍫","🍬","🍭","🍮","🍯","🍼","🥛","☕","🫖","🍵","🍶","🍾","🍷","🍸","🍹","🍺","🍻","🥂","🥃","🫗","🥤","🧋","🧃","🧉","🧊","🥢","🍽️","🍴","🥄","🔪","🫙","🏺"],"🌍":["🌍","🌎","🌏","🌐","🗺️","🗾","🧭","🏔️","⛰️","🌋","🗻","🏕️","🏖️","🏜️","🏝️","🏞️","🏟️","🏛️","🏗️","🧱","🪨","🪵","🛖","🏘️","🏚️","🏠","🏡","🏢","🏣","🏤","🏥","🏦","🏨","🏩","🏪","🏫","🏬","🏭","🏯","🏰","💒","🗼","🗽","⛪","🕌","🛕","🕍","⛩️","🕋","⛲","⛺","🌁","🌃","🏙️","🌄","🌅","🌆","🌇","🌉","♨️","🎠","🛝","🎡","🎢","💈","🎪","🚂","🚃","🚄","🚅","🚆","🚇","🚈","🚉","🚊","🚝","🚞","🚋","🚌","🚍","🚎","🚐","🚑","🚒","🚓","🚔","🚕","🚖","🚗","🚘","🚙","🛻","🚚","🚛","🚜","🏎️","🏍️","🛵","🦽","🦼","🛺","🚲","🛴","🛹","🛼","🚏","🛣️","🛤️","🛢️","⛽","🛞","🚨","🚥","🚦","🛑","🚧","⚓","🛟","⛵","🛶","🚤","🛳️","⛴️","🛥️","🚢","✈️","🛩️","🛫","🛬","🪂","💺","🚁","🚟","🚠","🚡","🛰️","🚀","🛸","🛎️","🧳","⌛","⏳","⌚","⏰","⏱️","⏲️","🕰️","🕛","🕧","🕐","🕜","🕑","🕝","🕒","🕞","🕓","🕟","🕔","🕠","🕕","🕡","🕖","🕢","🕗","🕣","🕘","🕤","🕙","🕥","🕚","🕦","🌑","🌒","🌓","🌔","🌕","🌖","🌗","🌘","🌙","🌚","🌛","🌜","🌡️","☀️","🌝","🌞","🪐","⭐","🌟","🌠","🌌","☁️","⛅","⛈️","🌤️","🌥️","🌦️","🌧️","🌨️","🌩️","🌪️","🌫️","🌬️","🌀","🌈","🌂","☂️","☔","⛱️","⚡","❄️","☃️","⛄","☄️","🔥","💧","🌊"],"🎃":["🎃","🎄","🎆","🎇","🧨","✨","🎈","🎉","🎊","🎋","🎍","🎎","🎏","🎐","🎑","🧧","🎀","🎁","🎗️","🎟️","🎫","🎖️","🏆","🏅","🥇","🥈","🥉","⚽","⚾","🥎","🏀","🏐","🏈","🏉","🎾","🥏","🎳","🏏","🏑","🏒","🥍","🏓","🏸","🥊","🥋","🥅","⛳","⛸️","🎣","🤿","🎽","🎿","🛷","🥌","🎯","🪀","🪁","🎱","🔮","🪄","🧿","🪬","🎮","🕹️","🎰","🎲","🧩","🧸","🪅","🪩","🪆","♠️","♥️","♦️","♣️","♟️","🃏","🀄","🎴","🎭","🖼️","🎨","🧵","🪡","🧶","🪢"],"👓":["👓","🕶️","🥽","🥼","🦺","👔","👕","👖","🧣","🧤","🧥","🧦","👗","👘","🥻","🩱","🩲","🩳","👙","👚","👛","👜","👝","🛍️","🎒","🩴","👞","👟","🥾","🥿","👠","👡","🩰","👢","👑","👒","🎩","🎓","🧢","🪖","⛑️","📿","💄","💍","💎","🔇","🔈","🔉","🔊","📢","📣","📯","🔔","🔕","🎼","🎵","🎶","🎙️","🎚️","🎛️","🎤","🎧","📻","🎷","🪗","🎸","🎹","🎺","🎻","🪕","🥁","🪘","📱","📲","☎️","📞","📟","📠","🔋","🪫","🔌","💻","🖥️","🖨️","⌨️","🖱️","🖲️","💽","💾","💿","📀","🧮","🎥","🎞️","📽️","🎬","📺","📷","📸","📹","📼","🔍","🔎","🕯️","💡","🔦","🏮","🪔","📔","📕","📖","📗","📘","📙","📚","📓","📒","📃","📜","📄","📰","🗞️","📑","🔖","🏷️","💰","🪙","💴","💵","💶","💷","💸","💳","🧾","💹","✉️","📧","📨","📩","📤","📥","📦","📫","📪","📬","📭","📮","🗳️","✏️","✒️","🖋️","🖊️","🖌️","🖍️","📝","💼","📁","📂","🗂️","📅","📆","🗒️","🗓️","📇","📈","📉","📊","📋","📌","📍","📎","🖇️","📏","📐","✂️","🗃️","🗄️","🗑️","🔒","🔓","🔏","🔐","🔑","🗝️","🔨","🪓","⛏️","⚒️","🛠️","🗡️","⚔️","🔫","🪃","🏹","🛡️","🪚","🔧","🪛","🔩","⚙️","🗜️","⚖️","🦯","🔗","⛓️","🪝","🧰","🧲","🪜","⚗️","🧪","🧫","🧬","🔬","🔭","📡","💉","🩸","💊","🩹","🩼","🩺","🩻","🚪","🛗","🪞","🪟","🛏️","🛋️","🪑","🚽","🪠","🚿","🛁","🪤","🪒","🧴","🧷","🧹","🧺","🧻","🪣","🧼","🫧","🪥","🧽","🧯","🛒","🚬","⚰️","🪦","⚱️","🗿","🪧","🪪"],"🏧":["🏧","🚮","🚰","♿","🚹","🚺","🚻","🚼","🚾","🛂","🛃","🛄","🛅","⚠️","🚸","⛔","🚫","🚳","🚭","🚯","🚱","🚷","📵","🔞","☢️","☣️","⬆️","↗️","➡️","↘️","⬇️","↙️","⬅️","↖️","↕️","↔️","↩️","↪️","⤴️","⤵️","🔃","🔄","🔙","🔚","🔛","🔜","🔝","🛐","⚛️","🕉️","✡️","☸️","☯️","✝️","☦️","☪️","☮️","🕎","🔯","♈","♉","♊","♋","♌","♍","♎","♏","♐","♑","♒","♓","⛎","🔀","🔁","🔂","▶️","⏩","⏭️","⏯️","◀️","⏪","⏮️","🔼","⏫","🔽","⏬","⏸️","⏹️","⏺️","⏏️","🎦","🔅","🔆","📶","📳","📴","♀️","♂️","⚧️","✖️","➕","➖","➗","🟰","♾️","‼️","⁉️","❓","❔","❕","❗","〰️","💱","💲","⚕️","♻️","⚜️","🔱","📛","🔰","⭕","✅","☑️","✔️","❌","❎","➰","➿","〽️","✳️","✴️","❇️","©️","®️","™️","#️⃣","*️⃣","0️⃣","1️⃣","2️⃣","3️⃣","4️⃣","5️⃣","6️⃣","7️⃣","8️⃣","9️⃣","🔟","🔠","🔡","🔢","🔣","🔤","🅰️","🆎","🅱️","🆑","🆒","🆓","ℹ️","🆔","Ⓜ️","🆕","🆖","🅾️","🆗","🅿️","🆘","🆙","🆚","🈁","🈂️","🈷️","🈶","🈯","🉐","🈹","🈚","🈲","🉑","🈸","🈴","🈳","㊗️","㊙️","🈺","🈵","🔴","🟠","🟡","🟢","🔵","🟣","🟤","⚫","⚪","🟥","🟧","🟨","🟩","🟦","🟪","🟫","⬛","⬜","◼️","◻️","◾","◽","▪️","▫️","🔶","🔷","🔸","🔹","🔺","🔻","💠","🔘","🔳","🔲"],"🏁":["🏁","🚩","🎌","🏴","🏳️","🏳️‍🌈","🏳️‍⚧️","🏴‍☠️","🇦🇨","🇦🇩","🇦🇪","🇦🇫","🇦🇬","🇦🇮","🇦🇱","🇦🇲","🇦🇴","🇦🇶","🇦🇷","🇦🇸","🇦🇹","🇦🇺","🇦🇼","🇦🇽","🇦🇿","🇧🇦","🇧🇧","🇧🇩","🇧🇪","🇧🇫","🇧🇬","🇧🇭","🇧🇮","🇧🇯","🇧🇱","🇧🇲","🇧🇳","🇧🇴","🇧🇶","🇧🇷","🇧🇸","🇧🇹","🇧🇻","🇧🇼","🇧🇾","🇧🇿","🇨🇦","🇨🇨","🇨🇩","🇨🇫","🇨🇬","🇨🇭","🇨🇮","🇨🇰","🇨🇱","🇨🇲","🇨🇳","🇨🇴","🇨🇵","🇨🇷","🇨🇺","🇨🇻","🇨🇼","🇨🇽","🇨🇾","🇨🇿","🇩🇪","🇩🇬","🇩🇯","🇩🇰","🇩🇲","🇩🇴","🇩🇿","🇪🇦","🇪🇨","🇪🇪","🇪🇬","🇪🇭","🇪🇷","🇪🇸","🇪🇹","🇪🇺","🇫🇮","🇫🇯","🇫🇰","🇫🇲","🇫🇴","🇫🇷","🇬🇦","🇬🇧","🇬🇩","🇬🇪","🇬🇫","🇬🇬","🇬🇭","🇬🇮","🇬🇱","🇬🇲","🇬🇳","🇬🇵","🇬🇶","🇬🇷","🇬🇸","🇬🇹","🇬🇺","🇬🇼","🇬🇾","🇭🇰","🇭🇲","🇭🇳","🇭🇷","🇭🇹","🇭🇺","🇮🇨","🇮🇩","🇮🇪","🇮🇱","🇮🇲","🇮🇳","🇮🇴","🇮🇶","🇮🇷","🇮🇸","🇮🇹","🇯🇪","🇯🇲","🇯🇴","🇯🇵","🇰🇪","🇰🇬","🇰🇭","🇰🇮","🇰🇲","🇰🇳","🇰🇵","🇰🇷","🇰🇼","🇰🇾","🇰🇿","🇱🇦","🇱🇧","🇱🇨","🇱🇮","🇱🇰","🇱🇷","🇱🇸","🇱🇹","🇱🇺","🇱🇻","🇱🇾","🇲🇦","🇲🇨","🇲🇩","🇲🇪","🇲🇫","🇲🇬","🇲🇭","🇲🇰","🇲🇱","🇲🇲","🇲🇳","🇲🇴","🇲🇵","🇲🇶","🇲🇷","🇲🇸","🇲🇹","🇲🇺","🇲🇻","🇲🇼","🇲🇽","🇲🇾","🇲🇿","🇳🇦","🇳🇨","🇳🇪","🇳🇫","🇳🇬","🇳🇮","🇳🇱","🇳🇴","🇳🇵","🇳🇷","🇳🇺","🇳🇿","🇴🇲","🇵🇦","🇵🇪","🇵🇫","🇵🇬","🇵🇭","🇵🇰","🇵🇱","🇵🇲","🇵🇳","🇵🇷","🇵🇸","🇵🇹","🇵🇼","🇵🇾","🇶🇦","🇷🇪","🇷🇴","🇷🇸","🇷🇺","🇷🇼","🇸🇦","🇸🇧","🇸🇨","🇸🇩","🇸🇪","🇸🇬","🇸🇭","🇸🇮","🇸🇯","🇸🇰","🇸🇱","🇸🇲","🇸🇳","🇸🇴","🇸🇷","🇸🇸","🇸🇹","🇸🇻","🇸🇽","🇸🇾","🇸🇿","🇹🇦","🇹🇨","🇹🇩","🇹🇫","🇹🇬","🇹🇭","🇹🇯","🇹🇰","🇹🇱","🇹🇲","🇹🇳","🇹🇴","🇹🇷","🇹🇹","🇹🇻","🇹🇼","🇹🇿","🇺🇦","🇺🇬","🇺🇲","🇺🇳","🇺🇸","🇺🇾","🇺🇿","🇻🇦","🇻🇨","🇻🇪","🇻🇬","🇻🇮","🇻🇳","🇻🇺","🇼🇫","🇼🇸","🇽🇰","🇾🇪","🇾🇹","🇿🇦","🇿🇲","🇿🇼","🏴󠁧󠁢󠁥󠁮󠁧󠁿","🏴󠁧󠁢󠁳󠁣󠁴󠁿","🏴󠁧󠁢󠁷󠁬󠁳󠁿"]}`, "map_provider": "openstreetmap", "map_google_tile_type": "regular", + "map_mapbox_ak": "", "mime_mapping": `{".xlsx":"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",".xltx":"application/vnd.openxmlformats-officedocument.spreadsheetml.template",".potx":"application/vnd.openxmlformats-officedocument.presentationml.template",".ppsx":"application/vnd.openxmlformats-officedocument.presentationml.slideshow",".pptx":"application/vnd.openxmlformats-officedocument.presentationml.presentation",".sldx":"application/vnd.openxmlformats-officedocument.presentationml.slide",".docx":"application/vnd.openxmlformats-officedocument.wordprocessingml.document",".dotx":"application/vnd.openxmlformats-officedocument.wordprocessingml.template",".xlam":"application/vnd.ms-excel.addin.macroEnabled.12",".xlsb":"application/vnd.ms-excel.sheet.binary.macroEnabled.12",".apk":"application/vnd.android.package-archive",".hqx":"application/mac-binhex40",".cpt":"application/mac-compactpro",".doc":"application/msword",".ogg":"application/ogg",".pdf":"application/pdf",".rtf":"text/rtf",".mif":"application/vnd.mif",".xls":"application/vnd.ms-excel",".ppt":"application/vnd.ms-powerpoint",".odc":"application/vnd.oasis.opendocument.chart",".odb":"application/vnd.oasis.opendocument.database",".odf":"application/vnd.oasis.opendocument.formula",".odg":"application/vnd.oasis.opendocument.graphics",".otg":"application/vnd.oasis.opendocument.graphics-template",".odi":"application/vnd.oasis.opendocument.image",".odp":"application/vnd.oasis.opendocument.presentation",".otp":"application/vnd.oasis.opendocument.presentation-template",".ods":"application/vnd.oasis.opendocument.spreadsheet",".ots":"application/vnd.oasis.opendocument.spreadsheet-template",".odt":"application/vnd.oasis.opendocument.text",".odm":"application/vnd.oasis.opendocument.text-master",".ott":"application/vnd.oasis.opendocument.text-template",".oth":"application/vnd.oasis.opendocument.text-web",".sxw":"application/vnd.sun.xml.writer",".stw":"application/vnd.sun.xml.writer.template",".sxc":"application/vnd.sun.xml.calc",".stc":"application/vnd.sun.xml.calc.template",".sxd":"application/vnd.sun.xml.draw",".std":"application/vnd.sun.xml.draw.template",".sxi":"application/vnd.sun.xml.impress",".sti":"application/vnd.sun.xml.impress.template",".sxg":"application/vnd.sun.xml.writer.global",".sxm":"application/vnd.sun.xml.math",".sis":"application/vnd.symbian.install",".wbxml":"application/vnd.wap.wbxml",".wmlc":"application/vnd.wap.wmlc",".wmlsc":"application/vnd.wap.wmlscriptc",".bcpio":"application/x-bcpio",".torrent":"application/x-bittorrent",".bz2":"application/x-bzip2",".vcd":"application/x-cdlink",".pgn":"application/x-chess-pgn",".cpio":"application/x-cpio",".csh":"application/x-csh",".dvi":"application/x-dvi",".spl":"application/x-futuresplash",".gtar":"application/x-gtar",".hdf":"application/x-hdf",".jar":"application/x-java-archive",".jnlp":"application/x-java-jnlp-file",".js":"application/x-javascript",".ksp":"application/x-kspread",".chrt":"application/x-kchart",".kil":"application/x-killustrator",".latex":"application/x-latex",".rpm":"application/x-rpm",".sh":"application/x-sh",".shar":"application/x-shar",".swf":"application/x-shockwave-flash",".sit":"application/x-stuffit",".sv4cpio":"application/x-sv4cpio",".sv4crc":"application/x-sv4crc",".tar":"application/x-tar",".tcl":"application/x-tcl",".tex":"application/x-tex",".man":"application/x-troff-man",".me":"application/x-troff-me",".ms":"application/x-troff-ms",".ustar":"application/x-ustar",".src":"application/x-wais-source",".zip":"application/zip",".m3u":"audio/x-mpegurl",".ra":"audio/x-pn-realaudio",".wav":"audio/x-wav",".wma":"audio/x-ms-wma",".wax":"audio/x-ms-wax",".pdb":"chemical/x-pdb",".xyz":"chemical/x-xyz",".bmp":"image/bmp",".gif":"image/gif",".ief":"image/ief",".png":"image/png",".wbmp":"image/vnd.wap.wbmp",".ras":"image/x-cmu-raster",".pnm":"image/x-portable-anymap",".pbm":"image/x-portable-bitmap",".pgm":"image/x-portable-graymap",".ppm":"image/x-portable-pixmap",".rgb":"image/x-rgb",".xbm":"image/x-xbitmap",".xpm":"image/x-xpixmap",".xwd":"image/x-xwindowdump",".css":"text/css",".rtx":"text/richtext",".tsv":"text/tab-separated-values",".jad":"text/vnd.sun.j2me.app-descriptor",".wml":"text/vnd.wap.wml",".wmls":"text/vnd.wap.wmlscript",".etx":"text/x-setext",".mxu":"video/vnd.mpegurl",".flv":"video/x-flv",".wm":"video/x-ms-wm",".wmv":"video/x-ms-wmv",".wmx":"video/x-ms-wmx",".wvx":"video/x-ms-wvx",".avi":"video/x-msvideo",".movie":"video/x-sgi-movie",".ice":"x-conference/x-cooltalk",".3gp":"video/3gpp",".ai":"application/postscript",".aif":"audio/x-aiff",".aifc":"audio/x-aiff",".aiff":"audio/x-aiff",".asc":"text/plain",".atom":"application/atom+xml",".au":"audio/basic",".bin":"application/octet-stream",".cdf":"application/x-netcdf",".cgm":"image/cgm",".class":"application/octet-stream",".dcr":"application/x-director",".dif":"video/x-dv",".dir":"application/x-director",".djv":"image/vnd.djvu",".djvu":"image/vnd.djvu",".dll":"application/octet-stream",".dmg":"application/octet-stream",".dms":"application/octet-stream",".dtd":"application/xml-dtd",".dv":"video/x-dv",".dxr":"application/x-director",".eps":"application/postscript",".exe":"application/octet-stream",".ez":"application/andrew-inset",".gram":"application/srgs",".grxml":"application/srgs+xml",".gz":"application/x-gzip",".htm":"text/html",".html":"text/html",".ico":"image/x-icon",".ics":"text/calendar",".ifb":"text/calendar",".iges":"model/iges",".igs":"model/iges",".jp2":"image/jp2",".jpe":"image/jpeg",".jpeg":"image/jpeg",".jpg":"image/jpeg",".kar":"audio/midi",".lha":"application/octet-stream",".lzh":"application/octet-stream",".m4a":"audio/mp4a-latm",".m4p":"audio/mp4a-latm",".m4u":"video/vnd.mpegurl",".m4v":"video/x-m4v",".mac":"image/x-macpaint",".mathml":"application/mathml+xml",".mesh":"model/mesh",".mid":"audio/midi",".midi":"audio/midi",".mov":"video/quicktime",".mp2":"audio/mpeg",".mp3":"audio/mpeg",".mp4":"video/mp4",".mpe":"video/mpeg",".mpeg":"video/mpeg",".mpg":"video/mpeg",".mpga":"audio/mpeg",".msh":"model/mesh",".nc":"application/x-netcdf",".oda":"application/oda",".ogv":"video/ogv",".pct":"image/pict",".pic":"image/pict",".pict":"image/pict",".pnt":"image/x-macpaint",".pntg":"image/x-macpaint",".ps":"application/postscript",".qt":"video/quicktime",".qti":"image/x-quicktime",".qtif":"image/x-quicktime",".ram":"audio/x-pn-realaudio",".rdf":"application/rdf+xml",".rm":"application/vnd.rn-realmedia",".roff":"application/x-troff",".sgm":"text/sgml",".sgml":"text/sgml",".silo":"model/mesh",".skd":"application/x-koan",".skm":"application/x-koan",".skp":"application/x-koan",".skt":"application/x-koan",".smi":"application/smil",".smil":"application/smil",".snd":"audio/basic",".so":"application/octet-stream",".svg":"image/svg+xml",".t":"application/x-troff",".texi":"application/x-texinfo",".texinfo":"application/x-texinfo",".tif":"image/tiff",".tiff":"image/tiff",".tr":"application/x-troff",".txt":"text/plain; charset=utf-8",".vrml":"model/vrml",".vxml":"application/voicexml+xml",".webm":"video/webm",".wrl":"model/vrml",".xht":"application/xhtml+xml",".xhtml":"application/xhtml+xml",".xml":"application/xml",".xsl":"application/xml",".xslt":"application/xslt+xml",".xul":"application/vnd.mozilla.xul+xml",".webp":"image/webp",".323":"text/h323",".aab":"application/x-authoware-bin",".aam":"application/x-authoware-map",".aas":"application/x-authoware-seg",".acx":"application/internet-property-stream",".als":"audio/X-Alpha5",".amc":"application/x-mpeg",".ani":"application/octet-stream",".asd":"application/astound",".asf":"video/x-ms-asf",".asn":"application/astound",".asp":"application/x-asap",".asr":"video/x-ms-asf",".asx":"video/x-ms-asf",".avb":"application/octet-stream",".awb":"audio/amr-wb",".axs":"application/olescript",".bas":"text/plain",".bin ":"application/octet-stream",".bld":"application/bld",".bld2":"application/bld2",".bpk":"application/octet-stream",".c":"text/plain",".cal":"image/x-cals",".cat":"application/vnd.ms-pkiseccat",".ccn":"application/x-cnc",".cco":"application/x-cocoa",".cer":"application/x-x509-ca-cert",".cgi":"magnus-internal/cgi",".chat":"application/x-chat",".clp":"application/x-msclip",".cmx":"image/x-cmx",".co":"application/x-cult3d-object",".cod":"image/cis-cod",".conf":"text/plain",".cpp":"text/plain",".crd":"application/x-mscardfile",".crl":"application/pkix-crl",".crt":"application/x-x509-ca-cert",".csm":"chemical/x-csml",".csml":"chemical/x-csml",".cur":"application/octet-stream",".dcm":"x-lml/x-evm",".dcx":"image/x-dcx",".der":"application/x-x509-ca-cert",".dhtml":"text/html",".dot":"application/msword",".dwf":"drawing/x-dwf",".dwg":"application/x-autocad",".dxf":"application/x-autocad",".ebk":"application/x-expandedbook",".emb":"chemical/x-embl-dl-nucleotide",".embl":"chemical/x-embl-dl-nucleotide",".epub":"application/epub+zip",".eri":"image/x-eri",".es":"audio/echospeech",".esl":"audio/echospeech",".etc":"application/x-earthtime",".evm":"x-lml/x-evm",".evy":"application/envoy",".fh4":"image/x-freehand",".fh5":"image/x-freehand",".fhc":"image/x-freehand",".fif":"application/fractals",".flr":"x-world/x-vrml",".fm":"application/x-maker",".fpx":"image/x-fpx",".fvi":"video/isivideo",".gau":"chemical/x-gaussian-input",".gca":"application/x-gca-compressed",".gdb":"x-lml/x-gdb",".gps":"application/x-gps",".h":"text/plain",".hdm":"text/x-hdml",".hdml":"text/x-hdml",".hlp":"application/winhlp",".hta":"application/hta",".htc":"text/x-component",".hts":"text/html",".htt":"text/webviewhtml",".ifm":"image/gif",".ifs":"image/ifs",".iii":"application/x-iphone",".imy":"audio/melody",".ins":"application/x-internet-signup",".ips":"application/x-ipscript",".ipx":"application/x-ipix",".isp":"application/x-internet-signup",".it":"audio/x-mod",".itz":"audio/x-mod",".ivr":"i-world/i-vrml",".j2k":"image/j2k",".jam":"application/x-jam",".java":"text/plain",".jfif":"image/pipeg",".jpz":"image/jpeg",".jwc":"application/jwc",".kjx":"application/x-kjx",".lak":"x-lml/x-lak",".lcc":"application/fastman",".lcl":"application/x-digitalloca",".lcr":"application/x-digitalloca",".lgh":"application/lgh",".lml":"x-lml/x-lml",".lmlpack":"x-lml/x-lmlpack",".log":"text/plain",".lsf":"video/x-la-asf",".lsx":"video/x-la-asf",".m13":"application/x-msmediaview",".m14":"application/x-msmediaview",".m15":"audio/x-mod",".m3url":"audio/x-mpegurl",".m4b":"audio/mp4a-latm",".ma1":"audio/ma1",".ma2":"audio/ma2",".ma3":"audio/ma3",".ma5":"audio/ma5",".map":"magnus-internal/imagemap",".mbd":"application/mbedlet",".mct":"application/x-mascot",".mdb":"application/x-msaccess",".mdz":"audio/x-mod",".mel":"text/x-vmel",".mht":"message/rfc822",".mhtml":"message/rfc822",".mi":"application/x-mif",".mil":"image/x-cals",".mio":"audio/x-mio",".mmf":"application/x-skt-lbs",".mng":"video/x-mng",".mny":"application/x-msmoney",".moc":"application/x-mocha",".mocha":"application/x-mocha",".mod":"audio/x-mod",".mof":"application/x-yumekara",".mol":"chemical/x-mdl-molfile",".mop":"chemical/x-mopac-input",".mpa":"video/mpeg",".mpc":"application/vnd.mpohun.certificate",".mpg4":"video/mp4",".mpn":"application/vnd.mophun.application",".mpp":"application/vnd.ms-project",".mps":"application/x-mapserver",".mpv2":"video/mpeg",".mrl":"text/x-mrml",".mrm":"application/x-mrm",".msg":"application/vnd.ms-outlook",".mts":"application/metastream",".mtx":"application/metastream",".mtz":"application/metastream",".mvb":"application/x-msmediaview",".mzv":"application/metastream",".nar":"application/zip",".nbmp":"image/nbmp",".ndb":"x-lml/x-ndb",".ndwn":"application/ndwn",".nif":"application/x-nif",".nmz":"application/x-scream",".nokia-op-logo":"image/vnd.nok-oplogo-color",".npx":"application/x-netfpx",".nsnd":"audio/nsnd",".nva":"application/x-neva1",".nws":"message/rfc822",".oom":"application/x-AtlasMate-Plugin",".p10":"application/pkcs10",".p12":"application/x-pkcs12",".p7b":"application/x-pkcs7-certificates",".p7c":"application/x-pkcs7-mime",".p7m":"application/x-pkcs7-mime",".p7r":"application/x-pkcs7-certreqresp",".p7s":"application/x-pkcs7-signature",".pac":"audio/x-pac",".pae":"audio/x-epac",".pan":"application/x-pan",".pcx":"image/x-pcx",".pda":"image/x-pda",".pfr":"application/font-tdpfr",".pfx":"application/x-pkcs12",".pko":"application/ynd.ms-pkipko",".pm":"application/x-perl",".pma":"application/x-perfmon",".pmc":"application/x-perfmon",".pmd":"application/x-pmd",".pml":"application/x-perfmon",".pmr":"application/x-perfmon",".pmw":"application/x-perfmon",".pnz":"image/png",".pot,":"application/vnd.ms-powerpoint",".pps":"application/vnd.ms-powerpoint",".pqf":"application/x-cprplayer",".pqi":"application/cprplayer",".prc":"application/x-prc",".prf":"application/pics-rules",".prop":"text/plain",".proxy":"application/x-ns-proxy-autoconfig",".ptlk":"application/listenup",".pub":"application/x-mspublisher",".pvx":"video/x-pv-pvx",".qcp":"audio/vnd.qcelp",".r3t":"text/vnd.rn-realtext3d",".rar":"application/octet-stream",".rc":"text/plain",".rf":"image/vnd.rn-realflash",".rlf":"application/x-richlink",".rmf":"audio/x-rmf",".rmi":"audio/mid",".rmm":"audio/x-pn-realaudio",".rmvb":"audio/x-pn-realaudio",".rnx":"application/vnd.rn-realplayer",".rp":"image/vnd.rn-realpix",".rt":"text/vnd.rn-realtext",".rte":"x-lml/x-gps",".rtg":"application/metastream",".rv":"video/vnd.rn-realvideo",".rwc":"application/x-rogerwilco",".s3m":"audio/x-mod",".s3z":"audio/x-mod",".sca":"application/x-supercard",".scd":"application/x-msschedule",".sct":"text/scriptlet",".sdf":"application/e-score",".sea":"application/x-stuffit",".setpay":"application/set-payment_old-initiation",".setreg":"application/set-registration-initiation",".shtml":"text/html",".shtm":"text/html",".shw":"application/presentations",".si6":"image/si6",".si7":"image/vnd.stiwap.sis",".si9":"image/vnd.lgtwap.sis",".slc":"application/x-salsa",".smd":"audio/x-smd",".smp":"application/studiom",".smz":"audio/x-smd",".spc":"application/x-pkcs7-certificates",".spr":"application/x-sprite",".sprite":"application/x-sprite",".sdp":"application/sdp",".spt":"application/x-spt",".sst":"application/vnd.ms-pkicertstore",".stk":"application/hyperstudio",".stl":"application/vnd.ms-pkistl",".stm":"text/html",".svf":"image/vnd",".svh":"image/svh",".svr":"x-world/x-svr",".swfl":"application/x-shockwave-flash",".tad":"application/octet-stream",".talk":"text/x-speech",".taz":"application/x-tar",".tbp":"application/x-timbuktu",".tbt":"application/x-timbuktu",".tgz":"application/x-compressed",".thm":"application/vnd.eri.thm",".tki":"application/x-tkined",".tkined":"application/x-tkined",".toc":"application/toc",".toy":"image/toy",".trk":"x-lml/x-gps",".trm":"application/x-msterminal",".tsi":"audio/tsplayer",".tsp":"application/dsptype",".ttf":"application/octet-stream",".ttz":"application/t-time",".uls":"text/iuls",".ult":"audio/x-mod",".uu":"application/x-uuencode",".uue":"application/x-uuencode",".vcf":"text/x-vcard",".vdo":"video/vdo",".vib":"audio/vib",".viv":"video/vivo",".vivo":"video/vivo",".vmd":"application/vocaltec-media-desc",".vmf":"application/vocaltec-media-file",".vmi":"application/x-dreamcast-vms-info",".vms":"application/x-dreamcast-vms",".vox":"audio/voxware",".vqe":"audio/x-twinvq-plugin",".vqf":"audio/x-twinvq",".vql":"audio/x-twinvq",".vre":"x-world/x-vream",".vrt":"x-world/x-vrt",".vrw":"x-world/x-vream",".vts":"workbook/formulaone",".wcm":"application/vnd.ms-works",".wdb":"application/vnd.ms-works",".web":"application/vnd.xara",".wi":"image/wavelet",".wis":"application/x-InstallShield",".wks":"application/vnd.ms-works",".wmd":"application/x-ms-wmd",".wmf":"application/x-msmetafile",".wmlscript":"text/vnd.wap.wmlscript",".wmz":"application/x-ms-wmz",".wpng":"image/x-up-wpng",".wps":"application/vnd.ms-works",".wpt":"x-lml/x-gps",".wri":"application/x-mswrite",".wrz":"x-world/x-vrml",".ws":"text/vnd.wap.wmlscript",".wsc":"application/vnd.wap.wmlscriptc",".wv":"video/wavelet",".wxl":"application/x-wxl",".x-gzip":"application/x-gzip",".xaf":"x-world/x-vrml",".xar":"application/vnd.xara",".xdm":"application/x-xdma",".xdma":"application/x-xdma",".xdw":"application/vnd.fujixerox.docuworks",".xhtm":"application/xhtml+xml",".xla":"application/vnd.ms-excel",".xlc":"application/vnd.ms-excel",".xll":"application/x-excel",".xlm":"application/vnd.ms-excel",".xlt":"application/vnd.ms-excel",".xlw":"application/vnd.ms-excel",".xm":"audio/x-mod",".xmz":"audio/x-mod",".xof":"x-world/x-vrml",".xpi":"application/x-xpinstall",".xsit":"text/xml",".yz1":"application/x-yz1",".z":"application/x-compress",".zac":"application/x-zaurus-zac",".json":"application/json"}`, "logto_enabled": "0", "logto_config": `{"direct_sign_in":true,"display_name":"vas.sso"}`, diff --git a/pkg/setting/provider.go b/pkg/setting/provider.go index 017ff2fb..0deb6dac 100644 --- a/pkg/setting/provider.go +++ b/pkg/setting/provider.go @@ -310,6 +310,7 @@ func (s *settingProvider) MapSetting(ctx context.Context) *MapSetting { return &MapSetting{ Provider: MapProvider(s.getString(ctx, "map_provider", "openstreetmap")), GoogleTileType: MapGoogleTileType(s.getString(ctx, "map_google_tile_type", "roadmap")), + MapboxAK: s.getString(ctx, "map_mapbox_ak", ""), } } diff --git a/pkg/setting/types.go b/pkg/setting/types.go index 28344ab7..849ee589 100644 --- a/pkg/setting/types.go +++ b/pkg/setting/types.go @@ -160,6 +160,7 @@ type MapProvider string const ( MapProviderOpenStreetMap = MapProvider("openstreetmap") MapProviderGoogle = MapProvider("google") + MapProviderMapbox = MapProvider("mapbox") ) type MapGoogleTileType string @@ -173,6 +174,7 @@ const ( type MapSetting struct { Provider MapProvider GoogleTileType MapGoogleTileType + MapboxAK string } // Viewer related diff --git a/service/basic/site.go b/service/basic/site.go index 474cd984..5c34a50a 100644 --- a/service/basic/site.go +++ b/service/basic/site.go @@ -47,6 +47,7 @@ type SiteConfig struct { EmojiPreset string `json:"emoji_preset,omitempty"` MapProvider setting.MapProvider `json:"map_provider,omitempty"` GoogleMapTileType setting.MapGoogleTileType `json:"google_map_tile_type,omitempty"` + MapboxAK string `json:"mapbox_ak,omitempty"` FileViewers []types.ViewerGroup `json:"file_viewers,omitempty"` MaxBatchSize int `json:"max_batch_size,omitempty"` ThumbnailWidth int `json:"thumbnail_width,omitempty"` @@ -111,6 +112,7 @@ func (s *GetSettingService) GetSiteConfig(c *gin.Context) (*SiteConfig, error) { Icons: explorerSettings.Icons, MapProvider: mapSettings.Provider, GoogleMapTileType: mapSettings.GoogleTileType, + MapboxAK: mapSettings.MapboxAK, ThumbnailWidth: w, ThumbnailHeight: h, CustomProps: customProps, From 78f7ec8b084541bd45c6479bd85405fde5e8f3cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=85=B8=E6=9F=A0=E6=AA=AC=E7=8C=B9Char?= Date: Sat, 27 Sep 2025 22:04:38 +0800 Subject: [PATCH 56/74] fix: Some containers won't auto restart in the current Docker Compose (#2932) Add "restart: unless-stopped" to the database and redis container. --- docker-compose.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index ff95bc0a..d9f7f73d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ services: depends_on: - postgresql - redis - restart: always + restart: unless-stopped ports: - 5212:5212 - 6888:6888 @@ -26,6 +26,7 @@ services: # backup & consult https://www.postgresql.org/docs/current/pgupgrade.html image: postgres:17 container_name: postgresql + restart: unless-stopped environment: - POSTGRES_USER=cloudreve - POSTGRES_DB=cloudreve @@ -36,6 +37,7 @@ services: redis: image: redis:latest container_name: redis + restart: unless-stopped volumes: - redis_data:/data From e3e08a9b7545aed1401c6c1313f8cdc5af0ef1aa Mon Sep 17 00:00:00 2001 From: Darren Yu Date: Sun, 12 Oct 2025 10:28:40 +0800 Subject: [PATCH 57/74] feat(share): adapt to keep specified path in V3 sharing link (#2958) --- service/share/visit.go | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/service/share/visit.go b/service/share/visit.go index cf07622a..c47f10fb 100644 --- a/service/share/visit.go +++ b/service/share/visit.go @@ -2,6 +2,7 @@ package share import ( "context" + "strings" "github.com/cloudreve/Cloudreve/v4/application/dependency" "github.com/cloudreve/Cloudreve/v4/ent" @@ -25,7 +26,26 @@ type ( ) func (s *ShortLinkRedirectService) RedirectTo(c *gin.Context) string { - return routes.MasterShareLongUrl(s.ID, s.Password).String() + shareLongUrl := routes.MasterShareLongUrl(s.ID, s.Password) + + shortLinkQuery := c.Request.URL.Query() // Query in ShortLink, adapt to Cloudreve V3 + shareLongUrlQuery := shareLongUrl.Query() + + userSpecifiedPath := shortLinkQuery.Get("path") + if userSpecifiedPath != "" { + masterPath := shareLongUrlQuery.Get("path") + masterPath += "/" + strings.TrimPrefix(userSpecifiedPath, "/") + + shareLongUrlQuery.Set("path", masterPath) + } + + shortLinkQuery.Del("path") // 防止用户指定的 Path 就是空字符串 + for k, vals := range shortLinkQuery { + shareLongUrlQuery[k] = append(shareLongUrlQuery[k], vals...) + } + + shareLongUrl.RawQuery = shareLongUrlQuery.Encode() + return shareLongUrl.String() } type ( From e7d6fb25e4647b0d0ad1dc14fd1bd6be2335b050 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Tue, 14 Oct 2025 08:49:45 +0800 Subject: [PATCH 58/74] feat(oss): upgrade to SDK v2 (#2963) --- assets | 2 +- go.mod | 4 +- go.sum | 4 +- pkg/filemanager/driver/oss/media.go | 16 +- pkg/filemanager/driver/oss/oss.go | 266 +++++++++++++++++----------- 5 files changed, 179 insertions(+), 113 deletions(-) diff --git a/assets b/assets index 0bf85fa0..7c07a4ca 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 0bf85fa0abdfa25c4bd20305e2013ac307cfc106 +Subproject commit 7c07a4cab9a2f13ee6f80cbbe21f070f0a0696a9 diff --git a/go.mod b/go.mod index c1834d6f..259da672 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,9 @@ require ( entgo.io/ent v0.13.0 github.com/Masterminds/semver/v3 v3.3.1 github.com/abslant/gzip v0.0.9 - github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible + github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.3.0 github.com/aws/aws-sdk-go v1.31.5 + github.com/bodgit/sevenzip v1.6.0 github.com/cloudflare/cfssl v1.6.1 github.com/dhowden/tag v0.0.0-20230630033851-978a0926ee25 github.com/dsoprea/go-exif/v3 v3.0.1 @@ -70,7 +71,6 @@ require ( github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3 // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect github.com/bodgit/plumbing v1.3.0 // indirect - github.com/bodgit/sevenzip v1.6.0 // indirect github.com/bodgit/windows v1.0.1 // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/bytedance/sonic v1.11.6 // indirect diff --git a/go.sum b/go.sum index ed10902b..72b414e5 100644 --- a/go.sum +++ b/go.sum @@ -100,8 +100,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F7Y6wUM49Lcha2FMXt4UM8g= -github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8= +github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.3.0 h1:wQlqotpyjYPjJz+Noh5bRu7Snmydk8SKC5Z6u1CR20Y= +github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.3.0/go.mod h1:FTzydeQVmR24FI0D6XWUOMKckjXehM/jgMn1xC+DA9M= github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3 h1:8PmGpDEZl9yDpcdEr6Odf23feCxK3LNUNMxjXg41pZQ= github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= diff --git a/pkg/filemanager/driver/oss/media.go b/pkg/filemanager/driver/oss/media.go index 210eec16..11db9483 100644 --- a/pkg/filemanager/driver/oss/media.go +++ b/pkg/filemanager/driver/oss/media.go @@ -5,16 +5,17 @@ import ( "encoding/json" "encoding/xml" "fmt" - "github.com/aliyun/aliyun-oss-go-sdk/oss" - "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver" - "github.com/cloudreve/Cloudreve/v4/pkg/mediameta" - "github.com/cloudreve/Cloudreve/v4/pkg/request" - "github.com/samber/lo" "math" "net/http" "strconv" "strings" "time" + + "github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss" + "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver" + "github.com/cloudreve/Cloudreve/v4/pkg/mediameta" + "github.com/cloudreve/Cloudreve/v4/pkg/request" + "github.com/samber/lo" ) const ( @@ -265,13 +266,14 @@ func (handler *Driver) extractImageMeta(ctx context.Context, path string) ([]dri // extractMediaInfo Sends API calls to OSS IMM service to extract media info. func (handler *Driver) extractMediaInfo(ctx context.Context, path string, category string, forceSign bool) (string, error) { - mediaOption := []oss.Option{oss.Process(category)} mediaInfoExpire := time.Now().Add(mediaInfoTTL) thumbURL, err := handler.signSourceURL( ctx, path, &mediaInfoExpire, - mediaOption, + &oss.GetObjectRequest{ + Process: oss.Ptr(category), + }, forceSign, ) if err != nil { diff --git a/pkg/filemanager/driver/oss/oss.go b/pkg/filemanager/driver/oss/oss.go index 9d468078..363895e7 100644 --- a/pkg/filemanager/driver/oss/oss.go +++ b/pkg/filemanager/driver/oss/oss.go @@ -15,7 +15,8 @@ import ( "strings" "time" - "github.com/aliyun/aliyun-oss-go-sdk/oss" + "github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss" + "github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss/credentials" "github.com/cloudreve/Cloudreve/v4/ent" "github.com/cloudreve/Cloudreve/v4/inventory/types" "github.com/cloudreve/Cloudreve/v4/pkg/boolset" @@ -52,7 +53,6 @@ type Driver struct { policy *ent.StoragePolicy client *oss.Client - bucket *oss.Bucket settings setting.Provider l logging.Logger config conf.ConfigProvider @@ -102,21 +102,27 @@ func New(ctx context.Context, policy *ent.StoragePolicy, settings setting.Provid // CORS 创建跨域策略 func (handler *Driver) CORS() error { - return handler.client.SetBucketCORS(handler.policy.BucketName, []oss.CORSRule{ - { - AllowedOrigin: []string{"*"}, - AllowedMethod: []string{ - "GET", - "POST", - "PUT", - "DELETE", - "HEAD", + _, err := handler.client.PutBucketCors(context.Background(), &oss.PutBucketCorsRequest{ + Bucket: &handler.policy.BucketName, + CORSConfiguration: &oss.CORSConfiguration{ + CORSRules: []oss.CORSRule{ + { + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{ + "GET", + "POST", + "PUT", + "DELETE", + "HEAD", + }, + ExposeHeaders: []string{}, + AllowedHeaders: []string{"*"}, + MaxAgeSeconds: oss.Ptr(int64(3600)), + }, }, - ExposeHeader: []string{}, - AllowedHeader: []string{"*"}, - MaxAgeSeconds: 3600, - }, - }) + }}) + + return err } // InitOSSClient 初始化OSS鉴权客户端 @@ -125,34 +131,28 @@ func (handler *Driver) InitOSSClient(forceUsePublicEndpoint bool) error { return errors.New("empty policy") } - opt := make([]oss.ClientOption, 0) - // 决定是否使用内网 Endpoint endpoint := handler.policy.Server + useCname := false if handler.policy.Settings.ServerSideEndpoint != "" && !forceUsePublicEndpoint { endpoint = handler.policy.Settings.ServerSideEndpoint } else if handler.policy.Settings.UseCname { - opt = append(opt, oss.UseCname(true)) + useCname = true } if !strings.HasPrefix(endpoint, "http://") && !strings.HasPrefix(endpoint, "https://") { endpoint = "https://" + endpoint } + cfg := oss.LoadDefaultConfig(). + WithCredentialsProvider(credentials.NewStaticCredentialsProvider(handler.policy.AccessKey, handler.policy.SecretKey, "")). + WithEndpoint(endpoint). + WithRegion(handler.policy.Settings.Region). + WithUseCName(useCname) + // 初始化客户端 - client, err := oss.New(endpoint, handler.policy.AccessKey, handler.policy.SecretKey, opt...) - if err != nil { - return err - } + client := oss.NewClient(cfg) handler.client = client - - // 初始化存储桶 - bucket, err := client.Bucket(handler.policy.BucketName) - if err != nil { - return err - } - handler.bucket = bucket - return nil } @@ -166,38 +166,40 @@ func (handler *Driver) List(ctx context.Context, base string, onProgress driver. var ( delimiter string - marker string objects []oss.ObjectProperties - commons []string + commons []oss.CommonPrefix ) if !recursive { delimiter = "/" } - for { - subRes, err := handler.bucket.ListObjects(oss.Marker(marker), oss.Prefix(base), - oss.MaxKeys(1000), oss.Delimiter(delimiter)) + p := handler.client.NewListObjectsPaginator(&oss.ListObjectsRequest{ + Bucket: &handler.policy.BucketName, + Prefix: &base, + MaxKeys: 1000, + Delimiter: &delimiter, + }) + + for p.HasNext() { + page, err := p.NextPage(ctx) if err != nil { return nil, err } - objects = append(objects, subRes.Objects...) - commons = append(commons, subRes.CommonPrefixes...) - marker = subRes.NextMarker - if marker == "" { - break - } + + objects = append(objects, page.Contents...) + commons = append(commons, page.CommonPrefixes...) } // 处理列取结果 res := make([]fs.PhysicalObject, 0, len(objects)+len(commons)) // 处理目录 for _, object := range commons { - rel, err := filepath.Rel(base, object) + rel, err := filepath.Rel(base, *object.Prefix) if err != nil { continue } res = append(res, fs.PhysicalObject{ - Name: path.Base(object), + Name: path.Base(*object.Prefix), RelativePath: filepath.ToSlash(rel), Size: 0, IsDir: true, @@ -208,17 +210,17 @@ func (handler *Driver) List(ctx context.Context, base string, onProgress driver. // 处理文件 for _, object := range objects { - rel, err := filepath.Rel(base, object.Key) + rel, err := filepath.Rel(base, *object.Key) if err != nil { continue } res = append(res, fs.PhysicalObject{ - Name: path.Base(object.Key), - Source: object.Key, + Name: path.Base(*object.Key), + Source: *object.Key, RelativePath: filepath.ToSlash(rel), Size: object.Size, IsDir: false, - LastModify: object.LastModified, + LastModify: *object.LastModified, }) } onProgress(len(res)) @@ -245,25 +247,34 @@ func (handler *Driver) Put(ctx context.Context, file *fs.UploadRequest) error { // 是否允许覆盖 overwrite := file.Mode&fs.ModeOverwrite == fs.ModeOverwrite - options := []oss.Option{ - oss.WithContext(ctx), - oss.Expires(time.Now().Add(credentialTTL * time.Second)), - oss.ForbidOverWrite(!overwrite), - oss.ContentType(mimeType), - } + forbidOverwrite := oss.Ptr(strconv.FormatBool(!overwrite)) + exipires := oss.Ptr(time.Now().Add(credentialTTL * time.Second).Format(time.RFC3339)) // 小文件直接上传 if file.Props.Size < MultiPartUploadThreshold { - return handler.bucket.PutObject(file.Props.SavePath, file, options...) + _, err := handler.client.PutObject(ctx, &oss.PutObjectRequest{ + Bucket: &handler.policy.BucketName, + Key: &file.Props.SavePath, + Body: file, + ForbidOverwrite: forbidOverwrite, + ContentType: oss.Ptr(mimeType), + }) + return err } // 超过阈值时使用分片上传 - imur, err := handler.bucket.InitiateMultipartUpload(file.Props.SavePath, options...) + imur, err := handler.client.InitiateMultipartUpload(ctx, &oss.InitiateMultipartUploadRequest{ + Bucket: &handler.policy.BucketName, + Key: &file.Props.SavePath, + ContentType: oss.Ptr(mimeType), + ForbidOverwrite: forbidOverwrite, + Expires: exipires, + }) if err != nil { return fmt.Errorf("failed to initiate multipart upload: %w", err) } - parts := make([]oss.UploadPart, 0) + parts := make([]*oss.UploadPartResult, 0) chunks := chunk.NewChunkGroup(file, handler.chunkSize, &backoff.ConstantBackoff{ Max: handler.settings.ChunkRetryLimit(ctx), @@ -271,7 +282,13 @@ func (handler *Driver) Put(ctx context.Context, file *fs.UploadRequest) error { }, handler.settings.UseChunkBuffer(ctx), handler.l, handler.settings.TempPath(ctx)) uploadFunc := func(current *chunk.ChunkGroup, content io.Reader) error { - part, err := handler.bucket.UploadPart(imur, content, current.Length(), current.Index()+1, oss.WithContext(ctx)) + part, err := handler.client.UploadPart(ctx, &oss.UploadPartRequest{ + Bucket: &handler.policy.BucketName, + Key: &file.Props.SavePath, + UploadId: imur.UploadId, + PartNumber: int32(current.Index() + 1), + Body: content, + }) if err == nil { parts = append(parts, part) } @@ -280,14 +297,27 @@ func (handler *Driver) Put(ctx context.Context, file *fs.UploadRequest) error { for chunks.Next() { if err := chunks.Process(uploadFunc); err != nil { - handler.cancelUpload(imur) + handler.cancelUpload(*imur) return fmt.Errorf("failed to upload chunk #%d: %w", chunks.Index(), err) } } - _, err = handler.bucket.CompleteMultipartUpload(imur, parts, oss.ForbidOverWrite(!overwrite), oss.WithContext(ctx)) + _, err = handler.client.CompleteMultipartUpload(ctx, &oss.CompleteMultipartUploadRequest{ + Bucket: &handler.policy.BucketName, + Key: imur.Key, + UploadId: imur.UploadId, + CompleteMultipartUpload: &oss.CompleteMultipartUpload{ + Parts: lo.Map(parts, func(part *oss.UploadPartResult, i int) oss.UploadPart { + return oss.UploadPart{ + PartNumber: int32(i + 1), + ETag: part.ETag, + } + }), + }, + ForbidOverwrite: oss.Ptr(strconv.FormatBool(!overwrite)), + }) if err != nil { - handler.cancelUpload(imur) + handler.cancelUpload(*imur) } return err @@ -302,7 +332,12 @@ func (handler *Driver) Delete(ctx context.Context, files ...string) ([]string, e for index, group := range groups { handler.l.Debug("Process delete group #%d: %v", index, group) // 删除文件 - delRes, err := handler.bucket.DeleteObjects(group) + delRes, err := handler.client.DeleteMultipleObjects(ctx, &oss.DeleteMultipleObjectsRequest{ + Bucket: &handler.policy.BucketName, + Objects: lo.Map(group, func(v string, i int) oss.DeleteObject { + return oss.DeleteObject{Key: &v} + }), + }) if err != nil { failed = append(failed, group...) lastError = err @@ -310,7 +345,14 @@ func (handler *Driver) Delete(ctx context.Context, files ...string) ([]string, e } // 统计未删除的文件 - failed = append(failed, util.SliceDifference(files, delRes.DeletedObjects)...) + failed = append( + failed, + util.SliceDifference(files, + lo.Map(delRes.DeletedObjects, func(v oss.DeletedInfo, i int) string { + return *v.Key + }), + )..., + ) } if len(failed) > 0 && lastError == nil { @@ -343,12 +385,14 @@ func (handler *Driver) Thumb(ctx context.Context, expire *time.Time, ext string, thumbParam += fmt.Sprintf("/format,%s", enco.Format) } - thumbOption := []oss.Option{oss.Process(thumbParam)} + req := &oss.GetObjectRequest{ + Process: oss.Ptr(thumbParam), + } thumbURL, err := handler.signSourceURL( ctx, e.Source(), expire, - thumbOption, + req, false, ) if err != nil { @@ -370,11 +414,11 @@ func (handler *Driver) Source(ctx context.Context, e fs.Entity, args *driver.Get } // 添加各项设置 - var signOptions = make([]oss.Option, 0, 2) + req := &oss.GetObjectRequest{} if args.IsDownload { encodedFilename := url.PathEscape(args.DisplayName) - signOptions = append(signOptions, oss.ResponseContentDisposition(fmt.Sprintf(`attachment; filename="%s"; filename*=UTF-8''%s`, - encodedFilename, encodedFilename))) + req.ResponseContentDisposition = oss.Ptr(fmt.Sprintf(`attachment; filename="%s"; filename*=UTF-8''%s`, + encodedFilename, encodedFilename)) } if args.Speed > 0 { // Byte 转换为 bit @@ -387,25 +431,33 @@ func (handler *Driver) Source(ctx context.Context, e fs.Entity, args *driver.Get if args.Speed > 838860800 { args.Speed = 838860800 } - signOptions = append(signOptions, oss.TrafficLimitParam(args.Speed)) + req.TrafficLimit = args.Speed } - return handler.signSourceURL(ctx, e.Source(), args.Expire, signOptions, false) + return handler.signSourceURL(ctx, e.Source(), args.Expire, req, false) } -func (handler *Driver) signSourceURL(ctx context.Context, path string, expire *time.Time, options []oss.Option, forceSign bool) (string, error) { - ttl := int64(86400 * 365 * 20) +func (handler *Driver) signSourceURL(ctx context.Context, path string, expire *time.Time, req *oss.GetObjectRequest, forceSign bool) (string, error) { + ttl := time.Duration(24) * time.Hour * 365 * 20 if expire != nil { - ttl = int64(time.Until(*expire).Seconds()) + ttl = time.Until(*expire) + } + + if req == nil { + req = &oss.GetObjectRequest{} } - signedURL, err := handler.bucket.SignURL(path, oss.HTTPGet, ttl, options...) + req.Bucket = &handler.policy.BucketName + req.Key = &path + + // signedURL, err := handler.client.Presign(path, oss.HTTPGet, ttl, options...) + result, err := handler.client.Presign(ctx, req, oss.PresignExpires(ttl)) if err != nil { return "", err } // 将最终生成的签名URL域名换成用户自定义的加速域名(如果有) - finalURL, err := url.Parse(signedURL) + finalURL, err := url.Parse(result.URL) if err != nil { return "", err } @@ -454,34 +506,36 @@ func (handler *Driver) Token(ctx context.Context, uploadSession *fs.UploadSessio } // 初始化分片上传 - options := []oss.Option{ - oss.WithContext(ctx), - oss.Expires(uploadSession.Props.ExpireAt), - oss.ForbidOverWrite(true), - oss.ContentType(mimeType), - } - imur, err := handler.bucket.InitiateMultipartUpload(file.Props.SavePath, options...) + imur, err := handler.client.InitiateMultipartUpload(ctx, &oss.InitiateMultipartUploadRequest{ + Bucket: &handler.policy.BucketName, + Key: &file.Props.SavePath, + ContentType: oss.Ptr(mimeType), + ForbidOverwrite: oss.Ptr(strconv.FormatBool(true)), + Expires: oss.Ptr(uploadSession.Props.ExpireAt.Format(time.RFC3339)), + }) if err != nil { return nil, fmt.Errorf("failed to initialize multipart upload: %w", err) } - uploadSession.UploadID = imur.UploadID + uploadSession.UploadID = *imur.UploadId // 为每个分片签名上传 URL chunks := chunk.NewChunkGroup(file, handler.chunkSize, &backoff.ConstantBackoff{}, false, handler.l, "") urls := make([]string, chunks.Num()) - ttl := int64(time.Until(uploadSession.Props.ExpireAt).Seconds()) + ttl := time.Until(uploadSession.Props.ExpireAt) for chunks.Next() { err := chunks.Process(func(c *chunk.ChunkGroup, chunk io.Reader) error { - signedURL, err := handler.bucket.SignURL(file.Props.SavePath, oss.HTTPPut, - ttl, - oss.AddParam(partNumberParam, strconv.Itoa(c.Index()+1)), - oss.AddParam(uploadIdParam, imur.UploadID), - oss.ContentType("application/octet-stream")) + signedURL, err := handler.client.Presign(ctx, &oss.UploadPartRequest{ + Bucket: &handler.policy.BucketName, + Key: &file.Props.SavePath, + UploadId: imur.UploadId, + PartNumber: int32(c.Index() + 1), + Body: chunk, + }, oss.PresignExpires(ttl)) if err != nil { return err } - urls[c.Index()] = signedURL + urls[c.Index()] = signedURL.URL return nil }) if err != nil { @@ -490,21 +544,22 @@ func (handler *Driver) Token(ctx context.Context, uploadSession *fs.UploadSessio } // 签名完成分片上传的URL - completeURL, err := handler.bucket.SignURL(file.Props.SavePath, oss.HTTPPost, ttl, - oss.ContentType("application/octet-stream"), - oss.AddParam(uploadIdParam, imur.UploadID), - oss.Expires(time.Now().Add(time.Duration(ttl)*time.Second)), - oss.SetHeader(completeAllHeader, "yes"), - oss.ForbidOverWrite(true), - oss.AddParam(callbackParam, callbackPolicyEncoded)) + completeURL, err := handler.client.Presign(ctx, &oss.CompleteMultipartUploadRequest{ + Bucket: &handler.policy.BucketName, + Key: &file.Props.SavePath, + UploadId: imur.UploadId, + CompleteAll: oss.Ptr("yes"), + ForbidOverwrite: oss.Ptr(strconv.FormatBool(true)), + Callback: oss.Ptr(callbackPolicyEncoded), + }, oss.PresignExpires(ttl)) if err != nil { return nil, err } return &fs.UploadCredential{ - UploadID: imur.UploadID, + UploadID: *imur.UploadId, UploadURLs: urls, - CompleteURL: completeURL, + CompleteURL: completeURL.URL, SessionID: uploadSession.Props.UploadSessionID, ChunkSize: handler.chunkSize, }, nil @@ -512,7 +567,12 @@ func (handler *Driver) Token(ctx context.Context, uploadSession *fs.UploadSessio // 取消上传凭证 func (handler *Driver) CancelToken(ctx context.Context, uploadSession *fs.UploadSession) error { - return handler.bucket.AbortMultipartUpload(oss.InitiateMultipartUploadResult{UploadID: uploadSession.UploadID, Key: uploadSession.Props.SavePath}, oss.WithContext(ctx)) + _, err := handler.client.AbortMultipartUpload(ctx, &oss.AbortMultipartUploadRequest{ + Bucket: &handler.policy.BucketName, + Key: &uploadSession.Props.SavePath, + UploadId: &uploadSession.UploadID, + }) + return err } func (handler *Driver) CompleteUpload(ctx context.Context, session *fs.UploadSession) error { @@ -556,7 +616,11 @@ func (handler *Driver) LocalPath(ctx context.Context, path string) string { } func (handler *Driver) cancelUpload(imur oss.InitiateMultipartUploadResult) { - if err := handler.bucket.AbortMultipartUpload(imur); err != nil { + if _, err := handler.client.AbortMultipartUpload(context.Background(), &oss.AbortMultipartUploadRequest{ + Bucket: &handler.policy.BucketName, + Key: imur.Key, + UploadId: imur.UploadId, + }); err != nil { handler.l.Warning("failed to abort multipart upload: %s", err) } } From 213eaa54ddf0005e3bfbccf4acb36d3dc4e985ea Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Tue, 14 Oct 2025 09:29:24 +0800 Subject: [PATCH 59/74] update submodule --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index 7c07a4ca..71e5fbd2 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 7c07a4cab9a2f13ee6f80cbbe21f070f0a0696a9 +Subproject commit 71e5fbd240824ad0b6e8ebe5e47d25704a82d7c4 From 46897e288018e96fa1dc38628216d691e0c66ed5 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Tue, 14 Oct 2025 10:21:43 +0800 Subject: [PATCH 60/74] fix(oss): presigned multipart upload mismatch --- assets | 2 +- pkg/filemanager/driver/oss/oss.go | 33 ++++++++++++++++++++----------- pkg/filemanager/fs/fs.go | 4 ++-- 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/assets b/assets index 71e5fbd2..1c38544e 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 71e5fbd240824ad0b6e8ebe5e47d25704a82d7c4 +Subproject commit 1c38544ef7fd51b4404797cf57355795c28683c1 diff --git a/pkg/filemanager/driver/oss/oss.go b/pkg/filemanager/driver/oss/oss.go index 363895e7..178eca78 100644 --- a/pkg/filemanager/driver/oss/oss.go +++ b/pkg/filemanager/driver/oss/oss.go @@ -65,12 +65,8 @@ type Driver struct { type key int const ( - chunkRetrySleep = time.Duration(5) * time.Second - uploadIdParam = "uploadId" - partNumberParam = "partNumber" - callbackParam = "callback" - completeAllHeader = "x-oss-complete-all" - maxDeleteBatch = 1000 + chunkRetrySleep = time.Duration(5) * time.Second + maxDeleteBatch = 1000 // MultiPartUploadThreshold 服务端使用分片上传的阈值 MultiPartUploadThreshold int64 = 5 * (1 << 30) // 5GB @@ -530,6 +526,11 @@ func (handler *Driver) Token(ctx context.Context, uploadSession *fs.UploadSessio UploadId: imur.UploadId, PartNumber: int32(c.Index() + 1), Body: chunk, + RequestCommon: oss.RequestCommon{ + Headers: map[string]string{ + "Content-Type": "application/octet-stream", + }, + }, }, oss.PresignExpires(ttl)) if err != nil { return err @@ -545,12 +546,19 @@ func (handler *Driver) Token(ctx context.Context, uploadSession *fs.UploadSessio // 签名完成分片上传的URL completeURL, err := handler.client.Presign(ctx, &oss.CompleteMultipartUploadRequest{ - Bucket: &handler.policy.BucketName, - Key: &file.Props.SavePath, - UploadId: imur.UploadId, - CompleteAll: oss.Ptr("yes"), - ForbidOverwrite: oss.Ptr(strconv.FormatBool(true)), - Callback: oss.Ptr(callbackPolicyEncoded), + Bucket: &handler.policy.BucketName, + Key: &file.Props.SavePath, + UploadId: imur.UploadId, + RequestCommon: oss.RequestCommon{ + Parameters: map[string]string{ + "callback": callbackPolicyEncoded, + }, + Headers: map[string]string{ + "Content-Type": "application/octet-stream", + "x-oss-complete-all": "yes", + "x-oss-forbid-overwrite": "true", + }, + }, }, oss.PresignExpires(ttl)) if err != nil { return nil, err @@ -562,6 +570,7 @@ func (handler *Driver) Token(ctx context.Context, uploadSession *fs.UploadSessio CompleteURL: completeURL.URL, SessionID: uploadSession.Props.UploadSessionID, ChunkSize: handler.chunkSize, + Callback: callbackPolicyEncoded, }, nil } diff --git a/pkg/filemanager/fs/fs.go b/pkg/filemanager/fs/fs.go index 20681d65..8536c98c 100644 --- a/pkg/filemanager/fs/fs.go +++ b/pkg/filemanager/fs/fs.go @@ -244,8 +244,8 @@ type ( UploadURLs []string `json:"upload_urls,omitempty"` Credential string `json:"credential,omitempty"` UploadID string `json:"uploadID,omitempty"` - Callback string `json:"callback,omitempty"` // 回调地址 - Uri string `json:"uri,omitempty"` // 存储路径 + Callback string `json:"callback,omitempty"` + Uri string `json:"uri,omitempty"` // 存储路径 AccessKey string `json:"ak,omitempty"` KeyTime string `json:"keyTime,omitempty"` // COS用有效期 CompleteURL string `json:"completeURL,omitempty"` From e29237d593464c195833cbfe0cdef528e008ed2e Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Wed, 15 Oct 2025 10:28:31 +0800 Subject: [PATCH 61/74] fix(webdav): error code for missing parent in mkcol should be `409` instead of `404` (#2953) --- pkg/webdav/webdav.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pkg/webdav/webdav.go b/pkg/webdav/webdav.go index fd630736..b8c3707d 100644 --- a/pkg/webdav/webdav.go +++ b/pkg/webdav/webdav.go @@ -212,7 +212,13 @@ func handleMkcol(c *gin.Context, user *ent.User, fm manager.FileManager) (status _, err = fm.Create(ctx, uri, types.FileTypeFolder, dbfs.WithNoChainedCreation(), dbfs.WithErrorOnConflict()) if err != nil { - return purposeStatusCodeFromError(err), err + code := purposeStatusCodeFromError(err) + if code == http.StatusNotFound { + // When the MKCOL operation creates a new collection resource, all ancestors MUST already exist, + // or the method MUST fail with a 409 (Conflict) status code. + return http.StatusConflict, err + } + return code, err } return http.StatusCreated, nil From 21cdafb2af3f4a3401d09218a81943cc86380d8f Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Thu, 16 Oct 2025 07:46:22 +0800 Subject: [PATCH 62/74] fix(oss): traffic limit should be in query instead of headers (#2977) --- pkg/filemanager/driver/oss/oss.go | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/pkg/filemanager/driver/oss/oss.go b/pkg/filemanager/driver/oss/oss.go index 178eca78..b2b7ae91 100644 --- a/pkg/filemanager/driver/oss/oss.go +++ b/pkg/filemanager/driver/oss/oss.go @@ -65,8 +65,11 @@ type Driver struct { type key int const ( - chunkRetrySleep = time.Duration(5) * time.Second - maxDeleteBatch = 1000 + chunkRetrySleep = time.Duration(5) * time.Second + maxDeleteBatch = 1000 + completeAllHeader = "x-oss-complete-all" + forbidOverwriteHeader = "x-oss-forbid-overwrite" + trafficLimitHeader = "x-oss-traffic-limit" // MultiPartUploadThreshold 服务端使用分片上传的阈值 MultiPartUploadThreshold int64 = 5 * (1 << 30) // 5GB @@ -427,7 +430,9 @@ func (handler *Driver) Source(ctx context.Context, e fs.Entity, args *driver.Get if args.Speed > 838860800 { args.Speed = 838860800 } - req.TrafficLimit = args.Speed + req.Parameters = map[string]string{ + trafficLimitHeader: strconv.FormatInt(args.Speed, 10), + } } return handler.signSourceURL(ctx, e.Source(), args.Expire, req, false) @@ -464,7 +469,7 @@ func (handler *Driver) signSourceURL(ctx context.Context, path string, expire *t query.Del("OSSAccessKeyId") query.Del("Signature") query.Del("response-content-disposition") - query.Del("x-oss-traffic-limit") + query.Del(trafficLimitHeader) finalURL.RawQuery = query.Encode() } return finalURL.String(), nil @@ -554,9 +559,9 @@ func (handler *Driver) Token(ctx context.Context, uploadSession *fs.UploadSessio "callback": callbackPolicyEncoded, }, Headers: map[string]string{ - "Content-Type": "application/octet-stream", - "x-oss-complete-all": "yes", - "x-oss-forbid-overwrite": "true", + "Content-Type": "application/octet-stream", + completeAllHeader: "yes", + forbidOverwriteHeader: "true", }, }, }, oss.PresignExpires(ttl)) From 6bd30a8af780068789e2238890e2478566022b9b Mon Sep 17 00:00:00 2001 From: Darren Yu Date: Thu, 16 Oct 2025 11:49:21 +0800 Subject: [PATCH 63/74] fix(oss): change default expire ttl and sign param to adapt SDK v2 (#2979) * fix(oss): change default expire ttl and sign param to adapt SDK v2 * fix(oss): add expire ttl limit --- pkg/filemanager/driver/oss/oss.go | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/pkg/filemanager/driver/oss/oss.go b/pkg/filemanager/driver/oss/oss.go index b2b7ae91..b6c17471 100644 --- a/pkg/filemanager/driver/oss/oss.go +++ b/pkg/filemanager/driver/oss/oss.go @@ -67,6 +67,7 @@ type key int const ( chunkRetrySleep = time.Duration(5) * time.Second maxDeleteBatch = 1000 + maxSignTTL = time.Duration(24) * time.Hour * 7 completeAllHeader = "x-oss-complete-all" forbidOverwriteHeader = "x-oss-forbid-overwrite" trafficLimitHeader = "x-oss-traffic-limit" @@ -439,9 +440,13 @@ func (handler *Driver) Source(ctx context.Context, e fs.Entity, args *driver.Get } func (handler *Driver) signSourceURL(ctx context.Context, path string, expire *time.Time, req *oss.GetObjectRequest, forceSign bool) (string, error) { - ttl := time.Duration(24) * time.Hour * 365 * 20 + // V4 Sign 最大过期时间为7天 + ttl := maxSignTTL if expire != nil { ttl = time.Until(*expire) + if ttl > maxSignTTL { + ttl = maxSignTTL + } } if req == nil { @@ -466,10 +471,12 @@ func (handler *Driver) signSourceURL(ctx context.Context, path string, expire *t // 公有空间替换掉Key及不支持的头 if !handler.policy.IsPrivate && !forceSign { query := finalURL.Query() - query.Del("OSSAccessKeyId") - query.Del("Signature") + query.Del("x-oss-credential") + query.Del("x-oss-date") + query.Del("x-oss-expires") + query.Del("x-oss-signature") + query.Del("x-oss-signature-version") query.Del("response-content-disposition") - query.Del(trafficLimitHeader) finalURL.RawQuery = query.Encode() } return finalURL.String(), nil From 16b02b1fb335b36c42716492c828f8969724ae4a Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Tue, 21 Oct 2025 14:53:52 +0800 Subject: [PATCH 64/74] feat: file blob encryption --- application/dependency/dependency.go | 101 +++-- assets | 2 +- ent/entity.go | 18 +- ent/entity/entity.go | 6 +- ent/entity/where.go | 12 +- ent/entity_create.go | 66 ++-- ent/entity_update.go | 40 +- ent/internal/schema.go | 2 +- ent/mutation.go | 78 ++-- ent/schema/entity.go | 5 +- inventory/file.go | 22 +- inventory/setting.go | 10 + inventory/types/types.go | 20 +- pkg/filemanager/driver/local/entity.go | 13 +- pkg/filemanager/encrypt/aes256ctr.go | 360 ++++++++++++++++++ pkg/filemanager/encrypt/encrypt.go | 97 +++++ pkg/filemanager/encrypt/masterkey.go | 30 ++ pkg/filemanager/fs/dbfs/dbfs.go | 21 +- pkg/filemanager/fs/dbfs/manage.go | 6 +- pkg/filemanager/fs/dbfs/options.go | 9 + pkg/filemanager/fs/dbfs/upload.go | 37 +- pkg/filemanager/fs/fs.go | 74 ++-- pkg/filemanager/manager/entity.go | 8 +- .../manager/entitysource/entitysource.go | 187 ++++++--- pkg/filemanager/manager/manager.go | 3 +- pkg/filemanager/manager/recycle.go | 2 +- pkg/filemanager/manager/upload.go | 41 +- pkg/filemanager/workflows/archive.go | 25 +- pkg/filemanager/workflows/extract.go | 8 +- pkg/mediameta/ffprobe.go | 2 +- pkg/setting/adapters.go | 5 +- pkg/setting/provider.go | 13 +- pkg/thumb/ffmpeg.go | 12 +- pkg/thumb/libreoffice.go | 2 +- pkg/thumb/vips.go | 2 +- service/admin/file.go | 4 +- service/explorer/response.go | 68 ++-- service/explorer/upload.go | 25 +- 38 files changed, 1120 insertions(+), 316 deletions(-) create mode 100644 pkg/filemanager/encrypt/aes256ctr.go create mode 100644 pkg/filemanager/encrypt/encrypt.go create mode 100644 pkg/filemanager/encrypt/masterkey.go diff --git a/application/dependency/dependency.go b/application/dependency/dependency.go index 308ea10a..445638cc 100644 --- a/application/dependency/dependency.go +++ b/application/dependency/dependency.go @@ -17,6 +17,7 @@ import ( "github.com/cloudreve/Cloudreve/v4/pkg/conf" "github.com/cloudreve/Cloudreve/v4/pkg/credmanager" "github.com/cloudreve/Cloudreve/v4/pkg/email" + "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/encrypt" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs/mime" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/lock" "github.com/cloudreve/Cloudreve/v4/pkg/hashid" @@ -129,50 +130,55 @@ type Dep interface { WebAuthn(ctx context.Context) (*webauthn.WebAuthn, error) // UAParser Get a singleton uaparser.Parser instance for user agent parsing. UAParser() *uaparser.Parser + // MasterEncryptKeyVault Get a singleton encrypt.MasterEncryptKeyVault instance for master encrypt key vault. + MasterEncryptKeyVault() encrypt.MasterEncryptKeyVault + // EncryptorFactory Get a new encrypt.CryptorFactory instance. + EncryptorFactory() encrypt.CryptorFactory } type dependency struct { - configProvider conf.ConfigProvider - logger logging.Logger - statics iofs.FS - serverStaticFS static.ServeFileSystem - dbClient *ent.Client - rawEntClient *ent.Client - kv cache.Driver - navigatorStateKv cache.Driver - settingClient inventory.SettingClient - fileClient inventory.FileClient - shareClient inventory.ShareClient - settingProvider setting.Provider - userClient inventory.UserClient - groupClient inventory.GroupClient - storagePolicyClient inventory.StoragePolicyClient - taskClient inventory.TaskClient - nodeClient inventory.NodeClient - davAccountClient inventory.DavAccountClient - directLinkClient inventory.DirectLinkClient - emailClient email.Driver - generalAuth auth.Auth - hashidEncoder hashid.Encoder - tokenAuth auth.TokenAuth - lockSystem lock.LockSystem - requestClient request.Client - ioIntenseQueue queue.Queue - thumbQueue queue.Queue - mediaMetaQueue queue.Queue - entityRecycleQueue queue.Queue - slaveQueue queue.Queue - remoteDownloadQueue queue.Queue - ioIntenseQueueTask queue.Task - mediaMeta mediameta.Extractor - thumbPipeline thumb.Generator - mimeDetector mime.MimeDetector - credManager credmanager.CredManager - nodePool cluster.NodePool - taskRegistry queue.TaskRegistry - webauthn *webauthn.WebAuthn - parser *uaparser.Parser - cron *cron.Cron + configProvider conf.ConfigProvider + logger logging.Logger + statics iofs.FS + serverStaticFS static.ServeFileSystem + dbClient *ent.Client + rawEntClient *ent.Client + kv cache.Driver + navigatorStateKv cache.Driver + settingClient inventory.SettingClient + fileClient inventory.FileClient + shareClient inventory.ShareClient + settingProvider setting.Provider + userClient inventory.UserClient + groupClient inventory.GroupClient + storagePolicyClient inventory.StoragePolicyClient + taskClient inventory.TaskClient + nodeClient inventory.NodeClient + davAccountClient inventory.DavAccountClient + directLinkClient inventory.DirectLinkClient + emailClient email.Driver + generalAuth auth.Auth + hashidEncoder hashid.Encoder + tokenAuth auth.TokenAuth + lockSystem lock.LockSystem + requestClient request.Client + ioIntenseQueue queue.Queue + thumbQueue queue.Queue + mediaMetaQueue queue.Queue + entityRecycleQueue queue.Queue + slaveQueue queue.Queue + remoteDownloadQueue queue.Queue + ioIntenseQueueTask queue.Task + mediaMeta mediameta.Extractor + thumbPipeline thumb.Generator + mimeDetector mime.MimeDetector + credManager credmanager.CredManager + nodePool cluster.NodePool + taskRegistry queue.TaskRegistry + webauthn *webauthn.WebAuthn + parser *uaparser.Parser + cron *cron.Cron + masterEncryptKeyVault encrypt.MasterEncryptKeyVault configPath string isPro bool @@ -206,6 +212,19 @@ func (d *dependency) RequestClient(opts ...request.Option) request.Client { return request.NewClient(d.ConfigProvider(), opts...) } +func (d *dependency) MasterEncryptKeyVault() encrypt.MasterEncryptKeyVault { + if d.masterEncryptKeyVault != nil { + return d.masterEncryptKeyVault + } + + d.masterEncryptKeyVault = encrypt.NewMasterEncryptKeyVault(d.SettingProvider()) + return d.masterEncryptKeyVault +} + +func (d *dependency) EncryptorFactory() encrypt.CryptorFactory { + return encrypt.NewCryptorFactory(d.MasterEncryptKeyVault()) +} + func (d *dependency) WebAuthn(ctx context.Context) (*webauthn.WebAuthn, error) { if d.webauthn != nil { return d.webauthn, nil diff --git a/assets b/assets index 1c38544e..1c9dd8d9 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 1c38544ef7fd51b4404797cf57355795c28683c1 +Subproject commit 1c9dd8d9adbb6842b404ecd908a625ce519b754f diff --git a/ent/entity.go b/ent/entity.go index 6e57c30b..7434f74d 100644 --- a/ent/entity.go +++ b/ent/entity.go @@ -42,8 +42,8 @@ type Entity struct { CreatedBy int `json:"created_by,omitempty"` // UploadSessionID holds the value of the "upload_session_id" field. UploadSessionID *uuid.UUID `json:"upload_session_id,omitempty"` - // RecycleOptions holds the value of the "recycle_options" field. - RecycleOptions *types.EntityRecycleOption `json:"recycle_options,omitempty"` + // Props holds the value of the "props" field. + Props *types.EntityProps `json:"props,omitempty"` // Edges holds the relations/edges for other nodes in the graph. // The values are being populated by the EntityQuery when eager-loading is set. Edges EntityEdges `json:"edges"` @@ -105,7 +105,7 @@ func (*Entity) scanValues(columns []string) ([]any, error) { switch columns[i] { case entity.FieldUploadSessionID: values[i] = &sql.NullScanner{S: new(uuid.UUID)} - case entity.FieldRecycleOptions: + case entity.FieldProps: values[i] = new([]byte) case entity.FieldID, entity.FieldType, entity.FieldSize, entity.FieldReferenceCount, entity.FieldStoragePolicyEntities, entity.FieldCreatedBy: values[i] = new(sql.NullInt64) @@ -196,12 +196,12 @@ func (e *Entity) assignValues(columns []string, values []any) error { e.UploadSessionID = new(uuid.UUID) *e.UploadSessionID = *value.S.(*uuid.UUID) } - case entity.FieldRecycleOptions: + case entity.FieldProps: if value, ok := values[i].(*[]byte); !ok { - return fmt.Errorf("unexpected type %T for field recycle_options", values[i]) + return fmt.Errorf("unexpected type %T for field props", values[i]) } else if value != nil && len(*value) > 0 { - if err := json.Unmarshal(*value, &e.RecycleOptions); err != nil { - return fmt.Errorf("unmarshal field recycle_options: %w", err) + if err := json.Unmarshal(*value, &e.Props); err != nil { + return fmt.Errorf("unmarshal field props: %w", err) } } default: @@ -289,8 +289,8 @@ func (e *Entity) String() string { builder.WriteString(fmt.Sprintf("%v", *v)) } builder.WriteString(", ") - builder.WriteString("recycle_options=") - builder.WriteString(fmt.Sprintf("%v", e.RecycleOptions)) + builder.WriteString("props=") + builder.WriteString(fmt.Sprintf("%v", e.Props)) builder.WriteByte(')') return builder.String() } diff --git a/ent/entity/entity.go b/ent/entity/entity.go index ed8e402d..94c01762 100644 --- a/ent/entity/entity.go +++ b/ent/entity/entity.go @@ -35,8 +35,8 @@ const ( FieldCreatedBy = "created_by" // FieldUploadSessionID holds the string denoting the upload_session_id field in the database. FieldUploadSessionID = "upload_session_id" - // FieldRecycleOptions holds the string denoting the recycle_options field in the database. - FieldRecycleOptions = "recycle_options" + // FieldProps holds the string denoting the props field in the database. + FieldProps = "recycle_options" // EdgeFile holds the string denoting the file edge name in mutations. EdgeFile = "file" // EdgeUser holds the string denoting the user edge name in mutations. @@ -79,7 +79,7 @@ var Columns = []string{ FieldStoragePolicyEntities, FieldCreatedBy, FieldUploadSessionID, - FieldRecycleOptions, + FieldProps, } var ( diff --git a/ent/entity/where.go b/ent/entity/where.go index 90fbec37..de7ffe2f 100644 --- a/ent/entity/where.go +++ b/ent/entity/where.go @@ -521,14 +521,14 @@ func UploadSessionIDNotNil() predicate.Entity { return predicate.Entity(sql.FieldNotNull(FieldUploadSessionID)) } -// RecycleOptionsIsNil applies the IsNil predicate on the "recycle_options" field. -func RecycleOptionsIsNil() predicate.Entity { - return predicate.Entity(sql.FieldIsNull(FieldRecycleOptions)) +// PropsIsNil applies the IsNil predicate on the "props" field. +func PropsIsNil() predicate.Entity { + return predicate.Entity(sql.FieldIsNull(FieldProps)) } -// RecycleOptionsNotNil applies the NotNil predicate on the "recycle_options" field. -func RecycleOptionsNotNil() predicate.Entity { - return predicate.Entity(sql.FieldNotNull(FieldRecycleOptions)) +// PropsNotNil applies the NotNil predicate on the "props" field. +func PropsNotNil() predicate.Entity { + return predicate.Entity(sql.FieldNotNull(FieldProps)) } // HasFile applies the HasEdge predicate on the "file" edge. diff --git a/ent/entity_create.go b/ent/entity_create.go index 48768bdd..5679d344 100644 --- a/ent/entity_create.go +++ b/ent/entity_create.go @@ -135,9 +135,9 @@ func (ec *EntityCreate) SetNillableUploadSessionID(u *uuid.UUID) *EntityCreate { return ec } -// SetRecycleOptions sets the "recycle_options" field. -func (ec *EntityCreate) SetRecycleOptions(tro *types.EntityRecycleOption) *EntityCreate { - ec.mutation.SetRecycleOptions(tro) +// SetProps sets the "props" field. +func (ec *EntityCreate) SetProps(tp *types.EntityProps) *EntityCreate { + ec.mutation.SetProps(tp) return ec } @@ -336,9 +336,9 @@ func (ec *EntityCreate) createSpec() (*Entity, *sqlgraph.CreateSpec) { _spec.SetField(entity.FieldUploadSessionID, field.TypeUUID, value) _node.UploadSessionID = &value } - if value, ok := ec.mutation.RecycleOptions(); ok { - _spec.SetField(entity.FieldRecycleOptions, field.TypeJSON, value) - _node.RecycleOptions = value + if value, ok := ec.mutation.Props(); ok { + _spec.SetField(entity.FieldProps, field.TypeJSON, value) + _node.Props = value } if nodes := ec.mutation.FileIDs(); len(nodes) > 0 { edge := &sqlgraph.EdgeSpec{ @@ -586,21 +586,21 @@ func (u *EntityUpsert) ClearUploadSessionID() *EntityUpsert { return u } -// SetRecycleOptions sets the "recycle_options" field. -func (u *EntityUpsert) SetRecycleOptions(v *types.EntityRecycleOption) *EntityUpsert { - u.Set(entity.FieldRecycleOptions, v) +// SetProps sets the "props" field. +func (u *EntityUpsert) SetProps(v *types.EntityProps) *EntityUpsert { + u.Set(entity.FieldProps, v) return u } -// UpdateRecycleOptions sets the "recycle_options" field to the value that was provided on create. -func (u *EntityUpsert) UpdateRecycleOptions() *EntityUpsert { - u.SetExcluded(entity.FieldRecycleOptions) +// UpdateProps sets the "props" field to the value that was provided on create. +func (u *EntityUpsert) UpdateProps() *EntityUpsert { + u.SetExcluded(entity.FieldProps) return u } -// ClearRecycleOptions clears the value of the "recycle_options" field. -func (u *EntityUpsert) ClearRecycleOptions() *EntityUpsert { - u.SetNull(entity.FieldRecycleOptions) +// ClearProps clears the value of the "props" field. +func (u *EntityUpsert) ClearProps() *EntityUpsert { + u.SetNull(entity.FieldProps) return u } @@ -817,24 +817,24 @@ func (u *EntityUpsertOne) ClearUploadSessionID() *EntityUpsertOne { }) } -// SetRecycleOptions sets the "recycle_options" field. -func (u *EntityUpsertOne) SetRecycleOptions(v *types.EntityRecycleOption) *EntityUpsertOne { +// SetProps sets the "props" field. +func (u *EntityUpsertOne) SetProps(v *types.EntityProps) *EntityUpsertOne { return u.Update(func(s *EntityUpsert) { - s.SetRecycleOptions(v) + s.SetProps(v) }) } -// UpdateRecycleOptions sets the "recycle_options" field to the value that was provided on create. -func (u *EntityUpsertOne) UpdateRecycleOptions() *EntityUpsertOne { +// UpdateProps sets the "props" field to the value that was provided on create. +func (u *EntityUpsertOne) UpdateProps() *EntityUpsertOne { return u.Update(func(s *EntityUpsert) { - s.UpdateRecycleOptions() + s.UpdateProps() }) } -// ClearRecycleOptions clears the value of the "recycle_options" field. -func (u *EntityUpsertOne) ClearRecycleOptions() *EntityUpsertOne { +// ClearProps clears the value of the "props" field. +func (u *EntityUpsertOne) ClearProps() *EntityUpsertOne { return u.Update(func(s *EntityUpsert) { - s.ClearRecycleOptions() + s.ClearProps() }) } @@ -1222,24 +1222,24 @@ func (u *EntityUpsertBulk) ClearUploadSessionID() *EntityUpsertBulk { }) } -// SetRecycleOptions sets the "recycle_options" field. -func (u *EntityUpsertBulk) SetRecycleOptions(v *types.EntityRecycleOption) *EntityUpsertBulk { +// SetProps sets the "props" field. +func (u *EntityUpsertBulk) SetProps(v *types.EntityProps) *EntityUpsertBulk { return u.Update(func(s *EntityUpsert) { - s.SetRecycleOptions(v) + s.SetProps(v) }) } -// UpdateRecycleOptions sets the "recycle_options" field to the value that was provided on create. -func (u *EntityUpsertBulk) UpdateRecycleOptions() *EntityUpsertBulk { +// UpdateProps sets the "props" field to the value that was provided on create. +func (u *EntityUpsertBulk) UpdateProps() *EntityUpsertBulk { return u.Update(func(s *EntityUpsert) { - s.UpdateRecycleOptions() + s.UpdateProps() }) } -// ClearRecycleOptions clears the value of the "recycle_options" field. -func (u *EntityUpsertBulk) ClearRecycleOptions() *EntityUpsertBulk { +// ClearProps clears the value of the "props" field. +func (u *EntityUpsertBulk) ClearProps() *EntityUpsertBulk { return u.Update(func(s *EntityUpsert) { - s.ClearRecycleOptions() + s.ClearProps() }) } diff --git a/ent/entity_update.go b/ent/entity_update.go index dfd9c66a..fe04ac25 100644 --- a/ent/entity_update.go +++ b/ent/entity_update.go @@ -190,15 +190,15 @@ func (eu *EntityUpdate) ClearUploadSessionID() *EntityUpdate { return eu } -// SetRecycleOptions sets the "recycle_options" field. -func (eu *EntityUpdate) SetRecycleOptions(tro *types.EntityRecycleOption) *EntityUpdate { - eu.mutation.SetRecycleOptions(tro) +// SetProps sets the "props" field. +func (eu *EntityUpdate) SetProps(tp *types.EntityProps) *EntityUpdate { + eu.mutation.SetProps(tp) return eu } -// ClearRecycleOptions clears the value of the "recycle_options" field. -func (eu *EntityUpdate) ClearRecycleOptions() *EntityUpdate { - eu.mutation.ClearRecycleOptions() +// ClearProps clears the value of the "props" field. +func (eu *EntityUpdate) ClearProps() *EntityUpdate { + eu.mutation.ClearProps() return eu } @@ -383,11 +383,11 @@ func (eu *EntityUpdate) sqlSave(ctx context.Context) (n int, err error) { if eu.mutation.UploadSessionIDCleared() { _spec.ClearField(entity.FieldUploadSessionID, field.TypeUUID) } - if value, ok := eu.mutation.RecycleOptions(); ok { - _spec.SetField(entity.FieldRecycleOptions, field.TypeJSON, value) + if value, ok := eu.mutation.Props(); ok { + _spec.SetField(entity.FieldProps, field.TypeJSON, value) } - if eu.mutation.RecycleOptionsCleared() { - _spec.ClearField(entity.FieldRecycleOptions, field.TypeJSON) + if eu.mutation.PropsCleared() { + _spec.ClearField(entity.FieldProps, field.TypeJSON) } if eu.mutation.FileCleared() { edge := &sqlgraph.EdgeSpec{ @@ -669,15 +669,15 @@ func (euo *EntityUpdateOne) ClearUploadSessionID() *EntityUpdateOne { return euo } -// SetRecycleOptions sets the "recycle_options" field. -func (euo *EntityUpdateOne) SetRecycleOptions(tro *types.EntityRecycleOption) *EntityUpdateOne { - euo.mutation.SetRecycleOptions(tro) +// SetProps sets the "props" field. +func (euo *EntityUpdateOne) SetProps(tp *types.EntityProps) *EntityUpdateOne { + euo.mutation.SetProps(tp) return euo } -// ClearRecycleOptions clears the value of the "recycle_options" field. -func (euo *EntityUpdateOne) ClearRecycleOptions() *EntityUpdateOne { - euo.mutation.ClearRecycleOptions() +// ClearProps clears the value of the "props" field. +func (euo *EntityUpdateOne) ClearProps() *EntityUpdateOne { + euo.mutation.ClearProps() return euo } @@ -892,11 +892,11 @@ func (euo *EntityUpdateOne) sqlSave(ctx context.Context) (_node *Entity, err err if euo.mutation.UploadSessionIDCleared() { _spec.ClearField(entity.FieldUploadSessionID, field.TypeUUID) } - if value, ok := euo.mutation.RecycleOptions(); ok { - _spec.SetField(entity.FieldRecycleOptions, field.TypeJSON, value) + if value, ok := euo.mutation.Props(); ok { + _spec.SetField(entity.FieldProps, field.TypeJSON, value) } - if euo.mutation.RecycleOptionsCleared() { - _spec.ClearField(entity.FieldRecycleOptions, field.TypeJSON) + if euo.mutation.PropsCleared() { + _spec.ClearField(entity.FieldProps, field.TypeJSON) } if euo.mutation.FileCleared() { edge := &sqlgraph.EdgeSpec{ diff --git a/ent/internal/schema.go b/ent/internal/schema.go index 6380a862..1c9c1d99 100644 --- a/ent/internal/schema.go +++ b/ent/internal/schema.go @@ -6,4 +6,4 @@ // Package internal holds a loadable version of the latest schema. package internal -const Schema = "{\"Schema\":\"github.com/cloudreve/Cloudreve/v4/ent/schema\",\"Package\":\"github.com/cloudreve/Cloudreve/v4/ent\",\"Schemas\":[{\"name\":\"DavAccount\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"owner\",\"type\":\"User\",\"field\":\"owner_id\",\"ref_name\":\"dav_accounts\",\"unique\":true,\"inverse\":true,\"required\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"uri\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"password\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0},\"sensitive\":true},{\"name\":\"options\",\"type\":{\"Type\":5,\"Ident\":\"*boolset.BooleanSet\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"PkgName\":\"boolset\",\"Nillable\":true,\"RType\":{\"Name\":\"BooleanSet\",\"Ident\":\"boolset.BooleanSet\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"Methods\":{\"Enabled\":{\"In\":[{\"Name\":\"int\",\"Ident\":\"int\",\"Kind\":2,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"bool\",\"Ident\":\"bool\",\"Kind\":1,\"PkgPath\":\"\",\"Methods\":null}]},\"MarshalBinary\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Scan\":{\"In\":[{\"Name\":\"\",\"Ident\":\"interface {}\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"String\":{\"In\":[],\"Out\":[{\"Name\":\"string\",\"Ident\":\"string\",\"Kind\":24,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalBinary\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Value\":{\"In\":[],\"Out\":[{\"Name\":\"Value\",\"Ident\":\"driver.Value\",\"Kind\":20,\"PkgPath\":\"database/sql/driver\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"props\",\"type\":{\"Type\":3,\"Ident\":\"*types.DavAccountProps\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"DavAccountProps\",\"Ident\":\"types.DavAccountProps\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"owner_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}}],\"indexes\":[{\"unique\":true,\"fields\":[\"owner_id\",\"password\"]}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"DirectLink\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"file\",\"type\":\"File\",\"field\":\"file_id\",\"ref_name\":\"direct_links\",\"unique\":true,\"inverse\":true,\"required\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"downloads\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"file_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"speed\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Entity\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"file\",\"type\":\"File\",\"ref_name\":\"entities\",\"inverse\":true},{\"name\":\"user\",\"type\":\"User\",\"field\":\"created_by\",\"ref_name\":\"entities\",\"unique\":true,\"inverse\":true},{\"name\":\"storage_policy\",\"type\":\"StoragePolicy\",\"field\":\"storage_policy_entities\",\"ref_name\":\"entities\",\"unique\":true,\"inverse\":true,\"required\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"type\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"source\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"size\",\"type\":{\"Type\":13,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"reference_count\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":1,\"default_kind\":2,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"storage_policy_entities\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"created_by\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"upload_session_id\",\"type\":{\"Type\":4,\"Ident\":\"uuid.UUID\",\"PkgPath\":\"github.com/gofrs/uuid\",\"PkgName\":\"uuid\",\"Nillable\":false,\"RType\":{\"Name\":\"UUID\",\"Ident\":\"uuid.UUID\",\"Kind\":17,\"PkgPath\":\"github.com/gofrs/uuid\",\"Methods\":{\"Bytes\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}]},\"Format\":{\"In\":[{\"Name\":\"State\",\"Ident\":\"fmt.State\",\"Kind\":20,\"PkgPath\":\"fmt\",\"Methods\":null},{\"Name\":\"int32\",\"Ident\":\"int32\",\"Kind\":5,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"MarshalBinary\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"MarshalText\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Scan\":{\"In\":[{\"Name\":\"\",\"Ident\":\"interface {}\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"SetVariant\":{\"In\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"SetVersion\":{\"In\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"String\":{\"In\":[],\"Out\":[{\"Name\":\"string\",\"Ident\":\"string\",\"Kind\":24,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalBinary\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalText\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Value\":{\"In\":[],\"Out\":[{\"Name\":\"Value\",\"Ident\":\"driver.Value\",\"Kind\":20,\"PkgPath\":\"database/sql/driver\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Variant\":{\"In\":[],\"Out\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}]},\"Version\":{\"In\":[],\"Out\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":6,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"recycle_options\",\"type\":{\"Type\":3,\"Ident\":\"*types.EntityRecycleOption\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"EntityRecycleOption\",\"Ident\":\"types.EntityRecycleOption\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"position\":{\"Index\":7,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"File\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"owner\",\"type\":\"User\",\"field\":\"owner_id\",\"ref_name\":\"files\",\"unique\":true,\"inverse\":true,\"required\":true},{\"name\":\"storage_policies\",\"type\":\"StoragePolicy\",\"field\":\"storage_policy_files\",\"ref_name\":\"files\",\"unique\":true,\"inverse\":true},{\"name\":\"parent\",\"type\":\"File\",\"field\":\"file_children\",\"ref\":{\"name\":\"children\",\"type\":\"File\"},\"unique\":true,\"inverse\":true},{\"name\":\"metadata\",\"type\":\"Metadata\"},{\"name\":\"entities\",\"type\":\"Entity\"},{\"name\":\"shares\",\"type\":\"Share\"},{\"name\":\"direct_links\",\"type\":\"DirectLink\"}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"type\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"owner_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"size\",\"type\":{\"Type\":13,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":0,\"default_kind\":6,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"primary_entity\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":6,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"file_children\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":7,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"is_symbolic\",\"type\":{\"Type\":1,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":false,\"default_kind\":1,\"position\":{\"Index\":8,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"props\",\"type\":{\"Type\":3,\"Ident\":\"*types.FileProps\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"FileProps\",\"Ident\":\"types.FileProps\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"position\":{\"Index\":9,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"storage_policy_files\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":10,\"MixedIn\":false,\"MixinIndex\":0}}],\"indexes\":[{\"unique\":true,\"fields\":[\"file_children\",\"name\"]},{\"fields\":[\"file_children\",\"type\",\"updated_at\"]},{\"fields\":[\"file_children\",\"type\",\"size\"]}],\"hooks\":[{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}]},{\"name\":\"Group\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"users\",\"type\":\"User\"},{\"name\":\"storage_policies\",\"type\":\"StoragePolicy\",\"field\":\"storage_policy_id\",\"ref_name\":\"groups\",\"unique\":true,\"inverse\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"max_storage\",\"type\":{\"Type\":13,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"speed_limit\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"permissions\",\"type\":{\"Type\":5,\"Ident\":\"*boolset.BooleanSet\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"PkgName\":\"boolset\",\"Nillable\":true,\"RType\":{\"Name\":\"BooleanSet\",\"Ident\":\"boolset.BooleanSet\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"Methods\":{\"Enabled\":{\"In\":[{\"Name\":\"int\",\"Ident\":\"int\",\"Kind\":2,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"bool\",\"Ident\":\"bool\",\"Kind\":1,\"PkgPath\":\"\",\"Methods\":null}]},\"MarshalBinary\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Scan\":{\"In\":[{\"Name\":\"\",\"Ident\":\"interface {}\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"String\":{\"In\":[],\"Out\":[{\"Name\":\"string\",\"Ident\":\"string\",\"Kind\":24,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalBinary\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Value\":{\"In\":[],\"Out\":[{\"Name\":\"Value\",\"Ident\":\"driver.Value\",\"Kind\":20,\"PkgPath\":\"database/sql/driver\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"settings\",\"type\":{\"Type\":3,\"Ident\":\"*types.GroupSetting\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"GroupSetting\",\"Ident\":\"types.GroupSetting\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"default\":true,\"default_value\":{},\"default_kind\":22,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"storage_policy_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Metadata\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"file\",\"type\":\"File\",\"field\":\"file_id\",\"ref_name\":\"metadata\",\"unique\":true,\"inverse\":true,\"required\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"value\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"file_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"is_public\",\"type\":{\"Type\":1,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":false,\"default_kind\":1,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}}],\"indexes\":[{\"unique\":true,\"fields\":[\"file_id\",\"name\"]}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Node\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"storage_policy\",\"type\":\"StoragePolicy\"}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"status\",\"type\":{\"Type\":6,\"Ident\":\"node.Status\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"enums\":[{\"N\":\"active\",\"V\":\"active\"},{\"N\":\"suspended\",\"V\":\"suspended\"}],\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"type\",\"type\":{\"Type\":6,\"Ident\":\"node.Type\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"enums\":[{\"N\":\"master\",\"V\":\"master\"},{\"N\":\"slave\",\"V\":\"slave\"}],\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"server\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"slave_key\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"capabilities\",\"type\":{\"Type\":5,\"Ident\":\"*boolset.BooleanSet\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"PkgName\":\"boolset\",\"Nillable\":true,\"RType\":{\"Name\":\"BooleanSet\",\"Ident\":\"boolset.BooleanSet\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"Methods\":{\"Enabled\":{\"In\":[{\"Name\":\"int\",\"Ident\":\"int\",\"Kind\":2,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"bool\",\"Ident\":\"bool\",\"Kind\":1,\"PkgPath\":\"\",\"Methods\":null}]},\"MarshalBinary\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Scan\":{\"In\":[{\"Name\":\"\",\"Ident\":\"interface {}\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"String\":{\"In\":[],\"Out\":[{\"Name\":\"string\",\"Ident\":\"string\",\"Kind\":24,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalBinary\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Value\":{\"In\":[],\"Out\":[{\"Name\":\"Value\",\"Ident\":\"driver.Value\",\"Kind\":20,\"PkgPath\":\"database/sql/driver\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"settings\",\"type\":{\"Type\":3,\"Ident\":\"*types.NodeSetting\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"NodeSetting\",\"Ident\":\"types.NodeSetting\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"default\":true,\"default_value\":{},\"default_kind\":22,\"position\":{\"Index\":6,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"weight\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":0,\"default_kind\":2,\"position\":{\"Index\":7,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Passkey\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"user\",\"type\":\"User\",\"field\":\"user_id\",\"ref_name\":\"passkey\",\"unique\":true,\"inverse\":true,\"required\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"user_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"credential_id\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"credential\",\"type\":{\"Type\":3,\"Ident\":\"*webauthn.Credential\",\"PkgPath\":\"github.com/go-webauthn/webauthn/webauthn\",\"PkgName\":\"webauthn\",\"Nillable\":true,\"RType\":{\"Name\":\"Credential\",\"Ident\":\"webauthn.Credential\",\"Kind\":22,\"PkgPath\":\"github.com/go-webauthn/webauthn/webauthn\",\"Methods\":{\"Descriptor\":{\"In\":[],\"Out\":[{\"Name\":\"CredentialDescriptor\",\"Ident\":\"protocol.CredentialDescriptor\",\"Kind\":25,\"PkgPath\":\"github.com/go-webauthn/webauthn/protocol\",\"Methods\":null}]},\"Verify\":{\"In\":[{\"Name\":\"Provider\",\"Ident\":\"metadata.Provider\",\"Kind\":20,\"PkgPath\":\"github.com/go-webauthn/webauthn/metadata\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0},\"sensitive\":true},{\"name\":\"used_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}}],\"indexes\":[{\"unique\":true,\"fields\":[\"user_id\",\"credential_id\"]}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Setting\",\"config\":{\"Table\":\"\"},\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"unique\":true,\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"value\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"optional\":true,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Share\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"user\",\"type\":\"User\",\"ref_name\":\"shares\",\"unique\":true,\"inverse\":true},{\"name\":\"file\",\"type\":\"File\",\"ref_name\":\"shares\",\"unique\":true,\"inverse\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"password\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"views\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":0,\"default_kind\":2,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"downloads\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":0,\"default_kind\":2,\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"expires\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"remain_downloads\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"props\",\"type\":{\"Type\":3,\"Ident\":\"*types.ShareProps\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"ShareProps\",\"Ident\":\"types.ShareProps\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"StoragePolicy\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"groups\",\"type\":\"Group\"},{\"name\":\"files\",\"type\":\"File\"},{\"name\":\"entities\",\"type\":\"Entity\"},{\"name\":\"node\",\"type\":\"Node\",\"field\":\"node_id\",\"ref_name\":\"storage_policy\",\"unique\":true,\"inverse\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"type\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"server\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"bucket_name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"is_private\",\"type\":{\"Type\":1,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"access_key\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"optional\":true,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"secret_key\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"optional\":true,\"position\":{\"Index\":6,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"max_size\",\"type\":{\"Type\":13,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":7,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"dir_name_rule\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":8,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"file_name_rule\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":9,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"settings\",\"type\":{\"Type\":3,\"Ident\":\"*types.PolicySetting\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"PolicySetting\",\"Ident\":\"types.PolicySetting\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"default\":true,\"default_value\":{\"file_type\":null,\"native_media_processing\":false,\"s3_path_style\":false,\"token\":\"\"},\"default_kind\":22,\"position\":{\"Index\":10,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"node_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":11,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Task\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"user\",\"type\":\"User\",\"field\":\"user_tasks\",\"ref_name\":\"tasks\",\"unique\":true,\"inverse\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"type\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"status\",\"type\":{\"Type\":6,\"Ident\":\"task.Status\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"enums\":[{\"N\":\"queued\",\"V\":\"queued\"},{\"N\":\"processing\",\"V\":\"processing\"},{\"N\":\"suspending\",\"V\":\"suspending\"},{\"N\":\"error\",\"V\":\"error\"},{\"N\":\"canceled\",\"V\":\"canceled\"},{\"N\":\"completed\",\"V\":\"completed\"}],\"default\":true,\"default_value\":\"queued\",\"default_kind\":24,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"public_state\",\"type\":{\"Type\":3,\"Ident\":\"*types.TaskPublicState\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"TaskPublicState\",\"Ident\":\"types.TaskPublicState\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"private_state\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"optional\":true,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"correlation_id\",\"type\":{\"Type\":4,\"Ident\":\"uuid.UUID\",\"PkgPath\":\"github.com/gofrs/uuid\",\"PkgName\":\"uuid\",\"Nillable\":false,\"RType\":{\"Name\":\"UUID\",\"Ident\":\"uuid.UUID\",\"Kind\":17,\"PkgPath\":\"github.com/gofrs/uuid\",\"Methods\":{\"Bytes\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}]},\"Format\":{\"In\":[{\"Name\":\"State\",\"Ident\":\"fmt.State\",\"Kind\":20,\"PkgPath\":\"fmt\",\"Methods\":null},{\"Name\":\"int32\",\"Ident\":\"int32\",\"Kind\":5,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"MarshalBinary\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"MarshalText\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Scan\":{\"In\":[{\"Name\":\"\",\"Ident\":\"interface {}\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"SetVariant\":{\"In\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"SetVersion\":{\"In\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"String\":{\"In\":[],\"Out\":[{\"Name\":\"string\",\"Ident\":\"string\",\"Kind\":24,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalBinary\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalText\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Value\":{\"In\":[],\"Out\":[{\"Name\":\"Value\",\"Ident\":\"driver.Value\",\"Kind\":20,\"PkgPath\":\"database/sql/driver\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Variant\":{\"In\":[],\"Out\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}]},\"Version\":{\"In\":[],\"Out\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"optional\":true,\"immutable\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"user_tasks\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"User\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"group\",\"type\":\"Group\",\"field\":\"group_users\",\"ref_name\":\"users\",\"unique\":true,\"inverse\":true,\"required\":true},{\"name\":\"files\",\"type\":\"File\"},{\"name\":\"dav_accounts\",\"type\":\"DavAccount\"},{\"name\":\"shares\",\"type\":\"Share\"},{\"name\":\"passkey\",\"type\":\"Passkey\"},{\"name\":\"tasks\",\"type\":\"Task\"},{\"name\":\"entities\",\"type\":\"Entity\"}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"email\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":100,\"unique\":true,\"validators\":1,\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"nick\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":100,\"validators\":1,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"password\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0},\"sensitive\":true},{\"name\":\"status\",\"type\":{\"Type\":6,\"Ident\":\"user.Status\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"enums\":[{\"N\":\"active\",\"V\":\"active\"},{\"N\":\"inactive\",\"V\":\"inactive\"},{\"N\":\"manual_banned\",\"V\":\"manual_banned\"},{\"N\":\"sys_banned\",\"V\":\"sys_banned\"}],\"default\":true,\"default_value\":\"active\",\"default_kind\":24,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"storage\",\"type\":{\"Type\":13,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":0,\"default_kind\":6,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"two_factor_secret\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0},\"sensitive\":true},{\"name\":\"avatar\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":6,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"settings\",\"type\":{\"Type\":3,\"Ident\":\"*types.UserSetting\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"UserSetting\",\"Ident\":\"types.UserSetting\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"default\":true,\"default_value\":{},\"default_kind\":22,\"position\":{\"Index\":7,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"group_users\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":8,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]}],\"Features\":[\"intercept\",\"schema/snapshot\",\"sql/upsert\",\"sql/upsert\",\"sql/execquery\"]}" +const Schema = "{\"Schema\":\"github.com/cloudreve/Cloudreve/v4/ent/schema\",\"Package\":\"github.com/cloudreve/Cloudreve/v4/ent\",\"Schemas\":[{\"name\":\"DavAccount\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"owner\",\"type\":\"User\",\"field\":\"owner_id\",\"ref_name\":\"dav_accounts\",\"unique\":true,\"inverse\":true,\"required\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"uri\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"password\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0},\"sensitive\":true},{\"name\":\"options\",\"type\":{\"Type\":5,\"Ident\":\"*boolset.BooleanSet\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"PkgName\":\"boolset\",\"Nillable\":true,\"RType\":{\"Name\":\"BooleanSet\",\"Ident\":\"boolset.BooleanSet\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"Methods\":{\"Enabled\":{\"In\":[{\"Name\":\"int\",\"Ident\":\"int\",\"Kind\":2,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"bool\",\"Ident\":\"bool\",\"Kind\":1,\"PkgPath\":\"\",\"Methods\":null}]},\"MarshalBinary\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Scan\":{\"In\":[{\"Name\":\"\",\"Ident\":\"interface {}\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"String\":{\"In\":[],\"Out\":[{\"Name\":\"string\",\"Ident\":\"string\",\"Kind\":24,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalBinary\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Value\":{\"In\":[],\"Out\":[{\"Name\":\"Value\",\"Ident\":\"driver.Value\",\"Kind\":20,\"PkgPath\":\"database/sql/driver\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"props\",\"type\":{\"Type\":3,\"Ident\":\"*types.DavAccountProps\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"DavAccountProps\",\"Ident\":\"types.DavAccountProps\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"owner_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}}],\"indexes\":[{\"unique\":true,\"fields\":[\"owner_id\",\"password\"]}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"DirectLink\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"file\",\"type\":\"File\",\"field\":\"file_id\",\"ref_name\":\"direct_links\",\"unique\":true,\"inverse\":true,\"required\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"downloads\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"file_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"speed\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Entity\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"file\",\"type\":\"File\",\"ref_name\":\"entities\",\"inverse\":true},{\"name\":\"user\",\"type\":\"User\",\"field\":\"created_by\",\"ref_name\":\"entities\",\"unique\":true,\"inverse\":true},{\"name\":\"storage_policy\",\"type\":\"StoragePolicy\",\"field\":\"storage_policy_entities\",\"ref_name\":\"entities\",\"unique\":true,\"inverse\":true,\"required\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"type\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"source\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"size\",\"type\":{\"Type\":13,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"reference_count\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":1,\"default_kind\":2,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"storage_policy_entities\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"created_by\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"upload_session_id\",\"type\":{\"Type\":4,\"Ident\":\"uuid.UUID\",\"PkgPath\":\"github.com/gofrs/uuid\",\"PkgName\":\"uuid\",\"Nillable\":false,\"RType\":{\"Name\":\"UUID\",\"Ident\":\"uuid.UUID\",\"Kind\":17,\"PkgPath\":\"github.com/gofrs/uuid\",\"Methods\":{\"Bytes\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}]},\"Format\":{\"In\":[{\"Name\":\"State\",\"Ident\":\"fmt.State\",\"Kind\":20,\"PkgPath\":\"fmt\",\"Methods\":null},{\"Name\":\"int32\",\"Ident\":\"int32\",\"Kind\":5,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"MarshalBinary\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"MarshalText\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Scan\":{\"In\":[{\"Name\":\"\",\"Ident\":\"interface {}\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"SetVariant\":{\"In\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"SetVersion\":{\"In\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"String\":{\"In\":[],\"Out\":[{\"Name\":\"string\",\"Ident\":\"string\",\"Kind\":24,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalBinary\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalText\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Value\":{\"In\":[],\"Out\":[{\"Name\":\"Value\",\"Ident\":\"driver.Value\",\"Kind\":20,\"PkgPath\":\"database/sql/driver\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Variant\":{\"In\":[],\"Out\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}]},\"Version\":{\"In\":[],\"Out\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":6,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"props\",\"type\":{\"Type\":3,\"Ident\":\"*types.EntityProps\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"EntityProps\",\"Ident\":\"types.EntityProps\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"storage_key\":\"recycle_options\",\"position\":{\"Index\":7,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"File\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"owner\",\"type\":\"User\",\"field\":\"owner_id\",\"ref_name\":\"files\",\"unique\":true,\"inverse\":true,\"required\":true},{\"name\":\"storage_policies\",\"type\":\"StoragePolicy\",\"field\":\"storage_policy_files\",\"ref_name\":\"files\",\"unique\":true,\"inverse\":true},{\"name\":\"parent\",\"type\":\"File\",\"field\":\"file_children\",\"ref\":{\"name\":\"children\",\"type\":\"File\"},\"unique\":true,\"inverse\":true},{\"name\":\"metadata\",\"type\":\"Metadata\"},{\"name\":\"entities\",\"type\":\"Entity\"},{\"name\":\"shares\",\"type\":\"Share\"},{\"name\":\"direct_links\",\"type\":\"DirectLink\"}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"type\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"owner_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"size\",\"type\":{\"Type\":13,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":0,\"default_kind\":6,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"primary_entity\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":6,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"file_children\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":7,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"is_symbolic\",\"type\":{\"Type\":1,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":false,\"default_kind\":1,\"position\":{\"Index\":8,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"props\",\"type\":{\"Type\":3,\"Ident\":\"*types.FileProps\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"FileProps\",\"Ident\":\"types.FileProps\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"position\":{\"Index\":9,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"storage_policy_files\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":10,\"MixedIn\":false,\"MixinIndex\":0}}],\"indexes\":[{\"unique\":true,\"fields\":[\"file_children\",\"name\"]},{\"fields\":[\"file_children\",\"type\",\"updated_at\"]},{\"fields\":[\"file_children\",\"type\",\"size\"]}],\"hooks\":[{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}]},{\"name\":\"Group\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"users\",\"type\":\"User\"},{\"name\":\"storage_policies\",\"type\":\"StoragePolicy\",\"field\":\"storage_policy_id\",\"ref_name\":\"groups\",\"unique\":true,\"inverse\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"max_storage\",\"type\":{\"Type\":13,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"speed_limit\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"permissions\",\"type\":{\"Type\":5,\"Ident\":\"*boolset.BooleanSet\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"PkgName\":\"boolset\",\"Nillable\":true,\"RType\":{\"Name\":\"BooleanSet\",\"Ident\":\"boolset.BooleanSet\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"Methods\":{\"Enabled\":{\"In\":[{\"Name\":\"int\",\"Ident\":\"int\",\"Kind\":2,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"bool\",\"Ident\":\"bool\",\"Kind\":1,\"PkgPath\":\"\",\"Methods\":null}]},\"MarshalBinary\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Scan\":{\"In\":[{\"Name\":\"\",\"Ident\":\"interface {}\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"String\":{\"In\":[],\"Out\":[{\"Name\":\"string\",\"Ident\":\"string\",\"Kind\":24,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalBinary\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Value\":{\"In\":[],\"Out\":[{\"Name\":\"Value\",\"Ident\":\"driver.Value\",\"Kind\":20,\"PkgPath\":\"database/sql/driver\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"settings\",\"type\":{\"Type\":3,\"Ident\":\"*types.GroupSetting\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"GroupSetting\",\"Ident\":\"types.GroupSetting\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"default\":true,\"default_value\":{},\"default_kind\":22,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"storage_policy_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Metadata\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"file\",\"type\":\"File\",\"field\":\"file_id\",\"ref_name\":\"metadata\",\"unique\":true,\"inverse\":true,\"required\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"value\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"file_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"is_public\",\"type\":{\"Type\":1,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":false,\"default_kind\":1,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}}],\"indexes\":[{\"unique\":true,\"fields\":[\"file_id\",\"name\"]}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Node\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"storage_policy\",\"type\":\"StoragePolicy\"}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"status\",\"type\":{\"Type\":6,\"Ident\":\"node.Status\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"enums\":[{\"N\":\"active\",\"V\":\"active\"},{\"N\":\"suspended\",\"V\":\"suspended\"}],\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"type\",\"type\":{\"Type\":6,\"Ident\":\"node.Type\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"enums\":[{\"N\":\"master\",\"V\":\"master\"},{\"N\":\"slave\",\"V\":\"slave\"}],\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"server\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"slave_key\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"capabilities\",\"type\":{\"Type\":5,\"Ident\":\"*boolset.BooleanSet\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"PkgName\":\"boolset\",\"Nillable\":true,\"RType\":{\"Name\":\"BooleanSet\",\"Ident\":\"boolset.BooleanSet\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/pkg/boolset\",\"Methods\":{\"Enabled\":{\"In\":[{\"Name\":\"int\",\"Ident\":\"int\",\"Kind\":2,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"bool\",\"Ident\":\"bool\",\"Kind\":1,\"PkgPath\":\"\",\"Methods\":null}]},\"MarshalBinary\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Scan\":{\"In\":[{\"Name\":\"\",\"Ident\":\"interface {}\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"String\":{\"In\":[],\"Out\":[{\"Name\":\"string\",\"Ident\":\"string\",\"Kind\":24,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalBinary\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Value\":{\"In\":[],\"Out\":[{\"Name\":\"Value\",\"Ident\":\"driver.Value\",\"Kind\":20,\"PkgPath\":\"database/sql/driver\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"settings\",\"type\":{\"Type\":3,\"Ident\":\"*types.NodeSetting\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"NodeSetting\",\"Ident\":\"types.NodeSetting\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"default\":true,\"default_value\":{},\"default_kind\":22,\"position\":{\"Index\":6,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"weight\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":0,\"default_kind\":2,\"position\":{\"Index\":7,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Passkey\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"user\",\"type\":\"User\",\"field\":\"user_id\",\"ref_name\":\"passkey\",\"unique\":true,\"inverse\":true,\"required\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"user_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"credential_id\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"credential\",\"type\":{\"Type\":3,\"Ident\":\"*webauthn.Credential\",\"PkgPath\":\"github.com/go-webauthn/webauthn/webauthn\",\"PkgName\":\"webauthn\",\"Nillable\":true,\"RType\":{\"Name\":\"Credential\",\"Ident\":\"webauthn.Credential\",\"Kind\":22,\"PkgPath\":\"github.com/go-webauthn/webauthn/webauthn\",\"Methods\":{\"Descriptor\":{\"In\":[],\"Out\":[{\"Name\":\"CredentialDescriptor\",\"Ident\":\"protocol.CredentialDescriptor\",\"Kind\":25,\"PkgPath\":\"github.com/go-webauthn/webauthn/protocol\",\"Methods\":null}]},\"Verify\":{\"In\":[{\"Name\":\"Provider\",\"Ident\":\"metadata.Provider\",\"Kind\":20,\"PkgPath\":\"github.com/go-webauthn/webauthn/metadata\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0},\"sensitive\":true},{\"name\":\"used_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}}],\"indexes\":[{\"unique\":true,\"fields\":[\"user_id\",\"credential_id\"]}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Setting\",\"config\":{\"Table\":\"\"},\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"unique\":true,\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"value\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"optional\":true,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Share\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"user\",\"type\":\"User\",\"ref_name\":\"shares\",\"unique\":true,\"inverse\":true},{\"name\":\"file\",\"type\":\"File\",\"ref_name\":\"shares\",\"unique\":true,\"inverse\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"password\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"views\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":0,\"default_kind\":2,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"downloads\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":0,\"default_kind\":2,\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"expires\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"remain_downloads\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"props\",\"type\":{\"Type\":3,\"Ident\":\"*types.ShareProps\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"ShareProps\",\"Ident\":\"types.ShareProps\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"StoragePolicy\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"groups\",\"type\":\"Group\"},{\"name\":\"files\",\"type\":\"File\"},{\"name\":\"entities\",\"type\":\"Entity\"},{\"name\":\"node\",\"type\":\"Node\",\"field\":\"node_id\",\"ref_name\":\"storage_policy\",\"unique\":true,\"inverse\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"type\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"server\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"bucket_name\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"is_private\",\"type\":{\"Type\":1,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"access_key\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"optional\":true,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"secret_key\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"optional\":true,\"position\":{\"Index\":6,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"max_size\",\"type\":{\"Type\":13,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":7,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"dir_name_rule\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":8,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"file_name_rule\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":9,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"settings\",\"type\":{\"Type\":3,\"Ident\":\"*types.PolicySetting\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"PolicySetting\",\"Ident\":\"types.PolicySetting\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"default\":true,\"default_value\":{\"file_type\":null,\"native_media_processing\":false,\"s3_path_style\":false,\"token\":\"\"},\"default_kind\":22,\"position\":{\"Index\":10,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"node_id\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":11,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"Task\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"user\",\"type\":\"User\",\"field\":\"user_tasks\",\"ref_name\":\"tasks\",\"unique\":true,\"inverse\":true}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"type\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"status\",\"type\":{\"Type\":6,\"Ident\":\"task.Status\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"enums\":[{\"N\":\"queued\",\"V\":\"queued\"},{\"N\":\"processing\",\"V\":\"processing\"},{\"N\":\"suspending\",\"V\":\"suspending\"},{\"N\":\"error\",\"V\":\"error\"},{\"N\":\"canceled\",\"V\":\"canceled\"},{\"N\":\"completed\",\"V\":\"completed\"}],\"default\":true,\"default_value\":\"queued\",\"default_kind\":24,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"public_state\",\"type\":{\"Type\":3,\"Ident\":\"*types.TaskPublicState\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"TaskPublicState\",\"Ident\":\"types.TaskPublicState\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"private_state\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":2147483647,\"optional\":true,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"correlation_id\",\"type\":{\"Type\":4,\"Ident\":\"uuid.UUID\",\"PkgPath\":\"github.com/gofrs/uuid\",\"PkgName\":\"uuid\",\"Nillable\":false,\"RType\":{\"Name\":\"UUID\",\"Ident\":\"uuid.UUID\",\"Kind\":17,\"PkgPath\":\"github.com/gofrs/uuid\",\"Methods\":{\"Bytes\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}]},\"Format\":{\"In\":[{\"Name\":\"State\",\"Ident\":\"fmt.State\",\"Kind\":20,\"PkgPath\":\"fmt\",\"Methods\":null},{\"Name\":\"int32\",\"Ident\":\"int32\",\"Kind\":5,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"MarshalBinary\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"MarshalText\":{\"In\":[],\"Out\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Scan\":{\"In\":[{\"Name\":\"\",\"Ident\":\"interface {}\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"SetVariant\":{\"In\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"SetVersion\":{\"In\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[]},\"String\":{\"In\":[],\"Out\":[{\"Name\":\"string\",\"Ident\":\"string\",\"Kind\":24,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalBinary\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"UnmarshalText\":{\"In\":[{\"Name\":\"\",\"Ident\":\"[]uint8\",\"Kind\":23,\"PkgPath\":\"\",\"Methods\":null}],\"Out\":[{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Value\":{\"In\":[],\"Out\":[{\"Name\":\"Value\",\"Ident\":\"driver.Value\",\"Kind\":20,\"PkgPath\":\"database/sql/driver\",\"Methods\":null},{\"Name\":\"error\",\"Ident\":\"error\",\"Kind\":20,\"PkgPath\":\"\",\"Methods\":null}]},\"Variant\":{\"In\":[],\"Out\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}]},\"Version\":{\"In\":[],\"Out\":[{\"Name\":\"uint8\",\"Ident\":\"uint8\",\"Kind\":8,\"PkgPath\":\"\",\"Methods\":null}]}}}},\"optional\":true,\"immutable\":true,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"user_tasks\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]},{\"name\":\"User\",\"config\":{\"Table\":\"\"},\"edges\":[{\"name\":\"group\",\"type\":\"Group\",\"field\":\"group_users\",\"ref_name\":\"users\",\"unique\":true,\"inverse\":true,\"required\":true},{\"name\":\"files\",\"type\":\"File\"},{\"name\":\"dav_accounts\",\"type\":\"DavAccount\"},{\"name\":\"shares\",\"type\":\"Share\"},{\"name\":\"passkey\",\"type\":\"Passkey\"},{\"name\":\"tasks\",\"type\":\"Task\"},{\"name\":\"entities\",\"type\":\"Entity\"}],\"fields\":[{\"name\":\"created_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"immutable\":true,\"position\":{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"updated_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_kind\":19,\"update_default\":true,\"position\":{\"Index\":1,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"deleted_at\",\"type\":{\"Type\":2,\"Ident\":\"\",\"PkgPath\":\"time\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"nillable\":true,\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":true,\"MixinIndex\":0},\"schema_type\":{\"mysql\":\"datetime\"}},{\"name\":\"email\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":100,\"unique\":true,\"validators\":1,\"position\":{\"Index\":0,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"nick\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"size\":100,\"validators\":1,\"position\":{\"Index\":1,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"password\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":2,\"MixedIn\":false,\"MixinIndex\":0},\"sensitive\":true},{\"name\":\"status\",\"type\":{\"Type\":6,\"Ident\":\"user.Status\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"enums\":[{\"N\":\"active\",\"V\":\"active\"},{\"N\":\"inactive\",\"V\":\"inactive\"},{\"N\":\"manual_banned\",\"V\":\"manual_banned\"},{\"N\":\"sys_banned\",\"V\":\"sys_banned\"}],\"default\":true,\"default_value\":\"active\",\"default_kind\":24,\"position\":{\"Index\":3,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"storage\",\"type\":{\"Type\":13,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"default\":true,\"default_value\":0,\"default_kind\":6,\"position\":{\"Index\":4,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"two_factor_secret\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":5,\"MixedIn\":false,\"MixinIndex\":0},\"sensitive\":true},{\"name\":\"avatar\",\"type\":{\"Type\":7,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"optional\":true,\"position\":{\"Index\":6,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"settings\",\"type\":{\"Type\":3,\"Ident\":\"*types.UserSetting\",\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"PkgName\":\"types\",\"Nillable\":true,\"RType\":{\"Name\":\"UserSetting\",\"Ident\":\"types.UserSetting\",\"Kind\":22,\"PkgPath\":\"github.com/cloudreve/Cloudreve/v4/inventory/types\",\"Methods\":{}}},\"optional\":true,\"default\":true,\"default_value\":{},\"default_kind\":22,\"position\":{\"Index\":7,\"MixedIn\":false,\"MixinIndex\":0}},{\"name\":\"group_users\",\"type\":{\"Type\":12,\"Ident\":\"\",\"PkgPath\":\"\",\"PkgName\":\"\",\"Nillable\":false,\"RType\":null},\"position\":{\"Index\":8,\"MixedIn\":false,\"MixinIndex\":0}}],\"hooks\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}],\"interceptors\":[{\"Index\":0,\"MixedIn\":true,\"MixinIndex\":0}]}],\"Features\":[\"intercept\",\"schema/snapshot\",\"sql/upsert\",\"sql/upsert\",\"sql/execquery\"]}" diff --git a/ent/mutation.go b/ent/mutation.go index 5a612a26..fffa45ab 100644 --- a/ent/mutation.go +++ b/ent/mutation.go @@ -1723,7 +1723,7 @@ type EntityMutation struct { reference_count *int addreference_count *int upload_session_id *uuid.UUID - recycle_options **types.EntityRecycleOption + props **types.EntityProps clearedFields map[string]struct{} file map[int]struct{} removedfile map[int]struct{} @@ -2294,53 +2294,53 @@ func (m *EntityMutation) ResetUploadSessionID() { delete(m.clearedFields, entity.FieldUploadSessionID) } -// SetRecycleOptions sets the "recycle_options" field. -func (m *EntityMutation) SetRecycleOptions(tro *types.EntityRecycleOption) { - m.recycle_options = &tro +// SetProps sets the "props" field. +func (m *EntityMutation) SetProps(tp *types.EntityProps) { + m.props = &tp } -// RecycleOptions returns the value of the "recycle_options" field in the mutation. -func (m *EntityMutation) RecycleOptions() (r *types.EntityRecycleOption, exists bool) { - v := m.recycle_options +// Props returns the value of the "props" field in the mutation. +func (m *EntityMutation) Props() (r *types.EntityProps, exists bool) { + v := m.props if v == nil { return } return *v, true } -// OldRecycleOptions returns the old "recycle_options" field's value of the Entity entity. +// OldProps returns the old "props" field's value of the Entity entity. // If the Entity object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *EntityMutation) OldRecycleOptions(ctx context.Context) (v *types.EntityRecycleOption, err error) { +func (m *EntityMutation) OldProps(ctx context.Context) (v *types.EntityProps, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldRecycleOptions is only allowed on UpdateOne operations") + return v, errors.New("OldProps is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldRecycleOptions requires an ID field in the mutation") + return v, errors.New("OldProps requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldRecycleOptions: %w", err) + return v, fmt.Errorf("querying old value for OldProps: %w", err) } - return oldValue.RecycleOptions, nil + return oldValue.Props, nil } -// ClearRecycleOptions clears the value of the "recycle_options" field. -func (m *EntityMutation) ClearRecycleOptions() { - m.recycle_options = nil - m.clearedFields[entity.FieldRecycleOptions] = struct{}{} +// ClearProps clears the value of the "props" field. +func (m *EntityMutation) ClearProps() { + m.props = nil + m.clearedFields[entity.FieldProps] = struct{}{} } -// RecycleOptionsCleared returns if the "recycle_options" field was cleared in this mutation. -func (m *EntityMutation) RecycleOptionsCleared() bool { - _, ok := m.clearedFields[entity.FieldRecycleOptions] +// PropsCleared returns if the "props" field was cleared in this mutation. +func (m *EntityMutation) PropsCleared() bool { + _, ok := m.clearedFields[entity.FieldProps] return ok } -// ResetRecycleOptions resets all changes to the "recycle_options" field. -func (m *EntityMutation) ResetRecycleOptions() { - m.recycle_options = nil - delete(m.clearedFields, entity.FieldRecycleOptions) +// ResetProps resets all changes to the "props" field. +func (m *EntityMutation) ResetProps() { + m.props = nil + delete(m.clearedFields, entity.FieldProps) } // AddFileIDs adds the "file" edge to the File entity by ids. @@ -2542,8 +2542,8 @@ func (m *EntityMutation) Fields() []string { if m.upload_session_id != nil { fields = append(fields, entity.FieldUploadSessionID) } - if m.recycle_options != nil { - fields = append(fields, entity.FieldRecycleOptions) + if m.props != nil { + fields = append(fields, entity.FieldProps) } return fields } @@ -2573,8 +2573,8 @@ func (m *EntityMutation) Field(name string) (ent.Value, bool) { return m.CreatedBy() case entity.FieldUploadSessionID: return m.UploadSessionID() - case entity.FieldRecycleOptions: - return m.RecycleOptions() + case entity.FieldProps: + return m.Props() } return nil, false } @@ -2604,8 +2604,8 @@ func (m *EntityMutation) OldField(ctx context.Context, name string) (ent.Value, return m.OldCreatedBy(ctx) case entity.FieldUploadSessionID: return m.OldUploadSessionID(ctx) - case entity.FieldRecycleOptions: - return m.OldRecycleOptions(ctx) + case entity.FieldProps: + return m.OldProps(ctx) } return nil, fmt.Errorf("unknown Entity field %s", name) } @@ -2685,12 +2685,12 @@ func (m *EntityMutation) SetField(name string, value ent.Value) error { } m.SetUploadSessionID(v) return nil - case entity.FieldRecycleOptions: - v, ok := value.(*types.EntityRecycleOption) + case entity.FieldProps: + v, ok := value.(*types.EntityProps) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetRecycleOptions(v) + m.SetProps(v) return nil } return fmt.Errorf("unknown Entity field %s", name) @@ -2770,8 +2770,8 @@ func (m *EntityMutation) ClearedFields() []string { if m.FieldCleared(entity.FieldUploadSessionID) { fields = append(fields, entity.FieldUploadSessionID) } - if m.FieldCleared(entity.FieldRecycleOptions) { - fields = append(fields, entity.FieldRecycleOptions) + if m.FieldCleared(entity.FieldProps) { + fields = append(fields, entity.FieldProps) } return fields } @@ -2796,8 +2796,8 @@ func (m *EntityMutation) ClearField(name string) error { case entity.FieldUploadSessionID: m.ClearUploadSessionID() return nil - case entity.FieldRecycleOptions: - m.ClearRecycleOptions() + case entity.FieldProps: + m.ClearProps() return nil } return fmt.Errorf("unknown Entity nullable field %s", name) @@ -2837,8 +2837,8 @@ func (m *EntityMutation) ResetField(name string) error { case entity.FieldUploadSessionID: m.ResetUploadSessionID() return nil - case entity.FieldRecycleOptions: - m.ResetRecycleOptions() + case entity.FieldProps: + m.ResetProps() return nil } return fmt.Errorf("unknown Entity field %s", name) diff --git a/ent/schema/entity.go b/ent/schema/entity.go index a2b39993..ba64cc74 100644 --- a/ent/schema/entity.go +++ b/ent/schema/entity.go @@ -25,8 +25,9 @@ func (Entity) Fields() []ent.Field { field.UUID("upload_session_id", uuid.Must(uuid.NewV4())). Optional(). Nillable(), - field.JSON("recycle_options", &types.EntityRecycleOption{}). - Optional(), + field.JSON("props", &types.EntityProps{}). + Optional(). + StorageKey("recycle_options"), } } diff --git a/inventory/file.go b/inventory/file.go index 788808ee..45f8cade 100644 --- a/inventory/file.go +++ b/inventory/file.go @@ -130,6 +130,7 @@ type ( Size int64 UploadSessionID uuid.UUID Importing bool + EncryptMetadata *types.EncryptMetadata } RelocateEntityParameter struct { @@ -188,7 +189,7 @@ type FileClient interface { // Copy copies a layer of file to its corresponding destination folder. dstMap is a map from src parent ID to dst parent Files. Copy(ctx context.Context, files []*ent.File, dstMap map[int][]*ent.File) (map[int][]*ent.File, StorageDiff, error) // Delete deletes a group of files (and related models) with given entity recycle option - Delete(ctx context.Context, files []*ent.File, options *types.EntityRecycleOption) ([]*ent.Entity, StorageDiff, error) + Delete(ctx context.Context, files []*ent.File, options *types.EntityProps) ([]*ent.Entity, StorageDiff, error) // StaleEntities returns stale entities of a given file. If ID is not provided, all entities // will be examined. StaleEntities(ctx context.Context, ids ...int) ([]*ent.Entity, error) @@ -469,7 +470,7 @@ func (f *fileClient) DeleteByUser(ctx context.Context, uid int) error { return nil } -func (f *fileClient) Delete(ctx context.Context, files []*ent.File, options *types.EntityRecycleOption) ([]*ent.Entity, StorageDiff, error) { +func (f *fileClient) Delete(ctx context.Context, files []*ent.File, options *types.EntityProps) ([]*ent.Entity, StorageDiff, error) { // 1. Decrease reference count for all entities; // entities stores the relation between its reference count in `files` and entity ID. entities := make(map[int]int) @@ -525,7 +526,7 @@ func (f *fileClient) Delete(ctx context.Context, files []*ent.File, options *typ for _, chunk := range chunks { if err := f.client.Entity.Update(). Where(entity.IDIn(chunk...)). - SetRecycleOptions(options). + SetProps(options). Exec(ctx); err != nil { return nil, nil, fmt.Errorf("failed to update recycle options for entities %v: %w", chunk, err) } @@ -884,6 +885,17 @@ func (f *fileClient) RemoveStaleEntities(ctx context.Context, file *ent.File) (S func (f *fileClient) CreateEntity(ctx context.Context, file *ent.File, args *EntityParameters) (*ent.Entity, StorageDiff, error) { createdBy := UserFromContext(ctx) + var opt *types.EntityProps + if args.EncryptMetadata != nil { + opt = &types.EntityProps{ + EncryptMetadata: &types.EncryptMetadata{ + Algorithm: args.EncryptMetadata.Algorithm, + Key: args.EncryptMetadata.Key, + IV: args.EncryptMetadata.IV, + }, + } + } + stm := f.client.Entity. Create(). SetType(int(args.EntityType)). @@ -891,6 +903,10 @@ func (f *fileClient) CreateEntity(ctx context.Context, file *ent.File, args *Ent SetSize(args.Size). SetStoragePolicyID(args.StoragePolicyID) + if opt != nil { + stm.SetProps(opt) + } + if createdBy != nil && !IsAnonymousUser(createdBy) { stm.SetUser(createdBy) } diff --git a/inventory/setting.go b/inventory/setting.go index 12bce190..5b926073 100644 --- a/inventory/setting.go +++ b/inventory/setting.go @@ -2,8 +2,11 @@ package inventory import ( "context" + "crypto/rand" + "encoding/base64" "encoding/json" "fmt" + "io" "github.com/cloudreve/Cloudreve/v4/ent" "github.com/cloudreve/Cloudreve/v4/ent/setting" @@ -661,6 +664,7 @@ var DefaultSettings = map[string]string{ "headless_footer_html": "", "headless_bottom_html": "", "sidebar_bottom_html": "", + "encrypt_master_key": "", } func init() { @@ -721,4 +725,10 @@ func init() { panic(err) } DefaultSettings["mail_reset_template"] = string(mailResetTemplates) + + key := make([]byte, 32) + if _, err := io.ReadFull(rand.Reader, key); err != nil { + panic(err) + } + DefaultSettings["encrypt_master_key"] = base64.StdEncoding.EncodeToString(key) } diff --git a/inventory/types/types.go b/inventory/types/types.go index d59806bc..50a8046d 100644 --- a/inventory/types/types.go +++ b/inventory/types/types.go @@ -103,6 +103,8 @@ type ( QiniuUploadCdn bool `json:"qiniu_upload_cdn,omitempty"` // ChunkConcurrency the number of chunks to upload concurrently. ChunkConcurrency int `json:"chunk_concurrency,omitempty"` + // Whether to enable file encryption. + Encryption bool `json:"encryption,omitempty"` } FileType int @@ -154,8 +156,18 @@ type ( MasterSiteVersion string `json:"master_site_version,omitempty"` } - EntityRecycleOption struct { - UnlinkOnly bool `json:"unlink_only,omitempty"` + EntityProps struct { + UnlinkOnly bool `json:"unlink_only,omitempty"` + EncryptMetadata *EncryptMetadata `json:"encrypt_metadata,omitempty"` + } + + Algorithm string + + EncryptMetadata struct { + Algorithm Algorithm `json:"algorithm"` + Key []byte `json:"key"` + KeyPlainText []byte `json:"key_plain_text,omitempty"` + IV []byte `json:"iv"` } DavAccountProps struct { @@ -347,3 +359,7 @@ const ( ProfileAllShare = ShareLinksInProfileLevel("all_share") ProfileHideShare = ShareLinksInProfileLevel("hide_share") ) + +const ( + AlgorithmAES256CTR Algorithm = "aes-256-ctr" +) diff --git a/pkg/filemanager/driver/local/entity.go b/pkg/filemanager/driver/local/entity.go index d725ed45..521a9b8f 100644 --- a/pkg/filemanager/driver/local/entity.go +++ b/pkg/filemanager/driver/local/entity.go @@ -1,13 +1,14 @@ package local import ( + "os" + "time" + "github.com/cloudreve/Cloudreve/v4/ent" "github.com/cloudreve/Cloudreve/v4/inventory/types" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs" "github.com/cloudreve/Cloudreve/v4/pkg/util" "github.com/gofrs/uuid" - "os" - "time" ) // NewLocalFileEntity creates a new local file entity. @@ -73,3 +74,11 @@ func (l *localFileEntity) UploadSessionID() *uuid.UUID { func (l *localFileEntity) Model() *ent.Entity { return nil } + +func (l *localFileEntity) Props() *types.EntityProps { + return nil +} + +func (l *localFileEntity) Encrypted() bool { + return false +} diff --git a/pkg/filemanager/encrypt/aes256ctr.go b/pkg/filemanager/encrypt/aes256ctr.go new file mode 100644 index 00000000..1b21dd53 --- /dev/null +++ b/pkg/filemanager/encrypt/aes256ctr.go @@ -0,0 +1,360 @@ +// Package encrypt provides AES-256-CTR encryption and decryption functionality +// compatible with the JavaScript EncryptedBlob implementation. +// +// # Usage Example +// +// Basic usage with encrypted metadata: +// +// // Create AES256CTR instance +// aes := NewAES256CTR(masterKeyVault) +// +// // Load encrypted metadata (key is encrypted with master key) +// err := aes.LoadMetadata(ctx, encryptedMetadata, masterKeyVault) +// if err != nil { +// return err +// } +// +// // Set encrypted source stream +// err = aes.SetSource(encryptedStream, 0) +// if err != nil { +// return err +// } +// +// // Read decrypted data +// decryptedData, err := io.ReadAll(aes) +// if err != nil { +// return err +// } +// aes.Close() +// +// Usage with plain metadata (already decrypted): +// +// aes := NewAES256CTR(masterKeyVault) +// err := aes.LoadPlainMetadata(plainMetadata) +// err = aes.SetSource(encryptedStream, 0) +// // Read decrypted data... +// +// Usage with counter offset (for chunked/sliced streams): +// +// // If reading from byte offset 1048576 (1MB) of the encrypted file +// aes := NewAES256CTR(masterKeyVault) +// err := aes.LoadPlainMetadata(metadata) +// err = aes.SetSource(encryptedStreamStartingAt1MB, 1048576) +// // This ensures proper counter alignment for correct decryption +// +// Using the Seeker interface (requires seekable source): +// +// aes := NewAES256CTR(masterKeyVault) +// err := aes.LoadPlainMetadata(metadata) +// err = aes.SetSource(seekableEncryptedStream, 0) +// aes.SetSize(totalFileSize) // Required for io.SeekEnd +// +// // Seek to position 1048576 +// newPos, err := aes.Seek(1048576, io.SeekStart) +// // Read from that position... +// +// // Seek relative to current position +// newPos, err = aes.Seek(100, io.SeekCurrent) +// +// // Seek from end (requires SetSize to be called first) +// newPos, err = aes.Seek(-1024, io.SeekEnd) +// +// Using the factory pattern: +// +// factory := NewDecrypterFactory(masterKeyVault) +// decrypter, err := factory(types.AlgorithmAES256CTR) +// if err != nil { +// return err +// } +// err = decrypter.LoadMetadata(ctx, encryptedMetadata, masterKeyVault) +// err = decrypter.SetSource(encryptedStream, 0) +// defer decrypter.Close() +package encrypt + +import ( + "context" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "fmt" + "io" + + "github.com/cloudreve/Cloudreve/v4/inventory/types" +) + +// AES256CTR provides both encryption and decryption for AES-256-CTR. +// It implements both Cryptor and Decrypter interfaces. +type AES256CTR struct { + masterKeyVault MasterEncryptKeyVault + + // Decryption fields + src io.ReadCloser // Source encrypted stream + seeker io.Seeker // Seeker for the source stream + stream cipher.Stream // AES-CTR cipher stream + metadata *types.EncryptMetadata + counterOffset int64 // Byte offset for sliced streams + pos int64 // Current read position relative to counterOffset + size int64 // Total size of encrypted data (for SeekEnd support, -1 if unknown) + eof bool // EOF flag +} + +func NewAES256CTR(masterKeyVault MasterEncryptKeyVault) *AES256CTR { + return &AES256CTR{ + masterKeyVault: masterKeyVault, + size: -1, // Unknown by default + } +} + +func (e *AES256CTR) GenerateMetadata(ctx context.Context) (*types.EncryptMetadata, error) { + // Generate random 32-byte key for AES-256 + key := make([]byte, 32) + if _, err := io.ReadFull(rand.Reader, key); err != nil { + return nil, err + } + + // Generate random 16-byte IV for CTR mode + iv := make([]byte, 16) + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + return nil, err + } + + // Get master key from vault + masterKey, err := e.masterKeyVault.GetMasterKey(ctx) + if err != nil { + return nil, err + } + + // Encrypt the key with master key + encryptedKey, err := EncryptWithMasterKey(masterKey, key) + if err != nil { + return nil, err + } + + return &types.EncryptMetadata{ + Algorithm: types.AlgorithmAES256CTR, + Key: encryptedKey, + KeyPlainText: key, + IV: iv, + }, nil +} + +// LoadMetadata loads and decrypts the encryption metadata using the master key. +func (e *AES256CTR) LoadMetadata(ctx context.Context, encryptedMetadata *types.EncryptMetadata) error { + if encryptedMetadata == nil { + return fmt.Errorf("encryption metadata is nil") + } + + if encryptedMetadata.Algorithm != types.AlgorithmAES256CTR { + return fmt.Errorf("unsupported algorithm: %s", encryptedMetadata.Algorithm) + } + + if len(encryptedMetadata.KeyPlainText) > 0 { + e.metadata = encryptedMetadata + return nil + } + + // Decrypt the encryption key + decryptedKey, err := DecriptKey(ctx, e.masterKeyVault, encryptedMetadata.Key) + if err != nil { + return fmt.Errorf("failed to decrypt encryption key: %w", err) + } + + // Store decrypted metadata + e.metadata = &types.EncryptMetadata{ + Algorithm: encryptedMetadata.Algorithm, + KeyPlainText: decryptedKey, + IV: encryptedMetadata.IV, + } + + return nil +} + +// SetSource sets the encrypted data source and initializes the cipher stream. +// The counterOffset parameter allows for proper decryption of sliced streams, +// where the stream doesn't start at byte 0 of the original file. +// +// For non-block-aligned offsets (offset % 16 != 0), this method advances the +// cipher stream to the correct position within the block to ensure proper decryption. +func (e *AES256CTR) SetSource(src io.ReadCloser, seeker io.Seeker, size, counterOffset int64) error { + if e.metadata == nil { + return fmt.Errorf("metadata not loaded, call LoadMetadata first") + } + + e.src = src + e.seeker = seeker + e.counterOffset = counterOffset + e.pos = 0 // Reset position to start + e.eof = false // Reset EOF flag + e.size = size + + // Initialize cipher stream at counterOffset position + return e.initCipherStream(counterOffset) +} + +// Read implements io.Reader interface to read decrypted data. +// It reads encrypted data from the source and decrypts it on-the-fly. +func (e *AES256CTR) Read(p []byte) (int, error) { + if e.src == nil { + return 0, fmt.Errorf("source not set, call SetSource first") + } + + if e.eof { + return 0, io.EOF + } + + // Read encrypted data from source + n, err := e.src.Read(p) + if err != nil { + if err == io.EOF { + e.eof = true + if n == 0 { + return 0, io.EOF + } + } else { + return n, err + } + } + + // Decrypt data in place + if n > 0 { + e.stream.XORKeyStream(p[:n], p[:n]) + e.pos += int64(n) // Update current position + } + + return n, err +} + +// Close implements io.Closer interface. +func (e *AES256CTR) Close() error { + if e.src != nil { + return e.src.Close() + } + return nil +} + +// Seek implements io.Seeker interface for seeking within the encrypted stream. +// It properly adjusts the AES-CTR counter based on the seek position. +// +// Parameters: +// - offset: byte offset relative to whence +// - whence: io.SeekStart, io.SeekCurrent, or io.SeekEnd +// +// Returns the new absolute position (relative to counterOffset start). +// +// Note: For io.SeekEnd to work, you must call SetSize() first, otherwise it returns an error. +// Also note that seeking requires the underlying source to support seeking (io.Seeker). +func (e *AES256CTR) Seek(offset int64, whence int) (int64, error) { + if e.metadata == nil { + return 0, fmt.Errorf("metadata not loaded, call LoadMetadata first") + } + + if e.src == nil { + return 0, fmt.Errorf("source not set, call SetSource first") + } + + // Check if source supports seeking + if e.seeker == nil { + return 0, fmt.Errorf("source does not support seeking") + } + + // Calculate new absolute position + var newPos int64 + switch whence { + case io.SeekStart: + newPos = offset + case io.SeekCurrent: + newPos = e.pos + offset + case io.SeekEnd: + if e.size < 0 { + return 0, fmt.Errorf("size unknown, call SetSize before using SeekEnd") + } + newPos = e.size + offset + default: + return 0, fmt.Errorf("invalid whence: %d", whence) + } + + // Validate new position + if newPos < 0 { + return 0, fmt.Errorf("negative position: %d", newPos) + } + + // Seek in the underlying source stream + // The absolute position in the source is counterOffset + newPos + absPos := e.counterOffset + newPos + _, err := e.seeker.Seek(absPos, io.SeekStart) + if err != nil { + return 0, fmt.Errorf("failed to seek source: %w", err) + } + + // Reinitialize cipher stream with new counter position + if err := e.initCipherStream(absPos); err != nil { + return 0, fmt.Errorf("failed to reinitialize cipher stream: %w", err) + } + + // Update position and reset EOF flag + e.pos = newPos + e.eof = false + + return newPos, nil +} + +// initCipherStream initializes the cipher stream with proper counter alignment +// for the given absolute byte position. +func (e *AES256CTR) initCipherStream(absolutePosition int64) error { + // Create AES cipher block + block, err := aes.NewCipher(e.metadata.KeyPlainText) + if err != nil { + return fmt.Errorf("failed to create AES cipher: %w", err) + } + + // Create counter value (16 bytes IV) and apply offset for position + counter := make([]byte, 16) + copy(counter, e.metadata.IV) + + // Apply counter offset based on byte position (each block is 16 bytes) + if absolutePosition > 0 { + blockOffset := absolutePosition / 16 + incrementCounter(counter, blockOffset) + } + + // Create CTR cipher stream + e.stream = cipher.NewCTR(block, counter) + + // For non-block-aligned offsets, we need to advance the stream position + // within the current block to match the offset + offsetInBlock := absolutePosition % 16 + if offsetInBlock > 0 { + // Create a dummy buffer to advance the stream + dummy := make([]byte, offsetInBlock) + e.stream.XORKeyStream(dummy, dummy) + } + + return nil +} + +// incrementCounter increments a counter ([]byte) by a given number of blocks. +// This matches the JavaScript implementation's incrementCounter function. +// The counter is treated as a big-endian 128-bit integer. +func incrementCounter(counter []byte, blocks int64) { + // Convert blocks to add into bytes (big-endian) + // We only need to handle the lower 64 bits since blocks is int64 + for i := 15; i >= 0 && blocks > 0; i-- { + // Add the lowest byte of blocks to current counter byte + sum := uint64(counter[i]) + uint64(blocks&0xff) + counter[i] = byte(sum & 0xff) + + // Shift blocks right by 8 bits for next iteration + blocks = blocks >> 8 + + // Add carry from this position to the next + if sum > 0xff { + carry := sum >> 8 + // Propagate carry to higher bytes + for j := i - 1; j >= 0 && carry > 0; j-- { + sum = uint64(counter[j]) + carry + counter[j] = byte(sum & 0xff) + carry = sum >> 8 + } + } + } +} diff --git a/pkg/filemanager/encrypt/encrypt.go b/pkg/filemanager/encrypt/encrypt.go new file mode 100644 index 00000000..2e03d05f --- /dev/null +++ b/pkg/filemanager/encrypt/encrypt.go @@ -0,0 +1,97 @@ +package encrypt + +import ( + "context" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "fmt" + "io" + + "github.com/cloudreve/Cloudreve/v4/inventory/types" +) + +type ( + Cryptor interface { + io.ReadCloser + io.Seeker + // LoadMetadata loads and decrypts the encryption metadata using the master key + LoadMetadata(ctx context.Context, encryptedMetadata *types.EncryptMetadata) error + // SetSource sets the encrypted data source and initializes the cipher stream + SetSource(src io.ReadCloser, seeker io.Seeker, size, counterOffset int64) error + // GenerateMetadata generates a new encryption metadata + GenerateMetadata(ctx context.Context) (*types.EncryptMetadata, error) + } + + CryptorFactory func(algorithm types.Algorithm) (Cryptor, error) +) + +func NewCryptorFactory(masterKeyVault MasterEncryptKeyVault) CryptorFactory { + return func(algorithm types.Algorithm) (Cryptor, error) { + switch algorithm { + case types.AlgorithmAES256CTR: + return NewAES256CTR(masterKeyVault), nil + default: + return nil, fmt.Errorf("unknown algorithm: %s", algorithm) + } + } +} + +// EncryptWithMasterKey encrypts data using the master key with AES-256-CTR +// Returns: [16-byte IV] + [encrypted data] +func EncryptWithMasterKey(masterKey, data []byte) ([]byte, error) { + // Create AES cipher with master key + block, err := aes.NewCipher(masterKey) + if err != nil { + return nil, err + } + + // Generate random IV for encryption + iv := make([]byte, 16) + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + return nil, err + } + + // Encrypt data + stream := cipher.NewCTR(block, iv) + encrypted := make([]byte, len(data)) + stream.XORKeyStream(encrypted, data) + + // Return IV + encrypted data + result := append(iv, encrypted...) + return result, nil +} + +func DecriptKey(ctx context.Context, keyVault MasterEncryptKeyVault, encryptedKey []byte) ([]byte, error) { + masterKey, err := keyVault.GetMasterKey(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get master key: %w", err) + } + return DecryptWithMasterKey(masterKey, encryptedKey) +} + +// DecryptWithMasterKey decrypts data using the master key with AES-256-CTR +// Input format: [16-byte IV] + [encrypted data] +func DecryptWithMasterKey(masterKey, encryptedData []byte) ([]byte, error) { + // Validate input length + if len(encryptedData) < 16 { + return nil, aes.KeySizeError(len(encryptedData)) + } + + // Extract IV and encrypted data + iv := encryptedData[:16] + encrypted := encryptedData[16:] + + // Create AES cipher with master key + block, err := aes.NewCipher(masterKey) + if err != nil { + return nil, err + } + + // Decrypt data + stream := cipher.NewCTR(block, iv) + decrypted := make([]byte, len(encrypted)) + stream.XORKeyStream(decrypted, encrypted) + + return decrypted, nil +} diff --git a/pkg/filemanager/encrypt/masterkey.go b/pkg/filemanager/encrypt/masterkey.go new file mode 100644 index 00000000..d339143c --- /dev/null +++ b/pkg/filemanager/encrypt/masterkey.go @@ -0,0 +1,30 @@ +package encrypt + +import ( + "context" + "errors" + + "github.com/cloudreve/Cloudreve/v4/pkg/setting" +) + +// MasterEncryptKeyVault is a vault for the master encrypt key. +type MasterEncryptKeyVault interface { + GetMasterKey(ctx context.Context) ([]byte, error) +} + +func NewMasterEncryptKeyVault(setting setting.Provider) MasterEncryptKeyVault { + return &settingMasterEncryptKeyVault{setting: setting} +} + +// settingMasterEncryptKeyVault is a vault for the master encrypt key that gets the key from the setting KV. +type settingMasterEncryptKeyVault struct { + setting setting.Provider +} + +func (v *settingMasterEncryptKeyVault) GetMasterKey(ctx context.Context) ([]byte, error) { + key := v.setting.MasterEncryptKey(ctx) + if key == nil { + return nil, errors.New("master encrypt key is not set") + } + return key, nil +} diff --git a/pkg/filemanager/fs/dbfs/dbfs.go b/pkg/filemanager/fs/dbfs/dbfs.go index 7d57c7d6..5292efb9 100644 --- a/pkg/filemanager/fs/dbfs/dbfs.go +++ b/pkg/filemanager/fs/dbfs/dbfs.go @@ -18,6 +18,7 @@ import ( "github.com/cloudreve/Cloudreve/v4/inventory" "github.com/cloudreve/Cloudreve/v4/inventory/types" "github.com/cloudreve/Cloudreve/v4/pkg/cache" + "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/encrypt" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/lock" "github.com/cloudreve/Cloudreve/v4/pkg/hashid" @@ -47,7 +48,7 @@ type ( func NewDatabaseFS(u *ent.User, fileClient inventory.FileClient, shareClient inventory.ShareClient, l logging.Logger, ls lock.LockSystem, settingClient setting.Provider, storagePolicyClient inventory.StoragePolicyClient, hasher hashid.Encoder, userClient inventory.UserClient, - cache, stateKv cache.Driver, directLinkClient inventory.DirectLinkClient) fs.FileSystem { + cache, stateKv cache.Driver, directLinkClient inventory.DirectLinkClient, encryptorFactory encrypt.CryptorFactory) fs.FileSystem { return &DBFS{ user: u, navigators: make(map[string]Navigator), @@ -62,6 +63,7 @@ func NewDatabaseFS(u *ent.User, fileClient inventory.FileClient, shareClient inv cache: cache, stateKv: stateKv, directLinkClient: directLinkClient, + encryptorFactory: encryptorFactory, } } @@ -80,6 +82,7 @@ type DBFS struct { cache cache.Driver stateKv cache.Driver mu sync.Mutex + encryptorFactory encrypt.CryptorFactory } func (f *DBFS) Recycle() { @@ -287,6 +290,7 @@ func (f *DBFS) CreateEntity(ctx context.Context, file fs.File, policy *ent.Stora Source: req.Props.SavePath, Size: req.Props.Size, UploadSessionID: uuid.FromStringOrNil(o.UploadRequest.Props.UploadSessionID), + EncryptMetadata: o.encryptMetadata, }) if err != nil { _ = inventory.Rollback(tx) @@ -617,6 +621,7 @@ func (f *DBFS) createFile(ctx context.Context, parent *File, name string, fileTy ModifiedAt: o.UploadRequest.Props.LastModified, UploadSessionID: uuid.FromStringOrNil(o.UploadRequest.Props.UploadSessionID), Importing: o.UploadRequest.ImportFrom != nil, + EncryptMetadata: o.encryptMetadata, } } @@ -645,6 +650,20 @@ func (f *DBFS) createFile(ctx context.Context, parent *File, name string, fileTy return newFile(parent, file), nil } +func (f *DBFS) generateEncryptMetadata(ctx context.Context, uploadRequest *fs.UploadRequest, policy *ent.StoragePolicy) (*types.EncryptMetadata, error) { + relayEnabled := policy.Settings != nil && policy.Settings.Relay + if (len(uploadRequest.Props.EncryptionSupported) > 0 && uploadRequest.Props.EncryptionSupported[0] == types.AlgorithmAES256CTR) || relayEnabled { + encryptor, err := f.encryptorFactory(types.AlgorithmAES256CTR) + if err != nil { + return nil, fmt.Errorf("failed to get encryptor: %w", err) + } + + return encryptor.GenerateMetadata(ctx) + } + + return nil, nil +} + // getPreferredPolicy tries to get the preferred storage policy for the given file. func (f *DBFS) getPreferredPolicy(ctx context.Context, file *File) (*ent.StoragePolicy, error) { ownerGroup := file.Owner().Edges.Group diff --git a/pkg/filemanager/fs/dbfs/manage.go b/pkg/filemanager/fs/dbfs/manage.go index f0471df5..2ffe171a 100644 --- a/pkg/filemanager/fs/dbfs/manage.go +++ b/pkg/filemanager/fs/dbfs/manage.go @@ -312,9 +312,9 @@ func (f *DBFS) Delete(ctx context.Context, path []*fs.URI, opts ...fs.Option) ([ o.apply(opt) } - var opt *types.EntityRecycleOption + var opt *types.EntityProps if o.UnlinkOnly { - opt = &types.EntityRecycleOption{ + opt = &types.EntityProps{ UnlinkOnly: true, } } @@ -756,7 +756,7 @@ func (f *DBFS) setCurrentVersion(ctx context.Context, target *File, versionId in return nil } -func (f *DBFS) deleteFiles(ctx context.Context, targets map[Navigator][]*File, fc inventory.FileClient, opt *types.EntityRecycleOption) ([]fs.Entity, inventory.StorageDiff, error) { +func (f *DBFS) deleteFiles(ctx context.Context, targets map[Navigator][]*File, fc inventory.FileClient, opt *types.EntityProps) ([]fs.Entity, inventory.StorageDiff, error) { if f.user.Edges.Group == nil { return nil, nil, fmt.Errorf("user group not loaded") } diff --git a/pkg/filemanager/fs/dbfs/options.go b/pkg/filemanager/fs/dbfs/options.go index b4e01224..98b4ccb2 100644 --- a/pkg/filemanager/fs/dbfs/options.go +++ b/pkg/filemanager/fs/dbfs/options.go @@ -2,6 +2,7 @@ package dbfs import ( "github.com/cloudreve/Cloudreve/v4/ent" + "github.com/cloudreve/Cloudreve/v4/inventory/types" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs" ) @@ -26,6 +27,7 @@ type dbfsOption struct { streamListResponseCallback func(parent fs.File, file []fs.File) ancestor *File notRoot bool + encryptMetadata *types.EncryptMetadata } func newDbfsOption() *dbfsOption { @@ -50,6 +52,13 @@ func (f optionFunc) Apply(o any) { } } +// WithEncryptMetadata sets the encrypt metadata for the upload operation. +func WithEncryptMetadata(encryptMetadata *types.EncryptMetadata) fs.Option { + return optionFunc(func(o *dbfsOption) { + o.encryptMetadata = encryptMetadata + }) +} + // WithFilePublicMetadata enables loading file public metadata. func WithFilePublicMetadata() fs.Option { return optionFunc(func(o *dbfsOption) { diff --git a/pkg/filemanager/fs/dbfs/upload.go b/pkg/filemanager/fs/dbfs/upload.go index c289617f..986d2a35 100644 --- a/pkg/filemanager/fs/dbfs/upload.go +++ b/pkg/filemanager/fs/dbfs/upload.go @@ -129,6 +129,20 @@ func (f *DBFS) PrepareUpload(ctx context.Context, req *fs.UploadRequest, opts .. return nil, err } + // Encryption setting + var ( + encryptMetadata *types.EncryptMetadata + ) + if !policy.Settings.Encryption || req.ImportFrom != nil || len(req.Props.EncryptionSupported) == 0 { + req.Props.EncryptionSupported = nil + } else { + res, err := f.generateEncryptMetadata(ctx, req, policy) + if err != nil { + return nil, serializer.NewError(serializer.CodeInternalSetting, "Failed to generate encrypt metadata", err) + } + encryptMetadata = res + } + // validate upload request if err := validateNewFile(req.Props.Uri.Name(), req.Props.Size, policy); err != nil { return nil, err @@ -170,6 +184,7 @@ func (f *DBFS) PrepareUpload(ctx context.Context, req *fs.UploadRequest, opts .. entity, err := f.CreateEntity(ctx, ancestor, policy, entityType, req, WithPreviousVersion(req.Props.PreviousVersion), fs.WithUploadRequest(req), + WithEncryptMetadata(encryptMetadata), WithRemoveStaleEntities(), ) if err != nil { @@ -185,6 +200,7 @@ func (f *DBFS) PrepareUpload(ctx context.Context, req *fs.UploadRequest, opts .. WithPreferredStoragePolicy(policy), WithErrorOnConflict(), WithAncestor(ancestor), + WithEncryptMetadata(encryptMetadata), ) if err != nil { _ = inventory.Rollback(dbTx) @@ -215,14 +231,15 @@ func (f *DBFS) PrepareUpload(ctx context.Context, req *fs.UploadRequest, opts .. session := &fs.UploadSession{ Props: &fs.UploadProps{ - Uri: req.Props.Uri, - Size: req.Props.Size, - SavePath: req.Props.SavePath, - LastModified: req.Props.LastModified, - UploadSessionID: req.Props.UploadSessionID, - ExpireAt: req.Props.ExpireAt, - EntityType: req.Props.EntityType, - Metadata: req.Props.Metadata, + Uri: req.Props.Uri, + Size: req.Props.Size, + SavePath: req.Props.SavePath, + LastModified: req.Props.LastModified, + UploadSessionID: req.Props.UploadSessionID, + ExpireAt: req.Props.ExpireAt, + EntityType: req.Props.EntityType, + Metadata: req.Props.Metadata, + ClientSideEncrypted: req.Props.ClientSideEncrypted, }, FileID: fileId, NewFileCreated: !fileExisted, @@ -234,6 +251,10 @@ func (f *DBFS) PrepareUpload(ctx context.Context, req *fs.UploadRequest, opts .. LockToken: lockToken, // Prevent lock being released. } + if encryptMetadata != nil { + session.EncryptMetadata = encryptMetadata + } + // TODO: frontend should create new upload session if resumed session does not exist. return session, nil } diff --git a/pkg/filemanager/fs/fs.go b/pkg/filemanager/fs/fs.go index 8536c98c..80ffa2d4 100644 --- a/pkg/filemanager/fs/fs.go +++ b/pkg/filemanager/fs/fs.go @@ -183,6 +183,8 @@ type ( UploadSessionID() *uuid.UUID CreatedBy() *ent.User Model() *ent.Entity + Props() *types.EntityProps + Encrypted() bool } FileExtendedInfo struct { @@ -238,38 +240,40 @@ type ( // UploadCredential for uploading files in client side. UploadCredential struct { - SessionID string `json:"session_id"` - ChunkSize int64 `json:"chunk_size"` // 分块大小,0 为部分快 - Expires int64 `json:"expires"` // 上传凭证过期时间, Unix 时间戳 - UploadURLs []string `json:"upload_urls,omitempty"` - Credential string `json:"credential,omitempty"` - UploadID string `json:"uploadID,omitempty"` - Callback string `json:"callback,omitempty"` - Uri string `json:"uri,omitempty"` // 存储路径 - AccessKey string `json:"ak,omitempty"` - KeyTime string `json:"keyTime,omitempty"` // COS用有效期 - CompleteURL string `json:"completeURL,omitempty"` - StoragePolicy *ent.StoragePolicy - CallbackSecret string `json:"callback_secret,omitempty"` - MimeType string `json:"mime_type,omitempty"` // Expected mimetype - UploadPolicy string `json:"upload_policy,omitempty"` // Upyun upload policy + SessionID string `json:"session_id"` + ChunkSize int64 `json:"chunk_size"` // 分块大小,0 为部分快 + Expires int64 `json:"expires"` // 上传凭证过期时间, Unix 时间戳 + UploadURLs []string `json:"upload_urls,omitempty"` + Credential string `json:"credential,omitempty"` + UploadID string `json:"uploadID,omitempty"` + Callback string `json:"callback,omitempty"` + Uri string `json:"uri,omitempty"` // 存储路径 + AccessKey string `json:"ak,omitempty"` + KeyTime string `json:"keyTime,omitempty"` // COS用有效期 + CompleteURL string `json:"completeURL,omitempty"` + StoragePolicy *ent.StoragePolicy + CallbackSecret string `json:"callback_secret,omitempty"` + MimeType string `json:"mime_type,omitempty"` // Expected mimetype + UploadPolicy string `json:"upload_policy,omitempty"` // Upyun upload policy + EncryptMetadata *types.EncryptMetadata `json:"encrypt_metadata,omitempty"` } // UploadSession stores the information of an upload session, used in server side. UploadSession struct { - UID int // 发起者 - Policy *ent.StoragePolicy - FileID int // ID of the placeholder file - EntityID int // ID of the new entity - Callback string // 回调 URL 地址 - CallbackSecret string // Callback secret - UploadID string // Multi-part upload ID - UploadURL string - Credential string - ChunkSize int64 - SentinelTaskID int - NewFileCreated bool // If new file is created for this session - Importing bool // If the upload is importing from another file + UID int // 发起者 + Policy *ent.StoragePolicy + FileID int // ID of the placeholder file + EntityID int // ID of the new entity + Callback string // 回调 URL 地址 + CallbackSecret string // Callback secret + UploadID string // Multi-part upload ID + UploadURL string + Credential string + ChunkSize int64 + SentinelTaskID int + NewFileCreated bool // If new file is created for this session + Importing bool // If the upload is importing from another file + EncryptMetadata *types.EncryptMetadata LockToken string // Token of the locked placeholder file Props *UploadProps @@ -288,8 +292,10 @@ type ( PreviousVersion string // EntityType is the type of the entity to be created. If not set, a new file will be created // with a default version entity. This will be set in update request for existing files. - EntityType *types.EntityType - ExpireAt time.Time + EntityType *types.EntityType + ExpireAt time.Time + EncryptionSupported []types.Algorithm + ClientSideEncrypted bool // Whether the file stream is already encrypted by client side. } // FsOption options for underlying file system. @@ -782,6 +788,14 @@ func (e *DbEntity) Model() *ent.Entity { return e.model } +func (e *DbEntity) Props() *types.EntityProps { + return e.model.Props +} + +func (e *DbEntity) Encrypted() bool { + return e.model.Props != nil && e.model.Props.EncryptMetadata != nil +} + func NewEmptyEntity(u *ent.User) Entity { return &DbEntity{ model: &ent.Entity{ diff --git a/pkg/filemanager/manager/entity.go b/pkg/filemanager/manager/entity.go index bf88ef70..8384d113 100644 --- a/pkg/filemanager/manager/entity.go +++ b/pkg/filemanager/manager/entity.go @@ -120,7 +120,7 @@ func (m *manager) GetDirectLink(ctx context.Context, urls ...*fs.URI) ([]DirectL } 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), m.dep.EncryptorFactory()) sourceUrl, err := source.Url(ctx, entitysource.WithSpeedLimit(int64(m.user.Edges.Group.SpeedLimit)), entitysource.WithDisplayName(file.Name()), @@ -182,7 +182,7 @@ func (m *manager) GetUrlForRedirectedDirectLink(ctx context.Context, dl *ent.Dir } source := entitysource.NewEntitySource(primaryEntity, 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), m.dep.EncryptorFactory()) downloadUrl, err := source.Url(ctx, entitysource.WithExpire(o.Expire), entitysource.WithDownload(o.IsDownload), @@ -282,7 +282,7 @@ func (m *manager) GetEntityUrls(ctx context.Context, args []GetEntityUrlArgs, op // Cache miss, Generate new url 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), m.dep.EncryptorFactory()) downloadUrl, err := source.Url(ctx, entitysource.WithExpire(o.Expire), entitysource.WithDownload(o.IsDownload), @@ -349,7 +349,7 @@ func (m *manager) GetEntitySource(ctx context.Context, entityID int, opts ...fs. } return entitysource.NewEntitySource(entity, handler, policy, m.auth, m.settings, m.hasher, m.dep.RequestClient(), m.l, - m.config, m.dep.MimeDetector(ctx), entitysource.WithContext(ctx), entitysource.WithThumb(o.IsThumb)), nil + m.config, m.dep.MimeDetector(ctx), m.dep.EncryptorFactory(), entitysource.WithContext(ctx), entitysource.WithThumb(o.IsThumb)), nil } func (l *manager) SetCurrentVersion(ctx context.Context, path *fs.URI, version int) error { diff --git a/pkg/filemanager/manager/entitysource/entitysource.go b/pkg/filemanager/manager/entitysource/entitysource.go index 2e640edb..e00b2acf 100644 --- a/pkg/filemanager/manager/entitysource/entitysource.go +++ b/pkg/filemanager/manager/entitysource/entitysource.go @@ -22,6 +22,7 @@ import ( "github.com/cloudreve/Cloudreve/v4/pkg/conf" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver/local" + "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/encrypt" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs/mime" "github.com/cloudreve/Cloudreve/v4/pkg/hashid" @@ -83,6 +84,7 @@ type EntitySourceOptions struct { OneTimeDownloadKey string Ctx context.Context IsThumb bool + DisableCryptor bool } type EntityUrl struct { @@ -143,22 +145,31 @@ func WithThumb(isThumb bool) EntitySourceOption { }) } +// WithDisableCryptor disable cryptor for file source, file stream will be +// presented as is. +func WithDisableCryptor() EntitySourceOption { + return EntitySourceOptionFunc(func(option any) { + option.(*EntitySourceOptions).DisableCryptor = true + }) +} + func (f EntitySourceOptionFunc) Apply(option any) { f(option) } type ( entitySource struct { - e fs.Entity - handler driver.Handler - policy *ent.StoragePolicy - generalAuth auth.Auth - settings setting.Provider - hasher hashid.Encoder - c request.Client - l logging.Logger - config conf.ConfigProvider - mime mime.MimeDetector + e fs.Entity + handler driver.Handler + policy *ent.StoragePolicy + generalAuth auth.Auth + settings setting.Provider + hasher hashid.Encoder + c request.Client + l logging.Logger + config conf.ConfigProvider + mime mime.MimeDetector + encryptorFactory encrypt.CryptorFactory rsc io.ReadCloser pos int64 @@ -197,20 +208,22 @@ func NewEntitySource( l logging.Logger, config conf.ConfigProvider, mime mime.MimeDetector, + encryptorFactory encrypt.CryptorFactory, opts ...EntitySourceOption, ) EntitySource { s := &entitySource{ - e: e, - handler: handler, - policy: policy, - generalAuth: generalAuth, - settings: settings, - hasher: hasher, - c: c, - config: config, - l: l, - mime: mime, - o: &EntitySourceOptions{}, + e: e, + handler: handler, + policy: policy, + generalAuth: generalAuth, + settings: settings, + hasher: hasher, + c: c, + config: config, + l: l, + mime: mime, + encryptorFactory: encryptorFactory, + o: &EntitySourceOptions{}, } for _, opt := range opts { opt.Apply(s.o) @@ -237,7 +250,7 @@ func (f *entitySource) CloneToLocalSrc(t types.EntityType, src string) (EntitySo policy := &ent.StoragePolicy{Type: types.PolicyTypeLocal} handler := local.New(policy, f.l, f.config) - newSrc := NewEntitySource(e, handler, policy, f.generalAuth, f.settings, f.hasher, f.c, f.l, f.config, f.mime).(*entitySource) + newSrc := NewEntitySource(e, handler, policy, f.generalAuth, f.settings, f.hasher, f.c, f.l, f.config, f.mime, f.encryptorFactory).(*entitySource) newSrc.o = f.o return newSrc, nil } @@ -328,6 +341,20 @@ func (f *entitySource) Serve(w http.ResponseWriter, r *http.Request, opts ...Ent response.Header.Del("ETag") response.Header.Del("Content-Disposition") response.Header.Del("Cache-Control") + + // If the response is successful, decrypt the body if needed + if response.StatusCode >= 200 && response.StatusCode < 300 { + // Parse offset from Content-Range header if present + offset := parseContentRangeOffset(response.Header.Get("Content-Range")) + + body, err := f.getDecryptedRsc(response.Body, offset) + if err != nil { + return fmt.Errorf("failed to get decrypted rsc: %w", err) + } + + response.Body = body + } + logging.Request(f.l, false, response.StatusCode, @@ -554,7 +581,7 @@ func (f *entitySource) ShouldInternalProxy(opts ...EntitySourceOption) bool { } handlerCapability := f.handler.Capabilities() return f.e.ID() == 0 || handlerCapability.StaticFeatures.Enabled(int(driver.HandlerCapabilityProxyRequired)) || - f.policy.Settings.InternalProxy && !f.o.NoInternalProxy + (f.policy.Settings.InternalProxy || f.e.Encrypted()) && !f.o.NoInternalProxy } func (f *entitySource) Url(ctx context.Context, opts ...EntitySourceOption) (*EntityUrl, error) { @@ -582,6 +609,7 @@ func (f *entitySource) Url(ctx context.Context, opts ...EntitySourceOption) (*En // 1. Internal proxy is required by driver's definition // 2. Internal proxy is enabled in Policy setting and not disabled by option // 3. It's an empty entity. + // 4. The entity is encrypted and internal proxy not disabled by option handlerCapability := f.handler.Capabilities() if f.ShouldInternalProxy() { siteUrl := f.settings.SiteURL(ctx) @@ -655,6 +683,7 @@ func (f *entitySource) resetRequest() error { func (f *entitySource) getRsc(pos int64) (io.ReadCloser, error) { // For inbound files, we can use the handler to open the file directly + var rsc io.ReadCloser if f.IsLocal() { file, err := f.handler.Open(f.o.Ctx, f.e.Source()) if err != nil { @@ -670,46 +699,75 @@ func (f *entitySource) getRsc(pos int64) (io.ReadCloser, error) { if f.o.SpeedLimit > 0 { bucket := ratelimit.NewBucketWithRate(float64(f.o.SpeedLimit), f.o.SpeedLimit) - return lrs{file, ratelimit.Reader(file, bucket)}, nil + rsc = lrs{file, ratelimit.Reader(file, bucket)} + } else { + rsc = file + } + } else { + var urlStr string + now := time.Now() + + // Check if we have a valid cached URL and expiry + if f.cachedUrl != "" && now.Before(f.cachedExpiry.Add(-time.Minute)) { + // Use cached URL if it's still valid (with 1 minute buffer before expiry) + urlStr = f.cachedUrl } else { - return file, nil + // Generate new URL and cache it + expire := now.Add(defaultUrlExpire) + u, err := f.Url(driver.WithForcePublicEndpoint(f.o.Ctx, false), WithNoInternalProxy(), WithExpire(&expire)) + if err != nil { + return nil, fmt.Errorf("failed to generate download url: %w", err) + } + + // Cache the URL and expiry + f.cachedUrl = u.Url + f.cachedExpiry = expire + urlStr = u.Url + } + + h := http.Header{} + h.Set("Range", fmt.Sprintf("bytes=%d-", pos)) + resp := f.c.Request(http.MethodGet, urlStr, nil, + request.WithContext(f.o.Ctx), + request.WithLogger(f.l), + request.WithHeader(h), + ).CheckHTTPResponse(http.StatusOK, http.StatusPartialContent) + if resp.Err != nil { + return nil, fmt.Errorf("failed to request download url: %w", resp.Err) } + rsc = resp.Response.Body + } + + var err error + rsc, err = f.getDecryptedRsc(rsc, pos) + if err != nil { + return nil, fmt.Errorf("failed to get decrypted rsc: %w", err) } - var urlStr string - now := time.Now() + return rsc, nil +} - // Check if we have a valid cached URL and expiry - if f.cachedUrl != "" && now.Before(f.cachedExpiry.Add(-time.Minute)) { - // Use cached URL if it's still valid (with 1 minute buffer before expiry) - urlStr = f.cachedUrl - } else { - // Generate new URL and cache it - expire := now.Add(defaultUrlExpire) - u, err := f.Url(driver.WithForcePublicEndpoint(f.o.Ctx, false), WithNoInternalProxy(), WithExpire(&expire)) +func (f *entitySource) getDecryptedRsc(rsc io.ReadCloser, pos int64) (io.ReadCloser, error) { + props := f.e.Props() + if props != nil && props.EncryptMetadata != nil && !f.o.DisableCryptor { + cryptor, err := f.encryptorFactory(props.EncryptMetadata.Algorithm) if err != nil { - return nil, fmt.Errorf("failed to generate download url: %w", err) + return nil, fmt.Errorf("failed to create decryptor: %w", err) + } + err = cryptor.LoadMetadata(f.o.Ctx, props.EncryptMetadata) + if err != nil { + return nil, fmt.Errorf("failed to load metadata: %w", err) } - // Cache the URL and expiry - f.cachedUrl = u.Url - f.cachedExpiry = expire - urlStr = u.Url - } + if err := cryptor.SetSource(rsc, nil, f.e.Size(), pos); err != nil { + return nil, fmt.Errorf("failed to set source: %w", err) + } - h := http.Header{} - h.Set("Range", fmt.Sprintf("bytes=%d-", pos)) - resp := f.c.Request(http.MethodGet, urlStr, nil, - request.WithContext(f.o.Ctx), - request.WithLogger(f.l), - request.WithHeader(h), - ).CheckHTTPResponse(http.StatusOK, http.StatusPartialContent) - if resp.Err != nil { - return nil, fmt.Errorf("failed to request download url: %w", resp.Err) + return cryptor, nil } - return resp.Response.Body, nil + return rsc, nil } // capExpireTime make sure expire time is not too long or too short (if min or max is set) @@ -1002,6 +1060,33 @@ func sumRangesSize(ranges []httpRange) (size int64) { return } +// parseContentRangeOffset parses the start offset from a Content-Range header. +// Content-Range format: "bytes start-end/total" (e.g., "bytes 100-200/1000") +// Returns 0 if the header is empty, invalid, or cannot be parsed. +func parseContentRangeOffset(contentRange string) int64 { + if contentRange == "" { + return 0 + } + + // Content-Range format: "bytes start-end/total" + if !strings.HasPrefix(contentRange, "bytes ") { + return 0 + } + + rangeSpec := strings.TrimPrefix(contentRange, "bytes ") + dashPos := strings.Index(rangeSpec, "-") + if dashPos <= 0 { + return 0 + } + + start, err := strconv.ParseInt(rangeSpec[:dashPos], 10, 64) + if err != nil { + return 0 + } + + return start +} + // countingWriter counts how many bytes have been written to it. type countingWriter int64 diff --git a/pkg/filemanager/manager/manager.go b/pkg/filemanager/manager/manager.go index bc52b579..bce3208b 100644 --- a/pkg/filemanager/manager/manager.go +++ b/pkg/filemanager/manager/manager.go @@ -147,7 +147,8 @@ func NewFileManager(dep dependency.Dep, u *ent.User) FileManager { user: u, settings: dep.SettingProvider(), fs: dbfs.NewDatabaseFS(u, dep.FileClient(), dep.ShareClient(), dep.Logger(), dep.LockSystem(), - dep.SettingProvider(), dep.StoragePolicyClient(), dep.HashIDEncoder(), dep.UserClient(), dep.KV(), dep.NavigatorStateKV(), dep.DirectLinkClient()), + dep.SettingProvider(), dep.StoragePolicyClient(), dep.HashIDEncoder(), dep.UserClient(), dep.KV(), dep.NavigatorStateKV(), + dep.DirectLinkClient(), dep.EncryptorFactory()), kv: dep.KV(), config: config, auth: dep.GeneralAuth(), diff --git a/pkg/filemanager/manager/recycle.go b/pkg/filemanager/manager/recycle.go index ee9cfbe8..419f3c55 100644 --- a/pkg/filemanager/manager/recycle.go +++ b/pkg/filemanager/manager/recycle.go @@ -222,7 +222,7 @@ func (m *manager) RecycleEntities(ctx context.Context, force bool, entityIDs ... toBeDeletedSrc := lo.Map(lo.Filter(chunk, func(item fs.Entity, index int) bool { // Only delete entities that are not marked as "unlink only" - return item.Model().RecycleOptions == nil || !item.Model().RecycleOptions.UnlinkOnly + return item.Model().Props == nil || !item.Model().Props.UnlinkOnly }), func(entity fs.Entity, index int) string { return entity.Source() }) diff --git a/pkg/filemanager/manager/upload.go b/pkg/filemanager/manager/upload.go index 7d93ed35..136a8ed0 100644 --- a/pkg/filemanager/manager/upload.go +++ b/pkg/filemanager/manager/upload.go @@ -29,7 +29,7 @@ type ( // ConfirmUploadSession confirms whether upload session is valid for upload. ConfirmUploadSession(ctx context.Context, session *fs.UploadSession, chunkIndex int) (fs.File, error) // Upload uploads file data to storage - Upload(ctx context.Context, req *fs.UploadRequest, policy *ent.StoragePolicy) error + Upload(ctx context.Context, req *fs.UploadRequest, policy *ent.StoragePolicy, session *fs.UploadSession) error // CompleteUpload completes upload session and returns file object CompleteUpload(ctx context.Context, session *fs.UploadSession) (fs.File, error) // CancelUploadSession cancels upload session @@ -93,7 +93,8 @@ func (m *manager) CreateUploadSession(ctx context.Context, req *fs.UploadRequest uploadSession.ChunkSize = uploadSession.Policy.Settings.ChunkSize // Create upload credential for underlying storage driver credential := &fs.UploadCredential{} - if !uploadSession.Policy.Settings.Relay || m.stateless { + unrelayed := !uploadSession.Policy.Settings.Relay || m.stateless + if unrelayed { credential, err = d.Token(ctx, uploadSession, req) if err != nil { m.OnUploadFailed(ctx, uploadSession) @@ -103,12 +104,18 @@ func (m *manager) CreateUploadSession(ctx context.Context, req *fs.UploadRequest // For relayed upload, we don't need to create credential uploadSession.ChunkSize = 0 credential.ChunkSize = 0 + credential.EncryptMetadata = nil + uploadSession.Props.ClientSideEncrypted = false } credential.SessionID = uploadSession.Props.UploadSessionID credential.Expires = req.Props.ExpireAt.Unix() credential.StoragePolicy = uploadSession.Policy credential.CallbackSecret = uploadSession.CallbackSecret credential.Uri = uploadSession.Props.Uri.String() + credential.EncryptMetadata = uploadSession.EncryptMetadata + if !unrelayed { + credential.EncryptMetadata = nil + } // If upload sentinel check is required, queue a check task if d.Capabilities().StaticFeatures.Enabled(int(driver.HandlerCapabilityUploadSentinelRequired)) { @@ -178,12 +185,34 @@ func (m *manager) PrepareUpload(ctx context.Context, req *fs.UploadRequest, opts return m.fs.PrepareUpload(ctx, req, opts...) } -func (m *manager) Upload(ctx context.Context, req *fs.UploadRequest, policy *ent.StoragePolicy) error { +func (m *manager) Upload(ctx context.Context, req *fs.UploadRequest, policy *ent.StoragePolicy, session *fs.UploadSession) error { d, err := m.GetStorageDriver(ctx, m.CastStoragePolicyOnSlave(ctx, policy)) if err != nil { return err } + if session != nil && session.EncryptMetadata != nil && !req.Props.ClientSideEncrypted { + cryptor, err := m.dep.EncryptorFactory()(session.EncryptMetadata.Algorithm) + if err != nil { + return fmt.Errorf("failed to create cryptor: %w", err) + } + + err = cryptor.LoadMetadata(ctx, session.EncryptMetadata) + if err != nil { + return fmt.Errorf("failed to load encrypt metadata: %w", err) + } + + if err := cryptor.SetSource(req.File, req.Seeker, req.Props.Size, 0); err != nil { + return fmt.Errorf("failed to set source: %w", err) + } + + req.File = cryptor + + if req.Seeker != nil { + req.Seeker = cryptor + } + } + if err := d.Put(ctx, req); err != nil { return serializer.NewError(serializer.CodeIOFailed, "Failed to upload file", err) } @@ -301,6 +330,8 @@ func (m *manager) Update(ctx context.Context, req *fs.UploadRequest, opts ...fs. } req.Props.UploadSessionID = uuid.Must(uuid.NewV4()).String() + // Sever side supported encryption algorithms + req.Props.EncryptionSupported = []types.Algorithm{types.AlgorithmAES256CTR} if m.stateless { return m.updateStateless(ctx, req, o) @@ -312,7 +343,7 @@ func (m *manager) Update(ctx context.Context, req *fs.UploadRequest, opts ...fs. return nil, fmt.Errorf("faield to prepare uplaod: %w", err) } - if err := m.Upload(ctx, req, uploadSession.Policy); err != nil { + if err := m.Upload(ctx, req, uploadSession.Policy, uploadSession); err != nil { m.OnUploadFailed(ctx, uploadSession) return nil, fmt.Errorf("failed to upload new entity: %w", err) } @@ -368,7 +399,7 @@ func (m *manager) updateStateless(ctx context.Context, req *fs.UploadRequest, o } req.Props = res.Req.Props - if err := m.Upload(ctx, req, res.Session.Policy); err != nil { + if err := m.Upload(ctx, req, res.Session.Policy, res.Session); err != nil { if err := o.Node.OnUploadFailed(ctx, &fs.StatelessOnUploadFailedService{ UploadSession: res.Session, UserID: o.StatelessUserID, diff --git a/pkg/filemanager/workflows/archive.go b/pkg/filemanager/workflows/archive.go index bcef9bb2..378d0591 100644 --- a/pkg/filemanager/workflows/archive.go +++ b/pkg/filemanager/workflows/archive.go @@ -18,6 +18,7 @@ import ( "github.com/cloudreve/Cloudreve/v4/inventory" "github.com/cloudreve/Cloudreve/v4/inventory/types" "github.com/cloudreve/Cloudreve/v4/pkg/cluster" + "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/encrypt" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource" @@ -217,11 +218,18 @@ func (m *CreateArchiveTask) listEntitiesAndSendToSlave(ctx context.Context, dep user := inventory.UserFromContext(ctx) fm := manager.NewFileManager(dep, user) storagePolicyClient := dep.StoragePolicyClient() + masterKey, _ := dep.MasterEncryptKeyVault().GetMasterKey(ctx) failed, err := fm.CreateArchive(ctx, uris, io.Discard, fs.WithDryRun(func(name string, e fs.Entity) { + entityModel, err := decryptEntityKeyIfNeeded(masterKey, e.Model()) + if err != nil { + m.l.Warning("Failed to decrypt entity key for %q: %s", name, err) + return + } + payload.Entities = append(payload.Entities, SlaveCreateArchiveEntity{ - Entity: e.Model(), + Entity: entityModel, Path: name, }) if _, ok := payload.Policies[e.PolicyID()]; !ok { @@ -680,3 +688,18 @@ func (m *SlaveCreateArchiveTask) Progress(ctx context.Context) queue.Progresses return m.progress } + +func decryptEntityKeyIfNeeded(masterKey []byte, entity *ent.Entity) (*ent.Entity, error) { + if entity.Props == nil || entity.Props.EncryptMetadata == nil || entity.Props.EncryptMetadata.KeyPlainText != nil { + return entity, nil + } + + decryptedKey, err := encrypt.DecryptWithMasterKey(masterKey, entity.Props.EncryptMetadata.Key) + if err != nil { + return nil, fmt.Errorf("failed to decrypt entity key: %w", err) + } + + entity.Props.EncryptMetadata.KeyPlainText = decryptedKey + entity.Props.EncryptMetadata.Key = nil + return entity, nil +} diff --git a/pkg/filemanager/workflows/extract.go b/pkg/filemanager/workflows/extract.go index f48d8547..0ca5e6b7 100644 --- a/pkg/filemanager/workflows/extract.go +++ b/pkg/filemanager/workflows/extract.go @@ -194,9 +194,15 @@ func (m *ExtractArchiveTask) createSlaveExtractTask(ctx context.Context, dep dep return task.StatusError, fmt.Errorf("failed to get policy: %w", err) } + masterKey, _ := dep.MasterEncryptKeyVault().GetMasterKey(ctx) + entityModel, err := decryptEntityKeyIfNeeded(masterKey, archiveFile.PrimaryEntity().Model()) + if err != nil { + return task.StatusError, fmt.Errorf("failed to decrypt entity key for archive file %q: %s", archiveFile.DisplayName(), err) + } + payload := &SlaveExtractArchiveTaskState{ FileName: archiveFile.DisplayName(), - Entity: archiveFile.PrimaryEntity().Model(), + Entity: entityModel, Policy: policy, Encoding: m.state.Encoding, Dst: m.state.Dst, diff --git a/pkg/mediameta/ffprobe.go b/pkg/mediameta/ffprobe.go index 369985e3..dc9a27f5 100644 --- a/pkg/mediameta/ffprobe.go +++ b/pkg/mediameta/ffprobe.go @@ -100,7 +100,7 @@ func (f *ffprobeExtractor) Extract(ctx context.Context, ext string, source entit } var input string - if source.IsLocal() { + if source.IsLocal() && !source.Entity().Encrypted() { input = source.LocalPath(ctx) } else { expire := time.Now().Add(UrlExpire) diff --git a/pkg/setting/adapters.go b/pkg/setting/adapters.go index 47d80fa6..de16736f 100644 --- a/pkg/setting/adapters.go +++ b/pkg/setting/adapters.go @@ -2,13 +2,14 @@ package setting import ( "context" + "os" + "strings" + "github.com/cloudreve/Cloudreve/v4/inventory" "github.com/cloudreve/Cloudreve/v4/pkg/cache" "github.com/cloudreve/Cloudreve/v4/pkg/conf" "github.com/cloudreve/Cloudreve/v4/pkg/logging" "github.com/samber/lo" - "os" - "strings" ) const ( diff --git a/pkg/setting/provider.go b/pkg/setting/provider.go index 0deb6dac..b94d9880 100644 --- a/pkg/setting/provider.go +++ b/pkg/setting/provider.go @@ -2,6 +2,7 @@ package setting import ( "context" + "encoding/base64" "encoding/json" "fmt" "net/url" @@ -10,7 +11,6 @@ import ( "time" "github.com/cloudreve/Cloudreve/v4/inventory/types" - "github.com/cloudreve/Cloudreve/v4/pkg/auth/requestinfo" "github.com/cloudreve/Cloudreve/v4/pkg/boolset" ) @@ -208,6 +208,8 @@ type ( CustomHTML(ctx context.Context) *CustomHTML // FFMpegExtraArgs returns the extra arguments of ffmpeg thumb generator. FFMpegExtraArgs(ctx context.Context) string + // MasterEncryptKey returns the master encrypt key. + MasterEncryptKey(ctx context.Context) []byte } UseFirstSiteUrlCtxKey = struct{} ) @@ -235,6 +237,15 @@ type ( } ) +func (s *settingProvider) MasterEncryptKey(ctx context.Context) []byte { + encoded := s.getString(ctx, "encrypt_master_key", "") + key, err := base64.StdEncoding.DecodeString(encoded) + if err != nil { + return nil + } + return key +} + func (s *settingProvider) CustomHTML(ctx context.Context) *CustomHTML { return &CustomHTML{ HeadlessFooter: s.getString(ctx, "headless_footer_html", ""), diff --git a/pkg/thumb/ffmpeg.go b/pkg/thumb/ffmpeg.go index f016b3af..e0742509 100644 --- a/pkg/thumb/ffmpeg.go +++ b/pkg/thumb/ffmpeg.go @@ -9,7 +9,6 @@ import ( "strings" "time" - "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver" "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource" "github.com/cloudreve/Cloudreve/v4/pkg/logging" "github.com/cloudreve/Cloudreve/v4/pkg/setting" @@ -51,10 +50,17 @@ func (f *FfmpegGenerator) Generate(ctx context.Context, es entitysource.EntitySo input := "" expire := time.Now().Add(urlTimeout) - if es.IsLocal() { + if es.IsLocal() && !es.Entity().Encrypted() { input = es.LocalPath(ctx) } else { - src, err := es.Url(driver.WithForcePublicEndpoint(ctx, false), entitysource.WithNoInternalProxy(), entitysource.WithContext(ctx), entitysource.WithExpire(&expire)) + opts := []entitysource.EntitySourceOption{ + entitysource.WithContext(ctx), + entitysource.WithExpire(&expire), + } + if !es.Entity().Encrypted() { + opts = append(opts, entitysource.WithNoInternalProxy()) + } + src, err := es.Url(ctx, opts...) if err != nil { return &Result{Path: tempOutputPath}, fmt.Errorf("failed to get entity url: %w", err) } diff --git a/pkg/thumb/libreoffice.go b/pkg/thumb/libreoffice.go index e0626269..89fbdef4 100644 --- a/pkg/thumb/libreoffice.go +++ b/pkg/thumb/libreoffice.go @@ -42,7 +42,7 @@ func (l *LibreOfficeGenerator) Generate(ctx context.Context, es entitysource.Ent ) tempInputPath := "" - if es.IsLocal() { + if es.IsLocal() && !es.Entity().Encrypted() { tempInputPath = es.LocalPath(ctx) } else { // If not local policy files, download to temp folder diff --git a/pkg/thumb/vips.go b/pkg/thumb/vips.go index ee854d61..bd344fd2 100644 --- a/pkg/thumb/vips.go +++ b/pkg/thumb/vips.go @@ -46,7 +46,7 @@ func (v *VipsGenerator) Generate(ctx context.Context, es entitysource.EntitySour usePipe := true if runtime.GOOS == "windows" { // Pipe IO is not working on Windows for VIPS - if es.IsLocal() { + if es.IsLocal() && !es.Entity().Encrypted() { // escape [ and ] in file name input = fmt.Sprintf("[filename=\"%s\"]", es.LocalPath(ctx)) usePipe = false diff --git a/service/admin/file.go b/service/admin/file.go index aeb3fb01..5ea36059 100644 --- a/service/admin/file.go +++ b/service/admin/file.go @@ -347,7 +347,7 @@ func (s *SingleFileService) Url(c *gin.Context) (string, error) { } es := entitysource.NewEntitySource(fs.NewEntity(primaryEntity), driver, policy, dep.GeneralAuth(), - dep.SettingProvider(), dep.HashIDEncoder(), dep.RequestClient(), dep.Logger(), dep.ConfigProvider(), dep.MimeDetector(ctx)) + dep.SettingProvider(), dep.HashIDEncoder(), dep.RequestClient(), dep.Logger(), dep.ConfigProvider(), dep.MimeDetector(ctx), dep.EncryptorFactory()) expire := time.Now().Add(time.Hour * 1) url, err := es.Url(ctx, entitysource.WithExpire(&expire), entitysource.WithDisplayName(file.Name)) @@ -547,7 +547,7 @@ func (s *SingleEntityService) Url(c *gin.Context) (string, error) { } es := entitysource.NewEntitySource(fs.NewEntity(entity), driver, policy, dep.GeneralAuth(), - dep.SettingProvider(), dep.HashIDEncoder(), dep.RequestClient(), dep.Logger(), dep.ConfigProvider(), dep.MimeDetector(c)) + dep.SettingProvider(), dep.HashIDEncoder(), dep.RequestClient(), dep.Logger(), dep.ConfigProvider(), dep.MimeDetector(c), dep.EncryptorFactory()) expire := time.Now().Add(time.Hour * 1) url, err := es.Url(c, entitysource.WithDownload(true), entitysource.WithExpire(&expire), entitysource.WithDisplayName(path.Base(entity.Source))) diff --git a/service/explorer/response.go b/service/explorer/response.go index ee03137b..c4e76450 100644 --- a/service/explorer/response.go +++ b/service/explorer/response.go @@ -126,37 +126,49 @@ func BuildTaskResponse(task queue.Task, node *ent.Node, hasher hashid.Encoder) * } type UploadSessionResponse struct { - SessionID string `json:"session_id"` - UploadID string `json:"upload_id"` - ChunkSize int64 `json:"chunk_size"` // 分块大小,0 为部分快 - Expires int64 `json:"expires"` // 上传凭证过期时间, Unix 时间戳 - UploadURLs []string `json:"upload_urls,omitempty"` - Credential string `json:"credential,omitempty"` - AccessKey string `json:"ak,omitempty"` - KeyTime string `json:"keyTime,omitempty"` // COS用有效期 - CompleteURL string `json:"completeURL,omitempty"` - StoragePolicy *StoragePolicy `json:"storage_policy,omitempty"` - Uri string `json:"uri"` - CallbackSecret string `json:"callback_secret"` - MimeType string `json:"mime_type,omitempty"` - UploadPolicy string `json:"upload_policy,omitempty"` + SessionID string `json:"session_id"` + UploadID string `json:"upload_id"` + ChunkSize int64 `json:"chunk_size"` // 分块大小,0 为部分快 + Expires int64 `json:"expires"` // 上传凭证过期时间, Unix 时间戳 + UploadURLs []string `json:"upload_urls,omitempty"` + Credential string `json:"credential,omitempty"` + AccessKey string `json:"ak,omitempty"` + KeyTime string `json:"keyTime,omitempty"` // COS用有效期 + CompleteURL string `json:"completeURL,omitempty"` + StoragePolicy *StoragePolicy `json:"storage_policy,omitempty"` + Uri string `json:"uri"` + CallbackSecret string `json:"callback_secret"` + MimeType string `json:"mime_type,omitempty"` + UploadPolicy string `json:"upload_policy,omitempty"` + EncryptMetadata *types.EncryptMetadata `json:"encrypt_metadata,omitempty"` } func BuildUploadSessionResponse(session *fs.UploadCredential, hasher hashid.Encoder) *UploadSessionResponse { - return &UploadSessionResponse{ - SessionID: session.SessionID, - ChunkSize: session.ChunkSize, - Expires: session.Expires, - UploadURLs: session.UploadURLs, - Credential: session.Credential, - CompleteURL: session.CompleteURL, - Uri: session.Uri, - UploadID: session.UploadID, - StoragePolicy: BuildStoragePolicy(session.StoragePolicy, hasher), - CallbackSecret: session.CallbackSecret, - MimeType: session.MimeType, - UploadPolicy: session.UploadPolicy, + res := &UploadSessionResponse{ + SessionID: session.SessionID, + ChunkSize: session.ChunkSize, + Expires: session.Expires, + UploadURLs: session.UploadURLs, + Credential: session.Credential, + CompleteURL: session.CompleteURL, + Uri: session.Uri, + UploadID: session.UploadID, + StoragePolicy: BuildStoragePolicy(session.StoragePolicy, hasher), + CallbackSecret: session.CallbackSecret, + MimeType: session.MimeType, + UploadPolicy: session.UploadPolicy, + EncryptMetadata: session.EncryptMetadata, } + + if session.EncryptMetadata != nil { + res.EncryptMetadata = &types.EncryptMetadata{ + Algorithm: session.EncryptMetadata.Algorithm, + KeyPlainText: session.EncryptMetadata.KeyPlainText, + IV: session.EncryptMetadata.IV, + } + } + + return res } // WopiFileInfo Response for `CheckFileInfo` @@ -270,6 +282,7 @@ type StoragePolicy struct { MaxSize int64 `json:"max_size"` Relay bool `json:"relay,omitempty"` ChunkConcurrency int `json:"chunk_concurrency,omitempty"` + Encryption bool `json:"encryption,omitempty"` } type Entity struct { @@ -469,6 +482,7 @@ func BuildStoragePolicy(sp *ent.StoragePolicy, hasher hashid.Encoder) *StoragePo MaxSize: sp.MaxSize, Relay: sp.Settings.Relay, ChunkConcurrency: sp.Settings.ChunkConcurrency, + Encryption: sp.Settings.Encryption, } if sp.Settings.IsFileTypeDenyList { diff --git a/service/explorer/upload.go b/service/explorer/upload.go index 54b2a8b5..73e24646 100644 --- a/service/explorer/upload.go +++ b/service/explorer/upload.go @@ -3,6 +3,9 @@ package explorer import ( "context" "fmt" + "strconv" + "time" + "github.com/cloudreve/Cloudreve/v4/application/dependency" "github.com/cloudreve/Cloudreve/v4/inventory" "github.com/cloudreve/Cloudreve/v4/inventory/types" @@ -13,21 +16,20 @@ import ( "github.com/cloudreve/Cloudreve/v4/pkg/request" "github.com/cloudreve/Cloudreve/v4/pkg/serializer" "github.com/gin-gonic/gin" - "strconv" - "time" ) // CreateUploadSessionService 获取上传凭证服务 type ( CreateUploadSessionParameterCtx struct{} CreateUploadSessionService struct { - Uri string `json:"uri" binding:"required"` - Size int64 `json:"size" binding:"min=0"` - LastModified int64 `json:"last_modified"` - MimeType string `json:"mime_type"` - PolicyID string `json:"policy_id"` - Metadata map[string]string `json:"metadata" binding:"max=256"` - EntityType string `json:"entity_type" binding:"eq=|eq=live_photo|eq=version"` + Uri string `json:"uri" binding:"required"` + Size int64 `json:"size" binding:"min=0"` + LastModified int64 `json:"last_modified"` + MimeType string `json:"mime_type"` + PolicyID string `json:"policy_id"` + Metadata map[string]string `json:"metadata" binding:"max=256"` + EntityType string `json:"entity_type" binding:"eq=|eq=live_photo|eq=version"` + EncryptionSupported []types.Algorithm `json:"encryption_supported"` } ) @@ -68,6 +70,8 @@ func (service *CreateUploadSessionService) Create(c context.Context) (*UploadSes Metadata: service.Metadata, EntityType: entityType, PreferredStoragePolicy: policyId, + EncryptionSupported: service.EncryptionSupported, + ClientSideEncrypted: len(service.EncryptionSupported) > 0, }, } @@ -133,6 +137,7 @@ func (service *UploadService) SlaveUpload(c *gin.Context) error { } uploadSession := uploadSessionRaw.(fs.UploadSession) + uploadSession.Props.ClientSideEncrypted = true // Parse chunk index from query service.Index, _ = strconv.Atoi(c.Query("chunk")) @@ -175,7 +180,7 @@ func processChunkUpload(c *gin.Context, m manager.FileManager, session *fs.Uploa // 执行上传 ctx := context.WithValue(c, cluster.SlaveNodeIDCtx{}, strconv.Itoa(session.Policy.NodeID)) - err = m.Upload(ctx, req, session.Policy) + err = m.Upload(ctx, req, session.Policy, session) if err != nil { return err } From e3580d93517982dc37ed07ecf06118b1ced47041 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Fri, 24 Oct 2025 15:04:54 +0800 Subject: [PATCH 65/74] feat(encryption): add UI and settings for file encryption --- application/dependency/dependency.go | 13 +- application/dependency/options.go | 9 +- assets | 2 +- cmd/masterkey.go | 230 +++++++++++++++++++++++++++ cmd/root.go | 6 +- cmd/server.go | 5 - inventory/setting.go | 8 + inventory/types/types.go | 12 +- pkg/filemanager/encrypt/aes256ctr.go | 6 +- pkg/filemanager/encrypt/encrypt.go | 6 +- pkg/filemanager/encrypt/masterkey.go | 79 ++++++++- pkg/filemanager/fs/dbfs/dbfs.go | 4 +- pkg/filemanager/fs/fs.go | 2 +- pkg/filemanager/manager/entity.go | 8 +- pkg/filemanager/manager/manager.go | 2 +- pkg/filemanager/manager/thumbnail.go | 3 +- pkg/filemanager/manager/upload.go | 4 +- pkg/filemanager/workflows/archive.go | 2 +- pkg/filemanager/workflows/extract.go | 2 +- pkg/setting/provider.go | 18 +++ pkg/setting/types.go | 8 + service/admin/file.go | 4 +- service/admin/site.go | 3 +- service/basic/site.go | 41 ++--- service/explorer/response.go | 8 + service/explorer/upload.go | 2 +- 26 files changed, 415 insertions(+), 72 deletions(-) create mode 100644 cmd/masterkey.go diff --git a/application/dependency/dependency.go b/application/dependency/dependency.go index 445638cc..ae965538 100644 --- a/application/dependency/dependency.go +++ b/application/dependency/dependency.go @@ -131,9 +131,9 @@ type Dep interface { // UAParser Get a singleton uaparser.Parser instance for user agent parsing. UAParser() *uaparser.Parser // MasterEncryptKeyVault Get a singleton encrypt.MasterEncryptKeyVault instance for master encrypt key vault. - MasterEncryptKeyVault() encrypt.MasterEncryptKeyVault + MasterEncryptKeyVault(ctx context.Context) encrypt.MasterEncryptKeyVault // EncryptorFactory Get a new encrypt.CryptorFactory instance. - EncryptorFactory() encrypt.CryptorFactory + EncryptorFactory(ctx context.Context) encrypt.CryptorFactory } type dependency struct { @@ -183,7 +183,6 @@ type dependency struct { configPath string isPro bool requiredDbVersion string - licenseKey string // Protects inner deps that can be reloaded at runtime. mu sync.Mutex @@ -212,17 +211,17 @@ func (d *dependency) RequestClient(opts ...request.Option) request.Client { return request.NewClient(d.ConfigProvider(), opts...) } -func (d *dependency) MasterEncryptKeyVault() encrypt.MasterEncryptKeyVault { +func (d *dependency) MasterEncryptKeyVault(ctx context.Context) encrypt.MasterEncryptKeyVault { if d.masterEncryptKeyVault != nil { return d.masterEncryptKeyVault } - d.masterEncryptKeyVault = encrypt.NewMasterEncryptKeyVault(d.SettingProvider()) + d.masterEncryptKeyVault = encrypt.NewMasterEncryptKeyVault(ctx, d.SettingProvider()) return d.masterEncryptKeyVault } -func (d *dependency) EncryptorFactory() encrypt.CryptorFactory { - return encrypt.NewCryptorFactory(d.MasterEncryptKeyVault()) +func (d *dependency) EncryptorFactory(ctx context.Context) encrypt.CryptorFactory { + return encrypt.NewCryptorFactory(d.MasterEncryptKeyVault(ctx)) } func (d *dependency) WebAuthn(ctx context.Context) (*webauthn.WebAuthn, error) { diff --git a/application/dependency/options.go b/application/dependency/options.go index 9c92319e..7046670e 100644 --- a/application/dependency/options.go +++ b/application/dependency/options.go @@ -1,6 +1,8 @@ package dependency import ( + "io/fs" + "github.com/cloudreve/Cloudreve/v4/ent" "github.com/cloudreve/Cloudreve/v4/inventory" "github.com/cloudreve/Cloudreve/v4/pkg/auth" @@ -11,7 +13,6 @@ import ( "github.com/cloudreve/Cloudreve/v4/pkg/logging" "github.com/cloudreve/Cloudreve/v4/pkg/setting" "github.com/gin-contrib/static" - "io/fs" ) // Option 发送请求的额外设置 @@ -67,12 +68,6 @@ func WithProFlag(c bool) Option { }) } -func WithLicenseKey(c string) Option { - return optionFunc(func(o *dependency) { - o.licenseKey = c - }) -} - // WithRawEntClient Set the default raw ent client. func WithRawEntClient(c *ent.Client) Option { return optionFunc(func(o *dependency) { diff --git a/assets b/assets index 1c9dd8d9..8b91fca9 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 1c9dd8d9adbb6842b404ecd908a625ce519b754f +Subproject commit 8b91fca9291b58edd100949954039fc71524f97d diff --git a/cmd/masterkey.go b/cmd/masterkey.go new file mode 100644 index 00000000..ede35bce --- /dev/null +++ b/cmd/masterkey.go @@ -0,0 +1,230 @@ +package cmd + +import ( + "context" + "crypto/rand" + "encoding/base64" + "fmt" + "io" + "os" + + "github.com/cloudreve/Cloudreve/v4/application/dependency" + "github.com/cloudreve/Cloudreve/v4/ent/entity" + "github.com/cloudreve/Cloudreve/v4/inventory/types" + "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/encrypt" + "github.com/cloudreve/Cloudreve/v4/pkg/setting" + "github.com/spf13/cobra" +) + +var ( + outputToFile string + newMasterKeyFile string +) + +func init() { + rootCmd.AddCommand(masterKeyCmd) + masterKeyCmd.AddCommand(masterKeyGenerateCmd) + masterKeyCmd.AddCommand(masterKeyGetCmd) + masterKeyCmd.AddCommand(masterKeyRotateCmd) + + masterKeyGenerateCmd.Flags().StringVarP(&outputToFile, "output", "o", "", "Output master key to file instead of stdout") + masterKeyRotateCmd.Flags().StringVarP(&newMasterKeyFile, "new-key", "n", "", "Path to file containing the new master key (base64 encoded).") +} + +var masterKeyCmd = &cobra.Command{ + Use: "master-key", + Short: "Master encryption key management", + Long: "Manage master encryption keys for file encryption. Use subcommands to generate, get, or rotate keys.", + Run: func(cmd *cobra.Command, args []string) { + _ = cmd.Help() + }, +} + +var masterKeyGenerateCmd = &cobra.Command{ + Use: "generate", + Short: "Generate a new master encryption key", + Long: "Generate a new random 32-byte (256-bit) master encryption key and output it in base64 format.", + Run: func(cmd *cobra.Command, args []string) { + // Generate 32-byte random key + key := make([]byte, 32) + if _, err := io.ReadFull(rand.Reader, key); err != nil { + fmt.Fprintf(os.Stderr, "Error: Failed to generate random key: %v\n", err) + os.Exit(1) + } + + // Encode to base64 + encodedKey := base64.StdEncoding.EncodeToString(key) + + if outputToFile != "" { + // Write to file + if err := os.WriteFile(outputToFile, []byte(encodedKey), 0600); err != nil { + fmt.Fprintf(os.Stderr, "Error: Failed to write key to file: %v\n", err) + os.Exit(1) + } + fmt.Printf("Master key generated and saved to: %s\n", outputToFile) + } else { + // Output to stdout + fmt.Println(encodedKey) + } + }, +} + +var masterKeyGetCmd = &cobra.Command{ + Use: "get", + Short: "Get the current master encryption key", + Long: "Retrieve and display the current master encryption key from the configured vault (setting, env, or file).", + Run: func(cmd *cobra.Command, args []string) { + ctx := context.Background() + dep := dependency.NewDependency( + dependency.WithConfigPath(confPath), + ) + logger := dep.Logger() + + // Get the master key vault + vault := encrypt.NewMasterEncryptKeyVault(ctx, dep.SettingProvider()) + + // Retrieve the master key + key, err := vault.GetMasterKey(ctx) + if err != nil { + logger.Error("Failed to get master key: %s", err) + os.Exit(1) + } + + // Encode to base64 and display + encodedKey := base64.StdEncoding.EncodeToString(key) + fmt.Println("") + fmt.Println(encodedKey) + }, +} + +var masterKeyRotateCmd = &cobra.Command{ + Use: "rotate", + Short: "Rotate the master encryption key", + Long: `Rotate the master encryption key by re-encrypting all encrypted file keys with a new master key. +This operation: +1. Retrieves the current master key +2. Loads a new master key from file +3. Re-encrypts all file encryption keys with the new master key +4. Updates the master key in the settings database + +Warning: This is a critical operation. Make sure to backup your database before proceeding.`, + Run: func(cmd *cobra.Command, args []string) { + ctx := context.Background() + dep := dependency.NewDependency( + dependency.WithConfigPath(confPath), + ) + logger := dep.Logger() + + logger.Info("Starting master key rotation...") + + // Get the old master key + vault := encrypt.NewMasterEncryptKeyVault(ctx, dep.SettingProvider()) + oldMasterKey, err := vault.GetMasterKey(ctx) + if err != nil { + logger.Error("Failed to get current master key: %s", err) + os.Exit(1) + } + logger.Info("Retrieved current master key") + + // Get or generate the new master key + var newMasterKey []byte + // Load from file + keyData, err := os.ReadFile(newMasterKeyFile) + if err != nil { + logger.Error("Failed to read new master key file: %s", err) + os.Exit(1) + } + newMasterKey, err = base64.StdEncoding.DecodeString(string(keyData)) + if err != nil { + logger.Error("Failed to decode new master key: %s", err) + os.Exit(1) + } + if len(newMasterKey) != 32 { + logger.Error("Invalid new master key: must be 32 bytes (256 bits), got %d bytes", len(newMasterKey)) + os.Exit(1) + } + logger.Info("Loaded new master key from file: %s", newMasterKeyFile) + + // Query all entities with encryption metadata + db := dep.DBClient() + entities, err := db.Entity.Query(). + Where(entity.Not(entity.PropsIsNil())). + All(ctx) + if err != nil { + logger.Error("Failed to query entities: %s", err) + os.Exit(1) + } + + logger.Info("Found %d entities to check for encryption", len(entities)) + + // Re-encrypt each entity's encryption key + encryptedCount := 0 + for _, ent := range entities { + if ent.Props == nil || ent.Props.EncryptMetadata == nil { + continue + } + + encMeta := ent.Props.EncryptMetadata + + // Decrypt the file key with old master key + decryptedFileKey, err := encrypt.DecryptWithMasterKey(oldMasterKey, encMeta.Key) + if err != nil { + logger.Error("Failed to decrypt key for entity %d: %s", ent.ID, err) + os.Exit(1) + } + + // Re-encrypt the file key with new master key + newEncryptedKey, err := encrypt.EncryptWithMasterKey(newMasterKey, decryptedFileKey) + if err != nil { + logger.Error("Failed to re-encrypt key for entity %d: %s", ent.ID, err) + os.Exit(1) + } + + // Update the entity + newProps := *ent.Props + newProps.EncryptMetadata = &types.EncryptMetadata{ + Algorithm: encMeta.Algorithm, + Key: newEncryptedKey, + KeyPlainText: nil, // Don't store plaintext + IV: encMeta.IV, + } + + err = db.Entity.UpdateOne(ent). + SetProps(&newProps). + Exec(ctx) + if err != nil { + logger.Error("Failed to update entity %d: %s", ent.ID, err) + os.Exit(1) + } + + encryptedCount++ + } + + logger.Info("Re-encrypted %d file keys", encryptedCount) + + // Update the master key in settings + keyStore := dep.SettingProvider().MasterEncryptKeyVault(ctx) + if keyStore == setting.MasterEncryptKeyVaultTypeSetting { + encodedNewKey := base64.StdEncoding.EncodeToString(newMasterKey) + err = dep.SettingClient().Set(ctx, map[string]string{ + "encrypt_master_key": encodedNewKey, + }) + if err != nil { + logger.Error("Failed to update master key in settings: %s", err) + logger.Error("WARNING: File keys have been re-encrypted but master key update failed!") + logger.Error("Please manually update the encrypt_master_key setting.") + os.Exit(1) + } + } else { + logger.Info("Current master key is stored in %q", keyStore) + if keyStore == setting.MasterEncryptKeyVaultTypeEnv { + logger.Info("Please update the new master encryption key in your \"CR_ENCRYPT_MASTER_KEY\" environment variable.") + } else if keyStore == setting.MasterEncryptKeyVaultTypeFile { + logger.Info("Please update the new master encryption key in your key file: %q", dep.SettingProvider().MasterEncryptKeyFile(ctx)) + } + logger.Info("Last step: Please manually update the new master encryption key in your ENV or key file.") + } + + logger.Info("Master key rotation completed successfully") + }, +} diff --git a/cmd/root.go b/cmd/root.go index 0b7b2ff9..47acb08a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,14 +2,16 @@ package cmd import ( "fmt" + "os" + "github.com/cloudreve/Cloudreve/v4/pkg/util" "github.com/spf13/cobra" "github.com/spf13/pflag" - "os" ) var ( - confPath string + confPath string + licenseKey string ) func init() { diff --git a/cmd/server.go b/cmd/server.go index 1713a31e..08b51178 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -12,10 +12,6 @@ import ( "github.com/spf13/cobra" ) -var ( - licenseKey string -) - func init() { rootCmd.AddCommand(serverCmd) serverCmd.PersistentFlags().StringVarP(&licenseKey, "license-key", "l", "", "License key of your Cloudreve Pro") @@ -29,7 +25,6 @@ var serverCmd = &cobra.Command{ dependency.WithConfigPath(confPath), dependency.WithProFlag(constants.IsProBool), dependency.WithRequiredDbVersion(constants.BackendVersion), - dependency.WithLicenseKey(licenseKey), ) server := application.NewServer(dep) logger := dep.Logger() diff --git a/inventory/setting.go b/inventory/setting.go index 5b926073..8ba62d24 100644 --- a/inventory/setting.go +++ b/inventory/setting.go @@ -665,6 +665,14 @@ var DefaultSettings = map[string]string{ "headless_bottom_html": "", "sidebar_bottom_html": "", "encrypt_master_key": "", + "encrypt_master_key_vault": "setting", + "encrypt_master_key_file": "", + "show_encryption_status": "1", +} + +var RedactedSettings = map[string]struct{}{ + "encrypt_master_key": {}, + "secret_key": {}, } func init() { diff --git a/inventory/types/types.go b/inventory/types/types.go index 50a8046d..3c7a6ff5 100644 --- a/inventory/types/types.go +++ b/inventory/types/types.go @@ -161,13 +161,13 @@ type ( EncryptMetadata *EncryptMetadata `json:"encrypt_metadata,omitempty"` } - Algorithm string + Cipher string EncryptMetadata struct { - Algorithm Algorithm `json:"algorithm"` - Key []byte `json:"key"` - KeyPlainText []byte `json:"key_plain_text,omitempty"` - IV []byte `json:"iv"` + Algorithm Cipher `json:"algorithm"` + Key []byte `json:"key"` + KeyPlainText []byte `json:"key_plain_text,omitempty"` + IV []byte `json:"iv"` } DavAccountProps struct { @@ -361,5 +361,5 @@ const ( ) const ( - AlgorithmAES256CTR Algorithm = "aes-256-ctr" + CipherAES256CTR Cipher = "aes-256-ctr" ) diff --git a/pkg/filemanager/encrypt/aes256ctr.go b/pkg/filemanager/encrypt/aes256ctr.go index 1b21dd53..ff7dfbf1 100644 --- a/pkg/filemanager/encrypt/aes256ctr.go +++ b/pkg/filemanager/encrypt/aes256ctr.go @@ -62,7 +62,7 @@ // Using the factory pattern: // // factory := NewDecrypterFactory(masterKeyVault) -// decrypter, err := factory(types.AlgorithmAES256CTR) +// decrypter, err := factory(types.CipherAES256CTR) // if err != nil { // return err // } @@ -131,7 +131,7 @@ func (e *AES256CTR) GenerateMetadata(ctx context.Context) (*types.EncryptMetadat } return &types.EncryptMetadata{ - Algorithm: types.AlgorithmAES256CTR, + Algorithm: types.CipherAES256CTR, Key: encryptedKey, KeyPlainText: key, IV: iv, @@ -144,7 +144,7 @@ func (e *AES256CTR) LoadMetadata(ctx context.Context, encryptedMetadata *types.E return fmt.Errorf("encryption metadata is nil") } - if encryptedMetadata.Algorithm != types.AlgorithmAES256CTR { + if encryptedMetadata.Algorithm != types.CipherAES256CTR { return fmt.Errorf("unsupported algorithm: %s", encryptedMetadata.Algorithm) } diff --git a/pkg/filemanager/encrypt/encrypt.go b/pkg/filemanager/encrypt/encrypt.go index 2e03d05f..6f3781c6 100644 --- a/pkg/filemanager/encrypt/encrypt.go +++ b/pkg/filemanager/encrypt/encrypt.go @@ -23,13 +23,13 @@ type ( GenerateMetadata(ctx context.Context) (*types.EncryptMetadata, error) } - CryptorFactory func(algorithm types.Algorithm) (Cryptor, error) + CryptorFactory func(algorithm types.Cipher) (Cryptor, error) ) func NewCryptorFactory(masterKeyVault MasterEncryptKeyVault) CryptorFactory { - return func(algorithm types.Algorithm) (Cryptor, error) { + return func(algorithm types.Cipher) (Cryptor, error) { switch algorithm { - case types.AlgorithmAES256CTR: + case types.CipherAES256CTR: return NewAES256CTR(masterKeyVault), nil default: return nil, fmt.Errorf("unknown algorithm: %s", algorithm) diff --git a/pkg/filemanager/encrypt/masterkey.go b/pkg/filemanager/encrypt/masterkey.go index d339143c..22e4f78c 100644 --- a/pkg/filemanager/encrypt/masterkey.go +++ b/pkg/filemanager/encrypt/masterkey.go @@ -2,18 +2,33 @@ package encrypt import ( "context" + "encoding/base64" "errors" + "fmt" + "os" "github.com/cloudreve/Cloudreve/v4/pkg/setting" ) +const ( + EnvMasterEncryptKey = "CR_ENCRYPT_MASTER_KEY" +) + // MasterEncryptKeyVault is a vault for the master encrypt key. type MasterEncryptKeyVault interface { GetMasterKey(ctx context.Context) ([]byte, error) } -func NewMasterEncryptKeyVault(setting setting.Provider) MasterEncryptKeyVault { - return &settingMasterEncryptKeyVault{setting: setting} +func NewMasterEncryptKeyVault(ctx context.Context, settings setting.Provider) MasterEncryptKeyVault { + vaultType := settings.MasterEncryptKeyVault(ctx) + switch vaultType { + case setting.MasterEncryptKeyVaultTypeEnv: + return NewEnvMasterEncryptKeyVault() + case setting.MasterEncryptKeyVaultTypeFile: + return NewFileMasterEncryptKeyVault(settings.MasterEncryptKeyFile(ctx)) + default: + return NewSettingMasterEncryptKeyVault(settings) + } } // settingMasterEncryptKeyVault is a vault for the master encrypt key that gets the key from the setting KV. @@ -21,6 +36,10 @@ type settingMasterEncryptKeyVault struct { setting setting.Provider } +func NewSettingMasterEncryptKeyVault(setting setting.Provider) MasterEncryptKeyVault { + return &settingMasterEncryptKeyVault{setting: setting} +} + func (v *settingMasterEncryptKeyVault) GetMasterKey(ctx context.Context) ([]byte, error) { key := v.setting.MasterEncryptKey(ctx) if key == nil { @@ -28,3 +47,59 @@ func (v *settingMasterEncryptKeyVault) GetMasterKey(ctx context.Context) ([]byte } return key, nil } + +func NewEnvMasterEncryptKeyVault() MasterEncryptKeyVault { + return &envMasterEncryptKeyVault{} +} + +type envMasterEncryptKeyVault struct { +} + +var envMasterKeyCache = []byte{} + +func (v *envMasterEncryptKeyVault) GetMasterKey(ctx context.Context) ([]byte, error) { + if len(envMasterKeyCache) > 0 { + return envMasterKeyCache, nil + } + + key := os.Getenv(EnvMasterEncryptKey) + if key == "" { + return nil, errors.New("master encrypt key is not set") + } + + decodedKey, err := base64.StdEncoding.DecodeString(key) + if err != nil { + return nil, fmt.Errorf("failed to decode master encrypt key: %w", err) + } + + envMasterKeyCache = decodedKey + return decodedKey, nil +} + +func NewFileMasterEncryptKeyVault(path string) MasterEncryptKeyVault { + return &fileMasterEncryptKeyVault{path: path} +} + +var fileMasterKeyCache = []byte{} + +type fileMasterEncryptKeyVault struct { + path string +} + +func (v *fileMasterEncryptKeyVault) GetMasterKey(ctx context.Context) ([]byte, error) { + if len(fileMasterKeyCache) > 0 { + return fileMasterKeyCache, nil + } + + key, err := os.ReadFile(v.path) + if err != nil { + return nil, fmt.Errorf("invalid master encrypt key file") + } + + decodedKey, err := base64.StdEncoding.DecodeString(string(key)) + if err != nil { + return nil, fmt.Errorf("invalid master encrypt key") + } + fileMasterKeyCache = decodedKey + return fileMasterKeyCache, nil +} diff --git a/pkg/filemanager/fs/dbfs/dbfs.go b/pkg/filemanager/fs/dbfs/dbfs.go index 5292efb9..7f346f53 100644 --- a/pkg/filemanager/fs/dbfs/dbfs.go +++ b/pkg/filemanager/fs/dbfs/dbfs.go @@ -652,8 +652,8 @@ func (f *DBFS) createFile(ctx context.Context, parent *File, name string, fileTy func (f *DBFS) generateEncryptMetadata(ctx context.Context, uploadRequest *fs.UploadRequest, policy *ent.StoragePolicy) (*types.EncryptMetadata, error) { relayEnabled := policy.Settings != nil && policy.Settings.Relay - if (len(uploadRequest.Props.EncryptionSupported) > 0 && uploadRequest.Props.EncryptionSupported[0] == types.AlgorithmAES256CTR) || relayEnabled { - encryptor, err := f.encryptorFactory(types.AlgorithmAES256CTR) + if (len(uploadRequest.Props.EncryptionSupported) > 0 && uploadRequest.Props.EncryptionSupported[0] == types.CipherAES256CTR) || relayEnabled { + encryptor, err := f.encryptorFactory(types.CipherAES256CTR) if err != nil { return nil, fmt.Errorf("failed to get encryptor: %w", err) } diff --git a/pkg/filemanager/fs/fs.go b/pkg/filemanager/fs/fs.go index 80ffa2d4..76a6195e 100644 --- a/pkg/filemanager/fs/fs.go +++ b/pkg/filemanager/fs/fs.go @@ -294,7 +294,7 @@ type ( // with a default version entity. This will be set in update request for existing files. EntityType *types.EntityType ExpireAt time.Time - EncryptionSupported []types.Algorithm + EncryptionSupported []types.Cipher ClientSideEncrypted bool // Whether the file stream is already encrypted by client side. } diff --git a/pkg/filemanager/manager/entity.go b/pkg/filemanager/manager/entity.go index 8384d113..e89504dc 100644 --- a/pkg/filemanager/manager/entity.go +++ b/pkg/filemanager/manager/entity.go @@ -120,7 +120,7 @@ func (m *manager) GetDirectLink(ctx context.Context, urls ...*fs.URI) ([]DirectL } source := entitysource.NewEntitySource(target, d, policy, m.auth, m.settings, m.hasher, m.dep.RequestClient(), - m.l, m.config, m.dep.MimeDetector(ctx), m.dep.EncryptorFactory()) + m.l, m.config, m.dep.MimeDetector(ctx), m.dep.EncryptorFactory(ctx)) sourceUrl, err := source.Url(ctx, entitysource.WithSpeedLimit(int64(m.user.Edges.Group.SpeedLimit)), entitysource.WithDisplayName(file.Name()), @@ -182,7 +182,7 @@ func (m *manager) GetUrlForRedirectedDirectLink(ctx context.Context, dl *ent.Dir } source := entitysource.NewEntitySource(primaryEntity, d, policy, m.auth, m.settings, m.hasher, m.dep.RequestClient(), - m.l, m.config, m.dep.MimeDetector(ctx), m.dep.EncryptorFactory()) + m.l, m.config, m.dep.MimeDetector(ctx), m.dep.EncryptorFactory(ctx)) downloadUrl, err := source.Url(ctx, entitysource.WithExpire(o.Expire), entitysource.WithDownload(o.IsDownload), @@ -282,7 +282,7 @@ func (m *manager) GetEntityUrls(ctx context.Context, args []GetEntityUrlArgs, op // Cache miss, Generate new url source := entitysource.NewEntitySource(target, d, policy, m.auth, m.settings, m.hasher, m.dep.RequestClient(), - m.l, m.config, m.dep.MimeDetector(ctx), m.dep.EncryptorFactory()) + m.l, m.config, m.dep.MimeDetector(ctx), m.dep.EncryptorFactory(ctx)) downloadUrl, err := source.Url(ctx, entitysource.WithExpire(o.Expire), entitysource.WithDownload(o.IsDownload), @@ -349,7 +349,7 @@ func (m *manager) GetEntitySource(ctx context.Context, entityID int, opts ...fs. } return entitysource.NewEntitySource(entity, handler, policy, m.auth, m.settings, m.hasher, m.dep.RequestClient(), m.l, - m.config, m.dep.MimeDetector(ctx), m.dep.EncryptorFactory(), entitysource.WithContext(ctx), entitysource.WithThumb(o.IsThumb)), nil + m.config, m.dep.MimeDetector(ctx), m.dep.EncryptorFactory(ctx), entitysource.WithContext(ctx), entitysource.WithThumb(o.IsThumb)), nil } func (l *manager) SetCurrentVersion(ctx context.Context, path *fs.URI, version int) error { diff --git a/pkg/filemanager/manager/manager.go b/pkg/filemanager/manager/manager.go index bce3208b..4ff5b8b2 100644 --- a/pkg/filemanager/manager/manager.go +++ b/pkg/filemanager/manager/manager.go @@ -148,7 +148,7 @@ func NewFileManager(dep dependency.Dep, u *ent.User) FileManager { settings: dep.SettingProvider(), fs: dbfs.NewDatabaseFS(u, dep.FileClient(), dep.ShareClient(), dep.Logger(), dep.LockSystem(), dep.SettingProvider(), dep.StoragePolicyClient(), dep.HashIDEncoder(), dep.UserClient(), dep.KV(), dep.NavigatorStateKV(), - dep.DirectLinkClient(), dep.EncryptorFactory()), + dep.DirectLinkClient(), dep.EncryptorFactory(context.TODO())), kv: dep.KV(), config: config, auth: dep.GeneralAuth(), diff --git a/pkg/filemanager/manager/thumbnail.go b/pkg/filemanager/manager/thumbnail.go index 605d47ee..3d71afcd 100644 --- a/pkg/filemanager/manager/thumbnail.go +++ b/pkg/filemanager/manager/thumbnail.go @@ -64,7 +64,8 @@ func (m *manager) Thumbnail(ctx context.Context, uri *fs.URI) (entitysource.Enti capabilities := handler.Capabilities() // Check if file extension and size is supported by native policy generator. if capabilities.ThumbSupportAllExts || util.IsInExtensionList(capabilities.ThumbSupportedExts, file.DisplayName()) && - (capabilities.ThumbMaxSize == 0 || latest.Size() <= capabilities.ThumbMaxSize) { + (capabilities.ThumbMaxSize == 0 || latest.Size() <= capabilities.ThumbMaxSize) && + !latest.Encrypted() { thumbSource, err := m.GetEntitySource(ctx, 0, fs.WithEntity(latest), fs.WithUseThumb(true)) if err != nil { return nil, fmt.Errorf("failed to get latest entity source: %w", err) diff --git a/pkg/filemanager/manager/upload.go b/pkg/filemanager/manager/upload.go index 136a8ed0..7cd68fbc 100644 --- a/pkg/filemanager/manager/upload.go +++ b/pkg/filemanager/manager/upload.go @@ -192,7 +192,7 @@ func (m *manager) Upload(ctx context.Context, req *fs.UploadRequest, policy *ent } if session != nil && session.EncryptMetadata != nil && !req.Props.ClientSideEncrypted { - cryptor, err := m.dep.EncryptorFactory()(session.EncryptMetadata.Algorithm) + cryptor, err := m.dep.EncryptorFactory(ctx)(session.EncryptMetadata.Algorithm) if err != nil { return fmt.Errorf("failed to create cryptor: %w", err) } @@ -331,7 +331,7 @@ func (m *manager) Update(ctx context.Context, req *fs.UploadRequest, opts ...fs. req.Props.UploadSessionID = uuid.Must(uuid.NewV4()).String() // Sever side supported encryption algorithms - req.Props.EncryptionSupported = []types.Algorithm{types.AlgorithmAES256CTR} + req.Props.EncryptionSupported = []types.Cipher{types.CipherAES256CTR} if m.stateless { return m.updateStateless(ctx, req, o) diff --git a/pkg/filemanager/workflows/archive.go b/pkg/filemanager/workflows/archive.go index 378d0591..53fe21b1 100644 --- a/pkg/filemanager/workflows/archive.go +++ b/pkg/filemanager/workflows/archive.go @@ -218,7 +218,7 @@ func (m *CreateArchiveTask) listEntitiesAndSendToSlave(ctx context.Context, dep user := inventory.UserFromContext(ctx) fm := manager.NewFileManager(dep, user) storagePolicyClient := dep.StoragePolicyClient() - masterKey, _ := dep.MasterEncryptKeyVault().GetMasterKey(ctx) + masterKey, _ := dep.MasterEncryptKeyVault(ctx).GetMasterKey(ctx) failed, err := fm.CreateArchive(ctx, uris, io.Discard, fs.WithDryRun(func(name string, e fs.Entity) { diff --git a/pkg/filemanager/workflows/extract.go b/pkg/filemanager/workflows/extract.go index 0ca5e6b7..9fd2dce4 100644 --- a/pkg/filemanager/workflows/extract.go +++ b/pkg/filemanager/workflows/extract.go @@ -194,7 +194,7 @@ func (m *ExtractArchiveTask) createSlaveExtractTask(ctx context.Context, dep dep return task.StatusError, fmt.Errorf("failed to get policy: %w", err) } - masterKey, _ := dep.MasterEncryptKeyVault().GetMasterKey(ctx) + masterKey, _ := dep.MasterEncryptKeyVault(ctx).GetMasterKey(ctx) entityModel, err := decryptEntityKeyIfNeeded(masterKey, archiveFile.PrimaryEntity().Model()) if err != nil { return task.StatusError, fmt.Errorf("failed to decrypt entity key for archive file %q: %s", archiveFile.DisplayName(), err) diff --git a/pkg/setting/provider.go b/pkg/setting/provider.go index b94d9880..0cf77f00 100644 --- a/pkg/setting/provider.go +++ b/pkg/setting/provider.go @@ -210,6 +210,12 @@ type ( FFMpegExtraArgs(ctx context.Context) string // MasterEncryptKey returns the master encrypt key. MasterEncryptKey(ctx context.Context) []byte + // MasterEncryptKeyVault returns the master encrypt key vault type. + MasterEncryptKeyVault(ctx context.Context) MasterEncryptKeyVaultType + // MasterEncryptKeyFile returns the master encrypt key file. + MasterEncryptKeyFile(ctx context.Context) string + // ShowEncryptionStatus returns true if encryption status is shown. + ShowEncryptionStatus(ctx context.Context) bool } UseFirstSiteUrlCtxKey = struct{} ) @@ -237,6 +243,18 @@ type ( } ) +func (s *settingProvider) ShowEncryptionStatus(ctx context.Context) bool { + return s.getBoolean(ctx, "show_encryption_status", true) +} + +func (s *settingProvider) MasterEncryptKeyFile(ctx context.Context) string { + return s.getString(ctx, "encrypt_master_key_file", "") +} + +func (s *settingProvider) MasterEncryptKeyVault(ctx context.Context) MasterEncryptKeyVaultType { + return MasterEncryptKeyVaultType(s.getString(ctx, "encrypt_master_key_vault", "setting")) +} + func (s *settingProvider) MasterEncryptKey(ctx context.Context) []byte { encoded := s.getString(ctx, "encrypt_master_key", "") key, err := base64.StdEncoding.DecodeString(encoded) diff --git a/pkg/setting/types.go b/pkg/setting/types.go index 849ee589..9a51273e 100644 --- a/pkg/setting/types.go +++ b/pkg/setting/types.go @@ -223,3 +223,11 @@ type CustomHTML struct { HeadlessBody string `json:"headless_bottom,omitempty"` SidebarBottom string `json:"sidebar_bottom,omitempty"` } + +type MasterEncryptKeyVaultType string + +const ( + MasterEncryptKeyVaultTypeSetting = MasterEncryptKeyVaultType("setting") + MasterEncryptKeyVaultTypeEnv = MasterEncryptKeyVaultType("env") + MasterEncryptKeyVaultTypeFile = MasterEncryptKeyVaultType("file") +) diff --git a/service/admin/file.go b/service/admin/file.go index 5ea36059..e7c690f0 100644 --- a/service/admin/file.go +++ b/service/admin/file.go @@ -347,7 +347,7 @@ func (s *SingleFileService) Url(c *gin.Context) (string, error) { } es := entitysource.NewEntitySource(fs.NewEntity(primaryEntity), driver, policy, dep.GeneralAuth(), - dep.SettingProvider(), dep.HashIDEncoder(), dep.RequestClient(), dep.Logger(), dep.ConfigProvider(), dep.MimeDetector(ctx), dep.EncryptorFactory()) + dep.SettingProvider(), dep.HashIDEncoder(), dep.RequestClient(), dep.Logger(), dep.ConfigProvider(), dep.MimeDetector(ctx), dep.EncryptorFactory(ctx)) expire := time.Now().Add(time.Hour * 1) url, err := es.Url(ctx, entitysource.WithExpire(&expire), entitysource.WithDisplayName(file.Name)) @@ -547,7 +547,7 @@ func (s *SingleEntityService) Url(c *gin.Context) (string, error) { } es := entitysource.NewEntitySource(fs.NewEntity(entity), driver, policy, dep.GeneralAuth(), - dep.SettingProvider(), dep.HashIDEncoder(), dep.RequestClient(), dep.Logger(), dep.ConfigProvider(), dep.MimeDetector(c), dep.EncryptorFactory()) + dep.SettingProvider(), dep.HashIDEncoder(), dep.RequestClient(), dep.Logger(), dep.ConfigProvider(), dep.MimeDetector(c), dep.EncryptorFactory(c)) expire := time.Now().Add(time.Hour * 1) url, err := es.Url(c, entitysource.WithDownload(true), entitysource.WithExpire(&expire), entitysource.WithDisplayName(path.Base(entity.Source))) diff --git a/service/admin/site.go b/service/admin/site.go index 7b5a6332..19693cc6 100644 --- a/service/admin/site.go +++ b/service/admin/site.go @@ -193,7 +193,8 @@ type ( func (s *GetSettingService) GetSetting(c *gin.Context) (map[string]string, error) { dep := dependency.FromContext(c) res, err := dep.SettingClient().Gets(c, lo.Filter(s.Keys, func(item string, index int) bool { - return item != "secret_key" + _, ok := inventory.RedactedSettings[strings.ToLower(item)] + return !ok })) if err != nil { return nil, serializer.NewError(serializer.CodeDBError, "Failed to get settings", err) diff --git a/service/basic/site.go b/service/basic/site.go index 5c34a50a..1a61b469 100644 --- a/service/basic/site.go +++ b/service/basic/site.go @@ -43,16 +43,17 @@ type SiteConfig struct { PrivacyPolicyUrl string `json:"privacy_policy_url,omitempty"` // Explorer section - Icons string `json:"icons,omitempty"` - EmojiPreset string `json:"emoji_preset,omitempty"` - MapProvider setting.MapProvider `json:"map_provider,omitempty"` - GoogleMapTileType setting.MapGoogleTileType `json:"google_map_tile_type,omitempty"` - MapboxAK string `json:"mapbox_ak,omitempty"` - FileViewers []types.ViewerGroup `json:"file_viewers,omitempty"` - MaxBatchSize int `json:"max_batch_size,omitempty"` - ThumbnailWidth int `json:"thumbnail_width,omitempty"` - ThumbnailHeight int `json:"thumbnail_height,omitempty"` - CustomProps []types.CustomProps `json:"custom_props,omitempty"` + Icons string `json:"icons,omitempty"` + EmojiPreset string `json:"emoji_preset,omitempty"` + MapProvider setting.MapProvider `json:"map_provider,omitempty"` + GoogleMapTileType setting.MapGoogleTileType `json:"google_map_tile_type,omitempty"` + MapboxAK string `json:"mapbox_ak,omitempty"` + FileViewers []types.ViewerGroup `json:"file_viewers,omitempty"` + MaxBatchSize int `json:"max_batch_size,omitempty"` + ThumbnailWidth int `json:"thumbnail_width,omitempty"` + ThumbnailHeight int `json:"thumbnail_height,omitempty"` + CustomProps []types.CustomProps `json:"custom_props,omitempty"` + ShowEncryptionStatus bool `json:"show_encryption_status,omitempty"` // Thumbnail section ThumbExts []string `json:"thumb_exts,omitempty"` @@ -100,6 +101,7 @@ func (s *GetSettingService) GetSiteConfig(c *gin.Context) (*SiteConfig, error) { fileViewers := settings.FileViewers(c) customProps := settings.CustomProps(c) maxBatchSize := settings.MaxBatchedFile(c) + showEncryptionStatus := settings.ShowEncryptionStatus(c) w, h := settings.ThumbSize(c) for i := range fileViewers { for j := range fileViewers[i].Viewers { @@ -107,15 +109,16 @@ func (s *GetSettingService) GetSiteConfig(c *gin.Context) (*SiteConfig, error) { } } return &SiteConfig{ - MaxBatchSize: maxBatchSize, - FileViewers: fileViewers, - Icons: explorerSettings.Icons, - MapProvider: mapSettings.Provider, - GoogleMapTileType: mapSettings.GoogleTileType, - MapboxAK: mapSettings.MapboxAK, - ThumbnailWidth: w, - ThumbnailHeight: h, - CustomProps: customProps, + MaxBatchSize: maxBatchSize, + FileViewers: fileViewers, + Icons: explorerSettings.Icons, + MapProvider: mapSettings.Provider, + GoogleMapTileType: mapSettings.GoogleTileType, + MapboxAK: mapSettings.MapboxAK, + ThumbnailWidth: w, + ThumbnailHeight: h, + CustomProps: customProps, + ShowEncryptionStatus: showEncryptionStatus, }, nil case "emojis": emojis := settings.EmojiPresets(c) diff --git a/service/explorer/response.go b/service/explorer/response.go index c4e76450..7049d4c8 100644 --- a/service/explorer/response.go +++ b/service/explorer/response.go @@ -292,6 +292,7 @@ type Entity struct { CreatedAt time.Time `json:"created_at"` StoragePolicy *StoragePolicy `json:"storage_policy,omitempty"` CreatedBy *user.User `json:"created_by,omitempty"` + EncryptedWith types.Cipher `json:"encrypted_with,omitempty"` } type Share struct { @@ -452,6 +453,12 @@ func BuildEntity(extendedInfo *fs.FileExtendedInfo, e fs.Entity, hasher hashid.E userRedacted := user.BuildUserRedacted(e.CreatedBy(), user.RedactLevelAnonymous, hasher) u = &userRedacted } + + encryptedWith := types.Cipher("") + if e.Encrypted() { + encryptedWith = e.Props().EncryptMetadata.Algorithm + } + return Entity{ ID: hashid.EncodeEntityID(hasher, e.ID()), Type: e.Type(), @@ -459,6 +466,7 @@ func BuildEntity(extendedInfo *fs.FileExtendedInfo, e fs.Entity, hasher hashid.E StoragePolicy: BuildStoragePolicy(extendedInfo.EntityStoragePolicies[e.PolicyID()], hasher), Size: e.Size(), CreatedBy: u, + EncryptedWith: encryptedWith, } } diff --git a/service/explorer/upload.go b/service/explorer/upload.go index 73e24646..d0046fa7 100644 --- a/service/explorer/upload.go +++ b/service/explorer/upload.go @@ -29,7 +29,7 @@ type ( PolicyID string `json:"policy_id"` Metadata map[string]string `json:"metadata" binding:"max=256"` EntityType string `json:"entity_type" binding:"eq=|eq=live_photo|eq=version"` - EncryptionSupported []types.Algorithm `json:"encryption_supported"` + EncryptionSupported []types.Cipher `json:"encryption_supported"` } ) From f27969d74f06c8a0631630fd91be1ef1aae314a3 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Fri, 24 Oct 2025 15:07:12 +0800 Subject: [PATCH 66/74] chore: update required golang version and gzip middleware --- go.mod | 55 ++++++++++++---------- go.sum | 113 +++++++++++++++++++++++----------------------- routers/router.go | 8 ++-- 3 files changed, 90 insertions(+), 86 deletions(-) diff --git a/go.mod b/go.mod index 259da672..f059f966 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,12 @@ module github.com/cloudreve/Cloudreve/v4 -go 1.23.0 +go 1.24.0 + +toolchain go1.24.9 require ( entgo.io/ent v0.13.0 github.com/Masterminds/semver/v3 v3.3.1 - github.com/abslant/gzip v0.0.9 github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.3.0 github.com/aws/aws-sdk-go v1.31.5 github.com/bodgit/sevenzip v1.6.0 @@ -19,11 +20,12 @@ require ( github.com/dsoprea/go-utility v0.0.0-20200711062821-fab8125e9bdf github.com/fatih/color v1.18.0 github.com/gin-contrib/cors v1.3.0 + github.com/gin-contrib/gzip v1.2.4 github.com/gin-contrib/sessions v1.0.2 github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2 - github.com/gin-gonic/gin v1.10.0 + github.com/gin-gonic/gin v1.11.0 github.com/go-ini/ini v1.50.0 - github.com/go-playground/validator/v10 v10.20.0 + github.com/go-playground/validator/v10 v10.28.0 github.com/go-sql-driver/mysql v1.6.0 github.com/go-webauthn/webauthn v0.11.2 github.com/gofrs/uuid v4.0.0+incompatible @@ -50,16 +52,16 @@ require ( github.com/speps/go-hashids v2.0.0+incompatible github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.11.1 github.com/tencentyun/cos-go-sdk-v5 v0.7.54 github.com/ua-parser/uap-go v0.0.0-20250213224047-9c035f085b90 github.com/upyun/go-sdk v2.1.0+incompatible github.com/wneessen/go-mail v0.6.2 golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e golang.org/x/image v0.0.0-20211028202545-6944b10bf410 - golang.org/x/text v0.23.0 + golang.org/x/text v0.30.0 golang.org/x/time v0.5.0 - golang.org/x/tools v0.24.0 + golang.org/x/tools v0.38.0 modernc.org/sqlite v1.30.0 ) @@ -73,11 +75,11 @@ require ( github.com/bodgit/plumbing v1.3.0 // indirect github.com/bodgit/windows v1.0.1 // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect - github.com/bytedance/sonic v1.11.6 // indirect - github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.14.1 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/clbanning/mxj v1.8.4 // indirect - github.com/cloudwego/base64x v0.1.4 // indirect - github.com/cloudwego/iasm v0.2.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3 // indirect github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect @@ -88,18 +90,19 @@ require ( github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.3 // indirect - github.com/gin-contrib/sse v0.1.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.10 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-errors/errors v1.4.2 // indirect github.com/go-openapi/inflect v0.19.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-webauthn/x v0.1.14 // indirect github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b // indirect - github.com/goccy/go-json v0.10.2 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-tpm v0.9.1 // indirect github.com/gorilla/context v1.1.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect @@ -112,7 +115,7 @@ require ( github.com/jmespath/go-jmespath v0.3.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.11 // indirect - github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/pgzip v1.2.6 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/leodido/go-urn v1.4.0 // indirect @@ -127,25 +130,27 @@ require ( github.com/mozillazg/go-httpheader v0.4.0 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/nwaples/rardecode/v2 v2.1.0 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.55.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/sorairolake/lzip-go v0.3.5 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.12 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect github.com/ulikunitz/xz v0.5.12 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/zclconf/go-cty v1.8.0 // indirect go4.org v0.0.0-20230225012048-214862532bf5 // indirect - golang.org/x/arch v0.8.0 // indirect - golang.org/x/crypto v0.36.0 // indirect - golang.org/x/mod v0.20.0 // indirect - golang.org/x/net v0.38.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.31.0 // indirect - google.golang.org/protobuf v1.34.2 // indirect + golang.org/x/arch v0.22.0 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect diff --git a/go.sum b/go.sum index 72b414e5..f978d9b4 100644 --- a/go.sum +++ b/go.sum @@ -87,8 +87,6 @@ github.com/STARRY-S/zip v0.2.1/go.mod h1:xNvshLODWtC4EJ702g7cTYn13G53o1+X9BWnPFp github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= -github.com/abslant/gzip v0.0.9 h1:zxuOQ8QmPwni7vwgE3EyOygdmeCo2UkCmO5t+7Ms6cA= -github.com/abslant/gzip v0.0.9/go.mod h1:IcN2c50tZn2y54oysNcIavbTAc1s0B2f5TqTEA+WCas= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= @@ -148,10 +146,12 @@ github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4= github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= -github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= -github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= -github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w= +github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/caarlos0/ctrlc v1.0.0/go.mod h1:CdXpj4rmq0q/1Eb44M9zi2nKB0QraNKuRGYGrrHhcQw= github.com/campoy/unique v0.0.0-20180121183637-88950e537e7e/go.mod h1:9IOqJGCPMSc6E5ydlp5NIonxObaeu/Iub/X03EKPVYo= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= @@ -175,10 +175,8 @@ github.com/cloudflare/backoff v0.0.0-20161212185259-647f3cdfc87a/go.mod h1:rzgs2 github.com/cloudflare/cfssl v1.6.1 h1:aIOUjpeuDJOpWjVJFP2ByplF53OgqG8I1S40Ggdlk3g= github.com/cloudflare/cfssl v1.6.1/go.mod h1:ENhCj4Z17+bY2XikpxVmTHDg/C2IsG2Q0ZBeXpAqhCk= github.com/cloudflare/redoctober v0.0.0-20201013214028-99c99a8e7544/go.mod h1:6Se34jNoqrd8bTxrmJB2Bg2aoZ2CdSXonils9NsiNgo= -github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= -github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= -github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= -github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= @@ -297,24 +295,26 @@ github.com/fullstorydev/grpcurl v1.8.0/go.mod h1:Mn2jWbdMrQGJQ8UD62uNyMumT2acsZU github.com/fullstorydev/grpcurl v1.8.1/go.mod h1:3BWhvHZwNO7iLXaQlojdg5NA6SxUDePli4ecpK1N7gw= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= -github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= +github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/cors v1.3.0 h1:PolezCc89peu+NgkIWt9OB01Kbzt6IP0J/JvkG6xxlg= github.com/gin-contrib/cors v1.3.0/go.mod h1:artPvLlhkF7oG06nK8v3U8TNz6IeX+w1uzCSEId5/Vc= +github.com/gin-contrib/gzip v1.2.4 h1:yNz4EhPC2kHSZJD1oc1zwp7MLEhEZ3goQeGM3a1b6jU= +github.com/gin-contrib/gzip v1.2.4/go.mod h1:aomRgR7ftdZV3uWY0gW/m8rChfxau0n8YVvwlOHONzw= github.com/gin-contrib/sessions v1.0.2 h1:UaIjUvTH1cMeOdj3in6dl+Xb6It8RiKRF9Z1anbUyCA= github.com/gin-contrib/sessions v1.0.2/go.mod h1:KxKxWqWP5LJVDCInulOl4WbLzK2KSPlLesfZ66wRvMs= github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2 h1:xLG16iua01X7Gzms9045s2Y2niNpvSY/Zb1oBwgNYZY= github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2/go.mod h1:VhW/Ch/3FhimwZb8Oj+qJmdMmoB8r7lmJ5auRjm50oQ= github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= -github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= -github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= -github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= +github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs= @@ -348,10 +348,9 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+ github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-playground/validator/v10 v10.8.0/go.mod h1:9JhgTzTaE31GZDpH/HSvHiRJrJ3iKAgqqH0Bl/Ocjdk= -github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= -github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= +github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= @@ -367,8 +366,10 @@ github.com/go-webauthn/x v0.1.14 h1:1wrB8jzXAofojJPAaRxnZhRgagvLGnLjhCAwg3kTpT0= github.com/go-webauthn/x v0.1.14/go.mod h1:UuVvFZ8/NbOnkDz3y1NaxtUN87pmtpC1PQ+/5BBQRdc= github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b h1:khEcpUM4yFcxg4/FHQWkvVRmgijNXRfzkIDHh23ggEo= github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= @@ -441,8 +442,9 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-github/v28 v28.1.1/go.mod h1:bsqJWQX05omyWVmc00nEUql9mhQyv38lDZ8kPZcQVoM= github.com/google/go-licenses v0.0.0-20210329231322-ce1d9163b77d/go.mod h1:+TYOmkVoJOpwnS0wfdsJCV9CoD5nJYsHoFk/0CrTK4M= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= @@ -623,12 +625,10 @@ github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0 github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= -github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= -github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= @@ -649,7 +649,6 @@ github.com/kylelemons/go-gypsy v1.0.0/go.mod h1:chkXM0zjdpXOiqkCW1XcCHDfjfk14PH2 github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= -github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= @@ -679,7 +678,6 @@ github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= @@ -783,8 +781,8 @@ github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FI github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= @@ -840,6 +838,10 @@ github.com/qiniu/dyn v1.3.0/go.mod h1:E8oERcm8TtwJiZvkQPbcAh0RL8jO1G0VXJMW3FAWdk github.com/qiniu/go-sdk/v7 v7.19.0 h1:k3AzDPil8QHIQnki6xXt4YRAjE52oRoBUXQ4bV+Wc5U= github.com/qiniu/go-sdk/v7 v7.19.0/go.mod h1:nqoYCNo53ZlGA521RvRethvxUDvXKt4gtYXOwye868w= github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk= +github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U= github.com/rafaeljusto/redigomock v0.0.0-20191117212112-00b2509252a1 h1:leEwA4MD1ew0lNgzz6Q4G76G3AEfeci+TMggN6WuFRs= github.com/rafaeljusto/redigomock v0.0.0-20191117212112-00b2509252a1/go.mod h1:JaY6n2sDr+z2WTsXkOmNRUfDy6FN0L6Nk7x06ndm4tY= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= @@ -936,9 +938,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.563/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/kms v1.0.563/go.mod h1:uom4Nvi9W+Qkom0exYiJ9VWJjXwyxtPYTkKkaLMlfE0= @@ -961,8 +962,8 @@ github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGr github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= -github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= -github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= @@ -1032,6 +1033,8 @@ go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= @@ -1044,9 +1047,8 @@ go4.org v0.0.0-20200411211856-f5505b9728dd/go.mod h1:CIiUVy99QCPfoE13bO4EZaz5GZM go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc= go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU= gocloud.dev v0.19.0/go.mod h1:SmKwiR8YwIMMJvQBKLsC3fHNyMwXLw3PMDO+VVteJMI= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= -golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= +golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/crypto v0.0.0-20180501155221-613d6eafa307/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -1073,8 +1075,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1121,8 +1123,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= -golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1183,8 +1185,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1218,8 +1220,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1251,7 +1253,6 @@ golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1299,8 +1300,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -1328,8 +1329,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1406,8 +1407,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= -golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1539,8 +1540,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.25.1-0.20200805231151-a709e31e5d12/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1613,10 +1614,8 @@ modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= -nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= pack.ag/amqp v0.11.2/go.mod h1:4/cbmt4EJXSKlG6LCfWHoqmN0uFdy5i/+YFz+fTfhV4= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= diff --git a/routers/router.go b/routers/router.go index 7d388ce4..18d9266c 100644 --- a/routers/router.go +++ b/routers/router.go @@ -3,7 +3,6 @@ package routers import ( "net/http" - "github.com/abslant/gzip" "github.com/cloudreve/Cloudreve/v4/application/constants" "github.com/cloudreve/Cloudreve/v4/application/dependency" "github.com/cloudreve/Cloudreve/v4/inventory/types" @@ -24,6 +23,7 @@ import ( sharesvc "github.com/cloudreve/Cloudreve/v4/service/share" usersvc "github.com/cloudreve/Cloudreve/v4/service/user" "github.com/gin-contrib/cors" + "github.com/gin-contrib/gzip" "github.com/gin-gonic/gin" ) @@ -206,9 +206,9 @@ func initMasterRouter(dep dependency.Dep) *gin.Engine { /* 静态资源 */ - r.Use(gzip.GzipHandler()) // Done - r.Use(middleware.FrontendFileHandler(dep)) // Done - r.GET("manifest.json", controllers.Manifest) // Done + r.Use(gzip.Gzip(gzip.DefaultCompression, gzip.WithExcludedPaths([]string{"/api/"}))) + r.Use(middleware.FrontendFileHandler(dep)) + r.GET("manifest.json", controllers.Manifest) noAuth := r.Group(constants.APIPrefix) wopi := noAuth.Group("file/wopi", middleware.HashID(hashid.FileID), middleware.ViewerSessionValidation()) From 4785be81c2d34cfa249f5081882230878854c8b2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 10:50:54 +0800 Subject: [PATCH 67/74] chore(deps): bump github.com/wneessen/go-mail from 0.6.2 to 0.7.1 (#2939) Bumps [github.com/wneessen/go-mail](https://github.com/wneessen/go-mail) from 0.6.2 to 0.7.1. - [Release notes](https://github.com/wneessen/go-mail/releases) - [Commits](https://github.com/wneessen/go-mail/compare/v0.6.2...v0.7.1) --- updated-dependencies: - dependency-name: github.com/wneessen/go-mail dependency-version: 0.7.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 42 ++---------------------------------------- 2 files changed, 3 insertions(+), 41 deletions(-) diff --git a/go.mod b/go.mod index f059f966..709f274f 100644 --- a/go.mod +++ b/go.mod @@ -56,7 +56,7 @@ require ( github.com/tencentyun/cos-go-sdk-v5 v0.7.54 github.com/ua-parser/uap-go v0.0.0-20250213224047-9c035f085b90 github.com/upyun/go-sdk v2.1.0+incompatible - github.com/wneessen/go-mail v0.6.2 + github.com/wneessen/go-mail v0.7.1 golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e golang.org/x/image v0.0.0-20211028202545-6944b10bf410 golang.org/x/text v0.30.0 diff --git a/go.sum b/go.sum index f978d9b4..1ef85fb3 100644 --- a/go.sum +++ b/go.sum @@ -442,7 +442,6 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-github/v28 v28.1.1/go.mod h1:bsqJWQX05omyWVmc00nEUql9mhQyv38lDZ8kPZcQVoM= @@ -981,8 +980,8 @@ github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+ github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= github.com/weppos/publicsuffix-go v0.13.1-0.20210123135404-5fd73613514e/go.mod h1:HYux0V0Zi04bHNwOHy4cXJVz/TQjYonnF6aoYhj+3QE= github.com/weppos/publicsuffix-go v0.15.1-0.20210511084619-b1f36a2d6c0b/go.mod h1:HYux0V0Zi04bHNwOHy4cXJVz/TQjYonnF6aoYhj+3QE= -github.com/wneessen/go-mail v0.6.2 h1:c6V7c8D2mz868z9WJ+8zDKtUyLfZ1++uAZmo2GRFji8= -github.com/wneessen/go-mail v0.6.2/go.mod h1:L/PYjPK3/2ZlNb2/FjEBIn9n1rUWjW+Toy531oVmeb4= +github.com/wneessen/go-mail v0.7.1 h1:rvy63sp14N06/kdGqCYwW8Na5gDCXjTQM1E7So4PuKk= +github.com/wneessen/go-mail v0.7.1/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xanzy/go-gitlab v0.31.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug= @@ -1071,10 +1070,6 @@ golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf/go.mod h1:P+XmwS30IXTQdn5 golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1119,10 +1114,6 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1179,12 +1170,7 @@ golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1215,11 +1201,6 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1295,24 +1276,13 @@ golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1324,11 +1294,6 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1404,9 +1369,6 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 670b79eef381a3fd5198c9e564b82268dda34cf3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 10:53:40 +0800 Subject: [PATCH 68/74] chore(deps): bump github.com/gin-contrib/cors from 1.3.0 to 1.6.0 (#2097) Bumps [github.com/gin-contrib/cors](https://github.com/gin-contrib/cors) from 1.3.0 to 1.6.0. - [Release notes](https://github.com/gin-contrib/cors/releases) - [Changelog](https://github.com/gin-contrib/cors/blob/master/.goreleaser.yaml) - [Commits](https://github.com/gin-contrib/cors/compare/v1.3.0...v1.6.0) --- updated-dependencies: - dependency-name: github.com/gin-contrib/cors dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 709f274f..ca67f43a 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/dsoprea/go-tiff-image-structure v0.0.0-20221003165014-8ecc4f52edca github.com/dsoprea/go-utility v0.0.0-20200711062821-fab8125e9bdf github.com/fatih/color v1.18.0 - github.com/gin-contrib/cors v1.3.0 + github.com/gin-contrib/cors v1.6.0 github.com/gin-contrib/gzip v1.2.4 github.com/gin-contrib/sessions v1.0.2 github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2 diff --git a/go.sum b/go.sum index 1ef85fb3..acc1b3ed 100644 --- a/go.sum +++ b/go.sum @@ -299,19 +299,17 @@ github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIp github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gin-contrib/cors v1.3.0 h1:PolezCc89peu+NgkIWt9OB01Kbzt6IP0J/JvkG6xxlg= -github.com/gin-contrib/cors v1.3.0/go.mod h1:artPvLlhkF7oG06nK8v3U8TNz6IeX+w1uzCSEId5/Vc= +github.com/gin-contrib/cors v1.6.0 h1:0Z7D/bVhE6ja07lI8CTjTonp6SB07o8bNuFyRbsBUQg= +github.com/gin-contrib/cors v1.6.0/go.mod h1:cI+h6iOAyxKRtUtC6iF/Si1KSFvGm/gK+kshxlCi8ro= github.com/gin-contrib/gzip v1.2.4 h1:yNz4EhPC2kHSZJD1oc1zwp7MLEhEZ3goQeGM3a1b6jU= github.com/gin-contrib/gzip v1.2.4/go.mod h1:aomRgR7ftdZV3uWY0gW/m8rChfxau0n8YVvwlOHONzw= github.com/gin-contrib/sessions v1.0.2 h1:UaIjUvTH1cMeOdj3in6dl+Xb6It8RiKRF9Z1anbUyCA= github.com/gin-contrib/sessions v1.0.2/go.mod h1:KxKxWqWP5LJVDCInulOl4WbLzK2KSPlLesfZ66wRvMs= -github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2 h1:xLG16iua01X7Gzms9045s2Y2niNpvSY/Zb1oBwgNYZY= github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2/go.mod h1:VhW/Ch/3FhimwZb8Oj+qJmdMmoB8r7lmJ5auRjm50oQ= -github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= @@ -673,7 +671,6 @@ github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HN github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= @@ -1516,7 +1513,6 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= -gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= From 6085f2090f9d168a661a2d892df09bc07ec3419b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 10:56:01 +0800 Subject: [PATCH 69/74] chore(deps): bump golang.org/x/image (#2093) Bumps [golang.org/x/image](https://github.com/golang/image) from 0.0.0-20211028202545-6944b10bf410 to 0.18.0. - [Commits](https://github.com/golang/image/commits/v0.18.0) --- updated-dependencies: - dependency-name: golang.org/x/image dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index ca67f43a..e24c834d 100644 --- a/go.mod +++ b/go.mod @@ -58,7 +58,7 @@ require ( github.com/upyun/go-sdk v2.1.0+incompatible github.com/wneessen/go-mail v0.7.1 golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e - golang.org/x/image v0.0.0-20211028202545-6944b10bf410 + golang.org/x/image v0.18.0 golang.org/x/text v0.30.0 golang.org/x/time v0.5.0 golang.org/x/tools v0.38.0 diff --git a/go.sum b/go.sum index acc1b3ed..eb04d758 100644 --- a/go.sum +++ b/go.sum @@ -1085,8 +1085,8 @@ golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeId golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190501045829-6d32002ffd75/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ= -golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= +golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= From deecc5c20be8dbb5d5b5de067beb7e0173e36344 Mon Sep 17 00:00:00 2001 From: Darren Yu Date: Wed, 12 Nov 2025 13:49:32 +0800 Subject: [PATCH 70/74] feat(thumb blob path): support magic variables in thumb blob path (#3030) --- inventory/migration.go | 17 ++++++ inventory/setting.go | 2 +- pkg/filemanager/fs/dbfs/dbfs.go | 69 ++---------------------- pkg/filemanager/fs/dbfs/upload.go | 2 +- pkg/filemanager/manager/thumbnail.go | 5 +- pkg/setting/provider.go | 2 +- pkg/util/common.go | 80 +++++++++++++++++++++++++++- 7 files changed, 107 insertions(+), 70 deletions(-) diff --git a/inventory/migration.go b/inventory/migration.go index 7ba8aea2..12fa1623 100644 --- a/inventory/migration.go +++ b/inventory/migration.go @@ -477,6 +477,23 @@ var patches = []Patch{ return fmt.Errorf("failed to update mail_reset_template setting: %w", err) } + return nil + }, + }, + { + Name: "apply_thumb_path_magic_var", + EndVersion: "4.10.0", + Func: func(l logging.Logger, client *ent.Client, ctx context.Context) error { + thumbSuffixSetting, err := client.Setting.Query().Where(setting.Name("thumb_entity_suffix")).First(ctx) + if err != nil { + return fmt.Errorf("failed to query thumb_entity_suffix setting: %w", err) + } + + newThumbSuffix := fmt.Sprintf("{blob_path}/{blob_name}%s", thumbSuffixSetting.Value) + if _, err := client.Setting.UpdateOne(thumbSuffixSetting).SetValue(newThumbSuffix).Save(ctx); err != nil { + return fmt.Errorf("failed to update thumb_entity_suffix setting: %w", err) + } + return nil }, }, diff --git a/inventory/setting.go b/inventory/setting.go index 8ba62d24..44f935de 100644 --- a/inventory/setting.go +++ b/inventory/setting.go @@ -555,7 +555,7 @@ var DefaultSettings = map[string]string{ "captcha_cap_asset_server": "jsdelivr", "thumb_width": "400", "thumb_height": "300", - "thumb_entity_suffix": "._thumb", + "thumb_entity_suffix": "{blob_path}/{blob_name}._thumb", "thumb_slave_sidecar_suffix": "._thumb_sidecar", "thumb_encode_method": "png", "thumb_gc_after_gen": "0", diff --git a/pkg/filemanager/fs/dbfs/dbfs.go b/pkg/filemanager/fs/dbfs/dbfs.go index 7f346f53..ce63ed4e 100644 --- a/pkg/filemanager/fs/dbfs/dbfs.go +++ b/pkg/filemanager/fs/dbfs/dbfs.go @@ -4,12 +4,8 @@ import ( "context" "errors" "fmt" - "math/rand" "path" "path/filepath" - "regexp" - "strconv" - "strings" "sync" "time" @@ -126,7 +122,7 @@ func (f *DBFS) List(ctx context.Context, path *fs.URI, opts ...fs.Option) (fs.Fi parent, err := f.getFileByPath(ctx, navigator, path) if err != nil { - return nil, nil, fmt.Errorf("Parent not exist: %w", err) + return nil, nil, fmt.Errorf("parent not exist: %w", err) } pageSize := 0 @@ -787,71 +783,16 @@ func (f *DBFS) navigatorId(path *fs.URI) string { // generateSavePath generates the physical save path for the upload request. func generateSavePath(policy *ent.StoragePolicy, req *fs.UploadRequest, user *ent.User) string { currentTime := time.Now() - originName := req.Props.Uri.Name() - - dynamicReplace := func(regPattern string, rule string, pathAvailable bool) string { - re := regexp.MustCompile(regPattern) - return re.ReplaceAllStringFunc(rule, func(match string) string { - switch match { - case "{timestamp}": - return strconv.FormatInt(currentTime.Unix(), 10) - case "{timestamp_nano}": - return strconv.FormatInt(currentTime.UnixNano(), 10) - case "{datetime}": - return currentTime.Format("20060102150405") - case "{date}": - return currentTime.Format("20060102") - case "{year}": - return currentTime.Format("2006") - case "{month}": - return currentTime.Format("01") - case "{day}": - return currentTime.Format("02") - case "{hour}": - return currentTime.Format("15") - case "{minute}": - return currentTime.Format("04") - case "{second}": - return currentTime.Format("05") - case "{uid}": - return strconv.Itoa(user.ID) - case "{randomkey16}": - return util.RandStringRunes(16) - case "{randomkey8}": - return util.RandStringRunes(8) - case "{randomnum8}": - return strconv.Itoa(rand.Intn(8)) - case "{randomnum4}": - return strconv.Itoa(rand.Intn(4)) - case "{randomnum3}": - return strconv.Itoa(rand.Intn(3)) - case "{randomnum2}": - return strconv.Itoa(rand.Intn(2)) - case "{uuid}": - return uuid.Must(uuid.NewV4()).String() - case "{path}": - if pathAvailable { - return req.Props.Uri.Dir() + fs.Separator - } - return match - case "{originname}": - return originName - case "{ext}": - return filepath.Ext(originName) - case "{originname_without_ext}": - return strings.TrimSuffix(originName, filepath.Ext(originName)) - default: - return match - } - }) + dynamicReplace := func(rule string, pathAvailable bool) string { + return util.ReplaceMagicVar(rule, fs.Separator, pathAvailable, false, currentTime, user.ID, req.Props.Uri.Name(), req.Props.Uri.Dir(), "") } dirRule := policy.DirNameRule dirRule = filepath.ToSlash(dirRule) - dirRule = dynamicReplace(`\{[^{}]+\}`, dirRule, true) + dirRule = dynamicReplace(dirRule, true) nameRule := policy.FileNameRule - nameRule = dynamicReplace(`\{[^{}]+\}`, nameRule, false) + nameRule = dynamicReplace(nameRule, false) return path.Join(path.Clean(dirRule), nameRule) } diff --git a/pkg/filemanager/fs/dbfs/upload.go b/pkg/filemanager/fs/dbfs/upload.go index 986d2a35..a50ae1d6 100644 --- a/pkg/filemanager/fs/dbfs/upload.go +++ b/pkg/filemanager/fs/dbfs/upload.go @@ -160,7 +160,7 @@ func (f *DBFS) PrepareUpload(ctx context.Context, req *fs.UploadRequest, opts .. if req.Props.SavePath == "" || isThumbnailAndPolicyNotAvailable { req.Props.SavePath = generateSavePath(policy, req, f.user) if isThumbnailAndPolicyNotAvailable { - req.Props.SavePath = req.Props.SavePath + f.settingClient.ThumbEntitySuffix(ctx) + req.Props.SavePath = path.Clean(util.ReplaceMagicVar(f.settingClient.ThumbEntitySuffix(ctx), fs.Separator, true, true, time.Now(), f.user.ID, req.Props.Uri.Name(), req.Props.Uri.Path(), req.Props.SavePath)) } } diff --git a/pkg/filemanager/manager/thumbnail.go b/pkg/filemanager/manager/thumbnail.go index 3d71afcd..1b979382 100644 --- a/pkg/filemanager/manager/thumbnail.go +++ b/pkg/filemanager/manager/thumbnail.go @@ -4,8 +4,8 @@ import ( "context" "errors" "fmt" - "github.com/cloudreve/Cloudreve/v4/pkg/thumb" "os" + "path" "runtime" "time" @@ -18,6 +18,7 @@ import ( "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource" "github.com/cloudreve/Cloudreve/v4/pkg/logging" "github.com/cloudreve/Cloudreve/v4/pkg/queue" + "github.com/cloudreve/Cloudreve/v4/pkg/thumb" "github.com/cloudreve/Cloudreve/v4/pkg/util" "github.com/samber/lo" ) @@ -185,7 +186,7 @@ func (m *manager) generateThumb(ctx context.Context, uri *fs.URI, ext string, es Props: &fs.UploadProps{ Uri: uri, Size: fileInfo.Size(), - SavePath: es.Entity().Source() + m.settings.ThumbEntitySuffix(ctx), + SavePath: path.Clean(util.ReplaceMagicVar(m.settings.ThumbEntitySuffix(ctx), fs.Separator, true, true, time.Now(), m.user.ID, uri.Name(), uri.Path(), es.Entity().Source())), MimeType: m.dep.MimeDetector(ctx).TypeByName("thumb.jpg"), EntityType: &entityType, }, diff --git a/pkg/setting/provider.go b/pkg/setting/provider.go index 0cf77f00..7c156638 100644 --- a/pkg/setting/provider.go +++ b/pkg/setting/provider.go @@ -510,7 +510,7 @@ func (s *settingProvider) ThumbEncode(ctx context.Context) *ThumbEncode { } func (s *settingProvider) ThumbEntitySuffix(ctx context.Context) string { - return s.getString(ctx, "thumb_entity_suffix", "._thumb") + return s.getString(ctx, "thumb_entity_suffix", "{blob_path}/{blob_name}._thumb") } func (s *settingProvider) ThumbSlaveSidecarSuffix(ctx context.Context) string { diff --git a/pkg/util/common.go b/pkg/util/common.go index a93472dd..db7035ed 100644 --- a/pkg/util/common.go +++ b/pkg/util/common.go @@ -3,12 +3,16 @@ package util import ( "context" "fmt" - "github.com/gin-gonic/gin" "math/rand" + "path/filepath" "regexp" + "strconv" "strings" "time" "unicode/utf8" + + "github.com/gin-gonic/gin" + "github.com/gofrs/uuid" ) func init() { @@ -95,6 +99,80 @@ func Replace(table map[string]string, s string) string { return s } +// ReplaceMagicVar 动态替换字符串中的魔法变量 +func ReplaceMagicVar(rawString string, fsSeparator string, pathAvailable bool, blobAvailable bool, + timeConst time.Time, userId int, originName string, originPath string, completeBlobPath string) string { + re := regexp.MustCompile(`\{[^{}]+\}`) + return re.ReplaceAllStringFunc(rawString, func(match string) string { + switch match { + case "{randomkey16}": + return RandStringRunes(16) + case "{randomkey8}": + return RandStringRunes(8) + case "{timestamp}": + return strconv.FormatInt(timeConst.Unix(), 10) + case "{timestamp_nano}": + return strconv.FormatInt(timeConst.UnixNano(), 10) + case "{randomnum2}": + return strconv.Itoa(rand.Intn(2)) + case "{randomnum3}": + return strconv.Itoa(rand.Intn(3)) + case "{randomnum4}": + return strconv.Itoa(rand.Intn(4)) + case "{randomnum8}": + return strconv.Itoa(rand.Intn(8)) + case "{uid}": + return strconv.Itoa(userId) + case "{datetime}": + return timeConst.Format("20060102150405") + case "{date}": + return timeConst.Format("20060102") + case "{year}": + return timeConst.Format("2006") + case "{month}": + return timeConst.Format("01") + case "{day}": + return timeConst.Format("02") + case "{hour}": + return timeConst.Format("15") + case "{minute}": + return timeConst.Format("04") + case "{second}": + return timeConst.Format("05") + case "{uuid}": + return uuid.Must(uuid.NewV4()).String() + case "{ext}": + return filepath.Ext(originName) + case "{originname}": + return originName + case "{originname_without_ext}": + return strings.TrimSuffix(originName, filepath.Ext(originName)) + case "{path}": + if pathAvailable { + return originPath + fsSeparator + } + return match + case "{blob_name}": + if blobAvailable { + return filepath.Base(completeBlobPath) + } + return match + case "{blob_name_without_ext}": + if blobAvailable { + return strings.TrimSuffix(filepath.Base(completeBlobPath), filepath.Ext(completeBlobPath)) + } + return match + case "{blob_path}": + if blobAvailable { + return filepath.Dir(completeBlobPath) + fsSeparator + } + return match + default: + return match + } + }) +} + // BuildRegexp 构建用于SQL查询用的多条件正则 func BuildRegexp(search []string, prefix, suffix, condition string) string { var res string From b507c1b8939512898b2e9275bbc254fed0b3c3f3 Mon Sep 17 00:00:00 2001 From: Darren Yu Date: Wed, 12 Nov 2025 13:55:38 +0800 Subject: [PATCH 71/74] docs: update feature description (#3023) * docs: update feature description * Apply suggestion from @HFO4 --------- Co-authored-by: AaronLiu --- README.md | 6 +++--- README_zh-CN.md | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index aaee56bf..4ced299b 100644 --- a/README.md +++ b/README.md @@ -38,12 +38,12 @@ ## :sparkles: Features -- :cloud: Support storing files into Local, Remote node, OneDrive, S3 compatible API, Qiniu, Aliyun OSS, Tencent COS, Upyun. +- :cloud: Support storing files into Local, Remote node, OneDrive, S3 compatible API, Qiniu Kodo, Aliyun OSS, Tencent COS, Huawei Cloud OBS, Kingsoft Cloud KS3, Upyun. - :outbox_tray: Upload/Download in directly transmission from client to storage providers. - 💾 Integrate with Aria2/qBittorrent to download files in background, use multiple download nodes to share the load. -- 📚 Compress/Extract files, download files in batch. +- 📚 Compress/Extract/Preview archived files, download files in batch. - 💻 WebDAV support covering all storage providers. -- :zap:Drag&Drop to upload files or folders, with resumable upload support. +- :zap:Drag&Drop to upload files or folders, with parallel resumable upload support. - :card_file_box: Extract media metadata from files, search files by metadata or tags. - :family_woman_girl_boy: Multi-users with multi-groups. - :link: Create share links for files and folders with expiration date. diff --git a/README_zh-CN.md b/README_zh-CN.md index 17b724ad..e7bc7830 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -39,12 +39,12 @@ ## :sparkles: 特性 -- :cloud: 支持本机、从机、七牛、阿里云 OSS、腾讯云 COS、华为云 OBS、又拍云、OneDrive (包括世纪互联版) 、S3 兼容协议 作为存储端 +- :cloud: 支持本机、从机、七牛 Kodo、阿里云 OSS、腾讯云 COS、华为云 OBS、金山云 KS3、又拍云、OneDrive (包括世纪互联版) 、S3 兼容协议 作为存储端 - :outbox_tray: 上传/下载 支持客户端直传,支持下载限速 -- 💾 可对接 Aria2 离线下载,可使用多个从机节点分担下载任务 -- 📚 在线 压缩/解压缩、多文件打包下载 +- 💾 可对接 Aria2/qBittorrent 离线下载,可使用多个从机节点分担下载任务 +- 📚 在线 压缩/解压缩/压缩包预览、多文件打包下载 - 💻 覆盖全部存储策略的 WebDAV 协议支持 -- :zap: 拖拽上传、目录上传、分片上传 +- :zap: 拖拽上传、目录上传、并行分片上传 - :card_file_box: 提取媒体元数据,通过元数据或标签搜索文件 - :family_woman_girl_boy: 多用户、用户组、多存储策略 - :link: 创建文件、目录的分享链接,可设定自动过期 From 994ef7af81a19e1ddab79d094cfffe70a9ecc719 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Wed, 12 Nov 2025 13:57:13 +0800 Subject: [PATCH 72/74] fix(search): multiple metadata search does not work (#3027) --- assets | 2 +- inventory/file_utils.go | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/assets b/assets index 8b91fca9..51bbced0 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 8b91fca9291b58edd100949954039fc71524f97d +Subproject commit 51bbced0b36c1d5de5fa1b8e49955c771082449f diff --git a/inventory/file_utils.go b/inventory/file_utils.go index 890d4191..b0b04264 100644 --- a/inventory/file_utils.go +++ b/inventory/file_utils.go @@ -78,7 +78,7 @@ func (f *fileClient) searchQuery(q *ent.FileQuery, args *SearchFileParameters, p return metadata.And(metadata.NameEQ(item.Key), metadata.ValueEQ(item.Value)) } - nameEq := metadata.NameEQ(item.Key) + nameEq := metadata.And(metadata.IsPublic(true), metadata.NameEQ(item.Key)) if item.Value == "" { return nameEq } else { @@ -86,8 +86,9 @@ func (f *fileClient) searchQuery(q *ent.FileQuery, args *SearchFileParameters, p return metadata.And(nameEq, valueContain) } }) - metaPredicates = append(metaPredicates, metadata.IsPublic(true)) - q.Where(file.HasMetadataWith(metadata.And(metaPredicates...))) + q.Where(file.And(lo.Map(metaPredicates, func(item predicate.Metadata, index int) predicate.File { + return file.HasMetadataWith(item) + })...)) } if args.SizeLte > 0 || args.SizeGte > 0 { From 6ad72e07f4ba80509f88f6a71bd2bf05993ba3f4 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Fri, 14 Nov 2025 11:18:39 +0800 Subject: [PATCH 73/74] update submodule --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index 51bbced0..8c73fb85 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 51bbced0b36c1d5de5fa1b8e49955c771082449f +Subproject commit 8c73fb8551cf79dfd4f73885987593c0fb695b10 From 67c6f937c9d140eb4e27ae5f426c41423b7a2f47 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Sat, 15 Nov 2025 11:59:09 +0800 Subject: [PATCH 74/74] fix(oss): disable RSA min key size check for OSS callback (#3038) --- assets | 2 +- main.go | 2 ++ pkg/filemanager/driver/oss/callback.go | 5 +++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/assets b/assets index 8c73fb85..1b1f9f4c 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 8c73fb8551cf79dfd4f73885987593c0fb695b10 +Subproject commit 1b1f9f4c8e35d72ac60216af611c81355bd4f7ce diff --git a/main.go b/main.go index 931c5053..d9385366 100644 --- a/main.go +++ b/main.go @@ -1,8 +1,10 @@ +//go:debug rsa1024min=0 package main import ( _ "embed" "flag" + "github.com/cloudreve/Cloudreve/v4/cmd" "github.com/cloudreve/Cloudreve/v4/pkg/util" ) diff --git a/pkg/filemanager/driver/oss/callback.go b/pkg/filemanager/driver/oss/callback.go index 36a70bb2..7d4f8514 100644 --- a/pkg/filemanager/driver/oss/callback.go +++ b/pkg/filemanager/driver/oss/callback.go @@ -10,12 +10,13 @@ import ( "encoding/pem" "errors" "fmt" - "github.com/cloudreve/Cloudreve/v4/pkg/cache" - "github.com/cloudreve/Cloudreve/v4/pkg/request" "io" "net/http" "net/url" "strings" + + "github.com/cloudreve/Cloudreve/v4/pkg/cache" + "github.com/cloudreve/Cloudreve/v4/pkg/request" ) const (