DirectoryUtils.cs 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. namespace MeloongCore;
  2. public static class DirectoryUtils {
  3. /// <summary>
  4. /// 创建文件夹,或文件所在的文件夹。
  5. /// 文件夹已存在时不会抛出异常。
  6. /// </summary>
  7. public static void Create(string path) {
  8. if (DirectoryUtils.Exists(path)) return;
  9. Logger.Trace($"新建文件夹:{path}");
  10. Directory.CreateDirectory(PathUtils.ForApi(path));
  11. }
  12. /// <summary>
  13. /// 判断文件夹是否存在。
  14. /// </summary>
  15. public static bool Exists(string path)
  16. => Directory.Exists(PathUtils.ForApi(path));
  17. /// <summary>
  18. /// 获取 <see cref="DirectoryInfo"/> 对象。
  19. /// </summary>
  20. public static DirectoryInfo GetInfo(string path)
  21. => new(PathUtils.ForApi(path));
  22. /// <summary>
  23. /// 返回指定路径下的所有文件,不以 \\?\ 开头。
  24. /// 如果文件夹不存在,返回空列表。
  25. /// </summary>
  26. public static IEnumerable<string> GetFiles(string path, bool topDirectoryOnly = false, string searchPattern = "*") {
  27. if (!DirectoryUtils.Exists(path)) return [];
  28. return Directory.EnumerateFiles(PathUtils.ForApi(path), searchPattern, topDirectoryOnly ? SearchOption.TopDirectoryOnly : SearchOption.AllDirectories).
  29. Select(PathUtils.WithoutLongPath);
  30. }
  31. /// <summary>
  32. /// 返回指定路径下的所有文件夹,不以分隔符结尾,不以 \\?\ 开头。
  33. /// 如果文件夹不存在,返回空列表。
  34. /// </summary>
  35. public static IEnumerable<string> GetDirectories(string path, bool topDirectoryOnly = false, string searchPattern = "*") {
  36. if (!DirectoryUtils.Exists(path)) return [];
  37. return Directory.EnumerateDirectories(PathUtils.ForApi(path), searchPattern, topDirectoryOnly ? SearchOption.TopDirectoryOnly : SearchOption.AllDirectories).
  38. Select(PathUtils.WithoutLongPath);
  39. }
  40. /// <summary>
  41. /// 该文件夹是否为空。
  42. /// 如果文件夹不存在,返回 true。
  43. /// </summary>
  44. public static bool IsEmpty(string path) {
  45. if (!DirectoryUtils.Exists(path)) return true;
  46. return !Directory.EnumerateFileSystemEntries(PathUtils.ForApi(path)).Any();
  47. }
  48. /// <summary>
  49. /// 复制文件夹。
  50. /// 若原文件夹不存在,则不执行。
  51. /// </summary>
  52. public static void Copy(string sourceFolder, string destFolder, Action<double>? progressHandler = null) {
  53. if (!DirectoryUtils.Exists(sourceFolder)) {
  54. Logger.Info($"尝试复制文件夹,但原文件夹不存在,已跳过复制:{sourceFolder} → {destFolder}");
  55. progressHandler?.Invoke(1);
  56. return;
  57. }
  58. sourceFolder = PathUtils.ForCompare(sourceFolder);
  59. destFolder = PathUtils.ForCompare(destFolder);
  60. int doneCount = 0;
  61. Lazy<int> totalCount = new(() => DirectoryUtils.GetFiles(sourceFolder).Count());
  62. Logger.Trace($"复制文件夹:{sourceFolder} → {destFolder}");
  63. foreach (var file in DirectoryUtils.GetFiles(sourceFolder)) {
  64. FileUtils.Copy(file, file.Replace(sourceFolder, destFolder));
  65. doneCount++;
  66. if (progressHandler is not null) progressHandler?.Invoke((double) doneCount / totalCount.Value);
  67. }
  68. }
  69. /// <summary>
  70. /// 剪切文件夹。
  71. /// </summary>
  72. public static void Move(string sourceFolder, string destFolder) {
  73. if (!DirectoryUtils.Exists(sourceFolder)) return;
  74. DirectoryUtils.Delete(destFolder); // Move 要求不存在对应文件夹
  75. Logger.Trace($"剪切文件夹:{sourceFolder} → {destFolder}");
  76. Directory.Move(PathUtils.ForApi(sourceFolder), PathUtils.ForApi(destFolder));
  77. }
  78. /// <summary>
  79. /// 删除文件夹。
  80. /// </summary>
  81. public static void Delete(string folder, bool toRecycleBin = false) {
  82. // 安全检查
  83. if (!DirectoryUtils.Exists(folder)) return;
  84. Logger.Trace($"{(toRecycleBin ? "将文件夹删除到回收站" : "删除文件夹")}:{folder}");
  85. folder = PathUtils.ForCompare(folder);
  86. if (folder == PathUtils.ForCompare(Path.GetPathRoot(folder)))
  87. throw new UnauthorizedAccessException($"不应删除磁盘根目录:{folder}");
  88. if (criticalFolders.Value.Any(f => f.StartsWithF(folder, ignoreCase: true)))
  89. throw new UnauthorizedAccessException($"不应删除的文件夹:{folder}");
  90. // 删除
  91. if (toRecycleBin) {
  92. FileUtils.DeleteToRecycleBin(folder); // 删除到回收站
  93. } else {
  94. DeleteInternal(folder); // 永久删除
  95. static void DeleteInternal(string folder) {
  96. try {
  97. folder = PathUtils.ForApi(folder);
  98. foreach (string filePath in DirectoryUtils.GetFiles(folder, true)) FileUtils.Delete(filePath); // 删除文件
  99. foreach (string str in DirectoryUtils.GetDirectories(folder, true)) DeleteInternal(str); // 递归删除子文件夹
  100. ResilientUtils.RetryOn<IOException>(() => Directory.Delete(folder, true)); // 最终删除文件夹
  101. } catch (DirectoryNotFoundException ex) { // #4549,也可能已被其他线程删除
  102. if (DirectoryUtils.Exists(folder)) {
  103. Logger.Warn(ex, $"该文件夹可能为孤立的符号链接,尝试直接删除({folder})");
  104. Directory.Delete(folder, true);
  105. } else {
  106. throw;
  107. }
  108. }
  109. }
  110. }
  111. }
  112. private static readonly Lazy<HashSet<string>> criticalFolders = new(() => new(new[] {
  113. PathUtils.CurrentFolder,
  114. Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
  115. Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory),
  116. Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
  117. Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Downloads") // 下载文件夹没有 SpecialFolder 枚举
  118. }.Select(f => PathUtils.ForCompare(f))));
  119. }