275 lines
10 KiB
Python
275 lines
10 KiB
Python
"""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"}
|