feat: Implement file download token management and restructure file routes

- Added DownloadTokenManager for creating and verifying JWT download tokens.
- Introduced new download routes for creating download tokens and downloading files using tokens.
- Restructured file upload routes into a dedicated sub-router.
- Updated file upload session management with improved error handling and response structures.
- Created a new MCP (Microservice Communication Protocol) router with basic request and response models.
- Added base models for MCP requests and responses, including method enumeration.
This commit is contained in:
2025-12-23 18:12:11 +08:00
parent 4cd13e4075
commit 16cec42181
14 changed files with 1884 additions and 396 deletions

View File

@@ -96,4 +96,3 @@
- 最大文件大小 - 最大文件大小
- 平台 支持列表 ['all', 'mobile', 'desktop'] - 平台 支持列表 ['all', 'mobile', 'desktop']
- 是否在新窗口打开 - 是否在新窗口打开

View File

@@ -171,7 +171,7 @@ fastapi dev
fastapi run fastapi run
``` ```
访问 http://localhost:8000/docs 查看 API 文档。 访问 <http://localhost:8000/docs> 查看 API 文档。
## 测试 ## 测试

View File

@@ -21,6 +21,7 @@
### 已完成 ### 已完成
#### 基础架构 #### 基础架构
- [x] FastAPI 应用框架搭建 - [x] FastAPI 应用框架搭建
- [x] SQLModel ORM 集成 - [x] SQLModel ORM 集成
- [x] 异步数据库支持 (aiosqlite) - [x] 异步数据库支持 (aiosqlite)
@@ -28,6 +29,7 @@
- [x] 开发规范文档 (CLAUDE.md) - [x] 开发规范文档 (CLAUDE.md)
#### 数据模型 #### 数据模型
- [x] 基类定义 (SQLModelBase, TableBase, UUIDTableBase) - [x] 基类定义 (SQLModelBase, TableBase, UUIDTableBase)
- [x] 用户模型 (User) - [x] 用户模型 (User)
- [x] 用户组模型 (Group, GroupOptions) - [x] 用户组模型 (Group, GroupOptions)
@@ -40,6 +42,7 @@
- [x] 其他模型 (Order, Redeem, Report, Task, SourceLink, StoragePack, Download, Node) - [x] 其他模型 (Order, Redeem, Report, Task, SourceLink, StoragePack, Download, Node)
#### 用户系统 #### 用户系统
- [x] 用户注册接口 - [x] 用户注册接口
- [x] 用户登录接口 (OAuth2.1 Password Grant) - [x] 用户登录接口 (OAuth2.1 Password Grant)
- [x] JWT 令牌认证 - [x] JWT 令牌认证
@@ -47,6 +50,7 @@
- [x] 用户存储空间查询 - [x] 用户存储空间查询
#### 认证安全 #### 认证安全
- [x] Argon2 密码哈希 - [x] Argon2 密码哈希
- [x] JWT 令牌生成与验证 - [x] JWT 令牌生成与验证
- [x] 认证中间件 - [x] 认证中间件
@@ -54,6 +58,7 @@
- [x] WebAuthn 注册初始化 - [x] WebAuthn 注册初始化
#### 测试 #### 测试
- [x] pytest 测试框架配置 - [x] pytest 测试框架配置
- [x] 单元测试结构 - [x] 单元测试结构
- [x] 集成测试结构 - [x] 集成测试结构
@@ -62,17 +67,20 @@
### 进行中 ### 进行中
#### 用户系统 #### 用户系统
- [ ] WebAuthn 完整流程 - [ ] WebAuthn 完整流程
- [ ] OAuth 第三方登录 (QQ, GitHub) - [ ] OAuth 第三方登录 (QQ, GitHub)
- [ ] 用户设置管理 - [ ] 用户设置管理
- [ ] 头像上传/Gravatar - [ ] 头像上传/Gravatar
#### 目录系统 #### 目录系统
- [ ] 目录浏览接口 - [ ] 目录浏览接口
- [ ] 目录创建接口 - [ ] 目录创建接口
- [ ] 路径解析优化 - [ ] 路径解析优化
#### 存储策略 #### 存储策略
- [ ] 本地存储策略实现 - [ ] 本地存储策略实现
- [ ] S3 存储策略实现 - [ ] S3 存储策略实现

View File

@@ -38,15 +38,6 @@ async def AuthRequired(
except InvalidTokenError: except InvalidTokenError:
raise credentials_exception raise credentials_exception
async def SignRequired(
session: SessionDep,
token: Annotated[str, Depends(JWT.oauth2_scheme)],
) -> User | None:
"""
SignAuthRequired 需要验证请求签名
"""
pass
async def AdminRequired( async def AdminRequired(
user: Annotated[User, Depends(AuthRequired)], user: Annotated[User, Depends(AuthRequired)],
) -> User: ) -> User:

View File

@@ -8,6 +8,10 @@ from .user import (
UserResponse, UserResponse,
UserSettingResponse, UserSettingResponse,
WebAuthnInfo, WebAuthnInfo,
# 管理员DTO
UserAdminUpdateRequest,
UserCalibrateResponse,
UserAdminDetailResponse,
) )
from .user_authn import AuthnResponse, UserAuthn from .user_authn import AuthnResponse, UserAuthn
from .color import ThemeResponse from .color import ThemeResponse
@@ -27,7 +31,11 @@ from .node import (
NodeStatus, NodeStatus,
NodeType, NodeType,
) )
from .group import Group, GroupBase, GroupOptions, GroupOptionsBase, GroupResponse from .group import (
Group, GroupBase, GroupOptions, GroupOptionsBase, GroupResponse,
# 管理员DTO
GroupCreateRequest, GroupUpdateRequest, GroupDetailResponse, GroupListResponse,
)
from .object import ( from .object import (
CreateFileRequest, CreateFileRequest,
CreateUploadSessionRequest, CreateUploadSessionRequest,
@@ -50,13 +58,21 @@ from .object import (
UploadSession, UploadSession,
UploadSessionBase, UploadSessionBase,
UploadSessionResponse, UploadSessionResponse,
# 管理员DTO
AdminFileResponse,
AdminFileListResponse,
FileBanRequest,
) )
from .physical_file import PhysicalFile, PhysicalFileBase from .physical_file import PhysicalFile, PhysicalFileBase
from .order import Order, OrderStatus, OrderType from .order import Order, OrderStatus, OrderType
from .policy import Policy, PolicyOptions, PolicyOptionsBase, PolicyType from .policy import Policy, PolicyOptions, PolicyOptionsBase, PolicyType
from .redeem import Redeem, RedeemType from .redeem import Redeem, RedeemType
from .report import Report, ReportReason from .report import Report, ReportReason
from .setting import Setting, SettingsType, SiteConfigResponse from .setting import (
Setting, SettingsType, SiteConfigResponse,
# 管理员DTO
SettingItem, SettingsUpdateRequest, SettingsGetResponse,
)
from .share import Share from .share import Share
from .source_link import SourceLink from .source_link import SourceLink
from .storage_pack import StoragePack from .storage_pack import StoragePack
@@ -66,13 +82,10 @@ from .webdav import WebDAV
from .database import engine, get_session from .database import engine, get_session
from .model_base import (
import uuid MCPBase,
from sqlmodel import Field MCPMethod,
from .base import SQLModelBase MCPRequestBase,
MCPResponseBase,
class ResponseBase(SQLModelBase): ResponseBase,
"""通用响应模型""" )
instance_id: uuid.UUID = Field(default_factory=uuid.uuid4)
"""实例ID用于标识请求的唯一性"""

View File

@@ -45,6 +45,151 @@ class GroupOptionsBase(SQLModelBase):
# ==================== DTO 模型 ==================== # ==================== DTO 模型 ====================
class GroupCreateRequest(SQLModelBase):
"""创建用户组请求 DTO"""
name: str = Field(max_length=255)
"""用户组名称"""
max_storage: int = Field(default=0, ge=0)
"""最大存储空间字节0表示不限制"""
share_enabled: bool = False
"""是否允许创建分享"""
web_dav_enabled: bool = False
"""是否允许使用WebDAV"""
speed_limit: int = Field(default=0, ge=0)
"""速度限制 (KB/s), 0为不限制"""
# 用户组选项
share_download: bool = False
"""是否允许分享下载"""
share_free: bool = False
"""是否免积分获取需要积分的内容"""
relocate: bool = False
"""是否允许文件重定位"""
source_batch: int = Field(default=0, ge=0)
"""批量获取源地址数量"""
select_node: bool = False
"""是否允许选择节点"""
advance_delete: bool = False
"""是否允许高级删除"""
archive_download: bool = False
"""是否允许打包下载"""
archive_task: bool = False
"""是否允许创建打包任务"""
webdav_proxy: bool = False
"""是否允许WebDAV代理"""
aria2: bool = False
"""是否允许使用aria2"""
redirected_source: bool = False
"""是否使用重定向源"""
policy_ids: list[UUID] = []
"""关联的存储策略UUID列表"""
class GroupUpdateRequest(SQLModelBase):
"""更新用户组请求 DTO所有字段可选"""
name: str | None = Field(default=None, max_length=255)
"""用户组名称"""
max_storage: int | None = Field(default=None, ge=0)
"""最大存储空间(字节)"""
share_enabled: bool | None = None
"""是否允许创建分享"""
web_dav_enabled: bool | None = None
"""是否允许使用WebDAV"""
speed_limit: int | None = Field(default=None, ge=0)
"""速度限制 (KB/s)"""
# 用户组选项
share_download: bool | None = None
share_free: bool | None = None
relocate: bool | None = None
source_batch: int | None = None
select_node: bool | None = None
advance_delete: bool | None = None
archive_download: bool | None = None
archive_task: bool | None = None
webdav_proxy: bool | None = None
aria2: bool | None = None
redirected_source: bool | None = None
policy_ids: list[UUID] | None = None
"""关联的存储策略UUID列表"""
class GroupDetailResponse(SQLModelBase):
"""用户组详情响应 DTO"""
id: UUID
"""用户组UUID"""
name: str
"""用户组名称"""
max_storage: int = 0
"""最大存储空间(字节)"""
share_enabled: bool = False
"""是否允许创建分享"""
web_dav_enabled: bool = False
"""是否允许使用WebDAV"""
admin: bool = False
"""是否为管理员组"""
speed_limit: int = 0
"""速度限制 (KB/s)"""
user_count: int = 0
"""用户数量"""
policy_ids: list[UUID] = []
"""关联的存储策略UUID列表"""
# 选项
share_download: bool = False
share_free: bool = False
relocate: bool = False
source_batch: int = 0
select_node: bool = False
advance_delete: bool = False
archive_download: bool = False
archive_task: bool = False
webdav_proxy: bool = False
aria2: bool = False
redirected_source: bool = False
class GroupListResponse(SQLModelBase):
"""用户组列表响应 DTO"""
groups: list["GroupDetailResponse"] = []
"""用户组列表"""
total: int = 0
"""总数"""
class GroupResponse(GroupBase, GroupOptionsBase): class GroupResponse(GroupBase, GroupOptionsBase):
"""用户组响应 DTO""" """用户组响应 DTO"""

39
models/model_base.py Normal file
View File

@@ -0,0 +1,39 @@
import uuid
from enum import StrEnum
from sqlmodel import Field
from .base import SQLModelBase
class MCPMethod(StrEnum):
"""MCP 方法枚举"""
PING = "ping"
"""Ping 方法,用于测试连接"""
class MCPBase(SQLModelBase):
"""MCP 请求基础模型"""
jsonrpc: str = "2.0"
"""JSON-RPC 版本"""
id: uuid.UUID = Field(default_factory=uuid.uuid4)
"""请求/响应 ID用于标识请求/响应的唯一性"""
class MCPRequestBase(MCPBase):
"""MCP 请求模型基础类"""
method: str
"""方法名称"""
class MCPResponseBase(MCPBase):
"""MCP 响应模型基础类"""
result: str
"""方法返回结果"""
class ResponseBase(SQLModelBase):
"""通用响应模型"""
instance_id: uuid.UUID = Field(default_factory=uuid.uuid4)
"""实例ID用于标识请求的唯一性"""

View File

@@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Literal
from uuid import UUID from uuid import UUID
from enum import StrEnum from enum import StrEnum
from sqlmodel import Field, Relationship, UniqueConstraint, CheckConstraint, Index from sqlmodel import Field, Relationship, UniqueConstraint, CheckConstraint, Index, text
from .base import SQLModelBase from .base import SQLModelBase
from .mixin import UUIDTableBaseMixin from .mixin import UUIDTableBaseMixin
@@ -258,11 +258,39 @@ class Object(ObjectBase, UUIDTableBaseMixin):
) )
"""存储策略UUID文件直接使用目录作为子文件的默认策略""" """存储策略UUID文件直接使用目录作为子文件的默认策略"""
# ==================== 封禁相关字段 ====================
is_banned: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")})
"""是否被封禁"""
banned_at: datetime | None = None
"""封禁时间"""
banned_by: UUID | None = Field(
default=None,
foreign_key="user.id",
index=True,
ondelete="SET NULL",
sa_column_kwargs={"name": "banned_by"}
)
"""封禁操作者UUID"""
ban_reason: str | None = Field(default=None, max_length=500)
"""封禁原因"""
# ==================== 关系 ==================== # ==================== 关系 ====================
owner: "User" = Relationship(back_populates="objects") owner: "User" = Relationship(
back_populates="objects",
sa_relationship_kwargs={"foreign_keys": "[Object.owner_id]"}
)
"""所有者""" """所有者"""
banner: "User" = Relationship(
sa_relationship_kwargs={"foreign_keys": "[Object.banned_by]"}
)
"""封禁操作者"""
policy: "Policy" = Relationship(back_populates="objects") policy: "Policy" = Relationship(back_populates="objects")
"""存储策略""" """存储策略"""
@@ -642,3 +670,47 @@ class ObjectPropertyDetailResponse(ObjectPropertyResponse):
reference_count: int = 1 reference_count: int = 1
"""物理文件引用计数(仅文件有效)""" """物理文件引用计数(仅文件有效)"""
# ==================== 管理员文件管理 DTO ====================
class AdminFileResponse(ObjectResponse):
"""管理员文件响应 DTO"""
owner_id: UUID
"""所有者UUID"""
owner_username: str
"""所有者用户名"""
policy_name: str
"""存储策略名称"""
is_banned: bool = False
"""是否被封禁"""
banned_at: datetime | None = None
"""封禁时间"""
ban_reason: str | None = None
"""封禁原因"""
class FileBanRequest(SQLModelBase):
"""文件封禁请求 DTO"""
is_banned: bool = True
"""是否封禁"""
reason: str | None = Field(default=None, max_length=500)
"""封禁原因"""
class AdminFileListResponse(SQLModelBase):
"""管理员文件列表响应 DTO"""
files: list[AdminFileResponse] = []
"""文件列表"""
total: int = 0
"""总数"""

View File

@@ -40,6 +40,32 @@ class SiteConfigResponse(SQLModelBase):
"""验证码密钥""" """验证码密钥"""
# ==================== 管理员设置 DTO ====================
class SettingItem(SQLModelBase):
"""设置项 DTO"""
name: str
"""设置项名称"""
value: str | None = None
"""设置值"""
class SettingsUpdateRequest(SQLModelBase):
"""更新设置请求 DTO"""
settings: dict[str, dict[str, str | None]]
"""按类型分组的设置项,格式: {"basic": {"siteName": "xxx", ...}, ...}"""
class SettingsGetResponse(SQLModelBase):
"""获取设置响应 DTO"""
settings: dict[str, dict[str, str | None]] = {}
"""按类型分组的设置项"""
# ==================== 数据库模型 ==================== # ==================== 数据库模型 ====================
class SettingsType(StrEnum): class SettingsType(StrEnum):

View File

@@ -201,6 +201,68 @@ class UserSettingResponse(SQLModelBase):
"""用户UUID""" """用户UUID"""
# ==================== 管理员用户管理 DTO ====================
class UserAdminUpdateRequest(SQLModelBase):
"""管理员更新用户请求 DTO"""
nickname: str | None = Field(default=None, max_length=50)
"""昵称"""
password: str | None = None
"""新密码(为空则不修改)"""
group_id: UUID | None = None
"""用户组UUID"""
status: bool | None = None
"""用户状态"""
score: int | None = Field(default=None, ge=0)
"""积分"""
storage: int | None = Field(default=None, ge=0)
"""已用存储空间(用于手动校准)"""
group_expires: datetime | None = None
"""用户组过期时间"""
class UserCalibrateResponse(SQLModelBase):
"""用户存储校准响应 DTO"""
user_id: UUID
"""用户UUID"""
previous_storage: int
"""校准前的存储空间(字节)"""
current_storage: int
"""校准后的存储空间(字节)"""
difference: int
"""差异值(字节)"""
file_count: int
"""实际文件数量"""
class UserAdminDetailResponse(UserPublic):
"""管理员用户详情响应 DTO"""
two_factor_enabled: bool = False
"""是否启用两步验证"""
file_count: int = 0
"""文件数量"""
share_count: int = 0
"""分享数量"""
task_count: int = 0
"""任务数量"""
# 前向引用导入 # 前向引用导入
from .group import GroupResponse # noqa: E402 from .group import GroupResponse # noqa: E402
from .user_authn import AuthnResponse # noqa: E402 from .user_authn import AuthnResponse # noqa: E402

View File

@@ -7,12 +7,13 @@ from .admin import admin_group_router
from .admin import admin_policy_router from .admin import admin_policy_router
from .admin import admin_share_router from .admin import admin_share_router
from .admin import admin_task_router from .admin import admin_task_router
from .admin import admin_user_router
from .admin import admin_vas_router from .admin import admin_vas_router
from .callback import callback_router from .callback import callback_router
from .directory import directory_router from .directory import directory_router
from .download import download_router from .download import download_router
from .file import file_router, file_upload_router from .file import router as file_router
from .object import object_router from .object import object_router
from .share import share_router from .share import share_router
from .site import site_router from .site import site_router
@@ -30,13 +31,13 @@ router.include_router(admin_group_router)
router.include_router(admin_policy_router) router.include_router(admin_policy_router)
router.include_router(admin_share_router) router.include_router(admin_share_router)
router.include_router(admin_task_router) router.include_router(admin_task_router)
router.include_router(admin_user_router)
router.include_router(admin_vas_router) router.include_router(admin_vas_router)
router.include_router(callback_router) router.include_router(callback_router)
router.include_router(directory_router) router.include_router(directory_router)
router.include_router(download_router) router.include_router(download_router)
router.include_router(file_router) router.include_router(file_router)
router.include_router(file_upload_router)
router.include_router(object_router) router.include_router(object_router)
router.include_router(share_router) router.include_router(share_router)
router.include_router(site_router) router.include_router(site_router)

File diff suppressed because it is too large Load Diff

View File

@@ -3,19 +3,21 @@
提供文件上传、下载、创建等核心功能。 提供文件上传、下载、创建等核心功能。
路由前缀 路由结构
- /file - 文件操作 - /file - 文件操作
- /file/upload - 上传相关操作 - /file/upload - 上传相关操作
- /file/download - 下载相关操作
""" """
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
from typing import Annotated from typing import Annotated
from uuid import UUID from uuid import UUID
import jwt
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from loguru import logger as l from loguru import logger as l
from middleware.auth import AuthRequired, SignRequired from middleware.auth import AuthRequired
from middleware.dependencies import SessionDep from middleware.dependencies import SessionDep
from models import ( from models import (
CreateFileRequest, CreateFileRequest,
@@ -25,28 +27,61 @@ from models import (
PhysicalFile, PhysicalFile,
Policy, Policy,
PolicyType, PolicyType,
ResponseBase,
UploadChunkResponse, UploadChunkResponse,
UploadSession, UploadSession,
UploadSessionResponse, UploadSessionResponse,
User, User,
) )
from models import ResponseBase from service.storage import LocalStorageService
from service.storage import LocalStorageService, StorageFileNotFoundError from utils.JWT import SECRET_KEY
file_router = APIRouter(
prefix="/file",
tags=["file"]
)
file_upload_router = APIRouter(
prefix="/file/upload",
tags=["file"]
)
# ==================== 上传会话管理 ==================== # ==================== 下载令牌管理 ====================
@file_upload_router.put( class DownloadTokenManager:
"""下载令牌管理器JWT 无状态)"""
_ttl: timedelta = timedelta(hours=1)
@classmethod
def create(cls, file_id: UUID, owner_id: int) -> str:
"""创建下载令牌"""
payload = {
"file_id": str(file_id),
"owner_id": owner_id,
"exp": datetime.now(timezone.utc) + cls._ttl,
"type": "download",
}
return jwt.encode(payload, SECRET_KEY, algorithm="HS256")
@classmethod
def verify(cls, token: str) -> tuple[UUID, int] | None:
"""
验证令牌并返回 (file_id, owner_id)
:return: (file_id, owner_id) 或 None验证失败
"""
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
if payload.get("type") != "download":
return None
return UUID(payload["file_id"]), payload["owner_id"]
except (jwt.ExpiredSignatureError, jwt.InvalidTokenError):
return None
# ==================== 主路由 ====================
router = APIRouter(prefix="/file", tags=["file"])
# ==================== 上传子路由 ====================
_upload_router = APIRouter(prefix="/upload")
@_upload_router.put(
path='/', path='/',
summary='创建上传会话', summary='创建上传会话',
description='创建文件上传会话返回会话ID用于后续分片上传。', description='创建文件上传会话返回会话ID用于后续分片上传。',
@@ -65,11 +100,6 @@ async def create_upload_session(
3. 验证文件大小限制 3. 验证文件大小限制
4. 创建上传会话并生成存储路径 4. 创建上传会话并生成存储路径
5. 返回会话信息 5. 返回会话信息
:param session: 数据库会话
:param user: 当前登录用户
:param request: 创建请求
:return: 上传会话信息
""" """
# 验证文件名 # 验证文件名
if not request.file_name or '/' in request.file_name or '\\' in request.file_name: if not request.file_name or '/' in request.file_name or '\\' in request.file_name:
@@ -121,7 +151,6 @@ async def create_upload_session(
) )
storage_path = full_path storage_path = full_path
else: else:
# S3 后续实现
raise HTTPException(status_code=501, detail="S3 存储暂未实现") raise HTTPException(status_code=501, detail="S3 存储暂未实现")
# 创建上传会话 # 创建上传会话
@@ -131,7 +160,7 @@ async def create_upload_session(
chunk_size=chunk_size, chunk_size=chunk_size,
total_chunks=total_chunks, total_chunks=total_chunks,
storage_path=storage_path, storage_path=storage_path,
expires_at=datetime.now() + timedelta(hours=24), # 24小时过期 expires_at=datetime.now() + timedelta(hours=24),
owner_id=user.id, owner_id=user.id,
parent_id=request.parent_id, parent_id=request.parent_id,
policy_id=policy_id, policy_id=policy_id,
@@ -151,7 +180,7 @@ async def create_upload_session(
) )
@file_upload_router.post( @_upload_router.post(
path='/{session_id}/{chunk_index}', path='/{session_id}/{chunk_index}',
summary='上传文件分片', summary='上传文件分片',
description='上传指定分片分片索引从0开始。', description='上传指定分片分片索引从0开始。',
@@ -171,13 +200,6 @@ async def upload_chunk(
2. 写入分片数据 2. 写入分片数据
3. 更新会话进度 3. 更新会话进度
4. 如果所有分片上传完成,创建 Object 记录 4. 如果所有分片上传完成,创建 Object 记录
:param session: 数据库会话
:param user: 当前登录用户
:param session_id: 上传会话UUID
:param chunk_index: 分片索引从0开始
:param file: 上传的文件分片
:return: 上传进度信息
""" """
# 获取上传会话 # 获取上传会话
upload_session = await UploadSession.get(session, UploadSession.id == session_id) upload_session = await UploadSession.get(session, UploadSession.id == session_id)
@@ -262,7 +284,7 @@ async def upload_chunk(
) )
@file_upload_router.delete( @_upload_router.delete(
path='/{session_id}', path='/{session_id}',
summary='删除上传会话', summary='删除上传会话',
description='取消上传并删除会话及已上传的临时文件。', description='取消上传并删除会话及已上传的临时文件。',
@@ -272,14 +294,7 @@ async def delete_upload_session(
user: Annotated[User, Depends(AuthRequired)], user: Annotated[User, Depends(AuthRequired)],
session_id: UUID, session_id: UUID,
) -> ResponseBase: ) -> ResponseBase:
""" """删除上传会话端点"""
删除上传会话端点
:param session: 数据库会话
:param user: 当前登录用户
:param session_id: 上传会话UUID
:return: 删除结果
"""
upload_session = await UploadSession.get(session, UploadSession.id == session_id) upload_session = await UploadSession.get(session, UploadSession.id == session_id)
if not upload_session or upload_session.owner_id != user.id: if not upload_session or upload_session.owner_id != user.id:
raise HTTPException(status_code=404, detail="上传会话不存在") raise HTTPException(status_code=404, detail="上传会话不存在")
@@ -298,7 +313,7 @@ async def delete_upload_session(
return ResponseBase(data={"deleted": True}) return ResponseBase(data={"deleted": True})
@file_upload_router.delete( @_upload_router.delete(
path='/', path='/',
summary='清除所有上传会话', summary='清除所有上传会话',
description='清除当前用户的所有上传会话。', description='清除当前用户的所有上传会话。',
@@ -307,13 +322,7 @@ async def clear_upload_sessions(
session: SessionDep, session: SessionDep,
user: Annotated[User, Depends(AuthRequired)], user: Annotated[User, Depends(AuthRequired)],
) -> ResponseBase: ) -> ResponseBase:
""" """清除所有上传会话端点"""
清除所有上传会话端点
:param session: 数据库会话
:param user: 当前登录用户
:return: 清除结果
"""
# 获取所有会话 # 获取所有会话
sessions = await UploadSession.get( sessions = await UploadSession.get(
session, session,
@@ -337,28 +346,74 @@ async def clear_upload_sessions(
return ResponseBase(data={"deleted": deleted_count}) return ResponseBase(data={"deleted": deleted_count})
# ==================== 文件下载 ==================== @_upload_router.get(
path='/archive/{session_id}/archive.zip',
@file_upload_router.get( summary='打包并下载文件',
path='/download/{file_id}', description='获取打包后的文件。',
summary='下载文件',
description='下载指定文件。',
) )
async def download_file( async def download_archive(session_id: str) -> ResponseBase:
"""打包下载"""
raise HTTPException(status_code=501, detail="打包下载功能暂未实现")
# ==================== 下载子路由 ====================
_download_router = APIRouter(prefix="/download")
@_download_router.put(
path='/{file_id}',
summary='创建下载令牌',
description='为指定文件创建下载令牌JWT有效期1小时。',
)
async def create_download_token(
session: SessionDep, session: SessionDep,
user: Annotated[User, Depends(AuthRequired)], user: Annotated[User, Depends(AuthRequired)],
file_id: UUID, file_id: UUID,
) -> ResponseBase:
"""
创建下载令牌端点
验证文件存在且属于当前用户后,生成 JWT 下载令牌。
"""
file_obj = await Object.get(session, Object.id == file_id)
if not file_obj or file_obj.owner_id != user.id:
raise HTTPException(status_code=404, detail="文件不存在")
if not file_obj.is_file:
raise HTTPException(status_code=400, detail="对象不是文件")
token = DownloadTokenManager.create(file_id, user.id)
l.debug(f"创建下载令牌: file_id={file_id}, user_id={user.id}")
return ResponseBase(data={"token": token, "expires_in": 3600})
@_download_router.get(
path='/{token}',
summary='下载文件',
description='使用下载令牌下载文件。',
)
async def download_file(
session: SessionDep,
token: str,
) -> FileResponse: ) -> FileResponse:
""" """
下载文件端点 下载文件端点
:param session: 数据库会话 验证 JWT 令牌后返回文件内容。
:param user: 当前登录用户
:param file_id: 文件UUID
:return: 文件响应
""" """
# 验证令牌
result = DownloadTokenManager.verify(token)
if not result:
raise HTTPException(status_code=401, detail="下载令牌无效或已过期")
file_id, owner_id = result
# 获取文件对象
file_obj = await Object.get(session, Object.id == file_id) file_obj = await Object.get(session, Object.id == file_id)
if not file_obj or file_obj.owner_id != user.id: if not file_obj or file_obj.owner_id != owner_id:
raise HTTPException(status_code=404, detail="文件不存在") raise HTTPException(status_code=404, detail="文件不存在")
if not file_obj.is_file: if not file_obj.is_file:
@@ -386,9 +441,15 @@ async def download_file(
raise HTTPException(status_code=501, detail="S3 存储暂未实现") raise HTTPException(status_code=501, detail="S3 存储暂未实现")
# ==================== 包含子路由 ====================
router.include_router(_upload_router)
router.include_router(_download_router)
# ==================== 创建空白文件 ==================== # ==================== 创建空白文件 ====================
@file_router.post( @router.post(
path='/create', path='/create',
summary='创建空白文件', summary='创建空白文件',
description='在指定目录下创建空白文件。', description='在指定目录下创建空白文件。',
@@ -398,14 +459,7 @@ async def create_empty_file(
user: Annotated[User, Depends(AuthRequired)], user: Annotated[User, Depends(AuthRequired)],
request: CreateFileRequest, request: CreateFileRequest,
) -> ResponseBase: ) -> ResponseBase:
""" """创建空白文件端点"""
创建空白文件端点
:param session: 数据库会话
:param user: 当前登录用户
:param request: 创建请求
:return: 创建结果
"""
# 存储 user.id避免后续 save() 导致 user 过期后无法访问 # 存储 user.id避免后续 save() 导致 user 过期后无法访问
user_id = user.id user_id = user.id
@@ -482,175 +536,146 @@ async def create_empty_file(
# ==================== 文件外链(保留原有端点结构) ==================== # ==================== 文件外链(保留原有端点结构) ====================
@file_router.get( @router.get(
path='/get/{id}/{name}', path='/get/{id}/{name}',
summary='文件外链(直接输出文件数据)', summary='文件外链(直接输出文件数据)',
description='通过外链直接获取文件内容。', description='通过外链直接获取文件内容。',
) )
async def router_file_get( async def file_get(
session: SessionDep, session: SessionDep,
id: str, id: str,
name: str, name: str,
) -> FileResponse: ) -> FileResponse:
""" """文件外链端点(直接输出)"""
文件外链端点(直接输出)
TODO: 实现签名验证和权限控制
"""
raise HTTPException(status_code=501, detail="外链功能暂未实现") raise HTTPException(status_code=501, detail="外链功能暂未实现")
@file_router.get( @router.get(
path='/source/{id}/{name}', path='/source/{id}/{name}',
summary='文件外链(301跳转)', summary='文件外链(301跳转)',
description='通过外链获取文件重定向地址。', description='通过外链获取文件重定向地址。',
) )
async def router_file_source_redirect(id: str, name: str) -> ResponseBase: async def file_source_redirect(id: str, name: str) -> ResponseBase:
""" """文件外链端点301跳转"""
文件外链端点301跳转
TODO: 实现签名验证和重定向
"""
raise HTTPException(status_code=501, detail="外链功能暂未实现") raise HTTPException(status_code=501, detail="外链功能暂未实现")
@file_router.put( @router.put(
path='/update/{id}', path='/update/{id}',
summary='更新文件', summary='更新文件',
description='更新文件内容。', description='更新文件内容。',
dependencies=[Depends(AuthRequired)] dependencies=[Depends(AuthRequired)]
) )
async def router_file_update(id: str) -> ResponseBase: async def file_update(id: str) -> ResponseBase:
"""更新文件内容""" """更新文件内容"""
raise HTTPException(status_code=501, detail="更新文件功能暂未实现") raise HTTPException(status_code=501, detail="更新文件功能暂未实现")
@file_router.get( @router.get(
path='/preview/{id}', path='/preview/{id}',
summary='预览文件', summary='预览文件',
description='获取文件预览。', description='获取文件预览。',
dependencies=[Depends(AuthRequired)] dependencies=[Depends(AuthRequired)]
) )
async def router_file_preview(id: str) -> ResponseBase: async def file_preview(id: str) -> ResponseBase:
"""预览文件""" """预览文件"""
raise HTTPException(status_code=501, detail="预览功能暂未实现") raise HTTPException(status_code=501, detail="预览功能暂未实现")
@file_router.get( @router.get(
path='/content/{id}', path='/content/{id}',
summary='获取文本文件内容', summary='获取文本文件内容',
description='获取文本文件内容。', description='获取文本文件内容。',
dependencies=[Depends(AuthRequired)] dependencies=[Depends(AuthRequired)]
) )
async def router_file_content(id: str) -> ResponseBase: async def file_content(id: str) -> ResponseBase:
"""获取文本文件内容""" """获取文本文件内容"""
raise HTTPException(status_code=501, detail="文本内容功能暂未实现") raise HTTPException(status_code=501, detail="文本内容功能暂未实现")
@file_router.get( @router.get(
path='/doc/{id}', path='/doc/{id}',
summary='获取Office文档预览地址', summary='获取Office文档预览地址',
description='获取Office文档在线预览地址。', description='获取Office文档在线预览地址。',
dependencies=[Depends(AuthRequired)] dependencies=[Depends(AuthRequired)]
) )
async def router_file_doc(id: str) -> ResponseBase: async def file_doc(id: str) -> ResponseBase:
"""获取Office文档预览地址""" """获取Office文档预览地址"""
raise HTTPException(status_code=501, detail="Office预览功能暂未实现") raise HTTPException(status_code=501, detail="Office预览功能暂未实现")
@file_router.get( @router.get(
path='/thumb/{id}', path='/thumb/{id}',
summary='获取文件缩略图', summary='获取文件缩略图',
description='获取文件缩略图。', description='获取文件缩略图。',
dependencies=[Depends(AuthRequired)] dependencies=[Depends(AuthRequired)]
) )
async def router_file_thumb(id: str) -> ResponseBase: async def file_thumb(id: str) -> ResponseBase:
"""获取文件缩略图""" """获取文件缩略图"""
raise HTTPException(status_code=501, detail="缩略图功能暂未实现") raise HTTPException(status_code=501, detail="缩略图功能暂未实现")
@file_router.post( @router.post(
path='/source/{id}', path='/source/{id}',
summary='取得文件外链', summary='取得文件外链',
description='获取文件的外链地址。', description='获取文件的外链地址。',
dependencies=[Depends(AuthRequired)] dependencies=[Depends(AuthRequired)]
) )
async def router_file_source(id: str) -> ResponseBase: async def file_source(id: str) -> ResponseBase:
"""获取文件外链""" """获取文件外链"""
raise HTTPException(status_code=501, detail="外链功能暂未实现") raise HTTPException(status_code=501, detail="外链功能暂未实现")
@file_router.post( @router.post(
path='/archive', path='/archive',
summary='打包要下载的文件', summary='打包要下载的文件',
description='将多个文件打包下载。', description='将多个文件打包下载。',
dependencies=[Depends(AuthRequired)] dependencies=[Depends(AuthRequired)]
) )
async def router_file_archive() -> ResponseBase: async def file_archive() -> ResponseBase:
"""打包文件""" """打包文件"""
raise HTTPException(status_code=501, detail="打包功能暂未实现") raise HTTPException(status_code=501, detail="打包功能暂未实现")
@file_router.post( @router.post(
path='/compress', path='/compress',
summary='创建文件压缩任务', summary='创建文件压缩任务',
description='创建文件压缩任务。', description='创建文件压缩任务。',
dependencies=[Depends(AuthRequired)] dependencies=[Depends(AuthRequired)]
) )
async def router_file_compress() -> ResponseBase: async def file_compress() -> ResponseBase:
"""创建压缩任务""" """创建压缩任务"""
raise HTTPException(status_code=501, detail="压缩功能暂未实现") raise HTTPException(status_code=501, detail="压缩功能暂未实现")
@file_router.post( @router.post(
path='/decompress', path='/decompress',
summary='创建文件解压任务', summary='创建文件解压任务',
description='创建文件解压任务。', description='创建文件解压任务。',
dependencies=[Depends(AuthRequired)] dependencies=[Depends(AuthRequired)]
) )
async def router_file_decompress() -> ResponseBase: async def file_decompress() -> ResponseBase:
"""创建解压任务""" """创建解压任务"""
raise HTTPException(status_code=501, detail="解压功能暂未实现") raise HTTPException(status_code=501, detail="解压功能暂未实现")
@file_router.post( @router.post(
path='/relocate', path='/relocate',
summary='创建文件转移任务', summary='创建文件转移任务',
description='创建文件转移任务。', description='创建文件转移任务。',
dependencies=[Depends(AuthRequired)] dependencies=[Depends(AuthRequired)]
) )
async def router_file_relocate() -> ResponseBase: async def file_relocate() -> ResponseBase:
"""创建转移任务""" """创建转移任务"""
raise HTTPException(status_code=501, detail="转移功能暂未实现") raise HTTPException(status_code=501, detail="转移功能暂未实现")
@file_router.get( @router.get(
path='/search/{type}/{keyword}', path='/search/{type}/{keyword}',
summary='搜索文件', summary='搜索文件',
description='按关键字搜索文件。', description='按关键字搜索文件。',
dependencies=[Depends(AuthRequired)] dependencies=[Depends(AuthRequired)]
) )
async def router_file_search(type: str, keyword: str) -> ResponseBase: async def file_search(type: str, keyword: str) -> ResponseBase:
"""搜索文件""" """搜索文件"""
raise HTTPException(status_code=501, detail="搜索功能暂未实现") raise HTTPException(status_code=501, detail="搜索功能暂未实现")
@file_upload_router.get(
path='/archive/{sessionID}/archive.zip',
summary='打包并下载文件',
description='获取打包后的文件。',
)
async def router_file_archive_download(sessionID: str) -> ResponseBase:
"""打包下载"""
raise HTTPException(status_code=501, detail="打包下载功能暂未实现")
@file_router.put(
path='/download/{id}',
summary='创建文件下载会话',
description='创建文件下载会话。',
dependencies=[Depends(AuthRequired)]
)
async def router_file_download_session(id: str) -> ResponseBase:
"""创建下载会话"""
raise HTTPException(status_code=501, detail="下载会话功能暂未实现")

View File

@@ -0,0 +1,19 @@
from fastapi import APIRouter
from models import MCPRequestBase, MCPResponseBase, MCPMethod
# MCP 路由
MCP_router = APIRouter(
prefix='/mcp',
tags=["mcp"],
)
@MCP_router.get(
"/",
)
async def mcp_root(
param: MCPRequestBase
):
match param.method:
case MCPMethod.PING:
return MCPResponseBase(result="pong", **param.model_dump())