"""YakPanel - Database API""" import os from fastapi import APIRouter, Depends, HTTPException, Query from fastapi.responses import FileResponse from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func from pydantic import BaseModel from app.core.database import get_db from app.core.config import get_runtime_config from app.core.notification import send_email from app.api.auth import get_current_user from app.models.user import User from app.models.database import Database from app.services.database_service import ( create_mysql_database, drop_mysql_database, backup_mysql_database, restore_mysql_database, change_mysql_password, create_postgresql_database, drop_postgresql_database, backup_postgresql_database, restore_postgresql_database, change_postgresql_password, create_mongodb_database, drop_mongodb_database, backup_mongodb_database, restore_mongodb_database, change_mongodb_password, ) router = APIRouter(prefix="/database", tags=["database"]) class CreateDatabaseRequest(BaseModel): name: str username: str password: str db_type: str = "MySQL" ps: str = "" class UpdateDbPasswordRequest(BaseModel): password: str @router.get("/list") async def database_list( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """List databases""" result = await db.execute(select(Database).order_by(Database.id)) rows = result.scalars().all() return [{"id": r.id, "name": r.name, "username": r.username, "db_type": r.db_type, "ps": r.ps} for r in rows] @router.post("/create") async def database_create( body: CreateDatabaseRequest, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Create database (panel DB + actual MySQL/PostgreSQL when supported)""" result = await db.execute(select(Database).where(Database.name == body.name)) if result.scalar_one_or_none(): raise HTTPException(status_code=400, detail="Database already exists") if body.db_type == "MySQL": ok, msg = create_mysql_database(body.name, body.username, body.password) if not ok: raise HTTPException(status_code=400, detail=msg) elif body.db_type == "PostgreSQL": ok, msg = create_postgresql_database(body.name, body.username, body.password) if not ok: raise HTTPException(status_code=400, detail=msg) elif body.db_type == "MongoDB": ok, msg = create_mongodb_database(body.name, body.username, body.password) if not ok: raise HTTPException(status_code=400, detail=msg) dbo = Database( name=body.name, username=body.username, password=body.password, db_type=body.db_type, ps=body.ps, ) db.add(dbo) await db.commit() return {"status": True, "msg": "Database created", "id": dbo.id} @router.put("/{db_id}/password") async def database_update_password( db_id: int, body: UpdateDbPasswordRequest, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Change database user password (MySQL, PostgreSQL, or MongoDB)""" result = await db.execute(select(Database).where(Database.id == db_id)) dbo = result.scalar_one_or_none() if not dbo: raise HTTPException(status_code=404, detail="Database not found") if dbo.db_type not in ("MySQL", "PostgreSQL", "MongoDB"): raise HTTPException(status_code=400, detail="Only MySQL, PostgreSQL, and MongoDB supported") if not body.password or len(body.password) < 6: raise HTTPException(status_code=400, detail="Password must be at least 6 characters") if dbo.db_type == "MySQL": ok, msg = change_mysql_password(dbo.username, body.password) elif dbo.db_type == "PostgreSQL": ok, msg = change_postgresql_password(dbo.username, body.password) else: ok, msg = change_mongodb_password(dbo.username, dbo.name, body.password) if not ok: raise HTTPException(status_code=400, detail=msg) dbo.password = body.password await db.commit() return {"status": True, "msg": "Password updated"} @router.delete("/{db_id}") async def database_delete( db_id: int, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Delete database (panel record + actual MySQL/PostgreSQL when supported)""" result = await db.execute(select(Database).where(Database.id == db_id)) dbo = result.scalar_one_or_none() if not dbo: raise HTTPException(status_code=404, detail="Database not found") if dbo.db_type == "MySQL": ok, msg = drop_mysql_database(dbo.name, dbo.username) if not ok: raise HTTPException(status_code=400, detail=msg) elif dbo.db_type == "PostgreSQL": ok, msg = drop_postgresql_database(dbo.name, dbo.username) if not ok: raise HTTPException(status_code=400, detail=msg) elif dbo.db_type == "MongoDB": ok, msg = drop_mongodb_database(dbo.name, dbo.username) if not ok: raise HTTPException(status_code=400, detail=msg) await db.delete(dbo) await db.commit() return {"status": True, "msg": "Database deleted"} @router.get("/count") async def database_count( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Get database count""" result = await db.execute(select(func.count()).select_from(Database)) return {"count": result.scalar() or 0} @router.post("/{db_id}/backup") async def database_backup( db_id: int, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Create database backup (MySQL: mysqldump, PostgreSQL: pg_dump, MongoDB: mongodump)""" result = await db.execute(select(Database).where(Database.id == db_id)) dbo = result.scalar_one_or_none() if not dbo: raise HTTPException(status_code=404, detail="Database not found") if dbo.db_type not in ("MySQL", "PostgreSQL", "MongoDB"): raise HTTPException(status_code=400, detail="Backup not supported for this database type") cfg = get_runtime_config() backup_dir = os.path.join(cfg["backup_path"], "database") if dbo.db_type == "MySQL": ok, msg, filename = backup_mysql_database(dbo.name, backup_dir) elif dbo.db_type == "PostgreSQL": ok, msg, filename = backup_postgresql_database(dbo.name, backup_dir) else: ok, msg, filename = backup_mongodb_database(dbo.name, backup_dir) if not ok: raise HTTPException(status_code=500, detail=msg) send_email( subject=f"YakPanel - Database backup: {dbo.name}", body=f"Backup completed: {filename}\nDatabase: {dbo.name}", ) return {"status": True, "msg": "Backup created", "filename": filename} @router.get("/{db_id}/backups") async def database_backups_list( db_id: int, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """List backups for a database""" result = await db.execute(select(Database).where(Database.id == db_id)) dbo = result.scalar_one_or_none() if not dbo: raise HTTPException(status_code=404, detail="Database not found") cfg = get_runtime_config() backup_dir = os.path.join(cfg["backup_path"], "database") if not os.path.isdir(backup_dir): return {"backups": []} prefix = f"{dbo.name}_" backups = [] for f in os.listdir(backup_dir): if f.startswith(prefix) and f.endswith(".sql.gz"): p = os.path.join(backup_dir, f) backups.append({"filename": f, "size": os.path.getsize(p) if os.path.isfile(p) else 0}) backups.sort(key=lambda x: x["filename"], reverse=True) return {"backups": backups} @router.get("/{db_id}/backups/download") async def database_backup_download( db_id: int, file: str = Query(...), current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Download database backup""" result = await db.execute(select(Database).where(Database.id == db_id)) dbo = result.scalar_one_or_none() if not dbo: raise HTTPException(status_code=404, detail="Database not found") if ".." in file or "/" in file or "\\" in file or not file.startswith(f"{dbo.name}_"): raise HTTPException(status_code=400, detail="Invalid filename") if not (file.endswith(".sql.gz") or file.endswith(".tar.gz")): raise HTTPException(status_code=400, detail="Invalid filename") cfg = get_runtime_config() path = os.path.join(cfg["backup_path"], "database", file) if not os.path.isfile(path): raise HTTPException(status_code=404, detail="Backup not found") return FileResponse(path, filename=file) class RestoreRequest(BaseModel): filename: str @router.post("/{db_id}/restore") async def database_restore( db_id: int, body: RestoreRequest, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Restore database from backup""" result = await db.execute(select(Database).where(Database.id == db_id)) dbo = result.scalar_one_or_none() if not dbo: raise HTTPException(status_code=404, detail="Database not found") file = body.filename if ".." in file or "/" in file or "\\" in file or not file.startswith(f"{dbo.name}_"): raise HTTPException(status_code=400, detail="Invalid filename") valid_ext = file.endswith(".sql.gz") or file.endswith(".tar.gz") if not valid_ext: raise HTTPException(status_code=400, detail="Invalid filename") if dbo.db_type in ("MySQL", "PostgreSQL") and not file.endswith(".sql.gz"): raise HTTPException(status_code=400, detail="Wrong backup format for this database type") if dbo.db_type == "MongoDB" and not file.endswith(".tar.gz"): raise HTTPException(status_code=400, detail="Wrong backup format for MongoDB") cfg = get_runtime_config() backup_path = os.path.join(cfg["backup_path"], "database", file) if dbo.db_type == "MySQL": ok, msg = restore_mysql_database(dbo.name, backup_path) elif dbo.db_type == "PostgreSQL": ok, msg = restore_postgresql_database(dbo.name, backup_path) else: ok, msg = restore_mongodb_database(dbo.name, backup_path) if not ok: raise HTTPException(status_code=500, detail=msg) return {"status": True, "msg": "Restored"}