From ef0f2ff54d2542f27a9f28e6fd4c299757fad644 Mon Sep 17 00:00:00 2001 From: babaev-an Date: Sun, 8 Jun 2025 21:57:58 +0300 Subject: [PATCH] =?UTF-8?q?20250608-1=20[File]=20=D0=BE=D0=B1=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D0=BB=D1=91=D0=BD=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=20Rem?= =?UTF-8?q?oveDir.=20=D0=A2=D0=B5=D0=BF=D0=B5=D1=80=D1=8C=20=D0=BE=D0=BD?= =?UTF-8?q?=20=D0=BA=D0=BE=D1=80=D1=80=D0=B5=D0=BA=D1=82=D0=BD=D0=BE=20?= =?UTF-8?q?=D1=83=D0=B4=D0=B0=D0=BB=D1=8F=D0=B5=D1=82=20=D0=BB=D1=8E=D0=B1?= =?UTF-8?q?=D1=83=D1=8E=20=D0=B4=D0=B8=D1=80=D0=B5=D0=BA=D1=82=D0=BE=D1=80?= =?UTF-8?q?=D0=B8=D1=8E.=20=D0=A2=D0=B0=D0=BA=D0=B6=D0=B5=20=D0=B8=D0=B7?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D0=B8=D0=BB=D1=81=D1=8F=20=D1=81=D0=B8=D0=BD?= =?UTF-8?q?=D1=82=D0=B0=D0=BA=D1=81=D0=B8=D1=81=20=D1=8D=D1=82=D0=BE=D0=B3?= =?UTF-8?q?=D0=BE=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=D0=B0:=20public=20stati?= =?UTF-8?q?c=20function=20RemoveDir=20(string=20$directory,=20array=20$err?= =?UTF-8?q?orMessages=20=3D=20self::REMOVE=5FDIRECTORY=5FERROR=5FMESSAGES)?= =?UTF-8?q?:=20ActionState,=20=D0=B3=D0=B4=D0=B5=20$errorMessages=20--=20?= =?UTF-8?q?=D0=BC=D0=B0=D1=81=D1=81=D0=B8=D0=B2=20=D1=81=20=D0=BB=D0=BE?= =?UTF-8?q?=D0=BA=D0=B0=D0=BB=D0=B8=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=D0=BD?= =?UTF-8?q?=D1=8B=D0=BC=20=D1=81=D0=BF=D0=B8=D1=81=D0=BA=D0=BE=D0=BC=20?= =?UTF-8?q?=D0=BE=D1=88=D0=B8=D0=B1=D0=BE=D0=BA.=20=D0=A2=D0=B5=D0=BF?= =?UTF-8?q?=D0=B5=D1=80=D1=8C=20=D0=B2=D0=BC=D0=B5=D1=81=D1=82=D0=BE=20boo?= =?UTF-8?q?l=20=D0=B2=D0=BE=D0=B7=D0=B2=D1=80=D0=B0=D1=89=D0=B0=D0=B5?= =?UTF-8?q?=D1=82=D1=81=D1=8F=20ActionState,=20=D0=BA=D1=83=D0=B4=D0=B0=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=BD=D0=BE=D1=81=D1=8F=D1=82=D1=81=D1=8F=20=D0=B2?= =?UTF-8?q?=D1=81=D0=B5=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B8=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D0=B8=D0=B8?= =?UTF-8?q?.=20[File]=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=20FileSize=20(string=20$filen?= =?UTF-8?q?ame,=20array=20$errorLocalization=20=3D=20self::FILE=5FSIZE=5FE?= =?UTF-8?q?RROR=5FMESSAGES):=20ActionState,=20=D0=BA=D0=BE=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D1=8B=D0=B9=20=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B0=D0=B5?= =?UTF-8?q?=D1=82=20=D1=80=D0=B0=D0=B7=D0=BC=D0=B5=D1=80=20=D1=84=D0=B0?= =?UTF-8?q?=D0=B9=D0=BB=D0=B0=20[File]=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=20FileSizeT?= =?UTF-8?q?oString=20(int=20$fileSize,=20array=20$fileSizeUnits=20=3D=20se?= =?UTF-8?q?lf::FILE=5FSIZE=5FUNITS,=20string=20$decimalSeparator=20=3D=20'?= =?UTF-8?q?,'):=20string,=20=D0=BA=D0=BE=D1=82=D0=BE=D1=80=D1=8B=D0=B9=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B5=D0=BE=D0=B1=D1=80=D0=B0=D0=B7=D1=83=D0=B5?= =?UTF-8?q?=D1=82=20=D1=80=D0=B0=D0=B7=D0=BC=D0=B5=D1=80=20=D1=84=D0=B0?= =?UTF-8?q?=D0=B9=D0=BB=D0=B0=20=D0=B2=20=D0=B1=D0=B0=D0=B9=D1=82=D0=B0?= =?UTF-8?q?=D1=85=20=D0=B2=20=D0=BA=D1=80=D0=B0=D1=81=D0=B8=D0=B2=D0=BE?= =?UTF-8?q?=D0=B5=20=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=BE=D0=B2=D0=BE=D0=B5?= =?UTF-8?q?=20=D0=BF=D1=80=D0=B5=D0=B4=D1=81=D1=82=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sources/classes/File.php | 217 +++++++++++++++++++++++++++++++++---- tests/classes/FileTest.php | 22 +++- 2 files changed, 214 insertions(+), 25 deletions(-) diff --git a/sources/classes/File.php b/sources/classes/File.php index ddb8077..f17c242 100644 --- a/sources/classes/File.php +++ b/sources/classes/File.php @@ -2,6 +2,7 @@ namespace goodboyalex\php_components_pack\classes; +use Exception; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; @@ -10,11 +11,35 @@ use RecursiveIteratorIterator; * * @author Александр Бабаев * @package php_components_pack - * @version 1.0.1 + * @version 1.0.2 * @since 1.0.21 */ final class File { + /** + * @var array Массив сообщений об ошибках при удалении директории. + */ + public const array REMOVE_DIRECTORY_ERROR_MESSAGES = [ + 'directory_not_exist' => "Директория не существует или нет доступа на запись!", + 'error_deleting_file_or_link' => 'Ошибка удаления файла или ссылки: %s!', + 'error_deleting_directory' => 'Ошибка удаления каталога: %s. Код возврата: %d!', + 'unhandled_error' => 'Ошибка удаления директории %s: %s!' + ]; + + /** + * @var array Массив сообщений об ошибках при получении размера файла. + */ + public const array FILE_SIZE_ERROR_MESSAGES = [ + 'file_not_exist' => 'Файл не существует!', + 'not_a_file' => 'Указанный путь не является файлом!', + 'cannot_get_size' => 'Не удалось получить размер файла!' + ]; + + /** + * @var array Массив локализации размеров файлов. + */ + public const array FILE_SIZE_UNITS = ['байт', 'КБ', 'МБ', 'ГБ', 'ТБ']; + /** * Получает список файлов в директории и поддиректориях, соответствующей шаблону $pattern. * @@ -109,34 +134,96 @@ final class File * Удаляет директорию вместе со всеми файлами и поддиректориями. * * @param string $directory Полный путь к директории. + * @param array $errorMessages Сообщения об ошибках удаления (по умолчанию, см. + * {@link REMOVE_DIRECTORY_ERROR_MESSAGES}). * - * @return bool Результат удаления. + * @return ActionState Результат удаления. */ - public static function RemoveDir (string $directory): bool + public static function RemoveDir (string $directory, + array $errorMessages = self::REMOVE_DIRECTORY_ERROR_MESSAGES): ActionState { - // Проверяем, существует ли директория - if (!self::DirectoryExists(directory: $directory, checkWriteAccess: true)) - // - если нет, то возвращаем false - return false; + // Создаю результат + $result = new ActionState(false); - // Получаем список файлов и каталогов в заданной директории - $files = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($directory), - RecursiveIteratorIterator::CHILD_FIRST - ); + try { + // Проверяем наличие директории и доступ на запись + if (!self::DirectoryExists(directory: $directory, checkWriteAccess: true)) { + // - если нет, то добавляем ошибку + $result->AddError($errorMessages['directory_not_exist']); - // Перебираем файлы и каталоги - foreach ($files as $file) - // - если это каталог - if ($file->isDir()) - // -- то удаляем его - rmdir($file->getRealPath()); - else - // -- иначе удаляем файл - unlink($file->getRealPath()); + // - и возвращаем результат + return $result; + } - // Удаляем директорию - return rmdir($directory); + // Создаем рекурсивный итерационный объект для перебора всего дерева каталогов + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($directory), + RecursiveIteratorIterator::CHILD_FIRST + ); + + // Проходим по каждому элементу (каталогам и файлам) + foreach ($iterator as $item) { + // - получаем путь к файлу + $realPath = $item->getRealPath(); + + // - если это файл или ссылка + if ($item->isFile() || $item->isLink()) + // -- то удаляем его + if (!@unlink($realPath)) { + // --- если не удалось удалить, то добавляем ошибку + $result->AddError(sprintf($errorMessages['error_deleting_file_or_link'], $realPath)); + + // --- и возвращаем результат + return $result; + } + } + + // Определение текущей операционной системы + $os = strtolower(PHP_OS_FAMILY); + + // Экранируем аргумент для предотвращения инъекций + $escapedDirectory = escapeshellarg($directory); + + // Дальнейшие действия зависят от операционной системы + switch ($os) { + // - для Windows + case 'windows': + // -- выполняем команду Windows + exec("rd /s /q $escapedDirectory", $output, $returnCode); + break; + + // - для Linux/macOS + default: + // -- выполняем команду Linux/macOS + exec("rm -rf $escapedDirectory", $output, $returnCode); + break; + } + + // Проверяем код возврата + if ($returnCode !== 0) { + // - если не удалось удалить, то добавляем ошибку + $result->AddError(sprintf($errorMessages['error_deleting_directory'], $directory, $returnCode)); + + // --- и возвращаем результат + return $result; + } + + // Если все прошло успешно (а если мы сюда попали, то все должно быть хорошо), то добавляем результат true + $result->Value = true; + + // - и возвращаем его + return $result; + } + catch (Exception $exception) { + // Если произошла ошибка, то добавляем ошибку + $result->AddError(sprintf($errorMessages['unhandled_error'], $directory, $exception->getMessage())); + + // - задаем результат false + $result->Value = false; + + // - и возвращаем его + return $result; + } } /** @@ -177,4 +264,88 @@ final class File // Если все проверки пройдены успешно, то возвращаем true return true; } + + /** + * Получает размер файла в байтах. + * + * @param string $filename Имя файла. + * @param array $errorLocalization Массив сообщений об ошибках при получении размера файла (по умолчанию, см. + * {@link FILE_SIZE_ERROR_MESSAGES}). + * + * @return ActionState Результат с размером файла в байтах. + */ + public static function FileSize (string $filename, + array $errorLocalization = self::FILE_SIZE_ERROR_MESSAGES): ActionState + { + // Очищаем кэш + clearstatcache(); + + // Создаём результат + $result = new ActionState(-1); + + // Проверяем, существует ли файл + if (!file_exists($filename)) { + // - если нет, то добавляем ошибку + $result->AddError($errorLocalization['file_not_exist']); + // - и возвращаем результат + return $result; + } + + // Проверяем, является ли $filename файлом + if (!is_file($filename)) { + // - если нет, то добавляем ошибку + $result->AddError($errorLocalization['not_a_file']); + // - и возвращаем результат + return $result; + } + + // Получаем размер файла + $size = filesize($filename); + + // Проверяем, получилось ли получить размер файла + if ($size === false) { + // - если нет, то добавляем ошибку + $result->AddError($errorLocalization['cannot_get_size']); + // - и возвращаем результат + return $result; + } + + // Устанавливаем значение результата + $result->Value = $size; + + // Возвращаем результат + return $result; + } + + /** + * Преобразует размер файла в байтах в красивое строковое представление. + * + * @param int $fileSize Размер файла в байтах. + * @param array $fileSizeUnits Локализованные единицы измерения размера файла (по умолчанию, см. + * {@link FILE_SIZE_UNITS}). + * @param string $decimalSeparator Разделитель десятичной части (по умолчанию, запятая). + * + * @return string Размер файла в красивом строковом представлении. Например, если размер файла составляет 1500 + * байт, вывод будет «1.46 КБ». + */ + public static function FileSizeToString (int $fileSize, array $fileSizeUnits = self::FILE_SIZE_UNITS, + string $decimalSeparator = ','): string + { + /** + * Вычисление степени для преобразования: берём минимум из 4 и результата округления до ближайшего целого числа + * в меньшую сторону логарифма размера файла в байтах по основанию 1024 (это показывает, сколько раз нужно + * разделить размер файла на 1024, чтобы получить значение в более крупных единицах измерения). Ограничение в 4 + * необходимо для того, чтобы соответствовать единице измерения ТБ (терабайт). + */ + $power = min(4, floor(log($fileSize, 1024))); + + /** + * Преобразование размера файла: размер файла делим на 1024 в степени, равной степени $power, + * затем округляем полученное до 2 цифр после запятой. + */ + $size = number_format(round($fileSize / pow(1024, $power), 2), 2, $decimalSeparator); + + // Возвращаем преобразованное значение вместе с единицей измерения + return "$size $fileSizeUnits[$power]"; + } } \ No newline at end of file diff --git a/tests/classes/FileTest.php b/tests/classes/FileTest.php index c5a01c0..a75b3c5 100644 --- a/tests/classes/FileTest.php +++ b/tests/classes/FileTest.php @@ -68,9 +68,27 @@ class FileTest extends TestCase $this->PrepareForTest(); - $result = File::RemoveDir("D:\\TestDelete"); + $result = File::RemoveDir("D:/TestDelete"); - $this->assertTrue($result); + $this->assertTrue($result->Value); $this->assertFalse(File::DirectoryExists("D:\\TestDelete")); } + + public function testFileSize () + { + $this->PrepareForTest(); + + $size = File::FileSize("C:\\Windows/explorer.exe"); + + $this->assertEquals(2774080, $size->Value); + } + + public function testFileSizeString () + { + $this->PrepareForTest(); + + $size = File::FileSizeToString(2774080); + + $this->assertEquals("2,65 МБ", $size); + } } \ No newline at end of file