PathUtils.cs 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. namespace MeloongCore;
  2. public static class PathUtils {
  3. #region 分隔符
  4. /// <summary>
  5. /// 确保路径的末尾包含任意文件夹分隔符。
  6. /// </summary>
  7. public static string AddSlashSuffix(string folder)
  8. => folder + (folder.EndsWithF(Path.DirectorySeparatorChar) || folder.EndsWithF(Path.AltDirectorySeparatorChar) ? "" : Path.DirectorySeparatorChar);
  9. /// <summary>
  10. /// 确保路径的结尾不包含文件夹分隔符。
  11. /// </summary>
  12. public static string RemoveSlashSuffix(string folder)
  13. => folder.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
  14. #endregion
  15. #region 长路径
  16. /// <summary>
  17. /// 若路径较长,则尽量将其转换为短路径。
  18. /// <para/>结果的开头不含 <c>\\?\</c>,结尾不含分隔符。
  19. /// </summary>
  20. public static string ToShortPath(string fullName, bool keepFileName = false) {
  21. if (string.IsNullOrEmpty(fullName)) return fullName;
  22. if (!fullName.Contains(@":\")) return fullName;
  23. if (fullName.Length <= 200) return fullName;
  24. fullName = PathUtils.ForCompare(fullName);
  25. // 保留文件名
  26. string pathToKeep = "";
  27. string pathToShorten = fullName;
  28. if (fullName.EndsWithF(".jar", true)) keepFileName = true; // jar 文件的文件名需要保留原样,否则会导致 Forge 1.20.1 无法通过文件名识别模块名
  29. if (keepFileName && FileUtils.Exists(fullName)) {
  30. pathToKeep = PathUtils.GetLastPart(fullName);
  31. pathToShorten = PathUtils.RemoveLastPart(fullName);
  32. }
  33. // 逐级向上寻找已存在的文件夹,将不存在的部分挪到 suffix,不再缩短
  34. while (!DirectoryUtils.Exists(pathToShorten) && !FileUtils.Exists(pathToShorten)) { // 如果路径不存在
  35. string parentPath = Path.GetDirectoryName(pathToShorten);
  36. if (string.IsNullOrEmpty(parentPath) || parentPath == pathToShorten) return fullName; // 已经到达根目录,全都不存在,直接返回
  37. pathToKeep = Path.Combine(PathUtils.GetLastPart(pathToShorten), pathToKeep);
  38. pathToShorten = parentPath;
  39. }
  40. if (pathToShorten.Length <= 10) return fullName;
  41. // 实际缩短路径
  42. char[] buffer = new char[260];
  43. int result = GetShortPathNameW(PathUtils.ForApi(pathToShorten), buffer, buffer.Length);
  44. if (result == 0) return fullName;
  45. string shortPath = new(buffer, 0, result);
  46. return PathUtils.RemoveSlashSuffix(PathUtils.RemoveExtendedPrefix(Path.Combine(shortPath, pathToKeep)));
  47. }
  48. [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
  49. private static extern int GetShortPathNameW(string lpszLongPath, [Out] char[] buffer, int cchBuffer);
  50. /// <summary>
  51. /// 将完整路径转换为以 <c>\\?\</c> 开头的扩展格式。
  52. /// 这会去除路径末尾的分隔符,将 / 替换为 \。
  53. /// </summary>
  54. public static string ToExtendedFormat(string path) {
  55. if (string.IsNullOrWhiteSpace(path)) return path;
  56. if (path.StartsWithF(@"\\?\")) return path; // 已经是长路径
  57. path = RemoveSlashSuffix(path).Replace('/', '\\'); // API 要求这个格式……
  58. if (path.StartsWithF(@"\\")) {
  59. return $@"\\?\UNC\{path[2..]}";
  60. } else {
  61. return $@"\\?\{path}";
  62. }
  63. }
  64. /// <summary>
  65. /// 去除路径开头的 <c>\\?\</c>。
  66. /// </summary>
  67. public static string RemoveExtendedPrefix(string path) {
  68. if (path.StartsWithF(@"\\?\UNC\")) return @"\\" + path.AfterLast(@"\\?\UNC\");
  69. if (path.StartsWithF(@"\\?\")) return path.AfterLast(@"\\?\");
  70. return path;
  71. }
  72. #endregion
  73. #region 路径处理
  74. /// <summary>
  75. /// 去除路径的最后一级,并移除末尾的分隔符。
  76. /// <code>
  77. /// "C:\foo\bar.txt" → "C:\foo"
  78. /// "C:\foo\bar\" → "C:\foo"
  79. /// "C:\foo\bar" → "C:\foo"
  80. /// "C:\foo\" → "C:"
  81. /// "https://foo.bar/file/pack.zip?arg=1" → "https://foo.bar/file"
  82. /// </code></summary>
  83. public static string RemoveLastPart(string path) {
  84. if (path!.Contains("://")) { // 网络路径
  85. path = PathUtils.RemoveSlashSuffix(path.BeforeFirst("#").BeforeFirst("?")); // 去除参数
  86. }
  87. return PathUtils.RemoveSlashSuffix(path!).BeforeLastOfAny([@"\", "/"]);
  88. }
  89. /// <summary>
  90. /// 获取路径的最后一级,即文件名,或当前文件夹名。
  91. /// <code>
  92. /// "C:\foo\bar.txt" → "bar.txt"
  93. /// "C:\foo\bar\" → "bar"
  94. /// "C:\foo\bar" → "bar"
  95. /// "https://foo.bar/file/pack.zip?arg=1" → "pack.zip"
  96. /// </code></summary>
  97. public static string GetLastPart(string path) {
  98. if (path!.Contains("://")) { // 网络路径
  99. path = PathUtils.RemoveSlashSuffix(path.BeforeFirst("#").BeforeFirst("?")); // 去除参数
  100. } else { // 文件路径
  101. path = PathUtils.RemoveSlashSuffix(path);
  102. }
  103. return path.AfterLastOfAny([@"\", "/"]);
  104. }
  105. /// <summary>
  106. /// 获取路径或文件名中,仅不包含最后一级扩展名的部分。
  107. /// <code>
  108. /// "C:\foo\bar.txt" → "bar"
  109. /// "create.jar.disabled" → "create.jar"
  110. /// "https://foo.bar/file/page.xaml.vb?arg=1" → "page.xaml"
  111. /// </code>
  112. /// </summary>
  113. public static string GetFileNameWithoutExtension(string path)
  114. => GetLastPart(path).BeforeLast(".");
  115. /// <summary>
  116. /// 获取路径或文件名中的最后一级文件扩展名,不包含 <c>.</c>,固定小写。
  117. /// 如果没有 <c>.</c> 则返回空字符串。
  118. /// <code>
  119. /// "C:\FOO\BAR.TXT" → "txt"
  120. /// "create.jar.disabled" → "disabled"
  121. /// "C:\foo\bar" → ""
  122. /// "https://foo.bar/file/page.xaml.vb?arg=1" → "vb"
  123. /// </code>
  124. /// </summary>
  125. public static string GetExtension(string path) {
  126. var name = GetLastPart(path);
  127. return name.Contains(".") ? name.AfterLast(".").Lower() : "";
  128. }
  129. /// <summary>
  130. /// 获取路径的盘符,返回一个大写字母。
  131. /// 若路径不包含盘符,则返回 <c>null</c>。
  132. /// </summary>
  133. public static char? GetDiskName(string? path) {
  134. if (string.IsNullOrEmpty(path)) return null;
  135. path = PathUtils.RemoveExtendedPrefix(path!);
  136. if (path.StartsWithF(@"\\") || path.StartsWithF("/")) return null;
  137. return char.ToUpper(path[0]);
  138. }
  139. /// <summary>
  140. /// 统一路径格式,以便比较。
  141. /// <para/>具体而言:将短路径展开,去除前导的 <c>\\?\</c>,将分隔符改为 \,去除末尾的分隔符。
  142. /// </summary>
  143. public static string ForCompare(string path)
  144. => PathUtils.RemoveSlashSuffix(PathUtils.RemoveExtendedPrefix(Path.GetFullPath(path)).Replace("/", @"\"));
  145. /// <summary>
  146. /// 将路径转换为兼容各种 Windows API 的格式。
  147. /// <para/>具体而言:将分隔符改为 \。添加前导的 <c>\\?\</c>。若为驱动器则添加末尾分隔符,否则去除分隔符。
  148. /// </summary>
  149. public static string ForApi(string path) {
  150. path = PathUtils.ToExtendedFormat(path);
  151. if (path.EndsWithF(":")) path += @"\"; // 驱动器路径必须保留末尾的分隔符
  152. if (path.EndsWithF(@":\")) return path;
  153. return PathUtils.RemoveSlashSuffix(path);
  154. }
  155. #endregion
  156. #region 常用路径
  157. /// <summary>
  158. /// 程序可执行文件的所在文件夹,以 \ 结尾。
  159. /// </summary>
  160. public static string CurrentFolder => PathUtils.AddSlashSuffix(AppDomain.CurrentDomain.BaseDirectory);
  161. #endregion
  162. }