| 模式 | 命令連接 | 數據連接 | 防火牆友好度 |
|---|---|---|---|
| 主動模式 | 客戶端 → 伺服器 21 | 伺服器 20 → 客戶端隨機端口 | 差(需客戶端開放入站) |
| 被動模式 | 客戶端 → 伺服器 21 | 客戶端 → 伺服器隨機端口 | 好(僅需伺服器開放範圍) |
生產環境強制使用被動模式,並配置伺服器 `pasv_address`(公網 IP)與 `pasv_min_port`/`pasv_max_port` 範圍。
SIZE 指令)REST offset 指令告知伺服器從偏移處寫入RandomAccessFile.seek(offset) 定位讀取
public class ResumableFtpClient {
private FTPClient ftpClient;
public void uploadWithResume(File localFile, String remotePath) throws IOException {
// 1. 連結並進入被動模式
ftpClient.connect(server, port);
ftpClient.login(user, pass);
ftpClient.enterLocalPassiveMode();
ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
// 2. 檢查遠端檔案大小
long remoteSize = 0;
if (ftpClient.getFileSize(remotePath) > 0) {
remoteSize = ftpClient.getFileSize(remotePath);
}
long localSize = localFile.length();
if (remoteSize >= localSize) {
log.info("檔案已完整或本地較舊,跳過: {}", remotePath);
return;
}
// 3. 斷點續傳:設置偏移量
try (RandomAccessFile raf = new RandomAccessFile(localFile, "r")) {
raf.seek(remoteSize); // 定位到未上傳處
// 發送 REST 指令(Apache Commons Net 支援)
ftpClient.setRestartOffset(remoteSize);
// 4. 流式上傳
try (InputStream is = new BufferedInputStream(new FileInputStream(raf.getFD()))) {
boolean success = ftpClient.storeFile(remotePath, is);
if (!success) throw new IOException("上傳失敗: " + ftpClient.getReplyString());
}
}
}
public void downloadWithResume(String remotePath, File localFile) throws IOException {
long localSize = localFile.exists() ? localFile.length() : 0;
long remoteSize = ftpClient.getFileSize(remotePath);
if (localSize >= remoteSize) return; // 已完整
try (RandomAccessFile raf = new RandomAccessFile(localFile, "rw")) {
raf.seek(localSize);
ftpClient.setRestartOffset(localSize);
try (OutputStream os = new BufferedOutputStream(new FileOutputStream(raf.getFD()))) {
boolean success = ftpClient.retrieveFile(remotePath, os);
if (!success) throw new IOException("下載失敗: " + ftpClient.getReplyString());
}
}
}
}
FTP 伺服器部署於內網(192.168.x.x),客戶端從公網訪問。需在邊界防火牆/路由器配置:
pasv_address=公網IP、pasv_min_port=50000、pasv_max_port=50100否則客戶端收到內網 IP 發起數據連接失敗。
JSObject.getWindow(this)
/* java.policy 關鍵權限 */
grant {
permission java.io.FilePermission "<>", "read,write,delete";
permission java.net.SocketPermission "*", "connect,accept,resolve";
permission java.lang.RuntimePermission "accessClassInPackage.sun.net.www.protocol.ftp";
permission java.security.AllPermission; // 簽名 Applet 通常需全權限
};
部署時將 JRE + 簽名 JAR + policy 打包為安裝包(如 Install4j、IzPack),簡化用戶安裝。
| 方案 | 原理 | 優勢 | 適用場景 |
|---|---|---|---|
| 預簽名 URL | 後端生成帶簽名的 S3/OSS 上傳 URL,前端直傳 | 無伺服器壓力、海量併發、支持斷點續傳(分片上傳) | 圖片、視頻、文檔上傳 |
| WebUploader / Uppy | 前端分片、並發、斷點續傳、進度條 | 純前端、無插件、用戶體驗佳 | 通用文件上傳 |
| WebRTC DataChannel | P2P 傳輸,伺服器僅信令 | 極低延遲、去中心化 | 大文件點對點、內網穿透 |
| Tus.io 協議 | 開放斷點續傳標準,支援多語言客戶端 | 標準化、可斷點、可暫停 | 企業級文件上傳服務 |
@RestController
public class UploadController {
@Autowired private AmazonS3 s3Client;
@PostMapping("/presign")
public Map<String, String> presign(@RequestParam String fileName,
@RequestParam String contentType) {
String key = "uploads/" + UUID.randomUUID() + "_" + fileName;
GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(bucket, key)
.withMethod(HttpMethod.PUT)
.withExpiration(Date.from(Instant.now().plusMinutes(15)))
.withContentType(contentType);
URL url = s3Client.generatePresignedUrl(request);
return Map.of("uploadUrl", url.toString(), "key", key, "method", "PUT");
}
@PostMapping("/complete")
public void complete(@RequestParam String key) {
// 可選:驗證文件完整性、觸發後續處理(轉碼、索引等)
}
}