diff --git a/confsync/utils/file_utils.py b/confsync/utils/file_utils.py new file mode 100644 index 0000000..67b1c6a --- /dev/null +++ b/confsync/utils/file_utils.py @@ -0,0 +1,105 @@ +"""File utility functions for ConfSync.""" + +import hashlib +import os +from pathlib import Path +from typing import List, Optional + + +def calculate_file_hash(content: str) -> str: + """Calculate SHA256 hash of content.""" + return hashlib.sha256(content.encode('utf-8')).hexdigest() + + +def read_file_safe(path: str, encoding: str = 'utf-8') -> Optional[str]: + """Safely read a file, returning None on error.""" + try: + with open(path, 'r', encoding=encoding) as f: + return f.read() + except (IOError, OSError, UnicodeDecodeError) as e: + print(f"Warning: Could not read file {path}: {e}") + return None + + +def write_file_safe(content: str, path: str, encoding: str = 'utf-8') -> bool: + """Safely write to a file, returning success status.""" + try: + Path(path).parent.mkdir(parents=True, exist_ok=True) + with open(path, 'w', encoding=encoding) as f: + f.write(content) + return True + except (IOError, OSError) as e: + print(f"Error: Could not write to file {path}: {e}") + return False + + +def find_files_matching( + base_path: str, + patterns: List[str], + recursive: bool = False +) -> List[str]: + """Find files matching any of the given patterns.""" + base = Path(base_path) + matches: List[str] = [] + + for pattern in patterns: + if recursive: + matches.extend(str(m) for m in base.rglob(pattern)) + else: + matches.extend(str(m) for m in base.glob(pattern)) + + return list(set(matches)) + + +def get_file_size(path: str) -> int: + """Get file size in bytes.""" + try: + return os.path.getsize(path) + except OSError: + return 0 + + +def get_file_mtime(path: str) -> float: + """Get file modification time.""" + try: + return os.path.getmtime(path) + except OSError: + return 0 + + +def ensure_directory(path: str) -> bool: + """Ensure directory exists, creating if necessary.""" + try: + Path(path).mkdir(parents=True, exist_ok=True) + return True + except OSError as e: + print(f"Error creating directory {path}: {e}") + return False + + +def normalize_line_endings(content: str, style: str = 'unix') -> str: + """Normalize line endings to Unix (LF) or Windows (CRLF) style.""" + if style == 'windows': + return content.replace('\n', '\r\n') + return content.replace('\r\n', '\n') + + +def is_binary_content(content: bytes) -> bool: + """Check if content appears to be binary.""" + text_chars = bytes([7, 8, 9, 10, 12, 13, 27]) + bytes(range(0x20, 0x100)) + return bool(content.translate(None, text_chars)) + + +def split_into_lines(content: str, max_line_length: int = 1000) -> List[str]: + """Split content into lines, handling various formats.""" + return content.splitlines() + + +def read_binary_file(path: str) -> Optional[bytes]: + """Read a binary file, returning None on error.""" + try: + with open(path, 'rb') as f: + return f.read() + except (IOError, OSError) as e: + print(f"Warning: Could not read binary file {path}: {e}") + return None