from dataclasses import asdict, dataclass, field
|
|
from pathlib import Path
|
|
|
|
# from typing import Self
|
|
|
|
import json
|
|
|
|
from . import version as transpose_version
|
|
from .exceptions import TransposeError
|
|
from .utils import move, remove, symlink
|
|
|
|
|
|
@dataclass
|
|
class TransposeEntry:
|
|
name: str
|
|
path: str
|
|
|
|
|
|
@dataclass
|
|
class TransposeConfig:
|
|
entries: dict = field(default_factory=dict)
|
|
version: str = field(default=transpose_version)
|
|
|
|
def add(self, name: str, path: str) -> None:
|
|
"""
|
|
Add a new entry to the entries
|
|
|
|
Args:
|
|
name: The name of the entry (must not exist)
|
|
path: The path where the entry originally exists
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
if self.entries.get(name):
|
|
raise TransposeError(f"'{name}' already exists")
|
|
|
|
self.entries[name] = TransposeEntry(name=name, path=path)
|
|
|
|
def get(self, name: str) -> TransposeEntry:
|
|
"""
|
|
Get an entry by the name
|
|
|
|
Args:
|
|
name: The name of the entry (must exist)
|
|
|
|
Returns:
|
|
TransposeEntry
|
|
"""
|
|
try:
|
|
return self.entries[name]
|
|
except KeyError:
|
|
raise TransposeError(f"'{name}' does not exist in Transpose config entries")
|
|
|
|
def remove(self, name: str) -> None:
|
|
"""
|
|
Remove an entry by name
|
|
|
|
Args:
|
|
name: The name of the entry (must exist)
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
try:
|
|
del self.entries[name]
|
|
except KeyError:
|
|
raise TransposeError(f"'{name}' does not exist in Transpose config entries")
|
|
|
|
def update(self, name: str, path: str) -> None:
|
|
"""
|
|
Update an entry by name
|
|
|
|
Args:
|
|
name: The name of the entry (must exist)
|
|
path: The path where the entry originally exists
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
try:
|
|
self.entries[name].path = path
|
|
except KeyError:
|
|
raise TransposeError(f"'{name}' does not exist in Transpose config entries")
|
|
|
|
@staticmethod
|
|
def load(config_path: str): # -> Self:
|
|
in_config = json.load(open(config_path, "r"))
|
|
config = TransposeConfig()
|
|
try:
|
|
for name in in_config["entries"]:
|
|
config.add(name, in_config["entries"][name]["path"])
|
|
except (KeyError, TypeError) as e:
|
|
raise TransposeError(f"Unrecognized Transpose config file format: {e}")
|
|
|
|
return config
|
|
|
|
def save(self, config_path: str) -> None:
|
|
"""
|
|
Save the Config to a location in JSON format
|
|
|
|
Args:
|
|
path: The path to save the json file
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
config_path = Path(config_path)
|
|
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
with open(str(config_path), "w") as f:
|
|
json.dump(self.to_dict(), f)
|
|
|
|
def to_dict(self) -> dict:
|
|
return asdict(self)
|
|
|
|
|
|
class Transpose:
|
|
config: TransposeConfig
|
|
config_path: Path
|
|
store_path: Path
|
|
|
|
def __init__(self, config_path: str) -> None:
|
|
self.config = TransposeConfig.load(config_path)
|
|
self.config_path = Path(config_path)
|
|
self.store_path = self.config_path.parent
|
|
|
|
if not self.store_path.exists():
|
|
self.store_path.mkdir(parents=True)
|
|
|
|
def apply(self, name: str, force: bool = False) -> None:
|
|
"""
|
|
Create/recreate the symlink to an existing entry
|
|
|
|
Args:
|
|
name: The name of the entry (must exist)
|
|
force: If enabled and path already exists, move the path to '{path}-bak'
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
if not self.config.entries.get(name):
|
|
raise TransposeError(f"Entry does not exist: '{name}'")
|
|
|
|
if self.config.entries[name].path.exists():
|
|
if self.config.entries[name].path.is_symlink():
|
|
remove(self.config.entries[name].path)
|
|
elif force: # Backup the existing path, just in case
|
|
move(
|
|
self.config.entries[name].path,
|
|
self.config.entries[name].path.joinpath("-bak"),
|
|
)
|
|
else:
|
|
raise TransposeError(
|
|
f"Entry path already exists, cannot restore (force required): '{self.config.entries[name].path}'"
|
|
)
|
|
|
|
symlink(
|
|
target_path=self.store_path.joinpath(name),
|
|
symlink_path=self.config.entries[name].path,
|
|
)
|
|
|
|
def restore(self, name: str, force: bool = False) -> None:
|
|
"""
|
|
Remove the symlink and move the stored entry back to it's original path
|
|
|
|
Args:
|
|
name: The name of the entry (must exist)
|
|
force: If enabled and path already exists, move the path to '{path}-bak'
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
if not self.config.entries.get(name):
|
|
raise TransposeError(f"Could not locate entry by name: '{name}'")
|
|
|
|
if self.config.entries[name].path.exists():
|
|
if self.config.entries[name].path.is_symlink():
|
|
remove(self.config.entries[name].path)
|
|
elif force: # Backup the existing path, just in case
|
|
move(
|
|
self.config.entries[name].path,
|
|
self.config.entries[name].path.joinpath("-bak"),
|
|
)
|
|
else:
|
|
raise TransposeError(
|
|
f"Entry path already exists, cannot restore (force required): '{self.config.entries[name].path}'"
|
|
)
|
|
|
|
move(self.store_path.joinpath(name), self.config.entries[name].path)
|
|
|
|
self.config.remove(name)
|
|
self.config.save(self.config_path)
|
|
|
|
def store(self, name: str, source_path: str) -> None:
|
|
"""
|
|
Move the source path to the store path, create a symlink, and update the config
|
|
|
|
Args:
|
|
name: The name of the entry (must exist)
|
|
source_path: The directory or file to be stored
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
if self.config.entries.get(name):
|
|
raise TransposeError(
|
|
f"Entry already exists: '{name}' ({self.config.entries[name].path})"
|
|
)
|
|
|
|
storage_path = self.store_path.joinpath(name)
|
|
if storage_path.exists():
|
|
raise TransposeError(f"Store path already exists: '{storage_path}'")
|
|
|
|
source_path = self.config.entries[name].path
|
|
if not source_path.exists():
|
|
raise TransposeError(f"Source path does not exist: '{source_path}'")
|
|
|
|
if not source_path.is_dir() and not source_path.is_file():
|
|
raise TransposeError(
|
|
f"Source path must be a directory or file: '{source_path}'"
|
|
)
|
|
|
|
move(source=source_path, destination=storage_path)
|
|
symlink(target_path=storage_path, symlink_path=source_path)
|
|
|
|
self.config.add(name, source_path)
|
|
self.config.save(self.config_path)
|