• spring boot实现文件上传到ftp(PASV被动模式)


    ===============================================

     2022/3/13_第1次修改                       ccb_warlock

     

    ===============================================

    今年1月临时接手了一个spring boot项目的开发,其中包含了文件上传和获取的功能。但是发现原功能通过直接压缩文件成字符串然后存入数据库来实现,于是我准备改写存入FTP来优化。但是发现查了很多文章,几乎找不到一篇代码结构清晰且能跑的起来的代码片段,于是我整理了这篇记录供需要的人参考。

    这里我只实现文件的上传功能,文件的下载因为是内网项目,所以我还是采取了nginx代理FTP的的方式直接通过url来访问。
     

    一、部署ftp

    docker部署ftp参考:https://www.cnblogs.com/straycats/p/16002473.html


    二、pom引用

    这里我使用的是java中普遍操作ftp的轮子org.apache.commons.net。
    <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

     

     

  • 相关阅读:
    textarea宽度、高度自动适应处理方法
    Table嵌套去掉子table的外边框
    发现原来自己挺能给自己找理由开脱的
    Life is not the amount of breath you take.
    在遍历ResultSet的循环中再执行SQL会发生什么(前提:同一个Statement)
    按月查询数据
    Oracle SQL 判断某表是否存在
    在Python程序中执行linux命令
    在Oracle中十分钟内创建一张千万级别的表
    Redis Sentinel结构 及相关文档
  • 原文地址:https://www.cnblogs.com/straycats/p/16002572.html
Copyright © 2020-2023  润新知