Source code for laborchestrator.log_archive_manager


"""
Purely AI generated code that zippes old log files into neet archives. It works fine but only god and the AI know how ;-)
"""
import os
import shutil
import zipfile
from datetime import datetime
from typing import Optional, Tuple

ARCHIVE_DIR_NAME = "archives"
ARCHIVE_TARGET_BYTES = 10 * 1024 * 1024
ARCHIVE_KEEP_LAST = 10

_SESSION_PREFIX = "session_"
_SESSION_TIME_FORMAT = "session_%a_%b_%d_%H_%M_%S_%Y"
_ARCHIVE_TIME_FORMAT = "%Y%m%d_%H%M%S"


[docs] def _parse_session_datetime(session_name: str) -> Optional[datetime]: try: return datetime.strptime(session_name, _SESSION_TIME_FORMAT) except ValueError: return None
[docs] def _collect_sessions(base_path: str): sessions = [] for entry in os.listdir(base_path): if not entry.startswith(_SESSION_PREFIX): continue full_path = os.path.join(base_path, entry) if not os.path.isdir(full_path): continue session_dt = _parse_session_datetime(entry) if session_dt is None: session_dt = datetime.fromtimestamp(os.path.getmtime(full_path)) sessions.append((session_dt, entry, full_path)) sessions.sort(key=lambda item: item[0]) return sessions
[docs] def _dir_size_bytes(path: str) -> int: total = 0 for root, _, files in os.walk(path): for name in files: try: total += os.path.getsize(os.path.join(root, name)) except OSError: continue return total
[docs] def _build_archive_name(start_dt: datetime, end_dt: datetime) -> str: return f"sessions_{start_dt:{_ARCHIVE_TIME_FORMAT}}__{end_dt:{_ARCHIVE_TIME_FORMAT}}.zip"
[docs] def _parse_archive_name(name: str) -> Optional[Tuple[datetime, datetime]]: if not (name.startswith("sessions_") and name.endswith(".zip")): return None core = name[len("sessions_"):-4] if "__" not in core: return None start_str, end_str = core.split("__", 1) try: start_dt = datetime.strptime(start_str, _ARCHIVE_TIME_FORMAT) end_dt = datetime.strptime(end_str, _ARCHIVE_TIME_FORMAT) except ValueError: return None return start_dt, end_dt
[docs] def _select_existing_archive(archive_dir: str) -> Optional[Tuple[str, datetime, datetime, int]]: candidates = [] for entry in os.listdir(archive_dir): parsed = _parse_archive_name(entry) if parsed is None: continue start_dt, end_dt = parsed full_path = os.path.join(archive_dir, entry) if not os.path.isfile(full_path): continue size = os.path.getsize(full_path) candidates.append((end_dt, start_dt, full_path, size)) if not candidates: return None candidates.sort(key=lambda item: item[0]) end_dt, start_dt, full_path, size = candidates[-1] return full_path, start_dt, end_dt, size
[docs] def _rename_archive_if_needed(archive_dir: str, current_path: str, start_dt: datetime, end_dt: datetime) -> str: target_name = _build_archive_name(start_dt, end_dt) target_path = os.path.join(archive_dir, target_name) if os.path.abspath(target_path) == os.path.abspath(current_path): return current_path if os.path.exists(target_path): suffix = 1 base, ext = os.path.splitext(target_path) while os.path.exists(f"{base}_{suffix}{ext}"): suffix += 1 target_path = f"{base}_{suffix}{ext}" os.replace(current_path, target_path) return target_path
[docs] def _archive_session_dir(zip_path: str, base_path: str, session_path: str) -> None: with zipfile.ZipFile(zip_path, "a", compression=zipfile.ZIP_DEFLATED) as archive: for root, _, files in os.walk(session_path): for name in files: full_path = os.path.join(root, name) rel_path = os.path.relpath(full_path, base_path) archive.write(full_path, rel_path) shutil.rmtree(session_path)
[docs] def archive_old_sessions( base_path: str, keep_last: int = ARCHIVE_KEEP_LAST, target_bytes: int = ARCHIVE_TARGET_BYTES, ): sessions = _collect_sessions(base_path) if len(sessions) <= keep_last: return archive_dir = os.path.join(base_path, ARCHIVE_DIR_NAME) os.makedirs(archive_dir, exist_ok=True) current_archive = _select_existing_archive(archive_dir) if current_archive is None or current_archive[3] >= target_bytes: current_path = None current_start = None current_end = None current_size = 0 else: current_path, current_start, current_end, current_size = current_archive for session_dt, _, session_path in sessions[:-keep_last]: if current_path is None or current_size >= target_bytes: current_start = session_dt current_end = session_dt current_name = _build_archive_name(current_start, current_end) current_path = os.path.join(archive_dir, current_name) current_size = 0 session_size = _dir_size_bytes(session_path) _archive_session_dir(current_path, base_path, session_path) current_end = session_dt current_size += session_size current_path = _rename_archive_if_needed(archive_dir, current_path, current_start, current_end)