I have made a program that given a directory, will get the permissions for that directory and all sub-directories. The output is stored in a string that can be easily written to .csv. Each permissions entry is given its own line. The path is displayed first, with each directory level split into separate cells. Obviously, if the directory nesting has varying depth, the rows have a different number of cells. The permissions data follows, aligned after the deepest level, so the output would look like this:
Right now, the only way I see to do this is a multiple iteration of the results. I must go through the file system to fetch all the directories and save them, then find the longest path, and then format them all into a string. Needless to say, this takes a very long time on large file systems, such as one of my company's network shares with over 200,000 directories. It takes over a day for the program to run on this share, but if I make the path print in a single cell, using only one iteration, it takes minutes.
For more narrow queries, the program runs acceptably well, usually within a few seconds. However, I'm sure there is some way to reduce the multiple iterations so that the time required is reduced also.
PermissionsChecker
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Extensions.CollectionExtensions;
namespace DirectoryPermissionsChecker
{
internal class PermissionsChecker
{
public static readonly string OutputPath =
Path.Combine(
Environment.GetFolderPath(
Environment.SpecialFolder.MyDocuments),
"DirectoryPermissionsChecker");
private const string LogFileName = "Log";
private const string OutputFileName = "DirectoryPermissions";
private const string CsvExtension = ".csv";
private const string DateTimeFormat = "yyyyMMddTHHmmss";
private readonly CancellationTokenSource _cancellationTokenSource;
private readonly DirectoryGetter _directoryGetter;
private readonly List<string> _log;
internal PermissionsChecker(
string rootPath,
SearchDepth searchDepth)
{
_cancellationTokenSource = new CancellationTokenSource();
_log = new List<string>();
_directoryGetter = new DirectoryGetter(
rootPath, searchDepth, _cancellationTokenSource.Token, _log);
Directory.CreateDirectory(OutputPath);
}
internal List<DirectoryInfo> Directories { get; private set; }
internal void Cancel()
{
_cancellationTokenSource.Cancel();
}
internal async Task Execute()
{
var task = Task.Run(
() =>
{
Directories = _directoryGetter.GetDirectories();
var resultsFileName = Path.Combine(
OutputPath,
$"{OutputFileName} - " +
$"{DateTime.Now.ToString(DateTimeFormat)}{CsvExtension}");
File.WriteAllText(
resultsFileName,
new DirectoryFormatter(
Directories, _cancellationTokenSource.Token, _log)
.FormatDirectories());
},
_cancellationTokenSource.Token);
await task;
if (_log.IsEmpty()) return;
var fileName = Path.Combine(
OutputPath,
$"{LogFileName} - " +
$"{DateTime.Now.ToString(DateTimeFormat)}{CsvExtension}");
File.WriteAllLines(fileName, _log);
}
}
}
DirectoryGetter
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace DirectoryPermissionsChecker
{
internal enum SearchDepth
{
All,
Children,
Current,
None
}
internal class DirectoryGetter
{
private const int RootLevel = 0;
private readonly DirectoryInfo _rootDirectory;
private readonly SearchDepth _searchDepth;
private readonly CancellationToken _cancellationToken;
private readonly List<string> _log;
internal DirectoryGetter(
string rootPath,
SearchDepth searchDepth,
CancellationToken cancellationToken,
List<string> log)
{
if (!Directory.Exists(rootPath))
{
throw new DirectoryNotFoundException(
$"Could not find directory '{rootPath}'");
}
_rootDirectory = new DirectoryInfo(rootPath);
if (searchDepth == SearchDepth.None)
{
throw new ArgumentException("Search Depth must not be null.");
}
_searchDepth = searchDepth;
_cancellationToken = cancellationToken;
_log = log;
}
internal List<DirectoryInfo> GetDirectories()
{
var directories = new List<DirectoryInfo>();
GetDirectories(_rootDirectory, directories, RootLevel);
return directories;
}
private void GetDirectories(
DirectoryInfo directory,
ICollection<DirectoryInfo> directories,
int currentLevel)
{
directories.Add(directory);
if (_searchDepth == SearchDepth.Current) return;
if (_searchDepth == SearchDepth.Children &&
currentLevel > 0)
return;
try
{
foreach (var subDirectory in directory.GetDirectories())
{
_cancellationToken.ThrowIfCancellationRequested();
GetDirectories(
subDirectory, directories, currentLevel + 1);
}
}
catch (PathTooLongException e)
{
_log.Add(e.Message);
}
catch (UnauthorizedAccessException e)
{
_log.Add(e.Message);
}
}
}
}
DirectoryFormatter
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Threading;
using Extensions.PrimitiveExtensions;
namespace DirectoryPermissionsChecker
{
internal class DirectoryFormatter
{
private const char Comma = ',';
private const char Quote = '"';
private const char Newline = '\n';
private readonly IEnumerable<DirectoryInfo> _directories;
private readonly CancellationToken _cancellationToken;
private readonly List<string> _log;
internal DirectoryFormatter(
IEnumerable<DirectoryInfo> directories,
CancellationToken cancellationToken,
List<string> log)
{
_directories = directories;
_cancellationToken = cancellationToken;
_log = log;
}
internal string FormatDirectories()
{
var result = string.Empty;
var directoriesParts = new List<DirectoryParts>();
foreach (var directory in _directories)
{
_cancellationToken.ThrowIfCancellationRequested();
try
{
var fullName = directory.FullName;
var fullNameSplit = fullName.Split(
Path.DirectorySeparatorChar).Where(
s => !s.IsNullOrWhiteSpace()).ToArray();
directoriesParts.Add(
new DirectoryParts
{
SplitPath = fullNameSplit,
AccessRules =
directory.GetAccessControl().GetAccessRules(
true, true, typeof(NTAccount))
});
}
catch (PathTooLongException e)
{
_log.Add(e.Message);
}
catch (UnauthorizedAccessException e)
{
_log.Add(e.Message);
}
}
var longestPath = 0;
foreach (var directoryParts in directoriesParts)
{
_cancellationToken.ThrowIfCancellationRequested();
if (directoryParts.SplitPath.Length > longestPath)
longestPath = directoryParts.SplitPath.Length;
}
for (var i = 0; i < longestPath; i++)
{
result += $"Level {i}".Wrap(Quote) + Comma;
}
result += string.Join(
Comma.ToString(),
"Identity".Wrap(Quote),
"File System Rights".Wrap(Quote),
"Access Control Type".Wrap(Quote),
"Is Inherited?".Wrap(Quote),
Newline);
foreach (var directoryParts in directoriesParts)
{
_cancellationToken.ThrowIfCancellationRequested();
var pathString = string.Empty;
for (var i = 0; i < longestPath; i++)
{
_cancellationToken.ThrowIfCancellationRequested();
if (i < directoryParts.SplitPath.Length)
{
pathString += directoryParts.SplitPath[i].Wrap(Quote) +
Comma;
}
else
{
pathString += string.Empty.Wrap(Quote) + Comma;
}
}
foreach (FileSystemAccessRule accessRule in
directoryParts.AccessRules)
{
_cancellationToken.ThrowIfCancellationRequested();
result += pathString + string.Join(
Comma.ToString(),
accessRule.IdentityReference.Value.Wrap(Quote),
accessRule.FileSystemRights.ToString().Wrap(Quote),
accessRule.AccessControlType.ToString().Wrap(Quote),
accessRule.IsInherited.ToString().Wrap(Quote),
Newline);
}
}
return result;
}
private struct DirectoryParts
{
internal string[] SplitPath { get; set; }
internal AuthorizationRuleCollection AccessRules { get; set; }
}
}
}
The code is executed by creating a new PermissionsChecker
and calling Execute()
.
Like I said before, if I change the program so that the string building is handled inside the directory getting, thereby only iterating over each directory once, the program runs many times faster, but at the expense of being able to split the paths and then "right-align" the permissions result. I thought about displaying the permissions first, and then the path, but that goes against the spec I was given to work by.