아래 SftpTransfer 는 JSch(포크판 com.github.mwiede:jsch) 로 만든 실제 구현 예시입니다. jar 가 필요해 이 저장소에서는 컴파일하지 않습니다.
// build.gradle: implementation 'com.github.mwiede:jsch:0.2.19'
public class SftpTransfer implements FileTransfer {
private final String host, user, keyPath, knownHosts;
private Session session;
private ChannelSftp channel;
public SftpTransfer(String host, String user, String keyPath, String knownHosts) {
this.host = host; this.user = user;
this.keyPath = keyPath; this.knownHosts = knownHosts;
}
@Override
public void connect() throws TransferException {
try {
JSch jsch = new JSch();
jsch.addIdentity(keyPath); // 키 인증
jsch.setKnownHosts(knownHosts); // 호스트 키 검증, 끄지 않는다
session = jsch.getSession(user, host, 22);
session.setConfig("StrictHostKeyChecking", "yes");
session.setTimeout(10_000); // 연결·읽기 타임아웃
session.setServerAliveInterval(30_000); // keepalive
session.connect();
channel = (ChannelSftp) session.openChannel("sftp");
channel.connect();
} catch (JSchException e) {
throw new TransferException(Kind.CONNECT, "SFTP 연결 실패", e);
}
}
@Override
public void upload(String localPath, String remoteName) throws TransferException {
try (InputStream in = Files.newInputStream(Paths.get(localPath))) {
channel.put(in, remoteName); // 내부적으로 스트리밍 전송
} catch (SftpException | IOException e) {
throw new TransferException(Kind.IO, "업로드 실패: " + remoteName, e);
}
}
@Override
public void rename(String from, String to) throws TransferException {
try {
channel.rename(from, to);
} catch (SftpException e) {
throw new TransferException(Kind.IO, "이름변경 실패", e);
}
}
@Override
public void close() {
if (channel != null) channel.disconnect();
if (session != null) session.disconnect();
}
// list·download·delete·size 는 channel.ls/get/rm/lstat 로 구현
}FileTransfer 인터페이스를 그대로 구현했으므로, Main 의 safeUpload·pollAndProcess 는 손대지 않고 new LocalTransfer(...) 자리를 new SftpTransfer(...) 로 바꾸기만 하면 됩니다.
상대가 FTP 만 지원할 때는 Commons Net 이 더 간단합니다. 이 코드도 jar 가 필요해 컴파일하지 않습니다.
// build.gradle: implementation 'commons-net:commons-net:3.11.1'
FTPClient ftp = new FTPClient();
ftp.connect(host, 21);
ftp.login(user, password);
ftp.enterLocalPassiveMode(); // 수동 모드로 방화벽 이슈 회피
ftp.setFileType(FTP.BINARY_FILE_TYPE);
try (InputStream in = Files.newInputStream(local)) {
ftp.storeFile(remoteName + ".tmp", in);
}
ftp.rename(remoteName + ".tmp", remoteName); // 원자적 전송 동일 적용
ftp.logout();
ftp.disconnect();배치를 붙이기 전에는 항상 손으로 먼저 접속해 확인합니다. 자세한 옵션은 리눅스 명령어 06 레슨을 참고합니다.
# batch.txt 에 put/get/rename 명령을 적어 한 번에 실행
sftp -b batch.txt user@host
# 증분만 올리고, 완료 후 원자적으로 이름 붙이는 대신
# rsync 자체의 임시 파일(.filename.XXXX) + rename 방식 사용
rsync -avz --partial-dir=.rsync-partial ./out/ user@host:/incoming/