feat: 实现SMB文件服务并优化合同文件管理
- 新增SmbFileService服务类,支持SMB/CIFS协议的文件操作 - 修改合同文件管理逻辑,支持SMB路径检查与目录创建 - 优化BankTableCell实现工厂模式并更新相关文档 - 调整Redis配置并添加连接测试 - 修复合同发票视图模型的时间处理问题 - 更新项目版本至0.0.134-SNAPSHOT
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package com.ecep.contract.cloud.u8.ctx;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.text.NumberFormat;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
@@ -50,6 +51,7 @@ import com.ecep.contract.ds.vendor.model.Vendor;
|
||||
import com.ecep.contract.ds.vendor.model.VendorEntity;
|
||||
import com.ecep.contract.ds.vendor.service.VendorEntityService;
|
||||
import com.ecep.contract.ds.vendor.service.VendorService;
|
||||
import com.ecep.contract.service.SmbFileService;
|
||||
import com.ecep.contract.util.BeanContext;
|
||||
import com.ecep.contract.util.FileUtils;
|
||||
import com.ecep.contract.util.NumberUtils;
|
||||
@@ -63,6 +65,7 @@ import com.ecep.contract.vo.CustomerVo;
|
||||
import com.ecep.contract.vo.ProjectSaleTypeVo;
|
||||
import com.ecep.contract.vo.ProjectVo;
|
||||
import com.ecep.contract.vo.VendorEntityVo;
|
||||
import com.hierynomus.smbj.common.SmbPath;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
@@ -995,13 +998,20 @@ public class ContractCtx extends AbstractYongYouU8Ctx {
|
||||
}
|
||||
|
||||
public boolean updateContractPath(ContractVo contract, MessageHolder holder) {
|
||||
// 如果合同路径存在
|
||||
if (CompanyFileUtils.exists(contract.getPath())) {
|
||||
File dir = new File(contract.getPath());
|
||||
if (dir.exists()) {
|
||||
return false;
|
||||
// 如果设置了合同路径,检查路径是否存在
|
||||
SmbFileService smbFileService = getCachedBean(SmbFileService.class);
|
||||
if (StringUtils.hasText(contract.getPath())) {
|
||||
var smbPath = SmbPath.parse(contract.getPath());
|
||||
try {
|
||||
if (smbFileService.exists(smbPath)) {
|
||||
return false;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
holder.warn("检查合同路径 " + smbPath + " 异常:" + e.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 尝试创建合同路径
|
||||
holder.debug("合同目录不存在,尝试创建");
|
||||
|
||||
@@ -1022,15 +1032,17 @@ public class ContractCtx extends AbstractYongYouU8Ctx {
|
||||
return false;
|
||||
}
|
||||
File contractPath = new File(parent.getPath());
|
||||
if (!contractPath.exists()) {
|
||||
if (!smbFileService.exists(contractPath)) {
|
||||
holder.debug("父合同 " + parentCode + " 目录不存在 " + parent.getPath());
|
||||
return false;
|
||||
}
|
||||
String contractDirName = contract.getCode();
|
||||
File path = new File(contractPath, contractDirName);
|
||||
if (!path.exists()) {
|
||||
if (!path.mkdir()) {
|
||||
holder.warn("创建目录失败 = " + path.getAbsolutePath());
|
||||
if (!smbFileService.exists(path)) {
|
||||
try {
|
||||
smbFileService.mkdir(path);
|
||||
} catch (IOException e) {
|
||||
holder.warn("创建目录失败 = " + path.getAbsolutePath() + ", 异常: " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1049,9 +1061,12 @@ public class ContractCtx extends AbstractYongYouU8Ctx {
|
||||
File dir = new File(saleType.getPath());
|
||||
if (saleType.isStoreByYear()) {
|
||||
dir = new File(dir, "20" + projectVo.getCodeYear());
|
||||
if (!dir.exists()) {
|
||||
if (dir.mkdir()) {
|
||||
holder.info("新建目录 " + dir.getAbsolutePath());
|
||||
if (!smbFileService.exists(dir)) {
|
||||
try {
|
||||
smbFileService.mkdir(dir);
|
||||
} catch (IOException e) {
|
||||
holder.warn("创建目录失败 = " + dir.getAbsolutePath() + ", 异常: " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1074,10 +1089,11 @@ public class ContractCtx extends AbstractYongYouU8Ctx {
|
||||
|
||||
String contractDirName = contract.getCode() + "-" + FileUtils.escapeFileName(contract.getName());
|
||||
File path = new File(contractPath, contractDirName);
|
||||
if (!path.exists()) {
|
||||
if (!path.mkdir()) {
|
||||
//
|
||||
holder.warn("创建目录失败 = " + path.getAbsolutePath());
|
||||
if (!smbFileService.exists(path)) {
|
||||
try {
|
||||
smbFileService.mkdir(path);
|
||||
} catch (IOException e) {
|
||||
holder.warn("创建目录失败 = " + path.getAbsolutePath() + ", 异常: " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1086,6 +1102,7 @@ public class ContractCtx extends AbstractYongYouU8Ctx {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
public boolean updateContractAmount(ContractVo contract, MessageHolder holder) {
|
||||
@@ -1190,8 +1207,10 @@ public class ContractCtx extends AbstractYongYouU8Ctx {
|
||||
if (!StringUtils.hasText(contractPath)) {
|
||||
return false;
|
||||
}
|
||||
SmbFileService smbFileService = getCachedBean(SmbFileService.class);
|
||||
// 检查合同目录是否存在
|
||||
File dir = new File(contractPath);
|
||||
if (!dir.exists()) {
|
||||
if (!smbFileService.exists(dir)) {
|
||||
holder.warn("合同目录不存在:" + contractPath);
|
||||
return false;
|
||||
}
|
||||
@@ -1228,11 +1247,11 @@ public class ContractCtx extends AbstractYongYouU8Ctx {
|
||||
}
|
||||
|
||||
// 遍历合同目录下的文件,如果未创建,创建
|
||||
File[] files = dir.listFiles();
|
||||
if (files != null) {
|
||||
try {
|
||||
List<File> files = smbFileService.listFiles(dir);
|
||||
for (File file : files) {
|
||||
// 只处理文件
|
||||
if (!file.isFile()) {
|
||||
if (!smbFileService.isFile(file)) {
|
||||
continue;
|
||||
}
|
||||
String fileName = file.getName();
|
||||
@@ -1247,6 +1266,9 @@ public class ContractCtx extends AbstractYongYouU8Ctx {
|
||||
syncContractFile(contractFile, file, holder);
|
||||
retrieveFiles.add(contractFile);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
holder.error("遍历合同目录下的文件失败:" + contractPath + ",错误:" + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (retrieveFiles.isEmpty()) {
|
||||
|
||||
@@ -253,8 +253,10 @@ public class CustomerCtx extends AbstractYongYouU8Ctx {
|
||||
updated = true;
|
||||
}
|
||||
|
||||
ContractCtx ctx = getContractCtx();
|
||||
ctx.initializeRepository(holder);
|
||||
for (CompanyCustomerEntity entity : entities) {
|
||||
if (getContractCtx().syncByCustomerEntity(companyCustomer, entity, holder)) {
|
||||
if (ctx.syncByCustomerEntity(companyCustomer, entity, holder)) {
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,8 +249,10 @@ public class VendorCtx extends AbstractYongYouU8Ctx {
|
||||
}
|
||||
|
||||
// 同步供应商关联的合同
|
||||
ContractCtx ctx = getContractCtx();
|
||||
ctx.initializeRepository(holder);
|
||||
for (VendorEntity entity : entities) {
|
||||
if (getContractCtx().syncByVendorEntity(vendor, entity, holder)) {
|
||||
if (ctx.syncByVendorEntity(vendor, entity, holder)) {
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
30
server/src/main/java/com/ecep/contract/config/SmbConfig.java
Normal file
30
server/src/main/java/com/ecep/contract/config/SmbConfig.java
Normal file
@@ -0,0 +1,30 @@
|
||||
package com.ecep.contract.config;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import com.hierynomus.smbj.SMBClient;
|
||||
import com.hierynomus.smbj.auth.NtlmAuthenticator;
|
||||
|
||||
@Configuration()
|
||||
public class SmbConfig {
|
||||
|
||||
@Value("${smb.server.username}")
|
||||
@Getter
|
||||
private String username;
|
||||
|
||||
@Value("${smb.server.password}")
|
||||
@Getter
|
||||
private String password;
|
||||
|
||||
@Bean
|
||||
public SMBClient smbClient() {
|
||||
var smbConfig = com.hierynomus.smbj.SmbConfig.builder()
|
||||
.withMultiProtocolNegotiate(true).withSigningRequired(true)
|
||||
// .withAuthenticators(new NtlmAuthenticator(username, password))
|
||||
.build();
|
||||
return new SMBClient(smbConfig);
|
||||
}
|
||||
}
|
||||
@@ -34,4 +34,7 @@ public class WebSocketConfig implements WebSocketConfigurer {
|
||||
new HttpSessionHandshakeInterceptor(List.of("JSESSIONID", "loginHistoryId", "employeeId")))
|
||||
.setAllowedOrigins("*");
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.ecep.contract.ds.company;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
@@ -9,12 +10,17 @@ import java.util.function.Function;
|
||||
|
||||
import com.ecep.contract.constant.CompanyConstant;
|
||||
import com.ecep.contract.util.FileUtils;
|
||||
import com.hierynomus.smbj.common.SmbPath;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import com.ecep.contract.CompanyFileType;
|
||||
import com.ecep.contract.MyDateTimeUtils;
|
||||
import com.ecep.contract.SpringApp;
|
||||
import com.ecep.contract.constant.CloudServiceConstant;
|
||||
import com.ecep.contract.ds.company.model.CompanyFile;
|
||||
import com.ecep.contract.service.SmbFileService;
|
||||
|
||||
public class CompanyFileUtils {
|
||||
|
||||
@@ -130,10 +136,24 @@ public class CompanyFileUtils {
|
||||
return modified;
|
||||
}
|
||||
|
||||
public static boolean exists(SmbPath path) throws IOException {
|
||||
return SpringApp.getBean(SmbFileService.class).exists(path);
|
||||
}
|
||||
|
||||
public static boolean exists(String path) {
|
||||
if (!StringUtils.hasText(path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (path.startsWith("\\\\")) {
|
||||
var smbPath = SmbPath.parse(path);
|
||||
try {
|
||||
return SpringApp.getBean(SmbFileService.class).exists(smbPath);
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
File file = new File(path);
|
||||
return file.exists();
|
||||
}
|
||||
|
||||
@@ -287,6 +287,7 @@ public abstract class AbstContractRepairTasker extends Tasker<Object> {
|
||||
SalesOrderCtx ctx = getSalesOrderCtx();
|
||||
ctx.initializeRepository(holder);
|
||||
if (ctx.getRepository() == null) {
|
||||
holder.warn("未启用 " + CloudServiceConstant.U8_NAME + " 服务");
|
||||
return;
|
||||
}
|
||||
List<SalesOrder> orders = ctx.syncByContract(contract, holder);
|
||||
@@ -294,6 +295,11 @@ public abstract class AbstContractRepairTasker extends Tasker<Object> {
|
||||
return;
|
||||
}
|
||||
SalesBillVoucherCtx voucherCtx = getSalesBillVoucherCtx();
|
||||
voucherCtx.initializeRepository(holder);
|
||||
if (voucherCtx.getRepository() == null) {
|
||||
holder.warn("未启用 " + CloudServiceConstant.U8_NAME + " 服务");
|
||||
return;
|
||||
}
|
||||
for (SalesOrder order : orders) {
|
||||
voucherCtx.syncBySalesOrder(order, holder);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,493 @@
|
||||
package com.ecep.contract.service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.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.smbj.SMBClient;
|
||||
import com.hierynomus.smbj.auth.AuthenticationContext;
|
||||
import com.hierynomus.smbj.common.SmbPath;
|
||||
import com.hierynomus.smbj.share.DiskShare;
|
||||
import com.hierynomus.smbj.share.File;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* SMB文件服务类,提供SMB/CIFS协议的文件操作功能
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class SmbFileService implements DisposableBean {
|
||||
private final SMBClient client;
|
||||
private AuthenticationContext authContext;
|
||||
private final SmbConfig smbConfig;
|
||||
private final ReentrantLock authLock = new ReentrantLock();
|
||||
|
||||
// 连接空闲超时时间:3分钟
|
||||
private static final long CONNECTION_IDLE_TIMEOUT_MS = 3 * 60 * 1000;
|
||||
|
||||
// 连接信息内部类,用于存储连接和最后使用时间
|
||||
private static class ConnectionInfo {
|
||||
private final com.hierynomus.smbj.connection.Connection connection;
|
||||
private volatile long lastUsedTimestamp;
|
||||
|
||||
public ConnectionInfo(com.hierynomus.smbj.connection.Connection connection) {
|
||||
this.connection = connection;
|
||||
this.lastUsedTimestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 连接池,使用ConcurrentHashMap确保线程安全
|
||||
private final Map<String, ConnectionInfo> 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);
|
||||
|
||||
// 初始化定时清理任务,每30秒运行一次
|
||||
this.cleanupScheduler = executor;
|
||||
|
||||
// 启动定时清理任务,延迟1分钟后开始,每30秒执行一次
|
||||
this.cleanupScheduler.scheduleAtFixedRate(this::cleanupIdleConnections, 1, 30, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取认证上下文,线程安全实现
|
||||
*
|
||||
* @param host 主机名
|
||||
* @return 认证上下文
|
||||
*/
|
||||
private AuthenticationContext getAuthenticationContext(String host) {
|
||||
// 双重检查锁定模式,确保线程安全
|
||||
if (authContext == null) {
|
||||
authLock.lock();
|
||||
try {
|
||||
if (authContext == null) {
|
||||
log.debug("Creating new AuthenticationContext for host: {}", host);
|
||||
authContext = new AuthenticationContext(
|
||||
smbConfig.getUsername(),
|
||||
smbConfig.getPassword().toCharArray(),
|
||||
"");
|
||||
}
|
||||
} finally {
|
||||
authLock.unlock();
|
||||
}
|
||||
}
|
||||
return authContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行SMB操作的通用方法,简化连接和会话的创建
|
||||
*
|
||||
* @param smbPath SMB路径
|
||||
* @param operation 要执行的操作
|
||||
* @param <T> 操作返回类型
|
||||
* @return 操作结果
|
||||
* @throws IOException 如果操作失败
|
||||
*/
|
||||
/**
|
||||
* 从连接池获取或创建连接
|
||||
*
|
||||
* @param hostname 主机名
|
||||
* @return SMB连接
|
||||
* @throws IOException 如果创建连接失败
|
||||
*/
|
||||
/**
|
||||
* 从连接池获取或创建连接
|
||||
*
|
||||
* @param hostname 主机名
|
||||
* @return SMB连接
|
||||
* @throws IOException 如果创建连接失败
|
||||
*/
|
||||
private com.hierynomus.smbj.connection.Connection getConnection(String hostname) throws IOException {
|
||||
// 首先检查连接池是否已有该主机的连接
|
||||
ConnectionInfo connectionInfo = connectionPool.get(hostname);
|
||||
com.hierynomus.smbj.connection.Connection connection = null;
|
||||
|
||||
// 如果连接存在且有效,则更新最后使用时间并返回
|
||||
if (connectionInfo != null) {
|
||||
connection = connectionInfo.getConnection();
|
||||
if (connection != null && connection.isConnected()) {
|
||||
// 更新连接的最后使用时间
|
||||
connectionInfo.updateLastUsedTimestamp();
|
||||
log.debug("Reusing SMB connection for host: {}", hostname);
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果连接不存在或已关闭,则创建新连接
|
||||
connectionPoolLock.lock();
|
||||
try {
|
||||
// 双重检查锁定模式
|
||||
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 connection;
|
||||
}
|
||||
// 如果连接已失效,从池中移除
|
||||
connectionPool.remove(hostname);
|
||||
}
|
||||
|
||||
// 创建新连接
|
||||
log.debug("Creating new SMB connection for host: {}", hostname);
|
||||
connection = client.connect(hostname);
|
||||
connectionInfo = new ConnectionInfo(connection);
|
||||
connectionPool.put(hostname, connectionInfo);
|
||||
} finally {
|
||||
connectionPoolLock.unlock();
|
||||
}
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理空闲连接的定时任务
|
||||
*/
|
||||
private void cleanupIdleConnections() {
|
||||
log.debug("Running idle connections cleanup task");
|
||||
|
||||
// 创建要移除的连接列表,避免在迭代时修改Map
|
||||
List<String> idleHostnames = new java.util.ArrayList<>();
|
||||
|
||||
// 查找所有空闲连接
|
||||
for (Map.Entry<String, ConnectionInfo> entry : connectionPool.entrySet()) {
|
||||
String hostname = entry.getKey();
|
||||
ConnectionInfo connectionInfo = entry.getValue();
|
||||
|
||||
// 检查连接是否空闲超时
|
||||
if (connectionInfo != null && 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 {
|
||||
log.debug("Closing idle connection for host: {}", hostname);
|
||||
connectionInfo.getConnection().close();
|
||||
} catch (IOException e) {
|
||||
log.error("Error closing idle connection for host: {}", hostname, e);
|
||||
}
|
||||
connectionPool.remove(hostname);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
connectionPoolLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("Idle connections cleanup completed, closed {} connections", idleHostnames.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行SMB操作的通用方法,使用连接池
|
||||
*
|
||||
* @param smbPath SMB路径
|
||||
* @param operation 要执行的操作
|
||||
* @param <T> 操作返回类型
|
||||
* @return 操作结果
|
||||
* @throws IOException 如果操作失败
|
||||
*/
|
||||
private <T> T executeSmbOperation(SmbPath smbPath, SmbOperation<T> operation) throws IOException {
|
||||
String hostname = smbPath.getHostname();
|
||||
com.hierynomus.smbj.connection.Connection connection = null;
|
||||
|
||||
try {
|
||||
// 从连接池获取连接
|
||||
connection = getConnection(hostname);
|
||||
|
||||
// 使用获取的连接进行身份验证
|
||||
var session = connection.authenticate(getAuthenticationContext(hostname));
|
||||
|
||||
try (var share = (DiskShare) session.connectShare(smbPath.getShareName())) {
|
||||
return operation.execute(share, smbPath.getPath());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// 如果操作失败且连接存在,检查连接状态
|
||||
if (connection != null && !connection.isConnected()) {
|
||||
// 从连接池移除失效的连接
|
||||
connectionPool.remove(hostname);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件到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<java.io.File> 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭并清理所有连接资源
|
||||
*/
|
||||
public void shutdown() {
|
||||
log.debug("Shutting down SMB connection pool");
|
||||
|
||||
// 关闭定时清理任务
|
||||
try {
|
||||
cleanupScheduler.shutdown();
|
||||
if (!cleanupScheduler.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
cleanupScheduler.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
cleanupScheduler.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
|
||||
// 关闭所有连接
|
||||
connectionPoolLock.lock();
|
||||
try {
|
||||
for (Map.Entry<String, ConnectionInfo> entry : connectionPool.entrySet()) {
|
||||
try {
|
||||
log.debug("Closing connection to host: {}", entry.getKey());
|
||||
entry.getValue().getConnection().close();
|
||||
} catch (IOException e) {
|
||||
log.error("Error closing connection to host: {}", entry.getKey(), e);
|
||||
}
|
||||
}
|
||||
connectionPool.clear();
|
||||
|
||||
// 关闭SMB客户端
|
||||
client.close();
|
||||
} finally {
|
||||
connectionPoolLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SMB操作接口,用于执行具体的SMB操作
|
||||
*/
|
||||
@FunctionalInterface
|
||||
private interface SmbOperation<T> {
|
||||
T execute(DiskShare share, String path) throws IOException;
|
||||
}
|
||||
}
|
||||
@@ -25,9 +25,21 @@ spring.jpa.show-sql=false
|
||||
spring.data.jpa.repositories.enabled=true
|
||||
|
||||
spring.lifecycle.timeout-per-shutdown-phase=10s
|
||||
# my.downloadsPath = C:\\Users\\SQQ\\Downloads\\
|
||||
spring.data.redis.host=10.84.209.229
|
||||
|
||||
spring.data.redis.client-type=lettuce
|
||||
spring.data.redis.host=10.84.210.110
|
||||
spring.data.redis.port=6379
|
||||
spring.data.redis.username=default
|
||||
spring.data.redis.password=redis_NFYa7z
|
||||
spring.data.redis.database=3
|
||||
spring.data.redis.repositories.enabled=false
|
||||
|
||||
# Redis连接池和容错配置
|
||||
spring.data.redis.lettuce.pool.max-active=8
|
||||
spring.data.redis.lettuce.pool.max-wait=10000
|
||||
spring.data.redis.lettuce.pool.max-idle=8
|
||||
spring.data.redis.lettuce.pool.min-idle=0
|
||||
spring.data.redis.timeout=60000
|
||||
|
||||
logging.level.org.hibernate.tool.hbm2ddl=DEBUG
|
||||
|
||||
@@ -53,3 +65,6 @@ spring.cache.redis.cache-null-values=true
|
||||
server.error.whitelabel.enabled=false
|
||||
# 设置错误处理路径,确保404等错误能被全局异常处理器捕获
|
||||
spring.web.resources.add-mappings=true
|
||||
|
||||
smb.server.username=qiqing.song
|
||||
smb.server.password=huez8310
|
||||
|
||||
Reference in New Issue
Block a user