快速搭建和访问 FTP 服务器
随着以 minio 为代表的分布式系统的广泛应用,使用 FTP 的场景就越来越少了,目前仍然在一些简单的应用场景中使用。
本篇博客使用 fauria/vsftpd 的 docker 镜像,介绍 FTP 服务器搭建的两种方式:匿名访问方式 和 使用账号密码访问方式。然后使用 SpringBoot 程序通过代码访问操作搭建好的两种 FTP 服务器,在本篇博客的最后会提供源代码的下载。
由于国外的 dockerhub 网站无法访问,这里推荐另外一个很不错的网站:https://cloud.tencent.com/developer/article/2471124
我的虚拟机 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
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!