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 .download import Download
|
||||
from .file import File
|
||||
from .folder import Folder
|
||||
from .object import Object, ObjectType
|
||||
from .group import Group
|
||||
from .node import Node
|
||||
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:
|
||||
from .user import User
|
||||
from .group import Group
|
||||
from .object import Object, ObjectType
|
||||
from .database import get_session
|
||||
|
||||
log.info('初始化管理员用户...')
|
||||
@@ -210,7 +211,7 @@ async def init_default_user() -> None:
|
||||
admin_password = Password.generate(8)
|
||||
hashed_admin_password = Password.hash(admin_password)
|
||||
|
||||
await User(
|
||||
admin_user = await User(
|
||||
username="admin",
|
||||
nick="admin",
|
||||
status=True,
|
||||
@@ -218,6 +219,15 @@ async def init_default_user() -> None:
|
||||
password=hashed_admin_password,
|
||||
).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_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 .base import TableBase
|
||||
from enum import StrEnum
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .file import File
|
||||
from .folder import Folder
|
||||
from .object import Object
|
||||
|
||||
class PolicyType(StrEnum):
|
||||
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}
|
||||
|
||||
# 关系
|
||||
files: List["File"] = Relationship(back_populates="policy")
|
||||
folders: List["Folder"] = Relationship(back_populates="policy")
|
||||
objects: list["Object"] = Relationship(back_populates="policy")
|
||||
"""策略下的所有对象"""
|
||||
|
||||
@staticmethod
|
||||
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="目录下的对象列表")
|
||||
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 sqlmodel import Field, Relationship, text, CheckConstraint, UniqueConstraint, Index
|
||||
from sqlmodel import Field, Relationship, text, UniqueConstraint, Index
|
||||
from .base import TableBase
|
||||
from datetime import datetime
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .user import User
|
||||
from .report import Report
|
||||
from .object import Object
|
||||
|
||||
|
||||
class Share(TableBase, table=True):
|
||||
"""分享模型"""
|
||||
|
||||
__table_args__ = (
|
||||
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_user_created", "user_id", "created_at"),
|
||||
Index("ix_share_object", "object_id"),
|
||||
)
|
||||
|
||||
code: str = Field(max_length=64, nullable=False, index=True, 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="是否为目录分享")
|
||||
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="浏览次数")
|
||||
downloads: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="下载次数")
|
||||
remain_downloads: int | None = 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="是否允许预览")
|
||||
source_name: str | None = Field(default=None, max_length=255, description="源名称(冗余字段,便于展示)")
|
||||
score: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="兑换此分享所需的积分")
|
||||
|
||||
code: str = Field(max_length=64, nullable=False, index=True)
|
||||
"""分享码"""
|
||||
|
||||
password: str | None = Field(default=None, max_length=255)
|
||||
"""分享密码(加密后)"""
|
||||
|
||||
object_id: int = Field(foreign_key="object.id", index=True)
|
||||
"""关联的对象ID"""
|
||||
|
||||
views: int = Field(default=0, sa_column_kwargs={"server_default": "0"})
|
||||
"""浏览次数"""
|
||||
|
||||
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")
|
||||
reports: list["Report"] = Relationship(back_populates="share")
|
||||
"""分享创建者"""
|
||||
|
||||
object: "Object" = Relationship(back_populates="shares")
|
||||
"""关联的对象"""
|
||||
|
||||
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 .base import TableBase
|
||||
from datetime import datetime
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .file import File
|
||||
from .object import Object
|
||||
|
||||
|
||||
class SourceLink(TableBase, table=True):
|
||||
"""链接模型"""
|
||||
|
||||
__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="链接名称")
|
||||
downloads: int = Field(default=0, sa_column_kwargs={"server_default": "0"}, description="通过此链接的下载次数")
|
||||
|
||||
name: str = Field(max_length=255)
|
||||
"""链接名称"""
|
||||
|
||||
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:
|
||||
from .group import Group
|
||||
from .download import Download
|
||||
from .file import File
|
||||
from .folder import Folder
|
||||
from .object import Object
|
||||
from .order import Order
|
||||
from .share import Share
|
||||
from .storage_pack import StoragePack
|
||||
@@ -125,8 +124,8 @@ class User(TableBase, table=True):
|
||||
)
|
||||
|
||||
downloads: list["Download"] = Relationship(back_populates="user")
|
||||
files: list["File"] = Relationship(back_populates="user")
|
||||
folders: list["Folder"] = Relationship(back_populates="owner")
|
||||
objects: list["Object"] = Relationship(back_populates="owner")
|
||||
"""用户的所有对象(文件和目录)"""
|
||||
orders: list["Order"] = Relationship(back_populates="user")
|
||||
shares: list["Share"] = Relationship(back_populates="user")
|
||||
storage_packs: list["StoragePack"] = Relationship(back_populates="user")
|
||||
|
||||
@@ -1,45 +1,148 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from middleware.auth import SignRequired
|
||||
from models import response
|
||||
from typing import Annotated
|
||||
|
||||
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(
|
||||
prefix="/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:
|
||||
ResponseModel: A model containing the response data for the directory creation.
|
||||
"""
|
||||
pass
|
||||
|
||||
class DirectoryCreateRequest(BaseModel):
|
||||
"""创建目录请求"""
|
||||
|
||||
path: str
|
||||
"""目录路径,如 /docs/images"""
|
||||
|
||||
policy_id: int | None = None
|
||||
"""存储策略ID,不指定则继承父目录"""
|
||||
|
||||
@directory_router.get(
|
||||
path='/{path:path}',
|
||||
summary='获取目录内容',
|
||||
description='Get directory contents endpoint.',
|
||||
dependencies=[Depends(SignRequired)]
|
||||
path="/{path:path}",
|
||||
summary="获取目录内容",
|
||||
)
|
||||
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:
|
||||
path (str): The path of the directory to retrieve contents from.
|
||||
|
||||
Returns:
|
||||
ResponseModel: A model containing the response data for the directory contents.
|
||||
获取目录内容
|
||||
|
||||
:param session: 数据库会话
|
||||
:param user: 当前登录用户
|
||||
:param path: 目录路径,空或 "/" 表示根目录
|
||||
:return: 目录内容
|
||||
"""
|
||||
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(
|
||||
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