65.9K
CodeProject 正在变化。 阅读更多。
Home

Secure Delete .NET

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.77/5 (13投票s)

2011 年 1 月 18 日

CPOL

8分钟阅读

viewsIcon

44739

downloadIcon

1342

一个类似 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 相当简单,您应该能立即认出来。

SS-2011.01.14-11.31.16.png
左窗格:目录视图

左侧的窗格将显示连接到您计算机的任何固定驱动器以及您的“我的文档”文件夹。

单击树视图中的节点将使其展开并显示所有子目录。

右窗格:文件视图

当您单击目录视图中的项目时,除了展开树之外,它还将加载右窗格中包含的所有文件。

文件视图是您可以选择要对其执行安全删除的任何文件的位置。

选择文件

使用标准的 Windows 选择方法(用鼠标多选高亮,或在单击文件时使用 Shift/Ctrl 键)来执行多选。

突出显示文件后,使用拖放将文件放入“已选定”列表中。

底部窗格:已选定文件

当前为删除选定的任何文件都显示在底部窗格中。通过右键单击窗口,可以从此视图访问上下文菜单。

  • 删除已选项目。将开始删除操作,并安全删除列表中的任何文件。
  • 从列表中移除已选项目。如果您决定不想删除某些文件,可以进行多选并选择此菜单选项从列表中移除文件。
列表视图

两个列表视图的设计都类似于 Windows Explorer 的“详细信息”视图。为了达到这种外观和感觉,我们需要检索几条信息:

  • 与文件扩展名关联的图标
  • 与扩展名关联的文件描述

这两者都可以通过调用 SHGetFileInfo 并传递特定标志来检索。

当目录展开并填充视图时,程序将检索有关文件扩展名的信息,并将其添加到“已知文件”缓存中。这有助于提高性能,以免我们不断调用 API。有关详细信息,请参阅主 SecureDelete 窗体中的 LoadFilesToList 方法。

注意:我知道 Icon.ExtractAssociatedIcon 可用于检索文件图标,但除了 SHGetFileInfo 之外,还有其他方法可以检索“文件类型描述”吗?我决定在这里同时做这两件事。

欢迎评论!

用户选项

用户选项屏幕非常简单,提供了一些可以修改的设置:

SS-2011.01.14-11.46.22.png
  • 删除文件时执行的次数。默认设置为 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,当进程启动时,您会看到一个对话框询问您是否接受条款。单击“接受”以继续操作。

我们可以通过让此程序自动创建注册表项来解决这个问题。

如何创建 sdelete 的“接受条款”注册表项

不过,我还是让您自己阅读条款并单击“接受”!

Points

这并不是一个功能齐全的 Windows Explorer 副本,目前也没有实现您可能期望从 Explorer 界面获得的所有行为。本文中的源代码旨在展示一些可用的技术,以向用户提供响应迅速、熟悉的 UI。

应用程序中的一些类最初来自我在 CodeProject 上阅读的文章。我在类头中包含了指向原始文章的链接。

历史

  • 2011/01/21 - UI 修复
    • 添加了自定义 TreeViewListView 控件,减少了闪烁
    • 改进了对目录安全异常的处理
    • 对几个方法进行了通用清理
  • 2011/01/20 - 错误修复
    • ProgressWindow 添加了一个标志,以便在删除过程中不执行 FileSystemWatcher 事件
    • 重构了几个方法,使 FileSystemWatcher 的“Changed”事件不必重新填充整个列表视图。
    • 在删除过程后添加了刷新,窗口在所有情况下都没有正确更新
  • 2011/01/14 - 初始发布
© . All rights reserved.