mirror of
https://github.com/pacnpal/Pac-cogs.git
synced 2025-12-20 10:51:05 -05:00
178 lines
6.1 KiB
Python
178 lines
6.1 KiB
Python
"""Module for directory management operations"""
|
|
|
|
import os
|
|
import logging
|
|
import asyncio
|
|
from pathlib import Path
|
|
from typing import List, Optional, Tuple
|
|
|
|
from .utils.exceptions import FileCleanupError
|
|
from .utils.file_deletion import SecureFileDeleter
|
|
|
|
logger = logging.getLogger("DirectoryManager")
|
|
|
|
class DirectoryManager:
|
|
"""Handles directory operations and cleanup"""
|
|
|
|
def __init__(self):
|
|
self.file_deleter = SecureFileDeleter()
|
|
|
|
async def cleanup_directory(
|
|
self,
|
|
directory_path: str,
|
|
recursive: bool = True,
|
|
delete_empty: bool = True
|
|
) -> Tuple[int, List[str]]:
|
|
"""Clean up a directory by removing files and optionally empty subdirectories
|
|
|
|
Args:
|
|
directory_path: Path to the directory to clean
|
|
recursive: Whether to clean subdirectories
|
|
delete_empty: Whether to delete empty directories
|
|
|
|
Returns:
|
|
Tuple[int, List[str]]: (Number of files deleted, List of errors)
|
|
|
|
Raises:
|
|
FileCleanupError: If cleanup fails critically
|
|
"""
|
|
if not os.path.exists(directory_path):
|
|
return 0, []
|
|
|
|
deleted_count = 0
|
|
errors = []
|
|
|
|
try:
|
|
# Process files and directories
|
|
deleted, errs = await self._process_directory_contents(
|
|
directory_path,
|
|
recursive,
|
|
delete_empty
|
|
)
|
|
deleted_count += deleted
|
|
errors.extend(errs)
|
|
|
|
# Clean up empty directories if requested
|
|
if delete_empty:
|
|
dir_errs = await self._cleanup_empty_directories(directory_path)
|
|
errors.extend(dir_errs)
|
|
|
|
if errors:
|
|
logger.warning(f"Cleanup completed with {len(errors)} errors")
|
|
else:
|
|
logger.info(f"Successfully cleaned directory: {directory_path}")
|
|
|
|
return deleted_count, errors
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error during cleanup of {directory_path}: {e}")
|
|
raise FileCleanupError(f"Directory cleanup failed: {str(e)}")
|
|
|
|
async def _process_directory_contents(
|
|
self,
|
|
directory_path: str,
|
|
recursive: bool,
|
|
delete_empty: bool
|
|
) -> Tuple[int, List[str]]:
|
|
"""Process contents of a directory"""
|
|
deleted_count = 0
|
|
errors = []
|
|
|
|
try:
|
|
for entry in os.scandir(directory_path):
|
|
try:
|
|
if entry.is_file():
|
|
# Delete file
|
|
if await self.file_deleter.delete_file(entry.path):
|
|
deleted_count += 1
|
|
else:
|
|
errors.append(f"Failed to delete file: {entry.path}")
|
|
elif entry.is_dir() and recursive:
|
|
# Process subdirectory
|
|
subdir_deleted, subdir_errors = await self.cleanup_directory(
|
|
entry.path,
|
|
recursive=True,
|
|
delete_empty=delete_empty
|
|
)
|
|
deleted_count += subdir_deleted
|
|
errors.extend(subdir_errors)
|
|
except Exception as e:
|
|
errors.append(f"Error processing {entry.path}: {str(e)}")
|
|
|
|
except Exception as e:
|
|
errors.append(f"Error scanning directory {directory_path}: {str(e)}")
|
|
|
|
return deleted_count, errors
|
|
|
|
async def _cleanup_empty_directories(self, start_path: str) -> List[str]:
|
|
"""Remove empty directories recursively"""
|
|
errors = []
|
|
|
|
try:
|
|
for root, dirs, files in os.walk(start_path, topdown=False):
|
|
for name in dirs:
|
|
try:
|
|
dir_path = os.path.join(root, name)
|
|
if not os.listdir(dir_path): # Check if directory is empty
|
|
await self._remove_directory(dir_path)
|
|
except Exception as e:
|
|
errors.append(f"Error removing directory {name}: {str(e)}")
|
|
|
|
except Exception as e:
|
|
errors.append(f"Error walking directory tree: {str(e)}")
|
|
|
|
return errors
|
|
|
|
async def _remove_directory(self, dir_path: str) -> None:
|
|
"""Remove a directory safely"""
|
|
try:
|
|
await asyncio.to_thread(os.rmdir, dir_path)
|
|
except Exception as e:
|
|
logger.error(f"Failed to remove directory {dir_path}: {e}")
|
|
raise
|
|
|
|
async def ensure_directory(self, directory_path: str) -> None:
|
|
"""Ensure a directory exists and is accessible
|
|
|
|
Args:
|
|
directory_path: Path to the directory to ensure
|
|
|
|
Raises:
|
|
FileCleanupError: If directory cannot be created or accessed
|
|
"""
|
|
try:
|
|
path = Path(directory_path)
|
|
path.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Verify directory is writable
|
|
if not os.access(directory_path, os.W_OK):
|
|
raise FileCleanupError(f"Directory {directory_path} is not writable")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error ensuring directory {directory_path}: {e}")
|
|
raise FileCleanupError(f"Failed to ensure directory: {str(e)}")
|
|
|
|
async def get_directory_size(self, directory_path: str) -> int:
|
|
"""Get total size of a directory in bytes
|
|
|
|
Args:
|
|
directory_path: Path to the directory
|
|
|
|
Returns:
|
|
int: Total size in bytes
|
|
"""
|
|
total_size = 0
|
|
try:
|
|
for entry in os.scandir(directory_path):
|
|
try:
|
|
if entry.is_file():
|
|
total_size += entry.stat().st_size
|
|
elif entry.is_dir():
|
|
total_size += await self.get_directory_size(entry.path)
|
|
except Exception as e:
|
|
logger.warning(f"Error getting size for {entry.path}: {e}")
|
|
except Exception as e:
|
|
logger.error(f"Error calculating directory size: {e}")
|
|
|
|
return total_size
|