快速搭建和访问 FTP 服务器

news/2024/11/28 12:55:22/文章来源:https://www.cnblogs.com/studyjobs/p/18571231

随着以 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 目录和相关文件,具体结构如下:

image

注意:由于匿名访问的 FTP 用户需要有文件夹的读写权限,因此这里将 pub 目录的权限设置为 777

chmod -R 777 /data/ftp1/pub

编写 docker-compose.yml 文件内容如下:

version: '3.2'
services:ftp:restart: alwaysimage: fauria/vsftpd:latestcontainer_name: ftpprivileged: trueports:- "20:20"- "21:21"# 被动模式访问的端口- "21100-21110:21100-21110"volumes:# 匿名用户访问的目录# 注意:必须把docker外面映射的目录设置为可读可写的权限)- ./pub:/var/ftp/pub# 想要匿名访问的话,就映射该文件到 docker 容器中- ./vsftpd.conf:/etc/vsftpd/vsftpd.confenvironment:# 设置 FTP 服务器的 ipPASV_ADDRESS: 192.168.136.128# 下面两个环境变量,设置被动访问模式使用的最大端口和最小端口PASV_MIN_PORT: 21100PASV_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 目录中可以任意上传修改下载文件。

image


二、使用账号密码访问的 FTP 搭建

在 /data 目录创建 ftp12目录,内部创建子目录 home 目录和相关文件,具体结构如下:

image

这里的 home 目录,我没有通过 chmod 把它设置为 777 权限。

编写 docker-compose.yml 文件内容如下:

version: '3.2'
services:ftp:restart: alwaysimage: fauria/vsftpd:latestcontainer_name: ftpprivileged: trueports:- "20:20"- "21:21"# 被动模式访问的端口- "21100-21110:21100-21110"volumes:# 普通用户访问的目录- ./home:/home/vsftpdenvironment:# 自定义账号名称FTP_USER: admin# 自定义账号密码FTP_PASS: 123456# 设置 FTP 服务器的 ipPASV_ADDRESS: 192.168.136.128# 下面两个环境变量,设置被动访问模式使用的最大端口和最小端口PASV_MIN_PORT: 21100PASV_MAX_PORT: 21110

最后在 docker-compose.yml 文件所在目录,运行 docker-compose up -d 启动服务即可。

打开我的电脑,访问 ftp://192.168.136.128 即可弹出登录框,输入账号密码后即可,如下图所示:

image


三、使用 SpringBoot 代码访问

新建一个名称为 springboot_ftp 的项目,结构如下所示:

image

首先看一下 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.0http://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.128port: 21# 使用账号密码,访问 FTP 服务器username: adminpassword: 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;@Beanpublic 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 {@Autowiredprivate 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 {@Autowiredprivate FTPService ftpService;//上传文件@Testpublic 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);}//修改文件名@Testpublic 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 服务器文件列表@Testpublic 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);});}//下载文件@Testpublic 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);}//删除文件@Testpublic 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 {@Autowiredprivate FTPService ftpService;//上传文件@Testpublic 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);}//修改文件名@Testpublic 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 服务器文件列表@Testpublic 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);});}//下载文件@Testpublic 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);}//删除文件@Testpublic 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/841902.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Beta阶段——第十周Scrum Meeting记录

1.目前进度: (1)实现沙盒模式,基础逻辑门组件的搭建功能; (2)组件的增加,移动,旋转,删除; (3)逻辑电路的布线及删除; (4)高低电平测试;2.目前团队中存在的问题: (1)前期未能很好的使用Github仓库,导致工作进度难以同步; (2)大多数成员对Unity和C#编程语…

从软件工程的角度,谈模块为什么总是不兼容

前言 今天刚刷上Apatch,发现其没有提供Zygisk,又去酷安搜了一搜,似乎有人反应刷Lsposed不起作用,大致了解了一下,并查了些资料。下面我开始猜测以及进行理论。 说是从软件工程出发,但是实际上我并不算一个好学生,更无法代表软件工程,这或许很标题党,但是我确实想以这个…

uml用例图-2024/11/26

超市进销存管理系统

MySQL报错:sql_mode=only_full_group_by解决方法

MySQL报错:sql_mode=only_full_group_by解决方法 登录mysql之后,执行命令查看当前的sql_mode配置 select @@global.sql_mode;​​ 可以发现MySQL的sql_mode是开启了ONLY_FULL_GROUP_NY。 解决方法 把 sql_mode 中的 ONLY_FULL_GROUP_NY​去掉,其他不变即可。 找到MySQL的配置…

使用Lombok导致打印的tostring中缺少父类的属性

背景 实体类UserDto extends BaseEntity,两个类的上方都有标注,Lombok的@Data注解,但是使用时UserDto的实例对象调用toString方法时发现,只打印出来自身子类的属性信息,并没有打印出来父类的信息。@Data public class UserDto extends BaseEntity { /*** 姓名*/@TableFiel…

20222322 2024-2025-1 《网络与系统攻防技术》实验五实验报告

1.实验内容 1.1实验要求 (1)从www.besti.edu.cn、baidu.com、sina.com.cn中选择一个DNS域名进行查询,获取相关信息。 (2)尝试获取BBS、论坛、QQ、MSN中某一好友的IP地址,并查询获取该好友所在的具体地理位置。 (3)使用nmap开源软件对靶机环境进行扫描,回答以下问题并给…

华为鸿蒙智家品牌升级背后:开拓者,引领者,赋能者

今天,华为重磅推出全新品牌“华为鸿蒙智家”亮相华为Mate品牌盛典。 华为作为产业的开拓者,一直引领产业进化,带动产业从懵懂到成熟。这一次品牌升级将借势鸿蒙,为空间智能产业打开更大的想象空间。持续进化,带来更高阶的智感 作为一个热门赛道,科技巨头和家电企业均积极…

Threejs的三维坐标系

在三维空间中,所有的物体和相机都需要基于一个统一的坐标系来进行定位和操作。理解坐标系的基本概念,对于创建稳定、准确的三维效果至关重要。 基础 Three.js 采用的是右手坐标系,这意味着如果你将右手的三个手指伸直,分别指向 X、Y 和 Z 轴的方向,你的拇指指向的方向即为…

图像尺寸变换scalepadding方法

在深度学习中,当需要将图像调整到特定尺寸时,直接resize可能会导致图像失真,特别是当目标尺寸与原始图像的宽高比不一致时。为了解决这个问题,一种最常见的方法是首先按照原始图像的宽高比将图像调整到与目标尺寸最接近的尺寸,然后在剩余的空间中使用padding进行填充,以得…

笔记本电脑复活记

0.前言 一次偶然的停电让我蒙受了巨大的损失。恢复供电之后发现笔记本电脑无法正常启动,经检查发现硬盘坏了。经过检查确认是硬件坏了,但没有损伤盘片。决定通过数据恢复拯救损失的数据。与此同时,电脑的散热风扇已经响了很久了,决定这一次也更换了它。 经过几天的折腾,笔…

某水准测量工具 vb6 程序注册分析

某水准测量工具 vb6 程序注册分析 目录某水准测量工具 vb6 程序注册分析文件信息VB Decompiler Pro注册点击事件 Command1_ClickRJZC.UncrypStrpypsvb 程序,接触不多,仅作记录文件信息 PE32操作系统: Windows(95)[I386, 32 位, GUI]链接程序: Microsoft Linker(6.0)编译器: M…

WinUI(WASDK)使用BotSharp框架开发多智能体桌面机器人管理助手(生图开关灯不在话下)

前言 大语言模型(Large Language Models, LLMs)近年来在各行各业中展现出了巨大的潜力和影响力。从自然语言处理到自动化客服,从内容生成到智能助手,LLMs正在改变我们与技术互动的方式。随着技术的不断进步,LLMs的应用场景也在不断扩展,成为未来发展的重要趋势。这篇文章…