兼容win7版应用
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

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);
}
}
}