feat: add theme preset system with admin CRUD, public listing, and user theme settings
- 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>
This commit is contained in:
161
routers/api/v1/trash/__init__.py
Normal file
161
routers/api/v1/trash/__init__.py
Normal file
@@ -0,0 +1,161 @@
|
||||
"""
|
||||
回收站路由
|
||||
|
||||
提供回收站管理功能:列出、恢复、永久删除、清空。
|
||||
|
||||
路由前缀:/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} 个对象")
|
||||
Reference in New Issue
Block a user