Files
MRCC/launcher/PCL-CE/PCL.Core/IO/Directories.cs
T
xyou f70b061d1a
CI / Go Backend (push) Canceled after 0s
feat: 项目初始化 + 3D方块世界原型 + AI助搭系统
初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器

HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块

Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚

AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
2026-08-08 14:07:56 +08:00

307 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Security.AccessControl;
namespace PCL.Core.IO;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Logging;
public static class Directories {
/// <summary>
/// 异步检查是否拥有对指定文件夹的读写权限。
/// 如果文件夹不存在或没有权限,返回 false,不修改文件系统。
/// </summary>
/// <param name="path">要检查的文件夹路径。</param>
/// <param name="cancellationToken">取消操作的令牌。</param>
/// <returns>如果拥有读写权限且文件夹存在,返回 true;否则返回 false。</returns>
public static async Task<bool> CheckPermissionAsync(string? path, CancellationToken cancellationToken = default) {
try {
if (string.IsNullOrWhiteSpace(path)) {
return false;
}
// 排除特殊系统文件夹
if (IsSystemProtectedFolder(path)) {
return false;
}
// 检查文件夹是否存在
if (!Directory.Exists(path)) {
return false;
}
// 检查目录访问权限
var directoryInfo = new DirectoryInfo(path);
var security = await Task.Run(() => directoryInfo.GetAccessControl(), cancellationToken);
var rules = security.GetAccessRules(true, true, typeof(System.Security.Principal.NTAccount));
// 检查当前用户是否有读写权限,优先考虑拒绝规则
var currentUser = System.Security.Principal.WindowsIdentity.GetCurrent();
var principal = new System.Security.Principal.WindowsPrincipal(currentUser);
var isDenied = false;
var isAllowed = false;
foreach (FileSystemAccessRule rule in rules) {
if (!rule.FileSystemRights.HasFlag(FileSystemRights.Write))
continue;
// 检查规则是否适用于当前用户或其组
if (principal.IsInRole(rule.IdentityReference.Value)) {
if (rule.AccessControlType == AccessControlType.Deny) {
isDenied = true;
break; // 拒绝优先,直接返回
}
if (rule.AccessControlType == AccessControlType.Allow) {
isAllowed = true;
}
}
}
if (isDenied || !isAllowed) {
return false;
}
// 尝试枚举目录内容以确认实际访问能力
await Task.Run(() => Directory.EnumerateFiles(path, "*", SearchOption.TopDirectoryOnly).Any(), cancellationToken);
return true;
} catch (OperationCanceledException) {
LogWrapper.Warn("权限检查被取消");
return false;
} catch (Exception ex) {
LogWrapper.Warn(ex, $"没有对文件夹 {path} 的权限,请尝试以管理员权限运行。");
return false;
}
}
/// <summary>
/// 异步检查文件夹权限,若无权限或文件夹不存在则抛出异常。
/// 不修改文件系统。
/// </summary>
/// <param name="path">要检查的文件夹路径。</param>
/// <param name="cancellationToken">取消操作的令牌。</param>
/// <exception cref="ArgumentNullException">路径为空或只包含空白字符。</exception>
/// <exception cref="DirectoryNotFoundException">文件夹不存在。</exception>
/// <exception cref="UnauthorizedAccessException">无访问权限。</exception>
/// <exception cref="OperationCanceledException">操作被取消。</exception>
public static async Task CheckPermissionWithExceptionAsync(string? path, CancellationToken cancellationToken = default) {
if (string.IsNullOrWhiteSpace(path)) {
throw new ArgumentNullException(nameof(path), "文件夹路径不能为空!");
}
if (IsSystemProtectedFolder(path)) {
throw new UnauthorizedAccessException($"无法访问受保护的系统文件夹:{path}");
}
if (!Directory.Exists(path)) {
throw new DirectoryNotFoundException($"文件夹不存在:{path}");
}
try {
var directoryInfo = new DirectoryInfo(path);
var security = await Task.Run(() => directoryInfo.GetAccessControl(), cancellationToken);
var rules = security.GetAccessRules(true, true, typeof(System.Security.Principal.NTAccount));
var hasAccess = rules.Cast<FileSystemAccessRule>()
.Any(rule => rule.FileSystemRights.HasFlag(FileSystemRights.Write) &&
rule.AccessControlType == AccessControlType.Allow);
if (!hasAccess) {
throw new UnauthorizedAccessException($"没有对文件夹 {path} 的写权限");
}
// 确认实际访问能力
await Task.Run(() => Directory.EnumerateFiles(path, "*", SearchOption.TopDirectoryOnly).Any(), cancellationToken);
} catch (UnauthorizedAccessException) {
throw;
} catch (OperationCanceledException) {
throw;
} catch (Exception ex) {
throw new UnauthorizedAccessException($"无法访问文件夹 {path}{ex.Message}", ex);
}
}
/// <summary>
/// 检查是否为受保护的系统文件夹。
/// </summary>
private static bool IsSystemProtectedFolder(string path) {
return path.EndsWith(":\\System Volume Information", StringComparison.OrdinalIgnoreCase) ||
path.EndsWith(":\\$RECYCLE.BIN", StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// 异步删除文件夹及其内容,返回删除的文件数。支持忽略错误。
/// </summary>
/// <param name="path">要删除的文件夹路径。</param>
/// <param name="ignoreIssue">是否忽略删除过程中的错误。</param>
/// <param name="cancellationToken">取消操作的令牌。</param>
/// <returns>成功删除的文件数。</returns>
/// <exception cref="OperationCanceledException">操作被取消。</exception>
public static async Task<int> DeleteDirectoryAsync(string? path, bool ignoreIssue = false, CancellationToken cancellationToken = default) {
if (string.IsNullOrEmpty(path) || !Directory.Exists(path)) {
return 0;
}
var deletedCount = 0;
try {
// 枚举文件,延迟加载以提高性能
foreach (var filePath in Directory.EnumerateFiles(path)) {
cancellationToken.ThrowIfCancellationRequested();
for (var attempt = 0; attempt < 2; attempt++) {
try {
await FileDeleteAsync(filePath, cancellationToken).ConfigureAwait(false);
deletedCount++;
break;
} catch (Exception ex) when (attempt == 0) {
LogWrapper.Error(ex, $"删除文件失败,将在 0.3s 后重试({filePath}");
await Task.Delay(300, cancellationToken).ConfigureAwait(false);
} catch (Exception ex) {
if (ignoreIssue) {
LogWrapper.Error(ex, "删除单个文件可忽略地失败");
} else {
throw;
}
}
}
}
// 递归删除子目录
foreach (var subDir in Directory.EnumerateDirectories(path)) {
cancellationToken.ThrowIfCancellationRequested();
deletedCount += await DeleteDirectoryAsync(subDir, ignoreIssue, cancellationToken).ConfigureAwait(false);
}
// 删除空目录
for (var attempt = 0; attempt < 2; attempt++) {
try {
Directory.Delete(path, true);
break;
} catch (Exception ex) when (attempt == 0) {
LogWrapper.Error(ex, $"删除文件夹失败,将在 0.3s 后重试({path}");
await Task.Delay(300, cancellationToken).ConfigureAwait(false);
} catch (Exception ex) {
if (ignoreIssue) {
LogWrapper.Error(ex, "删除单个文件夹可忽略地失败");
} else {
throw;
}
}
}
} catch (DirectoryNotFoundException ex) {
// 处理疑似符号链接的情况
LogWrapper.Error(ex, $"疑似为孤立符号链接,尝试直接删除({path})", "Developer");
try {
Directory.Delete(path);
} catch (Exception deleteEx) {
if (!ignoreIssue) {
throw;
}
LogWrapper.Error(deleteEx, $"删除符号链接文件夹失败({path})");
}
}
return deletedCount;
}
/// <summary>
/// 异步复制文件夹及其内容,失败时抛出异常。
/// </summary>
/// <param name="fromPath">源文件夹路径。</param>
/// <param name="toPath">目标文件夹路径。</param>
/// <param name="progressIncrementHandler">进度更新回调,接收 0 到 1 的进度值。</param>
/// <param name="cancellationToken">取消操作的令牌。</param>
/// <exception cref="ArgumentNullException">源或目标文件夹路径为空。</exception>
/// <exception cref="OperationCanceledException">操作被取消。</exception>
public static async Task CopyDirectoryAsync(string? fromPath, string? toPath, Action<double>? progressIncrementHandler = null, CancellationToken cancellationToken = default) {
if (string.IsNullOrEmpty(fromPath)) {
throw new ArgumentNullException(nameof(fromPath), "源文件夹路径为空");
}
if (string.IsNullOrEmpty(toPath)) {
throw new ArgumentNullException(nameof(toPath), "目标文件夹路径为空");
}
// 规范化路径
fromPath = Path.GetFullPath(fromPath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
toPath = Path.GetFullPath(toPath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
var allFiles = (await EnumerateFilesAsync(fromPath, cancellationToken).ConfigureAwait(false)).ToList();
var totalFiles = allFiles.Count;
long copiedFiles = 0;
foreach (var file in allFiles) {
cancellationToken.ThrowIfCancellationRequested();
var relativePath = file.FullName[fromPath.Length..];
var destFilePath = Path.Combine(toPath, relativePath);
// 确保目标目录存在
Directory.CreateDirectory(Path.GetDirectoryName(destFilePath)!);
for (var attempt = 0; attempt < 2; attempt++) {
try {
await FileCopyAsync(file.FullName, destFilePath, overwrite: true, cancellationToken).ConfigureAwait(false);
copiedFiles++;
progressIncrementHandler?.Invoke((double)copiedFiles / totalFiles);
break;
} catch (Exception ex) when (attempt == 0) {
LogWrapper.Error(ex, $"复制文件失败,将在 0.3s 后重试({file.FullName} 到 {destFilePath}");
await Task.Delay(300, cancellationToken).ConfigureAwait(false);
} catch (Exception ex) {
LogWrapper.Error(ex, $"复制文件失败({file.FullName} 到 {destFilePath}");
throw;
}
}
}
}
/// <summary>
/// 异步遍历文件夹中的所有文件。
/// </summary>
/// <param name="directory">要遍历的文件夹路径。</param>
/// <param name="cancellationToken">取消操作的令牌。</param>
/// <returns>文件信息的枚举器。</returns>
public static async Task<IEnumerable<FileInfo>> EnumerateFilesAsync(string? directory, CancellationToken cancellationToken = default) {
if (string.IsNullOrEmpty(directory) || !Directory.Exists(directory)) {
throw new DirectoryNotFoundException($"目录不存在:{directory}");
}
try {
// DirectoryInfo.EnumerateFiles 是同步的,使用 Task.Run 包装
return await Task.Run(() => new DirectoryInfo(directory).EnumerateFiles("*", SearchOption.AllDirectories).ToList(), cancellationToken).ConfigureAwait(false);
} catch (OperationCanceledException) {
LogWrapper.Warn("文件夹遍历被取消");
return [];
} catch (Exception ex) {
LogWrapper.Error(ex, $"遍历文件夹失败({directory}");
return [];
}
}
// 辅助方法:异步打开 FileStream
private static async Task<FileStream> FileStreamOpenAsync(string path, FileMode mode, FileAccess access, FileShare share, CancellationToken cancellationToken) {
var fs = new FileStream(path, mode, access, share, bufferSize: 4096, useAsync: true);
await Task.Yield(); // 确保异步上下文
cancellationToken.ThrowIfCancellationRequested();
return fs;
}
// 辅助方法:异步删除文件
private static async Task FileDeleteAsync(string path, CancellationToken cancellationToken) {
await Task.Run(() => File.Delete(path), cancellationToken).ConfigureAwait(false);
}
// 辅助方法:异步复制文件
private static async Task FileCopyAsync(string sourceFileName, string destFileName, bool overwrite, CancellationToken cancellationToken) {
await using FileStream sourceStream = new(sourceFileName, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, useAsync: true);
await using FileStream destStream = new(destFileName, overwrite ? FileMode.Create : FileMode.CreateNew, FileAccess.Write, FileShare.None, 4096, useAsync: true);
await sourceStream.CopyToAsync(destStream, cancellationToken).ConfigureAwait(false);
}
}