Система загрузки файлов

На интервью на лида важно не начать с “загружаем файл в S3”, а показать, что
система загрузки файлов — это не просто upload endpoint, а контур: приём
файла, хранение, метаданные, права доступа, проверка, обработка, ретраи,
безопасность, лимиты, lifecycle, скачивание и наблюдаемость.

Хороший ответ можно строить так.


1. Сначала уточняю требования

Я бы начал с вопросов:

“Сначала я уточню характер файлов и требования к загрузке, потому что
загрузка аватарок, банковских документов, видео и медицинских файлов — это
разные системы.”

Я бы спросил:

  • Какие файлы загружаем: изображения, PDF, видео, архивы, документы?
  • Максимальный размер файла?
  • Средний размер файла?
  • Сколько загрузок в день / час / секунду?
  • Нужна ли resumable upload, то есть продолжение загрузки после обрыва?
  • Нужна ли multipart upload для больших файлов?
  • Кто имеет доступ к файлам?
  • Файлы публичные или приватные?
  • Нужна ли антивирусная проверка?
  • Нужна ли обработка после загрузки: thumbnails, transcoding, OCR, parsing?
  • Нужно ли хранить версии файлов?
  • Нужно ли удаление по TTL / lifecycle policy?
  • Нужен ли audit trail?
  • Какие требования по compliance: GDPR, PII, encryption, data retention?
  • Нужна ли CDN-раздача?
  • Нужно ли ограничивать типы файлов?
  • Что считаем успешной загрузкой: файл попал в storage или файл ещё и прошёл
    проверку?

И дальше можно сказать:

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


2. Главная архитектурная идея

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

Плохой базовый вариант:

Client -> Backend -> File Storage

Почему плохо:

  • backend становится бутылочным горлышком;
  • растёт нагрузка на сеть и память;
  • сложнее масштабировать;
  • большие файлы могут убивать API-инстансы;
  • дороже гонять трафик через приложение.

Лучше:

Client
  |
  | 1. request upload session
  v
Backend API
  |
  | 2. create metadata + presigned URL
  v
Object Storage

Client
  |
  | 3. upload file directly
  v
Object Storage

Object Storage Event
  |
  v
Queue
  |
  v
Processing Workers

То есть backend управляет правами, метаданными, upload session и
состоянием
, но сам файл напрямую идёт в object storage: S3, Google Cloud
Storage, Azure Blob Storage, MinIO.


3. Базовые компоненты системы

Client
  |
  v
Upload API
  |
  +--> Metadata DB
  +--> Auth / Permission Service
  +--> Object Storage
  +--> Message Queue
          |
          v
     Processing Workers
          |
          +--> Antivirus Scanner
          +--> Thumbnail Generator
          +--> Metadata Extractor
          +--> Transcoding / OCR / Parsing
          |
          v
     File Status Updater

Основные компоненты:

  1. Upload API — создаёт upload session, проверяет права, выдаёт presigned
    URL.
  2. Object Storage — хранит binary content.
  3. Metadata DB — хранит статус, владельца, размер, MIME type, storage key,
    checksum.
  4. Queue — запускает асинхронную обработку после загрузки.
  5. Workers — проверяют файл, сканируют, генерируют preview, обновляют
    статус.
  6. Download API — контролирует доступ при скачивании.
  7. CDN — для публичных или часто скачиваемых файлов.
  8. Audit / Observability — логи, метрики, tracing.

4. Основной upload flow

Шаг 1. Клиент создаёт upload session

POST /files/upload-session

Пример запроса:

{
  "fileName": "contract.pdf",
  "contentType": "application/pdf",
  "size": 10485760,
  "checksum": "sha256:abc123",
  "context": {
    "projectId": "project-123"
  }
}

Backend проверяет:

  • пользователь авторизован;
  • пользователь имеет право загружать файл в этот project / workspace;
  • размер не превышает лимит;
  • MIME type разрешён;
  • quota не превышена;
  • filename безопасный;
  • checksum валиден по формату.

После этого backend создаёт запись в базе:

files
- id
- owner_id
- project_id
- original_file_name
- storage_key
- content_type
- size
- checksum
- status
- created_at
- updated_at

Статус сначала:

PENDING_UPLOAD

И возвращает клиенту presigned URL:

{
  "fileId": "file-789",
  "uploadUrl": "https://storage-provider.com/presigned-url",
  "expiresAt": "2026-06-14T21:30:00Z",
  "requiredHeaders": {
    "Content-Type": "application/pdf"
  }
}

Шаг 2. Клиент загружает файл напрямую в object storage

Client -> Object Storage

Backend в этот момент не держит файл у себя.

Это важный архитектурный пункт:

“Я бы предпочёл direct upload to object storage через presigned URL. Backend
остаётся control plane, а object storage — data plane. Так мы разделяем
управление и передачу тяжёлых данных.”


Шаг 3. Storage сообщает о завершении загрузки

Есть два варианта.

Вариант 1. Клиент сам сообщает backend

POST /files/{fileId}/complete

Backend проверяет, что объект реально появился в storage:

HEAD object

Проверяет:

  • размер;
  • content type;
  • checksum;
  • storage key;
  • upload session not expired.

Потом переводит файл в статус:

UPLOADED

И кладёт событие в очередь:

file.uploaded

Вариант 2. Object storage сам публикует событие

Например:

S3 ObjectCreated event -> Queue -> Worker

Это надёжнее для server-side обнаружения загрузки, но всё равно часто
оставляют /complete, чтобы клиент мог быстрее получить статус.


5. Статусы файла

Я бы явно заложил state machine.

PENDING_UPLOAD
UPLOADED
PROCESSING
READY
FAILED
REJECTED
DELETED

Расшифровка:

PENDING_UPLOAD — upload session создана, файл ещё не загружен
UPLOADED       — файл появился в object storage
PROCESSING     — идёт проверка / обработка
READY          — файл доступен пользователю
FAILED         — техническая ошибка обработки
REJECTED       — файл запрещён: вирус, неправильный тип, policy violation
DELETED        — файл удалён логически или физически

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

Хорошая фраза для интервью:

“Я бы разделил факт загрузки и факт готовности файла. Uploaded не равно
Ready. Файл может быть загружен в storage, но ещё не прошёл security scan,
preview generation или business validation.”


6. Metadata DB

Хранилище метаданных может быть PostgreSQL.

Пример таблицы:

files
- id
- owner_id
- workspace_id
- original_file_name
- normalized_file_name
- storage_bucket
- storage_key
- content_type
- detected_content_type
- size
- checksum_sha256
- status
- visibility
- created_at
- updated_at
- deleted_at

Дополнительно:

file_versions
- id
- file_id
- version
- storage_key
- size
- checksum_sha256
- created_at

file_processing_jobs
- id
- file_id
- job_type
- status
- attempt_count
- error_message
- created_at
- updated_at

file_access_audit
- id
- file_id
- user_id
- action
- ip
- user_agent
- created_at

7. Object Storage

Для binary content я бы использовал object storage:

S3 / GCS / Azure Blob / MinIO

Почему не база данных:

  • файлы могут быть большими;
  • object storage дешевле;
  • проще lifecycle policy;
  • проще CDN;
  • выше durability;
  • удобнее multipart upload;
  • не раздуваем основную transactional DB.

Storage key лучше делать не по имени файла, а по внутреннему ID:

/workspace/{workspaceId}/files/{fileId}/original

Или:

/files/2026/06/14/{fileId}

Не стоит использовать пользовательский filename как путь, потому что:

  • могут быть спецсимволы;
  • могут быть коллизии;
  • могут быть path traversal-попытки;
  • имя файла может содержать чувствительные данные.

8. Большие файлы и multipart upload

Если файлы маленькие, достаточно простого presigned PUT.

Но для больших файлов нужен multipart upload:

1. Client requests multipart upload session
2. Backend creates multipart upload in storage
3. Backend returns presigned URLs for parts
4. Client uploads parts in parallel
5. Client calls complete multipart upload
6. Storage assembles object
7. Backend verifies object and updates status

API может быть такой:

POST /files/multipart/start
POST /files/{fileId}/multipart/parts
POST /files/{fileId}/multipart/complete
POST /files/{fileId}/multipart/abort

На интервью:

“Для больших файлов я бы использовал multipart upload с presigned URLs. Это
даёт параллельную загрузку, возможность ретраить отдельные части и не
начинать весь upload заново при сетевом сбое.”


9. Resumable upload

Если требуется продолжение загрузки после обрыва, есть варианты:

Multipart upload
TUS protocol
Custom chunk upload

Если строить своё:

upload_sessions
- id
- file_id
- user_id
- status
- total_size
- uploaded_parts
- expires_at

Клиент может спросить:

GET /files/{fileId}/upload-status

Ответ:

{
  "fileId": "file-789",
  "uploadedParts": [1, 2, 3, 7],
  "missingParts": [4, 5, 6, 8]
}

Но на интервью хорошо сказать:

“Я бы не изобретал resumable upload без необходимости. Если object storage
уже поддерживает multipart upload, лучше использовать его механизм, а не
строить собственный протокол передачи байтов через backend.”


10. Проверка безопасности

После загрузки файл не сразу становится доступен.

Pipeline:

UPLOADED
  -> PROCESSING
  -> Antivirus scan
  -> MIME type detection
  -> File extension validation
  -> Optional content validation
  -> READY or REJECTED

Проверки:

  • антивирус;
  • реальный MIME type, а не только заголовок от клиента;
  • расширение файла;
  • размер;
  • checksum;
  • запрет executable content;
  • запрет архивов с zip bomb;
  • проверка количества файлов внутри архива;
  • проверка path traversal внутри архивов;
  • ограничение PDF/scripts/macros, если нужно;
  • PII / DLP-проверки, если домен требует.

Очень важная фраза:

“Я бы не доверял content type, который прислал клиент. Его нужно считать
user input. Реальный тип файла должен определяться server-side.”


11. Обработка файлов

После security scan можно делать обработку.

Для изображений:

thumbnail generation
image resizing
EXIF extraction
EXIF cleanup

Для видео:

transcoding
preview generation
duration extraction

Для документов:

PDF preview
OCR
text extraction
indexing for search

Для аудио:

transcription
duration extraction
waveform generation

Это должно быть асинхронно:

file.uploaded -> queue -> workers -> update metadata

Если обработка тяжёлая, можно разделить очереди:

file-antivirus-queue
file-thumbnail-queue
file-ocr-queue
file-transcoding-queue

12. Доступ к файлам

Скачивание тоже не должно быть просто:

GET /files/{fileId}/download

Backend должен:

  • проверить авторизацию;
  • проверить права пользователя;
  • проверить статус READY;
  • проверить, что файл не удалён;
  • проверить tenant/workspace isolation;
  • создать short-lived presigned download URL.

Flow:

Client -> Download API
Download API -> Auth check
Download API -> DB metadata check
Download API -> generate presigned URL
Client -> Object Storage

На интервью:

“Для приватных файлов я бы не делал bucket публичным. Backend выдаёт
короткоживущие signed URLs только после проверки прав.”

Для публичных файлов можно использовать CDN:

CDN -> Object Storage

Но даже там можно использовать signed cookies / signed URLs, если доступ
ограниченный.


13. Авторизация и права доступа

Нужны уровни:

owner
workspace member
admin
public
shared by link
temporary access

В metadata можно хранить:

visibility:
- private
- workspace
- public
- shared_link

Отдельно можно хранить ACL:

file_permissions
- file_id
- subject_type: user / group / workspace
- subject_id
- permission: read / write / delete / owner

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

project -> task -> attachment

То есть если файл прикреплён к задаче, доступ к файлу определяется доступом к
задаче.

Хорошая фраза:

“Я бы старался не дублировать сложную permission model внутри file service.
Если файл является attachment к business entity, доступ лучше вычислять
через доступ к этой entity, а file service использовать как storage/control
layer.”


14. Идемпотентность

При загрузке файлов тоже нужна идемпотентность.

Проблемы:

  • клиент повторил создание upload session;
  • сеть оборвалась;
  • клиент не получил ответ;
  • /complete вызван два раза;
  • storage event пришёл дважды;
  • worker обработал одно событие дважды.

Поэтому:

idempotency_key
unique(owner_id, idempotency_key)

Для /complete:

if status already READY / PROCESSING / UPLOADED:
    return current state

Для workers:

processing job unique(file_id, job_type)

Фраза:

“Я бы проектировал обработку как at-least-once, но идемпотентную. Storage
events и queue messages могут приходить повторно, поэтому duplicate event не
должен создавать duplicate file или duplicate processing.”


15. Consistency model

Система загрузки файлов почти всегда eventually consistent.

Например:

File uploaded to storage
Metadata updated later
Scan completed later
Preview generated later
Search index updated later

Важно честно разделить состояния.

Пользователь может видеть:

Uploading...
Processing...
Ready
Failed
Rejected

На интервью:

“Я бы не пытался делать всю цепочку синхронной. Успешный upload означает,
что файл принят системой. А доступность файла для использования наступает
после асинхронной проверки и обработки.”


16. Cleanup и orphan files

Один из важных, часто забываемых пунктов.

Проблемы:

metadata created, but file never uploaded
file uploaded, but /complete not called
multipart upload started, but never completed
processing failed and left temp files
user deleted file, but object remained in storage

Нужны cleanup jobs:

delete expired PENDING_UPLOAD sessions
abort stale multipart uploads
delete orphan objects
delete temporary processing files
apply retention policy

Например:

PENDING_UPLOAD older than 24 hours -> mark expired -> delete object if exists
multipart upload older than 24 hours -> abort
DELETED older than 30 days -> physical delete

Фраза:

“Для file upload system я бы обязательно заложил cleanup-механику. Иначе
object storage со временем превращается в кладбище orphan files.”


17. Versioning

Если нужны версии файлов:

file_id = logical file
file_version_id = physical uploaded object

Пример:

files
- id
- current_version_id
- owner_id
- status

file_versions
- id
- file_id
- version_number
- storage_key
- checksum
- size
- created_at

Тогда можно:

  • откатиться на старую версию;
  • хранить историю;
  • сравнивать версии;
  • не ломать ссылки на logical file.

Если версии не нужны, можно не усложнять MVP.


18. Deduplication

Если много одинаковых файлов, можно делать дедупликацию по checksum:

checksum_sha256
size
content_type

Но осторожно:

  • нельзя раскрывать пользователю факт, что такой файл уже есть у другого
    пользователя;
  • права доступа всё равно отдельные;
  • физический объект может быть один, но logical file records разные;
  • при удалении одного logical file нельзя удалить physical object, если на
    него есть ссылки.

На интервью можно сказать:

“Дедупликацию я бы рассматривал как оптимизацию второго этапа. В MVP
достаточно хранить checksum для integrity check и будущей дедупликации.”


19. Rate limiting и quotas

Нужно ограничивать:

max file size
max files per user
max total storage per user/workspace
uploads per minute
bandwidth
number of active upload sessions
number of multipart parts

Иначе один пользователь может:

  • забить storage;
  • создать слишком много upload sessions;
  • перегрузить processing workers;
  • сгенерировать большой счёт за storage/egress.

20. Storage lifecycle

Нужны политики:

temporary uploads -> delete after 24h
deleted files -> hard delete after 30 days
old versions -> move to cold storage
logs/audit -> retain according to policy
processed previews -> regenerate or store

Для S3/GCS/Azure Blob можно использовать lifecycle rules.


21. CDN

Если файлы часто скачиваются или публичные:

Client -> CDN -> Object Storage

Для приватного доступа:

signed CDN URL
signed cookies
short TTL

Для аватарок, изображений, публичных документов CDN сильно снижает нагрузку
и latency.

Но для чувствительных файлов CDN нужно использовать аккуратно:

  • короткий TTL;
  • запрет кэширования приватных файлов без контроля;
  • signed URLs;
  • cache invalidation при удалении/замене.

22. Observability

Метрики:

upload_sessions_created_total
uploads_completed_total
uploads_failed_total
upload_latency
processing_latency
files_ready_total
files_rejected_total
antivirus_failures_total
queue_lag
storage_errors_total
presigned_url_generation_errors_total
orphan_files_count
multipart_abandoned_count
download_requests_total
download_denied_total

Логи:

fileId
uploadSessionId
userId
workspaceId
storageKey
status
contentType
size
checksum
correlationId
errorCode

Алерты:

processing queue lag grows
antivirus scanner unavailable
storage error rate grows
too many rejected files
orphan files grow
upload completion rate drops
download 403 spike

Фраза:

“В file upload system observability критична, потому что пользователь видит
только ‘загрузка зависла’ или ‘файл не открылся’. Без статусов, correlation
id, queue lag и истории обработки команда будет слепой.”


23. Failure scenarios

Клиент начал upload, но не закончил

PENDING_UPLOAD expires
cleanup job deletes session
multipart upload aborted

Файл загрузился, но backend не получил complete

storage event detects object
or reconciliation job checks storage
status updated server-side

Storage event пришёл дважды

idempotent handler
same fileId + storageKey
no duplicate processing

Worker упал во время обработки

message redelivery
processing job status
retry with backoff
DLQ after max attempts

Антивирус недоступен

do not mark file READY
keep PROCESSING or FAILED_TEMPORARY
retry later
alert team

Файл оказался вредоносным

status = REJECTED
quarantine or delete object
audit event
notify user if needed

Пользователь пытается скачать файл до проверки

deny download
return status PROCESSING

Presigned URL истёк

client requests new upload URL
same upload session if still valid

Пользователь загрузил файл с расширением .jpg, но внутри executable

server-side MIME detection
reject mismatch by policy

24. Security

Обязательно:

authentication
authorization
short-lived presigned URLs
private bucket
encryption at rest
encryption in transit
server-side MIME validation
virus scan
file size limits
extension allowlist
PII masking in logs
audit trail
tenant isolation
least privilege IAM

Особенно важно:

“Storage bucket не должен быть публичным по умолчанию. Upload и download
должны происходить через short-lived signed URLs, а права должны
проверяться backend-ом.”

Также нельзя логировать:

  • полные presigned URLs;
  • чувствительные filenames;
  • содержимое файлов;
  • PII из payload.

25. API

Минимальный набор API:

POST /files/upload-session
POST /files/{fileId}/complete
GET  /files/{fileId}
GET  /files/{fileId}/download-url
DELETE /files/{fileId}
GET  /files?projectId=...

Для multipart:

POST /files/multipart/start
POST /files/{fileId}/multipart/parts
POST /files/{fileId}/multipart/complete
POST /files/{fileId}/multipart/abort

Для статуса:

GET /files/{fileId}/status

Пример ответа:

{
  "fileId": "file-789",
  "fileName": "contract.pdf",
  "status": "PROCESSING",
  "size": 10485760,
  "contentType": "application/pdf",
  "createdAt": "2026-06-14T20:15:00Z"
}

26. MVP

Для MVP я бы сделал:

Upload API
Metadata DB
Object Storage
Presigned upload URL
Presigned download URL
Basic status tracking
File size limit
Content type allowlist
Basic async worker
Basic antivirus scan if domain requires
Basic cleanup job
Basic metrics/logs

Не стал бы сразу делать:

advanced deduplication
complex ACL per file
full resumable custom protocol
multi-provider storage abstraction
AI/OCR/transcoding pipeline
full DLP
advanced lifecycle tiers

Но архитектуру надо оставить такой, чтобы это можно было добавить.


27. Короткий сильный ответ на интервью

Можно ответить так:

“Я бы проектировал file upload system как систему, где backend управляет
метаданными, правами и состояниями, но сами байты файла идут напрямую в
object storage через presigned URL. Это разделяет control plane и data plane
и снимает с backend тяжёлый трафик.

Flow такой: клиент запрашивает upload session, backend проверяет права,
лимиты, тип файла, создаёт metadata record со статусом PENDING_UPLOAD и
выдаёт presigned URL. Клиент загружает файл напрямую в object storage.
После completion backend или storage event переводит файл в UPLOADED, кладёт
событие в очередь, workers выполняют antivirus scan, MIME detection,
preview generation или другую обработку. Только после этого файл получает
статус READY.

Для больших файлов я бы использовал multipart upload, чтобы можно было
ретраить отдельные части и продолжать загрузку после сетевых сбоев. Для
скачивания backend проверяет права и выдаёт short-lived presigned download
URL. Bucket остаётся private.

Отдельно я бы заложил idempotency, cleanup для orphan uploads, retry/DLQ
для processing jobs, rate limits, quotas, audit trail, encryption,
observability и lifecycle policies.”


28. Самая сильная финальная формулировка

“Система загрузки файлов — это не endpoint, который принимает
multipart/form-data. Это контур управления жизненным циклом файла: upload
session, storage, metadata, access control, security scan, processing,
status machine, download authorization, cleanup, observability и retention.
Главные риски здесь — потерять файл, дать доступ не тому пользователю,
принять вредоносный файл, положить backend большими upload-ами, накопить
orphan objects или сделать файл доступным до проверки. Поэтому я бы строил
систему вокруг object storage, presigned URLs, metadata DB, очередей,
асинхронной обработки, идемпотентности и строгого контроля доступа.”

Прокрутить вверх