# Copyright © 2025 Dr.-Ing. Paul Wilhelm <paul@wilhelm.dev>
# This file is part of Archive Agent. See LICENSE for details.
import typer
from logging import Logger
import json
import shutil
from pathlib import Path
from copy import deepcopy
from typing import Dict, Any
from abc import ABC, abstractmethod
from archive_agent.util.format import format_file
class StorageManager(ABC):
"""
Storage manager.
"""
def __init__(self, logger: Logger, file_path: Path, default: Dict[str, Any]) -> None:
"""
Initialize storage manager.
:param file_path: File path.
:param default: Default data.
"""
self.logger = logger
self.file_path = file_path
self.default = default
self.data: Dict[str, Any] = {}
self.load_or_create()
def load_or_create(self) -> None:
"""
Load or create file.
"""
try:
if not self.file_path.exists():
self.create()
else:
self.load()
except Exception as e:
self.logger.exception(f"Failed to load {format_file(self.file_path)}: {e}")
raise typer.Exit(code=1)
def create(self) -> None:
"""
Create file.
"""
self.data = deepcopy(self.default)
self.save()
self.logger.info(f"Created default {format_file(self.file_path)}")
def load(self) -> None:
"""
Load file.
"""
with open(self.file_path, "r", encoding="utf-8") as f:
self.data = json.load(f)
upgraded = self.upgrade()
missing_keys = self.default.keys() - self.data.keys()
if missing_keys:
self.logger.error(f"Missing keys in {format_file(self.file_path)}: {missing_keys}")
raise typer.Exit(code=1)
if not self.validate():
self.logger.error(f"Invalid data in {format_file(self.file_path)}")
raise typer.Exit(code=1)
self.logger.debug(f"Loaded existing {format_file(self.file_path)}")
if upgraded:
self.logger.debug(f"Upgraded existing {format_file(self.file_path)}")
self.save()
def save(self) -> None:
"""
Save file (atomic write).
"""
if not self.validate():
self.logger.error(f"Invalid data in {format_file(self.file_path)}")
raise typer.Exit(code=1)
self.file_path.parent.mkdir(parents=True, exist_ok=True)
temp_path = self.file_path.with_suffix(".tmp")
with open(temp_path, "w", encoding="utf-8") as f:
# noinspection PyTypeChecker
json.dump(self.data, f, indent=4)
shutil.move(temp_path, self.file_path)
self.logger.debug(f"Saved {format_file(self.file_path)}")
@abstractmethod
def upgrade(self) -> bool:
"""
Upgrade data.
:return: True if data upgraded, False otherwise.
"""
raise NotImplementedError
@abstractmethod
def validate(self) -> bool:
"""
Validate data.
:return: True if data is valid, False otherwise.
"""
raise NotImplementedError