ftp服务器的搭建,以及Springboot整合ftp服务实现文件上传

一、ftp服务的搭建与简单使用

1、windows下搭建

步骤一: 安装FTP服务器支持和IIS管理平台。

操作步骤: 电脑 => 控制面板 => 程序和功能 => 启用和关闭Windows功能 => Internet Infomation Services => 勾选【FTP服务器】和Web管理工具的【IIS管理控制台】=> 点击确定等待安装完成
在这里插入图片描述

步骤二: 打开IIS管理器

操作步骤: 电脑 => 控制面板 => 管理工具 => Internet Infomation Services(IIS)管理器
在这里插入图片描述

步骤三: 创建FTP服务器

操作1:
在某个盘符如D盘,创建一个FTP共享文件夹,用于FTP共享文件存放地址
操作2:
右键IIS管理器左边导航栏 => 添加FTP站点

在这里插入图片描述

操作3:
指定【站点名称】和【FTP共享的文件夹路径】

在这里插入图片描述

操作4:
配置FTP服务器相关信息

在这里插入图片描述

操作5:
配置FTP服务器验证和权限信息 
【注意:如果想通过程序实现上传、下载功能,身份验证中的基本选项需要勾选上,后面程序需要通过这个方式使用账号和密码登录到FTP服务器】

在这里插入图片描述

操作6:
在同一网段的小伙伴可以通过:ftp://ftp配置的ip地址 格式访问到FTP服务器。

在这里插入图片描述

其他问题

问题: FTP按照流程搭建完成后,在同一网段的小伙伴却无法访问!
原因: 可能是开启了防火墙拦截,需要在防火墙放行FTP服务器。
解决: 电脑 => 控制面板 => Windows Defender 防火墙 => 允许应用通过Windows Defender 防火墙进行通信 => 勾选【FTP服务器】

在这里插入图片描述

2、linux下搭建(vsftpd)

其实在linux下的搭建很简单,分为三步,下载、安装、配置。
其中vsftpd的配置项比较多,所以本文重点会在vsftpd的配置里

2.1、安装启动

 # 1. 安装
 yum install vsftpd -y  # 如果没有外网,那么可以下载离线包然后安装
 
 # 2. 启动
 systemctl start vsftpd.service
 
 # 3.查看进程是否启动
 ps -ef | grep vsftpd
 
 # 4.查看端口号
 netstat -anplut | grep vsftpd
 
 # 5.修改配置文件
 vim /etc/vsftpd/vsftpd.conf

2.2、 配置

(1)配置文件说明

选项说明
anonymous_enable设置是否允许匿名用户登录服务器
local_enable设置是否允许本地用户登录服务器
write_enable设置是否允许写操作
local_umask设置本地用户创建文件的umask值 (umask 是一个权限掩码,用于控制新创建文件或目录的默认权限。)
local_umaskumask 077:表示文件权限为 600(即 rw-------),目录权限为 700(即 rwx------)。这意味着只有文件或目录的所有者可以读写或执行,其他用户没有任何权限。
local_umaskumask 022:表示文件权限为 644(即 rw-r–r–),目录权限为 755(即 rwxr-xr-x)。这意味着所有者有读写权限,其他用户只有读权限。
anon*_upload_enable*设置是否允许匿名用户上传文件
anon*_mkdir_write_enable*设置是否允许匿名用户建立目录
xferlog_enable是否激活日志功能
chown_uploads修改匿名用户上传文件的所有者
chown_username=whoever启用chown_uploads=YES时,指定为主用户账户,whoever表示你的ftp用户
chroot*_local_user*设置是否将所有用户限制在其主目录
chroot*_list_enable*设置是否启用限制用户的名单
chroot*_list_file*设置是否限制/排除主目录下的用户名单,限制/排除有chrootlocaluser值决定
allow*_writeable_choot*设置chroot目录的写权限

2.3 登录

vsftpd允许用户以3种认证模式登录到FTP服务器上。

  • 匿名用户:任何人都可以直接登录服务器
  • 本地用户:通过本地用户输入密码登录服务器( 在生产环境中,我们通常使用本地用户登录这种方式
  • 虚拟用户:本身不存在、是一个虚拟出来的用户、就算黑客破解的用户信息也无法登录服务器
2.3.1 匿名登录

匿名用户登录的参数说明

参数作用
anonymous_enable=YES允许匿名用户访问
anon_umask=022匿名用户上传文件的umask值
anon_upload_enable=YES允许匿名用户上传文件
anon_mkdir_write_enable=YES允许匿名用户创建文件
anon_other_write_enable=YES允许匿名用户修改目录名称或删除目录

(a) 修改配置文件:

vi /etc/vsftpd/vsftpd.conf

修改内容如下,前面是行号

12 anonymous_enable=YES
29 anon_upload_enable=YES  # 允许上传文件 如果前面有# 删除最前面的 “#”
33 anon_mkdir_write_enable=YES  # 允许创建文件

(b) 重启FTP服务

systemctl restart vsftpd

可能出现的问题
1 、连接ftp服务后,创建文件夹或者文件失败,可能是文件夹权限不够导致,使用如下命令更改权限

chmod 777 /var/ftp/pub  #给目录所属设置为ftp

2 、查看 SELinux 的状态

sestatus

在这里插入图片描述

如果是启用的Enforcing状态,则需要调整相关策略

setsebool -P ftpd_full_access on
setsebool -P ftpd_use_passive_mode on
2.3.2 本地用户登录

(a) 首先在linux上创建ftp用户

# 第一步、创建ftp用户的所属目录
mkdir /usr/local/ftp
# 第二步、创建用户
useradd ftpUser -d  /usr/local/ftp
# 第三步、给用户设置密码
echo "123" | passwd --stdin ftpUser

(b) 修改配置文件

vim /etc/vsftpd/vsftpd.conf

# 不允许匿名用户访问
12 anonymous_enable=NO 

# 对应(a)中第一步,“创建ftp用户的所属目录”
13 local_root=/web/www/html  # 添加
102 chroot_list_enable=YES  # 取消注释  激活chroot功能
104 chroot_list_file=/etc/vsftpd/chroot_list # 取消注释 锁定用户在根目录中的列表文件

# 启用chroot,允许chroot限制
105 allow_writeable_chroot=YES # 添加
106 write_enable=yes # 添加

(c ) 建立/etc/vsftpd/chroot_list文件 [此处目录对应的是(b)中第“104”行配置]

vim /etc/vsftpd/chroot_list

#加入下面的用户名
ftpUser

(d) 重启FTP服务

systemctl restart vsftpd

二、Springboot整合ftp服务,实现文件上传下载功能

1、配置ftp连接设置

(a)application.yml文件配置如下

global-config:
  ftp:
    client:
      # ftp客户端文件使用的字符集
      charset: UTF-8
    server:
      # ftp服务器绑定ip或者域名
      hostname: 192.168.3.210
      # 端口
      port: 21
      # 连接ftp服务器的用户名
      username: ftpuser2
      # 密码
      password: 123
      # ftp的共享文件路径
      workingPath: /usr/local/ftp/
      # ftp服务器文件使用的字符集(用于上传包含中文名的文件和下载包含中文名的文件 - 很重要)
      charset: ISO-8859-1

(b) 编写config配置文件,读取application.yml中的配置

@Data
@Component
@ConfigurationProperties("global-config")
public class GlobalConfig {
    @Value("${global-config.ftp.server.hostname}")
    private String ftpHost;

    @Value("${global-config.ftp.server.port}")
    private String ftpPort;

    @Value("${global-config.ftp.server.username}")
    private String ftpUsername;

    @Value("${global-config.ftp.server.password}")
    private String ftpPassword;

    @Value("${global-config.ftp.server.workingPath}")
    private String ftpWorkingPath;

    @Value("${global-config.ftp.server.charset}")
    private String ftpCharset;
}

2、拿到连接信息,实现文件的上传与下载

FTPUtil.java代码

package com.ztjz.share.ops.file.util;

import com.ztjz.share.ops.config.GlobalConfig;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.net.ftp.FTPClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;

/**
 * @author c先生
 * @describe:
 * @date 2025年02月10日 下午 05:52
 */
@Data
@Slf4j
@Component
public class FTPUtil {

    @Autowired
    private GlobalConfig globalConfig;

    private FTPClient ftpClient;

    private boolean connectServer(String ip, String port, String user, String pwd) {
        ftpClient = new FTPClient();
        Boolean isSuccess = false;
        try {
            ftpClient.setDefaultPort(Integer.parseInt(port));
            ftpClient.connect(ip);
            isSuccess = ftpClient.login(user, pwd);
        } catch (IOException e) {
            log.error("连接ftp服务器失败", e);
        }
        return isSuccess;
    }

    public boolean uploadFile(String remotePath, List<File> fileList) throws IOException {
        boolean upload = true;
        FileInputStream fileInputStream = null;
        //connect to ftpServer
        if (connectServer(globalConfig.getFtpHost(), globalConfig.getFtpPort(), globalConfig.getFtpUsername(), globalConfig.getFtpPassword())) {
            try {
                ftpClient.changeWorkingDirectory(remotePath);
                ftpClient.setBufferSize(1024);
                ftpClient.setControlEncoding("UTF-8");
                ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE);
                ftpClient.enterLocalPassiveMode();
                for (File fileItem : fileList
                ) {
                    fileInputStream = new FileInputStream(fileItem);
                    ftpClient.storeFile(fileItem.getName(), fileInputStream);
                }
            } catch (IOException e) {
                log.error("上传文件异常", e);
                upload = false;
            } finally {
                if (fileInputStream != null) {
                    fileInputStream.close();
                }
                ftpClient.disconnect();
            }
        }
        return upload;
    }

    public boolean uploadToFtp(String remotePath, String fileName, File file, String ftpHost, String ftpPort, String ftpUserName, String ftpPassword) throws IOException {
        boolean upload = true;
        FileInputStream fileInputStream = null;
        //connect to ftpServer
        if (connectServer(ftpHost, ftpPort, ftpUserName, ftpPassword)) {
            try {
                remotePath = new String(remotePath.getBytes(), globalConfig.getFtpCharset());
                boolean flag = createDirectories(ftpClient, remotePath);
//                int mkd = ftpClient.mkd(remotePath);
                if(!flag){
                    throw new IOException("创建文件夹失败");
                }
                flag = ftpClient.changeWorkingDirectory(remotePath);
                if(!flag){
                    throw new IOException("切换工作目录失败");
                }
                ftpClient.setBufferSize(1024);
                ftpClient.setControlEncoding("UTF-8");
                ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE);
                ftpClient.enterLocalPassiveMode();
                fileName = new String(fileName.getBytes(), globalConfig.getFtpCharset());
                //上传文件 参数:上传后的文件名,输入流
                upload = ftpClient.storeFile(fileName, new FileInputStream(file));
            } catch (IOException e) {
                log.error("上传文件异常", e);
                upload = false;
            } finally {
                if (fileInputStream != null) {
                    fileInputStream.close();
                }
                ftpClient.disconnect();
            }
        }
        return upload;
    }

    //下载
    public boolean download(String remotePath, String fileName, String ftpHost, String ftpPort, String ftpUserName, String ftpPassword) throws Exception {
        if (connectServer(ftpHost, ftpPort, ftpUserName, ftpPassword)) {
            remotePath = new String(remotePath.getBytes(), globalConfig.getFtpCharset());
            ftpClient.changeWorkingDirectory(remotePath);
            ftpClient.setBufferSize(1024);
            ftpClient.setControlEncoding("UTF-8");
            ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE);
            ftpClient.enterLocalPassiveMode();
            String newFileName = new String(fileName.getBytes(), globalConfig.getFtpCharset());
            FileOutputStream fos = new FileOutputStream(System.getProperty("java.io.tmpdir") + "/" + fileName);
            ;
            boolean b = ftpClient.retrieveFile(newFileName, fos);
            fos.close();
            return b;
        } else {
            throw new Exception("ftp服务器连接失败");
        }
    }
    
 	/**
     * 创建文件夹
     * @param ftpClient
     * @param path
     * @return
     * @throws IOException
     */
    public boolean createDirectories(FTPClient ftpClient, String path) throws IOException {
        String[] directories = path.split("/");
        StringBuilder currentPath = new StringBuilder();

        for (String dir : directories) {
            if (dir.isEmpty()) {
                continue; // 忽略空路径(如开头的 "/")
            }
            currentPath.append("/").append(dir);

            // 检查目录是否存在
            if (!ftpClient.changeWorkingDirectory(currentPath.toString())) {
                // 目录不存在,尝试创建
                int returnCode = ftpClient.mkd(currentPath.toString());
                if (!FTPReply.isPositiveCompletion(returnCode)) {
                    System.err.println("创建目录失败: " + currentPath);
                    return false;
                }
            }
        }
        return true;
    }
}

特别注意
1、在对ftp文件服务器上的文件进行操作前,需要先指定工作目录,也就是下面这几行代码不可以省略

//将FTP服务器上的当前工作目录更改为remotePath指定的路径。
ftpClient.changeWorkingDirectory(remotePath);
//设置数据传输的缓冲区大小为1024字节。影响传输效率,服务器允许的话尽可能大
ftpClient.setBufferSize(1024);
//设置控制通道的编码为UTF-8,确保服务器和客户端之间的命令和响应使用UTF-8编码,避免乱码。
ftpClient.setControlEncoding(“UTF-8”);
//设置文件传输模式为二进制模式,适用于传输图片、压缩文件等二进制文件,防止数据损坏。
ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE);
//启用本地被动模式(PASV),客户端连接服务器的数据端口,适用于客户端位于防火墙后的情况。
ftpClient.enterLocalPassiveMode();

2、ftp对字符串的编码格式要求为ISO-8859-1格式,所以需要进行格式转换,否则会出现中文乱码的问题

String newFileName = new String(fileName.getBytes(),globalConfig.getFtpCharset());

Controller层

package com.ztjz.share.ops.file.controller;

import com.forest.support.core.result.Result;
import com.forest.support.extend.file.constant.NameFormat;
import com.forest.support.utils.UUIDGenerator;
import com.ztjz.share.ops.file.service.FtpFileService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * @author c先生
 * @describe: ftp文件服务接口
 * @date 2025年02月10日 下午 03:14
 */
@Tag(name = "ftp文件上传")
@RestController
@RequestMapping("/ftp/file")
@Slf4j
public class FtpFileController {

    private final static SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyyMMddHHmmss");

    @Autowired
    private FtpFileService ftpFileService;

    @Operation(summary = "文件上传")
    @PostMapping("/upload")
    public Result<?> add(@RequestParam("file") MultipartFile file , @RequestParam(value = "format", required = false, defaultValue = "02") String format , @RequestParam(value = "folder", required = false) String folder) {
        try {
            String fileName = getFileName(file, NameFormat.fromValue(format));
            List<MultipartFile> files = new ArrayList<>();
            files.add(file);
            ftpFileService.upload(files,folder,fileName);
            return Result.ok(fileName);
        } catch (IOException e) {
            log.error("文件上传失败,{}",e.getMessage());
            return Result.error("文件上传失败");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Operation(summary = "文件下载")
    @GetMapping("/download")
    public void download(@RequestParam String fileName, HttpServletResponse response) throws Exception {
        String folder = "";
        if(fileName.contains("/")){
            folder = fileName.substring(0,fileName.lastIndexOf('/'));
            fileName = fileName.substring(fileName.lastIndexOf('/')+1);
        }
        boolean flag = ftpFileService.download(folder,fileName);
        if(flag){
            wapperResponse(fileName,response);
        }
    }

    private void wapperResponse(String fileName,HttpServletResponse response){
        FileInputStream in = null;
        try {
            response.setContentType("application/octet-stream;charset=utf-8");
            response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));
            response.addHeader("Content-type", "application/octet-stream");
            response.setCharacterEncoding("UTF-8");
            //将字节从InputStream复制到OutputStream 。
            in = new FileInputStream(System.getProperty("java.io.tmpdir")+"/"+fileName);
            IOUtils.copy(in, response.getOutputStream());
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 为文件创建名称
     * @param multipartFile 上传文件
     * @return 返回文件名
     */
    private String getFileName(MultipartFile multipartFile, NameFormat nameFormat ){
        String newFileName = null;
        String originalFilename = multipartFile.getOriginalFilename();
        String fileName;
        String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
        switch (nameFormat){
            case UUID:
                fileName = UUIDGenerator.randomUUID();
                newFileName = fileName + extension;
                break;
            case ORIGINAL:
                newFileName = originalFilename;
                break;
            case TIMESTAMP:
                fileName = originalFilename.substring(0, originalFilename.lastIndexOf("."));
                newFileName = fileName + "_" + DATE_FORMAT.format(new Date()) + extension;
                break;
        }
        return newFileName;
    }
}

service层

package com.ztjz.share.ops.file.service.impl;

import com.ztjz.share.ops.config.GlobalConfig;
import com.ztjz.share.ops.file.service.FtpFileService;
import com.ztjz.share.ops.file.util.FTPUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;

/**
 * @author c先生
 * @describe:
 * @date 2025年02月10日 下午 03:14
 */
@Service
@Slf4j
public class FtpFileServiceImpl implements FtpFileService {

    @Autowired
    private FTPUtil ftpUtil;

    @Autowired
    private GlobalConfig globalConfig;


    @Override
    public String upload(List<MultipartFile> fileList,String folder,String fileName) throws Exception {
        if(ObjectUtils.isNotEmpty(fileList)){
            MultipartFile multipartFile = fileList.get(0);
            //3、将MultipartFile转化为File
            File file = this.multipartFileToFile(multipartFile);
            String remotePath = "/";
            if(StringUtils.isNotBlank(folder)){
                remotePath = folder;
                if(!folder.startsWith("/")){
                    remotePath = "/"+folder;
                }
                if(!folder.endsWith("/")){
                    remotePath = folder + "/";
                }
                if(!folder.endsWith("/")&&!folder.startsWith("/")){
                    remotePath = "/" + folder + "/";
                }
            }
            //4、上传至ftp服务器
            if (ftpUtil.uploadToFtp(remotePath, fileName, file)) {
                System.out.println("上传至ftp服务器!");
            } else {
                System.out.println("上传至ftp服务器失败!");
            }
            return fileName;
        }else {
            throw new Exception("文件为空,上传失败");
        }
    }

    @Override
    public boolean download(String folder,String objectName) throws Exception {
        String remotePath = "/";
        if(StringUtils.isNotBlank(folder)){
            remotePath = folder;
            if(!folder.startsWith("/")){
                remotePath = "/"+folder;
            }
            if(!folder.endsWith("/")){
                remotePath = folder + "/";
            }
            if(!folder.endsWith("/")&&!folder.startsWith("/")){
                remotePath = "/" + folder + "/";
            }
        }
        return ftpUtil.download(remotePath,objectName);
    }






    public File multipartFileToFile(MultipartFile file) throws Exception {

        File toFile = null;
        if (file.equals("") || file.getSize() <= 0) {
            file = null;
        } else {
            InputStream ins = null;
            ins = file.getInputStream();
            toFile = new File(file.getOriginalFilename());
            inputStreamToFile(ins, toFile);
            ins.close();
        }
        return toFile;
    }

    //获取流文件
    private static void inputStreamToFile(InputStream ins, File file) {
        try {
            OutputStream os = new FileOutputStream(file);
            int bytesRead = 0;
            byte[] buffer = new byte[8192];
            while ((bytesRead = ins.read(buffer, 0, 8192)) != -1) {
                os.write(buffer, 0, bytesRead);
            }
            os.close();
            ins.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

灰什么鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值