Содержание

Загрузка и обработка изображений в Go

Изображение в backend-задаче - это не просто "файл, который надо принять". Это пользовательский ввод, потенциально большой бинарный объект и будущая запись в хранилище. Его нужно принять, ограничить по размеру, проверить тип, возможно обработать, сохранить и связать с пользователем или сущностью.

Типичный путь:

  1. Принять файл через multipart/form-data.
  2. Проверить размер.
  3. Проверить тип по содержимому.
  4. Декодировать изображение.
  5. При необходимости сделать превью или сжатие.
  6. Сохранить файл в S3/MinIO или на диск.
  7. Записать метаданные в базу.

multipart/form-data

HTML-форма или frontend отправляют файл как multipart/form-data. В Go handler может прочитать файл так:

func uploadAvatar(w http.ResponseWriter, r *http.Request) { if err := r.ParseMultipartForm(10 << 20); err != nil { http.Error(w, "file is too large", http.StatusBadRequest) return } file, header, err := r.FormFile("avatar") if err != nil { http.Error(w, "avatar is required", http.StatusBadRequest) return } defer file.Close() fmt.Println(header.Filename, header.Size) }

10 << 20 - это 10 MB. Но одного ParseMultipartForm недостаточно для полноценной защиты: нужно еще явно проверять размер и тип.

Ограничение размера

Слишком большой файл может съесть память, диск или время обработки. Проверяйте размер:

if header.Size > 5*1024*1024 { return errors.New("file is too large") }

Для HTTP handler также полезно ограничить тело запроса:

r.Body = http.MaxBytesReader(w, r.Body, 10<<20)

Так вы ограничиваете весь request body, а не только значение из multipart header.

MIME-type

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

Лучше прочитать первые байты:

buffer := make([]byte, 512) n, err := file.Read(buffer) if err != nil { return err } contentType := http.DetectContentType(buffer[:n])

После чтения верните позицию файла в начало:

if _, err := file.Seek(0, io.SeekStart); err != nil { return err }

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

Декодирование изображения

Стандартная библиотека умеет декодировать изображения:

img, format, err := image.Decode(file) if err != nil { return err } fmt.Println(format, img.Bounds())

Чтобы работали JPEG и PNG, импортируйте декодеры:

import ( _ "image/jpeg" _ "image/png" )

Подчеркивание означает blank import: пакет импортируется ради регистрации декодера, хотя напрямую его имена не используются.

Проверка размеров изображения

Большой файл в мегабайтах - не единственная проблема. Изображение может быть огромным по пикселям. Проверяйте bounds:

bounds := img.Bounds() width := bounds.Dx() height := bounds.Dy() if width > 5000 || height > 5000 { return errors.New("image dimensions are too large") }

Это защищает обработку от слишком тяжелых изображений.

Кодирование JPEG

Можно перекодировать изображение в JPEG:

var out bytes.Buffer err := jpeg.Encode(&out, img, &jpeg.Options{Quality: 85}) if err != nil { return err }

Quality влияет на размер и качество. Для аватаров и превью часто хватает 80-85.

Для resize обычно используют отдельную библиотеку. Стандартная библиотека умеет читать и писать изображения, но не дает удобного high-level resize API.

Безопасные имена файлов

Не используйте оригинальное имя файла как путь в хранилище:

../../secret.txt avatar final FINAL.png

Сгенерируйте свое имя:

objectKey := fmt.Sprintf("users/%s/avatar/%s.jpg", userID, uuid.NewString())

Оригинальное имя можно сохранить отдельно как метаданные, если оно нужно для UI.

Где хранить файлы

Для локального учебного проекта можно сохранить на диск. Для сервиса лучше использовать S3-совместимое хранилище:

  • файл хранится в bucket;
  • в базе хранится key, MIME-type, размер и владелец;
  • пользователь получает URL или signed URL.

В базе лучше хранить:

  • owner id;
  • object key;
  • original filename;
  • content type;
  • size;
  • width и height;
  • created_at.

Превью и фоновые задачи

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

загрузка -> сохранение оригинала -> событие ImageUploaded -> worker создает превью

Так HTTP-запрос не висит слишком долго, а пользователь может получить статус обработки.

Чеклист

  1. Размер request body ограничен.
  2. Размер файла проверяется явно.
  3. MIME-type проверяется по содержимому.
  4. Поддерживаются только нужные форматы.
  5. Размеры изображения по пикселям ограничены.
  6. Имя файла генерируется сервером.
  7. Metadata хранится в базе.
  8. Большие файлы не загружаются целиком без необходимости.
  9. Приватные файлы отдаются через signed URL.

Следующий шаг после статьи

Закрепите тему в реальном проекте и продолжайте обучение на курсах Praxis.

Продолжить изучение

Выбери следующую статью по маршруту или углубись в смежную тему.

Похожие статьи