feat: Enhance models and README with additional features and improvements

- Updated README to include KodBox in project vision.
- Added model descriptions for clarity in Download, File, Folder, Group, Node, Order, Policy, Redeem, Report, Request, Response, Setting, Share, SourceLink, StoragePack, Tag, Task, User, and WebDAV.
- Changed optional fields from Optional[...] to the new union type syntax (e.g., str | None).
- Improved foreign key references in models for consistency.
- Refactored relationships in models to use singular forms where appropriate.
- Updated login service to reflect changes in request model types.
This commit is contained in:
2025-11-27 21:22:40 +08:00
parent 1533d9e89c
commit b364b740ca
21 changed files with 156 additions and 147 deletions

View File

@@ -8,7 +8,7 @@
本来此项目并没有这么快开始,但由于我曾经使用过的某个类似产品的作者吃相实在难看,故决定自己实现一个。 本来此项目并没有这么快开始,但由于我曾经使用过的某个类似产品的作者吃相实在难看,故决定自己实现一个。
此项目的愿景是集百家之长Cloudreve + Alist + FnOS。你也可以考虑付费支持我们的发展 -> `DiskNext Pro`. 此项目的愿景是集百家之长Cloudreve + Alist + FnOS + KodBox)。你也可以考虑付费支持我们的发展 -> `DiskNext Pro`.
目前正处于 `OMEGA` 实验阶段,比 `Alpha` 版还更早期,仅供测试。 目前正处于 `OMEGA` 实验阶段,比 `Alpha` 版还更早期,仅供测试。
@@ -25,12 +25,12 @@
- 在线预览/编辑多种文件包括但不限于视频、图片、音频、PDF、ePub、Office、Markdown、图表等 - 在线预览/编辑多种文件包括但不限于视频、图片、音频、PDF、ePub、Office、Markdown、图表等
- 自定义主题色、深浅色主题、PWA、i18n - 自定义主题色、深浅色主题、PWA、i18n
## :alembic: 技术栈 ## ⚗️ 技术栈
* [Python](https://www.python.org/) + [FastAPI](https://fastapi.tiangolo.com/) * [Python](https://www.python.org/) + [FastAPI](https://fastapi.tiangolo.com/)
<!-- * [React](https://github.com/facebook/react) + [Redux](https://github.com/reduxjs/redux) + [Material-UI](https://github.com/mui-org/material-ui) --> <!-- * [React](https://github.com/facebook/react) + [Redux](https://github.com/reduxjs/redux) + [Material-UI](https://github.com/mui-org/material-ui) -->
## :scroll: 许可证 ## 📜 许可证
GPL V3 GPL V3

View File

@@ -11,7 +11,8 @@ class DownloadBase(SQLModelBase):
pass pass
class Download(DownloadBase, UUIDTableBase, table=True): class Download(DownloadBase, UUIDTableBase, table=True):
__tablename__ = 'downloads' """离线下载任务模型"""
__table_args__ = ( __table_args__ = (
UniqueConstraint("node_id", "g_id", name="uq_download_node_gid"), UniqueConstraint("node_id", "g_id", name="uq_download_node_gid"),
) )
@@ -21,18 +22,18 @@ class Download(DownloadBase, UUIDTableBase, table=True):
source: str = Field(description="来源URL或标识") source: str = Field(description="来源URL或标识")
total_size: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="总大小(字节)") total_size: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="总大小(字节)")
downloaded_size: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="已下载大小(字节)") downloaded_size: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="已下载大小(字节)")
g_id: Optional[str] = Field(default=None, index=True, description="Aria2 GID") g_id: str | None = Field(default=None, index=True, description="Aria2 GID")
speed: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="下载速度 (bytes/s)") speed: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="下载速度 (bytes/s)")
parent: Optional[str] = Field(default=None, description="父任务标识") parent: str | None = Field(default=None, description="父任务标识")
attrs: Optional[str] = Field(default=None, description="额外属性 (JSON格式)") attrs: str | None = Field(default=None, description="额外属性 (JSON格式)")
# attrs 示例: {"gid":"65c5faf38374cc63","status":"removed","totalLength":"0","completedLength":"0","uploadLength":"0","bitfield":"","downloadSpeed":"0","uploadSpeed":"0","infoHash":"ca159db2b1e78f6e95fd972be72251f967f639d4","numSeeders":"0","seeder":"","pieceLength":"16384","numPieces":"0","connections":"0","errorCode":"31","errorMessage":"","followedBy":null,"belongsTo":"","dir":"/data/ccaaDown/aria2/7a208304-9126-46d2-ba47-a6959f236a07","files":[{"index":"1","path":"[METADATA]zh-cn_windows_11_consumer_editions_version_21h2_updated_aug_2022_x64_dvd_a29983d5.iso","length":"0","completedLength":"0","selected":"true","uris":[]}],"bittorrent":{"announceList":[["udp://tracker.opentrackr.org:1337/announce"],["udp://9.rarbg.com:2810/announce"],["udp://tracker.openbittorrent.com:6969/announce"],["https://opentracker.i2p.rocks:443/announce"],["http://tracker.openbittorrent.com:80/announce"],["udp://open.stealth.si:80/announce"],["udp://tracker.torrent.eu.org:451/announce"],["udp://exodus.desync.com:6969/announce"],["udp://tracker.tiny-vps.com:6969/announce"],["udp://tracker.pomf.se:80/announce"],["udp://tracker.moeking.me:6969/announce"],["udp://tracker.dler.org:6969/announce"],["udp://open.demonii.com:1337/announce"],["udp://explodie.org:6969/announce"],["udp://chouchou.top:8080/announce"],["udp://bt.oiyo.tk:6969/announce"],["https://tracker.nanoha.org:443/announce"],["https://tracker.lilithraws.org:443/announce"],["http://tracker3.ctix.cn:8080/announce"],["http://tracker.nucozer-tracker.ml:2710/announce"]],"comment":"","creationDate":0,"mode":"","info":{"name":""}}} # attrs 示例: {"gid":"65c5faf38374cc63","status":"removed","totalLength":"0","completedLength":"0","uploadLength":"0","bitfield":"","downloadSpeed":"0","uploadSpeed":"0","infoHash":"ca159db2b1e78f6e95fd972be72251f967f639d4","numSeeders":"0","seeder":"","pieceLength":"16384","numPieces":"0","connections":"0","errorCode":"31","errorMessage":"","followedBy":null,"belongsTo":"","dir":"/data/ccaaDown/aria2/7a208304-9126-46d2-ba47-a6959f236a07","files":[{"index":"1","path":"[METADATA]zh-cn_windows_11_consumer_editions_version_21h2_updated_aug_2022_x64_dvd_a29983d5.iso","length":"0","completedLength":"0","selected":"true","uris":[]}],"bittorrent":{"announceList":[["udp://tracker.opentrackr.org:1337/announce"],["udp://9.rarbg.com:2810/announce"],["udp://tracker.openbittorrent.com:6969/announce"],["https://opentracker.i2p.rocks:443/announce"],["http://tracker.openbittorrent.com:80/announce"],["udp://open.stealth.si:80/announce"],["udp://tracker.torrent.eu.org:451/announce"],["udp://exodus.desync.com:6969/announce"],["udp://tracker.tiny-vps.com:6969/announce"],["udp://tracker.pomf.se:80/announce"],["udp://tracker.moeking.me:6969/announce"],["udp://tracker.dler.org:6969/announce"],["udp://open.demonii.com:1337/announce"],["udp://explodie.org:6969/announce"],["udp://chouchou.top:8080/announce"],["udp://bt.oiyo.tk:6969/announce"],["https://tracker.nanoha.org:443/announce"],["https://tracker.lilithraws.org:443/announce"],["http://tracker3.ctix.cn:8080/announce"],["http://tracker.nucozer-tracker.ml:2710/announce"]],"comment":"","creationDate":0,"mode":"","info":{"name":""}}}
error: Optional[str] = Field(default=None, description="错误信息") error: str | None = Field(default=None, description="错误信息")
dst: str = Field(description="目标存储路径") dst: str = Field(description="目标存储路径")
# 外键 # 外键
user_id: int = Field(foreign_key="users.id", index=True, description="所属用户ID") user_id: int = Field(foreign_key="user.id", index=True, description="所属用户ID")
task_id: Optional[int] = Field(default=None, foreign_key="tasks.id", index=True, description="关联的任务ID") task_id: int | None = Field(default=None, foreign_key="task.id", index=True, description="关联的任务ID")
node_id: int = Field(foreign_key="nodes.id", index=True, description="执行下载的节点ID") node_id: int = Field(foreign_key="node.id", index=True, description="执行下载的节点ID")
# 关系 # 关系
user: "User" = Relationship(back_populates="downloads") user: "User" = Relationship(back_populates="downloads")

View File

@@ -1,8 +1,7 @@
from typing import Optional, TYPE_CHECKING from typing import TYPE_CHECKING
from sqlmodel import Field, Relationship, UniqueConstraint, CheckConstraint, Index from sqlmodel import Field, Relationship, UniqueConstraint, CheckConstraint, Index
from .base import TableBase from .base import TableBase
from datetime import datetime
if TYPE_CHECKING: if TYPE_CHECKING:
from .user import User from .user import User
@@ -11,7 +10,6 @@ if TYPE_CHECKING:
from .source_link import SourceLink from .source_link import SourceLink
class File(TableBase, table=True): class File(TableBase, table=True):
__tablename__ = 'files'
__table_args__ = ( __table_args__ = (
UniqueConstraint("folder_id", "name", name="uq_file_folder_name_active"), UniqueConstraint("folder_id", "name", name="uq_file_folder_name_active"),
CheckConstraint("name NOT LIKE '%/%' AND name NOT LIKE '%\\%'", name="ck_file_name_no_slash"), CheckConstraint("name NOT LIKE '%/%' AND name NOT LIKE '%\\%'", name="ck_file_name_no_slash"),
@@ -21,18 +19,18 @@ class File(TableBase, table=True):
) )
name: str = Field(max_length=255, description="文件名") name: str = Field(max_length=255, description="文件名")
source_name: Optional[str] = Field(default=None, description="源文件名") source_name: str | None = Field(default=None, description="源文件名")
size: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="文件大小(字节)") size: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="文件大小(字节)")
upload_session_id: Optional[str] = Field(default=None, max_length=255, unique=True, index=True, description="分块上传会话ID") upload_session_id: str | None = Field(default=None, max_length=255, unique=True, index=True, description="分块上传会话ID")
file_metadata: Optional[str] = Field(default=None, description="文件元数据 (JSON格式)") # 后续可以考虑模型继承? file_metadata: str | None = Field(default=None, description="文件元数据 (JSON格式)") # 后续可以考虑模型继承?
# 外键 # 外键
user_id: int = Field(foreign_key="users.id", index=True, description="所属用户ID") user_id: int = Field(foreign_key="user.id", index=True, description="所属用户ID")
folder_id: int = Field(foreign_key="folders.id", index=True, description="所在目录ID") folder_id: int = Field(foreign_key="folder.id", index=True, description="所在目录ID")
policy_id: int = Field(foreign_key="policies.id", index=True, description="所属存储策略ID") policy_id: int = Field(foreign_key="policy.id", index=True, description="所属存储策略ID")
# 关系 # 关系
user: list["User"] = Relationship(back_populates="files") user: "User" = Relationship(back_populates="files")
folder: list["Folder"] = Relationship(back_populates="files") folder: "Folder" = Relationship(back_populates="files")
policy: list["Policy"] = Relationship(back_populates="files") policy: "Policy" = Relationship(back_populates="files")
source_links: list["SourceLink"] = Relationship(back_populates="file") source_links: list["SourceLink"] = Relationship(back_populates="file")

View File

@@ -10,7 +10,6 @@ if TYPE_CHECKING:
from .file import File from .file import File
class Folder(TableBase, table=True): class Folder(TableBase, table=True):
__tablename__ = 'folders'
__table_args__ = ( __table_args__ = (
UniqueConstraint( UniqueConstraint(
"owner_id", "owner_id",
@@ -27,9 +26,9 @@ class Folder(TableBase, table=True):
name: str = Field(max_length=255, nullable=False, description="目录名") name: str = Field(max_length=255, nullable=False, description="目录名")
# 外键 # 外键
parent_id: Optional[int] = Field(default=None, foreign_key="folders.id", index=True, description="父目录ID") parent_id: int | None = Field(default=None, foreign_key="folder.id", index=True, description="父目录ID")
owner_id: int = Field(foreign_key="users.id", index=True, description="所有者用户ID") owner_id: int = Field(foreign_key="user.id", index=True, description="所有者用户ID")
policy_id: int = Field(foreign_key="policies.id", index=True, description="所属存储策略ID") policy_id: int = Field(foreign_key="policy.id", index=True, description="所属存储策略ID")
# 关系 # 关系
owner: "User" = Relationship(back_populates="folders") owner: "User" = Relationship(back_populates="folders")

View File

@@ -8,24 +8,24 @@ if TYPE_CHECKING:
from .user import User from .user import User
class GroupOptions(SQLModel): class GroupOptions(SQLModel):
archive_download: Optional[bool] = False archive_download: bool | None = False
archive_task: Optional[bool] = False archive_task: bool | None = False
share_download: Optional[bool] = False share_download: bool | None = False
share_free: Optional[bool] = False share_free: bool | None = False
webdav_proxy: Optional[bool] = False webdav_proxy: bool | None = False
aria2: Optional[bool] = False aria2: bool | None = False
relocate: Optional[bool] = False relocate: bool | None = False
source_batch: Optional[int] = 10 source_batch: int | None = 10
redirected_source: Optional[bool] = False redirected_source: bool | None = False
available_nodes: Optional[List[int]] = [] available_nodes: List[int] | None = []
select_node: Optional[bool] = False select_node: bool | None = False
advance_delete: Optional[bool] = False advance_delete: bool | None = False
class Group(TableBase, table=True): class Group(TableBase, table=True):
__tablename__ = 'groups' """用户组模型"""
name: str = Field(max_length=255, unique=True, description="用户组名") name: str = Field(max_length=255, unique=True, description="用户组名")
policies: Optional[str] = Field(default=None, max_length=255, description="允许的策略ID列表逗号分隔") policies: str | None = Field(default=None, max_length=255, description="允许的策略ID列表逗号分隔")
max_storage: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="最大存储空间(字节)") max_storage: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="最大存储空间(字节)")
share_enabled: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")}, description="是否允许创建分享") share_enabled: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")}, description="是否允许创建分享")
web_dav_enabled: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")}, description="是否允许使用WebDAV") web_dav_enabled: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")}, description="是否允许使用WebDAV")
@@ -34,11 +34,11 @@ class Group(TableBase, table=True):
options: GroupOptions = Field(default=GroupOptions, sa_column=Column(JSON), description="其他选项") options: GroupOptions = Field(default=GroupOptions, sa_column=Column(JSON), description="其他选项")
# 关系:一个组可以有多个用户 # 关系:一个组可以有多个用户
users: List["User"] = Relationship( user: List["User"] = Relationship(
back_populates="group", back_populates="group",
sa_relationship_kwargs={"foreign_keys": "User.group_id"} sa_relationship_kwargs={"foreign_keys": "User.group_id"}
) )
previous_users: List["User"] = Relationship( previous_user: List["User"] = Relationship(
back_populates="previous_group", back_populates="previous_group",
sa_relationship_kwargs={"foreign_keys": "User.previous_group_id"} sa_relationship_kwargs={"foreign_keys": "User.previous_group_id"}
) )
@@ -46,13 +46,13 @@ class Group(TableBase, table=True):
@staticmethod @staticmethod
async def create( async def create(
group: Optional["Group"] = None, group: Optional["Group"] = None,
name: Optional[str] = None, name: str | None = None,
policies: Optional[str] = None, policies: str | None = None,
max_storage: int = 0, max_storage: int = 0,
share_enabled: bool = False, share_enabled: bool = False,
web_dav_enabled: bool = False, web_dav_enabled: bool = False,
speed_limit: int = 0, speed_limit: int = 0,
options: Optional[dict] = None, options: dict | None = None,
) -> "Group": ) -> "Group":
""" """
向数据库内添加用户组。如果提供了 `group` 参数,则使用该对象,否则创建一个新的用户组对象。 向数据库内添加用户组。如果提供了 `group` 参数,则使用该对象,否则创建一个新的用户组对象。
@@ -128,13 +128,13 @@ class Group(TableBase, table=True):
@staticmethod @staticmethod
async def set( async def set(
id: int, id: int,
name: Optional[str] = None, name: str | None = None,
policies: Optional[str] = None, policies: str | None = None,
max_storage: Optional[int] = None, max_storage: int | None = None,
share_enabled: Optional[bool] = None, share_enabled: bool | None = None,
web_dav_enabled: Optional[bool] = None, web_dav_enabled: bool | None = None,
speed_limit: Optional[int] = None, speed_limit: int | None = None,
options: Optional[str] = None options: str | None = None
) -> Optional["Group"]: ) -> Optional["Group"]:
""" """
更新用户组信息。 更新用户组信息。
@@ -142,19 +142,19 @@ class Group(TableBase, table=True):
:param id: 用户组ID :param id: 用户组ID
:type id: int :type id: int
:param name: 用户组名 :param name: 用户组名
:type name: Optional[str] :type name: str | None
:param policies: 允许的策略ID列表逗号分隔 :param policies: 允许的策略ID列表逗号分隔
:type policies: Optional[str] :type policies: str | None
:param max_storage: 最大存储空间(字节) :param max_storage: 最大存储空间(字节)
:type max_storage: Optional[int] :type max_storage: int | None
:param share_enabled: 是否允许创建分享 :param share_enabled: 是否允许创建分享
:type share_enabled: Optional[bool] :type share_enabled: bool | None
:param web_dav_enabled: 是否允许使用WebDAV :param web_dav_enabled: 是否允许使用WebDAV
:type web_dav_enabled: Optional[bool] :type web_dav_enabled: bool | None
:param speed_limit: 速度限制 (KB/s), 0为不限制 :param speed_limit: 速度限制 (KB/s), 0为不限制
:type speed_limit: Optional[int] :type speed_limit: int | None
:param options: 其他选项 (JSON格式) :param options: 其他选项 (JSON格式)
:type options: Optional[str] :type options: str | None
:return: 更新后的用户组对象或 None :return: 更新后的用户组对象或 None
:rtype: Optional[Group] :rtype: Optional[Group]
""" """

View File

@@ -1,23 +1,22 @@
from typing import Optional, TYPE_CHECKING from typing import TYPE_CHECKING
from sqlmodel import Field, Relationship, text, Column, func, DateTime from sqlmodel import Field, Relationship, text
from .base import TableBase from .base import TableBase
from datetime import datetime
if TYPE_CHECKING: if TYPE_CHECKING:
from .download import Download from .download import Download
class Node(TableBase, table=True): class Node(TableBase, table=True):
__tablename__ = 'nodes' """节点模型"""
status: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="节点状态: 0=正常, 1=离线") status: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="节点状态: 0=正常, 1=离线")
name: str = Field(max_length=255, unique=True, description="节点名称") name: str = Field(max_length=255, unique=True, description="节点名称")
type: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="节点类型") type: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="节点类型")
server: str = Field(max_length=255, description="节点地址IP或域名") server: str = Field(max_length=255, description="节点地址IP或域名")
slave_key: Optional[str] = Field(default=None, description="从机通讯密钥") slave_key: str | None = Field(default=None, description="从机通讯密钥")
master_key: Optional[str] = Field(default=None, description="主机通讯密钥") master_key: str | None = Field(default=None, description="主机通讯密钥")
aria2_enabled: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")}, description="是否启用Aria2") aria2_enabled: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")}, description="是否启用Aria2")
aria2_options: Optional[str] = Field(default=None, description="Aria2配置 (JSON格式)") aria2_options: str | None = Field(default=None, description="Aria2配置 (JSON格式)")
rank: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="节点排序权重") rank: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="节点排序权重")
# 关系 # 关系

View File

@@ -1,26 +1,25 @@
from typing import Optional, TYPE_CHECKING from typing import TYPE_CHECKING
from sqlmodel import Field, Relationship, Column, func, DateTime from sqlmodel import Field, Relationship
from .base import TableBase from .base import TableBase
from datetime import datetime
if TYPE_CHECKING: if TYPE_CHECKING:
from .user import User from .user import User
class Order(TableBase, table=True): class Order(TableBase, table=True):
__tablename__ = 'orders' """订单模型"""
order_no: str = Field(max_length=255, unique=True, index=True, description="订单号,唯一") order_no: str = Field(max_length=255, unique=True, index=True, description="订单号,唯一")
type: int = Field(description="订单类型") type: int = Field(description="订单类型")
method: Optional[str] = Field(default=None, max_length=255, description="支付方式") method: str | None = Field(default=None, max_length=255, description="支付方式")
product_id: Optional[int] = Field(default=None, description="商品ID") product_id: int | None = Field(default=None, description="商品ID")
num: int = Field(default=1, sa_column_kwargs={"server_default": "1"}, description="购买数量") num: int = Field(default=1, sa_column_kwargs={"server_default": "1"}, description="购买数量")
name: str = Field(max_length=255, description="商品名称") name: str = Field(max_length=255, description="商品名称")
price: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="订单价格(分)") price: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="订单价格(分)")
status: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="订单状态: 0=待支付, 1=已完成, 2=已取消") status: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="订单状态: 0=待支付, 1=已完成, 2=已取消")
# 外键 # 外键
user_id: int = Field(foreign_key="users.id", index=True, description="所属用户ID") user_id: int = Field(foreign_key="user.id", index=True, description="所属用户ID")
# 关系 # 关系
user: "User" = Relationship(back_populates="orders") user: "User" = Relationship(back_populates="orders")

View File

@@ -1,30 +1,29 @@
from typing import Optional, List, TYPE_CHECKING from typing import Optional, List, TYPE_CHECKING
from sqlmodel import Field, Relationship, text, Column, func, DateTime from sqlmodel import Field, Relationship, text
from .base import TableBase from .base import TableBase
from datetime import datetime
if TYPE_CHECKING: if TYPE_CHECKING:
from .file import File from .file import File
from .folder import Folder from .folder import Folder
class Policy(TableBase, table=True): class Policy(TableBase, table=True):
__tablename__ = 'policies' """存储策略模型"""
name: str = Field(max_length=255, unique=True, description="策略名称") name: str = Field(max_length=255, unique=True, description="策略名称")
type: str = Field(max_length=255, description="存储类型 (e.g. 'local', 's3')") type: str = Field(max_length=255, description="存储类型 (e.g. 'local', 's3')")
server: Optional[str] = Field(default=None, max_length=255, description="服务器地址(本地策略为路径)") server: str | None = Field(default=None, max_length=255, description="服务器地址(本地策略为路径)")
bucket_name: Optional[str] = Field(default=None, max_length=255, description="存储桶名称") bucket_name: str | None = Field(default=None, max_length=255, description="存储桶名称")
is_private: bool = Field(default=True, sa_column_kwargs={"server_default": text("true")}, description="是否为私有空间") is_private: bool = Field(default=True, sa_column_kwargs={"server_default": text("true")}, description="是否为私有空间")
base_url: Optional[str] = Field(default=None, max_length=255, description="访问文件的基础URL") base_url: str | None = Field(default=None, max_length=255, description="访问文件的基础URL")
access_key: Optional[str] = Field(default=None, description="Access Key") access_key: str | None = Field(default=None, description="Access Key")
secret_key: Optional[str] = Field(default=None, description="Secret Key") secret_key: str | None = Field(default=None, description="Secret Key")
max_size: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="允许上传的最大文件尺寸(字节)") max_size: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="允许上传的最大文件尺寸(字节)")
auto_rename: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")}, description="是否自动重命名") auto_rename: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")}, description="是否自动重命名")
dir_name_rule: Optional[str] = Field(default=None, max_length=255, description="目录命名规则") dir_name_rule: str | None = Field(default=None, max_length=255, description="目录命名规则")
file_name_rule: Optional[str] = Field(default=None, max_length=255, description="文件命名规则") file_name_rule: str | None = Field(default=None, max_length=255, description="文件命名规则")
is_origin_link_enable: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")}, description="是否开启源链接访问") is_origin_link_enable: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")}, description="是否开启源链接访问")
options: Optional[str] = Field(default=None, description="其他选项 (JSON格式)") options: str | None = Field(default=None, description="其他选项 (JSON格式)")
# options 示例: {"token":"","file_type":null,"mimetype":"","od_redirect":"http://127.0.0.1:8000/...","chunk_size":52428800,"s3_path_style":false} # options 示例: {"token":"","file_type":null,"mimetype":"","od_redirect":"http://127.0.0.1:8000/...","chunk_size":52428800,"s3_path_style":false}
# 关系 # 关系

View File

@@ -1,14 +1,11 @@
from sqlmodel import Field, text
from typing import Optional
from sqlmodel import Field, text, Column, func, DateTime
from .base import TableBase from .base import TableBase
from datetime import datetime
class Redeem(TableBase, table=True): class Redeem(TableBase, table=True):
__tablename__ = 'redeems' """兑换码模型"""
type: int = Field(description="兑换码类型") type: int = Field(description="兑换码类型")
product_id: Optional[int] = Field(default=None, description="关联的商品/权益ID") product_id: int | None = Field(default=None, description="关联的商品/权益ID")
num: int = Field(default=1, sa_column_kwargs={"server_default": "1"}, description="可兑换数量/时长等") num: int = Field(default=1, sa_column_kwargs={"server_default": "1"}, description="可兑换数量/时长等")
code: str = Field(unique=True, index=True, description="兑换码,唯一") code: str = Field(unique=True, index=True, description="兑换码,唯一")
used: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")}, description="是否已使用") used: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")}, description="是否已使用")

View File

@@ -1,20 +1,19 @@
from typing import Optional, TYPE_CHECKING from typing import TYPE_CHECKING
from sqlmodel import Field, Relationship, Column, func, DateTime from sqlmodel import Field, Relationship
from .base import TableBase from .base import TableBase
from datetime import datetime
if TYPE_CHECKING: if TYPE_CHECKING:
from .share import Share from .share import Share
class Report(TableBase, table=True): class Report(TableBase, table=True):
__tablename__ = 'reports' """举报模型"""
reason: int = Field(description="举报原因代码") reason: int = Field(description="举报原因代码")
description: Optional[str] = Field(default=None, max_length=255, description="补充描述") description: str | None = Field(default=None, max_length=255, description="补充描述")
# 外键 # 外键
share_id: int = Field(foreign_key="shares.id", index=True, description="被举报的分享ID") share_id: int = Field(foreign_key="share.id", index=True, description="被举报的分享ID")
# 关系 # 关系
share: "Share" = Relationship(back_populates="reports") share: "Share" = Relationship(back_populates="reports")

View File

@@ -13,5 +13,5 @@ class LoginRequest(BaseModel):
""" """
username: str = Field(..., description="用户名或邮箱") username: str = Field(..., description="用户名或邮箱")
password: str = Field(..., description="用户密码") password: str = Field(..., description="用户密码")
captcha: Optional[str] = Field(None, description="验证码") captcha: str | None = Field(None, description="验证码")
twoFaCode: Optional[str] = Field(None, description="两步验证代码") twoFaCode: str | None = Field(None, description="两步验证代码")

View File

@@ -13,7 +13,7 @@ class ResponseModel(BaseModel):
''' '''
code: int = Field(default=0, description="系统内部状态码, 0表示成功其他表示失败", lt=60000, gt=0) code: int = Field(default=0, description="系统内部状态码, 0表示成功其他表示失败", lt=60000, gt=0)
data: Union[dict, list, str, int, float, None] = Field(None, description="响应数据") data: Union[dict, list, str, int, float, None] = Field(None, description="响应数据")
msg: Optional[str] = Field(default=None, description="响应消息,可以是错误消息或信息提示") msg: str | None = Field(default=None, description="响应消息,可以是错误消息或信息提示")
instance_id: str = Field(default_factory=lambda: str(uuid4()), description="实例ID用于标识请求的唯一性") instance_id: str = Field(default_factory=lambda: str(uuid4()), description="实例ID用于标识请求的唯一性")
class ThemeModel(BaseModel): class ThemeModel(BaseModel):
@@ -81,12 +81,12 @@ class SiteConfigModel(ResponseModel):
title: str = Field(default="DiskNext", description="网站标题") title: str = Field(default="DiskNext", description="网站标题")
themes: dict = Field(default_factory=dict, description="网站主题配置") themes: dict = Field(default_factory=dict, description="网站主题配置")
default_theme: str = Field(default="default", description="默认主题RGB色号") default_theme: str = Field(default="default", description="默认主题RGB色号")
site_notice: Optional[str] = Field(default=None, description="网站公告") site_notice: str | None = Field(default=None, description="网站公告")
user: dict = Field(default_factory=dict, description="用户信息") user: dict = Field(default_factory=dict, description="用户信息")
logo_light: Optional[str] = Field(default=None, description="网站Logo URL") logo_light: str | None = Field(default=None, description="网站Logo URL")
logo_dark: Optional[str] = Field(default=None, description="网站Logo URL深色模式") logo_dark: str | None = Field(default=None, description="网站Logo URL深色模式")
captcha_type: Literal['none', 'default', 'gcaptcha', 'cloudflare turnstile'] = Field(default='none', description="验证码类型") captcha_type: Literal['none', 'default', 'gcaptcha', 'cloudflare turnstile'] = Field(default='none', description="验证码类型")
captcha_key: Optional[str] = Field(default=None, description="验证码密钥") captcha_key: str | None = Field(default=None, description="验证码密钥")
class AuthnModel(BaseModel): class AuthnModel(BaseModel):
''' '''
@@ -100,7 +100,7 @@ class UserSettingModel(BaseModel):
用户设置模型 用户设置模型
''' '''
authn: Optional[AuthnModel] = Field(default=None, description="认证信息") authn: Optional[AuthnModel] = Field(default=None, description="认证信息")
group_expires: Optional[datetime] = Field(default=None, description="用户组过期时间") group_expires: datetime | None = Field(default=None, description="用户组过期时间")
prefer_theme: str = Field(default="#5898d4", description="用户首选主题") prefer_theme: str = Field(default="#5898d4", description="用户首选主题")
qq: str | bool = Field(default=False, description="QQ号") qq: str | bool = Field(default=False, description="QQ号")
themes: dict = Field(default_factory=dict, description="用户主题配置") themes: dict = Field(default_factory=dict, description="用户主题配置")

View File

@@ -34,18 +34,19 @@ SETTINGS_TYPE = Literal[
# 数据库模型 # 数据库模型
class Setting(TableBase, table=True): class Setting(TableBase, table=True):
__tablename__ = 'settings' """设置模型"""
__table_args__ = (UniqueConstraint("type", "name", name="uq_setting_type_name"),) __table_args__ = (UniqueConstraint("type", "name", name="uq_setting_type_name"),)
type: str = Field(max_length=255, description="设置类型/分组") type: str = Field(max_length=255, description="设置类型/分组")
name: str = Field(max_length=255, description="设置项名称") name: str = Field(max_length=255, description="设置项名称")
value: Optional[str] = Field(default=None, description="设置值") value: str | None = Field(default=None, description="设置值")
@staticmethod @staticmethod
async def add( async def add(
type: SETTINGS_TYPE = None, type: SETTINGS_TYPE = None,
name: str = None, name: str = None,
value: Optional[str] = None value: str | None = None
) -> None: ) -> None:
""" """
向数据库内添加设置项目。 向数据库内添加设置项目。
@@ -55,7 +56,7 @@ class Setting(TableBase, table=True):
:param name: 设置项名称 :param name: 设置项名称
:type name: str :type name: str
:param value: 设置值,默认为 None :param value: 设置值,默认为 None
:type value: Optional[str] :type value: str | None
""" """
from .database import get_session from .database import get_session
@@ -114,7 +115,7 @@ class Setting(TableBase, table=True):
async def set( async def set(
type: SETTINGS_TYPE, type: SETTINGS_TYPE,
name: str, name: str,
value: Optional[str] = None value: str | None = None
) -> None: ) -> None:
""" """
更新指定类型和名称的设置项的值。 更新指定类型和名称的设置项的值。
@@ -124,7 +125,7 @@ class Setting(TableBase, table=True):
:param name: 设置项名称 :param name: 设置项名称
:type name: str :type name: str
:param value: 新的设置值,默认为 None :param value: 新的设置值,默认为 None
:type value: Optional[str] :type value: str | None
:raises ValueError: 如果设置项不存在,则抛出异常 :raises ValueError: 如果设置项不存在,则抛出异常
""" """

View File

@@ -10,7 +10,8 @@ if TYPE_CHECKING:
from .report import Report from .report import Report
class Share(TableBase, table=True): class Share(TableBase, table=True):
__tablename__ = 'shares' """分享模型"""
__table_args__ = ( __table_args__ = (
UniqueConstraint("code", name="uq_share_code"), UniqueConstraint("code", name="uq_share_code"),
CheckConstraint("(file_id IS NOT NULL) <> (folder_id IS NOT NULL)", name="ck_share_xor"), CheckConstraint("(file_id IS NOT NULL) <> (folder_id IS NOT NULL)", name="ck_share_xor"),
@@ -19,22 +20,22 @@ class Share(TableBase, table=True):
) )
code: str = Field(max_length=64, nullable=False, index=True, description="分享码") code: str = Field(max_length=64, nullable=False, index=True, description="分享码")
password: Optional[str] = Field(default=None, max_length=255, description="分享密码(加密后)") password: str | None = Field(default=None, max_length=255, description="分享密码(加密后)")
is_dir: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")}, description="是否为目录分享") is_dir: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")}, description="是否为目录分享")
file_id: Optional[int] = Field(default=None, foreign_key="files.id", index=True, description="文件ID二选一") file_id: int | None = Field(default=None, foreign_key="file.id", index=True, description="文件ID二选一")
folder_id: Optional[int] = Field(default=None, foreign_key="folders.id", index=True, description="目录ID二选一") folder_id: int | None = Field(default=None, foreign_key="folder.id", index=True, description="目录ID二选一")
views: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="浏览次数") views: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="浏览次数")
downloads: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="下载次数") downloads: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="下载次数")
remain_downloads: Optional[int] = Field(default=None, description="剩余下载次数 (NULL为不限制)") remain_downloads: int | None = Field(default=None, description="剩余下载次数 (NULL为不限制)")
expires: Optional[datetime] = Field(default=None, description="过期时间 (NULL为永不过期)") expires: datetime | None = Field(default=None, description="过期时间 (NULL为永不过期)")
preview_enabled: bool = Field(default=True, sa_column_kwargs={"server_default": text("true")}, description="是否允许预览") preview_enabled: bool = Field(default=True, sa_column_kwargs={"server_default": text("true")}, description="是否允许预览")
source_name: Optional[str] = Field(default=None, max_length=255, index=True, description="源名称(冗余字段,便于展示)") source_name: str | None = Field(default=None, max_length=255, index=True, description="源名称(冗余字段,便于展示)")
score: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="兑换此分享所需的积分") score: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="兑换此分享所需的积分")
# 外键 # 外键
user_id: int = Field(foreign_key="users.id", index=True, description="创建分享的用户ID") user_id: int = Field(foreign_key="user.id", index=True, description="创建分享的用户ID")
# 关系 # 关系
user: "User" = Relationship(back_populates="shares") user: "User" = Relationship(back_populates="shares")

View File

@@ -8,7 +8,8 @@ if TYPE_CHECKING:
from .file import File from .file import File
class SourceLink(TableBase, table=True): class SourceLink(TableBase, table=True):
__tablename__ = 'source_links' """链接模型"""
__table_args__ = ( __table_args__ = (
Index("ix_sourcelink_file_name", "file_id", "name"), Index("ix_sourcelink_file_name", "file_id", "name"),
) )
@@ -17,7 +18,7 @@ class SourceLink(TableBase, table=True):
downloads: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="通过此链接的下载次数") downloads: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="通过此链接的下载次数")
# 外键 # 外键
file_id: int = Field(foreign_key="files.id", index=True, description="关联的文件ID") file_id: int = Field(foreign_key="file.id", index=True, description="关联的文件ID")
# 关系 # 关系
file: "File" = Relationship(back_populates="source_links") file: "File" = Relationship(back_populates="source_links")

View File

@@ -9,15 +9,15 @@ if TYPE_CHECKING:
from .user import User from .user import User
class StoragePack(TableBase, table=True): class StoragePack(TableBase, table=True):
__tablename__ = 'storage_packs' """容量包模型"""
name: str = Field(max_length=255, description="容量包名称") name: str = Field(max_length=255, description="容量包名称")
active_time: Optional[datetime] = Field(default=None, description="激活时间") active_time: datetime | None = Field(default=None, description="激活时间")
expired_time: Optional[datetime] = Field(default=None, index=True, description="过期时间") expired_time: datetime | None = Field(default=None, index=True, description="过期时间")
size: int = Field(description="容量包大小(字节)") size: int = Field(description="容量包大小(字节)")
# 外键 # 外键
user_id: int = Field(foreign_key="users.id", index=True, description="所属用户ID") user_id: int = Field(foreign_key="user.id", index=True, description="所属用户ID")
# 关系 # 关系
user: "User" = Relationship(back_populates="storage_packs") user: "User" = Relationship(back_populates="storage_packs")

View File

@@ -8,17 +8,18 @@ if TYPE_CHECKING:
from .user import User from .user import User
class Tag(TableBase, table=True): class Tag(TableBase, table=True):
__tablename__ = 'tags' """标签模型"""
__table_args__ = (UniqueConstraint("name", "user_id", name="uq_tag_name_user"),) __table_args__ = (UniqueConstraint("name", "user_id", name="uq_tag_name_user"),)
name: str = Field(max_length=255, description="标签名称") name: str = Field(max_length=255, description="标签名称")
icon: Optional[str] = Field(default=None, max_length=255, description="标签图标") icon: str | None = Field(default=None, max_length=255, description="标签图标")
color: Optional[str] = Field(default=None, max_length=255, description="标签颜色") color: str | None = Field(default=None, max_length=255, description="标签颜色")
type: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="标签类型: 0=手动, 1=自动") type: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="标签类型: 0=手动, 1=自动")
expression: Optional[str] = Field(default=None, description="自动标签的匹配表达式") expression: str | None = Field(default=None, description="自动标签的匹配表达式")
# 外键 # 外键
user_id: int = Field(foreign_key="users.id", index=True, description="所属用户ID") user_id: int = Field(foreign_key="user.id", index=True, description="所属用户ID")
# 关系 # 关系
user: "User" = Relationship(back_populates="tags") user: "User" = Relationship(back_populates="tags")

View File

@@ -9,7 +9,8 @@ if TYPE_CHECKING:
from .download import Download from .download import Download
class Task(TableBase, table=True): class Task(TableBase, table=True):
__tablename__ = 'tasks' """任务模型"""
__table_args__ = ( __table_args__ = (
CheckConstraint("progress BETWEEN 0 AND 100", name="ck_task_progress_range"), CheckConstraint("progress BETWEEN 0 AND 100", name="ck_task_progress_range"),
) )
@@ -17,11 +18,11 @@ class Task(TableBase, table=True):
status: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="任务状态: 0=排队中, 1=处理中, 2=完成, 3=错误") status: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="任务状态: 0=排队中, 1=处理中, 2=完成, 3=错误")
type: int = Field(description="任务类型") type: int = Field(description="任务类型")
progress: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="任务进度 (0-100)") progress: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="任务进度 (0-100)")
error: Optional[str] = Field(default=None, description="错误信息") error: str | None = Field(default=None, description="错误信息")
props: Optional[str] = Field(default=None, description="任务属性 (JSON格式)") props: str | None = Field(default=None, description="任务属性 (JSON格式)")
# 外键 # 外键
user_id: int = Field(foreign_key="users.id", index=True, description="所属用户ID") user_id: int = Field(foreign_key="user.id", index=True, description="所属用户ID")
# 关系 # 关系
user: "User" = Relationship(back_populates="tasks") user: "User" = Relationship(back_populates="tasks")

View File

@@ -15,7 +15,7 @@ from .task import Task
from .webdav import WebDAV from .webdav import WebDAV
class User(TableBase, table=True): class User(TableBase, table=True):
__tablename__ = 'users' """用户模型"""
email: str = Field(max_length=100, unique=True, index=True) email: str = Field(max_length=100, unique=True, index=True)
"""用户邮箱,唯一""" """用户邮箱,唯一"""
@@ -25,44 +25,56 @@ class User(TableBase, table=True):
nick: str | None = Field(default=None, max_length=50) nick: str | None = Field(default=None, max_length=50)
"""用户昵称""" """用户昵称"""
password: str = Field(max_length=255) password: str = Field(max_length=255)
"""用户密码(加密后)""" """用户密码(加密后)"""
status: bool | None = Field(default=None, sa_column_kwargs={"server_default": "0"})
"""用户状态: True=正常, None=未激活, False=封禁""" status: bool = Field(default=True, sa_column_kwargs={"server_default": "true"})
"""用户状态: True=正常, False=封禁"""
storage: int = Field(default=0, sa_column_kwargs={"server_default": "0"}) storage: int = Field(default=0, sa_column_kwargs={"server_default": "0"})
"""已用存储空间(字节)""" """已用存储空间(字节)"""
two_factor: str | None = Field(default=None, max_length=255) two_factor: str | None = Field(default=None, max_length=255)
"""两步验证密钥""" """两步验证密钥"""
avatar: str | None = Field(default=None, max_length=255) avatar: str | None = Field(default=None, max_length=255)
"""头像地址""" """头像地址"""
options: str | None = Field(default=None) options: str | None = Field(default=None)
"""用户个人设置 (JSON格式)""" """用户个人设置 (JSON格式)"""
authn: str | None = Field(default=None) authn: str | None = Field(default=None)
"""WebAuthn 凭证""" """WebAuthn 凭证"""
open_id: str | None = Field(default=None, max_length=255, unique=True, index=True) open_id: str | None = Field(default=None, max_length=255, unique=True, index=True)
"""第三方登录OpenID""" """第三方登录OpenID"""
score: int = Field(default=0, sa_column_kwargs={"server_default": "0"}) score: int = Field(default=0, sa_column_kwargs={"server_default": "0"})
"""用户积分""" """用户积分"""
group_expires: datetime | None = Field(default=None) group_expires: datetime | None = Field(default=None)
"""当前用户组过期时间""" """当前用户组过期时间"""
phone: str | None = Field(default=None, max_length=255, unique=True, index=True) phone: str | None = Field(default=None, max_length=255, unique=True, index=True)
"""手机号""" """手机号"""
# 外键 # 外键
group_id: int = Field(foreign_key="groups.id", index=True) group_id: int = Field(foreign_key="group.id", index=True)
"""所属用户组ID""" """所属用户组ID"""
previous_group_id: int | None = Field(default=None, foreign_key="groups.id")
previous_group_id: int | None = Field(default=None, foreign_key="group.id")
"""之前的用户组ID用于过期后恢复""" """之前的用户组ID用于过期后恢复"""
# 关系 # 关系
group: "Group" = Relationship( group: "Group" = Relationship(
back_populates="users", back_populates="user",
sa_relationship_kwargs={ sa_relationship_kwargs={
"foreign_keys": "User.group_id" "foreign_keys": "User.group_id"
} }
) )
previous_group: Optional["Group"] = Relationship( previous_group: Optional["Group"] = Relationship(
back_populates="previous_users", back_populates="previous_user",
sa_relationship_kwargs={ sa_relationship_kwargs={
"foreign_keys": "User.previous_group_id" "foreign_keys": "User.previous_group_id"
} }

View File

@@ -7,17 +7,18 @@ if TYPE_CHECKING:
from .user import User from .user import User
class WebDAV(TableBase, table=True): class WebDAV(TableBase, table=True):
__tablename__ = 'webdavs' """WebDAV账户模型"""
__table_args__ = (UniqueConstraint("name", "user_id", name="uq_webdav_name_user"),) __table_args__ = (UniqueConstraint("name", "user_id", name="uq_webdav_name_user"),)
name: str = Field(max_length=255, description="WebDAV账户名") name: str = Field(max_length=255, description="WebDAV账户名")
password: str = Field(max_length=255, description="WebDAV密码(加密后)") password: str = Field(max_length=255, description="WebDAV密码")
root: str = Field(default="/", sa_column_kwargs={"server_default": "'/'"}, description="根目录路径") root: str = Field(default="/", sa_column_kwargs={"server_default": "'/'"}, description="根目录路径")
readonly: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")}, description="是否只读") readonly: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")}, description="是否只读")
use_proxy: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")}, description="是否使用代理下载") use_proxy: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")}, description="是否使用代理下载")
# 外键 # 外键
user_id: int = Field(foreign_key="users.id", index=True, description="所属用户ID") user_id: int = Field(foreign_key="user.id", index=True, description="所属用户ID")
# 关系 # 关系
user: "User" = Relationship(back_populates="webdavs") user: "User" = Relationship(back_populates="webdavs")

View File

@@ -18,9 +18,9 @@ async def Login(LoginRequest: LoginRequest) -> TokenModel | bool | None:
:param password: 用户密码 :param password: 用户密码
:type password: str :type password: str
:param captcha: 验证码 :param captcha: 验证码
:type captcha: Optional[str] :type captcha: str | None
:param twoFaCode: 两步验证代码 :param twoFaCode: 两步验证代码
:type twoFaCode: Optional[str] :type twoFaCode: str | None
:return: TokenModel 对象或状态码或 None :return: TokenModel 对象或状态码或 None
:rtype: TokenModel | int | None :rtype: TokenModel | int | None