ソースを参照

文件操作大小写兼容

龙腾猫跃 5 ヶ月 前
親
コミット
0ff787c5d9
3 ファイル変更、130 行追加、48 行削除
  1. 61 25
      Shared/Utils/DirectoryUtils.cs
  2. 58 23
      Shared/Utils/FileUtils.cs
  3. 11 0
      Shared/Utils/PathUtils.cs

+ 61 - 25
Shared/Utils/DirectoryUtils.cs

@@ -11,6 +11,15 @@ public static class DirectoryUtils {
         Directory.CreateDirectory(PathUtils.ForApi(path));
     }
 
+    /// <summary>
+    /// 在临时文件夹下创建一个随机名称的文件夹,并返回其路径。
+    /// </summary>
+    public static string CreateRandom() {
+        var tempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+        DirectoryUtils.Create(tempDir);
+        return tempDir;
+    }
+
     /// <summary>
     /// 判断文件夹是否存在。
     /// </summary>
@@ -54,48 +63,62 @@ public static class DirectoryUtils {
 
     /// <summary>
     /// 复制文件夹。
-    /// 若原文件夹不存在,则不执行。
+    /// 若复制自身到自身,则不执行操作;若仅大小写不同,则重命名此文件夹。
     /// </summary>
-    public static void Copy(string sourceFolder, string destFolder, Action<double>? progressHandler = null) {
-        if (!DirectoryUtils.Exists(sourceFolder)) {
-            Logger.Info($"尝试复制文件夹,但原文件夹不存在,已跳过复制:{sourceFolder} → {destFolder}");
-            progressHandler?.Invoke(1);
-            return;
-        }
+    public static void Copy(string sourceFolder, string destFolder) {
         sourceFolder = PathUtils.ForCompare(sourceFolder);
         destFolder = PathUtils.ForCompare(destFolder);
-        int doneCount = 0;
-        Lazy<int> totalCount = new(() => DirectoryUtils.GetFiles(sourceFolder).Count());
-        Logger.Trace($"复制文件夹:{sourceFolder} → {destFolder}");
-        foreach (var file in DirectoryUtils.GetFiles(sourceFolder)) {
-            FileUtils.Copy(file, file.Replace(sourceFolder, destFolder));
-            doneCount++;
-            if (progressHandler is not null) progressHandler?.Invoke((double) doneCount / totalCount.Value);
+        if (string.Compare(sourceFolder, destFolder, ignoreCase: false) == 0) {
+            // 复制自身到自身,则不执行操作
+            Logger.Trace($"复制文件夹到自身,不执行操作:{sourceFolder} → {destFolder}");
+        } else if (string.Compare(sourceFolder, destFolder, ignoreCase: true) == 0) {
+            // 路径仅大小写不同,等效于重命名
+            Logger.Trace($"复制文件夹到自身,但大小写不同,等效于重命名文件夹:{sourceFolder} → {destFolder}");
+            DirectoryUtils.Move(sourceFolder, destFolder);
+        } else {
+            // 实际的复制
+            Logger.Trace($"复制文件夹:{sourceFolder} → {destFolder}");
+            foreach (var file in DirectoryUtils.GetFiles(sourceFolder)) FileUtils.Copy(file, file.Replace(sourceFolder, destFolder));
         }
     }
 
     /// <summary>
     /// 剪切文件夹。
+    /// 会创建对应文件夹、覆盖已有的文件夹。
     /// </summary>
     public static void Move(string sourceFolder, string destFolder) {
-        if (!DirectoryUtils.Exists(sourceFolder)) return;
-        DirectoryUtils.Delete(destFolder); // Move 要求不存在对应文件夹
-        Logger.Trace($"剪切文件夹:{sourceFolder} → {destFolder}");
-        Directory.Move(PathUtils.ForApi(sourceFolder), PathUtils.ForApi(destFolder));
+        SafetyCheck(sourceFolder);
+        sourceFolder = PathUtils.ForCompare(sourceFolder);
+        destFolder = PathUtils.ForCompare(destFolder);
+        if (string.Compare(sourceFolder, destFolder, ignoreCase: false) == 0) {
+            // 剪切自身到自身,则不执行操作
+            Logger.Trace($"剪切文件夹到自身,不执行操作:{sourceFolder} → {destFolder}");
+        } else if (string.Compare(sourceFolder, destFolder, ignoreCase: true) == 0) {
+            // 路径仅大小写不同
+            Logger.Trace($"剪切文件夹到自身,但大小写不同:{sourceFolder} → {destFolder}");
+            var temp = Path.Combine(PathUtils.RemoveLastPart(sourceFolder), Path.GetRandomFileName());
+            DirectoryUtils.Move(sourceFolder, temp);
+            DirectoryUtils.Move(temp, destFolder);
+        } else if (PathUtils.GetDiskName(sourceFolder) == PathUtils.GetDiskName(destFolder)) {
+            // 同一磁盘剪切,直接调用 Move(这只修改文件夹名,效率更高)
+            Logger.Trace($"剪切文件夹到同一磁盘:{sourceFolder} → {destFolder}");
+            DirectoryUtils.Delete(destFolder); // Move 要求此前不存在对应文件夹
+            Directory.Move(PathUtils.ForApi(sourceFolder), PathUtils.ForApi(destFolder));
+        } else {
+            // 不同磁盘,必须先复制再删除,这就是我们傻逼微软
+            Logger.Trace($"剪切文件夹到不同磁盘:{sourceFolder} → {destFolder}");
+            DirectoryUtils.Copy(sourceFolder, destFolder);
+            DirectoryUtils.Delete(sourceFolder);
+        }
     }
 
     /// <summary>
     /// 删除文件夹。
     /// </summary>
     public static void Delete(string folder, bool toRecycleBin = false) {
-        // 安全检查
         if (!DirectoryUtils.Exists(folder)) return;
         Logger.Trace($"{(toRecycleBin ? "将文件夹删除到回收站" : "删除文件夹")}:{folder}");
-        folder = PathUtils.ForCompare(folder);
-        if (folder == PathUtils.ForCompare(Path.GetPathRoot(folder)))
-            throw new UnauthorizedAccessException($"不应删除磁盘根目录:{folder}");
-        if (criticalFolders.Value.Any(f => f.StartsWithF(folder, ignoreCase: true)))
-            throw new UnauthorizedAccessException($"不应删除的文件夹:{folder}");
+        SafetyCheck(folder);
         // 删除
         if (toRecycleBin) {
             FileUtils.DeleteToRecycleBin(folder); // 删除到回收站
@@ -110,7 +133,7 @@ public static class DirectoryUtils {
                 } catch (DirectoryNotFoundException ex) { // #4549,也可能已被其他线程删除
                     if (DirectoryUtils.Exists(folder)) {
                         Logger.Warn(ex, $"该文件夹可能为孤立的符号链接,尝试直接删除({folder})");
-                        Directory.Delete(folder, true);
+                        Directory.Delete(folder);
                     } else {
                         throw;
                     }
@@ -118,11 +141,24 @@ public static class DirectoryUtils {
             }
         }
     }
+
+    /// <summary>
+    /// 进行安全检查:若尝试操作桌面、文档、下载、Windows、当前程序所在文件夹,则抛出异常。
+    /// </summary>
+    /// <exception cref="UnauthorizedAccessException" />
+    private static void SafetyCheck(string folder) {
+        folder = PathUtils.ForCompare(folder);
+        if (folder == PathUtils.ForCompare(Path.GetPathRoot(folder)))
+            throw new UnauthorizedAccessException($"不应操作磁盘根目录:{folder}");
+        if (criticalFolders.Value.Any(f => f.StartsWithF(folder, ignoreCase: true)))
+            throw new UnauthorizedAccessException($"不应操作文件夹:{folder}");
+    }
     private static readonly Lazy<HashSet<string>> criticalFolders = new(() => new(new[] {
         PathUtils.CurrentFolder,
         Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
         Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory),
         Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
+        Environment.GetFolderPath(Environment.SpecialFolder.Windows),
         Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Downloads") // 下载文件夹没有 SpecialFolder 枚举
     }.Select(f => PathUtils.ForCompare(f))));
 

+ 58 - 23
Shared/Utils/FileUtils.cs

@@ -74,8 +74,10 @@ public static class FileUtils {
     public static void Write(string filePath, byte[] content) {
         DirectoryUtils.Create(PathUtils.RemoveLastPart(filePath));
         Logger.Trace($"写入文件:{filePath}({content.Length} 字节)");
-        ResilientUtils.RetryOn<IOException>(() 
-            => File.WriteAllBytes(PathUtils.ForApi(filePath), content));
+        ResilientUtils.RetryOn<IOException>(() => {
+            FileUtils.Delete(filePath);
+            File.WriteAllBytes(PathUtils.ForApi(filePath), content);
+        });
     }
 
     /// <summary>
@@ -84,10 +86,13 @@ public static class FileUtils {
     /// 若文件已存在,则会覆盖原文件。
     /// </summary>
     public static void Write(string filePath, Stream stream) {
-        using FileStream fileStream = FileUtils.CreateAsStream(filePath);
-        if (stream.CanSeek && stream.Position != 0) stream.Seek(0, SeekOrigin.Begin);
-        Logger.Trace($"写入文件:{filePath}({stream.GetType().Name}{(stream.CanSeek ? $" {stream.Length} 字节" : "")})");
-        stream.CopyTo(fileStream);
+        ResilientUtils.RetryOn<IOException>(() => {
+            FileUtils.Delete(filePath);
+            using FileStream fileStream = FileUtils.CreateAsStream(filePath);
+            if (stream.CanSeek && stream.Position != 0) stream.Seek(0, SeekOrigin.Begin);
+            Logger.Trace($"写入文件:{filePath}({stream.GetType().Name}{(stream.CanSeek ? $" {stream.Length} 字节" : "")})");
+            stream.CopyTo(fileStream);
+        });
     }
     /// <summary>
     /// 创建文件,并打开 <see cref="FileStream"/>。
@@ -96,10 +101,18 @@ public static class FileUtils {
     public static FileStream CreateAsStream(string filePath) {
         DirectoryUtils.Create(PathUtils.RemoveLastPart(filePath));
         Logger.Trace($"创建文件流:{filePath}");
-        return ResilientUtils.RetryOn<IOException, FileStream>(()
-            => new FileStream(PathUtils.ForApi(filePath), FileMode.Create));
+        return ResilientUtils.RetryOn<IOException, FileStream>(() => {
+            FileUtils.Delete(filePath);
+            return new FileStream(PathUtils.ForApi(filePath), FileMode.Create);
+        });
     }
 
+    /// <summary>
+    /// 在临时文件夹下创建一个随机名称的文件,并返回其路径。
+    /// </summary>
+    public static string CreateRandom()
+        => ResilientUtils.RetryOn<IOException, string>(Path.GetTempFileName);
+
     #endregion
 
     #region 复制 / 剪切
@@ -107,18 +120,25 @@ public static class FileUtils {
     /// <summary>
     /// 复制文件。
     /// 会创建对应文件夹、覆盖已有的文件。
-    /// 若原文件不存在,则不执行。
+    /// 若复制自身到自身,则不执行操作;若仅大小写不同,则重命名此文件。
     /// </summary>
     public static void Copy(string sourceFilePath, string destFilePath) {
-        if (sourceFilePath == destFilePath) return; // 如果复制同一个文件则跳过
-        if (!FileUtils.Exists(sourceFilePath)) {
-            Logger.Info($"尝试复制文件,但原文件不存在,已跳过复制:{sourceFilePath} → {destFilePath}");
-            return;
+        sourceFilePath = PathUtils.ForCompare(sourceFilePath);
+        destFilePath = PathUtils.ForCompare(destFilePath);
+        if (string.Compare(sourceFilePath, destFilePath, ignoreCase:false) == 0) {
+            // 复制自身到自身,则不执行操作
+            Logger.Trace($"复制文件到自身,不执行操作:{sourceFilePath} → {destFilePath}");
+        } else if (string.Compare(sourceFilePath, destFilePath, ignoreCase:true) == 0) {
+            // 路径仅大小写不同,等效于重命名
+            Logger.Trace($"复制文件到自身,但大小写不同,等效于重命名文件:{sourceFilePath} → {destFilePath}");
+            FileUtils.Move(sourceFilePath, destFilePath);
+        } else {
+            // 实际的复制
+            DirectoryUtils.Create(PathUtils.RemoveLastPart(destFilePath));
+            Logger.Trace($"复制文件:{sourceFilePath} → {destFilePath}");
+            ResilientUtils.RetryOn<IOException>(() 
+                => File.Copy(PathUtils.ForApi(sourceFilePath), PathUtils.ForApi(destFilePath), true));
         }
-        DirectoryUtils.Create(PathUtils.RemoveLastPart(destFilePath));
-        Logger.Trace($"复制文件:{sourceFilePath} → {destFilePath}");
-        ResilientUtils.RetryOn<IOException>(()
-            => File.Copy(PathUtils.ForApi(sourceFilePath), PathUtils.ForApi(destFilePath), true));
     }
 
     /// <summary>
@@ -126,12 +146,27 @@ public static class FileUtils {
     /// 会创建对应文件夹、覆盖已有的文件。
     /// </summary>
     public static void Move(string sourceFilePath, string destFilePath) {
-        if (sourceFilePath == destFilePath) return; // 如果移动同一个文件则跳过
-        DirectoryUtils.Create(PathUtils.RemoveLastPart(destFilePath));
-        FileUtils.Delete(destFilePath);
-        Logger.Trace($"剪切文件:{sourceFilePath} → {destFilePath}");
-        ResilientUtils.RetryOn<IOException>(()
-            => File.Move(PathUtils.ForApi(sourceFilePath), PathUtils.ForApi(destFilePath)));
+        sourceFilePath = PathUtils.ForCompare(sourceFilePath);
+        destFilePath = PathUtils.ForCompare(destFilePath);
+        if (string.Compare(sourceFilePath, destFilePath, ignoreCase: false) == 0) {
+            // 剪切自身到自身,则不执行操作
+            Logger.Trace($"剪切文件到自身,不执行操作:{sourceFilePath} → {destFilePath}");
+        } else if (string.Compare(sourceFilePath, destFilePath, ignoreCase: true) == 0) {
+            // 路径仅大小写不同
+            Logger.Trace($"剪切文件到自身,但大小写不同:{sourceFilePath} → {destFilePath}");
+            ResilientUtils.RetryOn<IOException>(() => {
+                var temp = Path.Combine(PathUtils.RemoveLastPart(sourceFilePath), Path.GetRandomFileName());
+                File.Move(PathUtils.ForApi(sourceFilePath), PathUtils.ForApi(temp));
+                File.Move(PathUtils.ForApi(temp), PathUtils.ForApi(destFilePath));
+            });
+        } else {
+            // 实际的剪切
+            DirectoryUtils.Create(PathUtils.RemoveLastPart(destFilePath));
+            FileUtils.Delete(destFilePath);
+            Logger.Trace($"剪切文件:{sourceFilePath} → {destFilePath}");
+            ResilientUtils.RetryOn<IOException>(()
+                => File.Move(PathUtils.ForApi(sourceFilePath), PathUtils.ForApi(destFilePath)));
+        }
     }
 
     #endregion

+ 11 - 0
Shared/Utils/PathUtils.cs

@@ -146,6 +146,17 @@ public static class PathUtils {
         return name.Contains(".") ? name.AfterLast(".").Lower() : "";
     }
 
+    /// <summary>
+    /// 获取路径的盘符,返回一个大写字母。
+    /// 若路径不包含盘符,则返回 <c>null</c>。
+    /// </summary>
+    public static char? GetDiskName(string? path) {
+        if (string.IsNullOrEmpty(path)) return null;
+        path = PathUtils.WithoutLongPath(path!);
+        if (path.StartsWithF(@"\\") || path.StartsWithF("/")) return null;
+        return char.ToUpper(path[0]);
+    }
+
     /// <summary>
     /// 统一路径格式,以便比较。
     /// <para/>具体而言:将短路径展开,去除前导的 <c>\\?\</c>,将分隔符改为 \,去除末尾的分隔符。