“Zipper” A Zipping Component In .NET 3.5 Using C# 3.0

Recently I have posted a zipping plugin – “On the Fly Zip and Attach” – for Windows Live Writer. I actually developed this component that was consumed in the writer’s zipping plugin. I thought to share with you the approach of design/development of this component, from use cases to its implementation along with unit tests using Visual Studio-2008 Unit Test infrastructure.

The functionalities that we are looking for are pretty straight forward :-

  • The component shall Zip files and Directories
  • The component shall provides functionality to Unzip the files in some user specified directory.

These functionalities can be broken down into use cases, that leads/binds to the interface “IZipper”, shown in the diagram below:-

ZipperPrimaryUseCases

The outcome of the use cases is the interface for our Zipper component, here is the code for it in C# :-

    public interface IZipper
    {
        // Zipping use cases
        bool Zip();
        bool ZipAs(string zipFilePath);
        bool ZipAs(string zipFilePath, bool useRelativePath);
        void AddFile(string absoluteFilePath);
        void AddFiles(string[] files);
        void AddFolder(string absoluteFolderPath);
        bool Clear();

        // Unzipping use cases
        bool Unzip(string destFilesPath);
        string[] UnzipVirtual();
    }

In order to realize the interface/use cases, I planned to use .NET 3.5 frameworks “{System.IO.Packaging.ZipPackage}”. This is part of the larger framework “System.IO.Packaging” that was primarily developed by Microsoft for its document system, XPS (the basis for Open XML documents), you can read about its specification from Microsoft, here is the link “XML Paper Specification”. The assembly you need to reference for it is “ReachFramework.dll”.

Here is the static design or class diagram for it. “Zipper” class is the realization of “IZipper” interface. Zipper class uses a static “inner” class “ZipperHelper” that eventually uses the .NET Packaging System.

Zipper

Since the central role is of the class ZipperHelper, here is how it looks like -

/// <summary>
        /// Zipper helper, a generic zipping api/utility class
        /// </summary>
        public static class ZipperHelper
        {
            /// <summary>
            /// uses the external container to zip the file...
            /// </summary>
            /// <param name="zipFilePath"></param>
            /// <param name="filesPathList"></param>
            /// <param name="useRelativePath"></param>
            /// <returns></returns>
            public static bool ZipAs(string zipFilePath, List<string> filesPathList, bool useRelativePath)
            {
                bool isZippedOk = false;
                if (filesPathList.Count > 0)
                {
                    FileInfo fileInfo = new FileInfo(zipFilePath);

                    // <bad file check>, delete it, if it contains zero bytes...
                    if (fileInfo.Exists && (fileInfo.Length == 0))
                    {
                        fileInfo.Delete();
                    }

                    using (Package package = Package.Open(zipFilePath, FileMode.OpenOrCreate))
                    {
                        foreach (string filePath in filesPathList)
                        {
                            string fileCurrentPath = ZipperHelper.MakePathRelative(filePath, useRelativePath);

                            Uri uri = new Uri(fileCurrentPath, UriKind.Relative);
                            uri = PackUriHelper.CreatePartUri(uri);
                            if (package.PartExists(uri))
                            {
                                package.DeletePart(uri);
                            }

                            PackagePart part = package.CreatePart(uri, string.Empty, CompressionOption.Maximum);
                            using (Stream streamIn = new FileStream(filePath, FileMode.Open, FileAccess.Read))
                            using (Stream streamOut = part.GetStream())
                            {
                                Core.CopyStream(streamIn, streamOut);
                            }
                        }
                        isZippedOk = true;
                    }
                }
                return isZippedOk;
            }

            /// <summary>
            /// uses external zip file path and extracts files to the provided destination files path 
            /// </summary>
            /// <param name="zipFilePath"></param>
            /// <param name="destFilesPath"></param>
            /// <returns></returns>
            public static bool UnzipAs(string zipFilePath, string destFilesPath)
            {
                bool isUnZipOk = false;

                FileInfo fileInfo = new FileInfo(zipFilePath);
                if (fileInfo.Exists && (fileInfo.Length > 0))
                {
                    using (Package package = Package.Open(zipFilePath, FileMode.Open))
                    {
                        if (package.GetParts().Count() > 0)
                        {
                            foreach (PackagePart part in package.GetParts())
                            {
                                // combine the paths...
                                string filePath = Path.Combine(destFilesPath, ZipperHelper.GetNormalizedPath(part.Uri.OriginalString));

                                // if directory doesnt exist create it...
                                if (!Directory.Exists(Path.GetDirectoryName(filePath)))
                                {
                                    Directory.CreateDirectory(Path.GetDirectoryName(filePath));
                                }

                                using (Stream streamOut = new FileStream(filePath, FileMode.Create))
                                using (Stream streamIn = part.GetStream())
                                {
                                    Core.CopyStream(streamIn, streamOut);
                                }
                            }
                            isUnZipOk = true;
                        }
                    }
                }
                return isUnZipOk;
            }

            /// <summary>
            /// Get the internal list of a zip file.
            /// </summary>
            /// <param name="zipFilePath"></param>
            /// <returns>string[]</returns>
            public static string[] UnzipVirtual(string zipFilePath)
            {
                List<string> filesList = new List<string>();
                FileInfo fileInfo = new FileInfo(zipFilePath);
                if (fileInfo.Exists && (fileInfo.Length > 0))
                {
                    using (Package package = Package.Open(zipFilePath, FileMode.Open))
                    {
                        if (package.GetParts().Count() > 0)
                        {
                            foreach (PackagePart part in package.GetParts())
                            {
                                filesList.Add(ZipperHelper.GetProperPath(ZipperHelper.GetNormalizedPath(part.Uri.OriginalString)));
                            }
                        }
                    }
                }
                return filesList.ToArray();
            }

            private static string MakePathRelative(string filePath)
            {
                return MakePathRelative(filePath, true);
            }

            private static string MakePathRelative(string filePath, bool useRelativePath)
            {
                string fileCurrentPath = filePath;
                if (filePath.Contains(":"))
                {
                    if (useRelativePath)
                    {
                        // simple approach, the absolute path has a colon ":", so split it and get the next one... 
                        fileCurrentPath = filePath.Split(new char[] { ':' })[1];
                    }
                    else
                    {
                        fileCurrentPath = filePath.Replace(":", "_");
                    }
                }
                return fileCurrentPath;
            }

            // c_\windows\explorer.exe --> c:\windows\explorer.exe
            private static string GetProperPath(string filePath)
            {
                // work on a copy
                string fileCurrentPath = filePath;
                // tokenize the paths
                string[] filePaths = filePath.Split(new char[] { '\\' });
                // replace the first one that contains "_" with ":"
                filePaths[0] = filePaths[0].Replace("_", ":");
                // remove the first path from the original
                fileCurrentPath = fileCurrentPath.Remove(0, filePaths[0].Length);
                // now add the replaced token
                fileCurrentPath = filePaths[0] + fileCurrentPath;

                return fileCurrentPath.Replace("%20", " ").Replace("%5B", "[").Replace("%5D", "]");
            }

            private static string GetNormalizedPath(string filePath)
            {
                string path = filePath.Replace(@"/", @"\");
                if (path.StartsWith(@"\"))
                {
                    path = path.Remove(0, 1);
                }
                return path;
            }           
        }

There are three public methods, ZipAs(…), UnzipAs(…) and UnzipVirtual(…). If you look into the code, you have  noticed that we don't see any where the “System.IO.Packaging.ZipPackage”. Its actually created when you call the static method Open of the abstract Package class, i.e “Package.Open(zipFilePath, FileMode.Open))” Rest of the functionality is straight forward, Here is the sequence

  • Create the Package.
  • Package will be composed of Parts and those Parts represents the corresponding files.
  • While Zipping you Serialize them to File using the Core Utility Class’s CopyStream method.
  • In Case of Unzipping things will become reversed and You get the Parts using the package.GetParts(), method and then Serialize those files to output streams.

For its usage, I have included the Unit Tests, and here is the  class model for it.

ZipperTestProject

The Unit tests conforms to the IZipper interface/Use cases and the concrete implementation i.e Zipper object is created in the Factory method CreateIZipper(). Here is the usage code for the Unit Tests:-

/// <summary>
    ///This is a test class for IZipperTest and is intended
    ///to contain all IZipperTest Unit Tests
    ///</summary>
    [TestClass()]
    public class IZipperTest
    {
        private const string ZipFilePath = @"c:\shams\zipper\test.zip";
        private TestContext testContextInstance;

        /// <summary>
        ///Gets or sets the test context which provides
        ///information about and functionality for the current test run.
        ///</summary>
        public TestContext TestContext
        {
            get
            {
                return testContextInstance;
            }
            set
            {
                testContextInstance = value;
            }
        }

        #region Additional test attributes
        #endregion

        /// <summary>
        /// Factory method.
        /// </summary>
        /// <returns></returns>
        internal virtual IZipper CreateIZipper()
        {
            // TODO: Instantiate an appropriate concrete class.
            IZipper target = new Shams.IO.ZippingPackage.Utils.Zipper(IZipperTest.ZipFilePath);
            return target;
        }

        /// <summary>
        ///A test for Zip
        ///</summary>
        [TestMethod()]
        public void ZipTest()
        {
            IZipper zipper = CreateIZipper();       // Initialize to an appropriate value
            zipper.AddFile(@"c:\windows\notepad.exe");
            zipper.AddFile(@"c:\windows\regedit.exe");
            zipper.AddFile(@"c:\windows\explorer.exe");
            zipper.AddFile(@"c:\windows\system32\winlogon.exe");

            bool expected = true;       // Initialize to an appropriate value
            bool actual = zipper.Zip();

            Assert.AreEqual(expected, actual);
            //Assert.Inconclusive("Verify the correctness of this test method.");
        }

        /// <summary>
        ///A test for ZipAs
        ///</summary>
        [TestMethod()]
        public void ZipAsTest()
        {
            IZipper zipper = CreateIZipper(); // TODO: Initialize to an appropriate value
            zipper.AddFile(@"c:\windows\notepad.exe");
            zipper.AddFile(@"c:\windows\regedit.exe");
            zipper.AddFile(@"c:\windows\explorer.exe");
            zipper.AddFile(@"c:\windows\system32\winlogon.exe");

            string zipFilePath = @"c:\shams\zipper\temp.zip";

            bool expected = true;
            bool actual = zipper.ZipAs(zipFilePath);

            Assert.AreEqual(expected, actual);
        }

        /// <summary>
        ///A test for ZipAs
        ///</summary>
        [TestMethod()]
        public void ZipAsTest1()
        {
            IZipper zipper = CreateIZipper(); // TODO: Initialize to an appropriate value
            zipper.AddFile(@"c:\windows\notepad.exe");
            zipper.AddFile(@"c:\windows\regedit.exe");
            zipper.AddFile(@"c:\windows\explorer.exe");
            zipper.AddFile(@"c:\windows\system32\winlogon.exe");

            string zipFilePath = @"c:\shams\zipper\temp.zip";

            // by default it is false, lets make it relative here.
            bool useRelativePath = true;

            bool expected = true;
            bool actual = zipper.ZipAs(zipFilePath, useRelativePath);
            Assert.AreEqual(expected, actual);
        }

        /// <summary>
        ///A test for UnZipVirtual
        ///</summary>
        [TestMethod()]
        public void UnzipVirtualTest()
        {
            IZipper zipper = CreateIZipper(); // TODO: Initialize to an appropriate value
            string[] expected = new string[] {@"c:\windows\explorer.exe",
                @"c:\windows\notepad.exe", 
                @"c:\windows\regedit.exe", 
                @"c:\windows\system32\winlogon.exe"};

            zipper.AddFiles(expected);
           
            string[] actual = zipper.UnzipVirtual();
            Assert.AreEqual(expected, actual);
            
            //Assert.Inconclusive("Verify the correctness of this test method.");
        }

         /// <summary>
        ///A test for UnZip
        ///</summary>
        [TestMethod()]
        public void UnzipTest()
        {
            IZipper zipper = CreateIZipper();
            string destFilesPath = @"c:\shams\zipper\temp1";
            bool expected = true;
            bool actual = zipper.Unzip(destFilesPath);
            Assert.AreEqual(expected, actual);
            //Assert.Inconclusive("Verify the correctness of this test method.");
        }

        /// <summary>
        ///A test for Clear
        ///</summary>
        [TestMethod()]
        public void ClearTest()
        {
            IZipper zipper = CreateIZipper(); // TODO: Initialize to an appropriate value
            string[] files = new string[] {@"c:\windows\notepad.exe", 
                @"c:\windows\regedit.exe", 
                @"c:\windows\explorer.exe", 
                @"c:\windows\system32\winlogon.exe"};

            zipper.AddFiles(files);
            bool expected = true; 
            bool actual = zipper.Clear();
            Assert.AreEqual(expected, actual);
            //Assert.Inconclusive("Verify the correctness of this test method.");
        }

        /// <summary>
        ///A test for AddFolder
        ///</summary>
        [TestMethod()]
        public void AddFolderTest()
        {
            IZipper zipper = CreateIZipper();
            string absoluteFolderPath = @"c:\shams\zipper";
            zipper.AddFolder(absoluteFolderPath);

            Assert.Inconclusive("A method that does not return a value cannot be verified.");
        }

        /// <summary>
        ///A test for AddFiles
        ///</summary>
        [TestMethod()]
        public void AddFilesTest()
        {
            IZipper zipper = CreateIZipper(); // TODO: Initialize to an appropriate value
            string[] files = new string[] {@"c:\windows\notepad.exe", 
                @"c:\windows\regedit.exe", 
                @"c:\windows\explorer.exe", 
                @"c:\windows\system32\winlogon.exe"};

            zipper.AddFiles(files);
            Assert.Inconclusive("A method that does not return a value cannot be verified.");
        }

        /// <summary>
        ///A test for AddFile
        ///</summary>
        [TestMethod()]
        public void AddFileTest()
        {
            IZipper zipper = CreateIZipper();
            string absoluteFilePath = @"c:\windows\explorer.exe";
            zipper.AddFile(absoluteFilePath);

            Assert.Inconclusive("A method that does not return a value cannot be verified.");
        }
    }

That's all for now, the source code and unit tests are all included in the attached project. Enjoy :)

 

Download File - Zipping-Package

 

If you enjoyed reading this blog, leave your valuable feedback and consider subscribing to the RSS feed. You can also subscribe to it by email. Also, you can follow me on Twitter. Thank you!
Technorati Tags: ,,,
Comments are closed