diff --git a/dsp/pom.xml b/dsp/pom.xml index da6fe5e..52dc383 100644 --- a/dsp/pom.xml +++ b/dsp/pom.xml @@ -121,6 +121,11 @@ poi-ooxml 5.2.4 + + com.jcraft + jsch + 0.1.55 + diff --git a/dsp/src/main/java/com/jsc/dsp/utils/SFTPConnector.java b/dsp/src/main/java/com/jsc/dsp/utils/SFTPConnector.java new file mode 100644 index 0000000..ce877b2 --- /dev/null +++ b/dsp/src/main/java/com/jsc/dsp/utils/SFTPConnector.java @@ -0,0 +1,138 @@ +package com.jsc.dsp.utils; + +import com.jcraft.jsch.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +@Component +public class SFTPConnector { + + private static final Logger log = LoggerFactory.getLogger(SFTPConnector.class); + + @Value("${sftp.host}") + private String host; + + @Value("${sftp.port:22}") // SFTP 默认端口 22 + private Integer port; + + @Value("${sftp.username}") + private String username; + + @Value("${sftp.password}") // 支持密码认证(生产环境建议改用私钥) + private String password; + + @Value("${sftp.timeout:30000}") + private Integer timeout; // 单位:毫秒 + + @Value("${sftp.strictHostKeyChecking:false}") // false 仅用于测试环境! + private boolean strictHostKeyChecking; + + /** + * 上传文件到 SFTP 服务器(密码认证) + * + * @param inputStream 源文件流(方法内部负责关闭) + * @param remotePath 远程绝对路径,如 /upload/2024/file.pdf + * @return 上传成功返回 true + */ + public boolean uploadFile(InputStream inputStream, String remotePath) { + Session session = null; + ChannelSftp channelSftp = null; + try { + // 1. 初始化 JSch 会话 + JSch jsch = new JSch(); + session = jsch.getSession(username, host, port); + session.setPassword(password); + session.setTimeout(timeout); + + // 2. 配置 SSH 连接参数(安全提示:生产环境必须启用 StrictHostKeyChecking 并配置 known_hosts) + Properties config = new Properties(); + config.put("StrictHostKeyChecking", String.valueOf(strictHostKeyChecking)); + session.setConfig(config); + + // 3. 建立连接 + session.connect(); + channelSftp = (ChannelSftp) session.openChannel("sftp"); + channelSftp.connect(timeout); + + // 4. 确保目标目录存在 + ensureDirectoryExists(channelSftp, remotePath); + + // 5. 上传文件(JSch 会完整读取流,但不关闭流) + channelSftp.put(inputStream, remotePath); + log.info("SFTP 文件上传成功: {}", remotePath); + return true; + + } catch (JSchException | SftpException e) { + log.error("SFTP 上传失败 [host={}, path={}]: {}", host, remotePath, e.getMessage(), e); + return false; + } catch (Exception e) { + log.error("SFTP 上传异常 [path={}]: {}", remotePath, e.getMessage(), e); + return false; + } finally { + // 6. 资源清理(先关流,再关通道/会话) + closeQuietly(inputStream); + if (channelSftp != null && channelSftp.isConnected()) { + try { + channelSftp.disconnect(); + } catch (Exception e) { + log.warn("关闭 SFTP 通道异常", e); + } + } + if (session != null && session.isConnected()) { + session.disconnect(); + } + } + } + + /** + * 递归创建远程目录(基于 ChannelSftp) + * + * @param sftp SFTP 通道 + * @param remotePath 完整远程文件路径(含文件名) + * @throws SftpException 目录创建失败时抛出 + */ + private void ensureDirectoryExists(ChannelSftp sftp, String remotePath) throws SftpException { + String dirPath = extractDirectory(remotePath); + if ("/".equals(dirPath)) return; + + String[] dirs = dirPath.split("/"); + StringBuilder current = new StringBuilder(); + for (String dir : dirs) { + if (dir.isEmpty()) continue; + current.append("/").append(dir); + try { + sftp.cd(current.toString()); // 尝试进入目录 + } catch (SftpException e) { + sftp.mkdir(current.toString()); // 不存在则创建 + sftp.cd(current.toString()); + } + } + } + + /** + * 从完整路径提取目录部分(如 /a/b/file.txt → /a/b) + */ + private String extractDirectory(String path) { + int lastSlash = path.lastIndexOf('/'); + return (lastSlash <= 0) ? "/" : path.substring(0, lastSlash); + } + + /** + * 安静关闭输入流 + */ + private void closeQuietly(InputStream is) { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + log.debug("关闭输入流时忽略异常", e); + } + } + } +} \ No newline at end of file