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

1import logging 

2import mimetypes 

3import os 

4import shutil 

5import uuid 

6from pathlib import Path 

7from typing import Optional 

8 

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 

38 

39log = logging.getLogger(__name__) 

40 

41 

42router = APIRouter() 

43 

44 

45from open_webui.utils.access_control.folders import has_folder_access as _has_folder_access 

46 

47 

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) 

53 

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) 

62 

63 return unread_counts 

64 

65 

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 ) 

84 

85 

86############################ 

87# Get Folders 

88############################ 

89 

90 

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) 

98 

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} 

101 

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 

109 

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)} 

113 

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) 

121 

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 ) 

131 

132 folder_list.append(folder) 

133 

134 unread_counts = await get_folder_unread_counts(user.id, db=db) 

135 

136 return [ 

137 FolderNameIdResponse(**folder.model_dump(), unread_count=unread_counts.get(folder.id, 0)) 

138 for folder in folder_list 

139 ] 

140 

141 

142############################ 

143# Create Folder 

144############################ 

145 

146 

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 ) 

158 

159 if folder: 

160 raise HTTPException( 

161 status_code=status.HTTP_400_BAD_REQUEST, 

162 detail=ERROR_MESSAGES.DEFAULT('Folder already exists'), 

163 ) 

164 

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 ) 

204 

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 ) 

214 

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 ) 

232 

233 

234############################ 

235# Get Shared Folders 

236############################ 

237 

238 

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} 

249 

250 folder_perms = await Folders.get_shared_folder_ids_for_user(user.id, group_ids, db=db) 

251 

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] 

254 

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} 

257 

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 ] 

266 

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 ) 

281 

282 return results 

283 

284 

285############################ 

286# Get Folders By Id 

287############################ 

288 

289 

290class FolderResponse(FolderModel): 

291 access_grants: list[dict] = [] 

292 write_access: bool = False 

293 

294 

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 ) 

308 

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 ) 

318 

319 raise HTTPException( 

320 status_code=status.HTTP_404_NOT_FOUND, 

321 detail=ERROR_MESSAGES.NOT_FOUND, 

322 ) 

323 

324 

325############################ 

326# Update Folder Name By Id 

327############################ 

328 

329 

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 ) 

348 

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 ) 

360 

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 ) 

373 

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 ) 

391 

392 

393############################ 

394# Update Folder Parent Id By Id 

395############################ 

396 

397 

398class FolderParentIdForm(BaseModel): 

399 parent_id: Optional[str] = None 

400 

401 

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 ) 

416 

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 ) 

422 

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 ) 

430 

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 ) 

453 

454 

455############################ 

456# Update Folder Is Expanded By Id 

457############################ 

458 

459 

460class FolderIsExpandedForm(BaseModel): 

461 is_expanded: bool 

462 

463 

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 

478 

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 ) 

497 

498 

499############################ 

500# Update Folder Access By Id 

501############################ 

502 

503 

504class FolderAccessGrantsForm(BaseModel): 

505 access_grants: list[dict] 

506 

507 

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 ) 

523 

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 ) 

531 

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 ) 

540 

541 await AccessGrants.set_access_grants('folder', id, form_data.access_grants, db=db) 

542 

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 } 

555 

556 

557############################ 

558# Get Shared Folder Chats 

559############################ 

560 

561 

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 ) 

580 

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) 

585 

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 ) 

591 

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) 

604 

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) 

618 

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 

626 

627 

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 ) 

642 

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 ) 

650 

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) 

657 

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 } 

664 

665 

666############################ 

667# Delete Folder By Id 

668############################ 

669 

670 

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) 

681 

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 ) 

695 

696 folder_owner_id = folder.user_id 

697 

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 ) 

708 

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) 

716 

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) 

720 

721 await Chats.move_chats_by_folder_id(folder_id, None, db=db) 

722 

723 # Clean up access grants for this folder 

724 await AccessGrants.revoke_all_access('folder', folder_id, db=db) 

725 

726 await Automations.clear_folder_ids(folder_owner_id, folder_ids, db=db) 

727 

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) 

747 

748 else: 

749 raise HTTPException( 

750 status_code=status.HTTP_404_NOT_FOUND, 

751 detail=ERROR_MESSAGES.NOT_FOUND, 

752 )