using System.IO;
using AxiOmron.PcbCheck.Models;
using AxiOmron.PcbCheck.Options;
using AxiOmron.PcbCheck.Services.Interfaces;
namespace AxiOmron.PcbCheck.Services.Implementations;
///
/// 提供本地磁盘目录的文件存在性校验能力。
///
public sealed class LocalFileCheckProvider : IFileCheckProvider
{
private readonly IAppLogger _logger;
///
/// 初始化本地文件校验提供程序。
///
/// 日志记录器。
public LocalFileCheckProvider(IAppLogger logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
///
/// 获取当前提供程序对应的通道类型。
///
public FileCheckProvider Provider => FileCheckProvider.Local;
///
/// 按条码检查本地目录中是否存在目标文件。
///
/// 条码内容。
/// 根配置对象。
/// 取消令牌。取消后立即终止目录枚举。
/// 文件校验结果。
public async Task CheckFileAsync(string barcode, AppConfig config, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(config);
if (string.IsNullOrWhiteSpace(barcode))
{
return new FileCheckOutcome
{
Exists = false,
IsConfigurationError = true,
ErrorMessage = "条码为空,无法执行本地文件查询。"
};
}
FileCheckTestOutcome testOutcome = ValidateRootPath(config.FileCheck.Local.RootPath);
if (!testOutcome.IsSuccess)
{
return new FileCheckOutcome
{
Exists = false,
IsConfigurationError = testOutcome.IsConfigurationError,
IsSystemError = testOutcome.IsSystemError,
ErrorMessage = testOutcome.StatusMessage
};
}
return await Task.Run(() => CheckInternal(barcode, config, cancellationToken), cancellationToken).ConfigureAwait(false);
}
///
/// 删除本地文件校验返回的精确命中文件。
/// 删除前会校验目标必须位于配置的本地根目录内,文件已不存在时按幂等成功处理。
///
/// 文件校验返回的精确命中文件路径。
/// 根配置对象。
/// 取消令牌。取消后终止尚未完成的删除操作。
/// 文件删除结果。
public async Task DeleteFileAsync(string filePath, AppConfig config, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(config);
if (string.IsNullOrWhiteSpace(filePath))
{
return new FileDeleteOutcome
{
IsSuccess = false,
FilePath = filePath,
ErrorMessage = "待删除的本地产品 ID 文件路径为空。"
};
}
return await Task.Run(
() => DeleteInternal(filePath, config.FileCheck.Local.RootPath, cancellationToken),
cancellationToken)
.ConfigureAwait(false);
}
///
/// 测试本地文件校验目录是否可访问。
///
/// 根配置对象。
/// 取消令牌。取消后立即终止测试。
/// 通道测试结果。
public Task TestConnectionAsync(AppConfig config, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(config);
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult(ValidateRootPath(config.FileCheck.Local.RootPath));
}
///
/// 在同步上下文中执行本地文件查找。
///
/// 条码内容。
/// 根配置对象。
/// 取消令牌。
/// 文件校验结果。
private FileCheckOutcome CheckInternal(string barcode, AppConfig config, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
string rootPath = Path.GetFullPath(config.FileCheck.Local.RootPath);
string candidateName = FileCheckPathMatcher.BuildExpectedFileName(barcode, config.FileCheck.FileNamePattern);
string? matchedPath = Directory.EnumerateFiles(rootPath)
.FirstOrDefault(path =>
{
cancellationToken.ThrowIfCancellationRequested();
return FileCheckPathMatcher.IsMatch(Path.GetFileName(path), candidateName, barcode);
});
if (matchedPath is null)
{
return new FileCheckOutcome
{
Exists = false,
ConnectionSucceeded = true,
ErrorMessage = $"未找到与条码 {barcode} 匹配的本地文件。"
};
}
return new FileCheckOutcome
{
Exists = true,
ConnectionSucceeded = true,
MatchedFilePath = matchedPath
};
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or System.Security.SecurityException)
{
_logger.LogError(ex, "本地文件校验异常");
return new FileCheckOutcome
{
Exists = false,
IsSystemError = true,
ErrorMessage = $"本地文件校验异常: {ex.Message}"
};
}
}
///
/// 在同步上下文中安全删除本地产品 ID 文件。
///
/// 待删除文件路径。
/// 配置的本地根目录。
/// 取消令牌。
/// 文件删除结果。
private FileDeleteOutcome DeleteInternal(string filePath, string rootPath, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
string fullRootPath = Path.GetFullPath(rootPath);
string fullFilePath = Path.GetFullPath(filePath);
if (!IsPathWithinRoot(fullFilePath, fullRootPath))
{
return new FileDeleteOutcome
{
IsSuccess = false,
FilePath = fullFilePath,
ErrorMessage = $"拒绝删除本地根目录之外的文件: {fullFilePath}"
};
}
if (!File.Exists(fullFilePath))
{
return new FileDeleteOutcome
{
IsSuccess = true,
WasAlreadyMissing = true,
FilePath = fullFilePath
};
}
File.Delete(fullFilePath);
return new FileDeleteOutcome
{
IsSuccess = true,
FilePath = fullFilePath
};
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex) when (ex is IOException
or UnauthorizedAccessException
or System.Security.SecurityException
or ArgumentException
or NotSupportedException)
{
_logger.LogError(ex, "删除本地产品 ID 文件异常,路径={FilePath}", true, filePath);
return new FileDeleteOutcome
{
IsSuccess = false,
FilePath = filePath,
ErrorMessage = $"删除本地产品 ID 文件失败: {ex.Message}"
};
}
}
///
/// 判断目标文件是否位于配置根目录内,防止外部路径被误删。
///
/// 目标文件绝对路径。
/// 配置根目录绝对路径。
/// 目标位于根目录内返回 ;否则返回 。
private static bool IsPathWithinRoot(string fullFilePath, string fullRootPath)
{
string relativePath = Path.GetRelativePath(fullRootPath, fullFilePath);
return !Path.IsPathRooted(relativePath)
&& !string.Equals(relativePath, "..", StringComparison.Ordinal)
&& !relativePath.StartsWith($"..{Path.DirectorySeparatorChar}", StringComparison.Ordinal)
&& !relativePath.StartsWith($"..{Path.AltDirectorySeparatorChar}", StringComparison.Ordinal);
}
///
/// 验证本地根目录配置和可访问性。
///
/// 本地根目录。
/// 通道测试结果。
private static FileCheckTestOutcome ValidateRootPath(string rootPath)
{
if (string.IsNullOrWhiteSpace(rootPath))
{
return new FileCheckTestOutcome
{
IsConfigurationError = true,
StatusMessage = "本地文件校验目录未配置。"
};
}
string fullPath;
try
{
fullPath = Path.GetFullPath(rootPath);
}
catch (Exception ex) when (ex is ArgumentException or NotSupportedException or PathTooLongException)
{
return new FileCheckTestOutcome
{
IsConfigurationError = true,
StatusMessage = $"本地文件校验目录配置无效: {ex.Message}"
};
}
if (!Directory.Exists(fullPath))
{
return new FileCheckTestOutcome
{
IsConfigurationError = true,
StatusMessage = $"本地文件校验目录不存在: {fullPath}"
};
}
return new FileCheckTestOutcome
{
IsSuccess = true,
ConnectionSucceeded = true,
RootPathAccessible = true,
StatusMessage = $"本地文件校验目录可访问: {fullPath}"
};
}
}