토이 프로젝트/Spring&Java 갖고놀기

Spring Boot Web Chatting : 스프링 부트로 실시간 화상 채팅 만들기(13) minIO 배포 & 파일업로드/다운로드 구현하기(feat.minIO ssl 적용, mixed-content 에러 해결)

TerianP 2024. 1. 7.
728x90

1. 시작하면서

1) 서버 정리

이번에는 좀 오래만에 프로젝트 일지를 쓰게되었습니다. 2023 회고록에서 언급했듯이 사실 최근에 제 프로젝트에 나름? 많은 일이 있었습니다. 특히 기존에 있던 openstack 와 kubernetes 시스템을 완전히 다 뒤집고 다시 처음부터 작업했습니다.

물론 할일이 없어서나 심심해서 혹은 공부하기 위해...한거는 절대 아니고ㅋㅋㅋ 기존에 라즈베리파이 2대에 openstack + kubernetes 를 배포하고 사용했었는데 해당 라즈베리파이 2대를 모두 처분해버리고, 기존의 시스템 전부를 미니pc 로 옮기게 되었습니다. 사실 라즈베리파이라는 한계 때문인지 제가 설치를 잘못해서 그런건지 모르지만 오픈스택에 컴포넌트를 추가하는 것도 조금 문제가 많았고, 성능, 반응도 조금 늦었고 하여튼 문제가 많았습니다.

하여튼 그렇게 패기있게 시작했지만 결국 또 약 1주동안 과거 나와의 싸움에 들어갔습니다. 정리해놓은 것을 보면서 오픈스택을 다시 설치하는데ㅋㅋㅋㅋ대체 이걸 왜 이렇게 정리햇었지? 이게 어떤 옵션이지? 하면서 너무나도 잘 정리해둔 나를  열심히 칭찬하면서 욕하면서 결국 다시 구성에 성공할 수 있었습니다.

다만 아쉬움도 있었는데 미니 pc 2대만을 사용하다보니까 아무래도 아키텍쳐가 너무 이상하게 구성되었다는 점...? 기존에는 그래도 라즈베리파이1 은 오픈스택 컨트롤 노드, 라즈베리파이2  는 쿠버네티스 마스터노드, 미니pc 가 openstack compute 노드로 구성해서 나름 이쁘게 구성되었었는데, 이번에는 미니pc 1번이 오픈스택 컨트롤 노드이자, 쿠버네티스 마스터 노드가 되는 기괴한...구조가 되었습니다. 뭐 사실 전기세를 생각하면서 서버를 구성하기 위한 한계라고 생각합니다ㅠㅠ

 

2) ObjectStorage MinIO 

물론 이것만 한것은 아니고, 이번에는 드디어! S3 와 유사한 object storage 인 minIO 에 파일 업로드 다운로드 하는 기능을 넣어보았습니다. 기존에 사용하던 AWS 계정이 날아가고 나서 S3 도 못쓰게되면서 파일 업로드/다운로드가 안되는게 참으로 아쉬웠습니다.  그래서 S3 대신 다른 방법을 사용해야 했고, 어떻게 업로드/다운로드 기능을 어떻게 구현할 수 있을까? 했었습니다. 그렇게 찾은 것이S3 와 동일한 Object Storage minIO 였습니다.

minIO dashboard

 

0. minIO 가 뭐야? : 특징과 장점

MinIO는 현대적인 데이터 관리 요구에 맞춰 설계된 고성능, 분산 객체 저장 시스템입니다. 이 오픈 소스 플랫폼은 Amazon S3와 호환되는 API를 제공한다는 특징이 있다.
분산 스토리지 시스템: MinIO는 높은 가용성과 내결함성을 위해 데이터를 여러 노드에 걸쳐 저장한다. 이는 시스템이 하나의 노드에 문제가 생겨도 데이터를 안전하게 보호할 수 있음을 의미한다.
뛰어난 확장성: MinIO는 클라우드 환경에서의 사용에 최적화되어 있어 여러 회사에서 사용된다.
S3 호환 API: Amazon S3와의 호환성이 뛰어나서 기존 S3 기반의 애플리케이션과 서비스를 쉽게 MinIO로 이전할 수 있습니다.
보안과 효율성: TLS, 버킷 정책 및 IAM 스타일의 사용자 관리를 통해 강력한 보안 기능을 제공한다. 또한, 메모리와 네트워크 I/O를 최적화하여 뛰어난 성능을 발휘한다.

2. minIO 배포

사실 minIO 를 kbuernetes 에 올리는 것 자체는 정말 쉬웠다. 어려움이라고는 다시 yaml 을 작성하는 정도?? 사실 그마저도 이미 kubernetes 에 배포된 다른 서비스들과 비슷하게 하면 되는지라 배포하는데 오랜 시간이 걸리지는 않았던 것 같다. 이번에도 nfs 를 사용해서 minIO pod 가 재실행되더라도 기존의 데이터를 유지할 수 있도록 하였다.

 

또한 minIO 에 ssl 을 적용했기 때문에 minio-console 을 80 포트가 아닌 443 을 사용한다. 만약 ssl 적용이 안되었다면 혹은 필요없다면 443 대신 80 을 사용하면 된다. 특히 minio-tls-secret  나 mountPath: "/root/.minio/certs" 부분도 ssl 을 사용하지 않는다면 지워도 상관없다.

---
apiVersion: v1
kind: Namespace
metadata:
  name: minio-storage

---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-sc
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: Immediate

---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: minio-pv
  labels:
    type: nfs
spec:
  capacity:
    storage: 30Gi
  accessModes:
    - ReadWriteMany
  nfs:
    server: [NFS 서버 주소]
    path: "/nfs-data/minIO"
  storageClassName: nfs-sc

---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: minio-pvc
  namespace: minio-storage
spec:
  accessModes:
    - ReadWriteMany
  storageClassName: nfs-sc
  resources:
    requests:
      storage: 30Gi

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: minio-deployment
  namespace: minio-storage
spec:
  selector:
    matchLabels:
      app: minio
  replicas: 1
  template:
    metadata:
      labels:
        app: minio
    spec:
      volumes:
        - name: minio-storage
          persistentVolumeClaim:
            claimName: minio-pvc
        - name: minio-tls-cert
          secret:
            secretName: minio-tls-secret
      containers:
      - name: minio
        image: minio/minio
        ports:
        - containerPort: 443
        args:
        - server
        - /data
        - --console-address
        - ":443"
        env:
        - name: MINIO_ROOT_USER
          value: [user_id]
        - name: MINIO_ROOT_PASSWORD
          value: [userpw]
        volumeMounts:
        - name: minio-storage
          mountPath: "/data"
        - name: minio-tls-cert
          mountPath: "/root/.minio/certs"
---
apiVersion: v1
kind: Service
metadata:
  name: minio-service
  namespace: minio-storage
spec:
  selector:
    app: minio
  type: NodePort
  ports:
    - name: minio-api
      protocol: TCP
      port: 9000
      nodePort: [nodePort]
      targetPort: 9000
    - name: minio-console
      protocol: TCP
      port: 443
      nodePort: [nodePort]
      targetPort: 443

 


3. minIO ssl 적용하기 : Mixed Content 에러 해결하기

배포까지 마무리하고 로컬에서 minIO 에 파일을 업로드하는 코드까지도 정말 빠르게 작업할 수 있었다. 일단 기존의 S3 와 비슷한 부분이 많았고, 뭣보다 공식 api 문서가 정말 설명이 잘 되어있어서 코드를 짜는데 오랜 시간이 걸리지 않았다. 아니다 다를까 이번에도 여느때와 같이 '어? 쉽네' 하다가 갑자기 '엌ㅋㅋㅋ? 안되네' 라고 해버리는 그 패턴에 빠져버렸다.

업로드까지는 정말 일사천리로 되었지만 문제는 업로드된 파일을 화면에서 보여주고, 그것을 다운로드는 하는 부분에서 발생하였다. 알겠지만 webrtc 를 사용하기 위해서는 ssl 인증서를 넣은 https 프로토콜을 갖은 웹사이트를 사용해야한다. 문제는 이 부분이었는데 minIO 를 배포하면 기본적으로는 https 가 아닌 http 를 사용한다. 문제는 이 https 웹사이트에 특정 서버의 리소스(대표적으로 minIO 에 업로드된 이미지)를 요청하면 아래와 같은 에러가 발생하면서 이미지 로딩이 되지 않았다

Mixed Content: The page at 'https://hjproject.kro.kr' was loaded over HTTPS, 
but requested an insecure script 
'http://대충_minio_resource'. 
This request has been blocked; the content must be served over HTTPS.

 

처음 들었던 생각은 이게 뭔 에러지? 라는 것이었다. 이전에는 S3 에 파일업로드 다운로드는 아무런 이상이 없었는데? 라고 생각했기 때문이다. 그런데 이 부분은 생각해보니 당연한게 S3 는 무려 AWS 의 서비스이고, '당연하게도' ssl 적용이 되어있었을테니 말이다. 다음으로 들었던 생각은 minIO 에 어떻게 ssl 인증서를 적용시킬까? 였다.


4. 개인 SSL 인증서 만들기 및 minIO SSL 에 적용시키기

만약 공인 ssl 인증서를 발급받을 수 있다면 그냥 그것을 kubernetes 의 minio pod 에 적용시키기만 하면 된다. 다만 나처럼 사실 인증서를 만들어 사용해야하는 경우에는 기존에 화상을 위해서 인증서를 만들었던 방법과 동일하게 진행하면 된다. 조금은 복잡하지만 아래 사이트의 명령어를 그대로 따라가다보면 충분히 만들 수 있다.https://www.lesstif.com/system-admin/openssl-root-ca-ssl-6979614.html

 

OpenSSL 로 ROOT CA 생성 및 SSL 인증서 발급

서명에 사용할 해시 알고리즘을 변경하려면 -sha256, -sha384, -sha512 처럼 해시를 지정하는 옵션을 전달해 준다. 기본값은 -sha256 이며 openssl 1.0.2 이상이 필요

www.lesstif.com

 

1) minIO SSL 적용하기

minIO 에 ssl 을 적용시키기 위해서는 openssl 을 통해서 만들어진 private.key 와 public.crt 가 필요하다. 이 두가지를 이용해서 kubernetes secret 를 만들고, minio 에 적용시킬 것이다. secret 를 생성하는 명령어는 아래 명령어를 사용한다.여기서 중요한 부분은 시크릿에서 사용하는 crt 와 key 의 명칭(이름)을 public.crt 와 private.key 로 해주어야한다는 점이다. 만약  이 두가지가 아닌 임의의 값으로 해놓으면 TLS 적용이 안된다.

kubectl create secret generic [시크릿 이름] --from-file=public.crt=[파일위치] --from-file=private.key=[파일위치] -n [minio 네임스페이스]
kubectl create secret generic minio-tls-secret --from-file=public.crt=tls/chatforyou_public.crt --from-file=private.key=tls/chatforyou_private.key -n minio-storage

secret 생성 후 data 부분에 private.key 와  public.crt 로 지정되어야한다.

 

2) springboot - SSL 적용된 minIO 연결 에러

그렇기 SSL 인증서가 적용은 되었는데 이번에는 다른 문제가 발생한다. 바로 springboot 에서 minioClient 를 만들고 연결할때 에러가 발생한다. 이 문제의 원인은 바로 '자체 서명된 개인 인증서를 사용'하기 때문에 발생하는 에러라고 한다.

즉, 자체 인증서를 사용하기 때문에 JVM 에서 봤을 때 신뢰하는 인증기관 인증서 목록에 해당 인증서가 없고, 그래서 '이게 뭔데? 난 인정할 수 없어'라고 해서 발생한다.

Error: java.lang.RuntimeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target at MinioServiceImpl.getAllBuckets(MinioServiceImpl.java)

 

이를 해결하는 대표적인 방법은 jvm keystone 에 해당 인증서를 등록하는 방법이라고하는데 문제는 이 방법으로는 도저히 정말 도저히 해결 할 수 없었다. 다른 해결 방법이 없을까? 하다가 결국 다른 해결방법을 찾게 되었다. 코드적으로 minioClient 를 만들때 ssl 인증서를 확인하는 것을 무시하는 옵션을 거는 방법이다. 물론 이 방법이 결코 좋은 방법은 아니라는 걸 알고 있기에 추후 다른 방법을 찾게되면 언제든 바꿀 예정이다(아시는 분 댓글 달아주세요ㅠㅠ)

minioClient.ignoreCertCheck(); // ssl 인증서 체크 무시

5. minIO 파일 업로드/다운로드

늘 그렇듯 java 코드는 최대한 상세하게, js 및 프론트는 git 커밋 내역을 참고바랍니다.

0) MinioConfig

- access key, secret key , bucket name, url 등등 정보를 가져와서 minioClient 객체를 생성한다. minio_url 의 경우 application.properties 에서 가져오거나 환경 변수로 지정된 값을 가져올 수 있도록한다.

- 파일 다운로드시 에러 해결하기 위한 ssl 인증 연결

package webChat.config;

import io.minio.MinioClient;
import lombok.Getter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import webChat.utils.StringUtil;

import javax.annotation.PostConstruct;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;

@Configuration
@Getter
public class MinioConfig {

    @Value("${minio.access.key}")
    private String accessKey;

    @Value("${minio.access.secret}")
    private String secretKey;

    @Value("${minio.bucket.name}")
    private String bucketName;

    @Value("${minio.url}")
    private String url;

    private MinioClient minioClient;

    // minio 의 url 을 세팅하기 위한 postConstruct
    // 환경변수로 url 이 들어오면 해당 url 을 사용하고, 아니면 properties 에 정의 된 값을 사용
    @PostConstruct
    private void initMinioClient(){
        String envMinioUrl = System.getenv("MINIO_URL");
        if(!StringUtil.isNullOrEmpty(envMinioUrl)){
            url = envMinioUrl;
        }

        minioClient = MinioClient.builder()
                .endpoint(this.getUrl())
                .credentials(this.getAccessKey(), this.getSecretKey())
                .build();
        try {
            minioClient.ignoreCertCheck(); // ssl 인증서 연결 무시
        } catch (KeyManagementException | NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }
}

1) FileDto

package webChat.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class FileDto {

    private String fileName;  // 파일 원본 이름
    private String roomId; // 파일이 올라간 채팅방 ID
    private String filePath; // 파일 fullpath
    private String minioDataUrl; // 파일 링크
    private String contentType;
    private Status status;

    public enum Status {
        UPLOADED, FAIL
    }

}

 

2) minio file upload

- fileupload 부분은 비교적 단순하다. file 객체가 들어오면 해당 파일의 확장자를 체크한 후 path 와 fullpath 를 만든다. 이때 path 를 따로 두는 이유는 originalFilename 가 동일한 파일이 업로드 되는 경우가 있을 수 있기에 이를 분리하기 위함이다. 따라서 동일한 파일이 들어오더라도 UUID 를 사용하는 path 가 다르게 만들어지기에 파일이름의 중복 문제를 해결할 수 있다.

- minioClient 를 사용하는 부분은 아래에 해당 api 문서를 참고하자.

- 업로드가 정상 처리되었다면 minio 에 있는 업로드된 객체의 정보를 가져온 후 해당 내용을 fileDto 에 담아서 return 한다. 만약 어떠한 오류로 exception 이 발생했다면 filedto 에는 status 에 fail 을 담아서 return 한다. 이후 status 가 fail 인 경우에 대해서 처리는 프론트에서 진행한다.

      @Value("${allowed.file_extension}")
    ArrayList<String> allowedFileExtensions;
  
  
  // MultipartFile 과 transcation, roomId 를 전달받는다.
    // 이때 transcation 는 파일 이름 중복 방지를 위한 UUID 를 의미한다.
    @Override
    public FileDto uploadFile(MultipartFile file, String roomId) {
        String originFileName = file.getOriginalFilename();
        String path = UUID.randomUUID().toString().split("-")[0];
        String fullPath = roomId + "/" + path + "/" + originFileName;

        this.uploadFileSizeCheck(file);

        try {
            PutObjectArgs args = PutObjectArgs.builder()
                    .bucket(minioConfig.getBucketName())
                    .object(fullPath)
                    .stream(file.getInputStream(), file.getSize(), -1)
                    .contentType(file.getContentType())
                    .build();

            minioClient.putObject(args);

            String url = minioClient.getPresignedObjectUrl(
                    GetPresignedObjectUrlArgs.builder()
                            .method(Method.GET)
                            .bucket(minioConfig.getBucketName())
                            .object(fullPath)
                            .expiry(10, TimeUnit.MINUTES) // 다운로드 시간 제한
                            .build());

            // uploadDTO 객체 리턴
            return new FileDto().builder()
                    .fileName(originFileName)
                    .roomId(roomId)
                    .filePath(fullPath)
                    .minioDataUrl(url)
                    .contentType(file.getContentType())
                    .status(FileDto.Status.UPLOADED)
                    .build();

        } catch (Exception e) {
            log.error("fileUploadException {}", e.getMessage());
            e.printStackTrace();

            return new FileDto().builder()
                    .status(FileDto.Status.FAIL)
                    .build();
        }
    }
    
    
    
    
    @Override
    public void uploadFileSizeCheck(MultipartFile file) {
        String extension = StringUtil.getExtension(file);
        if (!allowedFileExtensions.contains(extension)) {
            throw new ExceptionController.FileExtensionException("file extension exception");
        }
    }

 

3) minio download

- minio 에 있는 파일을 다운로드 받는 코드.

- 이것은 이전에 S3 에서 파일 다운로드 받는 코드를 거의 그대로 사용했다. 다른 점은 minioClient 에서 object 를 가져온다는 점?

// byte 배열 타입을 return 한다.
@Override
public ResponseEntity<byte[]> getObject(String fileName, String fileDir) throws Exception {
    // bucket 와 fileDir 을 사용해서 minIO 에 있는 객체 - object - 를 가져온다.
    InputStream fileData = minioClient.getObject(
            GetObjectArgs.builder()
                    .bucket(minioConfig.getBucketName())
                    .object(fileDir)
                    .build()
    );

    // 이후 다시 byte 배열 형태로 변환한다.
    // 파일 전송을 위해서는 다시 byte[] 즉, binary 로 변환해서 전달해야햐기 때문
    byte[] bytes = IOUtils.toByteArray(fileData);

    // 여기는 httpHeader 에 파일 다운로드 요청을 하기 위한내용
    HttpHeaders httpHeaders = new HttpHeaders();
    httpHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);

    // 지정된 fileName 으로 파일이 다운로드 된다.
    httpHeaders.setContentDispositionFormData("attachment", fileName);

    log.info("HttpHeader : [{}]", httpHeaders);

    // 최종적으로 ResponseEntity 객체를 리턴하는데
    // --> ResponseEntity 란?
    // ResponseEntity 는 사용자의 httpRequest 에 대한 응답 테이터를 포함하는 클래스이다.
    // 단순히 body 에 데이터를 포함하는 것이 아니라, header 와 httpStatus 까지 넣어 줄 수 있다.
    // 이를 통해서 header 에 따라서 다른 동작을 가능하게 할 수 있다 => 파일 다운로드!!

    // 나는 object가 변환된 byte 데이터, httpHeader 와 HttpStatus 가 포함된다.
    return new ResponseEntity<>(bytes, httpHeaders, HttpStatus.OK);
}

 

 

4) Delete file

- minio 에서 파일을 삭제해야한느 경우는 총 2가지가 있다. 먼저 batch 가 돌면서 방을 삭제할때와 유저가 방을 삭제할때이다.

- 이때 중요한 것은 roomID 를 기준으로 아래 있는 모든 파일을 삭제해야한다는 점이다. listObjects 로 roomId 로 시작하는 모든 객체들을 가져온다. 이후 해당 결과를 for 문으로 돌리면서 removeObject 를 사용해서 모든 객체를 삭제한다.

// path 아래있는 모든 파일을 삭제한다.
// 이때 path 는 roomId 가 된다 => minIO 에 roomId/변경된 파일명(uuid)/원본 파일명 으로 되어있기 때문에
// roomId 를 적어주면 기준이 되는 roomId 아래의 모든 파일이 삭제된다.
@Override
public void deleteFileDir(String roomId) {

    try {
        Iterable<Result<Item>> results = minioClient.listObjects(
                ListObjectsArgs.builder()
                        .bucket(minioConfig.getBucketName())
                        .prefix(roomId) // roomId 로 시작하는 모든 객체들을 가져옴
                        .recursive(true) // prefix 로 시작하는 하위 모든 디렉토리/파일을 가져옴
                        .build());

        for (Result<Item> result : results) {
            minioClient.removeObject(RemoveObjectArgs.builder()
                    .bucket(minioConfig.getBucketName())
                    .object(result.get().objectName())
                    .build());
        }
    } catch (Exception e) {
        log.error("error Message ::: {}", e.getCause());
        e.printStackTrace();
    }
}

 

5) fileUtil.js

- 일반 채팅에서 사용하는 파일 업로드 다운로드 관련 js

- 기본은 확장자를 검사하고, 파일을 업로드하거나 다운로드하는 로직을 갖는다. 사실 여기는 코드가 어렵지 않아서 패스

/**
 * 파일 관련 js
 * fileUpload, fileDownload
 */

const fileUtil = {
    isinit: false,
    init: function () {
        self = this;
        if (!self.isinit) {
            $('#uploadFile').on('click', function () {
                self.uploadFile();
            });

            self.isinit = true;
        }
    },
    uploadFile: function () { /// 파일 업로드 부분 ////

        var file = $('#file')[0].files[0];
        var formData = new FormData();
        formData.append('file', file);
        formData.append('roomId', roomId);

        // 확장자 추출
        var fileDot = file.name.lastIndexOf('.');

        // 확장자 검사
        var fileType = file.name.substring(fileDot + 1, file.name.length);
        // console.log('type : ' + fileType);

        if (!(fileType == 'png' || fileType == 'jpg' || fileType == 'jpeg' || fileType == 'gif')) {
            alert('파일 업로드는 png, jpg, gif, jpeg 만 가능합니다');
            return;
        }

        let successCallback = function (data) {

            // console.log('업로드 성공')
            if (data.status === 'FAIL') {
                alert('서버와의 연결 문제로 파일 업로드에 실패했습니다 \n 잠시 후 다시 시도해주세요')
                return;
            }

            var chatMessage = {
                'roomId': roomId,
                sender: username,
                message: username + '님의 파일 업로드',
                type: 'TALK',
                file: data
            };

            // 해당 내용을 발신한다.
            stompClient.send('/pub/chat/sendMessage', {}, JSON.stringify(chatMessage));
        };

        let errorCallback = function (error) {

        };

        // ajax 로 multipart/form-data 를 넘겨줄 때는
        //         processData: false,
        //         contentType: false
        // 처럼 설정해주어야 한다.
        // 동작 순서
        // post 로 rest 요청한다.
        // 1. 먼저 upload 로 파일 업로드를 요청한다.
        // 2. upload 가 성공적으로 완료되면 data 에 upload 객체를 받고,
        // 이를 이용해 chatMessage 를 작성한다.
        fileUploadAjax('/file/upload', 'POST', true, formData, successCallback, errorCallback);

    },
    downloadFile: function (name, dir) { // 파일 다운로드
        // console.log("파일 이름 : "+name);
        // console.log("파일 경로 : " + dir);

        let url = "/file/download/" + name;
        let data = {
            "fileName": name,
            "filePath": dir // 파일의 경로를 파라미터로 넣는다.
        };

        let successCallback = function (data) {
            var link = document.createElement('a');
            link.href = URL.createObjectURL(data);
            link.download = name;
            link.click();
        };

        let errorCallback = function (error) {

        };

        fileDownloadAjax(url, 'POST', '', data, successCallback, errorCallback);
    }
}

 

6) dataChannelFileUtil.js

- 이쪽은 datachannel 을 사용하여 파일을 업로드/다운로드 하기 위한 js

- 일반 채팅과 분리한 이유는 기능상 중복되는 부분도 있지만 datachannel 을 사용한다는 점이 있기에 이를 구분해서 개발하고 싶어서 일단 나눠두었다. 나중에 코드 정리가 필요하다면 하나로 합쳐볼 생각이다.

- 로직 자체는 위에잇는 fileUtil 과 거의 비슷하다. 다만 파일 업로드 후 datachannel 로 message 를 보낸다. 자세한 부분은 아래의 datachannel.js 에서...!!

/**
 * DataChannel 로 file 다루기 위한 util js
 */
const dataChannelFileUtil = {
    isinit: false,
    init : function(){
        let self = this;
        if (!self.isinit) {
            $('#uploadFile').on('click', function () {
                $('#file').click();
            });

            $('#file').on('change', function(){
                // 파일선택으로 change 되면 실행
                self.uploadFile();
            })

            self.isinit = true;
        }

    },
    uploadFile : function(){

        // 1. 다른 사용자에게 파일 전송
        var file = $('#file')[0].files[0];
        if (!file) {
            console.log('No file chosen');
        }

        var formData = new FormData();
        formData.append('file', file);
        formData.append('roomId', roomId);

        // 확장자 추출
        var fileDot = file.name.lastIndexOf('.');

        // 확장자 검사
        var fileType = file.name.substring(fileDot + 1, file.name.length);
        // console.log('type : ' + fileType);

        if (!(fileType == 'png' || fileType == 'jpg' || fileType == 'jpeg' || fileType == 'gif')) {
            alert('파일 업로드는 png, jpg, gif, jpeg 만 가능합니다');
            return;
        }

        let successCallback = function (data) {

            // console.log('업로드 성공')
            if (data.status === 'FAIL') {
                alert('서버와의 연결 문제로 파일 업로드에 실패했습니다 \n 잠시 후 다시 시도해주세요')
                return;
            }

            var fileData = {
                type: 'file',
                'roomId': roomId,
                fileMeta: data
            };

            dataChannel.sendFileMessage(fileData);

        };

        let errorCallback = function (error) {
            let errorJson = error?.responseJSON;
            if (!errorJson) {
                alert('파일 업로드 용량 또는 파일 확장자를 확인해주세요 \n 확장자 : jpg, jepg, png, gif \n 용량 제한 : Max 10MB');
                return;
            }
            if (errorJson?.code === '40022') {
                alert('업로드는 jpg, jepg, png, gif 파일 만 가능합니다');
            }
        };

        // 2. 서버에 파일 전송
        fileUploadAjax('/file/upload', 'POST', true, formData, successCallback, errorCallback);

    },
    downloadFile : function (name, dir) {
        // console.log("파일 이름 : "+name);
        // console.log("파일 경로 : " + dir);

        let url = "/file/download/" + name;
        let data = {
            "fileName": name,
            "filePath": dir // 파일의 경로를 파라미터로 넣는다.
        };

        let successCallback = function (data) {
            var link = document.createElement('a');
            link.href = URL.createObjectURL(data);
            link.download = name;
            link.click();
        };

        let errorCallback = function (error) {

        };

        fileDownloadAjax(url, 'POST', '', data, successCallback, errorCallback);
    }
}

 

7) datachannel.js

- 기존에 handleDataChannelMessageReceived 의 로직을 조금 수정하였다. 파일 전송과 관련된 메시지는 type 에 file 을 넣어서 구분한다.

-  showNewFileMessage 가 총 2번 동작하는데 먼저 datachannelFileUtils 에서 sendFileMessage 를 부를 때 자기자신에게 'self' 에게 보여주기 위해 한번 실행하고, datachannel.send 해서 file 데이터를 전송한 후 상대방의 채팅창에 그려주기 위해 한번 더 실행한다.

쉽게 생각하자면 a.jpg 를 업로드 했을 때 나에게도 보여지고, 상대방에게도 보여주기 위한 처리를 하기 위함이라고 보면 된다.

/**
 * DataChannel 을 다루기 위한 js
 */
let chanId = 0;

const dataChannel = {
    user : null,
    handleDataChannelMessageReceived: function(event) { // datachannel 메시지 받는 부분
        if (this.isNullOrUndefined(event)) return;
        // console.log("dataChannel.OnMessage:", event);
        let recvMessage = JSON.parse(event.data);

        if (recvMessage.type === "file") {
            let file = recvMessage.fileMeta;

            // 파일 메시지 처리
            console.log("Received file:", file.fileName);

            let sendUser = recvMessage.userName;
            let message = sendUser + " 님이 파일을 업로드하였습니다";

            this.showNewMessage(message, 'other');
            this.showNewFileMessage(file, 'other');

        } else {
            // 일반 메시지 처리
            let message = recvMessage.userName + " : " + recvMessage.message;
            this.showNewMessage(message, "other");
        }
    },
    sendFileMessage : function(fileMeta){
        fileMeta.userName = this.user.name;
        this.user.rtcPeer.send(JSON.stringify(fileMeta));
        this.showNewFileMessage(fileMeta.fileMeta, 'self');
    },
    showNewFileMessage : function(file, type){

        // 이미지 요소 생성 및 설정
        var imgElement = $('<img>', {
            src: file.minioDataUrl,
            width: 300,
            height: 300
        });
        imgElement.addClass(type);

        // 다운로드 버튼 요소 생성 및 설정
        var downBtnElement = $('<button>', {
            class: 'btn fa fa-download',
            id: 'downBtn',
            name: file.fileName
        }).on('click', function() {
            dataChannelFileUtil.downloadFile(file.fileName, file.filePath);
        });

        // contentElement 생성
        var contentElement = $('<li>').append(imgElement, downBtnElement);
        contentElement.addClass(type);

        // $messagesContainer에 contentElement 추가
        dataChannelChatting.$messagesContainer.append(contentElement);
    }
}

6. 구현 영상!

오랜만에 정말 오랜만에 기능다운 기능을 만든 것 같아서 너무 기쁘다. 특히 datachannel 을 이용해서 파일 업로드/다운로드를 구현해보면서 이것을 다른 방향으로 정말 여러가지로 사용해볼 수 있는 아이디어도 떠오르고 공부도 많이 되었던 것 같다.


Reference

minIO API

https://min.io/docs/minio/linux/developers/java/API.html#removeObjects

 

Java Client API Reference — MinIO Object Storage for Linux

Map - Contains form-data to upload an object using POST method.

min.io

 

JVM 인증서 에러

https://www.lesstif.com/java/java-pkix-path-building-failed-98926844.html

 

Java PKIX path building failed: 에러 해결

JAVA_HOME 변수가 설정되어 있어야 하며 root 로 실행해야 합니다.

www.lesstif.com

 

minio ssl 인증서 확인 무시처리

https://stackoverflow.com/questions/72461218/minio-client-connection-error-java-spring-framework

 

Minio client connection error java spring framework

//config MinioClient minioClient = MinioClient.builder().endpoint(minioEndpoint).credentials(accessKey, accessSecret).build(); Error: java.lang.RuntimeException: PKIX path building failed: sun.secu...

stackoverflow.com

 

https://ko.javascript.info/optional-chaining

 

옵셔널 체이닝 '?.'

 

ko.javascript.info

 

https://stackoverflow.com/questions/50878454/using-https-for-minio-server

 

Using https for minio server

I am trying to get a minio server to run on https but everytime i try to run it i get the following error: {"level":"FATAL","time":"2018-06-15T15:12:19.2189519Z","error":{"message":"The parameter...

stackoverflow.com

 

https://stackoverflow.com/questions/9619030/resolving-javax-net-ssl-sslhandshakeexception-sun-security-validator-validatore

 

Resolving javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed Error?

Edit : I tried to format the question and accepted answer in more presentable way at my blog. Here is the original issue. I am getting this error: detailed message sun.security.validator.

stackoverflow.com

 

댓글