Files
JARVIS/backend/app/routers/folder.py
WIN-JHFT4D3SIVT\caoxiaozhu 8c7cf0732b 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
2026-04-09 17:26:37 +08:00

155 lines
5.0 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import and_, select
from sqlalchemy.ext.asyncio import AsyncSession
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.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:
children = build_folder_tree(folders, folder.id)
tree.append(FolderTreeOut(
id=folder.id,
name=folder.name,
parent_id=folder.parent_id,
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),
):
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),
):
if folder_data.parent_id:
result = await db.execute(
select(Folder).where(
and_(Folder.id == folder_data.parent_id, Folder.user_id == current_user.id)
)
)
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,
)
)
)
if result.scalar_one_or_none():
raise HTTPException(status_code=400, detail="同名文件夹已存在")
folder = Folder(
user_id=current_user.id,
name=folder_data.name,
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),
):
result = await db.execute(
select(Folder).where(
and_(Folder.id == folder_id, Folder.user_id == current_user.id)
)
)
folder = result.scalar_one_or_none()
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),
):
from app.models.document import Document
from app.services.knowledge_service import KnowledgeService
result = await db.execute(
select(Folder).where(
and_(Folder.id == folder_id, Folder.user_id == current_user.id)
)
)
folder = result.scalar_one_or_none()
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)
)
for doc in docs.scalars():
knowledge_service = KnowledgeService(db, current_user.id)
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)