Backend: - Complete FastAPI backend with 42+ REST endpoints (auth, servers, config, players, bans, missions, mods, games, system) - Game adapter architecture with Arma 3 as first-class adapter - WebSocket real-time events for status, metrics, logs, players - Background thread system (process monitor, metrics, log tail, RCon poller) - Fernet encryption for sensitive config fields at rest - JWT auth with admin/viewer roles, bcrypt password hashing - SQLite with WAL mode, parameterized queries, migration system - APScheduler cleanup jobs for logs, metrics, events Frontend: - Server Detail page with 7 tabs (overview, config, players, bans, missions, mods, logs) - Settings page with password change and admin user management - Create Server wizard (4-step; known bug: silent validation failure) - New hooks: useServerDetail, useAuth, useGames - New components: ServerHeader, ConfigEditor, PlayerTable, BanTable, MissionList, ModList, LogViewer, PasswordChange, UserManager - WebSocket onEvent callback for real-time log accumulation - 120 unit tests passing (Vitest + React Testing Library) Docs: - Added .gitignore, CLAUDE.md, README.md - Updated FRONTEND.md, ARCHITECTURE.md with current implementation state - Added .env.example for backend configuration Known issues: - Create Server form: "Next" buttons don't validate before advancing, causing silent submit failure when fields are invalid - Config sub-tabs need UX redesign for non-technical users
65 lines
1.9 KiB
Python
65 lines
1.9 KiB
Python
"""Game-agnostic file operations."""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
|
|
def get_server_dir(server_id: int) -> Path:
|
|
"""Return the absolute directory path for a server's data."""
|
|
from config import settings
|
|
base = Path(settings.servers_dir).resolve()
|
|
return base / str(server_id)
|
|
|
|
|
|
def ensure_server_dirs(server_id: int, layout: list[str] | None = None) -> None:
|
|
"""
|
|
Create servers/{id}/ and any subdirectories from adapter layout.
|
|
layout example: ["server", "battleye", "mpmissions"]
|
|
"""
|
|
server_dir = get_server_dir(server_id)
|
|
server_dir.mkdir(parents=True, exist_ok=True)
|
|
if layout:
|
|
for subdir in layout:
|
|
(server_dir / subdir).mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
def safe_delete_file(path: Path) -> bool:
|
|
"""Delete a file if it exists. Returns True if deleted."""
|
|
try:
|
|
path.unlink(missing_ok=True)
|
|
return True
|
|
except OSError:
|
|
return False
|
|
|
|
|
|
def sanitize_filename(filename: str) -> str:
|
|
"""
|
|
Sanitize a filename for safe disk storage.
|
|
|
|
Rules:
|
|
- Strip path separators (/ \\ and ..)
|
|
- Allow only alphanumeric, dots, hyphens, underscores, @ signs
|
|
- Collapse consecutive dots (prevent ../ tricks)
|
|
- Truncate to 255 characters
|
|
- Raise ValueError if the result is empty
|
|
"""
|
|
# Take only the basename — strip any directory components
|
|
filename = filename.replace("\\", "/").split("/")[-1]
|
|
|
|
# Remove null bytes and control characters
|
|
filename = re.sub(r"[\x00-\x1f\x7f]", "", filename)
|
|
|
|
# Allow only safe characters: alphanum, dot, hyphen, underscore, @
|
|
filename = re.sub(r"[^\w.\-@]", "_", filename)
|
|
|
|
# Collapse consecutive dots to prevent tricks like ".../.."
|
|
filename = re.sub(r"\.{2,}", ".", filename)
|
|
|
|
# Truncate
|
|
filename = filename[:255]
|
|
|
|
if not filename or filename in (".", ".."):
|
|
raise ValueError(f"Filename '{filename}' is not safe for storage")
|
|
|
|
return filename |