feat: 合并 File 和 Folder 模型为统一的 Object 模型,优化对象管理逻辑
refactor: 更新相关模型和路由以支持新对象模型,移除冗余代码
This commit is contained in:
12
.editorconfig
Normal file
12
.editorconfig
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# EditorConfig is awesome: https://EditorConfig.org
|
||||||
|
|
||||||
|
# top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
end_of_line = crlf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
insert_final_newline = false
|
||||||
@@ -4,8 +4,7 @@ from .user import User
|
|||||||
from .user_authn import UserAuthn
|
from .user_authn import UserAuthn
|
||||||
|
|
||||||
from .download import Download
|
from .download import Download
|
||||||
from .file import File
|
from .object import Object, ObjectType
|
||||||
from .folder import Folder
|
|
||||||
from .group import Group
|
from .group import Group
|
||||||
from .node import Node
|
from .node import Node
|
||||||
from .order import Order
|
from .order import Order
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
from sqlmodel import Field, Relationship, UniqueConstraint, CheckConstraint, Index
|
|
||||||
from .base import TableBase
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .user import User
|
|
||||||
from .folder import Folder
|
|
||||||
from .policy import Policy
|
|
||||||
from .source_link import SourceLink
|
|
||||||
|
|
||||||
class File(TableBase, table=True):
|
|
||||||
__table_args__ = (
|
|
||||||
UniqueConstraint("folder_id", "name", name="uq_file_folder_name_active"),
|
|
||||||
CheckConstraint("name NOT LIKE '%/%' AND name NOT LIKE '%\\%'", name="ck_file_name_no_slash"),
|
|
||||||
Index("ix_file_user_updated", "user_id", "updated_at"),
|
|
||||||
Index("ix_file_folder_updated", "folder_id", "updated_at"),
|
|
||||||
Index("ix_file_user_size", "user_id", "size"),
|
|
||||||
)
|
|
||||||
|
|
||||||
name: str = Field(max_length=255, description="文件名")
|
|
||||||
source_name: str | None = Field(default=None, description="源文件名")
|
|
||||||
size: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="文件大小(字节)")
|
|
||||||
upload_session_id: str | None = Field(default=None, max_length=255, unique=True, index=True, description="分块上传会话ID")
|
|
||||||
file_metadata: str | None = Field(default=None, description="文件元数据 (JSON格式)") # 后续可以考虑模型继承?
|
|
||||||
|
|
||||||
# 外键
|
|
||||||
user_id: int = Field(foreign_key="user.id", index=True, description="所属用户ID")
|
|
||||||
folder_id: int = Field(foreign_key="folder.id", index=True, description="所在目录ID")
|
|
||||||
policy_id: int = Field(foreign_key="policy.id", index=True, description="所属存储策略ID")
|
|
||||||
|
|
||||||
# 关系
|
|
||||||
user: "User" = Relationship(back_populates="files")
|
|
||||||
folder: "Folder" = Relationship(back_populates="files")
|
|
||||||
policy: "Policy" = Relationship(back_populates="files")
|
|
||||||
source_links: list["SourceLink"] = Relationship(back_populates="file")
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
|
|
||||||
from typing import Optional, List, TYPE_CHECKING
|
|
||||||
from sqlmodel import Field, Relationship, UniqueConstraint, CheckConstraint
|
|
||||||
from .base import TableBase
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .user import User
|
|
||||||
from .policy import Policy
|
|
||||||
from .file import File
|
|
||||||
|
|
||||||
class Folder(TableBase, table=True):
|
|
||||||
__table_args__ = (
|
|
||||||
UniqueConstraint(
|
|
||||||
"owner_id",
|
|
||||||
"parent_id",
|
|
||||||
"name",
|
|
||||||
name="uq_folder_name_parent",
|
|
||||||
),
|
|
||||||
CheckConstraint(
|
|
||||||
"name NOT LIKE '%/%' AND name NOT LIKE '%\\%'",
|
|
||||||
name="ck_folder_name_no_slash",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
name: str = Field(max_length=255, nullable=False, description="目录名")
|
|
||||||
|
|
||||||
# 外键
|
|
||||||
parent_id: int | None = Field(default=None, foreign_key="folder.id", index=True, description="父目录ID")
|
|
||||||
owner_id: int = Field(foreign_key="user.id", index=True, description="所有者用户ID")
|
|
||||||
policy_id: int = Field(foreign_key="policy.id", index=True, description="所属存储策略ID")
|
|
||||||
|
|
||||||
# 关系
|
|
||||||
owner: "User" = Relationship(back_populates="folders")
|
|
||||||
policy: "Policy" = Relationship(back_populates="folders")
|
|
||||||
|
|
||||||
# 自我引用关系
|
|
||||||
parent: Optional["Folder"] = Relationship(back_populates="children", sa_relationship_kwargs={"remote_side": "Folder.id"})
|
|
||||||
children: List["Folder"] = Relationship(back_populates="parent")
|
|
||||||
|
|
||||||
files: List["File"] = Relationship(back_populates="folder")
|
|
||||||
@@ -192,6 +192,7 @@ async def init_default_group() -> None:
|
|||||||
async def init_default_user() -> None:
|
async def init_default_user() -> None:
|
||||||
from .user import User
|
from .user import User
|
||||||
from .group import Group
|
from .group import Group
|
||||||
|
from .object import Object, ObjectType
|
||||||
from .database import get_session
|
from .database import get_session
|
||||||
|
|
||||||
log.info('初始化管理员用户...')
|
log.info('初始化管理员用户...')
|
||||||
@@ -210,7 +211,7 @@ async def init_default_user() -> None:
|
|||||||
admin_password = Password.generate(8)
|
admin_password = Password.generate(8)
|
||||||
hashed_admin_password = Password.hash(admin_password)
|
hashed_admin_password = Password.hash(admin_password)
|
||||||
|
|
||||||
await User(
|
admin_user = await User(
|
||||||
username="admin",
|
username="admin",
|
||||||
nick="admin",
|
nick="admin",
|
||||||
status=True,
|
status=True,
|
||||||
@@ -218,6 +219,15 @@ async def init_default_user() -> None:
|
|||||||
password=hashed_admin_password,
|
password=hashed_admin_password,
|
||||||
).save(session)
|
).save(session)
|
||||||
|
|
||||||
|
# 为管理员创建根目录(使用默认存储策略)
|
||||||
|
await Object(
|
||||||
|
name="~",
|
||||||
|
type=ObjectType.FOLDER,
|
||||||
|
owner_id=admin_user.id,
|
||||||
|
parent_id=None,
|
||||||
|
policy_id=1, # 默认本地存储策略
|
||||||
|
).save(session)
|
||||||
|
|
||||||
log.info(f'初始管理员账号: admin')
|
log.info(f'初始管理员账号: admin')
|
||||||
log.info(f'初始管理员密码: {admin_password}')
|
log.info(f'初始管理员密码: {admin_password}')
|
||||||
|
|
||||||
|
|||||||
185
models/object.py
Normal file
185
models/object.py
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
from enum import StrEnum
|
||||||
|
from sqlmodel import Field, Relationship, UniqueConstraint, CheckConstraint, Index
|
||||||
|
from .base import TableBase
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .user import User
|
||||||
|
from .policy import Policy
|
||||||
|
from .source_link import SourceLink
|
||||||
|
from .share import Share
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectType(StrEnum):
|
||||||
|
"""对象类型枚举"""
|
||||||
|
FILE = "file"
|
||||||
|
FOLDER = "folder"
|
||||||
|
|
||||||
|
|
||||||
|
class Object(TableBase, table=True):
|
||||||
|
"""
|
||||||
|
统一对象模型
|
||||||
|
|
||||||
|
合并了原有的 File 和 Folder 模型,通过 type 字段区分文件和目录。
|
||||||
|
|
||||||
|
根目录规则:
|
||||||
|
- 每个用户有一个显式根目录对象(name="~", parent_id=NULL)
|
||||||
|
- 用户创建的文件/文件夹的 parent_id 指向根目录或其他文件夹的 id
|
||||||
|
- 根目录的 policy_id 指定用户默认存储策略
|
||||||
|
"""
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
# 同一父目录下名称唯一(包括 parent_id 为 NULL 的情况)
|
||||||
|
UniqueConstraint("owner_id", "parent_id", "name", name="uq_object_parent_name"),
|
||||||
|
# 名称不能包含斜杠
|
||||||
|
CheckConstraint(
|
||||||
|
"name NOT LIKE '%/%' AND name NOT LIKE '%\\%'",
|
||||||
|
name="ck_object_name_no_slash",
|
||||||
|
),
|
||||||
|
# 性能索引
|
||||||
|
Index("ix_object_owner_updated", "owner_id", "updated_at"),
|
||||||
|
Index("ix_object_parent_updated", "parent_id", "updated_at"),
|
||||||
|
Index("ix_object_owner_type", "owner_id", "type"),
|
||||||
|
Index("ix_object_owner_size", "owner_id", "size"),
|
||||||
|
)
|
||||||
|
|
||||||
|
# ==================== 基础字段 ====================
|
||||||
|
|
||||||
|
name: str = Field(max_length=255)
|
||||||
|
"""对象名称(文件名或目录名)"""
|
||||||
|
|
||||||
|
type: ObjectType
|
||||||
|
"""对象类型:file 或 folder"""
|
||||||
|
|
||||||
|
# ==================== 文件专属字段 ====================
|
||||||
|
|
||||||
|
source_name: str | None = None
|
||||||
|
"""源文件名(仅文件有效)"""
|
||||||
|
|
||||||
|
size: int = Field(default=0, sa_column_kwargs={"server_default": "0"})
|
||||||
|
"""文件大小(字节),目录为 0"""
|
||||||
|
|
||||||
|
upload_session_id: str | None = Field(default=None, max_length=255, unique=True, index=True)
|
||||||
|
"""分块上传会话ID(仅文件有效)"""
|
||||||
|
|
||||||
|
file_metadata: str | None = None
|
||||||
|
"""文件元数据 (JSON格式),仅文件有效"""
|
||||||
|
|
||||||
|
# ==================== 外键 ====================
|
||||||
|
|
||||||
|
parent_id: int | None = Field(default=None, foreign_key="object.id", index=True)
|
||||||
|
"""父目录ID,NULL 表示这是用户的根目录"""
|
||||||
|
|
||||||
|
owner_id: int = Field(foreign_key="user.id", index=True)
|
||||||
|
"""所有者用户ID"""
|
||||||
|
|
||||||
|
policy_id: int = Field(foreign_key="policy.id", index=True)
|
||||||
|
"""存储策略ID(文件直接使用,目录作为子文件的默认策略)"""
|
||||||
|
|
||||||
|
# ==================== 关系 ====================
|
||||||
|
|
||||||
|
owner: "User" = Relationship(back_populates="objects")
|
||||||
|
"""所有者"""
|
||||||
|
|
||||||
|
policy: "Policy" = Relationship(back_populates="objects")
|
||||||
|
"""存储策略"""
|
||||||
|
|
||||||
|
# 自引用关系
|
||||||
|
parent: Optional["Object"] = Relationship(
|
||||||
|
back_populates="children",
|
||||||
|
sa_relationship_kwargs={"remote_side": "Object.id"},
|
||||||
|
)
|
||||||
|
"""父目录"""
|
||||||
|
|
||||||
|
children: list["Object"] = Relationship(back_populates="parent")
|
||||||
|
"""子对象(文件和子目录)"""
|
||||||
|
|
||||||
|
# 仅文件有效的关系
|
||||||
|
source_links: list["SourceLink"] = Relationship(back_populates="object")
|
||||||
|
"""源链接列表(仅文件有效)"""
|
||||||
|
|
||||||
|
shares: list["Share"] = Relationship(back_populates="object")
|
||||||
|
"""分享列表"""
|
||||||
|
|
||||||
|
# ==================== 业务属性 ====================
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_file(self) -> bool:
|
||||||
|
"""是否为文件"""
|
||||||
|
return self.type == ObjectType.FILE
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_folder(self) -> bool:
|
||||||
|
"""是否为目录"""
|
||||||
|
return self.type == ObjectType.FOLDER
|
||||||
|
|
||||||
|
# ==================== 业务方法 ====================
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_root(cls, session, user_id: int) -> "Object | None":
|
||||||
|
"""
|
||||||
|
获取用户的根目录
|
||||||
|
|
||||||
|
:param session: 数据库会话
|
||||||
|
:param user_id: 用户ID
|
||||||
|
:return: 根目录对象,不存在则返回 None
|
||||||
|
"""
|
||||||
|
return await cls.get(
|
||||||
|
session,
|
||||||
|
(cls.owner_id == user_id) & (cls.parent_id == None)
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_by_path(cls, session, user_id: int, path: str) -> "Object | None":
|
||||||
|
"""
|
||||||
|
根据路径获取对象
|
||||||
|
|
||||||
|
:param session: 数据库会话
|
||||||
|
:param user_id: 用户ID
|
||||||
|
:param path: 路径,如 "/" 或 "/docs/images"
|
||||||
|
:return: Object 或 None
|
||||||
|
"""
|
||||||
|
path = path.strip()
|
||||||
|
if not path or path == "/" or path == "~":
|
||||||
|
return await cls.get_root(session, user_id)
|
||||||
|
|
||||||
|
# 移除开头的斜杠并分割路径
|
||||||
|
if path.startswith("/"):
|
||||||
|
path = path[1:]
|
||||||
|
parts = [p for p in path.split("/") if p]
|
||||||
|
|
||||||
|
if not parts:
|
||||||
|
return await cls.get_root(session, user_id)
|
||||||
|
|
||||||
|
# 从根目录开始遍历
|
||||||
|
current = await cls.get_root(session, user_id)
|
||||||
|
|
||||||
|
for part in parts:
|
||||||
|
if not current:
|
||||||
|
return None
|
||||||
|
|
||||||
|
current = await cls.get(
|
||||||
|
session,
|
||||||
|
(cls.owner_id == user_id) &
|
||||||
|
(cls.parent_id == current.id) &
|
||||||
|
(cls.name == part)
|
||||||
|
)
|
||||||
|
|
||||||
|
return current
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_children(cls, session, user_id: int, parent_id: int) -> list["Object"]:
|
||||||
|
"""
|
||||||
|
获取目录下的所有子对象
|
||||||
|
|
||||||
|
:param session: 数据库会话
|
||||||
|
:param user_id: 用户ID
|
||||||
|
:param parent_id: 父目录ID
|
||||||
|
:return: 子对象列表
|
||||||
|
"""
|
||||||
|
return await cls.get(
|
||||||
|
session,
|
||||||
|
(cls.owner_id == user_id) & (cls.parent_id == parent_id),
|
||||||
|
fetch_mode="all"
|
||||||
|
)
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
|
|
||||||
from typing import Optional, List, TYPE_CHECKING
|
from typing import Optional, TYPE_CHECKING
|
||||||
from sqlmodel import Field, Relationship, text
|
from sqlmodel import Field, Relationship, text
|
||||||
from .base import TableBase
|
from .base import TableBase
|
||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .file import File
|
from .object import Object
|
||||||
from .folder import Folder
|
|
||||||
|
|
||||||
class PolicyType(StrEnum):
|
class PolicyType(StrEnum):
|
||||||
LOCAL = "local"
|
LOCAL = "local"
|
||||||
@@ -58,8 +57,8 @@ class Policy(TableBase, table=True):
|
|||||||
# 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}
|
||||||
|
|
||||||
# 关系
|
# 关系
|
||||||
files: List["File"] = Relationship(back_populates="policy")
|
objects: list["Object"] = Relationship(back_populates="policy")
|
||||||
folders: List["Folder"] = Relationship(back_populates="policy")
|
"""策略下的所有对象"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def create(
|
async def create(
|
||||||
|
|||||||
@@ -132,6 +132,9 @@ class DirectoryModel(BaseModel):
|
|||||||
'''
|
'''
|
||||||
目录模型
|
目录模型
|
||||||
'''
|
'''
|
||||||
parent: str = Field(default=..., description="父目录ID")
|
|
||||||
|
parent: str | None
|
||||||
|
"""父目录ID,根目录为None"""
|
||||||
|
|
||||||
objects: list[ObjectModel] = Field(default_factory=list, description="目录下的对象列表")
|
objects: list[ObjectModel] = Field(default_factory=list, description="目录下的对象列表")
|
||||||
policy: PolicyModel = Field(default_factory=PolicyModel, description="存储策略")
|
policy: PolicyModel = Field(default_factory=PolicyModel, description="存储策略")
|
||||||
@@ -1,42 +1,71 @@
|
|||||||
|
|
||||||
from typing import Optional, TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from sqlmodel import Field, Relationship, text, CheckConstraint, UniqueConstraint, Index
|
from sqlmodel import Field, Relationship, text, UniqueConstraint, 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
|
||||||
from .report import Report
|
from .report import Report
|
||||||
|
from .object import Object
|
||||||
|
|
||||||
|
|
||||||
class Share(TableBase, table=True):
|
class Share(TableBase, table=True):
|
||||||
"""分享模型"""
|
"""分享模型"""
|
||||||
|
|
||||||
__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"),
|
|
||||||
Index("ix_share_source_name", "source_name"),
|
Index("ix_share_source_name", "source_name"),
|
||||||
Index("ix_share_user_created", "user_id", "created_at"),
|
Index("ix_share_user_created", "user_id", "created_at"),
|
||||||
|
Index("ix_share_object", "object_id"),
|
||||||
)
|
)
|
||||||
|
|
||||||
code: str = Field(max_length=64, nullable=False, index=True, description="分享码")
|
code: str = Field(max_length=64, nullable=False, index=True)
|
||||||
password: str | None = Field(default=None, max_length=255, description="分享密码(加密后)")
|
"""分享码"""
|
||||||
|
|
||||||
is_dir: bool = Field(default=False, sa_column_kwargs={"server_default": text("false")}, description="是否为目录分享")
|
password: str | None = Field(default=None, max_length=255)
|
||||||
file_id: int | None = Field(default=None, foreign_key="file.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="浏览次数")
|
object_id: int = Field(foreign_key="object.id", index=True)
|
||||||
downloads: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="下载次数")
|
"""关联的对象ID"""
|
||||||
remain_downloads: int | None = Field(default=None, description="剩余下载次数 (NULL为不限制)")
|
|
||||||
expires: datetime | None = Field(default=None, description="过期时间 (NULL为永不过期)")
|
views: int = Field(default=0, sa_column_kwargs={"server_default": "0"})
|
||||||
preview_enabled: bool = Field(default=True, sa_column_kwargs={"server_default": text("true")}, description="是否允许预览")
|
"""浏览次数"""
|
||||||
source_name: str | None = Field(default=None, max_length=255, description="源名称(冗余字段,便于展示)")
|
|
||||||
score: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="兑换此分享所需的积分")
|
downloads: int = Field(default=0, sa_column_kwargs={"server_default": "0"})
|
||||||
|
"""下载次数"""
|
||||||
|
|
||||||
|
remain_downloads: int | None = None
|
||||||
|
"""剩余下载次数 (NULL为不限制)"""
|
||||||
|
|
||||||
|
expires: datetime | None = None
|
||||||
|
"""过期时间 (NULL为永不过期)"""
|
||||||
|
|
||||||
|
preview_enabled: bool = Field(default=True, sa_column_kwargs={"server_default": text("true")})
|
||||||
|
"""是否允许预览"""
|
||||||
|
|
||||||
|
source_name: str | None = Field(default=None, max_length=255)
|
||||||
|
"""源名称(冗余字段,便于展示)"""
|
||||||
|
|
||||||
|
score: int = Field(default=0, sa_column_kwargs={"server_default": "0"})
|
||||||
|
"""兑换此分享所需的积分"""
|
||||||
|
|
||||||
# 外键
|
# 外键
|
||||||
user_id: int = Field(foreign_key="user.id", index=True, description="创建分享的用户ID")
|
user_id: int = Field(foreign_key="user.id", index=True)
|
||||||
|
"""创建分享的用户ID"""
|
||||||
|
|
||||||
# 关系
|
# 关系
|
||||||
user: "User" = Relationship(back_populates="shares")
|
user: "User" = Relationship(back_populates="shares")
|
||||||
|
"""分享创建者"""
|
||||||
|
|
||||||
|
object: "Object" = Relationship(back_populates="shares")
|
||||||
|
"""关联的对象"""
|
||||||
|
|
||||||
reports: list["Report"] = Relationship(back_populates="share")
|
reports: list["Report"] = Relationship(back_populates="share")
|
||||||
|
"""举报列表"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_dir(self) -> bool:
|
||||||
|
"""是否为目录分享(向后兼容属性)"""
|
||||||
|
from .object import ObjectType
|
||||||
|
return self.object.type == ObjectType.FOLDER if self.object else False
|
||||||
|
|||||||
@@ -1,24 +1,29 @@
|
|||||||
|
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING
|
||||||
from sqlmodel import Field, Relationship, Index
|
from sqlmodel import Field, Relationship, Index
|
||||||
from .base import TableBase
|
from .base import TableBase
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .file import File
|
from .object import Object
|
||||||
|
|
||||||
|
|
||||||
class SourceLink(TableBase, table=True):
|
class SourceLink(TableBase, table=True):
|
||||||
"""链接模型"""
|
"""链接模型"""
|
||||||
|
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
Index("ix_sourcelink_file_name", "file_id", "name"),
|
Index("ix_sourcelink_object_name", "object_id", "name"),
|
||||||
)
|
)
|
||||||
|
|
||||||
name: str = Field(max_length=255, description="链接名称")
|
name: str = Field(max_length=255)
|
||||||
downloads: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="通过此链接的下载次数")
|
"""链接名称"""
|
||||||
|
|
||||||
|
downloads: int = Field(default=0, sa_column_kwargs={"server_default": "0"})
|
||||||
|
"""通过此链接的下载次数"""
|
||||||
|
|
||||||
# 外键
|
# 外键
|
||||||
file_id: int = Field(foreign_key="file.id", index=True, description="关联的文件ID")
|
object_id: int = Field(foreign_key="object.id", index=True)
|
||||||
|
"""关联的对象ID(必须是文件类型)"""
|
||||||
|
|
||||||
# 关系
|
# 关系
|
||||||
file: "File" = Relationship(back_populates="source_links")
|
object: "Object" = Relationship(back_populates="source_links")
|
||||||
|
"""关联的对象"""
|
||||||
@@ -9,8 +9,7 @@ from .base import TableBase, SQLModelBase
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .group import Group
|
from .group import Group
|
||||||
from .download import Download
|
from .download import Download
|
||||||
from .file import File
|
from .object import Object
|
||||||
from .folder import Folder
|
|
||||||
from .order import Order
|
from .order import Order
|
||||||
from .share import Share
|
from .share import Share
|
||||||
from .storage_pack import StoragePack
|
from .storage_pack import StoragePack
|
||||||
@@ -125,8 +124,8 @@ class User(TableBase, table=True):
|
|||||||
)
|
)
|
||||||
|
|
||||||
downloads: list["Download"] = Relationship(back_populates="user")
|
downloads: list["Download"] = Relationship(back_populates="user")
|
||||||
files: list["File"] = Relationship(back_populates="user")
|
objects: list["Object"] = Relationship(back_populates="owner")
|
||||||
folders: list["Folder"] = Relationship(back_populates="owner")
|
"""用户的所有对象(文件和目录)"""
|
||||||
orders: list["Order"] = Relationship(back_populates="user")
|
orders: list["Order"] = Relationship(back_populates="user")
|
||||||
shares: list["Share"] = Relationship(back_populates="user")
|
shares: list["Share"] = Relationship(back_populates="user")
|
||||||
storage_packs: list["StoragePack"] = Relationship(back_populates="user")
|
storage_packs: list["StoragePack"] = Relationship(back_populates="user")
|
||||||
|
|||||||
@@ -1,45 +1,148 @@
|
|||||||
from fastapi import APIRouter, Depends
|
from typing import Annotated
|
||||||
from middleware.auth import SignRequired
|
|
||||||
from models import response
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from middleware.auth import AuthRequired
|
||||||
|
from middleware.dependencies import SessionDep
|
||||||
|
from models import Object, ObjectType, User, response
|
||||||
|
|
||||||
directory_router = APIRouter(
|
directory_router = APIRouter(
|
||||||
prefix="/directory",
|
prefix="/directory",
|
||||||
tags=["directory"]
|
tags=["directory"]
|
||||||
)
|
)
|
||||||
|
|
||||||
@directory_router.put(
|
|
||||||
path='/',
|
|
||||||
summary='创建目录',
|
|
||||||
description='Create a directory endpoint.',
|
|
||||||
dependencies=[Depends(SignRequired)]
|
|
||||||
)
|
|
||||||
def router_directory_create() -> response.ResponseModel:
|
|
||||||
"""
|
|
||||||
Create a directory endpoint.
|
|
||||||
|
|
||||||
Returns:
|
class DirectoryCreateRequest(BaseModel):
|
||||||
ResponseModel: A model containing the response data for the directory creation.
|
"""创建目录请求"""
|
||||||
"""
|
|
||||||
pass
|
path: str
|
||||||
|
"""目录路径,如 /docs/images"""
|
||||||
|
|
||||||
|
policy_id: int | None = None
|
||||||
|
"""存储策略ID,不指定则继承父目录"""
|
||||||
|
|
||||||
@directory_router.get(
|
@directory_router.get(
|
||||||
path='/{path:path}',
|
path="/{path:path}",
|
||||||
summary='获取目录内容',
|
summary="获取目录内容",
|
||||||
description='Get directory contents endpoint.',
|
|
||||||
dependencies=[Depends(SignRequired)]
|
|
||||||
)
|
)
|
||||||
def router_directory_get(path: str) -> response.ResponseModel:
|
async def router_directory_get(
|
||||||
|
session: SessionDep,
|
||||||
|
user: Annotated[User, Depends(AuthRequired)],
|
||||||
|
path: str = ""
|
||||||
|
) -> response.ResponseModel:
|
||||||
"""
|
"""
|
||||||
Get directory contents endpoint.
|
获取目录内容
|
||||||
|
|
||||||
Args:
|
:param session: 数据库会话
|
||||||
path (str): The path of the directory to retrieve contents from.
|
:param user: 当前登录用户
|
||||||
|
:param path: 目录路径,空或 "/" 表示根目录
|
||||||
Returns:
|
:return: 目录内容
|
||||||
ResponseModel: A model containing the response data for the directory contents.
|
|
||||||
"""
|
"""
|
||||||
|
folder = await Object.get_by_path(session, user.id, path or "/")
|
||||||
|
|
||||||
|
if not folder:
|
||||||
|
raise HTTPException(status_code=404, detail="目录不存在")
|
||||||
|
|
||||||
|
if not folder.is_folder:
|
||||||
|
raise HTTPException(status_code=400, detail="指定路径不是目录")
|
||||||
|
|
||||||
|
children = await Object.get_children(session, user.id, folder.id)
|
||||||
|
policy = await folder.awaitable_attrs.policy
|
||||||
|
|
||||||
|
objects = [
|
||||||
|
response.ObjectModel(
|
||||||
|
id=str(child.id),
|
||||||
|
name=child.name,
|
||||||
|
path=f"/{child.name}", # TODO: 完整路径
|
||||||
|
thumb=False,
|
||||||
|
size=child.size,
|
||||||
|
type="folder" if child.is_folder else "file",
|
||||||
|
date=child.updated_at,
|
||||||
|
create_date=child.created_at,
|
||||||
|
source_enabled=False,
|
||||||
|
)
|
||||||
|
for child in children
|
||||||
|
]
|
||||||
|
|
||||||
return response.ResponseModel(
|
return response.ResponseModel(
|
||||||
data=response.DirectoryModel(
|
data=response.DirectoryModel(
|
||||||
|
parent=str(folder.parent_id) if folder.parent_id else None,
|
||||||
|
objects=objects,
|
||||||
|
policy=response.PolicyModel(
|
||||||
|
id=str(policy.id),
|
||||||
|
name=policy.name,
|
||||||
|
type=policy.type.value,
|
||||||
|
max_size=policy.max_size,
|
||||||
|
file_type=[],
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@directory_router.put(
|
||||||
|
path="/",
|
||||||
|
summary="创建目录",
|
||||||
|
)
|
||||||
|
async def router_directory_create(
|
||||||
|
session: SessionDep,
|
||||||
|
user: Annotated[User, Depends(AuthRequired)],
|
||||||
|
request: DirectoryCreateRequest
|
||||||
|
) -> response.ResponseModel:
|
||||||
|
"""
|
||||||
|
创建目录
|
||||||
|
|
||||||
|
:param session: 数据库会话
|
||||||
|
:param user: 当前登录用户
|
||||||
|
:param request: 创建请求
|
||||||
|
:return: 创建结果
|
||||||
|
"""
|
||||||
|
path = request.path.strip()
|
||||||
|
if not path or path == "/":
|
||||||
|
raise HTTPException(status_code=400, detail="路径不能为空或根目录")
|
||||||
|
|
||||||
|
# 解析路径
|
||||||
|
if path.startswith("/"):
|
||||||
|
path = path[1:]
|
||||||
|
parts = [p for p in path.split("/") if p]
|
||||||
|
|
||||||
|
if not parts:
|
||||||
|
raise HTTPException(status_code=400, detail="无效的目录路径")
|
||||||
|
|
||||||
|
new_folder_name = parts[-1]
|
||||||
|
parent_path = "/" + "/".join(parts[:-1]) if len(parts) > 1 else "/"
|
||||||
|
|
||||||
|
parent = await Object.get_by_path(session, user.id, parent_path)
|
||||||
|
if not parent:
|
||||||
|
raise HTTPException(status_code=404, detail="父目录不存在")
|
||||||
|
|
||||||
|
if not parent.is_folder:
|
||||||
|
raise HTTPException(status_code=400, detail="父路径不是目录")
|
||||||
|
|
||||||
|
# 检查是否已存在同名对象
|
||||||
|
existing = await Object.get(
|
||||||
|
session,
|
||||||
|
(Object.owner_id == user.id) &
|
||||||
|
(Object.parent_id == parent.id) &
|
||||||
|
(Object.name == new_folder_name)
|
||||||
|
)
|
||||||
|
if existing:
|
||||||
|
raise HTTPException(status_code=409, detail="同名文件或目录已存在")
|
||||||
|
|
||||||
|
policy_id = request.policy_id if request.policy_id else parent.policy_id
|
||||||
|
|
||||||
|
new_folder = await Object(
|
||||||
|
name=new_folder_name,
|
||||||
|
type=ObjectType.FOLDER,
|
||||||
|
owner_id=user.id,
|
||||||
|
parent_id=parent.id,
|
||||||
|
policy_id=policy_id,
|
||||||
|
).save(session)
|
||||||
|
|
||||||
|
return response.ResponseModel(
|
||||||
|
data={
|
||||||
|
"id": new_folder.id,
|
||||||
|
"name": new_folder.name,
|
||||||
|
"path": f"{parent_path.rstrip('/')}/{new_folder_name}",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user