Selaa lähdekoodia

PathUtils.RemoveLastPart

龙腾猫跃 5 kuukautta sitten
vanhempi
sitoutus
4f1664459d

+ 4 - 4
Shared/Extensions/StringExtensions.cs

@@ -98,7 +98,7 @@ public static class StringExtensions {
     /// <code>"2024/11/08".BeforeFirst("/") => "2024"</code>
     /// <code>"2024/11/08".BeforeFirst("/") => "2024"</code>
     /// </summary>
     /// </summary>
     public static string BeforeFirst(this string str, string text, bool ignoreCase = false) {
     public static string BeforeFirst(this string str, string text, bool ignoreCase = false) {
-        int pos = string.IsNullOrEmpty(text) ? -1 : str.IndexOfF(text!, ignoreCase);
+        int pos = string.IsNullOrEmpty(text) ? -1 : str.IndexOfF(text, ignoreCase);
         if (pos < 0) return str;
         if (pos < 0) return str;
         return str.Substring(0, pos);
         return str.Substring(0, pos);
     }
     }
@@ -107,7 +107,7 @@ public static class StringExtensions {
     /// <code>"2024/11/08".BeforeLast("/") => "2024/11"</code>
     /// <code>"2024/11/08".BeforeLast("/") => "2024/11"</code>
     /// </summary>
     /// </summary>
     public static string BeforeLast(this string str, string text, bool ignoreCase = false) {
     public static string BeforeLast(this string str, string text, bool ignoreCase = false) {
-        int pos = string.IsNullOrEmpty(text) ? -1 : str.LastIndexOfF(text!, ignoreCase);
+        int pos = string.IsNullOrEmpty(text) ? -1 : str.LastIndexOfF(text, ignoreCase);
         if (pos < 0) return str;
         if (pos < 0) return str;
         return str.Substring(0, pos);
         return str.Substring(0, pos);
     }
     }
@@ -117,7 +117,7 @@ public static class StringExtensions {
     /// <code>"2024/11/08".AfterFirst("/") => "11/08"</code>
     /// <code>"2024/11/08".AfterFirst("/") => "11/08"</code>
     /// </summary>
     /// </summary>
     public static string AfterFirst(this string str, string text, bool ignoreCase = false) {
     public static string AfterFirst(this string str, string text, bool ignoreCase = false) {
-        int pos = string.IsNullOrEmpty(text) ? -1 : str.IndexOfF(text!, ignoreCase);
+        int pos = string.IsNullOrEmpty(text) ? -1 : str.IndexOfF(text, ignoreCase);
         if (pos < 0) return str;
         if (pos < 0) return str;
         return str.Substring(pos + text!.Length);
         return str.Substring(pos + text!.Length);
     }
     }
@@ -126,7 +126,7 @@ public static class StringExtensions {
     /// <code>"2024/11/08".AfterLast("/") => "08"</code>
     /// <code>"2024/11/08".AfterLast("/") => "08"</code>
     /// </summary>
     /// </summary>
     public static string AfterLast(this string str, string text, bool ignoreCase = false) {
     public static string AfterLast(this string str, string text, bool ignoreCase = false) {
-        int pos = string.IsNullOrEmpty(text) ? -1 : str.LastIndexOfF(text!, ignoreCase);
+        int pos = string.IsNullOrEmpty(text) ? -1 : str.LastIndexOfF(text, ignoreCase);
         if (pos < 0) return str;
         if (pos < 0) return str;
         return str.Substring(pos + text!.Length);
         return str.Substring(pos + text!.Length);
     }
     }

+ 2 - 2
Shared/General/MsDelta.cs

@@ -15,7 +15,7 @@ public static class MsDelta {
     public static void Create(string oldFilePath, string newFilePath, string deltaFilePath) {
     public static void Create(string oldFilePath, string newFilePath, string deltaFilePath) {
         if (!FileUtils.Exists(oldFilePath)) throw new FileNotFoundException($"旧文件不存在:{oldFilePath}", oldFilePath);
         if (!FileUtils.Exists(oldFilePath)) throw new FileNotFoundException($"旧文件不存在:{oldFilePath}", oldFilePath);
         if (!FileUtils.Exists(newFilePath)) throw new FileNotFoundException($"新文件不存在:{newFilePath}", newFilePath);
         if (!FileUtils.Exists(newFilePath)) throw new FileNotFoundException($"新文件不存在:{newFilePath}", newFilePath);
-        DirectoryUtils.Create(deltaFilePath, isFilePath: true);
+        DirectoryUtils.Create(PathUtils.RemoveLastPart(deltaFilePath));
         Logger.Info($"正在生成补丁:{oldFilePath} → {newFilePath},补丁文件将输出到:{deltaFilePath}");
         Logger.Info($"正在生成补丁:{oldFilePath} → {newFilePath},补丁文件将输出到:{deltaFilePath}");
         // fileTypeSet:https://learn.microsoft.com/en-us/previous-versions/bb417345(v=msdn.10)?redirectedfrom=MSDN#file-type-sets (15L: DELTA_FILE_TYPE_SET_EXECUTABLES)
         // fileTypeSet:https://learn.microsoft.com/en-us/previous-versions/bb417345(v=msdn.10)?redirectedfrom=MSDN#file-type-sets (15L: DELTA_FILE_TYPE_SET_EXECUTABLES)
         // 131072L: IgnoreFileSizeLimit, 32u: Crc32
         // 131072L: IgnoreFileSizeLimit, 32u: Crc32
@@ -32,7 +32,7 @@ public static class MsDelta {
     public static void Apply(string oldFilePath, string deltaFilePath, string newFilePath) {
     public static void Apply(string oldFilePath, string deltaFilePath, string newFilePath) {
         if (!FileUtils.Exists(oldFilePath)) throw new FileNotFoundException($"旧文件不存在:{oldFilePath}", oldFilePath);
         if (!FileUtils.Exists(oldFilePath)) throw new FileNotFoundException($"旧文件不存在:{oldFilePath}", oldFilePath);
         if (!FileUtils.Exists(deltaFilePath)) throw new FileNotFoundException($"补丁文件不存在:{deltaFilePath}", deltaFilePath);
         if (!FileUtils.Exists(deltaFilePath)) throw new FileNotFoundException($"补丁文件不存在:{deltaFilePath}", deltaFilePath);
-        DirectoryUtils.Create(newFilePath, isFilePath: true);
+        DirectoryUtils.Create(PathUtils.RemoveLastPart(newFilePath));
         Logger.Info($"正在应用补丁:原始文件 {oldFilePath} + 补丁文件 {deltaFilePath} → 输出至 {newFilePath}");
         Logger.Info($"正在应用补丁:原始文件 {oldFilePath} + 补丁文件 {deltaFilePath} → 输出至 {newFilePath}");
         // 1L: AllowLegacy
         // 1L: AllowLegacy
         if (!ApplyDelta(1L, PathUtils.WithLongPath(oldFilePath), PathUtils.WithLongPath(deltaFilePath), PathUtils.WithLongPath(newFilePath)))
         if (!ApplyDelta(1L, PathUtils.WithLongPath(oldFilePath), PathUtils.WithLongPath(deltaFilePath), PathUtils.WithLongPath(newFilePath)))

+ 1 - 2
Shared/Utils/DirectoryUtils.cs

@@ -5,8 +5,7 @@ public static class DirectoryUtils {
     /// 创建文件夹,或文件所在的文件夹。
     /// 创建文件夹,或文件所在的文件夹。
     /// 文件夹已存在时不会抛出异常。
     /// 文件夹已存在时不会抛出异常。
     /// </summary>
     /// </summary>
-    public static void Create(string path, bool isFilePath = false) {
-        if (isFilePath) path = PathUtils.RemoveFileName(path);
+    public static void Create(string path) {
         if (DirectoryUtils.Exists(path)) return;
         if (DirectoryUtils.Exists(path)) return;
         Logger.Trace($"新建文件夹:{path}");
         Logger.Trace($"新建文件夹:{path}");
         path = PathUtils.WithLongPath(path);
         path = PathUtils.WithLongPath(path);

+ 7 - 6
Shared/Utils/FileUtils.cs

@@ -68,7 +68,7 @@ public static class FileUtils {
     /// 若文件已存在,则会覆盖原文件。
     /// 若文件已存在,则会覆盖原文件。
     /// </summary>
     /// </summary>
     public static void Write(string filePath, byte[] content) {
     public static void Write(string filePath, byte[] content) {
-        DirectoryUtils.Create(filePath, isFilePath: true);
+        DirectoryUtils.Create(PathUtils.RemoveLastPart(filePath));
         Logger.Trace($"写入文件:{filePath}({content.Length} 字节)");
         Logger.Trace($"写入文件:{filePath}({content.Length} 字节)");
         ResilientUtils.RetryOn<IOException>(() 
         ResilientUtils.RetryOn<IOException>(() 
             => File.WriteAllBytes(PathUtils.WithLongPath(filePath), content));
             => File.WriteAllBytes(PathUtils.WithLongPath(filePath), content));
@@ -90,7 +90,7 @@ public static class FileUtils {
     /// 若文件已存在,则会覆盖原文件。
     /// 若文件已存在,则会覆盖原文件。
     /// </summary>
     /// </summary>
     public static FileStream CreateAsStream(string filePath) {
     public static FileStream CreateAsStream(string filePath) {
-        DirectoryUtils.Create(filePath, isFilePath: true);
+        DirectoryUtils.Create(PathUtils.RemoveLastPart(filePath));
         Logger.Trace($"创建文件流:{filePath}");
         Logger.Trace($"创建文件流:{filePath}");
         return ResilientUtils.RetryOn<IOException, FileStream>(()
         return ResilientUtils.RetryOn<IOException, FileStream>(()
             => new FileStream(PathUtils.WithLongPath(filePath), FileMode.Create));
             => new FileStream(PathUtils.WithLongPath(filePath), FileMode.Create));
@@ -111,7 +111,7 @@ public static class FileUtils {
             Logger.Info($"尝试复制文件,但原文件不存在,已跳过复制:{sourceFilePath} → {destFilePath}");
             Logger.Info($"尝试复制文件,但原文件不存在,已跳过复制:{sourceFilePath} → {destFilePath}");
             return;
             return;
         }
         }
-        DirectoryUtils.Create(destFilePath, isFilePath: true);
+        DirectoryUtils.Create(PathUtils.RemoveLastPart(destFilePath));
         Logger.Trace($"复制文件:{sourceFilePath} → {destFilePath}");
         Logger.Trace($"复制文件:{sourceFilePath} → {destFilePath}");
         ResilientUtils.RetryOn<IOException>(()
         ResilientUtils.RetryOn<IOException>(()
             => File.Copy(PathUtils.WithLongPath(sourceFilePath), PathUtils.WithLongPath(destFilePath), true));
             => File.Copy(PathUtils.WithLongPath(sourceFilePath), PathUtils.WithLongPath(destFilePath), true));
@@ -123,7 +123,7 @@ public static class FileUtils {
     /// </summary>
     /// </summary>
     public static void Move(string sourceFilePath, string destFilePath) {
     public static void Move(string sourceFilePath, string destFilePath) {
         if (sourceFilePath == destFilePath) return; // 如果移动同一个文件则跳过
         if (sourceFilePath == destFilePath) return; // 如果移动同一个文件则跳过
-        DirectoryUtils.Create(destFilePath, isFilePath: true);
+        DirectoryUtils.Create(PathUtils.RemoveLastPart(destFilePath));
         FileUtils.Delete(destFilePath);
         FileUtils.Delete(destFilePath);
         Logger.Trace($"剪切文件:{sourceFilePath} → {destFilePath}");
         Logger.Trace($"剪切文件:{sourceFilePath} → {destFilePath}");
         ResilientUtils.RetryOn<IOException>(()
         ResilientUtils.RetryOn<IOException>(()
@@ -265,6 +265,7 @@ public static class FileUtils {
             using var fileStream = FileUtils.ReadAsStream(compressionFile);
             using var fileStream = FileUtils.ReadAsStream(compressionFile);
             using var gzipStream = new GZipStream(fileStream, CompressionMode.Decompress);
             using var gzipStream = new GZipStream(fileStream, CompressionMode.Decompress);
             FileUtils.Write(outFilePath, gzipStream);
             FileUtils.Write(outFilePath, gzipStream);
+            progressHandler?.Invoke(1);
             return;
             return;
         }
         }
         // 解压 zip
         // 解压 zip
@@ -294,7 +295,7 @@ public static class FileUtils {
     public static void CreateZipFromDirectory(string outputFullPath, string sourceDirectory) {
     public static void CreateZipFromDirectory(string outputFullPath, string sourceDirectory) {
         outputFullPath = PathUtils.WithLongPath(outputFullPath);
         outputFullPath = PathUtils.WithLongPath(outputFullPath);
         sourceDirectory = PathUtils.WithLongPath(sourceDirectory);
         sourceDirectory = PathUtils.WithLongPath(sourceDirectory);
-        DirectoryUtils.Create(outputFullPath, isFilePath: true);
+        DirectoryUtils.Create(PathUtils.RemoveLastPart(outputFullPath));
         FileUtils.Delete(outputFullPath);
         FileUtils.Delete(outputFullPath);
         Logger.Trace($"将文件夹中的内容压缩为 zip 文件:{sourceDirectory} → {outputFullPath}");
         Logger.Trace($"将文件夹中的内容压缩为 zip 文件:{sourceDirectory} → {outputFullPath}");
         ResilientUtils.RetryOn<IOException>(()
         ResilientUtils.RetryOn<IOException>(()
@@ -322,7 +323,7 @@ public static class FileUtils {
     /// <param name="sources">键为 zip 文件下的路径,值为文件的本地路径。</param>
     /// <param name="sources">键为 zip 文件下的路径,值为文件的本地路径。</param>
     public static void CreateZipFromFiles(string outputFullPath, IDictionary<string, string> sources) {
     public static void CreateZipFromFiles(string outputFullPath, IDictionary<string, string> sources) {
         outputFullPath = PathUtils.WithLongPath(outputFullPath);
         outputFullPath = PathUtils.WithLongPath(outputFullPath);
-        DirectoryUtils.Create(outputFullPath, isFilePath: true);
+        DirectoryUtils.Create(PathUtils.RemoveLastPart(outputFullPath));
         FileUtils.Delete(outputFullPath);
         FileUtils.Delete(outputFullPath);
         using var archive = ResilientUtils.RetryOn<IOException, ZipArchive>(()
         using var archive = ResilientUtils.RetryOn<IOException, ZipArchive>(()
             => ZipFile.Open(outputFullPath, ZipArchiveMode.Create));
             => ZipFile.Open(outputFullPath, ZipArchiveMode.Create));

+ 16 - 8
Shared/Utils/PathUtils.cs

@@ -34,7 +34,7 @@ public static class PathUtils {
         if (fullName.EndsWithF(".jar", true)) keepFileName = true; // jar 文件的文件名需要保留原样,否则会导致 Forge 1.20.1 无法通过文件名识别模块名
         if (fullName.EndsWithF(".jar", true)) keepFileName = true; // jar 文件的文件名需要保留原样,否则会导致 Forge 1.20.1 无法通过文件名识别模块名
         if (keepFileName && FileUtils.Exists(fullName)) {
         if (keepFileName && FileUtils.Exists(fullName)) {
             pathToKeep = Path.GetFileName(fullName);
             pathToKeep = Path.GetFileName(fullName);
-            pathToShorten = PathUtils.RemoveFileName(fullName);
+            pathToShorten = PathUtils.RemoveLastPart(fullName);
         }
         }
 
 
         // 逐级向上寻找已存在的文件夹,将不存在的部分挪到 suffix,不再缩短
         // 逐级向上寻找已存在的文件夹,将不存在的部分挪到 suffix,不再缩短
@@ -85,13 +85,21 @@ public static class PathUtils {
     #region 路径处理
     #region 路径处理
 
 
     /// <summary>
     /// <summary>
-    /// 去除路径末尾的文件名。
-    /// 若路径以分隔符结尾,则不作处理。
-    /// </summary>
-    public static string RemoveFileName(string? path) {
-        if (path is null || string.IsNullOrEmpty(path)) return "";
-        if (path.EndsWithF(Path.DirectorySeparatorChar) || path.EndsWithF(Path.AltDirectorySeparatorChar)) return path;
-        return Path.GetDirectoryName(path) ?? "";
+    /// 去除路径的最后一级,并移除末尾的分隔符。
+    /// <code>
+    /// "C:\foo\bar.txt" → "C:\foo"
+    /// "C:\foo\bar" → "C:\foo"
+    /// "C:\foo\bar\" → "C:\foo"
+    /// "C:\foo\" → "C:"
+    /// "https://foo.bar/file/pack.zip?arg=1" → "https://foo.bar/file"
+    /// </code></summary>
+    public static string RemoveLastPart(string? path) {
+        if (path!.Contains("://")) { // 网络路径
+            path = PathUtils.WithoutSeparator(path.BeforeFirst("#").BeforeFirst("?")); // 去除参数
+            return PathUtils.WithoutSeparator(path).BeforeLast("/");
+        } else { // 文件路径
+            return PathUtils.WithoutSeparator(path!).BeforeLast("\\");
+        }
     }
     }
 
 
     /// <summary>
     /// <summary>

+ 4 - 4
Tests/Utils/FileUtilsTest.cs

@@ -6,7 +6,7 @@ public class FileUtilsTest : TestWithFolder {
     [Theory]
     [Theory]
     [InlineData("GB Encoding.zip")]
     [InlineData("GB Encoding.zip")]
     [InlineData("UTF8 Encoding.zip")]
     [InlineData("UTF8 Encoding.zip")]
-    public void Compression_ReadZip(string testFile) {
+    public void 压缩包_ReadZip(string testFile) {
         double progress = 0;
         double progress = 0;
         string output = Path.Combine(tempFolder, "Extracted");
         string output = Path.Combine(tempFolder, "Extracted");
         FileUtils.ExtractToDirectory(GetTestFile(testFile), output, p => progress = p);
         FileUtils.ExtractToDirectory(GetTestFile(testFile), output, p => progress = p);
@@ -19,7 +19,7 @@ public class FileUtilsTest : TestWithFolder {
 
 
     [Theory]
     [Theory]
     [InlineData("GZ.gz", "LTCat")]
     [InlineData("GZ.gz", "LTCat")]
-    public void Compression_ReadGz(string testFile, string containsText) {
+    public void 压缩包_ReadGz(string testFile, string containsText) {
         double progress = 0;
         double progress = 0;
         string output = Path.Combine(tempFolder, "Extracted");
         string output = Path.Combine(tempFolder, "Extracted");
         FileUtils.ExtractToDirectory(GetTestFile(testFile), output, p => progress = p);
         FileUtils.ExtractToDirectory(GetTestFile(testFile), output, p => progress = p);
@@ -30,14 +30,14 @@ public class FileUtilsTest : TestWithFolder {
     [Theory]
     [Theory]
     [InlineData("Corrupted.zip")]
     [InlineData("Corrupted.zip")]
     [InlineData("Not zip.zip")]
     [InlineData("Not zip.zip")]
-    public void Compression_ReadBad(string testFile) {
+    public void 压缩包_ReadBad(string testFile) {
         Assert.Throws<InvalidDataException>(() => FileUtils.ExtractToDirectory(GetTestFile(testFile), tempFolder));
         Assert.Throws<InvalidDataException>(() => FileUtils.ExtractToDirectory(GetTestFile(testFile), tempFolder));
     }
     }
 
 
     [Theory]
     [Theory]
     [InlineData("DotDot ZipSlip.zip")]
     [InlineData("DotDot ZipSlip.zip")]
     [InlineData("AbsPath ZipSlip.zip")]
     [InlineData("AbsPath ZipSlip.zip")]
-    public void Compression_ZipSlip(string testFile) {
+    public void 压缩包_ZipSlip(string testFile) {
         Assert.Throws<FileUtils.ZipSlipException>(() => FileUtils.ExtractToDirectory(GetTestFile(testFile), tempFolder));
         Assert.Throws<FileUtils.ZipSlipException>(() => FileUtils.ExtractToDirectory(GetTestFile(testFile), tempFolder));
     }
     }
 
 

+ 20 - 0
Tests/Utils/PathUtilsTest.cs

@@ -0,0 +1,20 @@
+namespace MeloongCore.Tests;
+public class PathUtilsTests {
+
+    [Theory]
+    [InlineData(@"\\?\C:\foo\bar.txt", @"\\?\C:\foo")]
+    [InlineData(@"C:\foo\bar.txt", @"C:\foo")]
+    [InlineData(@"\\?\C:\foo\bar", @"\\?\C:\foo")]
+    [InlineData(@"C:\foo\bar", @"C:\foo")]
+    [InlineData(@"C:\foo\bar\", @"C:\foo")]
+    [InlineData(@"\\?\C:\foo\", @"\\?\C:")]
+    [InlineData(@"C:\foo", @"C:")]
+    [InlineData(@"C:\foo\", @"C:")]
+    [InlineData("https://foo.bar/file/long/pack.zip?arg=1", "https://foo.bar/file/long")]
+    [InlineData("https://foo.bar/file/", "https://foo.bar")]
+    [InlineData("https://foo.bar/file/?arg=1", "https://foo.bar")]
+    [InlineData("https://foo.bar/file?arg=1", "https://foo.bar")]
+    public void 路径处理_RemoveLastPart(string? input, string expected) 
+        => Assert.Equal(expected, PathUtils.RemoveLastPart(input));
+
+}