Creating A File Zipper/unzipper

Summary
I'll create a file(s) zipper/unzipper in this tutorial. You suggestions are welcome to make the idea (or this application) better.




Background
The idea came from a post I answered few days back. The post is here -- http://forum.codecal...ges-to-streams/. The problem was that, the poster wanted to write images as well as other things like text, number etc in a binary stream. He also wanted to get the data back as separate items he wrote. So the problem is a general one -- writing multiple files in a stream and retrieving the files from that stream. The BinraryWriter & BinaryReader classes are there for the job. In BinaryWriter class there is Write method with various overloaded version which takes bool, String, numbers etc as the parameter and we can get back the same data with ReadBoolean, ReadInt, ReadString, ReadDouble methods of BindaryReader class. Perhaps you noticed that there is a single method - Write - for writing everything but multiple methods for reading. Yeah, this is because we can pass multiple arguments in a method but a method can only returns one. There is an overloaded of Write method which takes byte[] type parameter. So while reading you can use ReadBytes method of BinaryReader but you need to specify/know how many bytes you want to read. In case of ReadInt, ReadDouble, ReadString the BinaryReader knows the length of bytes need to read from the stream, but you need to specify the amount of bytes you want to read while using ReadBytes method. So the solution I gave him was simple -- write the number of bytes you are writing before writing the bytes itself. I follow the same protocol/logic while I send/receive data in case of networking applications. I guess the BinaryWriter does the same thing while we call Write method with various parameters (except the byte[]) -- that is while we call Write with int type, it first write 4 (size for int) in a byte onto the stream followed by writing the actual 4 bytes data we passed. Same thing for other types as well as String -- except the byte[]. I think that is possible to do the same thing in case of byte[] but I don't why they didn't.

However, after answering on the post, I decided to write a files merger/demerger. After finishing the app (just merge files into a single one and in reverse, demerge the file into the files it was merge from), I decided to go a bit farther. I decided to make a zipper/unzipper application using some opensource tools which will do the compressing/decompressing of data. And thus, this tutorial got its way to find out a place in CodeCall (as well as a demo app). However, there is still a thing left to do and I'll tell about it when that point will come up.


The Protocol
The protocol I'll follow while writing and reading to/from the singled merged file is as follows...
<a bool value which will indicates whether the current item is a file or folder using Write(bool)/ReadBoolean()> < a string for the file/folder name using Write(String)/ReadString()> <4 bytes for the length of file content using Write(int)/ReadInt32()> <The files content>
If the item is a folder, we will not write the last two parts.


The Merging/Demerging of Files
The code that will merge the files into a single one is as follows. While merging, it will also update on GUI (in a grid view)
/// <summary>
/// This method will merge a file into an already opened stream
/// </summary>
/// <param name="writer">The binary-writter that will write into the stream</param>
/// <param name="filename">The file to be write</param>
public static void OpenAndMergeFile(BinaryWriter writer, String filename)
{
using (FileStream fileStream = File.OpenRead(filename)) {
int lenght = (int)fileStream.Length;
byte[] data = new byte[lenght];
fileStream.Read(data, 0, data.Length);
// Later we will use some code to compress the data.
byte[] compressedData = data;
writer.Write(compressedData.Length);
writer.Write(compressedData);
fileStream.Close();
}
}


/// <summary>
/// This method will write all files in the dictionary into a single file.
/// The protocol that we will follow to merge the files as follows...
/// <a bool value which will indicates whether the current item is a file or
/// folder using Write(bool)/ReadBoolean()> < a string for the file/folder name using
/// Write(String)/ReadString()> <4 bytes for the length of file content using
/// Write(int)/ReadInt32()> <The files content>
/// </summary>
/// <param name="outputFile">The merged output file</param>
/// <param name="basePath">The base path of all the files/folder</param>
/// <param name="rows">all the files in each row of datagrid view</param>
public void MergeFiles(String outputFile, String basePath, Dictionary<DataGridViewRow, String> rows)
{
if (rows.Count > 0) {
using (FileStream fileStream = File.Open(outputFile, FileMode.Create)) {
BinaryWriter writer = new Bina
ryWriter(fileStream);
writer.Write(rows.Count);
foreach (KeyValuePair<DataGridViewRow, String> row in rows) {
String file = row.Value;
if (File.Exists(file) || Directory.Exists(file)) {
row.Key.Cells[1].Value = "Archiving...";
_dgvFiles.Refresh();
bool isDir = (File.GetAttributes(file) & FileAttributes.Directory) == FileAttributes.Directory;
writer.Write(isDir);
String relativePath = file.Replace(basePath, "");
writer.Write(relativePath);
if (!isDir) {
OpenAndMergeFile(writer, file);
}
row.Key.Cells[1].Value = "Done!";
_dgvFiles.Refresh();
}
else {
row.Key.Cells[1].Value = "File does not exist!";
_dgvFiles.Refresh();
}
}
fileStream.Close();
}
}
}

The code that will demerge the files from a single one is as follows. While demerging, it will also update on GUI (in a grid view)...
/// <summary>
/// This method will extract content of a file from a merged stream.
/// </summary>
/// <param name="reader">The binary-reader that will read from the stream</param>
/// <param name="filename">the name of the file to be read save to.</param>
public void ExtractAndSaveFile(BinaryReader reader, string filename)
{
using (FileStream fileStream = File.Open(filename, FileMode.Create)) {
int length = reader.ReadInt32();
byte[] data = reader.ReadBytes(length);
// Later will use some compression library to decompress the data.
byte[] decompressedData = data;
fileStream.Write(decompressedData, 0, decompressedData.Length);
fileStream.Close();
}
}


/// <summary>
/// The method will extract data from a merged file and save the files into a given path.
/// </summary>
/// <param name="outputDir">The path where the extracted file will be saved.</param>
/// <param name="achriveFile">The archived file itself from we will demerge other files.</param>
public void ExtractFiles(String outputDir, String achriveFile)
{
_dgvFiles.Rows.Clear();
if (File.Exists(achriveFile)) {
using (FileStream fileStream = File.OpenRead(achriveFile)) {
BinaryReader reader = new BinaryReader(fileStream);
int count = reader.ReadInt32();
for (int i = 0; i < count; i++) {
bool isDir = reader.ReadBoolean();
String filename = outputDir + reader.ReadString();
_dgvFiles.Rows.Add(new String[] {filename, "Extracting..." });
_dgvFiles.Refresh();
if (isDir) {
Directory.CreateDirectory(filename);
}
else {
ExtractAndSaveFile(reader, filename);
}
_dgvFiles.Rows[_dgvFiles.Rows.Count - 2].Cells[1].Value = "Done";
_dgvFiles.Refresh();
}
fileStream.Close();
}
}
}


The Compression/Decompression of Content of File
Though the first idea was to just create a merger/demerger, later I decided to use some code to add the compression/decompression part. I used the 7zip LZMA SDK to perform this part. It is free and opensource. Please read more about it here -- http://www.7-zip.org/sdk.html. I was looking for method which will take some bytes and returns some bytes after compression/decompression. I made something like it (with the help of Google) using the default parameters for the corresponding methods from the SDK. I found something like following...
// For compressing, I used the following code inside method 'OpenAndMergeFile'.
byte[] compressedData = SevenZip.Compression.LZMA.LZMAHelper.Compress(data);

// For decompressing, I used the following code inside method 'ExtractAndSaveFile'.
byte[] decompressedData = SevenZip.Compression.LZMA.LZMAHelper.Decompress(data);


The File Association with FileSystem
Though I desired to do everything from file system like WinRAR but I'm not. Well, there are two parts ---
  • I need to associate an extention (.kcl) to this app. I'm able to do that. It just adding the extension under HKEY_CLASSES_ROOT registry (with DefaultIcon and command keys). So when a file with kcl extension will get double clicked (or open by right clicking), this app will get called (see the command key's value) with the file as argument.
  • Adding a context menu for every types of file and folder. Well, its easy, you just need to add an entry for '*', 'Directory' & 'Folder' keys of HKEY_CLASSES_ROOT.
/// <summary>
/// It will register for a file extention into the registry.
/// </summary>
/// <param name="appPath">The full path of the application for which the extension will get bounded.</param>
/// <param name="command">The command that will be default and shown when user right click on a file with the given extension</param>
/// <param name="ext">The extention itself</param>
public static void RegisterExtention(String appPath, String command, String ext)
{
try {
RegistryKey key = Registry.ClassesRoot.CreateSubKey("." + ext + @"shell" + command + @"Command",
RegistryKeyPermissionCheck.ReadWriteSubTree);
key.SetValue("", String.Format(""{0}"", appPath) + " -extract "%1"", RegistryValueKind.String);

key = Registry.ClassesRoot.CreateSubKey("." + ext + @"DefaultIcon", RegistryKeyPermissionCheck.ReadWriteSubTree);
key.SetValue("", String.Format("{0},0", appPath), RegistryValueKind.String);
}
catch (Exception) { }
}


/// <summary>
/// This method will add an entry in the registry for an extension.
/// </summary>
/// <param name="appPath"></param>
/// <param name="menu">The command that will be shown when user right click on a file with the given extension</param>
///
<param name="ext">The extention itself</param>
public static void AddShellContextMenuForExtention(String appPath, String menu, String ext)
{
try {
RegistryKey key = Registry.ClassesRoot.OpenSubKey(ext, true);
if (key != null) {
key = key.CreateSubKey(String.Format(@"shell{0}command", menu) , RegistryKeyPermissionCheck.ReadWriteSubTree);
key.SetValue("", String.Format(""{0}"", appPath) + " -archive "%1"");
}
}
catch (Exception) { }
}

I used the above two methods to make the association (as well as deleting the association) of the extensions (kcl, *, Folder, Directory) with our application. The 'Install' method will get called if we pass '-i' switch to the app. The 'Uninstall' method will get called if we pass '-u' switch to the app.
static void Install()
{
WindowsRegistry.RegistryHanlder.RegisterExtention(Application.ExecutablePath, "Extract With KCL Unzipper" , "kcl");
WindowsRegistry.RegistryHanlder.AddShellContextMenuForExtention(Application.ExecutablePath, "Archive with KCL Zipper", "*");
WindowsRegistry.RegistryHanlder.AddShellContextMenuForExtention(Application.ExecutablePath, "Archive with KCL Zipper", "Directory");
WindowsRegistry.RegistryHanlder.AddShellContextMenuForExtention(Application.ExecutablePath, "Archive with KCL Zipper", "Folder");
}

static void Uninstall()
{
WindowsRegistry.RegistryHanlder.DeregistrerExtention("kcl");
WindowsRegistry.RegistryHanlder.RemoveShellContextMenuForExtention("Archive with KCL Zipper", "*");
WindowsRegistry.RegistryHanlder.RemoveShellContextMenuForExtention("Archive with KCL Zipper", "Directory");
WindowsRegistry.RegistryHanlder.RemoveShellContextMenuForExtention("Archive with KCL Zipper", "Folder");
}

Now, the problem I'm facing with the association is that if I click multiple files/folder and click on the "Archive with KCL Zipper" menu, it calls the application for each of the file and folder (I mean each instance of application is running for each file/folder). But I want it to get only one instance of the application. I tried to find out a solution to this -- but no hope so far. Actually, I'm looking for a registry tweaking way so that the explorer/shell will do that for me - not able to find such a way so far. Another way can be --- doing the trick from inside the application itself. I have to make the application singleinstance so when second or more instance will run, it will send file/folder name it got to the first application. I can do that but that will take a bit time. Besides I'm looking for some simple solution. Can you guys help on this?


Future Enhancements
  • Adding the necessary changes so that the compressed files generated by this app can be extracted by other applications like WinZIP and vice versa.
  • Making the application cross-platform with Qt/QML.

No comments:

Post a Comment