Загрузка файлов на сервер: PHP-решения и практические советы

Раздел: Разработка на 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

Загрузка файлов на сервер PHP - comments

En
Upload files php (php)