Генерация уникального имени файла в 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 простой причине :)
В одной папке есть лимит количества файлов, после которого просто система заглохнет читать папку, а удалять файлы в такой папке будет пыткой даже для СИшных приложений и будет очень дорогостоящей операцией для системы.
Дмитрий ЕлисеевУвы, но это не причина.
Ra – yabbarov.ruя делал так:
$DS=DIRECTORY_SEPARATOR; $date = new DateTime('now'); $path = Yii::app()->basePath.$DS.'..'.$DS.'uploads'.$DS.$date->format('Y').$DS.$date->format('m').$DS.$date->format('d'); if (is_dir($path)===false) { if (!mkdir($path,$this->permissions,$recursive=true)) {Не знаю, правильно, или нет, но так в одной папке много не скопится :)
И такой вопрос: а если надо сохранить имена файлов (ещё и на русском языке)? Сервер корёжит имена при загрузке.
Дмитрий ЕлисеевДа, кстати, лучше создавать папки по дате. Так многие CMS делают (тот же WordPress).
И касательно имён: можно хранить имя в ещё одной ячейке, а файлы отдавать через x-sendfile в Nginx с передачей оригинального имени.
Валерий – valion.uaЯ делал проще с использованием TIME():
if($_FILES){ $coun = count($_FILES[foto][name]); $time=time(); for($i=0;$i<=$coun;$i++){ $ext = strtolower(substr(strrchr($_FILES['foto']['name'][$i],'.'), 1)); $name_img = $time.$i.'.'.$ext; $path = $_SERVER[DOCUMENT_ROOT]."/tmp/".$name_img; copy($_FILES['foto']['tmp_name'][$i],$path) } }
AndreyСтатья вводит бедных юниоров в заблуждение. Для действительного случайного имени нужно использовать tempnam, а не указанный код.
Дмитрий ЕлисеевУвы, эта функция не припишет к имени файла расширение. После конкатенации расширения её также придётся оборачивать в этот же цикл do-while.
AndreyИ зачем ему расширение?
Очень прошу, поменяйте хотя бы для первых блоков (с uniqid и md5(microtime)) описание, допишите, что этот способ использовать _нельзя_. Ну открывают же и копируют, не глядя.
Дмитрий ЕлисеевА Вы изображения без расширений загружаете?
Максим ТимофеевВ Yii2 есть методы:
Для тех кто не любит MD5.
AndreyПосмотрел на код нашего юниора, который использует эту замечательную статью, вскрылся очередной косяк:
// Генерируем уникальное имя файла с этим расширением
$filename = DFileHelper::getRandomFileName($path, $extension);
// ВОТ ТУТ ДРУГОЙ ПРОЦЕСС СОЗДАЕТ ФАЙЛ С ТАКИМ ЖЕ ИМЕНЕМ
// Собираем адрес файла назначения
$target = $path . '/' . $filename . '.' . $extension;
// Загружаем
move_uploaded_file($_FILES['image']['tmp_name'], $target);
// ОПА - А ТАКОЙ ФАЙЛ УЖЕ ЕСТЬ
Слушайте, ну уберите статью, ну пожалуйста. Она не имеет абсолютно никакого смысла и только запутывает людей. Пожалуйста, посмотрите в исходниках PHP, как работает tempnam (подсказка: через mkstemp), посмотрите, что такое mkstemp и как она отличается от mktemp. В качестве бонуса попробуйте осознать понятия "атомарность" и "параллельное выполнение".
Ваша статья в гугле висит на первых местах и несёт страдание в этот мир.
Дмитрий ЕлисеевДа запросто. Если подскажете, как сгенерировать случайное имя с расширением – удалю.
AndreyДа хотя бы переработайте свой же код, чтобы он не просто пытался сгенерировать случайное имя файла, а пытался _создать_ файл со случайным именем до тех пор, пока у него это не получится, будет значительно лучше.
В простой генерации случайного несуществующего имени практического смысла мало - кроме гарантии, что _на момент_ генерации имени такого файла не существовало, больше никаких гарантий нет. А это достаточно бестолково :)
Дмитрий ЕлисеевПереработал ещё позавчера.
Виктор – brainforce.kiev.uaИсправьте вместо
do { $name = md5(microtime() . rand(0, 9999)); $file = $path . $name . $extension; } while (file_exists($file));нужно
do { $name = md5(microtime() . rand(0, 9999)); $file = $path . $name . $extension; } while ( ! file_exists($file));Иначе с цикла мы выйдем когда найдем файл с таким же именем? что нам как раз не нужно
Дмитрий ЕлисеевНет. Наоборот. Это цикл while, а не until.
Виктор – brainforce.kiev.uaДа, ошибся, сорри
ИгорьС интересом читаю все ваши статьи, но относительного правильности способа генерации уникального имени файла, который изложен в этом материале, возникают сомнения.
Все, что делает статический метод DFileHelper::getRandomFileName() - это генерирует уникальное (на момент генерации) имя файла в рамках заданной директории.
Однако, представим, что с приложением параллельно работают несколько клиентов (клиент1 и клиент2):
Клиент1 вызывает метод getRandomFileName() - генерируется уникальное имя;
Клиент2 вызывает этот же метод с некоторым запозданием, но еще до вызова Клиентом1 метода, который запишет файл в директорию.
В этой ситуации может произойти такое, что и для Клиента1, и для Клиента2 будет создано одно и тоже "уникальное" имя, но Клиент2 не узнает об этом, поскольку Клиент1 еще не произвел запись файла в каталог, а ведь лишь после этого метод file_exists() сможет определить, что файл с таким именем уже существует в директории.
Считаю, что после генерации уникального имени необходимо сразу же записывать файл в директорию (пустой) и после этого возвращать, действительно уникальное имя клиенту.
Дмитрий ЕлисеевСейчаc от этого спасает только rand(0, 9999) в имени, что даёт крайне малую верятность этого события:
Можно повысить верхний предел до миллиона, что снизит возможность совпадения до одной миллионной.
Но всё равно создание пустого файла не спасёт, так как это «сразу же» тоже не будет мгновенным.
ИгорьА разве так не надежнее будет?
alexsandrчтобы исключить возможность ошибки надо просто содать отдельную дирректорию для файлов пользователя с именем ID
СергейЯ пользуюсь алгоритмом генерации файлов как у автора, только не проверяю директорию на наличие такого файла уже, какова вероятность что два файла записанные в разное время будут иметь одинаковый хеш?, плюс ко всему у меня у каждого юзера своя папка с файлами, название которой соответствуют id юзера, который уникальный в системе...
Вопрос, какова вероятность перезаписи файла в моем случаи, я хеш не обрезаю вообще
ИгорьЗначит в Вашем случае способ, предложенный автором, используется просто как генератор имени файла.
Если Вы не проверяете директорию на наличие в ней файла с именем, которое было сгенерировано для записи нового файла - вероятность перезаписи существует, как если бы файлы записывались "в разное время", так и одновременно, т.к. в md5 могут быть коллизии.
АлексейХорошая информация
RusAlexПочему не использовать tempnam() ?
Дмитрий ЕлисеевУже отвечал выше. А именно из-за невозможности работать с расширениями файлов.
Ra – yabbarov.ruМожет так?
<?php const DS = DIRECTORY_SEPARATOR; class FileHelper { /** * Creates file with given path and extension, random name (using unigid()) * @param string $path * @param string $ext * @return string $name * @throws CHttpException if file not created */ public static function getUniqueFileName($path = '', $ext = '') { $attempts = 238328; // 62 x 62 x 62 - глупо но не знаю как лучше $path = $path ? $path . DS : ''; $ext = $ext ? '.' . $ext : ''; for( $count = 0; $count < $attempts; ++$count) { $filename = uniqid().$ext; $fullfilename = $path . DS . $filename; if( !($fd = @fopen($fullfilename, "x+")) ) continue; fclose($fd); return $filename; } throw new CHttpException(500,'Could not create file '.$fullfilename); } } class Article extends CActiveRecord { public $permissions=0755; /** * Renames uploaded file, creates folder for it, and saves it there * @return boolean * @throws CHttpException if folder not created */ protected function beforeSave() { if (parent::beforeSave()) { if ($file = CUploadedFile::getInstance($this, 'image')) { $date = new DateTime('now'); //как бы это укоротить :) и тут webroot не прокатывает под виндой $path = Yii::getPathOfAlias('application').DS.'..'.DS.'media'.DS.'uploads'.DS.'news'. $date->format('Y').DS.$date->format('m').DS.$date->format('d'); if (is_dir($path)===false) { if (!mkdir($path,$this->permissions,$recursive=true)) { $ext = strtolower($file->extensionName); $filename = FileHelper::getUniqueFileName($path, $ext); if ($file->saveAs($filename)) // здесь тоже захардкожено, исправить $model->image = '/media/uploads/news/'.$date->format('Y').'/'. $date->format('m').'/'.$date->format('d').'/'.$filename; } else throw new CHttpException(500, 'Could not create folder '.$path); } } return true; } else return false; } }
ДимаОтличная статья! Краткость сестра таланта!
НикитаПредлагаю использовать такую функцию $filename = md5(microtime() . rand(0, 9999)); Только еще дописывать идентификатор пользователя - он уж точно будет уникальным. Если один и тот же пользователь добавляет одновременно много файлов в цикле просто еще крутить счетчик и будет что то типа $filename = md5(microtime() . rand(0, 9999)) . $_SESSION['userId'].$i; $i++;
Дмитрий Елисеев:)
Влад – infoblog1.ruВот тут можно проверить правильность работы скрипта
Генератор
Виктор ТуляковПриветствую Коллеги!
~~~
Ищу способ задания уникального имени файла, но мне необходимо учесть еще один параметр.
На сайте хранится около 1000-3000 фотографий, фотки на сайт может загружать только один человек (Администратор сайта). В момент загрузки Я понял, задам уникальное имя файла, а что делать если Администратор сайта случайно захочет загрузить фотографию, которую он уже загружал месяц назад?
В связи с этим вот какой вопрос! Можно ли определить уникальное имя загружаемому файлу не по случайному значению, например времени, а по, например, внутренней хеш-сумме изображения? Чтобы при повторной загрузке именно этого изобращения система выдавала сообщение "Такой файл уже существует!".
Дмитрий ЕлисеевДа, можно сгенерировать имя через md5_file:
а потом при наличии такого файла по результату file_exists($file) выдавать это сообщение.
Виктор ТуляковБлагодарю Уважаемый!
Бауржан АТиновспасибо, работает
EvgenyНа 2х разных проектах нашел одинаковый код, написанный разными разработчиками, удивился, погуглил, оказывается они содрали этот треш отсюда. Пожалуйста не пишите больше, джуниоры еще глупые - тянут всякую каку в руки.
Дмитрий ЕлисеевНапишите в комментарии правильное решение. Добавлю в статью.
Максим ТимофеевМожно на Ваше элегантное решение глянуть?
thouя просто добавляю в конце имени файла цифру: _1, _2, _3 ...
$uploaddir = '../files/'; $uploadfile = $uploaddir. $name. "." . $type; $i = 0; if (file_exists($uploadfile)) { while(file_exists($uploaddir. $name. '_'. $i. '.' . $type)) { ++$i; } $uploadfile = $uploaddir. $name. '_'. $i. '.'. $type; }
АлексейЗдравствуйте.
Подскажите пожалуйста, зачем генерировать имя файла использую хеш, кроме причины, чтобы файл не совпал с уже загруженным? Есть ли другие причины?
Дмитрий ЕлисеевЧтобы не было мучений с кириллицей в имени файла, например.
Максим Тимофеевназвание получается правильным несмотря ни на что. Есть файлы с точками в имени. Есть с кириллицей. Это не единственный метод, но самый простой.
Aleksmd5_file() Вам в помощь! )
Дмитрий ЕлисеевДва пользователя загружают одинаковый файл... и второй пользователь свой файл удаляет. И у первого файл тоже пропадает.
марат – icomans.workя сделал так (файловый обменник) :
// Максимально допустимый размер загружаемого файла - 500Мб $MaxFileSizeInBytes = 5242880000; // Разрешение расширения файлов для загрузки $AllowFileExtension = array('jpg', 'png', 'jpeg', 'gif', 'rar', 'zip', 'doc', 'pdf', 'djvu','ico','exe','pkg','app'); // Оригинальное название файла $FileName = $_FILES['uploaded_file']['name']; // Полный путь до временного файла $TempName = $_FILES['uploaded_file']['tmp_name']; // Папка где будут загружатся файлы $UploadDir = "uploads/"; // Полный путь к новому файлу в папке сервера $NewFilePatch = $UploadDir.uniqid();; if($FileName) { // Проверка если расширение файла находится в массиве доступных $FileExtension = pathinfo($FileName, PATHINFO_EXTENSION); if(!in_array($FileExtension, $AllowFileExtension)) { echo "Файлы с расширением {$FileExtension} не допускаются"; } else { // Проверка размера файла if(filesize($TempName) > $MaxFileSizeInBytes) { echo "Размер загружаемого файла превышает 5МБ"; } else { // Проверяем права доступа на папку if(!is_writable($UploadDir)) { echo "Папка ".$UploadDir." не имеет прав на запись"; } else { // Копируем содержимое временного файла $TempName и создаем нового в папке сервера $CopyFile = copy($TempName, $NewFilePatch); if(!$CopyFile) { echo "Возникла ошибка, файл не удалось загрузить!"; } else { echo "Файл успешно загружен!<br>Ссылка на файл: <a href='{$NewFilePatch}'>{$NewFilePatch}</a>"; } } } } }
СашкаДобрый день Дмитрий!
У Вас в коде:
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
Спасибо!
Дениссам нашел, пригодится кому-нибудь кто не знает английский например
function wp_modify_uploaded_file_names($file) { $info = pathinfo($file['name']); $ext = empty($info['extension']) ? '' : '.' . $info['extension']; $name = basename($file['name'], $ext); //$file['name'] = uniqid() . $ext; // uniqid method $file['name'] = md5($name) . $ext; // md5 method // $file['name'] = base64_encode($name) . $ext; // base64 method return $file; } add_filter('wp_handle_upload_prefilter', 'wp_modify_uploaded_file_names', 1, 1);
ПавелДенис, в коде две строки лишние, ибо //. И может быть такой код будет удобнее для WP пользователей:
function wp_modify_uploaded_file_names($file) { $info = pathinfo($file['name']); $ext = empty($info['extension']) ? '' : '.' . $info['extension']; $name = basename($file['name'], $ext); $usrnm = get_current_user_id(); $file['name'] = base_convert(uniqid($usrnm, true),16,36) . $ext; return $file; }Используем uniqid с префиксом на основе ID пользователя и добавляем "энтропию".
Алексей – irogex.ruА мне так функция на 5 с +++, добавил свои плюшки, вообщем спасибо..
МихаилДоброго времени!
Мне нужна строковая функция чтобы в строке:
1. пробел(ов) заменить одним знаком "-"
2. изменить регистр в нижний,
3. удалить все знаки кроме букв и цифр.
4. и заменить все буквы на английский
Если можете помогите кто нибудь пожалуйста.
Дмитрий ЕлисеевПри наличии php_intl:
function slugify($string) { $string = transliterator_transliterate("Any-Latin; NFD; [:Nonspacing Mark:] Remove; NFC; [:Punctuation:] Remove; Lower();", $string); $string = preg_replace('/[-\s]+/', '-', $string); return trim($string, '-'); }Иначе вручную по вашим действиям:
function slugify($string) { $string = preg_replace('#\s+#is', '-', $string); $string = mb_strtolower($string, 'utf-8'); $string = preg_replace('#[^\w_]+#uis', '', $string); $string = strtr($string, [ 'а' => 'a', 'б' => 'b', ... ]); return trim($string, '-'); }
МихаилСпасибо за поддержку!
Я понятие не имею что значит При наличии php_intl:
Наверное мне подойдет 2-ой вариант.
Нужно чтобы переформатированный текст стал названием изображении.
Попробую, о результате сообщу вам.
Спасибо еще раз.
МихаилПробовал добавить функцию
function slugify($string) { $string = preg_replace('#\s+#is', '-', $string); $string = mb_strtolower($string, 'utf-8'); $string = preg_replace('#[^\w_]+#uis', '', $string); $string = strtr($string, [ 'а' => 'a', 'б' => 'b', ... ]); return trim($string, '-'); }выдал ошибку:
Так как у меня отсутствует PHP знание, я не в силе исправиться с этими ошибками.
Посмотрите пожалуйста может что не дописали.
Дмитрий ЕлисеевУ вас PHP ранней версии. Используйте array(...) для массивов вместо [...]:
$string = strtr($string, array( 'а' => 'a', 'б' => 'b', ... ));
МихаилСпасибо большое, попробую.
АлексейЕсли достаточно латинизировать, а не транскрибировать на английский, тогда проще:
СаняА у меня вопрос такой:
$filename = DFileHelper::getRandomFileName(self::IMAGE_PATH, $extension);
Мне рандом не упал, могу ли я просто вписать имя файла?
То, есть у меня в папке только один должен хранится и заменятся
PHPDevil – webwizardry.ruА файл вы грузите к чему? В поле модели?
Так пофиг как называется - пихаем название файла в поле, а самому имени файла приписываем id этой же записи... ну и как бы решена проблема
Дмитрий ЕлисеевЕсли id автоинкрементный, то он неизвестен перед сохранением.
PHPDevil – webwizardry.ruЕсли это форма для юзера - то исходя из что-там происходит обрабатываем фал сразу из temp
ArmanОх как не люблю такие условия для циклов =) Какие гарантии что не уйдет в цикл завтра? Допустим джуниор поменяет реализацию генерации и на деле выйдет несколько возможных вариантов имени. Всегда стараюсь делать через for с конечным количеством итераций, а дальше кидаю исключение.