PathUtils.cs 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  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 pathName, bool keepFileName = false) {
  21. if (string.IsNullOrEmpty(pathName)) return pathName;
  22. if (!pathName.Contains(":")) return pathName;
  23. if (pathName.Length <= 200) return pathName;
  24. pathName = PathUtils.ForCompare(pathName);
  25. // 保留文件名
  26. string pathToKeep = "";
  27. string pathToShorten = pathName;
  28. if (pathName.EndsWithF(".jar", true)) keepFileName = true; // jar 文件的文件名需要保留原样,否则会导致 Forge 1.20.1 无法通过文件名识别模块名
  29. if (keepFileName && FileUtils.Exists(pathName)) {
  30. pathToKeep = PathUtils.GetLastPart(pathName);
  31. pathToShorten = PathUtils.RemoveLastPart(pathName);
  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 pathName; // 已经到达根目录,全都不存在,直接返回
  37. pathToKeep = Path.Combine(PathUtils.GetLastPart(pathToShorten), pathToKeep);
  38. pathToShorten = parentPath;
  39. }
  40. if (pathToShorten.Length <= 10) return pathName;
  41. // 实际缩短路径
  42. char[] buffer = new char[260];
  43. int result = GetShortPathNameW(PathUtils.ForApi(pathToShorten), buffer, buffer.Length);
  44. if (result == 0) return pathName;
  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 pathName) {
  55. if (string.IsNullOrWhiteSpace(pathName)) return pathName;
  56. if (!pathName.Contains(@":")) return pathName; // 可能是相对路径,保持不变
  57. if (pathName.StartsWithF(@"\\?\")) return pathName; // 已经是长路径
  58. pathName = RemoveSlashSuffix(pathName).Replace('/', '\\'); // API 要求这个格式……
  59. if (pathName.StartsWithF(@"\\")) {
  60. return $@"\\?\UNC\{pathName[2..]}";
  61. } else {
  62. return $@"\\?\{pathName}";
  63. }
  64. }
  65. /// <summary>
  66. /// 去除路径开头的 <c>\\?\</c>。
  67. /// </summary>
  68. public static string RemoveExtendedPrefix(string pathName) {
  69. if (pathName.StartsWithF(@"\\?\UNC\")) return @"\\" + pathName.AfterLast(@"\\?\UNC\");
  70. if (pathName.StartsWithF(@"\\?\")) return pathName.AfterLast(@"\\?\");
  71. return pathName;
  72. }
  73. #endregion
  74. #region 路径处理
  75. /// <summary>
  76. /// 去除路径的最后一级,并移除末尾的分隔符。
  77. /// <code>
  78. /// "C:\foo\bar.txt" → "C:\foo"
  79. /// "C:\foo\bar\" → "C:\foo"
  80. /// "C:\foo\bar" → "C:\foo"
  81. /// "C:\foo\" → "C:"
  82. /// "https://foo.bar/file/pack.zip?arg=1" → "https://foo.bar/file"
  83. /// </code></summary>
  84. public static string RemoveLastPart(string pathName) {
  85. if (pathName!.Contains("://")) { // 网络路径
  86. pathName = PathUtils.RemoveSlashSuffix(pathName.BeforeFirst("#").BeforeFirst("?")); // 去除参数
  87. }
  88. return PathUtils.RemoveSlashSuffix(pathName!).BeforeLastOfAny([@"\", "/"]);
  89. }
  90. /// <summary>
  91. /// 获取路径的最后一级,即文件名,或当前文件夹名。
  92. /// <code>
  93. /// "C:\foo\bar.txt" → "bar.txt"
  94. /// "C:\foo\bar\" → "bar"
  95. /// "C:\foo\bar" → "bar"
  96. /// "https://foo.bar/file/pack.zip?arg=1" → "pack.zip"
  97. /// </code></summary>
  98. public static string GetLastPart(string pathName) {
  99. if (pathName!.Contains("://")) { // 网络路径
  100. pathName = PathUtils.RemoveSlashSuffix(pathName.BeforeFirst("#").BeforeFirst("?")); // 去除参数
  101. } else { // 文件路径
  102. pathName = PathUtils.RemoveSlashSuffix(pathName);
  103. }
  104. return pathName.AfterLastOfAny([@"\", "/"]);
  105. }
  106. /// <summary>
  107. /// 获取路径或文件名中,仅不包含最后一级扩展名的部分。
  108. /// <code>
  109. /// "C:\foo\bar.txt" → "bar"
  110. /// "create.jar.disabled" → "create.jar"
  111. /// "https://foo.bar/file/page.xaml.vb?arg=1" → "page.xaml"
  112. /// </code>
  113. /// </summary>
  114. public static string GetFileNameWithoutExtension(string pathName)
  115. => GetLastPart(pathName).BeforeLast(".");
  116. /// <summary>
  117. /// 获取路径或文件名中的最后一级文件扩展名,不包含 <c>.</c>,固定小写。
  118. /// 如果没有 <c>.</c> 则返回空字符串。
  119. /// <code>
  120. /// "C:\FOO\BAR.TXT" → "txt"
  121. /// "create.jar.disabled" → "disabled"
  122. /// "C:\foo\bar" → ""
  123. /// "https://foo.bar/file/page.xaml.vb?arg=1" → "vb"
  124. /// </code>
  125. /// </summary>
  126. public static string GetExtension(string pathName) {
  127. var name = GetLastPart(pathName);
  128. return name.Contains(".") ? name.AfterLast(".").Lower() : "";
  129. }
  130. /// <summary>
  131. /// 获取路径的盘符,返回一个大写字母。
  132. /// 若路径不包含盘符,则返回 <c>null</c>。
  133. /// </summary>
  134. public static char? GetDiskName(string? pathName) {
  135. if (string.IsNullOrEmpty(pathName)) return null;
  136. pathName = PathUtils.RemoveExtendedPrefix(pathName!);
  137. if (pathName.StartsWithF(@"\\") || pathName.StartsWithF("/")) return null;
  138. return char.ToUpper(pathName[0]);
  139. }
  140. /// <summary>
  141. /// 统一路径格式,以便比较。
  142. /// <para/>具体而言:将短路径展开,去除前导的 <c>\\?\</c>,将分隔符改为 \,去除末尾的分隔符。
  143. /// </summary>
  144. public static string ForCompare(string pathName) {
  145. try {
  146. return PathUtils.RemoveSlashSuffix(PathUtils.RemoveExtendedPrefix(Path.GetFullPath(pathName)).Replace("/", @"\"));
  147. } catch (ArgumentException ex) {
  148. throw new ArgumentException($"路径不正确:{pathName}", ex);
  149. }
  150. }
  151. /// <summary>
  152. /// 将路径转换为兼容各种 Windows API 的格式。
  153. /// <para/>具体而言:将分隔符改为 \。添加前导的 <c>\\?\</c>。若为驱动器则添加末尾分隔符,否则去除分隔符。
  154. /// </summary>
  155. public static string ForApi(string pathName) {
  156. pathName = PathUtils.ToExtendedFormat(pathName);
  157. if (pathName.EndsWithF(":")) pathName += @"\"; // 驱动器路径必须保留末尾的分隔符
  158. if (pathName.EndsWithF(@":\")) return pathName;
  159. return PathUtils.RemoveSlashSuffix(pathName);
  160. }
  161. #endregion
  162. /// <summary>
  163. /// 判断 <param name="parentPath"/> 是否为 <paramref name="childPath"/> 的父路径,或两者是否相同。
  164. /// </summary>
  165. public static bool IsParentOf(string parentPath, string childPath) {
  166. parentPath = PathUtils.ForCompare(parentPath);
  167. childPath = PathUtils.ForCompare(childPath);
  168. return childPath.StartsWithF(parentPath + @"\") || childPath == parentPath;
  169. }
  170. }
  171. /// <summary>
  172. /// 常用路径合集。
  173. /// 所有路径均以分隔符结尾。
  174. /// </summary>
  175. public static class Paths {
  176. /// <summary>
  177. /// 可执行文件所在的文件夹。
  178. /// </summary>
  179. public static string Base => PathUtils.AddSlashSuffix(AppContext.BaseDirectory);
  180. /// <summary>
  181. /// 可执行文件所在的文件夹中,名为 <see cref="Main.AppName"/> 的子文件夹。
  182. /// </summary>
  183. public static string BaseThenName => PathUtils.AddSlashSuffix(Base + Main.AppName);
  184. /// <summary>
  185. /// %AppData% 文件夹。
  186. /// </summary>
  187. public static string AppData => PathUtils.AddSlashSuffix(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData));
  188. /// <summary>
  189. /// %AppData% 文件夹中,名为 <see cref="Main.AppName"/> 的子文件夹。
  190. /// </summary>
  191. public static string AppDataThenName => PathUtils.AddSlashSuffix(AppData + Main.AppName);
  192. }