Search code examples
c#wpfdirectorysizefreeze

Calculating folder size / Enumerate filesystem


I am trying to calculate folder size but the problem is; it is working fast for D:\ drive or another folders, but whenever I try to click on C:\ drive, app is freezing for a while approximately 7-8 seconds. (My drive list are on treeview) When I remove folder size, everything is ok. Do you guys have any idea about this?

   public FolderModel(string folderPath)
    {
        try
        {

            //File = new FileInfo(folderPath);
            //FolderInfo = new DirectoryInfo(folderPath);
            //_createdTime = FolderInfo.CreationTime.ToShortDateString();
            //_folderName = FolderInfo.Name;
            //_folderPath = folderPath;
            //Fileextension = File.Extension.ToLower();
            //this.Children = new ObservableCollection<FolderModel>();

            _folderSize = CalculatorSize(GetDirectorySize(folderPath));
           
        }
        catch (Exception e)
        {
            //
        }
    }




    internal string CalculatorSize(long bytes)
    {
        var suffix = new[] { "B", "KB", "MB", "GB", "TB" };
        float byteNumber = bytes;
        for (var i = 0; i < suffix.Length; i++)
        {
            if (byteNumber < 1000)
            {
                if (i == 0)
                    return $"{byteNumber} {suffix[i]}";
                else
                    return $"{byteNumber:0.#0} {suffix[i]}";
            }
            else
            {
                byteNumber /= 1024;
            }
        }
        return $"{byteNumber:N} {suffix[suffix.Length - 1]}";
    }



    internal static long GetDirectorySize(string directoryPath)
    {
        try
        {
            if (Directory.Exists(directoryPath))
            {
                var d = new DirectoryInfo(directoryPath);
                return d.EnumerateFiles("*", SearchOption.AllDirectories).Sum(fi => fi.Length);
            }

            return new FileInfo(directoryPath).Length;
        }
        catch (UnauthorizedAccessException)
        {
            return 0;
        }
        catch (FileNotFoundException)
        {
            return 0;
        }
        catch (DirectoryNotFoundException)
        {
            return 0;
        }
    }

Solution

  • You must enumerate the folder on a background thread.

    Suggestions to improve performance
    When using the DriveInfo API you can further improve the performance for the case that the folder path is a drive. In this case, you can omit the enumeration of the complete drive, which usually takes a while.
    Furthermore, your current implementation aborts the calculation when the enumeration throws the UnauthorizedAccessException exception. You don't want that. You want the algorithm to ignore forbidden filesystem paths.

    The following two examples show a fixed and improved version of your implementation.
    The first solution targets the modern .NET Standard 2.1 compliant .NET versions.
    The second solution targets the old .NET Framework.

    .NET Standard 2.1 (.NET Core 3.0, .NET 5)

    When using a .NET version compatible with .NET Standard 2.1 like .NET Core 3.0 and .NET 5 you can eliminate the exception handling. Using EnumerationOptions as an argument allows the API to ignore inaccessible directories, which significantly improves performance (no more UnauthorizedAccessException exceptions) and readability:

    internal static async Task<bool> TryGetDirectorySizeAsync(string directoryPath, out long spaceUsedInBytes)
    {
      spaceUsedInBytes = -1;
      DriveInfo[] drives = DriveInfo.GetDrives();
      DriveInfo targetDrive = drives.FirstOrDefault(drive => drive.Name.Equals(directoryPath, StringComparison.OrdinalIgnoreCase));
    
      // Directory is a drive: skip the expensive enumeration 
      // of complete drive and use drive information instead
      if (targetDrive != null)
      {
        spaceUsedInBytes = targetDrive.TotalSize - targetDrive.TotalFreeSpace;
        return true;
      }
    
      if (!Directory.Exists(folderPath))
      {
        return false;
      }
    
      // Consider to make this local variable a private property
      var enumerationOptions = new EnumerationOptions { RecurseSubdirectories = true };
    
      var targetFolderInfo = new DirectoryInfo(directoryPath);
      spaceUsedInBytes = await Task.Run(
        () => targetFolderInfo.EnumerateFiles("*", enumerationOptions)
          .Sum(fileInfo => fileInfo.Length));
    
      return true;
    }
    

    .NET Framework

    A .NET Framework compliant version. It fixes the issue with your original code where the enumeration is aborted as soon as an UnauthorizedAccessException exception is thrown. This version continues to enumerate all remaining directories using recursion:

    internal static async Task<long> GetDirectorySizeAsync(string directoryPath)
    {
      long spaceUsedInBytes = -1;
      DriveInfo[] drives = DriveInfo.GetDrives();
      DriveInfo targetDrive =  drives.FirstOrDefault(drive => drive.Name.Equals(directoryPath, StringComparison.OrdinalIgnoreCase));
    
      // Directory is a drive: skip expensive enumeration 
      // of complete drive and use drive information instead
      if (targetDrive != null)
      {
        spaceUsedInBytes = targetDrive.TotalSize - targetDrive.TotalFreeSpace;
        return spaceUsedInBytes;
      }
    
      var targetDirectoryInfo = new DirectoryInfo(directoryPath);
      spaceUsedInBytes = await Task.Run(() => SumDirectorySize(targetDirectoryInfo));
      return spaceUsedInBytes;
    }
    
    private static long SumDirectorySize(DirectoryInfo parentDirectoryInfo)
    {
      long spaceUsedInBytes = 0;
      try
      {
        // Throws an exception if current directory is not accessible
        // ==> catch exception and skip directory
        spaceUsedInBytes = parentDirectoryInfo
          .EnumerateFiles("*", SearchOption.TopDirectoryOnly)
          .Sum(fileInfo => fileInfo.Length);
      }
      catch (UnauthorizedAccessException)
      {
        return 0;
      }
    
      foreach (var subdirectoryInfo in parentDirectoryInfo.EnumerateDirectories("*", SearchOption.TopDirectoryOnly))
      {
        spaceUsedInBytes += SumDirectorySize(subdirectoryInfo);
      }
    
      return spaceUsedInBytes;
    }
    

    How to instantiate a type that requires to run async operations on construction

    FolderModel.cs

    class FolderModel
    {
      // Make a constructor private to force instantiation using the factory method
      private FolderModel(string folderPath)
      {
        // Do non-async initialization
      }
    
      // Async factory method: add constructor parameters to async factory method
      public static async Task<FolderModel> CreateAsync(string folderPath)
      {
        var instance = new FolderModel(folderPath);
        await instance.InitializeAsync(folderPath);
        return instance;
      }
    
      // Define member as protected virtual to allow derived classes to add initialization routines
      protected virtual async Task InitializeAsync(string directoryPath)
      {
        // Consider to throw an exception here ONLY in case the folder is generated programmatically.
        // If folder is retrieved from user input, use input validation 
        // or even better use a folder picker dialog
        // to ensure that the provided path is always valid!
        if (!Directory.Exists(directoryPath))
        {
          throw new DirectoryNotFoundException($"Invalid directory path '{directoryPath}'.");
        }
    
        long folderSize = await GetDirectorySize(directoryPath);
    
        // TODO::Do something with the 'folderSize' value 
        // and execute other async code if necessary
      }
    }
    

    Usage

    // Create an instance of FolderModel example
    private async Task SomeMethod()
    {
      // Always await async methods (methods that return a Task).
      // Call static CreateAsync method instead of the constructor.
      FolderModel folderModel = await FolderModel.CreateAsync(@"C:\");
    }
    

    In a more advanced scenario when you want to defer the initialization for example because you want to avoid to allocate expensive resources that are not needed now or never, you can make the instance call InitializeAsync when a certain member that depends on these resources is referenced or you can make the constructor and the InitializeAsync method public to allow the user of the class to call InitializeAsync explicitly.