DirectoryUtils.cs 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  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 string CreateRandom() {
  16. var tempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
  17. DirectoryUtils.Create(tempDir);
  18. return tempDir;
  19. }
  20. /// <summary>
  21. /// 判断文件夹是否存在。
  22. /// </summary>
  23. public static bool Exists(string path)
  24. => Directory.Exists(PathUtils.ForApi(path));
  25. /// <summary>
  26. /// 获取 <see cref="DirectoryInfo"/> 对象。
  27. /// </summary>
  28. public static DirectoryInfo GetInfo(string path)
  29. => new(PathUtils.ForApi(path));
  30. /// <summary>
  31. /// 返回指定路径下的所有文件,不以 \\?\ 开头。
  32. /// 如果文件夹不存在,返回空列表。
  33. /// </summary>
  34. public static IEnumerable<string> GetFiles(string path, bool topDirectoryOnly = false, string searchPattern = "*") {
  35. if (!DirectoryUtils.Exists(path)) return [];
  36. return Directory.EnumerateFiles(PathUtils.ForApi(path), searchPattern, topDirectoryOnly ? SearchOption.TopDirectoryOnly : SearchOption.AllDirectories).
  37. Select(PathUtils.RemoveExtendedPrefix);
  38. }
  39. /// <summary>
  40. /// 返回指定路径下的所有文件夹,不以分隔符结尾,不以 \\?\ 开头。
  41. /// 如果文件夹不存在,返回空列表。
  42. /// </summary>
  43. public static IEnumerable<string> GetDirectories(string path, bool topDirectoryOnly = false, string searchPattern = "*") {
  44. if (!DirectoryUtils.Exists(path)) return [];
  45. return Directory.EnumerateDirectories(PathUtils.ForApi(path), searchPattern, topDirectoryOnly ? SearchOption.TopDirectoryOnly : SearchOption.AllDirectories).
  46. Select(PathUtils.RemoveExtendedPrefix);
  47. }
  48. /// <summary>
  49. /// 该文件夹是否为空。
  50. /// 如果文件夹不存在,返回 true。
  51. /// </summary>
  52. public static bool IsEmpty(string path) {
  53. if (!DirectoryUtils.Exists(path)) return true;
  54. return !Directory.EnumerateFileSystemEntries(PathUtils.ForApi(path)).Any();
  55. }
  56. /// <summary>
  57. /// 复制文件夹。
  58. /// 若复制自身到自身,则不执行操作;若仅大小写不同,则重命名此文件夹。
  59. /// </summary>
  60. public static void Copy(string sourceFolder, string destFolder) {
  61. sourceFolder = PathUtils.ForCompare(sourceFolder);
  62. destFolder = PathUtils.ForCompare(destFolder);
  63. if (string.Compare(sourceFolder, destFolder, ignoreCase: false) == 0) {
  64. // 复制自身到自身,则不执行操作
  65. Logger.Trace($"复制文件夹到自身,不执行操作:{sourceFolder} → {destFolder}");
  66. } else if (string.Compare(sourceFolder, destFolder, ignoreCase: true) == 0) {
  67. // 路径仅大小写不同,等效于重命名
  68. Logger.Trace($"复制文件夹到自身,但大小写不同,等效于重命名文件夹:{sourceFolder} → {destFolder}");
  69. DirectoryUtils.Move(sourceFolder, destFolder);
  70. } else {
  71. // 实际的复制
  72. Logger.Trace($"复制文件夹:{sourceFolder} → {destFolder}");
  73. foreach (var file in DirectoryUtils.GetFiles(sourceFolder)) FileUtils.Copy(file, file.Replace(sourceFolder, destFolder));
  74. }
  75. }
  76. /// <summary>
  77. /// 剪切文件夹。
  78. /// 会创建对应文件夹、覆盖已有的文件夹。
  79. /// </summary>
  80. public static void Move(string sourceFolder, string destFolder) {
  81. SafetyCheck(sourceFolder);
  82. sourceFolder = PathUtils.ForCompare(sourceFolder);
  83. destFolder = PathUtils.ForCompare(destFolder);
  84. if (string.Compare(sourceFolder, destFolder, ignoreCase: false) == 0) {
  85. // 剪切自身到自身,则不执行操作
  86. Logger.Trace($"剪切文件夹到自身,不执行操作:{sourceFolder} → {destFolder}");
  87. } else if (string.Compare(sourceFolder, destFolder, ignoreCase: true) == 0) {
  88. // 路径仅大小写不同
  89. Logger.Trace($"剪切文件夹到自身,但大小写不同:{sourceFolder} → {destFolder}");
  90. var temp = Path.Combine(PathUtils.RemoveLastPart(sourceFolder), Path.GetRandomFileName());
  91. DirectoryUtils.Move(sourceFolder, temp);
  92. DirectoryUtils.Move(temp, destFolder);
  93. } else if (PathUtils.GetDiskName(sourceFolder) == PathUtils.GetDiskName(destFolder)) {
  94. // 同一磁盘剪切,直接调用 Move(这只修改文件夹名,效率更高)
  95. Logger.Trace($"剪切文件夹到同一磁盘:{sourceFolder} → {destFolder}");
  96. DirectoryUtils.Delete(destFolder); // Move 要求此前不存在对应文件夹
  97. ResilientUtils.RetryOn<IOException>(()
  98. => Directory.Move(PathUtils.ForApi(sourceFolder), PathUtils.ForApi(destFolder)));
  99. } else {
  100. // 不同磁盘,必须先复制再删除,这就是我们傻逼微软
  101. Logger.Trace($"剪切文件夹到不同磁盘:{sourceFolder} → {destFolder}");
  102. DirectoryUtils.Copy(sourceFolder, destFolder);
  103. DirectoryUtils.Delete(sourceFolder);
  104. }
  105. }
  106. /// <summary>
  107. /// 删除文件夹。
  108. /// </summary>
  109. public static void Delete(string folder, bool toRecycleBin = false) {
  110. if (!DirectoryUtils.Exists(folder)) return;
  111. Logger.Trace($"{(toRecycleBin ? "将文件夹删除到回收站" : "删除文件夹")}:{folder}");
  112. SafetyCheck(folder);
  113. // 删除
  114. if (toRecycleBin) {
  115. FileUtils.DeleteToRecycleBin(folder); // 删除到回收站
  116. } else {
  117. DeleteInternal(folder); // 永久删除
  118. static void DeleteInternal(string folder) {
  119. try {
  120. folder = PathUtils.ForApi(folder);
  121. foreach (string filePath in DirectoryUtils.GetFiles(folder, true)) FileUtils.Delete(filePath); // 删除文件
  122. foreach (string str in DirectoryUtils.GetDirectories(folder, true)) DeleteInternal(str); // 递归删除子文件夹
  123. ResilientUtils.RetryOn<IOException>(() => Directory.Delete(folder, true)); // 最终删除文件夹
  124. } catch (DirectoryNotFoundException ex) { // #4549,也可能已被其他线程删除
  125. if (DirectoryUtils.Exists(folder)) {
  126. Logger.Warn(ex, $"该文件夹可能为孤立的符号链接,尝试直接删除({folder})");
  127. Directory.Delete(folder);
  128. } else {
  129. throw;
  130. }
  131. }
  132. }
  133. }
  134. }
  135. /// <summary>
  136. /// 进行安全检查:若尝试操作桌面、文档、下载、Windows、当前程序所在文件夹,则抛出异常。
  137. /// </summary>
  138. /// <exception cref="UnauthorizedAccessException" />
  139. private static void SafetyCheck(string folder) {
  140. folder = PathUtils.ForCompare(folder);
  141. if (folder == PathUtils.ForCompare(Path.GetPathRoot(folder)))
  142. throw new UnauthorizedAccessException($"不应操作磁盘根目录:{folder}");
  143. if (criticalFolders.Value.Any(f => f.StartsWithF(folder, ignoreCase: true)))
  144. throw new UnauthorizedAccessException($"不应操作文件夹:{folder}");
  145. }
  146. private static readonly Lazy<HashSet<string>> criticalFolders = new(() => new(new[] {
  147. PathUtils.CurrentFolder,
  148. Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
  149. Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory),
  150. Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
  151. Environment.GetFolderPath(Environment.SpecialFolder.Windows),
  152. Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Downloads") // 下载文件夹没有 SpecialFolder 枚举
  153. }.Select(f => PathUtils.ForCompare(f))));
  154. }