В цифровую эпоху видеоконтент стал неотъемлемой частью нашего виртуального опыта. Видеоролики используются повсюду — от платформ социальных сетей до образовательных сайтов. Однако работа с большими видеофайлами может быть сложной, особенно когда речь идет о загрузке их на сервер. В этой статье рассмотрим, как реализовать надежную систему для загрузки больших видеофайлов с помощью Node.js, Express и современных веб-технологий.

Введение

Загрузка больших файлов, особенно видео, представляет собой уникальную проблему для веб-разработки. Традиционные методы загрузки часто не работают, когда речь идет о файлах, превышающих определенные ограничения по размеру. Это может привести к тайм-аутам, проблемам с памятью и плохому пользовательскому опыту. В данном руководстве будет подробно описано решение, позволяющее эффективно и надежно загружать большие видеофайлы с помощью Node.js на стороне сервера и современного JavaScript на стороне клиента.

Проблема загрузки больших файлов

Прежде чем перейти к решению проблемы загрузки больших файлов, определим ее причины.

  1. Ограничения сервера. Многие серверы имеют ограничения на максимальный размер входящих запросов, который может быть легко превышен большими видеофайлами.
  1. Проблемы с тайм-аутом. Загрузка больших файлов занимает много времени, и при длительных процессах загрузки соединения между клиентом и сервером могут прерываться.
  1. Нехватка памяти. Загрузка большого файла в память сервера целиком может быстро исчерпать доступные ресурсы, особенно на серверах, обрабатывающих несколько одновременных загрузок.
  1. Нестабильность сети. Длительное время загрузки увеличивает риск перебоев в работе сети, что может привести к неудачной загрузке и разочарованию пользователей.
  1. Пользовательский опыт. Без надлежащей обратной связи пользователи могут остаться в неведении относительно статуса своей загрузки, что приведет к ухудшению пользовательского опыта.

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

Предлагаемый подход: загрузка файлов с разбивкой на фрагменты

Основная идея загрузки файлов фрагментами (chunks) заключается в том, чтобы разбить большой файл на более мелкие части на стороне клиента, отправить эти части на сервер по отдельности, а затем снова собрать их на сервере. Такой подход дает несколько преимуществ.

  1. Обход ограничений по размеру. Отправляя небольшие фрагменты, можно обойти ограничения сервера на размер запросов.
  1. Повышенная надежность. Если загрузка не удалась, достаточно повторно отправить только текущий фрагмент, а не весь файл.
  2. Улучшение пользовательского опыта. Пользователю предоставляется более точная информация о ходе выполнения.
  1. Снижение потребления памяти. Серверу нужно обрабатывать только один фрагмент за раз, что значительно снижает требования к памяти.
  1. Возможность возобновления. Несмотря на то, что в текущем решении эта функция не реализована, загрузка фрагментами упрощает реализацию функций приостановки и возобновления.

Теперь погрузимся в детали реализации, начиная с бэкенда.

Реализация бэкенда с помощью Node.js и Express

Предлагаемое бэкенд-решение использует Node.js с фреймворком Express, а также несколько ключевых библиотек для эффективной обработки загрузки файлов.

Настройка сервера

Для начала рассмотрим основные настройки сервера Express:

import express from 'express';
import morgan from 'morgan';
import cors from 'cors';
import multer from 'multer'
import videoRouter from './routes/video.js'
const app = express();
app.use(express.json({
limit: '500MB',
}));
// Включение CORS для всех маршрутов
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
next();
});
app.use(cors());
app.use(morgan('dev'));
// Промежуточное ПО для обработки ошибок
app.use((err, req, res, next) => {
if (err instanceof multer.MulterError) {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).send('File size limit exceeded (max 500MB).');
}
}
res.status(500).send(err.message);
});
app.use(express.static('public'));
// Маршрут для главной страницы
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
app.use('/api', videoRouter)
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`app running on port: ${port}`);
});

Файл video.js:

import express from 'express';
import multer from 'multer';
import fs from 'fs-extra';
import path from 'path';

const router = express.Router();
const uploadPath = path.join(process.cwd(), 'uploads');
const uploadPathChunks = path.join(process.cwd(), 'chunks');

// Должны существовать загрузочные каталоги
await fs.mkdir(uploadPath, { recursive: true });
await fs.mkdir(uploadPathChunks, { recursive: true });

Здесь импортируем необходимые модули и настраиваем пути к файлам. Используем fs-extra, модуль расширенной файловой системы, для создания каталогов загрузки, если они не существуют. Определяем два пути:

  • uploadPath: конечный пункт назначения для объединенных видеофайлов;
  • uploadPathChunks: временное место хранения отдельных фрагментов.

Настройка Multer для загрузки файлов

Теперь настроим Multer, промежуточное ПО для работы с multipart/form-data (в основном используется для загрузки файлов).

Файл video.js:

const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, uploadPathChunks);
},
filename: (req, file, cb) => {
const baseFileName = file.originalname.replace(/\s+/g, '');

fs.readdir(uploadPathChunks, (err, files) => {
if (err) {
return cb(err);
}

// Отфильтруйте файлы, которые совпадают с базовым именем файла
const matchingFiles = files.filter((f) => f.startsWith(baseFileName));

let chunkNumber = 0;
if (matchingFiles.length > 0) {
// Извлеките самый большой номер фрагмента
const highestChunk = Math.max(
...matchingFiles.map((f) => {
const match = f.match(/\.part_(\d+)$/);
return match ? parseInt(match[1], 10) : -1;
})
);
chunkNumber = highestChunk + 1;
}

const fileName = `${baseFileName}.part_${chunkNumber}`;
cb(null, fileName);
});
},
});

const upload = multer({
storage: storage,
limits: { fileSize: 500 * 1024 * 1024 }, // ограничение - 500 МБ
fileFilter: (req, file, cb) => {
if (
file.mimetype.startsWith('video/') ||
file.mimetype === 'application/octet-stream'
) {
cb(null, true);
} else {
cb(new Error('Not a video file. Please upload only videos.'));
}
},
});

Эта конфигурация выполняет несколько важных действий:

  1. Устанавливает место назначения для загружаемых фрагментов в каталог uploadPathChunks.
  1. Генерирует уникальные имена файлов для каждого фрагмента, добавляя номер фрагмента, чтобы избежать конфликтов.
  1. Устанавливает ограничение на размер файла в 500 МБ на фрагмент.
  1. Включает фильтр файлов, чтобы гарантировать принятие только видеофайлов (или потоков октетов, как могут быть идентифицированы фрагменты).

Работа с фрагментами файла

Теперь посмотрим, как обрабатывается загрузка отдельных фрагментов:

router.post('/upload', upload.single('video'), async (req, res) => {
if (!req.file) {
return res.status(400).json({ error: 'No video file uploaded.' });
}

try {
const chunkNumber = Number(req.body.chunk);
const totalChunks = Number(req.body.totalChunks);
const fileName = req.body.originalname.replace(/\s+/g, '');

if (chunkNumber === totalChunks - 1) {
await mergeChunks(fileName, totalChunks);
}

const fileInfo = {
filename: fileName,
originalName: req.body.originalname,
size: req.file.size,
mimetype: req.file.mimetype,
baseURL: 'https://xyz.com/dist/video/',
videoUrl: `https://xyz.com/dist/video/${fileName}`,
};

res.status(200).json({
message: 'Chunk uploaded successfully',
file: fileInfo,
});
} catch (error) {
console.error('Error during file upload:', error);
res
.status(500)
.json({ error: 'An error occurred while uploading the video.' });
}
});

Этот обработчик маршрута выполняет следующее:

  1. Использует Multer для обработки загруженного фрагмента.
  1. Проверяет, получен ли последний фрагмент (если chunkNumber === totalChunks - 1).
  1. Если получен последний фрагмент, запускается функция mergeChunks для объединения всех фрагментов в конечный видеофайл.
  1. Отправляет ответ с информацией о загруженном файле.

Объединение фрагментов файла

С использованием функции mergeChunks начинается магия, которая происходит при сборке видеофайла:

const MAX_RETRIES = 5;
const RETRY_DELAY = 1000; // 1 second
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function mergeChunks(fileName, totalChunks) {
const writeStream = fs.createWriteStream(path.join(uploadPath, fileName));

for (let i = 0; i < totalChunks; i++) {
const chunkPath = path.join(uploadPathChunks, `${fileName}.part_${i}`);
let retries = 0;

while (retries < MAX_RETRIES) {
try {
const chunkStream = fs.createReadStream(chunkPath);
await new Promise((resolve, reject) => {
chunkStream.pipe(writeStream, { end: false });
chunkStream.on('end', resolve);
chunkStream.on('error', reject);
});
console.log(`Chunk ${i} merged successfully`);
await fs.promises.unlink(chunkPath);
console.log(`Chunk ${i} deleted successfully`);
break; // Success, move to next chunk
} catch (error) {
if (error.code === 'EBUSY') {
console.log(
`Chunk ${i} is busy, retrying... (${retries + 1}/${MAX_RETRIES})`
);
await delay(RETRY_DELAY);
retries++;
} else {
throw error; // Unexpected error, rethrow
}
}
}

if (retries === MAX_RETRIES) {
console.error(`Failed to merge chunk ${i} after ${MAX_RETRIES} retries`);
writeStream.end();
throw new Error(`Failed to merge chunk ${i}`);
}
}

writeStream.end();
console.log('Chunks merged successfully');
}

Эта функция выполняет несколько важных действий:

  1. Создает поток записи для конечного видеофайла.
  1. Перебирает все фрагменты, читая каждый из них и добавляя его в конечный файл.
  1. Реализует механизм повторных попыток для обработки возможных ошибок занятости файловой системы.
  2. После успешного объединения фрагмента удаляет файл с фрагментами, чтобы освободить место.
  1. Если все фрагменты успешно объединены, закрывает поток записи, завершая работу с файлом.

Очистка и обработка ошибок

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

router.use((err, req, res, next) => {
if (err instanceof multer.MulterError) {
console.log('Multer error:', err.message);
return res.status(400).json({ error: err.message });
}
if (err) {
fs.readdir(uploadPathChunks, (err, files) => {
if (err) {
return console.error('Unable to scan directory: ' + err);
}

// Выполнение итерации по файлам и Iterate over the files and delete each one
files.forEach(file => {
const filePath = path.join(uploadPathChunks, file);

fs.promises.unlink(filePath, err => {
if (err) {
console.error('Error deleting file:', filePath, err);
} else {
console.log('Successfully deleted file:', filePath);
}
});
});
});
console.log('General error:', err.message);
return res.status(500).json({ error: err.message });
}
next();
});

Этот обработчик ошибок выполняет несколько ключевых операций:

  1. Различает ошибки, специфичные для Multer, и другие типы ошибок.
  1. В случае общей ошибки пытается очистить все частичные загрузки, удаляя файлы в каталоге chunks (каталоге фрагментов).
  1. Отправляет соответствующие ответы на ошибки клиенту.

После завершения реализации бэкенда перейдем к фронтенду.

Реализация фронтенда

Фронтенд большой системы загрузки видео состоит из HTML для структуры, CSS для стилизации и JavaScript для обработки процесса загрузки.

Структура HTML

Вот базовая структура HTML для формы загрузки:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Large Video Upload</title>
<!-- Здесь будут CSS-стили -->
</head>
<body>
<form id="upload-form">
<input type="file" name="video" accept="video/*" required />
<button type="submit">Upload Video</button>
<div id="progress-container">
<div id="progress-bar">
<div id="progress"></div>
</div>
<div id="status"></div>
</div>
</form>

<!-- Здесь будет JavaScript -->
</body>
</html>

Эта структура представляет собой простую форму с вводом файла, кнопкой отправки и элементами для отображения прогресса загрузки.

Стилизация формы загрузки

Чтобы сделать форму загрузки более визуально привлекательной и удобной для пользователя, добавим немного CSS:

<style>
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #f0f0f0;
}
form {
background-color: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
width: 300px;
}
input[type='file'] {
margin-bottom: 1rem;
width: 100%;
}
button {
background-color: #4caf50;
color: white;
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
width: 100%;
}
button:hover {
background-color: #45a049;
}
#progress-container {
margin-top: 1rem;
display: none;
}
#progress-bar {
width: 100%;
height: 20px;
background-color: #f0f0f0;
border-radius: 10px;
overflow: hidden;
}
#progress {
width: 0;
height: 100%;
background-color: #4caf50;
transition: width 0.3s ease;
}
#status {
margin-top: 0.5rem;
text-align: center;
}
</style>

CSS создает центрированную форму, похожую на карточку, со стилизованным вводом файла, кнопкой отправки и индикатором выполнения.

JavaScript для пакетной загрузки

Теперь реализуем JavaScript, который будет обрабатывать процесс пакетной загрузки:

<script>
const form = document.getElementById('upload-form');
const progressContainer = document.getElementById('progress-container');
const progressBar = document.getElementById('progress');
const statusElement = document.getElementById('status');

form.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(form);
const file = formData.get('video');

if (!file) {
alert('Please select a video file');
return;
}

if (file.size > 501 * 1024 * 1024) { // 500 МБ в байтах
alert('The file size exceeds 500MB. Please upload a smaller file.');
return;
}

progressContainer.style.display = 'block';
statusElement.textContent = 'Uploading...';

try {
await uploadFileInChunks(file);
statusElement.textContent = 'Upload successful!';
} catch (error) {
console.error('Error:', error);
statusElement.textContent = 'Upload failed. Please try again.';
}
});

async function uploadFileInChunks(file) {
const chunkSize = 10 * 1024 * 1024; // 10 МБ фрагментов
const chunks = Math.ceil(file.size / chunkSize);

for (let start = 0; start < file.size; start += chunkSize) {
const chunk = file.slice(start, start + chunkSize);
const formData = new FormData();
formData.append('video', chunk, file.name);
formData.append('chunk', Math.floor(start / chunkSize));
formData.append('totalChunks', chunks);
formData.append('originalname', file.name);

const response = await fetch('http://localhost:3000/api/upload', {
method: 'POST',
body: formData,
});

if (!response.ok) {
throw new Error('Chunk upload failed');
}

const progress = ((start + chunk.size) / file.size) * 100;
progressBar.style.width = `${progress}%`;
statusElement.textContent = `Uploading... ${Math.round(progress)}%`;
}
}
</script>

Разберем эту реализацию JavaScript.

1. Обработчик отправки формы:

  • предотвращает отправку формы по умолчанию;
  • проверяет, выбран ли файл и соответствует ли он ограничениям по размеру (500 МБ);
  • отображает контейнер прогресса и инициирует процесс загрузки.

2. Функция загрузки фрагментов (uploadFileInChunks):

  • определяет размер фрагмента в 10 МБ;
  • вычисляет общее количество фрагментов на основе размера файла;
  • итеративно просматривает файл, разбивая его на фрагменты;
  • для каждого фрагмента создает новый объект FormData и добавляет в него необходимую информацию (сам фрагмент, номер фрагмента, общее количество фрагментов, оригинальное имя файла);
  • отправляет каждый фрагмент на сервер с помощью запроса fetch;
  • обновляет индикатор выполнения и сообщение о состоянии после каждой успешной загрузки фрагмента.

3. Обработка ошибок:

  • если какой-либо фрагмент не удается загрузить, выдается ошибка;
  • ошибка перехватывается обработчиком отправки, который соответствующим образом обновляет сообщение о статусе.

4. Визуализация прогресса:

  • для отображения прогресса загрузки используется простой индикатор выполнения;
  • сообщение о состоянии обновляется текущим процентом завершенной загрузки.

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

Собираем все вместе

Теперь, рассмотрев реализацию бэкенда и фронтенда, воссоздадим работу всей системы.

  1. Пользователь выбирает видеофайл с помощью HTML-формы.
  1. Когда форма отправлена, код JavaScript проверяет размер файла и запускает процесс его загрузки с разбиением на фрагменты.
  1. На стороне клиента файл делится на фрагменты размером 10 МБ.
  1. Каждый фрагмент отправляется на сервер в виде отдельного HTTP-запроса вместе с метаданными о фрагменте и файле в целом.
  1. Сервер (Node.js с Express) получает каждый фрагмент и сохраняет его во временном каталоге.
  1. Когда все фрагменты получены, сервер объединяет их в один файл.
  1. На протяжении всего процесса JavaScript на стороне клиента обновляет индикатор выполнения и сообщение о состоянии.
  1. Если загрузка прошла успешно, пользователь получает уведомление. Если произошла ошибка, выводится сообщение об ошибке.

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

Лучшие практики и рекомендации

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

1. Безопасность:

  • Внедрите аутентификацию пользователей, чтобы только авторизованные пользователи могли загружать файлы.
  • Используйте HTTPS для шифрования данных при передаче.
  • Проверяйте и деперсонифицируйте все входящие данные на стороне сервера.

2. Проверка файлов:

  • Реализуйте более надежную проверку типов файлов на стороне сервера — возможно, с использованием библиотек типа file-type.
  • Рассмотрите возможность проверки загружаемых файлов на наличие вирусов или вредоносного ПО.

3. Обработка ошибок и восстановление:

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

4. Масштабируемость:

  • Для приложений с высоким трафиком используйте облачное хранилище, например Amazon S3, для хранения файлов.
  • Реализуйте систему очередей для обработки загрузок, чтобы справиться со сценариями с большим количеством одновременных загрузок.

5. Пользовательский опыт:

  • Добавьте возможность отмены текущих загрузок.
  • Реализуйте интерфейс перетаскивания для выбора файлов.
  • Предоставьте более подробную информацию о скорости загрузки и предполагаемом оставшемся времени.

6. Бэкенд-обработка:

  • Реализуйте систему обработки загруженных видео (например, перекодирование, создание миниатюр) с помощью очереди фоновых заданий.

7. Очистка:

  • Реализуйте надежную систему очистки временных файлов и неудачных загрузок.

8. Тестирование:

  • Тщательно протестируйте систему при различных размерах файлов, сетевых условиях и одновременных загрузках.

9. Мониторинг и логгинг:

  • Внедрите всесторонний логгинг для отслеживания загрузок и диагностики проблем.
  • Настройте мониторинг для оповещения о неудачных загрузках или системных проблемах.

Заключение

Обработка больших загрузок видео — сложная, но важная задача во многих современных веб-приложениях. Реализовав систему загрузки файлов с разбивкой их на фрагменты с помощью Node.js и современного JavaScript, можно преодолеть многие из традиционных проблем, связанных с загрузкой больших файлов.

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

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

Следуя описанному подходу и учитывая лучшие практики, вы сможете успешно обрабатывать загрузки больших видео в приложениях Node.js.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Suneel Kumar: Uploading Large Videos with Node.js: A Comprehensive Guide

Предыдущая статья5 продвинутых операторов Kubernetes, о которых должен знать каждый инженер DevOps