# main.py — FastAPI backend entrypoint for dicta2stream from fastapi import FastAPI, Request, Response, status, Form, UploadFile, File, Depends, HTTPException from fastapi.responses import HTMLResponse, RedirectResponse, StreamingResponse, JSONResponse from fastapi.staticfiles import StaticFiles from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.gzip import GZipMiddleware import os import io import traceback import shutil import mimetypes from typing import Optional from models import User, UploadLog, UserQuota, get_user_by_uid from sqlmodel import Session, select, SQLModel from database import get_db, engine from log import log_violation import secrets import time import json import subprocess from datetime import datetime from dotenv import load_dotenv load_dotenv() # Ensure all tables exist at startup SQLModel.metadata.create_all(engine) ADMIN_SECRET = os.getenv("ADMIN_SECRET") import os debug_mode = os.getenv("DEBUG", "0") in ("1", "true", "True") from fastapi.responses import JSONResponse from fastapi.requests import Request as FastAPIRequest from fastapi.exception_handlers import RequestValidationError from fastapi.exceptions import HTTPException as FastAPIHTTPException app = FastAPI(debug=debug_mode, docs_url=None, redoc_url=None, openapi_url=None) # Override default HTML error handlers to return JSON from fastapi.exceptions import RequestValidationError, HTTPException as FastAPIHTTPException from fastapi.responses import JSONResponse from starlette.exceptions import HTTPException as StarletteHTTPException @app.exception_handler(StarletteHTTPException) async def http_exception_handler(request, exc): return JSONResponse( status_code=exc.status_code, content={"detail": exc.detail} ) # --- CORS Middleware for SSE and API access --- from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.gzip import GZipMiddleware # Add GZip middleware for compression app.add_middleware(GZipMiddleware, minimum_size=1000) # Configure CORS app.add_middleware( CORSMiddleware, allow_origins=["https://dicta2stream.net", "http://localhost:8000", "http://127.0.0.1:8000"], allow_credentials=True, allow_methods=["GET", "POST", "OPTIONS"], allow_headers=["*"], expose_headers=["Content-Type", "Content-Length", "Cache-Control", "ETag", "Last-Modified"], max_age=3600, # 1 hour ) from fastapi.staticfiles import StaticFiles import os if not os.path.exists("data"): os.makedirs("data") # Secure audio file serving endpoint (replaces static mount) from fastapi.responses import FileResponse from fastapi import Security def get_current_user(request: Request, db: Session = Depends(get_db)): # Use your existing session/cookie/token mechanism here uid = request.headers.get("x-uid") or request.query_params.get("uid") or request.cookies.get("uid") if not uid: raise HTTPException(status_code=403, detail="Not authenticated") user = get_user_by_uid(uid) if not user or not user.confirmed: raise HTTPException(status_code=403, detail="Invalid user") return user from range_response import range_response @app.get("/audio/{uid}/{filename}") def get_audio(uid: str, filename: str, request: Request): # Allow public access ONLY to stream.opus # Use the database session context manager with get_db() as db: try: # Use email-based UID directly for file system access # If UID contains @, it's an email - use it directly if '@' in uid: from models import User user = db.query(User).filter(User.email == uid).first() if not user: raise HTTPException(status_code=404, detail="User not found") filesystem_uid = uid # Use email directly for directory else: # Legacy support for username-based UIDs - convert to email from models import User user = db.query(User).filter(User.username == uid).first() if not user: raise HTTPException(status_code=404, detail="User not found") filesystem_uid = user.email # Convert username to email for directory except Exception as e: db.rollback() raise HTTPException(status_code=500, detail=f"Database error: {str(e)}") user_dir = os.path.join("data", filesystem_uid) file_path = os.path.join(user_dir, filename) real_user_dir = os.path.realpath(user_dir) real_file_path = os.path.realpath(file_path) if not real_file_path.startswith(real_user_dir): raise HTTPException(status_code=403, detail="Path traversal detected") if not os.path.isfile(real_file_path): raise HTTPException(status_code=404, detail="File not found") if filename == "stream.opus": # Use range_response for browser seeking support return range_response(request, real_file_path, content_type="audio/ogg") # Otherwise, require authentication and owner check try: from fastapi import Security current_user = get_current_user(request, db) except Exception: raise HTTPException(status_code=403, detail="Not allowed") if uid != current_user.username: raise HTTPException(status_code=403, detail="Not allowed") return FileResponse(real_file_path, media_type="audio/ogg") if debug_mode: # Debug messages disabled pass # Global error handler to always return JSON from slowapi.errors import RateLimitExceeded from models import get_user_by_uid, UserQuota @app.exception_handler(RateLimitExceeded) async def rate_limit_handler(request: Request, exc: RateLimitExceeded): return JSONResponse(status_code=429, content={"detail": "Rate limit exceeded. Please try again later."}) @app.exception_handler(FastAPIHTTPException) async def http_exception_handler(request: FastAPIRequest, exc: FastAPIHTTPException): return JSONResponse(status_code=exc.status_code, content={"detail": exc.detail}) @app.exception_handler(RequestValidationError) async def validation_exception_handler(request: FastAPIRequest, exc: RequestValidationError): return JSONResponse(status_code=422, content={"detail": exc.errors()}) @app.exception_handler(Exception) async def generic_exception_handler(request: FastAPIRequest, exc: Exception): return JSONResponse(status_code=500, content={"detail": str(exc)}) # Debug endpoint to list all routes @app.get("/debug/routes") async def list_routes(): routes = [] for route in app.routes: if hasattr(route, "methods") and hasattr(route, "path"): routes.append({ "path": route.path, "methods": list(route.methods) if hasattr(route, "methods") else [], "name": route.name if hasattr(route, "name") else "", "endpoint": str(route.endpoint) if hasattr(route, "endpoint") else "", "router": str(route) # Add router info for debugging }) # Sort routes by path for easier reading routes.sort(key=lambda x: x["path"]) # Also print to console for server logs print("\n=== Registered Routes ===") for route in routes: print(f"{', '.join(route['methods']).ljust(20)} {route['path']}") print("======================\n") return {"routes": routes} # include routers from submodules from register import router as register_router from magic import router as magic_router from upload import router as upload_router from streams import router as streams_router from auth_router import router as auth_router app.include_router(streams_router) from list_streams import router as list_streams_router from account_router import router as account_router # Include all routers app.include_router(auth_router, prefix="/api") app.include_router(account_router) app.include_router(register_router) app.include_router(magic_router) app.include_router(upload_router) app.include_router(list_streams_router) @app.get("/user-files/{uid}") async def list_user_files(uid: str): from pathlib import Path # Get the user's directory and check for files first user_dir = Path("data") / uid if not user_dir.exists() or not user_dir.is_dir(): return {"files": []} # Get all files that actually exist on disk existing_files = {f.name for f in user_dir.iterdir() if f.is_file()} # Use the database session context manager for all database operations with get_db() as db: # Verify the user exists user_check = db.query(User).filter((User.username == uid) | (User.email == uid)).first() if not user_check: raise HTTPException(status_code=404, detail="User not found") # Query the UploadLog table for this user all_upload_logs = db.query(UploadLog).filter( UploadLog.uid == uid ).order_by(UploadLog.created_at.desc()).all() # Track processed files to avoid duplicates processed_files = set() files_metadata = [] for log in all_upload_logs: # Skip if no processed filename if not log.processed_filename: continue # Skip if we've already processed this file if log.processed_filename in processed_files: continue # Skip stream.opus from uploads list (it's a special file) if log.processed_filename == 'stream.opus': continue # Skip if file doesn't exist on disk # Files are stored with the pattern: {upload_id}_{processed_filename} expected_filename = f"{log.id}_{log.processed_filename}" if expected_filename not in existing_files: # Only delete records older than 5 minutes to avoid race conditions from datetime import datetime, timedelta cutoff_time = datetime.utcnow() - timedelta(minutes=5) if log.created_at < cutoff_time: print(f"[CLEANUP] Removing orphaned DB record (older than 5min): {expected_filename}") db.delete(log) continue # Add to processed files to avoid duplicates processed_files.add(log.processed_filename) # Always use the original filename if present display_name = log.filename if log.filename else log.processed_filename # Only include files that exist on disk # Files are stored with the pattern: {upload_id}_{processed_filename} stored_filename = f"{log.id}_{log.processed_filename}" file_path = user_dir / stored_filename if file_path.exists() and file_path.is_file(): try: # Get the actual file size in case it changed actual_size = file_path.stat().st_size files_metadata.append({ "original_name": display_name, "stored_name": log.processed_filename, "size": actual_size }) except OSError: # If we can't access the file, skip it continue # Commit any database changes (deletions of non-existent files) try: db.commit() except Exception as e: print(f"[ERROR] Failed to commit database changes: {e}") db.rollback() return {"files": files_metadata} # Serve static files app.mount("/static", StaticFiles(directory="static"), name="static") # Serve audio files os.makedirs("data", exist_ok=True) # Ensure the data directory exists app.mount("/audio", StaticFiles(directory="data"), name="audio") @app.post("/log-client") async def log_client(request: Request): try: data = await request.json() msg = data.get("msg", "") ip = request.client.host timestamp = datetime.utcnow().isoformat() log_dir = os.path.join(os.path.dirname(__file__), "log") os.makedirs(log_dir, exist_ok=True) log_path = os.path.join(log_dir, "debug.log") log_entry = f"[{timestamp}] IP={ip} MSG={msg}\n" with open(log_path, "a") as f: f.write(log_entry) if os.getenv("DEBUG", "0") in ("1", "true", "True"): print(f"[CLIENT-DEBUG] {log_entry.strip()}") return {"status": "ok"} except Exception as e: # Enhanced error logging import sys import traceback error_log_dir = os.path.join(os.path.dirname(__file__), "log") os.makedirs(error_log_dir, exist_ok=True) error_log_path = os.path.join(error_log_dir, "debug-errors.log") tb = traceback.format_exc() try: req_body = await request.body() except Exception: req_body = b"" error_entry = ( f"[{datetime.utcnow().isoformat()}] /log-client ERROR: {type(e).__name__}: {e}\n" f"Request IP: {getattr(request.client, 'host', None)}\n" f"Request body: {req_body}\n" f"Traceback:\n{tb}\n" ) try: with open(error_log_path, "a") as ef: ef.write(error_entry) except Exception as ef_exc: print(f"[CLIENT-DEBUG-ERROR] Failed to write error log: {ef_exc}", file=sys.stderr) print(error_entry, file=sys.stderr) return {"status": "error", "detail": str(e)} @app.get("/", response_class=HTMLResponse) def serve_index(): with open("static/index.html") as f: return f.read() @app.get("/me", response_class=HTMLResponse) def serve_me(): with open("static/index.html") as f: return f.read() @app.get("/admin/stats") def admin_stats(request: Request, db: Session = Depends(get_db)): from sqlmodel import select users = db.query(User).all() users_count = len(users) total_quota = db.query(UserQuota).all() total_quota_sum = sum(q.storage_bytes for q in total_quota) violations_log = 0 try: with open("log.txt") as f: violations_log = sum(1 for _ in f) except FileNotFoundError: pass secret = request.headers.get("x-admin-secret") if secret != ADMIN_SECRET: raise HTTPException(status_code=403, detail="Forbidden") return { "total_users": users_count, "total_quota_mb": round(total_quota_sum / (1024 * 1024), 2), "violation_log_entries": violations_log } @app.get("/status") def status(): return {"status": "ok"} @app.get("/debug") def debug(request: Request): return { "ip": request.client.host, "headers": dict(request.headers), } MAX_QUOTA_BYTES = 100 * 1024 * 1024 # Delete account endpoint - fallback implementation since account_router.py has loading issues @app.post("/api/delete-account") async def delete_account_fallback(request: Request, db: Session = Depends(get_db)): try: # Get request data data = await request.json() uid = data.get("uid") if not uid: raise HTTPException(status_code=400, detail="Missing UID") ip = request.client.host # Debug messages disabled # Find user by email or username user = None if '@' in uid: user = db.exec(select(User).where(User.email == uid)).first() if not user: user = db.exec(select(User).where(User.username == uid)).first() # If still not found, check if this UID exists in upload logs and try to find the associated user if not user: # Look for upload logs with this UID to find the real user upload_log = db.exec(select(UploadLog).where(UploadLog.uid == uid)).first() if upload_log: # Try to find a user that might be associated with this UID # Check if there's a user with the same IP or similar identifier all_users = db.exec(select(User)).all() for potential_user in all_users: # Use the first confirmed user as fallback (for orphaned UIDs) if potential_user.confirmed: user = potential_user # Debug messages disabled break if not user: # Debug messages disabled raise HTTPException(status_code=404, detail="User not found") if user.ip != ip: raise HTTPException(status_code=403, detail="Unauthorized: IP address does not match") # Delete user data from database using the original UID # The original UID is what's stored in the database records # Delete upload logs for all possible UIDs (original UID, email, username) upload_logs_to_delete = [] # Check for upload logs with original UID upload_logs_original = db.query(UploadLog).filter(UploadLog.uid == uid).all() if upload_logs_original: # Debug messages disabled upload_logs_to_delete.extend(upload_logs_original) # Check for upload logs with user email upload_logs_email = db.query(UploadLog).filter(UploadLog.uid == user.email).all() if upload_logs_email: # Debug messages disabled upload_logs_to_delete.extend(upload_logs_email) # Check for upload logs with username upload_logs_username = db.query(UploadLog).filter(UploadLog.uid == user.username).all() if upload_logs_username: # Debug messages disabled upload_logs_to_delete.extend(upload_logs_username) # Delete all found upload log records for log in upload_logs_to_delete: try: db.delete(log) except Exception as e: # Debug messages disabled pass # Debug messages disabled # Delete user quota for both the original UID and user email (to cover all cases) quota_original = db.get(UserQuota, uid) if quota_original: # Debug messages disabled db.delete(quota_original) quota_email = db.get(UserQuota, user.email) if quota_email: # Debug messages disabled db.delete(quota_email) # Delete user sessions sessions = db.query(DBSession).filter(DBSession.user_id == user.username).all() # Debug messages disabled for session in sessions: db.delete(session) # Delete public stream entries for all possible UIDs # Use select() instead of get() to find all matching records public_streams_to_delete = [] # Check for public stream with original UID public_stream_original = db.query(PublicStream).filter(PublicStream.uid == uid).first() if public_stream_original: # Debug messages disabled public_streams_to_delete.append(public_stream_original) # Check for public stream with user email public_stream_email = db.query(PublicStream).filter(PublicStream.uid == user.email).first() if public_stream_email: # Debug messages disabled public_streams_to_delete.append(public_stream_email) # Check for public stream with username public_stream_username = db.query(PublicStream).filter(PublicStream.uid == user.username).first() if public_stream_username: # Debug messages disabled public_streams_to_delete.append(public_stream_username) # Delete all found public stream records for ps in public_streams_to_delete: try: # Debug messages disabled db.delete(ps) except Exception as e: # Debug messages disabled pass # Debug messages disabled # Delete user directory BEFORE deleting user record - check all possible locations import shutil # Try to delete directory with UID (email) - current standard uid_dir = os.path.join('data', uid) if os.path.exists(uid_dir): # Debug messages disabled shutil.rmtree(uid_dir, ignore_errors=True) # Also try to delete directory with email (in case of different UID formats) email_dir = os.path.join('data', user.email) if os.path.exists(email_dir) and email_dir != uid_dir: # Debug messages disabled shutil.rmtree(email_dir, ignore_errors=True) # Also try to delete directory with username (legacy format) username_dir = os.path.join('data', user.username) if os.path.exists(username_dir) and username_dir != uid_dir and username_dir != email_dir: # Debug messages disabled shutil.rmtree(username_dir, ignore_errors=True) # Delete user account AFTER directory cleanup db.delete(user) db.commit() # Debug messages disabled return {"status": "success", "message": "Account deleted successfully"} except HTTPException: raise except Exception as e: # Debug messages disabled db.rollback() raise HTTPException(status_code=500, detail=f"Failed to delete account: {str(e)}") # Cleanup endpoint for orphaned public streams @app.post("/api/cleanup-streams") async def cleanup_orphaned_streams(request: Request, db: Session = Depends(get_db)): try: # Get request data data = await request.json() admin_secret = data.get("admin_secret") # Verify admin access if admin_secret != ADMIN_SECRET: raise HTTPException(status_code=403, detail="Unauthorized") # Find orphaned public streams (streams without corresponding user accounts) all_streams = db.query(PublicStream).all() all_users = db.query(User).all() # Create sets of valid UIDs from user accounts valid_uids = set() for user in all_users: valid_uids.add(user.email) valid_uids.add(user.username) orphaned_streams = [] for stream in all_streams: if stream.uid not in valid_uids: orphaned_streams.append(stream) # Delete orphaned streams deleted_count = 0 for stream in orphaned_streams: try: print(f"[CLEANUP] Deleting orphaned stream: {stream.uid} (username: {stream.username})") db.delete(stream) deleted_count += 1 except Exception as e: print(f"[CLEANUP] Error deleting stream {stream.uid}: {e}") db.commit() print(f"[CLEANUP] Deleted {deleted_count} orphaned public streams") return { "status": "success", "message": f"Deleted {deleted_count} orphaned public streams", "deleted_streams": [s.uid for s in orphaned_streams] } except HTTPException: raise except Exception as e: print(f"[CLEANUP] Error: {str(e)}") db.rollback() raise HTTPException(status_code=500, detail=f"Cleanup failed: {str(e)}") # Original delete account endpoint has been moved to account_router.py @app.delete("/uploads/{uid}/{filename}") async def delete_file(uid: str, filename: str, request: Request): """ Delete a file for a specific user. Args: uid: The username of the user (used as UID in routes) filename: The name of the file to delete request: The incoming request object db: Database session Returns: Dict with status message """ try: # Get the user by username (which is used as UID in routes) user = get_user_by_uid(uid) if not user: raise HTTPException(status_code=404, detail="User not found") # Get client IP and verify it matches the user's IP ip = request.client.host if user.ip != ip: raise HTTPException(status_code=403, detail="Device/IP mismatch. Please log in again.") # Set up user directory using email (matching upload logic) user_dir = os.path.join('data', user.email) os.makedirs(user_dir, exist_ok=True) # Decode URL-encoded filename from urllib.parse import unquote filename = unquote(filename) # Debug: Print the user info and filename being used # Debug messages disabled # Debug messages disabled # Debug messages disabled # Debug messages disabled if os.path.exists(user_dir): # Debug messages disabled pass # Construct and validate target path target_path = os.path.join(user_dir, filename) real_target_path = os.path.realpath(target_path) real_user_dir = os.path.realpath(user_dir) # Debug: Print the constructed paths # Debug messages disabled # Debug messages disabled # Debug messages disabled # Security check: Ensure the target path is inside the user's directory if not real_target_path.startswith(real_user_dir + os.sep): # Debug messages disabled raise HTTPException(status_code=403, detail="Invalid file path") # Check if file exists if not os.path.isfile(real_target_path): # Debug: List files in the directory to help diagnose the issue try: # Debug messages disabled # Debug messages disabled # Debug messages disabled if os.path.exists(real_user_dir): files_in_dir = os.listdir(real_user_dir) # Debug messages disabled # Print detailed file info for f in files_in_dir: full_path = os.path.join(real_user_dir, f) try: # Debug messages disabled pass except Exception as e: # Debug messages disabled pass # Debug messages disabled # Debug messages disabled # Debug messages disabled # Try to find a matching file (case-insensitive, partial match) matching_files = [f for f in files_in_dir if filename.lower() in f.lower()] if matching_files: # Debug messages disabled # Use the first matching file real_target_path = os.path.join(real_user_dir, matching_files[0]) # Debug messages disabled # Debug messages disabled else: # Debug messages disabled raise HTTPException(status_code=404, detail=f"File not found: {filename}") else: # Debug messages disabled raise HTTPException(status_code=404, detail=f"User directory not found") except HTTPException: raise except Exception as e: # Debug messages disabled raise HTTPException(status_code=404, detail=f"File not found: {filename}") # Delete both the target file and its UUID-only variant deleted_files = [] try: # First delete the requested file (with log ID prefix) if os.path.exists(real_target_path): os.remove(real_target_path) deleted_files.append(filename) log_violation("DELETE", ip, uid, f"Deleted {filename}") # Then try to find and delete the UUID-only variant (without log ID prefix) if '_' in filename: # If filename has a log ID prefix (e.g., "123_uuid.opus") uuid_part = filename.split('_', 1)[1] # Get the part after the first underscore uuid_path = os.path.join(user_dir, uuid_part) if os.path.exists(uuid_path): os.remove(uuid_path) deleted_files.append(uuid_part) log_violation("DELETE", ip, uid, f"Deleted UUID variant: {uuid_part}") file_deleted = len(deleted_files) > 0 if not file_deleted: log_violation("DELETE_WARNING", ip, uid, f"No files found to delete for: {filename}") except Exception as e: log_violation("DELETE_ERROR", ip, uid, f"Error deleting file {filename}: {str(e)}") file_deleted = False # Try to refresh the user's playlist, but don't fail if we can't try: subprocess.run(["/root/scripts/refresh_user_playlist.sh", user.username], check=False, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL) except Exception as e: log_violation("PLAYLIST_REFRESH_WARNING", ip, uid, f"Failed to refresh playlist: {str(e)}") # Clean up the database record for this file try: with get_db() as db: try: # Find and delete the upload log entry log_entry = db.query(UploadLog).filter( UploadLog.uid == uid, UploadLog.processed_filename == filename ).first() if log_entry: db.delete(log_entry) db.commit() log_violation("DB_CLEANUP", ip, uid, f"Removed DB record for {filename}") except Exception as e: db.rollback() raise e except Exception as e: log_violation("DB_CLEANUP_ERROR", ip, uid, f"Failed to clean up DB record: {str(e)}") # Regenerate stream.opus after file deletion try: from concat_opus import concat_opus_files from pathlib import Path user_dir_path = Path(user_dir) stream_path = user_dir_path / "stream.opus" concat_opus_files(user_dir_path, stream_path) log_violation("STREAM_UPDATE", ip, uid, "Regenerated stream.opus after file deletion") except Exception as e: log_violation("STREAM_UPDATE_ERROR", ip, uid, f"Failed to regenerate stream.opus: {str(e)}") # Update user quota in a separate try-except to not fail the entire operation try: with get_db() as db: try: # Use verify_and_fix_quota to ensure consistency between disk and DB total_size = verify_and_fix_quota(db, user.username, user_dir) log_violation("QUOTA_UPDATE", ip, uid, f"Updated quota: {total_size} bytes") except Exception as e: db.rollback() raise e except Exception as e: log_violation("QUOTA_ERROR", ip, uid, f"Quota update failed: {str(e)}") return {"status": "deleted"} except Exception as e: # Log the error and re-raise with a user-friendly message error_detail = str(e) log_violation("DELETE_ERROR", request.client.host, uid, f"Failed to delete {filename}: {error_detail}") if not isinstance(e, HTTPException): raise HTTPException(status_code=500, detail=f"Failed to delete file: {error_detail}") raise @app.get("/confirm/{uid}") def confirm_user(uid: str, request: Request): ip = request.client.host user = get_user_by_uid(uid) if not user or user.ip != ip: raise HTTPException(status_code=403, detail="Unauthorized") return {"username": user.username, "email": user.email} def verify_and_fix_quota(db: Session, uid: str, user_dir: str) -> int: """ Verify and fix the user's quota based on the size of stream.opus file. Returns the size of stream.opus in bytes. """ stream_opus_path = os.path.join(user_dir, 'stream.opus') total_size = 0 # Only consider stream.opus for quota if os.path.isfile(stream_opus_path): try: total_size = os.path.getsize(stream_opus_path) # Debug messages disabled except (OSError, FileNotFoundError) as e: # Debug messages disabled pass else: # Debug messages disabled pass # Update quota in database q = db.get(UserQuota, uid) or UserQuota(uid=uid, storage_bytes=0) q.storage_bytes = total_size db.add(q) # Clean up any database records for files that don't exist # BUT only for records older than 5 minutes to avoid race conditions with recent uploads from datetime import datetime, timedelta cutoff_time = datetime.utcnow() - timedelta(minutes=5) uploads = db.query(UploadLog).filter( UploadLog.uid == uid, UploadLog.created_at < cutoff_time # Only check older records ).all() for upload in uploads: if upload.processed_filename: # Only check if processed_filename exists stored_filename = f"{upload.id}_{upload.processed_filename}" file_path = os.path.join(user_dir, stored_filename) if not os.path.isfile(file_path): # Debug messages disabled db.delete(upload) try: db.commit() # Debug messages disabled except Exception as e: # Debug messages disabled db.rollback() raise return total_size @app.get("/me/{uid}") def get_me(uid: str, request: Request, response: Response): # Add headers to prevent caching response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" response.headers["Pragma"] = "no-cache" response.headers["Expires"] = "0" # Debug messages disabled # Use the database session context manager for all database operations with get_db() as db: try: # Get user info user = db.query(User).filter((User.username == uid) | (User.email == uid)).first() if not user: print(f"[ERROR] User with UID {uid} not found") raise HTTPException(status_code=404, detail="User not found") # Only enforce IP check in production if not debug_mode: if user.ip != request.client.host: print(f"[WARNING] IP mismatch for UID {uid}: {request.client.host} != {user.ip}") # In production, we might want to be more strict if not debug_mode: raise HTTPException(status_code=403, detail="IP address mismatch") # Get user directory user_dir = os.path.join('data', uid) os.makedirs(user_dir, exist_ok=True) # Get all upload logs for this user using the query interface upload_logs = db.query(UploadLog).filter( UploadLog.uid == uid ).order_by(UploadLog.created_at.desc()).all() # Debug messages disabled # Build file list from database records, checking if files exist on disk files = [] seen_files = set() # Track seen files to avoid duplicates # Debug messages disabled for i, log in enumerate(upload_logs): if not log.filename or not log.processed_filename: # Debug messages disabled continue # The actual filename on disk has the log ID prepended stored_filename = f"{log.id}_{log.processed_filename}" file_path = os.path.join(user_dir, stored_filename) # Skip if we've already seen this file if stored_filename in seen_files: # Debug messages disabled continue seen_files.add(stored_filename) # Only include the file if it exists on disk and is not stream.opus if os.path.isfile(file_path) and stored_filename != 'stream.opus': try: # Get the actual file size in case it changed file_size = os.path.getsize(file_path) file_info = { "name": stored_filename, "original_name": log.filename, "size": file_size } files.append(file_info) # Debug messages disabled except OSError as e: print(f"[WARNING] Could not access file {stored_filename}: {e}") else: # Debug messages disabled pass # Log all files being returned # Debug messages disabled # for i, file_info in enumerate(files, 1): # print(f" {i}. {file_info['name']} (original: {file_info['original_name']}, size: {file_info['size']} bytes)") # Verify and fix quota based on actual files on disk total_size = verify_and_fix_quota(db, uid, user_dir) quota_mb = round(total_size / (1024 * 1024), 2) max_quota_mb = round(MAX_QUOTA_BYTES / (1024 * 1024), 2) # Debug messages disabled response_data = { "files": files, "quota": { "used": quota_mb, "max": max_quota_mb, "used_bytes": total_size, "max_bytes": MAX_QUOTA_BYTES, "percentage": round((total_size / MAX_QUOTA_BYTES) * 100, 2) if MAX_QUOTA_BYTES > 0 else 0 } } # Debug messages disabled return response_data except HTTPException: # Re-raise HTTP exceptions as they are raise except Exception as e: # Log the full traceback for debugging import traceback error_trace = traceback.format_exc() print(f"[ERROR] Error in /me/{uid} endpoint: {str(e)}\n{error_trace}") # Rollback any database changes in case of error db.rollback() # Return a 500 error with a generic message raise HTTPException(status_code=500, detail="Internal server error")