20251008
This commit is contained in:
274
anb_python_components/classes/file.py
Normal file
274
anb_python_components/classes/file.py
Normal 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)
|
Reference in New Issue
Block a user