|
|
"""
|
|
|
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
|
|
|
|