diff --git a/frontend/public/locales/en-US/application.json b/frontend/public/locales/en-US/application.json index 5395a3d5..144447ff 100644 --- a/frontend/public/locales/en-US/application.json +++ b/frontend/public/locales/en-US/application.json @@ -855,6 +855,8 @@ "enableVersionRetention": "Enable version retention", "enableVersionRetentionDes": "If enabled, historical versions of files that meet the conditions will be retained.", "versionRetention": "Version retention", + "trashRetention": "Trash retention (days)", + "trashRetentionDes": "Days a file stays in the trash before automatic deletion. 0 inherits the group default. Applies to files moved to trash after the change.", "languageDes": "Select the display language and preferred email language.", "timezoneDes": "Set the display timezone, default is system timezone", "externalApps": "External Apps", diff --git a/frontend/public/locales/zh-CN/application.json b/frontend/public/locales/zh-CN/application.json index 3a5d3fa4..1f0a4fc7 100644 --- a/frontend/public/locales/zh-CN/application.json +++ b/frontend/public/locales/zh-CN/application.json @@ -855,6 +855,8 @@ "enableVersionRetention": "启用版本保留", "enableVersionRetentionDes": "启用后,对于符合条件的文件,系统会保留其历史版本", "versionRetention": "版本保留", + "trashRetention": "回收站保留时间(天)", + "trashRetentionDes": "文件移入回收站后自动删除前保留的天数。0 表示继承用户组默认值,仅影响修改后移入回收站的文件。", "languageDes": "设置应用展示语言和首选邮件语言", "timezoneDes": "设置展示时区,默认跟随系统时区", "externalApps": "外部应用", diff --git a/frontend/src/api/user.ts b/frontend/src/api/user.ts index c8600891..8dfc65e3 100644 --- a/frontend/src/api/user.ts +++ b/frontend/src/api/user.ts @@ -144,6 +144,7 @@ export interface UserSettings { share_links_in_profile: ShareLinksInProfileLevel; share_default_private?: boolean; preferred_viewers?: Record; + trash_retention?: number; oauth_grants?: OAuthGrant[]; } @@ -171,6 +172,8 @@ export interface PatchUserSetting { // Tri-state: "true" / "false" / "" (inherit the site default). share_default_private?: string; preferred_viewers?: Record; + // Trash retention override in seconds; 0 inherits the group setting. + trash_retention?: number; } export interface PasskeyCredentialOption { diff --git a/frontend/src/component/Pages/Setting/PreferenceSetting.tsx b/frontend/src/component/Pages/Setting/PreferenceSetting.tsx index ca22b2ff..e3a8f887 100644 --- a/frontend/src/component/Pages/Setting/PreferenceSetting.tsx +++ b/frontend/src/component/Pages/Setting/PreferenceSetting.tsx @@ -86,6 +86,29 @@ const PreferenceSetting = ({ setting, setSetting }: PreferenceSettingProps) => { const [folderClickAction, setFolderClickAction] = useState( SessionManager.getWithFallback(UserSettings.FolderClickAction), ); + const [trashRetentionDays, setTrashRetentionDays] = useState( + Math.round((setting.trash_retention ?? 0) / 86400), + ); + + const onTrashRetentionChange = (e: React.ChangeEvent) => { + const days = Math.max(0, parseInt(e.target.value) || 0); + setTrashRetentionDays(days); + }; + + const saveTrashRetention = () => { + const seconds = trashRetentionDays * 86400; + if (seconds == (setting.trash_retention ?? 0)) { + return; + } + setLoading(true); + dispatch(sendUpdateUserSetting({ trash_retention: seconds })) + .then(() => { + setSetting({ ...setting, trash_retention: seconds }); + }) + .finally(() => { + setLoading(false); + }); + }; const onRetentionCheckChange = (e: React.ChangeEvent) => { setVersionRetentionEnabled(e.target.checked); @@ -107,6 +130,7 @@ const PreferenceSetting = ({ setting, setSetting }: PreferenceSettingProps) => { setVersionRetentionEnabled(setting.version_retention_enabled); setVersionRetentionMax(setting.version_retention_max); setVersionRetentionExts(setting.version_retention_ext); + setTrashRetentionDays(Math.round((setting.trash_retention ?? 0) / 86400)); }, [setting]); const selectTimeZone = (value: string) => { @@ -349,6 +373,16 @@ const PreferenceSetting = ({ setting, setSetting }: PreferenceSettingProps) => { + + + 0 { + retention = us.TrashRetention + } if err := fc.UpsertMetadata(ctx, target.Model, map[string]string{ MetadataRestoreUri: target.Uri(true).String(), MetadataExpectedCollectTime: strconv.FormatInt( - time.Now().Add(time.Duration(target.Owner().Edges.Group.Settings.TrashRetention)*time.Second).Unix(), + time.Now().Add(time.Duration(retention)*time.Second).Unix(), 10), }, nil); err != nil { _ = inventory.Rollback(tx) diff --git a/service/user/response.go b/service/user/response.go index 7ff8f8f3..d2c053a9 100644 --- a/service/user/response.go +++ b/service/user/response.go @@ -34,6 +34,7 @@ type UserSettings struct { ShareLinksInProfile string `json:"share_links_in_profile"` ShareDefaultPrivate *bool `json:"share_default_private,omitempty"` PreferredViewers map[string]string `json:"preferred_viewers,omitempty"` + TrashRetention int `json:"trash_retention,omitempty"` OAuthGrants []OauthGrant `json:"oauth_grants,omitempty"` } @@ -51,6 +52,7 @@ func BuildUserSettings(u *ent.User, passkeys []*ent.Passkey, parser *uaparser.Pa ShareLinksInProfile: string(u.Settings.ShareLinksInProfile), ShareDefaultPrivate: u.Settings.ShareDefaultPrivate, PreferredViewers: u.Settings.PreferredViewers, + TrashRetention: u.Settings.TrashRetention, OAuthGrants: lo.Map(grants, func(item *ent.OAuthGrant, index int) OauthGrant { return BuildOauthGrant(item) }), diff --git a/service/user/setting.go b/service/user/setting.go index 79527302..293282e9 100644 --- a/service/user/setting.go +++ b/service/user/setting.go @@ -236,6 +236,9 @@ type ( ShareDefaultPrivate *string `json:"share_default_private" binding:"omitempty"` // PreferredViewers replaces the whole extension → viewer-id map. PreferredViewers *map[string]string `json:"preferred_viewers" binding:"omitempty"` + // TrashRetention overrides group trash retention, in seconds. + // 0 clears the override. Capped at ~10 years. + TrashRetention *int `json:"trash_retention" binding:"omitempty,min=0,max=315360000"` } PatchUserSettingParamsCtx struct{} ) @@ -334,6 +337,11 @@ func (s *PatchUserSetting) Patch(c *gin.Context) error { saveSetting = true } + if s.TrashRetention != nil { + u.Settings.TrashRetention = *s.TrashRetention + saveSetting = true + } + if s.CurrentPassword != nil && s.NewPassword != nil { if err := auth.CheckScope(c, types.ScopeUserSecurityInfoWrite); err != nil { return err