Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

How to Create Optical File Images using IMAPIv2.0

0.00/5 (No votes)
29 Feb 2008 1  
Create disk ISO images using IMAPI 2

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);//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); 

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 interfaces 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 interfaces.
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:

/// 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;
}

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!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here