feat: add expired share cleanup functionality

This commit is contained in:
shiyu
2025-09-14 16:27:46 +08:00
parent 7b25e6d3b6
commit 0732b611a9
6 changed files with 54 additions and 4 deletions
+12
View File
@@ -83,6 +83,18 @@ async def get_my_shares(current_user: User = Depends(get_current_active_user)):
return [ShareInfo.from_orm(s) for s in shares] return [ShareInfo.from_orm(s) for s in shares]
@router.delete("/expired")
async def delete_expired_shares(
current_user: User = Depends(get_current_active_user),
):
"""
删除当前用户的所有已过期分享。
"""
user_account = await UserAccount.get(id=current_user.id)
deleted_count = await share_service.delete_expired_shares(user=user_account)
return success({"deleted_count": deleted_count})
@router.delete("/{share_id}") @router.delete("/{share_id}")
async def delete_share( async def delete_share(
share_id: int, share_id: int,
+11 -1
View File
@@ -90,6 +90,16 @@ class ShareService:
raise HTTPException(status_code=404, detail="分享链接不存在") raise HTTPException(status_code=404, detail="分享链接不存在")
await share.delete() await share.delete()
@staticmethod
async def delete_expired_shares(user: UserAccount) -> int:
"""
删除当前用户所有已过期的分享链接,返回删除数量。
条件:expires_at 非空 且 小于等于当前时间(UTC)。
"""
now = datetime.now(timezone.utc)
deleted_count = await ShareLink.filter(user=user, expires_at__lte=now).delete()
return deleted_count
@staticmethod @staticmethod
async def get_shared_item_details(share: ShareLink, sub_path: str = ""): async def get_shared_item_details(share: ShareLink, sub_path: str = ""):
""" """
@@ -122,4 +132,4 @@ class ShareService:
raise e raise e
share_service = ShareService() share_service = ShareService()
+6 -1
View File
@@ -23,10 +23,15 @@ export interface ShareCreatePayload {
password?: string; password?: string;
} }
export interface ClearExpiredResult {
deleted_count: number;
}
export const shareApi = { export const shareApi = {
create: (payload: ShareCreatePayload) => request<ShareInfoWithPassword>('/shares', { method: 'POST', json: payload }), create: (payload: ShareCreatePayload) => request<ShareInfoWithPassword>('/shares', { method: 'POST', json: payload }),
list: () => request<ShareInfo[]>('/shares'), list: () => request<ShareInfo[]>('/shares'),
remove: (shareId: number) => request<void>(`/shares/${shareId}`, { method: 'DELETE' }), remove: (shareId: number) => request<void>(`/shares/${shareId}`, { method: 'DELETE' }),
clearExpired: () => request<ClearExpiredResult>(`/shares/expired`, { method: 'DELETE' }),
get: (token: string) => request<ShareInfo>(`/s/${token}`), get: (token: string) => request<ShareInfo>(`/s/${token}`),
verifyPassword: (token: string, password: string) => request<void>(`/s/${token}/verify`, { method: 'POST', json: { password } }), verifyPassword: (token: string, password: string) => request<void>(`/s/${token}/verify`, { method: 'POST', json: { password } }),
listDir: (token: string, path: string = '/', password?: string) => { listDir: (token: string, path: string = '/', password?: string) => {
@@ -40,4 +45,4 @@ export const shareApi = {
const url = `${API_BASE_URL}/s/${token}/download?path=${encodeURIComponent(path)}`; const url = `${API_BASE_URL}/s/${token}/download?path=${encodeURIComponent(path)}`;
return password ? `${url}&password=${encodeURIComponent(password)}` : url; return password ? `${url}&password=${encodeURIComponent(password)}` : url;
}, },
}; };
+3
View File
@@ -53,6 +53,9 @@ export const en = {
'Cancel failed': 'Cancel failed', 'Cancel failed': 'Cancel failed',
'Load failed': 'Load failed', 'Load failed': 'Load failed',
'Are you sure to cancel share?': 'Are you sure to cancel share?', 'Are you sure to cancel share?': 'Are you sure to cancel share?',
'Clear expired shares': 'Clear expired shares',
'Confirm clear expired shares?': 'Confirm clear expired shares?',
'Cleared {count} expired shares': 'Cleared {count} expired shares',
'Share Name': 'Share Name', 'Share Name': 'Share Name',
'Share Content': 'Share Content', 'Share Content': 'Share Content',
+4 -1
View File
@@ -25,7 +25,7 @@ export const zh = {
'Account Settings': '账户设置', 'Account Settings': '账户设置',
'Language': '语言', 'Language': '语言',
'Chinese': '中文', 'Chinese': '中文',
'English': '英文', 'English': 'English',
'Full Name': '昵称', 'Full Name': '昵称',
'Email': '邮箱', 'Email': '邮箱',
'Change Password': '修改密码', 'Change Password': '修改密码',
@@ -57,6 +57,9 @@ export const zh = {
'Cancel failed': '取消失败', 'Cancel failed': '取消失败',
'Load failed': '加载失败', 'Load failed': '加载失败',
'Are you sure to cancel share?': '确认取消分享?', 'Are you sure to cancel share?': '确认取消分享?',
'Clear expired shares': '清空过期分享',
'Confirm clear expired shares?': '确认清空过期分享?',
'Cleared {count} expired shares': '已清理 {count} 个过期分享',
'Share Name': '分享名称', 'Share Name': '分享名称',
'Share Content': '分享内容', 'Share Content': '分享内容',
'Created At': '创建时间', 'Created At': '创建时间',
+18 -1
View File
@@ -44,6 +44,16 @@ const SharePage = memo(function SharePage() {
} }
}; };
const handleClearExpired = async () => {
try {
const res = await shareApi.clearExpired();
message.success(t('Cleared {count} expired shares', { count: String(res.deleted_count) }));
fetchList();
} catch (e: any) {
message.error(e.message || t('Clear failed'));
}
};
const columns = [ const columns = [
{ {
title: t('Share Name'), title: t('Share Name'),
@@ -100,7 +110,14 @@ const SharePage = memo(function SharePage() {
return ( return (
<PageCard <PageCard
title={t('My Shares')} title={t('My Shares')}
extra={<Button onClick={fetchList} loading={loading}>{t('Refresh')}</Button>} extra={
<Space>
<Button onClick={fetchList} loading={loading}>{t('Refresh')}</Button>
<Popconfirm title={t('Confirm clear expired shares?')} onConfirm={handleClearExpired}>
<Button danger>{t('Clear expired shares')}</Button>
</Popconfirm>
</Space>
}
> >
<Table <Table
rowKey="id" rowKey="id"