Add HTTP exception helpers and update models
Introduced utils/http/http_exceptions.py with common HTTP exception helpers for FastAPI. Updated main.py to use a global exception handler that logs and hides internal errors. Refined models/README.md to document new models and relationships, including PhysicalFile and UploadSession, and updated DTO and enum documentation. Simplified ThemeResponse in models/color.py. Improved models/download.py with type annotations, index changes, and import optimizations. Fixed a parameter type in clean.py. Co-Authored-By: 砂糖橘 <54745033+Foxerine@users.noreply.github.com>
This commit is contained in:
2
clean.py
2
clean.py
@@ -34,7 +34,7 @@ def parse_args() -> argparse.Namespace:
|
|||||||
help='仅列出将要删除的文件,不实际删除')
|
help='仅列出将要删除的文件,不实际删除')
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
def confirm_action(message: str, auto_yes: str = False) -> bool:
|
def confirm_action(message: str, auto_yes: bool = False) -> bool:
|
||||||
if auto_yes:
|
if auto_yes:
|
||||||
return True
|
return True
|
||||||
return input(f"{message} (y/N): ").lower() == 'y'
|
return input(f"{message} (y/N): ").lower() == 'y'
|
||||||
|
|||||||
22
main.py
22
main.py
@@ -1,11 +1,15 @@
|
|||||||
from fastapi import FastAPI
|
from typing import NoReturn
|
||||||
|
|
||||||
|
from fastapi import FastAPI, Request
|
||||||
|
|
||||||
from utils.conf import appmeta
|
from utils.conf import appmeta
|
||||||
|
from utils.http.http_exceptions import raise_internal_error
|
||||||
from utils.lifespan import lifespan
|
from utils.lifespan import lifespan
|
||||||
from models.database import init_db
|
from models.database import init_db
|
||||||
from models.migration import migration
|
from models.migration import migration
|
||||||
from utils.JWT import JWT
|
from utils.JWT import JWT
|
||||||
from routers import router
|
from routers import router
|
||||||
|
from loguru import logger as l
|
||||||
|
|
||||||
# 添加初始化数据库启动项
|
# 添加初始化数据库启动项
|
||||||
lifespan.add_startup(init_db)
|
lifespan.add_startup(init_db)
|
||||||
@@ -24,11 +28,21 @@ app = FastAPI(
|
|||||||
debug=appmeta.debug,
|
debug=appmeta.debug,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@app.exception_handler(Exception)
|
||||||
|
async def handle_unexpected_exceptions(request: Request, exc: Exception) -> NoReturn:
|
||||||
|
"""
|
||||||
|
捕获所有未经处理的fastapi异常,防止敏感信息泄露。
|
||||||
|
"""
|
||||||
|
l.exception(exc)
|
||||||
|
l.error(f"An unhandled exception occurred for request: {request.method} {request.url.path}")
|
||||||
|
|
||||||
|
raise_internal_error()
|
||||||
|
|
||||||
# 挂载路由
|
# 挂载路由
|
||||||
app.include_router(router)
|
app.include_router(router)
|
||||||
|
|
||||||
# 防止直接运行 main.py
|
# 防止直接运行 main.py
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
from loguru import logger
|
l.error("请用 fastapi ['dev', 'run'] 命令启动服务")
|
||||||
logger.error("请用 fastapi ['dev', 'run'] 命令启动服务")
|
exit(1)
|
||||||
exit(1)
|
|
||||||
234
models/README.md
234
models/README.md
@@ -18,7 +18,8 @@ models/
|
|||||||
├── user_authn.py # 用户 WebAuthn 凭证
|
├── user_authn.py # 用户 WebAuthn 凭证
|
||||||
├── group.py # 用户组模型
|
├── group.py # 用户组模型
|
||||||
├── policy.py # 存储策略模型
|
├── policy.py # 存储策略模型
|
||||||
├── object.py # 统一对象模型(文件/目录)
|
├── physical_file.py # 物理文件模型(文件去重)
|
||||||
|
├── object.py # 统一对象模型(文件/目录)+ 上传会话
|
||||||
├── share.py # 分享模型
|
├── share.py # 分享模型
|
||||||
├── tag.py # 标签模型
|
├── tag.py # 标签模型
|
||||||
├── download.py # 离线下载任务
|
├── download.py # 离线下载任务
|
||||||
@@ -32,7 +33,7 @@ models/
|
|||||||
├── storage_pack.py # 容量包模型
|
├── storage_pack.py # 容量包模型
|
||||||
├── webdav.py # WebDAV 账户模型
|
├── webdav.py # WebDAV 账户模型
|
||||||
├── color.py # 主题颜色 DTO
|
├── color.py # 主题颜色 DTO
|
||||||
├── response.py # 响应 DTO
|
├── model_base.py # 响应基类 DTO
|
||||||
└── database.py # 数据库连接配置
|
└── database.py # 数据库连接配置
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -118,7 +119,7 @@ class User(UserBase, UUIDTableBaseMixin): # 不需要再写 SQLModelBase
|
|||||||
| `username` | `str` | 用户名,唯一,不可更改 |
|
| `username` | `str` | 用户名,唯一,不可更改 |
|
||||||
| `nickname` | `str?` | 用户昵称 |
|
| `nickname` | `str?` | 用户昵称 |
|
||||||
| `password` | `str` | 密码(加密后) |
|
| `password` | `str` | 密码(加密后) |
|
||||||
| `status` | `bool` | 用户状态:True=正常,False=封禁 |
|
| `status` | `UserStatus` | 用户状态:active/admin_banned/system_banned |
|
||||||
| `storage` | `int` | 已用存储空间(字节) |
|
| `storage` | `int` | 已用存储空间(字节) |
|
||||||
| `two_factor` | `str?` | 两步验证密钥 |
|
| `two_factor` | `str?` | 两步验证密钥 |
|
||||||
| `avatar` | `str` | 头像类型/地址 |
|
| `avatar` | `str` | 头像类型/地址 |
|
||||||
@@ -130,6 +131,10 @@ class User(UserBase, UUIDTableBaseMixin): # 不需要再写 SQLModelBase
|
|||||||
| `group_id` | `UUID` | 所属用户组(外键) |
|
| `group_id` | `UUID` | 所属用户组(外键) |
|
||||||
| `previous_group_id` | `UUID?` | 之前的用户组(用于过期后恢复) |
|
| `previous_group_id` | `UUID?` | 之前的用户组(用于过期后恢复) |
|
||||||
|
|
||||||
|
**关系**:
|
||||||
|
- `group`: 所属用户组
|
||||||
|
- `previous_group`: 之前的用户组(用于过期后恢复)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 2. UserAuthn(WebAuthn 凭证)
|
### 2. UserAuthn(WebAuthn 凭证)
|
||||||
@@ -248,7 +253,38 @@ class User(UserBase, UUIDTableBaseMixin): # 不需要再写 SQLModelBase
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 8. Object(统一对象)
|
### 8. PhysicalFile(物理文件)
|
||||||
|
|
||||||
|
**表名**: `physicalfile`
|
||||||
|
**基类**: `UUIDTableBaseMixin`
|
||||||
|
|
||||||
|
表示磁盘上的实际文件。多个 Object 可以引用同一个 PhysicalFile,实现文件共享而不复制物理文件。
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `id` | `UUID` | 物理文件 UUID(主键) |
|
||||||
|
| `storage_path` | `str` | 物理存储路径(相对于存储策略根目录) |
|
||||||
|
| `size` | `int` | 文件大小(字节) |
|
||||||
|
| `checksum_md5` | `str?` | MD5 校验和(用于文件去重和完整性校验) |
|
||||||
|
| `policy_id` | `UUID` | 存储策略(外键) |
|
||||||
|
| `reference_count` | `int` | 引用计数(有多少个 Object 引用此物理文件) |
|
||||||
|
|
||||||
|
**索引**:
|
||||||
|
- `ix_physical_file_policy_path`: (policy_id, storage_path)
|
||||||
|
- `ix_physical_file_checksum`: (checksum_md5)
|
||||||
|
|
||||||
|
**关系**:
|
||||||
|
- `policy`: 存储策略
|
||||||
|
- `objects`: 引用此物理文件的所有逻辑对象(一对多)
|
||||||
|
|
||||||
|
**业务方法**:
|
||||||
|
- `increment_reference()`: 增加引用计数
|
||||||
|
- `decrement_reference()`: 减少引用计数
|
||||||
|
- `can_be_deleted`: 属性,是否可物理删除(引用计数为 0)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9. Object(统一对象)
|
||||||
|
|
||||||
**表名**: `object`
|
**表名**: `object`
|
||||||
**基类**: `UUIDTableBaseMixin`
|
**基类**: `UUIDTableBaseMixin`
|
||||||
@@ -261,23 +297,45 @@ class User(UserBase, UUIDTableBaseMixin): # 不需要再写 SQLModelBase
|
|||||||
| `name` | `str` | 对象名称(文件名或目录名) |
|
| `name` | `str` | 对象名称(文件名或目录名) |
|
||||||
| `type` | `ObjectType` | 对象类型:file/folder |
|
| `type` | `ObjectType` | 对象类型:file/folder |
|
||||||
| `password` | `str?` | 对象独立密码 |
|
| `password` | `str?` | 对象独立密码 |
|
||||||
| `source_name` | `str?` | 源文件名(仅文件) |
|
|
||||||
| `size` | `int` | 文件大小(字节),目录为 0 |
|
| `size` | `int` | 文件大小(字节),目录为 0 |
|
||||||
| `upload_session_id` | `str?` | 分块上传会话 ID |
|
| `upload_session_id` | `str?` | 分块上传会话 ID |
|
||||||
|
| `physical_file_id` | `UUID?` | 关联的物理文件(仅文件有效,目录为 NULL) |
|
||||||
| `parent_id` | `UUID?` | 父目录(外键,NULL 表示根目录) |
|
| `parent_id` | `UUID?` | 父目录(外键,NULL 表示根目录) |
|
||||||
| `owner_id` | `UUID` | 所有者用户(外键) |
|
| `owner_id` | `UUID` | 所有者用户(外键) |
|
||||||
| `policy_id` | `UUID` | 存储策略(外键) |
|
| `policy_id` | `UUID` | 存储策略(外键) |
|
||||||
|
| `is_banned` | `bool` | 是否被封禁 |
|
||||||
|
| `banned_at` | `datetime?` | 封禁时间 |
|
||||||
|
| `banned_by` | `UUID?` | 封禁操作者 UUID |
|
||||||
|
| `ban_reason` | `str?` | 封禁原因 |
|
||||||
|
|
||||||
**约束**:
|
**约束**:
|
||||||
- 同一父目录下名称唯一
|
- 同一父目录下名称唯一(owner_id + parent_id + name)
|
||||||
- 名称不能包含斜杠
|
- 名称不能包含斜杠
|
||||||
|
|
||||||
|
**索引**:
|
||||||
|
- `ix_object_owner_updated`: (owner_id, updated_at)
|
||||||
|
- `ix_object_parent_updated`: (parent_id, updated_at)
|
||||||
|
- `ix_object_owner_type`: (owner_id, type)
|
||||||
|
- `ix_object_owner_size`: (owner_id, size)
|
||||||
|
|
||||||
**关系**:
|
**关系**:
|
||||||
- `file_metadata`: 一对一关联 FileMetadata
|
- `file_metadata`: 一对一关联 FileMetadata
|
||||||
|
- `physical_file`: 关联的物理文件(仅文件有效)
|
||||||
|
- `owner`: 所有者用户
|
||||||
|
- `banner`: 封禁操作者
|
||||||
|
- `parent`: 父目录(自引用)
|
||||||
|
- `children`: 子对象列表(自引用)
|
||||||
|
- `source_links`: 源链接列表
|
||||||
|
- `shares`: 分享列表
|
||||||
|
|
||||||
|
**业务属性**:
|
||||||
|
- `source_name`: 向后兼容属性,返回物理文件的存储路径
|
||||||
|
- `is_file`: 是否为文件
|
||||||
|
- `is_folder`: 是否为目录
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 9. FileMetadata(文件元数据)
|
### 10. FileMetadata(文件元数据)
|
||||||
|
|
||||||
**表名**: `filemetadata`
|
**表名**: `filemetadata`
|
||||||
**基类**: `UUIDTableBaseMixin`
|
**基类**: `UUIDTableBaseMixin`
|
||||||
@@ -286,21 +344,53 @@ class User(UserBase, UUIDTableBaseMixin): # 不需要再写 SQLModelBase
|
|||||||
|------|------|------|
|
|------|------|------|
|
||||||
| `id` | `UUID` | 主键 |
|
| `id` | `UUID` | 主键 |
|
||||||
| `object_id` | `UUID` | 关联的对象(外键,唯一) |
|
| `object_id` | `UUID` | 关联的对象(外键,唯一) |
|
||||||
| `width` | `int?` | 图片/视频宽度 |
|
| `width` | `int?` | 图片/视频宽度(像素) |
|
||||||
| `height` | `int?` | 图片/视频高度 |
|
| `height` | `int?` | 图片/视频高度(像素) |
|
||||||
| `duration` | `float?` | 音视频时长(秒) |
|
| `duration` | `float?` | 音视频时长(秒) |
|
||||||
| `mime_type` | `str?` | MIME类型 |
|
| `bitrate` | `int?` | 比特率(kbps) |
|
||||||
| `bit_rate` | `int?` | 比特率 |
|
| `mime_type` | `str?` | MIME 类型 |
|
||||||
| `sample_rate` | `int?` | 采样率 |
|
| `checksum_md5` | `str?` | MD5 校验和 |
|
||||||
| `channels` | `int?` | 音频通道数 |
|
| `checksum_sha256` | `str?` | SHA256 校验和 |
|
||||||
| `codec` | `str?` | 编解码器 |
|
|
||||||
| `frame_rate` | `float?` | 视频帧率 |
|
**关系**:
|
||||||
| `orientation` | `int?` | 图片方向 |
|
- `object`: 关联的 Object(一对一)
|
||||||
| `has_thumbnail` | `bool` | 是否有缩略图 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 10. SourceLink(源链接)
|
### 11. UploadSession(上传会话)
|
||||||
|
|
||||||
|
**表名**: `uploadsession`
|
||||||
|
**基类**: `UUIDTableBaseMixin`
|
||||||
|
|
||||||
|
用于管理分片上传的会话状态。会话有效期为 24 小时,过期后自动失效。
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `id` | `UUID` | 会话 UUID(主键) |
|
||||||
|
| `file_name` | `str` | 原始文件名 |
|
||||||
|
| `file_size` | `int` | 文件总大小(字节) |
|
||||||
|
| `chunk_size` | `int` | 分片大小(字节) |
|
||||||
|
| `total_chunks` | `int` | 总分片数 |
|
||||||
|
| `uploaded_chunks` | `int` | 已上传分片数 |
|
||||||
|
| `uploaded_size` | `int` | 已上传大小(字节) |
|
||||||
|
| `storage_path` | `str?` | 文件存储路径 |
|
||||||
|
| `expires_at` | `datetime` | 会话过期时间 |
|
||||||
|
| `owner_id` | `UUID` | 上传者用户(外键) |
|
||||||
|
| `parent_id` | `UUID` | 目标父目录(外键) |
|
||||||
|
| `policy_id` | `UUID` | 存储策略(外键) |
|
||||||
|
|
||||||
|
**关系**:
|
||||||
|
- `owner`: 上传者用户
|
||||||
|
- `parent`: 目标父目录
|
||||||
|
- `policy`: 存储策略
|
||||||
|
|
||||||
|
**业务属性**:
|
||||||
|
- `is_expired`: 会话是否已过期
|
||||||
|
- `is_complete`: 上传是否完成
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 12. SourceLink(源链接)
|
||||||
|
|
||||||
**表名**: `sourcelink`
|
**表名**: `sourcelink`
|
||||||
**基类**: `TableBaseMixin`
|
**基类**: `TableBaseMixin`
|
||||||
@@ -314,7 +404,7 @@ class User(UserBase, UUIDTableBaseMixin): # 不需要再写 SQLModelBase
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 11. Share(分享)
|
### 13. Share(分享)
|
||||||
|
|
||||||
**表名**: `share`
|
**表名**: `share`
|
||||||
**基类**: `TableBaseMixin`
|
**基类**: `TableBaseMixin`
|
||||||
@@ -336,7 +426,7 @@ class User(UserBase, UUIDTableBaseMixin): # 不需要再写 SQLModelBase
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 12. Report(举报)
|
### 14. Report(举报)
|
||||||
|
|
||||||
**表名**: `report`
|
**表名**: `report`
|
||||||
**基类**: `TableBaseMixin`
|
**基类**: `TableBaseMixin`
|
||||||
@@ -350,7 +440,7 @@ class User(UserBase, UUIDTableBaseMixin): # 不需要再写 SQLModelBase
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 13. Tag(标签)
|
### 15. Tag(标签)
|
||||||
|
|
||||||
**表名**: `tag`
|
**表名**: `tag`
|
||||||
**基类**: `TableBaseMixin`
|
**基类**: `TableBaseMixin`
|
||||||
@@ -369,7 +459,7 @@ class User(UserBase, UUIDTableBaseMixin): # 不需要再写 SQLModelBase
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 14. Task(任务)
|
### 16. Task(任务)
|
||||||
|
|
||||||
**表名**: `task`
|
**表名**: `task`
|
||||||
**基类**: `TableBaseMixin`
|
**基类**: `TableBaseMixin`
|
||||||
@@ -391,7 +481,7 @@ class User(UserBase, UUIDTableBaseMixin): # 不需要再写 SQLModelBase
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 15. TaskProps(任务属性)
|
### 17. TaskProps(任务属性)
|
||||||
|
|
||||||
**表名**: `taskprops`
|
**表名**: `taskprops`
|
||||||
**基类**: `TableBaseMixin`(主键为外键 task_id)
|
**基类**: `TableBaseMixin`(主键为外键 task_id)
|
||||||
@@ -405,7 +495,7 @@ class User(UserBase, UUIDTableBaseMixin): # 不需要再写 SQLModelBase
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 16. Download(离线下载)
|
### 18. Download(离线下载)
|
||||||
|
|
||||||
**表名**: `download`
|
**表名**: `download`
|
||||||
**基类**: `UUIDTableBaseMixin`
|
**基类**: `UUIDTableBaseMixin`
|
||||||
@@ -437,7 +527,7 @@ class User(UserBase, UUIDTableBaseMixin): # 不需要再写 SQLModelBase
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 17. DownloadAria2Info(Aria2下载信息)
|
### 19. DownloadAria2Info(Aria2下载信息)
|
||||||
|
|
||||||
**表名**: `downloadaria2info`
|
**表名**: `downloadaria2info`
|
||||||
**基类**: `TableBaseMixin`(主键为外键 download_id)
|
**基类**: `TableBaseMixin`(主键为外键 download_id)
|
||||||
@@ -457,7 +547,7 @@ class User(UserBase, UUIDTableBaseMixin): # 不需要再写 SQLModelBase
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 18. DownloadAria2File(Aria2下载文件)
|
### 20. DownloadAria2File(Aria2下载文件)
|
||||||
|
|
||||||
**表名**: `downloadaria2file`
|
**表名**: `downloadaria2file`
|
||||||
**基类**: `TableBaseMixin`
|
**基类**: `TableBaseMixin`
|
||||||
@@ -474,7 +564,7 @@ class User(UserBase, UUIDTableBaseMixin): # 不需要再写 SQLModelBase
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 19. Node(节点)
|
### 21. Node(节点)
|
||||||
|
|
||||||
**表名**: `node`
|
**表名**: `node`
|
||||||
**基类**: `TableBaseMixin`
|
**基类**: `TableBaseMixin`
|
||||||
@@ -499,7 +589,7 @@ class User(UserBase, UUIDTableBaseMixin): # 不需要再写 SQLModelBase
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 20. Aria2Configuration(Aria2配置)
|
### 22. Aria2Configuration(Aria2配置)
|
||||||
|
|
||||||
**表名**: `aria2configuration`
|
**表名**: `aria2configuration`
|
||||||
**基类**: `TableBaseMixin`
|
**基类**: `TableBaseMixin`
|
||||||
@@ -516,7 +606,7 @@ class User(UserBase, UUIDTableBaseMixin): # 不需要再写 SQLModelBase
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 21. Order(订单)
|
### 23. Order(订单)
|
||||||
|
|
||||||
**表名**: `order`
|
**表名**: `order`
|
||||||
**基类**: `TableBaseMixin`
|
**基类**: `TableBaseMixin`
|
||||||
@@ -536,7 +626,7 @@ class User(UserBase, UUIDTableBaseMixin): # 不需要再写 SQLModelBase
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 22. Redeem(兑换码)
|
### 24. Redeem(兑换码)
|
||||||
|
|
||||||
**表名**: `redeem`
|
**表名**: `redeem`
|
||||||
**基类**: `TableBaseMixin`
|
**基类**: `TableBaseMixin`
|
||||||
@@ -552,7 +642,7 @@ class User(UserBase, UUIDTableBaseMixin): # 不需要再写 SQLModelBase
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 23. StoragePack(容量包)
|
### 25. StoragePack(容量包)
|
||||||
|
|
||||||
**表名**: `storagepack`
|
**表名**: `storagepack`
|
||||||
**基类**: `TableBaseMixin`
|
**基类**: `TableBaseMixin`
|
||||||
@@ -568,7 +658,7 @@ class User(UserBase, UUIDTableBaseMixin): # 不需要再写 SQLModelBase
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 24. WebDAV(WebDAV 账户)
|
### 26. WebDAV(WebDAV 账户)
|
||||||
|
|
||||||
**表名**: `webdav`
|
**表名**: `webdav`
|
||||||
**基类**: `TableBaseMixin`
|
**基类**: `TableBaseMixin`
|
||||||
@@ -587,7 +677,7 @@ class User(UserBase, UUIDTableBaseMixin): # 不需要再写 SQLModelBase
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 25. Setting(系统设置)
|
### 27. Setting(系统设置)
|
||||||
|
|
||||||
**表名**: `setting`
|
**表名**: `setting`
|
||||||
**基类**: `TableBaseMixin`
|
**基类**: `TableBaseMixin`
|
||||||
@@ -636,6 +726,22 @@ class User(UserBase, UUIDTableBaseMixin): # 不需要再写 SQLModelBase
|
|||||||
└───────────────────────────────────────────────────────────────────┘
|
└───────────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**新增关系**:
|
||||||
|
```
|
||||||
|
┌───────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 一对多关系(新增) │
|
||||||
|
├───────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ PhysicalFile ◄──────────────────> Object (多个) │
|
||||||
|
│ physical_file_id (FK) 文件去重:多个Object可引用 │
|
||||||
|
│ 同一个PhysicalFile │
|
||||||
|
│ │
|
||||||
|
│ User ◄──────────────────────────> UploadSession │
|
||||||
|
│ owner_id (FK) 用户的上传会话列表 │
|
||||||
|
│ │
|
||||||
|
└───────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
| 主表 | 从表 | 外键 | 说明 |
|
| 主表 | 从表 | 外键 | 说明 |
|
||||||
|------|------|------|------|
|
|------|------|------|------|
|
||||||
| Group | GroupOptions | `group_id` (unique) | 每个用户组有且仅有一个选项配置 |
|
| Group | GroupOptions | `group_id` (unique) | 每个用户组有且仅有一个选项配置 |
|
||||||
@@ -693,7 +799,10 @@ class User(UserBase, UUIDTableBaseMixin): # 不需要再写 SQLModelBase
|
|||||||
| **User** | Task | `user_id` | 用户的任务 |
|
| **User** | Task | `user_id` | 用户的任务 |
|
||||||
| **User** | WebDAV | `user_id` | 用户的 WebDAV 账户 |
|
| **User** | WebDAV | `user_id` | 用户的 WebDAV 账户 |
|
||||||
| **User** | UserAuthn | `user_id` | 用户的 WebAuthn 凭证 |
|
| **User** | UserAuthn | `user_id` | 用户的 WebAuthn 凭证 |
|
||||||
|
| **User** | UploadSession | `owner_id` | 用户的上传会话 |
|
||||||
| **Policy** | Object | `policy_id` | 存储策略下的对象 |
|
| **Policy** | Object | `policy_id` | 存储策略下的对象 |
|
||||||
|
| **Policy** | PhysicalFile | `policy_id` | 存储策略下的物理文件 |
|
||||||
|
| **PhysicalFile** | Object | `physical_file_id` | 物理文件被多个逻辑对象引用(文件去重) |
|
||||||
| **Object** | Object | `parent_id` | 目录的子文件/子目录(自引用) |
|
| **Object** | Object | `parent_id` | 目录的子文件/子目录(自引用) |
|
||||||
| **Object** | SourceLink | `object_id` | 文件的源链接 |
|
| **Object** | SourceLink | `object_id` | 文件的源链接 |
|
||||||
| **Object** | Share | `object_id` | 对象的分享 |
|
| **Object** | Share | `object_id` | 对象的分享 |
|
||||||
@@ -811,6 +920,36 @@ class PolicyType(StrEnum):
|
|||||||
S3 = "s3" # S3 兼容存储
|
S3 = "s3" # S3 兼容存储
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### StorageType
|
||||||
|
```python
|
||||||
|
class StorageType(StrEnum):
|
||||||
|
LOCAL = "local" # 本地存储
|
||||||
|
QINIU = "qiniu" # 七牛云
|
||||||
|
TENCENT = "tencent" # 腾讯云
|
||||||
|
ALIYUN = "aliyun" # 阿里云
|
||||||
|
ONEDRIVE = "onedrive" # OneDrive
|
||||||
|
GOOGLE_DRIVE = "google_drive" # Google Drive
|
||||||
|
DROPBOX = "dropbox" # Dropbox
|
||||||
|
WEBDAV = "webdav" # WebDAV
|
||||||
|
REMOTE = "remote" # 远程存储
|
||||||
|
```
|
||||||
|
|
||||||
|
### UserStatus
|
||||||
|
```python
|
||||||
|
class UserStatus(StrEnum):
|
||||||
|
ACTIVE = "active" # 正常
|
||||||
|
ADMIN_BANNED = "admin_banned" # 管理员封禁
|
||||||
|
SYSTEM_BANNED = "system_banned" # 系统封禁
|
||||||
|
```
|
||||||
|
|
||||||
|
### CaptchaType
|
||||||
|
```python
|
||||||
|
class CaptchaType(StrEnum):
|
||||||
|
DEFAULT = "default" # 默认验证码
|
||||||
|
GCAPTCHA = "gcaptcha" # Google reCAPTCHA
|
||||||
|
CLOUD_FLARE_TURNSTILE = "cloudflare turnstile" # Cloudflare Turnstile
|
||||||
|
```
|
||||||
|
|
||||||
### ThemeType
|
### ThemeType
|
||||||
```python
|
```python
|
||||||
class ThemeType(StrEnum):
|
class ThemeType(StrEnum):
|
||||||
@@ -893,6 +1032,9 @@ class OrderStatus(StrEnum):
|
|||||||
| `UserSettingResponse` | 用户设置响应 |
|
| `UserSettingResponse` | 用户设置响应 |
|
||||||
| `WebAuthnInfo` | WebAuthn 信息 |
|
| `WebAuthnInfo` | WebAuthn 信息 |
|
||||||
| `AuthnResponse` | WebAuthn 响应 |
|
| `AuthnResponse` | WebAuthn 响应 |
|
||||||
|
| `UserAdminUpdateRequest` | 管理员更新用户请求 |
|
||||||
|
| `UserCalibrateResponse` | 用户存储校准响应 |
|
||||||
|
| `UserAdminDetailResponse` | 管理员用户详情响应 |
|
||||||
|
|
||||||
### 用户组相关
|
### 用户组相关
|
||||||
|
|
||||||
@@ -918,14 +1060,38 @@ class OrderStatus(StrEnum):
|
|||||||
| `DirectoryResponse` | 目录响应 |
|
| `DirectoryResponse` | 目录响应 |
|
||||||
| `ObjectMoveRequest` | 移动对象请求 |
|
| `ObjectMoveRequest` | 移动对象请求 |
|
||||||
| `ObjectDeleteRequest` | 删除对象请求 |
|
| `ObjectDeleteRequest` | 删除对象请求 |
|
||||||
|
| `ObjectCopyRequest` | 复制对象请求 |
|
||||||
|
| `ObjectRenameRequest` | 重命名对象请求 |
|
||||||
|
| `ObjectPropertyResponse` | 对象基本属性响应 |
|
||||||
|
| `ObjectPropertyDetailResponse` | 对象详细属性响应 |
|
||||||
| `PolicyResponse` | 存储策略响应 |
|
| `PolicyResponse` | 存储策略响应 |
|
||||||
|
|
||||||
### 其他
|
### 上传相关
|
||||||
|
|
||||||
|
| DTO | 说明 |
|
||||||
|
|-----|------|
|
||||||
|
| `CreateUploadSessionRequest` | 创建上传会话请求 |
|
||||||
|
| `UploadSessionResponse` | 上传会话响应 |
|
||||||
|
| `UploadChunkResponse` | 上传分片响应 |
|
||||||
|
| `CreateFileRequest` | 创建空白文件请求 |
|
||||||
|
|
||||||
|
### 管理员文件管理
|
||||||
|
|
||||||
|
| DTO | 说明 |
|
||||||
|
|-----|------|
|
||||||
|
| `AdminFileResponse` | 管理员文件响应 |
|
||||||
|
| `FileBanRequest` | 文件封禁请求 |
|
||||||
|
| `AdminFileListResponse` | 管理员文件列表响应 |
|
||||||
|
|
||||||
|
### 系统设置
|
||||||
|
|
||||||
| DTO | 说明 |
|
| DTO | 说明 |
|
||||||
|-----|------|
|
|-----|------|
|
||||||
| `SiteConfigResponse` | 站点配置响应 |
|
| `SiteConfigResponse` | 站点配置响应 |
|
||||||
| `ThemeResponse` | 主题颜色响应 |
|
| `ThemeResponse` | 主题颜色响应 |
|
||||||
|
| `SettingItem` | 设置项 |
|
||||||
|
| `SettingsUpdateRequest` | 更新设置请求 |
|
||||||
|
| `SettingsGetResponse` | 获取设置响应 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -3,29 +3,5 @@ from .base import SQLModelBase
|
|||||||
class ThemeResponse(SQLModelBase):
|
class ThemeResponse(SQLModelBase):
|
||||||
"""主题响应 DTO"""
|
"""主题响应 DTO"""
|
||||||
|
|
||||||
primary: str = "#3f51b5"
|
pass
|
||||||
"""主色调"""
|
|
||||||
|
|
||||||
secondary: str = "#f50057"
|
|
||||||
"""次要色"""
|
|
||||||
|
|
||||||
accent: str = "#9c27b0"
|
|
||||||
"""强调色"""
|
|
||||||
|
|
||||||
dark: str = "#1d1d1d"
|
|
||||||
"""深色"""
|
|
||||||
|
|
||||||
dark_page: str = "#121212"
|
|
||||||
"""深色页面背景"""
|
|
||||||
|
|
||||||
positive: str = "#21ba45"
|
|
||||||
"""正面/成功色"""
|
|
||||||
|
|
||||||
negative: str = "#c10015"
|
|
||||||
"""负面/错误色"""
|
|
||||||
|
|
||||||
info: str = "#31ccec"
|
|
||||||
"""信息色"""
|
|
||||||
|
|
||||||
warning: str = "#f2c037"
|
|
||||||
"""警告色"""
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Annotated
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from sqlmodel import Field, Relationship, UniqueConstraint, Index
|
from sqlmodel import Field, Relationship, UniqueConstraint, Index
|
||||||
@@ -7,6 +7,10 @@ from sqlmodel import Field, Relationship, UniqueConstraint, Index
|
|||||||
from .base import SQLModelBase
|
from .base import SQLModelBase
|
||||||
from .mixin import UUIDTableBaseMixin, TableBaseMixin
|
from .mixin import UUIDTableBaseMixin, TableBaseMixin
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .user import User
|
||||||
|
from .task import Task
|
||||||
|
from .node import Node
|
||||||
|
|
||||||
class DownloadStatus(StrEnum):
|
class DownloadStatus(StrEnum):
|
||||||
"""下载状态枚举"""
|
"""下载状态枚举"""
|
||||||
@@ -24,18 +28,12 @@ class DownloadType(StrEnum):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .user import User
|
|
||||||
from .task import Task
|
|
||||||
from .node import Node
|
|
||||||
|
|
||||||
|
|
||||||
# ==================== Aria2 信息模型 ====================
|
# ==================== Aria2 信息模型 ====================
|
||||||
|
|
||||||
class DownloadAria2InfoBase(SQLModelBase):
|
class DownloadAria2InfoBase(SQLModelBase):
|
||||||
"""Aria2下载信息基础模型"""
|
"""Aria2下载信息基础模型"""
|
||||||
|
|
||||||
info_hash: str | None = Field(default=None, max_length=40)
|
info_hash: Annotated[str | None, Field(max_length=40)] = None
|
||||||
"""InfoHash(BT种子)"""
|
"""InfoHash(BT种子)"""
|
||||||
|
|
||||||
piece_length: int = 0
|
piece_length: int = 0
|
||||||
@@ -118,11 +116,10 @@ class Download(DownloadBase, UUIDTableBaseMixin):
|
|||||||
|
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
UniqueConstraint("node_id", "g_id", name="uq_download_node_gid"),
|
UniqueConstraint("node_id", "g_id", name="uq_download_node_gid"),
|
||||||
Index("ix_download_status", "status"),
|
|
||||||
Index("ix_download_user_status", "user_id", "status"),
|
Index("ix_download_user_status", "user_id", "status"),
|
||||||
)
|
)
|
||||||
|
|
||||||
status: DownloadStatus = Field(default=DownloadStatus.RUNNING, sa_column_kwargs={"server_default": "'running'"})
|
status: DownloadStatus = Field(default=DownloadStatus.RUNNING, sa_column_kwargs={"server_default": "'running'"}, index=True)
|
||||||
"""下载状态"""
|
"""下载状态"""
|
||||||
|
|
||||||
type: int = Field(default=0, sa_column_kwargs={"server_default": "0"})
|
type: int = Field(default=0, sa_column_kwargs={"server_default": "0"})
|
||||||
@@ -196,5 +193,4 @@ class Download(DownloadBase, UUIDTableBaseMixin):
|
|||||||
|
|
||||||
node: "Node" = Relationship(back_populates="downloads")
|
node: "Node" = Relationship(back_populates="downloads")
|
||||||
"""执行下载的节点"""
|
"""执行下载的节点"""
|
||||||
|
|
||||||
|
|
||||||
73
utils/http/http_exceptions.py
Normal file
73
utils/http/http_exceptions.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
from typing import Any, NoReturn
|
||||||
|
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
from starlette.status import (
|
||||||
|
HTTP_400_BAD_REQUEST,
|
||||||
|
HTTP_401_UNAUTHORIZED,
|
||||||
|
HTTP_402_PAYMENT_REQUIRED,
|
||||||
|
HTTP_403_FORBIDDEN,
|
||||||
|
HTTP_404_NOT_FOUND,
|
||||||
|
HTTP_409_CONFLICT,
|
||||||
|
HTTP_429_TOO_MANY_REQUESTS,
|
||||||
|
HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
HTTP_501_NOT_IMPLEMENTED,
|
||||||
|
HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
HTTP_504_GATEWAY_TIMEOUT,
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- 400 ---
|
||||||
|
|
||||||
|
def ensure_request_param(to_check: Any, detail: str) -> None:
|
||||||
|
"""
|
||||||
|
Ensures a parameter exists. If not, raises a 400 Bad Request.
|
||||||
|
This function returns None if the check passes.
|
||||||
|
"""
|
||||||
|
if not to_check:
|
||||||
|
raise HTTPException(status_code=HTTP_400_BAD_REQUEST, detail=detail)
|
||||||
|
|
||||||
|
def raise_bad_request(detail: str = '') -> NoReturn:
|
||||||
|
"""Raises an HTTP 400 Bad Request exception."""
|
||||||
|
raise HTTPException(status_code=HTTP_400_BAD_REQUEST, detail=detail)
|
||||||
|
|
||||||
|
def raise_unauthorized(detail: str) -> NoReturn:
|
||||||
|
"""Raises an HTTP 401 Unauthorized exception."""
|
||||||
|
raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail=detail)
|
||||||
|
|
||||||
|
def raise_insufficient_quota(detail: str = "积分不足,请充值") -> NoReturn:
|
||||||
|
"""Raises an HTTP 402 Payment Required exception."""
|
||||||
|
raise HTTPException(status_code=HTTP_402_PAYMENT_REQUIRED, detail=detail)
|
||||||
|
|
||||||
|
def raise_forbidden(detail: str) -> NoReturn:
|
||||||
|
"""Raises an HTTP 403 Forbidden exception."""
|
||||||
|
raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail=detail)
|
||||||
|
|
||||||
|
def raise_not_found(detail: str) -> NoReturn:
|
||||||
|
"""Raises an HTTP 404 Not Found exception."""
|
||||||
|
raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail=detail)
|
||||||
|
|
||||||
|
def raise_conflict(detail: str) -> NoReturn:
|
||||||
|
"""Raises an HTTP 409 Conflict exception."""
|
||||||
|
raise HTTPException(status_code=HTTP_409_CONFLICT, detail=detail)
|
||||||
|
|
||||||
|
def raise_too_many_requests(detail: str) -> NoReturn:
|
||||||
|
"""Raises an HTTP 429 Too Many Requests exception."""
|
||||||
|
raise HTTPException(status_code=HTTP_429_TOO_MANY_REQUESTS, detail=detail)
|
||||||
|
|
||||||
|
# --- 500 ---
|
||||||
|
|
||||||
|
def raise_internal_error(detail: str = "服务器出现故障,请稍后再试或联系管理员") -> NoReturn:
|
||||||
|
"""Raises an HTTP 500 Internal Server Error exception."""
|
||||||
|
raise HTTPException(status_code=HTTP_500_INTERNAL_SERVER_ERROR, detail=detail)
|
||||||
|
|
||||||
|
def raise_not_implemented(detail: str = "尚未支持这种方法") -> NoReturn:
|
||||||
|
"""Raises an HTTP 501 Not Implemented exception."""
|
||||||
|
raise HTTPException(status_code=HTTP_501_NOT_IMPLEMENTED, detail=detail)
|
||||||
|
|
||||||
|
def raise_service_unavailable(detail: str) -> NoReturn:
|
||||||
|
"""Raises an HTTP 503 Service Unavailable exception."""
|
||||||
|
raise HTTPException(status_code=HTTP_503_SERVICE_UNAVAILABLE, detail=detail)
|
||||||
|
|
||||||
|
def raise_gateway_timeout(detail: str) -> NoReturn:
|
||||||
|
"""Raises an HTTP 504 Gateway Timeout exception."""
|
||||||
|
raise HTTPException(status_code=HTTP_504_GATEWAY_TIMEOUT, detail=detail)
|
||||||
Reference in New Issue
Block a user