基于 JSch 实现 Sftp 文件传输
概述
JSch 是一个纯 Java 实现的 SSH2 类库,支持连接到一个 sshd 服务器。这里记录一下如何通过 JSch 实现 Sftp 传输文件的逻辑。
以下代码中的 IOStreamx、Filex 等工具类由 central-framework [链接]提供。
上传文件(夹)
java
/**
* 将文件(夹)上传到服务器的指定路径
*
* @param host 主机名
* @param username 用户名
* @param port 端口
* @param password 密码
* @param localFile 本地文件(夹)
* @param remotePath 服务器路径,支持相对路径,如 ~/test
*/
public void sftpTo(String host, String username, int port, String password, File localFile, Path remotePath) throws JSchException {
var factory = new JSch();
// 创建会话
var session = factory.getSession(username, host, port);
// 设置连接密码
session.setPassword(password);
session.setConfig("StrictHostKeyChecking", "no");
try {
// 连接服务器
session.connect();
// 传输频道
var channel = (ChannelSftp) session.openChannel("sftp");
try {
channel.connect();
// 进入指定路径
this.cd(channel, remotePath);
// 传输文件(夹)
if (localFile.isDirectory()) {
this.sftpDirectoryTo(channel, localFile);
} else if (localFile.isFile()) {
this.sftpFileTo(channel, localFile);
} else {
throw new IOException("不支持的文件: " + localFile.getAbsolutePath());
}
} catch (SftpException | IOException ex) {
throw new RuntimeException(ex.getLocalizedMessage(), ex);
} finally {
// 结束连接
channel.disconnect();
}
} finally {
session.disconnect();
}
}
/**
* 传输文件夹到服务器
*
* @param channel 传输频道
* @param directory 文件夹
*/
private void sftpDirectoryTo(ChannelSftp channel, File directory) throws IOException, SftpException {
this.cd(channel, directory.getName());
var files = directory.listFiles();
if (Arrayx.isNotEmpty(files)) {
for (var file : files) {
if (file.isDirectory()) {
this.sftpDirectoryTo(channel, file);
} else if (file.isFile()) {
this.sftpFileTo(channel, file);
} else {
throw new IOException("不支持的文件: " + file.getAbsolutePath());
}
}
}
this.cd(channel, "..");
}
/**
* 传输文件到服务器
*
* @param channel 传输频道
* @param file 文件
*/
private void sftpFileTo(ChannelSftp channel, File file) throws IOException, SftpException {
try (var input = IOStreamx.buffered(Files.newInputStream(file.toPath(), StandardOpenOption.READ))) {
channel.put(input, file.getName());
}
}
/**
* 进入文件夹,如果没有,则创建
*
* @param channel 传输频道
* @param directory 文件夹
*/
private void cd(ChannelSftp channel, String directory) throws SftpException {
// 尝试直接进入目录
try {
channel.cd(directory);
} catch (SftpException ex) {
// 进入失败,可能目录不存在
if (ex.id == 2) {
// No such file
channel.mkdir(directory);
channel.cd(directory);
} else {
throw new ShellException("无法进入文件夹: " + directory, ex);
}
}
}
/**
* 进入指定路径
*
* @param channel 传输频道
* @param path 路径
*/
private void cd(ChannelSftp channel, Path path) throws SftpException {
if (path.isAbsolute()) {
// 如果是绝对路径,则需要先进入 /
this.cd(channel, "/");
}
for (int i = 0, count = path.getNameCount(); i < count; i++) {
var name = path.getName(i).toString();
if ("~".equals(name)) {
this.cd(channel, Path.of(channel.getHome()));
} else if (".".equals(name)) {
// 进入当前目录,忽略
} else {
this.cd(channel, name);
}
}
}
下载文件(夹)
java
/**
* 将服务器上的文件(夹)传输到本地指定路径
*
* @param host 主机名
* @param username 用户名
* @param port 端口
* @param password 密码
* @param remoteFile 服务器文件(夹)
* @param localPath 本地路径
*/
public void sftpFrom(String host, String username, int port, String password, Path remoteFile, Path localPath) throws JSchException {
var factory = new JSch();
// 创建会话
var session = factory.getSession(username, host, port);
// 设置连接密码
session.setPassword(password);
session.setConfig("StrictHostKeyChecking", "no");
try {
// 连接服务器
session.connect();
// 传输频道
var channel = (ChannelSftp) session.openChannel("sftp");
try {
channel.connect();
// 将相对路径转成绝对路径
remoteFile = toAbsolute(channel, remoteFile);
// 查询路径状态
// 文件不存在时会抛异常
var stat = channel.stat(remoteFile.toString());
if (stat.isDir()) {
this.sftpDirectoryFrom(channel, remoteFile, localPath);
} else {
this.sftpFileFrom(channel, remoteFile, localPath);
}
} catch (SftpException | IOException ex) {
throw new RuntimeException(ex.getLocalizedMessage(), ex);
} finally {
// 结束连接
channel.disconnect();
}
} finally {
session.disconnect();
}
}
/**
* 将相对路径转成绝对路径
*
* @param channel 传输路径
* @param path 路径
*/
private Path toAbsolute(ChannelSftp channel, Path path) throws SftpException, IOException {
if (path.isAbsolute()) {
return path.toAbsolutePath();
} else {
return switch (path.getName(0).toString()) {
case "." -> Path.of(channel.pwd()).resolve(path.subpath(1, path.getNameCount())).toAbsolutePath();
case ".." -> Path.of(channel.pwd()).resolve(path).toAbsolutePath();
case "~" -> Path.of(channel.getHome()).resolve(path.subpath(1, path.getNameCount())).toAbsolutePath();
default -> throw new IOException("解析路径异常");
};
}
}
/**
* 将服务器文件夹传输到本地
*
* @param channel 传输频道
* @param directory 文件夹
*/
private void sftpDirectoryFrom(ChannelSftp channel, Path directory, Path localPath) throws IOException, SftpException {
var localDirectory = localPath.resolve(directory.getFileName());
if (!localDirectory.toFile().exists() || !localDirectory.toFile().isDirectory()) {
Assertx.mustTrue(localDirectory.toFile().mkdirs(), IOException::new, "无法访问本地指定路径: " + localPath);
}
var files = channel.ls(directory.toString());
for (var file : files) {
if (file instanceof ChannelSftp.LsEntry entry) {
if (".".equals(entry.getFilename()) || "..".equals(entry.getFilename())) {
continue;
} else {
if (entry.getAttrs().isDir()) {
sftpDirectoryFrom(channel, directory.resolve(entry.getFilename()), localDirectory);
} else {
sftpFileFrom(channel, directory.resolve(entry.getFilename()), localDirectory);
}
}
}
}
}
/**
* 将服务器文件传输到本地
*
* @param channel 传输频道
* @param file 文件
*/
private void sftpFileFrom(ChannelSftp channel, Path file, Path localPath) throws IOException, SftpException {
var localFile = localPath.resolve(file.getFileName()).toFile();
if (localFile.exists()) {
if (localFile.isFile()) {
Filex.delete(localFile);
}
}
Assertx.mustTrue(localFile.createNewFile(), IOException::new, "无法访问路径: " + localPath);
try (var output = IOStreamx.buffered(Files.newOutputStream(localFile.toPath(), StandardOpenOption.WRITE))) {
channel.get(file.toString(), output);
}
}