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 였습니다.
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
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
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
JVM 인증서 에러
https://www.lesstif.com/java/java-pkix-path-building-failed-98926844.html
minio ssl 인증서 확인 무시처리
https://stackoverflow.com/questions/72461218/minio-client-connection-error-java-spring-framework
https://ko.javascript.info/optional-chaining
https://stackoverflow.com/questions/50878454/using-https-for-minio-server
댓글