RU: Spring Boot - File Upload, Download - via Minio or AWS S3 SDK

RU: Spring Boot  - File Upload, Download - via Minio or AWS S3 SDK

Цель:

1. Пример работы Spring Boot и загрузки файла на сервер
2. Использовать MinIO клиент и загрузка фалов через него
3. Использовать AWS S3 SDK клиент и загрузка файлов на MinIO сервер(AWS S3 compatible)

Что нам потребуется создать spring boot проект, для этого обратимся к spring initialzr-у https://start.spring.io/

Создадим проект с минимальным набором копонентов - прежде всего это Spring Web собственно и всё остальное нам знать не обязательно

Мы создадим основные методы по загрузки файлов на minio хранилище

скачиванию файлов, удалению их по ключу(имени) и просмотр списка файлов в бакете. Бакет в рамках AWS S3 называется главная директория, в которой мы сохраняем свои файлы - например, мы можем разделять набор таких бакетов для video, photo, documents, - определяя при загрузке, где что будет хранится и какие типы, но это больше частное архитектурное решение и к примеру не относится.

Для работы с хранилищем через minio client-а или Amazon S3 Java SDK нам потребуется указать в pom.xml соответсвующие зависимости:

        <!-- ... -->
        <!-- minio client -->
        <dependency>
            <groupId>io.minio</groupId>
            <artifactId>minio</artifactId>
            <version>7.0.2</version>
        </dependency>
        <!-- aws java sdk client -->
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-java-sdk</artifactId>
            <version>${aws.sdk.version}</version>
        </dependency>
        <!-- ioutils будем использовать из этой библиотеки для простоты         -->
		<dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
            <version>1.3.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-io</artifactId>
            <version>1.3.2</version>
        </dependency>
        ...

Итак для работы с хранилищем MinIO нам потребуются два ключевых параметра - это ACCESS KEY и SECRET KEY, рекомендуется задать их в переменных окружения, для нашего спринг бут приложения зададим их в конфиг файле application.properties

s3.url=https://play.min.io
s3.accessKey=Q3AM3UQ867SPQQA43P2F
s3.secretKey=zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG

spring.servlet.multipart.max-file-size=100MB
spring.servlet.multipart.max-request-size=100MB

Как можно заметить тут указаны еще два параметра необходимых для загрузки файлов, которые повышают стандартный размер файла до 100 mb, c 10 mb default(по умолчанию)

Иии ... вопрос, а причем тут параметр??? s3.url=https://play.min.io - верный вопрос. Для простоты примера, мы используем онлайн сервер minio хранилища, который открыт бесплатно для тестирования API. Так что нам даже не обязательно его устанавливать, разве это не прекрасно?!😂

Отлично, что дальше - создаем свой конфигурационный файл, где указываем клиента AmazonS3 или MinioClient - бины для работы с хранилищами. В нашем примере укажем оба, но при реализации контроллера остановимся на одном конечном AmazonS3 👇👇👇

package com.timurisachenko.microstorage.configs;

import com.amazonaws.ClientConfiguration;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import io.minio.MinioClient;
import io.minio.errors.InvalidEndpointException;
import io.minio.errors.InvalidPortException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

    @Value("${s3.url}")
    private String s3Url;
    @Value("${s3.accessKey}")
    private String accessKey;
    @Value("${s3.secretKey}")
    private String secretKey;

    @Bean
    public MinioClient s3Client() throws InvalidPortException, InvalidEndpointException {
        return new MinioClient(s3Url, accessKey, secretKey);
    }

    @Bean
    public AmazonS3 s3() {
        AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
        ClientConfiguration clientConfiguration = new ClientConfiguration();
        clientConfiguration.setSignerOverride("AWSS3V4SignerType");

        AmazonS3 s3Client = AmazonS3ClientBuilder
                .standard()
                .withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(s3Url, Regions.US_EAST_1.name()))
                .withPathStyleAccessEnabled(true)
                .withClientConfiguration(clientConfiguration)
                .withCredentials(new AWSStaticCredentialsProvider(credentials))
                .build();
        return s3Client;
    }

}

Отлично у нас есть настроенный клиент, теперь можно перейти к написанию endpoint-ов в нашем контроллере StorageController
```java

package com.timurisachenko.microstorage.controllers;

import com.timurisachenko.microstorage.services.AmazonS3Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/s3")
public class StorageController {
    private AmazonS3Service s3Service;

    @Autowired
    public StorageController(@Qualifier("amazonS3ServiceImpl") AmazonS3Service s3Service) {
        this.s3Service = s3Service;
    }

    @PostMapping(value = "/{bucketName}/files", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE})
    public Map<String, String> upload(@PathVariable("bucketName") String bucketName, @RequestPart(value = "file") MultipartFile files) throws Exception {
        s3Service.uploadFile(bucketName, files.getOriginalFilename(), files.getBytes());
        Map<String, String> result = new HashMap<>();
        result.put("key", files.getOriginalFilename());
        return result;
    }

    @GetMapping(value = "/{bucketName}/{keyName}", consumes = "application/octet-stream")
    public ResponseEntity<ByteArrayResource> downloadFile(@PathVariable("bucketName") String bucketName, @PathVariable("keyName") String keyName) throws Exception {
        byte[] data = s3Service.downloadFile(bucketName, keyName);
        ByteArrayResource resource = new ByteArrayResource(data);

        return ResponseEntity
                .ok()
                .contentLength(data.length)
                .header("Content-type", "application/octet-stream")
                .header("Content-disposition", "attachment; filename=\"" + keyName + "\"")
                .body(resource);
    }

    @DeleteMapping("/{bucketName}/files/{keyName}")
    public void delete(@PathVariable("bucketName") String bucketName, @PathVariable(value = "keyName") String keyName) throws Exception {
        s3Service.deleteFile(bucketName, keyName);
    }

    @GetMapping("/{bucketName}/files")
    public List<String> listObjects(@PathVariable("bucketName") String bucketName) throws Exception {
        return s3Service.listFiles(bucketName);
    }
}

Вот так выглядит наш контроллер, AmazonS3Service - это интерфейс с конечной реализацией процессов по загрузке, скачиванию и т.д.
Ключевыми деталями контроллера являются в большей мере это то, что при загрузке на сервер мы указываем в каком формате приходит запрос на POST метод /s3/{bucketName}/files  - MediaType.MULTIPART_FORM_DATA_VALUE
а также типом запроса служит тип MultipartFile

При скачивании файла - тип ответа возвращается в формате "application/octet-stream" а возвразаемый объект в формате массива байтов с заголовком disposition

Вернемся к контроллеру, в нем как вы могли заметить был указан AmazonS3Service -  это интерфейс, с набором общих методов для обоих клиентов указанных в AppConfig классе.

package com.timurisachenko.microstorage.services;

import java.io.File;
import java.util.List;

public interface AmazonS3Service {
    void uploadFile(String bucketName, String originalFilename, byte[] bytes) throws Exception;

    byte[] downloadFile(String bucketName, String fileUrl) throws Exception;

    void deleteFile(String bucketName, String fileUrl) throws Exception;

    List<String> listFiles(String bucketName) throws Exception;

    File upload(String bucketName, String name, byte[] content) throws Exception;

    byte[] getFile(String bucketName, String key) throws Exception;
}

Перейдем к реализации AmazonS3ServiceImpl

package com.timurisachenko.microstorage.services.impl;

import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.S3Object;
import com.amazonaws.services.s3.model.S3ObjectInputStream;
import com.timurisachenko.microstorage.services.AmazonS3Service;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.*;
import java.util.LinkedList;
import java.util.List;

@Service
public class AmazonS3ServiceImpl implements AmazonS3Service {
    private final AmazonS3 s3;

    @Autowired
    public AmazonS3ServiceImpl(AmazonS3 s3) {
        this.s3 = s3;
    }

    @Override
    public void uploadFile(String bucketName, String originalFilename, byte[] bytes) throws Exception {
        File file = upload(bucketName, originalFilename, bytes);
        s3.putObject(bucketName, originalFilename, file);

    }

    @Override
    public byte[] downloadFile(String bucketName, String fileUrl) throws Exception {
        return getFile(bucketName, fileUrl);
    }

    @Override
    public void deleteFile(String bucketName, String fileUrl) throws Exception {
        s3.deleteObject(bucketName, fileUrl);
    }

    @Override
    public List<String> listFiles(String bucketName) throws Exception {
        List<String> list = new LinkedList<>();
        s3.listObjects(bucketName).getObjectSummaries().forEach(itemResult -> {
            list.add(itemResult.getKey());
            System.out.println(itemResult.getKey());
        });
        return list;
    }

    @Override
    public File upload(String bucketName, String name, byte[] content) throws Exception{
        File file = new File("/tmp/" + name);
        file.canWrite();
        file.canRead();
        FileOutputStream iofs = null;
        iofs = new FileOutputStream(file);
        iofs.write(content);
        return file;
    }

    @Override
    public byte[] getFile(String bucketName, String key) throws Exception {
        S3Object obj = s3.getObject(bucketName, key);
        S3ObjectInputStream stream = obj.getObjectContent();
        try {
            byte[] content = IOUtils.toByteArray(stream);
            obj.close();
            return content;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

СОБСТВЕННО ЭТО ВСЁ! СПАСИБО ЗА ВНИМАНИЕ!
Если было полезно, или бесполезно - просьба оставить комментарий или написать мне, если есть отличные идеи какой пример разобрать.
Спасибо, что дочитали до сюда!
🙏
Ссылка на github исходники👈

Предыдущий пост введение в MinIO
https://timurisachenko.com/minio/