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

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM