<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>3.9.0</version> <!-- 請檢查最新版本 -->
</dependency>
enterLocalPassiveMode(),適應防火牆/NAT 環境。setFileType(FTP.BINARY_FILE_TYPE),避免文字模式轉換損壞檔案。setRestartOffset(localFile.length()) + retrieveFile(remote, outputStream),OutputStream 以 append=true 開啟。setRestartOffset(remoteFileSize) + storeFile(remote, inputStream),InputStream skip(remoteFileSize) 定位。makeDirectory + changeWorkingDirectory 確保遠端路徑存在。package utils;
import org.apache.commons.net.ftp.*;
import org.apache.commons.net.PrintCommandListener;
import java.io.*;
import java.io.PrintWriter;
public class FtpResumeClient {
private FTPClient ftp = new FTPClient();
public FtpResumeClient() {
ftp.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(System.out), true));
}
public boolean connect(String host, int port, String user, String pass) throws IOException {
ftp.connect(host, port);
if (!FTPReply.isPositiveCompletion(ftp.getReplyCode())) {
disconnect();
return false;
}
return ftp.login(user, pass);
}
public boolean download(String remote, String local) throws IOException {
ftp.enterLocalPassiveMode();
ftp.setFileType(FTP.BINARY_FILE_TYPE);
File localFile = new File(local);
FTPFile[] files = ftp.listFiles(remote);
if (files.length != 1) return false;
long remoteSize = files[0].getSize();
try (OutputStream out = new FileOutputStream(localFile, localFile.exists())) {
if (localFile.exists()) {
if (localFile.length() >= remoteSize) return false; // 已完整
ftp.setRestartOffset(localFile.length());
}
return ftp.retrieveFile(remote, out);
}
}
public enum UploadStatus {
SUCCESS, FILE_EXISTS, REMOTE_LARGER, CREATE_DIR_FAIL, DELETE_FAIL, UPLOAD_FAIL
}
public UploadStatus upload(String local, String remote) throws IOException {
ftp.enterLocalPassiveMode();
ftp.setFileType(FTP.BINARY_FILE_TYPE);
// 解析遠端目錄與檔名
String remoteFileName = remote.contains("/") ? remote.substring(remote.lastIndexOf('/') + 1) : remote;
String remoteDir = remote.contains("/") ? remote.substring(0, remote.lastIndexOf('/') + 1) : "/";
// 確保遠端目錄存在
if (!remoteDir.equals("/") && !ftp.changeWorkingDirectory(remoteDir)) {
if (!createRemoteDirs(remoteDir)) return UploadStatus.CREATE_DIR_FAIL;
}
FTPFile[] remoteFiles = ftp.listFiles(remoteFileName);
File localFile = new File(local);
long localSize = localFile.length();
if (remoteFiles.length == 1) {
long remoteSize = remoteFiles[0].getSize();
if (remoteSize == localSize) return UploadStatus.FILE_EXISTS;
if (remoteSize > localSize) return UploadStatus.REMOTE_LARGER;
// 斷點續傳
try (InputStream in = new FileInputStream(localFile)) {
if (in.skip(remoteSize) == remoteSize) {
ftp.setRestartOffset(remoteSize);
if (ftp.storeFile(remoteFileName, in)) return UploadStatus.SUCCESS;
}
}
// 續傳失敗:刪除遠端檔案重傳
if (!ftp.deleteFile(remoteFileName)) return UploadStatus.DELETE_FAIL;
}
// 全新上傳
try (InputStream in = new FileInputStream(localFile)) {
return ftp.storeFile(remoteFileName, in) ? UploadStatus.SUCCESS : UploadStatus.UPLOAD_FAIL;
}
}
private boolean createRemoteDirs(String path) throws IOException {
int start = path.startsWith("/") ? 1 : 0;
int end = path.indexOf('/', start);
while (true) {
String sub = path.substring(start, end > 0 ? end : path.length());
if (!ftp.changeWorkingDirectory(sub)) {
if (!ftp.makeDirectory(sub) || !ftp.changeWorkingDirectory(sub)) return false;
}
if (end < 0) break;
start = end + 1;
end = path.indexOf('/', start);
}
return true;
}
public void disconnect() throws IOException {
if (ftp.isConnected()) ftp.disconnect();
}
// 測試主程式略
}
ftp.getReplyString() 觀察指令回應。ftp.setControlKeepAliveTimeout(300) 發送 NOOP 保持連線。ftp.setControlEncoding("UTF-8")(Server 端也需支援)。Apache Commons Net 提供了完整的 FTP 協定支援,配合 setRestartOffset 與隨機存取流,即可實現健壯的斷點續傳。實務上建議封裝重試邏輯、進度回呼與校驗和(MD5/SHA)以確保資料完整性。