连接
@PostMapping("/connect")
public RestBean<Void> connect(@RequestBody SftpConnectionVO vo) {
String connected = sftpService.connect(vo);
return connected == null ? RestBean.success() : RestBean.failure(400, "连接失败,请检查服务器是否开放端口");
}
// SftpUtils.java
public static Map<String, SftpInfo> sftpMap = new ConcurrentHashMap<>();
public String getSftpConnection(SftpConnectionVO vo) {
// 定义一个 JSch 对象,用于执行 SFTP 操作
JSch jsch = new JSch();
Session session = null;
Channel channel = null;
ChannelSftp sftp = null;
try {
log.info("准备连接 {},用户名 {},密码 {},端口 {}", vo.getHost(), vo.getUsername(), vo.getPassword(), vo.getPort());
// 通过 JSch 对象,根据用户名,地址,端口,获取一个 SFTP 会话对象
session = jsch.getSession(vo.getUsername(), vo.getHost(), vo.getPort());
// 设置 SFTP 会话的密码
session.setPassword(vo.getPassword());
// 设置 SFTP 会话的配置,关闭严格的主机密钥检查
session.setConfig("StrictHostKeyChecking", "no");
session.setTimeout(5000);
// 连接 SFTP 会话
session.connect();
// 通过 SFTP 会话,打开一个 SFTP 通道
channel = session.openChannel("sftp");
// 连接 SFTP 通道
channel.connect();
// 把 SFTP 通道转换成 ChannelSftp 对象
sftp = (ChannelSftp) channel;
sftpMap.put(vo.getHost(), new SftpInfo(session, channel, sftp));
log.info("{} 已经连接", vo.getHost());
return null;
} catch (JSchException e) {
String message = e.getMessage();
if (message.equals("Auth fail")) {
log.error("连接Sftp失败,用户名或密码错误,登录失败");
return "登录Sftp失败,用户名或密码错误";
} else if (message.contains("Connection refused")) {
log.error("连接Sftp失败,连接被拒绝,可能是没有启动Sftp服务或是放开端口");
return "连接被拒绝,可能是没有启动Sftp服务或是放开端口";
} else { // 如果异常信息是其他情况,表示 Sftp 连接出现其他错误
log.error("连接Sftp时出现错误", e);
return "连接Sftp时出现错误";
}
}
}
@Data
public class SftpInfo {
private final Session session;
private final Channel channel;
private final ChannelSftp sftp;
public SftpInfo(Session session, Channel channel, ChannelSftp sftp) {
this.session = session;
this.channel = channel;
this.sftp = sftp;
}
}
上传 下载功能实现
// SftpController.java
@PostMapping("/upload")
public RestBean<Void> handleFileUpload(@RequestParam("file") MultipartFile file, @RequestParam String host) {
sftpUtils.handleFileUpload(file,host);
return RestBean.success();
}
@GetMapping("/downloadFile")
public ResponseEntity<InputStreamResource> downloadFile(@RequestParam String host, @RequestParam String file) {
return sftpUtils.downloadFile(host, java.net.URLDecoder.decode(file, StandardCharsets.UTF_8));
}
// SftpUtils.java
private static ChannelSftp getChannelSftp(String host) {
SftpInfo sftpInfo = sftpMap.get(host);
if (sftpInfo == null) {
throw new com.example.controller.exception.SftpException(400, "Sftp连接不存在");
}
ChannelSftp sftp = sftpInfo.getSftp();
return sftp;
}
public void handleFileUpload(MultipartFile file, String host) {
ChannelSftp sftp = getChannelSftp(host);
try {
sftp.put(file.getInputStream(), file.getOriginalFilename());
} catch (SftpException | IOException e) {
throw new com.example.controller.exception.SftpException(400, "上传失败:文件已存在或权限不足");
}
}
public ResponseEntity<InputStreamResource> downloadFile(String host, String file) {
try {
ChannelSftp sftp = getChannelSftp(host);
InputStream inputStream = sftp.get(file);
byte[] fileBytes = IOUtils.toByteArray(inputStream);
HttpHeaders headers = new HttpHeaders();
String encodedFilename = URLEncoder.encode(file, StandardCharsets.UTF_8.name());//用utf8编码 否则中文文件名无法传输
headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encodedFilename);
return ResponseEntity.ok()
.headers(headers)
.contentLength(fileBytes.length)
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(new InputStreamResource(new ByteArrayInputStream(fileBytes)));
} catch (IOException | SftpException e) {
e.printStackTrace();
return ResponseEntity.badRequest().body(null);
}
}
//pwd cd 功能
public void cd(String host, String path) {
ChannelSftp sftp = getChannelSftp(host);
try {
sftp.cd(path);
} catch (SftpException e) {
throw new com.example.controller.exception.SftpException(400, "目录不存在");
}
}
public String pwd(String host) throws SftpException {
ChannelSftp sftp = getChannelSftp(host);
return sftp.pwd();
}
// 关闭连接
public void closeConnection(String host) {
// 获取指定host的SFTP连接信息
SftpInfo sftpInfo = sftpMap.get(host);
if (sftpInfo != null) {
// 通过连接信息获取会话和通道
Session session = sftpInfo.getSession();
Channel channel = sftpInfo.getChannel();
try {
// 关闭SFTP通道
if (channel != null && channel.isConnected()) {
channel.disconnect();
}
// 关闭会话
if (session != null && session.isConnected()) {
session.disconnect();
}
} finally {
// 从缓存连接池中移除
sftpMap.remove(host);
log.info("SFTP connection to {} closed", host);
}
}
}
与前端交互
// 前端使用elementui plus 绑定表格数据
<div>
<div class="navigation">
<el-breadcrumb :separator-icon="ArrowRight">
<el-breadcrumb-item v-for="(item, index) in path" :key="index">
<el-tag @click="cd(index)" effect="light">{{ item }}</el-tag>
</el-breadcrumb-item> </el-breadcrumb> </div>
<!-- <div @click.right.native="showContextMenu($event)">-->
<div style="width: 500px">
<div class="op">
<el-button @click="getFiles">获取文件列表</el-button>
<el-button @click="pwd">pwd</el-button>
<el-button type="primary" plain @click="openNewFileModal" :icon="DocumentAdd">新建</el-button>
<el-button type="success" plain @click="handleDownload" :icon="Download">下载</el-button>
<el-popover placement="right" :width="400" trigger="click">
<template #reference>
<el-button type="success" plain :icon="Upload">上传</el-button>
</template> <el-upload class="upload-demo"
drag
:action="`${axios.defaults.baseURL}/api/sftp/upload?host=${props.info.host}`"
:on-success="getFiles"
multiple
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
拖拽 <em>或点击上传</em>
</div> <template #tip>
<div class="el-upload__tip">
在这里上传文件
</div>
</template> </el-upload> </el-popover>
</div> <el-table class="mac-table" :data="files" max-height="650px"
@row-click="handleItemClick" style="width: 100%"
v-loading="fileLoading" element-loading-text="文件加载中..."
@selection-change="handleSelectionChange"
:default-sort="{prop: 'name', order: 'ascending'}">
<el-table-column type="selection" width="40"></el-table-column>
<el-table-column prop="name" label="名称" width="160" sortable>
<template #default="{ row }">
<div class="cell-content">
<el-icon v-if="row.directory"><Folder/></el-icon>
<el-icon v-else><Document/></el-icon>
{{ row.name }}
</div>
</template> </el-table-column> <el-table-column prop="size" width="100" label="大小"></el-table-column>
<el-table-column prop="modifyTime" width="180" label="修改时间" sortable></el-table-column>
</el-table> </div>
</div>
// 发送下载请求
function handleDownload() {
if (select.value.length === 0) {
ElMessage.warning('请选择要下载的文件');
return;
}
const files = select.value.map(file => file.name);
files.forEach(file => {
axios.get('api/sftp/downloadFile?host=' + props.info.host + '&file=' + encodeURIComponent(file), { responseType: 'blob' })
.then(response => {
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', file); // 使用文件的原始名称作为下载的文件名
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
})
.catch(error => {
ElMessage.error(`下载文件${file}时出错: ${error}`);
});
});
}
效果