""" Simplified Files Router ====================== Simplified version of the files router with auto-processing removed. Keeps only essential functionality for file management and manual processing triggers. This replaces the complex auto-processing system with simple file storage. """ import os import uuid import logging from typing import Dict, List, Optional, Any from pathlib import Path from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, BackgroundTasks from fastapi.responses import JSONResponse from modules.auth.supabase_bearer import SupabaseBearer from modules.database.supabase.utils.client import SupabaseServiceRoleClient from modules.database.supabase.utils.storage import StorageAdmin from modules.logger_tool import initialise_logger router = APIRouter() auth = SupabaseBearer() logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True) def _choose_bucket(scope: str, user_id: str, school_id: Optional[str]) -> str: """Choose appropriate bucket based on scope - matches old system logic.""" scope = (scope or 'teacher').lower() if scope == 'school' and school_id: return f"cc.institutes.{school_id}.private" # teacher / student fall back to users bucket for now return 'cc.users' @router.post("/files/upload") async def upload_file( cabinet_id: str = Form(...), path: str = Form(...), scope: str = Form(...), file: UploadFile = File(...), payload: Dict[str, Any] = Depends(auth) ): """ SIMPLIFIED file upload - no automatic processing. Just stores the file and creates a database record. This is the legacy endpoint maintained for backward compatibility. """ try: user_id = payload.get('sub') or payload.get('user_id') if not user_id: raise HTTPException(status_code=401, detail="User ID required") # Read file content file_bytes = await file.read() file_size = len(file_bytes) mime_type = file.content_type or 'application/octet-stream' filename = file.filename or path logger.info(f"📤 Simplified upload: {filename} ({file_size} bytes) for user {user_id}") # Initialize services client = SupabaseServiceRoleClient() storage = StorageAdmin() # Generate file ID and storage path file_id = str(uuid.uuid4()) # Use same bucket logic as old system for consistency bucket = _choose_bucket('teacher', user_id, None) storage_path = f"{cabinet_id}/{file_id}/{filename}" # Store file in Supabase storage try: storage.upload_file(bucket, storage_path, file_bytes, mime_type, upsert=True) except Exception as e: logger.error(f"Storage upload failed for {file_id}: {e}") raise HTTPException(status_code=500, detail=f"Storage upload failed: {str(e)}") # Create database record try: insert_res = client.supabase.table('files').insert({ 'id': file_id, 'name': filename, 'cabinet_id': cabinet_id, 'bucket': bucket, 'path': storage_path, 'mime_type': mime_type, 'uploaded_by': user_id, 'size_bytes': file_size, 'source': 'classroomcopilot-web', 'is_directory': False, 'processing_status': 'uploaded', # No auto-processing 'relative_path': filename }).execute() if not insert_res.data: # Clean up storage on DB failure try: storage.delete_file(bucket, storage_path) except: pass raise HTTPException(status_code=500, detail="Failed to create file record") file_record = insert_res.data[0] except Exception as e: logger.error(f"Database insert failed for {file_id}: {e}") # Clean up storage try: storage.delete_file(bucket, storage_path) except: pass raise HTTPException(status_code=500, detail=f"Database error: {str(e)}") logger.info(f"✅ Simplified upload completed: {file_id}") return { 'status': 'success', 'message': 'File uploaded successfully (no auto-processing)', 'file': file_record, 'auto_processing_disabled': True, 'next_steps': 'Use manual processing endpoints if needed' } except HTTPException: raise except Exception as e: logger.error(f"Upload error: {e}") raise HTTPException(status_code=500, detail=f"Upload failed: {str(e)}") @router.get("/files") def list_files(cabinet_id: str, payload: Dict[str, Any] = Depends(auth)): """List files in a cabinet.""" client = SupabaseServiceRoleClient() res = client.supabase.table('files').select('*').eq('cabinet_id', cabinet_id).execute() return res.data @router.get("/files/{file_id}") def get_file(file_id: str, payload: Dict[str, Any] = Depends(auth)): """Get file details.""" client = SupabaseServiceRoleClient() res = client.supabase.table('files').select('*').eq('id', file_id).single().execute() if not res.data: raise HTTPException(status_code=404, detail="File not found") return res.data @router.delete("/files/{file_id}") def delete_file(file_id: str, payload: Dict[str, Any] = Depends(auth)): """Delete a file.""" client = SupabaseServiceRoleClient() storage = StorageAdmin() # Get file info first res = client.supabase.table('files').select('*').eq('id', file_id).single().execute() if not res.data: raise HTTPException(status_code=404, detail="File not found") file_data = res.data # Delete from storage try: storage.delete_file(file_data['bucket'], file_data['path']) except Exception as e: logger.warning(f"Failed to delete file from storage: {e}") # Delete from database delete_res = client.supabase.table('files').delete().eq('id', file_id).execute() logger.info(f"🗑️ Deleted file: {file_id}") return { 'status': 'success', 'message': 'File deleted successfully' } @router.post("/files/{file_id}/process-manual") async def trigger_manual_processing( file_id: str, processing_type: str = Form('basic'), # basic, advanced, custom payload: Dict[str, Any] = Depends(auth) ): """ Trigger manual processing for a file. This is where users can manually start processing when they want it. """ # TODO: Implement manual processing triggers # This would call the archived processing logic when the user explicitly requests it logger.info(f"🔧 Manual processing requested for file {file_id} (type: {processing_type})") return { 'status': 'accepted', 'message': f'Manual processing queued for file {file_id}', 'processing_type': processing_type, 'note': 'Manual processing not yet implemented - will use archived auto-processing logic' } @router.get("/files/{file_id}/status") def get_processing_status(file_id: str, payload: Dict[str, Any] = Depends(auth)): """Get processing status for a file.""" client = SupabaseServiceRoleClient() res = client.supabase.table('files').select('processing_status, error_message, extra').eq('id', file_id).single().execute() if not res.data: raise HTTPException(status_code=404, detail="File not found") return { 'file_id': file_id, 'status': res.data.get('processing_status', 'unknown'), 'error': res.data.get('error_message'), 'details': res.data.get('extra', {}) } # Keep existing artefacts endpoints for backward compatibility @router.get("/files/{file_id}/artefacts") def list_file_artefacts(file_id: str, payload: Dict[str, Any] = Depends(auth)): """List artefacts for a file.""" client = SupabaseServiceRoleClient() res = client.supabase.table('document_artefacts').select('*').eq('file_id', file_id).execute() return res.data or [] @router.get("/files/{file_id}/viewer-artefacts") def list_viewer_artefacts(file_id: str, payload: Dict[str, Any] = Depends(auth)): """List artefacts organized for the viewer.""" client = SupabaseServiceRoleClient() # Get all artefacts res = client.supabase.table('document_artefacts').select('*').eq('file_id', file_id).execute() artefacts = res.data or [] # Simple organization - no complex bundle logic organized = { 'document_analysis': [], 'processing_bundles': [], 'raw_data': [] } for artefact in artefacts: artefact_type = artefact.get('type', '') if 'analysis' in artefact_type.lower(): organized['document_analysis'].append(artefact) elif any(bundle_type in artefact_type for bundle_type in ['docling', 'bundle']): organized['processing_bundles'].append(artefact) else: organized['raw_data'].append(artefact) return organized