Thursday, May 16, 2013

Zip Files in .NET 3.5 - Create your own assembly

Many times we come across features that are around to implement and there are hardly any information available online. I tried to implement compress files to zip format in .NET 3.5 but hardly found any information. In 4.5 Microsoft provided new assembly System.IO.Compression.FileSystem.dll to create Zip file easily but it is not the case in 3.5.

At the same time there are many ways to compress files; hence decided to create separate assembly which can be referenced by any solution in .NET 3.5 or higher. There are some free zip libraries available but decided to go against that to have a control over the implementation. One main reason is frequently change in requirement, it will be easy for us to change the implementation without making any major change in calling solutions.

In this implementation, considering comtaility in .NET 3.5 implemented Zipping using System.IO.Packaging in WindowsBase.dll, external library DotNetZip and using Shell32.dll. DotNetZip is an free library available and used it here for the purpose of knowledge only. Actual implementation doesn't have the implementation of DotNetZip.

But, lets first understand Factory Method

Wiki says - The factory method pattern is an object-oriented creational design pattern to implement the concept of factories and deals with the problem of creating objects (products) without specifying the exact class of object that will be created. The essence of this pattern is to "Define an interface for creating an object, but let the classes that implement the interface decide which class to instantiate. The Factory method lets a class defer instantiation to subclasses."

Codeproject says - The Factory Method Design Pattern allows you to create objects without being bothered on what is the actual class being created. The benefit is that the client code (calling code) can just say "give me an object that can do XYZ" without knowing what is the actual class that can do "XYZ".

I opted to implement Factory Method using Abstract Class rather Interface because there are some commonality which I tried to implement in base class. So, lets first understand why and how I achieved it

Base class implementation has only those objects and methods which are common.
// FACTORY CLASS
public abstract class ArchiveObj
{
    internal String strPath;
    internal List<String> lError;
    internal List<String> lFiles;
    public String[] ErrorList
    {
        get
        {
            return lError.ToArray();
        }
    }
    public ArchiveObj()
    {
        lFiles = new List<string>();
        lError = new List<string>();
    }
    public Boolean AddFile(String strFile)
    {
        lFiles.Add(strFile);
        return true;
    }
    public abstract int SaveArchive();
}
// CREATOR CLASS
public abstract class ArchiveCreator
{
    internal String strPath;
    public abstract ArchiveObj GetArchieve();
}

First implementation using System.IO.Packaging
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO.Packaging;

namespace Archive
{
    class WinBaseZip : ArchiveObj 
    {
        private WinBaseZip(){}
        public WinBaseZip(String sPath)
        {
            strPath = sPath;
        }
        public override int SaveArchive()
        {
            foreach (String strFile in lFiles)
                zipFile(strFile);
            if (lError.Count > 0)
            {
                if (lError.Count < lFiles.Count)
                    return 0;
                else
                    return -1;
            }
           return 1;
        }
        private bool zipFile(String strFile)
        {
            PackagePart pkgPart = null;
            using (Package Zip = Package.Open(strPath, System.IO.FileMode.OpenOrCreate, System.IO.FileAccess.ReadWrite))
            {
                String strTemp = strFile.Replace(" ", "_");
                String zipURI = String.Concat("/", System.IO.Path.GetFileName(strTemp));
                Uri parturi = new Uri(zipURI, UriKind.Relative);
                try
                {
                    pkgPart = Zip.CreatePart(parturi, System.Net.Mime.MediaTypeNames.Application.Zip, CompressionOption.Normal);
                }
                catch(Exception ex)
                {
                    lError.Add(strFile + "; Error : " + ex.Message);
                    return false;
                }
                Byte[] bites = System.IO.File.ReadAllBytes(strFile);
                pkgPart.GetStream().Write(bites, 0, bites.Length);
                Zip.Close();
            }
            return true;
        }
    }
    public class WinBaseZipCreator : ArchiveCreator
    {
        private WinBaseZipCreator(){}
        public WinBaseZipCreator(String sPath)
        {
            strPath = sPath;
        }
        public override ArchiveObj GetArchieve()
        {
            return new WinBaseZip(strPath);
        }
    }
}


Second implementation using DotNetZip
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Ionic.Zip;

namespace Archive
{
    class IonicZip : ArchiveObj
    {
        private IonicZip(){}
        public IonicZip(String sPath)
        {
            strPath = sPath;
        }
        public override int SaveArchive()
        {
            ZipFile zip = new ZipFile();
            foreach (String strFile in lFiles)
                zip.AddFile(strFile);
            zip.Save(strPath);
            return 1;
        }
    }

    public class IonicZipCreator : ArchiveCreator
    {
        private IonicZipCreator() { }
        public IonicZipCreator(String sPath)
        {
            strPath = sPath;
        }
        public override ArchiveObj GetArchieve()
        {
            return new IonicZip(strPath);
        }
    }
}

Now, here is the STAR of all and simply out of box implementation and full credit goes to Justin.
His codeproject.com article is the reference from where I derived this implementation. This implementation implemented in two steps. 1st Step create empty Zip file and 2nd Step add files in the Zip file. To add reference choose COM tab of the "Add Reference" screen and add the component named "Microsoft Shell Controls And Automation".

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO.Compression;
using Shell32;

namespace Archive
{
    class ZipShell : ArchiveObj
    {
        private ZipShell(){}
        public ZipShell(String sPath)
        {
            strPath = sPath;
        }
        public override int SaveArchive()
        {
            CreateZip(strPath);
            foreach (String strFile in lFiles)
                ZipCopyFile(strFile);
            return 1;
        }
        public bool CreateZip(String strZipFile)
        {
            try
            {
                System.Text.ASCIIEncoding Encoder = new System.Text.ASCIIEncoding();
                byte[] baHeader = System.Text.Encoding.ASCII.GetBytes(("PK" + (char)5 + (char)6).PadRight(22, (char)0));
                System.IO.FileStream fs = System.IO.File.Create(strZipFile);
                fs.Write(baHeader, 0, baHeader.Length);
                fs.Flush();
                fs.Close();
                fs = null;
            }
            catch(Exception ex)
            {
            }
            return true;
        }
        private bool ZipCopyFile(String strFile)
        {
            Shell32.Shell Shell = new Shell32.Shell();
            int iCnt = Shell.NameSpace(strPath).Items().Count;
            Shell.NameSpace(strPath).CopyHere(strFile, 0); // Copy file in Zip
            if (Shell.NameSpace(strPath).Items().Count == (iCnt + 1))
            {
                System.Threading.Thread.Sleep(100);
            }
            return true;
        }
    }
    public class ZipShellCreator : ArchiveCreator
    {
        private ZipShellCreator() { }
        public ZipShellCreator(String sPath)
        {
            strPath = sPath;
        }
        public override ArchiveObj GetArchieve()
        {
            return new ZipShell(strPath);
        }
    }
}

Now, lets see how these features implemented in a sample console application and how easily these three can be interchanged. If you see just changing the Creator class constructor we can change the whole implementation.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Archive; // Added reference of new assembly

namespace TestConsole
{
    class Program
    {
        static void Main(string[] args)
        {
            ArchiveCreator ss = new WinBaseZipCreator(@"d:\testzip1.zip");
            ArchiveObj xx = ss.GetArchieve();
            xx.AddFile(@"d:\TEST1.txt");
            xx.AddFile(@"d:\TEST2.txt");
            xx.SaveArchive();

            ArchiveCreator sss = new IonicZipCreator(@"d:\testzip2.zip");
            ArchiveObj xxx = sss.GetArchieve();
            xxx.AddFile(@"d:\TEST1.txt");
            xxx.AddFile(@"d:\TEST2.txt");
            xxx.SaveArchive();

            ArchiveCreator ssss = new ZipShellCreator(@"d:\testzip3.zip");
            ArchiveObj xxxx = ssss.GetArchieve();
            xxxx.AddFile(@"d:\TEST1.txt");
            xxxx.AddFile(@"d:\TEST2.txt");
            xxxx.SaveArchive();
        }
    }
}

3 comments:

  1. This is an excellent implementation but here I see that you didnt coded for List lError, I am assuming that you left it to user to implement it.

    Jak

    ReplyDelete
    Replies
    1. Hi Jak,
      Thanks... yes i kept the error handling incomplete... let the user implement it as per their requirement.

      Delete
  2. Hi thanks for the article. Its working fine. But pls explain the code.

    ReplyDelete