Introduction
Secure Delete .NET is a Windows Explorer like user interface that uses the sdelete program to perform secure file shredding.
sdelete is an executable program with a command line interface. The source code in this article simply provides a way to browse directories, add multiple files to a 'selected for deletion' list and perform the delete.
Requirements
- Visual Studio 2010
- .NET 4.0 Framework
- sdelete
NB: You must download sdelete from the technet link above and install in your %system%
directory before this program will work.
Background
While there is nothing groundbreaking in this article, I think that it solves a number of common issues and will show you how to tie them all together. Some of the problems addressed by this article include:
- Using a class to create a mutex and only allow 1 instance of the application to run
- An implementation of a
UserOptions
class to store and retrieve user choices through serialization
- Using
ToolStripContainer
and SplitterContainer
controls to create a multi-paned, resizeable interface
- Using a Task to query directory details
- Using Task Continuations to process exceptions or success of original Task
- Loading a directory structure into a
TreeView
as a continuation
- Loading associated directories and files into a ListView
- Loading a large amount of
ListViewItems
into a ListView
with good performance without using VirtualMode
- How to drag and drop
ListViewItems
between two ListViews
- Implementing a
FileSystemWatcher
to watch for directory changes and correctly updating the UI by invoking delegates
- Using a
BackgroundWorker
to perform a threaded task
- A custom implementation of the
BackgroundWorker
Cancellation and Exception event model to provide our own behaviour
- Displaying a progress form while the background worker is running and updating the UI
Using the Program
The UI is fairly simple and should be instantly recognisable to you.
The Left Pane: Directory View
The pane on the left will display any fixed drives attached to your computer and also your 'My Documents' folder.
Clicking on a node in the tree view will cause it to expand and display all the subdirectories.
The Right Pane: Files View
When you click on an item in the directory view, as well as expanding the tree it will also load all files that are contained in the directory to the right hand pane.
The files view is where you can select any files you wish to perform a secure deletion on.
Selecting Files
Use standard windows selection methods (multi select highlight using the mouse, or using the Shift\Ctrl keys while clicking the files) to perform a multiple selection.
Once files are highlighted, use drag and drop to put the files in the 'Selected' list.
The Bottom Pane: Selected Files
Any files you have currently selected for deletion are displayed in the bottom pane. A context menu is available on this view by right clicking the windows.
- Delete Selected. Will begin the delete operation and securely delete any of the files in the list.
- Remove selected from List. If you decide you don't want to delete certain files, you can perform a multiple select and choose this menu option to remove the files from the list.
List Views
Both of the list views are designed to look similar to windows explorer 'Details' view. There are a couple of pieces of information that we need to retrieve in order to achieve this look and feel:
- The icon associated with the file extension
- The file description associated with the extension
Both of these can be retrieved from a call to SHGetFileInfo
and passing certain flags.
As directories are expanded and views populated, the program will retrieve information about file extensions and add them to a 'Known Files' cache. This helps with performance so we don't have to keep calling the API. See the LoadFilesToList
method in the main SecureDelete
form for details.
NB: I know Icon.ExtractAssociatedIcon
can be used to retrieve a file icon, but is there any other way to retrieve 'File Type Description' other than SHGetFileInfo
? I decided to do both at the same time here.
Comments welcome!
User Options
The user options screen is very simple and provides a few settings that can be amended:
- The number of passes to perform when deleting a file. The default setting is
3
. The higher you set here, the longer it will take to delete files.
- Logging enabled. If you select this, it will capture the output of sdelete and write to the log file.
- Log File Location. The directory you want the
sdelete-log-file
to be written to.
When you click OK on the options dialog, you will perform a serialisation of the UserOptions
class to your application data folder.
These settings are then persisted between application sessions.
Loading the Directory Tree
We can use a Task
to get all of the directory details since Directory.GetDirectories
can be quite slow when there are lots of directories to retrieve (e.g. C:\Windows).
var task = Task.Factory.StartNew(() =>
{
List<string> directoryDetails = new List<string>();
foreach (string directory in Directory.GetDirectories(startPath))
{
directoryDetails.Add(directory);
}
return directoryDetails;
});
Task failed = task.ContinueWith(t => HandleTaskError(t.Exception),
TaskContinuationOptions.OnlyOnFaulted);
Task ok = task.ContinueWith(t =>
PopulateTree(t.Result, startPath, node),
TaskContinuationOptions.OnlyOnRanToCompletion);
Loading the files contained within the directory is also contained in a Task
.
The method GetFilesFromDirectory
starts a new task that calls Directory.GetFiles
and generates an ExplorerFileInfo
instance for each found file.
For each file retrieved, it checks the cache of 'Known File Types' for a file Icon and file type description. If one isn't found, it calls the API SHGetFileInfo
to retrieve the information. It then passes a list of ExplorerFileInfo
instances to LoadFilesToList
.
private void LoadFilesToList(List<ExplorerFileInfo> files)
{
try
{
this.filesList.Items.Clear();
this.filesList.BeginUpdate();
ListViewItem[] viewItems = new ListViewItem[files.Count];
for (int i = 0; i < viewItems.Length; ++i)
{
ExplorerFileInfo file = files[i];
string[] items = new string[]{ file.Name, file.FileSize,
file.FileTypeDescription,
file.LastWriteTime.ToString()};
ListViewItem listItem =
new ListViewItem(items, file.Extension);
listItem.Name = file.FullName;
listItem.SubItems[1].Tag = file.Length;
viewItems[i] = listItem;
}
this.filesList.Items.AddRange(viewItems);
}
finally
{
this.filesList.EndUpdate();
}
}
There are a couple of things to note in LoadFilesToList
.
- Use of
BeginUpdate
and EndUpdate
. Really helps with performance, but I don't see enough people using them.
- Building up a list of items and adding to the
ListView
using AddRange
. This is much quicker than adding to the ListView
within the loop via the Items.Add
method.
Try browsing to a location that has a large number of files in (many thousand) - performance should still be acceptable.
The Deletion Process
Depending on the number of files selected and the number of passes to perform while deleting the files, the process could take a significant amount of time to complete. We should therefore ensure the UI is responsive during this period and show a window that displays the current percentage completed.
The BackgroundWorker
is the obvious choice to use for this since it provides events for marshalling back to the UI thread for both reporting progress and 'Work Completed'.
While the Task pattern provides excellent threading options, the BackgroundWorker
still has its place because of its ability to provide frequent progress reporting.
Reporting progress from Tasks is an excellent blog on this subject.
The following code checks for the existence of the sdelete program, builds a list of files to delete, initialises the progress window, starts the worker thread and passes a custom object to its arguments and finally displays the progress window as a modal dialog.
if (Program.AskQuestion(Resources.DeletionPrompt) == DialogResult.Yes)
{
if (!FileCleaner.CheckForDeleteProgram())
{
Program.ShowError(Resources.DeletionProgramNotFound);
return;
}
List<string> filesToDelete = new List<string>();
foreach (ListViewItem item in this.selectedFilesList.Items)
{
filesToDelete.Add(item.Name);
}
progressWindow.Begin(0, filesToDelete.Count);
DeletionStartArguments arguments =
new DeletionStartArguments(filesToDelete, UserOptions.Current.NumberOfPasses);
this.deletionWorker.RunWorkerAsync(arguments);
progressWindow.ShowDialog(this);
}
The deletionWorker_DoWork
method provides the implementation for the worker thread. In this method, I have provided some custom behaviour for cases where either a Cancellation or Exception had occurred.
Why change the standard behaviour?
The background worker model states that we should set e.Cancel
to true
if a cancellation is pending on the thread. If an exception occurs in the worker method, it should be left unhandled and it would be reported in the Error
field of RunWorkerCompletedEventArgs
.
However, this program was designed without access to Transactional NTFS. Additionally, some of the files being deleted could be quite large so using one of the Transactional APIs for .NET would not be advisable. If the delete process in DoWork
has deleted 2 files out of 10 when a cancellation or exception event occurs, we just want to exit DoWork
and report on the files that were deleted before the event.
Therefore, we always want to exit the routine and pass back a DeletionCompletedArguments
object to the RunWorkerCompleted
event handler which contains the files that have been deleted.
If we didn't do this, trying to access the Result
object would cause either a TargetInvocationException
in the case of an error, or an InvalidOperationException
in the case of a cancellation.
We can still implement the same behaviour to test for cancellations or exceptions in the RunWorkerCompleted
method, but we've taken control of the operation by wrapping into our own objects.
Deleting a File
We can delete the file by starting a process and passing in the required arguments. A class called FileCleaner
wraps this functionality:
using (Process p = new Process())
{
p.StartInfo.FileName = Resources.DeletionProgram;
p.StartInfo.Arguments = string.Format("-p {0} -q \"{1}\"", passes, fileName);
p.StartInfo.CreateNoWindow = true;
p.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
p.StartInfo.UseShellExecute = false;
p.StartInfo.RedirectStandardOutput = true;
p.Start();
string output = p.StandardOutput.ReadToEnd();
p.WaitForExit();
fileDeleted = !File.Exists(fileName);
if (UserOptions.Current.LogOperations)
{
log.Info(output);
log.Info(string.Format(Resources.FileDeletionStatus,
fileName, fileDeleted.ToString()));
}
}
NB: If this is the first time you have ever used sdelete, when the process is started you will see a dialog result asking you to accept the terms. Click accept to continue with the operation.
We could get around this by having this program automatically creating the registry key.
How to create the sdelete 'Accepted Terms' registry key
However, I'll leave it to you to read the terms and click Accept!
Points
This isn't a fully blown replica of Windows Explorer and doesn't currently implement all of the behaviour you might expect from an explorer interface. The source in this article was designed to show some of the techniques available to provide a responsive, familiar UI to a user.
Some of the classes in the application were originally from articles I've read on CodeProject. I've included links to the original articles in the class headers.
History
- 21/01/2011 - UI fixes
- Added custom
TreeView
and ListView
controls that reduce flicker
- Better handling of directory security exceptions
- General tidy up of a few methods
- 20/01/2011 - Bug fixes
- Added flag to
ProgressWindow
so the FileSystemWatcher
events don't execute if a deletion process is in operation
- Refactored a few methods so the
FileSystemWatcher
'Changed
' event doesn't have to repopulate the entire list view.
- Added a refresh after a deletion process, the window wasn't updating properly in all cases
- 14/01/2011 - Initial release