Feat: 파일 전송 기능 구현 (업로드/다운로드)

- RETR 명령어 구현: 파일 다운로드
  * 파일 존재 여부 확인
  * 데이터 연결을 통한 파일 스트림 전송
  * 전송 바이트 수 로깅
  * 에러 처리 및 연결 자동 종료
- STOR 명령어 구현: 파일 업로드
  * 현재 디렉토리에 파일 생성
  * 데이터 연결을 통한 파일 스트림 수신
  * 전송 완료 후 파일 크기 확인
  * 에러 처리 및 연결 자동 종료
- 두 명령어 모두 PASV 선행 필수
- FileInputStream/FileOutputStream을 사용한 스트림 처리
- 전송 성공/실패에 따른 적절한 응답 코드 반환

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-01 02:55:28 +09:00
parent 5b276a4d8a
commit 74bf67a9b2

View File

@@ -107,6 +107,12 @@ public class FTPSession implements Runnable {
case "PASV":
handlePasv();
break;
case "RETR":
handleRetr(argument);
break;
case "STOR":
handleStor(argument);
break;
case "NOOP":
handleNoop();
break;
@@ -369,6 +375,104 @@ public class FTPSession implements Runnable {
}
}
private void handleRetr(String fileName) throws IOException {
if (!isAuthenticated) {
sendResponse(FTPResponse.NOT_LOGGED_IN, "Please login first");
return;
}
if (fileName.isEmpty()) {
sendResponse(FTPResponse.SYNTAX_ERROR_PARAMETERS, "No file name specified");
return;
}
if (dataConnection == null) {
sendResponse(FTPResponse.CANNOT_OPEN_DATA_CONNECTION, "Use PASV first");
return;
}
java.io.File file = fileSystem.getFile(fileName);
if (file == null || !file.exists() || !file.isFile()) {
sendResponse(FTPResponse.FILE_UNAVAILABLE, "File not found");
dataConnection.close();
dataConnection = null;
return;
}
sendResponse(FTPResponse.FILE_STATUS_OK, "Opening data connection for " + fileName + " (" + file.length() + " bytes)");
if (dataConnection.acceptConnection()) {
try {
java.io.FileInputStream fis = new java.io.FileInputStream(file);
if (dataConnection.transferStream(fis)) {
fis.close();
dataConnection.close();
sendResponse(FTPResponse.CLOSING_DATA_CONNECTION, "Transfer complete");
Log.i(TAG, "File sent: " + fileName + " (" + file.length() + " bytes)");
} else {
fis.close();
dataConnection.close();
sendResponse(FTPResponse.CONNECTION_CLOSED, "Transfer failed");
}
} catch (Exception e) {
Log.e(TAG, "Error sending file: " + e.getMessage());
dataConnection.close();
sendResponse(FTPResponse.CONNECTION_CLOSED, "Transfer error: " + e.getMessage());
}
} else {
dataConnection.close();
sendResponse(FTPResponse.CANNOT_OPEN_DATA_CONNECTION, "Cannot open data connection");
}
dataConnection = null;
}
private void handleStor(String fileName) throws IOException {
if (!isAuthenticated) {
sendResponse(FTPResponse.NOT_LOGGED_IN, "Please login first");
return;
}
if (fileName.isEmpty()) {
sendResponse(FTPResponse.SYNTAX_ERROR_PARAMETERS, "No file name specified");
return;
}
if (dataConnection == null) {
sendResponse(FTPResponse.CANNOT_OPEN_DATA_CONNECTION, "Use PASV first");
return;
}
java.io.File file = new java.io.File(fileSystem.getCurrentDirectory(), fileName);
sendResponse(FTPResponse.FILE_STATUS_OK, "Opening data connection for " + fileName);
if (dataConnection.acceptConnection()) {
try {
java.io.FileOutputStream fos = new java.io.FileOutputStream(file);
if (dataConnection.receiveStream(fos)) {
fos.close();
dataConnection.close();
sendResponse(FTPResponse.CLOSING_DATA_CONNECTION, "Transfer complete");
Log.i(TAG, "File received: " + fileName + " (" + file.length() + " bytes)");
} else {
fos.close();
dataConnection.close();
sendResponse(FTPResponse.CONNECTION_CLOSED, "Transfer failed");
}
} catch (Exception e) {
Log.e(TAG, "Error receiving file: " + e.getMessage());
dataConnection.close();
sendResponse(FTPResponse.CONNECTION_CLOSED, "Transfer error: " + e.getMessage());
}
} else {
dataConnection.close();
sendResponse(FTPResponse.CANNOT_OPEN_DATA_CONNECTION, "Cannot open data connection");
}
dataConnection = null;
}
private void handleNoop() throws IOException {
sendResponse(FTPResponse.COMMAND_OK, "OK");
}