Align knowledge storage with real folders and add WebDAV import surface
Knowledge files were only partitioned in the database, which made nested uploads, local folder visibility, and delete behavior diverge from the UI. This change makes folder selection drive physical storage paths, keeps original filenames, adds a minimal WebDAV mount/sync path, and reshapes the knowledge panel so local and remote sources can share the same surface. Constraint: Existing knowledge flow already depends on local-folder-backed uploads and document indexing Rejected: Real-time bidirectional WebDAV sync | too much conflict and lifecycle complexity for the first pass Confidence: medium Scope-risk: moderate Reversibility: messy Directive: Keep remote mounts single-direction into local knowledge folders until etag-based incremental sync and conflict rules are verified Tested: Python py_compile on new/modified backend files; LSP diagnostics on new frontend/backend files; manual targeted code-path inspection Not-tested: Full pytest/vitest end-to-end runs blocked by environment temp/cache permission errors; live WebDAV server interoperability
This commit is contained in:
@@ -1,17 +1,20 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import and_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, and_
|
||||
from typing import List
|
||||
import shutil
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.folder import Folder
|
||||
from app.models.user import User
|
||||
from app.schemas.folder import FolderCreate, FolderUpdate, FolderOut, FolderTreeOut
|
||||
from app.routers.auth import get_current_user
|
||||
from app.schemas.folder import FolderCreate, FolderOut, FolderTreeOut, FolderUpdate
|
||||
from app.services.document_service import DocumentService
|
||||
|
||||
router = APIRouter(prefix="/api/folders", tags=["文件夹"])
|
||||
|
||||
|
||||
def build_folder_tree(folders: list[Folder], parent_id: str = None) -> List[FolderTreeOut]:
|
||||
"""递归构建文件夹树"""
|
||||
tree = []
|
||||
for folder in folders:
|
||||
if folder.parent_id == parent_id:
|
||||
@@ -20,30 +23,29 @@ def build_folder_tree(folders: list[Folder], parent_id: str = None) -> List[Fold
|
||||
id=folder.id,
|
||||
name=folder.name,
|
||||
parent_id=folder.parent_id,
|
||||
children=children
|
||||
children=children,
|
||||
))
|
||||
return tree
|
||||
|
||||
|
||||
@router.get("", response_model=List[FolderTreeOut])
|
||||
async def get_folders(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取用户的完整文件夹树"""
|
||||
result = await db.execute(
|
||||
select(Folder).where(Folder.user_id == current_user.id)
|
||||
)
|
||||
folders = result.scalars().all()
|
||||
return build_folder_tree(list(folders))
|
||||
|
||||
|
||||
@router.post("", response_model=FolderOut, status_code=status.HTTP_201_CREATED)
|
||||
async def create_folder(
|
||||
folder_data: FolderCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""创建文件夹"""
|
||||
# 验证父文件夹存在且属于当前用户
|
||||
if folder_data.parent_id:
|
||||
result = await db.execute(
|
||||
select(Folder).where(
|
||||
@@ -53,13 +55,12 @@ async def create_folder(
|
||||
if not result.scalar_one_or_none():
|
||||
raise HTTPException(status_code=404, detail="父文件夹不存在")
|
||||
|
||||
# 检查同名文件夹
|
||||
result = await db.execute(
|
||||
select(Folder).where(
|
||||
and_(
|
||||
Folder.user_id == current_user.id,
|
||||
Folder.parent_id == folder_data.parent_id,
|
||||
Folder.name == folder_data.name
|
||||
Folder.name == folder_data.name,
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -69,21 +70,24 @@ async def create_folder(
|
||||
folder = Folder(
|
||||
user_id=current_user.id,
|
||||
name=folder_data.name,
|
||||
parent_id=folder_data.parent_id
|
||||
parent_id=folder_data.parent_id,
|
||||
)
|
||||
db.add(folder)
|
||||
await db.commit()
|
||||
await db.refresh(folder)
|
||||
|
||||
document_service = DocumentService(db, current_user.id)
|
||||
await document_service.ensure_folder_directory(current_user.id, folder.id)
|
||||
return folder
|
||||
|
||||
|
||||
@router.put("/{folder_id}", response_model=FolderOut)
|
||||
async def rename_folder(
|
||||
folder_id: str,
|
||||
folder_data: FolderUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""重命名文件夹"""
|
||||
result = await db.execute(
|
||||
select(Folder).where(
|
||||
and_(Folder.id == folder_id, Folder.user_id == current_user.id)
|
||||
@@ -93,18 +97,22 @@ async def rename_folder(
|
||||
if not folder:
|
||||
raise HTTPException(status_code=404, detail="文件夹不存在")
|
||||
|
||||
old_name = folder.name
|
||||
folder.name = folder_data.name
|
||||
|
||||
document_service = DocumentService(db, current_user.id)
|
||||
await document_service.rename_folder_directory(current_user.id, folder.id, old_name, folder_data.name)
|
||||
await db.commit()
|
||||
await db.refresh(folder)
|
||||
return folder
|
||||
|
||||
|
||||
@router.delete("/{folder_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_folder(
|
||||
folder_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""删除文件夹(级联删除文档)"""
|
||||
from app.models.document import Document
|
||||
from app.services.knowledge_service import KnowledgeService
|
||||
|
||||
@@ -117,15 +125,16 @@ async def delete_folder(
|
||||
if not folder:
|
||||
raise HTTPException(status_code=404, detail="文件夹不存在")
|
||||
|
||||
document_service = DocumentService(db, current_user.id)
|
||||
folder_path = await document_service._get_storage_directory(current_user.id, folder_id)
|
||||
|
||||
async def delete_recursive(fid: str):
|
||||
# 删除子文件夹(先递归)
|
||||
children = await db.execute(
|
||||
select(Folder).where(Folder.parent_id == fid)
|
||||
)
|
||||
for child in children.scalars():
|
||||
await delete_recursive(child.id)
|
||||
|
||||
# 删除文档
|
||||
docs = await db.execute(
|
||||
select(Document).where(Document.folder_id == fid)
|
||||
)
|
||||
@@ -134,10 +143,12 @@ async def delete_folder(
|
||||
await knowledge_service.delete_from_vectorstore(current_user.id, doc.id)
|
||||
await db.delete(doc)
|
||||
|
||||
# 删除文件夹本身
|
||||
folder_to_delete = await db.get(Folder, fid)
|
||||
if folder_to_delete:
|
||||
await db.delete(folder_to_delete)
|
||||
|
||||
await delete_recursive(folder_id)
|
||||
await db.commit()
|
||||
|
||||
if folder_path.exists():
|
||||
shutil.rmtree(folder_path, ignore_errors=True)
|
||||
|
||||
Reference in New Issue
Block a user