Files
Nexus/server/api/sync_v2.py
T
Your Name cb5b4c42de 多Worker守护 + 告警服务器名 + 清理PHP残留
- main.py: Redis主Worker选举,防止多Worker重复执行后台任务
- agent.py: 告警/恢复广播使用真实服务器名(查MySQL)替代"server-{id}"
- sync_v2.py: 修复browse目录解析死代码,正确取parts[8]
- install.html: 移除过时install.php引用,更新为"删除.env重装"
- nginx配置: 移除PHP-FPM路由(install.html纯静态无需PHP)
- schedule_runner: 清理未使用的get_redis_sync导入

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 11:54:11 +08:00

148 lines
4.8 KiB
Python

"""Nexus — Sync Engine v2 API Routes (Step 4)
S1: POST /api/sync/files — rsync file sync
S2: POST /api/sync/commands — SSH exec batch
S3: POST /api/sync/config — system config push
S4: POST /api/sync/sftp — SFTP transfer
"""
from fastapi import APIRouter, Depends, HTTPException, Request
from server.api.schemas import SyncFiles, SyncCommands, SyncConfig, SyncSftp, SyncBrowse
from server.api.dependencies import get_server_service
from server.application.services.sync_engine_v2 import SyncEngineV2
from server.infrastructure.database.server_repo import ServerRepositoryImpl
from server.infrastructure.database.sync_log_repo import SyncLogRepositoryImpl
from server.infrastructure.database.audit_log_repo import AuditLogRepositoryImpl
from server.infrastructure.database.push_schedule_repo import PushRetryJobRepositoryImpl
from server.api.auth_jwt import get_current_admin
from server.domain.models import Admin
router = APIRouter(prefix="/api/sync", tags=["sync"])
async def _get_sync_engine(request: Request) -> SyncEngineV2:
"""DI factory for SyncEngineV2"""
session = request.state.db
return SyncEngineV2(
server_repo=ServerRepositoryImpl(session),
sync_log_repo=SyncLogRepositoryImpl(session),
audit_repo=AuditLogRepositoryImpl(session),
retry_repo=PushRetryJobRepositoryImpl(session),
)
@router.post("/files")
async def sync_files(
payload: SyncFiles,
request: Request,
admin: Admin = Depends(get_current_admin),
):
"""S1: File sync via rsync over SSH (with parallel execution)"""
engine = await _get_sync_engine(request)
return await engine.sync_files(
server_ids=payload.server_ids,
source_path=payload.source_path,
target_path=payload.target_path,
sync_mode=payload.sync_mode,
operator=admin.username,
batch_size=payload.batch_size,
concurrency=payload.concurrency,
)
@router.post("/commands")
async def sync_commands(
payload: SyncCommands,
request: Request,
admin: Admin = Depends(get_current_admin),
):
"""S2: Execute commands on multiple servers via SSH in parallel"""
engine = await _get_sync_engine(request)
return await engine.sync_commands(
server_ids=payload.server_ids,
commands=payload.commands,
operator=admin.username,
timeout=payload.timeout,
concurrency=payload.concurrency,
)
@router.post("/config")
async def sync_config(
payload: SyncConfig,
request: Request,
admin: Admin = Depends(get_current_admin),
):
"""S3: Push system configuration changes to multiple servers"""
engine = await _get_sync_engine(request)
return await engine.sync_config(
server_ids=payload.server_ids,
config_updates=payload.config,
operator=admin.username,
concurrency=payload.concurrency,
)
@router.post("/sftp")
async def sftp_transfer(
payload: SyncSftp,
request: Request,
admin: Admin = Depends(get_current_admin),
):
"""S4: SFTP file transfer to/from a server"""
engine = await _get_sync_engine(request)
return await engine.sftp_transfer(
server_id=payload.server_id,
operation=payload.operation,
local_path=payload.local_path,
remote_path=payload.remote_path,
operator=admin.username,
)
@router.post("/browse")
async def browse_directory(
payload: SyncBrowse,
request: Request,
admin: Admin = Depends(get_current_admin),
):
"""Browse remote directory listing via SSH"""
from server.infrastructure.ssh.asyncssh_pool import exec_ssh_command
from server.infrastructure.database.server_repo import ServerRepositoryImpl
session = request.state.db
server_id = payload.server_id
path = payload.path
if not server_id:
raise HTTPException(status_code=400, detail="server_id required")
repo = ServerRepositoryImpl(session)
server = await repo.get_by_id(server_id)
if not server:
raise HTTPException(status_code=404, detail="Server not found")
# Use ls -la for detailed listing, parse output
import shlex
result = await exec_ssh_command(server, f"ls -la {shlex.quote(path)}", timeout=10)
if result["exit_code"] != 0:
return {"path": path, "entries": [], "error": result["stderr"][:500]}
entries = []
for line in result["stdout"].split("\n")[1:]: # Skip "total" line
if not line.strip():
continue
parts = line.split(None, 8)
if len(parts) >= 9:
is_dir = parts[0].startswith("d")
entries.append({
"name": parts[8], # Last field after split(None, 8) — handles spaces in names
"is_dir": is_dir,
"size": parts[4],
"perms": parts[0],
"owner": parts[2],
"modified": " ".join(parts[5:8]),
})
return {"path": path, "entries": entries}