Files
yakpanel-core/YakPanel-server/backend/app/api/ftp.py
2026-04-07 13:23:35 +05:30

134 lines
4.3 KiB
Python

"""YakPanel - FTP API"""
import os
from fastapi import APIRouter, Depends, HTTPException, Query
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.security import get_password_hash
from app.api.auth import get_current_user
from app.models.user import User
from app.models.ftp import Ftp
from app.services.ftp_service import create_ftp_user, delete_ftp_user, update_ftp_password
router = APIRouter(prefix="/ftp", tags=["ftp"])
class CreateFtpRequest(BaseModel):
name: str
password: str
path: str
pid: int = 0
ps: str = ""
class UpdateFtpPasswordRequest(BaseModel):
password: str
@router.get("/list")
async def ftp_list(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""List FTP accounts"""
result = await db.execute(select(Ftp).order_by(Ftp.id))
rows = result.scalars().all()
return [{"id": r.id, "name": r.name, "path": r.path, "ps": r.ps} for r in rows]
@router.post("/create")
async def ftp_create(
body: CreateFtpRequest,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Create FTP account (panel + Pure-FTPd when available)"""
result = await db.execute(select(Ftp).where(Ftp.name == body.name))
if result.scalar_one_or_none():
raise HTTPException(status_code=400, detail="FTP account already exists")
ok, msg = create_ftp_user(body.name, body.password, body.path)
if not ok:
raise HTTPException(status_code=400, detail=f"FTP: {msg}")
ftp = Ftp(
name=body.name,
password=get_password_hash(body.password),
path=body.path,
pid=body.pid,
ps=body.ps,
)
db.add(ftp)
await db.commit()
return {"status": True, "msg": "FTP account created", "id": ftp.id}
@router.put("/{ftp_id}/password")
async def ftp_update_password(
ftp_id: int,
body: UpdateFtpPasswordRequest,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Update FTP account password"""
result = await db.execute(select(Ftp).where(Ftp.id == ftp_id))
ftp = result.scalar_one_or_none()
if not ftp:
raise HTTPException(status_code=404, detail="FTP account not found")
if not body.password or len(body.password) < 6:
raise HTTPException(status_code=400, detail="Password must be at least 6 characters")
ok, msg = update_ftp_password(ftp.name, body.password)
if not ok:
raise HTTPException(status_code=400, detail=f"FTP: {msg}")
ftp.password = get_password_hash(body.password)
await db.commit()
return {"status": True, "msg": "Password updated"}
@router.delete("/{ftp_id}")
async def ftp_delete(
ftp_id: int,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Delete FTP account (panel + Pure-FTPd when available)"""
result = await db.execute(select(Ftp).where(Ftp.id == ftp_id))
ftp = result.scalar_one_or_none()
if not ftp:
raise HTTPException(status_code=404, detail="FTP account not found")
ok, msg = delete_ftp_user(ftp.name)
if not ok:
raise HTTPException(status_code=400, detail=f"FTP: {msg}")
await db.delete(ftp)
await db.commit()
return {"status": True, "msg": "FTP account deleted"}
@router.get("/logs")
async def ftp_logs(
lines: int = Query(default=200, ge=1, le=5000),
current_user: User = Depends(get_current_user),
):
"""Tail common Pure-FTPd log paths if readable (non-destructive)."""
candidates = [
"/var/log/pure-ftpd/pure-ftpd.log",
"/var/log/pureftpd.log",
"/var/log/messages",
]
for path in candidates:
if os.path.isfile(path):
out, err = exec_shell_sync(f'tail -n {int(lines)} "{path}" 2>/dev/null', timeout=15)
text = (out or "") + (err or "")
return {"path": path, "content": text[-800000:] or "(empty)"}
return {"path": None, "content": "No known FTP log file found on this server."}
@router.get("/count")
async def ftp_count(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Get FTP count"""
result = await db.execute(select(func.count()).select_from(Ftp))
return {"count": result.scalar() or 0}