Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Backend/Application/Interfaces/IFileRepository.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,11 @@ def get_all(self) -> list[File]:
@abstractmethod
def update_status(self, file_id: str, status: FileStatus) -> None:
pass

@abstractmethod
def update_folder(self, file_id: str, folder_id: str | None) -> None:
pass

@abstractmethod
def get_by_folder_id(self, folder_id: str) -> list[File]:
pass
29 changes: 29 additions & 0 deletions Backend/Application/Interfaces/IFolderRepository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""文件夹仓储抽象接口,定义文件夹的持久化操作契约。"""
from abc import ABC, abstractmethod
from Backend.Domain.Entities.folder import Folder


class IFolderRepository(ABC):
@abstractmethod
def save(self, folder: Folder) -> None:
pass

@abstractmethod
def get_by_id(self, folder_id: str) -> Folder | None:
pass

@abstractmethod
def get_by_name(self, name: str) -> Folder | None:
pass

@abstractmethod
def get_all(self) -> list[Folder]:
pass

@abstractmethod
def update_name(self, folder_id: str, new_name: str) -> None:
pass

@abstractmethod
def delete(self, folder_id: str) -> None:
pass
4 changes: 2 additions & 2 deletions Backend/Application/Skills/FaultTreeSkill.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
},
"gate_type": {
"type": "string",
"enum": ["AND", "OR"],
"enum": ["AND", "OR", "XOR", "INHIBIT", "PRIORITY_AND"],
"description": "逻辑门类型,仅当 node_type 为 gate 时需要",
},
"remark": {
Expand Down Expand Up @@ -114,7 +114,7 @@
},
"gate_type": {
"type": "string",
"enum": ["AND", "OR"],
"enum": ["AND", "OR", "XOR", "INHIBIT", "PRIORITY_AND"],
"description": "逻辑门类型,仅 gate 节点需要",
},
"remark": {"type": "string", "description": "备注信息"},
Expand Down
63 changes: 63 additions & 0 deletions Backend/Application/UseCases/FolderUseCase.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""文件夹用例层,处理创建、重命名、删除文件夹及文件归类的业务逻辑。"""
import logging
from Backend.Domain.Entities.folder import Folder
from Backend.Application.Interfaces.IFolderRepository import IFolderRepository
from Backend.Application.Interfaces.IFileRepository import IFileRepository

logger = logging.getLogger(__name__)


class FolderUseCase:
def __init__(
self,
folder_repository: IFolderRepository,
file_repository: IFileRepository,
):
self._folder_repo = folder_repository
self._file_repo = file_repository

def create_folder(self, name: str) -> Folder:
existing = self._folder_repo.get_by_name(name)
if existing is not None:
raise ValueError(f"Folder already exists: {name}")
folder = Folder(name=name)
self._folder_repo.save(folder)
logger.info("Folder created: %s (%s)", folder.name, folder.id)
return folder

def rename_folder(self, folder_id: str, new_name: str) -> Folder:
folder = self._folder_repo.get_by_id(folder_id)
if folder is None:
raise ValueError(f"Folder not found: {folder_id}")
existing = self._folder_repo.get_by_name(new_name)
if existing is not None and existing.id != folder_id:
raise ValueError(f"Folder name already taken: {new_name}")
folder.rename(new_name)
self._folder_repo.update_name(folder_id, new_name)
logger.info("Folder renamed: %s -> %s", folder_id, new_name)
return folder

def list_folders(self) -> list[dict]:
folders = self._folder_repo.get_all()
result = []
for folder in folders:
files = self._file_repo.get_by_folder_id(folder.id)
folder_dict = folder.to_dict()
folder_dict["files"] = [f.to_dict() for f in files]
result.append(folder_dict)
return result

def delete_folder(self, folder_id: str) -> None:
folder = self._folder_repo.get_by_id(folder_id)
if folder is None:
raise ValueError(f"Folder not found: {folder_id}")
self._folder_repo.delete(folder_id)
logger.info("Folder deleted: %s (%s)", folder.name, folder_id)

def move_file_to_folder(self, file_id: str, folder_id: str | None) -> None:
if folder_id is not None:
folder = self._folder_repo.get_by_id(folder_id)
if folder is None:
raise ValueError(f"Folder not found: {folder_id}")
self._file_repo.update_folder(file_id, folder_id)
logger.info("File %s moved to folder %s", file_id, folder_id)
3 changes: 3 additions & 0 deletions Backend/Domain/Entities/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ def __init__(
file_id: str | None = None,
created_at: datetime | None = None,
status: FileStatus = FileStatus.PENDING,
folder_id: str | None = None,
):
self.id = file_id or str(uuid.uuid4())
self.file_name = file_name
self.file_type = file_type
self.created_at = created_at or datetime.now()
self.status = status
self.folder_id = folder_id

def mark_embedded(self):
self.status = FileStatus.EMBEDDED
Expand All @@ -32,4 +34,5 @@ def to_dict(self) -> dict:
"file_type": self.file_type.value,
"created_at": self.created_at.isoformat(),
"status": self.status.value,
"folder_id": self.folder_id,
}
25 changes: 25 additions & 0 deletions Backend/Domain/Entities/folder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""文件夹领域实体,用于对上传文件进行分类管理。"""
import uuid
from datetime import datetime


class Folder:
def __init__(
self,
name: str,
folder_id: str | None = None,
created_at: datetime | None = None,
):
self.id = folder_id or str(uuid.uuid4())
self.name = name
self.created_at = created_at or datetime.now()

def rename(self, new_name: str):
self.name = new_name

def to_dict(self) -> dict:
return {
"id": self.id,
"name": self.name,
"created_at": self.created_at.isoformat(),
}
21 changes: 19 additions & 2 deletions Backend/Infrastructure/persistence/FileRepository.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ def save(self, file: File) -> None:
conn = get_connection()
try:
conn.execute(
"INSERT INTO files (id, file_name, file_type, created_at, status) VALUES (?, ?, ?, ?, ?)",
(file.id, file.file_name, file.file_type.value, file.created_at.isoformat(), file.status.value),
"INSERT INTO files (id, file_name, file_type, created_at, status, folder_id) VALUES (?, ?, ?, ?, ?, ?)",
(file.id, file.file_name, file.file_type.value, file.created_at.isoformat(), file.status.value, file.folder_id),
)
conn.commit()
finally:
Expand Down Expand Up @@ -52,6 +52,22 @@ def update_status(self, file_id: str, status: FileStatus) -> None:
finally:
conn.close()

def update_folder(self, file_id: str, folder_id: str | None) -> None:
conn = get_connection()
try:
conn.execute("UPDATE files SET folder_id = ? WHERE id = ?", (folder_id, file_id))
conn.commit()
finally:
conn.close()

def get_by_folder_id(self, folder_id: str) -> list[File]:
conn = get_connection()
try:
rows = conn.execute("SELECT * FROM files WHERE folder_id = ? ORDER BY created_at DESC", (folder_id,)).fetchall()
return [self._row_to_entity(row) for row in rows]
finally:
conn.close()

@staticmethod
def _row_to_entity(row) -> File:
return File(
Expand All @@ -60,4 +76,5 @@ def _row_to_entity(row) -> File:
file_type=FileType(row["file_type"]),
created_at=datetime.fromisoformat(row["created_at"]),
status=FileStatus(row["status"]),
folder_id=row["folder_id"],
)
71 changes: 71 additions & 0 deletions Backend/Infrastructure/persistence/FolderRepository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""文件夹仓储的 SQLite 实现,负责文件夹的增删改查持久化。"""
from datetime import datetime
from Backend.Application.Interfaces.IFolderRepository import IFolderRepository
from Backend.Domain.Entities.folder import Folder
from Backend.Infrastructure.persistence.database import get_connection


class SQLiteFolderRepository(IFolderRepository):
def save(self, folder: Folder) -> None:
conn = get_connection()
try:
conn.execute(
"INSERT INTO folders (id, name, created_at) VALUES (?, ?, ?)",
(folder.id, folder.name, folder.created_at.isoformat()),
)
conn.commit()
finally:
conn.close()

def get_by_id(self, folder_id: str) -> Folder | None:
conn = get_connection()
try:
row = conn.execute("SELECT * FROM folders WHERE id = ?", (folder_id,)).fetchone()
if row is None:
return None
return self._row_to_entity(row)
finally:
conn.close()

def get_by_name(self, name: str) -> Folder | None:
conn = get_connection()
try:
row = conn.execute("SELECT * FROM folders WHERE name = ?", (name,)).fetchone()
if row is None:
return None
return self._row_to_entity(row)
finally:
conn.close()

def get_all(self) -> list[Folder]:
conn = get_connection()
try:
rows = conn.execute("SELECT * FROM folders ORDER BY created_at DESC").fetchall()
return [self._row_to_entity(row) for row in rows]
finally:
conn.close()

def update_name(self, folder_id: str, new_name: str) -> None:
conn = get_connection()
try:
conn.execute("UPDATE folders SET name = ? WHERE id = ?", (new_name, folder_id))
conn.commit()
finally:
conn.close()

def delete(self, folder_id: str) -> None:
conn = get_connection()
try:
conn.execute("UPDATE files SET folder_id = NULL WHERE folder_id = ?", (folder_id,))
conn.execute("DELETE FROM folders WHERE id = ?", (folder_id,))
conn.commit()
finally:
conn.close()

@staticmethod
def _row_to_entity(row) -> Folder:
return Folder(
folder_id=row["id"],
name=row["name"],
created_at=datetime.fromisoformat(row["created_at"]),
)
13 changes: 12 additions & 1 deletion Backend/Infrastructure/persistence/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,17 @@
file_name TEXT NOT NULL,
file_type TEXT NOT NULL,
created_at TEXT NOT NULL,
status TEXT NOT NULL
status TEXT NOT NULL,
folder_id TEXT,
FOREIGN KEY (folder_id) REFERENCES folders(id)
);
"""

_CREATE_FOLDERS_SQL = """
CREATE TABLE IF NOT EXISTS folders (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
created_at TEXT NOT NULL
);
"""

Expand Down Expand Up @@ -85,6 +95,7 @@ def init_db() -> None:
conn = get_connection()
try:
with _lock:
conn.execute(_CREATE_FOLDERS_SQL)
conn.execute(_CREATE_TABLE_SQL)
conn.execute(_CREATE_CONVERSATIONS_SQL)
conn.execute(_CREATE_CHAT_ROUNDS_SQL)
Expand Down
73 changes: 73 additions & 0 deletions Backend/Web/Endpoints/FolderEndpoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""文件夹管理 REST API 端点,提供文件夹的增删改查及文件移动接口。"""
from flask import Blueprint, request
from Backend.Application.UseCases.FolderUseCase import FolderUseCase

folder_bp = Blueprint("folders", __name__, url_prefix="/api/folders")


def create_folder_blueprint(folder_use_case: FolderUseCase) -> Blueprint:

@folder_bp.route("", methods=["POST"])
def create_folder():
data = request.get_json()
if not data or not data.get("name"):
return {"error": "Folder name is required"}, 400
name = data["name"].strip()
if not name:
return {"error": "Folder name cannot be empty"}, 400
try:
folder = folder_use_case.create_folder(name)
return folder.to_dict(), 201
except ValueError as e:
return {"error": str(e)}, 409

@folder_bp.route("", methods=["GET"])
def list_folders():
folders = folder_use_case.list_folders()
return folders

@folder_bp.route("/<folder_id>", methods=["PATCH"])
def rename_folder(folder_id: str):
data = request.get_json()
if not data or not data.get("name"):
return {"error": "New folder name is required"}, 400
new_name = data["name"].strip()
if not new_name:
return {"error": "Folder name cannot be empty"}, 400
try:
folder = folder_use_case.rename_folder(folder_id, new_name)
return folder.to_dict(), 200
except ValueError as e:
return {"error": str(e)}, 404

@folder_bp.route("/<folder_id>", methods=["DELETE"])
def delete_folder(folder_id: str):
try:
folder_use_case.delete_folder(folder_id)
return {"message": "Folder deleted"}, 200
except ValueError as e:
return {"error": str(e)}, 404

@folder_bp.route("/<folder_id>/files", methods=["POST"])
def move_file_to_folder(folder_id: str):
data = request.get_json()
if not data or not data.get("file_id"):
return {"error": "file_id is required"}, 400
try:
folder_use_case.move_file_to_folder(data["file_id"], folder_id)
return {"message": "File moved to folder"}, 200
except ValueError as e:
return {"error": str(e)}, 404

@folder_bp.route("/unfile", methods=["POST"])
def remove_file_from_folder():
data = request.get_json()
if not data or not data.get("file_id"):
return {"error": "file_id is required"}, 400
try:
folder_use_case.move_file_to_folder(data["file_id"], None)
return {"message": "File removed from folder"}, 200
except ValueError as e:
return {"error": str(e)}, 404

return folder_bp
Loading