Files
Vault/src/core/manifest.py

239 lines
7.3 KiB
Python
Raw Normal View History

2026-01-30 07:07:11 +01:00
"""Manifest dataclass for vault metadata."""
import json
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Literal
from uuid import uuid4
from src.core.file_entry import FileEntry
LocationStatus = Literal["active", "unreachable"]
@dataclass
class Location:
"""Represents a vault replica location.
Attributes:
path: Absolute path to the .vault file
last_seen: Last time this location was accessible
status: Current status (active/unreachable)
"""
path: str
last_seen: datetime
status: LocationStatus = "active"
def to_dict(self) -> dict:
"""Convert to dictionary for JSON serialization."""
return {
"path": self.path,
"last_seen": self.last_seen.isoformat(),
"status": self.status,
}
@classmethod
def from_dict(cls, data: dict) -> "Location":
"""Create Location from dictionary."""
return cls(
path=data["path"],
last_seen=datetime.fromisoformat(data["last_seen"]),
status=data["status"],
)
@dataclass
class Manifest:
"""Vault manifest containing all metadata.
This is stored as .vault/manifest.json inside each vault image.
Attributes:
vault_id: Unique identifier for the vault (UUID)
vault_name: Human-readable name
version: Manifest format version
created: Creation timestamp
last_modified: Last modification timestamp
image_size_mb: Size of the vault image in MB
locations: List of all replica locations
files: List of all files in the vault
"""
vault_id: str
vault_name: str
image_size_mb: int
created: datetime
last_modified: datetime
version: int = 1
locations: list[Location] = field(default_factory=list)
files: list[FileEntry] = field(default_factory=list)
@classmethod
def create_new(cls, vault_name: str, image_size_mb: int, location_path: str) -> "Manifest":
"""Create a new manifest for a fresh vault.
Args:
vault_name: Human-readable name for the vault
image_size_mb: Size of the vault image in MB
location_path: Path to the first .vault file
Returns:
New Manifest instance
"""
now = datetime.now()
resolved = str(Path(location_path).resolve())
return cls(
vault_id=str(uuid4()),
vault_name=vault_name,
image_size_mb=image_size_mb,
created=now,
last_modified=now,
locations=[Location(path=resolved, last_seen=now, status="active")],
files=[],
)
def to_dict(self) -> dict:
"""Convert to dictionary for JSON serialization."""
return {
"vault_id": self.vault_id,
"vault_name": self.vault_name,
"version": self.version,
"created": self.created.isoformat(),
"last_modified": self.last_modified.isoformat(),
"image_size_mb": self.image_size_mb,
"locations": [loc.to_dict() for loc in self.locations],
"files": [f.to_dict() for f in self.files],
}
@classmethod
def from_dict(cls, data: dict) -> "Manifest":
"""Create Manifest from dictionary."""
return cls(
vault_id=data["vault_id"],
vault_name=data["vault_name"],
version=data["version"],
created=datetime.fromisoformat(data["created"]),
last_modified=datetime.fromisoformat(data["last_modified"]),
image_size_mb=data["image_size_mb"],
locations=[Location.from_dict(loc) for loc in data["locations"]],
files=[FileEntry.from_dict(f) for f in data["files"]],
)
def save(self, mount_point: Path) -> None:
"""Save manifest to .vault/manifest.json.
Args:
mount_point: Path where the vault is mounted
"""
vault_dir = mount_point / ".vault"
vault_dir.mkdir(exist_ok=True)
manifest_path = vault_dir / "manifest.json"
with open(manifest_path, "w", encoding="utf-8") as f:
json.dump(self.to_dict(), f, indent=2)
@classmethod
def load(cls, mount_point: Path) -> "Manifest":
"""Load manifest from .vault/manifest.json.
Args:
mount_point: Path where the vault is mounted
Returns:
Loaded Manifest instance
Raises:
FileNotFoundError: If manifest doesn't exist
"""
manifest_path = mount_point / ".vault" / "manifest.json"
with open(manifest_path, "r", encoding="utf-8") as f:
data = json.load(f)
return cls.from_dict(data)
def add_location(self, path: str) -> None:
"""Add a new replica location.
Args:
path: Absolute path to the .vault file
"""
resolved = str(Path(path).resolve())
# Don't add duplicate locations
for loc in self.locations:
if str(Path(loc.path).resolve()) == resolved:
loc.status = "active"
loc.last_seen = datetime.now()
self.last_modified = datetime.now()
return
self.locations.append(
Location(path=resolved, last_seen=datetime.now(), status="active")
)
self.last_modified = datetime.now()
def update_location_status(self, path: str, status: LocationStatus) -> None:
"""Update status of a location.
Args:
path: Path to the location
status: New status
"""
resolved = str(Path(path).resolve())
for loc in self.locations:
if str(Path(loc.path).resolve()) == resolved:
loc.status = status
if status == "active":
loc.last_seen = datetime.now()
break
self.last_modified = datetime.now()
def add_file(self, file_entry: FileEntry) -> None:
"""Add or update a file entry.
Args:
file_entry: File entry to add/update
"""
# Remove existing entry with same path
self.files = [f for f in self.files if f.path != file_entry.path]
self.files.append(file_entry)
self.last_modified = datetime.now()
def add_file_from_path(self, base_path: Path, file_path: Path) -> FileEntry:
"""Add a file entry from a file path.
Args:
base_path: Base path (mount point) for relative path calculation
file_path: Absolute path to the file
Returns:
The created FileEntry
"""
entry = FileEntry.from_path(base_path, file_path)
self.add_file(entry)
return entry
def remove_file(self, path: str) -> None:
"""Remove a file entry by path.
Args:
path: Relative path of the file to remove
"""
self.files = [f for f in self.files if f.path != path]
self.last_modified = datetime.now()
def get_file(self, path: str) -> FileEntry | None:
"""Get file entry by path.
Args:
path: Relative path of the file
Returns:
FileEntry if found, None otherwise
"""
for f in self.files:
if f.path == path:
return f
return None