main.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. from fastapi import FastAPI, WebSocket, WebSocketDisconnect
  2. from fastapi.staticfiles import StaticFiles
  3. from fastapi.middleware.cors import CORSMiddleware
  4. from fastapi.responses import FileResponse
  5. import asyncio
  6. import logging
  7. import os
  8. import json
  9. import pymysql
  10. from sqlalchemy import text
  11. from backend.app.core.config import settings
  12. from backend.app.core.database import engine, SessionLocal
  13. from backend.app.models import sql_models
  14. from backend.app.api.api import api_router
  15. from backend.app.services.video_core import video_manager
  16. from backend.app.services.scheduler import init_scheduler
  17. from backend.app.core import security
  18. # Setup logging
  19. logging.basicConfig(level=logging.INFO)
  20. logger = logging.getLogger(__name__)
  21. app = FastAPI(title=settings.PROJECT_NAME)
  22. # CORS
  23. app.add_middleware(
  24. CORSMiddleware,
  25. allow_origins=["*"],
  26. allow_credentials=True,
  27. allow_methods=["*"],
  28. allow_headers=["*"],
  29. )
  30. # API Routes
  31. app.include_router(api_router, prefix=settings.API_V1_STR)
  32. # Static Files
  33. # Ensure directories exist
  34. os.makedirs("backend/app/static", exist_ok=True)
  35. os.makedirs("backend/dist", exist_ok=True)
  36. os.makedirs("backend/dist/assets", exist_ok=True)
  37. app.mount("/static", StaticFiles(directory="backend/app/static"), name="static")
  38. def init_db_if_not_exists():
  39. """Check if database exists, create if not."""
  40. max_retries = 30
  41. retry_interval = 2 # seconds
  42. for attempt in range(max_retries):
  43. try:
  44. logger.info(f"Connecting to MySQL server at {settings.MYSQL_SERVER} to check database (Attempt {attempt + 1}/{max_retries})...")
  45. conn = pymysql.connect(
  46. host=settings.MYSQL_SERVER,
  47. user=settings.MYSQL_USER,
  48. password=settings.MYSQL_PASSWORD,
  49. port=int(settings.MYSQL_PORT),
  50. charset='utf8mb4',
  51. cursorclass=pymysql.cursors.DictCursor
  52. )
  53. with conn.cursor() as cursor:
  54. logger.info(f"Checking database {settings.MYSQL_DB}...")
  55. cursor.execute(f"CREATE DATABASE IF NOT EXISTS {settings.MYSQL_DB} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;")
  56. logger.info("Database checked/created.")
  57. conn.close()
  58. return # Success
  59. except pymysql.err.OperationalError as e:
  60. if e.args[0] == 2003: # Connection refused
  61. logger.warning(f"Database not ready yet, retrying in {retry_interval}s...")
  62. import time
  63. time.sleep(retry_interval)
  64. else:
  65. logger.error(f"Database initialization failed: {e}")
  66. # Don't raise immediately, maybe transient? But usually logic error.
  67. # Let's retry anyway to be safe, or break if critical.
  68. time.sleep(retry_interval)
  69. except Exception as e:
  70. logger.error(f"Database initialization failed: {e}")
  71. import time
  72. time.sleep(retry_interval)
  73. logger.error("Could not connect to database after multiple attempts.")
  74. # raise Exception("Database connection failed") # Optional: crash app
  75. # Startup Event
  76. @app.on_event("startup")
  77. async def startup_event():
  78. logger.info("Starting up...")
  79. # 1. Ensure Database Exists (from init_db_script.py)
  80. init_db_if_not_exists()
  81. # 2. Ensure Tables Exist
  82. try:
  83. sql_models.Base.metadata.create_all(bind=engine)
  84. logger.info("Tables created/verified.")
  85. except Exception as e:
  86. logger.error(f"Error creating tables: {e}")
  87. # 2.5 Migration: Check for new columns
  88. db = SessionLocal()
  89. try:
  90. logger.info("Checking for pending migrations...")
  91. # Check is_superuser
  92. try:
  93. db.execute(text("SELECT is_superuser FROM users LIMIT 1"))
  94. except Exception:
  95. logger.info("Column 'is_superuser' missing in users table. Adding it...")
  96. db.execute(text("ALTER TABLE users ADD COLUMN is_superuser BOOLEAN DEFAULT FALSE"))
  97. db.commit()
  98. logger.info("Column 'is_superuser' added.")
  99. except Exception as e:
  100. logger.error(f"Migration error: {e}")
  101. db.rollback()
  102. finally:
  103. db.close()
  104. # 3. Init Admin User (from reset_admin.py)
  105. db = SessionLocal()
  106. try:
  107. username = "admin"
  108. password = "HNYZ0821"
  109. admin = db.query(sql_models.User).filter(sql_models.User.username == username).first()
  110. if admin:
  111. logger.info(f"User '{username}' found. Resetting password...")
  112. admin.hashed_password = security.get_password_hash(password)
  113. admin.is_active = True
  114. admin.is_superuser = True
  115. db.commit()
  116. logger.info(f"Password for '{username}' has been reset and superuser granted.")
  117. else:
  118. logger.info(f"User '{username}' not found. Creating...")
  119. hashed_pwd = security.get_password_hash(password)
  120. user = sql_models.User(username=username, hashed_password=hashed_pwd, is_active=True, is_superuser=True)
  121. db.add(user)
  122. db.commit()
  123. logger.info(f"User '{username}' created.")
  124. except Exception as e:
  125. logger.error(f"Error initializing admin user: {e}")
  126. # Init Video Streams
  127. try:
  128. cameras = db.query(sql_models.Camera).all()
  129. for cam in cameras:
  130. if cam.stream_url:
  131. video_manager.add_stream(cam.id, cam.stream_url)
  132. except Exception as e:
  133. logger.error(f"Error loading cameras: {e}")
  134. db.close()
  135. # Init Scheduler
  136. init_scheduler()
  137. # WebSocket
  138. @app.websocket("/ws/stream")
  139. async def websocket_endpoint(websocket: WebSocket):
  140. await websocket.accept()
  141. try:
  142. while True:
  143. # 1 FPS
  144. await asyncio.sleep(1)
  145. snapshot_data = video_manager.get_all_snapshots_base64()
  146. await websocket.send_text(json.dumps(snapshot_data))
  147. except WebSocketDisconnect:
  148. logger.info("WebSocket disconnected")
  149. except Exception as e:
  150. logger.error(f"WebSocket error: {e}")
  151. # Frontend Static (Production)
  152. app.mount("/assets", StaticFiles(directory="backend/dist/assets"), name="assets")
  153. @app.get("/{full_path:path}")
  154. async def catch_all(full_path: str):
  155. # API requests are handled above.
  156. # This handles frontend routing.
  157. dist_path = "backend/dist"
  158. file_path = os.path.join(dist_path, full_path)
  159. if os.path.exists(file_path) and os.path.isfile(file_path):
  160. return FileResponse(file_path)
  161. # Default to index.html for SPA
  162. index_path = os.path.join(dist_path, "index.html")
  163. if os.path.exists(index_path):
  164. return FileResponse(index_path)
  165. return FileResponse(file_path) # Fallback (might 404)