- Add ChromaticColor (17 Tailwind colors) and NeutralColor (5 grays) enums - Add ThemePreset table with flat color columns and unique name constraint - Add admin theme endpoints (CRUD + set default) at /api/v1/admin/theme - Add public theme listing at /api/v1/site/themes - Add user theme settings (PATCH /theme) with color snapshot on User model - User.color_* columns store per-user overrides; fallback to default preset then builtin - Initialize default theme preset in migration - Remove legacy defaultTheme/themes settings Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
162 lines
4.6 KiB
Python
162 lines
4.6 KiB
Python
"""
|
||
回收站路由
|
||
|
||
提供回收站管理功能:列出、恢复、永久删除、清空。
|
||
|
||
路由前缀:/trash
|
||
"""
|
||
from typing import Annotated
|
||
|
||
from fastapi import APIRouter, Depends
|
||
from loguru import logger as l
|
||
|
||
from middleware.auth import auth_required
|
||
from middleware.dependencies import SessionDep
|
||
from sqlmodels import Object, User
|
||
from sqlmodels.object import TrashDeleteRequest, TrashItemResponse, TrashRestoreRequest
|
||
from service.storage.object import (
|
||
permanently_delete_objects,
|
||
restore_objects,
|
||
soft_delete_objects,
|
||
)
|
||
|
||
trash_router = APIRouter(
|
||
prefix="/trash",
|
||
tags=["trash"],
|
||
)
|
||
|
||
|
||
@trash_router.get(
|
||
path='/',
|
||
summary='列出回收站内容',
|
||
description='获取当前用户回收站中的所有顶层对象。',
|
||
)
|
||
async def router_trash_list(
|
||
session: SessionDep,
|
||
user: Annotated[User, Depends(auth_required)],
|
||
) -> list[TrashItemResponse]:
|
||
"""
|
||
列出回收站内容
|
||
|
||
认证:需要 JWT token
|
||
|
||
返回回收站中被直接删除的顶层对象列表,
|
||
不包含其子对象(子对象在恢复/永久删除时会随顶层对象一起处理)。
|
||
"""
|
||
items = await Object.get_trash_items(session, user.id)
|
||
|
||
return [
|
||
TrashItemResponse(
|
||
id=item.id,
|
||
name=item.name,
|
||
type=item.type,
|
||
size=item.size,
|
||
deleted_at=item.deleted_at,
|
||
original_parent_id=item.deleted_original_parent_id,
|
||
)
|
||
for item in items
|
||
]
|
||
|
||
|
||
@trash_router.patch(
|
||
path='/restore',
|
||
summary='恢复对象',
|
||
description='从回收站恢复一个或多个对象到原位置。如果原位置不存在则恢复到根目录。',
|
||
status_code=204,
|
||
)
|
||
async def router_trash_restore(
|
||
session: SessionDep,
|
||
user: Annotated[User, Depends(auth_required)],
|
||
request: TrashRestoreRequest,
|
||
) -> None:
|
||
"""
|
||
从回收站恢复对象
|
||
|
||
认证:需要 JWT token
|
||
|
||
流程:
|
||
1. 验证对象存在且在回收站中(deleted_at IS NOT NULL)
|
||
2. 检查原父目录是否存在且未删除
|
||
3. 存在 → 恢复到原位置;不存在 → 恢复到根目录
|
||
4. 处理同名冲突(自动重命名)
|
||
5. 清除 deleted_at 和 deleted_original_parent_id
|
||
"""
|
||
user_id = user.id
|
||
objects_to_restore: list[Object] = []
|
||
|
||
for obj_id in request.ids:
|
||
obj = await Object.get(
|
||
session,
|
||
(Object.id == obj_id) & (Object.owner_id == user_id) & (Object.deleted_at != None)
|
||
)
|
||
if obj:
|
||
objects_to_restore.append(obj)
|
||
|
||
if objects_to_restore:
|
||
restored_count = await restore_objects(session, objects_to_restore, user_id)
|
||
l.info(f"用户 {user_id} 从回收站恢复了 {restored_count} 个对象")
|
||
|
||
|
||
@trash_router.delete(
|
||
path='/',
|
||
summary='永久删除对象',
|
||
description='永久删除回收站中的指定对象,包括物理文件和数据库记录。',
|
||
status_code=204,
|
||
)
|
||
async def router_trash_delete(
|
||
session: SessionDep,
|
||
user: Annotated[User, Depends(auth_required)],
|
||
request: TrashDeleteRequest,
|
||
) -> None:
|
||
"""
|
||
永久删除回收站中的对象
|
||
|
||
认证:需要 JWT token
|
||
|
||
流程:
|
||
1. 验证对象存在且在回收站中
|
||
2. BFS 收集所有子文件的 PhysicalFile
|
||
3. 处理引用计数,引用为 0 时物理删除文件
|
||
4. 硬删除根 Object(CASCADE 清理子对象)
|
||
5. 更新用户存储配额
|
||
"""
|
||
user_id = user.id
|
||
objects_to_delete: list[Object] = []
|
||
|
||
for obj_id in request.ids:
|
||
obj = await Object.get(
|
||
session,
|
||
(Object.id == obj_id) & (Object.owner_id == user_id) & (Object.deleted_at != None)
|
||
)
|
||
if obj:
|
||
objects_to_delete.append(obj)
|
||
|
||
if objects_to_delete:
|
||
deleted_count = await permanently_delete_objects(session, objects_to_delete, user_id)
|
||
l.info(f"用户 {user_id} 永久删除了 {deleted_count} 个对象")
|
||
|
||
|
||
@trash_router.delete(
|
||
path='/empty',
|
||
summary='清空回收站',
|
||
description='永久删除回收站中的所有对象。',
|
||
status_code=204,
|
||
)
|
||
async def router_trash_empty(
|
||
session: SessionDep,
|
||
user: Annotated[User, Depends(auth_required)],
|
||
) -> None:
|
||
"""
|
||
清空回收站
|
||
|
||
认证:需要 JWT token
|
||
|
||
获取回收站中所有顶层对象,逐个执行永久删除。
|
||
"""
|
||
user_id = user.id
|
||
trash_items = await Object.get_trash_items(session, user_id)
|
||
|
||
if trash_items:
|
||
deleted_count = await permanently_delete_objects(session, trash_items, user_id)
|
||
l.info(f"用户 {user_id} 清空回收站,共删除 {deleted_count} 个对象")
|