Все статьи

Как загружать и хранить файлы в веб-приложении: S3, MinIO, FastAPI и React

Полное руководство по File Upload, Object Storage, Presigned URL, Multipart Upload, безопасной загрузке файлов, обработке изображений и организации файлового хранилища в Production

29 минут чтения

Представим, что вы разрабатываете SaaS-платформу на React, FastAPI и PostgreSQL. Пользователи создают проекты, загружают документы, прикрепляют изображения к задачам и скачивают отчёты. Пока приложение небольшое, файлы можно сохранять в локальную папку сервера. Например: backend/ ├── app/ ├── uploads/ │ ├── avatar.png │ ├── report.pdf │ └── document.docx └── main.py Такой подход кажется удобным.

Но со временем появляются проблемы. Что произойдёт, если приложение запущено в трёх Docker-контейнерах? Как пользователь скачает файл, загруженный через другой сервер? Что случится с документами после пересоздания контейнера? Как ограничить размер загрузки? Как защититься от вредоносных файлов? Как организовать доступ к приватным документам?

И что делать, если пользователи начнут загружать видео размером несколько гигабайт? Для решения этих задач используется Object Storage для веб-приложений — объектное хранилище, предназначенное для работы с файлами через API.

В этой статье разберём загрузку файлов React FastAPI, Amazon S3 и MinIO, Presigned URL, Multipart Upload, безопасность, хранение метаданных в PostgreSQL и архитектуру файлового сервиса для Production.


1. Почему хранение файлов — отдельная архитектурная задача

На ранних этапах разработки файлы часто воспринимаются как обычные данные. Пользователь отправляет изображение, Backend сохраняет его на диск, а затем возвращает URL. Но файл отличается от типичной записи в PostgreSQL. Он может занимать сотни мегабайт. Его загрузка может прерваться. Он может содержать вредоносное содержимое.

Его необходимо хранить, резервировать, удалять и предоставлять только авторизованным пользователям. Поэтому архитектура хранения файлов Backend должна учитывать не только загрузку, но и полный жизненный цикл объекта. Загрузка | v Проверка | v Хранение | v Обработка | v Выдача доступа | v Удаление Каждый этап имеет собственные требования к безопасности и производительности.

2. Какие способы хранения файлов существуют

Способы хранения файлов в веб-приложении можно разделить на три основные категории. Для небольшого приложения локальная файловая система может быть достаточной. Но для SaaS, где пользователи регулярно загружают документы, изображения и архивы, Object Storage обычно удобнее.

3. Почему не стоит хранить пользовательские файлы внутри Docker-контейнера

Docker хранение загруженных файлов требует отдельного подхода. Файловая система контейнера обычно не должна рассматриваться как постоянное хранилище пользовательских данных. Например: Container A | v /uploads/file.pdf Если контейнер удалён и пересоздан, данные в его записываемом слое могут исчезнуть. Docker Volumes позволяют сохранять данные независимо от контейнера.

Но если приложение масштабируется на несколько серверов, возникает необходимость организовать общее хранилище. Object Storage решает эту задачу через сетевой API.


4. Что такое Object Storage

Object Storage что это — модель хранения данных, в которой файлы представлены как объекты. Каждый объект обычно содержит:

  • содержимое;
  • уникальный ключ;
  • метаданные;
  • дополнительные свойства хранения.

Например: Bucket: project-files Object Key: organizations/42/documents/abc123.pdf Приложение обращается к объекту по ключу. При этом Object Storage не является обычной файловой системой с полноценной POSIX-семантикой. Операции переименования, блокировки и атомарного изменения файлов могут отличаться от поведения локального диска.

5. Что такое Amazon S3

Amazon S3 для хранения файлов — облачный сервис объектного хранения AWS. Он позволяет хранить объекты в Buckets и обращаться к ним через API. Типичные сценарии:

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

S3 поддерживает различные классы хранения, политики доступа, версионирование и механизмы управления жизненным циклом объектов. Но использование S3 не освобождает приложение от необходимости правильно проверять права доступа.

6. Что такое MinIO

MinIO S3 совместимое хранилище — объектное хранилище с API, совместимым с S3. Его часто используют в собственных инфраструктурах и локальных окружениях разработки. Например: FastAPI | v S3 API | v MinIO Для многих операций можно использовать те же клиентские библиотеки, что и для Amazon S3. Но совместимость не означает полного совпадения всех возможностей и настроек.

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

7. Amazon S3 vs MinIO

S3 vs MinIO сравнение: Для локальной разработки MinIO может быть удобным решением. Для Production выбор зависит от требований к доступности, стоимости, размещению данных и операционной поддержке.


8. Что такое Bucket

S3 Bucket что это — логический контейнер для объектов. Например: Bucket: application-files Внутри него могут находиться ключи: avatars/user-42.webp documents/project-15/report.pdf exports/export-2026-001.csv Разделители / в Object Key помогают организовать логическую структуру. Но в S3 это часть имени объекта, а не обычные вложенные папки файловой системы.

9. Как правильно формировать Object Key

S3 Object Key best practices предполагают использование контролируемых приложением идентификаторов. Не стоит использовать оригинальное имя файла как единственный ключ. Плохой вариант: uploads/report.pdf Если два пользователя загрузят файл с одинаковым названием, возможен конфликт. Лучше: organizations/42/files/550e8400-e29b-41d4-a716-446655440000.pdf Оригинальное имя файла можно хранить отдельно в PostgreSQL.

Это уменьшает риск конфликтов и упрощает управление доступом.

10. Почему нельзя доверять имени загруженного файла

Безопасность имени файла Upload требует осторожности. Пользователь может отправить: ../../config.env Или: invoice.pdf.exe Или файл с неожиданными Unicode-символами. Поэтому имя, переданное пользователем, нельзя напрямую использовать как путь на сервере. Лучше генерировать собственный Object Key и хранить исходное имя только как отображаемый атрибут после необходимой обработки.


11. Как реализовать File Upload в FastAPI

Начнём с простого примера. FastAPI UploadFile пример: from fastapi import ( FastAPI, File, UploadFile ) app = FastAPI() @app.post("/upload") async def upload_file( file: UploadFile = File(...) ): return { "filename": file.filename, "content_type": file.content_type } Для обработки Multipart Form Data потребуется библиотека: pip install python-multipart Этот Endpoint принимает файл, но не сохраняет его.

Также он не проверяет размер, тип и права пользователя. Для Production необходимы дополнительные меры защиты.

12. Что такое Multipart Form Data

Multipart Form Data FastAPI — формат HTTP-запроса, позволяющий отправлять файлы и другие поля формы. Например: POST /upload HTTP/1.1 Content-Type: multipart/form-data; boundary=... Тело запроса содержит отдельные части. Одна часть может содержать файл. Другая — описание документа. Третья — идентификатор проекта. FastAPI умеет обрабатывать такие запросы через UploadFile, File и Form.

13. Как отправить файл из React

React File Upload FormData:

import { useState } from "react";

export function FileUploader() { const [file, setFile] = useState<File | null>(null); const [loading, setLoading] = useState(false); async function upload() { if (!file) return; const formData = new FormData(); formData.append("file", file); setLoading(true); try { const response = await fetch("/api/upload", { method: "POST", body: formData, credentials: "include" }); if (!response.ok) {

throw new Error("Upload failed"); } } finally { setLoading(false); } } return ( <div> <input type="file" onChange={(event) => { setFile(event.target.files?.[0] ?? null); }} /> <button disabled={!file || loading} onClick={upload} > {loading ? "Загрузка..." : "Загрузить"} </button> </div> ); } Важно не устанавливать Content-Type: multipart/form-data вручную. Браузер самостоятельно добавит правильный Boundary.

14. Как ограничить размер файла

Ограничение размера файла FastAPI — обязательный элемент безопасной загрузки. На Frontend можно проверить: const MAX_SIZE = 10 * 1024 * 1024; if (file.size > MAX_SIZE) { throw new Error("Файл слишком большой"); } Но проверка в браузере не является защитой сервера. Пользователь может отправить запрос напрямую. Поэтому размер необходимо ограничивать и на Backend.

Кроме того, лимиты полезно настраивать на уровне Reverse Proxy.

15. Как читать файл частями

FastAPI UploadFile chunks позволяет обрабатывать содержимое без чтения всего файла в память одним вызовом. Пример: MAX_SIZE = 10 * 1024 * 1024 CHUNK_SIZE = 1024 * 1024 async def read_limited(file): chunks = [] total = 0 while chunk := await file.read(CHUNK_SIZE): total += len(chunk) if total > MAX_SIZE: raise ValueError( "File size limit exceeded" ) chunks.append(chunk) return b"".join(chunks)

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


16. Как подключить S3 к FastAPI

Для работы с S3 можно использовать Boto3. FastAPI S3 boto3 интеграция: pip install boto3 Создадим клиент:

import os

import boto3

s3 = boto3.client( "s3", region_name=os.environ.get( "AWS_REGION", "eu-central-1" ) ) В AWS предпочтительно использовать IAM Roles и стандартную цепочку получения Credentials. Не стоит записывать Access Key и Secret Key непосредственно в исходный код.

17. Как подключить MinIO через Boto3

Boto3 MinIO подключение:

import os

import boto3

s3 = boto3.client( "s3", endpoint_url=os.environ[ "S3_ENDPOINT_URL" ], aws_access_key_id=os.environ[ "S3_ACCESS_KEY" ], aws_secret_access_key=os.environ[ "S3_SECRET_KEY" ], region_name="us-east-1" ) Например: S3_ENDPOINT_URL=http://minio:9000 Такой адрес подходит для взаимодействия контейнеров внутри локальной Docker-сети.

В Production следует использовать защищённые соединения и подходящую систему управления секретами.

18. Как загрузить файл в S3

Загрузка файла в S3 Python: from uuid import uuid4 BUCKET = "application-files" def upload_to_s3(file_obj, content_type: str): object_key = f"uploads/{uuid4()}" s3.upload_fileobj( file_obj, BUCKET, object_key, ExtraArgs={ "ContentType": content_type } ) return object_key Этот пример показывает базовую загрузку. Но Content-Type здесь передаётся вызывающим кодом и не должен считаться проверенным.

В Production необходимо сначала проверить содержимое файла, допустимый тип, размер и права пользователя. Также важно учитывать, что синхронный Boto3 блокирует вызывающий поток. В асинхронном FastAPI Endpoint такие операции следует выполнять вне Event Loop либо использовать соответствующую архитектуру обработки.


19. Как устроена загрузка через Backend

Загрузка файлов через Backend FastAPI выглядит так: React | v FastAPI | v Validation | v S3 / MinIO Преимущества:

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

Недостатки:

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

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

20. Что такое Presigned URL

S3 Presigned URL что это — временная подписанная ссылка, разрешающая определённую операцию с объектом. Например, Backend может выдать URL для загрузки файла непосредственно в S3. React | v FastAPI | v Generate Presigned URL | v React | v Direct Upload to S3 Backend при этом не обязан принимать весь файл. Это снижает нагрузку на API.

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

21. Как создать Presigned PUT URL

Boto3 generate_presigned_url PUT: from uuid import uuid4 def create_upload_url(content_type: str): object_key = f"uploads/{uuid4()}" url = s3.generate_presigned_url( ClientMethod="put_object", Params={ "Bucket": BUCKET, "Key": object_key, "ContentType": content_type }, ExpiresIn=300 ) return { "upload_url": url, "object_key": object_key } URL действует ограниченное время. Однако сам по себе этот код не проверяет:

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

Эти проверки нужно реализовать отдельно. Также следует помнить, что Presigned PUT URL обычно не является одноразовым разрешением: пока подпись действительна, операция может быть повторена.

22. Как загружать файл напрямую из React в S3

React S3 Direct Upload: async function uploadDirectly(file: File) { const response = await fetch( "/api/files/presign", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ filename: file.name, content_type: file.type, size: file.size }) } ); if (!response.ok) { throw new Error("Cannot create upload URL"); } const data = await response.json();

const uploadResponse = await fetch( data.upload_url, { method: "PUT", headers: { "Content-Type": file.type }, body: file } ); if (!uploadResponse.ok) { throw new Error("Upload failed"); } return data.object_key; } Это базовый пример прямой загрузки. Для завершённой архитектуры после загрузки следует вызвать Backend Endpoint подтверждения.

Backend должен проверить существование объекта, его параметры и соответствие ранее созданной заявке на загрузку. Нельзя просто доверять заявлению браузера о том, что файл успешно сохранён.


23. Почему Presigned URL не заменяет авторизацию

Безопасность Presigned URL S3 зависит от того, кто и на каких условиях получает ссылку. Например, пользователь не должен иметь возможность запросить URL для произвольного ключа: organizations/999/private/report.pdf Если Backend принимает такой ключ без проверки, злоумышленник может получить доступ к чужим данным. Поэтому Object Key должен назначаться сервером или проходить строгую проверку.

Presigned URL не должен позволять пользователю выходить за пределы его прав.

24. Как ограничить размер прямой загрузки

S3 Presigned POST ограничения могут быть полезны, если требуется ограничивать параметры загрузки через Policy. Например, Presigned POST позволяет задать условие: conditions = [ ["content-length-range", 1, 10_000_000] ] В отличие от обычного Presigned PUT, POST Policy предоставляет удобный механизм ограничения размера на стороне S3-совместимого сервиса. Пример: def create_presigned_post(object_key: str):

return s3.generate_presigned_post( Bucket=BUCKET, Key=object_key, Conditions=[ ["content-length-range", 1, 10_000_000] ], ExpiresIn=300 ) Поддержку и точное поведение условий необходимо проверять для выбранного S3-совместимого хранилища.

25. Как настроить CORS для S3

При прямой загрузке браузер отправляет запрос на другой Origin. Поэтому нужен S3 CORS для React. Пример конфигурации: [ { "AllowedOrigins": [ "https://app.example.com" ], "AllowedMethods": [ "GET", "PUT", "POST" ], "AllowedHeaders": [ "Content-Type" ], "ExposeHeaders": [ "ETag" ], "MaxAgeSeconds": 300 } ] Это пример для конкретного набора операций.

Если приложение использует другие заголовки, их необходимо явно учесть. Важно: CORS управляет доступом браузерного JavaScript к cross-origin запросам и ответам. Он не заменяет IAM, Bucket Policy или проверку подписанных URL.


26. Что такое Multipart Upload в S3

S3 Multipart Upload — механизм загрузки большого объекта отдельными частями. Например: Файл 1 GB | +-- Part 1 +-- Part 2 +-- Part 3 +-- Part 4 +-- ... | v Complete Multipart Upload Каждая часть загружается отдельно. После загрузки всех частей объектное хранилище собирает итоговый объект. Это полезно для больших файлов и нестабильных соединений.

27. Чем Multipart Upload отличается от multipart/form-data

Эти термины часто путают. Multipart Upload vs FormData — разные механизмы. multipart/form-data — формат HTTP-запроса. S3 Multipart Upload — протокол загрузки одного объекта несколькими частями. Они могут использоваться в одном приложении, но решают разные задачи.

28. Как работает Multipart Upload API

S3 Multipart Upload API обычно включает три основных этапа. Первый — создание загрузки: CreateMultipartUpload Второй — загрузка частей: UploadPart Третий — завершение: CompleteMultipartUpload Если загрузка не завершена, необходимо предусмотреть очистку через AbortMultipartUpload или Lifecycle Rules. Иначе незавершённые части могут продолжать занимать место и создавать расходы.

29. Как реализовать Multipart Upload через Boto3

Boto3 Multipart Upload пример: response = s3.create_multipart_upload( Bucket=BUCKET, Key="videos/example.mp4", ContentType="video/mp4" ) upload_id = response["UploadId"] Загрузка одной части: part = s3.upload_part( Bucket=BUCKET, Key="videos/example.mp4", UploadId=upload_id, PartNumber=1, Body=b"example-content" ) После загрузки всех частей необходимо передать их номера и ETag в complete_multipart_upload.

Этот фрагмент демонстрирует API, но не является полной реализацией. В S3 существуют ограничения на размер частей и количество частей. Для обычных частей, кроме последней, минимальный размер составляет 5 MiB.

30. Как реализовать возобновляемую загрузку

Resumable Upload React S3 требует хранения состояния загрузки. Например: upload_id object_key uploaded_parts file_size status Если соединение прервалось, клиент может повторно загрузить отсутствующие части. Но для корректного возобновления необходимо надёжно сопоставлять:

  • пользователя;
  • файл;
  • Upload ID;
  • Object Key;
  • уже загруженные части.

Нельзя позволять пользователю завершать чужую Multipart Upload-сессию.


31. Как показывать прогресс загрузки в React

React Upload Progress Bar — полезный элемент интерфейса. Для отслеживания прогресса можно использовать XMLHttpRequest. Пример: function uploadWithProgress( url: string, file: File, onProgress: (value: number) => void ): Promise<void> { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open("PUT", url); xhr.setRequestHeader( "Content-Type", file.type );

xhr.upload.onprogress = (event) => { if (!event.lengthComputable) return; const progress = Math.round( (event.loaded / event.total) * 100 ); onProgress(progress); }; xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { resolve(); } else { reject(new Error("Upload failed")); } }; xhr.onerror = () => { reject(new Error("Network error")); }; xhr.send(file); }); }

При использовании Presigned URL необходимо соблюдать подписанные заголовки. Прогресс 100% означает, что браузер завершил передачу данных, но не обязательно что Backend уже проверил и активировал объект.

32. Как отменить загрузку

Отмена File Upload React может выполняться через AbortController для Fetch. Например: const controller = new AbortController(); fetch(uploadUrl, { method: "PUT", body: file, signal: controller.signal }); // Отмена controller.abort(); Но отмена HTTP-запроса не всегда означает удаление уже переданных данных. Для Multipart Upload необходимо отдельно завершать или отменять загрузочную сессию на стороне хранилища.


33. Как хранить метаданные файлов в PostgreSQL

Object Storage хранит содержимое файлов. PostgreSQL — бизнес-информацию о них. PostgreSQL метаданные файлов могут выглядеть так: CREATE TABLE files ( id UUID PRIMARY KEY, organization_id BIGINT NOT NULL, uploaded_by BIGINT NOT NULL, object_key TEXT NOT NULL UNIQUE, original_name TEXT NOT NULL, content_type TEXT, size_bytes BIGINT, status TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() );

Здесь organization_id помогает связывать файл с конкретной организацией. uploaded_by показывает, кто инициировал загрузку. status отражает состояние обработки. Например: pending uploaded scanning ready rejected deleted

34. Почему нельзя хранить только S3 URL

S3 Object Key vs URL — важное различие. Object Key — идентификатор объекта внутри Bucket. URL — способ обращения к объекту. URL может зависеть от:

  • региона;
  • CDN;
  • домена;
  • Presigned Parameters;
  • инфраструктуры хранения.

Поэтому в базе обычно лучше хранить Bucket и Object Key либо логический идентификатор файла. Ссылку можно генерировать по необходимости.

35. Как связать файл с организацией

Multi-Tenant File Storage требует изоляции данных арендаторов. Например: organizations/42/files/... organizations/73/files/... Но префикс в Object Key сам по себе не является механизмом авторизации. Backend должен проверять, что текущий пользователь действительно имеет доступ к соответствующей организации и объекту. Нельзя считать URL безопасным только потому, что он содержит UUID.


36. Как организовать приватное скачивание файлов

Private S3 Download можно реализовать через временный Presigned GET URL. Схема: React | v GET /api/files/{file_id}/download | v FastAPI | v Authorization Check | v Presigned GET URL | v Browser downloads from S3 Backend проверяет права пользователя. Только после этого выдаёт временную ссылку.

37. Как создать Presigned Download URL

Boto3 Presigned GET URL: def create_download_url(object_key: str): return s3.generate_presigned_url( ClientMethod="get_object", Params={ "Bucket": BUCKET, "Key": object_key }, ExpiresIn=120 ) Это только функция генерации ссылки. Проверка прав должна выполняться до её вызова. Важно также не включать Presigned URL в публичные логи, аналитику и другие системы, где его могут увидеть посторонние.

38. Public Bucket или Private Bucket

Public vs Private S3 Bucket — архитектурный выбор. Для пользовательских документов обычно нужен Private Bucket. Для публичных изображений можно использовать отдельный механизм доставки через CDN. Не следует делать весь Bucket публичным ради одного типа файлов. Безопаснее разделять публичные и приватные объекты и использовать минимально необходимые разрешения.


39. Как проверить MIME Type файла

Проверка MIME Type Upload необходима, поскольку браузер может отправить произвольное значение Content-Type. Например: Content-Type: image/png Но внутри может находиться совершенно другой формат. Поэтому нельзя доверять только заголовку. Нужно проверять фактическое содержимое. Для распространённых форматов можно использовать специализированные библиотеки определения типа файла.

40. Что такое Magic Bytes

Magic Bytes проверка файла — анализ характерных байтов в начале или других структурных частях файла. Например, PNG имеет известную сигнатуру. Но проверка сигнатуры не гарантирует полной безопасности. Файл может иметь корректный заголовок и при этом содержать опасные данные. Поэтому для сложных форматов необходима более глубокая проверка.

41. Как защититься от вредоносных файлов

Безопасная загрузка файлов OWASP предполагает несколько уровней защиты. Полезные меры:

  • Allowlist допустимых форматов;
  • ограничение размера;
  • проверка содержимого;
  • антивирусное сканирование при необходимости;
  • хранение файлов вне исполняемых директорий;
  • генерация собственных Object Key;
  • контроль прав доступа;
  • безопасная выдача Content-Type;
  • ограничение частоты загрузок;
  • мониторинг подозрительных действий.

Важно не полагаться на одну проверку. Безопасность достигается сочетанием нескольких механизмов.

42. Как использовать антивирусное сканирование

Антивирусная проверка файлов Backend может выполняться отдельным Worker. Например: Upload | v Quarantine Bucket | v Scan Worker | +-- Clean | | | v | Ready | +-- Suspicious | v Rejected До завершения проверки файл не должен становиться доступным другим пользователям. Для сканирования можно использовать специализированные антивирусные движки или управляемые сервисы.

Но ни один антивирус не гарантирует обнаружение всех угроз.


43. Почему опасно отдавать HTML и SVG как обычные файлы

SVG Upload XSS безопасность требует отдельного внимания. SVG — это XML-формат, который может содержать активные элементы. HTML-файлы также могут выполнять JavaScript в браузере. Если приложение отдаёт недоверенный HTML или SVG с собственного Origin, возникает риск XSS и других атак. Для таких файлов полезно использовать:

  • отдельный домен доставки;
  • безопасные Content-Disposition;
  • строгую Content Security Policy;
  • преобразование в безопасный растровый формат там, где это допустимо;
  • запрет опасных типов файлов, если они не нужны.

Не следует считать любое изображение безопасным только по расширению.

44. Что такое Content-Disposition

Content-Disposition attachment позволяет указать браузеру, что файл следует скачать, а не отображать непосредственно в странице. Например: Content-Disposition: attachment; filename="report.pdf" Это полезно для документов с недоверенным содержимым. Но заголовок не заменяет остальные меры защиты. Особенно важно контролировать Content-Type и Origin, с которого отдаются файлы.

45. Как защититься от ZIP Bomb

ZIP Bomb File Upload — архив, который после распаковки может занимать чрезвычайно большой объём данных. Например, небольшой ZIP-файл может содержать огромное количество повторяющихся данных. Если Backend распаковывает такие архивы без ограничений, возможны:

  • исчерпание дискового пространства;
  • перегрузка CPU;
  • переполнение памяти;
  • отказ в обслуживании.

Поэтому при распаковке нужно ограничивать:

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

Также необходимо защищаться от Zip Slip — записи файлов за пределы разрешённой директории.


46. Как обрабатывать изображения после загрузки

Image Processing FastAPI Celery позволяет выполнять преобразования отдельно от HTTP-запроса. Например: Upload Image | v S3 | v Celery Task | v Resize | v WebP / AVIF | v S3 Это удобно для:

  • аватаров;
  • изображений товаров;
  • превью документов;
  • фотографий;
  • медиафайлов.

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

47. Как создавать Thumbnail

Генерация Thumbnail Python Pillow: from io import BytesIO from PIL import Image def create_thumbnail(data: bytes) -> bytes: with Image.open(BytesIO(data)) as image: image.thumbnail((320, 320)) output = BytesIO() image.convert("RGB").save( output, format="WEBP", quality=80 ) return output.getvalue() Это базовый пример. В Production необходимо учитывать:

  • лимиты размера;
  • EXIF Orientation;
  • метаданные;
  • цветовые профили;
  • повреждённые файлы;
  • ограничения памяти;
  • защиту от Decompression Bomb.

48. Почему лучше хранить оригинал и производные версии

Image Variants Object Storage позволяет хранить разные версии изображения. Например: images/123/original.jpg images/123/thumb.webp images/123/medium.webp images/123/large.webp Это помогает не отдавать пользователю оригинальное изображение размером 12 MB там, где достаточно миниатюры 50 KB. При этом оригинал полезен для повторной обработки и создания новых форматов.


49. Как использовать CDN для файлов

CDN для S3 файлов помогает доставлять публичные ресурсы пользователям из географически распределённых точек. Например: User | v CDN Edge | v S3 Origin Если объект уже находится в кеше CDN, его не обязательно каждый раз загружать из Object Storage. Это особенно полезно для:

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

50. Как настроить Cache-Control

S3 Cache-Control заголовок управляет кешированием объектов. Например: Cache-Control: public, max-age=31536000, immutable Такой заголовок подходит для публичных неизменяемых файлов с версионированными именами. Например: assets/logo-a1b2c3.webp Но его нельзя бездумно применять к приватным документам или объектам, содержимое которых может изменяться по тому же URL.

51. Как обновлять файлы без проблем с кешем

Cache Busting S3 CDN можно реализовать через уникальные Object Key. Например: avatars/user-42/v1.webp avatars/user-42/v2.webp Или: avatars/user-42/550e8400.webp При обновлении изображения приложение сохраняет новый объект и обновляет ссылку в PostgreSQL. Это часто надёжнее, чем перезаписывать объект по тому же ключу и пытаться немедленно очистить все кеши.


52. Как удалить файл из S3

Удаление файла S3 boto3: s3.delete_object( Bucket=BUCKET, Key="uploads/example.pdf" ) Но удаление файла — не только вызов API. Нужно учитывать:

  • права пользователя;
  • связи с бизнес-объектами;
  • состояние обработки;
  • версии объекта;
  • резервные копии;
  • требования к хранению;
  • возможные ошибки удаления.

Например, если файл используется несколькими документами, немедленное удаление может нарушить работу приложения.

53. Что такое S3 Lifecycle Rules

S3 Lifecycle Rules позволяют автоматически управлять объектами по заданным правилам. Например:

  • удалять временные загрузки;
  • удалять незавершённые Multipart Upload;
  • переводить старые объекты в другие классы хранения;
  • удалять устаревшие версии при соответствующей политике.

Это особенно полезно для временных файлов и больших архивов. Но правила необходимо тестировать. Неправильно настроенный Lifecycle может удалить данные, которые ещё нужны пользователям.

54. Как организовать Soft Delete файлов

Soft Delete File Storage — подход, при котором файл сначала помечается как удалённый в базе. Например: UPDATE files SET status = 'deleted' WHERE id = '...'; После этого фоновый Worker может удалить объект из хранилища. Но такой процесс требует продуманной согласованности между PostgreSQL и S3. Если Worker завершится с ошибкой, объект может остаться в хранилище.

Поэтому необходимы Retry, мониторинг и периодическая сверка.


55. Что такое Orphaned Files

Orphaned Files S3 — объекты, которые существуют в Object Storage, но больше не связаны с актуальными записями приложения. Например:

  1. Пользователь запросил Presigned URL.
  2. Загрузил файл.
  3. Не вызвал Endpoint подтверждения.
  4. В PostgreSQL осталась незавершённая запись.

Или наоборот: объект загрузился, но сохранение метаданных завершилось ошибкой. Для решения полезны:

  • статусы загрузок;
  • TTL;
  • периодическая очистка;
  • сверка метаданных;
  • Lifecycle Rules.

56. Как реализовать двухэтапную загрузку

Two Phase File Upload — удобная архитектура для прямой загрузки. Первый этап — создание заявки. POST /api/files/init Backend:

  • проверяет пользователя;
  • проверяет квоту;
  • создаёт File ID;
  • назначает Object Key;
  • сохраняет статус pending;
  • возвращает Presigned URL.

Второй этап — подтверждение. POST /api/files/{id}/complete Backend:

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

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

57. Почему подтверждение загрузки должно быть серверным

S3 HeadObject проверка загрузки позволяет получить метаданные объекта. Например: response = s3.head_object( Bucket=BUCKET, Key=object_key ) size = response["ContentLength"] content_type = response.get("ContentType") Но ContentType — это метаданные объекта, а не доказательство фактического формата файла. Если безопасность зависит от содержимого, его необходимо проверять отдельно.

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


58. Как ограничивать квоты пользователей

File Storage Quota SaaS помогает контролировать расходы и предотвращать злоупотребления. Например: Free Plan: 1 GB Pro Plan: 100 GB Business Plan: 1 TB Но проверка квоты только перед загрузкой может быть недостаточной. Если пользователь одновременно инициирует несколько загрузок, возможна гонка. Поэтому необходимо учитывать зарезервированный объём и конкурентные операции. Например: used_bytes reserved_bytes

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

59. Как защититься от злоупотреблений загрузкой

Rate Limiting File Upload помогает ограничивать:

  • количество новых загрузок;
  • суммарный объём трафика;
  • количество незавершённых Multipart Upload;
  • частоту генерации Presigned URL;
  • число одновременно обрабатываемых файлов.

Но Rate Limiting не заменяет квоты. Пользователь может загружать файлы редко, но очень большого размера. Поэтому нужны оба механизма.


60. Как мониторить файловое хранилище

Мониторинг S3 MinIO должен включать технические и бизнес-метрики. Полезные показатели:

  • количество загруженных файлов;
  • общий объём хранения;
  • ошибки загрузки;
  • ошибки скачивания;
  • среднее время загрузки;
  • количество незавершённых Upload;
  • количество отклонённых файлов;
  • стоимость хранения;
  • исходящий трафик;
  • задержки Object Storage.

Особенно важно контролировать стоимость исходящего трафика. Для некоторых приложений скачивание файлов может обходиться дороже самого хранения.

61. Как логировать файловые операции

Audit Log File Storage полезен для корпоративных SaaS. Например: 2026-10-23 10:15 User 42 Downloaded File 123 2026-10-23 10:20 User 42 Deleted File 123 В Audit Log можно сохранять:

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

Но не следует записывать в логи Presigned URL и секретные Credentials.


62. Как тестировать File Upload

Тестирование File Upload pytest должно проверять:

  • успешную загрузку;
  • отсутствие авторизации;
  • превышение лимита размера;
  • неподдерживаемый тип;
  • повреждённый файл;
  • некорректный Object Key;
  • попытку доступа к чужому файлу;
  • повторное подтверждение загрузки;
  • ошибку Object Storage;
  • прерванную загрузку;
  • удаление объекта;
  • истечение Presigned URL.

Для Unit Tests можно использовать Mock. Но интеграционные тесты желательно выполнять с реальным S3-совместимым сервисом в тестовом окружении.

63. Как тестировать MinIO через Docker

MinIO Docker Compose для тестов позволяет запускать локальное хранилище. Пример: services: minio: image: minio/minio command: server /data --console-address ":9001" environment: MINIO_ROOT_USER: minioadmin MINIO_ROOT_PASSWORD: local-development-password ports: - "127.0.0.1:9000:9000" - "127.0.0.1:9001:9001" volumes: - minio_data:/data volumes: minio_data: Это конфигурация для локальной разработки.

Для Production нужны отдельные Credentials, защищённый доступ, корректная архитектура хранения и резервное копирование. Bucket также необходимо создать перед использованием.

64. Как тестировать загрузку через Playwright

Playwright File Upload React:

import {

test, expect } from "@playwright/test"; test("user can select a file", async ({ page }) => { await page.goto("/files"); await page .locator('input[type="file"]') .setInputFiles({ name: "report.txt", mimeType: "text/plain", buffer: Buffer.from("Hello") }); await expect( page.getByRole("button", { name: "Загрузить" }) ).toBeEnabled(); });

Для полноценного E2E-теста необходимо также проверить успешное сохранение файла и его отображение в интерфейсе.


65. Как масштабировать файловый сервис

Масштабирование File Upload Backend зависит от выбранной модели загрузки. Если файлы проходят через FastAPI, рост трафика увеличивает нагрузку на API. Если используется Direct Upload, основной поток данных идёт между браузером и Object Storage. Поэтому при больших объёмах прямые загрузки часто масштабируются удобнее. Но Backend всё равно отвечает за:

  • авторизацию;
  • квоты;
  • метаданные;
  • подтверждение;
  • проверку;
  • обработку;
  • аудит.

Object Storage не заменяет бизнес-логику приложения.

66. Как обеспечить отказоустойчивость

Отказоустойчивость Object Storage включает несколько уровней. Например:

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

Наличие нескольких копий объекта не всегда заменяет Backup. Если пользователь случайно удалит файл, репликация может распространить удаление. Поэтому необходимо различать доступность, репликацию, версионирование и резервное копирование.


67. Типичные ошибки при работе с S3 и MinIO

Первая ошибка — хранить файлы в записываемом слое Docker-контейнера. Вторая — делать весь Bucket публичным. Третья — доверять имени файла. Четвёртая — проверять размер только в React. Пятая — доверять Content-Type браузера. Шестая — использовать оригинальное имя файла как уникальный Object Key. Седьмая — выдавать Presigned URL без проверки авторизации. Восьмая — разрешать клиенту выбирать произвольный Object Key.

Девятая — считать Presigned URL одноразовым. Десятая — не проверять результат загрузки. Одиннадцатая — хранить только временный URL вместо Object Key. Двенадцатая — не учитывать Orphaned Files. Тринадцатая — не очищать незавершённые Multipart Upload. Четырнадцатая — не ограничивать количество одновременных загрузок. Пятнадцатая — не использовать квоты.

Шестнадцатая — отдавать недоверенный HTML с основного домена приложения. Семнадцатая — обрабатывать изображения без ограничений памяти. Восемнадцатая — не проверять права доступа к файлам других организаций. Девятнадцатая — не мониторить стоимость хранения и трафика. Двадцатая — считать Object Storage полноценной заменой PostgreSQL.

Эти ошибки S3 File Upload Production могут приводить к утечкам данных, потере файлов, перегрузке инфраструктуры и неожиданным расходам.


68. Практическая архитектура файлового сервиса для SaaS

Представим SaaS-приложение с поддержкой организаций, проектов и документов. Production архитектура File Storage React FastAPI S3: Browser | v React | v FastAPI | +------------+------------+ | | v v PostgreSQL S3 / MinIO | | | v | Quarantine | | | v | Scan Worker | | | v | Processing | | | v | Ready Objects | | +------------+------------+ | v Authorized Access | v Presigned GET | v Browser В этой архитектуре:

React отвечает за интерфейс загрузки и отображение прогресса. FastAPI проверяет права, квоты и параметры файлов. PostgreSQL хранит метаданные и бизнес-статусы. S3 или MinIO хранит содержимое. Celery Worker может выполнять антивирусное сканирование и обработку изображений. Приватные документы выдаются через временные ссылки после проверки доступа.

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

69. Чек-лист файлового хранилища для Production

Чек-лист S3 MinIO Production:

  • Файлы хранятся вне записываемого слоя Docker-контейнера.
  • Object Storage защищён от несанкционированного доступа.
  • Используются минимально необходимые права.
  • Credentials не находятся в исходном коде.
  • Production использует HTTPS.
  • Object Key генерируется сервером.
  • Оригинальное имя файла не используется как путь.
  • Размер ограничивается на нескольких уровнях.
  • Тип файла проверяется по содержимому.
  • Опасные форматы обрабатываются отдельно.
  • Пользователь не может получить доступ к чужому файлу.
  • Приватные Buckets не открыты публично.
  • Presigned URL имеют ограниченный срок действия.
  • Завершение загрузки подтверждается Backend.
  • Незавершённые загрузки очищаются.
  • Есть механизм обработки Orphaned Files.
  • Для крупных файлов используется подходящий Multipart Upload.
  • Настроены квоты и Rate Limiting.
  • Метаданные хранятся в PostgreSQL.
  • При необходимости используется антивирусное сканирование.
  • Обработка изображений ограничена по ресурсам.
  • Настроены Lifecycle Rules.
  • Есть резервное копирование и проверенный план восстановления.
  • Мониторятся ошибки, задержки и стоимость.
  • Критичные операции покрыты интеграционными тестами.

FAQ

Object Storage — модель хранения данных в виде объектов с ключами и метаданными. Amazon S3 — облачный сервис объектного хранения AWS. MinIO — объектное хранилище с API, совместимым с S3. Зависит от требований к инфраструктуре, стоимости, доступности и управлению данными. Да. MinIO часто используют для локальной разработки и тестирования.

Через UploadFile и multipart/form-data либо с помощью прямой загрузки в Object Storage. Можно использовать FormData и Fetch API. Временная подписанная ссылка, разрешающая определённую операцию с объектом. При правильной настройке и ограничениях он может быть безопасным механизмом доступа. Но сам URL следует считать временным секретом. Да. Для этого часто используют Presigned PUT или Presigned POST.

Механизм загрузки большого объекта отдельными частями. Multipart Upload — протокол загрузки объекта частями. FormData — формат HTTP-запроса. Обычно метаданные хранят в PostgreSQL, а содержимое — в Object Storage. Обычно удобнее хранить Object Key и при необходимости формировать URL. На Frontend, Backend, Reverse Proxy и, если возможно, на уровне политики загрузки Object Storage.

Необходимо анализировать фактическое содержимое, а не только расширение или Content-Type. Использовать Allowlist, лимиты, проверку содержимого, изоляцию, сканирование и безопасную выдачу файлов. Через Backend-проверку авторизации и временный Presigned GET URL. Для ускорения доставки публичных файлов и снижения нагрузки на Origin.

Правила автоматического управления объектами в зависимости от их возраста и других условий. Объекты, которые остались в хранилище, но больше не связаны с актуальными записями приложения. Не обязательно. Но Celery полезен для фоновой проверки, обработки изображений, конвертации и очистки. Да. Но для большого количества крупных файлов Object Storage часто удобнее.


Заключение

Загрузка и хранение файлов — важная часть архитектуры современного Full Stack приложения. На раннем этапе можно сохранять документы на локальный диск. Но по мере роста продукта появляются требования к масштабированию, безопасности, доступности и управлению жизненным циклом файлов. Object Storage позволяет отделить содержимое файлов от процессов Backend.

Amazon S3 предоставляет управляемую инфраструктуру объектного хранения. MinIO позволяет использовать S3-совместимый API в собственной инфраструктуре. FastAPI отвечает за авторизацию, проверку параметров и бизнес-логику. React обеспечивает удобный интерфейс загрузки, отображение прогресса и обработку ошибок. Presigned URL позволяют передавать файлы напрямую в Object Storage, снижая нагрузку на Backend.

Multipart Upload помогает работать с большими объектами и нестабильными соединениями. PostgreSQL хранит метаданные, связи с пользователями и организациями, а также состояния обработки. Но главное — не забывать о безопасности. Нельзя доверять имени файла, Content-Type или размеру, указанному браузером. Нельзя выдавать временные ссылки без проверки прав.

Нельзя делать приватные документы публичными ради удобства разработки. И нельзя считать успешный HTTP Upload доказательством того, что файл безопасен и готов к использованию. Главный принцип: файловое хранилище должно управлять не только байтами, но и доступом, проверкой, состоянием, жизненным циклом и стоимостью хранения объектов.

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


Наверх