Ajax загрузка нескольких файлов на сервер

Ajax загрузка нескольких файлов на сервер

В этой заметке хочу осветить интересный способ загрузки файлов на сервер. Срипт, который получится полностью рабочий и после некоторой адаптации может быть использован вами, в ваших проектах, если конечно будет вам интересен.

Да этот способ можно назвать "AJAX загрузка файлов на сервер", хотя он и не использует ни ActiveXObject ни XMLHttpRequest, тем не менее это то, что в последствии превратилось в AJAX. Вообще, здесь я раскрою несколько трюков разработки, которые будут довольно неплохой находкой для начинающих.

Во первых метод использует динамически добавляемые/удаляемые поля загрузки файлов ( листинг JavaScript прилагается ) - т.е. в данной заметке рассматривается загрузка и обработка неопределённого множества файлов на сервер, и как всегда я стараюсь писать как можно лаконичнее и понятнее.

Во вторых, конечно всё будет происходить, именно без перезагрузки страницы.

В третьих будет дан серверный скрипт, который принимает данные, выводит информацию, и обрабатывает ошибки загрузки файлов

И в четвёртых, в конце загрузки файлов, пользователю будет выведена информация о том, какие файлы загрузились, а какие нет, и почему. В общем ценная штука в арсенале разработчика.

Как мы уже знаем, при написании html форм атрибуты "name" - элементов формы, впоследствии являются ключами глобальных массивов: $_REQUEST $_GET или $_POST и если в форме будут два поля с одинаковым атрибутом "name" то значение последнего поля "затрёт" значения предыдущих в глобальном массиве. Т.е. если у нас есть следующее:

  <form action="" method="get">
    <input id="one" name="upfile" type="file" />
    <input id="two" name="upfile" type="file" />
  </form>

То в обрабатывающем скрипте ма увидим массив $_FILES, котором будет только один ключ "upfile", а значение первого поля с id="one" - затрётся.

Решается такая проблема банально просто: даём разные имена атрибутам "name" и "id". Но! Что делать, если поля для загрузки файлов должны добавляться динамически, и их количество заранее не известно? Как правило программисты предпочитают генерировать подобные вещи в цикле, добавляя к атрибуту "name" некий числовой суффикс, добиваясь тем самым уникальности атрибута "name", а атрибут id за редким исключением почти всегда можно опустить ( Вспоминаем методы DOM для доступа к элементам ) Серверный скрипт генерации формы может иметь следующий вид:

<form action="" method="get">

		<?php for($i = 0; $i < 50; $i++ ) : ?>        
			<p><input name="upfile_<?php echo $i ?>" type="file" /> Файл № <?php echo $i + 1 ?></p>
		<?php endfor ?>
    
        --- Здесь ещё какой ли бо код ---
        
</form>

Код выше сгенерит нам 50 полей для загрузки файлов и у каждого элемента атрибут "name" будет иметь уникальный суффикс: _n - номер итерации цикла. Получим мы следующий html код:

  <form action="" method="get">
    <p><input name="upfile_0" type="file" /> Файл № 1 </p>
    ....
    <p><input name="upfile_49" type="file" /> Файл № 50 </p>
  </form>

Теперь все эти поля благополучно свалятся на сервер, и всем будет хорошо... но не очень. Здесь нет динамического добавления/удаления полей загрузки файлов... а так хотелось.

Альтернативный вариант решения этой задачи в том, что бы не использовать генерацию формы на сервере, а озадачить этим браузер клиента. И что бы избежать лишнего цикла при создании полей мы не будем добавлять индекс к атрибуту "name", а сделаем этот тип данных не строкой, как по - умолчанию, а массивом. Да - да вспоминаем, что передевать методами GET и POST мы можем и массивы! Теперь атрибут "name" имеет следующий вид : name="file[]" (Название file - здесь я придумал сам ну, мы же файлы собрались передавать.) То есть тут нам нужен - его величество JavaScript (можно конечно и VBScript, и много чего ещё, но я знаю только JavaScript) Вот пример скрипта. Помещаем его куда угодно на странице ( Товарищи малознакомые с JS - не пугаемся там как всегда больше комментариев)

Скрипт для динамичной html формы

<script type="text/javascript">

// Объект "нэймспейса"
var uploader = uploader || {};

// Функция "Удалить поле"
uploader.delfield  = function (obj) {	
	obj.onclick = null;
	obj.parentNode.parentNode.removeChild(obj.parentNode);	
}

// Функция "Добавить поле"
uploader.addfield  = function (GLOB) {	
	/* 
	 * Здесь код формирует очередное поле загрузки файла,
	 * т.е. html код элементов, эквивалентный следующему:
	 * 	 
	 * <p>
	 *		<input name="file[]" type="file" size="30" />
	 *  	<button type="button" onclick="uploader.delfield(this)">DEL</button>
	 * </p>
	 * 
	 * Только обработчик onclick назначиться чуть по - другому.
	 */ 	
  var DOC            = GLOB.document,
      wrapper        = DOC.getElementById("filewrapper"),
      htmlP          = DOC.createElement("P"),
      htmlInput      = DOC.createElement("INPUT"),
      htmlButton     = DOC.createElement("BUTTON"),
      htmlButtonText = DOC.createTextNode("DEL");
			
  htmlInput.name     = "file[]";
  htmlInput.type     = "file";
  htmlInput.size     = "30";
	
  htmlButton.onclick = function() { uploader.delfield(htmlButton) };	
	
  // Добавляем всё это хозяйство в DOM дерево документа:
  wrapper.appendChild(htmlP);
  htmlP.appendChild(htmlInput);
  htmlP.appendChild(htmlButton);
  htmlButton.appendChild(htmlButtonText);	
}
</script>

А вот структура самой html формы без дополнительных телодвижений на сервере:

<form action="upserver.php" method="POST" target="uploadresult" name="upfiles" enctype="multipart/form-data">
  <!-- 
       Цель бытиЯ этого дива здесь - это то, что мы имеем 
       общий контейнер, для всех динамически создаваемых полей.
       Так ими удобнее управлять.
  //-->
  <div id="filewrapper">
  
    <p><input name="file[]" type="file" size="30" /><button type="button" onclick="uploader.delfield(this)">DEL</button></p>
    
  </div>
  
  <p>
    <button type="button" onclick="uploader.addfield(window)">Добавить поле</button>
    <button type="submit" name="send" value="true">Отправить</button>
  </p>  
  
</form>
<!-- В этом фрейме будем выводить данные о загрузке файлов //-->
<iframe name="uploadresult" frameborder="0" id="uploadresult" width="600" height="400"></iframe>

Не забываем указывать enctype="multipart/form-data" для формы!

А то потом долго будем гадать почему массив $_FILES пустой!

Здесь мы видим интересную "фишку" : если атрибуту target - формы передать имя фрейма (который идет ниже по коду), то при отправке формы перезагрузки страницы не произойдёт! Но во фрейме мы увидим результат работы серверного скрипта! Вспоминаем, что "есть такое" атрибут target:

Это имя окна или фрейма, куда обработчик события onsubmit будет загружать возвращаемый результат.

Собственно этот интересный эффект мы и пронаблюдаем с вами в конце. Ну, а с интерфейсом формы надеюсь всё понятно - можно добавить поле загрузки файлов, а можно удалить... итак до бесконечности...

Имейте ввиду!

Если где то в html коде формы, или фрейма будут синтаксические или другие ошибки IE - вам не простит! Он будет окрывать новое окно для отображения результатов загрузки или будет чудить иным образом. Если вы наблюдаете эту ситуацию внимательно проверьте html код клиентской части.

Настало время серверной стороны дела. Если мы к примеру загрузили 3 файла, то на сервере, при трассировке массива $_FILES мы увидим следующую картину:

Array
(
    [file] => Array
        (
            [name] => Array
                (
                    [0] => cms.edx
                    [1] => bookmarks.html
                    [2] => 2x4-cert.crt
                )

            [type] => Array
                (
                    [0] => application/octet-stream
                    [1] => text/html
                    [2] => application/x-x509-ca-cert
                )

            [tmp_name] => Array
                (
                    [0] => Z:\tmp\phpEC.tmp
                    [1] => Z:\tmp\phpED.tmp
                    [2] => Z:\tmp\phpEE.tmp
                )

            [error] => Array
                (
                    [0] => 0
                    [1] => 0
                    [2] => 0
                )

            [size] => Array
                (
                    [0] => 223530
                    [1] => 185298
                    [2] => 1183
                )
        )
)

По сути в массиве $_FILES мы теперь имеем массив [file] ключи которого так же являются массивами. Его то нам и нужно разобрать:

  • Ключ [name] - имена загруженных файлов.

  • Ключ [type] - типы MIME загруженных файлов.

  • Ключ [tmp_name] - путь к файлам куда они складываются на сервере *.

  • Ключ [error] - коды ошибок загрузки файлов *.

  • Ключ [size] - размеры файлов.

* - файлы при загрузке, сначала попадают в директорию, прописанную в файле php.ini, как значение директивы upload_tmp_dir ( как правило это папка tmp/ по умолчанию, она должна иметь соответствующие права на запись). Наша задача переместить их оттуда, туда, куда нам нужно.

* Значения ошибок загрузки файлов

UPLOAD_ERR_OK
Значение: 0; Ошибок нет, загрузка файла успешна.
UPLOAD_ERR_INI_SIZE
Значение: 1; Размер загружаемого файла превышает директиву upload_max_filesize в php.ini.
UPLOAD_ERR_FORM_SIZE
Значение: 2; Размер загружаемого файла превышает директиву MAX_FILE_SIZE , которая была указана в HTML-форме.
UPLOAD_ERR_PARTIAL
Значение: 3; Загруженный файл был получен только частично.
UPLOAD_ERR_NO_FILE
Значение: 4; Файл не был загружен.
UPLOAD_ERR_NO_TMP_DIR
Значение: 6; Отсутствие временной (tmp/) папки. PHP 4.3.10 и PHP 5.0.3.
UPLOAD_ERR_CANT_WRITE
Значение: 7; Ошибка записи файла на диск. PHP 5.1.0.
UPLOAD_ERR_EXTENSION
Значение: 8; Какое то расширение PHP остановило загрузку файла. PHP не предоставляет способ определить, какое из установленных расширений вызвало остановку загрузки файла; получить список загруженных расширений можно выполнив функцию phpinfo(). PHP 5.2.0.

Серверный скрипт для обработки загрузки неопределённого множества файлов


<?php
header( 'content-type: text/html; charset="utf-8"' );

if( !isset($_FILES['file']) ) exit( "Ошибка: файлы не дошли." );

$errors = array();

for( $i = 0, $length = count( $_FILES['file']['name'] ); $i < $length; $i++)
{
	// Если текущий элемент подмассива 'error' имеет значение отличное от 0
	// Значит с файлом проблемы...
	if( $_FILES['file']['error'][$i] !== 0 )
	{
		// Наполняем массив $errors - ключ это имя проблемного файла, а значение код ошибки.
		$errors[ $_FILES['file']['name'][$i] ] = $_FILES['file']['error'][$i];
		// Пропускаем итерацию:
		continue;
	}
	// Выводим информацию пользователю:
  echo '<p>Файл успешно загружен : 
            <strong style="color:green"> '.$_FILES['file']['name'][$i].'</strong>
            - '.$_FILES['file']['size'][$i].' Byte
        </p>';	
  // Перемещаем файл из временной директории в постоянную 'upfiles' - директория должна существовать
  // и иметь соответствующие права, разрешающие запись в неё. К имени нового файла добавим префикс, 
  // состоящий из мд5 хеша времени загрузки в микросек. таким образом даже, если мы загрузим два идентичных
  // файла - ошибки задвоения не произойдет ( идея заимствована в Joomla ) 
  move_uploaded_file( $_FILES['file']['tmp_name'][$i],
                      $_SERVER['DOCUMENT_ROOT'].DIRECTORY_SEPARATOR.
                      'upfiles'.DIRECTORY_SEPARATOR.md5( microtime() ).
                      '__'.$_FILES['file']['name'][$i] );
}

// Смотрим были ли ошибки загрузки:
if( count($errors) > 0 )
{
	echo '<p>Эти файлы не были загружены: </p>';
	
		// $errors - ассоциативный массив, у которого ключ это имя проблемного файла,
		// а значение ключа - это код ошибки: 
		foreach( $errors AS $fileName => $error )
		{
			echo '<p>'.$fileName.' - <strong style="color:red">'.getError( $error ).'</strong></p>';
		}
}

/*
 * Получить описание ошибки по номеру.
 * @param string: $error - номер ошибки из иассива $_FILES['...']['error']
 * return string
 */
function getError( /*int*/ $error )
{
	switch( $error )
	{
		case UPLOAD_ERR_INI_SIZE   : ;
		case UPLOAD_ERR_FORM_SIZE  : return 'Файл слишком большой.';
		  break;
		
		case UPLOAD_ERR_PARTIAL    : return 'Файл получен частично.';
			break;
			
		case UPLOAD_ERR_NO_FILE    : return 'Файл не был загружен.';
			break;
			
		case UPLOAD_ERR_NO_TMP_DIR : return 'Ошибка сервера: отсутствует временная папка.';
			break;
			
		case UPLOAD_ERR_CANT_WRITE : return 'Ошибка сервера: не удалось записать файл на диск.';
			break;
			
		case UPLOAD_ERR_EXTENSION  : return 'PHP-расширение остановило загрузку файла.';
			break;
	}
}

?>

Тут я надеюсь всё понятно - я вроде старался комментировать, но если что не ясно пишем в комменты.

Здесь стоит особо отметить для шустрых товарищей, которые уже успели скопировать код cкриптов - Как было сказано скрипты рабочие, НО! Здесь опущен какой либо функционал, обеспечивающий безопасность! Добавить нужные проверки не трудно, но это уже к теме не относиться. И... должен же я оставить и вам возможность подумать :)

Добавить комментарий


Защитный код
Обновить






Кто на сайте
Сейчас 26 гостей онлайн