FilePond: загрузка файлов — мастерская, PHP и 6 примеров
Создаём удобную загрузку файлов с FilePond: мини-мастерская и шесть живых интерфейсов. Предпросмотр, порядок, ограничения, отмена и повтор, PHP-обработчик и готовый проект с исходниками.

Пользователь выбирает фотографию профиля, прикладывает документы к заявке или переносит сразу несколько файлов. Ему нужно видеть, что выбрано, подходит ли формат, сколько осталось ждать и что делать после ошибки. Обычная кнопка выбора решает только часть этой задачи.
FilePond превращает файловое поле в удобный интерфейс с перетаскиванием, предпросмотром, ограничениями и состояниями обработки. Ниже — мини-мастерская, шесть живых сценариев, исходники и отдельный PHP-проект, в котором можно проверить настоящую загрузку, отмену и подтверждение набора.
FilePond, обычный input и FormData: что выбираем
Если нужно прикрепить один небольшой файл и отправить форму, стандартного HTML часто достаточно. Укажите подпись, допустимые расширения и enctype. Проверки на сервере нужны при любом интерфейсе.
<form action="./upload.php" method="post" enctype="multipart/form-data">
<label for="attachment">Документ для заявки</label>
<input id="attachment" type="file" name="attachment"
accept=".pdf,.txt" required>
<button type="submit">Отправить документ</button>
</form>
FormData позволяет отправить выбранный файл JavaScript-кодом без перезагрузки. Однако список файлов, удаление, прогресс, повтор и сообщения всё равно придётся написать. FilePond берёт на себя значительную часть этой работы, сохраняя обычный объект File доступным приложению.
// Фрагмент для собственного сервера: input и csrf уже получены.
const file = input.files[0];
if (!file) throw new Error('Сначала выберите файл');
const body = new FormData();
body.append('filepond', file, file.name);
const response = await fetch('./api/process.php', {
method: 'POST',
headers: { 'X-CSRF-Token': csrf },
body
});
if (!response.ok) throw new Error('Сервер не принял файл');
const serverId = (await response.text()).trim();
| Задача | input / FormData | FilePond |
|---|---|---|
| Один файл в простой форме | Минимум кода и зависимостей | Полезен, если нужен более наглядный выбор |
| Несколько файлов с удалением и ошибками | Состояния и список пишет приложение | Есть список, действия и события |
| Предпросмотр фотографий | Нужно создавать и освобождать preview | Подключается Image Preview |
| Хранение, права и привязка к заявке | Работа сервера | Также работа сервера: библиотека её не заменяет |
Не задавайте вручную Content-Type: multipart/form-data при передаче FormData: браузер сам добавит необходимую границу частей. И не считайте атрибут accept доказательством правильного содержимого — он помогает выбрать файл, но не проверяет его на сервере.
Источник: FormData и отправка файлов.
Установка и полный минимальный пример
В материале закреплены FilePond 4.32.12, Image Preview 4.6.12, File Validate Type 1.2.9 и File Validate Size 2.2.8. Эти пакеты распространяются по MIT. FilePond работает в обычном HTML5-проекте: React, Vue и сборщик для первого запуска не обязательны.
npm install filepond@4.32.12 filepond-plugin-image-preview@4.6.12 filepond-plugin-file-validate-type@1.2.9 filepond-plugin-file-validate-size@2.2.8
Этот полный HTML можно сохранить как index.html. Он показывает изображения, проверяет тип и размер, но ничего не отправляет. CDN используется только для подключения библиотек; в архиве те же зависимости лежат локально.
<!doctype html>
<html lang="ru">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Выбор изображения с FilePond</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/filepond@4.32.12/dist/filepond.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/filepond-plugin-image-preview@4.6.12/dist/filepond-plugin-image-preview.min.css">
<main style="max-width:640px;margin:32px auto;padding:16px">
<h1>Выберите изображение</h1>
<p>PNG, JPEG или WebP, до 2 МиБ. Файл останется в браузере.</p>
<label for="picture">Изображение для примера</label>
<input id="picture" type="file" name="filepond"
accept="image/png,image/jpeg,image/webp">
</main>
<script src="https://cdn.jsdelivr.net/npm/filepond@4.32.12/dist/filepond.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/filepond-plugin-image-preview@4.6.12/dist/filepond-plugin-image-preview.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/filepond-plugin-file-validate-type@1.2.9/dist/filepond-plugin-file-validate-type.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/filepond-plugin-file-validate-size@2.2.8/dist/filepond-plugin-file-validate-size.min.js"></script>
<script>
FilePond.registerPlugin(FilePondPluginImagePreview,
FilePondPluginFileValidateType, FilePondPluginFileValidateSize);
const pond = FilePond.create(document.querySelector('#picture'), {
allowMultiple: false,
server: null,
allowProcess: false,
acceptedFileTypes: ['image/png', 'image/jpeg', 'image/webp'],
maxFileSize: 2 * 1024 * 1024,
labelIdle: 'Перетащите файл или <span class="filepond--label-action">выберите</span>',
labelFileTypeNotAllowed: 'Этот формат не подходит',
fileValidateTypeLabelExpectedTypes: 'Нужен PNG, JPEG или WebP',
labelMaxFileSizeExceeded: 'Файл слишком большой',
labelMaxFileSize: 'Максимальный размер — 2 МиБ',
labelButtonRemoveItem: 'Удалить файл'
});
</script>
</html>
Плагины регистрируются до создания экземпляра. Для предпросмотра требуется отдельный CSS. Подключение только JavaScript без стилей — частая причина непонятного вида виджета. В проекте со сборщиком вместо глобальных объектов используйте импорты:
import * as FilePond from 'filepond';
import ImagePreview from 'filepond-plugin-image-preview';
import ValidateType from 'filepond-plugin-file-validate-type';
import ValidateSize from 'filepond-plugin-file-validate-size';
import 'filepond/dist/filepond.min.css';
import 'filepond-plugin-image-preview/dist/filepond-plugin-image-preview.min.css';
FilePond.registerPlugin(ImagePreview, ValidateType, ValidateSize);
Источник: установка FilePond без фреймворка и через npm.
Сначала выберите один способ подключения. Импорты CSS рассчитаны на сборщик, а обычная HTML-страница использует теги link и script. Не загружайте одновременно две копии FilePond и не создавайте два экземпляра на одном поле. При динамическом переходе между экранами сначала завершайте жизненный цикл старого экземпляра, затем создавайте новый.
Мини-мастерская: соберите свой загрузчик
Настройте допустимые файлы, количество, размеры и поведение обработки. Добавьте подготовленный пример или выберите свой файл, затем посмотрите на список, ошибки и сформированную конфигурацию. Здесь удобно проверить, подходит ли загрузчик конкретной форме, ещё до подключения PHP.
Попробуйте: оставьте только изображения, добавьте документ и прочитайте отказ. Затем разрешите нужный формат, добавьте несколько небольших файлов и уменьшите лимит количества. Отдельно сравните автоматический и ручной запуск локальной обработки. Подпись режима показывает, что сетевой отправки нет.
Изменение настроек пересоздаёт виджет и очищает прежний список. После применения новых ограничений добавьте файлы заново. Это позволяет испытать новую конфигурацию без неявного переноса результатов предыдущей обработки.
Экспорт JavaScript сохраняет настройки; JSON описывает выбранный набор и параметры. Автономный HTML содержит библиотеки и конфигурацию, но не включает выбранные вами файлы. После открытия такого HTML файлы нужно выбрать заново. Он остаётся демонстрацией в браузере и не подключает скрытый обработчик загрузки.
{
"allowMultiple": true,
"maxFiles": 4,
"acceptedFileTypes": ["application/pdf", "text/plain"],
"maxFileSize": 2097152,
"maxTotalFileSize": 6291456,
"instantUpload": false
}
Это пример данных конфигурации, а не готовое соединение с сервером. Размеры заданы в байтах: 1 МиБ равен 1 048 576 байтам. Так лимит в PHP и интерфейсе можно сравнить без неоднозначности между десятичными мегабайтами и двоичными мебибайтами.
Функции обработки нельзя сохранить обычным JSON без потери поведения. Поэтому настройки, исполнимый JavaScript и полный HTML выгружаются отдельно. Если собираете свой конструктор, храните разрешённые параметры как данные, а обработчики выбирайте из собственного кода. Строка, введённая в поле настройки, не должна незаметно превращаться в исполняемую функцию.
Шесть сценариев для интерфейсов веб-сервисов
Во всех сценариях можно выбрать файлы, удалить лишние и посмотреть результат действий. Подготовленные образцы помогают начать без поиска подходящих документов. Следите за разницей между выбором, локальной обработкой и подтверждением набора: это разные состояния даже тогда, когда они находятся рядом.
Скачать проект с шестью примерами, мастерской и настоящим PHP API. Публичная версия показывает работу интерфейса; серверный режим запускается отдельно на вашем компьютере.
| Сценарий | Лимит примера | Где пригодится |
|---|---|---|
| Аватар | 1 изображение, до 2 МиБ | Профиль, логотип организации, обложка карточки |
| Галерея | 6 изображений, до 3 МиБ каждое и 12 МиБ суммарно | Портфолио, карточка товара, фотографии работ |
| Документы | 3 PDF/TXT, до 2 МиБ каждый и 6 МиБ суммарно | Техническое задание, вложения к обращению |
| Пакет | 8 файлов, до 5 МиБ каждый и 20 МиБ суммарно | Материалы проекта, несколько вложений за один раз |
| Ручной запуск | 4 файла, до 5 МиБ каждый и 20 МиБ суммарно | Предварительная проверка набора перед подтверждением |
| Очередь и ошибки | 5 файлов, до 5 МиБ каждый и 20 МиБ суммарно | Отмена, повтор и понятное восстановление после сбоя |
1. Аватар: один файл и понятная замена
Профилю обычно нужна одна фотография. Интерфейс должен позволить быстро заменить её, увидеть выбранное изображение и удалить ошибочный вариант. Здесь разрешены PNG, JPEG и WebP до 2 МиБ; PDF и остальные форматы не подходят.
Попробуйте: добавьте образец изображения, затем замените его другим. Посмотрите, что в наборе остаётся один файл. Выберите документ и сравните сообщение об ошибке с нормальным предпросмотром.
const avatarOptions = {
allowMultiple: false,
allowReplace: true,
acceptedFileTypes: ['image/png', 'image/jpeg', 'image/webp'],
maxFileSize: 2 * 1024 * 1024,
imagePreviewHeight: 180
};
Image Preview создаёт уменьшенное отображение, но не уменьшает исходный файл для отправки. Круглая маска аватара тоже не означает, что изображение обрезано физически. Если нужно изменять пиксели, подключайте соответствующую обработку отдельно и объясняйте человеку её результат. Полноценный редактор Pintura — отдельный продукт, он для этих примеров не требуется.
Закрытие системного окна без выбора нового файла — нормальное действие, а не ошибка загрузки. Сохраняйте предыдущий вариант, пока пользователь явно не заменил или не удалил его. Для формы профиля это особенно важно: кнопка отмены выбора не должна выглядеть как команда стереть уже установленную фотографию.
Источник: что делает Image Preview.
2. Галерея: несколько изображений и порядок
В галерее важен не только набор, но и последовательность. Первое изображение может стать обложкой, остальные — иллюстрациями проекта. Лимиты помогают удержать форму понятной: до шести изображений, каждое до 3 МиБ, суммарно до 12 МиБ.
Попробуйте: добавьте несколько образцов, поменяйте порядок и удалите один файл. Сверьте последовательность в списке и экспортируемых данных. Для телефона и клавиатуры удобнее иметь явные команды перемещения, а не рассчитывать исключительно на перетаскивание.
pond.setOptions({
allowMultiple: true,
allowReorder: true,
maxFiles: 6,
maxFileSize: 3 * 1024 * 1024,
maxTotalFileSize: 12 * 1024 * 1024
});
pond.on('reorderfiles', (items) => {
const order = items.map(item => item.id);
console.log('Текущий порядок в браузере:', order);
});
// Команда «выше» для собственной доступной панели файлов.
function moveUp(pond, id) {
const index = pond.getFiles().findIndex(item => item.id === id);
if (index > 0) pond.moveFile(id, index - 1);
}
Встроенное перетаскивание FilePond для изменения порядка рассчитано на одну колонку. Сетку фотографий и сортировку нельзя считать взаимозаменяемыми настройками. Сохранение порядка в карточке товара выполняет приложение: сначала оно получает идентификаторы сохранённых файлов, затем записывает их последовательность.
Источник: allowReorder и параметры экземпляра.
3. Документы: формат, размер и полезный отказ
Вложение к заявке не всегда требует картинки. Для PDF и текста полезнее показать имя, размер, причину отказа и возможность удаления. Пример принимает до трёх PDF/TXT, каждый до 2 МиБ, общий размер до 6 МиБ.
Попробуйте: добавьте PDF и текстовый образец, затем изображение. Ошибка должна объяснить, какие форматы ожидаются. Длинное имя не должно закрывать кнопку удаления или растягивать страницу на телефоне.
const documentOptions = {
allowMultiple: true,
maxFiles: 3,
acceptedFileTypes: ['application/pdf', 'text/plain'],
maxFileSize: 2 * 1024 * 1024,
maxTotalFileSize: 6 * 1024 * 1024,
fileValidateTypeLabelExpectedTypes: 'Разрешены PDF и TXT',
labelFileTypeNotAllowed: 'Этот документ не подходит'
};
Проверка типов требует File Validate Type, проверка размера — File Validate Size. MIME в браузере иногда отсутствует или определяется неточно. Приложение может уточнять определение для удобства выбора, но окончательное решение принимает PHP по содержимому и правилам хранилища. Переименование файла в .pdf не превращает его в PDF.
Источник: плагин типов и ограничения браузерного MIME.
Источник: размер одного файла и всего набора.
4. Пакет: обработка нескольких файлов
Когда пользователь добавляет материалы проекта, полезно сразу показать очередь и состояние каждого элемента. Здесь разрешено до восьми изображений, PDF и TXT; обработка запускается автоматически, параллельно идут не более двух элементов.
Попробуйте: добавьте несколько образцов и наблюдайте, как очередь освобождает места. Удалите ненужный элемент. Общая готовность не должна скрывать файл с ошибкой: один зелёный индикатор для всей группы недостаточен.
pond.setOptions({
allowMultiple: true,
maxFiles: 8,
maxFileSize: 5 * 1024 * 1024,
maxTotalFileSize: 20 * 1024 * 1024,
instantUpload: true,
maxParallelUploads: 2
});
На этой странице обработка пакета моделируется таймером. В реальном соединении два параллельных запроса могут снизить ожидание, но не гарантируют удвоение скорости: остаются ограничения канала, сервера и обработки содержимого. Значение maxParallelUploads регулирует клиентскую очередь, а сервер независимо ограничивает ресурсы.
Для пакета показывайте и число файлов, и общий объём. Восемь небольших текстовых документов и восемь фотографий создают разную нагрузку, хотя счётчик одинаковый. При превышении общего лимита объясняйте, что нужно удалить часть набора; сообщение о размере одного файла в такой ситуации направит пользователя не к тому исправлению.
5. Ручной запуск: сначала проверить набор
Иногда человек должен сначала подобрать приложения к обращению, удалить лишнее и только потом начать отправку. Для этого отключается instantUpload. Отдельное подтверждение набора показывает ещё одну важную границу: завершение обработки файлов не означает создание заявки.
Попробуйте: добавьте несколько файлов, измените набор до запуска, затем выполните обработку и подтверждение. После нового изменения набора прежнее подтверждение не должно считаться актуальным. В публичном примере это локальное состояние, без создания записей на сервере.
pond.instantUpload = false;
startButton.addEventListener('click', async () => {
if (!pond.getFiles().length) {
message.textContent = 'Сначала добавьте файлы';
return;
}
startButton.disabled = true;
try {
await pond.processFiles();
message.textContent = 'Обработка завершена. Проверьте набор.';
} catch {
message.textContent = 'Есть незавершённые файлы. Исправьте ошибки.';
} finally {
startButton.disabled = false;
}
});
Фрагмент предполагает уже подключённый обработчик и существующие элементы кнопки и сообщения. Если server не настроен, вызов метода сам по себе не создаст сервер. Проверять готовность нужно по каждому файлу, а подтверждать только конкретный набор идентификаторов.
6. Очередь, отмена и повтор после ошибки
Сбой — обычная часть загрузки. Хороший интерфейс сохраняет понятный список, отмечает проблемный элемент и позволяет повторить его обработку. В этом сценарии управляемая первая ошибка помогает увидеть такой путь без отключения интернета.
Попробуйте: запустите набор, дождитесь демонстрационного отказа и повторите обработку проблемного файла. Отдельно отмените выполняющуюся операцию. Отмена, удаление из списка и отмена уже принятого сервером файла должны оставаться разными действиями.
// id — идентификатор элемента FilePond в текущем списке.
function cancelItem(pond, id) {
const item = pond.getFile(id);
if (item) item.abortProcessing();
}
async function retryItem(pond, id) {
try {
await pond.processFile(id);
} catch {
// FilePond оставит ошибку у файла; приложение обновляет свою сводку.
}
}
Ниже — самостоятельная модель процесса для изучения состояний. В ней нет fetch и XMLHttpRequest; идентификатор с префиксом demo_ существует только в этой демонстрации. Не используйте такое завершение как доказательство сохранения файла.
function createDemoProcess() {
const attempts = new WeakMap();
return (fieldName, file, metadata, load, error, progress, abort) => {
const attempt = (attempts.get(file) || 0) + 1;
attempts.set(file, attempt);
let percent = 0;
let done = false;
const timer = setInterval(() => {
if (done) return;
percent += 20;
progress(true, percent, 100);
if (attempt === 1 && percent === 40) {
done = true;
clearInterval(timer);
error('Демонстрационная ошибка. Повторите обработку.');
} else if (percent >= 100) {
done = true;
clearInterval(timer);
load('demo_' + crypto.randomUUID());
}
}, 150);
return { abort() {
if (done) return;
done = true;
clearInterval(timer);
abort();
}};
};
}
Источник: FileItem и abortProcessing.
События, идентификаторы и экспорт текущего набора
У элемента есть клиентский id, серверный serverId, состояние и исходный объект File. Клиентский идентификатор нужен для кнопок интерфейса; серверный — для операций с принятым вложением. Имя файла не заменяет ни один из них: одинаковые имена встречаются постоянно.
const snapshot = pond.getFiles().map(item => ({
id: item.id,
serverId: item.serverId || null,
name: item.filename,
size: item.fileSize,
type: item.file.type,
status: item.status
}));
console.log(JSON.stringify(snapshot, null, 2));
Такой JSON содержит описание набора, а не байты файлов. Показать имя в таблице можно через textContent. Если создаёте собственные URL предпросмотра через URL.createObjectURL, освобождайте их при удалении и смене состояния. Копирование объекта File обычным JSON не создаёт резервную копию содержимого.
function updateSummary() {
const files = pond.getFiles();
const ready = files.filter(item =>
item.status === FilePond.FileStatus.PROCESSING_COMPLETE).length;
summary.textContent = `Готово ${ready} из ${files.length}`;
}
pond.on('updatefiles', updateSummary);
pond.on('processfile', updateSummary);
pond.on('processfileabort', updateSummary);
pond.on('processfilerevert', updateSummary);
// При удалении экрана приложения.
pond.off('updatefiles', updateSummary);
pond.off('processfile', updateSummary);
pond.off('processfileabort', updateSummary);
pond.off('processfilerevert', updateSummary);
pond.destroy();
Для событий экземпляра применяется имя processfile, для DOM-события — FilePond:processfile. Не смешивайте эти варианты. Статусы лучше сравнивать с FilePond.FileStatus, а не с числом, смысл которого придётся искать при следующем изменении кода.
Источник: события экземпляра и DOM.
Как устроен настоящий серверный цикл
В локальном проекте файл сначала попадает во временное хранилище. PHP возвращает непрозрачный идентификатор; браузер хранит его вместо пути на диске. Затем пользователь подтверждает набор. Это позволяет отменить ненужное вложение до окончательной операции.
| Шаг проекта | Запрос | Результат |
|---|---|---|
| Получить состояние | GET api/status.php | Токен CSRF, лимиты и файлы текущей сессии |
| Принять файл | POST api/process.php, multipart/form-data | Непрозрачный идентификатор в text/plain |
| Отменить временную загрузку | DELETE api/revert.php, идентификатор в теле | Удаление принадлежащего сессии временного файла |
| Восстановить предпросмотр | GET api/restore.php?id=… | Байты доступного файла |
| Подтвердить набор | POST api/commit.php, JSON с ids | Подтверждение набора приложением |
commit — операция нашего приложения, а не обязательный метод FilePond. В этом учебном хранилище и временные, и подтверждённые файлы имеют срок жизни один час. Подтверждение не создаёт бессрочный архив и не оформляет настоящую заявку. Для продукта жизненный цикл вложений определяется отдельно.
// Выполняется только в локальном PHP-проекте.
const statusResponse = await fetch('./api/status.php', { cache: 'no-store' });
if (!statusResponse.ok) throw new Error('Не удалось получить состояние');
const status = await statusResponse.json();
const csrf = status.csrf;
const server = {
process: makePhpProcess(csrf),
revert: {
url: './api/revert.php',
method: 'DELETE',
headers: { 'X-CSRF-Token': csrf }
},
restore: './api/restore.php?id=',
load: null,
fetch: null
};
Переменная makePhpProcess определена в следующем разделе. Здесь используется один origin: интерфейс и PHP открыты с одного адреса. Публичные примеры статьи такую конфигурацию не получают. Запуск HTML напрямую через file:// также не запускает PHP.
Источник: контракт process, revert, restore и load.
Process через XMLHttpRequest: прогресс и отмена
Для измерения передачи файла удобно использовать событие xhr.upload.onprogress. Индикатор получает количество переданных байтов, а окончательное завершение — только после успешного ответа PHP. Переданные 100% ещё не означают, что сервер проверил и сохранил содержимое.
function makePhpProcess(csrf) {
return (fieldName, file, metadata, load, error, progress, abort) => {
const xhr = new XMLHttpRequest();
let settled = false;
const finish = (callback, value) => {
if (settled) return;
settled = true;
callback(value);
};
xhr.open('POST', './api/process.php');
xhr.timeout = 30000;
xhr.setRequestHeader('X-CSRF-Token', csrf);
xhr.upload.onprogress = event => {
if (!settled) progress(event.lengthComputable, event.loaded, event.total);
};
xhr.onload = () => {
if (settled) return;
const id = xhr.responseText.trim();
if (xhr.status >= 200 && xhr.status < 300 && /^[a-f0-9]{64}$/.test(id)) {
progress(true, file.size, file.size);
finish(load, id);
} else {
finish(error, 'Сервер не принял файл. Проверьте формат и лимиты.');
}
};
xhr.onerror = () => finish(error, 'Соединение прервано');
xhr.ontimeout = () => finish(error, 'Сервер не ответил вовремя');
xhr.onabort = () => finish(abort);
const body = new FormData();
body.append(fieldName, file, file.name);
xhr.send(body);
return { abort() { xhr.abort(); finish(abort); } };
};
}
Функция передаёт FilePond успех, ошибку или отмену ровно один раз. Проверка формата идентификатора соответствует нашему PHP-контракту, а не универсальному требованию библиотеки. При своём API согласуйте формат ответа отдельно. Метаданные можно передавать дополнительными полями, но имя, MIME и размер из браузера нельзя принимать за проверенный факт.
Для API на другом домене потребуется корректный CORS на сервере, включая используемые методы и заголовки. Сессионные запросы требуют согласованной передачи cookie; no-cors не является исправлением. При восстановлении имени файла через другой origin сервер должен разрешить чтение соответствующего Content-Disposition.
Источник: прогресс отправки XMLHttpRequest.
Источник: CORS, заголовки и запросы с cookie.
Что проверяет PHP до сохранения
PHP сначала проверяет результат загрузки, действительный размер временного файла и допустимость содержимого. Значения $_FILES['filepond']['type'] и исходное имя поступают от клиента. Сервер определяет MIME самостоятельно и сопоставляет его с разрешёнными расширениями.
function inspectUploadedFile(array $file): array
{
if (($file['error'] ?? null) !== UPLOAD_ERR_OK) {
throw new RuntimeException('PHP не получил файл целиком');
}
$tmp = $file['tmp_name'] ?? null;
if (!is_string($tmp) || !is_uploaded_file($tmp)) {
throw new RuntimeException('Недопустимый источник файла');
}
$size = filesize($tmp);
if ($size === false || $size < 1 || $size > 5 * 1024 * 1024) {
throw new RuntimeException('Размер должен быть от 1 байта до 5 МиБ');
}
$mime = (new finfo(FILEINFO_MIME_TYPE))->file($tmp);
$allowed = [
'image/jpeg' => ['jpg', 'jpeg'],
'image/png' => ['png'],
'image/webp' => ['webp'],
'application/pdf' => ['pdf'],
'text/plain' => ['txt'],
];
$name = $file['name'] ?? '';
if (!is_string($name)) throw new RuntimeException('Некорректное имя');
$extension = strtolower(pathinfo($name, PATHINFO_EXTENSION));
if (!isset($allowed[$mime]) || !in_array($extension, $allowed[$mime], true)) {
throw new RuntimeException('Формат файла не разрешён');
}
return ['tmp' => $tmp, 'size' => $size, 'mime' => $mime, 'name' => $name];
}
Это отдельный проверяющий фрагмент, не полный endpoint. В архиве дополнительно проверяются изображения и текст, квоты, владелец сессии и CSRF. getimagesize помогает узнать параметры изображения, но сам по себе не является универсальной проверкой безопасности. Для TXT ожидается обычный UTF-8 текст; SVG, HTML и PHP не входят в разрешённые форматы.
// После проверки загрузки, сессии, CSRF и квот.
// $storageRoot — заранее настроенный каталог вне public.
$info = inspectUploadedFile($_FILES['filepond']);
$id = bin2hex(random_bytes(32));
$target = $storageRoot . DIRECTORY_SEPARATOR . $id . '.blob';
if (!move_uploaded_file($info['tmp'], $target)) {
throw new RuntimeException('Не удалось сохранить файл');
}
// Полный проект также сохраняет метаданные и владельца.
header('Content-Type: text/plain; charset=utf-8');
echo $id;
Оригинальное имя остаётся подписью, а путь задаёт приложение. Временное хранилище вне public не выдаёт содержимое произвольному посетителю; получение файла проходит через проверку владельца. Это место ожидания дальнейшего решения. Если продукт требует антивирусной проверки, её нужно встроить отдельным этапом до выдачи пользователям — в данном проекте такой сервис не заявлен.
Для полей самой заявки — названия, email, дат, состава данных — пригодится Symfony Validator. Проверка формы и проверка байтов файла дополняют друг друга, но решают разные задачи.
Источник: загрузка файлов и структура $_FILES.
Источник: определение MIME с Fileinfo.
Источник: перемещение загруженного файла.
Источник: возможности и ограничения getimagesize.
Revert, restore и удаление: не путайте операции
Удалить карточку из браузера и убрать файл с сервера — разные действия. Для отмены временной загрузки FilePond использует revert. Восстановление временного файла выполняется через restore и происхождение limbo. Уже сохранённые серверные вложения обозначаются local; это слово здесь не означает «файл только в браузере».
// existingTemporaryIds получены от своего API с проверкой владельца.
pond.setOptions({
files: existingTemporaryIds.map(id => ({
source: id,
options: { type: 'limbo' }
}))
});
async function removeTemporaryItem(pond, itemId) {
try {
await pond.removeFile(itemId, { revert: true });
} catch {
throw new Error('Не удалось отменить временную загрузку');
}
}
Для удаления существующего вложения обычно нужна собственная операция приложения с проверкой прав. Не превращайте любое нажатие крестика в безусловное удаление файла с диска. В локальном проекте отмена и подтверждение проверяют текущую сессию; знание чужого идентификатора не даёт право получить его содержимое.
Если пользователь закрывает вкладку, вызов отмены не гарантирован. Поэтому временным объектам нужен срок жизни и уборка. Срок в один час в этом проекте относится также к демонстрационным подтверждённым наборам; в настоящем сервисе правила хранения будут зависеть от назначения документов.
Подтверждение набора после загрузки
В форме заказа файлы часто отправляются раньше самой формы. Перед окончательным действием убедитесь, что каждый файл обработан, у каждого есть серверный идентификатор и набор не меняется во время подтверждения. PHP заново проверяет принадлежность всех идентификаторов; клиентский зелёный статус не заменяет эту проверку.
async function commitSelection(pond, csrf) {
const files = pond.getFiles();
if (!files.length || files.some(item =>
item.status !== FilePond.FileStatus.PROCESSING_COMPLETE ||
!/^[a-f0-9]{64}$/.test(item.serverId || ''))) {
throw new Error('Сначала завершите загрузку всех файлов');
}
const ids = files.map(item => item.serverId);
const response = await fetch('./api/commit.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf },
body: JSON.stringify({ ids })
});
const result = await response.json();
if (!response.ok || !result.ok) throw new Error('Набор не подтверждён');
return result.order;
}
function requireOpaqueId(mixed $value): string
{
if (!is_string($value) || !preg_match('/\A[a-f0-9]{64}\z/', $value)) {
throw new InvalidArgumentException('Некорректный идентификатор');
}
return $value;
}
// Затем приложение проверяет владельца, состояние и срок жизни записи.
// Проверка формата не является проверкой доступа.
На время вызова приложение блокирует повторное подтверждение и изменение набора, затем снимает блокировку в finally. При ошибке сохраняйте список и понятное сообщение. Если отправка оборвалась после принятия запроса сервером, сначала уточните состояние: повторная операция не должна незаметно создавать ещё одну заявку.
В PHP-проекте повторное подтверждение того же массива идентификаторов в том же порядке возвращает прежний результат. Перестановка уже подтверждённого набора отклоняется, чтобы порядок галереи не потерялся молча. Подтверждение также не продлевает часовой срок хранения. Эти условия относятся к демонстрационному API; в своём продукте зафиксируйте соответствующее поведение явно.
Телефон, клавиатура и доступные состояния
Перетаскивание удобно на десктопе, но на телефоне основным действием остаётся системный выбор файла. В загрузчике должна быть понятная нажимаемая область. Для удаления, отмены и перемещения нужен различимый текст или доступная подпись; действия не должны зависеть только от наведения мыши.
.upload-section { min-width: 0; }
.upload-section .filepond--root { width: 100%; }
.upload-summary { overflow-wrap: anywhere; }
.upload-actions { display: flex; flex-wrap: wrap; gap: 10px; }
.upload-actions button { min-height: 44px; }
@media (max-width: 600px) {
.upload-actions { display: grid; grid-template-columns: 1fr; }
}
Не задавайте фиксированную высоту списку, если количество файлов меняется: нижние карточки и ошибки могут оказаться обрезаны. На узком экране проверьте длинные имена, сообщения о размере и состояние с несколькими ошибками. Предпросмотр должен помогать узнать изображение, не занимая всю страницу.
Показывайте словами «ожидает», «обрабатывается», «ошибка» и «готово». Не объявляйте через live region каждый процент: человеку достаточно значимых изменений. После ошибки сохраняйте доступ к повтору и удалению. Если есть собственная сортировка, добавьте команды «выше» и «ниже» и возвращайте фокус к перемещённому элементу.
Проверяйте не только пустой экран. Добавьте файл с длинным русским именем, несколько изображений, неподходящий документ и элемент после неудачного повтора. Пройдите действия клавишей Tab и системным выбором файлов. Если успешный сценарий удобен, а ошибка требует горизонтальной прокрутки или угадывания значка, интерфейс ещё нуждается в доработке.
Источник: стилизация и размеры элементов FilePond.
Что проверить, если загрузка не работает
| Симптом | Что проверить | Практический смысл |
|---|---|---|
| Файл выбран, запроса нет | server, allowProcess, instantUpload и ручной запуск | Выбор файла сам по себе не означает отправку |
| Типы или размеры не ограничиваются | Подключение и регистрация соответствующих плагинов | acceptedFileTypes и maxFileSize требуют своих модулей |
| Картинка есть, размер не уменьшился | Используется только Image Preview | Предпросмотр не меняет отправляемые байты |
| PHP получил пустой $_FILES | post_max_size, upload_max_filesize, имя поля, multipart boundary | Запрос мог быть отклонён до прикладной проверки |
| Прогресс дошёл до конца, затем ошибка | HTTP-статус и ответ process | Передача байтов и принятие файла — разные этапы |
| После крестика файл остался | revert/remove и состояние вложения | Удаление карточки не всегда удаляет серверный объект |
| В другом домене запрос блокируется | CORS и разрешённые заголовки на API | Клиент не может самостоятельно разрешить CORS |
| Включены чанки, но нет продолжения | Поддержка протокола POST/PATCH/HEAD сервером | chunkUploads не реализует серверный протокол автоматически |
Начните с небольшого разрешённого файла и одного запроса. Затем проверьте границу размера, неправильный формат, отмену, повтор, закрытие страницы и недоступный сервер. Отдельно убедитесь, что нельзя подтвердить чужой или уже исчезнувший временный идентификатор. Это проверки конкретного жизненного цикла, а не повод усложнять простую форму десятками лишних действий.
Исходники, локальный PHP и следующий шаг
Распакуйте архив и откройте терминал в каталоге PHP-проекта. Нужен PHP 8.2 или новее с Fileinfo; зависимости браузера уже включены. Composer для запуска этого примера не требуется. Если добавите PHP-пакеты для хранения или обработки, порядок установки разобран в руководстве по Composer.
php -v
php -m
php -d display_errors=0 -d upload_max_filesize=6M -d post_max_size=7M -S 127.0.0.1:8080 -t public
Откройте http://127.0.0.1:8080/. В архиве уже настроен серверный режим; его параметры находятся в public/runtime.js. В нём HTTP-загрузка ограничена 5 МиБ на файл, десятью файлами и 20 МиБ в сессии, включая подтверждённые. Параметр PHP post_max_size немного больше лимита файла, потому что запрос содержит служебные части. display_errors=0 сохраняет JSON-ответ чистым при превышении этого лимита: диагностическое предупреждение PHP не попадает перед ответом API. Ограничения конкретного сценария интерфейса могут быть строже общего серверного лимита.
Сначала загрузите образец, отмените его и убедитесь, что восстановление больше недоступно. Затем загрузите новый, восстановите временный набор и подтвердите его. Так вы проверите весь цикл, а не только появление зелёной полосы. Настоящее приложение сможет привязать подтверждённые идентификаторы к обращению или карточке пользователя.
Скачать исходники FilePond: мастерская, шесть интерфейсов и PHP-проект.
- Официальная документация FilePond — установка, API и плагины.
- Серверный протокол — process, revert, restore, load и чанки.
- Методы экземпляра — управление списком и обработкой.
- Пакет FilePond 4.32.12 в npm — закреплённая версия и лицензия.
- Руководство PHP по загрузке файлов — обработка запроса и ограничения.
Для первого внедрения выберите один сценарий: аватар, документы или набор к заявке. Согласуйте лимиты, подписи действий и момент окончательного подтверждения. Затем подключите тот же интерфейс к своему обработчику, сохранив понятное различие между выбранным файлом, временной загрузкой и сохранённым вложением.


