What's the best way to calculate the size of a directory in .NET?

Viewed 121547

I've written the following routine to manually traverse through a directory and calculate its size in C#/.NET:


protected static float CalculateFolderSize(string folder)
{
    float folderSize = 0.0f;
    try
    {
        //Checks if the path is valid or not
        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("Unable to calculate folder size: {0}", e.Message);
            }
        }
    }
    catch (UnauthorizedAccessException e)
    {
        Console.WriteLine("Unable to calculate folder size: {0}", e.Message);
    }
    return folderSize;
}

I have an application which is running this routine repeatedly for a large number of folders. I'm wondering if there's a more efficient way to calculate the size of a folder with .NET? I didn't see anything specific in the framework. Should I be using P/Invoke and a Win32 API? What's the most efficient way of calculating the size of a folder in .NET?

23 Answers

No, this looks like the recommended way to calculate directory size, the relevent method included below:

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;  
}

You would call with the root as:

Console.WriteLine("The size is {0} bytes.", DirSize(new DirectoryInfo(targetFolder));

...where targetFolder is the folder-size to calculate.

I do not believe there is a Win32 API to calculate the space consumed by a directory, although I stand to be corrected on this. If there were then I would assume Explorer would use it. If you get the Properties of a large directory in Explorer, the time it takes to give you the folder size is proportional to the number of files/sub-directories it contains.

Your routine seems fairly neat & simple. Bear in mind that you are calculating the sum of the file lengths, not the actual space consumed on the disk. Space consumed by wasted space at the end of clusters, file streams etc, are being ignored.

An alternative to Trikaldarshi's one line solution. (It avoids having to construct FileInfo objects)

long sizeInBytes = Directory.EnumerateFiles("{path}","*", SearchOption.AllDirectories).Sum(fileInfo => new FileInfo(fileInfo).Length);

This it the best way to calculate the size of a directory. Only other way would still use recursion but be a bit easier to use and isn't as flexible.

float folderSize = 0.0f;
FileInfo[] files = Directory.GetFiles(folder, "*", SearchOption.AllDirectories);
foreach(FileInfo file in files) folderSize += file.Length;

I've been fiddling with VS2008 and LINQ up until recently and this compact and short method works great for me (example is in VB.NET; requires LINQ / .NET FW 3.5+ of course):

Dim size As Int64 = (From strFile In My.Computer.FileSystem.GetFiles(strFolder, _
              FileIO.SearchOption.SearchAllSubDirectories) _
              Select New System.IO.FileInfo(strFile).Length).Sum()

Its short, it searches sub-directories and is simple to understand if you know LINQ syntax. You could even specify wildcards to search for specific files using the third parameter of the .GetFiles function.

I'm not a C# expert but you can add the My namespace on C# this way.

I think this way of obtaining a folder size is not only shorter and more modern than the way described on Hao's link, it basically uses the same loop-of-FileInfo method described there in the end.

As far as the best algorithm goes you probably have it right. I would recommend that you unravel the recursive function and use a stack of your own (remember a stack overflow is the end of the world in a .Net 2.0+ app, the exception can not be caught IIRC).

The most important thing is that if you are using it in any form of a UI put it on a worker thread that signals the UI thread with updates.

Directory.GetFiles(@"C:\Users\AliBayat","*",SearchOption.AllDirectories)
.Select (d => new FileInfo(d))
.Select (d => new { Directory = d.DirectoryName,FileSize = d.Length} )
.ToLookup (d => d.Directory )
.Select (d => new { Directory = d.Key,TotalSizeInMB =Math.Round(d.Select (x =>x.FileSize)
.Sum () /Math.Pow(1024.0,2),2)})
.OrderByDescending (d => d.TotalSizeInMB).ToList();

Calling GetFiles with SearchOption.AllDirectories returns the full name of all the files in all the subdirectories of the specified directory. The OS represents the size of files in bytes. You can retrieve the file’s size from its Length property. Dividing it by 1024 raised to the power of 2 gives you the size of the file in megabytes. Because a directory/folder can contain many files, d.Select(x => x.FileSize) returns a collection of file sizes measured in megabytes. The final call to Sum() finds the total size of the files in the specified directory.

Update: the filterMask="." does not work with files without extension

Multi thread example to calculate directory size from Microsoft Docs, which would be faster

using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

public class Example
{
   public static void Main()
   {
      long totalSize = 0;

      String[] args = Environment.GetCommandLineArgs();
      if (args.Length == 1) {
         Console.WriteLine("There are no command line arguments.");
         return;
      }
      if (! Directory.Exists(args[1])) {
         Console.WriteLine("The directory does not exist.");
         return;
      }

      String[] files = Directory.GetFiles(args[1]);
      Parallel.For(0, files.Length,
                   index => { FileInfo fi = new FileInfo(files[index]);
                              long size = fi.Length;
                              Interlocked.Add(ref totalSize, size);
                   } );
      Console.WriteLine("Directory '{0}':", args[1]);
      Console.WriteLine("{0:N0} files, {1:N0} bytes", files.Length, totalSize);
   }
}
// The example displaysoutput like the following:
//       Directory 'c:\windows\':
//       32 files, 6,587,222 bytes

This example only calculate the files in current folder, so if you want to calculate all the files recursively, you can change the

String[] files = Directory.GetFiles(args[1]);

to

String[] files = Directory.GetFiles(args[1], "*", SearchOption.AllDirectories);

This .NET core command line app here calculates directory sizes for a given path:

https://github.com/garethrbrown/folder-size

The key method is this one which recursively inspects sub-directories to come up with a total size.

private static long DirectorySize(SortDirection sortDirection, DirectoryInfo directoryInfo, DirectoryData directoryData)
{
        long directorySizeBytes = 0;

        // Add file sizes for current directory

        FileInfo[] fileInfos = directoryInfo.GetFiles();

        foreach (FileInfo fileInfo in fileInfos)
        {
            directorySizeBytes += fileInfo.Length;
        }

        directoryData.Name = directoryInfo.Name;

        directoryData.SizeBytes += directorySizeBytes;

        // Recursively add subdirectory sizes

        DirectoryInfo[] subDirectories = directoryInfo.GetDirectories();

        foreach (DirectoryInfo di in subDirectories)
        {
            var subDirectoryData = new DirectoryData(sortDirection);

            directoryData.DirectoryDatas.Add(subDirectoryData);

            directorySizeBytes += DirectorySize(sortDirection, di, subDirectoryData);
        }

        directoryData.SizeBytes = directorySizeBytes;

        return directorySizeBytes;
    }
}

In this link https://docs.microsoft.com/en-us/office/vba/language/reference/user-interface-help/size-property-filesystemobject-object there is a description of how to get the folder size directly using Visual Basic, without having to get a list of files and loop over them to add up their lengths.

Sub ShowFolderSize(filespec)
    Dim fs, f, s
    Set fs = CreateObject("Scripting.FileSystemObject")
    Set f = fs.GetFolder(filespec)
    s = UCase(f.Name) & " uses " & f.size & " bytes."
    MsgBox s, 0, "Folder Size Info"
End Sub

In a c# project, you can also add a reference to Microsoft Scripting and use the FileSystemObject.

Here is a routine for c# to use this method to output the sizes of all the folders in the given path. It recurses up to a specified level, examining subfolders whose size is larger than average in an attempt to find where storage use problems are caused.

using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;

namespace ShowFolderSizes
{
    public class ShowFolderSizesMain
    {
        double GBFactor = 1024.0 * 1024.0 * 1024.0;
        Scripting.FileSystemObject fileSystemObject = new Scripting.FileSystemObject();

        public static void Main(string[] args)
        {
            ShowFolderSizesMain instance = new ShowFolderSizesMain();
            instance.Run(args);
        }

        void Run(string[] args)
        {
            if (args.Length != 2)
            {
                Console.WriteLine("Usage: ShowFolderSizes path levels");
                return;
            }
            string path = args[0];
            if (!Int32.TryParse(args[1], out int levels))
            {
                Console.WriteLine($"Can't interpret {args[1]} as an integer.");
                return;
            }
            writeFolderSizes(path, levels);
            //Console.WriteLine("Press any key to continue...");
            //Console.ReadKey();
        }

        public void writeFolderSizes(string topPath, int levels)
        {
            List<string> folderNames;
            try
            {
                folderNames = new List<string>(Directory.GetDirectories(topPath));
            }
            catch (System.UnauthorizedAccessException e)
            {
                Console.WriteLine($"Can't access {topPath}");
                return;
            }
            if (folderNames.Count == 0)
            {
                return;
            }

            var dic = new Dictionary<string, long>();
            double sum = 0.0;
            foreach (string folderPath in folderNames)
            {
                Scripting.Folder folder = fileSystemObject.GetFolder(folderPath);
                try
                {
                    dynamic dsize = folder.Size;
                    long size = Convert.ToInt64(dsize);
                    dic.Add(folderPath, size);
                    sum += Convert.ToDouble(size);
                }
                catch (System.Security.SecurityException e)
                {
                    Console.WriteLine($"Can't access {folderPath}");
                    dic.Remove(folderPath);
                }
            }
            sum = sum / GBFactor;
            double avg = (sum / folderNames.Count);

            Console.WriteLine($"{topPath} {sum.ToString("0.000")} GB:");
            var sortedResults = (
                    from KeyValuePair<string, long> kvp in dic
                    orderby kvp.Value descending
                    select kvp);
            foreach (KeyValuePair<string, long> kvp in sortedResults)
            {
                double gb = Convert.ToDouble(kvp.Value) / GBFactor;
                Console.WriteLine($"{gb.ToString("000.000")} GB {kvp.Key}");
            }
            Console.WriteLine();

            if (levels > 0)
            {
                long cutoff = Convert.ToInt64(avg * GBFactor);
                var foldersToRecurse = (
                    from KeyValuePair<string, long> kvp in dic
                    where kvp.Value >= cutoff
                    orderby kvp.Value descending
                    select kvp.Key);
                int nextLevel = levels - 1;
                foreach (string folderPath in foldersToRecurse)
                {
                    writeFolderSizes(folderPath, nextLevel);
                }
            }
        }
    }
}

For it to be really useful, it often needs to be run as administrator, since trying to access folders like C:\Program files or C:\Users causes it to go into the "catch" parts with my normal user.

I try to change the sample (Alexandre Pepin and hao's Answer)

As is

    private long GetDirectorySize(string dirPath)
    {
        if (Directory.Exists(dirPath) == false)
        {
            return 0;
        }

        DirectoryInfo dirInfo = new DirectoryInfo(dirPath);

        long size = 0;

        // Add file sizes.
        FileInfo[] fis = dirInfo.GetFiles();
        foreach (FileInfo fi in fis)
        {
            size += fi.Length;
        }

        // Add subdirectory sizes.
        DirectoryInfo[] dis = dirInfo.GetDirectories();
        foreach (DirectoryInfo di in dis)
        {
            size += GetDirectorySize(di.FullName);
        }

        return size;
    }

To be

    private long GetDirectorySize2(string dirPath)
    {
        if (Directory.Exists(dirPath) == false)
        {
            return 0;
        }

        DirectoryInfo dirInfo = new DirectoryInfo(dirPath);

        long size = 0;

        // Add file sizes.
        IEnumerable<FileInfo> fis = dirInfo.EnumerateFiles("*.*", SearchOption.AllDirectories);
        foreach (FileInfo fi in fis)
        {
            size += fi.Length;
        }

        return size;
    }

finally you can check the result

        // ---------------------------------------------
        // size of directory
        using System.IO;

        string log1Path = @"D:\SampleDirPath1";
        string log2Path = @"D:\SampleDirPath2";
        string log1DirName = Path.GetDirectoryName(log1Path);
        string log2DirName = Path.GetDirectoryName(log2Path);
        long log1Size = GetDirectorySize(log1Path);
        long log2Size = GetDirectorySize(log2Path);
        long log1Size2 = GetDirectorySize2(log1Path);
        long log2Size2 = GetDirectorySize2(log2Path);

        Console.WriteLine($@"{log1DirName} Size: {SizeSuffix(log1Size)}, {SizeSuffix(log1Size2)}
        {log2DirName} Size: {SizeSuffix(log2Size)}, {SizeSuffix(log2Size2)}");

and this is the SizeSuffix function

    private static readonly string[] SizeSuffixes =
               { "bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" };

    /// <summary>
    /// Size Display
    /// </summary>
    /// <param name="value">bytes 數值</param>
    /// <param name="decimalPlaces">小數位數</param>
    /// <returns></returns>
    public static string SizeSuffix(Int64 value, int decimalPlaces = 2)
    {
        if (decimalPlaces < 0) { throw new ArgumentOutOfRangeException("decimalPlaces"); }
        if (value < 0) { return "-" + SizeSuffix(-value); }
        if (value == 0) { return string.Format("{0:n" + decimalPlaces + "} bytes", 0); }

        // mag is 0 for bytes, 1 for KB, 2, for MB, etc.
        int mag = (int)Math.Log(value, 1024);

        // 1L << (mag * 10) == 2 ^ (10 * mag) 
        // [i.e. the number of bytes in the unit corresponding to mag]
        decimal adjustedSize = (decimal)value / (1L << (mag * 10));

        // make adjustment when the value is large enough that
        // it would round up to 1000 or more
        if (Math.Round(adjustedSize, decimalPlaces) >= 1000)
        {
            mag += 1;
            adjustedSize /= 1024;
        }

        return string.Format("{0:n" + decimalPlaces + "} {1}",
            adjustedSize,
            SizeSuffixes[mag]);
    }

I know this not a .net solution but here it comes anyways. Maybe it comes handy for people that have windows 10 and want a faster solution. For example if you run this command con your command prompt or by pressing winKey + R:

bash -c "du -sh /mnt/c/Users/; sleep 5"    

The sleep 5 is so you have time to see the results and the windows does not closes

In my computer that displays:

enter image description here

Note at the end how it shows 85G (85 Gigabytes). It is supper fast compared to doing it with .Net. If you want to see the size more accurately remove the h which stands for human readable.

So just do something like Processes.Start("bash",... arguments) That is not the exact code but you get the idea.

Related