Интегрирован wesp в сайт
CI / quality (push) Canceled after 0s

This commit is contained in:
влад
2026-07-17 12:57:18 +03:00
parent 5dfa06ddbe
commit 355c0ef9f1
883 changed files with 194576 additions and 177 deletions
+172
View File
@@ -0,0 +1,172 @@
"""Сохранение логов, присланных полевыми клиентами на центральный сервер."""
from __future__ import annotations
import re
import time
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional
from sqlalchemy import or_, select
_LOG_NAME_RE = re.compile(r"^[\w.\-]{1,120}$")
def safe_path_segment(raw: str, max_len: int = 96) -> str:
s = (raw or "").strip()[:max_len]
if not s:
return "unknown"
return "".join(c if (c.isalnum() or c in "-_") else "_" for c in s)
def validate_log_basename(name: str) -> str:
n = (name or "").strip() or "wesp.log"
if not _LOG_NAME_RE.match(n):
raise ValueError("Недопустимое имя файла лога")
return n
def store_client_log(
*,
upload_dir: str,
client_id: str,
log_basename: str,
raw: bytes,
) -> Dict[str, Any]:
"""
Пишет байты в upload_dir/<safe_client>/<timestamp>_<log_basename>.
upload_dir должен быть абсолютным или относительным к cwd процесса.
"""
if not upload_dir or not str(upload_dir).strip():
raise ValueError("Каталог выгрузки логов не задан")
cid = safe_path_segment(client_id, max_len=80)
base = validate_log_basename(log_basename)
root = Path(upload_dir).resolve()
dest_dir = root / cid
dest_dir.mkdir(parents=True, exist_ok=True)
ts = time.strftime("%Y%m%d_%H%M%S", time.gmtime())
dest = dest_dir / f"{ts}_{base}"
dest.write_bytes(raw)
try:
rel = dest.relative_to(root)
except ValueError:
rel = dest
return {
"path": str(dest),
"relative": str(rel).replace("\\", "/"),
"bytes_written": len(raw),
}
def client_logs_root_resolved(upload_dir: str, base_dir: str) -> Path:
raw = (upload_dir or "").strip()
if not raw:
raise ValueError("Каталог выгрузки логов не задан")
p = Path(raw)
if not p.is_absolute():
p = Path(base_dir) / p
return p.resolve()
def _is_inside(parent: Path, child: Path) -> bool:
try:
child.resolve().relative_to(parent.resolve())
return True
except ValueError:
return False
def client_upload_log_dir(root: Path, client_id: str) -> Path:
seg = safe_path_segment(client_id, max_len=80)
d = (root / seg).resolve()
if not _is_inside(root, d) and d != root:
raise ValueError("Некорректный client_id")
return d
def list_uploaded_client_log_packages(root: Path) -> List[Dict[str, Any]]:
if not root.is_dir():
return []
out: List[Dict[str, Any]] = []
for sub in sorted(root.iterdir(), key=lambda x: x.name.lower()):
if not sub.is_dir():
continue
files: List[Dict[str, Any]] = []
for f in sorted(sub.iterdir(), key=lambda x: x.name):
if not f.is_file() or not _LOG_NAME_RE.match(f.name):
continue
st = f.stat()
files.append(
{
"name": f.name,
"size": st.st_size,
"mtime": datetime.fromtimestamp(st.st_mtime, tz=timezone.utc).isoformat(),
}
)
out.append({"dir_segment": sub.name, "file_count": len(files), "files": files})
return out
def build_uploaded_logs_index(db_session, root: Path) -> List[Dict[str, Any]]:
"""Список каталогов выгрузки + сопоставление с SyncClient по safe_path_segment(node_id)."""
from app.models import SyncClient, SyncClientDisplayName
packages = list_uploaded_client_log_packages(root)
clients = list(
db_session.execute(
select(SyncClient).where(
or_(SyncClient.is_deleted.is_(False), SyncClient.is_deleted.is_(None))
)
).scalars().all()
)
names = {
n.node_id: n.display_name
for n in db_session.execute(select(SyncClientDisplayName)).scalars().all()
}
by_seg = {safe_path_segment(c.node_id, max_len=80): c for c in clients}
result: List[Dict[str, Any]] = []
for p in packages:
seg = p["dir_segment"]
sc = by_seg.get(seg)
nid = sc.node_id if sc else None
result.append(
{
"dir_segment": seg,
"node_id": nid,
"client_name": (sc.client_name if sc else None),
"display_name": (names.get(nid) if nid else None),
"file_count": p["file_count"],
"files": p["files"],
"api_client_id": nid or seg,
}
)
return result
def latest_uploaded_log_file(client_dir: Path) -> Optional[Path]:
if not client_dir.is_dir():
return None
candidates = [
f
for f in client_dir.iterdir()
if f.is_file() and _LOG_NAME_RE.match(f.name)
]
if not candidates:
return None
return max(candidates, key=lambda p: p.stat().st_mtime)
def resolve_uploaded_client_log_file(root: Path, client_id: str, filename: str) -> Path:
d = client_upload_log_dir(root, client_id)
if not d.is_dir():
raise FileNotFoundError("Каталог клиента не найден")
fn = Path(filename).name
if not _LOG_NAME_RE.match(fn):
raise ValueError("Недопустимое имя файла")
fpath = (d / fn).resolve()
if not _is_inside(d, fpath) and fpath != d:
raise ValueError("Некорректный путь")
if not fpath.is_file():
raise FileNotFoundError("Файл не найден")
return fpath