Структура каталогов по ID в PHP: практические решения

Раздел: Разработка на PHP -> Работа с файловой системой в PHP

Создание каталогов по ID в PHP: организация файлового хранилища

Основное эффективное решение: разбиение идентификатора на подкаталоги с фиксированной глубиной (например, по принципу ID / 1000 или через хэш). Такой подход позволяет избежать переполнения одной директории и ускоряет поиск. Пример реализации с вложенными папками на основе числового ID:


function createStoragePath(int $id, int $levels = 3): string {
    $parts = [];
    $temp = $id;
    for ($i = 0; $i < $levels; $i++) {
        $parts[] = $temp % 1000;
        $temp = intdiv($temp, 1000);
    }
    $path = 'uploads/' . implode('/', array_reverse($parts));
    if (!is_dir($path)) {
        mkdir($path, 0755, true);
    }
    return $path;
}
  

Php site dir (директория сайта в php)

Для ID = 123456 будет создана структура uploads/123/456/. Цель: равномерное распределение папок по файловой системе, снижение числа записей в каждой директории.

Типичная ошибка: забыть флаг true в mkdir - тогда вложенные папки не создадутся, и скрипт упадет с ошибкой No such file or directory. Также важно проверять права на запись в корневую директорию.

Вариант 1. Прямое создание папки с именем ID

Вопрос: Как создать каталог с именем, равным ID пользователя?


$path = 'users/' . $userId;
if (!is_dir($path)) {
    mkdir($path, 0755);
}
  

Php include dir (подключение директории в php)

Цель: быстрое обращение к папке конкретного пользователя. Проблемы: при сотнях тысяч ID одна директория может содержать слишком много подпапок, что снижает производительность (лимит на количество записей в файловой системе). Кроме того, поиск внутри такой директории замедляется.

Ошибка: превышение лимита inode или максимального числа файлов в папке (зависит от файловой системы, например, ext4 - по умолчанию 64 000 записей на один каталог). Решение - использовать разбиение.

Вариант 2. Разбивка по остатку от деления на N

Вопрос: Как распределить папки по корзинам (buckets), чтобы уменьшить глубину одной директории?


$bucket = $userId % 1000;
$path = 'users/' . $bucket . '/' . $userId;
mkdir($path, 0755, true);
  

Php dir name (имя директории в php)

Цель: каждая корневая папка содержит не более 1000 подпапок. Недостаток: все ID с одинаковым остатком попадут в одну папку, что при большом количестве пользователей может привести к неравномерности.

Проблема: если количество пользователей превышает 1000×1000, корзины могут переполняться. Лучше использовать два уровня разбиения.

Вариант 3. Хэширование ID и разбиение по первым символам хэша

Вопрос: Как создать равномерное распределение папок без зависимости от закономерностей в ID?


$hash = md5($userId); // 32 символа
$level1 = substr($hash, 0, 2);
$level2 = substr($hash, 2, 2);
$path = 'storage/' . $level1 . '/' . $level2 . '/' . $userId;
mkdir($path, 0755, true);
  

Php get dir (получение директории в php)

Цель: равномерное распределение за счёт хэша, даже если ID последовательные. Проблема: длина пути увеличивается, возможны коллизии хэша (теоретически), но на практике для идентификаторов это несущественно.

Ошибка: использование md5 для больших объёмов может создать много лишних папок (256×256 = 65536 корневых папок). Необходимо оценить предполагаемое количество ID.

Вариант 4. Смешанный подход: ID + случайный суффикс

Вопрос: Как защитить каталоги от угадывания по ID?


$suffix = substr(uniqid(), -6);
$path = 'protected/' . $userId . '_' . $suffix;
mkdir($path, 0755);
// Сохранить соответствие $userId => $suffix в базе данных
  

Цель: предотвратить прямой доступ к файлам по известному ID (например, при атаке перебором). Недостаток - требуется хранение маппинга.

Проблема: если соль не хранится, восстановить соответствие невозможно. Также увеличивается сложность поиска по прямому пути.

Расширенные примеры организации каталогов по ID

Ниже приведены более сложные сценарии, которые могут потребоваться в реальных проектах.

Пример 1. Функция с настраиваемой глубиной и корзиной

Пример

function buildIdPath(int $id, int $baseDepth = 3, int $bucketSize = 100): string {
    $parts = [];
    $num = $id;
    for ($i = 0; $i < $baseDepth; $i++) {
        $parts[] = $num % $bucketSize;
        $num = intdiv($num, $bucketSize);
    }
    $parts[] = $id; // последний сегмент - сам ID
    return 'storage/' . implode('/', $parts);
}

// Использование:
$path = buildIdPath(123456, 2, 100);
// Результат: storage/56/34/123456
echo $path; 
  
storage/56/34/123456
  

Пояснение: глубина 2, каждый уровень делит ID на 100 (остаток). Функция гарантирует, что в каждой конечной папке лежит не более 100 подпапок, но при этом общая структура остаётся управляемой.

Пример 2. Создание каталога с проверкой прав и обработкой ошибок

Пример

function createSecureDirectory(int $id): ?string {
    $base = '/var/www/data';
    $hash = sha1($id);
    $sub1 = substr($hash, 0, 2);
    $sub2 = substr($hash, 2, 4);
    $full = $base . '/' . $sub1 . '/' . $sub2 . '/' . $id;
    
    if (is_dir($full)) {
        return $full;
    }
    
    // Попытка создания с проверкой ошибок
    if (!mkdir($full, 0700, true) && !is_dir($full)) {
        error_log("Не удалось создать каталог: $full");
        return null;
    }
    
    // Дополнительная проверка на запись
    if (!is_writable($full)) {
        chmod($full, 0700);
    }
    
    return $full;
}

$path = createSecureDirectory(202409);
if ($path) {
    echo "Каталог: $path";
} else {
    echo "Ошибка создания";
}
  
Каталог: /var/www/data/a1/bc3d/202409
  

Пояснение: используется SHA-1 для равномерного распределения. Проверка is_dir после mkdir защищает от race conditions. Права выставлены на 0700 (владелец). В реальном приложении логирование ошибок обязательно.

Пример 3. Рекурсивное удаление каталога по ID с подтверждением

Пример

function deleteUserDirectory(int $userId, string $basePath = 'uploads'): bool {
    $path = $basePath . '/' . floor($userId / 1000) . '/' . $userId;
    
    if (!is_dir($path)) {
        return false; // каталог не существует
    }
    
    // Удаляем содержимое рекурсивно
    $items = array_diff(scandir($path), ['.', '..']);
    foreach ($items as $item) {
        $itemPath = $path . '/' . $item;
        if (is_file($itemPath)) {
            unlink($itemPath);
        } elseif (is_dir($itemPath)) {
            deleteUserDirectory($userId . '_' . $item, $basePath); // рекурсия
        }
    }
    
    return rmdir($path);
}

// Пример вызова:
$result = deleteUserDirectory(12345);
var_dump($result);
  
bool(true)
  

Пояснение: функция удаляет каталог, созданный по схеме ID/1000. Внимание: рекурсия опасна при глубокой вложенности - лучше использовать итеративный подход (например, RecursiveDirectoryIterator).

Пример 4. Определение количества файлов в каждой корзине для мониторинга

Пример

function getBucketStats(string $rootDir = 'users'): array {
    $stats = [];
    $buckets = glob($rootDir . '/*', GLOB_ONLYDIR);
    foreach ($buckets as $bucket) {
        $count = count(glob($bucket . '/*', GLOB_ONLYDIR));
        $bucketName = basename($bucket);
        $stats[$bucketName] = $count;
    }
    return $stats;
}

print_r(getBucketStats());
  
Array
(
    [0] => 100
    [1] => 95
    [2] => 102
    ...
)
  

Пояснение: функция помогает выявить неравномерность распределения. Если одна корзина содержит значительно больше папок, стоит изменить алгоритм разбиения (например, увеличить число корзин).

Пример 5. Использование с базой данных для хранения соответствия

Пример

// Предполагаем таблицу user_storage (user_id, path_hash)
// Создание папки и запись в БД
$pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
$userId = 98765;
$salt = bin2hex(random_bytes(4));
$fullPath = 'uploads/' . $salt[0] . '/' . $salt[1] . '/' . $userId;

if (!is_dir($fullPath)) {
    mkdir($fullPath, 0755, true);
    $stmt = $pdo->prepare('INSERT INTO user_storage (user_id, path_hash) VALUES (?, ?)');
    $stmt->execute([$userId, $salt]);
}

// Получение пути по ID:
$stmt = $pdo->prepare('SELECT path_hash FROM user_storage WHERE user_id = ?');
$stmt->execute([$userId]);
$hash = $stmt->fetchColumn();
$actualPath = 'uploads/' . $hash[0] . '/' . $hash[1] . '/' . $userId;
echo $actualPath; // uploads/3/a/98765
  
uploads/3/a/98765
  

Пояснение: случайный соль (4 байта в hex) обеспечивает дополнительную защиту от угадывания. Путь генерируется только при первом обращении и сохраняется в БД. Проблема: при потере базы данных восстановить папки невозможно.

Каталоги по ID в PHP - comments

En
Dirs php id (php)