Pers.narod.ru. PHP. Статьи. Загрузка файлов на сервер с помощью PHP |
Соответствующий код я сейчас попробую написать в учебных целях, возможно, какие-то моменты учесть не получится, но основа для Ваших собственных решений будет.
Сначала сформулируем требования к скрипту для загрузки файлов или картинок на PHP.
Как минимум, такой код должен уметь следующее:
Сначала все задаваемые пользователем настройки соберём в файл config.php
:
<?php define ("MAX_WIDTH","800"); //Макс. ширина картинки в пикселах define ("MAX_HEIGHT","800"); //Макс. высота картинки в пикселах define ("FOLDER","./uploads"); //Путь к папке для картинок, БЕЗ слеша в конце, латинские буквы define ("MAX_SIZE","1048576"); //Максимальный размер файла, байт define ("USE_GDLIB","1"); //Использовать библиотеку GDLib (php_gd2.dll) для масштабирования больших изображений //1 - включить, 0 - выключить ?>
Никаких особенных настроек нашему скрипту не понадобится.
Если скрипт размещается на хостинге, на папку загрузок uploads
достаточно прав 755
(про права говорится в
этом месте сайта
), при размещении на локалхосте и локальном сервере Windows проще всего дать права 777
для
гостевой учетной записи интернета и учётной записи для запуска IIS (
ссылка).
Следует также помнить, что для успешной загрузки файлов должны быть такие же права на временную папку PHP, узнать которую
можно из настройки upload_tmp_dir
файла php.ini
(на хостинге обычно всё уже настроено и настройка пуста).
Так как нашему скрипту параметры будут передаваться только методом POST
, для обработки
параметров достаточно следующего кода (файл params.php
):
<?php while (list($num,$var) = each($params)) { if (!empty($_POST[$var])) $$var = htmlspecialchars($_POST[$var]); else $$var = ''; } ?>
Список разрешённых параметров будет задан в главном файле index.php
, хотя уже ясно,
что таковой понадобится только один - имя кнопки "Загрузить
".
Напишем несколько полезных для поставленной задачи функций и включим их в файл functions.php
.
Во-первых, это функция gdVersion
для определения версии библиотеки gdLib
(для надёжности
потребуем версию не ниже 2, что соответствует PHP 5 версии).
Функция tumbmaker
будет заниматься
обработкой изображений, её заголовок таков:
function tumbmaker ($src, $dest, $width, $height, $rgb=0xFFFFFF, $quality=65)
Параметры $src
и $dest
- ссылки на исходный и целевой файл, $width
и $height
- требуемые
ширина и высота рисунка в пикселах, параметр $rgb
задаёт фоновый цвет нового изображения (по
умолчанию принят белый), а параметр $quality
определяет качество генерируемого изображения
JPEG
(по умолчанию 65%
).
Эта функция преобразует размерности к тем, что мы указали. Чтобы вычислить "правильные" размерности и пропорционально сжать рисунок,
напишем маленькую вспомогательную функцию get_new_size
:
function get_new_size($width,$height,$max_width,$max_height)
Она пропорционально преобразует размеры $width
и $height
с учётом
максимального прямоугольника, куда разрешено вписывать рисунок $max_width
и $max_height
.
Четвёртая функция - let_to_num
- будет переводить размерности, хранящиеся в файле
php.ini
, в нормальное число байт - потому что 2 мегабайта, к примеру, там обозначены
как 2M
.
Наконец, функция cyr2lat
будет заменять в переданном ей тексте символы кириллицы на
аналогичные латинские буквы. Просто далеко не каждый хостинг сможет записать файл с
кириллицей в имени.
Вот что получилось (файл functions.php
):
<?php require_once "config.php"; function gdVersion() { $gdv=@gd_info(); $ver=$gdv['GD Version']; $v=0; if (preg_match("/.*([0-9]+)\.([0-9]+)\.([0-9]+).*/", $ver, $r)) $v=$r[1];//.".".$r[2].".".$r[3]; return $v; } function get_new_size($width,$height,$max_width,$max_height) { $w=$width; $h=$height; $dw=$max_width/$width; $dh=$max_height/$height; if ($width>$max_width and $height>$max_height) { if ($dw<$dh) { $w=$max_width; $h=floor($height*$dw); } else { $h=$max_height; $w=floor($width*$dh); } } else if ($width>$max_width and $height<=$max_height) { $w=$max_width; $h=floor($height*$dw); } else if ($width<=$max_width and $height>$max_height) { $h=$max_height; $w=floor($width*$dh); } return array ($w, $h); } function tumbmaker ($src, $dest, $width, $height, $rgb=0xFFFFFF, $quality=65) { if (!file_exists($src)) return false; $size = getimagesize($src); if ($size === false) return false; $format = strtolower(substr($size['mime'], strpos($size['mime'], '/')+1)); $icfunc = "imagecreatefrom" . $format; if (!function_exists($icfunc)) return false; $x_ratio = $width / $size[0]; $y_ratio = $height / $size[1]; $ratio = min($x_ratio, $y_ratio); $use_x_ratio = ($x_ratio == $ratio); $new_width = $use_x_ratio ? $width : floor($size[0] * $ratio); $new_height = !$use_x_ratio ? $height : floor($size[1] * $ratio); $new_left = $use_x_ratio ? 0 : floor(($width - $new_width) / 2); $new_top = !$use_x_ratio ? 0 : floor(($height - $new_height) / 2); $isrc = $icfunc($src); $idest = imagecreatetruecolor($width, $height); imagefill($idest, 0, 0, $rgb); imagecopyresampled($idest, $isrc, $new_left, $new_top, 0, 0, $new_width, $new_height, $size[0], $size[1]); imagejpeg($idest, $dest, $quality); imagedestroy($isrc); imagedestroy($idest); return true; } function let_to_num($v){ //Размерности php.ini в байты $l = substr($v, -1); $ret = substr($v, 0, -1); switch(strtoupper($l)){ case 'P': $ret *= 1024; case 'T': $ret *= 1024; case 'G': $ret *= 1024; case 'M': $ret *= 1024; case 'K': $ret *= 1024; break; } return $ret; } function cyr2lat ($text) { $cyr2lat_replacements = array ( "А" => "a","Б" => "b","В" => "v","Г" => "g","Д" => "d", "Е" => "e","Ё" => "yo","Ж" => "dg","З" => "z","И" => "i", "Й" => "y","К" => "k","Л" => "l","М" => "m","Н" => "n", "О" => "o","П" => "p","Р" => "r","С" => "s","Т" => "t", "У" => "u","Ф" => "f","Х" => "h","Ц" => "ts","Ч" => "ch", "Ш" => "sh","Щ" => "csh","Ъ" => "","Ы" => "i","Ь" => "", "Э" => "e","Ю" => "yu","Я" => "ya", "а" => "a","б" => "b","в" => "v","г" => "g","д" => "d", "е" => "e","ё" => "yo","ж" => "dg","з" => "z","и" => "i", "й" => "y","к" => "k","л" => "l","м" => "m","н" => "n", "о" => "o","п" => "p","р" => "r","с" => "s","т" => "t", "у" => "u","ф" => "f","х" => "h","ц" => "ts","ч" => "ch", "ш" => "sh","щ" => "sch","ъ" => "","ы" => "i","ь" => "", "э" => "e","ю" => "yu","я" => "ya", "-" => "_"," " => "_" ); return strtr ($text,$cyr2lat_replacements); } ?>
Осталось написать сам скрипт и поместить его в файл index.php
.
После формирования обычной титульной части html-файла:
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=windows-1251"> <title>Сервис для загрузки файла</title> </head> <body><div align="center">
подключим файл с функциями и наш обработчик параметров:
<?php require_once "functions.php"; $params = array ('ok'); require_once ("params.php"); ?>
Форма для загрузки файла будет включать только соответствующий элемент HTML и кнопку "Загрузить":
<form action="index.php" method="post" enctype="multipart/form-data"> <input type="file" name="url"> <input type="submit" name="ok" value="Загрузить"> </form>
Атрибуты method="post"
и enctype="multipart/form-data"
в такой форме обязательны -
иначе никакой загрузки не выйдет.
Вся остальная часть файла, если не считать закрывающих тегов в самом конце -
</div></body> </html>
- будет состоять из одного блока PHP-кода, основные действия которого мы сейчас напишем.
Если форма была отправлена кнопкой "Загрузить", в параметрах должна присутствовать переменная $ok
, если её нет -
вернуться:
if (empty($ok)) return;
Далее установим служебные переменные - сообщение об ошибке $error
, временное имя переданного файла $file
,
флаг загрузки файла $load
, флаг $need_move
для функции загрузки временного файла move_uploaded_file
(дело в том, что при масштабировании рисунка он может быть загружен и функцией tumbmaker
, тогда
стандартная move_uploaded_file
не нужна):
$error = $file = ''; $load = true; $need_move= true;
Всё остальное поместим в блок условия, проверяющего, что файл был передан:
if (isset($_FILES['url']) and !empty($_FILES['url']['tmp_name'])) { //здесь будет остальной код } else { $error .= '<br>Файл не передан.'; if (!empty($_FILES['url']['error'])) $error .=' Код ошибки: '.$_FILES['url']['error']; }
Получаем временное имя файла и его размер в байтах, а также ограничение на размер - это минимальная
из величин MAX_SIZE
(наше собственное ограничение из конфигурации), post_max_size
(настройка php.ini
, определяющая
максимальный разрешённый размер файла для передачи методом POST
) и
upload_max_filesize
(стандартная настройка для максимального размера загружаемого файла).
Если размер превышен, формируем соответствующее сообщение:
$file = $_FILES['url']['tmp_name']; $file_size = $_FILES['url']['size']; $php_max_size = min(let_to_num(ini_get('post_max_size')), let_to_num(ini_get('upload_max_filesize'))); $max_size=min(MAX_SIZE,$php_max_size); if ($file_size > $max_size) { $error .= '<br>Слишком большой по объёму файл. Ограничение в настройках '. ($max_size==MAX_SIZE?'скрипта (config.php)': 'PHP (см. настройки post_max_size,upload_max_filesize)'). ': '.$max_size.' байт'; $load=false; } if ($_FILES['url']['error']!=UPLOAD_ERR_OK) { $error .= '<br>Сервер вернул ошибку закачки файла. Код ошибки: '. $_FILES['url']['error']; $load=false; }
Извлекаем тип переданного файла, формируем путь к записываемому файлу $filename
, при этом имя файла,
возможно, будет изменено методом cyr2lat
:
$path_parts = pathinfo($_FILES['url']['name']); $filetype = strtolower($path_parts['extension']); $filename = FOLDER.'/'.cyr2lat(strtolower($path_parts['basename']));
По типу файла (не самый надёжный способ на самом деле) проверяем, загружается ли картинка. Для этого
введём флаг $is_pic
:
$is_pic = true; switch ($filetype) { case "jpeg": case "jpg": case "gif": case "png": break; default: $is_pic=false; }
Если это картинка, проверяем её размер, при необходимости пытаемся сжать, если включена настройка
USE_GDLIB
и библиотека доступна. Если сжать рисунок удалось, ставим $need_move=false
, чтобы
не грузить рисунок повторно методом move_uploaded_file
:
if ($is_pic) { $size = getimagesize($file); $width = $size[0]; $height = $size[1]; if ($width>MAX_WIDTH or $height>MAX_HEIGHT) { $ver = gdVersion(); if (USE_GDLIB=='1') { if ($ver>1) { $new_size = get_new_size($width,$height,MAX_WIDTH,MAX_HEIGHT); $r=tumbmaker ($file,$filename,$new_size[0],$new_size[1]); if (!$r) { $error .= '<br>Не удалось программно уменьшить изображение. Пожалуйста, проверьте работу библиотеки GBLib или загрузите картинку меньшего размера. Установки из настроек сайта: ширина до '.MAX_WIDTH.', высота до '.MAX_HEIGHT.' пикс.'; $load=false; } else $need_move=false; } else { $error .= '<br>В настройках сайта включено масштабирование больших изображений, но библиотека GDLib недоступна. Пожалуйста, настройте её или загрузите картинку меньшего размера. Установки из настроек сайта: ширина до '.MAX_WIDTH.', высота до '. MAX_HEIGHT.' пикс.'; $load=false; } } else { $error .= '<br>Превышен максимальный размер рисунка. Установки из настроек сайта: ширина до '.MAX_WIDTH.', высота до '.MAX_HEIGHT. ' пикс. Автоматическое масштабирование изображений: выключено'; $load=false; } } }
После всего этого, если установлены флаги $load
и $need_move
, можно загрузить картинку
стандартным методом. После этого ставим на загруженную картинку права 644
:
if ($load) { if ($need_move) { if (move_uploaded_file ($file,$filename)) { chmod ($filename,0644); } else { $error .= '<br>Не удалось закачать файл '.$filename.' из временного '. $file.'<br>Информация для отладки: '; print_r($_FILES); } } }
Теперь в конце основного блока кода можно проверить, удалён ли временный файл (по идее,
move_uploaded_file
удалит его сама после успешной загрузки), проверить, не нужно ли вывести сообщение об
ошибке, если нет - дать ссылку на загруженный файл:
if (file_exists($file)) @unlink ($file); if (!empty($error)) { print 'Возникли проблемы!'.$error; } else { print 'Файл загружен, вот он: <a href="'.$filename.'" target="_blank">'.$filename.'</a>'; }
Вот что получилось у меня после проверки кода на хостинге в браузере Google Chrome:
Конечно, в реальном скрипте нужно ещё добавить авторизацию пользователя, контроль лимита общего объёма загруженных файлов, возможность работать с их списком и т.д. Но всё это можно сделать на основе других статей, например, вот здесь есть построение дерева папок на PHP, а здесь - про авторизацию с помощью сессий.
Код этого примера в архиве ZIP (4 Кб)
Возможна ли безопасная загрузка файлов |
Отдельно нужно сказать несколько слов о безопасности загрузки файлов. Следует понимать, что любая форма для загрузки юзером файлов на ваш сайт потенциально опасна. Например, толковый юзер может загрузить shell-файл и получить доступ к вашему серверу. Чтобы такого не произошло, надо убрать права "остальным пользователям", то есть, поставить права 666 на папку, куда пользователи загружают файлы. По умолчанию там обычно права 755.
Но и этого может оказаться недостаточно. Я считаю, что
файлы можно "складывать" на сервер как обычно, но под именем,
полученным скриптом (функцией, uniqid
, например), а настоящее имя файла (если оно нужно) и
MIME-тип лучше хранить в базе. При этом "отдавать" файлы тоже лучше не "напрямую",
а скриптом. Вот пример простого класса для поддержки скачивания файлов:
<?php class download { var $properties = array( 'old_name' => "", 'new_name' => "", 'type' => "", 'size' => "", 'resume' => "", 'max_speed' => "" ); var $range = 0; function download($path, $name="", $resume=0, $max_speed=0) { $name = ($name == "") ? substr(strrchr("/".$path,"/"),1) : $name; $file_size = @filesize($path); $this->properties = array ( 'old_name' => $path, 'new_name' => $name, 'type'=> "application/force-download", 'size' => $file_size, 'resume' => $resume, 'max_speed' => $max_speed ); if ($this->properties['resume']) { if (isset($_SERVER['HTTP_RANGE'])) { $this->range = $_SERVER['HTTP_RANGE']; $this->range = str_replace("bytes=", "", $this->range); $this->range = str_replace("-", "", $this->range); } else { $this->range = 0; } if ($this->range > $this->properties['size']) $this->range = 0; } else { $this->range = 0; } } function download_file() { if ($this->range) { header($_SERVER['SERVER_PROTOCOL']." 206 Partial Content"); } else { header($_SERVER['SERVER_PROTOCOL']." 200 OK"); } header("Pragma: public"); header("Expires: 0"); header("Cache-Control:"); header("Cache-Control: public"); header("Content-Description: File Transfer"); header("Content-Type: ".$this->properties["type"]); header('Content-Disposition: attachment; filename="'.$this->properties["new_name"].'";'); header("Content-Transfer-Encoding: binary"); if ($this->properties['resume']) header("Accept-Ranges: bytes"); if ($this->range) { header("Content-Range: bytes {$this->range}-".($this->properties['size']-1)."/".$this->properties['size']); header("Content-Length: ".($this->properties['size']-$this->range)); } else { header("Content-Length: ".$this->properties['size']); } @ini_set('max_execution_time', 0); @set_time_limit(); $this->_download($this->properties["old_name"], $this->range); } function _download ($filename, $range=0) { @ob_end_clean(); if (($speed = $this->properties['max_speed']) > 0) $sleep_time = (8 / $speed) * 1e6; else $sleep_time = 0; $handle = fopen($filename, 'rb'); fseek($handle,$range); if ($handle === false) { return false; } while (!feof($handle)) { print (fread($handle, 1024*8)); ob_flush(); flush(); usleep($sleep_time); } fclose($handle); return true; } } ?>
гостевая; E-mail |