# Recursively search directories and their subdirectories for images and videos, except system or locked folders

Search directories and their subdirectories for images and videos, except:

• Windows system folders,
• locked folders,
• and folders selected by me. For example, as a developer I have dozens of Visual Studio folders with folders named bin and debug in them.

The goal was to write the program in a smart way. That's why I work with a List(of IO.FileInfo), which contains the files to be copied; and a List(of String), which contains the folder names that should not be searched. There is another List(of String); this one contains the allowed file extensions. The function calls itself again, so there is a List(Of IO.DirectoryInfo) that contains the folders that are already known. I also work with folder attributes so that the program doesn't try to search locked folders. That would throw an UnauthorizedAccessException and crash the program.

By the way: Sorting the two lists “allowedExtensions” and “iDontWant” was for me only for clarity when debugging.

Is there anything to improve here?

StringNaturalSortComparer.cs

using System.Collections.Generic;

namespace Backup
{
public class StringNaturalSortComparer : IComparer<string>
{
[System.Runtime.InteropServices.DllImport("shlwapi.dll", CharSet = System.Runtime.InteropServices.CharSet.Unicode)]
private static extern int StrCmpLogicalW(string psz1, string psz2);

public int Compare(string x, string y)
{
return StrCmpLogicalW(x, y);
}
}
}


FormMain.cs

using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Windows.Forms;

namespace Backup
{
public partial class FormMain : Form
{
private readonly List<System.IO.FileInfo> ListOfFilesToBeCopied = new List<System.IO.FileInfo>();
public FormMain()
{
InitializeComponent();
}

private readonly System.Globalization.CultureInfo Deu = new System.Globalization.CultureInfo("de-DE");

private void FormMain_Load(object sender, EventArgs e)
{
}

private void ButtonStart_Click(object sender, EventArgs e)
{
ListOfFilesToBeCopied.Clear();
SearchDirectories(new System.IO.DirectoryInfo(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)));
long totalSizeInBytes = 0L;

foreach (System.IO.FileInfo fi in ListOfFilesToBeCopied)
{
totalSizeInBytes += fi.Length;
}

Debug.WriteLine("");
Debug.WriteLine($"{ListOfFilesToBeCopied.Count} Files."); Debug.WriteLine($"{Math.Round((double)totalSizeInBytes / 1024.0, 0).ToString(Deu)} KB");
Debug.WriteLine($"{Math.Round((double)totalSizeInBytes / Math.Pow(1024.0, 2), 1).ToString(Deu)} MB"); Debug.WriteLine($"{Math.Round((double)totalSizeInBytes / Math.Pow(1024.0, 3), 1).ToString(Deu)} GB");
}

private void SearchDirectories(System.IO.DirectoryInfo source)
{
List<string> allowedExtensions = new List<string> { ".jpg", ".jpeg", ".bmp", ".png", ".webp" };
if (CheckBoxV.Checked)
{
allowedExtensions.AddRange(new string[] { ".avi", ".mp4" });
}
allowedExtensions = allowedExtensions.OrderBy(o => o, new StringNaturalSortComparer()).ToList();

List<string> iDontWant = new List<string> {
".config",
".dotnet",
".nuget",
".QtWebEngineProcess",
".templateengine",
".thumbnails",
".vscode",
"3D Objects",
"ansel",
"Anwendungsdaten",
"AppData",
"Contacts",
"Druckumgebung",
"Eigene Dateien",
"Favorites",
"Lokale Einstellungen",
"Netzwerkumgebung",
"OneDrive",
"Recent",
"Saved Games",
"Searches",
"SendTo",
"source",
"Startmenü",
"Vorlagen",
"InventoryApp",
".picasaoriginals",
".vs",
"v16",
"bin",
"Release",
"My Project",
"obj",
"Debug",
"TempPE",
"packages",
"lib",
"Benutzerdefinierte Office-Vorlagen",
"Custom Office Templates",
"OneNote_RecycleBin",
"config",
"Logs",
"TraceLogFiles"
};

iDontWant = iDontWant.OrderBy(o => o, new StringNaturalSortComparer()).ToList();

// Check for folders that are in the source folder and view their files.
int _9238 = (int)(System.IO.FileAttributes.Hidden | System.IO.FileAttributes.ReparsePoint | System.IO.FileAttributes.NotContentIndexed | System.IO.FileAttributes.System | System.IO.FileAttributes.Directory);
foreach (System.IO.DirectoryInfo subdirectory in source.GetDirectories())
{
// If that's a folder I don't want, or that's a system folder, move on to the next one!
if (iDontWant.Contains(subdirectory.Name) || (int)subdirectory.Attributes == _9238 || IAlreadyKnow.Contains(subdirectory))
{
continue;
}

// Unfortunately, there are many system folders that are locked for programmes. I cannot know all of them, especially their foreign names. Therefore, in addition to the “iDontWant” list, there is this try-catch block.
try
{
foreach (System.IO.FileInfo fi in subdirectory.GetFiles())
{
if (allowedExtensions.Contains(fi.Extension.ToLower(Deu)) && !ListOfFilesToBeCopied.Contains(fi))
{
}
}
SearchDirectories(subdirectory);
}
catch (System.UnauthorizedAccessException ex)
{
Debug.WriteLine(ex.Message);
}
}
}
}
}

• Why do you recreate and sort the iDontWant collection every time when someone calls the SearchDirectories method? May 23 at 6:53
• @PeterCsala So far, there is no reason for me to declare this list global. I don't need it anywhere else. The sorting was for me only for debugging purposes, so when I hover the over the list that I see what is in it. May 23 at 12:44

Let us first target the comment

Why do you recreate and sort the iDontWant collection every time when someone calls the SearchDirectories method? – Peter Csala yesterday

and let us only check about recreation of the List<string> iDontWant:

• on my Win10-PC the C:\Windows folder contains 142654 folders
• althougth in your app you only search inside the Environment.SpecialFolder.UserProfile folder on my Win10-PC this folder contains 16169 subfolders.

Because you search through each subfolder which isn't contained in the iDontWant list I would assume you will search through 10000 folders which means the said list will be created 10000 times. This leads to the allowedExtensions which in best case only contains 5 items, but there just isn't a good reason to recreate that list either each time the method is called.

You state as answer to the comment

So far, there is no reason for me to declare this list global. I don't need it anywhere else.

which is a valid point, but how about using an overloaded method like so

private void SearchDirectories(System.IO.DirectoryInfo source, List<string> allowedExtensions, List<string> excludedFolderNames)
{
int _9238 = (int)(System.IO.FileAttributes.Hidden | System.IO.FileAttributes.ReparsePoint | System.IO.FileAttributes.NotContentIndexed | System.IO.FileAttributes.System | System.IO.FileAttributes.Directory);
foreach (System.IO.DirectoryInfo subdirectory in source.GetDirectories())
{

// If that's a folder I don't want, or that's a system folder, move on to the next one!
if (excludedFolderNames.Contains(subdirectory.Name) || (int)subdirectory.Attributes == _9238 || IAlreadyKnow.Contains(subdirectory))
{
continue;
}

// Unfortunately, there are many system folders that are locked for programmes. I cannot know all of them, especially their foreign names. Therefore, in addition to the “iDontWant” list, there is this try-catch block.
try
{
foreach (System.IO.FileInfo fi in subdirectory.GetFiles())
{
if (allowedExtensions.Contains(fi.Extension.ToLower(Deu)) && !ListOfFilesToBeCopied.Contains(fi))
{
}
}
SearchDirectories(subdirectory, allowedExtensions, excludedFolderNames);
}
catch (System.UnauthorizedAccessException ex)
{
Console.WriteLine(ex.Message);
}
}
}


which you would call like

private void SearchDirectories(System.IO.DirectoryInfo source)
{
List<string> allowedExtensions = new List<string> { ".jpg", ".jpeg", ".bmp", ".png", ".webp" };
if (CheckBoxV.Checked)
{
allowedExtensions.AddRange(new string[] { ".avi", ".mp4" });
}
allowedExtensions = allowedExtensions.OrderBy(o => o, new StringNaturalSortComparer()).ToList();

List<string> iDontWant = new List<string> {

".config",
".dotnet",
".nuget",
".QtWebEngineProcess",
".templateengine",
".thumbnails",
".vscode",
"3D Objects",
"ansel",
"Anwendungsdaten",
"AppData",
"Contacts",
"Druckumgebung",
"Eigene Dateien",
"Favorites",
"Lokale Einstellungen",
"Netzwerkumgebung",
"OneDrive",
"Recent",
"Saved Games",
"Searches",
"SendTo",
"source",
"Startmenü",
"Vorlagen",
"InventoryApp",
".picasaoriginals",
".vs",
"v16",
"bin",
"Release",
"My Project",
"obj",
"Debug",
"TempPE",
"packages",
"lib",
"Benutzerdefinierte Office-Vorlagen",
"Custom Office Templates",
"OneNote_RecycleBin",
"config",
"Logs",
"TraceLogFiles"
};

iDontWant = iDontWant.OrderBy(o => o, new StringNaturalSortComparer()).ToList();
SearchDirectories(source, allowedExtensions, iDontWant);

}


But wait, in the current state of your code, we can do better....

• the code in question can never reach the same folder twice which means that you don't need to know which subfolder had been processed. In the same way the code won't reach the same file twice. This means you can get rid of IAlreadyKnow and the call to ListOfFilesToBeCopied.Contains(fi).

• we should pass the FileAttributes as well because there is no need to create them always. We don't need them to be converted to an int either.

• by extracting the check for valid files and folders to separate methods the whole code gets easier to read.

• instead of using GetFiles() and GetDirectories() we could use EnumerateFiles and EnumerateDirectories.

• we could take advantage of List<T>.AddRange()

In addition, you should extract the searching code to a separate class to separate UI and logic.

Summing up would look like so

private bool IsValidDirectory(System.IO.DirectoryInfo subdirectory, System.IO.FileAttributes attribute, List<string> excludedFolderNames)
{
return !excludedFolderNames.Contains(subdirectory.Name) && subdirectory.Attributes != attribute;
}
private bool IsValidFile(System.IO.FileInfo file, List<string> allowedExtensions)
{
return allowedExtensions.Contains(file.Extension.ToLower(Deu));
}
private void SearchDirectories(System.IO.DirectoryInfo source, List<string> allowedExtensions, System.IO.FileAttributes allowedAttributes, List<string> excludedFolderNames)
{

foreach (System.IO.DirectoryInfo subdirectory in source.EnumerateDirectories()
.Where(d => IsValidDirectory(d, allowedAttributes, excludedFolderNames)))
{
// Unfortunately, there are many system folders that are locked for programmes. I cannot know all of them, especially their foreign names. Therefore, in addition to the “iDontWant” list, there is this try-catch block.
try
{
SearchDirectories(subdirectory, allowedExtensions, allowedAttributes, excludedFolderNames);
}
catch (System.UnauthorizedAccessException ex)
{
Console.WriteLine(ex.Message);
}
}
}


And would be called like

System.IO.FileAttributes allowedAttributes = (System.IO.FileAttributes.Hidden | System.IO.FileAttributes.ReparsePoint | System.IO.FileAttributes.NotContentIndexed | System.IO.FileAttributes.System | System.IO.FileAttributes.Directory);
SearchDirectories(new System.IO.DirectoryInfo(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)), allowedExtensions, allowedAttributes, iDontWant);

• Thank you very much for your answer. I think it's especially great that the function could be shortened like this, since you used LINQ, namely EnumerateDirectories().Where and EnumerateFiles().Where in conjunction with the two new bool functions. You are right about the unnecessary recreation of the excludedFolderNames list (formerly iDonWant).... May 25 at 18:08