Media Flow (GraphQL + S3/MinIO)

Єдина схема завантаження/відображення медіа для accounts, company, taskManager, tg_client.

1. Як підключити у будь-яку GraphQL schema

from utils.graphql.service.media_mutations import (
    CreateMediaUploadUrl,
    GetMediaDisplayUrl,
)

class Mutation(graphene.ObjectType):
    create_media_upload_url = CreateMediaUploadUrl.Field()
    get_media_display_url = GetMediaDisplayUrl.Field()

Для аватара користувача окремо є CreateAvatarUploadUrl.

2. Запити для завантаження

2.1 Отримати presigned PUT URL

mutation CreateMediaUploadUrl {
  createMediaUploadUrl(
    source: "avatar",
    filename: "photo.jpg",
    contentType: "image/jpeg",
    expiresIn: 600
  ) {
    uploadUrl
    key
    expiresIn
  }
}

Результат: uploadUrl (тимчасовий), key (постійний шлях об'єкта).

2.2 Завантажити файл

PUT {uploadUrl}
Body: binary file
Для presigned URL використовуйте binary body (не form-data).
Не додавайте Authorization header.

3. Запис key у модель

Після успішного PUT зберігайте у вашому полі саме key, а не повний URL.

Наприклад для ImageField:

item.image = "avatar/1/71ccc17ecd7040de9e73d0c41eecaf25.jpg"

4. Отримати URL для відображення

4.1 Згенерувати/оновити presigned GET URL

mutation GetMediaDisplayUrl {
  getMediaDisplayUrl(
    key: "avatar/1/71ccc17ecd7040de9e73d0c41eecaf25.jpg",
    currentUrl: null,
    expiresIn: 259200
  ) {
    url
    key
    refreshed
    expiresIn
  }
}

refreshed=true означає, що URL був згенерований заново (старий прострочений/відсутній).

5. Повний фронтенд flow

async function uploadAndSaveImage({ file, source, saveMutation }) {
  const { uploadUrl, key } = await gqlCreateMediaUploadUrl({
    source,
    filename: file.name,
    contentType: file.type || "application/octet-stream",
    expiresIn: 600,
  });

  const putResp = await fetch(uploadUrl, {
    method: "PUT",
    body: file,
  });
  if (!putResp.ok) throw new Error(`Upload failed: ${putResp.status}`);

  await saveMutation({ imageKey: key }); // збереження key у вашій моделі
  return key;
}

async function resolveImageUrl(key, currentUrl) {
  const { url } = await gqlGetMediaDisplayUrl({
    key,
    currentUrl,
    expiresIn: 259200,
  });
  return url;
}

6. Типові помилки