|
@ -1,111 +1,228 @@ |
|
|
import pathlib |
|
|
|
|
|
|
|
|
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 .exceptions import TransposeError |
|
|
from .utils import check_path, create_cache, get_cache, move, remove, symlink |
|
|
|
|
|
|
|
|
from .utils import move, remove, symlink |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Transpose: |
|
|
|
|
|
def __init__( |
|
|
|
|
|
self, |
|
|
|
|
|
target_path: str, |
|
|
|
|
|
cache_filename: str = None, |
|
|
|
|
|
) -> None: |
|
|
|
|
|
self.target_path = pathlib.Path(target_path) |
|
|
|
|
|
|
|
|
@dataclass |
|
|
|
|
|
class TransposeEntry: |
|
|
|
|
|
name: str |
|
|
|
|
|
path: str |
|
|
|
|
|
|
|
|
if not cache_filename: |
|
|
|
|
|
cache_filename = ".transpose.json" |
|
|
|
|
|
self.cache_filename = cache_filename |
|
|
|
|
|
self.cache_path = pathlib.Path(self.target_path).joinpath(cache_filename) |
|
|
|
|
|
|
|
|
|
|
|
def apply(self) -> None: |
|
|
|
|
|
|
|
|
@dataclass |
|
|
|
|
|
class TransposeConfig: |
|
|
|
|
|
entries: dict = field(default_factory=dict) |
|
|
|
|
|
version: str = field(default=transpose_version) |
|
|
|
|
|
|
|
|
|
|
|
def add(self, name: str, path: str) -> None: |
|
|
""" |
|
|
""" |
|
|
Recreate the symlink from an existing cache file |
|
|
|
|
|
|
|
|
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 not self.cache_path.exists(): |
|
|
|
|
|
raise TransposeError( |
|
|
|
|
|
f"Cache file does not exist indicating target is not managed by Transpose: {self.cache_path}" |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
if self.entries.get(name): |
|
|
|
|
|
raise TransposeError(f"'{name}' already exists") |
|
|
|
|
|
|
|
|
|
|
|
self.entries[name] = TransposeEntry(name=name, path=path) |
|
|
|
|
|
|
|
|
cache = get_cache(self.cache_path) |
|
|
|
|
|
original_path = pathlib.Path(cache["original_path"]).expanduser() |
|
|
|
|
|
|
|
|
def get(self, name: str) -> TransposeEntry: |
|
|
|
|
|
""" |
|
|
|
|
|
Get an entry by the name |
|
|
|
|
|
|
|
|
if original_path.is_symlink(): |
|
|
|
|
|
remove(original_path) |
|
|
|
|
|
|
|
|
Args: |
|
|
|
|
|
name: The name of the entry (must exist) |
|
|
|
|
|
|
|
|
symlink(target_path=self.cache_path.parent, symlink_path=original_path) |
|
|
|
|
|
|
|
|
Returns: |
|
|
|
|
|
TransposeEntry |
|
|
|
|
|
""" |
|
|
|
|
|
try: |
|
|
|
|
|
return self.entries[name] |
|
|
|
|
|
except KeyError: |
|
|
|
|
|
raise TransposeError(f"'{name}' does not exist in Transpose config entries") |
|
|
|
|
|
|
|
|
def create(self, stored_path: str) -> None: |
|
|
|
|
|
|
|
|
def remove(self, name: str) -> None: |
|
|
""" |
|
|
""" |
|
|
Create the cache file from the target directory and stored directory |
|
|
|
|
|
|
|
|
Remove an entry by name |
|
|
|
|
|
|
|
|
This is useful if a path is already stored somewhere else but the cache file is missing |
|
|
|
|
|
|
|
|
Args: |
|
|
|
|
|
name: The name of the entry (must exist) |
|
|
|
|
|
|
|
|
Ideally, the target should be a symlink or not exist so a restore or apply can function |
|
|
|
|
|
|
|
|
Returns: |
|
|
|
|
|
None |
|
|
""" |
|
|
""" |
|
|
stored_path = pathlib.Path(stored_path) |
|
|
|
|
|
if not stored_path.exists(): |
|
|
|
|
|
raise TransposeError(f"Stored path does not exist: {stored_path}") |
|
|
|
|
|
|
|
|
try: |
|
|
|
|
|
del self.entries[name] |
|
|
|
|
|
except KeyError: |
|
|
|
|
|
raise TransposeError(f"'{name}' does not exist in Transpose config entries") |
|
|
|
|
|
|
|
|
self.cache_path = stored_path.joinpath(self.cache_filename) |
|
|
|
|
|
|
|
|
def update(self, name: str, path: str) -> None: |
|
|
|
|
|
""" |
|
|
|
|
|
Update an entry by name |
|
|
|
|
|
|
|
|
create_cache( |
|
|
|
|
|
cache_path=self.cache_path, |
|
|
|
|
|
original_path=self.target_path, |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
Args: |
|
|
|
|
|
name: The name of the entry (must exist) |
|
|
|
|
|
path: The path where the entry originally exists |
|
|
|
|
|
|
|
|
def restore(self) -> None: |
|
|
|
|
|
|
|
|
Returns: |
|
|
|
|
|
None |
|
|
""" |
|
|
""" |
|
|
Restores a previously Transpose managed directory to it's previous location. |
|
|
|
|
|
|
|
|
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: |
|
|
""" |
|
|
""" |
|
|
if not self.cache_path.exists(): |
|
|
|
|
|
raise TransposeError( |
|
|
|
|
|
f"Cache file does not exist indicating target is not managed by Transpose: {self.cache_path}" |
|
|
|
|
|
) |
|
|
|
|
|
if not self.target_path.exists(): |
|
|
|
|
|
raise TransposeError(f"Target path does not exist: {self.target_path}") |
|
|
|
|
|
|
|
|
Save the Config to a location in JSON format |
|
|
|
|
|
|
|
|
cache = get_cache(self.cache_path) |
|
|
|
|
|
original_path = pathlib.Path(cache["original_path"]).expanduser() |
|
|
|
|
|
|
|
|
Args: |
|
|
|
|
|
path: The path to save the json file |
|
|
|
|
|
|
|
|
if original_path.is_symlink(): |
|
|
|
|
|
remove(original_path) |
|
|
|
|
|
elif original_path.exists(): |
|
|
|
|
|
raise TransposeError( |
|
|
|
|
|
f"Original path in cache file already exists: {original_path}" |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
try: |
|
|
|
|
|
move(source=self.target_path, destination=original_path) |
|
|
|
|
|
except FileNotFoundError: |
|
|
|
|
|
raise TransposeError( |
|
|
|
|
|
f"Original path, {original_path}, does not exist. Use '-f' to create the path" |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
new_cache_path = pathlib.Path(original_path).joinpath(self.cache_filename) |
|
|
|
|
|
remove(new_cache_path) |
|
|
|
|
|
|
|
|
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 store(self, store_path: str, name: str = None) -> None: |
|
|
|
|
|
|
|
|
def apply(self, name: str, force: bool = False) -> None: |
|
|
""" |
|
|
""" |
|
|
Moves a directory to a central location and creates a symlink to the old path. |
|
|
|
|
|
|
|
|
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 name is None: |
|
|
|
|
|
name = self.target_path.name |
|
|
|
|
|
|
|
|
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, |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
new_location = pathlib.Path(store_path).joinpath(name) |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
if not check_path(path=self.target_path): |
|
|
|
|
|
|
|
|
Returns: |
|
|
|
|
|
None |
|
|
|
|
|
""" |
|
|
|
|
|
if self.config.entries.get(name): |
|
|
raise TransposeError( |
|
|
raise TransposeError( |
|
|
f"Target path, {self.target_path}, does not exist. Cannot continue." |
|
|
|
|
|
|
|
|
f"Entry already exists: '{name}' ({self.config.entries[name].path})" |
|
|
) |
|
|
) |
|
|
if check_path(path=new_location): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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( |
|
|
raise TransposeError( |
|
|
f"Store path, {new_location}, already exists. Cannot continue." |
|
|
|
|
|
|
|
|
f"Source path must be a directory or file: '{source_path}'" |
|
|
) |
|
|
) |
|
|
|
|
|
|
|
|
create_cache( |
|
|
|
|
|
cache_path=self.cache_path, |
|
|
|
|
|
original_path=self.target_path, |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
move(source=source_path, destination=storage_path) |
|
|
|
|
|
symlink(target_path=storage_path, symlink_path=source_path) |
|
|
|
|
|
|
|
|
move(source=self.target_path, destination=new_location) |
|
|
|
|
|
symlink(target_path=new_location, symlink_path=self.target_path) |
|
|
|
|
|
|
|
|
self.config.add(name, source_path) |
|
|
|
|
|
self.config.save(self.config_path) |