This commit is contained in:
2025-10-08 23:48:59 +03:00
parent c2ac2325ca
commit 681093d48f

View File

@@ -0,0 +1,274 @@
# anb_python_components/classes/action_state.py
import glob
import math
import os
import platform
import subprocess
from anb_python_components.classes.action_state import ActionState
class File:
"""
Класс для работы с файлами.
"""
# Словарь сообщений об ошибках для удаления директории
REMOVE_DIRECTORY_ERROR_MESSAGES: dict[str, str] = {
'directory_not_exist': "Директория не существует или нет доступа на запись!",
'error_deleting_directory': 'Ошибка удаления каталога: %s. Код возврата: %d!',
'unhandled_error': 'Ошибка удаления директории %s: %s!'
}
# Словарь сообщений об ошибках для получения размера файла
FILE_SIZE_ERROR_MESSAGES: dict[str, str] = {
'file_not_exist': 'Файл не существует!',
'not_a_file': 'Указанный путь не является файлом!',
'cannot_get_size': 'Не удалось получить размер файла!'
}
# Словарь локализации размеров файлов.
FILE_SIZE_UNITS: list[str] = ['байт', 'КБ', 'МБ', 'ГБ', 'ТБ']
@staticmethod
def find_files (directory: str, pattern: str = '*', exclude_list: set[str] = str()) -> list | bool:
"""
Ищет файлы, удовлетворяющие заданному паттерну, рекурсивно проходя по указанным директориям.
:param directory: Директория, в которой производится поиск.
:param pattern: Маска файла (по умолчанию '*').
:param exclude_list: Список директорий, которые нужно исключить из итогового списка. Внимание: будут исключены все поддиректории этих директорий.
:return: list|bool: Список найденных файлов или False, если произошла ошибка.
"""
try:
# Начальная точка поиска — указанный каталог
files = []
# Начинаем обход каталога и его вложенных подкаталогов
for root, dirs, filenames in os.walk(directory):
# - фильтруем директории, исключая заданные в exclude_list
dirs[:] = [d for d in dirs if d not in exclude_list]
# Применяем маску поиска (* или любую другую)
matches = glob.glob(os.path.join(root, pattern))
# Добавляем найденные файлы в общий список
files.extend(matches)
# Возвращаем список найденных файлов
return files
except OSError:
# Если возникает ошибка файловой операции, возвращаем False
return False
@staticmethod
def extract_file_name (file_path: str) -> str:
"""
Извлекает имя файла из полного пути к нему.
:param file_path: Полный путь к файлу.
:return: str: Имя файла.
"""
return os.path.basename(file_path)
@staticmethod
def extract_file_extension (file_path: str, with_dot: bool = True) -> str:
"""
Извлекает расширение файла из полного пути к нему.
:param file_path: Полный путь к файлу.
:param with_dot: Если True, точка перед расширением будет добавлена к результату, если False, точка будет удалена.
:return: str: Расширение файла.
"""
# Получаю расширение файла из полного пути к нему
_, extension = os.path.splitext(file_path)
# Если нужно добавить точку перед расширением, добавляю её
return extension if with_dot else extension.lstrip('.')
@staticmethod
def extract_file_name_without_extension (file_path: str) -> str:
# Имя файла без пути к нему
file_name_only = File.extract_file_name(file_path)
# Расширение файла
file_extension = File.extract_file_extension(file_path)
# Возвращаем имя файла без пути к нему и расширения
return file_name_only[:-len(file_extension)]
@staticmethod
def relative_path (full_path: str, base_path: str) -> str | bool:
"""
Возвращает относительный путь к файлу относительно заданной директории.
:param full_path: Полный путь к файлу.
:param base_path: Базовая директория.
:return: str|bool: Относительный путь к файлу или False, если путь не относится к заданной директории.
"""
return full_path[len(base_path):] if base_path.lower() in full_path.lower() else False
@staticmethod
def remove_dir (directory: str, error_messages: dict[str, str] | None = None) -> ActionState[bool]:
"""
Рекурсивно удаляет директорию с соответствующим результатом.
:param directory: Путь к директории.
:param error_messages: Слова для отображения ошибок. По умолчанию используются сообщения из REMOVE_DIRECTORY_ERROR_MESSAGES.
:return: Объект ActionState с информацией о результате.
"""
# Создаем объект ActionState для хранения результата
result = ActionState[bool](False)
# Если не заданы сообщения об ошибках
if error_messages is None:
# - устанавливаем сообщения по умолчанию
error_messages = File.REMOVE_DIRECTORY_ERROR_MESSAGES
try:
# Проверяем существование директории
if not File.directory_exists(directory):
# - если директория не существует, добавляем ошибку
result.add_error(error_messages['directory_not_exist'])
# - возвращаем результат
return result
# Определяем текущую операционную систему
system_os = platform.system().lower()
# Проверяем операционную систему. Если это Windows
if system_os == 'windows':
# - задаем команду для Windows
command = ['cmd.exe', '/C', 'rd', '/S', '/Q', directory]
else:
# - иначе задаем команду для Unix-подобных систем
command = ['rm', '-rf', directory]
# Запуск команды с безопасностью (используется subprocess.run)
process = subprocess.run(command, capture_output = True, text = True)
# Анализируем код возврата процесса и если он не равен 0
if process.returncode != 0:
# - добавляем ошибку
result.add_error(error_messages['error_deleting_directory'] % (directory, process.returncode))
# - возвращаем результат
return result
# Установка успешного результата
result.value = True
# Возвращаем результат
return result
except Exception as ex:
# Обработка необработанных исключений
result.add_error(error_messages['unhandled_error'] % (directory, str(ex)))
return result
@staticmethod
def directory_exists (directory: str, check_access_level: str = '') -> bool:
"""
Проверяет существование директории и доступность по правам доступа.
:param directory: Путь к директории.
:param check_access_level: Строка, содержащая символы 'r', 'w' и 'x', которые указывают на необходимость проверки прав доступа на чтение, запись и исполнение соответственно. Если строка пустая, проверка прав доступа не выполняется. Если нет какого-либо символа, проверка прав доступа не выполняется для этого типа прав. По умолчанию: ''.
:return: True, если директория существует и доступна, иначе False.
"""
# Проверяем существование директории
if not os.path.exists(directory):
# - и если директория не существует, возвращаем False
return False
# Проверяем, что это именно директория, а не файл
if not os.path.isdir(directory):
# - если это не директория, возвращаем False
return False
# Задаем флаги проверки прав доступа
access_level_check = check_access_level.lower()
# Проверяем права на чтение, если это требуется
if 'r' in access_level_check and not os.access(directory, os.R_OK):
# - если нет прав на чтение, возвращаем False
return False
# Проверяем права на запись, если это требуется
if 'w' in access_level_check and not os.access(directory, os.W_OK):
# - если нет прав на запись, возвращаем False
return False
# Проверяем права на исполнение, если это требуется
if 'x' in access_level_check and not os.access(directory, os.X_OK):
# - если нет прав на запись, возвращаем False
return False
# Если все проверки успешны, возвращаем True
return True
@staticmethod
def file_size (file_name: str, error_localization: dict[str, str] | None = None) -> ActionState[int]:
"""
Получает размер файла и формирует результат с возможными ошибками.
:param file_name: Путь к файлу.
:param error_localization: Локализации сообщений об ошибках. Ече если None, используются сообщения по умолчанию. По умолчанию: None
:return: Объект ActionState с размером файла или ошибками.
"""
# Создаем результат
result = ActionState(-1)
# Если не заданы сообщения об ошибках
if error_localization is None:
# - устанавливаем сообщения по умолчанию
error_localization = File.FILE_SIZE_ERROR_MESSAGES
# Проверяем существование файла
if not os.path.exists(file_name):
# - если файл не существует, добавляем ошибку
result.add_error(error_localization['file_not_exist'])
# - возвращаем результат
return result
# Проверяем, что это именно файл
if not os.path.isfile(file_name):
# - если это не файл, добавляем ошибку
result.add_error(error_localization['not_a_file'])
# - возвращаем результат
return result
# Пробуем получить размер файла
try:
size = os.path.getsize(file_name)
# - если размер файла получен успешно, добавляем его в результат
result.value = size
except OSError:
# - если возникла ошибка при получении размера файла, добавляем ошибку
result.add_error(error_localization['cannot_get_size'])
# Возвращаем результат
return result
@staticmethod
def file_size_to_string (
file_size: int, localize_file_size: dict[str, str] | None = None, decimal_separator: str = '.'
) -> str:
"""
Преобразует размер файла в строку с локализацией.
:param file_size: Размер файла в байтах.
:param localize_file_size: Словарь локализации размеров файлов. Если None, используются значения по умолчанию. По умолчанию: None.
:param decimal_separator: Разделитель дробной части числа. По умолчанию: '.'.
:return: str: Строка с размером файла.
"""
# Если не заданы локализации размеров файлов
if localize_file_size is None:
# - устанавливаем локализации по умолчанию
localize_file_size = File.FILE_SIZE_UNITS
# Вычисление степени для преобразования: берём минимум из 4 и результата округления до ближайшего целого числа
# в меньшую сторону логарифма размера файла в байтах по основанию 1024 (это показывает, сколько раз нужно
# разделить размер файла на 1024, чтобы получить значение в более крупных единицах измерения). Ограничение в 4
# необходимо для того, чтобы соответствовать единице измерения ТБ (терабайт).
power = min(4, math.floor(math.log(file_size, 1024))) if file_size > 0 else 0
# Преобразование размера файла: размер файла делим на 1024 в степени, равной степени $power,
# затем округляем полученное до 2 цифр после запятой.
size = round(file_size / (1024 ** power), 2)
# Возвращаем преобразованное значение вместе с единицей измерения
return f"{size:,.2f} {localize_file_size[power]}".replace('.', decimal_separator)