Files
Nexus/server/api/sync_v2.py
T
Your Name c9a99f4fb3 fix: 全项目文档对齐 + 代码清理
代码修复:
- web/agent/agent.py: datetime.utcnow() → datetime.now(timezone.utc)
- requirements.txt: 移除 paramiko==3.5.0 (已无活跃引用)
- 删除 server/infrastructure/ssh/pool.py (DEPRECATED, 无引用)

连接池三层对齐 (config.py/.env.example/session.py):
- DB_POOL_SIZE 100→160, DB_MAX_OVERFLOW 100→120
- 基于 MySQL max_connections=400, install.php 公式

文档修复 (21项):
- P0: 硬编码域名/IP替换为配置变量占位符
- P1: 10个过时设计文档加归档标注 (引用旧文件结构)
- P2: step-3-webssh报告 paramiko引用修正
- 6份审查报告连接池参数勘误 100/100→160/120
- ECC安全报告 EC5 datetime.utcnow 标记已修复
- docs/README.md 文档索引重写
- docs/memory/mem_nexus_overview.md 移除硬编码凭证
- docs/project/tech-stack-inventory.md paramiko标记已移除

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

152 lines
5.0 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:
perms, _, owner, group, size, *date_parts, name = (
parts[0], parts[1], parts[2], parts[3], parts[4],
parts[5:-1], parts[-1]
)
is_dir = parts[0].startswith("d")
entries.append({
"name": parts[-1],
"is_dir": is_dir,
"size": parts[4],
"perms": parts[0],
"owner": parts[2],
"modified": " ".join(parts[5:8]),
})
return {"path": path, "entries": entries}