Kravchenko

Web Lab

АудитБлогКонтакты

Kravchenko

Web Lab

Разрабатываем сайты и автоматизацию на современных фреймворках под ключ

Услуги
ЛендингМногостраничныйВизитка
E-commerceБронированиеПортфолио
Навигация
БлогКонтактыАудит
Обратная связь
+7 921 567-11-16
info@kravlab.ru
с 09:00 до 18:00

© 2026 Все права защищены

•

ИП Кравченко Никита Владимирович

•

ОГРНИП: 324784700339743

Политика конфиденциальности

Загрузки файлов без боли: прямой аплоуд в хранилище, части, подписи URL и антивирус — меньше сбоев и дешевле инфраструктура

Разработка и технологии19 апреля 2026 г.
Как перестать «прокачивать» гигабайты через бэкенд, ускорить загрузки для клиентов и снизить расходы на трафик и сервера. Разбираем архитектуру прямой загрузки в облачное хранилище, надёжную докачку частями, проверки целостности и антивирусный конвейер — с примерами кода и чек‑листом внедрения.
Загрузки файлов без боли: прямой аплоуд в хранилище, части, подписи URL и антивирус — меньше сбоев и дешевле инфраструктура

Содержание

  • Зачем бизнесу менять подход к загрузкам
  • Целевая архитектура: кто за что отвечает
  • Безопасность: не пускать мусор и вредонос
  • Клиент: надёжная загрузка частями с докачкой
  • Бэкенд: подписи URL, политики и контроль целостности
  • Антивирус и обработка файлов в фоне
  • Наблюдаемость и SLA: что мерить
  • Стоимость: где экономим и где нельзя экономить
  • План внедрения по шагам
  • Чек-лист готовности в прод

Зачем бизнесу менять подход к загрузкам

Когда пользователи заливают большие файлы «через бэкенд», ваши сервера превращаются в дорогой прокси: двукратный трафик (клиент → бэкенд → хранилище), пиковая нагрузка на CPU/память, очереди и таймауты. Ошибки сети и разрывы на 92% случаев — и пользователь начинает заново.

Что даёт переход на прямые загрузки в облачное хранилище (например, S3‑совместимое):

  • Меньше сбоев — докачка файла по частям и возобновление после обрыва.
  • Быстрее для клиента — соединение идёт до ближайшей точки провайдера (дополнительно помогает CDN/акселерация).
  • Дешевле — бэкенд перестаёт качать гигабайты, уменьшаются нужны по вертикальному масштабированию, снижается egress/ingress на серверах.
  • Безопаснее — вредоносные файлы изолируются в «карантине», к основной системе не попадают.
  • Предсказуемые SLA — измеримые этапы и точки контроля (подпись, загрузка, проверка, публикация).

Целевая архитектура: кто за что отвечает

Идеальная схема — бэкенд не переводит байты, а выдаёт короткоживущие подписи и оркестрирует процесс.

Клиент (браузер/моб) ── запрос на загрузку ─▶ Бэкенд (API)
                                    │ генерирует подписи (коротко живут)
                                    ▼
                            Облачное хранилище (S3)
                                    │ событие о новом объекте
                                    ▼
                Очередь (SQS/пабсаб) → Сканер (антивирус) → Трансформеры (превью/извлечь мета)
                                    │                        │
                                    └─────────── статус/вебхук/газетка в БД ◀────────┘

Ключевые принципы:

  • Прямая загрузка из клиента в S3 по подписанным URL.
  • Загрузка частями (многосегментная), докачка после обрыва.
  • Контроль целостности через контрольные суммы.
  • Антивирусное сканирование и «карантин» до публикации.
  • Чёткое разделение бакетов/префиксов: incoming (карантин) → safe (прошёл проверку).

Безопасность: не пускать мусор и вредонос

  • Подписанные URL живут минуты, действия ограничены (только PUT конкретного ключа и части).
  • Ограничиваем размер и тип содержимого, запрещаем исполняемые форматы при необходимости.
  • Всё попадает сначала в карантин (префикс/bucket). Без отметки «scan=clean» файл недоступен клиентам.
  • Встроенное шифрование в хранилище (например, SSE‑S3/SSE‑KMS).
  • Прозрачное логирование: кто запросил подпись, для какого файла, откуда грузили.

Клиент: надёжная загрузка частями с докачкой

Пример браузерного кода: многосегментная загрузка в S3 с докачкой и контрольной суммой SHA‑256. Сервер выдаёт ID загрузки и подписанные URL по запросу на каждую часть.

// client/upload.js
// Минимальный пример: делим файл на части по 8 МБ, возобновляем с нужной части.
// Требования: бэкенд даёт API: /uploads/init, /uploads/part-url, /uploads/complete

const CHUNK_SIZE = 8 * 1024 * 1024;

async function sha256(file) {
  const buf = await file.arrayBuffer();
  const digest = await crypto.subtle.digest('SHA-256', buf);
  return btoa(String.fromCharCode(...new Uint8Array(digest)));
}

async function initUpload(fileName, contentType, size, checksum) {
  const res = await fetch('/uploads/init', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ fileName, contentType, size, checksum })
  });
  if (!res.ok) throw new Error('init failed');
  return res.json(); // { uploadId, key }
}

async function getPartUrl(uploadId, partNumber) {
  const res = await fetch(`/uploads/part-url?uploadId=${encodeURIComponent(uploadId)}&partNumber=${partNumber}`);
  if (!res.ok) throw new Error('part-url failed');
  return res.json(); // { url }
}

async function completeUpload(uploadId, parts) {
  const res = await fetch('/uploads/complete', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ uploadId, parts }) // parts: [{PartNumber, ETag}]
  });
  if (!res.ok) throw new Error('complete failed');
  return res.json(); // { status: 'ok', key }
}

export async function uploadFile(file, onProgress) {
  const checksumB64 = await sha256(file);
  const { uploadId, key } = await initUpload(file.name, file.type || 'application/octet-stream', file.size, checksumB64);

  const totalParts = Math.ceil(file.size / CHUNK_SIZE);
  const uploadedParts = [];

  for (let partNumber = 1; partNumber <= totalParts; partNumber++) {
    const start = (partNumber - 1) * CHUNK_SIZE;
    const end = Math.min(start + CHUNK_SIZE, file.size);
    const blob = file.slice(start, end);

    const { url } = await getPartUrl(uploadId, partNumber);

    const resp = await fetch(url, {
      method: 'PUT',
      body: blob,
      headers: {
        // Если включены контрольные суммы частей на S3:
        // 'x-amz-checksum-sha256': await sha256(blob)
      }
    });

    if (!resp.ok) throw new Error(`part ${partNumber} upload failed`);

    const etag = resp.headers.get('ETag');
    uploadedParts.push({ PartNumber: partNumber, ETag: etag?.replaceAll('"', '') });

    onProgress?.({ uploaded: end, total: file.size, partNumber, totalParts });
  }

  const result = await completeUpload(uploadId, uploadedParts);
  return { key: result.key };
}

Советы по UX:

  • Храним состояние прогресса в localStorage — после перезагрузки страницы продолжаем с нужной части.
  • Показываем оценку времени и скорость, даём кнопку «Пауза/Продолжить».
  • Для мобильных сетей держим части 5–8 МБ, для стационарных — 16–32 МБ.

Бэкенд: подписи URL, политики и контроль целостности

Пример на Node.js (AWS SDK v3). Бэкенд хранит служебные данные загрузки (uploadId, key, ожидаемая сумма) в БД.

// server/uploads.js
import { S3Client, CreateMultipartUploadCommand, UploadPartCommand, CompleteMultipartUploadCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import express from 'express';

const s3 = new S3Client({ region: process.env.AWS_REGION });
const BUCKET = process.env.UPLOAD_BUCKET; // карантинный бакет/префикс

// Заглушка-хранилище для демо. В проде — БД.
const store = new Map();

const router = express.Router();

router.post('/uploads/init', async (req, res) => {
  const { fileName, contentType, size, checksum } = req.body;
  if (!fileName || !size) return res.status(400).json({ error: 'bad input' });

  // Генерируем уникальный ключ (например, с пространством по арендаторам)
  const tenant = req.headers['x-tenant'] || 'public';
  const key = `${tenant}/incoming/${Date.now()}_${encodeURIComponent(fileName)}`;

  // Инициируем многосегментную загрузку. Вешаем метаданные для сканера.
  const cmd = new CreateMultipartUploadCommand({
    Bucket: BUCKET,
    Key: key,
    ContentType: contentType || 'application/octet-stream',
    // В новых регионах можно включить встроенные контрольные суммы:
    // ChecksumAlgorithm: 'SHA256',
    Metadata: {
      'expected-sha256': checksum || '',
      'tenant': tenant,
      'state': 'pending-scan'
    },
    ServerSideEncryption: 'AES256'
  });
  const out = await s3.send(cmd);
  const uploadId = out.UploadId;

  // Сохраняем сессию загрузки
  store.set(uploadId, { key, expectedSha256: checksum, size, parts: [] });
  res.json({ uploadId, key });
});

router.get('/uploads/part-url', async (req, res) => {
  const { uploadId, partNumber } = req.query;
  if (!store.has(uploadId)) return res.status(404).json({ error: 'not found' });
  const { key } = store.get(uploadId);

  const cmd = new UploadPartCommand({ Bucket: BUCKET, Key: key, UploadId: uploadId, PartNumber: Number(partNumber) });
  const url = await getSignedUrl(s3, cmd, { expiresIn: 60 * 10 }); // 10 минут

  res.json({ url });
});

router.post('/uploads/complete', async (req, res) => {
  const { uploadId, parts } = req.body;
  const session = store.get(uploadId);
  if (!session) return res.status(404).json({ error: 'not found' });

  const cmd = new CompleteMultipartUploadCommand({
    Bucket: BUCKET,
    Key: session.key,
    UploadId: uploadId,
    MultipartUpload: { Parts: parts }
  });
  await s3.send(cmd);

  // Записываем в БД статус «ожидает сканирования». Публикация — после скана.
  store.delete(uploadId);
  res.json({ status: 'ok', key: session.key });
});

export default router;

Минимальная IAM‑политика для роли бэкенда (ограничиваемся нужным префиксом):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:CreateMultipartUpload",
        "s3:UploadPart",
        "s3:AbortMultipartUpload",
        "s3:CompleteMultipartUpload",
        "s3:PutObjectTagging",
        "s3:PutObject"
      ],
      "Resource": [
        "arn:aws:s3:::my-bucket",
        "arn:aws:s3:::my-bucket/*"
      ]
    }
  ]
}

Контроль целостности:

  • Простой путь: клиент присылает базовую64‑сумму SHA‑256, мы кладём её в метаданные и сверяем в сканере.
  • Более строго: использовать заголовок x-amz-checksum-sha256 на каждую часть (поддерживается не во всех регионах; проверяйте документацию провайдера).

Антивирус и обработка файлов в фоне

Файлы попадают в «карантин» (incoming). Событие о новом объекте идёт в очередь. Рабочий контейнер скачивает файл, прогоняет через антивирус (например, ClamAV), при успехе копирует в «безопасный» префикс и помечает в БД. При провале — помечает «infected» и удаляет или изолирует.

Пример обработчика на Python для AWS Lambda (подойдёт для файлов до нескольких сотен мегабайт при увеличенном tmp‑томе). Для больших — перенесите логику в ECS/Fargate.

# lambda_scan.py
import os
import json
import subprocess
import boto3

s3 = boto3.client('s3')
dynamodb = boto3.resource('dynamodb')
TABLE = os.environ.get('UPLOADS_TABLE')  # для хранения статусов
SAFE_BUCKET = os.environ.get('SAFE_BUCKET')

# В Лямбде используйте сборку слоя с ClamAV или контейнер с включённым ClamAV.

def handler(event, context):
    for record in event['Records']:
        s3info = record['s3']
        bucket = s3info['bucket']['name']
        key = s3info['object']['key']
        tmp_path = f"/tmp/{os.path.basename(key)}"

        # Скачиваем файл в /tmp
        s3.download_file(bucket, key, tmp_path)

        # Запускаем clamscan
        try:
            res = subprocess.run(["clamscan", "--stdout", tmp_path], capture_output=True, text=True)
            output = res.stdout
            infected = "FOUND" in output
        except Exception as e:
            infected = True
            output = str(e)

        # Сверяем контрольную сумму, если есть
        head = s3.head_object(Bucket=bucket, Key=key)
        expected = head['Metadata'].get('expected-sha256')
        if expected:
            import hashlib, base64
            h = hashlib.sha256()
            with open(tmp_path, 'rb') as f:
                for chunk in iter(lambda: f.read(1024 * 1024), b''):
                    h.update(chunk)
            actual_b64 = base64.b64encode(h.digest()).decode('utf-8')
            if actual_b64 != expected:
                infected = True
                output += "\nChecksum mismatch"

        # Обновляем статус в БД
        table = dynamodb.Table(TABLE)
        item_key = { 'object_key': key }

        if not infected:
            # Копируем в безопасный бакет/префикс
            dest_key = key.replace('/incoming/', '/safe/')
            s3.copy_object(Bucket=SAFE_BUCKET, Key=dest_key, CopySource={'Bucket': bucket, 'Key': key}, MetadataDirective='REPLACE', Metadata={ 'scan': 'clean' })
            # (по желанию) удаляем оригинал из карантина
            s3.delete_object(Bucket=bucket, Key=key)
            table.put_item(Item={ **item_key, 'status': 'clean', 'dest_key': dest_key })
        else:
            table.put_item(Item={ **item_key, 'status': 'infected', 'reason': output[-1000:] })
            # (по политике) либо удаляем, либо оставляем для разбирательства
            s3.delete_object(Bucket=bucket, Key=key)

    return { 'statusCode': 200 }

Рекомендации по сканированию:

  • Отделите очередь сканирования от очереди пост‑обработки (генерация превью, распаковка архивов, OCR), чтобы вредонос не попадал в обработчики.
  • Для крупных файлов используйте контейнеры с clamd (демон ClamAV) и горизонтальное масштабирование по длине очереди.
  • Логируйте хеши, размер, тип и исходный IP — это помогает отбивать злоупотребления.

Наблюдаемость и SLA: что мерить

  • Коэффициент успешных загрузок (по ключу/дню/арендатору).
  • Среднее/95‑й перцентиль времени на: подпись, загрузку, сканирование, публикацию.
  • Доля докачек (indirect метрика качества сети клиентов).
  • Объём трафика, проходящего через бэкенд (стремится к нулю) и через хранилище.
  • Очередь сканирования: длина и задержка — база для авто‑масштабирования.

Трассировка: один correlation‑id от инициализации до публикации. Кладём его в:

  • метаданные объекта в хранилище (x-amz-meta-correlation-id),
  • логи выдачи подписей,
  • сообщение в очередь,
  • запись статуса в БД.

Стоимость: где экономим и где нельзя экономить

Экономим:

  • Убираем двойной трафик через бэкенд.
  • Снижаем потребность в мощных инстансах API.
  • Включаем «жизненные циклы» в хранилище: авто‑удалять файлы в карантине старше N дней, чистить неуспешные загрузки.

Не экономим:

  • На проверках безопасности (антивирус, ограничения типов, шифрование).
  • На журналировании и метриках — это ваша страховка при инцидентах.
  • На доступности очереди и сканера: закладывайте избыточность.

План внедрения по шагам

  1. Карта требований: список типов файлов, максимальные размеры, кто и где их будет скачивать.
  2. Подготовка бакетов/префиксов: incoming (карантин), safe (прошёл скан), public/private (как будут потребляться).
  3. Бэкенд‑API для инициализации и завершения многосегментной загрузки. Храним сессию и ожидаемую сумму.
  4. Выдача подписей для частей с TTL 5–10 минут, проверка прав арендатора/пользователя.
  5. Клиентская библиотека с докачкой: обработка обрывов, пауза/продолжить, прогресс.
  6. Очередь событий от хранилища → сканер → статусы в БД. Запрет на публикацию до «clean».
  7. Публикация файлов: только из safe‑префикса. Если публичная раздача — через CDN, с корректными заголовками кэширования.
  8. Мониторинг: дашборды, алерты на рост очереди, падение доли успеха, рост времени сканирования.
  9. Миграция: временно поддерживать оба пути (через бэкенд и прямой) для отката. Переключить по фичефлагу на долю трафика, потом на 100%.
  10. Документация для поддержки: как найти файл по correlation‑id, как принудительно пометить файл, как посмотреть причину «infected».

Чек-лист готовности в прод

  • Подписанные URL ограничены по времени, по операции и по ключу.
  • Входные параметры валидируются: размер, тип, расширение, лимит по количеству одновременно активных загрузок на пользователя.
  • Части загружаются параллельно, но не больше N (настраивается), с бэкоффом и повтором при 5xx.
  • Контрольные суммы включены и сверяются.
  • Антивирусный контур изолирован от остальной обработки, предусмотрен карантин.
  • Жизненный цикл объектов настроен (авто‑удаление «битых» и старых файлов).
  • Метрики и алерты настроены, трассировка сквозная, логи доступны по запросу поддержки.
  • Докачка работает на мобильных сетях; максимально допустимое время загрузки и бездействия пользователя согласовано.
  • Политики доступа (IAM) минимально необходимые, ключи и роли хранятся безопасно.

Итог: перевод загрузок на прямую схему с частями и короткоживущими подписями снимает львиную долю проблем со стабильностью и затратами. При правильной изоляции и антивирусном конвейере вы ускоряете онбординг крупных клиентов и снижаете риски безопасности — это даёт прямую выгоду бизнесу и спокойствие команде.


безопасностьS3загрузка файлов