FTP 서버와 연결돼서파일 업로드 및 다운로드가 가능한 API 서버를 구축하려 합니다. FTP서버에 있는 영상자료 스트리밍까지 작업까지가 목표이지만,, 우선 차례차례.. ㅎㅎ
2024.09.25 - [Windows/FTP] - [Wondows] FTP 서버 구축하기
이전에 구축해 둔 FTP 서버에 접근하여 파일 업로드/다운로드 기능을 적용해보려 합니다.
🌟 작업 환경
Java 17, Spring Boot 3.2.2, Gradle 8.0.1, MSSQL Window11
기존에 생성해 둔 Spring Boot 프로젝트이긴 하지만 세팅이 거의 안 되어있는 상태여서 라이브러리 추가 부분도 함께 적어두었습니다.
implementation group: 'commons-net', name: 'commons-net', version: '3.6'
implementation group: 'org.apache.commons', name: 'commons-pool2', version: '2.11.1'
implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.12.0'
compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.18.30'
commons-net, commons-pool2, commons-lang3, lombok 을 추가해 주었습니다.
🌟 INDEX
⭐ FTP 설정 클래스 구현
Srping Boot 의 @ConfigurationProperties 어노테이션을 사용해서 ftp.server 접두사로 시작하는 프로퍼티를 자동으로 바인딩합니다.
@Data 어노테이션을 사용해서 Lombok 으로 Getter/Setter 메서드를 자동 생성합니다.
💫 FtpConfig.java
package com.liyo.besys.ftp;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import lombok.Data;
@Component
@ConfigurationProperties(prefix = "ftp.server")
@Data
public class FtpConfig {
private String host;
private int port;
private String username;
private String password;
private String dir;
private String encoding;
private String root;
private int maxtotal;
private int minidel;
private int maxidle;
private int maxwaitmillis;
}
⭐ FTP 설정 프로퍼티 관리
💫 application.yml
spring:
application:
profiles:
main:
banner:
messages:
config:
security:
datasource:
ftp:
server:
host: 192.168.0.123 // 호스트 주소
port: 21 // 포트번호
username: liyoFTP // 사용 자명
password: Liyo2024! // 패스워드
encoding: UTF-8
dir: /
root: /
max-total: 100
min-idle: 2
max-idle: 5
max-wait-millis: 3000
jpa:
devtools:
Gradle 빌드를 사용하고 있어 application.yml 파일에 FTP 서버 정보를 추가해 주었습니다.
spring 안에 DB 정보가 담겨있는 datasource 아래에 추가해 주었습니다.
순서는 관계없습니다.FTP 정보 외에는 생략하였습니다.
⭐ FTP 데이터 전송 객체 모델링
FTP 서버 연결 정보와 파일 전송 정보, 파일 전송 결과 정보 등을 담는 Value Object 와 Data Transfer Object 를 구현해 줍니다.
💫 FtpVo.java
package com.liyo.besys.ftp;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = false)
public class FtpVo {
private String downFile; // UP_ADD_FILE_NM
private String orginFile;
private String tableNm;
private String columnNm;
private String groupId;
private String fileNm;
private String fileId;
private boolean group;
}
💫 FtpDto.java
package com.liyo.besys.ftp;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.Locale;
import java.util.UUID;
import org.springframework.web.multipart.MultipartFile;
import lombok.Data;
@Data
public class FtpDto {
private String originalColumnname; // 여러첨부파일요청시에만...
private String saveColumnName; // 여러첨부파일요청시에만...
private String fileName; // 파일명
private String exe; // 확장자
private String path; // 경로
private String type; // 파일 종류
private boolean group; // 그룹 유무
private long groupId; // 그룹ID
private long fileGrpId; // 파일그룹ID
private long fileId; // 파일ID
private String originalFileName; // 원래 파일명
private String saveFileName; // 저장 파일명
private String tableName; // 테이블명
private String columnName; // 컬럼명
private MultipartFile file; // 저장할 파일
private long fileSize; // 파일 크기
public FtpDto(MultipartFile file, String tableName, String columnName) {
this.file = file;
this.tableName = tableName;
this.columnName = columnName;
this.originalFileName = file.getOriginalFilename();
this.fileSize = file.getSize();
String pattern = "yyyyMMddhhmmssSSS";
SimpleDateFormat sdfCurrent = new SimpleDateFormat(pattern, Locale.KOREA);
Timestamp timestamp = new Timestamp(System.currentTimeMillis());
String saveFileName = sdfCurrent.format(timestamp.getTime()) + UUID.randomUUID().toString().substring(0, 3);
this.fileName = saveFileName; // 파일명만
this.saveFileName = saveFileName
+ file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."));
}
}
⭐ FTP 클라이언트 구현
Apache Commons Net 라이브러리를 사용해서 FTP 클라이언트를 구현해 주었습니다.
- makeObject() : FTP 클라이언트 객체를 생성합니다.
- destroyObject() : FTP 클라이언트 객체를 종료하고 연결을 끊습니다.
- validateObject() : FTP 클라이언트 객체가 유효한지 확인합니다.
- activateObject() : FTP 클라이언트 객체를 활성화하고 FTP 서버에 연결합니다.
- passivateObject() : FTP 클라이언트 객체를 비활성화하고 FTP 서버에서 연결을 끊습니다.
💫 FtpClientFactory.java
package com.liyo.besys.ftp;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.apache.commons.pool2.PooledObjectFactory;
import java.io.IOException;
@Component
public class FtpClientFactory implements PooledObjectFactory<FTPClient> {
@Autowired
FtpConfig config;
@Override
public PooledObject<FTPClient> makeObject() {
FTPClient ftpClient = new FTPClient();// Create a client instance
ftpClient.setRemoteVerificationEnabled(false); // * is not same as server
return new DefaultPooledObject<>(ftpClient);
}
@Override
public void destroyObject(PooledObject<FTPClient> pooledObject) {
FTPClient ftpClient = pooledObject.getObject();
try {
ftpClient.logout();
if (ftpClient.isConnected()) {
ftpClient.disconnect();
}
} catch (IOException e) {
throw new RuntimeException("Could not disconnect from server.", e);
}
}
@Override
public boolean validateObject(PooledObject<FTPClient> pooledObject) {
FTPClient ftpClient = pooledObject.getObject();
try {
return ftpClient.sendNoOp();
} catch (IOException e) {
return false;
}
}
@Override
public void activateObject(PooledObject<FTPClient> pooledObject) throws Exception {
FTPClient ftpClient = pooledObject.getObject();
ftpClient.connect(config.getHost(), config.getPort());
ftpClient.login(config.getUsername(), config.getPassword());
ftpClient.setControlEncoding(config.getEncoding());
ftpClient.changeWorkingDirectory(config.getDir());
ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
}
@Override
public void passivateObject(PooledObject<FTPClient> pooledObject) throws Exception {
FTPClient ftpClient = pooledObject.getObject();
try {
ftpClient.changeWorkingDirectory(config.getRoot());
ftpClient.logout();
if (ftpClient.isConnected()) {
ftpClient.disconnect();
}
} catch (IOException e) {
throw new RuntimeException("Could not disconnect from server.", e);
}
}
public FtpConfig getConfig() {
return config;
}
}
⭐ FTP 클라이언트 풀 관리
FTP 클라이언트 풀을 관리하는 FtpPoop 클래스를 생성해 줍니다.
FtpClientFactory 주입 : 생성자에서 FtpClientFactory 객체를 주입받습니다. FtpClientFactory는 FTPClient 객체를 생성하는 역할을 합니다.
FTPClient 풀 생성: FtpConfig 객체에서 읽어온 설정 값을 사용하여 GenericObjectPool 를 생성합니다. 이 풀은 FTPClient 객체를 관리합니다.
FTPClient 가져오기 : getFTPClient() 메서드를 통해 FTPClient 객체를 풀에서 가져올 수 있습니다. 이 메서드는 풀에서 FTPClient 객체를 빌려오고, 사용이 끝나면 다시 풀에 반환합니다.
FTPClient 반환 : returnFTPClient() 메서드를 통해 사용이 끝난 FTPClient 객체를 풀에 반환합니다.
풀 종료 : destroy() 메서드를 통해 풀을 종료할 수 있습니다.
FTP클라이언트 객체의 생성과 관리를 효율적으로 처리하기 위해 객체 풀 패턴을 사용하고,
FTP클라이언트 객체의 생성 및 해제 비용을 줄이고, 성능을 향상할 수 있습니다.
💫 FtpPool.java
package com.liyo.besys.ftp;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class FtpPool {
FtpClientFactory factory;
private final GenericObjectPool<FTPClient> internalPool;
@SuppressWarnings("unchecked")
public FtpPool(@Autowired FtpClientFactory factory) {
this.factory = factory;
FtpConfig config = factory.getConfig();
@SuppressWarnings("rawtypes")
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
poolConfig.setMaxTotal(config.getMaxtotal());
poolConfig.setMinIdle(config.getMinidel());
poolConfig.setMaxIdle(config.getMaxidle());
poolConfig.setMaxWaitMillis(config.getMaxwaitmillis());
this.internalPool = new GenericObjectPool<FTPClient>(factory, poolConfig);
}
public FTPClient getFTPClient() {
try {
return internalPool.borrowObject();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public void returnFTPClient(FTPClient ftpClient) {
try {
internalPool.returnObject(ftpClient);
} catch (Exception e) {
e.printStackTrace();
}
}
public void destroy() {
try {
internalPool.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
⭐ FTP 서버 연결 및 파일 전송
FTP 관련 기능을 제공할 FTP 유틸리티 클래스를 만들어 줍니다.
💫 FtpUtil.java
package com.liyo.besys.ftp;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.net.ftp.FTPClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ResourceLoader;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import com.liyo.besys.constant.BesysConstants;
import com.liyo.cmm.service.Globals;
import com.liyo.cmm.service.GlobalsProperties;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.log4j.Log4j2;
@Log4j2
@Component
public class FtpUtil {
@Autowired
FtpConfig config;
@Autowired
ResourceLoader resourceLoader;
@Autowired
FtpPool pool;
/**
* FTP 업로더
*
* @param dto
* @param ftpClient
* @return
* @throws Exception
*/
private static FtpDto write(FtpDto dto, FTPClient ftpClient) throws Exception {
if (StringUtils.isEmpty(dto.getTableName()) || StringUtils.isEmpty(dto.getColumnName())
|| StringUtils.isEmpty(dto.getSaveFileName())) {
throw new Exception("필수값이 없습니다.");
}
MultipartFile file = dto.getFile();
InputStream input = file.getInputStream();
try {
String extensionsImage = GlobalsProperties.getProperty("globals.extensions.Images");
String extensionsVideo = GlobalsProperties.getProperty("globals.extensions.Video");
String extensionsDoc = GlobalsProperties.getProperty("globals.extensions.Doc");
String fileUploadMaxSizeString = GlobalsProperties.getProperty("globals.extensions.maxSize");
int fileUploadMaxSize = Integer.valueOf(GlobalsProperties.getProperty("globals.extensions.maxSize"));
int lastIndex = dto.getOriginalFileName().lastIndexOf(BesysConstants.DOT);
String fileExt = dto.getOriginalFileName().substring(lastIndex + 1).toLowerCase();
String rootName = BesysConstants.NOGROUP;
String fileType = BesysConstants.FILE_TYPE_ETC;
if (extensionsImage.contains(fileExt)) {
fileType = BesysConstants.FILE_TYPE_IMG;
} else if (extensionsVideo.contains(fileExt)) {
fileType = BesysConstants.FILE_TYPE_VIDEO;
} else if (extensionsDoc.contains(fileExt)) {
fileType = BesysConstants.FILE_TYPE_DOC;
}
if (dto.isGroup()) {
rootName = BesysConstants.FILE_TYPE_IMG_NAME;
}
dto.setType(fileType);
if (fileUploadMaxSize < dto.getFileSize()) {
throw new Exception("첨부 파일은 " + fileUploadMaxSizeString + "byte를 넘을 수 없습니다.");
}
StringBuilder path = new StringBuilder();
path.append(BesysConstants.SLASH).append(rootName).append(BesysConstants.SLASH).append(dto.getTableName())
.append(BesysConstants.SLASH).append(dto.getColumnName());
boolean dirExists = true;
String[] directories = path.toString().split(BesysConstants.SLASH);
for (String dir : directories) {
if (path.toString().equals(ftpClient.printWorkingDirectory())) {
continue;
}
if (!dir.isEmpty()) {
if (dirExists) {
dirExists = ftpClient.changeWorkingDirectory(dir);
}
if (!dirExists) {
if (!ftpClient.makeDirectory(dir)) {
throw new IOException("makeDirectory:: " + dir + "::" + ftpClient.getReplyString());
}
if (!ftpClient.changeWorkingDirectory(dir)) {
throw new IOException(
"changeWorkingDirectory:: " + dir + "::" + ftpClient.getReplyString());
}
}
}
}
path.append(BesysConstants.SLASH).append(dto.getSaveFileName());
dto.setPath(path.toString());
dto.setExe(fileExt);
boolean result = ftpClient.storeFile(path.toString(), input);// Execute file transfer
if (!result) {
throw new RuntimeException("파일 업로드를 실패했습니다.");
}
log.info("path1 : {}", path.toString());
} catch (Exception e) {
log.error("error write1 : {}", e);
} finally {
dto.setFile(null);
input.close();
}
return dto;
}
/**
* 파일 업로드
*
* @param dto
* @return
* @throws Exception
*/
public Map<String, String> upload(FtpDto dto) throws Exception {
FTPClient ftpClient = pool.getFTPClient();
/**
* 저장
*/
boolean isSave = false;
try {
dto = write(dto, ftpClient);
isSave = true;
} catch (Exception e) {
log.error("error upload : {}", e);
} finally {
pool.returnFTPClient(ftpClient);
}
Map<String, String> map = new HashMap<String, String>();
map.put(Globals.ORIGIN_FILE_NM, dto.getOriginalFileName());
map.put(Globals.UPLOAD_FILE_NM, dto.getFileName());
map.put(Globals.FILE_EXT, dto.getExe());
map.put(Globals.FILE_PATH, dto.getPath());
map.put(Globals.FILE_TYPE, dto.getType());
map.put(Globals.FILE_SIZE, String.valueOf(dto.getFileSize()));
map.put(Globals.IS_SAVE, isSave ? "TRUE" : "FALSE");
return map;
}
/**
* 파일 리스트 업로드
*
* @param fptDtoList
* @return
* @throws Exception
*/
public List<Map<String, String>> upload(List<FtpDto> fptDtoList) throws Exception {
FTPClient ftpClient = pool.getFTPClient();
/**
* 저장
*/
boolean isSave = false;
try {
for (FtpDto dto : fptDtoList) {
dto = write(dto, ftpClient);
}
isSave = true;
} catch (Exception e) {
log.error("error uploadList : {}", e);
} finally {
pool.returnFTPClient(ftpClient);
}
List<Map<String, String>> fileMap = new ArrayList<Map<String, String>>();
for (FtpDto dto : fptDtoList) {
Map<String, String> map = new HashMap<String, String>();
map.put(Globals.ORIGIN_FILE_NM, dto.getOriginalFileName());
map.put(Globals.UPLOAD_FILE_NM, dto.getFileName());
map.put(Globals.FILE_EXT, dto.getExe());
map.put(Globals.FILE_PATH, dto.getPath());
map.put(Globals.FILE_SIZE, String.valueOf(dto.getFileSize()));
map.put("originColumnName", dto.getOriginalColumnname());
map.put("saveColumnName", dto.getSaveColumnName());
map.put(Globals.IS_SAVE, isSave ? "TRUE" : "FALSE");
fileMap.add(map);
}
return fileMap;
}
/**
* 파일 다운로드
*
* @param vo
* @param resp
* @throws IOException
*/
public void download(FtpVo vo, HttpServletResponse resp) throws IOException {
StringBuilder path = new StringBuilder();
if (StringUtils.isEmpty(vo.getGroupId())) {
path.append(BesysConstants.SLASH).append(BesysConstants.NOGROUP).append(BesysConstants.SLASH)
.append(vo.getTableNm()).append(BesysConstants.SLASH).append(vo.getColumnNm())
.append(BesysConstants.SLASH).append(vo.getDownFile());
} else {
path.append(BesysConstants.SLASH).append(BesysConstants.GROUP).append(BesysConstants.SLASH)
.append(vo.getTableNm()).append(BesysConstants.SLASH).append(vo.getColumnNm())
.append(BesysConstants.SLASH).append(vo.getDownFile());
}
String orgFileName = vo.getOrginFile().replaceAll("\r", "").replaceAll("\n", "");
FTPClient ftpClient = pool.getFTPClient();
resp.setContentType("application/force-download");
resp.setHeader("Content-Transfer-Encoding", "binary");
resp.setHeader("Pragma", "no-cache");
resp.setHeader("Expires", "0");
resp.setHeader("Content-Disposition",
"attachment;filename=" + URLEncoder.encode(orgFileName, BesysConstants.UTF8) + ";");
OutputStream out = resp.getOutputStream();
ftpClient.retrieveFile(path.toString(), out);
out.flush();
out.close();
pool.returnFTPClient(ftpClient);
}
/**
* 이미지
* noGroup :
* http://localhost:8080/image/loadAttachImage.do?fileNm=/CR0010/UP_ADD_FILE_NM/20220707104046038c4d.jpg
* group : /image/loadImage.do?fileId=175 (SY0100,SY00B0,SY00B1 에서 패스/파일네임 조회)
*
* @param vo
* @param resp
* @throws IOException
*/
public void img(FtpVo vo, HttpServletResponse resp) throws IOException {
String fileNm = vo.getFileNm();
FTPClient ftpClient = pool.getFTPClient();
resp.setContentType("application/force-download");
resp.setHeader("Content-Transfer-Encoding", "binary");
resp.setHeader("Pragma", "no-cache");
resp.setHeader("Expires", "0");
resp.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileNm, "UTF-8") + ";");
OutputStream out = resp.getOutputStream();
InputStream inputStream = null;
if (vo.isGroup()) {
inputStream = ftpClient.retrieveFileStream(fileNm);
} else {
inputStream = ftpClient.retrieveFileStream(BesysConstants.NOGROUP + BesysConstants.SLASH + fileNm);
}
if (inputStream == null) {
throw new IOException(ftpClient.getReplyString());
}
int len;
byte[] buf = new byte[1024];
while ((len = inputStream.read(buf)) != -1) {
out.write(buf, 0, len);
}
out.flush();
out.close();
pool.returnFTPClient(ftpClient);
}
/**
* show
*
* @param fileName
* @return
*/
public ResponseEntity<?> show(String fileName) {
String username = config.getUsername();
String password = config.getPassword();
String host = config.getHost();
String work = config.getDir();
// ftp://root:root@192.168.xx.xx/path+fileName
return ResponseEntity.ok(
resourceLoader.getResource("ftp://" + username + ":" + password + "@" + host + work + "/" + fileName));
}
}
FTP업로더, 파일 업로드, 파일 리스트 업로드, 파일 다운로드, 이미지 를 처리하는 메서드들로 구성되어 있습니다.
각 부분에 대한 정리는 따로 해야 할 것 같습니다..
'Java > SpringBoot' 카테고리의 다른 글
[Spring Boot] ResourceCloseHelper Class 생성 - 리소스 관리 (0) | 2024.11.16 |
---|---|
[Spring Boot] WebUtil Class 생성 - XSS, SQL injection 보안 취약점 방지 (3) | 2024.11.14 |
[SpringBoot] Spring Cloud OpenFeign 생성하기 (1) | 2024.10.05 |
[Spring Boot] StringUtilClass (0) | 2024.09.29 |
[SpringBoot] 3.x 버전 마이그레이션 RestTemplate 오류 해결 (1) | 2024.08.05 |