Introduction
There are several free utilities that allow you to create file images, but the code is usually not available or available only in C++ like this CodeProject article or in other scripting languages.
Hence I tried to create this utility using managed code, so you can reuse it easily if another language is not an option.
Background
Initially available only on Windows Vista, in 2007 Microsoft introduced IMAPIv2.0 for Windows XP and Windows 2003 server.
Unlike the first version, the new one works with new types of media, and more importantly, for my purpose has some support for creating optical file images.
Unfortunately there is no managed API to use, so we still have to rely on COM Interop to use this functionality, nor is there any way I know of reading an optical file image using Image Mastering API v2.0.
One way to use COM objects is just to add references to them in your managed code project. In order to run or even to build this solution, you would have to have those COM objects registered, in our case IMAPIv2.0 installed. A better approach would be using the attribute based types, so we can hook them up with the right COM binaries at runtime, so we can at least build the solution with no hassle. Fortunately Microsoft provided such a shim file and I was glad to use it.
Creating an Optical Image as a File
There is no one API call that would do that, but doing so is not rocket science.
Using the Interop.cs file provided by Microsoft, add these namespaces:
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using IMAPI2.Interop;
Import this legacy Win32API...
[DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
static internal extern uint SHCreateStreamOnFile
(string pszFile, uint grfMode, out IStream ppstm);
... and you are ready to go:
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);
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);
As I promised it looks pretty simple, but with simplicity we also cut some corners that would make this solution unfit for a real world UI application.
You can notice that you can add only folders not individual files, you cannot gracefully cancel this time intensive process and you also don't get any update on the progress of the operation. So let's start addressing these issues.
Adding an Individual File to the Image
Adding a file involves some multiple IFsiDirectoryItem
creations if we want the original path to match the one in the image.
Also notice the presence of LoadCOMStream
that uses SHCreateStreamOnFile
to create the file added to the image.
Finally, I use the Update
event used to track the progress.
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");
}
Adding Support for Updating and Cancelling the Task
There are two time intensive tasks associated with an optical image creation.
The first is creating an in memory representation using the method above because of the calls to AddFile
of the FsiDirectoryItem
class. The second lengthy task is writing that data to the disk using the CopyTo
method of the IStream
class. Instead of using the types provided by Microsoft in the Interop.cs file, I've created my own types encapsulating existing types and explicitly implementing the interface
s that would look somewhat similar to the original interop types.
This was especially needed because we cannot use MsftDiscFormat2DataClass
to write the image into a file, so I had to come up with my own implementation of the same interface
s.
Creating the in-memory image is spread in many methods, but here is the heart of it:
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
}
}
Please notice that you would still use the quick approach using AddTree
if no update was needed.
The implementation of writing the data into the file image while providing updates is shown below:
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));
}
}
Notice the use of the Microsoft provided IProgressItem
type in the Update
event.
Other Important Remarks
If you were wondering what the _items
member of the ImageRepository
class is, it is an instance of a class derived from List<FileSystemInfo>
that makes sure every file or folder is represented only once in the image avoiding files already included in existing folders or folders that are the same because of the local drive mapping. The AddUniqueFile
and AddUniqueFolder
shown below would make this list really unique:
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; });
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;
}
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;
}
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('\\');
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;
}
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;
}
The methods above come in very handy when adding or dropping files and folders on the ‘Select Files’ tab of the application shown in the image below:
How to Use It
Once the files are selected, on the ‘Build File’ tab, you have the option to create your file image specifying a number of parameters.
I won't go over them, but I would point out that the work can be done on the same UI thread or a background worker thread and you can also choose not to have any UI update. If you chose no UI update, you will notice a significant speed improvement, but for foreground vs. background thread, I can't tell the difference on my Windows XP single proc machine. For the UI updates, I used the Update
event from both ImageRepository
and ISOFormatter
classes that hooked up with methods of the 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();
}
Notice than even when using the BackgroundWorker
I used my own Update
event rather than the existing ProgressChanged
.
Reusing the Code
The solution has two projects to separate the UI from the classes that actually do the image creation. You can easily reuse this functionality if you add a reference to the ISOImage.dll or to the FileImage
project that comes in two flavors for Visual Studio 2005 and 2008. Don't forget to install the Image Mastering API v2.0 before you run it and enjoy!