Click here to Skip to main content
15,867,453 members
Articles / Programming Languages / C#

Automatic Zip Extractor

Rate me:
Please Sign up or sign in to vote.
4.87/5 (17 votes)
10 Oct 2013CPOL4 min read 47.6K   2.1K   44   15
Automatically unblocks and extracts Zip files in a monitored folder.

Introduction

Do you get annoyed when you have to do so many things just to open up a compressed file you just downloaded? You have to (Vista+):

  1. Right-click
  2. Click Properties
  3. Click Unblock
  4. Click OK 
  5. Right-click
  6. Extract all 
  7. Extract
  8. Delete the .zip file

It's like so many other things in Windows, too much clicking and too many steps! Then, on XP machines, there's the problem that if the the Zip file has a ton of files inside which aren't inside of a sub-folder, you get a hundred files extracted to the same directory as the Zip file!

AutoExtractor unobtrusively sits in your system tray and monitors a directory you choose. It automatically pops up when new Zip files are available and all you have to do is select the file(s) you want to extract and hit Enter, and it unblocks the Zip file, extracts it to a directory with the same name as the Zip file, deletes the Zip file, opens the extracted folder, and hides itself back in the system tray again.

If you don't want it popping in your face every time a Zip file is added to the folder, you can turn that option off in the settings and use a keystroke command Ctrl+K (or whatever key you choose) to pop up the window instead. It uses the Windows APIs to ensure the window always pops up on top of other windows so you should be able to hit Ctrl+K+Enter and you get auto extraction!

AutoExtractor/screenshot.jpg

Note: You must have the Microsoft .NET 4.0 framework installed to allow for keyboard shortcuts to work correctly. You could probably retarget for 2.0 if you remove the keyboard hooks. The solution is VS 2010.

The Code

The application uses a FileSystemWatcher to monitor the chosen directory. The NotifyFilters are just right so that it gets notified when a new Zip file is downloaded, copied, or renamed, and it works with IE and Firefox as well as network copy's. Firefox and IE handle file downloads differently so I needed to handle the Changed event instead of the Created event, and in the Changed event, check to see if the file exists. For some reason, with IE, the Created event is fired, then the Deleted, then the Changed again, so it must be renaming the file in a way that comes across as a delete. Anyhow, the Changed event coupled with checking whether the file exists seems to catch everything.

C#
void fileWatcher_Changed(object sender, FileSystemEventArgs e)
{
    Trace.WriteLine(string.Format("File '{0}' changed. Change type: {1}.", 
                    e.FullPath, e.ChangeType));
    FileInfo file = new FileInfo(e.FullPath);
    if (file.Exists)
        OnFileCreated(file);
}

Once a new file is detected, it is added to the ListView on the form and the window is popped up (if the setting to do so is turned on). When the user chooses the files to extract and clicks Extract (or hits Enter), the chosen files are extracted simultaneously, each on their own thread. AutoExtractor maintains a List of ManualResetEvents, one for each thread, and in another "Monitoring" thread, it calls WaitHandle.WaitAll so it knows when all extraction threads have finished:

C#
private void ExtractFiles(FileInfo[] files)
{
    progressBar.Maximum = 0;
    ShowExtracting();
    _isExtracting = true;
    Trace.WriteLine(string.Format("Extracting {0} files...", files.Length));

    // update extract progress twice per second
    var progressUpdateTimer = new System.Timers.Timer(500);
    _filesExtracted = 0;
    progressUpdateTimer.Elapsed += (object sender, ElapsedEventArgs e) => 
      Dispatcher.BeginInvoke(new Action(() => progressBar.Value = _filesExtracted));
    progressUpdateTimer.Start();

    for (int i = 0; i < files.Length; i++)
    {
        FileInfo file = files[i];
        try
        {
            var resetEvent = new ManualResetEvent(false);
            threadSync.Add(resetEvent);
            var extractThread = new System.Threading.Thread(
                new ThreadStart(() => ExtractFile(file, resetEvent)));
            extractThread.SetApartmentState(System.Threading.ApartmentState.STA);
            extractThread.IsBackground = true;
            extractThread.Start();
        }
        catch (Exception ex)
        {
            if (_isExtracting)
                _cancelExtract = true;
            progressUpdateTimer.Stop();
            threadSync.Clear();
            HideExtracting();
            MessageBox.Show("Error extracting file:" + 
                       Environment.NewLine + ex.Message);
        }
    }

    Thread allFinishedThread = new Thread(new ThreadStart(() =>
    {
        Trace.WriteLine(string.Format("{0} thread(s) started. " + 
              "Waiting for all files to be extracted...", threadSync.Count));
        WaitHandle.WaitAll(threadSync.ToArray());
        Trace.WriteLine("All threads finished.");
        progressUpdateTimer.Stop();
        threadSync.Clear();
        _isExtracting = false;
        _cancelExtract = false;
        Dispatcher.Invoke(new Action(() => { HideExtracting(); MinimizeToTray(); }));
    }));
    allFinishedThread.SetApartmentState(ApartmentState.MTA);
    allFinishedThread.IsBackground = true;
    allFinishedThread.Start();
}

The ExtractFile method is the function called on each thread for each file and is doing all the heavy lifting. It uses DotNetZip to extract the files and after each entry in the zip file is extracted, it updates a counter back on the main Window. Before any of the extraction starts, a timer is started which elapses every 500 milliseconds and updates the ProgressBar with the current value of the counter. I did it this way because of a bug I found with Dispatcher.BeginInvoke and Invoke. If I called Dispatcher.BeginInvoke for each zip entry to update the ProgressBar, it would Blue Screen Windows! I guess Dispatcher.BeginInvoke can't handle rapid requests like that. Anyways, each entry in the Zip file is extracted and I call the ManualResetEvent's Set() method to tell it that the thread is finished.

C#
private void ExtractFile(FileInfo file, ManualResetEvent threadFinishedEvent)
{
    // This method should be run on a separate thread.
    Trace.WriteLine(string.Format("Thread ID {0} has started to extract '{1}'.", 
                    Thread.CurrentThread.ManagedThreadId, file.FullName));

    // Create the output directory
    string extractDirectoryPath = file.FullName.Remove(
      file.FullName.LastIndexOf(".zip", 
      StringComparison.InvariantCultureIgnoreCase), 4);
    DirectoryInfo extractDirectory = null;
    try
    {
        extractDirectory = new DirectoryInfo(extractDirectoryPath);
        if (false == extractDirectory.Exists)
        {
            Trace.WriteLine(string.Format(
              "Creating directory: {0}", extractDirectory.FullName));
            extractDirectory.Create();
        }
    }
    catch (Exception ex)
    {
        if (_isExtracting)
            _cancelExtract = true; // signals to the other threads to stop!

        MessageBox.Show("Error creating directory: " + 
          extractDirectoryPath + Environment.NewLine + ex.Message);
        return;
    }

    bool retry = true;
    while (retry)
    {
        try
        {
            using (var zippedFile = Ionic.Zip.ZipFile.Read(file.FullName))
            {
                if (zippedFile.Entries.Count > 0)
                {
                    Dispatcher.Invoke(new Action(() => 
                          progressBar.Maximum += zippedFile.Entries.Count));
                    Trace.WriteLine(string.Format(
                      "Thread {0} extracting file '{1}' to directory '{2}'...", 
                      Thread.CurrentThread.ManagedThreadId, file.FullName, 
                      extractDirectory.FullName));
                    foreach (var entry in zippedFile.Entries)
                    {
                        if (_cancelExtract)
                        {
                            Trace.WriteLine(string.Format(
                              "Thread {0} detected a cancel request and will " + 
                              "stop extracting {1}.", 
                              Thread.CurrentThread.ManagedThreadId, file.FullName));
                            threadFinishedEvent.Set();
                            return;
                        }

                        Trace.WriteLine(string.Format("    Thread {0} " + 
                          "extracting from {1,-50} to {2}...", 
                          Thread.CurrentThread.ManagedThreadId, 
                          file.Name, entry.FileName));
                        entry.Extract(extractDirectory.FullName, 
                          Ionic.Zip.ExtractExistingFileAction.OverwriteSilently);

                        _filesExtracted++;
                    }

                    Trace.WriteLine(string.Format("Thread {0} finished " + 
                      "extracting '{1}'.", 
                      Thread.CurrentThread.ManagedThreadId, file.FullName));
                    System.Diagnostics.Process.Start(extractDirectory.FullName);
                    threadFinishedEvent.Set();
                }
            }

            if (Properties.Settings.Default.DeleteAfterExtraction)
                file.Delete();

            retry = false;
        }
        catch (IOException ioEx)
        {
            var userResponse = MessageBox.Show(string.Format(
                "Error extracting file '{0}'.{1}Would you like to retry?", 
                file.FullName, Environment.NewLine + ioEx.Message + 
                Environment.NewLine + Environment.NewLine),
                "File access error.", MessageBoxButton.YesNo);
            if (userResponse == MessageBoxResult.Yes)
            {
                retry = true;
            }
            else
            {
                retry = false;

                if (_isExtracting)
                    _cancelExtract = true;
                    // signals to the other threads to stop!

                threadFinishedEvent.Set();
                return;
            }
        }
        catch (Exception ex)
        {
            if (_isExtracting)
                _cancelExtract = true; // signals to the other threads to stop!

            MessageBox.Show(string.Format("Error extracting file '{0}'{1}", 
              file.FullName, Environment.NewLine + ex.ToString()));
            threadFinishedEvent.Set();
            return;
        }
    }
}

The final interesting piece of the program is the KeyboardHook class. It was taken from here: http://learnwpf.com/post/2011/08/03/Adding-a-system-wide-keyboard-hook-to-your-WPF-Application.aspx.

I tweaked it just a bit for this program but it's pretty close to what the author posted. Apparently, you can only hook into keyboard modifiers with .NET 4.0. The author also shows how to define your own program entry point rather than the abstracted one WPF uses by default.

Points of Interest

As I mentioned, it was a real battle tracking down the reason why I was getting blue screens, I tried both DotNetZip and J#'s zip extraction methods, and both had the same result. Finally, I found that if I turned off updating my ProgressBar, there were no blue screens! The Dispatcher.BeginInvoke (or Invoke) was causing the problem. I think it's an MS bug!

The KeyboardHook class is really cool and could be used in all sorts of situations where you need your program running in the background but need to react on keyboard commands.

History

  • 08/26/2011 - Initial release.
  • 09/09/2011 - Bug fix in new folder name, thanks akemper! Other minor bug fixes.
  • 09/19/2011 - Cleaned up the solution, removed old files and code, some refactoring. Fixed a bug where the FileSystemWatcher would fire multiple Changed events which would confuse the OnFileCreated function.
  • 10/10/2013 - Been using this for years now, still love it! I uploaded new source including the 3rd party zip DLL. Also added a Bin downloadable so you can download it and try it out without compiling first.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer Veracity
United States United States
I'm a .NET developer, fluent in C# and VB.NET with a focus on SharePoint and experience in WinForms, WPF, Silverlight, ASP.NET, SQL Server. My roots come from a support/system administrator role so I know my way around a server room as well.

I have a passion for technology and I love what I do.

Comments and Discussions

 
QuestionSetup.exe Pin
ajhvdb25-Dec-13 4:26
ajhvdb25-Dec-13 4:26 
GeneralMy vote of 5 Pin
mbcrump9-Sep-11 8:01
mentormbcrump9-Sep-11 8:01 
QuestionMy vote of 5 Pin
Filip D'haene9-Sep-11 6:30
Filip D'haene9-Sep-11 6:30 
GeneralMy vote of 5 Pin
Stranger_than_Fiction6-Sep-11 5:34
Stranger_than_Fiction6-Sep-11 5:34 
This could be useful for automating custom installations.
GeneralRe: My vote of 5 Pin
jabit9-Sep-11 6:21
jabit9-Sep-11 6:21 
QuestionTarget Folder Pin
akemper5-Sep-11 21:40
akemper5-Sep-11 21:40 
AnswerRe: Target Folder [modified] Pin
jabit9-Sep-11 6:13
jabit9-Sep-11 6:13 
GeneralRe: Target Folder Pin
akemper9-Sep-11 6:47
akemper9-Sep-11 6:47 
QuestionIonic Zip Pin
ClarenceJr5-Sep-11 17:42
ClarenceJr5-Sep-11 17:42 
AnswerRe: Ionic Zip Pin
jabit9-Sep-11 6:42
jabit9-Sep-11 6:42 
QuestionCan't download ... Pin
akemper5-Sep-11 9:09
akemper5-Sep-11 9:09 
AnswerRe: Can't download ... Pin
jabit5-Sep-11 16:02
jabit5-Sep-11 16:02 
GeneralRe: Can't download ... Pin
akemper5-Sep-11 21:34
akemper5-Sep-11 21:34 
QuestionLambda Expressions Pin
ClarenceJr5-Sep-11 5:53
ClarenceJr5-Sep-11 5:53 
AnswerRe: Lambda Expressions Pin
jabit5-Sep-11 15:57
jabit5-Sep-11 15:57 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.