目录.枚举文件 => 未经授权的访问异常

44

.NET 4.0中有一种很好的新方法,可以通过枚举以流方式获取目录中的文件。

问题在于,如果想要枚举所有文件,则事先可能不知道哪些文件或文件夹受到保护而会引发UnauthorizedAccessException。

要重现此问题,只需运行此片段:

foreach (var file in Directory.EnumerateFiles(@"c:\", "*", SearchOption.AllDirectories))
{
   // whatever
}

在这个.NET方法出现之前,可以通过对字符串数组返回方法实现递归迭代器来实现大致相同的效果。但是它不像新的.NET方法那样懒惰。

那么该怎么做呢?UnauthorizedAccessException可以被抑制吗?还是在使用此方法时必然会发生?

在我看来,这个方法应该有一个重载,接受一个操作来处理任何异常。


是的,你的Dump()方法应该具有抗拒它试图转储的文件出现问题的能力。 给它一个重载。 - Hans Passant
6
那不是我的问题,汉斯。问题在于对文件迭代器(EnumerateFiles)进行 foreach 循环会引发 UnauthorizedAccessException 异常,从而停止进一步枚举,这在想要得到详尽结果集时是不可取的。 - Bent Rasmussen
@Hans - 这里的问题不在于 Dump() 方法,它只是遍历字符串枚举。问题在于 Directory.EnumerateFiles 方法本身。我认为没有办法解决这个问题。你必须使用 SearchOption.TopDirectoryOnly 并自己处理递归。 - Mormegil
1
这(以及其他原因)就是为什么我最终自己编写了一个NtQueryDirectoryFile的包装器。 - user541686
我已经在https://dev59.com/ZWcs5IYBdhLWcg3wGgNc上发布了一个解决方案。我发布的解决方案表现为真正的IEnumerable,因为它只在有工作被请求时才执行工作。 - Matthew Brubaker
2
对于新读者:与此同时,.NET(Core)现在具有EnumerationOptions来处理这个问题:https://learn.microsoft.com/en-us/dotnet/api/system.io.enumerationoptions?view=net-5.0 - Bent Rasmussen
6个回答

34

我无法让以上内容起作用,但这是我的实现方法,我已在“Win7”框中的 c:\users 上进行了测试,因为它拥有所有这些“恶意”目录:

SafeWalk.EnumerateFiles(@"C:\users", "*.jpg", SearchOption.AllDirectories).Take(10)

类:

public static class SafeWalk
{
    public static IEnumerable<string> EnumerateFiles(string path, string searchPattern, SearchOption searchOpt)
    {   
        try
        {
            var dirFiles = Enumerable.Empty<string>();
            if(searchOpt == SearchOption.AllDirectories)
            {
                dirFiles = Directory.EnumerateDirectories(path)
                                    .SelectMany(x => EnumerateFiles(x, searchPattern, searchOpt));
            }
            return dirFiles.Concat(Directory.EnumerateFiles(path, searchPattern));
        }
        catch(UnauthorizedAccessException ex)
        {
            return Enumerable.Empty<string>();
        }
    }
}

1
我也遇到了这个问题。我想出的解决方案可以在https://dev59.com/ZWcs5IYBdhLWcg3wGgNc找到。它表现得像一个真正的可枚举对象,只有在你要求下一个项目时才会执行工作。 - Matthew Brubaker
12
如果这个操作撞到了一个受限制的文件并抛出/忽略了异常,会发生什么?它会停在那里并忽略这个点后面的所有文件吗? - Micha Wiedenmann

9
上面答案的问题在于没有处理子目录中的异常。以下是更好的方式来处理这些异常,以便获取所有子目录中的所有文件,除了那些抛出访问异常的文件:
    /// <summary>
    /// A safe way to get all the files in a directory and sub directory without crashing on UnauthorizedException or PathTooLongException
    /// </summary>
    /// <param name="rootPath">Starting directory</param>
    /// <param name="patternMatch">Filename pattern match</param>
    /// <param name="searchOption">Search subdirectories or only top level directory for files</param>
    /// <returns>List of files</returns>
    public static IEnumerable<string> GetDirectoryFiles(string rootPath, string patternMatch, SearchOption searchOption)
    {
        var foundFiles = Enumerable.Empty<string>();

        if (searchOption == SearchOption.AllDirectories)
        {
            try
            {
                IEnumerable<string> subDirs = Directory.EnumerateDirectories(rootPath);
                foreach (string dir in subDirs)
                {
                    foundFiles = foundFiles.Concat(GetDirectoryFiles(dir, patternMatch, searchOption)); // Add files in subdirectories recursively to the list
                }
            }
            catch (UnauthorizedAccessException) { }
            catch (PathTooLongException) {}
        }

        try
        {
            foundFiles = foundFiles.Concat(Directory.EnumerateFiles(rootPath, patternMatch)); // Add files from the current directory
        }
        catch (UnauthorizedAccessException) { }

        return foundFiles;
    }

1
部分和不正确的解决方案。您可以访问一个目录并在特定文件上获得“访问被拒绝”的错误提示。 - Massimo
2
请不要通过“以上回答”的方式引用其他回答。 - leumasme
使用这个代码,您也可能会遇到“找不到路径的一部分”的错误。 - J. Scott Elblein

5

我理解是MoveNext抛出了异常。

我尝试编写一个方法,安全地遍历序列并尝试忽略MoveNext异常。但是我不确定MoveNext在抛出异常时是否会推进位置,所以这也可能是无限循环。这也是个坏主意,因为我们会依赖于实现细节。

但这只是非常有趣!

public static IEnumerable<T> SafeWalk<T> (this IEnumerable<T> source)
{
    var enumerator = source.GetEnumerator();
    bool? hasCurrent = null;

    do {
        try {
            hasCurrent = enumerator.MoveNext();
        } catch {
            hasCurrent = null; // we're not sure
        }

        if (hasCurrent ?? false) // if not sure, do not return value
            yield return enumerator.Current;

    } while (hasCurrent ?? true); // if not sure, continue walking
}

foreach (var file in Directory.EnumerateFiles("c:\\", "*", SearchOption.AllDirectories)
                              .SafeWalk())
{
    // ...
}

只有在以下关于框架实现此迭代器的条件为真时,此方法才能正常工作(参见 Reflector 中的 FileSystemEnumerableIterator<TSource>):

  • MoveNext 在失败时会移动其位置;
  • MoveNext 在最后一个元素上失败时,后续调用将返回 false 而不是抛出异常;
  • 此行为对于不同版本的 .NET Framework 是一致的;
  • 我没有犯任何逻辑或语法错误。

即使它可以工作,请永远不要在生产环境中使用!
但我真的很想知道它是否可以。


1
还不错,但对于我的情况行不通:当遇到异常时,我需要它继续执行并向前移动:安全遍历和普通遍历的唯一区别在于安全遍历只是停止枚举,而普通方法则会因为异常而停止。我需要它继续执行并忽略任何异常,也就是说它应该枚举所有可以访问的目录,并跳过无法访问的目录。不幸的是,这似乎需要一个新的BCL实现。 - Bent Rasmussen
如果它能正常工作,我在生产中使用它不会有任何问题;-)但即使如此,它仍需要进行一些修改:例如,您不希望捕获所有异常,它应该只是UnauthorizedAccessException,或者至少可以通过lambda进行过滤。 - Bent Rasmussen
5
很遗憾,当MoveNext()抛出异常时,它不会推进其位置。 - Dan Bechard
4
像DirectoryEnumerationPolicy.SkipUnauthorizedPaths这样的东西作为Directory.Enumerate {Directories,Files,FileSystemEntries}的附加参数会很不错。 - Bent Rasmussen
1
迄今为止最快的选项。大多数解决方案在返回它们之前将一个级别的所有条目加载到内存中 - 这在递归上很快建立起来。这个解决方案神奇地希望 MoveNext 能够工作,即使它抛出异常。有风险 - 在 netstandard 2.0 上进行了测试,并成功地在跨 C:\ 搜索时跳过了 $Recyclebin 和系统目录。 - Patrick Stalph

3
由于我没有足够的声望甚至无法编辑现有答案,所以我作为回答者进行回答。我的要求是尽可能减少内存分配、冗余变量,并使系统对目录进行单个枚举。
static IEnumerable<string> FindFiles(string path, string filter = "*", bool recursive = false)
{
    IEnumerator<string> fEnum;
    try
    {
        fEnum = Directory.EnumerateFiles(path, filter, recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly).GetEnumerator();
    }
    catch (UnauthorizedAccessException) { yield break; }
    while (true)
    {
        try { if (!fEnum.MoveNext()) break; }
        catch (UnauthorizedAccessException) { continue; }
        yield return fEnum.Current;
    }
}

Dan Bechard在评论中提到:

不幸的是,当它抛出异常时,MoveNext()不会推进其位置。

这个问题可能已经在较新版本的.NET或Windows 10构建中修复了?我在Windows 10上的.NET 5.0中没有遇到这个问题。通过搜索我的整个系统驱动器进行测试。


在VB.NET中:

Public Iterator Function FindFiles(path As String, Optional filter As String = "*", Optional recursive As Boolean = False) As IEnumerable(Of String)

    Dim fEnum As IEnumerator(Of String)
    Dim searchDepth = If(recursive, SearchOption.AllDirectories, SearchOption.TopDirectoryOnly)

    Try
        fEnum = Directory.EnumerateFiles(path, filter, searchDepth).GetEnumerator()
    Catch uae As UnauthorizedAccessException
        Return
    End Try

    Do While True
        Try
            If Not fEnum.MoveNext() Then Exit Do
            Yield fEnum.Current
        Catch uae As UnauthorizedAccessException

        End Try

    Loop

End Function

此解决方案不完整,而且也没有意义,因为如果您的目标是 .NET 5,您可以使用接受 EnumerationOptions 参数的 EnumerateDirectories过载。相反,我们需要的是在 .NET Framework 环境下工作的解决方案。 - user3700562

1

我迟到了,但我建议使用观察者模式:

public class FileUtil
{
  private static void FindFiles_(string path, string pattern,
    SearchOption option, IObserver<string> obs, CancellationToken token)
  {
    try
    {
      foreach (var file in Directory.EnumerateFiles(path, pattern,
        SearchOption.TopDirectoryOnly))
      {
        if (token.IsCancellationRequested) break;
        obs.OnNext(file);
      }

      if (option != SearchOption.AllDirectories) return;

      foreach (var dir in Directory.EnumerateDirectories(path, "*", 
        SearchOption.TopDirectoryOnly))
      {
        FindFiles_(dir, pattern, option, obs, token);
      }
    }
    catch (UnauthorizedAccessException) { }
    catch (PathTooLongException) { }
    catch (IOException) { }
    catch (Exception err) { obs.OnError(err); }
  }

  public static IObservable<string> GetFiles(string root, string pattern,
    SearchOption option)
  {
    return Observable.Create<string>(
      (obs, token) =>
        Task.Factory.StartNew(
          () =>
          {
            FindFiles_(root, pattern, option, obs, token);
            obs.OnCompleted();
          },
          token));
  }
}

0

我自己实现了一个类来解决这个问题,因为之前的答案似乎不能满足我的需求。它会跳过所有无法访问的文件和文件夹,并返回所有可以访问的文件。

public static class SafeWalk
{
    public static IEnumerable<string> EnumerateFiles(string path, string searchPattern, SearchOption searchOpt)
    {
        if (searchOpt == SearchOption.TopDirectoryOnly)
        {
            return Directory.EnumerateFiles(path, searchPattern, SearchOption.TopDirectoryOnly);
        }

        List<string> folders = new List<string>() { path };
        int folCount = 1;
        List<string> files = new List<string>() { };

        for (int i = 0; i < folCount; i++)
        {
            try
            {
                foreach (var newDir in Directory.EnumerateDirectories(folders[i], "*", SearchOption.TopDirectoryOnly))
                {
                    folders.Add(newDir);
                    folCount++;
                    try
                    {

                        foreach (var file in Directory.EnumerateFiles(newDir, searchPattern))
                        {
                            files.Add(file);
                        }
                    } catch (UnauthorizedAccessException)
                    {
                        // Failed to read a File, skipping it.
                    }
                }
            }
            catch (UnauthorizedAccessException)
            {
                // Failed to read a Folder, skipping it.
                continue;
            }
        }
        return files;
    }
}

可以像常规的EnumerateFiles函数一样使用,只需使用SafeWalk.EnumerateFiles(...)而不是Dictionary.EnumerateFiles(...)即可。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接