| """
|
| file_manager.py
|
|
|
| This module provides the FileManager class, a high-level interface for managing
|
| files and directories inside a specified media folder. It centralizes common
|
| operations such as reading, writing, listing, copying, moving, and deleting files,
|
| ensuring that all actions are safely constrained within the defined root directory.
|
| The class is designed to simplify file handling logic in larger applications by
|
| offering a consistent, validated, and extensible API for filesystem interactions.
|
| """
|
|
|
| from pathlib import Path
|
|
|
| class FileManager:
|
| """
|
| FileManager is a utility class that encapsulates common filesystem operations
|
| within a defined media directory. It ensures that all file manipulations remain
|
| inside the configured root folder and provides helper methods to interact with
|
| files and subdirectories in a structured, safe, and predictable manner.
|
|
|
| Typical use cases include managing uploaded media, performing batch operations
|
| on directory contents, and abstracting filesystem complexity behind a clean API.
|
| """
|
|
|
| def __init__(self, media_folder: Path):
|
| """
|
| Initialize the FileManager with a specific root directory for all file
|
| operations.
|
|
|
| Parameters
|
| ----------
|
| media_folder : Path
|
| The base directory where all file and folder operations will be performed.
|
| It must be a valid filesystem path. If the directory does not exist, the
|
| instance will attempt to create it automatically.
|
|
|
| Raises
|
| ------
|
| ValueError
|
| If the provided media_folder path is not a valid directory or cannot be
|
| created.
|
| """
|
| self.media_folder = Path(media_folder)
|
|
|
| if not self.media_folder.exists():
|
| try:
|
| self.media_folder.mkdir(parents=True)
|
| except Exception as exc:
|
| raise ValueError(
|
| f"Unable to create media folder at: {self.media_folder}"
|
| ) from exc
|
|
|
| if not self.media_folder.is_dir():
|
| raise ValueError(f"Media folder is not a directory: {self.media_folder}")
|
|
|
|
|
| def upload_file(self, file_handler, destination: Path):
|
| """
|
| Upload a file to a target destination inside the media folder.
|
|
|
| This method takes a file-like object and writes its contents to the
|
| specified destination within the media folder. The method ensures the path
|
| remains inside the media folder and creates necessary directories. It
|
| returns a structured response indicating whether the operation succeeded
|
| or failed, along with any relevant error message.
|
|
|
| Parameters
|
| ----------
|
| file_handler : file-like object
|
| A file-like object opened in binary mode that provides a `.read()` method.
|
| destination : Path
|
| The relative or absolute path (within the media folder) where the file
|
| should be saved.
|
|
|
| Returns
|
| -------
|
| dict
|
| A dictionary with:
|
| - "operation_success" (bool): True if the file was saved successfully.
|
| - "error" (str): An empty string on success, or the error message on failure.
|
|
|
| Raises
|
| ------
|
| None
|
| Any exceptions are captured and returned inside the result dictionary.
|
| """
|
| try:
|
| destination = self.media_folder / destination
|
| destination = destination.resolve()
|
|
|
|
|
| if self.media_folder not in destination.parents:
|
| return {
|
| "operation_success": False,
|
| "error": "Destination path is outside the media folder."
|
| }
|
|
|
|
|
| destination.parent.mkdir(parents=True, exist_ok=True)
|
|
|
| with open(destination, "wb") as f:
|
| f.write(file_handler.read())
|
|
|
| return {"operation_success": True, "error": ""}
|
|
|
| except Exception as exc:
|
| return {
|
| "operation_success": False,
|
| "error": str(exc)
|
| }
|
|
|
| def get_file(self, file_path: Path):
|
| """
|
| Retrieve a file inside the media folder and return an open file handler.
|
|
|
| This method receives a path pointing to a file expected to be located
|
| inside the media folder. If the file exists and is valid, the method
|
| returns a file handler opened in binary read mode. If the file does not
|
| exist or the resolved path escapes the media folder, the method returns
|
| None.
|
|
|
| Parameters
|
| ----------
|
| file_path : Path
|
| The relative or absolute path to the file within the media folder.
|
|
|
| Returns
|
| -------
|
| file object or None
|
| A file handler opened in 'rb' mode if the file exists and is accessible.
|
| Returns None if the file does not exist or the path is invalid.
|
|
|
| Raises
|
| ------
|
| None
|
| All exceptions are handled internally and result in returning None.
|
| """
|
| try:
|
| target = self.media_folder / file_path
|
| target = target.resolve()
|
|
|
|
|
| if self.media_folder not in target.parents:
|
| return None
|
|
|
| if not target.exists() or not target.is_file():
|
| return None
|
|
|
| return open(target, "rb")
|
|
|
| except Exception:
|
| return None
|
|
|
| def compute_sha1(self, file_handler):
|
| """
|
| Compute the SHA-1 checksum of a file handler.
|
|
|
| This method calculates the SHA-1 hash of the content provided by a
|
| file-like object opened in binary mode. The method preserves the
|
| current file pointer position by saving it, rewinding to the start
|
| of the file, computing the hash, and restoring the file pointer to
|
| its original position.
|
|
|
| Parameters
|
| ----------
|
| file_handler : file-like object
|
| A file-like object opened in binary mode, providing `.read()` and
|
| `.seek()` methods.
|
|
|
| Returns
|
| -------
|
| str
|
| The hexadecimal SHA-1 checksum of the file content.
|
|
|
| Raises
|
| ------
|
| ValueError
|
| If the provided file handler is invalid or does not support the
|
| required read/seek operations.
|
| """
|
| import hashlib
|
|
|
| if not hasattr(file_handler, "read") or not hasattr(file_handler, "seek"):
|
| raise ValueError("The provided file handler does not support read/seek operations.")
|
|
|
| original_pos = file_handler.tell()
|
|
|
| try:
|
| file_handler.seek(0)
|
| sha1 = hashlib.sha1()
|
|
|
| for chunk in iter(lambda: file_handler.read(8192), b""):
|
| sha1.update(chunk)
|
|
|
| file_handler.seek(original_pos)
|
| return sha1.hexdigest()
|
|
|
| except Exception as exc:
|
| try:
|
| file_handler.seek(original_pos)
|
| except Exception:
|
| pass
|
| raise ValueError(f"Unable to compute SHA-1 checksum: {exc}") from exc
|
|
|