package com.ecep.contract.service; import com.ecep.contract.SpringApp; import com.ecep.contract.config.SmbConfig; import com.hierynomus.msdtyp.AccessMask; import com.hierynomus.mserref.NtStatus; import com.hierynomus.msfscc.FileAttributes; import com.hierynomus.msfscc.fileinformation.FileAllInformation; import com.hierynomus.mssmb2.SMB2CreateDisposition; import com.hierynomus.mssmb2.SMB2CreateOptions; import com.hierynomus.mssmb2.SMB2ShareAccess; import com.hierynomus.mssmb2.SMBApiException; import com.hierynomus.protocol.transport.TransportException; import com.hierynomus.smbj.SMBClient; import com.hierynomus.smbj.auth.AuthenticationContext; import com.hierynomus.smbj.common.SMBRuntimeException; import com.hierynomus.smbj.common.SmbPath; import com.hierynomus.smbj.event.SessionLoggedOff; import com.hierynomus.smbj.share.DiskShare; import com.hierynomus.smbj.share.File; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.io.IOException; import java.io.OutputStream; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; /** * SMB文件服务类,提供SMB/CIFS协议的文件操作功能 */ @Slf4j @Service public class SmbFileService implements DisposableBean { private final SMBClient client; private final SmbConfig smbConfig; // 连接空闲超时时间:3分钟 private static final long CONNECTION_IDLE_TIMEOUT_MS = 3 * 60 * 1000; // Session信息内部类,用于存储session和最后使用时间 private static class SessionInfo implements AutoCloseable { private final com.hierynomus.smbj.session.Session session; private final String username; // 添加用户名字段 private volatile long lastUsedTimestamp; public SessionInfo(com.hierynomus.smbj.session.Session session, String username) { this.session = session; this.username = username; this.lastUsedTimestamp = System.currentTimeMillis(); } public String getUsername() { return username; } public com.hierynomus.smbj.session.Session getSession() { return session; } public long getLastUsedTimestamp() { return lastUsedTimestamp; } public void updateLastUsedTimestamp() { this.lastUsedTimestamp = System.currentTimeMillis(); } public boolean isIdle(long timeoutMs) { return (System.currentTimeMillis() - lastUsedTimestamp) > timeoutMs; } public void close() throws IOException { session.close(); } } // 连接信息内部类,用于存储连接和session连接池 private static class ConnectionInfo { private final com.hierynomus.smbj.connection.Connection connection; private volatile long lastUsedTimestamp; // Session连接池,使用List存储 private final List sessionPool; public ConnectionInfo(com.hierynomus.smbj.connection.Connection connection) { this.connection = connection; this.lastUsedTimestamp = System.currentTimeMillis(); this.sessionPool = Collections.synchronizedList(new ArrayList<>()); } public com.hierynomus.smbj.connection.Connection getConnection() { return connection; } public long getLastUsedTimestamp() { return lastUsedTimestamp; } public void updateLastUsedTimestamp() { this.lastUsedTimestamp = System.currentTimeMillis(); } public boolean isIdle(long timeoutMs) { return (System.currentTimeMillis() - lastUsedTimestamp) > timeoutMs; } // 清理空闲的session public int cleanupIdleSessions(long timeoutMs) { List idleSessions = new java.util.ArrayList<>(); // 查找所有空闲session for (SessionInfo sessionInfo : sessionPool) { if (sessionInfo != null && sessionInfo.isIdle(timeoutMs)) { idleSessions.add(sessionInfo); } } // 关闭并移除空闲session synchronized (sessionPool) { for (SessionInfo sessionInfo : idleSessions) { if (sessionInfo != null && sessionPool.contains(sessionInfo)) { try { sessionInfo.close(); } catch (IOException e) { log.error("Error closing idle session for username: {}", sessionInfo.getUsername(), e); } sessionPool.remove(sessionInfo); } } } return idleSessions.size(); } // 从session池中获取任意一个有效的session public SessionInfo peekSession() { if (sessionPool.isEmpty()) { return null; } return sessionPool.removeFirst(); } // 创建新session并添加到池中 public SessionInfo createSession(AuthenticationContext authContext) throws IOException { String username = authContext.getUsername(); // 创建新session com.hierynomus.smbj.session.Session session = connection.authenticate(authContext); SessionInfo newSession = new SessionInfo(session, username); return newSession; } // 更新session的最后使用时间 public void returnSession(SessionInfo sessionInfo) { // 重新添加到池中 sessionPool.addLast(sessionInfo); sessionInfo.updateLastUsedTimestamp(); } // 关闭所有session public void closeAllSessions() { // 创建副本以避免并发修改异常 List sessionsCopy = new ArrayList<>(); // 先获取副本并清空池 synchronized (sessionPool) { sessionsCopy.addAll(sessionPool); sessionPool.clear(); } // 关闭所有session for (SessionInfo sessionInfo : sessionsCopy) { try { if (sessionInfo != null) { sessionInfo.close(); } } catch (IOException e) { log.error("Error closing session for username: {}", sessionInfo.getUsername(), e); } } } } // 连接池,使用ConcurrentHashMap确保线程安全 private final Map connectionPool = new ConcurrentHashMap<>(); // 连接池锁,用于同步连接的创建和关闭 private final ReentrantLock connectionPoolLock = new ReentrantLock(); // 定时清理线程池 private final ScheduledExecutorService cleanupScheduler; /** * 构造函数,注入SMB客户端和配置,初始化定时清理任务 * * @param smbClient SMB客户端实例 */ public SmbFileService(@Autowired SMBClient smbClient, @Autowired ScheduledExecutorService executor) { this.client = smbClient; this.smbConfig = SpringApp.getBean(SmbConfig.class); this.smbConfig.subscribe(this); // 初始化定时清理任务,每30秒运行一次 this.cleanupScheduler = executor; // 启动定时清理任务,延迟1分钟后开始,每30秒执行一次 this.cleanupScheduler.scheduleAtFixedRate(this::cleanupIdleConnections, 1, 30, TimeUnit.SECONDS); } // // @net.engio.mbassy.listener.Handler // private void onSessionLoggedOff(SessionLoggedOff sessionLoggedOffEvent) { // // } /** * 获取认证上下文,根据主机名获取对应的认证信息 * * @param host 主机名 * @return 认证上下文 * @throws IOException 如果找不到对应的服务器配置 */ private AuthenticationContext getAuthenticationContext(String host) throws IOException { log.debug("Creating AuthenticationContext for host: {}", host); // 获取该主机对应的配置信息 SmbConfig.ServerConfig serverConfig = smbConfig.getServerConfig(host); // 检查是否找到配置 if (serverConfig == null) { String errorMsg = String.format("No SMB configuration found for host: %s", host); log.error(errorMsg); throw new IOException(errorMsg); } // 检查配置是否完整 if (serverConfig.getUsername() == null || serverConfig.getPassword() == null) { String errorMsg = String.format("Incomplete SMB configuration for host: %s, username or password missing", host); log.error(errorMsg); throw new IOException(errorMsg); } return new AuthenticationContext( serverConfig.getUsername(), serverConfig.getPassword().toCharArray(), ""); } /** * 从连接池获取或创建连接 * * @param hostname 主机名 * @return SMB连接 * @throws IOException 如果创建连接失败 */ private ConnectionInfo getConnectionInfo(String hostname) throws IOException { // 首先检查连接池是否已有该主机的连接 com.hierynomus.smbj.connection.Connection connection = null; int maxTrys = 3; while (maxTrys-- > 0) { ConnectionInfo connectionInfo = connectionPool.get(hostname); // 如果连接存在且有效,则更新最后使用时间并返回 if (connectionInfo != null) { connection = connectionInfo.getConnection(); if (connection != null && connection.isConnected()) { // 更新连接的最后使用时间 connectionInfo.updateLastUsedTimestamp(); log.debug("Reusing SMB connection for host: {}", hostname); return connectionInfo; } log.debug("Closing invalid SMB connection for host: {}", hostname); connectionInfo.closeAllSessions(); } // 如果连接不存在或已关闭,则创建新连接 connectionPoolLock.lock(); try { // 创建新连接 log.debug("Creating new SMB connection for host: {}", hostname); connection = client.connect(hostname); connectionInfo = new ConnectionInfo(connection); connectionPool.put(hostname, connectionInfo); return connectionInfo; } finally { connectionPoolLock.unlock(); } } return null; } /** * 从连接池获取或创建连接(兼容旧方法签名) * * @param hostname 主机名 * @return SMB连接 * @throws IOException 如果创建连接失败 */ private com.hierynomus.smbj.connection.Connection getConnection(String hostname) throws IOException { return getConnectionInfo(hostname).getConnection(); } // Session空闲超时时间:2分钟(比连接超时时间短) private static final long SESSION_IDLE_TIMEOUT_MS = 2 * 60 * 1000; /** * 清理空闲连接和session的定时任务 * 1. 检查并关闭所有超时的session * 2. 检查并关闭所有超时的连接 */ private void cleanupIdleConnections() { log.debug( "Running idle connections and sessions cleanup task with session timeout: {}ms, connection timeout: {}ms", SESSION_IDLE_TIMEOUT_MS, CONNECTION_IDLE_TIMEOUT_MS); // 创建要移除的连接列表,避免在迭代时修改Map List idleHostnames = new java.util.ArrayList<>(); int totalClosedSessions = 0; // 首先清理每个连接中的空闲session for (Map.Entry entry : connectionPool.entrySet()) { String hostname = entry.getKey(); ConnectionInfo connectionInfo = entry.getValue(); if (connectionInfo != null) { // 清理该连接下的空闲session - 检查session是否超时,超时则关闭 log.debug("Checking for idle sessions on host: {}", hostname); int closedSessions = connectionInfo.cleanupIdleSessions(SESSION_IDLE_TIMEOUT_MS); totalClosedSessions += closedSessions; if (closedSessions > 0) { log.debug("Closed {} idle/expired sessions for host: {}", closedSessions, hostname); } // 然后检查连接是否空闲超时 if (connectionInfo.isIdle(CONNECTION_IDLE_TIMEOUT_MS)) { idleHostnames.add(hostname); log.debug("Found idle connection for host: {}, will be closed", hostname); } } } // 关闭并移除空闲连接 if (!idleHostnames.isEmpty()) { connectionPoolLock.lock(); try { for (String hostname : idleHostnames) { ConnectionInfo connectionInfo = connectionPool.get(hostname); if (connectionInfo != null) { try { // 先关闭所有session connectionInfo.closeAllSessions(); log.debug("Closed all remaining sessions for host: {}", hostname); // 再关闭连接 log.debug("Closing idle connection for host: {}", hostname); connectionInfo.getConnection().close(); } catch (IOException e) { log.error("Error closing idle connection for host: {}", hostname, e); } finally { connectionPool.remove(hostname); log.debug("Removed connection from pool for host: {}", hostname); } } } } finally { connectionPoolLock.unlock(); } } log.debug( "Idle connections and sessions cleanup completed successfully. Results: closed {} connections and {} expired sessions", idleHostnames.size(), totalClosedSessions); } /** * 执行SMB操作的通用方法,使用连接池和session池 * * @param smbPath SMB路径 * @param operation 要执行的操作 * @param 操作返回类型 * @return 操作结果 * @throws IOException 如果操作失败 */ private T executeSmbOperation(SmbPath smbPath, SmbOperation operation) throws IOException { String hostname = smbPath.getHostname(); ConnectionInfo connectionInfo = null; SessionInfo sessionInfo = null; T re = null; // 尝试执行获取连接执行操作,当发生 TransportException 时 尝试重试,最多3次 int maxTrys = 3; while (maxTrys-- > 0) { try { // 获取连接 connectionInfo = getConnectionInfo(hostname); // 从session池获取session sessionInfo = connectionInfo.peekSession(); // 如果session不存在 if (sessionInfo == null) { // 获取认证上下文 AuthenticationContext authContext = getAuthenticationContext(hostname); // 创建新session并添加到池中 sessionInfo = connectionInfo.createSession(authContext); log.debug("Created new SMB session for host: {}", hostname); } else { log.debug("Reusing SMB session for host: {}", hostname); } // 连接共享 try (var share = (DiskShare) sessionInfo.getSession().connectShare(smbPath.getShareName())) { re = operation.execute(share, smbPath.getPath()); // 操作完成后更新session的最后使用时间,将session放回池中 connectionInfo.returnSession(sessionInfo); log.debug("Returned SMB session to pool for host: {}", hostname); } catch (SMBRuntimeException e) { sessionInfo.close(); throw e; } finally { } break; } catch (TransportException e) { log.warn("TransportException occurred while trying to connect to host: {}. Retrying...", hostname); // 延迟1秒 try { Thread.sleep(1000); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } continue; } catch (IOException e) { // 如果操作失败且连接信息存在,检查连接状态 if (connectionInfo != null) { com.hierynomus.smbj.connection.Connection connection = connectionInfo.getConnection(); if (connection != null && !connection.isConnected()) { // 从连接池移除失效的连接,并关闭所有session connectionInfo.closeAllSessions(); connectionPool.remove(hostname); log.debug("Removed disconnected SMB connection from pool for host: {}", hostname); } } throw e; } } return re; } /** * 上传文件到SMB服务器 * * @param filePath 文件路径 * @param fileContent 文件内容字节数组 * @throws IOException 如果上传失败 */ public void uploadFile(String filePath, byte[] fileContent) throws IOException { Objects.requireNonNull(filePath, "File path cannot be null"); Objects.requireNonNull(fileContent, "File content cannot be null"); log.debug("Uploading file: {} with size: {} bytes", filePath, fileContent.length); var smbPath = SmbPath.parse(filePath); executeSmbOperation(smbPath, (share, path) -> { // 创建目录(如果不存在) String directoryPath = path.substring(0, path.lastIndexOf('/')); if (!share.folderExists(directoryPath)) { share.mkdir(directoryPath); log.debug("Created directory: {}", directoryPath); } // 上传文件内容 try (File smbFile = share.openFile(smbPath.getPath(), EnumSet.of(AccessMask.GENERIC_WRITE), EnumSet.of(FileAttributes.FILE_ATTRIBUTE_NORMAL), EnumSet.of(SMB2ShareAccess.FILE_SHARE_WRITE), SMB2CreateDisposition.FILE_CREATE, EnumSet.noneOf(SMB2CreateOptions.class))) { try (OutputStream out = smbFile.getOutputStream()) { out.write(fileContent); } } log.info("Successfully uploaded file: {}", filePath); return null; }); } /** * 检查SMB路径是否存在 * * @param smbPath SMB路径 * @return 如果路径存在则返回true * @throws IOException 如果检查失败 */ public boolean exists(SmbPath smbPath) throws IOException { return executeSmbOperation(smbPath, (share, path) -> { try { FileAllInformation info = share.getFileInformation(path); if (info.getStandardInformation().isDirectory()) { return share.folderExists(path); } return share.fileExists(path); } catch (SMBApiException e) { if (e.getStatus().equals(NtStatus.STATUS_OBJECT_NAME_NOT_FOUND)) { return false; } log.error("Error checking if path exists: {}", path, e); throw e; } }); } /** * 检查文件是否存在(基于File对象) * * @param file 文件对象 * @return 如果文件存在则返回true */ public boolean exists(java.io.File file) { Objects.requireNonNull(file, "File cannot be null"); try { var smbPath = SmbPath.parse(file.getAbsolutePath()); return exists(smbPath); } catch (IOException e) { log.error("Error checking if file exists: {}", file.getAbsolutePath(), e); return false; } } /** * 创建目录 * * @param path 要创建的目录路径 * @throws IOException 如果创建失败 */ public void mkdir(java.io.File path) throws IOException { Objects.requireNonNull(path, "Path cannot be null"); var smbPath = SmbPath.parse(path.getAbsolutePath()); executeSmbOperation(smbPath, (share, smbFilePath) -> { if (!share.folderExists(smbFilePath)) { share.mkdir(smbFilePath); log.debug("Created directory: {}", smbFilePath); } return null; }); } /** * 列出目录中的文件(不包括子目录) * * @param dir 目录对象 * @return 文件列表 * @throws IOException 如果列出失败 */ public List listFiles(java.io.File dir) throws IOException { Objects.requireNonNull(dir, "Directory cannot be null"); var smbPath = SmbPath.parse(dir.getAbsolutePath()); return executeSmbOperation(smbPath, (share, path) -> { try { FileAllInformation info = share.getFileInformation(path); if (info.getStandardInformation().isDirectory() && share.folderExists(path)) { var files = share.list(path); return files.stream() .filter(f -> !f.getFileName().startsWith(".") && !f.getFileName().equals("..")) .filter(f -> { try { String fullPath = path + "\\" + f.getFileName(); return !share.getFileInformation(fullPath).getStandardInformation().isDirectory(); } catch (SMBApiException e) { log.warn("Error checking file type for: {}", f.getFileName(), e); return false; } }) .map(f -> new java.io.File(dir, f.getFileName())) .toList(); } } catch (SMBApiException e) { log.error("Error listing files in directory: {}", path, e); } return Collections.emptyList(); }); } /** * 检查是否为文件(非目录) * 修复:之前的实现逻辑错误,现在正确返回是否为文件 * * @param file 文件对象 * @return 如果是文件(非目录)则返回true * @throws IOException 如果检查失败 */ public boolean isFile(java.io.File file) throws IOException { Objects.requireNonNull(file, "File cannot be null"); var smbPath = SmbPath.parse(file.getAbsolutePath()); return executeSmbOperation(smbPath, (share, path) -> { FileAllInformation fileInformation = share.getFileInformation(path); // 修复:返回是否不是目录,即是否为文件 return !fileInformation.getStandardInformation().isDirectory(); }); } /** * 检查是否为目录 * * @param file 文件对象 * @return 如果是目录则返回true * @throws IOException 如果检查失败 */ public boolean isDirectory(java.io.File file) throws IOException { Objects.requireNonNull(file, "File cannot be null"); var smbPath = SmbPath.parse(file.getAbsolutePath()); return executeSmbOperation(smbPath, (share, path) -> { FileAllInformation fileInformation = share.getFileInformation(path); return fileInformation.getStandardInformation().isDirectory(); }); } @Override public void destroy() throws Exception { shutdown(); } /** * 关闭并清理所有连接和session资源 */ public void shutdown() { log.debug("Shutting down SMB connection pool and sessions"); // 关闭定时清理任务 try { cleanupScheduler.shutdown(); if (!cleanupScheduler.awaitTermination(5, TimeUnit.SECONDS)) { cleanupScheduler.shutdownNow(); } } catch (InterruptedException e) { cleanupScheduler.shutdownNow(); Thread.currentThread().interrupt(); } // 关闭所有连接和session connectionPoolLock.lock(); try { for (Map.Entry entry : connectionPool.entrySet()) { String hostname = entry.getKey(); ConnectionInfo connectionInfo = entry.getValue(); try { // 先关闭所有session connectionInfo.closeAllSessions(); // 再关闭连接 log.debug("Closing connection to host: {}", hostname); connectionInfo.getConnection().close(); } catch (IOException e) { log.error("Error closing connection or sessions to host: {}", hostname, e); } } connectionPool.clear(); // 关闭SMB客户端 client.close(); } finally { connectionPoolLock.unlock(); } } /** * SMB操作接口,用于执行具体的SMB操作 */ @FunctionalInterface private interface SmbOperation { T execute(DiskShare share, String path) throws IOException; } }