PHP загрузка файла: как перенести и безопасно сохранить файл на сервере

В PHP задачу move uploaded file решает функция move_uploaded_file, которая переносит принятый файл из временного каталога. PHP загрузка файла на сервер начинается с формы с multipart/form-data и обработчика, который проверяет ошибку, размер и содержимое. Вы разберете массив $_FILES, лимиты PHP, безопасное имя и два варианта мультизагрузки.

PHP загрузка файла на сервер: простая форма

HTML-форма отправляет файл методом POST, если у нее указаны правильные метод и тип кодирования. Атрибут name задает ключ, по которому PHP найдет файл в $_FILES.

<form method="post" action="basic.php" enctype="multipart/form-data">
    <label for="inputfile">Выберите файл</label>
    <input type="file" id="inputfile" name="inputfile">
    <button type="submit">Загрузить</button>
</form>
HTML-форма с полем выбора файла и кнопкой загрузки

После отправки PHP положит данные поля inputfile в $_FILES['inputfile']. Обработчик проверит метод запроса, наличие поля, код ошибки и факт загруженного файла.

<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['inputfile'])) {
    $file = $_FILES['inputfile'];

    if ($file['error'] === UPLOAD_ERR_OK && is_uploaded_file($file['tmp_name'])) {
        $name = bin2hex(random_bytes(16));
        $destination_path = __DIR__ . '/uploads/' . $name;

        if (move_uploaded_file($file['tmp_name'], $destination_path)) {
            echo 'File uploaded';
        } else {
            echo 'File not uploaded';
        }
    } else {
        echo 'Upload error: ' . $file['error'];
    }
}

Для этого учебного примера заранее создайте каталог uploads рядом с basic.php. Пользователь, под которым работает PHP, должен иметь право записи в каталог.

Этот пример показывает механику move uploaded file php. Он еще не проверяет MIME и расширение, поэтому публичный сайт должен использовать обработчик из раздела о безопасности.

Файлы в PHP: что хранит $_FILES?

$_FILES – глобальный массив PHP с данными файлов из POST-запроса.

Ключ массива совпадает с атрибутом name у поля формы.

Array
(
    [inputfile] => Array
        (
            [name] => photo.jpg
            [type] => image/jpeg
            [tmp_name] => /tmp/phpabc123
            [error] => 0
            [size] => 6887
        )
)
ПолеЧто хранитКак использовать
nameИсходное имя файлаНе использовать как имя на сервере
typeMIME, присланный клиентомНе считать защитной проверкой
tmp_nameПуть к временному файлуПередать в finfo_file и move_uploaded_file
errorКод результата загрузкиСначала проверить его значение
sizeРазмер файла в байтахСравнить с серверным лимитом

Браузер присылает поле type, поэтому клиент может подменить его. Проверяйте содержимое через Fileinfo и разрешайте только заранее выбранные расширения и MIME-типы.

КонстантаЗначениеЧто значитЧто сделать
UPLOAD_ERR_OK0Файл принят без ошибкиПродолжить проверки
UPLOAD_ERR_INI_SIZE1Размер превысил upload_max_filesizeПоказать ограничение
UPLOAD_ERR_FORM_SIZE2Размер превысил MAX_FILE_SIZEПоказать ошибку формы
UPLOAD_ERR_PARTIAL3Файл пришел частичноПопросить повторить загрузку
UPLOAD_ERR_NO_FILE4Файл не выбранПопросить выбрать файл
UPLOAD_ERR_NO_TMP_DIR6Нет временного каталогаИсправить конфигурацию сервера
UPLOAD_ERR_CANT_WRITE7Сервер не записал файлПроверить права каталога
UPLOAD_ERR_EXTENSION8Расширение PHP остановило загрузкуПроверить настройки расширений

Что такое move_uploaded_file?

Функция move_uploaded_file переносит файл из временного каталога PHP в указанное место. Она возвращает true, если перенос завершился успешно, и false, если сервер не смог его выполнить.

if (move_uploaded_file($file['tmp_name'], $destination_path)) {
    echo 'File uploaded';
} else {
    echo 'File not uploaded';
}

Функция принимает временный путь и путь назначения.

Перед переносом проверьте код ошибки, размер, содержимое и результат is_uploaded_file.

Изменяем лимит размера загружаемого файла

PHP ограничивает загрузку несколькими директивами. Значение upload_max_filesize задают в php.ini, .htaccess или .user.ini. Обычный ini_set внутри обработчика эту директиву не меняет.

upload_max_filesize = 10M
post_max_size = 12M

post_max_size задает предел всего POST-запроса, поэтому его ставят выше upload_max_filesize. При превышении post_max_size PHP передаст пустые массивы $_POST и $_FILES.

Скрытое поле называется MAX_FILE_SIZE и хранит лимит в байтах. PHP может вернуть UPLOAD_ERR_FORM_SIZE, но клиент меняет это поле. Сервер все равно сравнивает $_FILES['inputfile']['size'] со своим лимитом.

<input type="hidden" name="MAX_FILE_SIZE" value="10485760">

Размещайте MAX_FILE_SIZE перед полем выбора файла внутри формы. Для долгих загрузок учитывайте max_execution_time и max_input_time. Если код держит файл в памяти, проверьте также memory_limit.

Как обезопасить PHP-скрипт загрузки файлов

Безопасный обработчик не доверяет имени, расширению и MIME от браузера. Он проверяет размер по $_FILES['size'], читает MIME через Fileinfo и принимает только разрешенные сочетания расширения и содержимого.

Сначала подготовьте каталог вне веб-корня – папки, доступной через браузер. Команды для /srv/private_uploads рассчитаны на Linux-сервер с SSH-доступом. Для создания каталога и смены владельца нужны права администратора.

Определите пользователя PHP через временный файл php-user.php рядом с обработчиком. Этот способ требует включенного расширения POSIX в PHP.

<?php
$user = posix_getpwuid(posix_geteuid());
echo htmlspecialchars($user['name'], ENT_QUOTES, 'UTF-8');

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

Не запускайте его через консоль: консольный PHP может работать от другого пользователя.

  1. Откройте терминал на своем компьютере. Подключитесь командой ssh login@host, заменив login и host учетными данными SSH вашего сервера. После входа команды выполняются на сервере.
  2. Создайте хранилище командой sudo mkdir -p /srv/private_uploads. Введите пароль своей серверной учетной записи, если sudo запросит его. Учетная запись должна иметь разрешение на выполнение административных команд.
  3. Назначьте владельца командой sudo chown PHP_USER /srv/private_uploads. Замените PHP_USER именем, которое показал диагностический файл.
  4. Ограничьте доступ командой sudo chmod 750 /srv/private_uploads. Владелец получит право записи, а остальные пользователи вне группы потеряют доступ.

Эти команды предназначены для нового отдельного каталога загрузок. Если вы вошли как root, выполняйте их без sudo.

Без административного доступа используйте уже доступный PHP каталог вне веб-корня. Укажите его абсолютный путь вместо /srv/private_uploads во всех вызовах saveUpload. Для проверки создайте рядом с обработчиком временный PHP-файл с этим кодом:

<?php
$uploadDir = '/absolute/path/to/private_uploads';
echo is_dir($uploadDir) && is_writable($uploadDir)
    ? 'Каталог доступен для записи'
    : 'Каталог отсутствует или запись запрещена';

Замените путь своим, затем откройте файл через браузер. Сообщение «Каталог доступен для записи» подтверждает доступ текущего процесса PHP. После проверки удалите временный файл.

<?php
function saveUpload(array $file, array $allowed, int $maxSize, string $uploadDir): string
{
    if ($file['error'] !== UPLOAD_ERR_OK) {
        throw new RuntimeException('Ошибка загрузки: ' . $file['error']);
    }

    if ($file['size'] > $maxSize) {
        throw new RuntimeException('Файл превышает допустимый размер');
    }

    if (!is_uploaded_file($file['tmp_name'])) {
        throw new RuntimeException('Источник файла не прошел проверку');
    }

    $extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
    if (!isset($allowed[$extension])) {
        throw new RuntimeException('Расширение файла запрещено');
    }

    $finfo = new finfo(FILEINFO_MIME_TYPE);
    $mime = $finfo->file($file['tmp_name']);
    if ($mime === false || !in_array($mime, $allowed[$extension], true)) {
        throw new RuntimeException('Содержимое файла не соответствует типу');
    }

    $newName = bin2hex(random_bytes(16)) . '.' . $extension;
    $destination_path = rtrim($uploadDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $newName;

    if (!move_uploaded_file($file['tmp_name'], $destination_path)) {
        throw new RuntimeException('Сервер не сохранил файл');
    }

    return $newName;
}

$allowed = [
    'jpg' => ['image/jpeg'],
    'jpeg' => ['image/jpeg'],
    'png' => ['image/png'],
    'txt' => ['text/plain']
];

if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['inputfile'])) {
    try {
        $savedName = saveUpload($_FILES['inputfile'], $allowed, 1048576, '/srv/private_uploads');
        echo 'Файл сохранен: ' . htmlspecialchars($savedName, ENT_QUOTES, 'UTF-8');
    } catch (RuntimeException $error) {
        echo htmlspecialchars($error->getMessage(), ENT_QUOTES, 'UTF-8');
    }
}

Функция возвращает имя сохраненного файла, а ошибки передает через RuntimeException. Обработчик ловит исключение и выводит его текст после экранирования. Сохраните обработчик в basic.php перед HTML-формой одиночной загрузки.

Имя формирует random_bytes, поэтому исходное имя не перезаписывает соседний файл. Расширение берется из белого списка, а результат переноса проверяет условие.

Если каталог находится внутри веб-корня, запретите исполнение PHP для него на уровне веб-сервера.

Мультизагрузка файлов при помощи PHP-скрипта

Есть 2 способа принять несколько файлов: использовать разные имена полей или одно поле с массивом name[] и атрибутом multiple. В обоих случаях каждый файл проходит отдельную проверку.

Разные имена полей

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

У каждого поля должен быть свой id и свой ключ в $_FILES.

<form method="post" enctype="multipart/form-data">
    <label for="profilepic">Фото</label>
    <input type="file" id="profilepic" name="profilepic">
    <label for="resume">Резюме в формате TXT</label>
    <input type="file" id="resume" name="resume">
    <button type="submit">Загрузить</button>
</form>

Передайте каждое поле функции saveUpload из безопасного обработчика.

Создайте отдельный PHP-файл с функцией saveUpload, обработчиком и этой формой. Скопируйте только определение функции, без блока обработки поля inputfile.

<?php
$rules = [
    'profilepic' => [
        'jpg' => ['image/jpeg'],
        'jpeg' => ['image/jpeg'],
        'png' => ['image/png']
    ],
    'resume' => [
        'txt' => ['text/plain']
    ]
];

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    foreach ($rules as $field => $allowed) {
        if (!isset($_FILES[$field])) {
            echo 'Поле не найдено: ' . htmlspecialchars($field, ENT_QUOTES, 'UTF-8');
            continue;
        }

        try {
            $name = saveUpload($_FILES[$field], $allowed, 5242880, '/srv/private_uploads');
            echo 'Сохранен файл ' . htmlspecialchars($name, ENT_QUOTES, 'UTF-8');
        } catch (RuntimeException $error) {
            echo htmlspecialchars($error->getMessage(), ENT_QUOTES, 'UTF-8');
        }
    }
}

Массив name[] и атрибут multiple

Один элемент формы принимает несколько файлов, если у него есть имя с квадратными скобками и атрибут multiple. Индекс связывает имя, временный путь, ошибку и размер одного файла.

<form method="post" enctype="multipart/form-data">
    <label for="pic">Файлы</label>
    <input type="file" id="pic" name="pic[]" multiple>
    <button type="submit">Загрузить</button>
</form>

Для этого варианта также поместите определение saveUpload перед обработчиком. Форму разместите после PHP-кода в том же файле.

<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['pic'])) {
    $files = $_FILES['pic'];
    $allowed = [
        'jpg' => ['image/jpeg'],
        'jpeg' => ['image/jpeg'],
        'png' => ['image/png']
    ];

    foreach ($files['error'] as $index => $error) {
        $file = [
            'name' => $files['name'][$index],
            'type' => $files['type'][$index],
            'tmp_name' => $files['tmp_name'][$index],
            'error' => $error,
            'size' => $files['size'][$index]
        ];

        try {
            $name = saveUpload($file, $allowed, 1048576, '/srv/private_uploads');
            echo 'Файл сохранен: ' . htmlspecialchars($name, ENT_QUOTES, 'UTF-8');
        } catch (RuntimeException $uploadError) {
            echo 'Ошибка файла ' . ($index + 1) . ': ' . htmlspecialchars($uploadError->getMessage(), ENT_QUOTES, 'UTF-8');
        }
    }
}

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

Как проверить результат загрузки

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

Проверьте отказ в трех случаях: выберите слишком большой файл, переименуйте файл с запрещенным расширением и отправьте файл с неподходящим содержимым. Обработчик должен вывести ошибку и не создать новый файл.

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

Комментарии

Оставьте свой комментарий
Максим П.

Здравствуйте! Как мне указать путь сохранения для файла не в ту же директорию где лежит файл php, а в другую например:z:\home\localhost\www\rusakov\images\

ВГ
Владимир Г.

Спасибо за такую всеобъемлющую статью, полезно

ГБ
Григорій Б.

Подскажите, как можно загрузить файл на свой сервер, в том случае если у меня есть только ссылка на него. То-есть мне нужно форма для загрузки файлов на сервер, в котором можно вставлять ссылку на файл.

ВВ
Василий В.

Подкажите, как можно сделать так, чтобы каждый раз создавалась новая папка, куда будут помещаться файлы? Например 28122017-1645 (то есть состояла из даты и времени) ?

B

http://php.net/manual/ru/function.date.php

А
Андрей

А если вместо повтора строк тега <input type="file" id="pic" name="pic[]"> изменить его на <input type="file" id="pic" name="pic[]" multiple >, то должна получиться мультизагрузка