c9a99f4fb3
代码修复: - 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>
152 lines
5.0 KiB
Python
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} |