Как стать автором
Обновить

Работа с файлами через Yandex Object Storage в Spring Boot

Уровень сложностиСредний
Время на прочтение5 мин
Количество просмотров693

Yandex Cloud Object Storage - это совместимое с AWS S3 облачное хранилище. В этой статье мы интегрируем его в Spring Boot приложение, используя SDK Амазона.

Создание бакета в Yandex Cloud

Для начала создадим папку, в которой позже будет создано объектное хранилище

Здесь в сервисах находим и создаем s3

Указываем необходимые параметры

И переходим в наше ведерко

Создание аккаунта для доступа к бакету

Как указано в документации, для использования s3 необходимо создать сервисный аккаунт.
Для этого возвращаемся в созданную папку, переходим в Service accounts и создаем новый аккаунт:

Далее возвращаемся в наш бакет и переходим на вкладку security, чтобы добавить роль именно для бакета

Нажимаем на вкладку Assign bindings и добавляем роль editor для нашего аккаунта

Переходим обратно в аккаунт и создаем static access key, сохраняем key id и secret key

Выбираем зависимость для работы с S3

Yandex Object Storage совместим с AWS S3, поэтому именно их sdk мы будем использовать, но есть несколько разных зависимостей, использующих эту sdk.

  1. com.amazonaws:aws-java-sdk:1.x.x - устаревший sdk, который перестанет поддерживаться в этом году

  2. software.amazon.awssdk:s3:2.x.x - актуальный sdk

  3. io.awspring.cloud:spring-cloud-aws-starter-s3 - надстройка над sdk, поддерживаемая комьюинити и улучшающая интеграцию со спрингом

  4. org.springframework.cloud.stream.app:aws-s3-app-starters-common - стартер из проекта spring cloud stream, использующийся для интеграции этого проекта с S3

В этой статье используем актуальную версию sdk (вариант 2), так как нам нужна только базовая функциональность, без лишних абстракций.

Создаем контроллер

Создайте новый spring boot проект и добавьте в следующие зависимости:

implementation("software.amazon.awssdk:aws-sdk-java:2.29.33")
implementation("software.amazon.awssdk:apache-client:2.29.33")

implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0")
implementation("org.springframework.boot:spring-boot-starter-web")

Обратите внимание, что вместе с sdk нам так же необходимо добавить http-клиент амазона. Также, мы добавили springdoc-openapi для генерации Swagger UI, через который мы будем тестировать приложение.

Создадим контроллер с клиентом s3Client для работы с S3. В настоящем приложении клиента стоит создать отдельным бином и вынести все константы в application.properties, но для простоты я оставлю все в контроллере.

@RestController
@RequestMapping("/photos")
@Tag(name = "Photos")
@ApiResponses(@ApiResponse(responseCode = "200", useReturnTypeSchema = true))
public class PhotoController {

    private static final String KEY_ID = "YOUR_KEY_ID";
    private static final String SECRET_KEY = "YOUR_SECRET_KEY";
    private static final String REGION = "ru-central1";
    private static final String S3_ENDPOINT = "https://storage.yandexcloud.net";

    private static final String BUCKET = "spring-boot-s3-exmaple";

    private final S3Client s3Client;

    public PhotoController() {
        AwsCredentials credentials = AwsBasicCredentials.create(KEY_ID, SECRET_KEY);

        s3Client = S3Client.builder()
                .httpClient(ApacheHttpClient.create())
                .region(Region.of(REGION))
                .endpointOverride(URI.create(S3_ENDPOINT))
                .credentialsProvider(StaticCredentialsProvider.create(credentials))
                .build();
    }
}

Добавим метод для загрузки фотографий в бакет:

@PutMapping(consumes = MULTIPART_FORM_DATA_VALUE)
public String uploadFile(@RequestParam MultipartFile photo) throws IOException {

    String key = "photos/" + photo.getOriginalFilename();
    PutObjectRequest putObjectRequest = PutObjectRequest.builder()
            .bucket(BUCKET)
            .key(key)
            .contentType(photo.getContentType())
            .build();

    s3Client.putObject(putObjectRequest, RequestBody.fromBytes(photo.getBytes()));

    return key;
}

И для получения их из него:

@GetMapping
public ResponseEntity<byte[]> downloadFile(@RequestParam String key) throws IOException {

    GetObjectRequest objectRequest = GetObjectRequest.builder()
            .bucket(BUCKET)
            .key(key)
            .build();

    var inputStream = s3Client.getObject(objectRequest);
    byte[] data = inputStream.readAllBytes();

    var headers = new HttpHeaders();
    headers.add(HttpHeaders.CONTENT_DISPOSITION, "inline");
    headers.add(HttpHeaders.CONTENT_TYPE, inputStream.response().contentType());

    return ResponseEntity.ok()
            .headers(headers)
            .body(data);
}

Вот как будет выглядеть контроллер полностью:

import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.AwsCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.http.apache.ApacheHttpClient;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;

import java.io.IOException;
import java.net.URI;

import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE;

@RestController
@RequestMapping("/photos")
@Tag(name = "Photos")
@ApiResponses(@ApiResponse(responseCode = "200", useReturnTypeSchema = true))
public class PhotoController {

    private static final String KEY_ID = "YOR_KEY_ID";
    private static final String SECRET_KEY = "YOUR_SECRET_KEY";
    private static final String REGION = "ru-central1";
    private static final String S3_ENDPOINT = "https://storage.yandexcloud.net";

    private static final String BUCKET = "spring-boot-s3-exmaple";

    private final S3Client s3Client;

    public PhotoController() {
        AwsCredentials credentials = AwsBasicCredentials.create(KEY_ID, SECRET_KEY);

        s3Client = S3Client.builder()
                .httpClient(ApacheHttpClient.create())
                .region(Region.of(REGION))
                .endpointOverride(URI.create(S3_ENDPOINT))
                .credentialsProvider(StaticCredentialsProvider.create(credentials))
                .build();
    }


    @PutMapping(consumes = MULTIPART_FORM_DATA_VALUE)
    public String uploadFile(@RequestParam MultipartFile photo) throws IOException {

        String key = "photos/" + photo.getOriginalFilename();
        PutObjectRequest putObjectRequest = PutObjectRequest.builder()
                .bucket(BUCKET)
                .key(key)
                .contentType(photo.getContentType())
                .build();

        s3Client.putObject(putObjectRequest, RequestBody.fromBytes(photo.getBytes()));

        return key;
    }

    @GetMapping
    public ResponseEntity<byte[]> downloadFile(@RequestParam String key) throws IOException {

        GetObjectRequest objectRequest = GetObjectRequest.builder()
                .bucket(BUCKET)
                .key(key)
                .build();

        var inputStream = s3Client.getObject(objectRequest);
        byte[] data = inputStream.readAllBytes();

        var headers = new HttpHeaders();
        headers.add(HttpHeaders.CONTENT_DISPOSITION, "inline");
        headers.add(HttpHeaders.CONTENT_TYPE, inputStream.response().contentType());

        return ResponseEntity.ok()
                .headers(headers)
                .body(data);
    }
}

Тестируем приложение

Переходим на http://localhost:8080/swagger-ui/index.html и загружаем фото

Далее проверяем, что оно появилось в бакете. Как видите, S3 понимает, что если путь содержит / то нужно создать папку, поэтому наше фото добавилось в папку photos

Пробуем получить фото через наш эндпоинт:

Варианты локального S3 для тестовой среды

Возможно, вам нужен S3 только на проде, а при разработке вы хотели бы использовать локальный сервис. Два таких сервиса - MinIO и LocalStack. Я не буду разворачивать их в этой статье, но коротко сравню:

  • LocalStack — это инструмент для локальной разработки, который эмулирует облачные сервисы AWS. Используется только для разработки и тестирования. Если вы используете не только S3, то LocalStack вам подойдет.

  • MinIO — это объектное хранилище с открытым исходным кодом, совместимое с API Amazon S3. Можно использовать в продакшене.

👨‍💻 Джуниор

Теги:
Хабы:
0
Комментарии1

Публикации

Истории

Работа

Java разработчик
253 вакансии

Ближайшие события