Secure Delete .NET
一个类似 Windows Explorer 的界面,使用 sdelete 程序。
引言
Secure Delete .NET 是一个类似 Windows Explorer 的用户界面,它使用 sdelete 程序来执行安全文件粉碎。
sdelete 是一个具有命令行界面的可执行程序。本文中的源代码只是提供了一种浏览目录、将多个文件添加到“已选定删除”列表并执行删除的方法。
要求
- Visual Studio 2010
- .NET 4.0 框架
- sdelete
注意:在此程序运行之前,您必须从上面的 technet 链接下载 sdelete 并将其安装到您的 %system%
目录中。
背景
虽然本文没有什么突破性的内容,但我认为它解决了许多常见问题,并会向您展示如何将它们整合在一起。本文解决的一些问题包括:
- 使用一个类来创建互斥体,只允许应用程序运行一个实例
- 实现一个
UserOptions
类,通过序列化来存储和检索用户选择 - 使用
ToolStripContainer
和SplitterContainer
控件创建多窗格、可调整大小的界面 - 使用 Task 查询目录详细信息
- 使用 Task Continuations 来处理原始 Task 的异常或成功
- 将目录结构加载到
TreeView
中作为 continuation - 将关联的目录和文件加载到 ListView 中
- 在不使用
VirtualMode
的情况下,以良好的性能将大量ListViewItems
加载到ListView
中 - 如何在两个
ListViews
之间拖放ListViewItems
- 实现
FileSystemWatcher
来监视目录更改,并通过调用委托来正确更新 UI - 使用
BackgroundWorker
执行线程任务 - 自定义实现
BackgroundWorker
的 Cancellation 和 Exception 事件模型,以提供我们自己的行为 - 在后台工作者运行时显示进度窗体并更新 UI
使用程序
UI 相当简单,您应该能立即认出来。

左窗格:目录视图
左侧的窗格将显示连接到您计算机的任何固定驱动器以及您的“我的文档”文件夹。
单击树视图中的节点将使其展开并显示所有子目录。
右窗格:文件视图
当您单击目录视图中的项目时,除了展开树之外,它还将加载右窗格中包含的所有文件。
文件视图是您可以选择要对其执行安全删除的任何文件的位置。
选择文件
使用标准的 Windows 选择方法(用鼠标多选高亮,或在单击文件时使用 Shift/Ctrl 键)来执行多选。
突出显示文件后,使用拖放将文件放入“已选定”列表中。
底部窗格:已选定文件
当前为删除选定的任何文件都显示在底部窗格中。通过右键单击窗口,可以从此视图访问上下文菜单。
- 删除已选项目。将开始删除操作,并安全删除列表中的任何文件。
- 从列表中移除已选项目。如果您决定不想删除某些文件,可以进行多选并选择此菜单选项从列表中移除文件。
列表视图
两个列表视图的设计都类似于 Windows Explorer 的“详细信息”视图。为了达到这种外观和感觉,我们需要检索几条信息:
- 与文件扩展名关联的图标
- 与扩展名关联的文件描述
这两者都可以通过调用 SHGetFileInfo
并传递特定标志来检索。
当目录展开并填充视图时,程序将检索有关文件扩展名的信息,并将其添加到“已知文件”缓存中。这有助于提高性能,以免我们不断调用 API。有关详细信息,请参阅主 SecureDelete
窗体中的 LoadFilesToList
方法。
注意:我知道 Icon.ExtractAssociatedIcon
可用于检索文件图标,但除了 SHGetFileInfo
之外,还有其他方法可以检索“文件类型描述”吗?我决定在这里同时做这两件事。
欢迎评论!
用户选项
用户选项屏幕非常简单,提供了一些可以修改的设置:

- 删除文件时执行的次数。默认设置为
3
。此处设置的数字越高,删除文件所需的时间就越长。 - 启用日志记录。如果选中此项,它将捕获 sdelete 的输出并写入日志文件。
- 日志文件位置。您希望将
sdelete-log-file
写入的目录。
当您在选项对话框中单击“确定”时,将对 UserOptions
类执行序列化,将其保存到您的应用程序数据文件夹。
这些设置将在应用程序会话之间持久存在。
加载目录树
由于 Directory.GetDirectories
在检索大量目录时(例如 C:\Windows)可能相当慢,因此我们可以使用 Task
来获取所有目录的详细信息。
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);
加载目录中包含的文件也包含在 Task
中。
GetFilesFromDirectory
方法启动一个新的任务,该任务调用 Directory.GetFiles
并为找到的每个文件生成一个 ExplorerFileInfo
实例。
对于检索到的每个文件,它会检查“已知文件类型”缓存以获取文件图标和文件类型描述。如果未找到,它将调用 API SHGetFileInfo
来检索信息。然后,它将 ExplorerFileInfo
实例的列表传递给 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();
}
}
在 LoadFilesToList
中有几点需要注意。
- 使用
BeginUpdate
和EndUpdate
。这对于性能非常有帮助,但我发现很少有人使用它们。 - 构建一个项目列表并使用
AddRange
添加到ListView
中。这比在循环中通过Items.Add
方法添加到ListView
中快得多。
尝试浏览一个包含大量文件(数千个)的位置 - 性能仍应可接受。
删除过程
根据选定文件的数量和删除文件时执行的次数,该过程可能需要很长时间才能完成。因此,在此期间我们应确保 UI 响应迅速,并显示一个显示当前完成百分比的窗口。
BackgroundWorker
是显而易见的最佳选择,因为它提供了用于将回传到 UI 线程的事件,用于报告进度和“工作完成”。
虽然 Task 模式提供了出色的多线程选项,但 BackgroundWorker
因其提供频繁进度报告的能力而仍然占有一席之地。
Reporting progress from Tasks 是关于此主题的一篇优秀博文。
以下代码检查 sdelete 程序是否存在,构建要删除的文件列表,初始化进度窗口,启动工作线程并将自定义对象传递给其参数,最后将进度窗口显示为模态对话框。
if (Program.AskQuestion(Resources.DeletionPrompt) == DialogResult.Yes)
{
// Check here to show a message before starting a thread
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);
// Starts the deletion process on the worker thread and displays the progress
// of the operation
DeletionStartArguments arguments =
new DeletionStartArguments(filesToDelete, UserOptions.Current.NumberOfPasses);
this.deletionWorker.RunWorkerAsync(arguments);
progressWindow.ShowDialog(this);
}
deletionWorker_DoWork
方法提供了工作线程的实现。在此方法中,我针对取消或发生异常的情况提供了一些自定义行为。
为什么要更改标准行为?
后台工作者模型规定,如果线程上存在挂起的取消,我们应将 e.Cancel
设置为 true
。如果在工作方法中发生异常,它应该保持未处理状态,并在 RunWorkerCompletedEventArgs
的 Error
字段中报告。
但是,此程序是在没有事务性 NTFS 访问权限的情况下设计的。此外,一些被删除的文件可能非常大,因此不建议使用 .NET 的事务性 API。如果在取消或异常事件发生时,DoWork
中的删除过程已删除了 10 个文件中的 2 个,我们只想退出 DoWork
并报告事件发生前已删除的文件。
因此,我们始终希望退出例程并将 DeletionCompletedArguments
对象传递给 RunWorkerCompleted
事件处理程序,该对象包含已删除的文件。
如果我们不这样做,尝试访问 Result
对象将在发生错误时导致 TargetInvocationException
,或者在取消时导致 InvalidOperationException
。
我们仍然可以在 RunWorkerCompleted
方法中实现相同的行为来测试取消或异常,但我们已将操作封装到我们自己的对象中,从而控制了操作。
删除文件
我们可以通过启动一个进程并传入必要的参数来删除文件。一个名为 FileCleaner
的类封装了此功能。
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()));
}
}
注意:如果您是第一次使用 sdelete,当进程启动时,您会看到一个对话框询问您是否接受条款。单击“接受”以继续操作。
我们可以通过让此程序自动创建注册表项来解决这个问题。
不过,我还是让您自己阅读条款并单击“接受”!
Points
这并不是一个功能齐全的 Windows Explorer 副本,目前也没有实现您可能期望从 Explorer 界面获得的所有行为。本文中的源代码旨在展示一些可用的技术,以向用户提供响应迅速、熟悉的 UI。
应用程序中的一些类最初来自我在 CodeProject 上阅读的文章。我在类头中包含了指向原始文章的链接。
历史
- 2011/01/21 - UI 修复
- 添加了自定义
TreeView
和ListView
控件,减少了闪烁 - 改进了对目录安全异常的处理
- 对几个方法进行了通用清理
- 2011/01/20 - 错误修复
- 向
ProgressWindow
添加了一个标志,以便在删除过程中不执行FileSystemWatcher
事件 - 重构了几个方法,使
FileSystemWatcher
的“Changed
”事件不必重新填充整个列表视图。 - 在删除过程后添加了刷新,窗口在所有情况下都没有正确更新
- 2011/01/14 - 初始发布