Загрузка файлов на сервер: PHP-решения и практические советы
Основные подходы к загрузке файлов на сервер средствами PHP
Наиболее распространенным и эффективным решением является использование стандартной PHP-обработки multipart/form-data через массив $_FILES и функции move_uploaded_file.
Пример HTML-формы:
<form action="upload.php" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<button type="submit">Отправить</button>
</form>
Код обработчика upload.php:
<?php
$uploadDir = __DIR__ . '/uploads/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['file'])) {
$file = $_FILES['file'];
if ($file['error'] === UPLOAD_ERR_OK) {
$newName = uniqid() . '_' . basename($file['name']);
$destPath = $uploadDir . $newName;
if (move_uploaded_file($file['tmp_name'], $destPath)) {
echo 'Файл сохранён: ' . htmlspecialchars($newName);
} else {
echo 'Ошибка перемещения файла';
}
} else {
echo 'Код ошибки: ' . $file['error'];
}
}
?>
Важно: перед использованием необходимо создать папку uploads с правами на запись. Файл переименовывается во избежание конфликтов и атак на основе имени.
Как загрузить несколько файлов одновременно?
Используется атрибут multiple для поля ввода и массив имен name="files[]".
<form enctype="multipart/form-data">
<input type="file" name="files[]" multiple>
<button type="submit">Загрузить</button>
</form>
PHP-обработка:
foreach ($_FILES['files']['name'] as $index => $name) {
if ($_FILES['files']['error'][$index] === UPLOAD_ERR_OK) {
move_uploaded_file(
$_FILES['files']['tmp_name'][$index],
$uploadDir . uniqid() . '_' . basename($name)
);
}
}
Проблема: превышение лимита max_file_uploads или общего размера. Решение - увеличить соответствующие директивы в php.ini.
Каким образом ограничить типы загружаемых файлов?
Проверка MIME-типа с помощью finfo или mime_content_type. Надежнее проверять реальное содержимое, а не расширение.
$allowed = ['image/jpeg', 'image/png', 'application/pdf'];
$mime = (new finfo(FILEINFO_MIME_TYPE))->file($_FILES['file']['tmp_name']);
if (in_array($mime, $allowed)) {
// обработка
}
Типичная ошибка: полагаться только на расширение файла из имени - легко обходится. Всегда проверяется MIME и, при необходимости, «магические байты».
Как контролировать максимальный размер загружаемого файла?
Настройки PHP и проверка в коде:
upload_max_filesize- максимальный размер одного файлаpost_max_size- максимальный размер всего POST-запросаmax_file_uploads- максимальное количество файлов
В PHP-скрипте выполняется проверка $_FILES['file']['size'].
Проблема: если загруженный файл превышает upload_max_filesize, он не попадает в $_FILES, а ошибка становится UPLOAD_ERR_INI_SIZE. Пользователь видит пустую форму. Решение - предварительная валидация на клиенте с помощью JavaScript и увеличение лимитов на сервере.
Как загружать изображения с автоматическим созданием миниатюр?
После сохранения оригинала с помощью GD или Imagick создается уменьшенная копия.
$src = imagecreatefromjpeg($file['tmp_name']);
$thumb = imagescale($src, 200);
$thumbPath = $uploadDir . 'thumb_' . $newName;
imagejpeg($thumb, $thumbPath, 85);
imagedestroy($src);
imagedestroy($thumb);
Трудность: не все типы изображений поддерживаются (нужны библиотеки). Ошибка - попытка создать изображение из невалидного файла. Необходима предварительная проверка MIME.
Возможна ли загрузка файла через AJAX с индикацией прогресса?
Да, с использованием XMLHttpRequest и события progress или с fetch (без прогресса). Пример на JavaScript:
const formData = new FormData();
formData.append('file', fileInput.files[0]);
const xhr = new XMLHttpRequest();
xhr.open('POST', 'upload.php', true);
xhr.upload.onprogress = function(e) {
if (e.lengthComputable) {
const percent = (e.loaded / e.total) * 100;
console.log(percent + '%');
}
};
xhr.send(formData);
PHP-обработчик остается таким же, как в базовом решении.
Замечание: прогресс отслеживается только при отправке данных. Сервер не посылает промежуточных сигналов. Для точного прогресса на стороне сервера требуется сессионное хранение и дополнительный AJAX-запрос.
Как обезопасить загрузку файлов от атак?
- Переименование файла (например, уникальный хэш + расширение)
- Хранение за пределами document root (или использование .htaccess для запрета выполнения PHP)
- Проверка MIME и содержимого
- Ограничение размера и типов
- Экранирование имени файла в выводе (
htmlspecialchars)
$ext = pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION);
$newName = md5(uniqid()) . '.' . $ext;
Распространенная уязвимость: файлы с двойным расширением (например, image.php.jpg). При загрузке на сервер с неправильной конфигурацией Apache может быть выполнен как PHP. Решение - хранить файлы вне директории, где включена обработка PHP.
Продвинутые примеры загрузки файлов на PHP
Пример 1. Класс для безопасной загрузки с валидацией
class FileUploader {
private $uploadDir;
private $allowedMime = ['image/jpeg', 'image/png', 'application/pdf'];
private $maxSize = 10 * 1024 * 1024; // 10 MB
public function __construct($uploadDir) {
$this->uploadDir = rtrim($uploadDir, '/') . '/';
if (!is_dir($this->uploadDir)) {
mkdir($this->uploadDir, 0755, true);
}
}
public function upload($fileKey) {
if (!isset($_FILES[$fileKey]) || $_FILES[$fileKey]['error'] !== UPLOAD_ERR_OK) {
throw new Exception('Ошибка загрузки файла');
}
$file = $_FILES[$fileKey];
if ($file['size'] > $this->maxSize) {
throw new Exception('Превышен максимальный размер');
}
$mime = (new finfo(FILEINFO_MIME_TYPE))->file($file['tmp_name']);
if (!in_array($mime, $this->allowedMime)) {
throw new Exception('Недопустимый тип файла');
}
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$newName = bin2hex(random_bytes(16)) . '.' . $ext;
$dest = $this->uploadDir . $newName;
if (move_uploaded_file($file['tmp_name'], $dest)) {
return $newName;
}
throw new Exception('Не удалось сохранить файл');
}
}
// Использование
$uploader = new FileUploader(__DIR__ . '/uploads');
try {
$name = $uploader->upload('file');
echo 'Файл ' . htmlspecialchars($name) . ' загружен';
} catch (Exception $e) {
echo 'Ошибка: ' . $e->getMessage();
}
Файл a1b2c3d4e5f6g7h8.jpg загружен
Пример 2. Загрузка файла с помощью cURL (имитация отправки формы)
$ch = curl_init('https://example.com/upload.php');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => [
'file' => new CURLFile('/path/to/local/file.pdf', 'application/pdf', 'document.pdf')
],
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;
Файл загружен успешно
Пример 3. Загрузка большого файла по частям (chunked upload)
На стороне клиента файл делится на куски (например, по 1 MB), каждый кусок отправляется отдельным запросом. Сервер собирает куски в один файл.
// JavaScript (упрощенно)
const chunkSize = 1 * 1024 * 1024;
let offset = 0;
const file = fileInput.files[0];
const totalChunks = Math.ceil(file.size / chunkSize);
function uploadChunk(chunkIndex) {
const start = chunkIndex * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const blob = file.slice(start, end);
const formData = new FormData();
formData.append('chunk', blob);
formData.append('fileName', file.name);
formData.append('chunkIndex', chunkIndex);
formData.append('totalChunks', totalChunks);
fetch('upload_chunk.php', { method: 'POST', body: formData })
.then(r => r.text())
.then(msg => {
if (chunkIndex + 1 < totalChunks) {
uploadChunk(chunkIndex + 1);
} else {
alert('Загрузка завершена');
}
});
}
uploadChunk(0);
// upload_chunk.php
$fileName = $_POST['fileName'];
$chunkIndex = (int)$_POST['chunkIndex'];
$totalChunks = (int)$_POST['totalChunks'];
$uploadDir = __DIR__ . '/uploads/';
$tempFile = $uploadDir . $fileName . '.part';
file_put_contents($tempFile, file_get_contents($_FILES['chunk']['tmp_name']), FILE_APPEND);
if ($chunkIndex === $totalChunks - 1) {
rename($tempFile, $uploadDir . $fileName);
echo 'Файл собран';
} else {
echo 'Час' . $chunkIndex . ' получен';
}
Час0 получен Час1 получен ... Файл собран
Пример 4. Использование компонента Symfony HttpFoundation для загрузки
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
$request = Request::createFromGlobals();
/** @var UploadedFile $uploadedFile */
$uploadedFile = $request->files->get('file');
if ($uploadedFile) {
$newName = $uploadedFile->move($uploadDir, uniqid() . '.' . $uploadedFile->guessExtension());
echo $newName->getPathname();
}
/var/www/uploads/5f2a1b8c4d.jpg