✨ feat(file-check): 添加 PLC 应答后命中文件删除
* 新增默认关闭的删除开关,仅在收到 PLC 放行应答后执行 * 本地与 SFTP Provider 校验根目录并支持幂等删除 * 补充工作流、设置页和文件删除相关测试与文档
This commit is contained in:
@@ -61,6 +61,33 @@ public sealed class LocalFileCheckProvider : IFileCheckProvider
|
||||
return await Task.Run(() => CheckInternal(barcode, config, cancellationToken), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除本地文件校验返回的精确命中文件。
|
||||
/// 删除前会校验目标必须位于配置的本地根目录内,文件已不存在时按幂等成功处理。
|
||||
/// </summary>
|
||||
/// <param name="filePath">文件校验返回的精确命中文件路径。</param>
|
||||
/// <param name="config">根配置对象。</param>
|
||||
/// <param name="cancellationToken">取消令牌。取消后终止尚未完成的删除操作。</param>
|
||||
/// <returns>文件删除结果。</returns>
|
||||
public async Task<FileDeleteOutcome> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试本地文件校验目录是否可访问。
|
||||
/// </summary>
|
||||
@@ -129,6 +156,83 @@ public sealed class LocalFileCheckProvider : IFileCheckProvider
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在同步上下文中安全删除本地产品 ID 文件。
|
||||
/// </summary>
|
||||
/// <param name="filePath">待删除文件路径。</param>
|
||||
/// <param name="rootPath">配置的本地根目录。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>文件删除结果。</returns>
|
||||
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}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断目标文件是否位于配置根目录内,防止外部路径被误删。
|
||||
/// </summary>
|
||||
/// <param name="fullFilePath">目标文件绝对路径。</param>
|
||||
/// <param name="fullRootPath">配置根目录绝对路径。</param>
|
||||
/// <returns>目标位于根目录内返回 <see langword="true"/>;否则返回 <see langword="false"/>。</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证本地根目录配置和可访问性。
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user