使用 IMAPIv2.0 创建光学文件镜像






4.90/5 (30投票s)
使用 IMAPI 2 创建磁盘 ISO 映像。

引言
有几个免费的实用工具可以创建文件镜像,但通常不提供源代码,或者只提供 C++ 版本(如这个 CodeProject 文章)或其他脚本语言。
因此,我尝试使用托管代码创建这个实用工具,这样如果其他语言不是一个选项,您也可以轻松地重用它。
背景
最初只在 Windows Vista 上可用,在 2007 年,Microsoft 为 Windows XP 和 Windows 2003 服务器推出了 IMAPIv2.0。
与第一个版本不同,新版本支持新型介质,更重要的是,对我来说,它在创建光学文件镜像方面提供了一些支持。
不幸的是,没有托管 API 可以使用,所以我们仍然需要依赖 COM Interop 来使用这个功能,而且据我所知,也没有办法使用 Image Mastering API v2.0 读取光学文件镜像。
使用 COM 对象的一种方法是直接在您的托管代码项目中添加对它们的引用。为了运行甚至构建此解决方案,您需要注册这些 COM 对象,在本例中是安装 IMAPIv2.0。更好的方法是使用基于属性的类型,这样我们就可以在运行时将它们与正确的 COM 二进制文件挂钩,从而至少可以轻松构建解决方案。幸运的是,Microsoft 提供了这样一个代理文件,我很乐意使用它。
创建光学文件镜像
没有一个 API 调用可以做到这一点,但这样做并非难事。
使用 Microsoft 提供的 Interop.cs 文件,添加这些命名空间
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using IMAPI2.Interop;
导入这个旧的 Win32API...
[DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
static internal extern uint SHCreateStreamOnFile
(string pszFile, uint grfMode, out IStream ppstm);
...然后您就可以开始使用了
IFileSystemImage ifsi = new MsftFileSystemImage();
ifsi.ChooseImageDefaultsForMediaType(IMAPI_MEDIA_PHYSICAL_TYPE.IMAPI_MEDIA_TYPE_DISK);
ifsi.FileSystemsToCreate =
FsiFileSystems.FsiFileSystemJoliet | FsiFileSystems.FsiFileSystemISO9660;
ifsi.VolumeName = "YourVolume";
ifsi.Root.AddTree("c:\\YourDirToArchive", true);//use a valid folder
//this will implement the Write method for the formatter
IStream imagestream = ifsi.CreateResultImage().ImageStream;
if (imagestream != null)
{
System.Runtime.InteropServices.ComTypes.STATSTG stat;
imagestream.Stat(out stat, 0x01);
IStream newStream;
if (0 == SHCreateStreamOnFile
("C:\\YourImage.iso", 0x00001001, out newStream) && newStream != null)
{
IntPtr inBytes = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(long)));
IntPtr outBytes = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(long)));
try
{
imagestream.CopyTo(newStream, stat.cbSize, inBytes, outBytes);
Marshal.ReleaseComObject(imagestream);
imagestream = null;
newStream.Commit(0);
}
finally
{
Marshal.ReleaseComObject(newStream);
Marshal.FreeHGlobal(inBytes);
Marshal.FreeHGlobal(outBytes);
if (imagestream != null)
Marshal.ReleaseComObject(imagestream);
}
}
}
Marshal.ReleaseComObject(ifsi);
正如我承诺的那样,它看起来非常简单,但简单也意味着我们省略了一些会让这个解决方案不适合实际 UI 应用程序的环节。
您可以注意到,您只能添加文件夹而不能添加单个文件,您无法在此耗时过程中优雅地取消,也无法获得操作进度的任何更新。因此,让我们开始着手解决这些问题。
向镜像添加单个文件
添加文件涉及创建多个 IFsiDirectoryItem
,如果我们希望原始路径与镜像中的路径匹配的话。
还请注意 LoadCOMStream
的存在,它使用 SHCreateStreamOnFile
来创建添加到镜像的文件。
最后,我使用 Update
事件来跟踪进度。
private void CreateFSIFile(FileInfo file, IFsiDirectoryItem diritem)
{
string realpath = UniqueListFileSystemInfo.GetPhysicalPath(file.FullName);
int index = realpath.IndexOf(":\\") + 1;
if (_sysImage.Exists(realpath.Substring(index)) == FsiItemType.FsiItemNotFound)
{
IFsiDirectoryItem crtdiritem = diritem as IFsiDirectoryItem;
string name = Path.GetFileName(realpath);
if (string.Compare(diritem.FullPath,
Path.GetDirectoryName(realpath).Substring(index), true) != 0)
{
string fsipath = Path.GetDirectoryName(realpath).Substring
(index + 1 + diritem.FullPath.Length);
string[] dirs = fsipath.Split('\\');
//create the subdirs one by one
foreach (string dir in dirs)
{
if (dir.Length == 0)
continue;//in the root like C:\
try
{
string newpath = string.Format("{0}\\{1}", crtdiritem.FullPath, dir);
if (_sysImage.Exists(newpath) != FsiItemType.FsiItemDirectory)
crtdiritem.AddDirectory(dir);
}
catch
{
Cancel = true;
throw;
}
crtdiritem = crtdiritem[dir] as IFsiDirectoryItem;
}
}
System.Runtime.InteropServices.ComTypes.IStream newStream = null;
try
{
newStream = LoadCOMStream(realpath);
crtdiritem.AddFile(name, newStream);
}
finally
{
Marshal.ReleaseComObject(newStream);
}
_actualSize += ((FileInfo)file).Length;
if (Update != null && Update.GetInvocationList().Length > 0)
Update(file.FullName, ((FileInfo)file).Length);
}
else
throw new IOException("invalid file or folder");
}
添加更新和取消任务的支持
光学镜像创建涉及两个耗时的任务。
第一个是创建内存中的表示,使用上述方法,这是因为调用了 FsiDirectoryItem
类的 AddFile
。第二个耗时的任务是使用 IStream
类的 CopyTo
方法将数据写入磁盘。我没有使用 Microsoft 在 Interop.cs 文件中提供的类型,而是创建了自己的类型来封装现有类型,并显式实现与原始互操作类型相似的 interface
。
这尤其必要,因为我们不能使用 MsftDiscFormat2DataClass
将镜像写入文件,所以我不得不自己实现相同的 interface
。
创建内存中的镜像分散在许多方法中,但核心是这样的
IFileSystemImageResult IFileSystemImage.CreateResultImage()
{
if (_sysImage == null)
_cancel = false;
#if DEBUG
System.Diagnostics.Stopwatch tm = new System.Diagnostics.Stopwatch();
tm.Start();
#endif
try
{
FileSysImage = new MsftFileSystemImage();
_sysImage.ChooseImageDefaultsForMediaType(_mediatype);
IFsiDirectoryItem root = _sysImage.Root;
_sysImage.FileSystemsToCreate = _fsifs;
_sysImage.VolumeName = _volumeName;
_actualSize = 0;
_items.ForEach(delegate(FileSystemInfo item)
{
if (!_cancel)
if ((item.Attributes & FileAttributes.Directory) == 0)
CreateFSIFile(item as FileInfo, root);
else
if (this.Update == null ||
Update.GetInvocationList().Length == 0)
root.AddTree(item.FullName, true);
else
CreateFSIFolder(item as DirectoryInfo, root);
});
return _cancel ? null : _sysImage.CreateResultImage();
}
catch
{
Cancel = true;
throw;
}
finally
{
if (_cancel)
FileSysImage = null;
#if DEBUG
tm.Stop();
Debug.WriteLine(string.Format("Preparing the image lasted {0} ms",
tm.Elapsed.TotalMilliseconds.ToString("#,#")));
#endif
}
}
请注意,如果不需要更新,您仍然可以使用 AddTree
的快速方法。
下面显示了在提供更新的同时将数据写入文件镜像的实现
private void CreateProgressISOFile
(System.Runtime.InteropServices.ComTypes.IStream imagestream)
{
System.Runtime.InteropServices.ComTypes.STATSTG stat;
int bloksize = _fsres.BlockSize;
long totalblocks = _fsres.TotalBlocks;
#if DEBUG
System.Diagnostics.Stopwatch tm = new System.Diagnostics.Stopwatch();
tm.Start();
#endif
imagestream.Stat(out stat, 0x01);
if (stat.cbSize == totalblocks * bloksize)
{
byte[] buff = new byte[bloksize];
System.IO.BinaryWriter bw =
new BinaryWriter(new FileStream(_outputFileName, FileMode.Create));
IntPtr pcbRead = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(uint)));
IProgressItems prg = _fsres.ProgressItems;
IEnumerator enm = prg.GetEnumerator();
enm.MoveNext();
IProgressItem crtitem = enm.Current as IProgressItem;
try
{
Marshal.WriteInt32(pcbRead, 0);
for (float i = 0; i < totalblocks; i++)
{
imagestream.Read(buff, bloksize, pcbRead);
if (Marshal.ReadInt32(pcbRead) != bloksize)
{
string err = string.Format
("Failed because Marshal.ReadInt32(pcbRead)
= {0} != bloksize = {1}",
Marshal.ReadInt32(pcbRead), bloksize);
Debug.WriteLine(err);
throw new ApplicationException(err);
}
bw.Write(buff);
if (crtitem.LastBlock <= i)
{
if (enm.MoveNext())
crtitem = enm.Current as IProgressItem;
if (_cancel)
return;
}
if(Update != null)
Update(crtitem, i / totalblocks);
}
return;
}
catch (Exception ex)
{
Debug.WriteLine(string.Format
("Exception in : CreateProgressISOFile {0}", ex.Message));
throw;
}
finally
{
bw.Flush();
bw.Close();
Marshal.FreeHGlobal(pcbRead);
#if DEBUG
tm.Stop();
Debug.WriteLine(string.Format
("Time spent in CreateProgressISOFile: {0} ms",
tm.Elapsed.TotalMilliseconds.ToString("#,#")));
#endif
}
}
else
{
Debug.WriteLine(string.Format("failed because stat.cbSize({0}) !=
totalblocks({1}) * bloksize({2}) ", stat.cbSize,
totalblocks, bloksize));
}
}
请注意在 Update
事件中使用了 Microsoft 提供的 IProgressItem
类型。
其他重要说明
如果您想知道 ImageRepository
类中的 _items
成员是什么,它是一个派生自 List<FileSystemInfo>
的类的实例,该类确保每个文件或文件夹在镜像中只出现一次,避免了已包含在现有文件夹中的文件或因本地驱动器映射而相同的文件夹。下面所示的 AddUniqueFile
和 AddUniqueFolder
将使此列表真正唯一
/// Adds a new file only if it's not included
internal bool AddUniqueFile(string path)
{
path = GetPhysicalPath(path);
if (File.Exists(path))
{
bool bexists = false;
List folders = this.FindAll(delegate(FileSystemInfo it)
{ return it is DirectoryInfo; });
//don't add if it is already in an existing folder
if (folders.Count > 0)
{
string filedir = Path.GetDirectoryName(path);
bexists = folders.Exists(delegate(FileSystemInfo it)
{
string folder = it.FullName;
return string.Compare(filedir, 0, folder, 0, folder.Length, true) == 0;
});
if (bexists)
return false;
}
//don't add if it is already in the collection as a file
List files = this.FindAll(delegate(FileSystemInfo it)
{ return it is FileInfo; });
bexists = files.Exists(delegate(FileSystemInfo it)
{
string fname = it.FullName;
return string.Compare(path, 0, fname, 0, fname.Length, true) == 0;
});
if (!bexists)
this.Add(new FileInfo(path));
return !bexists;
}
return false;
}
/// Adds a new folder only if it's not included, strips files that are included in it
internal bool AddUniqueFolder(string path)
{
path = GetPhysicalPath(path);
if (Directory.Exists(path))
{
List folders = this.FindAll(delegate(FileSystemInfo it)
{ return it is DirectoryInfo; });
string dir = path.TrimEnd('\\');
//don't add if it is already in an existing folder
if (folders.Count > 0)
{
bool bexists = folders.Exists(delegate(FileSystemInfo it)
{
string folder = it.FullName;
return string.Compare(dir, 0, folder, 0, folder.Length, true) == 0;
});
if (bexists)
return false;
}
//remove the existing files that are contained in this current folder
List dupfiles = this.FindAll(delegate(FileSystemInfo it)
{
if (it is FileInfo)
{
string filedir = Path.GetDirectoryName(it.FullName);
return string.Compare(path, 0, filedir, 0, path.Length, true) == 0;
}
return false;
});
dupfiles.ForEach(delegate(FileSystemInfo it)
{
bool result = this.Remove(it);
});
this.Add(new DirectoryInfo(dir));
return true;
}
return false;
}
当在应用程序的“选择文件”选项卡中添加或删除文件和文件夹时(如下图所示),上述方法非常有用

如何使用它
文件选定后,在“构建文件”选项卡中,您可以选择创建文件镜像并指定一些参数。
我不会一一介绍,但我会指出,工作可以在同一个 UI 线程或后台工作线程上完成,您也可以选择不进行任何 UI 更新。如果您选择不进行 UI 更新,您会注意到速度有显著提升,但在我的 Windows XP 单处理器机器上,我无法区分前台和后台线程的区别。对于 UI 更新,我使用了 ImageRepository
和 ISOFormatter
类的 Update
事件,这些事件与 Form 的方法挂钩
void AsyncFormattingEvent(object o1, object o2)
{
Invoke(new DiscFormat2Data_EventsHandler(FormattingEvent), new Object[] { o1, o2 });
}
void FormattingEvent(object o1, object o2)
{
IMAPI2.Interop.IProgressItem it = o1 as IMAPI2.Interop.IProgressItem;
int i = (int)(Convert.ToSingle(o2) * 100);
this._progBar.Value = 100 + i;
if (it != null)
this._lblUpdate.Text = string.Format("Formatting {0}", it.Description);
if (!_ckWorker.Checked)
Application.DoEvents();
}
void AsyncRepositoryUpdate(object o1, object o2)
{
Invoke(new DiscFormat2Data_EventsHandler(RepositoryUpdate), new Object[] { o1, o2 });
}
void RepositoryUpdate(object o1, object o2)
{
string file = o1 as string;
long i = Convert.ToInt64(o2);
int pos = (int)((double)_repository.ActualSize /
_repository.SizeBeforeFormatting * 100);
_progBar.Value = pos;
_lblUpdate.Text = string.Format("Adding {0} size = {1}", file, i);
if (!_ckWorker.Checked)
Application.DoEvents();
}
请注意,即使在使用 BackgroundWorker
时,我也使用了自己的 Update
事件,而不是现有的 ProgressChanged
。
代码重用
该解决方案包含两个项目,将 UI 与实际执行镜像创建的类分开。如果您添加对 ISOImage.dll 或提供 Visual Studio 2005 和 2008 两个版本的 FileImage
项目的引用,您可以轻松重用此功能。在运行它之前,请不要忘记安装 Image Mastering API v2.0,然后尽情享用吧!