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
|
- ID:UUIDv7
|
||||||
- 创建时间
|
- 创建时间
|
||||||
- 更新时间
|
- 更新时间
|
||||||
- User
|
- User
|
||||||
- 用户名 唯一
|
- 用户名 唯一
|
||||||
- 昵称
|
- 昵称
|
||||||
- Argon2化的密码
|
- Argon2化的密码
|
||||||
- 用户当前的状态 `正常/手动封禁/系统封禁` 用 StrEnum 存储
|
- 用户当前的状态 `正常/手动封禁/系统封禁` 用 StrEnum 存储
|
||||||
- 已用存储空间
|
- 已用存储空间
|
||||||
- 用户积分
|
- 用户积分
|
||||||
- 默认用户组
|
- 默认用户组
|
||||||
- Optional[两步验证密钥]
|
- Optional[两步验证密钥]
|
||||||
- Optional[WebAuthn 凭证]
|
- Optional[WebAuthn 凭证]
|
||||||
- Optional[头像地址] -> None为昵称首字/图片/Gavatar头像
|
- Optional[头像地址] -> None为昵称首字/图片/Gavatar头像
|
||||||
- Optional[当前用户组过期时间] -> 用户组过期后回退默认用户组
|
- Optional[当前用户组过期时间] -> 用户组过期后回退默认用户组
|
||||||
- 用户的个人设置 计划中 考虑 BaseModel 进行嵌套
|
- 用户的个人设置 计划中 考虑 BaseModel 进行嵌套
|
||||||
- Group `与 User 的关系:一个用户组可以有多个用户,但一个用户只能在一个用户组中`
|
- Group `与 User 的关系:一个用户组可以有多个用户,但一个用户只能在一个用户组中`
|
||||||
- 组名 唯一
|
- 组名 唯一
|
||||||
- list[[允许的存储策略, 该策略下最大允许使用的容量, 该策略下允许上传文件的白名单/黑名单]]
|
- list[[允许的存储策略, 该策略下最大允许使用的容量, 该策略下允许上传文件的白名单/黑名单]]
|
||||||
- 默认存储策略
|
- 默认存储策略
|
||||||
- bool[分享内容]
|
- bool[分享内容]
|
||||||
- bool[是否管理员组]
|
- bool[是否管理员组]
|
||||||
- 速度限制
|
- 速度限制
|
||||||
- bool[打包下载]
|
- bool[打包下载]
|
||||||
- bool[创建压缩/解压缩任务]
|
- bool[创建压缩/解压缩任务]
|
||||||
- bool[WebDAV]
|
- bool[WebDAV]
|
||||||
- bool[WebDAV 反代]
|
- bool[WebDAV 反代]
|
||||||
- bool[离线下载]
|
- bool[离线下载]
|
||||||
- Object `计划:把 file 和 folder 合并为一个 Object 表,通过对象类型区分`
|
- Object `计划:把 file 和 folder 合并为一个 Object 表,通过对象类型区分`
|
||||||
- 对象名
|
- 对象名
|
||||||
- 区分大小写
|
- 区分大小写
|
||||||
- 禁止名称为特殊字段 (如 `/`, `\`, `:`, `*`, `?`, `<`, `>`, `:`, `"`)
|
- 禁止名称为特殊字段 (如 `/`, `\`, `:`, `*`, `?`, `<`, `>`, `:`, `"`)
|
||||||
- 对象类型[file, folder, link]
|
- 对象类型[file, folder, link]
|
||||||
- 当对象类型为file时
|
- 当对象类型为file时
|
||||||
- 源文件名
|
- 源文件名
|
||||||
- 文件大小
|
- 文件大小
|
||||||
- 分块上传会话ID
|
- 分块上传会话ID
|
||||||
- 文件MD5, SHA1, SHA256
|
- 文件MD5, SHA1, SHA256
|
||||||
- 文件元数据
|
- 文件元数据
|
||||||
- 音频:歌名、歌手名、专辑、流派...
|
- 音频:歌名、歌手名、专辑、流派...
|
||||||
- 图片: 尺寸、ISO、曝光、拍摄设备、地理位置...
|
- 图片: 尺寸、ISO、曝光、拍摄设备、地理位置...
|
||||||
- 其他需要记录的元数据
|
- 其他需要记录的元数据
|
||||||
- 当对象类型为 folder 时
|
- 当对象类型为 folder 时
|
||||||
- 当前文件夹的视图(网格/列表/画廊)
|
- 当前文件夹的视图(网格/列表/画廊)
|
||||||
- 排序规则(按名称/大小/上传时间/修改时间)
|
- 排序规则(按名称/大小/上传时间/修改时间)
|
||||||
- 排序方式(升序/降序)
|
- 排序方式(升序/降序)
|
||||||
- 当对象类型为 link 时
|
- 当对象类型为 link 时
|
||||||
- 目标对象ID
|
- 目标对象ID
|
||||||
- 外键
|
- 外键
|
||||||
- 用户ID 用于确保该对象的归属
|
- 用户ID 用于确保该对象的归属
|
||||||
- 目录ID 用于定位该对象存在哪个目录
|
- 目录ID 用于定位该对象存在哪个目录
|
||||||
- 策略ID 用于定位该对象存储于哪个存储策略
|
- 策略ID 用于定位该对象存储于哪个存储策略
|
||||||
- Policy
|
- Policy
|
||||||
- 存储策略名
|
- 存储策略名
|
||||||
- 策略类型(如 `本机`, `从机`, `s3`, `OSS`, `COS` 等)
|
- 策略类型(如 `本机`, `从机`, `s3`, `OSS`, `COS` 等)
|
||||||
- 服务器地址(本机则为路径)
|
- 服务器地址(本机则为路径)
|
||||||
- 存储桶名称
|
- 存储桶名称
|
||||||
- 允许上传文件的最大字节数
|
- 允许上传文件的最大字节数
|
||||||
- 目录命名规则
|
- 目录命名规则
|
||||||
- 文件命名规则
|
- 文件命名规则
|
||||||
- 文件后缀白名单/黑名单
|
- 文件后缀白名单/黑名单
|
||||||
- 分片上传大小
|
- 分片上传大小
|
||||||
- `待研究`
|
- `待研究`
|
||||||
- Tag
|
- Tag
|
||||||
- 标签名称
|
- 标签名称
|
||||||
- 标签图标
|
- 标签图标
|
||||||
- 标签颜色
|
- 标签颜色
|
||||||
- 外键
|
- 外键
|
||||||
- 用户ID 用于确保该标签的归属
|
- 用户ID 用于确保该标签的归属
|
||||||
|
|
||||||
- 运行环境与目标
|
- 运行环境与目标
|
||||||
- 数据库类型:主要支持 PostgreSQL 18,考虑兼容 SQLite/MySQL/早期版本PostgreSQL
|
- 数据库类型:主要支持 PostgreSQL 18,考虑兼容 SQLite/MySQL/早期版本PostgreSQL
|
||||||
- 驱动版本:做一定的向下兼容,主要支持 Python 3.13+
|
- 驱动版本:做一定的向下兼容,主要支持 Python 3.13+
|
||||||
- 异步栈:全量异步 AsyncSession,不接受兼容同步
|
- 异步栈:全量异步 AsyncSession,不接受兼容同步
|
||||||
- 业务语义与数据模型
|
- 业务语义与数据模型
|
||||||
- 时间与时区:统一存储 UTC,再根据用户选择的时区计算本地化时间
|
- 时间与时区:统一存储 UTC,再根据用户选择的时区计算本地化时间
|
||||||
- 文件/目录命名规则
|
- 文件/目录命名规则
|
||||||
- 文件名在同一目录下唯一
|
- 文件名在同一目录下唯一
|
||||||
- 目录名在同一账户下的同一父目录下唯一
|
- 目录名在同一账户下的同一父目录下唯一
|
||||||
- 资源分享
|
- 资源分享
|
||||||
- 用户可以分享单个文件,也可以分享整个目录
|
- 用户可以分享单个文件,也可以分享整个目录
|
||||||
- 关系与级联
|
- 关系与级联
|
||||||
- 删除文件夹时,同时删除该文件夹内的所有子文件夹及其所有文件
|
- 删除文件夹时,同时删除该文件夹内的所有子文件夹及其所有文件
|
||||||
- 删除用户时,同时删除该用户的所有文件,文件夹,分享,Tag
|
- 删除用户时,同时删除该用户的所有文件,文件夹,分享,Tag
|
||||||
- 文件预览与编辑
|
- 文件预览与编辑
|
||||||
- 预览应用 Literal['嵌入网页式应用', 'WOPI协议式应用']
|
- 预览应用 Literal['嵌入网页式应用', 'WOPI协议式应用']
|
||||||
- 是否启用
|
- 是否启用
|
||||||
@@ -96,4 +96,3 @@
|
|||||||
- 最大文件大小
|
- 最大文件大小
|
||||||
- 平台 支持列表 ['all', 'mobile', 'desktop']
|
- 平台 支持列表 ['all', 'mobile', 'desktop']
|
||||||
- 是否在新窗口打开
|
- 是否在新窗口打开
|
||||||
|
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ fastapi dev
|
|||||||
fastapi run
|
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] 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 存储策略实现
|
||||||
|
|
||||||
@@ -408,4 +416,4 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*最后更新:2025年12月*
|
*最后更新:2025年12月*
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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,用于标识请求的唯一性"""
|
|
||||||
145
models/group.py
145
models/group.py
@@ -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
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 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
|
||||||
|
"""总数"""
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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="下载会话功能暂未实现")
|
|
||||||
|
|||||||
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