fix: 全站 ruff 清零 — 77 errors → 0
Fixes: - F821: install.py site_url 未定义(NameError)、sync_v2.py os 未导入、script_jobs.py LOG f-string - F401: 移除 22 个未使用导入(models/__init__.py、install.py、auth.py 等) - B904: 20 个 except 块 raise 添加 from e/from None - S110: 20 个有意的 try-except-pass 添加 noqa 注释(SSH清理/DDL幂等/WS断连) - B007: 3 个未使用循环变量重命名(dirs→_dirs、server_id→_server_id) - F541: 3 个无占位符 f-string 修正 - F841: auth.py 未使用 ip_address 变量移除 - S105: auth_service.py Redis key prefix 误报 noqa - B023: script_execution_flush.py 循环变量绑定修复 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -334,7 +334,6 @@ async def issue_webssh_token(
|
||||
if not server.domain:
|
||||
raise HTTPException(status_code=400, detail="Server has no domain configured")
|
||||
|
||||
ip_address = request.client.host if request.client else ""
|
||||
result = await service.create_webssh_token(admin, payload.server_id)
|
||||
return {**result, "server_name": server.name}
|
||||
|
||||
|
||||
+10
-11
@@ -17,7 +17,7 @@ from urllib.parse import quote_plus
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
|
||||
from server.infrastructure.database.engine_compat import patch_aiomysql_do_ping
|
||||
|
||||
@@ -206,7 +206,7 @@ def _configure_guardian(install_dir: str, api_port: str) -> list[str]:
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
results.append(f"✓ 日志目录已创建 {log_dir}")
|
||||
except OSError:
|
||||
results.append(f"✗ 日志目录创建失败(需 root 权限)")
|
||||
results.append("✗ 日志目录创建失败(需 root 权限)")
|
||||
|
||||
# 2. Supervisor config
|
||||
if bt_panel:
|
||||
@@ -320,7 +320,6 @@ async def env_check():
|
||||
"""Detect environment readiness — Python, MySQL client, Redis, write permissions."""
|
||||
_reject_post_install_wizard()
|
||||
import sys
|
||||
import importlib
|
||||
|
||||
checks = []
|
||||
|
||||
@@ -406,12 +405,13 @@ async def init_db(req: InitDbRequest):
|
||||
|
||||
db_url = _make_db_url(req)
|
||||
redis_url = _build_redis_url(req)
|
||||
site_url = req.site_url.rstrip("/") if req.site_url else f"http://127.0.0.1:{req.api_port}"
|
||||
|
||||
try:
|
||||
patch_aiomysql_do_ping()
|
||||
engine = create_async_engine(db_url, pool_pre_ping=True, pool_recycle=300)
|
||||
except Exception as e:
|
||||
raise HTTPException(400, f"数据库引擎创建失败: {e}")
|
||||
raise HTTPException(400, f"数据库引擎创建失败: {e}") from e
|
||||
|
||||
try:
|
||||
async with engine.begin() as conn:
|
||||
@@ -435,7 +435,7 @@ async def init_db(req: InitDbRequest):
|
||||
for idx_sql in indexes:
|
||||
try:
|
||||
await conn.execute(text(idx_sql))
|
||||
except Exception:
|
||||
except Exception: # noqa: S110 — DDL idempotent, may already exist
|
||||
pass # Index may already exist
|
||||
|
||||
# Schema migrations for existing databases (safe — no-op if column exists)
|
||||
@@ -445,7 +445,7 @@ async def init_db(req: InitDbRequest):
|
||||
for mig_sql in migrations:
|
||||
try:
|
||||
await conn.execute(text(mig_sql))
|
||||
except Exception:
|
||||
except Exception: # noqa: S110 — DDL idempotent, may already exist
|
||||
pass # Column may already exist
|
||||
|
||||
# Calculate pool_size from max_connections
|
||||
@@ -462,7 +462,7 @@ async def init_db(req: InitDbRequest):
|
||||
secret_key = secrets.token_hex(32)
|
||||
api_key = secrets.token_hex(16)
|
||||
# Generate Fernet-compatible encryption key (32 url-safe base64 bytes)
|
||||
import base64, hashlib
|
||||
import base64
|
||||
encryption_key = base64.urlsafe_b64encode(secrets.token_bytes(32)).decode()
|
||||
|
||||
# Insert settings
|
||||
@@ -491,12 +491,11 @@ async def init_db(req: InitDbRequest):
|
||||
|
||||
except Exception as e:
|
||||
await engine.dispose()
|
||||
raise HTTPException(400, f"数据库初始化失败: {e}")
|
||||
raise HTTPException(400, f"数据库初始化失败: {e}") from e
|
||||
|
||||
await engine.dispose()
|
||||
|
||||
# Write .env
|
||||
site_url = req.site_url.rstrip("/") if req.site_url else f"http://127.0.0.1:{req.api_port}"
|
||||
_write_env(req, secret_key, api_key, encryption_key, pool_size, max_overflow, redis_url, site_url)
|
||||
|
||||
# Write config.json
|
||||
@@ -588,7 +587,7 @@ async def create_admin(req: CreateAdminRequest):
|
||||
|
||||
await engine.dispose()
|
||||
except Exception as e:
|
||||
raise HTTPException(400, f"创建管理员失败: {e}")
|
||||
raise HTTPException(400, f"创建管理员失败: {e}") from e
|
||||
|
||||
# Update config.json with brand
|
||||
if CONFIG_JSON.exists():
|
||||
@@ -597,7 +596,7 @@ async def create_admin(req: CreateAdminRequest):
|
||||
config = json.loads(CONFIG_JSON.read_text())
|
||||
config["app_name"] = req.system_name
|
||||
CONFIG_JSON.write_text(json.dumps(config, indent=2, ensure_ascii=False))
|
||||
except Exception:
|
||||
except Exception: # noqa: S110 — best-effort cleanup
|
||||
pass
|
||||
|
||||
# Update .env with brand
|
||||
|
||||
@@ -3,7 +3,6 @@ Presentation layer — receives HTTP requests, delegates to ScriptService.
|
||||
All operations require JWT authentication.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||
|
||||
@@ -19,7 +19,7 @@ from server.infrastructure.database.push_schedule_repo import PushScheduleReposi
|
||||
from server.infrastructure.database.password_preset_repo import PasswordPresetRepositoryImpl
|
||||
from server.infrastructure.database.ssh_key_preset_repo import SshKeyPresetRepositoryImpl
|
||||
from server.infrastructure.database.audit_log_repo import AuditLogRepositoryImpl
|
||||
from server.domain.models import Setting, PushSchedule, PushRetryJob, PasswordPreset, SshKeyPreset, AuditLog, Admin
|
||||
from server.domain.models import PushSchedule, PushRetryJob, PasswordPreset, SshKeyPreset, AuditLog, Admin
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -327,7 +327,7 @@ async def reveal_preset(
|
||||
try:
|
||||
plaintext = decrypt_value(preset.encrypted_pw)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=500, detail="解密失败")
|
||||
raise HTTPException(status_code=500, detail="解密失败") from None
|
||||
|
||||
audit_repo = AuditLogRepositoryImpl(db)
|
||||
await audit_repo.create(AuditLog(
|
||||
@@ -472,7 +472,7 @@ async def reveal_ssh_key_preset(
|
||||
try:
|
||||
plaintext = decrypt_value(preset.encrypted_private_key)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=500, detail="解密失败")
|
||||
raise HTTPException(status_code=500, detail="解密失败") from None
|
||||
|
||||
audit_repo = AuditLogRepositoryImpl(db)
|
||||
await audit_repo.create(AuditLog(
|
||||
@@ -689,7 +689,7 @@ async def telegram_get_chats(
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=502, detail=f"请求失败: {e}")
|
||||
raise HTTPException(status_code=502, detail=f"请求失败: {e}") from e
|
||||
|
||||
# Extract unique chats
|
||||
seen: set[int] = set()
|
||||
@@ -748,11 +748,11 @@ async def parse_subscription(
|
||||
raise HTTPException(status_code=400, detail=f"订阅链接返回 HTTP {resp.status_code}")
|
||||
raw = resp.text
|
||||
except httpx.TimeoutException:
|
||||
raise HTTPException(status_code=400, detail="请求订阅链接超时(15s)")
|
||||
raise HTTPException(status_code=400, detail="请求订阅链接超时(15s)") from None
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=f"获取订阅失败: {e}")
|
||||
raise HTTPException(status_code=400, detail=f"获取订阅失败: {e}") from e
|
||||
|
||||
hosts = parse_subscription_content(raw)
|
||||
if not hosts:
|
||||
@@ -914,7 +914,7 @@ async def get_ip_allowlist(
|
||||
):
|
||||
"""Return full allowlist state."""
|
||||
from server.config import settings as _settings
|
||||
from server.background.ip_allowlist_refresh import get_last_refresh_time, get_subscription_count
|
||||
from server.background.ip_allowlist_refresh import get_last_refresh_time
|
||||
|
||||
allowlist_on = (_settings.LOGIN_ALLOWLIST_ENABLED or "false").lower() in ("true","1","yes","on")
|
||||
sub_ips_raw = _settings.LOGIN_SUBSCRIPTION_IPS or ""
|
||||
|
||||
+13
-13
@@ -10,7 +10,6 @@ import logging
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
|
||||
from server.api.schemas import SyncFiles, SyncBrowse, SyncBrowseLocal, SyncPreview, FileOperation, LocalFileOperation, LocalFilePreview, SyncVerify, SyncCancel, ValidateSourcePath, SyncDiagnose, FileSyncDiff
|
||||
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
|
||||
@@ -227,7 +226,7 @@ async def browse_local_directory(
|
||||
except OSError:
|
||||
continue
|
||||
except PermissionError:
|
||||
raise HTTPException(status_code=403, detail="无权限访问该目录")
|
||||
raise HTTPException(status_code=403, detail="无权限访问该目录") from None
|
||||
|
||||
return {"path": real_path, "entries": entries}
|
||||
|
||||
@@ -261,7 +260,7 @@ async def local_file_operation(
|
||||
else:
|
||||
os.unlink(real_path)
|
||||
except OSError as e:
|
||||
raise HTTPException(status_code=500, detail=f"删除失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"删除失败: {e}") from e
|
||||
|
||||
await _audit_sync(
|
||||
"local_file_delete", "sync", 0,
|
||||
@@ -293,7 +292,7 @@ async def local_file_operation(
|
||||
try:
|
||||
os.rename(real_path, new_path)
|
||||
except OSError as e:
|
||||
raise HTTPException(status_code=500, detail=f"重命名失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"重命名失败: {e}") from e
|
||||
|
||||
await _audit_sync(
|
||||
"local_file_rename", "sync", 0,
|
||||
@@ -398,7 +397,7 @@ async def cancel_sync(
|
||||
redis = get_redis()
|
||||
await redis.set(f"sync:cancel:{payload.batch_id}", "1", ex=3600)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"取消操作失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"取消操作失败: {e}") from e
|
||||
|
||||
await _audit_sync(
|
||||
"sync_cancel", "sync", 0,
|
||||
@@ -745,7 +744,7 @@ async def upload_file(
|
||||
try:
|
||||
server_id = int(server_id_raw)
|
||||
except (ValueError, TypeError):
|
||||
raise HTTPException(status_code=400, detail="server_id 必须为数字")
|
||||
raise HTTPException(status_code=400, detail="server_id 必须为数字") from None
|
||||
|
||||
if not file or not hasattr(file, "filename") or not file.filename:
|
||||
raise HTTPException(status_code=400, detail="请选择要上传的文件")
|
||||
@@ -785,11 +784,11 @@ async def upload_file(
|
||||
finally:
|
||||
await ssh_pool.release(server.id)
|
||||
except asyncssh.PermissionDenied:
|
||||
raise HTTPException(status_code=403, detail=f"SSH 权限不足,无法写入 {full_remote_path}")
|
||||
raise HTTPException(status_code=403, detail=f"SSH 权限不足,无法写入 {full_remote_path}") from None
|
||||
except asyncssh.SFTPError as e:
|
||||
raise HTTPException(status_code=400, detail=f"SFTP 上传失败: {e}")
|
||||
raise HTTPException(status_code=400, detail=f"SFTP 上传失败: {e}") from e
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=502, detail=f"SSH 连接失败: {e}")
|
||||
raise HTTPException(status_code=502, detail=f"SSH 连接失败: {e}") from e
|
||||
|
||||
# Audit
|
||||
await _audit_sync(
|
||||
@@ -881,7 +880,7 @@ async def upload_zip(
|
||||
|
||||
# Build file listing (relative paths, capped at 500)
|
||||
files = []
|
||||
for root, dirs, fnames in os.walk(extract_dir):
|
||||
for root, _dirs, fnames in os.walk(extract_dir):
|
||||
for fn in fnames:
|
||||
rel = os.path.relpath(os.path.join(root, fn), extract_dir)
|
||||
files.append(rel)
|
||||
@@ -921,14 +920,14 @@ async def upload_zip(
|
||||
os.unlink(zip_path)
|
||||
except OSError:
|
||||
pass
|
||||
raise HTTPException(status_code=400, detail="无效的 ZIP 文件")
|
||||
raise HTTPException(status_code=400, detail="无效的 ZIP 文件") from None
|
||||
except Exception as e:
|
||||
shutil.rmtree(extract_dir, ignore_errors=True)
|
||||
try:
|
||||
os.unlink(zip_path)
|
||||
except OSError:
|
||||
pass
|
||||
raise HTTPException(status_code=500, detail=f"解压失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"解压失败: {e}") from e
|
||||
|
||||
|
||||
# ── Post-Push Verification (sha256sum comparison) ──
|
||||
@@ -947,6 +946,7 @@ async def verify_sync(
|
||||
"""
|
||||
import asyncio
|
||||
import hashlib
|
||||
import os
|
||||
import shlex
|
||||
from server.infrastructure.ssh.asyncssh_pool import exec_ssh_command
|
||||
from server.infrastructure.database.server_repo import ServerRepositoryImpl
|
||||
@@ -958,7 +958,7 @@ async def verify_sync(
|
||||
|
||||
# Build local file manifest: {relative_path: sha256hex}
|
||||
local_files = {}
|
||||
for root, dirs, fnames in os.walk(payload.source_path):
|
||||
for root, _dirs, fnames in os.walk(payload.source_path):
|
||||
for fn in fnames:
|
||||
full = os.path.join(root, fn)
|
||||
rel = os.path.relpath(full, payload.source_path)
|
||||
|
||||
@@ -15,7 +15,7 @@ import asyncio
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from typing import Dict, Set, Optional
|
||||
from typing import Dict, Optional
|
||||
|
||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Query
|
||||
|
||||
@@ -71,7 +71,7 @@ class ConnectionManager:
|
||||
)
|
||||
except RuntimeError:
|
||||
pass # No running event loop (e.g. during shutdown)
|
||||
except Exception:
|
||||
except Exception: # noqa: S110 — client disconnected, best-effort cleanup
|
||||
pass
|
||||
logger.info(f"WebSocket disconnected: {client_id}, total: {len(self._connections)}")
|
||||
|
||||
|
||||
+9
-10
@@ -5,7 +5,6 @@ import asyncio
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Query, Path
|
||||
@@ -166,11 +165,11 @@ async def terminal_ws(
|
||||
logger.error(f"WebSSH connection failed: {e}")
|
||||
try:
|
||||
await websocket.send_json({"type": MSG_ERROR, "message": f"SSH连接失败: {str(e)}"})
|
||||
except Exception:
|
||||
except Exception: # noqa: S110 — best-effort cleanup
|
||||
pass
|
||||
try:
|
||||
await websocket.close(code=4003, reason="SSH connection failed")
|
||||
except Exception:
|
||||
except Exception: # noqa: S110 — best-effort cleanup
|
||||
pass
|
||||
await _close_ssh_session(session_id)
|
||||
return
|
||||
@@ -199,11 +198,11 @@ async def terminal_ws(
|
||||
logger.error(f"WebSSH shell creation failed on fresh connection (server={server.id}): {type(e2).__name__}: {e2!r}")
|
||||
try:
|
||||
await websocket.send_json({"type": MSG_ERROR, "message": f"Shell创建失败: {type(e2).__name__}"})
|
||||
except Exception:
|
||||
except Exception: # noqa: S110 — best-effort cleanup
|
||||
pass
|
||||
try:
|
||||
await websocket.close(code=4003, reason="Shell creation failed")
|
||||
except Exception:
|
||||
except Exception: # noqa: S110 — best-effort cleanup
|
||||
pass
|
||||
ws_closed = True
|
||||
await _close_ssh_session(session_id)
|
||||
@@ -240,7 +239,7 @@ async def terminal_ws(
|
||||
"type": MSG_DATA,
|
||||
"data": data,
|
||||
})
|
||||
except Exception:
|
||||
except Exception: # noqa: S110 — SSH read loop, connection teardown
|
||||
pass
|
||||
|
||||
async def _read_websocket_input():
|
||||
@@ -296,7 +295,7 @@ async def terminal_ws(
|
||||
|
||||
except WebSocketDisconnect:
|
||||
pass
|
||||
except Exception:
|
||||
except Exception: # noqa: S110 — WebSocket read loop, connection teardown
|
||||
pass
|
||||
|
||||
# Run both directions concurrently
|
||||
@@ -323,7 +322,7 @@ async def terminal_ws(
|
||||
if not ws_closed:
|
||||
try:
|
||||
await websocket.send_json({"type": MSG_ERROR, "message": f"Shell创建失败: {type(e).__name__}"})
|
||||
except Exception:
|
||||
except Exception: # noqa: S110 — best-effort cleanup
|
||||
pass
|
||||
finally:
|
||||
# ── Cleanup ──
|
||||
@@ -333,12 +332,12 @@ async def terminal_ws(
|
||||
if not ws_closed:
|
||||
try:
|
||||
await websocket.send_json({"type": MSG_CLOSE, "session_id": session_id})
|
||||
except Exception:
|
||||
except Exception: # noqa: S110 — best-effort cleanup
|
||||
pass
|
||||
|
||||
try:
|
||||
await websocket.close()
|
||||
except Exception:
|
||||
except Exception: # noqa: S110 — best-effort cleanup
|
||||
pass
|
||||
|
||||
logger.info(f"WebSSH session closed: admin={admin.username}, server={server.name}, session={session_id}")
|
||||
|
||||
@@ -60,7 +60,7 @@ JWT_REFRESH_TOKEN_EXPIRE_DAYS = 30 # 30 days
|
||||
JWT_WEBSSH_TOKEN_EXPIRE_MINUTES = 15
|
||||
|
||||
# Redis key prefix for multi-device refresh token storage
|
||||
REFRESH_TOKEN_REDIS_PREFIX = "refresh_tokens:"
|
||||
REFRESH_TOKEN_REDIS_PREFIX = "refresh_tokens:" # noqa: S105
|
||||
|
||||
|
||||
class AuthService:
|
||||
|
||||
@@ -62,7 +62,7 @@ def build_long_task_command(
|
||||
"mkdir -p /var/log/nexus-job && "
|
||||
'LOG=/var/log/nexus-job/job-$(date +%Y%m%d-%H%M%S).log && '
|
||||
f"nohup bash -c '{inner}' >> \"$LOG\" 2>&1 & "
|
||||
f'echo $! > "${LOG}.pid" && '
|
||||
'echo $! > "${LOG}.pid" && '
|
||||
f'echo "started job_id={job_id} pid=$! log=$LOG"'
|
||||
)
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import asyncio
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import tempfile
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
@@ -4,7 +4,6 @@ MySQL only stores historical snapshots; frontend reads live data from Redis.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import logging
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from server.infrastructure.database.session import AsyncSessionLocal
|
||||
from server.domain.models import PushSchedule, SyncLog, AuditLog
|
||||
from server.domain.models import PushSchedule
|
||||
|
||||
logger = logging.getLogger("nexus.schedule_runner")
|
||||
|
||||
|
||||
@@ -26,8 +26,8 @@ async def script_execution_flush_loop():
|
||||
repo = ScriptExecutionRepositoryImpl(session)
|
||||
audit_repo = AuditLogRepositoryImpl(session)
|
||||
|
||||
async def _audit(action, target_type, target_id, operator):
|
||||
await audit_repo.create(AuditLog(
|
||||
async def _audit(action, target_type, target_id, operator, _repo=audit_repo): # noqa: B023
|
||||
await _repo.create(AuditLog(
|
||||
admin_username=operator or "system",
|
||||
action=action,
|
||||
target_type=target_type,
|
||||
|
||||
@@ -68,7 +68,7 @@ async def self_monitor_loop():
|
||||
# Redis recovery notification
|
||||
if redis_ok and not _prev_redis_ok:
|
||||
try:
|
||||
await send_telegram_system_alert(f"🟢 Redis连接已恢复正常", notify_key="NOTIFY_SYSTEM_REDIS")
|
||||
await send_telegram_system_alert("🟢 Redis连接已恢复正常", notify_key="NOTIFY_SYSTEM_REDIS")
|
||||
_last_telegram_time.pop("NOTIFY_SYSTEM_REDIS", None) # clear cooldown
|
||||
except Exception:
|
||||
logger.error("Failed to send Redis recovery via Telegram")
|
||||
@@ -92,7 +92,7 @@ async def self_monitor_loop():
|
||||
# MySQL recovery notification
|
||||
if mysql_ok and not _prev_mysql_ok:
|
||||
try:
|
||||
await send_telegram_system_alert(f"🟢 MySQL连接已恢复正常", notify_key="NOTIFY_SYSTEM_MYSQL")
|
||||
await send_telegram_system_alert("🟢 MySQL连接已恢复正常", notify_key="NOTIFY_SYSTEM_MYSQL")
|
||||
_last_telegram_time.pop("NOTIFY_SYSTEM_MYSQL", None) # clear cooldown
|
||||
except Exception:
|
||||
logger.error("Failed to send MySQL recovery via Telegram")
|
||||
|
||||
@@ -5,11 +5,9 @@ SQLAlchemy ORM models for all database tables.
|
||||
import datetime
|
||||
from datetime import timezone
|
||||
import uuid
|
||||
import base64
|
||||
import hashlib
|
||||
from sqlalchemy import (
|
||||
create_engine, Column, Integer, String, Boolean,
|
||||
DateTime, Text, ForeignKey, Enum, Index, JSON
|
||||
Column, Integer, String, Boolean,
|
||||
DateTime, Text, ForeignKey, Index, JSON
|
||||
)
|
||||
from sqlalchemy.orm import declarative_base, relationship
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ Protocol-based interfaces for dependency inversion.
|
||||
Concrete implementations in infrastructure/database/.
|
||||
"""
|
||||
|
||||
from typing import Protocol, Optional, List, Dict
|
||||
from typing import Protocol, Optional, List
|
||||
from server.domain.models import (
|
||||
Server, SyncLog, Script, ScriptExecution, DbCredential,
|
||||
Admin, LoginAttempt, Setting, PasswordPreset,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Nexus — Admin & LoginAttempt Repository (Async SQLAlchemy)"""
|
||||
|
||||
from typing import Optional, List
|
||||
from typing import Optional
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
|
||||
@@ -8,9 +8,8 @@ Also handles safe schema migrations for existing databases (adding new columns).
|
||||
|
||||
import logging
|
||||
from sqlalchemy import select, update, text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from server.domain.models import Server, Node, Platform, Base
|
||||
from server.domain.models import Server, Node, Platform
|
||||
from server.infrastructure.database.session import AsyncSessionLocal
|
||||
|
||||
logger = logging.getLogger("nexus.migration")
|
||||
|
||||
@@ -60,7 +60,7 @@ async def init_redis(app: FastAPI) -> None:
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Redis startup validation failed: {e}")
|
||||
raise SystemExit(f"Redis unavailable — Nexus cannot start (ADR-009). Error: {e}")
|
||||
raise SystemExit(f"Redis unavailable — Nexus cannot start (ADR-009). Error: {e}") from e
|
||||
|
||||
|
||||
async def close_redis() -> None:
|
||||
|
||||
@@ -74,10 +74,10 @@ class AsyncSSHPool:
|
||||
pass
|
||||
|
||||
async with self._lock:
|
||||
for server_id, pooled in self._pool.items():
|
||||
for _server_id, pooled in self._pool.items():
|
||||
try:
|
||||
pooled.conn.close()
|
||||
except Exception:
|
||||
except Exception: # noqa: S110 — best-effort cleanup
|
||||
pass
|
||||
self._pool.clear()
|
||||
logger.info("AsyncSSH pool stopped — all connections closed")
|
||||
@@ -118,7 +118,7 @@ class AsyncSSHPool:
|
||||
# Close our new connection, reuse the existing one
|
||||
try:
|
||||
conn.close()
|
||||
except Exception:
|
||||
except Exception: # noqa: S110 — best-effort cleanup
|
||||
pass
|
||||
pooled.ref_count += 1
|
||||
pooled.last_used = time.monotonic()
|
||||
@@ -153,7 +153,7 @@ class AsyncSSHPool:
|
||||
if pooled:
|
||||
try:
|
||||
pooled.conn.close()
|
||||
except Exception:
|
||||
except Exception: # noqa: S110 — best-effort cleanup
|
||||
pass
|
||||
logger.debug(f"Force-closed SSH connection: server={server_id}")
|
||||
|
||||
@@ -190,7 +190,7 @@ class AsyncSSHPool:
|
||||
conn = await asyncssh.connect(**connect_kwargs)
|
||||
return conn
|
||||
except asyncssh.Error as e:
|
||||
raise ConnectionError(f"SSH connection failed to {server.domain}:{server.port}: {e}")
|
||||
raise ConnectionError(f"SSH connection failed to {server.domain}:{server.port}: {e}") from e
|
||||
|
||||
async def _evict_one(self) -> bool:
|
||||
"""Evict the oldest idle connection from the pool.
|
||||
@@ -209,7 +209,7 @@ class AsyncSSHPool:
|
||||
pooled = self._pool.pop(oldest_idle)
|
||||
try:
|
||||
pooled.conn.close()
|
||||
except Exception:
|
||||
except Exception: # noqa: S110 — best-effort cleanup
|
||||
pass
|
||||
logger.debug(f"Evicted idle SSH connection: server={oldest_idle}")
|
||||
return True
|
||||
@@ -233,7 +233,7 @@ class AsyncSSHPool:
|
||||
pooled = self._pool.pop(server_id)
|
||||
try:
|
||||
pooled.conn.close()
|
||||
except Exception:
|
||||
except Exception: # noqa: S110 — best-effort cleanup
|
||||
pass
|
||||
|
||||
if to_remove:
|
||||
|
||||
@@ -47,7 +47,7 @@ def _parse_ss(line: str) -> Optional[str]:
|
||||
if "@" in decoded:
|
||||
host_part = decoded.split("@", 1)[1].split(":")[0]
|
||||
return host_part
|
||||
except Exception:
|
||||
except Exception: # noqa: S110 — non-critical URL parse fallback
|
||||
pass
|
||||
return None
|
||||
|
||||
@@ -93,7 +93,7 @@ def parse_subscription_content(raw_content: str) -> list[str]:
|
||||
decoded = _decode_b64(content)
|
||||
if any(decoded.startswith(p) for p in ("ss://", "vmess://", "vless://", "trojan://")):
|
||||
content = decoded
|
||||
except Exception:
|
||||
except Exception: # noqa: S110 — non-critical parse, treat as non-encoded
|
||||
pass
|
||||
|
||||
hosts: list[str] = []
|
||||
|
||||
Reference in New Issue
Block a user