DirectoryUtilsTest.cs 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. namespace MeloongCore.Tests;
  2. public class DirectoryUtilsTest : TestWithFolder {
  3. #region 复制
  4. [Test]
  5. public async Task 复制_自身到自身() {
  6. var src = Path.Combine(tempFolder, "SelfCopy");
  7. DirectoryUtils.Create(src);
  8. FileUtils.Write(Path.Combine(src, "file.txt"), "content");
  9. DirectoryUtils.Copy(src, src);
  10. await Assert.That(DirectoryUtils.Exists(src)).IsTrue();
  11. await Assert.That(FileUtils.Exists(Path.Combine(src, "file.txt"))).IsTrue();
  12. }
  13. [Test]
  14. public async Task 复制_大小写不同() {
  15. // 建立源文件夹(全大写)
  16. var src = Path.Combine(tempFolder, "CASECOPY");
  17. DirectoryUtils.Create(src);
  18. FileUtils.Write(Path.Combine(src, "file.txt"), "content");
  19. // 目标路径仅大小写不同,Copy 应等效于重命名
  20. var dest = Path.Combine(tempFolder, "casecopy");
  21. DirectoryUtils.Copy(src, dest);
  22. await Assert.That(FileUtils.Exists(Path.Combine(dest, "file.txt"))).IsTrue();
  23. }
  24. [Test]
  25. public async Task 复制_不同路径() {
  26. var src = Path.Combine(tempFolder, "CopySource");
  27. DirectoryUtils.Create(Path.Combine(src, "sub"));
  28. FileUtils.Write(Path.Combine(src, "a.txt"), "aaa");
  29. FileUtils.Write(Path.Combine(src, "sub", "b.txt"), "bbb");
  30. var dest = Path.Combine(tempFolder, "CopyDest");
  31. DirectoryUtils.Copy(src, dest);
  32. // 源文件夹和文件仍存在
  33. await Assert.That(DirectoryUtils.Exists(src)).IsTrue();
  34. await Assert.That(FileUtils.Exists(Path.Combine(src, "a.txt"))).IsTrue();
  35. // 目标文件夹的文件已正确复制
  36. await Assert.That(FileUtils.Exists(Path.Combine(dest, "a.txt"))).IsTrue();
  37. await Assert.That(FileUtils.Exists(Path.Combine(dest, "sub", "b.txt"))).IsTrue();
  38. await Assert.That(FileUtils.ReadAsString(Path.Combine(dest, "a.txt"))).IsEqualTo("aaa");
  39. }
  40. #endregion
  41. #region 剪切
  42. [Test]
  43. public async Task 剪切_自身到自身() {
  44. var src = Path.Combine(tempFolder, "SelfMove");
  45. DirectoryUtils.Create(src);
  46. FileUtils.Write(Path.Combine(src, "file.txt"), "content");
  47. DirectoryUtils.Move(src, src);
  48. await Assert.That(DirectoryUtils.Exists(src)).IsTrue();
  49. await Assert.That(FileUtils.Exists(Path.Combine(src, "file.txt"))).IsTrue();
  50. }
  51. [Test]
  52. public async Task 剪切_大小写不同() {
  53. var src = Path.Combine(tempFolder, "CASEMOVE");
  54. DirectoryUtils.Create(src);
  55. FileUtils.Write(Path.Combine(src, "file.txt"), "content");
  56. // 目标路径仅大小写不同,Move 应借助临时目录完成重命名
  57. var dest = Path.Combine(tempFolder, "casemove");
  58. DirectoryUtils.Move(src, dest);
  59. await Assert.That(FileUtils.Exists(Path.Combine(dest, "file.txt"))).IsTrue();
  60. }
  61. [Test]
  62. public async Task 剪切_同一磁盘() {
  63. var src = Path.Combine(tempFolder, "MoveSrc");
  64. DirectoryUtils.Create(Path.Combine(src, "sub"));
  65. FileUtils.Write(Path.Combine(src, "a.txt"), "aaa");
  66. FileUtils.Write(Path.Combine(src, "sub", "b.txt"), "bbb");
  67. var dest = Path.Combine(tempFolder, "MoveDest");
  68. DirectoryUtils.Move(src, dest);
  69. await Assert.That(DirectoryUtils.Exists(src)).IsFalse();
  70. await Assert.That(FileUtils.Exists(Path.Combine(dest, "a.txt"))).IsTrue();
  71. await Assert.That(FileUtils.Exists(Path.Combine(dest, "sub", "b.txt"))).IsTrue();
  72. await Assert.That(FileUtils.ReadAsString(Path.Combine(dest, "a.txt"))).IsEqualTo("aaa");
  73. }
  74. [Test]
  75. public async Task 剪切_不同磁盘() {
  76. // 找到两个不同的就绪固定磁盘;若只有一个则跳过此分支
  77. var drives = DriveInfo.GetDrives()
  78. .Where(d => d.IsReady && d.DriveType == DriveType.Fixed)
  79. .ToArray();
  80. if (drives.Length < 2) return;
  81. var id = Guid.NewGuid().ToString("N");
  82. var srcRoot = Path.Combine(drives[0].RootDirectory.FullName, "Temp", $"MeloongTest_{id}");
  83. var destRoot = Path.Combine(drives[1].RootDirectory.FullName, "Temp", $"MeloongTest_{id}");
  84. var src = Path.Combine(srcRoot, "src");
  85. var dest = Path.Combine(destRoot, "dest");
  86. try {
  87. DirectoryUtils.Create(src);
  88. FileUtils.Write(Path.Combine(src, "a.txt"), "aaa");
  89. DirectoryUtils.Move(src, dest);
  90. await Assert.That(DirectoryUtils.Exists(src)).IsFalse();
  91. await Assert.That(FileUtils.Exists(Path.Combine(dest, "a.txt"))).IsTrue();
  92. await Assert.That(FileUtils.ReadAsString(Path.Combine(dest, "a.txt"))).IsEqualTo("aaa");
  93. } finally {
  94. DirectoryUtils.Delete(srcRoot);
  95. DirectoryUtils.Delete(destRoot);
  96. }
  97. }
  98. [Test]
  99. public void 剪切_CurrentFolder_安全检查() {
  100. var dest = Path.Combine(tempFolder, "SafetyCheckDest");
  101. Assert.Throws<UnauthorizedAccessException>(() => DirectoryUtils.Move(PathUtils.CurrentFolder, dest));
  102. }
  103. #endregion
  104. #region 删除
  105. [Test]
  106. public async Task 删除_子文件夹中带孤立符号链接() {
  107. // 创建一个目标文件夹,供符号链接指向
  108. var target = Path.Combine(tempFolder, "SymlinkTarget");
  109. DirectoryUtils.Create(target);
  110. // 创建父文件夹及其子文件夹
  111. var parent = Path.Combine(tempFolder, "SymlinkParent");
  112. var child = Path.Combine(parent, "child");
  113. DirectoryUtils.Create(child);
  114. // 在子文件夹中建立指向目标的目录链接;若无权限则跳过
  115. var link = Path.Combine(child, "link");
  116. if (!TryCreateDirectoryLink(link, target)) return;
  117. await Assert.That(DirectoryUtils.Exists(link)).IsTrue();
  118. // 删除目标,使链接成为孤立链接
  119. Directory.Delete(PathUtils.ForApi(target));
  120. await Assert.That(DirectoryUtils.Exists(target)).IsFalse();
  121. // 删除含有孤立链接的父文件夹,不应抛出异常
  122. DirectoryUtils.Delete(parent);
  123. await Assert.That(DirectoryUtils.Exists(parent)).IsFalse();
  124. }
  125. /// <summary>
  126. /// 创建指向目标文件夹的目录链接(通过 P/Invoke,不经过 Shell,避免路径中特殊字符的问题)。
  127. /// Windows 下使用目录符号链接(需开发者模式);Linux/macOS 下使用符号链接。
  128. /// 若当前环境无权创建符号链接,则跳过测试(返回 <see langword="false"/>)。
  129. /// </summary>
  130. private static bool TryCreateDirectoryLink(string link, string target) {
  131. if (Environment.OSVersion.Platform == PlatformID.Win32NT) {
  132. // SYMBOLIC_LINK_FLAG_DIRECTORY (1) | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE (2)
  133. bool ok = NativeMethods.CreateSymbolicLink(link, target, 3u);
  134. if (!ok) {
  135. int err = Marshal.GetLastWin32Error();
  136. // ERROR_PRIVILEGE_NOT_HELD (1314) 或 ERROR_ACCESS_DENIED (5):无权限,跳过
  137. if (err == 1314 || err == 5) return false;
  138. throw new InvalidOperationException($"CreateSymbolicLink 失败,Win32 错误码:{err}");
  139. }
  140. return true;
  141. } else {
  142. int ret = NativeMethods.Symlink(target, link);
  143. if (ret != 0) throw new InvalidOperationException($"symlink 失败,errno:{Marshal.GetLastWin32Error()}");
  144. return true;
  145. }
  146. }
  147. private static class NativeMethods {
  148. [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "CreateSymbolicLinkW")]
  149. [return: MarshalAs(UnmanagedType.I1)]
  150. internal static extern bool CreateSymbolicLink(string lpSymlinkFileName, string lpTargetFileName, uint dwFlags);
  151. [DllImport("libc", SetLastError = true, EntryPoint = "symlink")]
  152. internal static extern int Symlink(string target, string linkpath);
  153. }
  154. #endregion
  155. }