增加主机信息功能
This commit is contained in:
677
web-api/src/main/java/com/jeesite/modules/utils/SftpUtil.java
Normal file
677
web-api/src/main/java/com/jeesite/modules/utils/SftpUtil.java
Normal file
@@ -0,0 +1,677 @@
|
||||
package com.jeesite.modules.utils;
|
||||
|
||||
import com.jcraft.jsch.*;
|
||||
import com.jeesite.modules.apps.Module.SftpResult;
|
||||
import com.jeesite.modules.biz.entity.MySftpAccounts;
|
||||
import io.micrometer.common.util.StringUtils;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* SFTP 工具类
|
||||
* 支持:上传、下载、删除、创建目录、重命名
|
||||
*
|
||||
* @author gaoxq
|
||||
*/
|
||||
public class SftpUtil {
|
||||
|
||||
private static final int SSH_TIMEOUT = 5000;
|
||||
|
||||
// ------------------------------------------------------------------ 连接
|
||||
|
||||
/**
|
||||
* 建立 SFTP Channel,调用方负责关闭
|
||||
*/
|
||||
private static ChannelSftp openChannel(MySftpAccounts account) throws JSchException {
|
||||
JSch jsch = new JSch();
|
||||
int port = Optional.ofNullable(account.getHostPort()).orElse(22);
|
||||
|
||||
if ("key".equalsIgnoreCase(account.getAuthType())
|
||||
&& StringUtils.isNotBlank(account.getPrivateKey())) {
|
||||
jsch.addIdentity(
|
||||
"tempKey",
|
||||
account.getPrivateKey().getBytes(StandardCharsets.UTF_8),
|
||||
null, null
|
||||
);
|
||||
}
|
||||
|
||||
Session session = jsch.getSession(account.getUsername(), account.getHostIp(), port);
|
||||
session.setTimeout(SSH_TIMEOUT);
|
||||
|
||||
if (!"key".equalsIgnoreCase(account.getAuthType())) {
|
||||
session.setPassword(account.getPassword());
|
||||
}
|
||||
|
||||
Properties config = new Properties();
|
||||
config.put("StrictHostKeyChecking", "no");
|
||||
session.setConfig(config);
|
||||
session.connect(SSH_TIMEOUT);
|
||||
|
||||
ChannelSftp channel = (ChannelSftp) session.openChannel("sftp");
|
||||
channel.connect(SSH_TIMEOUT);
|
||||
return channel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全关闭 channel 及其 session
|
||||
*/
|
||||
private static void close(ChannelSftp channel) {
|
||||
if (channel == null) return;
|
||||
try {
|
||||
Session session = channel.getSession();
|
||||
if (channel.isConnected()) channel.disconnect();
|
||||
if (session != null && session.isConnected()) session.disconnect();
|
||||
} catch (JSchException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 上传
|
||||
|
||||
/**
|
||||
* 上传本地文件到远程路径
|
||||
*
|
||||
* @param account SSH账号
|
||||
* @param localPath 本地文件绝对路径
|
||||
* @param remotePath 远程目标路径(含文件名),如 /data/app/test.txt
|
||||
*/
|
||||
public static SftpResult upload(MySftpAccounts account, String localPath, String remotePath) {
|
||||
ChannelSftp channel = null;
|
||||
try {
|
||||
channel = openChannel(account);
|
||||
channel.put(localPath, remotePath, ChannelSftp.OVERWRITE);
|
||||
return SftpResult.ok("上传成功: " + remotePath);
|
||||
} catch (Exception e) {
|
||||
return SftpResult.fail("上传失败: " + e.getMessage());
|
||||
} finally {
|
||||
close(channel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传字节流到远程路径
|
||||
*
|
||||
* @param account SSH账号
|
||||
* @param input 输入流
|
||||
* @param remotePath 远程目标路径(含文件名)
|
||||
*/
|
||||
public static SftpResult upload(MySftpAccounts account, InputStream input, String remotePath) {
|
||||
ChannelSftp channel = null;
|
||||
try {
|
||||
channel = openChannel(account);
|
||||
channel.put(input, remotePath, ChannelSftp.OVERWRITE);
|
||||
return SftpResult.ok("上传成功: " + remotePath);
|
||||
} catch (Exception e) {
|
||||
return SftpResult.fail("上传失败: " + e.getMessage());
|
||||
} finally {
|
||||
close(channel);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 下载
|
||||
|
||||
/**
|
||||
* 下载远程文件到本地
|
||||
* <p>
|
||||
* 示例:
|
||||
* - download(account, "/data/logs", "*.txt", "D:/tmp/") → 下载目录下所有txt
|
||||
* - download(account, "/data", "app*.zip", "D:/tmp/") → 匹配 app 开头的 zip
|
||||
* - download(account, "/data", "test.txt", "D:/tmp/test.txt") → 精确下载单个文件
|
||||
*
|
||||
* @param account SSH账号
|
||||
* @param remoteDir 远程目录,如 /data/logs
|
||||
* @param fileName 文件名或通配符,如 *.txt、app*.zip、test.txt
|
||||
* @param localPath 本地保存路径。以 / 或 \ 结尾则视为目录,自动用远程文件名;否则视为完整路径
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static SftpResult download(MySftpAccounts account, String remoteDir, String fileName, String localPath) {
|
||||
ChannelSftp channel = null;
|
||||
try {
|
||||
channel = openChannel(account);
|
||||
Vector<ChannelSftp.LsEntry> entries = channel.ls(remoteDir);
|
||||
boolean toDir = isDirectory(localPath);
|
||||
String outDir = normalizeDir(localPath);
|
||||
List<String> downloaded = new ArrayList<>();
|
||||
int skipped = 0;
|
||||
for (ChannelSftp.LsEntry entry : entries) {
|
||||
if (entry.getAttrs().isDir()) continue;
|
||||
if (!globMatch(fileName, entry.getFilename())) continue;
|
||||
String remoteFile = normalizeDir(remoteDir) + entry.getFilename();
|
||||
String target = toDir ? outDir + entry.getFilename() : localPath;
|
||||
try {
|
||||
channel.get(remoteFile, target);
|
||||
downloaded.add(entry.getFilename());
|
||||
} catch (Exception e) {
|
||||
skipped++;
|
||||
}
|
||||
}
|
||||
if (downloaded.isEmpty()) {
|
||||
return SftpResult.fail("无匹配文件: " + remoteDir + "/" + fileName);
|
||||
}
|
||||
String msg = "下载完成,成功 " + downloaded.size() + " 个" + (skipped > 0 ? ",失败 " + skipped + " 个" : "");
|
||||
return SftpResult.ok(msg, downloaded);
|
||||
} catch (Exception e) {
|
||||
return SftpResult.fail("下载失败: " + e.getMessage());
|
||||
} finally {
|
||||
close(channel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载远程文件,返回字节数组(单文件,不支持通配符)
|
||||
*
|
||||
* @param account SSH账号
|
||||
* @param remoteDir 远程目录
|
||||
* @param fileName 文件名(精确)
|
||||
*/
|
||||
public static SftpResult download(MySftpAccounts account, String remoteDir, String fileName) {
|
||||
ChannelSftp channel = null;
|
||||
try {
|
||||
channel = openChannel(account);
|
||||
String remotePath = normalizeDir(remoteDir) + fileName;
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
channel.get(remotePath, out);
|
||||
return SftpResult.ok("下载成功", out.toByteArray());
|
||||
} catch (Exception e) {
|
||||
return SftpResult.fail("下载失败: " + e.getMessage());
|
||||
} finally {
|
||||
close(channel);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 删除
|
||||
|
||||
/**
|
||||
* 删除远程文件
|
||||
* 支持通配符文件名匹配
|
||||
* <p>
|
||||
* 示例:
|
||||
* - delete(account, "/data/logs", "*.log") → 删除所有 log 文件
|
||||
* - delete(account, "/data", "test.txt") → 删除单个文件
|
||||
*
|
||||
* @param account SSH账号
|
||||
* @param remoteDir 远程目录
|
||||
* @param fileName 文件名或通配符,如 *.log、test.txt
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static SftpResult delete(MySftpAccounts account, String remoteDir, String fileName) {
|
||||
ChannelSftp channel = null;
|
||||
try {
|
||||
channel = openChannel(account);
|
||||
Vector<ChannelSftp.LsEntry> entries = channel.ls(remoteDir);
|
||||
List<String> deleted = new ArrayList<>();
|
||||
for (ChannelSftp.LsEntry entry : entries) {
|
||||
if (entry.getAttrs().isDir()) continue;
|
||||
if (!globMatch(fileName, entry.getFilename())) continue;
|
||||
try {
|
||||
channel.rm(normalizeDir(remoteDir) + entry.getFilename());
|
||||
deleted.add(entry.getFilename());
|
||||
} catch (SftpException ignored) {
|
||||
}
|
||||
}
|
||||
if (deleted.isEmpty()) {
|
||||
return SftpResult.fail("无匹配文件: " + remoteDir + "/" + fileName);
|
||||
}
|
||||
return SftpResult.ok("删除完成,共 " + deleted.size() + " 个文件", deleted);
|
||||
} catch (Exception e) {
|
||||
return SftpResult.fail("删除失败: " + e.getMessage());
|
||||
} finally {
|
||||
close(channel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除远程空目录
|
||||
*
|
||||
* @param account SSH账号
|
||||
* @param remoteDir 远程目录路径
|
||||
*/
|
||||
public static SftpResult deleteDir(MySftpAccounts account, String remoteDir) {
|
||||
ChannelSftp channel = null;
|
||||
try {
|
||||
channel = openChannel(account);
|
||||
channel.rmdir(remoteDir);
|
||||
return SftpResult.ok("目录删除成功: " + remoteDir);
|
||||
} catch (Exception e) {
|
||||
return SftpResult.fail("目录删除失败: " + e.getMessage());
|
||||
} finally {
|
||||
close(channel);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 创建目录
|
||||
|
||||
/**
|
||||
* 创建远程目录(单级)
|
||||
*
|
||||
* @param account SSH账号
|
||||
* @param remoteDir 远程目录路径
|
||||
*/
|
||||
public static SftpResult mkdir(MySftpAccounts account, String remoteDir) {
|
||||
ChannelSftp channel = null;
|
||||
try {
|
||||
channel = openChannel(account);
|
||||
channel.mkdir(remoteDir);
|
||||
return SftpResult.ok("目录创建成功: " + remoteDir);
|
||||
} catch (Exception e) {
|
||||
return SftpResult.fail("目录创建失败: " + e.getMessage());
|
||||
} finally {
|
||||
close(channel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归创建远程目录(多级,类似 mkdir -p)
|
||||
*
|
||||
* @param account SSH账号
|
||||
* @param remoteDir 远程目录路径,如 /data/a/b/c
|
||||
*/
|
||||
public static SftpResult mkdirs(MySftpAccounts account, String remoteDir) {
|
||||
ChannelSftp channel = null;
|
||||
try {
|
||||
channel = openChannel(account);
|
||||
mkdirsInternal(channel, remoteDir);
|
||||
return SftpResult.ok("目录创建成功: " + remoteDir);
|
||||
} catch (Exception e) {
|
||||
return SftpResult.fail("目录创建失败: " + e.getMessage());
|
||||
} finally {
|
||||
close(channel);
|
||||
}
|
||||
}
|
||||
|
||||
private static void mkdirsInternal(ChannelSftp channel, String path) throws SftpException {
|
||||
String[] parts = path.replaceAll("^/", "").split("/");
|
||||
StringBuilder current = new StringBuilder(path.startsWith("/") ? "/" : "");
|
||||
for (String part : parts) {
|
||||
if (part.isEmpty()) continue;
|
||||
current.append(part);
|
||||
try {
|
||||
channel.stat(current.toString());
|
||||
} catch (SftpException e) {
|
||||
if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) {
|
||||
channel.mkdir(current.toString());
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
current.append("/");
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 系统监控
|
||||
|
||||
/**
|
||||
* 获取远程主机系统信息:CPU、内存、磁盘、负载、在线时长
|
||||
* 通过 SSH exec 执行系统命令采集
|
||||
*
|
||||
* @param account SSH账号
|
||||
* @return SftpResult,data 为 Map 结构
|
||||
*/
|
||||
public static SftpResult systemInfo(MySftpAccounts account) {
|
||||
Session session = null;
|
||||
ChannelExec exec = null;
|
||||
try {
|
||||
session = openSession(account);
|
||||
exec = (ChannelExec) session.openChannel("exec");
|
||||
// 一次性采集所有指标
|
||||
exec.setCommand(
|
||||
"echo '===HOSTNAME===' && hostname && " +
|
||||
"echo '===UPTIME===' && uptime -p 2>/dev/null || uptime && " +
|
||||
"echo '===CPU===' && top -bn1 | head -5 && " +
|
||||
"echo '===MEM===' && free -m && " +
|
||||
"echo '===DISK===' && df -h"
|
||||
);
|
||||
InputStream in = exec.getInputStream();
|
||||
InputStream errIn = exec.getErrStream();
|
||||
exec.connect(SSH_TIMEOUT);
|
||||
|
||||
String output = readStream(in);
|
||||
String err = readStream(errIn);
|
||||
if (!err.isEmpty() && output.isEmpty()) {
|
||||
return SftpResult.fail("采集失败: " + err);
|
||||
}
|
||||
|
||||
Map<String, Object> info = new LinkedHashMap<>();
|
||||
info.put("hostname", extract(output, "HOSTNAME", true));
|
||||
info.put("uptime", extract(output, "UPTIME", true));
|
||||
info.put("cpu", parseCpu(extract(output, "CPU", false)));
|
||||
info.put("memory", parseMemory(extract(output, "MEM", false)));
|
||||
info.put("disk", parseDisk(extract(output, "DISK", false)));
|
||||
return SftpResult.ok("采集成功", info);
|
||||
} catch (Exception e) {
|
||||
return SftpResult.fail("采集失败: " + e.getMessage());
|
||||
} finally {
|
||||
if (exec != null && exec.isConnected()) exec.disconnect();
|
||||
if (session != null && session.isConnected()) session.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// ---- session 工具 ----
|
||||
|
||||
private static Session openSession(MySftpAccounts account) throws JSchException {
|
||||
JSch jsch = new JSch();
|
||||
int port = Optional.ofNullable(account.getHostPort()).orElse(22);
|
||||
if ("key".equalsIgnoreCase(account.getAuthType())
|
||||
&& StringUtils.isNotBlank(account.getPrivateKey())) {
|
||||
jsch.addIdentity("tempKey",
|
||||
account.getPrivateKey().getBytes(StandardCharsets.UTF_8),
|
||||
null, null);
|
||||
}
|
||||
Session session = jsch.getSession(account.getUsername(), account.getHostIp(), port);
|
||||
session.setTimeout(SSH_TIMEOUT);
|
||||
if (!"key".equalsIgnoreCase(account.getAuthType())) {
|
||||
session.setPassword(account.getPassword());
|
||||
}
|
||||
Properties config = new Properties();
|
||||
config.put("StrictHostKeyChecking", "no");
|
||||
session.setConfig(config);
|
||||
session.connect(SSH_TIMEOUT);
|
||||
return session;
|
||||
}
|
||||
|
||||
private static String readStream(InputStream in) throws IOException {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
byte[] buf = new byte[4096];
|
||||
int len;
|
||||
while ((len = in.read(buf)) != -1) {
|
||||
out.write(buf, 0, len);
|
||||
}
|
||||
return out.toString(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
// ---- 解析工具 ----
|
||||
|
||||
/**
|
||||
* 按 ===MARK=== 分割提取段落
|
||||
*/
|
||||
private static String extract(String output, String mark, boolean singleLine) {
|
||||
String start = "===" + mark + "===";
|
||||
int s = output.indexOf(start);
|
||||
if (s < 0) return "";
|
||||
s += start.length();
|
||||
if (s >= output.length()) return "";
|
||||
// 跳过紧跟的换行
|
||||
if (output.charAt(s) == '\n') s++;
|
||||
if (singleLine) {
|
||||
int e = output.indexOf('\n', s);
|
||||
return (e < 0 ? output.substring(s) : output.substring(s, e)).trim();
|
||||
}
|
||||
// 取到下一个 === 或末尾
|
||||
int e = output.indexOf("\n===", s);
|
||||
return (e < 0 ? output.substring(s) : output.substring(s, e)).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 CPU 段(top -bn1 输出头部)
|
||||
* 格式:Cpu(s): 1.2 us, 0.3 sy, 0.0 ni, 98.3 id, 0.1 wa, 0.1 si
|
||||
*/
|
||||
private static Map<String, String> parseCpu(String block) {
|
||||
Map<String, String> cpu = new LinkedHashMap<>();
|
||||
for (String line : block.split("\n")) {
|
||||
if (line.startsWith("Cpu") || line.startsWith("%Cpu")) {
|
||||
// 统一去掉 %Cpu(s): 前缀
|
||||
String data = line.replaceFirst(".*?:\\s*", "").replaceFirst("^%Cpu\\(s?\\):\\s*", "");
|
||||
for (String part : data.split(",")) {
|
||||
part = part.trim();
|
||||
String[] kv = part.split("\\s+", 2);
|
||||
if (kv.length >= 2) {
|
||||
cpu.put(kv[1].trim(), kv[0].trim());
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return cpu;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析内存段(free -m 输出)
|
||||
* Mem: total used free shared buff/cache available
|
||||
*/
|
||||
private static Map<String, String> parseMemory(String block) {
|
||||
Map<String, String> mem = new LinkedHashMap<>();
|
||||
for (String line : block.split("\n")) {
|
||||
if (line.startsWith("Mem:")) {
|
||||
String[] cols = line.trim().split("\\s+");
|
||||
if (cols.length >= 7) {
|
||||
mem.put("total", cols[1] + " MB");
|
||||
mem.put("used", cols[2] + " MB");
|
||||
mem.put("free", cols[3] + " MB");
|
||||
mem.put("available", cols[6] + " MB");
|
||||
// 使用率
|
||||
try {
|
||||
double total = Double.parseDouble(cols[1]);
|
||||
double used = Double.parseDouble(cols[2]);
|
||||
double pct = total > 0 ? used / total * 100 : 0;
|
||||
mem.put("usagePercent", String.format("%.1f%%", pct));
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return mem;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析磁盘段(df -h 输出)
|
||||
* Filesystem Size Used Avail Use% Mounted on
|
||||
*/
|
||||
private static List<Map<String, String>> parseDisk(String block) {
|
||||
List<Map<String, String>> disks = new ArrayList<>();
|
||||
String[] lines = block.split("\n");
|
||||
for (int i = 0; i < lines.length; i++) {
|
||||
String line = lines[i].trim();
|
||||
if (i == 0 && (line.startsWith("Filesystem") || line.startsWith("文件系统"))) continue;
|
||||
if (line.isEmpty()) continue;
|
||||
String[] cols = line.split("\\s+");
|
||||
if (cols.length >= 6) {
|
||||
Map<String, String> d = new LinkedHashMap<>();
|
||||
d.put("filesystem", cols[0]);
|
||||
d.put("size", cols[1]);
|
||||
d.put("used", cols[2]);
|
||||
d.put("avail", cols[3]);
|
||||
d.put("usePercent", cols[4]);
|
||||
d.put("mounted", cols[5]);
|
||||
disks.add(d);
|
||||
}
|
||||
}
|
||||
return disks;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 重命名/移动
|
||||
|
||||
/**
|
||||
* 重命名或移动远程文件/目录
|
||||
*
|
||||
* @param account SSH账号
|
||||
* @param oldPath 原路径
|
||||
* @param newPath 新路径
|
||||
*/
|
||||
public static SftpResult rename(MySftpAccounts account, String oldPath, String newPath) {
|
||||
ChannelSftp channel = null;
|
||||
try {
|
||||
channel = openChannel(account);
|
||||
channel.rename(oldPath, newPath);
|
||||
return SftpResult.ok("重命名成功: " + oldPath + " → " + newPath);
|
||||
} catch (Exception e) {
|
||||
return SftpResult.fail("重命名失败: " + e.getMessage());
|
||||
} finally {
|
||||
close(channel);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 列表
|
||||
|
||||
/**
|
||||
* 列出远程目录下的文件列表(含MD5)
|
||||
* 对非目录文件通过 SSH exec 批量执行 md5sum 获取摘要
|
||||
*
|
||||
* @param account SSH账号
|
||||
* @param remoteDir 远程目录路径
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static SftpResult list(MySftpAccounts account, String remoteDir) {
|
||||
ChannelSftp channel = null;
|
||||
try {
|
||||
channel = openChannel(account);
|
||||
Vector<ChannelSftp.LsEntry> entries = channel.ls(remoteDir);
|
||||
String dir = normalizeDir(remoteDir);
|
||||
|
||||
// 收集非目录文件名
|
||||
List<String> fileNames = new ArrayList<>();
|
||||
for (ChannelSftp.LsEntry entry : entries) {
|
||||
if (".".equals(entry.getFilename()) || "..".equals(entry.getFilename())) continue;
|
||||
if (!entry.getAttrs().isDir()) {
|
||||
fileNames.add(entry.getFilename());
|
||||
}
|
||||
}
|
||||
|
||||
// 批量获取 MD5(一个 exec channel,一次 md5sum 命令)
|
||||
Map<String, String> md5Map = batchMd5(account, dir, fileNames);
|
||||
|
||||
List<Map<String, Object>> files = new ArrayList<>();
|
||||
for (ChannelSftp.LsEntry entry : entries) {
|
||||
if (".".equals(entry.getFilename()) || "..".equals(entry.getFilename())) continue;
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("name", entry.getFilename());
|
||||
item.put("size", entry.getAttrs().getSize());
|
||||
item.put("isDir", entry.getAttrs().isDir());
|
||||
item.put("permissions", entry.getAttrs().getPermissionsString());
|
||||
item.put("modifyTime", new Date((long) entry.getAttrs().getMTime() * 1000));
|
||||
item.put("md5", md5Map.getOrDefault(entry.getFilename(), ""));
|
||||
files.add(item);
|
||||
}
|
||||
return SftpResult.ok("获取列表成功", files);
|
||||
} catch (Exception e) {
|
||||
return SftpResult.fail("获取列表失败: " + e.getMessage());
|
||||
} finally {
|
||||
close(channel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量执行 md5sum,返回 {文件名: md5}
|
||||
*/
|
||||
private static Map<String, String> batchMd5(MySftpAccounts account, String dir, List<String> fileNames) {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
if (fileNames.isEmpty()) return result;
|
||||
|
||||
Session session = null;
|
||||
ChannelExec exec = null;
|
||||
try {
|
||||
JSch jsch = new JSch();
|
||||
int port = Optional.ofNullable(account.getHostPort()).orElse(22);
|
||||
if ("key".equalsIgnoreCase(account.getAuthType())
|
||||
&& StringUtils.isNotBlank(account.getPrivateKey())) {
|
||||
jsch.addIdentity("tempKey",
|
||||
account.getPrivateKey().getBytes(StandardCharsets.UTF_8),
|
||||
null, null);
|
||||
}
|
||||
session = jsch.getSession(account.getUsername(), account.getHostIp(), port);
|
||||
session.setTimeout(SSH_TIMEOUT);
|
||||
if (!"key".equalsIgnoreCase(account.getAuthType())) {
|
||||
session.setPassword(account.getPassword());
|
||||
}
|
||||
Properties config = new Properties();
|
||||
config.put("StrictHostKeyChecking", "no");
|
||||
session.setConfig(config);
|
||||
session.connect(SSH_TIMEOUT);
|
||||
|
||||
exec = (ChannelExec) session.openChannel("exec");
|
||||
// 构建批量命令:md5sum dir/file1 dir/file2 ...
|
||||
StringBuilder cmd = new StringBuilder("md5sum");
|
||||
for (String name : fileNames) {
|
||||
cmd.append(" ").append(dir).append(name);
|
||||
}
|
||||
exec.setCommand(cmd.toString());
|
||||
ByteArrayOutputStream errOut = new ByteArrayOutputStream();
|
||||
exec.setErrStream(errOut);
|
||||
InputStream in = exec.getInputStream();
|
||||
exec.connect(SSH_TIMEOUT);
|
||||
|
||||
// 读取输出,格式:hash ./filepath 或 hash filepath
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (line.isEmpty()) continue;
|
||||
// md5sum 输出格式:<32位hex> <filepath>
|
||||
String[] parts = line.split("\\s+", 2);
|
||||
if (parts.length < 2) continue;
|
||||
String md5 = parts[0];
|
||||
String filepath = parts[1];
|
||||
// 去掉可能的 ./ 前缀,取纯文件名
|
||||
String name = filepath;
|
||||
if (name.startsWith("./")) name = name.substring(2);
|
||||
int slashIdx = name.lastIndexOf('/');
|
||||
if (slashIdx >= 0) name = name.substring(slashIdx + 1);
|
||||
// 处理文件名含 \0 的情况(md5sum -b 二进制模式输出)
|
||||
name = name.replace("\\", "");
|
||||
result.put(name, md5);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// MD5 获取失败不影响列表,静默返回空
|
||||
} finally {
|
||||
if (exec != null && exec.isConnected()) exec.disconnect();
|
||||
if (session != null && session.isConnected()) session.disconnect();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 通配符 & 路径工具
|
||||
|
||||
/**
|
||||
* 简易 glob 匹配,支持 * 和 ?
|
||||
* 将 glob 转为正则:转义其他正则特殊字符,*→.* ?→.
|
||||
*/
|
||||
private static boolean globMatch(String pattern, String name) {
|
||||
if (pattern == null || name == null) return false;
|
||||
// 先统一转义,再替换通配符
|
||||
StringBuilder regex = new StringBuilder("^");
|
||||
for (char c : pattern.toCharArray()) {
|
||||
switch (c) {
|
||||
case '*':
|
||||
regex.append(".*");
|
||||
break;
|
||||
case '?':
|
||||
regex.append(".");
|
||||
break;
|
||||
case '.':
|
||||
case '(':
|
||||
case ')':
|
||||
case '[':
|
||||
case ']':
|
||||
case '{':
|
||||
case '}':
|
||||
case '+':
|
||||
case '^':
|
||||
case '$':
|
||||
case '|':
|
||||
case '\\':
|
||||
regex.append("\\").append(c);
|
||||
break;
|
||||
default:
|
||||
regex.append(c);
|
||||
}
|
||||
}
|
||||
regex.append("$");
|
||||
return name.matches(regex.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保路径以 / 结尾
|
||||
*/
|
||||
private static String normalizeDir(String dir) {
|
||||
if (dir == null) return "/";
|
||||
return dir.endsWith("/") ? dir : dir + "/";
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断本地路径是否为目录(简单判断以分隔符结尾)
|
||||
*/
|
||||
private static boolean isDirectory(String path) {
|
||||
return path != null && (path.endsWith("/") || path.endsWith("\\"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user