springboot FTP服务器 上传&&下载示例demo

最近项目上需要使用ftp服务器和第三方进行资源交互,于是写了个小demo记录下~

基础知识

FTP服务器

FTP(File Transfer Protocol)即文件传输协议,是一种基于TCP的协议,采用客户/服务器模式。通过FTP协议,用户可以在FTP服务器中进行文件的上传或下载等操作。虽然现在通过HTTP协议下载的站点有很多,但是由于FTP协议可以很好地控制用户数量和宽带的分配,快速方便地上传、下载文件,因此FTP已成为网络中文件上传和下载的首选服务器。同时,它也是一个应用程序,用户可以通过它把自己的计算机与世界各地所有运行FTP协议的服务器相连,访问服务器上的大量程序和信息。FTP服务的功能是实现完整文件的异地传输。(来自百度百科)

多级目录下创建文件

一直以为ftp服务器多级目录创建和java中目录创建一样,然而并不是滴~ 可参考文档:关于ftp上传changeWorkingDirectory()方法的路径切换问题
将文件hello.json上传至/home/upload/目录下步骤:

  1. 先使用FTPClient.changeWorkingDirectory("home"),判断home目录是否存在?若不存在则2
  2. 使用FTPClient.makeDirectory("home") 创建目录,同时使用FTPClient.changeWorkingDirectory("home")将ftp session指向home目录
  3. 使用FTPClient.changeWorkingDirectory("upload")判断upload目录是否存在?若不存在则4
  4. 使用FTPClient.makeDirectory("upload")创建目录,同时使用FTPClient.changeWorkingDirectory("upload")将ftp session指向upload目录
  5. 使用FTPClient.storeFile(fileName, is); 上传文件流is

上传流程

FTP传输模式

FTP的主动模式和被动模式

对比方面主动模式被动模式
客户端随机开放一个端口(1024以上)-
服务端-在本地随机开放一个端口(1024以上)

实践出真知

引入依赖和加入配置

  1. pom.xml 中添加依赖
<dependency>
	<groupId>commons-net</groupId>
	<artifactId>commons-net</artifactId>
	<version>3.6</version>
</dependency>
  1. 在application.yml中添加ftp服务器配置(多环境则需要在application-dev/test/prod.yml中分别配置)
ftp:
  hostname: 172.16.1.1
  port: 21
  username: lizzy
  password: lizzy
  root-path: /upload/

定义配置bean

package com.lizzy.common.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import lombok.Data;

@ConfigurationProperties(prefix = "ftp")
@Data
@Component
public class FtpConfig {
	
	private String hostname;
	
	private Integer port;
	
	private String username;
	
	private String password;
	
	private String rootPath;
	
}

定义FTP工具类

获取FTPClient,设置超时

private FTPClient getFTPClient() {
		
	FTPClient client = new FTPClient();
	// 设置默认超时时间30s
	client.setDefaultTimeout(30000);
	// 设置链接超时时间
	client.setConnectTimeout(30000);
	// 设置数据传输时间
	client.setDataTimeout(30000);
	return client;
}

上传

/**
 * 上传文件至FTP服务器 
 * @param config ftp服务器配置
 * @param path 相对目录(不包含根目录)
 * @param fileName 文件名 
 * @param is 文件流 
 */
public static void upload(FtpConfig config, String path, String fileName, InputStream is) {
	
	FTPClient client = getFTPClient();
	try {
		// 连接服务器
		client.connect(config.getHostname(), config.getPort());
		// 登录
		client.login(config.getUsername(), config.getPassword());
		int reply = client.getReplyCode();
		if (!FTPReply.isPositiveCompletion(reply)) {
			
			// 连接失败 
			client.disconnect();
			log.info("FTP服务器配置链接失败!请检查配置:{}", config.toString());
			return ;
		}
		
		// 设置被动模式,开通一个端口来传输数据
		client.enterLocalPassiveMode();
		// 切换到上传目录
		final String filePath = config.getRootPath() + path;
		if (!client.changeWorkingDirectory(filePath)) {
			// 目录不存在则创建
			String[] dirs = filePath.split("/");
			for (String dir : dirs) {
				
				if (StringUtils.isEmpty(dir)) {
					continue ;
				}
				if (!client.changeWorkingDirectory(dir)) {
					client.makeDirectory(dir);
					client.changeWorkingDirectory(dir);
				}
			}
		}
		
		// 设置被动模式,开通一个端口来传输数据
		client.enterLocalPassiveMode();
		//设置上传文件的类型为二进制类型
		client.setFileType(FTP.BINARY_FILE_TYPE);
		client.storeFile(fileName, is);
		log.debug("文件{}成功上传至FTP服务器!", filePath + "/" + fileName);
		client.logout();
		
	} catch (IOException e) {
		log.info("FTP服务器配置链接失败!错误:{}", e.getMessage());
		e.printStackTrace();
	} finally {
		try {
			is.close();
			if (client.isConnected()) {
				client.disconnect();
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
}

下载

/**
 * 从FTP服务器下载文件流 
 * @param config FTP服务器配置
 * @param path 相对目录(不包含根目录)
 * @param fileName 文件名 
 * @return
 */
public static InputStream download(FtpConfig config, String path, String fileName) {
	
	FTPClient client = getFTPClient();
	try {
		// 连接服务器
		client.connect(config.getHostname(), config.getPort());
		// 登录
		client.login(config.getUsername(), config.getPassword());
		int reply = client.getReplyCode();
		if (!FTPReply.isPositiveCompletion(reply)) {
			
			// 连接失败 
			client.disconnect();
			log.info("FTP服务器配置链接失败!请检查配置:{}", config.toString());
			return null;
		}
		
		// 设置被动模式,开通一个端口来传输数据
		client.enterLocalPassiveMode();
		// 切换到下载目录
		final String filePath = config.getRootPath() + path;
		client.changeWorkingDirectory(filePath);
		log.debug("FTP session指向下载目录:{}", filePath);
		//设置上传文件的类型为二进制类型
		client.setFileType(FTP.BINARY_FILE_TYPE);
//			client.setControlEncoding("utf8");
		
		// 中文名会下载失败,文件名需转码 
		InputStream is = client.retrieveFileStream(new String(fileName.getBytes("gbk"), "ISO-8859-1"));
		if (null == is) {
			log.debug("下载文件:{}失败!读取长度为0!", fileName);
			return null;
		}
		InputStream retIs = copyStream(is);
		is.close();
		client.completePendingCommand();
		log.debug("从FTP服务器下载文件({})成功!", fileName);
		return retIs;
		
	} catch (IOException e) {
		log.info("FTP服务器配置链接失败!错误:{}", e.getMessage());
		e.printStackTrace();
	} finally {
		
		try {
			if (client.isConnected()) {
				log.info("从FTP服务器下载文件{}流程结束,关闭链接!", fileName);
				client.disconnect();
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	return null;
}

复制流方法如下:

private InputStream copyStream(InputStream is) throws IOException {
	
	ByteArrayOutputStream baos = new ByteArrayOutputStream(); 
	byte[] buffer = new byte[1024];
	int len;
	while ((len = is.read(buffer)) > -1 ) {
	    baos.write(buffer, 0, len);
	}
	baos.flush();
	
	return new ByteArrayInputStream(baos.toByteArray());
}

遇到的问题

文件覆盖

FTP服务器上同名文件是否覆盖的配置

retrieveFileStream卡死

出错前代码如下:我真是看了半天也不知道为嘛子就失败,于是乎开始打印各个文件的大小,终于发现了一点点端倪,两个不同的文件打印的文件大小一样!看出来了么,简直无语死了,慎用inputstream.available()方法判断流=_=||

// 中文名会下载失败,文件名需转码 
InputStream is = client.retrieveFileStream(new String(fileName.getBytes("gbk"), "ISO-8859-1"));
if (null == is || is.available() < 1) {
	log.debug("下载文件:{}失败!读取长度为0!", fileName);
	return null;
}
log.debug("结束读取文件:{},大小为:{}", fileName, is.available());
InputStream retIs = copyStream(is);
is.close();
client.completePendingCommand();

判断流操作慎用inputstream.available()

注意事项(采过的坑)

先connect后enterLocalActiveMode

参考文档:Ftpclient调用retrieveFileStream返回null, docker中下载失败问题

connect方法中,将模式设置为主动模式ACTIVE_LOCAL_DATA_CONNECTION_MODE.
enterLocalActiveMode方法则是将链接模式设置为被动模式

若先调用enterLocalActiveMode再connect,则链接模式还是为主动模式!

获取返回流一定要调用completePendingCommad

参考文档:FTPClient中使用completePendingCommand方法注意事项

编写FTP过程中遇到的问题就是,流程的框框都是固定的,不能多写也不能少写,不然就是一堆的block!

// 上传方法,之后若调用completePendingCommand会卡死 
public boolean storeFile(String remote, InputStream local)
// 上传方法,之后必须调用completePendingCommand 
public OutputStream storeFileStream(String remote)

// 下载方法,之后若调用completePendingCommand会卡死 
public boolean retrieveFile(String remote, OutputStream local)
// 下载方法,之后必须调用completePendingCommand 
public InputStream retrieveFileStream(String remote)

先inputStream.close()再completePendingCommad

参考文档:FTPClient中使用completePendingCommand方法时之踩坑

completePendingCommand()会一直在等FTP Server返回226 Transfer complete,但是FTP Server只有在InputStream执行close方法时,才会返回。所以先要执行close方法,再执行completePendingCommand

后记

demo写的很匆忙,后续遇到问题了再更新~

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值