feat: Enhance file management and user features
- Add file deduplication mechanism based on PhysicalFile reference counting. - Implement chunked upload support for large files with resumable uploads. - Update sharing page to automatically render README and preview content. - Integrate Redis for caching and token storage (optional). - Refactor project structure to include new models for download tasks, nodes, and tasks. - Introduce user filtering parameters for admin user management. - Add CORS middleware for handling cross-origin requests. - Improve error messages for authentication failures. - Update user model to include two-factor authentication key management. - Enhance API documentation and response models for clarity. - Implement admin checks for user management and permissions.
This commit is contained in:
70
README.md
70
README.md
@@ -27,10 +27,12 @@
|
|||||||
- **多存储策略**:支持本地存储、S3 兼容 API、阿里云 OSS、OneDrive 等多种存储后端
|
- **多存储策略**:支持本地存储、S3 兼容 API、阿里云 OSS、OneDrive 等多种存储后端
|
||||||
- **远程节点**:可对接从节点分担存储和下载任务
|
- **远程节点**:可对接从节点分担存储和下载任务
|
||||||
- **WebDAV 兼容**:提供标准 WebDAV 接口,支持第三方客户端访问
|
- **WebDAV 兼容**:提供标准 WebDAV 接口,支持第三方客户端访问
|
||||||
|
- **文件去重**:基于 PhysicalFile 引用计数的文件去重机制
|
||||||
|
|
||||||
### 文件管理
|
### 文件管理
|
||||||
|
|
||||||
- **统一对象模型**:文件和目录采用统一的 Object 模型管理
|
- **统一对象模型**:文件和目录采用统一的 Object 模型管理
|
||||||
|
- **分片上传**:支持大文件分片上传,可断点续传
|
||||||
- **在线压缩/解压**:支持批量打包下载
|
- **在线压缩/解压**:支持批量打包下载
|
||||||
- **离线下载**:内置离线下载服务,也可对接 Aria2/qBittorrent
|
- **离线下载**:内置离线下载服务,也可对接 Aria2/qBittorrent
|
||||||
|
|
||||||
@@ -50,7 +52,7 @@
|
|||||||
### 分享功能
|
### 分享功能
|
||||||
|
|
||||||
- **分享链接管理**:可设置密码、过期时间
|
- **分享链接管理**:可设置密码、过期时间
|
||||||
- **分享页展示** 自动渲染分享中的 README、在线预览内容
|
- **分享页展示**:自动渲染分享中的 README、在线预览内容
|
||||||
|
|
||||||
### 增值服务
|
### 增值服务
|
||||||
|
|
||||||
@@ -67,8 +69,10 @@
|
|||||||
| [Python 3.13+](https://www.python.org/) | 编程语言 |
|
| [Python 3.13+](https://www.python.org/) | 编程语言 |
|
||||||
| [FastAPI](https://fastapi.tiangolo.com/) | 高性能异步 Web 框架 |
|
| [FastAPI](https://fastapi.tiangolo.com/) | 高性能异步 Web 框架 |
|
||||||
| [SQLModel](https://sqlmodel.tiangolo.com/) | 类型安全的 ORM(SQLAlchemy + Pydantic) |
|
| [SQLModel](https://sqlmodel.tiangolo.com/) | 类型安全的 ORM(SQLAlchemy + Pydantic) |
|
||||||
|
| [Redis](https://redis.io/) | 缓存与令牌存储(可选) |
|
||||||
| [aiohttp](https://docs.aiohttp.org/) | 异步 HTTP 客户端 |
|
| [aiohttp](https://docs.aiohttp.org/) | 异步 HTTP 客户端 |
|
||||||
| [aiosqlite](https://aiosqlite.omnilib.dev/) | 异步 SQLite 驱动 |
|
| [aiosqlite](https://aiosqlite.omnilib.dev/) | 异步 SQLite 驱动 |
|
||||||
|
| [asyncpg](https://magicstack.github.io/asyncpg/) | 异步 PostgreSQL 驱动 |
|
||||||
| [Loguru](https://loguru.readthedocs.io/) | 现代化日志库 |
|
| [Loguru](https://loguru.readthedocs.io/) | 现代化日志库 |
|
||||||
| [PyJWT](https://pyjwt.readthedocs.io/) | JWT 令牌处理 |
|
| [PyJWT](https://pyjwt.readthedocs.io/) | JWT 令牌处理 |
|
||||||
| [WebAuthn](https://pypi.org/project/webauthn/) | Passkey 认证支持 |
|
| [WebAuthn](https://pypi.org/project/webauthn/) | Passkey 认证支持 |
|
||||||
@@ -81,35 +85,59 @@
|
|||||||
Server/
|
Server/
|
||||||
├── main.py # 应用入口
|
├── main.py # 应用入口
|
||||||
├── models/ # 数据模型
|
├── models/ # 数据模型
|
||||||
│ ├── base/ # 基类定义 (SQLModelBase, TableBase)
|
│ ├── base/ # 基类定义 (SQLModelBase)
|
||||||
|
│ ├── mixin/ # Mixin 模块 (TableBaseMixin, UUIDTableBaseMixin)
|
||||||
│ ├── user.py # 用户模型
|
│ ├── user.py # 用户模型
|
||||||
|
│ ├── user_authn.py # WebAuthn 凭证
|
||||||
│ ├── group.py # 用户组模型
|
│ ├── group.py # 用户组模型
|
||||||
│ ├── object.py # 文件/目录统一模型
|
│ ├── object.py # 文件/目录统一模型 + 上传会话
|
||||||
|
│ ├── physical_file.py # 物理文件模型(文件去重)
|
||||||
│ ├── policy.py # 存储策略模型
|
│ ├── policy.py # 存储策略模型
|
||||||
│ ├── share.py # 分享模型
|
│ ├── share.py # 分享模型
|
||||||
|
│ ├── download.py # 离线下载任务
|
||||||
|
│ ├── node.py # 节点模型
|
||||||
|
│ ├── task.py # 任务模型
|
||||||
│ └── ...
|
│ └── ...
|
||||||
├── routers/ # API 路由
|
├── routers/ # API 路由
|
||||||
│ └── api/v1/ # v1 版本 API
|
│ ├── api/v1/ # v1 版本 API
|
||||||
│ ├── user/ # 用户相关接口
|
│ │ ├── user/ # 用户相关接口
|
||||||
│ ├── directory/ # 目录相关接口
|
│ │ ├── directory/ # 目录相关接口
|
||||||
│ ├── file/ # 文件相关接口
|
│ │ ├── file/ # 文件上传/下载接口
|
||||||
│ ├── admin/ # 管理员接口
|
│ │ ├── object/ # 对象操作接口
|
||||||
│ └── ...
|
│ │ ├── share/ # 分享接口
|
||||||
|
│ │ ├── admin/ # 管理员接口
|
||||||
|
│ │ │ ├── user/ # 用户管理
|
||||||
|
│ │ │ ├── group/ # 用户组管理
|
||||||
|
│ │ │ ├── policy/ # 存储策略管理
|
||||||
|
│ │ │ ├── file/ # 文件管理
|
||||||
|
│ │ │ ├── share/ # 分享管理
|
||||||
|
│ │ │ ├── task/ # 任务管理
|
||||||
|
│ │ │ └── vas/ # 增值服务管理
|
||||||
|
│ │ └── ...
|
||||||
|
│ └── dav/ # WebDAV 路由
|
||||||
├── service/ # 业务服务层
|
├── service/ # 业务服务层
|
||||||
│ ├── user/ # 用户服务
|
│ ├── user/ # 用户服务(登录)
|
||||||
│ ├── captcha/ # 验证码服务
|
│ ├── storage/ # 存储服务(本地存储)
|
||||||
│ └── oauth/ # OAuth 服务
|
│ ├── captcha/ # 验证码服务(reCAPTCHA、Turnstile)
|
||||||
|
│ ├── oauth/ # OAuth 服务(QQ、GitHub)
|
||||||
|
│ └── redis/ # Redis 服务(连接管理、令牌存储)
|
||||||
├── middleware/ # 中间件
|
├── middleware/ # 中间件
|
||||||
│ ├── auth.py # 认证中间件
|
│ ├── auth.py # 认证中间件
|
||||||
│ └── dependencies.py # 依赖注入
|
│ └── dependencies.py # 依赖注入
|
||||||
├── utils/ # 工具函数
|
├── utils/ # 工具函数
|
||||||
│ ├── JWT/ # JWT 处理
|
│ ├── JWT/ # JWT 处理
|
||||||
│ ├── password/ # 密码处理
|
│ ├── password/ # 密码处理(Argon2、TOTP)
|
||||||
│ ├── conf/ # 配置管理
|
│ ├── conf/ # 配置管理
|
||||||
|
│ ├── http/ # HTTP 异常处理
|
||||||
│ └── lifespan/ # 生命周期管理
|
│ └── lifespan/ # 生命周期管理
|
||||||
└── tests/ # 测试用例
|
└── tests/ # 测试用例
|
||||||
├── unit/ # 单元测试
|
├── unit/ # 单元测试
|
||||||
|
│ ├── models/ # 模型测试
|
||||||
|
│ ├── service/ # 服务测试
|
||||||
|
│ └── utils/ # 工具测试
|
||||||
├── integration/ # 集成测试
|
├── integration/ # 集成测试
|
||||||
|
│ ├── api/ # API 测试
|
||||||
|
│ └── middleware/ # 中间件测试
|
||||||
└── fixtures/ # 测试夹具
|
└── fixtures/ # 测试夹具
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -121,7 +149,7 @@ Server/
|
|||||||
| 用户 | `/api/v1/user` | 用户注册、登录、设置 |
|
| 用户 | `/api/v1/user` | 用户注册、登录、设置 |
|
||||||
| 目录 | `/api/v1/directory` | 目录浏览和管理 |
|
| 目录 | `/api/v1/directory` | 目录浏览和管理 |
|
||||||
| 文件 | `/api/v1/file` | 文件上传、下载、管理 |
|
| 文件 | `/api/v1/file` | 文件上传、下载、管理 |
|
||||||
| 对象 | `/api/v1/object` | 文件和目录的通用操作 |
|
| 对象 | `/api/v1/object` | 文件和目录的通用操作(删除、移动、复制、重命名) |
|
||||||
| 分享 | `/api/v1/share` | 分享链接管理 |
|
| 分享 | `/api/v1/share` | 分享链接管理 |
|
||||||
| 下载 | `/api/v1/download` | 离线下载管理 |
|
| 下载 | `/api/v1/download` | 离线下载管理 |
|
||||||
| 标签 | `/api/v1/tag` | 用户标签管理 |
|
| 标签 | `/api/v1/tag` | 用户标签管理 |
|
||||||
@@ -137,6 +165,7 @@ Server/
|
|||||||
|
|
||||||
- Python 3.13 或更高版本
|
- Python 3.13 或更高版本
|
||||||
- uv (推荐) 或 pip
|
- uv (推荐) 或 pip
|
||||||
|
- Redis (可选,用于令牌存储和缓存)
|
||||||
|
|
||||||
### 安装
|
### 安装
|
||||||
|
|
||||||
@@ -157,8 +186,17 @@ uv sync
|
|||||||
# 调试模式
|
# 调试模式
|
||||||
DEBUG=false
|
DEBUG=false
|
||||||
|
|
||||||
|
# 运行模式: master(主节点)或 slave(从节点)
|
||||||
|
MODE=master
|
||||||
|
|
||||||
# 数据库连接(默认使用 SQLite)
|
# 数据库连接(默认使用 SQLite)
|
||||||
DATABASE_URL=sqlite+aiosqlite:///disknext.db
|
DATABASE_URL=sqlite+aiosqlite:///disknext.db
|
||||||
|
|
||||||
|
# Redis 配置(可选,不配置则使用内存缓存)
|
||||||
|
REDIS_URL=localhost
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_PASSWORD=
|
||||||
|
REDIS_DB=0
|
||||||
```
|
```
|
||||||
|
|
||||||
### 启动
|
### 启动
|
||||||
@@ -171,7 +209,9 @@ fastapi dev
|
|||||||
fastapi run
|
fastapi run
|
||||||
```
|
```
|
||||||
|
|
||||||
访问 <http://localhost:8000/docs> 查看 API 文档。
|
访问 <http://localhost:8000/docs> 查看 API 文档(仅 DEBUG=true 时可用)。
|
||||||
|
|
||||||
|
首次启动会自动初始化数据库并创建默认管理员账户,**请注意控制台输出的初始密码**。
|
||||||
|
|
||||||
## 测试
|
## 测试
|
||||||
|
|
||||||
|
|||||||
105
ROADMAP.md
105
ROADMAP.md
@@ -24,17 +24,25 @@
|
|||||||
|
|
||||||
- [x] FastAPI 应用框架搭建
|
- [x] FastAPI 应用框架搭建
|
||||||
- [x] SQLModel ORM 集成
|
- [x] SQLModel ORM 集成
|
||||||
- [x] 异步数据库支持 (aiosqlite)
|
- [x] 异步数据库支持 (aiosqlite, asyncpg)
|
||||||
- [x] 项目结构规范化
|
- [x] 项目结构规范化
|
||||||
- [x] 开发规范文档 (CLAUDE.md)
|
- [x] 开发规范文档 (AGENTS.md)
|
||||||
|
- [x] Redis 集成(可选,支持降级)
|
||||||
|
- [x] 生命周期管理(启动/关闭事件)
|
||||||
|
- [x] 数据库自动迁移/初始化
|
||||||
|
|
||||||
#### 数据模型
|
#### 数据模型
|
||||||
|
|
||||||
- [x] 基类定义 (SQLModelBase, TableBase, UUIDTableBase)
|
- [x] 基类定义 (SQLModelBase, TableBaseMixin, UUIDTableBaseMixin)
|
||||||
|
- [x] 自定义元类支持联表继承和多态
|
||||||
- [x] 用户模型 (User)
|
- [x] 用户模型 (User)
|
||||||
|
- [x] 用户 WebAuthn 凭证模型 (UserAuthn)
|
||||||
- [x] 用户组模型 (Group, GroupOptions)
|
- [x] 用户组模型 (Group, GroupOptions)
|
||||||
- [x] 统一对象模型 (Object) - 合并文件和目录
|
- [x] 统一对象模型 (Object) - 合并文件和目录
|
||||||
- [x] 存储策略模型 (Policy)
|
- [x] 物理文件模型 (PhysicalFile) - 文件去重
|
||||||
|
- [x] 上传会话模型 (UploadSession)
|
||||||
|
- [x] 文件元数据模型 (FileMetadata)
|
||||||
|
- [x] 存储策略模型 (Policy, PolicyOptions)
|
||||||
- [x] 分享模型 (Share)
|
- [x] 分享模型 (Share)
|
||||||
- [x] 标签模型 (Tag)
|
- [x] 标签模型 (Tag)
|
||||||
- [x] WebDAV 模型 (WebDAV)
|
- [x] WebDAV 模型 (WebDAV)
|
||||||
@@ -48,21 +56,62 @@
|
|||||||
- [x] JWT 令牌认证
|
- [x] JWT 令牌认证
|
||||||
- [x] 获取当前用户信息
|
- [x] 获取当前用户信息
|
||||||
- [x] 用户存储空间查询
|
- [x] 用户存储空间查询
|
||||||
|
- [x] 用户设置获取
|
||||||
|
|
||||||
|
#### 目录系统
|
||||||
|
|
||||||
|
- [x] 目录浏览接口(路径解析)
|
||||||
|
- [x] 目录创建接口
|
||||||
|
|
||||||
|
#### 文件系统
|
||||||
|
|
||||||
|
- [x] 创建上传会话
|
||||||
|
- [x] 分片上传接口
|
||||||
|
- [x] 上传会话管理(删除、清除)
|
||||||
|
- [x] 创建空白文件
|
||||||
|
- [x] 下载令牌生成
|
||||||
|
- [x] 文件下载(一次性令牌)
|
||||||
|
|
||||||
|
#### 对象操作
|
||||||
|
|
||||||
|
- [x] 对象删除(支持递归删除目录)
|
||||||
|
- [x] 对象移动
|
||||||
|
- [x] 对象复制(引用计数,不复制物理文件)
|
||||||
|
- [x] 对象重命名
|
||||||
|
- [x] 对象属性查询(基本属性、详细属性)
|
||||||
|
|
||||||
#### 认证安全
|
#### 认证安全
|
||||||
|
|
||||||
- [x] Argon2 密码哈希
|
- [x] Argon2 密码哈希
|
||||||
- [x] JWT 令牌生成与验证
|
- [x] JWT 令牌生成与验证
|
||||||
- [x] 认证中间件
|
- [x] 认证中间件
|
||||||
|
- [x] 管理员权限中间件
|
||||||
- [x] 两步验证 (2FA/TOTP) 初始化与启用
|
- [x] 两步验证 (2FA/TOTP) 初始化与启用
|
||||||
- [x] WebAuthn 注册初始化
|
- [x] WebAuthn 注册初始化
|
||||||
|
- [x] 下载令牌验证(一次性使用)
|
||||||
|
- [x] 令牌存储(Redis/内存降级)
|
||||||
|
|
||||||
|
#### 存储策略
|
||||||
|
|
||||||
|
- [x] 本地存储策略实现
|
||||||
|
- [x] 存储路径生成(支持命名规则)
|
||||||
|
- [x] 文件写入/读取/删除
|
||||||
|
- [x] 回收站机制
|
||||||
|
|
||||||
|
#### 管理后台
|
||||||
|
|
||||||
|
- [x] 管理员权限验证
|
||||||
|
- [x] 站点概况统计(用户数、文件数、分享数趋势)
|
||||||
|
- [x] 设置管理(获取、批量更新)
|
||||||
|
- [x] Aria2 连接测试
|
||||||
|
|
||||||
#### 测试
|
#### 测试
|
||||||
|
|
||||||
- [x] pytest 测试框架配置
|
- [x] pytest 测试框架配置
|
||||||
- [x] 单元测试结构
|
- [x] 单元测试结构(models, service, utils)
|
||||||
- [x] 集成测试结构
|
- [x] 集成测试结构(api, middleware)
|
||||||
- [x] 测试夹具 (fixtures)
|
- [x] 测试夹具 (fixtures)
|
||||||
|
- [x] 覆盖率报告配置
|
||||||
|
|
||||||
### 进行中
|
### 进行中
|
||||||
|
|
||||||
@@ -73,15 +122,8 @@
|
|||||||
- [ ] 用户设置管理
|
- [ ] 用户设置管理
|
||||||
- [ ] 头像上传/Gravatar
|
- [ ] 头像上传/Gravatar
|
||||||
|
|
||||||
#### 目录系统
|
|
||||||
|
|
||||||
- [ ] 目录浏览接口
|
|
||||||
- [ ] 目录创建接口
|
|
||||||
- [ ] 路径解析优化
|
|
||||||
|
|
||||||
#### 存储策略
|
#### 存储策略
|
||||||
|
|
||||||
- [ ] 本地存储策略实现
|
|
||||||
- [ ] S3 存储策略实现
|
- [ ] S3 存储策略实现
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -90,21 +132,16 @@
|
|||||||
|
|
||||||
### 文件操作
|
### 文件操作
|
||||||
|
|
||||||
- [ ] 文件上传(单文件)
|
|
||||||
- [ ] 文件上传(分块上传)
|
|
||||||
- [ ] 文件下载
|
|
||||||
- [ ] 文件预览 URL 生成
|
- [ ] 文件预览 URL 生成
|
||||||
- [ ] 缩略图生成
|
- [ ] 缩略图生成
|
||||||
- [ ] 文件移动/复制
|
- [ ] 文件内容获取(文本文件)
|
||||||
- [ ] 文件重命名
|
- [ ] Office 文档预览
|
||||||
- [ ] 文件删除(软删除/回收站)
|
- [ ] 文件外链功能
|
||||||
|
|
||||||
### 目录操作
|
### 目录操作
|
||||||
|
|
||||||
- [ ] 目录树查询
|
- [ ] 目录树查询
|
||||||
- [ ] 目录移动/复制
|
- [ ] 批量操作优化
|
||||||
- [ ] 目录删除(递归)
|
|
||||||
- [ ] 批量操作
|
|
||||||
|
|
||||||
### 存储策略完善
|
### 存储策略完善
|
||||||
|
|
||||||
@@ -117,10 +154,17 @@
|
|||||||
### 用户组权限
|
### 用户组权限
|
||||||
|
|
||||||
- [ ] 权限验证中间件
|
- [ ] 权限验证中间件
|
||||||
- [ ] 存储空间限制
|
- [ ] 存储空间限制检查
|
||||||
- [ ] 文件类型限制
|
- [ ] 文件类型限制
|
||||||
- [ ] 单文件大小限制
|
- [ ] 单文件大小限制
|
||||||
|
|
||||||
|
### 管理后台完善
|
||||||
|
|
||||||
|
- [ ] 用户管理接口
|
||||||
|
- [ ] 用户组管理接口
|
||||||
|
- [ ] 存储策略管理接口
|
||||||
|
- [ ] 文件管理接口(封禁/解封)
|
||||||
|
|
||||||
### Webhook 事件系统
|
### Webhook 事件系统
|
||||||
|
|
||||||
- [ ] 文件事件推送(创建、修改、删除、分享)
|
- [ ] 文件事件推送(创建、修改、删除、分享)
|
||||||
@@ -161,15 +205,12 @@
|
|||||||
- [ ] 存储容量包
|
- [ ] 存储容量包
|
||||||
- [ ] 订单管理
|
- [ ] 订单管理
|
||||||
|
|
||||||
### 管理后台
|
### 文件处理
|
||||||
|
|
||||||
- [ ] 用户管理接口
|
- [ ] 文件压缩任务
|
||||||
- [ ] 用户组管理接口
|
- [ ] 文件解压任务
|
||||||
- [ ] 存储策略管理接口
|
- [ ] 文件转移任务
|
||||||
- [ ] 系统设置接口
|
- [ ] 文件搜索功能
|
||||||
- [ ] 任务管理接口
|
|
||||||
- [ ] 文件管理接口
|
|
||||||
- [ ] 数据统计接口
|
|
||||||
|
|
||||||
### 协作功能
|
### 协作功能
|
||||||
|
|
||||||
@@ -416,4 +457,4 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*最后更新:2025年12月*
|
*最后更新:2026年1月*
|
||||||
|
|||||||
10
main.py
10
main.py
@@ -1,6 +1,7 @@
|
|||||||
from typing import NoReturn
|
from typing import NoReturn
|
||||||
|
|
||||||
from fastapi import FastAPI, Request
|
from fastapi import FastAPI, Request
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
from utils.conf import appmeta
|
from utils.conf import appmeta
|
||||||
from utils.http.http_exceptions import raise_internal_error
|
from utils.http.http_exceptions import raise_internal_error
|
||||||
@@ -33,6 +34,15 @@ app = FastAPI(
|
|||||||
openapi_url="/openapi.json" if appmeta.debug else None,
|
openapi_url="/openapi.json" if appmeta.debug else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 配置 CORS 中间件
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"], # 开发环境允许所有来源,生产环境应该限制为具体域名
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
@app.exception_handler(Exception)
|
@app.exception_handler(Exception)
|
||||||
async def handle_unexpected_exceptions(request: Request, exc: Exception) -> NoReturn:
|
async def handle_unexpected_exceptions(request: Request, exc: Exception) -> NoReturn:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ async def auth_required(
|
|||||||
return user
|
return user
|
||||||
|
|
||||||
except jwt.InvalidTokenError:
|
except jwt.InvalidTokenError:
|
||||||
http_exceptions.raise_unauthorized("账号或密码错误")
|
http_exceptions.raise_unauthorized("凭据过期或无效")
|
||||||
|
|
||||||
async def admin_required(
|
async def admin_required(
|
||||||
user: Annotated[User, Depends(auth_required)],
|
user: Annotated[User, Depends(auth_required)],
|
||||||
|
|||||||
@@ -5,15 +5,18 @@ FastAPI 依赖注入
|
|||||||
- SessionDep: 数据库会话依赖
|
- SessionDep: 数据库会话依赖
|
||||||
- TimeFilterRequestDep: 时间筛选查询依赖(用于 count 等统计接口)
|
- TimeFilterRequestDep: 时间筛选查询依赖(用于 count 等统计接口)
|
||||||
- TableViewRequestDep: 分页排序查询依赖(包含时间筛选 + 分页排序)
|
- TableViewRequestDep: 分页排序查询依赖(包含时间筛选 + 分页排序)
|
||||||
|
- UserFilterParamsDep: 用户筛选参数依赖(用于管理员用户列表)
|
||||||
"""
|
"""
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Annotated, Literal, TypeAlias
|
from typing import Annotated, Literal, TypeAlias
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi import Depends, Query
|
from fastapi import Depends, Query
|
||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
from models.database import get_session
|
from models.database import get_session
|
||||||
from models.mixin import TimeFilterRequest, TableViewRequest
|
from models.mixin import TimeFilterRequest, TableViewRequest
|
||||||
|
from models.user import UserFilterParams, UserStatus
|
||||||
|
|
||||||
|
|
||||||
# --- 数据库会话依赖 ---
|
# --- 数据库会话依赖 ---
|
||||||
@@ -70,3 +73,24 @@ async def _get_table_view_queries(
|
|||||||
|
|
||||||
TableViewRequestDep: TypeAlias = Annotated[TableViewRequest, Depends(_get_table_view_queries)]
|
TableViewRequestDep: TypeAlias = Annotated[TableViewRequest, Depends(_get_table_view_queries)]
|
||||||
"""获取分页排序和时间筛选参数的依赖"""
|
"""获取分页排序和时间筛选参数的依赖"""
|
||||||
|
|
||||||
|
|
||||||
|
# --- 用户筛选依赖 ---
|
||||||
|
|
||||||
|
async def _get_user_filter_params(
|
||||||
|
group_id: Annotated[UUID | None, Query(description="按用户组UUID筛选")] = None,
|
||||||
|
username: Annotated[str | None, Query(max_length=50, description="按用户名模糊搜索")] = None,
|
||||||
|
nickname: Annotated[str | None, Query(max_length=50, description="按昵称模糊搜索")] = None,
|
||||||
|
status: Annotated[UserStatus | None, Query(description="按用户状态筛选")] = None,
|
||||||
|
) -> UserFilterParams:
|
||||||
|
"""解析用户过滤查询参数"""
|
||||||
|
return UserFilterParams(
|
||||||
|
group_id=group_id,
|
||||||
|
username_contains=username,
|
||||||
|
nickname_contains=nickname,
|
||||||
|
status=status,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
UserFilterParamsDep: TypeAlias = Annotated[UserFilterParams, Depends(_get_user_filter_params)]
|
||||||
|
"""获取用户筛选参数的依赖(用于管理员用户列表)"""
|
||||||
|
|||||||
202
models/README.md
202
models/README.md
@@ -8,10 +8,10 @@
|
|||||||
models/
|
models/
|
||||||
├── base/ # 基础模型类
|
├── base/ # 基础模型类
|
||||||
│ ├── __init__.py # 导出 SQLModelBase
|
│ ├── __init__.py # 导出 SQLModelBase
|
||||||
│ └── sqlmodel_base.py # SQLModelBase 基类(元类魔法)
|
│ └── sqlmodel_base.py # SQLModelBase 基类(自定义元类,支持联表继承)
|
||||||
├── mixin/ # Mixin 模块
|
├── mixin/ # Mixin 模块
|
||||||
│ ├── __init__.py # 统一导出
|
│ ├── __init__.py # 统一导出
|
||||||
│ ├── table.py # TableBaseMixin, UUIDTableBaseMixin(CRUD + 时间戳)
|
│ ├── table.py # TableBaseMixin, UUIDTableBaseMixin(CRUD + 时间戳 + 分页)
|
||||||
│ ├── polymorphic.py # 联表继承工具(create_subclass_id_mixin 等)
|
│ ├── polymorphic.py # 联表继承工具(create_subclass_id_mixin 等)
|
||||||
│ └── info_response.py # DTO 用的 id/时间戳 Mixin
|
│ └── info_response.py # DTO 用的 id/时间戳 Mixin
|
||||||
├── user.py # 用户模型
|
├── user.py # 用户模型
|
||||||
@@ -19,7 +19,7 @@ models/
|
|||||||
├── group.py # 用户组模型
|
├── group.py # 用户组模型
|
||||||
├── policy.py # 存储策略模型
|
├── policy.py # 存储策略模型
|
||||||
├── physical_file.py # 物理文件模型(文件去重)
|
├── physical_file.py # 物理文件模型(文件去重)
|
||||||
├── object.py # 统一对象模型(文件/目录)+ 上传会话
|
├── object.py # 统一对象模型(文件/目录)+ 上传会话 + 文件元数据
|
||||||
├── share.py # 分享模型
|
├── share.py # 分享模型
|
||||||
├── tag.py # 标签模型
|
├── tag.py # 标签模型
|
||||||
├── download.py # 离线下载任务
|
├── download.py # 离线下载任务
|
||||||
@@ -33,7 +33,8 @@ models/
|
|||||||
├── storage_pack.py # 容量包模型
|
├── storage_pack.py # 容量包模型
|
||||||
├── webdav.py # WebDAV 账户模型
|
├── webdav.py # WebDAV 账户模型
|
||||||
├── color.py # 主题颜色 DTO
|
├── color.py # 主题颜色 DTO
|
||||||
├── model_base.py # 响应基类 DTO
|
├── model_base.py # 响应基类 DTO(ResponseBase, MCP 等)
|
||||||
|
├── migration.py # 数据库初始化和迁移
|
||||||
└── database.py # 数据库连接配置
|
└── database.py # 数据库连接配置
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -43,9 +44,13 @@ models/
|
|||||||
|
|
||||||
### SQLModelBase
|
### SQLModelBase
|
||||||
|
|
||||||
所有模型的基类,位于 `models.base.sqlmodel_base`,配置了:
|
所有模型的基类,位于 `models.base.sqlmodel_base`,使用自定义元类 `__DeclarativeMeta` 实现:
|
||||||
- `use_attribute_docstrings=True`:使用属性后的 docstring 作为字段描述
|
- `use_attribute_docstrings=True`:使用属性后的 docstring 作为字段描述
|
||||||
- `validate_by_name=True`:允许按名称验证
|
- `validate_by_name=True`:允许按名称验证
|
||||||
|
- **自动设置 table=True**:继承 TableBaseMixin 的类自动成为数据库表
|
||||||
|
- **联表继承支持**:自动检测并处理 Joined Table Inheritance
|
||||||
|
- **多态支持**:支持 `polymorphic_on`, `polymorphic_identity` 等参数
|
||||||
|
- **Python 3.14 兼容**:包含针对 PEP 649 的兼容性修复
|
||||||
|
|
||||||
### TableBaseMixin
|
### TableBaseMixin
|
||||||
|
|
||||||
@@ -61,13 +66,18 @@ models/
|
|||||||
|
|
||||||
提供的 CRUD 方法:
|
提供的 CRUD 方法:
|
||||||
- `add()` - 新增记录(类方法)
|
- `add()` - 新增记录(类方法)
|
||||||
- `save()` - 保存实例
|
- `save()` - 保存实例(**必须使用返回值**)
|
||||||
- `update()` - 更新记录
|
- `update()` - 更新记录(**必须使用返回值**)
|
||||||
- `delete()` - 删除记录
|
- `delete()` - 删除记录
|
||||||
- `get()` - 查询记录(类方法)
|
- `get()` - 查询记录(类方法,支持分页、排序、时间筛选、多态加载)
|
||||||
- `get_with_count()` - 分页查询(类方法)
|
- `get_with_count()` - 分页查询并返回总数(类方法,返回 `ListResponse[T]`)
|
||||||
- `get_exist_one()` - 获取存在的记录(不存在则抛出 404)
|
- `get_exist_one()` - 获取存在的记录(不存在则抛出 404)
|
||||||
- `count()` - 统计记录数(类方法)
|
- `count()` - 统计记录数(类方法,支持时间筛选)
|
||||||
|
|
||||||
|
分页排序请求类:
|
||||||
|
- `TimeFilterRequest` - 时间筛选参数
|
||||||
|
- `PaginationRequest` - 分页排序参数
|
||||||
|
- `TableViewRequest` - 组合分页排序和时间筛选
|
||||||
|
|
||||||
**使用方式**:
|
**使用方式**:
|
||||||
```python
|
```python
|
||||||
@@ -104,6 +114,23 @@ class User(UserBase, UUIDTableBaseMixin): # 不需要再写 SQLModelBase
|
|||||||
password: str
|
password: str
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### ListResponse[T]
|
||||||
|
|
||||||
|
泛型分页响应类,用于所有 LIST 端点的标准化响应格式:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ListResponse(BaseModel, Generic[ItemT]):
|
||||||
|
count: int # 符合条件的记录总数
|
||||||
|
items: list[T] # 当前页的记录列表
|
||||||
|
```
|
||||||
|
|
||||||
|
**使用示例**:
|
||||||
|
```python
|
||||||
|
@router.get("", response_model=ListResponse[UserResponse])
|
||||||
|
async def list_users(session: SessionDep, table_view: TableViewRequestDep):
|
||||||
|
return await User.get_with_count(session, table_view=table_view)
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 数据库表模型
|
## 数据库表模型
|
||||||
@@ -118,10 +145,10 @@ class User(UserBase, UUIDTableBaseMixin): # 不需要再写 SQLModelBase
|
|||||||
| `id` | `UUID` | 用户 UUID(主键) |
|
| `id` | `UUID` | 用户 UUID(主键) |
|
||||||
| `username` | `str` | 用户名,唯一,不可更改 |
|
| `username` | `str` | 用户名,唯一,不可更改 |
|
||||||
| `nickname` | `str?` | 用户昵称 |
|
| `nickname` | `str?` | 用户昵称 |
|
||||||
| `password` | `str` | 密码(加密后) |
|
| `password` | `str` | 密码(Argon2 加密) |
|
||||||
| `status` | `UserStatus` | 用户状态:active/admin_banned/system_banned |
|
| `status` | `UserStatus` | 用户状态:active/admin_banned/system_banned |
|
||||||
| `storage` | `int` | 已用存储空间(字节) |
|
| `storage` | `int` | 已用存储空间(字节) |
|
||||||
| `two_factor` | `str?` | 两步验证密钥 |
|
| `two_factor` | `str?` | 两步验证密钥(TOTP) |
|
||||||
| `avatar` | `str` | 头像类型/地址 |
|
| `avatar` | `str` | 头像类型/地址 |
|
||||||
| `score` | `int` | 用户积分 |
|
| `score` | `int` | 用户积分 |
|
||||||
| `group_expires` | `datetime?` | 当前用户组过期时间 |
|
| `group_expires` | `datetime?` | 当前用户组过期时间 |
|
||||||
@@ -134,6 +161,8 @@ class User(UserBase, UUIDTableBaseMixin): # 不需要再写 SQLModelBase
|
|||||||
**关系**:
|
**关系**:
|
||||||
- `group`: 所属用户组
|
- `group`: 所属用户组
|
||||||
- `previous_group`: 之前的用户组(用于过期后恢复)
|
- `previous_group`: 之前的用户组(用于过期后恢复)
|
||||||
|
- `tags`: 用户的标签列表
|
||||||
|
- `authns`: 用户的 WebAuthn 凭证列表
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -327,12 +356,17 @@ class User(UserBase, UUIDTableBaseMixin): # 不需要再写 SQLModelBase
|
|||||||
- `children`: 子对象列表(自引用)
|
- `children`: 子对象列表(自引用)
|
||||||
- `source_links`: 源链接列表
|
- `source_links`: 源链接列表
|
||||||
- `shares`: 分享列表
|
- `shares`: 分享列表
|
||||||
|
- `policy`: 存储策略
|
||||||
|
|
||||||
**业务属性**:
|
**业务属性**:
|
||||||
- `source_name`: 向后兼容属性,返回物理文件的存储路径
|
- `source_name`: 向后兼容属性,返回物理文件的存储路径
|
||||||
- `is_file`: 是否为文件
|
- `is_file`: 是否为文件
|
||||||
- `is_folder`: 是否为目录
|
- `is_folder`: 是否为目录
|
||||||
|
|
||||||
|
**类方法**:
|
||||||
|
- `get_by_path()`: 根据路径获取对象
|
||||||
|
- `get_children()`: 获取子对象列表
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 10. FileMetadata(文件元数据)
|
### 10. FileMetadata(文件元数据)
|
||||||
@@ -1026,8 +1060,8 @@ class OrderStatus(StrEnum):
|
|||||||
|-----|------|
|
|-----|------|
|
||||||
| `LoginRequest` | 登录请求 |
|
| `LoginRequest` | 登录请求 |
|
||||||
| `RegisterRequest` | 注册请求 |
|
| `RegisterRequest` | 注册请求 |
|
||||||
| `TokenResponse` | 访问令牌响应 |
|
| `TokenResponse` | 访问令牌响应(access_token, refresh_token, expires_in) |
|
||||||
| `UserResponse` | 用户信息响应 |
|
| `UserResponse` | 用户信息响应(包含 group) |
|
||||||
| `UserPublic` | 用户公开信息 |
|
| `UserPublic` | 用户公开信息 |
|
||||||
| `UserSettingResponse` | 用户设置响应 |
|
| `UserSettingResponse` | 用户设置响应 |
|
||||||
| `WebAuthnInfo` | WebAuthn 信息 |
|
| `WebAuthnInfo` | WebAuthn 信息 |
|
||||||
@@ -1042,37 +1076,44 @@ class OrderStatus(StrEnum):
|
|||||||
|-----|------|
|
|-----|------|
|
||||||
| `GroupBase` | 用户组基础字段 |
|
| `GroupBase` | 用户组基础字段 |
|
||||||
| `GroupOptionsBase` | 用户组选项基础字段 |
|
| `GroupOptionsBase` | 用户组选项基础字段 |
|
||||||
| `GroupResponse` | 用户组响应 |
|
| `GroupAllOptionsBase` | 用户组所有选项基础字段 |
|
||||||
|
| `GroupResponse` | 用户组响应(包含 options) |
|
||||||
|
| `GroupCreateRequest` | 管理员创建用户组请求 |
|
||||||
|
| `GroupUpdateRequest` | 管理员更新用户组请求 |
|
||||||
|
| `GroupDetailResponse` | 管理员用户组详情响应 |
|
||||||
|
| `GroupListResponse` | 用户组列表响应 |
|
||||||
|
|
||||||
### 存储策略相关
|
### 存储策略相关
|
||||||
|
|
||||||
| DTO | 说明 |
|
| DTO | 说明 |
|
||||||
|-----|------|
|
|-----|------|
|
||||||
|
| `PolicyBase` | 存储策略基础字段 |
|
||||||
| `PolicyOptionsBase` | 存储策略选项基础字段 |
|
| `PolicyOptionsBase` | 存储策略选项基础字段 |
|
||||||
|
| `PolicyResponse` | 存储策略响应(id, name, type, max_size) |
|
||||||
|
| `PolicySummary` | 存储策略摘要 |
|
||||||
|
|
||||||
### 对象相关
|
### 对象相关
|
||||||
|
|
||||||
| DTO | 说明 |
|
| DTO | 说明 |
|
||||||
|-----|------|
|
|-----|------|
|
||||||
| `ObjectBase` | 对象基础字段 |
|
| `ObjectBase` | 对象基础字段 |
|
||||||
| `ObjectResponse` | 对象响应 |
|
| `ObjectResponse` | 对象响应(目录列表中的单个项) |
|
||||||
| `DirectoryCreateRequest` | 创建目录请求 |
|
| `DirectoryCreateRequest` | 创建目录请求(parent_id, name, policy_id?) |
|
||||||
| `DirectoryResponse` | 目录响应 |
|
| `DirectoryResponse` | 目录响应(id, parent, objects, policy) |
|
||||||
| `ObjectMoveRequest` | 移动对象请求 |
|
| `ObjectMoveRequest` | 移动对象请求(src_ids, dst_id) |
|
||||||
| `ObjectDeleteRequest` | 删除对象请求 |
|
| `ObjectDeleteRequest` | 删除对象请求(ids) |
|
||||||
| `ObjectCopyRequest` | 复制对象请求 |
|
| `ObjectCopyRequest` | 复制对象请求(src_ids, dst_id) |
|
||||||
| `ObjectRenameRequest` | 重命名对象请求 |
|
| `ObjectRenameRequest` | 重命名对象请求(id, new_name) |
|
||||||
| `ObjectPropertyResponse` | 对象基本属性响应 |
|
| `ObjectPropertyResponse` | 对象基本属性响应 |
|
||||||
| `ObjectPropertyDetailResponse` | 对象详细属性响应 |
|
| `ObjectPropertyDetailResponse` | 对象详细属性响应(含元数据、分享统计) |
|
||||||
| `PolicyResponse` | 存储策略响应 |
|
|
||||||
|
|
||||||
### 上传相关
|
### 上传相关
|
||||||
|
|
||||||
| DTO | 说明 |
|
| DTO | 说明 |
|
||||||
|-----|------|
|
|-----|------|
|
||||||
| `CreateUploadSessionRequest` | 创建上传会话请求 |
|
| `CreateUploadSessionRequest` | 创建上传会话请求(file_name, file_size, parent_id) |
|
||||||
| `UploadSessionResponse` | 上传会话响应 |
|
| `UploadSessionResponse` | 上传会话响应(id, chunk_size, total_chunks) |
|
||||||
| `UploadChunkResponse` | 上传分片响应 |
|
| `UploadChunkResponse` | 上传分片响应(uploaded_chunks, is_complete) |
|
||||||
| `CreateFileRequest` | 创建空白文件请求 |
|
| `CreateFileRequest` | 创建空白文件请求 |
|
||||||
|
|
||||||
### 管理员文件管理
|
### 管理员文件管理
|
||||||
@@ -1083,15 +1124,52 @@ class OrderStatus(StrEnum):
|
|||||||
| `FileBanRequest` | 文件封禁请求 |
|
| `FileBanRequest` | 文件封禁请求 |
|
||||||
| `AdminFileListResponse` | 管理员文件列表响应 |
|
| `AdminFileListResponse` | 管理员文件列表响应 |
|
||||||
|
|
||||||
|
### 管理员概况
|
||||||
|
|
||||||
|
| DTO | 说明 |
|
||||||
|
|-----|------|
|
||||||
|
| `MetricsSummary` | 统计摘要(日期列表、每日增量、总计) |
|
||||||
|
| `LicenseInfo` | 许可证信息 |
|
||||||
|
| `VersionInfo` | 版本信息 |
|
||||||
|
| `AdminSummaryData` | 管理员概况数据 |
|
||||||
|
| `AdminSummaryResponse` | 管理员概况响应 |
|
||||||
|
|
||||||
### 系统设置
|
### 系统设置
|
||||||
|
|
||||||
| DTO | 说明 |
|
| DTO | 说明 |
|
||||||
|-----|------|
|
|-----|------|
|
||||||
| `SiteConfigResponse` | 站点配置响应 |
|
| `SiteConfigResponse` | 站点配置响应 |
|
||||||
| `ThemeResponse` | 主题颜色响应 |
|
| `ThemeResponse` | 主题颜色响应 |
|
||||||
| `SettingItem` | 设置项 |
|
| `SettingItem` | 设置项(type, name, value) |
|
||||||
| `SettingsUpdateRequest` | 更新设置请求 |
|
| `SettingsListResponse` | 设置列表响应 |
|
||||||
| `SettingsGetResponse` | 获取设置响应 |
|
| `SettingsUpdateRequest` | 更新设置请求(settings[]) |
|
||||||
|
| `SettingsUpdateResponse` | 更新设置响应(updated, created) |
|
||||||
|
|
||||||
|
### 分享相关
|
||||||
|
|
||||||
|
| DTO | 说明 |
|
||||||
|
|-----|------|
|
||||||
|
| `ShareBase` | 分享基础字段 |
|
||||||
|
| `ShareCreateRequest` | 创建分享请求 |
|
||||||
|
| `ShareResponse` | 分享响应 |
|
||||||
|
| `AdminShareListItem` | 管理员分享列表项 |
|
||||||
|
|
||||||
|
### 任务相关
|
||||||
|
|
||||||
|
| DTO | 说明 |
|
||||||
|
|-----|------|
|
||||||
|
| `TaskPropsBase` | 任务属性基础字段 |
|
||||||
|
| `TaskSummary` | 任务摘要 |
|
||||||
|
|
||||||
|
### 通用响应
|
||||||
|
|
||||||
|
| DTO | 说明 |
|
||||||
|
|-----|------|
|
||||||
|
| `ResponseBase` | 通用响应基类(code, msg, data) |
|
||||||
|
| `ListResponse[T]` | 泛型分页响应(count, items) |
|
||||||
|
| `MCPBase` | MCP 基类 |
|
||||||
|
| `MCPRequestBase` | MCP 请求基类 |
|
||||||
|
| `MCPResponseBase` | MCP 响应基类 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1115,6 +1193,13 @@ objects = await Object.get(
|
|||||||
(Object.owner_id == user_id) & (Object.type == ObjectType.FILE),
|
(Object.owner_id == user_id) & (Object.type == ObjectType.FILE),
|
||||||
fetch_mode="all"
|
fetch_mode="all"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 分页查询并返回总数
|
||||||
|
from models.mixin import TableViewRequest, ListResponse
|
||||||
|
|
||||||
|
table_view = TableViewRequest(offset=0, limit=20, desc=True, order="created_at")
|
||||||
|
result: ListResponse[User] = await User.get_with_count(session, table_view=table_view)
|
||||||
|
print(f"总数: {result.count}, 当前页: {len(result.items)}")
|
||||||
```
|
```
|
||||||
|
|
||||||
### 创建文件对象
|
### 创建文件对象
|
||||||
@@ -1126,15 +1211,64 @@ file = Object(
|
|||||||
size=1024,
|
size=1024,
|
||||||
owner_id=user.id,
|
owner_id=user.id,
|
||||||
parent_id=folder.id,
|
parent_id=folder.id,
|
||||||
policy_id=policy.id
|
policy_id=policy.id,
|
||||||
|
physical_file_id=physical_file.id,
|
||||||
)
|
)
|
||||||
file = await file.save(session)
|
file = await file.save(session) # 必须使用返回值
|
||||||
```
|
```
|
||||||
|
|
||||||
### 多对多关系操作
|
### 多对多关系操作
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# 为用户组添加存储策略
|
# 为用户组添加存储策略
|
||||||
group.policies.append(policy)
|
from models import GroupPolicyLink
|
||||||
await group.save(session)
|
|
||||||
|
link = GroupPolicyLink(group_id=group.id, policy_id=policy.id)
|
||||||
|
session.add(link)
|
||||||
|
await session.commit()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 文件上传流程
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 1. 创建上传会话
|
||||||
|
upload_session = UploadSession(
|
||||||
|
file_name="large_file.zip",
|
||||||
|
file_size=104857600, # 100MB
|
||||||
|
chunk_size=52428800, # 50MB
|
||||||
|
total_chunks=2,
|
||||||
|
owner_id=user.id,
|
||||||
|
parent_id=folder.id,
|
||||||
|
policy_id=policy.id,
|
||||||
|
)
|
||||||
|
upload_session = await upload_session.save(session)
|
||||||
|
|
||||||
|
# 2. 上传分片后更新进度
|
||||||
|
upload_session.uploaded_chunks += 1
|
||||||
|
upload_session.uploaded_size += chunk_size
|
||||||
|
upload_session = await upload_session.save(session)
|
||||||
|
|
||||||
|
# 3. 检查是否完成
|
||||||
|
if upload_session.is_complete:
|
||||||
|
# 创建 PhysicalFile 和 Object 记录
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 文件引用计数(去重)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 复制文件时,只增加引用计数,不复制物理文件
|
||||||
|
if src.is_file and src.physical_file_id:
|
||||||
|
physical_file = await PhysicalFile.get(session, PhysicalFile.id == src.physical_file_id)
|
||||||
|
physical_file.increment_reference()
|
||||||
|
await physical_file.save(session)
|
||||||
|
|
||||||
|
# 删除文件时,减少引用计数
|
||||||
|
physical_file.decrement_reference()
|
||||||
|
if physical_file.can_be_deleted:
|
||||||
|
# 引用计数为0,可以删除物理文件
|
||||||
|
await storage_service.delete_file(physical_file.storage_path)
|
||||||
|
await PhysicalFile.delete(session, physical_file)
|
||||||
|
else:
|
||||||
|
await physical_file.save(session)
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ default_settings: list[Setting] = [
|
|||||||
Setting(name="siteURL", value="http://localhost", type=SettingsType.BASIC),
|
Setting(name="siteURL", value="http://localhost", type=SettingsType.BASIC),
|
||||||
Setting(name="siteName", value="DiskNext", type=SettingsType.BASIC),
|
Setting(name="siteName", value="DiskNext", type=SettingsType.BASIC),
|
||||||
Setting(name="register_enabled", value="1", type=SettingsType.REGISTER),
|
Setting(name="register_enabled", value="1", type=SettingsType.REGISTER),
|
||||||
Setting(name="default_group", value="", type=SettingsType.REGISTER), # UUID 在组创建后更新
|
Setting(name="default_group", value="", type=SettingsType.REGISTER),
|
||||||
Setting(name="siteKeywords", value="网盘,网盘", type=SettingsType.BASIC),
|
Setting(name="siteKeywords", value="网盘,网盘", type=SettingsType.BASIC),
|
||||||
Setting(name="siteDes", value="DiskNext", type=SettingsType.BASIC),
|
Setting(name="siteDes", value="DiskNext", type=SettingsType.BASIC),
|
||||||
Setting(name="siteTitle", value="云星启智", type=SettingsType.BASIC),
|
Setting(name="siteTitle", value="云星启智", type=SettingsType.BASIC),
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
from typing import Literal, TYPE_CHECKING
|
from typing import Literal, TYPE_CHECKING, TypeVar
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from sqlmodel import Field, Relationship
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from sqlalchemy import BinaryExpression, ClauseElement, and_
|
||||||
|
from sqlmodel import Field, Relationship
|
||||||
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
from sqlmodel.main import RelationshipInfo
|
||||||
|
|
||||||
from .base import SQLModelBase
|
from .base import SQLModelBase
|
||||||
from .model_base import ResponseBase
|
from .model_base import ResponseBase
|
||||||
from .mixin import UUIDTableBaseMixin
|
from .mixin import UUIDTableBaseMixin, TableViewRequest, ListResponse
|
||||||
|
|
||||||
|
T = TypeVar("T", bound="User")
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .group import Group
|
from .group import Group
|
||||||
@@ -44,6 +49,27 @@ class UserStatus(StrEnum):
|
|||||||
SYSTEM_BANNED = "system_banned"
|
SYSTEM_BANNED = "system_banned"
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 筛选参数 ====================
|
||||||
|
|
||||||
|
class UserFilterParams(SQLModelBase):
|
||||||
|
"""
|
||||||
|
用户过滤参数
|
||||||
|
|
||||||
|
用于管理员搜索用户列表,支持用户组、用户名、昵称、状态等过滤。
|
||||||
|
"""
|
||||||
|
group_id: UUID | None = None
|
||||||
|
"""按用户组UUID筛选"""
|
||||||
|
|
||||||
|
username_contains: str | None = Field(default=None, max_length=50)
|
||||||
|
"""用户名包含(不区分大小写的模糊搜索)"""
|
||||||
|
|
||||||
|
nickname_contains: str | None = Field(default=None, max_length=50)
|
||||||
|
"""昵称包含(不区分大小写的模糊搜索)"""
|
||||||
|
|
||||||
|
status: UserStatus | None = None
|
||||||
|
"""按用户状态筛选"""
|
||||||
|
|
||||||
|
|
||||||
# ==================== Base 模型 ====================
|
# ==================== Base 模型 ====================
|
||||||
|
|
||||||
class UserBase(SQLModelBase):
|
class UserBase(SQLModelBase):
|
||||||
@@ -165,7 +191,7 @@ class UserPublic(UserBase):
|
|||||||
id: UUID | None = None
|
id: UUID | None = None
|
||||||
"""用户UUID"""
|
"""用户UUID"""
|
||||||
|
|
||||||
nick: str | None = None
|
nickname: str | None = None
|
||||||
"""昵称"""
|
"""昵称"""
|
||||||
|
|
||||||
storage: int = 0
|
storage: int = 0
|
||||||
@@ -180,6 +206,9 @@ class UserPublic(UserBase):
|
|||||||
group_id: UUID | None = None
|
group_id: UUID | None = None
|
||||||
"""所属用户组UUID"""
|
"""所属用户组UUID"""
|
||||||
|
|
||||||
|
two_factor: str | None = None
|
||||||
|
"""两步验证密钥(32位字符串,null 表示未启用)"""
|
||||||
|
|
||||||
created_at: datetime | None = None
|
created_at: datetime | None = None
|
||||||
"""创建时间"""
|
"""创建时间"""
|
||||||
|
|
||||||
@@ -235,6 +264,9 @@ class UserAdminUpdateRequest(SQLModelBase):
|
|||||||
group_expires: datetime | None = None
|
group_expires: datetime | None = None
|
||||||
"""用户组过期时间"""
|
"""用户组过期时间"""
|
||||||
|
|
||||||
|
two_factor: str | None = None
|
||||||
|
"""两步验证密钥(32位字符串,传 null 可清除,不传则不修改)"""
|
||||||
|
|
||||||
|
|
||||||
class UserCalibrateResponse(SQLModelBase):
|
class UserCalibrateResponse(SQLModelBase):
|
||||||
"""用户存储校准响应 DTO"""
|
"""用户存储校准响应 DTO"""
|
||||||
@@ -396,3 +428,58 @@ class User(UserBase, UUIDTableBaseMixin):
|
|||||||
def to_public(self) -> "UserPublic":
|
def to_public(self) -> "UserPublic":
|
||||||
"""转换为公开 DTO,排除敏感字段"""
|
"""转换为公开 DTO,排除敏感字段"""
|
||||||
return UserPublic.model_validate(self)
|
return UserPublic.model_validate(self)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_with_count(
|
||||||
|
cls: type[T],
|
||||||
|
session: AsyncSession,
|
||||||
|
condition: BinaryExpression | ClauseElement | None = None,
|
||||||
|
*,
|
||||||
|
filter_params: 'UserFilterParams | None' = None,
|
||||||
|
join: type[T] | tuple[type[T], ClauseElement] | None = None,
|
||||||
|
options: list | None = None,
|
||||||
|
load: RelationshipInfo | None = None,
|
||||||
|
order_by: list[ClauseElement] | None = None,
|
||||||
|
filter: BinaryExpression | ClauseElement | None = None,
|
||||||
|
table_view: TableViewRequest | None = None,
|
||||||
|
) -> 'ListResponse[T]':
|
||||||
|
"""
|
||||||
|
获取分页用户列表及总数,支持用户过滤参数
|
||||||
|
|
||||||
|
:param filter_params: UserFilterParams 过滤参数对象(用户组、用户名、昵称、状态等)
|
||||||
|
:param 其他参数: 继承自 UUIDTableBaseMixin.get_with_count()
|
||||||
|
"""
|
||||||
|
# 构建过滤条件
|
||||||
|
merged_condition = condition
|
||||||
|
if filter_params is not None:
|
||||||
|
filter_conditions: list[BinaryExpression] = []
|
||||||
|
|
||||||
|
if filter_params.group_id is not None:
|
||||||
|
filter_conditions.append(cls.group_id == filter_params.group_id)
|
||||||
|
|
||||||
|
if filter_params.username_contains is not None:
|
||||||
|
filter_conditions.append(cls.username.ilike(f"%{filter_params.username_contains}%"))
|
||||||
|
|
||||||
|
if filter_params.nickname_contains is not None:
|
||||||
|
filter_conditions.append(cls.nickname.ilike(f"%{filter_params.nickname_contains}%"))
|
||||||
|
|
||||||
|
if filter_params.status is not None:
|
||||||
|
filter_conditions.append(cls.status == filter_params.status)
|
||||||
|
|
||||||
|
if filter_conditions:
|
||||||
|
combined_filter = and_(*filter_conditions)
|
||||||
|
if merged_condition is not None:
|
||||||
|
merged_condition = and_(merged_condition, combined_filter)
|
||||||
|
else:
|
||||||
|
merged_condition = combined_filter
|
||||||
|
|
||||||
|
return await super().get_with_count(
|
||||||
|
session,
|
||||||
|
merged_condition,
|
||||||
|
join=join,
|
||||||
|
options=options,
|
||||||
|
load=load,
|
||||||
|
order_by=order_by,
|
||||||
|
filter=filter,
|
||||||
|
table_view=table_view,
|
||||||
|
)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends, status
|
||||||
from loguru import logger as l
|
from loguru import logger as l
|
||||||
from sqlalchemy import and_
|
from sqlalchemy import and_
|
||||||
|
|
||||||
@@ -56,6 +56,23 @@ admin_aria2_router = APIRouter(
|
|||||||
tags=['admin', 'admin_aria2']
|
tags=['admin', 'admin_aria2']
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@admin_router.get(
|
||||||
|
path='/',
|
||||||
|
summary='自己是否为管理员',
|
||||||
|
dependencies=[Depends(admin_required)],
|
||||||
|
status_code=status.HTTP_204_NO_CONTENT
|
||||||
|
)
|
||||||
|
async def is_admin() -> None:
|
||||||
|
"""
|
||||||
|
检查当前用户是否具有管理员权限。
|
||||||
|
|
||||||
|
如果用户是管理员,则返回 204 No Content 响应;否则返回 403 Forbidden 错误。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None: 无内容响应
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
@admin_router.get(
|
@admin_router.get(
|
||||||
path='/summary',
|
path='/summary',
|
||||||
summary='获取站点概况',
|
summary='获取站点概况',
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ from loguru import logger as l
|
|||||||
from sqlalchemy import func, and_
|
from sqlalchemy import func, and_
|
||||||
|
|
||||||
from middleware.auth import admin_required
|
from middleware.auth import admin_required
|
||||||
from middleware.dependencies import SessionDep, TableViewRequestDep
|
from middleware.dependencies import SessionDep, TableViewRequestDep, UserFilterParamsDep
|
||||||
from models import (
|
from models import (
|
||||||
User, ResponseBase, UserPublic, ListResponse,
|
User, ResponseBase, UserPublic, ListResponse,
|
||||||
Group, Object, ObjectType, )
|
Group, Object, ObjectType, )
|
||||||
from models.user import (
|
from models.user import (
|
||||||
UserAdminUpdateRequest, UserCalibrateResponse,
|
UserAdminUpdateRequest, UserCalibrateResponse,
|
||||||
)
|
)
|
||||||
from utils import Password
|
from utils import Password, http_exceptions
|
||||||
|
|
||||||
admin_user_router = APIRouter(
|
admin_user_router = APIRouter(
|
||||||
prefix="/user",
|
prefix="/user",
|
||||||
@@ -50,15 +50,17 @@ async def router_admin_get_user(session: SessionDep, user_id: int) -> ResponseBa
|
|||||||
async def router_admin_get_users(
|
async def router_admin_get_users(
|
||||||
session: SessionDep,
|
session: SessionDep,
|
||||||
table_view: TableViewRequestDep,
|
table_view: TableViewRequestDep,
|
||||||
|
filter_params: UserFilterParamsDep,
|
||||||
) -> ListResponse[UserPublic]:
|
) -> ListResponse[UserPublic]:
|
||||||
"""
|
"""
|
||||||
获取用户列表,支持分页、排序和时间筛选。
|
获取用户列表,支持分页、排序、时间筛选和用户筛选。
|
||||||
|
|
||||||
:param session: 数据库会话依赖项
|
:param session: 数据库会话依赖项
|
||||||
:param table_view: 分页排序参数依赖
|
:param table_view: 分页排序参数依赖
|
||||||
|
:param filter_params: 用户筛选参数(用户组、用户名、昵称、状态)
|
||||||
:return: 分页用户列表
|
:return: 分页用户列表
|
||||||
"""
|
"""
|
||||||
result = await User.get_with_count(session, table_view=table_view)
|
result = await User.get_with_count(session, filter_params=filter_params, table_view=table_view)
|
||||||
return ListResponse(
|
return ListResponse(
|
||||||
items=[user.to_public() for user in result.items],
|
items=[user.to_public() for user in result.items],
|
||||||
count=result.count,
|
count=result.count,
|
||||||
@@ -114,6 +116,10 @@ async def router_admin_update_user(
|
|||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(status_code=404, detail="用户不存在")
|
raise HTTPException(status_code=404, detail="用户不存在")
|
||||||
|
|
||||||
|
# 默认管理员(用户名为 admin)不允许更改用户组
|
||||||
|
if request.group_id and user.username == "admin" and request.group_id != user.group_id:
|
||||||
|
http_exceptions.raise_forbidden("默认管理员不允许更改用户组")
|
||||||
|
|
||||||
# 如果更新用户组,验证新组存在
|
# 如果更新用户组,验证新组存在
|
||||||
if request.group_id:
|
if request.group_id:
|
||||||
group = await Group.get(session, Group.id == request.group_id)
|
group = await Group.get(session, Group.id == request.group_id)
|
||||||
@@ -127,6 +133,11 @@ async def router_admin_update_user(
|
|||||||
elif 'password' in update_data:
|
elif 'password' in update_data:
|
||||||
del update_data['password'] # 空密码不更新
|
del update_data['password'] # 空密码不更新
|
||||||
|
|
||||||
|
# 验证两步验证密钥格式(如果提供了值且不为 None,长度必须为 32)
|
||||||
|
if 'two_factor' in update_data and update_data['two_factor'] is not None:
|
||||||
|
if len(update_data['two_factor']) != 32:
|
||||||
|
raise HTTPException(status_code=400, detail="两步验证密钥必须为32位字符串")
|
||||||
|
|
||||||
# 更新字段
|
# 更新字段
|
||||||
for key, value in update_data.items():
|
for key, value in update_data.items():
|
||||||
setattr(user, key, value)
|
setattr(user, key, value)
|
||||||
|
|||||||
@@ -260,7 +260,7 @@ def router_user_avatar(id: str, size: int = 128) -> models.ResponseBase:
|
|||||||
summary='获取用户信息',
|
summary='获取用户信息',
|
||||||
description='Get user information.',
|
description='Get user information.',
|
||||||
dependencies=[Depends(dependency=auth_required)],
|
dependencies=[Depends(dependency=auth_required)],
|
||||||
response_model=models.ResponseBase,
|
response_model=models.UserResponse,
|
||||||
)
|
)
|
||||||
async def router_user_me(
|
async def router_user_me(
|
||||||
session: SessionDep,
|
session: SessionDep,
|
||||||
@@ -285,7 +285,7 @@ async def router_user_me(
|
|||||||
# 异步加载 tags 关系
|
# 异步加载 tags 关系
|
||||||
user_tags = await user.awaitable_attrs.tags
|
user_tags = await user.awaitable_attrs.tags
|
||||||
|
|
||||||
user_response = models.UserResponse(
|
return models.UserResponse(
|
||||||
id=user.id,
|
id=user.id,
|
||||||
username=user.username,
|
username=user.username,
|
||||||
status=user.status,
|
status=user.status,
|
||||||
@@ -297,8 +297,6 @@ async def router_user_me(
|
|||||||
tags=[tag.name for tag in user_tags] if user_tags else [],
|
tags=[tag.name for tag in user_tags] if user_tags else [],
|
||||||
)
|
)
|
||||||
|
|
||||||
return models.ResponseBase(data=user_response.model_dump())
|
|
||||||
|
|
||||||
@user_router.get(
|
@user_router.get(
|
||||||
path='/storage',
|
path='/storage',
|
||||||
summary='存储信息',
|
summary='存储信息',
|
||||||
|
|||||||
@@ -16,21 +16,21 @@ def raise_bad_request(*args, **kwargs) -> NoReturn:
|
|||||||
"""Raises an HTTP 400 Bad Request exception."""
|
"""Raises an HTTP 400 Bad Request exception."""
|
||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, *args, **kwargs)
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, *args, **kwargs)
|
||||||
|
|
||||||
def raise_unauthorized(*args, **kwargs) -> NoReturn:
|
def raise_unauthorized(detail: str | None = None, *args, **kwargs) -> NoReturn:
|
||||||
"""Raises an HTTP 401 Unauthorized exception."""
|
"""Raises an HTTP 401 Unauthorized exception."""
|
||||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, *args, **kwargs)
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=detail, *args, **kwargs)
|
||||||
|
|
||||||
def raise_insufficient_quota(detail: str = "积分不足,请充值", *args, **kwargs) -> NoReturn:
|
def raise_insufficient_quota(detail: str | None = None, *args, **kwargs) -> NoReturn:
|
||||||
"""Raises an HTTP 402 Payment Required exception."""
|
"""Raises an HTTP 402 Payment Required exception."""
|
||||||
raise HTTPException(status_code=status.HTTP_402_PAYMENT_REQUIRED, detail=detail, *args, **kwargs)
|
raise HTTPException(status_code=status.HTTP_402_PAYMENT_REQUIRED, detail=detail, *args, **kwargs)
|
||||||
|
|
||||||
def raise_forbidden(*args, **kwargs) -> NoReturn:
|
def raise_forbidden(detail: str | None = None, *args, **kwargs) -> NoReturn:
|
||||||
"""Raises an HTTP 403 Forbidden exception."""
|
"""Raises an HTTP 403 Forbidden exception."""
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, *args, **kwargs)
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=detail, *args, **kwargs)
|
||||||
|
|
||||||
def raise_not_found(*args, **kwargs) -> NoReturn:
|
def raise_not_found(detail: str | None = None, *args, **kwargs) -> NoReturn:
|
||||||
"""Raises an HTTP 404 Not Found exception."""
|
"""Raises an HTTP 404 Not Found exception."""
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, *args, **kwargs)
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=detail, *args, **kwargs)
|
||||||
|
|
||||||
def raise_conflict(*args, **kwargs) -> NoReturn:
|
def raise_conflict(*args, **kwargs) -> NoReturn:
|
||||||
"""Raises an HTTP 409 Conflict exception."""
|
"""Raises an HTTP 409 Conflict exception."""
|
||||||
|
|||||||
Reference in New Issue
Block a user