Automatic Zip Extractor
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+):
- Right-click
- Click Properties
- Click Unblock
- Click OK
- Right-click
- Extract all
- Extract
- 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!
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 NotifyFilter
s 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.
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 ManualResetEvent
s, one for each thread, and in another "Monitoring" thread, it calls WaitHandle.WaitAll
so it knows when all extraction threads have finished:
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.
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 theOnFileCreated
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.