{"openapi":"3.1.0","info":{"title":"Rukki File Service — core API v1","description":"Микросервис хранения файлов в S3 (Timeweb Cloud) с метаданными в PostgreSQL для платформы Rukki 5.0\n\n**Группа core-v1** — REST API **file-service** для BFF: метаданные в **PostgreSQL** (схема `files`), бинарные объекты в **S3**.\n\n**Архитектура хранения:**\n- **PostgreSQL** (схема `files`, таблица `file_metadata`) — метаданные, владелец, привязка к сущности, политика доступа.\n- **S3** ([Timeweb Cloud](https://timeweb.cloud/services/s3-storage)):\n  - **DEFAULT** — bucket `${S3_BUCKET:rukki-s3}` (приватные файлы, presign/proxy);\n  - **STATIC_PUBLIC** — bucket `${S3_STATIC_BUCKET:rukki-s3-static}`, публичный URL `${S3_STATIC_PUBLIC_BASE_URL:https://s3.twcstorage.ru/rukki-s3-static}` (`publicUrl` = base + `storedKey`; path-style S3 или CDN Timeweb — см. [док](https://timeweb.cloud/docs/cdn#kogda-nujen-cdn)).\n- Ключ объекта: `ownerId/uuid/originalName`.\n- Endpoint: `https://s3.twcstorage.ru` (S3-совместимый API, path-style access).\n- Credentials: `app.s3.credentials-provider` — `auto`, `static`, `iam` (EC2/ECS/IRSA).\n- Health-check: `/actuator/health` → компонент `s3` (`HeadBucket`, результат кэшируется).\n- Удаление из S3: после commit soft-delete (`S3StorageCleanupListener`); orphan — `S3StorageReconciliationJob` (`app.file.reconciliation.*`).\n- Интеграция при DELETE: transactional outbox → Kafka `file.deleted.v1` (см. **Интеграция Kafka** в core-v1 info); equipment-service снимает ссылки на файл.\n- Кэш метаданных (Redis): только ключ `fileId` (`FileMetadataCacheService`: `@Cacheable` / `@CachePut` / `@CacheEvict`); RBAC на чтение после кэша — `AuthorizedFileAccessService` (метаданные, download head), `FileServiceImpl` / `FileDownloadService`.\n- `checksum_sha256` — SHA-256 hex при upload (один проход с записью в S3).\n- Миграции БД — Flyway (`flyway_schema_history_files`); JPA `ddl-auto: validate`.\n\n\n**Интеграция Kafka (file.deleted.v1, не REST):**\n- **Publisher:** file-service (transactional outbox, `FileDeletedIntegrationService#enqueue`) → топик `file.deleted.v1` (тот же брокер, что `spring.kafka.bootstrap-servers`).\n- **Consumer:** equipment-service при `KAFKA_ENABLED=true`; consumer group по умолчанию — `equipment-service-group` (`KAFKA_CONSUMER_GROUP`).\n- **Триггеры (только DELETE):** `/api/v1/files/{id}` — одно событие; `/api/v1/files?entityType=&entityId=` — по событию на каждый успешно удалённый файл.\n- **Назначение (downstream):** снять ссылки в PostgreSQL equipment-service: `equipment_categories.icon_file_id`, `equipment_types.icon_file_id`, строки `vehicles_photos` по `file_id`.\n- **Поток:** TX soft-delete + `integration_outbox` (`PENDING`) → HTTP **204** → S3 cleanup → `OutboxRelay` → Kafka (at-least-once).\n- **Контракт JSON:** `eventId`, `eventType`=`file.deleted.v1`, `occurredAt`, `fileId` (message key), `ownerId`, `entityType`, `entityId`, `fileStorageType`.\n- **Идемпотентность consumer:** `processed_integration_events` по `eventId`.\n- **Ошибки publisher:** retry в outbox до `app.kafka.outbox.max-retries`, затем DLQ `rukki.file.outbox.v1.dlq`. **Ошибки consumer:** DLQ `rukki.equipment.file-deleted.v1.dlq`.\n- **Ограничение:** при каскадном DELETE файлы без права не попадают в outbox — ссылки в equipment могут остаться.\n\n\n**Авторизация (JWT, OAuth2 Resource Server):**\n- Заголовок `Authorization: Bearer <access_token>`.\n- В Swagger UI нажмите **Authorize** и вставьте **только значение токена** (префикс `Bearer` подставляется автоматически).\n- Роли — claim `realm_access.roles`: `USER`, `MANAGER`, `ADMIN`, `SUPERADMIN`.\n- Идентификатор пользователя — claim `sub` (UUID v4); используется как `ownerId` при загрузке файлов.\n- JWKS: `app.security.jwt.jwks-url`; проверка `iss` / `aud` (identity-service).\n\n\n**Авторизация (два уровня):**\n- **Gateway** (`@PreAuthorize`, JWT): отказ по роли → **403**.\n- **Object-level** (сервис): `PUBLIC` / `PRIVATE`, владелец (`ownerId` = claim `sub`).\n\n**Роли на gateway:**\n- **Загрузка** (`POST /api/v1/files/upload`, `POST /api/v1/files/upload/batch`) — `USER`, `MANAGER`, `ADMIN`, `SUPERADMIN`.\n- **Чтение, PATCH, DELETE** — любой аутентифицированный JWT; детальные правила — в сервисе.\n\n**Object-level:**\n- **Чтение** (метаданные, список): JWT обязателен; `PUBLIC` — любой аутентифицированный;\n  `PRIVATE` — владелец или `MANAGER` / `ADMIN` / `SUPERADMIN`.\n- **Скачивание** (`/download`, `/download-url`): **без JWT** для `STATIC_PUBLIC` (CDN redirect / `publicUrl`);\n  для `DEFAULT` — JWT; `PUBLIC` — любой пользователь; `PRIVATE` — владелец или MANAGER+.\n  Чужой, удалённый (`DELETED`) или недоступный файл → **404** (не раскрываем существование).\n- **PATCH** `/api/v1/files/{id}` — `description`, `accessPolicy`; владелец или `ADMIN` / `SUPERADMIN` (не `MANAGER`) → иначе **403**;\n  пустое тело → **400** (`@AtLeastOnePatchField`).\n- **DELETE** `/api/v1/files/{id}` — владелец или `ADMIN` / `SUPERADMIN`; активная привязка (`entity_status`) → **422**;\n  при `app.kafka.enabled=true` — событие `file.deleted.v1` в outbox (очистка ссылок в equipment-service).\n- **Список** (`GET /api/v1/files`): записи `DELETED` не возвращаются; `ownerId` в query — `MANAGER` / `ADMIN` / `SUPERADMIN`.\n- **Список BFF** (`GET /api/v1/files?entityType=&entityId=`) — файлы сущности по `canRead` (в т.ч. чужие `PUBLIC`).\n- **Assets** — upload без `entityType`/`entityId` или `GET /api/v1/files?assetsOnly=true` (обычный `USER` — только свои).\n- **Каскадное удаление** (`DELETE /api/v1/files?entityType=&entityId=`) — soft-delete доступных файлов; без прав — пропуск;\n  для каждого удалённого файла — отдельное событие `file.deleted.v1` (если Kafka включён).\n- **Upload с привязкой:** `USER` — только `entityId` = JWT `sub` (иначе **404** для маскировки чужой сущности);\n  `EQUIPMENT`/`ORDER`/`NOTIFICATION` — `MANAGER`/`ADMIN`/`SUPERADMIN`, иначе **422**.\n\n**Коды ошибок:** 400 валидация (пустой PATCH/batch, `@ValidEntityBinding`, недопустимый `sort`);\n401 JWT; 403 gateway / PATCH·DELETE без прав; 404 недоступный файл;\n409 optimistic lock (`@Version`); 422 бизнес-правила (MIME + magic bytes для DEFAULT и STATIC_PUBLIC,\nпревышение `app.file.static-max-file-size`, batch сверх лимита, неизвестный `entityType`,\nBFF-привязка без MANAGER/ADMIN, DELETE при активной привязке);\n503 S3.\n\n\n**Загрузка с публичным доступом (`fileStorageType=STATIC_PUBLIC`):**\n\nДля static-ассетов (иконки, шрифты, изображения витрины), которые должны открываться по **постоянному HTTPS URL**\nбез presigned URL. Origin — S3 static bucket; публичный URL — `https://s3.twcstorage.ru/rukki-s3-static` + `storedKey` (path-style S3; CDN Timeweb — опционально через env, см. [док](https://timeweb.cloud/docs/cdn#kogda-nujen-cdn)).\nПосле upload используйте поле **`publicUrl`** в ответе.\n\n**Один файл — `POST /api/v1/files/upload`:**\n1. **Authorize** в Swagger UI (JWT, роли `USER` / `MANAGER` / `ADMIN` / `SUPERADMIN`).\n2. `Content-Type: multipart/form-data`, поле формы **`file`** — бинарное содержимое.\n3. Query-параметры (см. блок *Parameters* ниже):\n   - **`fileStorageType=STATIC_PUBLIC`** — обязательно для публичного bucket;\n   - `description` — опционально;\n   - **`accessPolicy` не нужен** — для `STATIC_PUBLIC` всегда принудительно **PUBLIC** (значение в query игнорируется);\n   - `entityType` / `entityId` — опционально (оба вместе или оба пустые — asset без привязки).\n4. Ответ **201** — в теле: `publicUrl`, `fileStorageType=STATIC_PUBLIC`, `accessPolicy=PUBLIC`, `downloadUrlPath=null`.\n\n**Пакет — `POST /api/v1/files/upload/batch`:** те же query-параметры, поле формы **`files`** (лимит `app.file.batch-upload-max-files`, по умолчанию 20).\n\n**Ограничения (иначе HTTP 422):**\n- MIME — только whitelist `app.file.static-allowed-mime-types` (image/jpeg, image/png, image/webp, image/gif, image/bmp, image/svg+xml, image/x-icon, font/woff, font/woff2, font/ttf, font/otf, …).\n- Проверка magic bytes (для SVG — только whitelist, без magic).\n- Максимальный размер — `app.file.static-max-file-size` (по умолчанию **10MB**, env `FILE_STATIC_MAX_FILE_SIZE`).\n\n**Скачивание и UI после upload:**\n- **HTML / витрина:** в `<img src>`, `<a href>`, CSS `background-image` подставляйте **`publicUrl`** (JWT и presign не нужны). Подробнее — `docs/cdn-static-storage.md` (интеграция).\n- **Без file-service:** открыть `publicUrl` напрямую.\n- **Через API** (JWT не нужен): `GET /api/v1/files/{id}/download` → **307** на `publicUrl`; `GET /api/v1/files/{id}/download-url` → JSON с `url` = `publicUrl`.\n\n| | `DEFAULT` (по умолчанию) | `STATIC_PUBLIC` |\n|---|---|---|\n| Query | `fileStorageType` опущен или `DEFAULT` | **`fileStorageType=STATIC_PUBLIC`** |\n| `accessPolicy` | `PRIVATE` (default) или `PUBLIC` | всегда **PUBLIC** |\n| URL в ответе | `downloadPath`, `downloadUrlPath` | **`publicUrl`** |\n| Скачивание BFF | presigned URL (`downloadUrlPath`) | `publicUrl` напрямую |\n\n\n**REST API v1 (PostgreSQL + S3, BFF-контур):**\n- **POST** `/api/v1/files/upload` — загрузка одного файла (`file` + query); **`fileStorageType=STATIC_PUBLIC`** — публичный CDN (`publicUrl`); asset — без `entityType`/`entityId`\n- **POST** `/api/v1/files/upload/batch` — пакетная загрузка (`files`, те же query); лимит `app.file.batch-upload-max-files` (default 20); пустой список → **400**; атомарный batch\n- **GET** `/api/v1/files` — список (`page` с 1, `sort` по whitelist); `DELETED` не в выдаче\n- **GET** `/api/v1/files/{id}` — метаданные + `downloadUrlPath` (BFF) и `downloadPath` (прокси)\n- **GET** `/api/v1/files/{id}/download` — прокси-скачивание (поток S3 для DEFAULT; **307** на CDN для STATIC_PUBLIC)\n- **GET** `/api/v1/files/{id}/download-url` — presigned URL (JSON) или `?redirect=true` → **302** на S3\n- **PATCH** `/api/v1/files/{id}` — `description`, `accessPolicy` (хотя бы одно поле, иначе **400**)\n- **DELETE** `/api/v1/files/{id}` — soft-delete; S3 после commit; outbox → Kafka `file.deleted.v1` (при `app.kafka.enabled=true`)\n- **DELETE** `/api/v1/files?entityType=&entityId=` — каскадное удаление файлов сущности (BFF); событие на каждый удалённый файл\n\n**Валидация:** MIME whitelist + magic bytes; `entityType`/`entityId` только вместе (`@ValidEntityBinding` → **400**).\n\n\n**Пагинация и сортировка** (`GET /api/v1/files`):\n- Query: `page`, `size`, `sort` (в Swagger — отдельные параметры).\n- **`page`** — номер страницы **с 1** (`page=1` — первая; `page=0` не используется).\n- **`size`** — элементов на странице (по умолчанию 20, максимум 100).\n- **`sort`** — `поле,направление` (например `createdAt,desc`); недопустимое поле → **400**.\n- Ответ: `PagedResponse` — `content`, `currentPage` (с 1), `pageSize`, `totalElements`.\n\n| Список | Допустимые поля `sort` |\n|--------|-------------------------|\n| files | `originalName`, `mimeType`, `sizeBytes`, `ownerId`, `entityType`, `entityId`, `accessPolicy`, `status`, `createdAt`, `updatedAt` |\n\n\n**Скачивание (два режима, без ETag / 304):**\n- **`STATIC_PUBLIC`:** `/download` и `/download-url` **без JWT** (как прямой `publicUrl` на CDN);\n  в Swagger UI для этих операций **Authorize не обязателен** (замок снят).\n- **BFF (рекомендуется):** `downloadUrlPath` → `GET /api/v1/files/{id}/download-url` → JSON `{ url, expiresAt }` (для DEFAULT; presign)\n  или `publicUrl` в метаданных (для STATIC_PUBLIC, без presign)\n  (`FileDownloadService` / presign, TTL `app.s3.presign-ttl`, по умолчанию 15m). Байты — напрямую в S3.\n  `redirect=true` → **302** `Location` на S3.\n- **Прокси:** `downloadPath` → `GET /api/v1/files/{id}/download` — поток через file-service (DEFAULT) или `307` на `publicUrl` (STATIC_PUBLIC):\n  `AuthorizedFileAccessService` (JPQL-проекция + RBAC) → `FileDownloadService` → ленивый `S3ObjectResource`.\n- Заголовки `/download`: `Cache-Control` (`PUBLIC` — `max-age=1h`; `PRIVATE` — `no-store`),\n  `Content-Disposition: attachment`, `Content-Type`, `Content-Length`.\n- Ошибки `/download` возвращаются как `application/json` (`ApiErrorDto`) даже при `Accept: application/octet-stream`\n  (например, **404** для несуществующего/недоступного файла).\n- Заголовки `/download-url`: `Cache-Control` по `accessPolicy`; при редиректе — `Location`.\n- Условных запросов (`If-None-Match`, `ETag`) **нет**.\n- Статусы файла для скачивания: только `READY`. `DELETED` отсекаются на уровне проекции (выдают **404**).\n\n\n**Настройки CORS:** Настройки CORS микросервиса загружаются из конфигурации профиля:\n- **Разрешенные домены (Origins):** `https://*.rukki.pro, https://rukki.pro, https://s3.twcstorage.ru`\n- **Разрешенные HTTP-методы:** `GET, POST, PUT, PATCH, DELETE, OPTIONS`\n- **Разрешенные заголовки:** `Authorization, Content-Type, Content-Disposition, If-None-Match`\n\nSwagger UI автоматически использует текущий домен для выполнения запросов. При развертывании в **Dokploy** параметры (`CORS_ALLOWED_ORIGINS`, `CORS_ALLOWED_METHODS`) задаются в секции **Environment** (значения через запятую, без пробелов).\n\n**Профиль запуска:** prod <br><br><span style=\"color:red\"><b>ВНИМАНИЕ (PROD):</b> В production Swagger должен быть закрыт: установите <i>springdoc.swagger-ui.enabled=false</i> и <i>springdoc.api-docs.enabled=false</i>.</span>\n\n**Build Time:** 2026-06-05T14:49:18.371Z | **Artifact:** file-service | **Java Version:** 21.0.11 | **OS:** Linux (amd64)","version":"1.0.0-SNAPSHOT"},"servers":[{"url":"https://s3.rukki.pro","description":"Production Server"}],"security":[{"bearerAuth":[]}],"tags":[{"name":"Files API V1","description":"Файлы: метаданные в PostgreSQL (`files.file_metadata`), объекты в S3.\nDELETE при `app.kafka.enabled=true` — `file.deleted.v1` (outbox → equipment-service, см. core-v1 info).\n"}],"paths":{"/api/v1/files/upload":{"post":{"tags":["Files API V1"],"summary":"Загрузить один файл (v1)","description":"Сохраняет объект в S3 и запись в `file_metadata`. Владелец — JWT claim `sub`.\n\n**Публичный доступ (CDN):** query **`fileStorageType=STATIC_PUBLIC`**. Ответ **201** содержит **`publicUrl`**\n(постоянная HTTPS-ссылка на CDN). `accessPolicy` в query не нужен — всегда **PUBLIC**.\nОграничения: whitelist `app.file.static-allowed-mime-types`, magic bytes, макс. размер `app.file.static-max-file-size` (default 10MB).\n\n**Приватный файл (default):** `fileStorageType` не указывать или `DEFAULT`; `accessPolicy` — `PRIVATE` (default) или `PUBLIC`;\nскачивание через `downloadPath` / presigned `downloadUrlPath`.\n\nПривязка: `entityType` + `entityId` только вместе; asset — оба пустые. `entityStatus` — только с привязкой.\nНарушение MIME/размера — **422** (`FileException`).\n","operationId":"upload","parameters":[{"name":"description","in":"query","description":"Описание файла (опционально)","required":false,"schema":{"type":"string"}},{"name":"fileStorageType","in":"query","description":"Класс S3-хранилища. **Публичный CDN:** укажите `STATIC_PUBLIC` — в ответе будет `publicUrl`, `accessPolicy` всегда PUBLIC (query `accessPolicy` игнорируется). По умолчанию — `DEFAULT` (приватный bucket, presign/proxy).","required":false,"schema":{"type":"string","default":"DEFAULT","description":"Класс S3-хранилища: DEFAULT — приватный bucket; STATIC_PUBLIC — публичный CDN","enum":["DEFAULT","STATIC_PUBLIC"],"example":"STATIC_PUBLIC"}},{"name":"accessPolicy","in":"query","description":"Политика доступа для **DEFAULT**: `PRIVATE` (если не указано) или `PUBLIC`. Для **STATIC_PUBLIC** не передавайте — всегда принудительно PUBLIC.","required":false,"schema":{"type":"string","enum":["PUBLIC","PRIVATE"]}},{"name":"entityType","in":"query","description":"Тип связанной сущности","required":false,"schema":{"type":"string"},"example":"EQUIPMENT"},{"name":"entityId","in":"query","description":"Идентификатор связанной сущности","required":false,"schema":{"type":"string","format":"uuid"},"example":"3fa85f64-5717-4562-b3fc-2c963f66afa6"},{"name":"entityStatus","in":"query","description":"Статус привязки к сущности","required":false,"schema":{"type":"string","enum":["IN_PROGRESS","PAID","ACTIVE","PENDING"]}}],"requestBody":{"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","format":"binary","description":"Файл для загрузки (multipart, поле file)"}},"required":["file"]}}}},"responses":{"201":{"description":"Файл загружен; для STATIC_PUBLIC в теле есть publicUrl","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FileMetadataResponseV1"}}}},"400":{"description":"Некорректный запрос: валидация (@Valid), пустой PATCH/batch, @ValidEntityBinding, недопустимый sort","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":400,"message":"Ошибка валидации","details":{"file":"не должно быть пустым"},"path":"/api/v1/files/upload","timestamp":"2026-03-26T10:32:26.961Z"}}}},"401":{"description":"Отсутствует или недействителен токен авторизации (JWT)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":401,"message":"Full authentication is required to access this resource","details":null,"path":"/api/v1/files","timestamp":"2026-03-26T10:32:26.961Z"}}}},"403":{"description":"Недостаточно роли JWT (gateway) или нет прав на PATCH/DELETE (object-level)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":403,"message":"Доступ запрещён","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6","timestamp":"2026-03-26T10:32:26.961Z"}}}},"406":{"description":"Клиент не принимает application/json для тела ошибки (часто на download с Accept: application/octet-stream)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":406,"message":"Клиент не принимает application/json для ответа об ошибке","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6/download","timestamp":"2026-03-26T10:32:26.961Z"}}}},"404":{"description":"Файл не найден или недоступен текущему пользователю","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":404,"message":"Файл не найден: id=...","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6","timestamp":"2026-03-26T10:32:26.961Z"}}}},"409":{"description":"Конфликт версий (оптимистичная блокировка @Version)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":409,"message":"Данные были изменены другим пользователем.","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6","timestamp":"2026-04-15T10:32:26.961Z"}}}},"422":{"description":"Нарушение бизнес-правил: MIME whitelist + magic bytes (DEFAULT и STATIC_PUBLIC), лимит static-файла, batch сверх лимита, BFF-привязка без прав, активная сущность при DELETE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":422,"message":"Удаление файла запрещено: сущность в активном статусе IN_PROGRESS","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6","timestamp":"2026-04-02T10:32:26.961Z"}}}},"500":{"description":"Внутренняя ошибка сервера","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":500,"message":"Internal Server Error","details":null,"path":"/api/v1/files","timestamp":"2026-04-02T10:32:26.961Z"}}}},"503":{"description":"Сбой S3 или недоступность хранилища","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":503,"message":"Временная ошибка хранилища файлов","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6/download","timestamp":"2026-04-02T10:32:26.961Z"}}}}}}},"/api/v1/files/upload/batch":{"post":{"tags":["Files API V1"],"summary":"Пакетная загрузка файлов (v1)","description":"Загружает несколько файлов одним запросом (поле `files`). Query-параметры — общие для всех файлов\n(в т.ч. **`fileStorageType=STATIC_PUBLIC`** для публичного CDN и `publicUrl` в каждом элементе ответа).\nЛимит количества — `app.file.batch-upload-max-files` (по умолчанию 20). Пустой `files` — **400**.\nТранзакция атомарна: ошибка на любом файле откатывает весь batch (**422** / **400**). Владелец — JWT `sub`.\n","operationId":"uploadBatch","parameters":[{"name":"description","in":"query","description":"Описание файла (опционально)","required":false,"schema":{"type":"string"}},{"name":"fileStorageType","in":"query","description":"Класс S3-хранилища. **Публичный CDN:** укажите `STATIC_PUBLIC` — в ответе будет `publicUrl`, `accessPolicy` всегда PUBLIC (query `accessPolicy` игнорируется). По умолчанию — `DEFAULT` (приватный bucket, presign/proxy).","required":false,"schema":{"type":"string","default":"DEFAULT","description":"Класс S3-хранилища: DEFAULT — приватный bucket; STATIC_PUBLIC — публичный CDN","enum":["DEFAULT","STATIC_PUBLIC"],"example":"STATIC_PUBLIC"}},{"name":"accessPolicy","in":"query","description":"Политика доступа для **DEFAULT**: `PRIVATE` (если не указано) или `PUBLIC`. Для **STATIC_PUBLIC** не передавайте — всегда принудительно PUBLIC.","required":false,"schema":{"type":"string","enum":["PUBLIC","PRIVATE"]}},{"name":"entityType","in":"query","description":"Тип связанной сущности","required":false,"schema":{"type":"string"},"example":"EQUIPMENT"},{"name":"entityId","in":"query","description":"Идентификатор связанной сущности","required":false,"schema":{"type":"string","format":"uuid"},"example":"3fa85f64-5717-4562-b3fc-2c963f66afa6"},{"name":"entityStatus","in":"query","description":"Статус привязки к сущности","required":false,"schema":{"type":"string","enum":["IN_PROGRESS","PAID","ACTIVE","PENDING"]}}],"requestBody":{"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"files":{"type":"array","description":"Файлы для загрузки (multipart, поле files; лимит — app.file.batch-upload-max-files)","items":{"type":"string","format":"binary"}}},"required":["files"]}}},"required":true},"responses":{"201":{"description":"Файлы загружены","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchUploadResponseV1"}}}},"400":{"description":"Некорректный запрос: валидация (@Valid), пустой PATCH/batch, @ValidEntityBinding, недопустимый sort","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":400,"message":"Ошибка валидации","details":{"file":"не должно быть пустым"},"path":"/api/v1/files/upload","timestamp":"2026-03-26T10:32:26.961Z"}}}},"401":{"description":"Отсутствует или недействителен токен авторизации (JWT)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":401,"message":"Full authentication is required to access this resource","details":null,"path":"/api/v1/files","timestamp":"2026-03-26T10:32:26.961Z"}}}},"403":{"description":"Недостаточно роли JWT (gateway) или нет прав на PATCH/DELETE (object-level)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":403,"message":"Доступ запрещён","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6","timestamp":"2026-03-26T10:32:26.961Z"}}}},"406":{"description":"Клиент не принимает application/json для тела ошибки (часто на download с Accept: application/octet-stream)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":406,"message":"Клиент не принимает application/json для ответа об ошибке","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6/download","timestamp":"2026-03-26T10:32:26.961Z"}}}},"404":{"description":"Файл не найден или недоступен текущему пользователю","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":404,"message":"Файл не найден: id=...","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6","timestamp":"2026-03-26T10:32:26.961Z"}}}},"409":{"description":"Конфликт версий (оптимистичная блокировка @Version)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":409,"message":"Данные были изменены другим пользователем.","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6","timestamp":"2026-04-15T10:32:26.961Z"}}}},"422":{"description":"Нарушение бизнес-правил: MIME whitelist + magic bytes (DEFAULT и STATIC_PUBLIC), лимит static-файла, batch сверх лимита, BFF-привязка без прав, активная сущность при DELETE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":422,"message":"Удаление файла запрещено: сущность в активном статусе IN_PROGRESS","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6","timestamp":"2026-04-02T10:32:26.961Z"}}}},"500":{"description":"Внутренняя ошибка сервера","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":500,"message":"Internal Server Error","details":null,"path":"/api/v1/files","timestamp":"2026-04-02T10:32:26.961Z"}}}},"503":{"description":"Сбой S3 или недоступность хранилища","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":503,"message":"Временная ошибка хранилища файлов","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6/download","timestamp":"2026-04-02T10:32:26.961Z"}}}}}}},"/api/v1/files/{id}":{"get":{"tags":["Files API V1"],"summary":"Получить метаданные файла по id (v1)","description":"Возвращает метаданные из PostgreSQL. В ответе `downloadUrlPath` (presigned URL для BFF)\nи `downloadPath` (прокси-скачивание). Чужой, удалённый или недоступный файл — 404.\n","operationId":"findById","parameters":[{"name":"id","in":"path","description":"Уникальный идентификатор файла (UUID v4)","required":true,"schema":{"type":"string","format":"uuid"},"example":"1fa85f64-5717-4562-b3fc-2c963f66afa6"}],"responses":{"200":{"description":"Метаданные успешно получены","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FileMetadataResponseV1"}}}},"400":{"description":"Некорректный запрос: валидация (@Valid), пустой PATCH/batch, @ValidEntityBinding, недопустимый sort","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":400,"message":"Ошибка валидации","details":{"file":"не должно быть пустым"},"path":"/api/v1/files/upload","timestamp":"2026-03-26T10:32:26.961Z"}}}},"401":{"description":"Отсутствует или недействителен токен авторизации (JWT)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":401,"message":"Full authentication is required to access this resource","details":null,"path":"/api/v1/files","timestamp":"2026-03-26T10:32:26.961Z"}}}},"403":{"description":"Недостаточно роли JWT (gateway) или нет прав на PATCH/DELETE (object-level)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":403,"message":"Доступ запрещён","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6","timestamp":"2026-03-26T10:32:26.961Z"}}}},"406":{"description":"Клиент не принимает application/json для тела ошибки (часто на download с Accept: application/octet-stream)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":406,"message":"Клиент не принимает application/json для ответа об ошибке","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6/download","timestamp":"2026-03-26T10:32:26.961Z"}}}},"404":{"description":"Файл не найден или недоступен текущему пользователю","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":404,"message":"Файл не найден: id=...","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6","timestamp":"2026-03-26T10:32:26.961Z"}}}},"409":{"description":"Конфликт версий (оптимистичная блокировка @Version)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":409,"message":"Данные были изменены другим пользователем.","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6","timestamp":"2026-04-15T10:32:26.961Z"}}}},"422":{"description":"Нарушение бизнес-правил: MIME whitelist + magic bytes (DEFAULT и STATIC_PUBLIC), лимит static-файла, batch сверх лимита, BFF-привязка без прав, активная сущность при DELETE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":422,"message":"Удаление файла запрещено: сущность в активном статусе IN_PROGRESS","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6","timestamp":"2026-04-02T10:32:26.961Z"}}}},"500":{"description":"Внутренняя ошибка сервера","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":500,"message":"Internal Server Error","details":null,"path":"/api/v1/files","timestamp":"2026-04-02T10:32:26.961Z"}}}},"503":{"description":"Сбой S3 или недоступность хранилища","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":503,"message":"Временная ошибка хранилища файлов","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6/download","timestamp":"2026-04-02T10:32:26.961Z"}}}}}},"delete":{"tags":["Files API V1"],"summary":"Удалить файл по id (v1)","description":"Soft-delete: запись `DELETED`, объект S3 — после commit (`S3StorageCleanupListener`).\nПри `app.kafka.enabled=true` — transactional outbox → топик `file.deleted.v1`\n(equipment-service снимает `icon_file_id` и `vehicles_photos` по `fileId`).\nДоставка асинхронная; HTTP **204** не означает, что consumer уже обработал событие.\nДоступ: владелец или ADMIN/SUPERADMIN. Активный `entity_status` → **422**.\n\n**Kafka** (`app.kafka.enabled=true`): `FileDeletedIntegrationService#enqueue` в той же TX, что soft-delete;\nпосле commit `OutboxRelay` → **`file.deleted.v1`** (key = `fileId`). HTTP **204** не ждёт equipment-service (см. core-v1 info).\n","operationId":"delete","parameters":[{"name":"id","in":"path","description":"Уникальный идентификатор файла (UUID v4)","required":true,"schema":{"type":"string","format":"uuid"},"example":"1fa85f64-5717-4562-b3fc-2c963f66afa6"}],"responses":{"204":{"description":"Файл успешно удалён"},"400":{"description":"Некорректный запрос: валидация (@Valid), пустой PATCH/batch, @ValidEntityBinding, недопустимый sort","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":400,"message":"Ошибка валидации","details":{"file":"не должно быть пустым"},"path":"/api/v1/files/upload","timestamp":"2026-03-26T10:32:26.961Z"}}}},"401":{"description":"Отсутствует или недействителен токен авторизации (JWT)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":401,"message":"Full authentication is required to access this resource","details":null,"path":"/api/v1/files","timestamp":"2026-03-26T10:32:26.961Z"}}}},"403":{"description":"Недостаточно роли JWT (gateway) или нет прав на PATCH/DELETE (object-level)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":403,"message":"Доступ запрещён","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6","timestamp":"2026-03-26T10:32:26.961Z"}}}},"406":{"description":"Клиент не принимает application/json для тела ошибки (часто на download с Accept: application/octet-stream)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":406,"message":"Клиент не принимает application/json для ответа об ошибке","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6/download","timestamp":"2026-03-26T10:32:26.961Z"}}}},"404":{"description":"Файл не найден или недоступен текущему пользователю","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":404,"message":"Файл не найден: id=...","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6","timestamp":"2026-03-26T10:32:26.961Z"}}}},"409":{"description":"Конфликт версий (оптимистичная блокировка @Version)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":409,"message":"Данные были изменены другим пользователем.","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6","timestamp":"2026-04-15T10:32:26.961Z"}}}},"422":{"description":"Нарушение бизнес-правил: MIME whitelist + magic bytes (DEFAULT и STATIC_PUBLIC), лимит static-файла, batch сверх лимита, BFF-привязка без прав, активная сущность при DELETE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":422,"message":"Удаление файла запрещено: сущность в активном статусе IN_PROGRESS","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6","timestamp":"2026-04-02T10:32:26.961Z"}}}},"500":{"description":"Внутренняя ошибка сервера","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":500,"message":"Internal Server Error","details":null,"path":"/api/v1/files","timestamp":"2026-04-02T10:32:26.961Z"}}}},"503":{"description":"Сбой S3 или недоступность хранилища","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":503,"message":"Временная ошибка хранилища файлов","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6/download","timestamp":"2026-04-02T10:32:26.961Z"}}}}}},"patch":{"tags":["Files API V1"],"summary":"Частично обновить метаданные файла (v1)","description":"PATCH метаданных: `description`, `accessPolicy`. Null-поля не затирают существующие значения.\nДоступ: владелец (JWT sub) или ADMIN/SUPERADMIN. Хотя бы одно поле в теле — иначе 400.\nНет прав — 403; файл недоступен — 404.\n","operationId":"partialUpdate","parameters":[{"name":"id","in":"path","description":"Уникальный идентификатор файла (UUID v4)","required":true,"schema":{"type":"string","format":"uuid"},"example":"1fa85f64-5717-4562-b3fc-2c963f66afa6"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateFileMetadataRequestV1"}}},"required":true},"responses":{"200":{"description":"Метаданные успешно обновлены","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FileMetadataResponseV1"}}}},"400":{"description":"Некорректный запрос: валидация (@Valid), пустой PATCH/batch, @ValidEntityBinding, недопустимый sort","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":400,"message":"Ошибка валидации","details":{"file":"не должно быть пустым"},"path":"/api/v1/files/upload","timestamp":"2026-03-26T10:32:26.961Z"}}}},"401":{"description":"Отсутствует или недействителен токен авторизации (JWT)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":401,"message":"Full authentication is required to access this resource","details":null,"path":"/api/v1/files","timestamp":"2026-03-26T10:32:26.961Z"}}}},"403":{"description":"Недостаточно роли JWT (gateway) или нет прав на PATCH/DELETE (object-level)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":403,"message":"Доступ запрещён","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6","timestamp":"2026-03-26T10:32:26.961Z"}}}},"406":{"description":"Клиент не принимает application/json для тела ошибки (часто на download с Accept: application/octet-stream)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":406,"message":"Клиент не принимает application/json для ответа об ошибке","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6/download","timestamp":"2026-03-26T10:32:26.961Z"}}}},"404":{"description":"Файл не найден или недоступен текущему пользователю","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":404,"message":"Файл не найден: id=...","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6","timestamp":"2026-03-26T10:32:26.961Z"}}}},"409":{"description":"Конфликт версий (оптимистичная блокировка @Version)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":409,"message":"Данные были изменены другим пользователем.","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6","timestamp":"2026-04-15T10:32:26.961Z"}}}},"422":{"description":"Нарушение бизнес-правил: MIME whitelist + magic bytes (DEFAULT и STATIC_PUBLIC), лимит static-файла, batch сверх лимита, BFF-привязка без прав, активная сущность при DELETE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":422,"message":"Удаление файла запрещено: сущность в активном статусе IN_PROGRESS","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6","timestamp":"2026-04-02T10:32:26.961Z"}}}},"500":{"description":"Внутренняя ошибка сервера","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":500,"message":"Internal Server Error","details":null,"path":"/api/v1/files","timestamp":"2026-04-02T10:32:26.961Z"}}}},"503":{"description":"Сбой S3 или недоступность хранилища","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":503,"message":"Временная ошибка хранилища файлов","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6/download","timestamp":"2026-04-02T10:32:26.961Z"}}}}}}},"/api/v1/files":{"get":{"tags":["Files API V1"],"summary":"Получить список файлов (v1)","description":"Возвращает страницу `file_metadata` с фильтрацией и сортировкой. Нумерация страниц с 1 (`page=1`).\nРежим BFF: при `entityType` и `entityId` — файлы сущности, доступные по canRead (включая чужие PUBLIC).\nИначе USER видит только свои файлы (ownerId из JWT sub). `assetsOnly=true` — без привязки к сущности.\nФильтр ownerId в query — только MANAGER / ADMIN / SUPERADMIN.\nСортировка: sort=originalName|mimeType|sizeBytes|ownerId|entityType|entityId|accessPolicy|status|createdAt|updatedAt.\n","operationId":"all","parameters":[{"name":"entityType","in":"query","description":"Тип связанной сущности (точное совпадение); с entityId — режим BFF","required":false,"schema":{"type":"string"},"example":"EQUIPMENT"},{"name":"entityId","in":"query","description":"Идентификатор связанной сущности; с entityType — режим BFF","required":false,"schema":{"type":"string","format":"uuid"}},{"name":"ownerId","in":"query","description":"Фильтр по владельцу (только MANAGER/ADMIN)","required":false,"schema":{"type":"string","format":"uuid"}},{"name":"status","in":"query","description":"Статус файла","required":false,"schema":{"type":"string","enum":["READY","DELETED"]}},{"name":"originalName","in":"query","description":"Подстрока в исходном имени файла, без учёта регистра","required":false,"schema":{"type":"string"}},{"name":"assetsOnly","in":"query","description":"Только assets: файлы без entityType/entityId (не сочетается с фильтром по сущности)","required":false,"schema":{"type":"boolean"}},{"name":"page","in":"query","description":"Номер страницы (с 1; первая страница — page=1)","required":false,"schema":{"type":"integer","default":1,"minimum":1},"example":"1"},{"name":"size","in":"query","description":"Размер страницы (по умолчанию 20, макс. 100)","required":false,"schema":{"type":"integer","default":20,"maximum":100,"minimum":1},"example":"20"},{"name":"sort","in":"query","description":"Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.","required":false,"schema":{"type":"array","items":{"type":"string"}}}],"responses":{"200":{"description":"Список успешно получен","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PagedResponseFileMetadataResponseV1"}}}},"400":{"description":"Некорректный запрос: валидация (@Valid), пустой PATCH/batch, @ValidEntityBinding, недопустимый sort","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":400,"message":"Ошибка валидации","details":{"file":"не должно быть пустым"},"path":"/api/v1/files/upload","timestamp":"2026-03-26T10:32:26.961Z"}}}},"401":{"description":"Отсутствует или недействителен токен авторизации (JWT)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":401,"message":"Full authentication is required to access this resource","details":null,"path":"/api/v1/files","timestamp":"2026-03-26T10:32:26.961Z"}}}},"403":{"description":"Недостаточно роли JWT (gateway) или нет прав на PATCH/DELETE (object-level)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":403,"message":"Доступ запрещён","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6","timestamp":"2026-03-26T10:32:26.961Z"}}}},"406":{"description":"Клиент не принимает application/json для тела ошибки (часто на download с Accept: application/octet-stream)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":406,"message":"Клиент не принимает application/json для ответа об ошибке","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6/download","timestamp":"2026-03-26T10:32:26.961Z"}}}},"404":{"description":"Файл не найден или недоступен текущему пользователю","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":404,"message":"Файл не найден: id=...","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6","timestamp":"2026-03-26T10:32:26.961Z"}}}},"409":{"description":"Конфликт версий (оптимистичная блокировка @Version)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":409,"message":"Данные были изменены другим пользователем.","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6","timestamp":"2026-04-15T10:32:26.961Z"}}}},"422":{"description":"Нарушение бизнес-правил: MIME whitelist + magic bytes (DEFAULT и STATIC_PUBLIC), лимит static-файла, batch сверх лимита, BFF-привязка без прав, активная сущность при DELETE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":422,"message":"Удаление файла запрещено: сущность в активном статусе IN_PROGRESS","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6","timestamp":"2026-04-02T10:32:26.961Z"}}}},"500":{"description":"Внутренняя ошибка сервера","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":500,"message":"Internal Server Error","details":null,"path":"/api/v1/files","timestamp":"2026-04-02T10:32:26.961Z"}}}},"503":{"description":"Сбой S3 или недоступность хранилища","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":503,"message":"Временная ошибка хранилища файлов","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6/download","timestamp":"2026-04-02T10:32:26.961Z"}}}}}},"delete":{"tags":["Files API V1"],"summary":"Удалить файлы сущности (v1)","description":"Soft-delete всех доступных файлов с `entityType` и `entityId` (query). Файлы без права DELETE пропускаются (outbox для них не создаётся).\nНа каждый успешно удалённый файл — своё событие `file.deleted.v1` (если Kafka включён). Ответ **204** даже при пустом наборе кандидатов.\n\n**Kafka** (`app.kafka.enabled=true`): `FileDeletedIntegrationService#enqueue` в той же TX, что soft-delete;\nпосле commit `OutboxRelay` → **`file.deleted.v1`** (key = `fileId`). HTTP **204** не ждёт equipment-service (см. core-v1 info).\n","operationId":"deleteByEntity","parameters":[{"name":"entityType","in":"query","description":"Тип связанной сущности","required":true,"schema":{"type":"string"},"example":"EQUIPMENT"},{"name":"entityId","in":"query","description":"Уникальный идентификатор сущности (UUID v4)","required":true,"schema":{"type":"string","format":"uuid"},"example":"3fa85f64-5717-4562-b3fc-2c963f66afa6"}],"responses":{"204":{"description":"Операция выполнена"},"400":{"description":"Некорректный запрос: валидация (@Valid), пустой PATCH/batch, @ValidEntityBinding, недопустимый sort","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":400,"message":"Ошибка валидации","details":{"file":"не должно быть пустым"},"path":"/api/v1/files/upload","timestamp":"2026-03-26T10:32:26.961Z"}}}},"401":{"description":"Отсутствует или недействителен токен авторизации (JWT)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":401,"message":"Full authentication is required to access this resource","details":null,"path":"/api/v1/files","timestamp":"2026-03-26T10:32:26.961Z"}}}},"403":{"description":"Недостаточно роли JWT (gateway) или нет прав на PATCH/DELETE (object-level)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":403,"message":"Доступ запрещён","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6","timestamp":"2026-03-26T10:32:26.961Z"}}}},"406":{"description":"Клиент не принимает application/json для тела ошибки (часто на download с Accept: application/octet-stream)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":406,"message":"Клиент не принимает application/json для ответа об ошибке","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6/download","timestamp":"2026-03-26T10:32:26.961Z"}}}},"404":{"description":"Файл не найден или недоступен текущему пользователю","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":404,"message":"Файл не найден: id=...","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6","timestamp":"2026-03-26T10:32:26.961Z"}}}},"409":{"description":"Конфликт версий (оптимистичная блокировка @Version)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":409,"message":"Данные были изменены другим пользователем.","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6","timestamp":"2026-04-15T10:32:26.961Z"}}}},"422":{"description":"Нарушение бизнес-правил: MIME whitelist + magic bytes (DEFAULT и STATIC_PUBLIC), лимит static-файла, batch сверх лимита, BFF-привязка без прав, активная сущность при DELETE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":422,"message":"Удаление файла запрещено: сущность в активном статусе IN_PROGRESS","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6","timestamp":"2026-04-02T10:32:26.961Z"}}}},"500":{"description":"Внутренняя ошибка сервера","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":500,"message":"Internal Server Error","details":null,"path":"/api/v1/files","timestamp":"2026-04-02T10:32:26.961Z"}}}},"503":{"description":"Сбой S3 или недоступность хранилища","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":503,"message":"Временная ошибка хранилища файлов","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6/download","timestamp":"2026-04-02T10:32:26.961Z"}}}}}}},"/api/v1/files/{id}/download":{"get":{"tags":["Files API V1"],"summary":"Скачать файл по id (v1)","description":"Прокси-скачивание через file-service (поток S3) для DEFAULT (нужен JWT, кроме PUBLIC — любой пользователь).\nДля STATIC_PUBLIC — 307 на `publicUrl` (CDN); **JWT не требуется**.\nЗаголовки (только для 200): Cache-Control, Content-Disposition, Content-Type, Content-Length.\nЧужой, удалённый или недоступный файл — 404.\nОшибки возвращаются как JSON (`ApiErrorDto`), даже если клиент запросил `application/octet-stream`.\n","operationId":"download","parameters":[{"name":"id","in":"path","description":"Уникальный идентификатор файла (UUID v4)","required":true,"schema":{"type":"string","format":"uuid"},"example":"1fa85f64-5717-4562-b3fc-2c963f66afa6"}],"responses":{"200":{"description":"Содержимое файла (поток, fileStorageType=DEFAULT)","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}},"application/json":{"schema":{"type":"string","format":"binary"}}}},"307":{"description":"Редирект на publicUrl (CDN, fileStorageType=STATIC_PUBLIC)","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}},"application/json":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Некорректный запрос: валидация (@Valid), пустой PATCH/batch, @ValidEntityBinding, недопустимый sort","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":400,"message":"Ошибка валидации","details":{"file":"не должно быть пустым"},"path":"/api/v1/files/upload","timestamp":"2026-03-26T10:32:26.961Z"}}}},"401":{"description":"Отсутствует или недействителен токен авторизации (JWT)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":401,"message":"Full authentication is required to access this resource","details":null,"path":"/api/v1/files","timestamp":"2026-03-26T10:32:26.961Z"}}}},"403":{"description":"Недостаточно роли JWT (gateway) или нет прав на PATCH/DELETE (object-level)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":403,"message":"Доступ запрещён","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6","timestamp":"2026-03-26T10:32:26.961Z"}}}},"406":{"description":"Клиент не принимает application/json для тела ошибки (часто на download с Accept: application/octet-stream)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":406,"message":"Клиент не принимает application/json для ответа об ошибке","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6/download","timestamp":"2026-03-26T10:32:26.961Z"}}}},"404":{"description":"Файл не найден или недоступен текущему пользователю","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":404,"message":"Файл не найден: id=...","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6","timestamp":"2026-03-26T10:32:26.961Z"}}}},"409":{"description":"Конфликт версий (оптимистичная блокировка @Version)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":409,"message":"Данные были изменены другим пользователем.","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6","timestamp":"2026-04-15T10:32:26.961Z"}}}},"422":{"description":"Нарушение бизнес-правил: MIME whitelist + magic bytes (DEFAULT и STATIC_PUBLIC), лимит static-файла, batch сверх лимита, BFF-привязка без прав, активная сущность при DELETE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":422,"message":"Удаление файла запрещено: сущность в активном статусе IN_PROGRESS","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6","timestamp":"2026-04-02T10:32:26.961Z"}}}},"500":{"description":"Внутренняя ошибка сервера","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":500,"message":"Internal Server Error","details":null,"path":"/api/v1/files","timestamp":"2026-04-02T10:32:26.961Z"}}}},"503":{"description":"Сбой S3 или недоступность хранилища","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":503,"message":"Временная ошибка хранилища файлов","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6/download","timestamp":"2026-04-02T10:32:26.961Z"}}}}},"security":[]}},"/api/v1/files/{id}/download-url":{"get":{"tags":["Files API V1"],"summary":"Presigned URL для скачивания из S3 (v1, BFF)","description":"Presigned HTTPS URL (TTL `app.s3.presign-ttl`) для DEFAULT (нужен JWT).\nДля STATIC_PUBLIC — `publicUrl` без presign; **JWT не требуется**. `redirect=true` — 302 на URL.\nЧужой, удалённый или недоступный файл — 404.\n","operationId":"downloadUrl","parameters":[{"name":"id","in":"path","description":"Уникальный идентификатор файла (UUID v4)","required":true,"schema":{"type":"string","format":"uuid"},"example":"1fa85f64-5717-4562-b3fc-2c963f66afa6"},{"name":"redirect","in":"query","description":"Если true — HTTP 302 на presigned URL вместо JSON","required":false,"schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"Presigned URL (JSON, `redirect=false` или параметр не передан)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FileDownloadUrlResponseV1"}}}},"302":{"description":"Редирект на S3 или CDN (`redirect=true`); тело отсутствует"},"400":{"description":"Некорректный запрос: валидация (@Valid), пустой PATCH/batch, @ValidEntityBinding, недопустимый sort","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":400,"message":"Ошибка валидации","details":{"file":"не должно быть пустым"},"path":"/api/v1/files/upload","timestamp":"2026-03-26T10:32:26.961Z"}}}},"401":{"description":"Отсутствует или недействителен токен авторизации (JWT)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":401,"message":"Full authentication is required to access this resource","details":null,"path":"/api/v1/files","timestamp":"2026-03-26T10:32:26.961Z"}}}},"403":{"description":"Недостаточно роли JWT (gateway) или нет прав на PATCH/DELETE (object-level)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":403,"message":"Доступ запрещён","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6","timestamp":"2026-03-26T10:32:26.961Z"}}}},"406":{"description":"Клиент не принимает application/json для тела ошибки (часто на download с Accept: application/octet-stream)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":406,"message":"Клиент не принимает application/json для ответа об ошибке","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6/download","timestamp":"2026-03-26T10:32:26.961Z"}}}},"404":{"description":"Файл не найден или недоступен текущему пользователю","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":404,"message":"Файл не найден: id=...","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6","timestamp":"2026-03-26T10:32:26.961Z"}}}},"409":{"description":"Конфликт версий (оптимистичная блокировка @Version)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":409,"message":"Данные были изменены другим пользователем.","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6","timestamp":"2026-04-15T10:32:26.961Z"}}}},"422":{"description":"Нарушение бизнес-правил: MIME whitelist + magic bytes (DEFAULT и STATIC_PUBLIC), лимит static-файла, batch сверх лимита, BFF-привязка без прав, активная сущность при DELETE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":422,"message":"Удаление файла запрещено: сущность в активном статусе IN_PROGRESS","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6","timestamp":"2026-04-02T10:32:26.961Z"}}}},"500":{"description":"Внутренняя ошибка сервера","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":500,"message":"Internal Server Error","details":null,"path":"/api/v1/files","timestamp":"2026-04-02T10:32:26.961Z"}}}},"503":{"description":"Сбой S3 или недоступность хранилища","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorDto"},"example":{"status":503,"message":"Временная ошибка хранилища файлов","details":null,"path":"/api/v1/files/1fa85f64-5717-4562-b3fc-2c963f66afa6/download","timestamp":"2026-04-02T10:32:26.961Z"}}}}},"security":[]}}},"components":{"schemas":{"FileMetadataResponseV1":{"type":"object","description":"Метаданные файла (file_metadata)","properties":{"id":{"type":"string","format":"uuid","description":"Идентификатор файла (UUID v4)","example":"1fa85f64-5717-4562-b3fc-2c963f66afa6"},"originalName":{"type":"string","description":"Исходное имя файла","example":"photo.jpg"},"mimeType":{"type":"string","description":"MIME-тип","example":"image/jpeg"},"sizeBytes":{"type":"integer","format":"int64","description":"Размер в байтах","example":204800},"checksumSha256":{"type":"string","description":"Контрольная сумма SHA-256; при upload пока не вычисляется — может быть null"},"ownerId":{"type":"string","format":"uuid","description":"Идентификатор владельца (JWT claim sub)","example":"1fa85f64-5717-4562-b3fc-2c963f66afa6"},"entityType":{"type":"string","description":"Тип связанной сущности","example":"EQUIPMENT"},"entityId":{"type":"string","format":"uuid","description":"Идентификатор связанной сущности","example":"3fa85f64-5717-4562-b3fc-2c963f66afa6"},"entityStatus":{"type":"string","description":"Статус привязки к бизнес-сущности","enum":["IN_PROGRESS","PAID","ACTIVE","PENDING"]},"description":{"type":"string","description":"Описание файла","example":"Фото техники"},"accessPolicy":{"type":"string","description":"Политика доступа","enum":["PUBLIC","PRIVATE"]},"status":{"type":"string","description":"Статус файла в системе","enum":["READY","DELETED"]},"fileStorageType":{"type":"string","description":"Класс хранения S3: DEFAULT — приватный bucket; STATIC_PUBLIC — CDN/public URL","enum":["DEFAULT","STATIC_PUBLIC"]},"createdAt":{"type":"string","format":"date-time","description":"Дата создания записи"},"updatedAt":{"type":"string","format":"date-time","description":"Дата последнего обновления"},"publicUrl":{"type":"string","description":"Публичный HTTPS URL на CDN; заполняется при fileStorageType=STATIC_PUBLIC после upload","example":"https://s3.twcstorage.ru/rukki-s3-static/owner-uuid/file-uuid/logo.png"},"downloadPath":{"type":"string","description":"Относительный путь прокси-скачивания через file-service"},"downloadUrlPath":{"type":"string","description":"Относительный путь presigned URL (BFF); для STATIC_PUBLIC — null"}}},"ApiErrorDto":{"type":"object","description":"Структура ошибки API","properties":{"status":{"type":"integer","format":"int32","description":"HTTP статус ошибки","example":400},"message":{"type":"string","description":"Краткое сообщение об ошибке","example":"Ошибка валидации запроса"},"details":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}},"description":"Дополнительные детали (например, список ошибок валидации по каждому полю)"},"path":{"type":"string","description":"URI ресурса, на котором произошла ошибка","example":"/api/v1/files"},"timestamp":{"type":"string","format":"date-time","description":"Время возникновения ошибки"}}},"BatchUploadResponseV1":{"type":"object","description":"Результат пакетной загрузки файлов","properties":{"files":{"type":"array","description":"Метаданные загруженных файлов","items":{"$ref":"#/components/schemas/FileMetadataResponseV1"}},"uploadedCount":{"type":"integer","format":"int32","description":"Количество загруженных файлов","example":3}}},"UpdateFileMetadataRequestV1":{"type":"object","description":"Тело запроса с изменяемыми полями метаданных","properties":{"description":{"type":"string","description":"Описание файла","example":"Фото техники","maxLength":2000,"minLength":0},"accessPolicy":{"type":"string","description":"Политика доступа (PRIVATE или PUBLIC)","enum":["PUBLIC","PRIVATE"]}}},"PagedResponseFileMetadataResponseV1":{"type":"object","description":"Постраничный ответ со списком элементов","properties":{"content":{"type":"array","description":"Элементы текущей страницы","items":{"$ref":"#/components/schemas/FileMetadataResponseV1"}},"currentPage":{"type":"integer","format":"int32","description":"Номер текущей страницы (с 1)","example":1},"pageSize":{"type":"integer","format":"int32","description":"Размер страницы","example":20},"totalElements":{"type":"integer","format":"int64","description":"Общее количество элементов","example":100}}},"FileDownloadUrlResponseV1":{"type":"object","description":"Presigned URL для прямого скачивания из S3 (ответ GET .../download-url)","properties":{"url":{"type":"string","description":"Presigned GET URL в bucket S3","example":"https://s3.twcstorage.ru/rukki-s3/owner/uuid/file.jpg?X-Amz-Signature=..."},"expiresAt":{"type":"string","format":"date-time","description":"Момент истечения подписи URL (now + app.s3.presign-ttl)","example":"2026-05-25T12:45:00+03:00"}}}},"securitySchemes":{"bearerAuth":{"type":"http","description":"JWT access token identity-service.\nВ Swagger UI вставьте только значение токена (префикс Bearer подставляется автоматически).\nРоли: claim realm_access.roles (USER, MANAGER, ADMIN, SUPERADMIN).\n","scheme":"bearer","bearerFormat":"JWT"}}}}