在.NET中计算目录大小的最佳方法是什么?

89

我编写了以下程序来手动遍历目录并在C#/.NET中计算其大小:


protected static float CalculateFolderSize(string folder)
{
    float folderSize = 0.0f;
    try
    {
        // 检查路径是否有效
        if (!Directory.Exists(folder))
            return folderSize;
        else
        {
            try
            {
                // 获取文件夹中的所有文件并计算它们的大小
                foreach (string file in Directory.GetFiles(folder))
                {
                    if (File.Exists(file))
                    {
                        FileInfo finfo = new FileInfo(file);
                        folderSize += finfo.Length; // 累加每个文件的大小
                    }
                }
// 获取文件夹中的所有子文件夹并递归计算它们的大小 foreach (string dir in Directory.GetDirectories(folder)) folderSize += CalculateFolderSize(dir); } catch (NotSupportedException e) { Console.WriteLine("无法计算文件夹大小:{0}", e.Message); } } } catch (UnauthorizedAccessException e) { Console.WriteLine("无法计算文件夹大小:{0}", e.Message); } return folderSize; }

我有一个应用程序,需要对大量文件夹重复运行此例程。 我想知道是否有更有效的方法来使用.NET计算文件夹的大小? 在框架中没有看到任何特定的内容。 是否应该使用P/Invoke和Win32 API? 在.NET中计算文件夹大小的最有效方法是什么?

24个回答

86

不,这看起来像是计算目录大小的推荐方法,相关的方法如下:

public static long DirSize(DirectoryInfo d) 
{    
    long size = 0;    
    // Add file sizes.
    FileInfo[] fis = d.GetFiles();
    foreach (FileInfo fi in fis) 
    {      
        size += fi.Length;    
    }
    // Add subdirectory sizes.
    DirectoryInfo[] dis = d.GetDirectories();
    foreach (DirectoryInfo di in dis) 
    {
        size += DirSize(di);   
    }
    return size;  
}
您需要使用根节点进行调用:
Console.WriteLine("The size is {0} bytes.", DirSize(new DirectoryInfo(targetFolder));

...其中targetFolder是要计算大小的文件夹。


1
更新的链接:http://msdn.microsoft.com/zh-cn/library/system.io.directory(v=vs.80).aspx,供参考。 - ladenedge
1
@ladenedge 更新链接了吗?那个链接会带你到v2.0... 答案链接是正确的。 - kzfabi
顺便说一句:推荐的链接不再包含示例。看起来 MSDN 已经更新了,那个示例也丢失了。 - kzfabi
6
使用 EnumerateFiles 和 EnumerateDirectories。 - Mauro Sampietro
你可以在 DirectoryInfo.GetFiles(...) 的第二个参数中使用 SearchOption.AllDirectories 选项来获取所有子目录。为了获得更好的性能,我会更倾向于使用 DirectoryInfo.EnumerateFiles(...) 方法。 - shashwat
显示剩余4条评论

52
DirectoryInfo dirInfo = new DirectoryInfo(@strDirPath);
long dirSize = await Task.Run(() => dirInfo.EnumerateFiles( "*", SearchOption.AllDirectories).Sum(file => file.Length));

1
这会得到“UnauthorizedAccess异常”,请查看:https://stackoverflow.com/questions/8877516/how-to-ignore-access-to-the-path-is-denied-unauthorizedaccess-exception-in-c - Ahmed Sabry
@AhmedSabry:你从这个bug中理解了什么? - Trikaldarshiii
它需要在此路径上的权限。 - Ahmed Sabry
@AhmedSabry 没错,这就是为什么这个一行代码很好的原因。如果所有权限都正常,那太好了。如果不是,它会失败,你不需要特定的自定义 try..catch,你会知道你有什么问题。 - Ofer Zelig
如果子目录的完整路径过长,这将会抛出异常。 - user2261015

27

我认为没有Win32 API可以计算目录占用的空间,虽然我在这方面可能会有所纠正。如果有的话,我认为资源管理器应该会使用它。当您在资源管理器中获取大型目录的属性时,给您提供文件夹大小的时间与它包含的文件/子目录数量成比例。

您的程序似乎相当简洁和简单。请记住,您正在计算文件长度之和,而不是实际占用磁盘空间。浪费在簇末尾、文件流等处的空间将被忽略。


6
这种方法还忽略了连接点、硬链接、压缩和离线存储。 - Anton Tykhyy
EnumerateFiles可能更可取,因为其中可能有包含10万个以上文件的文件夹。如上所述,联接可能会导致无限递归。 - Just another metaprogrammer
1
有一个API。FileSystemObject(COM)。使用GetFolder()方法http://msdn.microsoft.com/en-us/library/f1xtf7ta(v=vs.84).aspx和Size属性http://msdn.microsoft.com/en-us/library/2d66skaf(v=vs.84).aspx。 - Daniel Fisher lennybacon
@DanielFisherlennybacon,你知道它是如何实现的吗?fso.GetFolder().Size是否像DirSize()一样递归循环,或者Windows跟踪文件夹大小?它返回什么类型的“size” - jfs
为什么不使用这个“GetDiskFreeSpaceEx”函数?链接:https://msdn.microsoft.com/zh-cn/library/windows/desktop/aa364937(v=vs.85).aspx?f=255&MSPPError=-2147217396 - Ori Nachum

19

真正的问题是,您打算使用这个大小来做什么

你的第一个问题是,“文件大小”至少有四种定义:

  • “文件结束”偏移量,它是从文件开头跳到文件末尾需要跳过的字节数。
    换句话说,它是文件中从使用角度看逻辑上的字节数。

  • “有效数据长度”,等于第一个未实际存储的字节的偏移量。
    这始终小于或等于“文件结束”,并且是群集大小的倍数。
    例如,1 GB 文件的有效数据长度可以为 1 MB。如果您要求 Windows 读取前 8 MB,则它将读取前 1 MB 并假装其他数据在那里,将其返回为零。

  • 文件的“分配大小”。这始终大于或等于“文件结束”。
    这是操作系统为该文件分配的群集数乘以群集大小。
    与“文件结束”大于“有效数据长度”的情况不同,多余的字节被认为是文件的一部分,因此如果您尝试在文件结束后读取已分配的区域中的信息,操作系统将不会用零填充缓冲区。

  • 文件的“压缩大小”,仅适用于压缩(和稀疏?)文件。
    它等于群集大小乘以分配给此文件的卷上的群集数。
    如果文件是压缩的,则该大小为实际分配给文件的群集数乘以压缩系数。

对于非压缩和非稀疏文件,没有"压缩大小"的概念;您应该使用"分配大小"。

你的第二个问题是,像C:\Foo这样的"文件"实际上可以有多个数据流
这个名称只是指默认流。一个文件可能有备用流,比如C:\Foo:Bar,它的大小甚至在资源管理器中都不会显示!

你的第三个问题是,一个"文件"可以有多个名称("硬链接")。
例如,C:\Windows\notepad.exeC:\Windows\System32\notepad.exe 是同一个文件的两个名称。可以使用任何名称来打开任何流。

你的第四个问题是,一个"文件"(或目录)实际上可能并不是一个文件(或目录):
它可能是一个符号链接(一个"符号链接"或"重解释点")到另一个文件(或目录)。那个文件甚至可能不在同一驱动器上。它甚至可能指向网络上的某些东西,或者可能是递归的!如果它是递归的,大小应该是无穷大吗?

你的第五个问题是有一些“筛选器”驱动程序会使某些文件或目录看起来像实际的文件或目录,即使它们实际上不是。例如,微软的WIM图像文件(压缩文件)可以使用名为ImageX的工具“挂载”到一个文件夹上,这些文件夹看起来并不像重解析点或链接。它们看起来就像目录——只是它们实际上并不是目录,并且对于它们来说,“大小”的概念并没有真正意义。

你的第六个问题是每个文件都需要元数据。
例如,给同一个文件取10个名字需要更多的元数据,这需要空间。如果文件名很短,那么取10个名字可能和1个名字一样便宜——如果它们很长,那么使用多个名字可能会使用更多磁盘空间用于元数据。(多个流也是同样的情况。)
你也计算这些吗?


9
我困惑了。这不是一个答案,而是一个(相当长的)问题或多个问题。 - Ofer Zelig
2
虽然这还不是一个答案,但它让我们向找到答案迈出了一大步。 - TimTIM Wong

19
public static long DirSize(DirectoryInfo dir)
{
    return dir.GetFiles().Sum(fi => fi.Length) +
           dir.GetDirectories().Sum(di => DirSize(di));
}

1
这个解决方案存在几个问题,其中之一是在NTFS(也称为Junction Points)和Unix-SMB共享上递归符号链接目录缺乏终止。 - mbx
1
我同意。其他的是什么? - Grozz
2
PathTooLongException(请参阅此博客文章)和缺少读取某些子目录的凭据(UnauthorizedAccessException)是需要考虑的问题。一个不太重要的问题是在操作时拔掉可移动驱动器(USB 等)。在这里,异常处理是必须的 - 如果总结果有任何价值,只需在本地返回 0 并记录错误即可。顺便说一句:如果应用于远程共享,它可能看起来像是DOS攻击。我相信我至少漏掉了一个其他情况 :-) - mbx
在处理这些已知的异常时,对于更大的驱动器仍会出现StackOverflowException - mbx
2
我认为在所有这些情况下都抛出异常是可以接受的,因此默认行为是可以的。StackOverflowException 是唯一需要处理的异常,尽管我不相信它会在没有递归符号链接的情况下被触发。 - Grozz
显示剩余2条评论

16
var size = new DirectoryInfo("E:\\").GetDirectorySize();

这是这个扩展方法背后的代码:

public static long GetDirectorySize(this System.IO.DirectoryInfo directoryInfo, bool recursive = true)
{
    var startDirectorySize = default(long);
    if (directoryInfo == null || !directoryInfo.Exists)
        return startDirectorySize; //Return 0 while Directory does not exist.

    //Add size of files in the Current Directory to main size.
    foreach (var fileInfo in directoryInfo.GetFiles())
        System.Threading.Interlocked.Add(ref startDirectorySize, fileInfo.Length);

    if (recursive) //Loop on Sub Direcotries in the Current Directory and Calculate it's files size.
        System.Threading.Tasks.Parallel.ForEach(directoryInfo.GetDirectories(), (subDirectory) =>
    System.Threading.Interlocked.Add(ref startDirectorySize, GetDirectorySize(subDirectory, recursive)));

    return startDirectorySize;  //Return full Size of this Directory.
}

2
这段代码在我的情况下比使用EnumerateFiles()的任何答案都要快。 - Rm558

7

似乎以下方法比递归函数更快地执行任务:

long size = 0;
DirectoryInfo dir = new DirectoryInfo(folder);
foreach (FileInfo fi in dir.GetFiles("*.*", SearchOption.AllDirectories))
{
   size += fi.Length;
}

一个简单的控制台应用程序测试表明,这个循环比递归函数更快地对文件求和,并提供相同的结果。您可能想要使用LINQ方法(如Sum())来缩短此代码。


在我的测试中,包含源代码和许多隐藏文件(svn目录)的目录中,结果与文件系统浏览器报告的不同。 - David Doumèche
5
请始终使用""而不是".*"来表示没有扩展名的文件。 - Bernhard

7
更快!添加COM引用"Windows脚本宿主对象..."
public double GetWSHFolderSize(string Fldr)
    {
        //Reference "Windows Script Host Object Model" on the COM tab.
        IWshRuntimeLibrary.FileSystemObject FSO = new     IWshRuntimeLibrary.FileSystemObject();
        double FldrSize = (double)FSO.GetFolder(Fldr).Size;
        Marshal.FinalReleaseComObject(FSO);
        return FldrSize;
    }
private void button1_Click(object sender, EventArgs e)
        {
            string folderPath = @"C:\Windows";
        Stopwatch sWatch = new Stopwatch();

        sWatch.Start();
        double sizeOfDir = GetWSHFolderSize(folderPath);
        sWatch.Stop();
        MessageBox.Show("Directory size in Bytes : " + sizeOfDir + ", Time: " + sWatch.ElapsedMilliseconds.ToString());
          }

好的。但是它似乎给出的是文件大小,而不是磁盘上的实际大小(我有一个情况,大小为18154字节,磁盘上的大小为163840字节!) - NGI
救命稻草!COM又一次拯救了我。我真的需要更多地考虑使用它,它完美地工作,谢谢! - WiiLF
不处理错误 - Gray Programmerz

6

这个解决方案非常有效。 它可以收集所有子文件夹:

Directory.GetFiles(@"MainFolderPath", "*", SearchOption.AllDirectories).Sum(t => (new FileInfo(t).Length));

5
一种替代 Trikaldarshi 的一行解决方案的方法。(它避免了构造 FileInfo 对象的步骤)
long sizeInBytes = Directory.EnumerateFiles("{path}","*", SearchOption.AllDirectories).Sum(fileInfo => new FileInfo(fileInfo).Length);

变量fileInfo的类型是字符串,而不是FileInfo。 - user425678
构造许多 FileInfo 对象,这不是吗? - TimTIM Wong
请注意,这将为您提供“大小”,而不是“磁盘上的大小”,后者始终略高。 - Paul Efford

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