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:
165
PLAN.md
165
PLAN.md
@@ -1,89 +1,89 @@
|
||||
- 数据模型初步规划
|
||||
- 基类
|
||||
- ID:UUIDv7
|
||||
- 创建时间
|
||||
- 更新时间
|
||||
- User
|
||||
- 用户名 唯一
|
||||
- 昵称
|
||||
- Argon2化的密码
|
||||
- 用户当前的状态 `正常/手动封禁/系统封禁` 用 StrEnum 存储
|
||||
- 已用存储空间
|
||||
- 用户积分
|
||||
- 默认用户组
|
||||
- Optional[两步验证密钥]
|
||||
- Optional[WebAuthn 凭证]
|
||||
- Optional[头像地址] -> None为昵称首字/图片/Gavatar头像
|
||||
- Optional[当前用户组过期时间] -> 用户组过期后回退默认用户组
|
||||
- 用户的个人设置 计划中 考虑 BaseModel 进行嵌套
|
||||
- Group `与 User 的关系:一个用户组可以有多个用户,但一个用户只能在一个用户组中`
|
||||
- 组名 唯一
|
||||
- list[[允许的存储策略, 该策略下最大允许使用的容量, 该策略下允许上传文件的白名单/黑名单]]
|
||||
- 默认存储策略
|
||||
- bool[分享内容]
|
||||
- bool[是否管理员组]
|
||||
- 速度限制
|
||||
- bool[打包下载]
|
||||
- bool[创建压缩/解压缩任务]
|
||||
- bool[WebDAV]
|
||||
- bool[WebDAV 反代]
|
||||
- bool[离线下载]
|
||||
- Object `计划:把 file 和 folder 合并为一个 Object 表,通过对象类型区分`
|
||||
- 对象名
|
||||
- 区分大小写
|
||||
- 禁止名称为特殊字段 (如 `/`, `\`, `:`, `*`, `?`, `<`, `>`, `:`, `"`)
|
||||
- 对象类型[file, folder, link]
|
||||
- 当对象类型为file时
|
||||
- 源文件名
|
||||
- 文件大小
|
||||
- 分块上传会话ID
|
||||
- 文件MD5, SHA1, SHA256
|
||||
- 文件元数据
|
||||
- 音频:歌名、歌手名、专辑、流派...
|
||||
- 图片: 尺寸、ISO、曝光、拍摄设备、地理位置...
|
||||
- 其他需要记录的元数据
|
||||
- 当对象类型为 folder 时
|
||||
- 当前文件夹的视图(网格/列表/画廊)
|
||||
- 排序规则(按名称/大小/上传时间/修改时间)
|
||||
- 排序方式(升序/降序)
|
||||
- 当对象类型为 link 时
|
||||
- 目标对象ID
|
||||
- 外键
|
||||
- 用户ID 用于确保该对象的归属
|
||||
- 目录ID 用于定位该对象存在哪个目录
|
||||
- 策略ID 用于定位该对象存储于哪个存储策略
|
||||
- Policy
|
||||
- 存储策略名
|
||||
- 策略类型(如 `本机`, `从机`, `s3`, `OSS`, `COS` 等)
|
||||
- 服务器地址(本机则为路径)
|
||||
- 存储桶名称
|
||||
- 允许上传文件的最大字节数
|
||||
- 目录命名规则
|
||||
- 文件命名规则
|
||||
- 文件后缀白名单/黑名单
|
||||
- 分片上传大小
|
||||
- `待研究`
|
||||
- Tag
|
||||
- 标签名称
|
||||
- 标签图标
|
||||
- 标签颜色
|
||||
- 外键
|
||||
- 用户ID 用于确保该标签的归属
|
||||
|
||||
- 基类
|
||||
- ID:UUIDv7
|
||||
- 创建时间
|
||||
- 更新时间
|
||||
- User
|
||||
- 用户名 唯一
|
||||
- 昵称
|
||||
- Argon2化的密码
|
||||
- 用户当前的状态 `正常/手动封禁/系统封禁` 用 StrEnum 存储
|
||||
- 已用存储空间
|
||||
- 用户积分
|
||||
- 默认用户组
|
||||
- Optional[两步验证密钥]
|
||||
- Optional[WebAuthn 凭证]
|
||||
- Optional[头像地址] -> None为昵称首字/图片/Gavatar头像
|
||||
- Optional[当前用户组过期时间] -> 用户组过期后回退默认用户组
|
||||
- 用户的个人设置 计划中 考虑 BaseModel 进行嵌套
|
||||
- Group `与 User 的关系:一个用户组可以有多个用户,但一个用户只能在一个用户组中`
|
||||
- 组名 唯一
|
||||
- list[[允许的存储策略, 该策略下最大允许使用的容量, 该策略下允许上传文件的白名单/黑名单]]
|
||||
- 默认存储策略
|
||||
- bool[分享内容]
|
||||
- bool[是否管理员组]
|
||||
- 速度限制
|
||||
- bool[打包下载]
|
||||
- bool[创建压缩/解压缩任务]
|
||||
- bool[WebDAV]
|
||||
- bool[WebDAV 反代]
|
||||
- bool[离线下载]
|
||||
- Object `计划:把 file 和 folder 合并为一个 Object 表,通过对象类型区分`
|
||||
- 对象名
|
||||
- 区分大小写
|
||||
- 禁止名称为特殊字段 (如 `/`, `\`, `:`, `*`, `?`, `<`, `>`, `:`, `"`)
|
||||
- 对象类型[file, folder, link]
|
||||
- 当对象类型为file时
|
||||
- 源文件名
|
||||
- 文件大小
|
||||
- 分块上传会话ID
|
||||
- 文件MD5, SHA1, SHA256
|
||||
- 文件元数据
|
||||
- 音频:歌名、歌手名、专辑、流派...
|
||||
- 图片: 尺寸、ISO、曝光、拍摄设备、地理位置...
|
||||
- 其他需要记录的元数据
|
||||
- 当对象类型为 folder 时
|
||||
- 当前文件夹的视图(网格/列表/画廊)
|
||||
- 排序规则(按名称/大小/上传时间/修改时间)
|
||||
- 排序方式(升序/降序)
|
||||
- 当对象类型为 link 时
|
||||
- 目标对象ID
|
||||
- 外键
|
||||
- 用户ID 用于确保该对象的归属
|
||||
- 目录ID 用于定位该对象存在哪个目录
|
||||
- 策略ID 用于定位该对象存储于哪个存储策略
|
||||
- Policy
|
||||
- 存储策略名
|
||||
- 策略类型(如 `本机`, `从机`, `s3`, `OSS`, `COS` 等)
|
||||
- 服务器地址(本机则为路径)
|
||||
- 存储桶名称
|
||||
- 允许上传文件的最大字节数
|
||||
- 目录命名规则
|
||||
- 文件命名规则
|
||||
- 文件后缀白名单/黑名单
|
||||
- 分片上传大小
|
||||
- `待研究`
|
||||
- Tag
|
||||
- 标签名称
|
||||
- 标签图标
|
||||
- 标签颜色
|
||||
- 外键
|
||||
- 用户ID 用于确保该标签的归属
|
||||
|
||||
- 运行环境与目标
|
||||
- 数据库类型:主要支持 PostgreSQL 18,考虑兼容 SQLite/MySQL/早期版本PostgreSQL
|
||||
- 驱动版本:做一定的向下兼容,主要支持 Python 3.13+
|
||||
- 异步栈:全量异步 AsyncSession,不接受兼容同步
|
||||
- 数据库类型:主要支持 PostgreSQL 18,考虑兼容 SQLite/MySQL/早期版本PostgreSQL
|
||||
- 驱动版本:做一定的向下兼容,主要支持 Python 3.13+
|
||||
- 异步栈:全量异步 AsyncSession,不接受兼容同步
|
||||
- 业务语义与数据模型
|
||||
- 时间与时区:统一存储 UTC,再根据用户选择的时区计算本地化时间
|
||||
- 文件/目录命名规则
|
||||
- 文件名在同一目录下唯一
|
||||
- 目录名在同一账户下的同一父目录下唯一
|
||||
- 资源分享
|
||||
- 用户可以分享单个文件,也可以分享整个目录
|
||||
- 时间与时区:统一存储 UTC,再根据用户选择的时区计算本地化时间
|
||||
- 文件/目录命名规则
|
||||
- 文件名在同一目录下唯一
|
||||
- 目录名在同一账户下的同一父目录下唯一
|
||||
- 资源分享
|
||||
- 用户可以分享单个文件,也可以分享整个目录
|
||||
- 关系与级联
|
||||
- 删除文件夹时,同时删除该文件夹内的所有子文件夹及其所有文件
|
||||
- 删除用户时,同时删除该用户的所有文件,文件夹,分享,Tag
|
||||
- 删除文件夹时,同时删除该文件夹内的所有子文件夹及其所有文件
|
||||
- 删除用户时,同时删除该用户的所有文件,文件夹,分享,Tag
|
||||
- 文件预览与编辑
|
||||
- 预览应用 Literal['嵌入网页式应用', 'WOPI协议式应用']
|
||||
- 是否启用
|
||||
@@ -96,4 +96,3 @@
|
||||
- 最大文件大小
|
||||
- 平台 支持列表 ['all', 'mobile', 'desktop']
|
||||
- 是否在新窗口打开
|
||||
|
||||
|
||||
@@ -171,7 +171,7 @@ fastapi dev
|
||||
fastapi run
|
||||
```
|
||||
|
||||
访问 http://localhost:8000/docs 查看 API 文档。
|
||||
访问 <http://localhost:8000/docs> 查看 API 文档。
|
||||
|
||||
## 测试
|
||||
|
||||
|
||||
10
ROADMAP.md
10
ROADMAP.md
@@ -21,6 +21,7 @@
|
||||
### 已完成
|
||||
|
||||
#### 基础架构
|
||||
|
||||
- [x] FastAPI 应用框架搭建
|
||||
- [x] SQLModel ORM 集成
|
||||
- [x] 异步数据库支持 (aiosqlite)
|
||||
@@ -28,6 +29,7 @@
|
||||
- [x] 开发规范文档 (CLAUDE.md)
|
||||
|
||||
#### 数据模型
|
||||
|
||||
- [x] 基类定义 (SQLModelBase, TableBase, UUIDTableBase)
|
||||
- [x] 用户模型 (User)
|
||||
- [x] 用户组模型 (Group, GroupOptions)
|
||||
@@ -40,6 +42,7 @@
|
||||
- [x] 其他模型 (Order, Redeem, Report, Task, SourceLink, StoragePack, Download, Node)
|
||||
|
||||
#### 用户系统
|
||||
|
||||
- [x] 用户注册接口
|
||||
- [x] 用户登录接口 (OAuth2.1 Password Grant)
|
||||
- [x] JWT 令牌认证
|
||||
@@ -47,6 +50,7 @@
|
||||
- [x] 用户存储空间查询
|
||||
|
||||
#### 认证安全
|
||||
|
||||
- [x] Argon2 密码哈希
|
||||
- [x] JWT 令牌生成与验证
|
||||
- [x] 认证中间件
|
||||
@@ -54,6 +58,7 @@
|
||||
- [x] WebAuthn 注册初始化
|
||||
|
||||
#### 测试
|
||||
|
||||
- [x] pytest 测试框架配置
|
||||
- [x] 单元测试结构
|
||||
- [x] 集成测试结构
|
||||
@@ -62,17 +67,20 @@
|
||||
### 进行中
|
||||
|
||||
#### 用户系统
|
||||
|
||||
- [ ] WebAuthn 完整流程
|
||||
- [ ] OAuth 第三方登录 (QQ, GitHub)
|
||||
- [ ] 用户设置管理
|
||||
- [ ] 头像上传/Gravatar
|
||||
|
||||
#### 目录系统
|
||||
|
||||
- [ ] 目录浏览接口
|
||||
- [ ] 目录创建接口
|
||||
- [ ] 路径解析优化
|
||||
|
||||
#### 存储策略
|
||||
|
||||
- [ ] 本地存储策略实现
|
||||
- [ ] S3 存储策略实现
|
||||
|
||||
@@ -408,4 +416,4 @@
|
||||
|
||||
---
|
||||
|
||||
*最后更新:2025年12月*
|
||||
*最后更新:2025年12月*
|
||||
|
||||
@@ -38,15 +38,6 @@ async def AuthRequired(
|
||||
except InvalidTokenError:
|
||||
raise credentials_exception
|
||||
|
||||
async def SignRequired(
|
||||
session: SessionDep,
|
||||
token: Annotated[str, Depends(JWT.oauth2_scheme)],
|
||||
) -> User | None:
|
||||
"""
|
||||
SignAuthRequired 需要验证请求签名
|
||||
"""
|
||||
pass
|
||||
|
||||
async def AdminRequired(
|
||||
user: Annotated[User, Depends(AuthRequired)],
|
||||
) -> User:
|
||||
|
||||
@@ -8,6 +8,10 @@ from .user import (
|
||||
UserResponse,
|
||||
UserSettingResponse,
|
||||
WebAuthnInfo,
|
||||
# 管理员DTO
|
||||
UserAdminUpdateRequest,
|
||||
UserCalibrateResponse,
|
||||
UserAdminDetailResponse,
|
||||
)
|
||||
from .user_authn import AuthnResponse, UserAuthn
|
||||
from .color import ThemeResponse
|
||||
@@ -27,7 +31,11 @@ from .node import (
|
||||
NodeStatus,
|
||||
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 (
|
||||
CreateFileRequest,
|
||||
CreateUploadSessionRequest,
|
||||
@@ -50,13 +58,21 @@ from .object import (
|
||||
UploadSession,
|
||||
UploadSessionBase,
|
||||
UploadSessionResponse,
|
||||
# 管理员DTO
|
||||
AdminFileResponse,
|
||||
AdminFileListResponse,
|
||||
FileBanRequest,
|
||||
)
|
||||
from .physical_file import PhysicalFile, PhysicalFileBase
|
||||
from .order import Order, OrderStatus, OrderType
|
||||
from .policy import Policy, PolicyOptions, PolicyOptionsBase, PolicyType
|
||||
from .redeem import Redeem, RedeemType
|
||||
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 .source_link import SourceLink
|
||||
from .storage_pack import StoragePack
|
||||
@@ -66,13 +82,10 @@ from .webdav import WebDAV
|
||||
|
||||
from .database import engine, get_session
|
||||
|
||||
|
||||
import uuid
|
||||
from sqlmodel import Field
|
||||
from .base import SQLModelBase
|
||||
|
||||
class ResponseBase(SQLModelBase):
|
||||
"""通用响应模型"""
|
||||
|
||||
instance_id: uuid.UUID = Field(default_factory=uuid.uuid4)
|
||||
"""实例ID,用于标识请求的唯一性"""
|
||||
from .model_base import (
|
||||
MCPBase,
|
||||
MCPMethod,
|
||||
MCPRequestBase,
|
||||
MCPResponseBase,
|
||||
ResponseBase,
|
||||
)
|
||||
145
models/group.py
145
models/group.py
@@ -45,6 +45,151 @@ class GroupOptionsBase(SQLModelBase):
|
||||
|
||||
# ==================== 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):
|
||||
"""用户组响应 DTO"""
|
||||
|
||||
|
||||
39
models/model_base.py
Normal file
39
models/model_base.py
Normal 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,用于标识请求的唯一性"""
|
||||
@@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Literal
|
||||
from uuid import UUID
|
||||
|
||||
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 .mixin import UUIDTableBaseMixin
|
||||
@@ -258,11 +258,39 @@ class Object(ObjectBase, UUIDTableBaseMixin):
|
||||
)
|
||||
"""存储策略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")
|
||||
"""存储策略"""
|
||||
|
||||
@@ -642,3 +670,47 @@ class ObjectPropertyDetailResponse(ObjectPropertyResponse):
|
||||
|
||||
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
|
||||
"""总数"""
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -201,6 +201,68 @@ class UserSettingResponse(SQLModelBase):
|
||||
"""用户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 .user_authn import AuthnResponse # noqa: E402
|
||||
|
||||
@@ -7,12 +7,13 @@ from .admin import admin_group_router
|
||||
from .admin import admin_policy_router
|
||||
from .admin import admin_share_router
|
||||
from .admin import admin_task_router
|
||||
from .admin import admin_user_router
|
||||
from .admin import admin_vas_router
|
||||
|
||||
from .callback import callback_router
|
||||
from .directory import directory_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 .share import share_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_share_router)
|
||||
router.include_router(admin_task_router)
|
||||
router.include_router(admin_user_router)
|
||||
router.include_router(admin_vas_router)
|
||||
|
||||
router.include_router(callback_router)
|
||||
router.include_router(directory_router)
|
||||
router.include_router(download_router)
|
||||
router.include_router(file_router)
|
||||
router.include_router(file_upload_router)
|
||||
router.include_router(object_router)
|
||||
router.include_router(share_router)
|
||||
router.include_router(site_router)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,19 +3,21 @@
|
||||
|
||||
提供文件上传、下载、创建等核心功能。
|
||||
|
||||
路由前缀:
|
||||
路由结构:
|
||||
- /file - 文件操作
|
||||
- /file/upload - 上传相关操作
|
||||
- /file/download - 下载相关操作
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Annotated
|
||||
from uuid import UUID
|
||||
|
||||
import jwt
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
||||
from fastapi.responses import FileResponse
|
||||
from loguru import logger as l
|
||||
|
||||
from middleware.auth import AuthRequired, SignRequired
|
||||
from middleware.auth import AuthRequired
|
||||
from middleware.dependencies import SessionDep
|
||||
from models import (
|
||||
CreateFileRequest,
|
||||
@@ -25,28 +27,61 @@ from models import (
|
||||
PhysicalFile,
|
||||
Policy,
|
||||
PolicyType,
|
||||
ResponseBase,
|
||||
UploadChunkResponse,
|
||||
UploadSession,
|
||||
UploadSessionResponse,
|
||||
User,
|
||||
)
|
||||
from models import ResponseBase
|
||||
from service.storage import LocalStorageService, StorageFileNotFoundError
|
||||
|
||||
file_router = APIRouter(
|
||||
prefix="/file",
|
||||
tags=["file"]
|
||||
)
|
||||
|
||||
file_upload_router = APIRouter(
|
||||
prefix="/file/upload",
|
||||
tags=["file"]
|
||||
)
|
||||
from service.storage import LocalStorageService
|
||||
from utils.JWT import SECRET_KEY
|
||||
|
||||
|
||||
# ==================== 上传会话管理 ====================
|
||||
# ==================== 下载令牌管理 ====================
|
||||
|
||||
@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='/',
|
||||
summary='创建上传会话',
|
||||
description='创建文件上传会话,返回会话ID用于后续分片上传。',
|
||||
@@ -65,11 +100,6 @@ async def create_upload_session(
|
||||
3. 验证文件大小限制
|
||||
4. 创建上传会话并生成存储路径
|
||||
5. 返回会话信息
|
||||
|
||||
:param session: 数据库会话
|
||||
:param user: 当前登录用户
|
||||
:param request: 创建请求
|
||||
:return: 上传会话信息
|
||||
"""
|
||||
# 验证文件名
|
||||
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
|
||||
else:
|
||||
# S3 后续实现
|
||||
raise HTTPException(status_code=501, detail="S3 存储暂未实现")
|
||||
|
||||
# 创建上传会话
|
||||
@@ -131,7 +160,7 @@ async def create_upload_session(
|
||||
chunk_size=chunk_size,
|
||||
total_chunks=total_chunks,
|
||||
storage_path=storage_path,
|
||||
expires_at=datetime.now() + timedelta(hours=24), # 24小时过期
|
||||
expires_at=datetime.now() + timedelta(hours=24),
|
||||
owner_id=user.id,
|
||||
parent_id=request.parent_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}',
|
||||
summary='上传文件分片',
|
||||
description='上传指定分片,分片索引从0开始。',
|
||||
@@ -171,13 +200,6 @@ async def upload_chunk(
|
||||
2. 写入分片数据
|
||||
3. 更新会话进度
|
||||
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)
|
||||
@@ -262,7 +284,7 @@ async def upload_chunk(
|
||||
)
|
||||
|
||||
|
||||
@file_upload_router.delete(
|
||||
@_upload_router.delete(
|
||||
path='/{session_id}',
|
||||
summary='删除上传会话',
|
||||
description='取消上传并删除会话及已上传的临时文件。',
|
||||
@@ -272,14 +294,7 @@ async def delete_upload_session(
|
||||
user: Annotated[User, Depends(AuthRequired)],
|
||||
session_id: UUID,
|
||||
) -> ResponseBase:
|
||||
"""
|
||||
删除上传会话端点
|
||||
|
||||
:param session: 数据库会话
|
||||
:param user: 当前登录用户
|
||||
:param session_id: 上传会话UUID
|
||||
:return: 删除结果
|
||||
"""
|
||||
"""删除上传会话端点"""
|
||||
upload_session = await UploadSession.get(session, UploadSession.id == session_id)
|
||||
if not upload_session or upload_session.owner_id != user.id:
|
||||
raise HTTPException(status_code=404, detail="上传会话不存在")
|
||||
@@ -298,7 +313,7 @@ async def delete_upload_session(
|
||||
return ResponseBase(data={"deleted": True})
|
||||
|
||||
|
||||
@file_upload_router.delete(
|
||||
@_upload_router.delete(
|
||||
path='/',
|
||||
summary='清除所有上传会话',
|
||||
description='清除当前用户的所有上传会话。',
|
||||
@@ -307,13 +322,7 @@ async def clear_upload_sessions(
|
||||
session: SessionDep,
|
||||
user: Annotated[User, Depends(AuthRequired)],
|
||||
) -> ResponseBase:
|
||||
"""
|
||||
清除所有上传会话端点
|
||||
|
||||
:param session: 数据库会话
|
||||
:param user: 当前登录用户
|
||||
:return: 清除结果
|
||||
"""
|
||||
"""清除所有上传会话端点"""
|
||||
# 获取所有会话
|
||||
sessions = await UploadSession.get(
|
||||
session,
|
||||
@@ -337,28 +346,74 @@ async def clear_upload_sessions(
|
||||
return ResponseBase(data={"deleted": deleted_count})
|
||||
|
||||
|
||||
# ==================== 文件下载 ====================
|
||||
|
||||
@file_upload_router.get(
|
||||
path='/download/{file_id}',
|
||||
summary='下载文件',
|
||||
description='下载指定文件。',
|
||||
@_upload_router.get(
|
||||
path='/archive/{session_id}/archive.zip',
|
||||
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,
|
||||
user: Annotated[User, Depends(AuthRequired)],
|
||||
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:
|
||||
"""
|
||||
下载文件端点
|
||||
|
||||
:param session: 数据库会话
|
||||
:param user: 当前登录用户
|
||||
:param file_id: 文件UUID
|
||||
:return: 文件响应
|
||||
验证 JWT 令牌后返回文件内容。
|
||||
"""
|
||||
# 验证令牌
|
||||
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)
|
||||
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="文件不存在")
|
||||
|
||||
if not file_obj.is_file:
|
||||
@@ -386,9 +441,15 @@ async def download_file(
|
||||
raise HTTPException(status_code=501, detail="S3 存储暂未实现")
|
||||
|
||||
|
||||
# ==================== 包含子路由 ====================
|
||||
|
||||
router.include_router(_upload_router)
|
||||
router.include_router(_download_router)
|
||||
|
||||
|
||||
# ==================== 创建空白文件 ====================
|
||||
|
||||
@file_router.post(
|
||||
@router.post(
|
||||
path='/create',
|
||||
summary='创建空白文件',
|
||||
description='在指定目录下创建空白文件。',
|
||||
@@ -398,14 +459,7 @@ async def create_empty_file(
|
||||
user: Annotated[User, Depends(AuthRequired)],
|
||||
request: CreateFileRequest,
|
||||
) -> ResponseBase:
|
||||
"""
|
||||
创建空白文件端点
|
||||
|
||||
:param session: 数据库会话
|
||||
:param user: 当前登录用户
|
||||
:param request: 创建请求
|
||||
:return: 创建结果
|
||||
"""
|
||||
"""创建空白文件端点"""
|
||||
# 存储 user.id,避免后续 save() 导致 user 过期后无法访问
|
||||
user_id = user.id
|
||||
|
||||
@@ -482,175 +536,146 @@ async def create_empty_file(
|
||||
|
||||
# ==================== 文件外链(保留原有端点结构) ====================
|
||||
|
||||
@file_router.get(
|
||||
@router.get(
|
||||
path='/get/{id}/{name}',
|
||||
summary='文件外链(直接输出文件数据)',
|
||||
description='通过外链直接获取文件内容。',
|
||||
)
|
||||
async def router_file_get(
|
||||
async def file_get(
|
||||
session: SessionDep,
|
||||
id: str,
|
||||
name: str,
|
||||
) -> FileResponse:
|
||||
"""
|
||||
文件外链端点(直接输出)
|
||||
|
||||
TODO: 实现签名验证和权限控制
|
||||
"""
|
||||
"""文件外链端点(直接输出)"""
|
||||
raise HTTPException(status_code=501, detail="外链功能暂未实现")
|
||||
|
||||
|
||||
@file_router.get(
|
||||
@router.get(
|
||||
path='/source/{id}/{name}',
|
||||
summary='文件外链(301跳转)',
|
||||
description='通过外链获取文件重定向地址。',
|
||||
)
|
||||
async def router_file_source_redirect(id: str, name: str) -> ResponseBase:
|
||||
"""
|
||||
文件外链端点(301跳转)
|
||||
|
||||
TODO: 实现签名验证和重定向
|
||||
"""
|
||||
async def file_source_redirect(id: str, name: str) -> ResponseBase:
|
||||
"""文件外链端点(301跳转)"""
|
||||
raise HTTPException(status_code=501, detail="外链功能暂未实现")
|
||||
|
||||
|
||||
@file_router.put(
|
||||
@router.put(
|
||||
path='/update/{id}',
|
||||
summary='更新文件',
|
||||
description='更新文件内容。',
|
||||
dependencies=[Depends(AuthRequired)]
|
||||
)
|
||||
async def router_file_update(id: str) -> ResponseBase:
|
||||
async def file_update(id: str) -> ResponseBase:
|
||||
"""更新文件内容"""
|
||||
raise HTTPException(status_code=501, detail="更新文件功能暂未实现")
|
||||
|
||||
|
||||
@file_router.get(
|
||||
@router.get(
|
||||
path='/preview/{id}',
|
||||
summary='预览文件',
|
||||
description='获取文件预览。',
|
||||
dependencies=[Depends(AuthRequired)]
|
||||
)
|
||||
async def router_file_preview(id: str) -> ResponseBase:
|
||||
async def file_preview(id: str) -> ResponseBase:
|
||||
"""预览文件"""
|
||||
raise HTTPException(status_code=501, detail="预览功能暂未实现")
|
||||
|
||||
|
||||
@file_router.get(
|
||||
@router.get(
|
||||
path='/content/{id}',
|
||||
summary='获取文本文件内容',
|
||||
description='获取文本文件内容。',
|
||||
dependencies=[Depends(AuthRequired)]
|
||||
)
|
||||
async def router_file_content(id: str) -> ResponseBase:
|
||||
async def file_content(id: str) -> ResponseBase:
|
||||
"""获取文本文件内容"""
|
||||
raise HTTPException(status_code=501, detail="文本内容功能暂未实现")
|
||||
|
||||
|
||||
@file_router.get(
|
||||
@router.get(
|
||||
path='/doc/{id}',
|
||||
summary='获取Office文档预览地址',
|
||||
description='获取Office文档在线预览地址。',
|
||||
dependencies=[Depends(AuthRequired)]
|
||||
)
|
||||
async def router_file_doc(id: str) -> ResponseBase:
|
||||
async def file_doc(id: str) -> ResponseBase:
|
||||
"""获取Office文档预览地址"""
|
||||
raise HTTPException(status_code=501, detail="Office预览功能暂未实现")
|
||||
|
||||
|
||||
@file_router.get(
|
||||
@router.get(
|
||||
path='/thumb/{id}',
|
||||
summary='获取文件缩略图',
|
||||
description='获取文件缩略图。',
|
||||
dependencies=[Depends(AuthRequired)]
|
||||
)
|
||||
async def router_file_thumb(id: str) -> ResponseBase:
|
||||
async def file_thumb(id: str) -> ResponseBase:
|
||||
"""获取文件缩略图"""
|
||||
raise HTTPException(status_code=501, detail="缩略图功能暂未实现")
|
||||
|
||||
|
||||
@file_router.post(
|
||||
@router.post(
|
||||
path='/source/{id}',
|
||||
summary='取得文件外链',
|
||||
description='获取文件的外链地址。',
|
||||
dependencies=[Depends(AuthRequired)]
|
||||
)
|
||||
async def router_file_source(id: str) -> ResponseBase:
|
||||
async def file_source(id: str) -> ResponseBase:
|
||||
"""获取文件外链"""
|
||||
raise HTTPException(status_code=501, detail="外链功能暂未实现")
|
||||
|
||||
|
||||
@file_router.post(
|
||||
@router.post(
|
||||
path='/archive',
|
||||
summary='打包要下载的文件',
|
||||
description='将多个文件打包下载。',
|
||||
dependencies=[Depends(AuthRequired)]
|
||||
)
|
||||
async def router_file_archive() -> ResponseBase:
|
||||
async def file_archive() -> ResponseBase:
|
||||
"""打包文件"""
|
||||
raise HTTPException(status_code=501, detail="打包功能暂未实现")
|
||||
|
||||
|
||||
@file_router.post(
|
||||
@router.post(
|
||||
path='/compress',
|
||||
summary='创建文件压缩任务',
|
||||
description='创建文件压缩任务。',
|
||||
dependencies=[Depends(AuthRequired)]
|
||||
)
|
||||
async def router_file_compress() -> ResponseBase:
|
||||
async def file_compress() -> ResponseBase:
|
||||
"""创建压缩任务"""
|
||||
raise HTTPException(status_code=501, detail="压缩功能暂未实现")
|
||||
|
||||
|
||||
@file_router.post(
|
||||
@router.post(
|
||||
path='/decompress',
|
||||
summary='创建文件解压任务',
|
||||
description='创建文件解压任务。',
|
||||
dependencies=[Depends(AuthRequired)]
|
||||
)
|
||||
async def router_file_decompress() -> ResponseBase:
|
||||
async def file_decompress() -> ResponseBase:
|
||||
"""创建解压任务"""
|
||||
raise HTTPException(status_code=501, detail="解压功能暂未实现")
|
||||
|
||||
|
||||
@file_router.post(
|
||||
@router.post(
|
||||
path='/relocate',
|
||||
summary='创建文件转移任务',
|
||||
description='创建文件转移任务。',
|
||||
dependencies=[Depends(AuthRequired)]
|
||||
)
|
||||
async def router_file_relocate() -> ResponseBase:
|
||||
async def file_relocate() -> ResponseBase:
|
||||
"""创建转移任务"""
|
||||
raise HTTPException(status_code=501, detail="转移功能暂未实现")
|
||||
|
||||
|
||||
@file_router.get(
|
||||
@router.get(
|
||||
path='/search/{type}/{keyword}',
|
||||
summary='搜索文件',
|
||||
description='按关键字搜索文件。',
|
||||
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="搜索功能暂未实现")
|
||||
|
||||
|
||||
@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="下载会话功能暂未实现")
|
||||
|
||||
19
routers/api/v1/mcp/__init__.py
Normal file
19
routers/api/v1/mcp/__init__.py
Normal 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())
|
||||
Reference in New Issue
Block a user