- Add POST/GET source link endpoints for file sharing via permanent URLs - Enforce max_size check in PATCH /file/content to prevent size limit bypass - Support is_private (proxy) vs public (302 redirect) storage modes - Replace all ResponseBase(data=...) with proper DTOs or 204 responses - Add 18 integration tests for source link and policy rule enforcement Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
164 lines
4.0 KiB
Python
164 lines
4.0 KiB
Python
from datetime import datetime
|
|
from uuid import UUID
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from loguru import logger as l
|
|
|
|
from middleware.auth import admin_required
|
|
from middleware.dependencies import SessionDep, TableViewRequestDep
|
|
from sqlmodels import (
|
|
ListResponse,
|
|
Share, AdminShareListItem,
|
|
)
|
|
from sqlmodel_ext import SQLModelBase
|
|
|
|
|
|
class ShareDetailResponse(SQLModelBase):
|
|
"""分享详情响应"""
|
|
|
|
id: UUID
|
|
"""分享UUID"""
|
|
|
|
code: str
|
|
"""分享码"""
|
|
|
|
views: int
|
|
"""浏览次数"""
|
|
|
|
downloads: int
|
|
"""下载次数"""
|
|
|
|
remain_downloads: int | None
|
|
"""剩余下载次数"""
|
|
|
|
expires: datetime | None
|
|
"""过期时间"""
|
|
|
|
preview_enabled: bool
|
|
"""是否启用预览"""
|
|
|
|
score: int
|
|
"""评分"""
|
|
|
|
has_password: bool
|
|
"""是否有密码"""
|
|
|
|
user_id: str
|
|
"""用户UUID"""
|
|
|
|
username: str | None
|
|
"""用户名"""
|
|
|
|
object: dict | None
|
|
"""关联对象信息"""
|
|
|
|
created_at: str
|
|
"""创建时间"""
|
|
|
|
admin_share_router = APIRouter(
|
|
prefix='/share',
|
|
tags=['admin', 'admin_share']
|
|
)
|
|
|
|
@admin_share_router.get(
|
|
path='/list',
|
|
summary='获取分享列表',
|
|
description='Get share list',
|
|
dependencies=[Depends(admin_required)]
|
|
)
|
|
async def router_admin_get_share_list(
|
|
session: SessionDep,
|
|
table_view: TableViewRequestDep,
|
|
user_id: UUID | None = None,
|
|
) -> ListResponse[AdminShareListItem]:
|
|
"""
|
|
获取分享列表。
|
|
|
|
:param session: 数据库会话
|
|
:param table_view: 分页排序参数依赖
|
|
:param user_id: 按用户筛选
|
|
:return: 分页分享列表
|
|
"""
|
|
condition = Share.user_id == user_id if user_id else None
|
|
result = await Share.get_with_count(session, condition, table_view=table_view, load=Share.user)
|
|
|
|
items: list[AdminShareListItem] = []
|
|
for s in result.items:
|
|
user = await s.awaitable_attrs.user
|
|
obj = await s.awaitable_attrs.object
|
|
items.append(AdminShareListItem.from_share(s, user, obj))
|
|
|
|
return ListResponse(items=items, count=result.count)
|
|
|
|
|
|
@admin_share_router.get(
|
|
path='/{share_id}',
|
|
summary='获取分享详情',
|
|
description='Get share detail by ID',
|
|
dependencies=[Depends(admin_required)]
|
|
)
|
|
async def router_admin_get_share(
|
|
session: SessionDep,
|
|
share_id: UUID,
|
|
) -> ShareDetailResponse:
|
|
"""
|
|
获取分享详情。
|
|
|
|
:param session: 数据库会话
|
|
:param share_id: 分享ID
|
|
:return: 分享详情
|
|
"""
|
|
share = await Share.get(session, Share.id == share_id, load=Share.object)
|
|
if not share:
|
|
raise HTTPException(status_code=404, detail="分享不存在")
|
|
|
|
obj = await share.awaitable_attrs.object
|
|
user = await share.awaitable_attrs.user
|
|
|
|
return ShareDetailResponse(
|
|
id=share.id,
|
|
code=share.code,
|
|
views=share.views,
|
|
downloads=share.downloads,
|
|
remain_downloads=share.remain_downloads,
|
|
expires=share.expires,
|
|
preview_enabled=share.preview_enabled,
|
|
score=share.score,
|
|
has_password=bool(share.password),
|
|
user_id=str(share.user_id),
|
|
username=user.email if user else None,
|
|
object={
|
|
"id": str(obj.id),
|
|
"name": obj.name,
|
|
"type": obj.type.value,
|
|
"size": obj.size,
|
|
} if obj else None,
|
|
created_at=share.created_at.isoformat(),
|
|
)
|
|
|
|
|
|
@admin_share_router.delete(
|
|
path='/{share_id}',
|
|
summary='删除分享',
|
|
description='Delete share by ID',
|
|
dependencies=[Depends(admin_required)],
|
|
status_code=204,
|
|
)
|
|
async def router_admin_delete_share(
|
|
session: SessionDep,
|
|
share_id: UUID,
|
|
) -> None:
|
|
"""
|
|
删除分享。
|
|
|
|
:param session: 数据库会话
|
|
:param share_id: 分享ID
|
|
:return: 删除结果
|
|
"""
|
|
share = await Share.get(session, Share.id == share_id)
|
|
if not share:
|
|
raise HTTPException(status_code=404, detail="分享不存在")
|
|
|
|
await Share.delete(session, share)
|
|
|
|
l.info(f"管理员删除了分享: {share.code}") |