본문 바로가기
프로그래밍/Spring Boot

[Spring Boot] 이미지 스토리지 서버 만들기

by yonmoyonmo 2021. 7. 16.

원모 싸이버 스쿨 서비스 구성도

원모싸이버스쿨 앱 가운데 이미지를 업로드하고 업로드한 이미지를 다운로드 하거나 보여지게 하고싶다면 바로바로

원모 싸이버 이미지 서버를 이용하면 됩니다.

https://github.com/yonmoyonmo/wcs-image-server

 

yonmoyonmo/wcs-image-server

wonmo cyber school image server. Contribute to yonmoyonmo/wcs-image-server development by creating an account on GitHub.

github.com

 

 

Spring Boot File Upload / Download Rest API Example

Uploading and downloading files are very common tasks for which developers need to write code in their applications. In this article, You'll learn how to upload and download files in a RESTful spring boot web service. We'll first build the REST APIs for up

www.callicoder.com

이 분의 설명글을 참조했습니다.(거의 똑 같음)


원모 싸이버 이미지 서버

간단 설명

기능 :

  • 요청에 따라 알맞게 이미지 파일을 저장 후 경로를 제공
  • 저장된 이미지를 브라우저 상에서 볼 수 있도록 리소스 제공

두 가지 심플하고 딱 필요한 기능만 있습니다.

짱이죠?

코드도 엄청 간단합니다.


코드 살피기

application.properties

... 별 다를 것 없는 JPA, DATA SOURCE 속성 생략 ...

## multipart 관련 속성

# 멀티파트 업로드 가능하게 하긔
spring.servlet.multipart.enabled=true
# 파일이 디스크에 쓰인 후의 쓰레쉬홀드
spring.servlet.multipart.file-size-threshold=2KB
# 최대 파일 사이즈
spring.servlet.multipart.max-file-size=200MB
# 최대 리퀘스트 사이즈
spring.servlet.multipart.max-request-size=215MB

# 파일 저장될 디렉토리
# 프로퍼티 클래스 만들어서 쓰면 댑니다
file.upload-dir=./images

요런 속성이 필요합니다.

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "file")
public class FileStorageProperties {
    private String uploadDir;

    public String getUploadDir() {
        return uploadDir;
    }

    public void setUploadDir(String uploadDir) {
        this.uploadDir = uploadDir;
    }
}

업로드 디렉토리 정보를 application.properties에서 가져와서 쓸 수 있도록 합니다.

@SpringBootApplication
@EnableConfigurationProperties(FileStorageProperties.class)
public class ImageServerApplication {

	public static void main(String[] args) {
		SpringApplication.run(ImageServerApplication.class, args);
	}

메인 어플리케이션 클래스에 @EnableConfigurationProperties 써 주어야지 프로퍼티 클래스를 쓸 수 있습니다.

다음에는 Request와 Response를 처리하기 위한 Controller 클래스를 만들고 파일을 저장하거나 가져오는 로직을 담당하는 Service 클래스를 만들고 컨트롤러와 서비스 사이에 쓸 DTO와 Exception들을 만들어 주면됩니다.

FileController

import com.wonmocyberschool.imageserver.payload.Response;
import com.wonmocyberschool.imageserver.service.StorageService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;


@RestController
@RequestMapping("/wcs/image")
public class FileController {

    private static final Logger logger = LoggerFactory.getLogger(FileController.class);
    private final StorageService storageService;

    @Autowired
    public FileController(StorageService storageService){
        this.storageService = storageService;
    }

    @PostMapping("/upload")
    public ResponseEntity<Response> uploadImage(@RequestParam("file") MultipartFile file,
                                                @RequestParam("userName") String userName) throws IOException {
        Response res = new Response();
        try{
            String result = storageService.saveFile(file, userName);
            res.setImageLocation("/"+userName+"/"+result);
            res.setMessage("done");
            res.setSuccess(true);
            return new ResponseEntity<Response>(res, HttpStatus.OK);
        }catch (Exception e){
            res.setMessage("failed");
            res.setSuccess(false);
            return new ResponseEntity<Response>(res, HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    @PostMapping("/post/upload")
    public ResponseEntity<Response> postImageUpload(@RequestParam("files") MultipartFile[] files,
                                                    @RequestParam("postName")String postName) {
        Response res = new Response();
        List<String> results = new ArrayList<>();
        List<String> imageLocations = new ArrayList<>();
        try{
            results = storageService.saveFiles(files, postName);
            for(String result : results){
                imageLocations.add("/"+postName+"/"+result);
            }
            res.setImageLocations(imageLocations);
            res.setMessage("done");
            res.setSuccess(true);
            return new ResponseEntity<Response>(res, HttpStatus.OK);
        }catch (Exception e){
            res.setMessage("failed");
            res.setSuccess(false);
            return new ResponseEntity<Response>(res, HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    @GetMapping("/display/{userName}/{fileName:.+}")
    public ResponseEntity<Resource> displayImage(@PathVariable String fileName,
                                                 @PathVariable String userName,
                                                 HttpServletRequest request) {
        // Load file as Resource
        Resource resource = storageService.loadFileAsResource(userName, fileName);

        // Try to determine file's content type
        String contentType = null;
        try {
            contentType = request.getServletContext().getMimeType(resource.getFile().getAbsolutePath());
        } catch (IOException ex) {
            logger.info("Could not determine file type.");
        }

        // Fallback to the default content type if type could not be determined
        if(contentType == null) {
            contentType = "application/octet-stream";
        }

        return ResponseEntity.ok()
                .contentType(MediaType.parseMediaType(contentType))
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"")
                .body(resource);
    }
}

StorageService

package com.wonmocyberschool.imageserver.service;

import com.wonmocyberschool.imageserver.config.FileStorageProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

@Service
public class StorageService {

    private String uploadPath;

    @Autowired
    public StorageService(FileStorageProperties fileStorageProperties){
        this.uploadPath = fileStorageProperties.getUploadDir();
    }

    private String getRandomStr(){
        int leftLimit = 97; // letter 'a'
        int rightLimit = 122; // letter 'z'
        int targetStringLength = 10;
        Random random = new Random();
        String generatedString = random.ints(leftLimit, rightLimit + 1)
                .limit(targetStringLength)
                .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
                .toString();
        System.out.println("random : " + generatedString);
        return generatedString;
    }

    public List<String> saveFiles(MultipartFile[] files, String postName) throws IOException {
        String randomStr = getRandomStr();
        List<String> fileNames = new ArrayList<>();
        for(MultipartFile file : files) {
            fileNames.add(randomStr + StringUtils.cleanPath(file.getOriginalFilename()));
        }
        Path uploadPath = Paths.get(this.uploadPath+"/"+postName);
        if(!Files.exists(uploadPath)) {
            Files.createDirectories(uploadPath);
            System.out.println("make dir : " + uploadPath.toString());
        }
        for(int i =0; i< files.length; i++) {
            try (InputStream inputStream = files[i].getInputStream()) {
                Path filePath = uploadPath.resolve(fileNames.get(i));
                Files.copy(inputStream, filePath, StandardCopyOption.REPLACE_EXISTING);
            } catch (IOException ioe) {
                throw new IOException("Could not save image file: " + fileNames.get(i), ioe);
            }
        }
        return fileNames;
    }

    public String saveFile(MultipartFile file, String userName) throws IOException {

        String randomStr = getRandomStr();
        String fileName = randomStr + StringUtils.cleanPath(file.getOriginalFilename());

        Path uploadPath = Paths.get(this.uploadPath+"/"+userName);
        if(!Files.exists(uploadPath)) {
            Files.createDirectories(uploadPath);
        }

        try (InputStream inputStream = file.getInputStream()) {
            Path filePath = uploadPath.resolve(fileName);
            Files.copy(inputStream, filePath, StandardCopyOption.REPLACE_EXISTING);
            return fileName;
        } catch (IOException ioe) {
            throw new IOException("Could not save image file: " + fileName, ioe);
        }
    }

    public Resource loadFileAsResource(String userName, String fileName) {
        Path uploadPath = Paths.get(this.uploadPath+"/"+userName);
        try {
            Path filePath = uploadPath.resolve(fileName).normalize();
            Resource resource = new UrlResource(filePath.toUri());
            if(resource.exists()) {
                return resource;
            } else {
                throw new MyFileNotFoundException("File not found " + fileName);
            }
        } catch (MalformedURLException ex) {
            throw new MyFileNotFoundException("File not found " + fileName, ex);
        }
    }
}

Response

package com.wonmocyberschool.imageserver.payload;

import java.util.List;

public class Response {
    private String message;
    private String imageLocation;
    private List<String> imageLocations;
    private boolean success;

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public String getImageLocation() {
        return imageLocation;
    }

    public void setImageLocation(String imageLocation) {
        this.imageLocation = imageLocation;
    }

    public List<String> getImageLocations() {
        return imageLocations;
    }

    public void setImageLocations(List<String> imageLocations) {
        this.imageLocations = imageLocations;
    }

    public boolean isSuccess() {
        return success;
    }

    public void setSuccess(boolean success) {
        this.success = success;
    }
}

위의 코드 외에 생략한 코드들은 Cors 설정, 예외 클래스 등입니다. 깃허브에서 확인할 수 있습니닷.

요로케 하나 만들어 놓으면 든든- 합니다. 단점은 클러스터링을 하려 하면 프로세스별로 저장한 파일이 달라지므로 파일 동기화를 위한 작업을 해 줘야 한다는 점!

하지만 간단한 것이 주는 장점이 참 크지요?!

이미지 좀 저장하자고 S3쓰긴 돈아깝자나~~!


이 세상 할인이 아니다?

댓글