隨著以 minio 為代表的分散式系統的廣泛應用,使用 FTP 的場景就越來越少了,目前仍然在一些簡單的應用場景中使用。
本篇部落格使用 fauria/vsftpd 的 docker 映象,介紹 FTP 伺服器搭建的兩種方式:匿名訪問方式 和 使用賬號密碼訪問方式。然後使用 SpringBoot 程式透過程式碼訪問操作搭建好的兩種 FTP 伺服器,在本篇部落格的最後會提供原始碼的下載。
由於國外的 dockerhub 網站無法訪問,這裡推薦另外一個很不錯的網站:https://docker.fxxk.dedyn.io
我的虛擬機器 ip 是 192.168.136.128,已經安裝好了 docker 和 docker-compose
一、使用匿名訪問的 FTP 搭建
在 /data 目錄建立 ftp1 目錄,內部建立子目錄 pub 目錄和相關檔案,具體結構如下:
注意:由於匿名訪問的 FTP 使用者需要有資料夾的讀寫許可權,因此這裡將 pub 目錄的許可權設定為 777
chmod -R 777 /data/ftp1/pub
編寫 docker-compose.yml 檔案內容如下:
version: '3.2'
services:
ftp:
restart: always
image: fauria/vsftpd:latest
container_name: ftp
privileged: true
ports:
- "20:20"
- "21:21"
# 被動模式訪問的埠
- "21100-21110:21100-21110"
volumes:
# 匿名使用者訪問的目錄
# 注意:必須把docker外面對映的目錄設定為可讀可寫的許可權)
- ./pub:/var/ftp/pub
# 想要匿名訪問的話,就對映該檔案到 docker 容器中
- ./vsftpd.conf:/etc/vsftpd/vsftpd.conf
environment:
# 設定 FTP 伺服器的 ip
PASV_ADDRESS: 192.168.136.128
# 下面兩個環境變數,設定被動訪問模式使用的最大埠和最小埠
PASV_MIN_PORT: 21100
PASV_MAX_PORT: 21110
vsftpd.conf 是我從 docker 中複製主要的 FTP 配置檔案,其在 docker 容器中的路徑為:/etc/vsftpd/vsftpd.conf
這裡就不介紹如何複製出來了,為了實現匿名訪問,直接列出修改後的 vsftpd.conf 檔案,內容如下:
# Run in the foreground to keep the container running:
background=NO
# Allow anonymous FTP? (Beware - allowed by default if you comment this out).
# ====================================
# 將此配置修改為 YES ,啟動匿名訪問
anonymous_enable=YES
# 下面這 4 項配置是新增的內容
anon_upload_enable=YES
anon_mkdir_write_enable=YES
anon_other_write_enable=YES
anon_umask=022
# ====================================
# Uncomment this to allow local users to log in.
local_enable=YES
## Enable virtual users
guest_enable=YES
## Virtual users will use the same permissions as anonymous
virtual_use_local_privs=YES
# Uncomment this to enable any form of FTP write command.
write_enable=YES
## PAM file name
pam_service_name=vsftpd_virtual
## Home Directory for virtual users
user_sub_token=$USER
local_root=/home/vsftpd/$USER
# You may specify an explicit list of local users to chroot() to their home
# directory. If chroot_local_user is YES, then this list becomes a list of
# users to NOT chroot().
chroot_local_user=YES
# Workaround chroot check.
# See https://www.benscobie.com/fixing-500-oops-vsftpd-refusing-to-run-with-writable-root-inside-chroot/
# and http://serverfault.com/questions/362619/why-is-the-chroot-local-user-of-vsftpd-insecure
allow_writeable_chroot=YES
## Hide ids from user
hide_ids=YES
## Enable logging
xferlog_enable=YES
xferlog_file=/var/log/vsftpd/vsftpd.log
## Enable active mode
port_enable=YES
connect_from_port_20=YES
ftp_data_port=20
## Disable seccomp filter sanboxing
seccomp_sandbox=NO
### Variables set at container runtime
pasv_address=192.168.136.128
pasv_max_port=21110
pasv_min_port=21100
pasv_addr_resolve=NO
pasv_enable=YES
file_open_mode=0666
local_umask=077
xferlog_std_format=NO
reverse_lookup_enable=YES
pasv_promiscuous=NO
port_promiscuous=NO
注意:該配置檔案,末尾需要留一個空行。
因為我在實際測試中發現:每次重啟搭建好的 FTP 服務,vsftpd.conf 檔案末尾都會新增好多行重複的配置。
如果 vsftpd.conf 配置檔案末尾不留一個空行的話,vsftpd.conf 被新增好多行重複的配置後,配置檔案就亂了,會導致服務無法使用。
在以上配置檔案中,我修改的內容如下:
# ====================================
# 將此配置修改為 YES ,啟動匿名訪問
anonymous_enable=YES
# 下面這 4 項配置是新增的內容
anon_upload_enable=YES
anon_mkdir_write_enable=YES
anon_other_write_enable=YES
anon_umask=022
# ====================================
最後在 docker-compose.yml 檔案所在目錄,執行 docker-compose up -d
啟動服務即可。
開啟我的電腦,訪問 ftp://192.168.136.128
即可匿名訪問,如下圖所示,在 pub 目錄中可以任意上傳修改下載檔案。
二、使用賬號密碼訪問的 FTP 搭建
在 /data 目錄建立 ftp12目錄,內部建立子目錄 home 目錄和相關檔案,具體結構如下:
這裡的 home 目錄,我沒有透過 chmod 把它設定為 777 許可權。
編寫 docker-compose.yml 檔案內容如下:
version: '3.2'
services:
ftp:
restart: always
image: fauria/vsftpd:latest
container_name: ftp
privileged: true
ports:
- "20:20"
- "21:21"
# 被動模式訪問的埠
- "21100-21110:21100-21110"
volumes:
# 普通使用者訪問的目錄
- ./home:/home/vsftpd
environment:
# 自定義賬號名稱
FTP_USER: admin
# 自定義賬號密碼
FTP_PASS: 123456
# 設定 FTP 伺服器的 ip
PASV_ADDRESS: 192.168.136.128
# 下面兩個環境變數,設定被動訪問模式使用的最大埠和最小埠
PASV_MIN_PORT: 21100
PASV_MAX_PORT: 21110
最後在 docker-compose.yml 檔案所在目錄,執行 docker-compose up -d
啟動服務即可。
開啟我的電腦,訪問 ftp://192.168.136.128
即可彈出登入框,輸入賬號密碼後即可,如下圖所示:
三、使用 SpringBoot 程式碼訪問
新建一個名稱為 springboot_ftp 的專案,結構如下所示:
首先看一下 pom 檔案引入的依賴包(最主要是引入了 commons-net 包)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.jobs</groupId>
<artifactId>springboot_ftp</artifactId>
<version>1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.5</version>
</parent>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<scope>compile</scope>
</dependency>
<!--引入 commons-net 包來處理 ftp 相關操作-->
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>3.11.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>
</project>
在 application.yml 中自定義了訪問 FTP 服務的相關引數,如下所示:
ftp:
ip: 192.168.136.128
port: 21
# 使用賬號密碼,訪問 FTP 伺服器
username: admin
password: 123456
# 使用匿名方式,訪問 FTP 伺服器,配置的使用者名稱只能是 ftp,密碼可以不用配置
#username: ftp
#password:
如果搭建的是匿名訪問的 FTP 服務,那麼 username 填寫 ftp 即可,密碼不需要填寫,填寫了也沒啥影響。
我們在 FTPConfig 類中讀取 application.yml 中配置的引數,初始化 FTPClient 物件,並將其新增到 Spring 容器中。
package com.jobs.config;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPReply;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Slf4j
@Configuration
public class FTPConfig {
@Value("${ftp.ip}")
private String ip;
@Value("${ftp.port}")
private Integer port;
@Value("${ftp.username}")
private String username;
@Value("${ftp.password}")
private String password;
@Bean
public FTPClient getFTPClient() {
FTPClient ftpClient = new FTPClient();
// 設定連線超時時間
ftpClient.setConnectTimeout(30 * 1000);
// 設定ftp字符集
ftpClient.setControlEncoding("utf-8");
// 設定被動模式,檔案傳輸埠設定
ftpClient.enterLocalPassiveMode();
try {
int replyCode;
ftpClient.connect(ip, port);
ftpClient.login(username, password);
replyCode = ftpClient.getReplyCode();
if (!FTPReply.isPositiveCompletion(replyCode)) {
log.error("FTP伺服器 " + ip + " 連線失敗,返回狀態碼為:" + replyCode);
return null;
}
} catch (Exception ex) {
log.error("FTP伺服器 " + ip + " 連線失敗:" + ex.getMessage());
return null;
}
return ftpClient;
}
}
編寫一個 FTPService 類,使用 FTPClient 提供對 FTP 服務的檔案上傳、下載、改名、刪除、檢視目錄下檔案列表等操作:
package com.jobs.service;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.junit.platform.commons.util.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@Slf4j
@Service
public class FTPService {
@Autowired
private FTPClient ftpClient;
/**
* 上傳檔案
*
* @param localFileName 本地上傳的檔案全路徑
* @param ftpPath FTP 伺服器目錄全路徑
* @param ftpFileName Ftp檔名稱
* @return 是否成功
*/
public Boolean uploadFile(String localFileName, String ftpPath, String ftpFileName) {
boolean result = false;
if (ftpClient != null) {
try {
//設定檔案傳輸模式為二進位制
ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
ftpClient.enterLocalPassiveMode(); //採用被動模式
if (StringUtils.isNotBlank(ftpPath)) {
boolean flag = ftpClient.changeWorkingDirectory(ftpPath);
if (flag == false) {
ftpClient.makeDirectory(ftpPath);
}
ftpClient.changeWorkingDirectory(ftpPath);
} else {
ftpClient.changeWorkingDirectory("/");
}
try (FileInputStream fis = new FileInputStream(localFileName)) {
//上傳檔案
result = ftpClient.storeFile(ftpFileName, fis);
}
} catch (Exception ex) {
log.error("FTP檔案上傳失敗:" + ex.getMessage());
}
}
return result;
}
/**
* 修改 ftp 伺服器上的一個檔名稱
*
* @param ftpPath 檔案所在目錄全路徑
* @param oldName 舊檔名
* @param newName 新檔名
* @return 是否成功
*/
public Boolean renameFile(String ftpPath, String oldName, String newName) {
boolean result = false;
if (ftpClient != null) {
try {
if (StringUtils.isNotBlank(ftpPath)) {
ftpClient.changeWorkingDirectory(ftpPath);
} else {
ftpClient.changeWorkingDirectory("/");
}
result = ftpClient.rename(oldName, newName);
} catch (Exception ex) {
log.error("FTP修改檔名稱失敗:" + ex.getMessage());
}
}
return result;
}
/**
* 下載檔案
*
* @param ftpFilePath ftp檔案全路徑名稱
* @param localFilePath 本地檔案全路徑名稱
* @return 是否成功
*/
public Boolean downloadFile(String ftpFilePath, String localFilePath) {
boolean result = false;
if (ftpClient != null) {
try {
ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE);
ftpClient.enterLocalPassiveMode();
//獲取 FTP 檔案輸入流
InputStream inputStream = ftpClient.retrieveFileStream(ftpFilePath);
//獲取本地檔案輸出流
FileOutputStream outputStream = new FileOutputStream(localFilePath);
//採用高階流進行位元組複製
try (BufferedInputStream bis = new BufferedInputStream(inputStream);
BufferedOutputStream bos = new BufferedOutputStream(outputStream)) {
byte[] bArr = new byte[1024];
int len;
while ((len = bis.read(bArr)) != -1) {
bos.write(bArr, 0, len);
}
}
//下載完檔案後,必須呼叫該方法,告訴 FTP 伺服器已經完成檔案下載,否則後續 FTPClient 將無法執行。
ftpClient.completePendingCommand();
result = true;
} catch (Exception ex) {
log.error("FTP檔案下載失敗:" + ex.getMessage());
}
}
return result;
}
/**
* 刪除 FTP 伺服器上的檔案
*
* @param ftpFilePath ftp檔案全路徑名稱
* @return 是否成功
*/
public Boolean deleteFile(String ftpFilePath, boolean isDirectory) {
boolean result = false;
if (ftpClient != null) {
try {
if (isDirectory) {
result = ftpClient.removeDirectory(ftpFilePath);
} else {
result = ftpClient.deleteFile(ftpFilePath);
}
} catch (Exception ex) {
log.error("FTP檔案刪除失敗:" + ex.getMessage());
}
}
return result;
}
/**
* 獲取目錄下的檔案列表
*
* @param directory 全路徑資料夾
* @return 檔案列表
*/
public List<String> listFiles(String directory) {
List<String> fileNames = new ArrayList<>();
try {
ftpClient.changeWorkingDirectory(directory);
String[] names = ftpClient.listNames();
fileNames = Arrays.asList(names);
} catch (Exception ex) {
log.error("FTP獲取檔案列表失敗:" + ex.getMessage());
}
return fileNames;
}
}
最後寫了 2 個測試類,如果你搭建的是匿名訪問的 FTP 服務,使用 AnonymousFTPTest 類中的方法進行測試。
需要注意的是:匿名訪問的 FTP 服務,需要在根目錄下的 pub 目錄中對檔案進行操作。
package com.jobs;
import com.jobs.service.FTPService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
/**
* 使用匿名使用者訪問 FTP 伺服器
* application.yml 配置的使用者名稱為 ftp,密碼可以不用配置
* 匿名使用者的根目錄是 /pub ,必須在 /pub 裡面上傳檔案或建立資料夾等各種操作。
*/
@SpringBootTest
public class AnonymousFTPTest {
@Autowired
private FTPService ftpService;
//上傳檔案
@Test
public void test1() {
//在 FTP 伺服器上,建立一個名稱為【測試】的資料夾
//把本地的檔案,上傳到 FTP 伺服器上的【測試】資料夾中,檔名稱命名為【aaa.txt】
boolean result1 = ftpService.uploadFile("d:/我的測試.txt", "/pub/測試", "aaa.txt");
System.out.println(result1);
boolean result2 = ftpService.uploadFile("d:/我的測試.txt", "/pub", "bbb.txt");
System.out.println(result2);
}
//修改檔名
@Test
public void test2() {
boolean result1 = ftpService.renameFile("/pub/測試", "aaa.txt", "ccc.txt");
System.out.println(result1);
boolean result2 = ftpService.renameFile("/pub", "bbb.txt", "ddd.txt");
System.out.println(result2);
}
//檢視 Ftp 伺服器檔案列表
@Test
public void test3() {
//檢視根目錄下的檔案列表
//建議在 ftp 伺服器上,對檔案的命名都加上字尾名,對資料夾的命名不要加上字尾名,這樣比較容易區分檔案和資料夾。
List<String> list1 = ftpService.listFiles("/pub");
list1.forEach(f -> {
System.out.println(f);
});
System.out.println("==================================");
//檢視【測試】資料夾下面的檔案列表
List<String> list2 = ftpService.listFiles("/pub/測試");
list2.forEach(f -> {
System.out.println(f);
});
}
//下載檔案
@Test
public void test4() {
boolean result1 = ftpService.downloadFile("/pub/測試/ccc.txt", "d:/ccc.txt");
System.out.println(result1);
boolean result2 = ftpService.downloadFile("/pub/ddd.txt", "d:/ddd.txt");
System.out.println(result2);
}
//刪除檔案
@Test
public void test5() {
//刪除資料夾(這裡不會刪除成功,FTP 伺服器不允許刪除包含檔案的資料夾)
boolean result1 = ftpService.deleteFile("/pub/測試", true);
System.out.println(result1);
//先刪除資料夾中的檔案,然後再刪除資料夾
List<String> list2 = ftpService.listFiles("/pub/測試");
for (String f : list2) {
ftpService.deleteFile("/pub/測試/" + f, false);
}
boolean result2 = ftpService.deleteFile("/pub/測試", true);
System.out.println(result2);
//刪除檔案
boolean result3 = ftpService.deleteFile("/pub/ddd.txt", false);
System.out.println(result3);
}
}
如果你搭建是需要賬號密碼訪問的 FTP 服務,使用 FTPTest 類中的方法進行測試。
package com.jobs;
import com.jobs.service.FTPService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
/**
* 使用使用者名稱和密碼,訪問 FTP 伺服器
*/
@SpringBootTest
public class FTPTest {
@Autowired
private FTPService ftpService;
//上傳檔案
@Test
public void test1() {
//在 FTP 伺服器上,建立一個名稱為【測試】的資料夾
//把本地的檔案,上傳到 FTP 伺服器上的【測試】資料夾中,檔名稱命名為【aaa.txt】
boolean result1 = ftpService.uploadFile("d:/我的測試.txt", "/測試", "aaa.txt");
System.out.println(result1);
boolean result2 = ftpService.uploadFile("d:/我的測試.txt", "/", "bbb.txt");
System.out.println(result2);
}
//修改檔名
@Test
public void test2() {
boolean result1 = ftpService.renameFile("/測試", "aaa.txt", "ccc.txt");
System.out.println(result1);
boolean result2 = ftpService.renameFile("/", "bbb.txt", "ddd.txt");
System.out.println(result2);
}
//檢視 Ftp 伺服器檔案列表
@Test
public void test3() {
//檢視根目錄下的檔案列表
//建議在 ftp 伺服器上,對檔案的命名都加上字尾名,對資料夾的命名不要加上字尾名,這樣比較容易區分檔案和資料夾。
List<String> list1 = ftpService.listFiles("/");
list1.forEach(f -> {
System.out.println(f);
});
System.out.println("==================================");
//檢視【測試】資料夾下面的檔案列表
List<String> list2 = ftpService.listFiles("/測試");
list2.forEach(f -> {
System.out.println(f);
});
}
//下載檔案
@Test
public void test4() {
boolean result1 = ftpService.downloadFile("/測試/ccc.txt", "d:/ccc.txt");
System.out.println(result1);
boolean result2 = ftpService.downloadFile("/ddd.txt", "d:/ddd.txt");
System.out.println(result2);
}
//刪除檔案
@Test
public void test5() {
//刪除資料夾(這裡不會刪除成功,FTP 伺服器不允許刪除包含檔案的資料夾)
boolean result1 = ftpService.deleteFile("/測試", true);
System.out.println(result1);
//先刪除資料夾中的檔案,然後再刪除資料夾
List<String> list2 = ftpService.listFiles("/測試");
for (String f : list2) {
ftpService.deleteFile("/測試/" + f, false);
}
boolean result2 = ftpService.deleteFile("/測試", true);
System.out.println(result2);
//刪除檔案
boolean result3 = ftpService.deleteFile("/ddd.txt", false);
System.out.println(result3);
}
}
以上程式碼都經過實際測試無誤,具體細節可以下載原始碼進行驗證。
本篇部落格的原始碼下載地址為:https://files.cnblogs.com/files/blogs/699532/springboot_ftp.zip