Генерация уникального имени файла в PHP проектах
C самых первых проектов, поддерживающих загрузку файлов на сервер, любой программист сталкивается с необходимостью генерации уникальных случайных имён для загруженных файлов. Рассмотрим несколько вариантов решения данной проблемы.
Предположим, мы должны сохранять все файлы в одну папку. Чтобы файлы не повторялись и чтобы не возникало проблем с кириллицей нам лучше давать им уникальные имена вида
f1ga2343h4bc3 537f2ha8b321a dh420h3aac370
или
f1ga2343h4bc3534fa.jpg 537f2ha8b321adf676.jpg dh420h3aac370aac09.gif
Самый лёгкий вариант – это использование встроенной в PHP функции uniqid():
$filename = uniqid();
Эта функция вернёт случайную 13-символьную строку. Если же нужно делать имена длиннее, то можно использовать функции вычисления хэша от случайной строки:
$filename = md5(microtime() . rand(0, 9999));
Функция md5()
генерирует 32-символьную строку. Можно, конечно использовать любую другую функцю. При желании можно установить любую длину от 1 до 32 обрезав md5-хэш функцией substr()
:
$filename = substr(md5(microtime() . rand(0, 9999)), 0, 20);
Если нужно хранить файл с расширением, то его расширение можно легко приписывать к идентификатору:
$extension = 'jpg'; $filename = uniqid() . '.' . $extension;
Это простые способы, но у них есть один недостаток: уникальность имени не контролируется, а следовательно имеется вероятность перезаписи старого файла при случайной генерации такого же имени для нового файла. И эта вероятность тем выше, чем больше файлов сохранено.
Чтобы избежать перезаписи нам необходимо проверять папку на отсутствие такого же файла. Напишем функцию, избавленную от этого недостатка:
/** * @author ElisDN <mail@elisdn.ru> * @link https://elisdn.ru */ class DFileHelper { public static function getRandomFileName($path, $extension='') { $extension = $extension ? '.' . $extension : ''; $path = $path ? $path . '/' : ''; do { $name = md5(microtime() . rand(0, 9999)); $file = $path . $name . $extension; } while (file_exists($file)); return $name; } }
Здесь в постусловии цикла осуществляется проверка на существование файла. Если файл с таким именем уже есть, то генерируется следующее имя. Вместо md5(microtime() . rand(0, 9999))
для генерации идентификатора можно использовать любой вариант из разобранных выше.
Рассмотрим пример загрузки изображения с использованием данной функции:
$path = 'upload/images'; // Получаем расширение загруженного файла $extension = strtolower(substr(strrchr($_FILES['image']['name'], '.'), 1)); // Генерируем уникальное имя файла с этим расширением $filename = DFileHelper::getRandomFileName($path, $extension); // Собираем адрес файла назначения $target = $path . '/' . $filename . '.' . $extension; // Загружаем move_uploaded_file($_FILES['image']['tmp_name'], $target);
В Yii для получения расширения удобно пользоваться объектом $file
класса CUploadedFile
:
class Post extends CActiveRecord { const IMAGE_PATH = 'upload/images'; protected function beforeSave() { if (parent::beforeSave()) { if ($file = CUploadedFile::getInstance($this, 'image')) { $extension = strtolower($file->extensionName); $filename = DFileHelper::getRandomFileName(self::IMAGE_PATH, $extension); $basename = $filename . '.' . $extension; if ($file->saveAs(self::IMAGE_PATH . '/' . $basename)) $model->image = $basename; } return true; } else return false; } }
С помощью функции DFileHelper::getRandomFileName()
мы генерируем уникальное имя файла для папки upload/images
и используем это имя для загрузки. Теперь файлы никогда не перезапишутся, так как имена никогда не совпадут.
С микротаймом и мд5, коллизии никогда не будет по 1 простой причине :)
В одной папке есть лимит количества файлов, после которого просто система заглохнет читать папку, а удалять файлы в такой папке будет пыткой даже для СИшных приложений и будет очень дорогостоящей операцией для системы.
Увы, но это не причина.
я делал так:
Не знаю, правильно, или нет, но так в одной папке много не скопится :)
И такой вопрос: а если надо сохранить имена файлов (ещё и на русском языке)? Сервер корёжит имена при загрузке.
Да, кстати, лучше создавать папки по дате. Так многие CMS делают (тот же WordPress).
И касательно имён: можно хранить имя в ещё одной ячейке, а файлы отдавать через x-sendfile в Nginx с передачей оригинального имени.
Я делал проще с использованием TIME():
Статья вводит бедных юниоров в заблуждение. Для действительного случайного имени нужно использовать tempnam, а не указанный код.
Увы, эта функция не припишет к имени файла расширение. После конкатенации расширения её также придётся оборачивать в этот же цикл do-while.
И зачем ему расширение?
Очень прошу, поменяйте хотя бы для первых блоков (с uniqid и md5(microtime)) описание, допишите, что этот способ использовать _нельзя_. Ну открывают же и копируют, не глядя.
А Вы изображения без расширений загружаете?
В Yii2 есть методы:
Для тех кто не любит MD5.
Посмотрел на код нашего юниора, который использует эту замечательную статью, вскрылся очередной косяк:
// Генерируем уникальное имя файла с этим расширением
$filename = DFileHelper::getRandomFileName($path, $extension);
// ВОТ ТУТ ДРУГОЙ ПРОЦЕСС СОЗДАЕТ ФАЙЛ С ТАКИМ ЖЕ ИМЕНЕМ
// Собираем адрес файла назначения
$target = $path . '/' . $filename . '.' . $extension;
// Загружаем
move_uploaded_file($_FILES['image']['tmp_name'], $target);
// ОПА - А ТАКОЙ ФАЙЛ УЖЕ ЕСТЬ
Слушайте, ну уберите статью, ну пожалуйста. Она не имеет абсолютно никакого смысла и только запутывает людей. Пожалуйста, посмотрите в исходниках PHP, как работает tempnam (подсказка: через mkstemp), посмотрите, что такое mkstemp и как она отличается от mktemp. В качестве бонуса попробуйте осознать понятия "атомарность" и "параллельное выполнение".
Ваша статья в гугле висит на первых местах и несёт страдание в этот мир.
Да запросто. Если подскажете, как сгенерировать случайное имя с расширением – удалю.
Да хотя бы переработайте свой же код, чтобы он не просто пытался сгенерировать случайное имя файла, а пытался _создать_ файл со случайным именем до тех пор, пока у него это не получится, будет значительно лучше.
В простой генерации случайного несуществующего имени практического смысла мало - кроме гарантии, что _на момент_ генерации имени такого файла не существовало, больше никаких гарантий нет. А это достаточно бестолково :)
Переработал ещё позавчера.
Исправьте вместо
нужно
Иначе с цикла мы выйдем когда найдем файл с таким же именем? что нам как раз не нужно
Нет. Наоборот. Это цикл while, а не until.
Да, ошибся, сорри
С интересом читаю все ваши статьи, но относительного правильности способа генерации уникального имени файла, который изложен в этом материале, возникают сомнения.
Все, что делает статический метод DFileHelper::getRandomFileName() - это генерирует уникальное (на момент генерации) имя файла в рамках заданной директории.
Однако, представим, что с приложением параллельно работают несколько клиентов (клиент1 и клиент2):
Клиент1 вызывает метод getRandomFileName() - генерируется уникальное имя;
Клиент2 вызывает этот же метод с некоторым запозданием, но еще до вызова Клиентом1 метода, который запишет файл в директорию.
В этой ситуации может произойти такое, что и для Клиента1, и для Клиента2 будет создано одно и тоже "уникальное" имя, но Клиент2 не узнает об этом, поскольку Клиент1 еще не произвел запись файла в каталог, а ведь лишь после этого метод file_exists() сможет определить, что файл с таким именем уже существует в директории.
Считаю, что после генерации уникального имени необходимо сразу же записывать файл в директорию (пустой) и после этого возвращать, действительно уникальное имя клиенту.
Сейчаc от этого спасает только rand(0, 9999) в имени, что даёт крайне малую верятность этого события:
Можно повысить верхний предел до миллиона, что снизит возможность совпадения до одной миллионной.
Но всё равно создание пустого файла не спасёт, так как это «сразу же» тоже не будет мгновенным.
А разве так не надежнее будет?
чтобы исключить возможность ошибки надо просто содать отдельную дирректорию для файлов пользователя с именем ID
Я пользуюсь алгоритмом генерации файлов как у автора, только не проверяю директорию на наличие такого файла уже, какова вероятность что два файла записанные в разное время будут иметь одинаковый хеш?, плюс ко всему у меня у каждого юзера своя папка с файлами, название которой соответствуют id юзера, который уникальный в системе...
Вопрос, какова вероятность перезаписи файла в моем случаи, я хеш не обрезаю вообще
Значит в Вашем случае способ, предложенный автором, используется просто как генератор имени файла.
Если Вы не проверяете директорию на наличие в ней файла с именем, которое было сгенерировано для записи нового файла - вероятность перезаписи существует, как если бы файлы записывались "в разное время", так и одновременно, т.к. в md5 могут быть коллизии.
Хорошая информация
Почему не использовать tempnam() ?
Уже отвечал выше. А именно из-за невозможности работать с расширениями файлов.
Может так?
Отличная статья! Краткость сестра таланта!
Предлагаю использовать такую функцию $filename = md5(microtime() . rand(0, 9999)); Только еще дописывать идентификатор пользователя - он уж точно будет уникальным. Если один и тот же пользователь добавляет одновременно много файлов в цикле просто еще крутить счетчик и будет что то типа $filename = md5(microtime() . rand(0, 9999)) . $_SESSION['userId'].$i; $i++;
:)
Вот тут можно проверить правильность работы скрипта
Генератор
Приветствую Коллеги!
~~~
Ищу способ задания уникального имени файла, но мне необходимо учесть еще один параметр.
На сайте хранится около 1000-3000 фотографий, фотки на сайт может загружать только один человек (Администратор сайта). В момент загрузки Я понял, задам уникальное имя файла, а что делать если Администратор сайта случайно захочет загрузить фотографию, которую он уже загружал месяц назад?
В связи с этим вот какой вопрос! Можно ли определить уникальное имя загружаемому файлу не по случайному значению, например времени, а по, например, внутренней хеш-сумме изображения? Чтобы при повторной загрузке именно этого изобращения система выдавала сообщение "Такой файл уже существует!".
Да, можно сгенерировать имя через md5_file:
а потом при наличии такого файла по результату file_exists($file) выдавать это сообщение.
Благодарю Уважаемый!
спасибо, работает
На 2х разных проектах нашел одинаковый код, написанный разными разработчиками, удивился, погуглил, оказывается они содрали этот треш отсюда. Пожалуйста не пишите больше, джуниоры еще глупые - тянут всякую каку в руки.
Напишите в комментарии правильное решение. Добавлю в статью.
Можно на Ваше элегантное решение глянуть?
я просто добавляю в конце имени файла цифру: _1, _2, _3 ...
Здравствуйте.
Подскажите пожалуйста, зачем генерировать имя файла использую хеш, кроме причины, чтобы файл не совпал с уже загруженным? Есть ли другие причины?
Чтобы не было мучений с кириллицей в имени файла, например.
название получается правильным несмотря ни на что. Есть файлы с точками в имени. Есть с кириллицей. Это не единственный метод, но самый простой.
md5_file() Вам в помощь! )
Два пользователя загружают одинаковый файл... и второй пользователь свой файл удаляет. И у первого файл тоже пропадает.
я сделал так (файловый обменник) :
Добрый день Дмитрий!
У Вас в коде:
strtolower(substr(strrchr($_FILES['image']['name'], '.'), 1));
- А почему не используете pathinfo?
pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION)
Спасибо.
С ув. Ваш периодически-постоянный читатель!
Три года назад мне было всё равно :)
- Ну поправьте может)
Все таки репутация же у Вас то есть сейчас!
Статья обсуждаема, а значит нужна. В каком бы виде она не была. Иногда достаточно натолкнуть на мысль, а не дать готовое решение.
Кодеры кодеры
а для обычного человека есть код или плагин под ворпресс чтобы не вникать в смыслы создания вселенной?
нужен плгин, или код для functioins php
с учетом что фотки загружаются сюда
uploads/images
с учетом что стоит галочка, Organise my uploads into month- and year-based folders
Спасибо!
сам нашел, пригодится кому-нибудь кто не знает английский например
Денис, в коде две строки лишние, ибо //. И может быть такой код будет удобнее для WP пользователей:
Используем uniqid с префиксом на основе ID пользователя и добавляем "энтропию".
А мне так функция на 5 с +++, добавил свои плюшки, вообщем спасибо..
Доброго времени!
Мне нужна строковая функция чтобы в строке:
1. пробел(ов) заменить одним знаком "-"
2. изменить регистр в нижний,
3. удалить все знаки кроме букв и цифр.
4. и заменить все буквы на английский
Если можете помогите кто нибудь пожалуйста.
При наличии php_intl:
Иначе вручную по вашим действиям:
Спасибо за поддержку!
Я понятие не имею что значит При наличии php_intl:
Наверное мне подойдет 2-ой вариант.
Нужно чтобы переформатированный текст стал названием изображении.
Попробую, о результате сообщу вам.
Спасибо еще раз.
Пробовал добавить функцию
выдал ошибку:
Так как у меня отсутствует PHP знание, я не в силе исправиться с этими ошибками.
Посмотрите пожалуйста может что не дописали.
У вас PHP ранней версии. Используйте array(...) для массивов вместо [...]:
Спасибо большое, попробую.
Если достаточно латинизировать, а не транскрибировать на английский, тогда проще:
А у меня вопрос такой:
$filename = DFileHelper::getRandomFileName(self::IMAGE_PATH, $extension);
Мне рандом не упал, могу ли я просто вписать имя файла?
То, есть у меня в папке только один должен хранится и заменятся
А файл вы грузите к чему? В поле модели?
Так пофиг как называется - пихаем название файла в поле, а самому имени файла приписываем id этой же записи... ну и как бы решена проблема
Если id автоинкрементный, то он неизвестен перед сохранением.
Если это форма для юзера - то исходя из что-там происходит обрабатываем фал сразу из temp
Ох как не люблю такие условия для циклов =) Какие гарантии что не уйдет в цикл завтра? Допустим джуниор поменяет реализацию генерации и на деле выйдет несколько возможных вариантов имени. Всегда стараюсь делать через for с конечным количеством итераций, а дальше кидаю исключение.