2
0
HerobrineXia 3 долоо хоног өмнө
parent
commit
97d5dc9d17

+ 16 - 0
Shared/Utils/FileUtils.cs

@@ -461,6 +461,22 @@ public static class FileUtils {
     public static bool Exists(string filePath) 
         => File.Exists(PathUtils.ForApi(filePath));
 
+    /// <summary>
+    /// 检查文件是否未被其他进程占用
+    /// <para>被占用或无权读取时返回 <see langword="false"/>。</para>
+    /// </summary>
+    public static bool IsAvailable(string filePath) {
+        if (!Exists(filePath)) return true;
+        try {
+            Retrier.Attempt(maxAttempts: 3, delay: _ => TimeSpan.FromMilliseconds(200), isRetryAllowed: ex => ex is IOException, action: _ => {
+                using var stream = new FileStream(PathUtils.ForApi(filePath), FileMode.Open, FileAccess.Read, FileShare.None);
+            });
+            return true;
+        } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) {
+            return false;
+        }
+    }
+
     /// <summary>
     /// 获取 <see cref="FileInfo"/> 对象。
     /// </summary>

+ 33 - 0
Tests/Utils/FileUtilsTest.cs

@@ -1,6 +1,39 @@
 namespace MeloongCore.Tests;
 public class FileUtilsTest : TestWithFolder {
 
+    [Test]
+    public async Task 文件可用性_只读与占用() {
+        string path = Path.Combine(tempFolder, "available.txt");
+        await Assert.That(FileUtils.IsAvailable(path)).IsTrue();
+        FileUtils.Write(path, "test");
+        string apiPath = PathUtils.ForApi(path);
+        File.SetAttributes(apiPath, FileAttributes.ReadOnly);
+        try {
+            await Assert.That(FileUtils.IsAvailable(path)).IsTrue();
+            await Assert.That((File.GetAttributes(apiPath) & FileAttributes.ReadOnly) != 0).IsTrue();
+        } finally {
+            File.SetAttributes(apiPath, FileAttributes.Normal);
+        }
+        using (var stream = new FileStream(apiPath, FileMode.Open, FileAccess.Read, FileShare.None)) {
+            await Assert.That(FileUtils.IsAvailable(path)).IsFalse();
+        }
+        await Assert.That(FileUtils.IsAvailable(path)).IsTrue();
+    }
+
+    [Test]
+    public async Task 文件可用性_短暂占用后重试() {
+        string path = Path.Combine(tempFolder, "temporary-lock.txt");
+        FileUtils.Write(path, "test");
+        using var stream = new FileStream(PathUtils.ForApi(path), FileMode.Open, FileAccess.Read, FileShare.None);
+        var release = Task.Run(async () => {
+            await Task.Delay(100);
+            stream.Dispose();
+        });
+        bool available = FileUtils.IsAvailable(path);
+        await release;
+        await Assert.That(available).IsTrue();
+    }
+
     #region 解压
 
     [Test]