From 22a551f766a0a0cc9217b88bee39b483b6996be2 Mon Sep 17 00:00:00 2001 From: Christian Krakau-Louis Date: Tue, 1 Apr 2025 04:58:55 +0200 Subject: [PATCH] feat: refactor file upload handling and remove unused router files --- app/api.py | 32 ++++++- app/api/__init__.py | 0 app/api/router.py | 179 --------------------------------------- app/frontend/__init__.py | 0 app/frontend/router.py | 90 -------------------- app/main.py | 13 +-- 6 files changed, 32 insertions(+), 282 deletions(-) delete mode 100644 app/api/__init__.py delete mode 100644 app/api/router.py delete mode 100644 app/frontend/__init__.py delete mode 100644 app/frontend/router.py diff --git a/app/api.py b/app/api.py index 3afe1119..a417631d 100644 --- a/app/api.py +++ b/app/api.py @@ -233,8 +233,26 @@ def process_all_pdfs_in_workdir(): @require_login async def ui_upload(file: UploadFile = File(...)): """Endpoint to accept a user-uploaded file and enqueue it for processing.""" - workdir = "/workdir" - target_path = os.path.join(workdir, file.filename) + import uuid + import os.path + + workdir = settings.workdir + + # Extract just the filename without any path components to prevent path traversal + safe_filename = os.path.basename(file.filename) + + # Generate a unique filename with UUID to prevent overwriting and filename conflicts + unique_id = str(uuid.uuid4()) + # Keep the original extension if present + if "." in safe_filename: + file_extension = safe_filename.rsplit(".", 1)[1] + target_filename = f"{unique_id}.{file_extension}" + else: + target_filename = unique_id + + # Store both the safe original name and the unique name + target_path = os.path.join(workdir, target_filename) + try: with open(target_path, "wb") as f: content = await file.read() @@ -245,8 +263,16 @@ async def ui_upload(file: UploadFile = File(...)): detail=f"Failed to save file: {e}" ) + # Log the mapping between original and safe filename + print(f"Saved uploaded file '{safe_filename}' as '{target_filename}'") + task = process_document.delay(target_path) - return {"task_id": task.id, "status": "queued"} + return { + "task_id": task.id, + "status": "queued", + "original_filename": safe_filename, + "stored_filename": target_filename + } # Note: The api/router.py is now a submodule organization, # but we're keeping this file for compatibility until we've fully migrated diff --git a/app/api/__init__.py b/app/api/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/app/api/router.py b/app/api/router.py deleted file mode 100644 index ce98c249..00000000 --- a/app/api/router.py +++ /dev/null @@ -1,179 +0,0 @@ -from fastapi import APIRouter, HTTPException, UploadFile, File, Depends -import os - -from app.config import settings -from app.auth import get_current_user, require_login -from app.tasks.process_document import process_document -from app.tasks.upload_to_dropbox import upload_to_dropbox -from app.tasks.upload_to_paperless import upload_to_paperless -from app.tasks.upload_to_nextcloud import upload_to_nextcloud -from app.tasks.upload_to_google_drive import upload_to_google_drive -from app.tasks.send_to_all import send_to_all_destinations - -router = APIRouter() - -# Diagnostic endpoints -@router.get("/diagnostic/settings") -@require_login -async def diagnostic_settings(current_user: dict = Depends(get_current_user)): - """ - API endpoint to dump settings to the log and view basic config information - This endpoint doesn't expose sensitive information like passwords or tokens - """ - from app.utils.config_validator import dump_all_settings - # Dump full settings to log for admin to see - dump_all_settings() - - # Return safe subset of settings for API response - safe_settings = { - "workdir": settings.workdir, - "external_hostname": settings.external_hostname, - "configured_services": { - "email": bool(getattr(settings, 'email_host', None)), - "s3": bool(getattr(settings, 's3_bucket_name', None)), - "dropbox": bool(getattr(settings, 'dropbox_refresh_token', None)), - "nextcloud": bool(getattr(settings, 'nextcloud_upload_url', None)), - "sftp": bool(getattr(settings, 'sftp_host', None)), - "paperless": bool(getattr(settings, 'paperless_host', None)), - "google_drive": bool(getattr(settings, 'google_drive_credentials_json', None)), - }, - "imap_enabled": bool(getattr(settings, 'imap1_host', None) or getattr(settings, 'imap2_host', None)), - } - - return { - "status": "success", - "settings": safe_settings, - "message": "Full settings have been dumped to application logs" - } - -# File processing endpoints -@router.post("/process/") -@require_login -def process(file_path: str): - """API Endpoint to start document processing.""" - if not os.path.isabs(file_path): - file_path = os.path.join(settings.workdir, file_path) - - if not os.path.exists(file_path): - raise HTTPException( - status_code=400, detail=f"File {file_path} not found." - ) - - task = process_document.delay(file_path) - return {"task_id": task.id, "status": "queued"} - -@router.post("/send_to_dropbox/") -@require_login -def send_to_dropbox(file_path: str): - """Send a document to Dropbox.""" - if not os.path.isabs(file_path): - file_path = os.path.join(settings.workdir, 'processed', file_path) - if not os.path.exists(file_path): - raise HTTPException( - status_code=400, detail=f"File {file_path} not found." - ) - task = upload_to_dropbox.delay(file_path) - return {"task_id": task.id, "status": "queued"} - -@router.post("/send_to_paperless/") -@require_login -def send_to_paperless(file_path: str): - """Send a document to Paperless-ngx.""" - if not os.path.isabs(file_path): - file_path = os.path.join(settings.workdir, 'processed', file_path) - if not os.path.exists(file_path): - raise HTTPException( - status_code=400, detail=f"File {file_path} not found." - ) - task = upload_to_paperless.delay(file_path) - return {"task_id": task.id, "status": "queued"} - -@router.post("/send_to_nextcloud/") -@require_login -def send_to_nextcloud(file_path: str): - """Send a document to NextCloud.""" - if not os.path.isabs(file_path): - file_path = os.path.join(settings.workdir, 'processed', file_path) - if not os.path.exists(file_path): - raise HTTPException( - status_code=400, detail=f"File {file_path} not found." - ) - task = upload_to_nextcloud.delay(file_path) - return {"task_id": task.id, "status": "queued"} - -@router.post("/send_to_google_drive/") -@require_login -def send_to_google_drive(file_path: str): - """Send a document to Google Drive.""" - if not os.path.isabs(file_path): - file_path = os.path.join(settings.workdir, 'processed', file_path) - if not os.path.exists(file_path): - raise HTTPException( - status_code=400, detail=f"File {file_path} not found." - ) - task = upload_to_google_drive.delay(file_path) - return {"task_id": task.id, "status": "queued"} - -@router.post("/send_to_all_destinations/") -@require_login -def send_to_all_destinations_endpoint(file_path: str): - """Call the aggregator task that sends this file to all configured destinations.""" - if not os.path.isabs(file_path): - file_path = os.path.join(settings.workdir, 'processed', file_path) - - if not os.path.exists(file_path): - raise HTTPException( - status_code=400, detail=f"File {file_path} not found." - ) - - task = send_to_all_destinations.delay(file_path) - return {"task_id": task.id, "status": "queued", "file_path": file_path} - -@router.post("/processall") -@require_login -def process_all_pdfs_in_workdir(): - """Finds all .pdf files in and enqueues them for processing.""" - target_dir = settings.workdir - if not os.path.exists(target_dir): - raise HTTPException( - status_code=400, detail=f"Directory {target_dir} does not exist." - ) - - pdf_files = [] - for filename in os.listdir(target_dir): - if filename.lower().endswith(".pdf"): - pdf_files.append(filename) - - if not pdf_files: - return {"message": "No PDF files found in that directory."} - - task_ids = [] - for pdf in pdf_files: - file_path = os.path.join(target_dir, pdf) - task = process_document.delay(file_path) - task_ids.append(task.id) - - return { - "message": f"Enqueued {len(pdf_files)} PDFs to upload_to_s3", - "pdf_files": pdf_files, - "task_ids": task_ids - } - -@router.post("/ui-upload") -@require_login -async def ui_upload(file: UploadFile = File(...)): - """Endpoint to accept a user-uploaded file and enqueue it for processing.""" - workdir = "/workdir" - target_path = os.path.join(workdir, file.filename) - try: - with open(target_path, "wb") as f: - content = await file.read() - f.write(content) - except Exception as e: - raise HTTPException( - status_code=500, - detail=f"Failed to save file: {e}" - ) - - task = process_document.delay(target_path) - return {"task_id": task.id, "status": "queued"} diff --git a/app/frontend/__init__.py b/app/frontend/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/app/frontend/router.py b/app/frontend/router.py deleted file mode 100644 index 3542468d..00000000 --- a/app/frontend/router.py +++ /dev/null @@ -1,90 +0,0 @@ -from fastapi import APIRouter, Request, Depends, HTTPException -from fastapi.templating import Jinja2Templates -from pathlib import Path -from datetime import datetime -from sqlalchemy.orm import Session - -from app.config import settings -from app.auth import require_login -from app.database import SessionLocal - -router = APIRouter() - -# Set up Jinja2 templates -templates_dir = Path(__file__).parent.parent.parent / "frontend" / "templates" -templates = Jinja2Templates(directory=str(templates_dir)) - -def get_db(): - db = SessionLocal() - try: - yield db - finally: - db.close() - -@router.get("/", include_in_schema=False) -async def serve_index(request: Request): - return templates.TemplateResponse("index.html", {"request": request}) - -@router.get("/about", include_in_schema=False) -async def serve_about(request: Request): - return templates.TemplateResponse("about.html", {"request": request}) - -@router.get("/upload", include_in_schema=False) -@require_login -async def serve_upload(request: Request): - return templates.TemplateResponse("upload.html", {"request": request}) - -@router.get("/files") -@require_login -def files_page(request: Request, db: Session = Depends(get_db)): - return templates.TemplateResponse("files.html", {"request": request}) - -@router.get("/favicon.ico", include_in_schema=False) -def favicon(): - favicon_path = Path(__file__).parent.parent.parent / "frontend" / "static" / "favicon.ico" - return str(favicon_path) - -@router.get("/status") -@require_login -async def status_dashboard(request: Request): - """ - Status dashboard showing all configured integration targets - """ - from app.utils.config_validator import get_provider_status - - # Get provider status - providers = get_provider_status() - - return templates.TemplateResponse( - "status_dashboard.html", - { - "request": request, - "providers": providers, - "debug_enabled": getattr(settings, 'debug', False), - "last_check": datetime.now().strftime("%Y-%m-%d %H:%M:%S") - } - ) - -@router.get("/env") -@require_login -async def env_debug(request: Request): - """ - Debug endpoint to view environment variables and settings - Only shows values when DEBUG is True - """ - # Default DEBUG to True for this route - debug_enabled = True - - # Get settings data - from app.utils.config_validator import get_settings_for_display - settings_data = get_settings_for_display(show_values=debug_enabled) - - return templates.TemplateResponse( - "env_debug.html", - { - "request": request, - "settings": settings_data, - "debug_enabled": debug_enabled, - "app_version": getattr(settings, 'version', 'Unknown') - } - ) diff --git a/app/main.py b/app/main.py index 75a0e7b9..5cfc6185 100644 --- a/app/main.py +++ b/app/main.py @@ -16,14 +16,10 @@ from app.config import settings from app.utils.config_validator import check_all_configs # Import both the traditional and new routers - we'll keep both available for compatibility -from app.frontend.router import router as frontend_router_original -from app.api.router import router as api_router_original +from app.frontend import router as frontend_router_original +from app.api import router as api_router_original from app.auth import router as auth_router -# Import also the organized router modules -from app.frontend.router import router as frontend_router_organized -from app.api.router import router as api_router_organized - # Load configuration from .env for the session key config = Config(".env") SESSION_SECRET = config( @@ -105,7 +101,4 @@ app.include_router(frontend_router_original) app.include_router(auth_router) app.include_router(api_router_original, prefix="/api") -# Note: These are the future, organized router modules that we'll eventually switch to -# For now they are disabled to avoid duplicate route conflicts -# app.include_router(frontend_router_organized) -# app.include_router(api_router_organized, prefix="/api") +