This commit is contained in:
Timothy Jaeryang Baek
2026-09-21 10:19:59 -04:00
parent aceda892bd
commit a9541c18ca
5 changed files with 57 additions and 39 deletions
+6 -1
View File
@@ -625,6 +625,7 @@ class KnowledgeTable:
db=db,
),
breadcrumbs=await self.get_directory_breadcrumbs(
knowledge_id,
filter.get('directory_id') if filter else None,
db=db,
),
@@ -908,6 +909,7 @@ class KnowledgeTable:
async def get_directory_breadcrumbs(
self,
knowledge_id: str,
directory_id: Optional[str],
db: Optional[AsyncSession] = None,
) -> list[KnowledgeDirectoryModel]:
@@ -922,7 +924,10 @@ class KnowledgeTable:
while current_id and current_id not in seen:
seen.add(current_id)
result = await db.execute(select(KnowledgeDirectory).filter_by(id=current_id))
# Scoped by knowledge base so a caller-supplied id cannot walk another one's tree.
result = await db.execute(
select(KnowledgeDirectory).filter_by(id=current_id, knowledge_id=knowledge_id)
)
directory = result.scalars().first()
if not directory:
break
+10 -1
View File
@@ -224,6 +224,15 @@ async def process_uploaded_file(
f'{knowledge_id}: user {user.id} lacks write access'
)
else:
directory_id = file_metadata.get('directory_id') or None
if directory_id:
directory = await Knowledges.get_directory_by_id(directory_id, db=db_session)
if not directory or directory.knowledge_id != knowledge_id:
log.warning(
'Ignoring directory %s: not a directory of knowledge %s', directory_id, knowledge_id
)
directory_id = None
# Keep the generic file status stream open until the
# KB-specific vector write and durable link both finish.
await Files.update_file_data_by_id(file_item.id, {'status': 'processing'}, db=db_session)
@@ -237,7 +246,7 @@ async def process_uploaded_file(
knowledge_id=knowledge_id,
file_id=file_item.id,
user_id=user.id,
directory_id=file_metadata.get('directory_id'),
directory_id=directory_id,
db=db_session,
)
if not knowledge_file:
+31 -23
View File
@@ -151,6 +151,24 @@ def external_knowledge_error():
)
async def _verify_directory_in_knowledge(
id: str,
directory_id: str | None,
db: AsyncSession,
detail: str = ERROR_MESSAGES.NOT_FOUND,
):
"""Verify a caller-supplied directory belongs to the knowledge base in the URL. Unset means the root level."""
if not directory_id:
return None
directory = await Knowledges.get_directory_by_id(directory_id, db=db)
if not directory or directory.knowledge_id != id:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=detail,
)
@router.get('/', response_model=KnowledgeAccessListResponse)
async def get_knowledge_bases(
page: int | None = 1,
@@ -1437,6 +1455,8 @@ async def add_file_to_knowledge_by_id(
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
await _verify_directory_in_knowledge(id, form_data.directory_id, db, detail='Target directory not found.')
file = await Files.get_file_by_id(form_data.file_id, db=db)
if not file:
raise HTTPException(
@@ -2049,6 +2069,9 @@ async def add_files_to_knowledge_batch(
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
for directory_id in {form.directory_id for form in form_data if form.directory_id}:
await _verify_directory_in_knowledge(id, directory_id, db, detail='Target directory not found.')
# Batch-fetch all files to avoid N+1 queries
log.info('files/batch/add - %s files', len(form_data))
file_ids = [form.file_id for form in form_data]
@@ -2237,6 +2260,8 @@ async def create_knowledge_directory(
):
await _verify_knowledge_write_access(id, user, db)
await _verify_directory_in_knowledge(id, form_data.parent_id, db, detail='Parent directory not found.')
directory = await Knowledges.create_directory(
knowledge_id=id,
name=form_data.name,
@@ -2269,14 +2294,11 @@ async def update_knowledge_directory(
db: AsyncSession = Depends(get_async_session),
):
await _verify_knowledge_write_access(id, user, db)
await _verify_directory_in_knowledge(id, dir_id, db)
# Verify directory belongs to this knowledge base
directory = await Knowledges.get_directory_by_id(dir_id, db=db)
if not directory or directory.knowledge_id != id:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
# '__unset__' leaves the parent alone, None moves the directory to the root
if form_data.parent_id not in (None, '__unset__'):
await _verify_directory_in_knowledge(id, form_data.parent_id, db, detail='Parent directory not found.')
result = await Knowledges.update_directory(
directory_id=dir_id,
@@ -2309,14 +2331,7 @@ async def delete_knowledge_directory(
db: AsyncSession = Depends(get_async_session),
):
await _verify_knowledge_write_access(id, user, db)
# Verify directory belongs to this knowledge base
directory = await Knowledges.get_directory_by_id(dir_id, db=db)
if not directory or directory.knowledge_id != id:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
await _verify_directory_in_knowledge(id, dir_id, db)
# Collect before delete_directory drops the KnowledgeFile rows
files = [] if move_files else await Knowledges.get_files_by_id_and_directory_id(id, dir_id, db=db)
@@ -2375,14 +2390,7 @@ async def move_file_in_knowledge(
detail=ERROR_MESSAGES.NOT_FOUND,
)
# If target directory is set, verify it belongs to this knowledge base
if form_data.directory_id:
directory = await Knowledges.get_directory_by_id(form_data.directory_id, db=db)
if not directory or directory.knowledge_id != id:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail='Target directory not found.',
)
await _verify_directory_in_knowledge(id, form_data.directory_id, db, detail='Target directory not found.')
success = await Knowledges.move_file_to_directory(
knowledge_id=id,
+8 -12
View File
@@ -26,7 +26,7 @@
import { createMessagesList } from '$lib/utils';
import { getOutputText } from '$lib/components/chat/Messages/structuredOutput';
import { config, user, chatId as currentChatId, tags } from '$lib/stores';
import { refreshChatList } from '$lib/stores/chatList';
import { refreshSidebar } from '$lib/stores/chatList';
import Messages from '../chat/Messages.svelte';
import { goto } from '$app/navigation';
import EditPencilIcon from './Sidebar/icons/EditPencil.svelte';
@@ -65,10 +65,6 @@
};
let generating = false;
const refreshSidebar = async () => {
await refreshChatList(localStorage.token, { refreshPinned: true });
};
const cloneChatHandler = async (id) => {
const chat = chatList?.find((c) => c.id === id);
const res = await cloneChatById(
@@ -83,7 +79,7 @@
});
if (res) {
await refreshSidebar();
await refreshSidebar(localStorage.token);
await searchHandler();
}
};
@@ -95,7 +91,7 @@
});
if (res) {
await refreshSidebar();
await refreshSidebar(localStorage.token);
}
};
@@ -110,7 +106,7 @@
currentChatId.set('');
}
await refreshSidebar();
await refreshSidebar(localStorage.token);
toast.success(res?.archived ? $i18n.t('Chat archived.') : $i18n.t('Chat unarchived.'));
} catch (error) {
toast.error($i18n.t('Failed to archive chat.'));
@@ -132,7 +128,7 @@
currentChatId.set('');
}
await refreshSidebar();
await refreshSidebar(localStorage.token);
}
};
@@ -147,7 +143,7 @@
if (res) {
chatList = chatList?.filter((c) => c.id !== chatId) ?? null;
await refreshSidebar();
await refreshSidebar(localStorage.token);
toast.success($i18n.t('Chat moved successfully'));
}
}
@@ -182,7 +178,7 @@
editingChatId = null;
editingChatTitle = '';
await refreshSidebar();
await refreshSidebar(localStorage.token);
};
const cancelRename = () => {
@@ -841,7 +837,7 @@
}}
onClose={() => {}}
onPinChange={async () => {
await refreshSidebar();
await refreshSidebar(localStorage.token);
await searchHandler();
}}
>
@@ -41,7 +41,7 @@
settings,
user
} from '$lib/stores';
import { refreshChatList } from '$lib/stores/chatList';
import { refreshChatList, refreshSidebar } from '$lib/stores/chatList';
import ChatMenu from './ChatMenu.svelte';
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
@@ -241,7 +241,7 @@
if (res) {
goto(`/c/${res.id}`);
await refreshChatList(localStorage.token, { refreshPinned: true });
await refreshSidebar(localStorage.token);
}
};