Files
hockey_new/ui_builder/storage.py

94 lines
3.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Keep user UI data separate from replaceable release defaults."""
from __future__ import annotations
import json
import os
import shutil
import tempfile
from contextlib import contextmanager
from pathlib import Path
from threading import RLock
_migration_lock = RLock()
def atomic_write(path: Path, content: str) -> None:
"""A failed save leaves the previous complete file available."""
temporary = None
try:
with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", dir=path.parent,
prefix=path.name + ".", suffix=".tmp", delete=False) as stream:
temporary = Path(stream.name)
stream.write(content)
stream.flush()
os.fsync(stream.fileno())
os.replace(temporary, path)
finally:
if temporary is not None:
temporary.unlink(missing_ok=True)
@contextmanager
def _exclusive_migration(directory: Path):
# Startup can run in several server processes, including on Windows.
with _migration_lock, (directory / ".migration.lock").open("a+b") as stream:
stream.seek(0, os.SEEK_END)
if not stream.tell():
stream.write(b"0")
stream.flush()
stream.seek(0)
if os.name == "nt":
import msvcrt
msvcrt.locking(stream.fileno(), msvcrt.LK_LOCK, 1)
else:
import fcntl
fcntl.flock(stream.fileno(), fcntl.LOCK_EX)
try:
yield
finally:
stream.seek(0)
if os.name == "nt":
msvcrt.locking(stream.fileno(), msvcrt.LK_UNLCK, 1)
else:
fcntl.flock(stream.fileno(), fcntl.LOCK_UN)
def prepare_ui_storage(settings_dir: Path, *, filenames: tuple[str, ...], legacy_filename: str) -> Path:
configured = os.getenv("HOCKEY_UI_DATA_DIR", "").strip()
directory = Path(configured).expanduser() if configured else settings_dir / "local"
if configured and not directory.is_absolute():
raise ValueError("HOCKEY_UI_DATA_DIR должен содержать абсолютный путь")
directory.mkdir(parents=True, exist_ok=True)
with _exclusive_migration(directory):
for filename in filenames:
name = Path(filename).name
destination = directory / name
if destination.exists():
continue
source = settings_dir / name
if not source.exists():
source = settings_dir / Path(legacy_filename).name
if source.exists():
content = source.read_text(encoding="utf-8-sig")
try:
valid = isinstance(json.loads(content), dict)
except (ValueError, TypeError):
valid = False
if not valid:
raise ValueError(f"Не удалось перенести настройки интерфейса: {source.name} повреждён")
atomic_write(destination, content)
# Import recovery history exactly once. Otherwise every restart resurrects
# old backups that rolling retention already removed.
old_backups = settings_dir / "backups"
new_backups = directory / "backups"
new_backups.mkdir(exist_ok=True)
marker = directory / ".ui_backups_migrated_v1.json"
if not marker.exists():
for filename in (*filenames, legacy_filename):
for source in old_backups.glob(Path(filename).stem + "_*.json"):
destination = new_backups / source.name
if not destination.exists():
shutil.copy2(source, destination)
atomic_write(marker, json.dumps({"completed": True, "version": 1}))
return directory