mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
306 lines
9.1 KiB
Python
306 lines
9.1 KiB
Python
"""Filesystem Module.
|
|
|
|
Safe filesystem operations using standard library only.
|
|
|
|
Key Features:
|
|
- Safe file reading with error handling
|
|
- Safe file writing with atomic operations
|
|
- Path validation and sanitization
|
|
- File existence checking
|
|
- Directory operations
|
|
|
|
Dependencies:
|
|
- pathlib (standard library)
|
|
- os (standard library)
|
|
"""
|
|
|
|
from pathlib import Path
|
|
from typing import Optional, List
|
|
import os
|
|
import tempfile
|
|
import shutil
|
|
|
|
|
|
class FilesystemError(Exception):
|
|
"""Filesystem operation error"""
|
|
|
|
|
|
class Filesystem:
|
|
"""Handle safe filesystem operations.
|
|
|
|
All operations use standard library only and include comprehensive
|
|
error handling with graceful degradation.
|
|
"""
|
|
|
|
@staticmethod
|
|
def safe_read(file_path: Path, max_size: Optional[int] = None) -> bytes:
|
|
"""Safely read a file with size limits and error handling.
|
|
|
|
Args:
|
|
file_path: Path to file to read
|
|
max_size: Maximum file size in bytes (None = no limit)
|
|
|
|
Returns:
|
|
File contents as bytes
|
|
|
|
Raises:
|
|
FilesystemError: If file cannot be read or exceeds size limit
|
|
"""
|
|
try:
|
|
# Convert to Path object if string
|
|
if isinstance(file_path, str):
|
|
file_path = Path(file_path)
|
|
|
|
# Check if file exists
|
|
if not file_path.exists():
|
|
raise FilesystemError(f"File does not exist: {file_path}")
|
|
|
|
# Check if it's a file (not directory)
|
|
if not file_path.is_file():
|
|
raise FilesystemError(f"Path is not a file: {file_path}")
|
|
|
|
# Check file size if limit specified
|
|
if max_size is not None:
|
|
file_size = file_path.stat().st_size
|
|
if file_size > max_size:
|
|
raise FilesystemError(
|
|
f"File size {file_size} exceeds limit {max_size}: {file_path}"
|
|
)
|
|
|
|
# Read file
|
|
return file_path.read_bytes()
|
|
|
|
except OSError as e:
|
|
raise FilesystemError(f"Failed to read file {file_path}: {e}") from e
|
|
|
|
@staticmethod
|
|
def safe_write(file_path: Path, data: bytes, atomic: bool = True) -> None:
|
|
"""Safely write to a file with atomic operations.
|
|
|
|
Args:
|
|
file_path: Path to file to write
|
|
data: Data to write (bytes)
|
|
atomic: Use atomic write (write to temp then rename)
|
|
|
|
Raises:
|
|
FilesystemError: If file cannot be written
|
|
"""
|
|
try:
|
|
# Convert to Path object if string
|
|
if isinstance(file_path, str):
|
|
file_path = Path(file_path)
|
|
|
|
# Create parent directory if it doesn't exist
|
|
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
if atomic:
|
|
# Atomic write: write to temp file then rename
|
|
# This ensures file is never partially written
|
|
temp_fd, temp_path = tempfile.mkstemp(
|
|
dir=file_path.parent,
|
|
prefix=f".{file_path.name}.tmp"
|
|
)
|
|
try:
|
|
# Write to temp file
|
|
os.write(temp_fd, data)
|
|
os.close(temp_fd)
|
|
|
|
# Rename temp file to target (atomic on POSIX)
|
|
shutil.move(temp_path, file_path)
|
|
except Exception:
|
|
# Clean up temp file on error
|
|
try:
|
|
os.unlink(temp_path)
|
|
except OSError:
|
|
pass
|
|
raise
|
|
else:
|
|
# Direct write
|
|
file_path.write_bytes(data)
|
|
|
|
except OSError as e:
|
|
raise FilesystemError(f"Failed to write file {file_path}: {e}") from e
|
|
|
|
@staticmethod
|
|
def validate_path(file_path: Path, must_exist: bool = False) -> bool:
|
|
"""Validate file path.
|
|
|
|
Args:
|
|
file_path: Path to validate
|
|
must_exist: If True, path must exist
|
|
|
|
Returns:
|
|
True if path is valid
|
|
|
|
Raises:
|
|
FilesystemError: If path is invalid
|
|
"""
|
|
try:
|
|
# Convert to Path object if string
|
|
if isinstance(file_path, str):
|
|
file_path = Path(file_path)
|
|
|
|
# Resolve to absolute path
|
|
resolved = file_path.resolve()
|
|
|
|
# Check if must exist
|
|
if must_exist and not resolved.exists():
|
|
raise FilesystemError(f"Path does not exist: {file_path}")
|
|
|
|
return True
|
|
|
|
except (OSError, RuntimeError) as e:
|
|
raise FilesystemError(f"Invalid path {file_path}: {e}") from e
|
|
|
|
@staticmethod
|
|
def get_size(file_path: Path) -> int:
|
|
"""Get file size in bytes.
|
|
|
|
Args:
|
|
file_path: Path to file
|
|
|
|
Returns:
|
|
File size in bytes
|
|
|
|
Raises:
|
|
FilesystemError: If file size cannot be determined
|
|
"""
|
|
try:
|
|
if isinstance(file_path, str):
|
|
file_path = Path(file_path)
|
|
|
|
return file_path.stat().st_size
|
|
|
|
except OSError as e:
|
|
raise FilesystemError(f"Failed to get file size {file_path}: {e}") from e
|
|
|
|
@staticmethod
|
|
def list_directory(dir_path: Path, pattern: str = "*") -> List[Path]:
|
|
"""List files in directory matching pattern.
|
|
|
|
Args:
|
|
dir_path: Directory path
|
|
pattern: Glob pattern (default: "*")
|
|
|
|
Returns:
|
|
List of matching file paths
|
|
|
|
Raises:
|
|
FilesystemError: If directory cannot be listed
|
|
"""
|
|
try:
|
|
if isinstance(dir_path, str):
|
|
dir_path = Path(dir_path)
|
|
|
|
if not dir_path.is_dir():
|
|
raise FilesystemError(f"Path is not a directory: {dir_path}")
|
|
|
|
return list(dir_path.glob(pattern))
|
|
|
|
except OSError as e:
|
|
raise FilesystemError(f"Failed to list directory {dir_path}: {e}") from e
|
|
|
|
@staticmethod
|
|
def ensure_directory(dir_path: Path) -> None:
|
|
"""Ensure directory exists, create if necessary.
|
|
|
|
Args:
|
|
dir_path: Directory path
|
|
|
|
Raises:
|
|
FilesystemError: If directory cannot be created
|
|
"""
|
|
try:
|
|
if isinstance(dir_path, str):
|
|
dir_path = Path(dir_path)
|
|
|
|
dir_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
except OSError as e:
|
|
raise FilesystemError(f"Failed to create directory {dir_path}: {e}") from e
|
|
|
|
@staticmethod
|
|
def remove_file(file_path: Path, missing_ok: bool = True) -> None:
|
|
"""Safely remove a file.
|
|
|
|
Args:
|
|
file_path: Path to file to remove
|
|
missing_ok: If True, don't raise error if file doesn't exist
|
|
|
|
Raises:
|
|
FilesystemError: If file cannot be removed
|
|
"""
|
|
try:
|
|
if isinstance(file_path, str):
|
|
file_path = Path(file_path)
|
|
|
|
file_path.unlink(missing_ok=missing_ok)
|
|
|
|
except OSError as e:
|
|
raise FilesystemError(f"Failed to remove file {file_path}: {e}") from e
|
|
|
|
@staticmethod
|
|
def copy_file(src: Path, dst: Path, overwrite: bool = False) -> None:
|
|
"""Safely copy a file.
|
|
|
|
Args:
|
|
src: Source file path
|
|
dst: Destination file path
|
|
overwrite: If True, overwrite existing destination file
|
|
|
|
Raises:
|
|
FilesystemError: If file cannot be copied
|
|
"""
|
|
try:
|
|
if isinstance(src, str):
|
|
src = Path(src)
|
|
if isinstance(dst, str):
|
|
dst = Path(dst)
|
|
|
|
if not src.exists():
|
|
raise FilesystemError(f"Source file does not exist: {src}")
|
|
|
|
if dst.exists() and not overwrite:
|
|
raise FilesystemError(f"Destination file exists: {dst}")
|
|
|
|
# Ensure destination directory exists
|
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Copy file
|
|
shutil.copy2(src, dst)
|
|
|
|
except OSError as e:
|
|
raise FilesystemError(f"Failed to copy file {src} to {dst}: {e}") from e
|
|
|
|
@staticmethod
|
|
def move_file(src: Path, dst: Path, overwrite: bool = False) -> None:
|
|
"""Safely move a file.
|
|
|
|
Args:
|
|
src: Source file path
|
|
dst: Destination file path
|
|
overwrite: If True, overwrite existing destination file
|
|
|
|
Raises:
|
|
FilesystemError: If file cannot be moved
|
|
"""
|
|
try:
|
|
if isinstance(src, str):
|
|
src = Path(src)
|
|
if isinstance(dst, str):
|
|
dst = Path(dst)
|
|
|
|
if not src.exists():
|
|
raise FilesystemError(f"Source file does not exist: {src}")
|
|
|
|
if dst.exists() and not overwrite:
|
|
raise FilesystemError(f"Destination file exists: {dst}")
|
|
|
|
# Ensure destination directory exists
|
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Move file
|
|
shutil.move(str(src), str(dst))
|
|
|
|
except OSError as e:
|
|
raise FilesystemError(f"Failed to move file {src} to {dst}: {e}") from e
|