...
This commit is contained in:
199
_archive/lib/context/context.py
Normal file
199
_archive/lib/context/context.py
Normal file
@@ -0,0 +1,199 @@
|
||||
"""Context management module for handling file operations and tracking changes."""
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from herotools.pathtools import remove_file_if_exists
|
||||
from herotools.texttools import name_fix
|
||||
|
||||
|
||||
class MyFile:
|
||||
"""A class representing a file in the context system with tracking capabilities."""
|
||||
|
||||
def __init__(self, path: str):
|
||||
"""Initialize a MyFile instance.
|
||||
|
||||
Args:
|
||||
path: The path to the file
|
||||
|
||||
"""
|
||||
self.path = path
|
||||
self.exists = os.path.exists(self.path)
|
||||
self.changed_in_context = False # Indicates if the file is new or was changed
|
||||
self._md5 = ""
|
||||
|
||||
def md5(self) -> str:
|
||||
"""Calculate and return MD5 hash of the file.
|
||||
|
||||
Returns:
|
||||
str: The MD5 hash of the file's contents
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If the file does not exist
|
||||
|
||||
"""
|
||||
if not self.exists:
|
||||
raise FileNotFoundError(f"File does not exist: {self.path}")
|
||||
if not self._md5:
|
||||
with open(self.path, "rb") as file:
|
||||
self._md5 = hashlib.md5(file.read()).hexdigest()
|
||||
return self._md5
|
||||
|
||||
def name(self) -> str:
|
||||
"""Return the base name of the file.
|
||||
|
||||
Returns:
|
||||
str: The file's base name
|
||||
|
||||
"""
|
||||
return os.path.basename(self.path)
|
||||
|
||||
def ext(self) -> str:
|
||||
"""Return the file extension in lower case.
|
||||
|
||||
Returns:
|
||||
str: The file's extension in lowercase
|
||||
|
||||
"""
|
||||
return os.path.splitext(self.path)[1].lower()
|
||||
|
||||
|
||||
class Context:
|
||||
"""A class for managing file contexts and tracking file changes."""
|
||||
|
||||
def __init__(self, name: str = "default", reset: bool = False):
|
||||
"""Initialize a Context instance.
|
||||
|
||||
Args:
|
||||
name: The name of the context
|
||||
reset: Whether to reset (remove) the existing context
|
||||
|
||||
"""
|
||||
logging.basicConfig(level=logging.DEBUG, format="%(message)s")
|
||||
self.logger = logging.getLogger(__name__)
|
||||
contextroot = os.getenv("CONTEXTROOT", "~/context")
|
||||
self.name = name_fix(name)
|
||||
self.path = os.path.join(os.path.expanduser(contextroot), self.name)
|
||||
if reset:
|
||||
self._remove_context()
|
||||
|
||||
def _remove_context(self):
|
||||
"""Remove the context directory if it exists."""
|
||||
if os.path.exists(self.path):
|
||||
try:
|
||||
shutil.rmtree(self.path)
|
||||
self.logger.info(f"Context directory removed: {self.path}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error removing context directory: {e!s}")
|
||||
|
||||
def file_set(self, path: str, cat: str, name: str = "", content: str = "") -> MyFile:
|
||||
"""Set a file in the context with the given category.
|
||||
|
||||
Args:
|
||||
path: Source file path
|
||||
cat: Category for organizing files
|
||||
name: Optional custom name for the file
|
||||
content: Optional content to write to file
|
||||
|
||||
Returns:
|
||||
MyFile: A MyFile instance representing the file in context
|
||||
|
||||
Raises:
|
||||
ValueError: If both path and content are provided
|
||||
FileNotFoundError: If the source file does not exist
|
||||
|
||||
"""
|
||||
cat = name_fix(cat)
|
||||
name = name_fix(name)
|
||||
|
||||
if content:
|
||||
if path:
|
||||
raise ValueError("path and content cannot be both set")
|
||||
path = os.path.join(self.path, "files", cat, name)
|
||||
with open(path, "w") as file:
|
||||
file.write(content)
|
||||
|
||||
mf = MyFile(path=path)
|
||||
if not mf.exists:
|
||||
raise FileNotFoundError(f"Source file does not exist: {path}")
|
||||
|
||||
if not content:
|
||||
if not name:
|
||||
name = name_fix(mf.name())
|
||||
else:
|
||||
if os.path.splitext(name)[1].lower() != mf.ext():
|
||||
name_ext = os.path.splitext(name)[1]
|
||||
raise ValueError(f"Extension {name_ext} must match file extension {mf.ext()}")
|
||||
|
||||
file_path = os.path.join(self.path, "files", cat, name)
|
||||
file_path_md5 = os.path.join(self.path, "files", cat, name + ".md5")
|
||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||
|
||||
# Check if the MD5 hash of the file on disk
|
||||
md5_on_disk = ""
|
||||
if os.path.exists(file_path_md5):
|
||||
with open(file_path_md5) as file:
|
||||
md5_on_disk = file.read().strip()
|
||||
# Validate that it's a valid MD5 hash
|
||||
if len(md5_on_disk) != 32 or not all(c in "0123456789abcdef" for c in md5_on_disk.lower()):
|
||||
raise RuntimeError("Bug: hash is not in the right format")
|
||||
|
||||
new_md5 = mf.md5()
|
||||
|
||||
changed_in_context = False
|
||||
if not md5_on_disk or new_md5 != md5_on_disk:
|
||||
changed_in_context = True
|
||||
|
||||
md5_dir = os.path.join(self.path, "files", "md5")
|
||||
|
||||
if changed_in_context:
|
||||
# File did change
|
||||
old_name = os.path.basename(path)
|
||||
new_name = os.path.basename(file_path)
|
||||
self.logger.debug(f"File changed in context {self.name}: {old_name} -> {new_name}")
|
||||
if mf.path != file_path:
|
||||
shutil.copy2(mf.path, file_path)
|
||||
with open(file_path_md5, "w") as file:
|
||||
file.write(new_md5)
|
||||
# Remove the old MD5 link if it exists
|
||||
if md5_on_disk:
|
||||
old_md5_link = os.path.join(md5_dir, md5_on_disk)
|
||||
remove_file_if_exists(old_md5_link)
|
||||
|
||||
mf.path = file_path
|
||||
|
||||
os.makedirs(md5_dir, exist_ok=True)
|
||||
md5_link = os.path.join(md5_dir, mf.md5())
|
||||
if not os.path.exists(md5_link):
|
||||
os.symlink(os.path.relpath(file_path, md5_dir), md5_link)
|
||||
|
||||
return mf
|
||||
|
||||
def file_get(self, name: str, cat: str, needtoexist: bool = True) -> MyFile:
|
||||
"""Get a file from the context with the given category.
|
||||
|
||||
Args:
|
||||
name: Name of the file to retrieve
|
||||
cat: Category the file is stored under
|
||||
needtoexist: Whether to raise an error if file doesn't exist
|
||||
|
||||
Returns:
|
||||
MyFile: A MyFile instance representing the requested file
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If needtoexist is True and file doesn't exist
|
||||
|
||||
"""
|
||||
name = name_fix(name)
|
||||
cat = name_fix(cat)
|
||||
file_path = os.path.join(self.path, "files", cat, name)
|
||||
if needtoexist:
|
||||
if os.path.exists(file_path):
|
||||
return MyFile(file_path)
|
||||
else:
|
||||
self.logger.warning(f"File not found: {file_path}")
|
||||
raise FileNotFoundError(f"Context file does not exist: {file_path}")
|
||||
else:
|
||||
return MyFile(file_path)
|
Reference in New Issue
Block a user