Загрузка и хранение файлов с S3¶
Это руководство знакомит с S3-совместимым файловым хранилищем в HTTP-приложении Kora. В нем рассматривается, как маршруты загрузки и скачивания принимают multipart-данные, как S3-клиент Kora сохраняет объекты в корзину, и как службы приложения отделяют метаданные файлов от задач объектного хранилища. Вы также увидите, как локальная инфраструктура MinIO дает ту же форму API, что и S3-хранилище промышленного типа.
Если в процессе захочется сверить результат, используйте готовое рабочее приложение: Kora Java S3 App.
Если в процессе захочется сверить результат, используйте готовое рабочее приложение: Kora Kotlin S3 App.
Что вы создадите¶
В этом руководстве вы расширите приложение HTTP-сервера небольшим API файлового хранилища, основанным на S3-совместимом хранилище.
К концу приложение будет поддерживать:
- multipart-загрузку файлов через
POST /files/upload - список файлов через
GET /files - скачивание файла через
GET /files/{fileId} - удаление файла через
DELETE /files/{fileId} - декларативный S3-клиент, созданный с помощью
@S3.Client - локальную разработку и тесты на MinIO как S3-совместимой серверной части
Что понадобится¶
database-jdbc-advanced.md - JDK 17 или новее - Gradle 7+ - Docker для локальных запусков MinIO и тестов на основе контейнеров - текстовый редактор или среда разработки - пройденное руководство по HTTP-серверу
Требования¶
Обязательно: пройдите продвинутое руководство по HTTP-серверу
Это руководство предполагает, что вы уже прошли продвинутое руководство по HTTP-серверу и у вас уже есть приложение Kora с Application, UserController, DataController, общей связкой HTTP-сервера, а также знакомство с FormMultipart, JSON-ответами и контроллерами Kora.
Если вы еще не прошли продвинутое руководство по HTTP-серверу, сначала сделайте это, потому что здесь расширяется существующая область DataController поведением файлового хранилища, а не пересобирается HTTP-поверхность.
Обзор¶
Amazon S3-совместимое хранилище - это объектное хранилище, а не реляционная база данных и не локальная файловая система. Оно хранит объекты по корзине и ключу и предназначено для бинарного содержимого: файлов, изображений, документов, резервных копий и выгруженных данных. Приложения обычно держат метаданные в собственной доменной модели, а байты файла хранят в объектном хранилище.
Это различие важно, потому что у объектного хранилища другая модель доступа. Вы не обновляете отдельные колонки и не запрашиваете объекты через SQL. Вы кладете объект по ключу, получаете его по ключу, выводите список ключей и удаляете ключи. Приложение должно решить, как эти операции хранилища выглядят через HTTP.
Что такое S3¶
S3 - это API объектного хранилища и стандарт экосистемы, который начался с Amazon S3 и теперь поддерживается многими совместимыми системами, включая MinIO.
В отличие от реляционной базы данных, S3 не предназначен для структурированных запросов, соединений, транзакций или фильтрации бизнес-записей. Вместо этого он предназначен для хранения и получения больших бинарных объектов: файлов, изображений, видео, выгруженных отчетов, резервных копий и сгенерированных документов.
На практике S3 обычно работает рядом с базой данных, а не вместо базы данных:
- база данных хранит структурированные бизнес-данные, например пользователей, заказы, разрешения и ссылки на файлы
- S3-хранилище хранит само содержимое файла, обычно по объектному ключу
Такое разделение очень распространено в настоящих системах, потому что дает более удачную эксплуатационную модель:
- базы данных остаются сосредоточены на реляционных бизнес-данных
- большие файлы не раздувают таблицы базы данных и резервные копии
- файловое хранилище может масштабироваться независимо от основной базы данных приложения
- скачивание и загрузка файлов могут использовать инструменты и инфраструктуру, ориентированные на хранилище
Типичные реальные S3-сценарии:
- загруженные пользователями аватары, вложения, PDF и таблицы
- изображения товаров и медиакаталоги
- сгенерированные счета, отчеты и выгрузки
- архивы журналов и снимки резервных копий
- промежуточные файлы для аналитики и конвейеров машинного обучения
- открытые или закрытые статические ресурсы, отдаваемые через CDN-слои
Почему команды часто предпочитают S3-стиль хранилища для файлов:
- он хорошо масштабируется для большого числа объектов и больших размеров объектов
- модель доступа проста: сохранить по ключу, получить по ключу, удалить по ключу, вывести список по префиксу
- метаданные объекта и тип содержимого естественно идут вместе с файлом
- облачные и самостоятельные экосистемы уже предоставляют зрелые инструменты вокруг S3-совместимых хранилищ
Понятия объектного хранилища¶
Основные понятия S3 в этом руководстве:
- корзина: именованный контейнер для объектов
- ключ: идентификатор объекта внутри корзина
- тело объекта: содержимое файла
- метаданные: необязательная информация об объекте
- тип содержимого: тип медиа, который клиенты используют при скачивании или отображении объекта
В отличие от строки базы данных, объект обычно читается и записывается как поток байтов. Поэтому конечные точки загрузки и скачивания отличаются от JSON CRUD-маршрутов.
HTTP-границы загрузки и скачивания¶
Файловые API часто объединяют задачи HTTP и хранилища. HTTP-слой принимает multipart-данные, открывает ответы скачивания и сопоставляет операции удаления/списка с маршрутами. S3-клиент выполняет операции с объектами: put, get, list и delete. Четкая граница не дает коду контроллера превратиться в реализацию хранилища.
Это руководство сосредоточено на небольшом API файлового хранилища:
- загрузить файл из multipart-запроса
- вывести список сохраненных объектов
- скачать объект по ключу
- удалить объект по ключу
Практический ход такой:
- добавить модуль S3-клиента Kora и его зависимость HTTP-клиента
- настроить один декларативный S3-клиент для корзины
uploads - сопоставить multipart-запросы загрузки с записью объектов
- сопоставить маршруты скачивания с чтением объектов
- проверить то же поведение на MinIO в тестах
Локальный MinIO и промышленная форма¶
MinIO используется как локальная S3-совместимая инфраструктура, потому что его легко запускать для разработки и тестов. Код приложения все равно использует AWS S3-модуль Kora, поэтому настройка разработки остается близкой к объектному хранилищу промышленного типа без необходимости настоящей облачной учетной записи. Тесты используют контейнеры, чтобы поведение хранилища было повторяемым.
Зависимости¶
Мы строим поверх существующего приложения HTTP-сервера, поэтому добавляем только S3-модуль и модуль HTTP-клиента, который нужен реализации AWS S3.
http-client-ok нужен, потому что AWS S3-модулю под капотом требуется реализация HTTP-клиента.
Модули¶
Теперь подключаем S3-модуль к существующему приложению HTTP-сервера.
Обновите src/main/java/ru/tinkoff/kora/guide/s3/Application.java:
package ru.tinkoff.kora.guide.s3;
import ru.tinkoff.kora.application.graph.KoraApplication;
import ru.tinkoff.kora.common.KoraApp;
import ru.tinkoff.kora.config.hocon.HoconConfigModule;
import ru.tinkoff.kora.http.client.ok.OkHttpClientModule;
import ru.tinkoff.kora.http.server.undertow.UndertowHttpServerModule;
import ru.tinkoff.kora.json.module.JsonModule;
import ru.tinkoff.kora.logging.logback.LogbackModule;
import ru.tinkoff.kora.s3.client.aws.AwsS3ClientModule;
@KoraApp
public interface Application extends
HoconConfigModule,
JsonModule,
LogbackModule,
OkHttpClientModule,
AwsS3ClientModule, // <----- Подключили модуль
UndertowHttpServerModule {
static void main(String[] args) {
KoraApplication.run(ApplicationGraph::graph);
}
}
Обновите src/main/kotlin/ru/tinkoff/kora/guide/s3/Application.kt:
package ru.tinkoff.kora.guide.s3
import ru.tinkoff.kora.application.graph.KoraApplication
import ru.tinkoff.kora.common.KoraApp
import ru.tinkoff.kora.config.hocon.HoconConfigModule
import ru.tinkoff.kora.http.client.ok.OkHttpClientModule
import ru.tinkoff.kora.http.server.undertow.UndertowHttpServerModule
import ru.tinkoff.kora.json.module.JsonModule
import ru.tinkoff.kora.logging.logback.LogbackModule
import ru.tinkoff.kora.s3.client.aws.AwsS3ClientModule
@KoraApp
interface Application :
HoconConfigModule,
JsonModule,
LogbackModule,
OkHttpClientModule,
AwsS3ClientModule, // <----- Подключили модуль
UndertowHttpServerModule
fun main() {
KoraApplication.run(ApplicationGraph::graph)
}
Мы сохраняем те же модули HTTP-сервера из предыдущего руководства и добавляем только S3-специфичные части.
Конфигурация¶
Приложение по-прежнему использует ту же конфигурацию HTTP-сервера из предыдущего руководства. Здесь мы добавляем только новый раздел s3client.
Полный справочник по конфигурации смотрите в разделе S3-клиент.
s3client {
url = ${S3_URL} //(1)!
accessKey = ${S3_ACCESS_KEY} //(2)!
secretKey = ${S3_SECRET_KEY} //(3)!
region = "us-east-1" //(4)!
region = ${?S3_REGION} //(5)!
uploads {
bucket = "uploads" //(6)!
bucket = ${?S3_BUCKET} //(7)!
}
}
- Обязательный URL конечной точки S3 из
S3_URL. - Обязательный ключ доступа S3 из
S3_ACCESS_KEY. - Обязательный секретный ключ S3 из
S3_SECRET_KEY. - Регион S3 по умолчанию для локального MinIO.
- Необязательное переопределение региона из
S3_REGION. - Имя корзины по умолчанию для декларативного клиента
uploads. - Необязательное переопределение корзины из
S3_BUCKET.
s3client:
url: ${S3_URL} #(1)!
accessKey: ${S3_ACCESS_KEY} #(2)!
secretKey: ${S3_SECRET_KEY} #(3)!
region: ${?S3_REGION:"us-east-1"} #(4)!
uploads:
bucket: ${?S3_BUCKET:"uploads"} #(5)!
- Обязательный URL конечной точки S3 из
S3_URL. - Обязательный ключ доступа S3 из
S3_ACCESS_KEY. - Обязательный секретный ключ S3 из
S3_SECRET_KEY. - Регион S3 с локальным значением по умолчанию и необязательным переопределением
S3_REGION. - Корзина для загрузок с локальным значением по умолчанию и необязательным переопределением
S3_BUCKET.
Эта конфигурация делает две вещи:
- настраивает общее подключение AWS S3-клиента
- настраивает один именованный декларативный клиент в
s3client.uploads
Этот подраздел uploads важен, потому что на следующем шаге мы напрямую привяжем его к @S3.Client("s3client.uploads").
MinIO здесь все равно полезен, хотя мы используем реализацию AWS-клиента. MinIO говорит на S3 API, поэтому дает дешевое локальное окружение и при этом сохраняет код приложения согласованным с клиентами в стиле AWS.
Декларативный S3-клиент¶
S3-модуль Kora поддерживает декларативные клиенты в том же духе, что и поддержка HTTP-клиентов. Это главная идея, которой учит руководство.
Создайте src/main/java/ru/tinkoff/kora/guide/s3/s3/S3FileClient.java:
package ru.tinkoff.kora.guide.s3.s3;
import ru.tinkoff.kora.s3.client.annotation.S3;
import ru.tinkoff.kora.s3.client.model.S3Body;
import ru.tinkoff.kora.s3.client.model.S3Object;
import ru.tinkoff.kora.s3.client.model.S3ObjectList;
import ru.tinkoff.kora.s3.client.model.S3ObjectUpload;
@S3.Client("s3client.uploads")
public interface S3FileClient {
@S3.Put("files/{fileId}")
S3ObjectUpload uploadFile(String fileId, S3Body body);
@S3.Get("files/{fileId}")
S3Object downloadFile(String fileId);
@S3.List("files/")
S3ObjectList listFiles();
@S3.Delete("files/{fileId}")
void deleteFile(String fileId);
}
Создайте src/main/kotlin/ru/tinkoff/kora/guide/s3/s3/S3FileClient.kt:
package ru.tinkoff.kora.guide.s3.s3
import ru.tinkoff.kora.s3.client.annotation.S3
import ru.tinkoff.kora.s3.client.model.S3Body
import ru.tinkoff.kora.s3.client.model.S3Object
import ru.tinkoff.kora.s3.client.model.S3ObjectList
import ru.tinkoff.kora.s3.client.model.S3ObjectUpload
@S3.Client("s3client.uploads")
interface S3FileClient {
@S3.Put("files/{fileId}")
fun uploadFile(fileId: String, body: S3Body): S3ObjectUpload
@S3.Get("files/{fileId}")
fun downloadFile(fileId: String): S3Object
@S3.List("files/")
fun listFiles(): S3ObjectList
@S3.Delete("files/{fileId}")
fun deleteFile(fileId: String)
}
Этот интерфейс намеренно небольшой. Каждый метод почти один к одному соответствует операции хранилища, а аннотации определяют объектный ключ или префикс ключа.
Несколько важных деталей:
@S3.Put("files/{fileId}")строит итоговый объектный ключ из аргумента метода@S3.List("files/")ограничивает вывод списка одним префиксом, что делает пример детерминированнымS3Body- это способ передать поток или материализованное содержимое загруженного файла в S3-клиент
Больше шаблонов аннотаций, например шаблоны, ответы только с метаданными и варианты списков, смотрите в документации S3-клиента.
DTO метаданных¶
S3-модуль уже дает низкоуровневые ответы хранилища, но наш HTTP API должен открывать стабильный и удобный для руководства DTO.
Создайте src/main/java/ru/tinkoff/kora/guide/s3/s3/FileMetadata.java:
Мы открываем только те поля, которые действительно используем в руководстве:
fileIdдля договора открытого APIsizeиcontentTypeдля изучения файла
Контроллер метаданных¶
Теперь начинаем связывать декларативный S3-клиент с HTTP API.
На этом первом шаге контроллера реализуем операцию загрузки, которая начинает жизненный цикл файла в объектном хранилище.
С нее удобно начать, потому что она показывает главное разделение ответственностей в проектировании:
- контроллер понимает HTTP-детали, например
FormMultipart - S3-клиент понимает ключи хранилища и операции с объектами
Такое разделение оставляет контроллер сосредоточенным на разборе запроса и формировании ответа, а декларативный S3-клиент - на объектном хранилище.
Обновите src/main/java/ru/tinkoff/kora/guide/s3/controller/DataController.java:
package ru.tinkoff.kora.guide.s3.controller;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import ru.tinkoff.kora.common.Component;
import ru.tinkoff.kora.common.util.ByteBufferPublisherInputStream;
import ru.tinkoff.kora.guide.s3.s3.FileMetadata;
import ru.tinkoff.kora.guide.s3.s3.S3FileClient;
import ru.tinkoff.kora.http.common.HttpMethod;
import ru.tinkoff.kora.http.common.annotation.HttpRoute;
import ru.tinkoff.kora.http.common.body.HttpBody;
import ru.tinkoff.kora.http.common.form.FormMultipart;
import ru.tinkoff.kora.http.common.header.HttpHeaders;
import ru.tinkoff.kora.http.server.common.HttpServerResponse;
import ru.tinkoff.kora.http.server.common.HttpServerResponseException;
import ru.tinkoff.kora.http.server.common.annotation.HttpController;
import ru.tinkoff.kora.json.common.annotation.Json;
import ru.tinkoff.kora.s3.client.S3NotFoundException;
import ru.tinkoff.kora.s3.client.model.S3Body;
@Component
@HttpController
public final class DataController {
private final S3FileClient s3FileClient;
public DataController(S3FileClient s3FileClient) {
this.s3FileClient = s3FileClient;
}
@HttpRoute(method = HttpMethod.POST, path = "/files/upload")
@Json
public FileMetadata uploadFile(FormMultipart multipart) {
var filePart = multipart.parts().stream()
.filter(part -> "file".equals(part.name()))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("No file part named 'file' provided"));
if (filePart instanceof FormMultipart.FormPart.MultipartFile mf) {
return this.uploadStream(mf.fileName(), mf.contentType(), new ByteArrayInputStream(mf.content()));
}
if (filePart instanceof FormMultipart.FormPart.MultipartFileStream mfs) {
return this.uploadStream(mfs.fileName(), mfs.contentType(), new ByteBufferPublisherInputStream(mfs.content()));
}
throw new IllegalArgumentException("Part 'file' must be a multipart file");
}
private FileMetadata uploadStream(String fileName, String contentType, InputStream inputStream) {
String actualContentType = (contentType == null || contentType.isBlank()) ? "application/octet-stream" : contentType;
String fileId = java.util.UUID.randomUUID().toString();
S3Body body = S3Body.ofInputStreamReadAll(inputStream, actualContentType);
this.s3FileClient.uploadFile(fileId, body);
return new FileMetadata(fileId, body.size(), actualContentType);
}
private FileMetadata toMetadata(String key, Long size, String contentType) {
String normalized = key.startsWith("files/") ? key.substring("files/".length()) : key;
return new FileMetadata(normalized, size, contentType);
}
@Json
public record DeleteFileResponse(String message) {}
}
Обновите src/main/kotlin/ru/tinkoff/kora/guide/s3/controller/DataController.kt:
package ru.tinkoff.kora.guide.s3.controller
import java.io.ByteArrayInputStream
import java.io.InputStream
import ru.tinkoff.kora.common.Component
import ru.tinkoff.kora.common.util.ByteBufferPublisherInputStream
import ru.tinkoff.kora.guide.s3.s3.FileMetadata
import ru.tinkoff.kora.guide.s3.s3.S3FileClient
import ru.tinkoff.kora.http.common.HttpMethod
import ru.tinkoff.kora.http.common.annotation.HttpRoute
import ru.tinkoff.kora.http.common.body.HttpBody
import ru.tinkoff.kora.http.common.form.FormMultipart
import ru.tinkoff.kora.http.common.header.HttpHeaders
import ru.tinkoff.kora.http.server.common.HttpServerResponse
import ru.tinkoff.kora.http.server.common.HttpServerResponseException
import ru.tinkoff.kora.http.server.common.annotation.HttpController
import ru.tinkoff.kora.json.common.annotation.Json
import ru.tinkoff.kora.s3.client.S3NotFoundException
import ru.tinkoff.kora.s3.client.model.S3Body
@Component
@HttpController
class DataController(
private val s3FileClient: S3FileClient
) {
@HttpRoute(method = HttpMethod.POST, path = "/files/upload")
@Json
fun uploadFile(multipart: FormMultipart): FileMetadata {
val filePart = multipart.parts()
.firstOrNull { it.name() == "file" }
?: throw IllegalArgumentException("No file part named 'file' provided")
return when (filePart) {
is FormMultipart.FormPart.MultipartFile -> {
uploadStream(filePart.fileName(), filePart.contentType(), ByteArrayInputStream(filePart.content()))
}
is FormMultipart.FormPart.MultipartFileStream -> {
uploadStream(filePart.fileName(), filePart.contentType(), ByteBufferPublisherInputStream(filePart.content()))
}
else -> throw IllegalArgumentException("Part 'file' must be a multipart file")
}
}
private fun uploadStream(fileName: String?, contentType: String?, inputStream: InputStream): FileMetadata {
val actualContentType = if (contentType.isNullOrBlank()) "application/octet-stream" else contentType
val fileId = java.util.UUID.randomUUID().toString()
val body = S3Body.ofInputStreamReadAll(inputStream, actualContentType)
s3FileClient.uploadFile(fileId, body)
return FileMetadata(fileId, body.size(), actualContentType)
}
private fun toMetadata(key: String, size: Long?, contentType: String?): FileMetadata {
val normalized = if (key.startsWith("files/")) key.removePrefix("files/") else key
return FileMetadata(normalized, size, contentType)
}
@Json
data class DeleteFileResponse(val message: String)
}
Этот контроллер сохраняет пример честным:
- загрузка использует
FormMultipart, потому что это самая распространенная форма HTTP-загрузки файлов - ключи хранилища остаются внутренней деталью контроллера и S3-клиента
- открытый API раскрывает только
fileId, а не сырые детали корзины или пути, переданные пользователем
Список, скачивание и удаление¶
Когда загрузка готова, можно добавить остальную часть жизненного цикла файла: чтение сохраненного, скачивание содержимого и удаление файла, когда он больше не нужен.
Эти конечные точки решают оставшиеся части API для чтения и очистки:
GET /filesпозволяет клиентам посмотреть, что уже сохраненоGET /files/{fileId}превращает S3-объект в обычный HTTP-ответ скачиванияDELETE /files/{fileId}удаляет объект, когда он больше не нужен приложению
Этот шаг полезен, потому что показывает, почему контроллер все еще важен, даже когда хранилище декларативное. S3-клиент возвращает объекты, ориентированные на хранилище, но контроллер отвечает за:
- сопоставление результатов списка с открытым DTO, который мы хотим раскрыть
- преобразование отсутствующих объектов в чистый HTTP
404 - создание обычного скачиваемого HTTP-ответа с типом содержимого и заголовками
- координацию запросов удаления через тот же открытый договор
fileId
Добавьте оставшиеся конечные точки в src/main/java/ru/tinkoff/kora/guide/s3/controller/DataController.java:
@HttpRoute(method = HttpMethod.GET, path = "/files")
@Json
public java.util.List<FileMetadata> listFiles() {
return this.s3FileClient.listFiles().objects().stream()
.map(object -> this.toMetadata(object.key(), object.size(), object.body().type()))
.toList();
}
@HttpRoute(method = HttpMethod.GET, path = "/files/{fileId}")
public HttpServerResponse downloadFile(String fileId) {
try {
var object = this.s3FileClient.downloadFile(fileId);
var bytes = object.body().asBytes();
var contentType = object.body().type() == null ? "application/octet-stream" : object.body().type();
return HttpServerResponse.of(
200,
HttpHeaders.of("Content-Disposition", "attachment; filename=\"" + fileId + "\""),
HttpBody.of(contentType, bytes));
} catch (S3NotFoundException e) {
throw HttpServerResponseException.of(404, "File not found");
}
}
@HttpRoute(method = HttpMethod.DELETE, path = "/files/{fileId}")
@Json
public DeleteFileResponse deleteFile(String fileId) {
this.s3FileClient.deleteFile(fileId);
return new DeleteFileResponse("File deleted");
}
Добавьте оставшиеся конечные точки в src/main/kotlin/ru/tinkoff/kora/guide/s3/controller/DataController.kt:
@HttpRoute(method = HttpMethod.GET, path = "/files")
@Json
fun listFiles(): List<FileMetadata> {
return s3FileClient.listFiles().objects().map { object ->
toMetadata(object.key(), object.size(), object.body().type())
}
}
@HttpRoute(method = HttpMethod.GET, path = "/files/{fileId}")
fun downloadFile(fileId: String): HttpServerResponse {
return try {
val objectResponse = s3FileClient.downloadFile(fileId)
val bytes = objectResponse.body().asBytes()
val contentType = objectResponse.body().type() ?: "application/octet-stream"
HttpServerResponse.of(
200,
HttpHeaders.of("Content-Disposition", "attachment; filename=\"$fileId\""),
HttpBody.of(contentType, bytes)
)
} catch (_: S3NotFoundException) {
throw HttpServerResponseException.of(404, "File not found")
}
}
@HttpRoute(method = HttpMethod.DELETE, path = "/files/{fileId}")
@Json
fun deleteFile(fileId: String): DeleteFileResponse {
s3FileClient.deleteFile(fileId)
return DeleteFileResponse("File deleted")
}
Ключевая идея в том, что этот второй шаг завершает открытый жизненный цикл файла. Список переводит низкоуровневые объекты хранилища в ваш открытый договор FileMetadata, скачивание переводит
S3-объект в настоящий HTTP-ответ файла, а удаление дает API чистый способ убрать сохраненное содержимое по fileId.
Docker Compose¶
Код приложения использует AWS S3-клиент, но для локальной разработки нам все равно нужен S3-совместимый сервер. MinIO отлично для этого подходит.
Создайте docker-compose.yml в каталоге модуля приложения:
services:
minio:
image: minio/minio:latest
ports:
- "9000:9000"
- "9001:9001"
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
command: server /data --console-address ":9001"
Запустите его:
Затем запустите приложение с переменными окружения:
S3_URL=http://localhost:9000 \
S3_ACCESS_KEY=minioadmin \
S3_SECRET_KEY=minioadmin \
S3_BUCKET=uploads \
./gradlew run
В Windows PowerShell:
$env:S3_URL = 'http://localhost:9000'
$env:S3_ACCESS_KEY = 'minioadmin'
$env:S3_SECRET_KEY = 'minioadmin'
$env:S3_BUCKET = 'uploads'
./gradlew run
Запуск приложения¶
Сначала скомпилируйте:
Затем запустите приложение с теми же переменными окружения S3, которые показаны выше.
API можно проверить такими примерами:
curl -F "file=@./example.txt" http://localhost:8080/files/upload
curl http://localhost:8080/files
curl http://localhost:8080/files/<fileId>
curl -X DELETE http://localhost:8080/files/<fileId>
Запуск тестов¶
Тесты этого руководства не зависят от вручную запущенного экземпляра MinIO. Они используют MinIO Testcontainer и автоматически связывают значения подключения с графом приложения Kora.
Запустите их обычным потоком руководства:
Эта тестовая настройка проверяет две вещи:
- декларативный
S3FileClientможет загружать, скачивать и удалять объекты - расширенный
DataControllerможет открывать ожидаемое HTTP-поведение поверх S3-клиента
Лучшие практики¶
- Держите декларативный S3-клиент сфокусированным на одной корзине и одной понятной стратегии ключей.
- Раскрывайте стабильные HTTP-идентификаторы вроде
fileIdвместо того, чтобы протаскивать сырые объектные ключи в каждый маршрут API. - Используйте конструкторы
S3Body, которые соответствуют форме ваших данных. Для очень больших потоков предпочитайте потоковые варианты, а не буферизацию целых файлов в памяти. - Используйте MinIO локально, но сохраняйте код приложения согласованным с AWS-клиентским модулем, если промышленная среда будет использовать инфраструктуру в стиле AWS.
Итоги¶
В этом руководстве вы расширили приложение HTTP-сервера файловым хранилищем на основе S3.
Вы добавили:
- AWS S3-клиентский модуль и его зависимость HTTP-клиента
- декларативный
S3FileClient - стартовый компонент, который гарантирует существование корзины
- конечные точки загрузки, списка, скачивания и удаления файлов в
DataController - тесты S3-потока на основе MinIO
Главный урок в том, что декларативный S3-клиент Kora особенно хорошо работает, когда договор хранилища простой и стабильный, а окружающий контроллер отвечает за HTTP-специфичные задачи: разбор multipart и ответы скачивания.
Ключевые понятия¶
- S3-совместимое хранилище позволяет разрабатывать локально с MinIO и при этом целиться в клиенты и инфраструктуру в стиле AWS.
- Декларативные S3-клиенты сопоставляют операции хранилища с
@S3.Client,@S3.Put,@S3.Get,@S3.Listи@S3.Delete. - HTTP-задачи и задачи хранилища должны оставаться разделенными: контроллеры обрабатывают HTTP-договоры, а декларативные клиенты - договоры объектного хранилища.
- Тесты на основе MinIO дают реалистичную проверку без вручную управляемого S3-окружения.
Устранение неполадок¶
./gradlew clean падает из-за заблокированных файлов:
Остановите демоны Gradle и попробуйте снова:
Windows AccessDeniedException в кеше Gradle:
Обычно это значит, что демон или другой Java-процесс все еще удерживает файлы в кеше Gradle. Сначала остановите демоны, затем повторите команду.
Приложение не может подключиться к MinIO:
Проверьте, что:
- MinIO запущен на
http://localhost:9000 S3_URL,S3_ACCESS_KEYиS3_SECRET_KEYзаданы- имя корзины в
S3_BUCKETсовпадает с конфигурацией руководства
GET /files/{fileId} возвращает 404:
Это значит, что объектный ключ files/{fileId} не существует в настроенной корзине. Чаще всего это происходит потому, что:
- объект был удален ранее
- приложение указывает на другую корзину или экземпляр MinIO
- запрос загрузки не завершился успешно
Docker или Testcontainers не может запустить MinIO:
Убедитесь, что Docker запущен и доступен вашему пользователю. Если тесты на основе контейнеров падают, изучите журналы Docker и проверьте, что порты 9000 и 9001 свободны для ручных запусков.
Что дальше?¶
- Наблюдаемость, чтобы добавить метрики, трассировки, журналы и пробы вокруг файловых операций.
- HTTP-клиент, чтобы вызывать файловые конечные точки из другой службы Kora.
- Шаблоны отказоустойчивости, чтобы защитить вызовы хранилища от медленных или нестабильных зависимостей.
- База данных JDBC перед руководством по тестированию как черный ящик, если хотите сквозной тестовый путь с JDBC.
Помощь¶
Если что-то не сходится:
- сравните с Kora Java S3 App и Kora Kotlin S3 App
- вернитесь к продвинутому HTTP-серверу для обработки multipart-запросов
- проверьте документацию S3-клиента
- проверьте документацию HTTP-сервера
- проверьте документацию конфигурации