You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
176 lines
6.5 KiB
176 lines
6.5 KiB
using AIK.Common.SysCommon;
|
|
using AIK.Service.IService;
|
|
using SharpCompress.Archives;
|
|
using SharpCompress.Common;
|
|
using SharpCompress.Readers;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Security;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace AIK.Service.Service
|
|
{
|
|
public class FileOperationService : IFileOperationService
|
|
{
|
|
public async Task ExtractSingleArchiveAsync(
|
|
string archivePath,
|
|
string destinationDir,
|
|
bool overwrite = true,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (!Directory.Exists(destinationDir))
|
|
Directory.CreateDirectory(destinationDir);
|
|
|
|
try
|
|
{
|
|
using var archive = ArchiveFactory.Open(archivePath);
|
|
var allEntries = archive.Entries
|
|
.Where(e => !e.IsDirectory)
|
|
.ToList();
|
|
|
|
bool shouldStripReceivedData = HasRootReceivedDataFolder(allEntries);
|
|
|
|
foreach (var entry in allEntries)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
string normalizedEntryPath = NormalizeArchivePath(entry.Key);
|
|
|
|
string targetRelativePath;
|
|
if (shouldStripReceivedData)
|
|
{
|
|
if (normalizedEntryPath.StartsWith($"{ComStringHelper.K3ToolDirectory}/", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
targetRelativePath = normalizedEntryPath.Substring($"{ComStringHelper.K3ToolDirectory}/".Length);
|
|
}
|
|
else
|
|
{
|
|
continue; // 跳过非 received_data 内容
|
|
}
|
|
}
|
|
else
|
|
{
|
|
targetRelativePath = normalizedEntryPath;
|
|
}
|
|
|
|
// 安全校验(GetSafeExtractionPath 内部也应基于规范化路径)
|
|
string destPath = GetSafeExtractionPath(destinationDir, targetRelativePath);
|
|
Directory.CreateDirectory(Path.GetDirectoryName(destPath)!);
|
|
|
|
await Task.Run(() =>
|
|
{
|
|
entry.WriteToFile(destPath, new ExtractionOptions
|
|
{
|
|
Overwrite = overwrite,
|
|
PreserveFileTime = false
|
|
});
|
|
}, cancellationToken);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
throw new InvalidOperationException($"解压失败: {archivePath}", ex);
|
|
}
|
|
}
|
|
|
|
public async Task ExtractMultipleArchivesAsyncNotPoress(
|
|
IEnumerable<string> archivePaths,
|
|
string destinationDir,
|
|
bool overwrite = true,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (!Directory.Exists(destinationDir))
|
|
Directory.CreateDirectory(destinationDir);
|
|
|
|
foreach (string path in archivePaths)
|
|
{
|
|
await ExtractSingleArchiveAsync(path, destinationDir, overwrite, cancellationToken);
|
|
}
|
|
}
|
|
|
|
public async Task ExtractMultipleArchivesAsync(
|
|
IReadOnlyList<string> archivePaths,
|
|
string destDir,
|
|
IProgress<double> progress,
|
|
bool overwrite,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
int total = archivePaths.Count;
|
|
if (total == 0)
|
|
{
|
|
progress.Report(100);
|
|
return;
|
|
}
|
|
|
|
for (int i = 0; i < total; i++)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
string path = archivePaths[i];
|
|
|
|
if (File.Exists(path))
|
|
{
|
|
await Task.Run(() =>
|
|
{
|
|
System.IO.Compression.ZipFile.ExtractToDirectory(path, destDir);
|
|
}, cancellationToken);
|
|
}
|
|
|
|
double percent = (i + 1.0) / total * 100.0;
|
|
progress.Report(percent);
|
|
}
|
|
|
|
progress.Report(100);
|
|
}
|
|
// 私有工具方法
|
|
private static string GetSafeExtractionPath(string baseDir, string normalizedRelativePath)
|
|
{
|
|
// 此时 normalizedRelativePath 已是 clean 格式,如 "bin/app.exe"
|
|
if (string.IsNullOrEmpty(normalizedRelativePath))
|
|
throw new ArgumentException("路径为空");
|
|
|
|
// 拼接并获取完整路径
|
|
string fullPath = Path.GetFullPath(Path.Combine(baseDir, normalizedRelativePath));
|
|
|
|
// 安全校验:必须在 baseDir 内
|
|
string baseDirNormalized = Path.GetFullPath(baseDir) + Path.DirectorySeparatorChar;
|
|
if (!fullPath.StartsWith(baseDirNormalized, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
throw new SecurityException($"非法路径遍历: {normalizedRelativePath}");
|
|
}
|
|
|
|
// 仅对文件名部分处理非法字符
|
|
string dirPart = Path.GetDirectoryName(fullPath)!;
|
|
string filePart = Path.GetFileName(fullPath);
|
|
string safeFilePart = Path.GetInvalidFileNameChars()
|
|
.Aggregate(filePart, (current, c) => current.Replace(c.ToString(), "_"));
|
|
|
|
return Path.Combine(dirPart, safeFilePart);
|
|
}
|
|
|
|
private static bool HasRootReceivedDataFolder(List<IArchiveEntry> entries)
|
|
{
|
|
return entries.Any(e =>
|
|
e.Key.Replace('\\', '/').StartsWith($"{ComStringHelper.K3ToolDirectory}/", StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
private static string NormalizeArchivePath(string archivePath)
|
|
{
|
|
if (string.IsNullOrEmpty(archivePath))
|
|
return string.Empty;
|
|
|
|
// 1. 统一斜杠
|
|
string path = archivePath.Replace('\\', '/');
|
|
|
|
// 2. 分割并过滤危险/冗余段
|
|
var parts = path.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries)
|
|
.Where(part => part != "." && !part.Equals("..", StringComparison.OrdinalIgnoreCase))
|
|
.ToList();
|
|
|
|
// 3. 重建路径(无开头/结尾斜杠)
|
|
return parts.Count == 0 ? string.Empty : string.Join("/", parts);
|
|
}
|
|
}
|
|
}
|
|
|