diff --git a/app/api/files.py b/app/api/files.py index 865250a0..c2df2a41 100644 --- a/app/api/files.py +++ b/app/api/files.py @@ -275,6 +275,127 @@ def delete_file_record(request: Request, file_id: int, db: Session = Depends(get detail=f"Error deleting file record: {str(e)}" ) +@router.post("/files/bulk-delete") +@require_login +def bulk_delete_files(request: Request, file_ids: List[int], db: Session = Depends(get_db)): + """ + Delete multiple file records from the database. + This only removes the database entries, not the actual files. + """ + # Check if file deletion is allowed + if not settings.allow_file_delete: + raise HTTPException( + status_code=403, + detail="File deletion is disabled in the configuration" + ) + + try: + # Find all file records + file_records = db.query(FileRecord).filter(FileRecord.id.in_(file_ids)).all() + + if not file_records: + raise HTTPException( + status_code=404, + detail="No files found with the provided IDs" + ) + + deleted_count = len(file_records) + deleted_ids = [f.id for f in file_records] + + # Log the deletion + logger.info(f"Bulk deleting {deleted_count} file records: IDs={deleted_ids}") + + # Delete all records + for file_record in file_records: + db.delete(file_record) + + db.commit() + + return { + "status": "success", + "message": f"Successfully deleted {deleted_count} file records", + "deleted_ids": deleted_ids + } + + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.exception(f"Error bulk deleting file records: {str(e)}") + raise HTTPException( + status_code=500, + detail=f"Error bulk deleting file records: {str(e)}" + ) + + +@router.post("/files/bulk-reprocess") +@require_login +def bulk_reprocess_files(request: Request, file_ids: List[int], db: Session = Depends(get_db)): + """ + Reprocess multiple files by queuing them for processing. + """ + try: + # Find all file records + file_records = db.query(FileRecord).filter(FileRecord.id.in_(file_ids)).all() + + if not file_records: + raise HTTPException( + status_code=404, + detail="No files found with the provided IDs" + ) + + task_ids = [] + processed_files = [] + errors = [] + + for file_record in file_records: + try: + # Check if local file exists + if not file_record.local_filename or not os.path.exists(file_record.local_filename): + errors.append({ + "file_id": file_record.id, + "filename": file_record.original_filename, + "error": "Local file not found" + }) + continue + + # Queue the file for processing + task = process_document.delay(file_record.local_filename) + task_ids.append(task.id) + processed_files.append({ + "file_id": file_record.id, + "filename": file_record.original_filename, + "task_id": task.id + }) + + logger.info(f"Reprocessing file: ID={file_record.id}, Filename={file_record.original_filename}, TaskID={task.id}") + + except Exception as e: + logger.exception(f"Error reprocessing file {file_record.id}: {str(e)}") + errors.append({ + "file_id": file_record.id, + "filename": file_record.original_filename, + "error": str(e) + }) + + return { + "status": "success" if processed_files else "error", + "message": f"Successfully queued {len(processed_files)} files for reprocessing", + "processed_files": processed_files, + "errors": errors if errors else None, + "task_ids": task_ids + } + + except HTTPException: + raise + except Exception as e: + logger.exception(f"Error bulk reprocessing files: {str(e)}") + raise HTTPException( + status_code=500, + detail=f"Error bulk reprocessing files: {str(e)}" + ) + + @router.post("/ui-upload") @require_login async def ui_upload(request: Request, file: UploadFile = File(...)): diff --git a/app/views/files.py b/app/views/files.py index 8da5c545..ea793f33 100644 --- a/app/views/files.py +++ b/app/views/files.py @@ -29,7 +29,7 @@ def files_page( try: # Import the model here to avoid circular imports from app.models import FileRecord, ProcessingLog - from sqlalchemy import desc, asc + from sqlalchemy import desc, asc, or_ # Start with base query query = db.query(FileRecord) @@ -42,6 +42,41 @@ def files_page( if mime_type: query = query.filter(FileRecord.mime_type == mime_type) + # Apply status filter (before pagination for correct counts) + if status: + # Subquery to get file IDs matching the status + if status == "pending": + # Files with no logs + subq = db.query(ProcessingLog.file_id).distinct() + query = query.filter(~FileRecord.id.in_(subq)) + elif status == "processing": + # Files with in_progress logs + subq = db.query(ProcessingLog.file_id).filter( + ProcessingLog.status == "in_progress" + ).distinct() + query = query.filter(FileRecord.id.in_(subq)) + elif status == "failed": + # Files with failure logs + subq = db.query(ProcessingLog.file_id).filter( + ProcessingLog.status == "failure" + ).distinct() + query = query.filter(FileRecord.id.in_(subq)) + elif status == "completed": + # Files with success logs but no failures or in_progress + success_files = db.query(ProcessingLog.file_id).filter( + ProcessingLog.status == "success" + ).distinct().subquery() + + failed_files = db.query(ProcessingLog.file_id).filter( + or_(ProcessingLog.status == "failure", ProcessingLog.status == "in_progress") + ).distinct().subquery() + + query = query.filter( + FileRecord.id.in_(db.query(success_files.c.file_id)) + ).filter( + ~FileRecord.id.in_(db.query(failed_files.c.file_id)) + ) + # Get total count before pagination total_items = query.count() diff --git a/frontend/templates/files.html b/frontend/templates/files.html index 5fcfcc22..09c3795f 100644 --- a/frontend/templates/files.html +++ b/frontend/templates/files.html @@ -297,11 +297,34 @@ + + +
+ {% for file in files %} + @@ -382,7 +408,7 @@ {% else %} - + {% endfor %} @@ -513,6 +539,124 @@ function clearFilters() { window.location.href = '/files'; } + + // Bulk selection functionality + function toggleSelectAll() { + const selectAll = document.getElementById('selectAll'); + const checkboxes = document.querySelectorAll('.file-checkbox'); + checkboxes.forEach(checkbox => { + checkbox.checked = selectAll.checked; + }); + updateBulkActionsBar(); + } + + function updateBulkActionsBar() { + const checkboxes = document.querySelectorAll('.file-checkbox:checked'); + const selectedCount = checkboxes.length; + const bulkActionsBar = document.getElementById('bulkActionsBar'); + const selectedCountEl = document.getElementById('selectedCount'); + + if (selectedCount > 0) { + bulkActionsBar.style.display = 'block'; + selectedCountEl.textContent = selectedCount; + } else { + bulkActionsBar.style.display = 'none'; + } + + // Update "select all" checkbox state + const allCheckboxes = document.querySelectorAll('.file-checkbox'); + const selectAll = document.getElementById('selectAll'); + selectAll.checked = allCheckboxes.length > 0 && selectedCount === allCheckboxes.length; + } + + function clearSelection() { + document.querySelectorAll('.file-checkbox').forEach(cb => cb.checked = false); + document.getElementById('selectAll').checked = false; + updateBulkActionsBar(); + } + + function getSelectedFileIds() { + const checkboxes = document.querySelectorAll('.file-checkbox:checked'); + return Array.from(checkboxes).map(cb => parseInt(cb.value)); + } + + function bulkDelete() { + const fileIds = getSelectedFileIds(); + if (fileIds.length === 0) { + alert('No files selected'); + return; + } + + if (!confirm(`Are you sure you want to delete ${fileIds.length} file(s)?`)) { + return; + } + + fetch('/api/files/bulk-delete', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(fileIds) + }) + .then(response => { + if (!response.ok) { + return response.json().then(err => { + throw new Error(err.detail || 'Failed to delete files'); + }); + } + return response.json(); + }) + .then(data => { + alert(data.message); + window.location.reload(); + }) + .catch(error => { + console.error('Error:', error); + alert(`Error deleting files: ${error.message}`); + }); + } + + function bulkReprocess() { + const fileIds = getSelectedFileIds(); + if (fileIds.length === 0) { + alert('No files selected'); + return; + } + + if (!confirm(`Are you sure you want to reprocess ${fileIds.length} file(s)?`)) { + return; + } + + fetch('/api/files/bulk-reprocess', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(fileIds) + }) + .then(response => { + if (!response.ok) { + return response.json().then(err => { + throw new Error(err.detail || 'Failed to reprocess files'); + }); + } + return response.json(); + }) + .then(data => { + if (data.errors && data.errors.length > 0) { + const errorMsg = data.errors.map(e => `${e.filename}: ${e.error}`).join('\n'); + alert(`${data.message}\n\nErrors:\n${errorMsg}`); + } else { + alert(data.message); + } + clearSelection(); + window.location.reload(); + }) + .catch(error => { + console.error('Error:', error); + alert(`Error reprocessing files: ${error.message}`); + }); + } {% endblock %}
+ + ID @@ -359,6 +382,9 @@
+ + {{ file.id }} {{ file.original_filename }} {{ (file.file_size / 1024) | round(2) }} KB
No files foundNo files found