===============================================
2022/3/13_第1次修改 ccb_warlock
===============================================
<dependency> <groupId>commons-net</groupId> <artifactId>commons-net</artifactId> <version>3.8.0</version> </dependency>
三、application.yaml增加配置信息
將FTP的配置記錄到配置文件中。
ftp: # ftp服務的地址 host: 127.0.0.1 # 連接端口 port: 38021 # 用戶名 username: myftp # 密碼 password: 123456 # 模式(PORT.主動模式,PASV.被動模式) mode: PASV # http訪問的路徑前綴 url: http://127.0.0.1:8001/ftp
四、工具類封裝
為了方便后續調用,我抽象了ftp操作的方法集成到了一個獨立的工具類(FTPUtil)。
1 package com.example.demo.utils; 2 3 import lombok.extern.slf4j.Slf4j; 4 import org.apache.commons.lang3.StringUtils; 5 import org.apache.commons.net.ftp.FTP; 6 import org.apache.commons.net.ftp.FTPClient; 7 import org.apache.commons.net.ftp.FTPReply; 8 import org.springframework.beans.factory.annotation.Value; 9 import org.springframework.stereotype.Component; 10 import org.springframework.web.multipart.MultipartFile; 11 12 import java.io.IOException; 13 import java.io.InputStream; 14 15 @Slf4j 16 @Component 17 public class FTPUtil { 18 private static String host; 19 private static int port; 20 private static String userName; 21 private static String password; 22 private static String mode; 23 24 @Value("${ftp.host:127.0.0.1}") 25 private void setHost(String host) { 26 FTPUtil.host = host; 27 } 28 29 @Value("${ftp.port:21}") 30 private void setPort(int port){ 31 FTPUtil.port = port; 32 } 33 34 @Value("${ftp.username:''}") 35 private void setUserName(String userName){ 36 FTPUtil.userName = userName; 37 } 38 39 @Value("${ftp.password:''}") 40 private void setPassword(String password){ 41 FTPUtil.password = password; 42 } 43 44 @Value("${ftp.mode:PASV}") 45 private void setMode(String mode){ 46 FTPUtil.mode = mode; 47 } 48 49 private static FTPClient getInstance(String workingDirectory) { 50 FTPClient ftpClient = new FTPClient(); 51 ftpClient.setControlEncoding("UTF-8"); 52 53 try{ 54 ftpClient.connect(host, port); 55 ftpClient.login(userName, password); 56 57 int replyCode = ftpClient.getReplyCode(); 58 59 if(!FTPReply.isPositiveCompletion(replyCode)){ 60 log.error("FTP服務({}:{})連接失敗。", host, port); 61 throw new Exception("FTP服務連接失敗"); 62 } 63 log.info("FTP服務({}:{})連接成功。", host, port); 64 65 if("PORT".equals(mode)){ 66 ftpClient.enterLocalActiveMode(); 67 } 68 else{ 69 ftpClient.enterLocalPassiveMode(); 70 } 71 72 ftpClient.setFileType(FTP.BINARY_FILE_TYPE); 73 changeWorkingDirectory(ftpClient, workingDirectory); 74 } 75 catch(Exception e){ 76 e.printStackTrace(); 77 } 78 79 return ftpClient; 80 } 81 82 private static void changeWorkingDirectory(FTPClient ftpClient, String workingDirectory) throws IOException { 83 String[] directories = workingDirectory.split("/"); 84 85 for(String directory : directories){ 86 if(StringUtils.isBlank(directory)){ 87 continue; 88 } 89 90 if(ftpClient.changeWorkingDirectory(directory)){ 91 continue; 92 } 93 94 ftpClient.makeDirectory(directory); 95 ftpClient.changeWorkingDirectory(directory); 96 } 97 } 98 99 private static void close(FTPClient client){ 100 if(null == client){ 101 return; 102 } 103 104 try{ 105 client.logout(); 106 } 107 catch(Exception e){ 108 log.error("FTP退出登錄失敗。異常信息:{}", e.getMessage()); 109 } 110 finally { 111 if(client.isConnected()){ 112 try{ 113 client.disconnect(); 114 log.info("FTP斷開連接成功。"); 115 } 116 catch(Exception e){ 117 log.error("FTP斷開連接失敗。異常信息:{}", e.getMessage()); 118 } 119 } 120 } 121 } 122 123 public static void upload(String workingDirectory, String fileName, MultipartFile file) throws Exception { 124 InputStream inputStream = file.getInputStream(); 125 FTPClient client = getInstance(workingDirectory); 126 127 if (client.storeFile(fileName, inputStream)) { 128 log.info("上傳文件{}成功。", fileName); 129 } 130 else{ 131 log.error("上傳文件{}失敗({})。", fileName, client.getReplyString()); 132 } 133 134 close(client); 135 inputStream.close(); 136 } 137 138 }
五、調用
為了方便呈現,這里設計了一個post接口方便測試
1)服務(IFileService、FileServiceImpl)
IFileService
1 package com.example.demo.api.interfaces; 2 3 import org.springframework.web.multipart.MultipartFile; 4 5 public interface IFileService { 6 7 void uploadFile(long companyId, MultipartFile file) throws Exception; 8 9 }
FileServiceImpl
package com.example.demo.domain.service; import org.apache.commons.lang3.StringUtils; import com.example.demo.api.interfaces.IFileService; import com.example.demo.utils.FTPUtil; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.InputStream; import java.util.Calendar; import java.util.List; import java.util.UUID; @Service public class FileServiceImpl implements IFileService { @Value("${ftp:url:''}") private String ftpUrl; @Override public String uploadFile(long companyId, MultipartFile file) throws Exception { String fileName = getUuidFileName(file.getOriginalFilename()); //無多級目錄 FTPUtil.upload("", fileName, file); return ftpUrl + "/" + fileName; //存到company/{companyId}/images路徑下 //FTPUtil.upload("company/" + companyId + "/images", fileName, file); //return ftpUrl + "/company/" + companyId + "/images/" + fileName; } private String getUuidFileName(String originalFileName){ String uuid = getUuid(); if(StringUtils.isBlank(originalFileName)){ return uuid; } int i = originalFileName.lastIndexOf('.'); return -1 == i ? uuid : uuid + originalFileName.substring(i); } private String getUuid(){ String uuid = UUID.randomUUID().toString(); return uuid.replace("-", ""); } }
2)控制器(FileController)
PS. 這里的ApiResult是demo中封裝的接口輸出格式類
1 package com.example.demo.api.controller; 2 3 import com.example.demo.api.interfaces.IFileService; 4 import com.example.demo.common.base.BaseController; 5 import com.example.demo.entity.vo.ApiResult; 6 import io.swagger.annotations.Api; 7 import io.swagger.v3.oas.annotations.Operation; 8 import org.springframework.web.bind.annotation.*; 9 import org.springframework.web.multipart.MultipartFile; 10 11 import javax.annotation.Resource; 12 13 @Api(tags = "文件") 14 @RestController 15 @RequestMapping("file") 16 public class FileController { 17 18 @Resource 19 private IFileService fileService; 20 21 @Operation(summary = "上傳文件") 22 @PostMapping(path = "/images/{companyId}") 23 public ApiResult uploadFile(@PathVariable long companyId, @RequestParam("file") MultipartFile file) 24 throws Exception { 25 String url = fileService.uploadFile(companyId, file); 26 return ApiResult.success(url); 27 } 28 29 }
六、測試
接着我們用postman測試post接口,其中companyId隨便賦值一個數。
當FileServiceImpl使用“無多級目錄”的代碼時,文件將會保存在“FTP物理路徑/用戶名”的目錄下(如果ftp完全根據我提供的資料部署,則文件保存到/Users/mbp/docker/vol/vsftpd/data/myftp)。
當FileServiceImpl使用“存到company/{companyId}/images路徑下”,文件將會保存在“FTP物理路徑/用戶名/company/{companyId}/images”的目錄下如果ftp完全根據我提供的資料部署,則文件保存到/Users/mbp/docker/vol/vsftpd/data/myftp/company/{companyId}/images)。
七、我遇到的問題
1)500 Illegal PORT command.
答:
因為我部署的ftp是被動模式,所以ftp獲取客戶端實例時需要設置模式(詳見“四、工具類封裝”的65 - 70行代碼)。
2)Connection closed without indication.
答:
這是我在mac上通過docker部署時,如果容器的端口20映射筆記本的端口20、容器端口21映射筆記本的端口21,則會引起該報錯(如果有大佬願意指點,請在評論中留言)。我采取的解決方案是換筆記本的端口綁(38021、38022)。
3)上傳的文件損壞
答:
在初始化客戶端實例時需要設置其文件類型為二進制(詳見“四、工具類封裝”的第72行代碼)。
PS. 很多文章的代碼都沒注意這個問題,文件看着是上傳到ftp目錄了,但實際該文件損壞。
4)ftp多級目錄沒有自動生成
答:
客戶端實例的changeWorkingDirectory方法無法處理多級目錄,所以設計循環遍歷路徑,有需要則創建(詳見“四、工具類封裝”的82 - 97行代碼)。
參考資料:
1.https://www.cnblogs.com/wanisily/p/7699873.html
2.https://www.cnblogs.com/mickole/articles/3643819.html