Coverage for open_webui/routers/folders.py: 60%
313 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 05:07 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 05:07 +0000
1import logging
2import mimetypes
3import os
4import shutil
5import uuid
6from pathlib import Path
7from typing import Optional
9from fastapi import APIRouter, Depends, File, HTTPException, Query, Request, UploadFile, status
10from fastapi.responses import FileResponse, StreamingResponse
11from open_webui.config import UPLOAD_DIR
12from open_webui.constants import ERROR_MESSAGES
13from open_webui.events import EVENTS, publish_event
14from open_webui.internal.db import get_async_session
15from open_webui.models.chat_messages import ChatMessages
16from open_webui.models.config import Config
17from open_webui.models.chats import Chats
18from open_webui.models.folders import (
19 FolderForm,
20 FolderModel,
21 FolderNameIdResponse,
22 Folders,
23 FolderUpdateForm,
24)
25from open_webui.models.access_grants import AccessGrants
26from open_webui.models.automations import Automations
27from open_webui.models.groups import Groups
28from open_webui.models.users import Users
29from open_webui.utils.access_control import has_permission
30from open_webui.utils.access_control import (
31 filter_allowed_access_grants,
32)
33from open_webui.utils.access_control.files import can_read_all_folder_files, get_accessible_folder_files
34from open_webui.utils.auth import get_admin_user, get_verified_user
35from open_webui.tasks import has_active_tasks
36from pydantic import BaseModel
37from sqlalchemy.ext.asyncio import AsyncSession
39log = logging.getLogger(__name__)
42router = APIRouter()
45from open_webui.utils.access_control.folders import has_folder_access as _has_folder_access
48async def get_folder_unread_counts(user_id: str, db: AsyncSession | None = None) -> dict[str, int]:
49 folders = await Folders.get_folders_by_user_id(user_id, db=db)
50 parent_by_id = {folder.id: folder.parent_id for folder in folders}
51 unread_counts = dict.fromkeys(parent_by_id.keys(), 0)
52 direct_unread_counts = await Chats.count_unread_by_folder_ids(user_id, list(parent_by_id.keys()), db=db)
54 for unread_folder_id, unread_count in direct_unread_counts.items(): 54 ↛ 55line 54 didn't jump to line 55 because the loop on line 54 never started
55 current_id = unread_folder_id
56 seen = set()
57 while current_id and current_id not in seen:
58 seen.add(current_id)
59 if current_id in unread_counts:
60 unread_counts[current_id] += unread_count
61 current_id = parent_by_id.get(current_id)
63 return unread_counts
66async def check_folders_permission(request: Request, user, db=None):
67 """Verify the folders feature is enabled and the user has permission."""
68 config = await Config.get_many('folders.enable', 'user.permissions')
69 if config.get('folders.enable') is False: 69 ↛ 70line 69 didn't jump to line 70 because the condition on line 69 was never true
70 raise HTTPException(
71 status_code=status.HTTP_403_FORBIDDEN,
72 detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
73 )
74 if user.role != 'admin' and not await has_permission( 74 ↛ 80line 74 didn't jump to line 80 because the condition on line 74 was never true
75 user.id,
76 'features.folders',
77 config.get('user.permissions'),
78 db=db,
79 ):
80 raise HTTPException(
81 status_code=status.HTTP_403_FORBIDDEN,
82 detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
83 )
86############################
87# Get Folders
88############################
91@router.get('/', response_model=list[FolderNameIdResponse])
92async def get_folders(
93 request: Request,
94 user=Depends(get_verified_user),
95 db: AsyncSession = Depends(get_async_session),
96):
97 await check_folders_permission(request, user, db=db)
99 folders = await Folders.get_folders_by_user_id(user.id, db=db)
100 parent_by_id = {folder.id: folder.parent_id for folder in folders}
102 def is_in_parent_cycle(folder_id):
103 seen_ids = {folder_id}
104 current_id = parent_by_id.get(folder_id)
105 while current_id and current_id not in seen_ids:
106 seen_ids.add(current_id)
107 current_id = parent_by_id.get(current_id)
108 return current_id == folder_id
110 user_group_ids = None
111 if user.role != 'admin' and any(folder.data and 'files' in folder.data for folder in folders): 111 ↛ 112line 111 didn't jump to line 112 because the condition on line 111 was never true
112 user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id, db=db)}
114 # Verify folder data integrity
115 folder_list = []
116 for folder in folders:
117 # A missing or looping parent hides the folder from the tree, so put it back at the root
118 if folder.parent_id and (folder.parent_id not in parent_by_id or is_in_parent_cycle(folder.id)):
119 parent_by_id[folder.id] = None
120 folder = await Folders.update_folder_parent_id_by_id_and_user_id(folder.id, user.id, None, db=db)
122 if folder.data and 'files' in folder.data: 122 ↛ 123line 122 didn't jump to line 123 because the condition on line 122 was never true
123 accessible_files = await get_accessible_folder_files(
124 folder.data['files'], user, db=db, user_group_ids=user_group_ids
125 )
126 if len(accessible_files) != len(folder.data.get('files', [])):
127 folder.data['files'] = accessible_files
128 await Folders.update_folder_by_id_and_user_id(
129 folder.id, user.id, FolderUpdateForm(data=folder.data), db=db
130 )
132 folder_list.append(folder)
134 unread_counts = await get_folder_unread_counts(user.id, db=db)
136 return [
137 FolderNameIdResponse(**folder.model_dump(), unread_count=unread_counts.get(folder.id, 0))
138 for folder in folder_list
139 ]
142############################
143# Create Folder
144############################
147@router.post('/')
148async def create_folder(
149 request: Request,
150 form_data: FolderForm,
151 user=Depends(get_verified_user),
152 db: AsyncSession = Depends(get_async_session),
153):
154 await check_folders_permission(request, user, db=db)
155 folder = await Folders.get_folder_by_parent_id_and_user_id_and_name(
156 form_data.parent_id, user.id, form_data.name, db=db
157 )
159 if folder:
160 raise HTTPException(
161 status_code=status.HTTP_400_BAD_REQUEST,
162 detail=ERROR_MESSAGES.DEFAULT('Folder already exists'),
163 )
165 # Check if creating a subfolder in a shared folder
166 if form_data.parent_id:
167 parent = await Folders.get_folder_by_id(form_data.parent_id, db=db)
168 if parent and parent.user_id != user.id: 168 ↛ 170line 168 didn't jump to line 170 because the condition on line 168 was never true
169 # Creating subfolder in someone else's shared folder
170 if user.role != 'admin' and not await _has_folder_access(user.id, parent, 'write', db):
171 raise HTTPException(
172 status_code=status.HTTP_403_FORBIDDEN,
173 detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
174 )
175 if form_data.data and 'files' in form_data.data:
176 owner = await Users.get_user_by_id(parent.user_id, db=db)
177 if not owner:
178 raise HTTPException(
179 status_code=status.HTTP_404_NOT_FOUND,
180 detail=ERROR_MESSAGES.NOT_FOUND,
181 )
182 if not await can_read_all_folder_files(form_data.data['files'], owner, db=db):
183 raise HTTPException(
184 status_code=status.HTTP_403_FORBIDDEN,
185 detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
186 )
187 # Create as the folder owner's subfolder (keep tree consistent)
188 try:
189 folder = await Folders.insert_new_folder(parent.user_id, form_data, form_data.parent_id, db=db)
190 await publish_event(
191 request,
192 EVENTS.FOLDER_CREATED,
193 actor=user,
194 subject_id=folder.id,
195 data={'name': folder.name, 'parent_id': folder.parent_id, 'owner_id': folder.user_id},
196 )
197 return folder
198 except Exception as e:
199 log.exception(e)
200 raise HTTPException(
201 status_code=status.HTTP_400_BAD_REQUEST,
202 detail=ERROR_MESSAGES.DEFAULT('Error creating folder'),
203 )
205 if ( 205 ↛ 210line 205 didn't jump to line 210 because the condition on line 205 was never true
206 form_data.data
207 and 'files' in form_data.data
208 and not await can_read_all_folder_files(form_data.data['files'], user, db=db)
209 ):
210 raise HTTPException(
211 status_code=status.HTTP_403_FORBIDDEN,
212 detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
213 )
215 try:
216 folder = await Folders.insert_new_folder(user.id, form_data, form_data.parent_id, db=db)
217 await publish_event(
218 request,
219 EVENTS.FOLDER_CREATED,
220 actor=user,
221 subject_id=folder.id,
222 data={'name': folder.name, 'parent_id': folder.parent_id, 'owner_id': folder.user_id},
223 )
224 return folder
225 except Exception as e:
226 log.exception(e)
227 log.error('Error creating folder')
228 raise HTTPException(
229 status_code=status.HTTP_400_BAD_REQUEST,
230 detail=ERROR_MESSAGES.DEFAULT('Error creating folder'),
231 )
234############################
235# Get Shared Folders
236############################
239@router.get('/shared')
240async def get_shared_folders(
241 request: Request,
242 user=Depends(get_verified_user),
243 db: AsyncSession = Depends(get_async_session),
244):
245 """Get all folders shared with the current user (not owned by them)."""
246 await check_folders_permission(request, user, db=db)
247 groups = await Groups.get_groups_by_member_id(user.id, db=db)
248 group_ids = {g.id for g in groups}
250 folder_perms = await Folders.get_shared_folder_ids_for_user(user.id, group_ids, db=db)
252 folders = await Folders.get_folders_by_ids(list(folder_perms.keys()), db=db)
253 shared_folders = [folder for folder in folders if folder.user_id != user.id]
255 owners = await Users.get_users_by_user_ids([folder.user_id for folder in shared_folders], db=db)
256 owner_names = {owner.id: owner.name for owner in owners}
258 results = [
259 {
260 **folder.model_dump(),
261 'owner_name': owner_names.get(folder.user_id, 'Unknown'),
262 'permission': folder_perms[folder.id],
263 }
264 for folder in shared_folders
265 ]
267 # Also include child folders of shared folders (inheritance)
268 seen_ids = {folder.id for folder in shared_folders}
269 for folder in shared_folders: 269 ↛ 270line 269 didn't jump to line 270 because the loop on line 269 never started
270 children = await Folders.get_children_folders_by_id_and_user_id(folder.id, folder.user_id, db=db)
271 for child in children or []:
272 if child.id not in seen_ids:
273 seen_ids.add(child.id)
274 results.append(
275 {
276 **child.model_dump(),
277 'owner_name': owner_names.get(child.user_id, 'Unknown'),
278 'permission': folder_perms[folder.id],
279 }
280 )
282 return results
285############################
286# Get Folders By Id
287############################
290class FolderResponse(FolderModel):
291 access_grants: list[dict] = []
292 write_access: bool = False
295@router.get('/{id}', response_model=FolderResponse)
296async def get_folder_by_id(
297 request: Request, id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)
298):
299 await check_folders_permission(request, user, db=db)
300 folder = await Folders.get_folder_by_id_and_user_id(id, user.id, db=db)
301 if folder:
302 grants = await AccessGrants.get_grants_by_resource('folder', id, db=db)
303 return FolderResponse(
304 **folder.model_dump(),
305 access_grants=[g.model_dump() for g in grants],
306 write_access=True,
307 )
309 # Check shared access
310 folder = await Folders.get_folder_by_id(id, db=db)
311 if folder and (user.role == 'admin' or await _has_folder_access(user.id, folder, 'read', db)): 311 ↛ 312line 311 didn't jump to line 312 because the condition on line 311 was never true
312 grants = await AccessGrants.get_grants_by_resource('folder', id, db=db)
313 return FolderResponse(
314 **folder.model_dump(),
315 access_grants=[g.model_dump() for g in grants],
316 write_access=user.role == 'admin' or await _has_folder_access(user.id, folder, 'write', db),
317 )
319 raise HTTPException(
320 status_code=status.HTTP_404_NOT_FOUND,
321 detail=ERROR_MESSAGES.NOT_FOUND,
322 )
325############################
326# Update Folder Name By Id
327############################
330@router.post('/{id}/update')
331async def update_folder_name_by_id(
332 request: Request,
333 id: str,
334 form_data: FolderUpdateForm,
335 user=Depends(get_verified_user),
336 db: AsyncSession = Depends(get_async_session),
337):
338 await check_folders_permission(request, user, db=db)
339 folder = await Folders.get_folder_by_id_and_user_id(id, user.id, db=db)
340 if not folder:
341 # Check shared write access
342 folder = await Folders.get_folder_by_id(id, db=db)
343 if not folder or (user.role != 'admin' and not await _has_folder_access(user.id, folder, 'write', db)): 343 ↛ 349line 343 didn't jump to line 349 because the condition on line 343 was always true
344 raise HTTPException(
345 status_code=status.HTTP_404_NOT_FOUND,
346 detail=ERROR_MESSAGES.NOT_FOUND,
347 )
349 if folder: 349 ↛ exitline 349 didn't return from function 'update_folder_name_by_id' because the condition on line 349 was always true
350 if form_data.name is not None:
351 # Check if folder with same name exists
352 existing_folder = await Folders.get_folder_by_parent_id_and_user_id_and_name(
353 folder.parent_id, folder.user_id, form_data.name, db=db
354 )
355 if existing_folder and existing_folder.id != id:
356 raise HTTPException(
357 status_code=status.HTTP_400_BAD_REQUEST,
358 detail=ERROR_MESSAGES.DEFAULT('Folder already exists'),
359 )
361 if form_data.data and 'files' in form_data.data: 361 ↛ 362line 361 didn't jump to line 362 because the condition on line 361 was never true
362 owner = user if folder.user_id == user.id else await Users.get_user_by_id(folder.user_id, db=db)
363 if not owner:
364 raise HTTPException(
365 status_code=status.HTTP_404_NOT_FOUND,
366 detail=ERROR_MESSAGES.NOT_FOUND,
367 )
368 if not await can_read_all_folder_files(form_data.data['files'], owner, db=db):
369 raise HTTPException(
370 status_code=status.HTTP_403_FORBIDDEN,
371 detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
372 )
374 try:
375 folder = await Folders.update_folder_by_id_and_user_id(id, folder.user_id, form_data, db=db)
376 await publish_event(
377 request,
378 EVENTS.FOLDER_UPDATED,
379 actor=user,
380 subject_id=id,
381 data={'name': folder.name},
382 )
383 return folder
384 except Exception as e:
385 log.exception(e)
386 log.error(f'Error updating folder: {id}')
387 raise HTTPException(
388 status_code=status.HTTP_400_BAD_REQUEST,
389 detail=ERROR_MESSAGES.DEFAULT('Error updating folder'),
390 )
393############################
394# Update Folder Parent Id By Id
395############################
398class FolderParentIdForm(BaseModel):
399 parent_id: Optional[str] = None
402@router.post('/{id}/update/parent')
403async def update_folder_parent_id_by_id(
404 request: Request,
405 id: str,
406 form_data: FolderParentIdForm,
407 user=Depends(get_verified_user),
408 db: AsyncSession = Depends(get_async_session),
409):
410 await check_folders_permission(request, user, db=db)
411 folder = await Folders.get_folder_by_id_and_user_id(id, user.id, db=db)
412 if folder:
413 existing_folder = await Folders.get_folder_by_parent_id_and_user_id_and_name(
414 form_data.parent_id, user.id, folder.name, db=db
415 )
417 if existing_folder and existing_folder.id != id:
418 raise HTTPException(
419 status_code=status.HTTP_400_BAD_REQUEST,
420 detail=ERROR_MESSAGES.DEFAULT('Folder already exists'),
421 )
423 if form_data.parent_id and form_data.parent_id in await Folders.get_folder_ids_by_id_and_user_id_in_subtree( 423 ↛ 426line 423 didn't jump to line 426 because the condition on line 423 was never true
424 id, user.id, db=db
425 ):
426 raise HTTPException(
427 status_code=status.HTTP_400_BAD_REQUEST,
428 detail=ERROR_MESSAGES.DEFAULT('Cannot move a folder into itself or one of its subfolders'),
429 )
431 try:
432 folder = await Folders.update_folder_parent_id_by_id_and_user_id(id, user.id, form_data.parent_id, db=db)
433 await publish_event(
434 request,
435 EVENTS.FOLDER_PARENT_UPDATED,
436 actor=user,
437 subject_id=id,
438 data={'parent_id': form_data.parent_id},
439 )
440 return folder
441 except Exception as e:
442 log.exception(e)
443 log.error(f'Error updating folder: {id}')
444 raise HTTPException(
445 status_code=status.HTTP_400_BAD_REQUEST,
446 detail=ERROR_MESSAGES.DEFAULT('Error updating folder'),
447 )
448 else:
449 raise HTTPException(
450 status_code=status.HTTP_404_NOT_FOUND,
451 detail=ERROR_MESSAGES.NOT_FOUND,
452 )
455############################
456# Update Folder Is Expanded By Id
457############################
460class FolderIsExpandedForm(BaseModel):
461 is_expanded: bool
464@router.post('/{id}/update/expanded')
465async def update_folder_is_expanded_by_id(
466 request: Request,
467 id: str,
468 form_data: FolderIsExpandedForm,
469 user=Depends(get_verified_user),
470 db: AsyncSession = Depends(get_async_session),
471):
472 await check_folders_permission(request, user, db=db)
473 folder = await Folders.get_folder_by_id_and_user_id(id, user.id, db=db)
474 if not folder:
475 folder = await Folders.get_folder_by_id(id, db=db)
476 if folder and (user.role == 'admin' or await _has_folder_access(user.id, folder, 'read', db)): 476 ↛ 477line 476 didn't jump to line 477 because the condition on line 476 was never true
477 return folder
479 if folder:
480 try:
481 folder = await Folders.update_folder_is_expanded_by_id_and_user_id(
482 id, user.id, form_data.is_expanded, db=db
483 )
484 return folder
485 except Exception as e:
486 log.exception(e)
487 log.error(f'Error updating folder: {id}')
488 raise HTTPException(
489 status_code=status.HTTP_400_BAD_REQUEST,
490 detail=ERROR_MESSAGES.DEFAULT('Error updating folder'),
491 )
492 else:
493 raise HTTPException(
494 status_code=status.HTTP_404_NOT_FOUND,
495 detail=ERROR_MESSAGES.NOT_FOUND,
496 )
499############################
500# Update Folder Access By Id
501############################
504class FolderAccessGrantsForm(BaseModel):
505 access_grants: list[dict]
508@router.post('/{id}/access/update')
509async def update_folder_access_by_id(
510 request: Request,
511 id: str,
512 form_data: FolderAccessGrantsForm,
513 user=Depends(get_verified_user),
514 db: AsyncSession = Depends(get_async_session),
515):
516 await check_folders_permission(request, user, db=db)
517 folder = await Folders.get_folder_by_id(id, db=db)
518 if not folder:
519 raise HTTPException(
520 status_code=status.HTTP_404_NOT_FOUND,
521 detail=ERROR_MESSAGES.NOT_FOUND,
522 )
524 # Only owner, admin, or write-granted user can update access
525 if user.role != 'admin' and user.id != folder.user_id: 525 ↛ 526line 525 didn't jump to line 526 because the condition on line 525 was never true
526 if not await _has_folder_access(user.id, folder, 'write', db):
527 raise HTTPException(
528 status_code=status.HTTP_403_FORBIDDEN,
529 detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
530 )
532 form_data.access_grants = await filter_allowed_access_grants(
533 await Config.get('user.permissions'),
534 user.id,
535 user.role,
536 form_data.access_grants,
537 None,
538 db=db,
539 )
541 await AccessGrants.set_access_grants('folder', id, form_data.access_grants, db=db)
543 grants = await AccessGrants.get_grants_by_resource('folder', id, db=db)
544 await publish_event(
545 request,
546 EVENTS.FOLDER_ACCESS_UPDATED,
547 actor=user,
548 subject_id=id,
549 data={'grant_count': len(grants)},
550 )
551 return {
552 **folder.model_dump(),
553 'access_grants': [g.model_dump() for g in grants],
554 }
557############################
558# Get Shared Folder Chats
559############################
562@router.get('/{id}/shared/chats')
563async def get_shared_folder_chats(
564 request: Request,
565 id: str,
566 page: int | None = Query(None, ge=1),
567 sort_by: str = Query('unread_updated_at'),
568 sort_dir: str = Query('desc'),
569 user=Depends(get_verified_user),
570 db: AsyncSession = Depends(get_async_session),
571):
572 """Get chats within a shared folder. Returns readonly flag based on permission."""
573 await check_folders_permission(request, user, db=db)
574 folder = await Folders.get_folder_by_id(id, db=db)
575 if not folder:
576 raise HTTPException(
577 status_code=status.HTTP_404_NOT_FOUND,
578 detail=ERROR_MESSAGES.NOT_FOUND,
579 )
581 is_owner = user.id == folder.user_id
582 is_admin = user.role == 'admin'
583 has_write = is_owner or is_admin or await _has_folder_access(user.id, folder, 'write', db)
584 has_read = has_write or await _has_folder_access(user.id, folder, 'read', db)
586 if not has_read: 586 ↛ 587line 586 didn't jump to line 587 because the condition on line 586 was never true
587 raise HTTPException(
588 status_code=status.HTTP_403_FORBIDDEN,
589 detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
590 )
592 limit = 10
593 skip = (page - 1) * limit if page is not None else 0
594 chats = await Chats.get_all_chats_by_folder_id(
595 id,
596 skip=skip,
597 limit=limit if page is not None else 60,
598 sort_by=sort_by,
599 sort_dir=sort_dir,
600 unread_for_user_id=user.id,
601 db=db,
602 )
603 total = await Chats.count_all_chats_by_folder_id(id, db=db) if page is not None else len(chats)
605 # Resolve owner names for display (avatar URLs are constructed client-side)
606 owner_cache: dict[str, str] = {}
607 for chat in chats: 607 ↛ 608line 607 didn't jump to line 608 because the loop on line 607 never started
608 uid = chat['user_id']
609 if uid not in owner_cache:
610 u = await Users.get_user_by_id(uid, db=db)
611 owner_cache[uid] = u.name if u else 'Unknown'
612 chat['owner_name'] = owner_cache[uid]
613 chat['active'] = False
614 if chat['user_id'] != user.id:
615 chat['last_read_at'] = chat['updated_at']
616 if await has_active_tasks(request.app.state.redis, chat['id']):
617 chat['active'] = await ChatMessages.has_unfinished_assistant_by_chat_id(chat['id'], db=db)
619 response = {
620 'chats': [{**chat, 'readonly': chat['user_id'] != user.id} for chat in chats],
621 'folder_permission': 'write' if has_write else 'read',
622 }
623 if page is not None:
624 response.update({'total': total, 'has_more': skip + limit < total})
625 return response
628@router.post('/{id}/read')
629async def mark_folder_chats_read_by_id(
630 request: Request,
631 id: str,
632 user=Depends(get_verified_user),
633 db: AsyncSession = Depends(get_async_session),
634):
635 await check_folders_permission(request, user, db=db)
636 folder = await Folders.get_folder_by_id(id, db=db)
637 if not folder:
638 raise HTTPException(
639 status_code=status.HTTP_404_NOT_FOUND,
640 detail=ERROR_MESSAGES.NOT_FOUND,
641 )
643 is_owner = user.id == folder.user_id
644 is_admin = user.role == 'admin'
645 if not (is_owner or is_admin or await _has_folder_access(user.id, folder, 'read', db)): 645 ↛ 646line 645 didn't jump to line 646 because the condition on line 645 was never true
646 raise HTTPException(
647 status_code=status.HTTP_403_FORBIDDEN,
648 detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
649 )
651 folder_ids = (
652 await Folders.get_folder_ids_by_id_and_user_id_in_subtree(id, folder.user_id, db=db)
653 if is_owner or is_admin
654 else [id]
655 )
656 updated_count = await Chats.mark_chats_read_by_folder_ids(user.id, folder_ids, db=db)
658 return {
659 'folder_id': id,
660 'folder_ids': folder_ids,
661 'updated_count': updated_count,
662 'folder_unread_counts': await get_folder_unread_counts(user.id, db=db),
663 }
666############################
667# Delete Folder By Id
668############################
671@router.delete('/{id}')
672async def delete_folder_by_id(
673 request: Request,
674 id: str,
675 delete_contents: Optional[bool] = True,
676 user=Depends(get_verified_user),
677 db: AsyncSession = Depends(get_async_session),
678):
679 await check_folders_permission(request, user, db=db)
680 folder = await Folders.get_folder_by_id_and_user_id(id, user.id, db=db)
682 if not folder: 682 ↛ 696line 682 didn't jump to line 696 because the condition on line 682 was always true
683 # Deletion cascades into the owner's data, so only the owner or an admin may delete
684 folder = await Folders.get_folder_by_id(id, db=db)
685 if not folder: 685 ↛ 690line 685 didn't jump to line 690 because the condition on line 685 was always true
686 raise HTTPException(
687 status_code=status.HTTP_404_NOT_FOUND,
688 detail=ERROR_MESSAGES.NOT_FOUND,
689 )
690 if user.role != 'admin':
691 raise HTTPException(
692 status_code=status.HTTP_403_FORBIDDEN,
693 detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
694 )
696 folder_owner_id = folder.user_id
698 folder_ids = await Folders.get_folder_ids_by_id_and_user_id_in_subtree(id, folder_owner_id, db=db)
699 if delete_contents and await Chats.count_chats_by_folder_ids_and_user_id(folder_ids, folder_owner_id, db=db):
700 chat_delete_permission = await has_permission(
701 user.id, 'chat.delete', await Config.get('user.permissions'), db=db
702 )
703 if user.role != 'admin' and not chat_delete_permission:
704 raise HTTPException(
705 status_code=status.HTTP_403_FORBIDDEN,
706 detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
707 )
709 folders = []
710 folders.append(folder)
711 while folders:
712 folder = folders.pop()
713 if folder:
714 try:
715 folder_ids = await Folders.delete_folder_by_id_and_user_id(folder.id, folder_owner_id, db=db)
717 for folder_id in folder_ids:
718 if delete_contents:
719 await Chats.delete_chats_by_user_id_and_folder_id(folder_owner_id, folder_id, db=db)
721 await Chats.move_chats_by_folder_id(folder_id, None, db=db)
723 # Clean up access grants for this folder
724 await AccessGrants.revoke_all_access('folder', folder_id, db=db)
726 await Automations.clear_folder_ids(folder_owner_id, folder_ids, db=db)
728 await publish_event(
729 request,
730 EVENTS.FOLDER_DELETED,
731 actor=user,
732 subject_id=id,
733 data={'folder_ids': folder_ids, 'delete_contents': delete_contents},
734 )
735 return True
736 except Exception as e:
737 log.exception(e)
738 log.error(f'Error deleting folder: {id}')
739 raise HTTPException(
740 status_code=status.HTTP_400_BAD_REQUEST,
741 detail=ERROR_MESSAGES.DEFAULT('Error deleting folder'),
742 )
743 finally:
744 # Get all subfolders
745 subfolders = await Folders.get_folders_by_parent_id_and_user_id(folder.id, folder_owner_id, db=db)
746 folders.extend(subfolders)
748 else:
749 raise HTTPException(
750 status_code=status.HTTP_404_NOT_FOUND,
751 detail=ERROR_MESSAGES.NOT_FOUND,
752 )