spring boot实现文件上传到ftp(PASV被动模式)
===============================================
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