Introduction
DeleteOld is a C# console application to delete 'old' files. The application uses a series of command-line switches to control the execution. I've taken ideas from similar apps, so certainly can't claim to have invented this concept!
I found that the (free) applications out there, that do this, either don't work fully or were a bit 'clunky' — so I decided to 'reinvent the wheel' and write my own implementation in C#.
Updates (Oct 2008)
This version now includes the following new functionality (mostly from comments on the article)
- Allow files to be deleted 'newer' than a certain age as well as 'older'
- Allow files to be deleted based on an absolute date (overriding the timeframe arguments)
- Allow the 'root' path to be preserved if 'remove empty folders' is selected and the path specified is empty.
- Fixed a bug in Arguments parsing regex as noted by dudik
- Changed output datetime format to 'full' rather than specific dd/mm/yyyy as noted by a.plus.01
Using the Code
The biggest challenge originally was command line parameters. I used a slightly modified version of Richard Lopes's Arguments class to achieve what I wanted with a variety of mandatory and optional parameters — some are name/value pairs and others are simple switches.
The rest is fairly straightforward (in C# terms) and should be relatively easy to follow. Once the input parameters have been validated, the main processing is a fairly simple recursive loop around finding and deleting files in a directory tree.
It's important to note that we need to set a consistent time for file time comparison purposes — so the following code gets a 'benchmark date' (based on the requested timeframe) before processing begins.
private void SetDeleteTimeBenchmark()
{
if (age != int.MaxValue)
{
if (timeFrame == AgeIncrement.Second)
deleteBaselineDate = runTime.AddSeconds(-age);
if (timeFrame == AgeIncrement.Minute)
deleteBaselineDate = runTime.AddMinutes(-age);
if (timeFrame == AgeIncrement.Hour)
deleteBaselineDate = runTime.AddHours(-age);
if (timeFrame == AgeIncrement.Day)
deleteBaselineDate = runTime.AddDays(-age);
if (timeFrame == AgeIncrement.Month)
deleteBaselineDate = runTime.AddMonths(-age);
if (timeFrame == AgeIncrement.Year)
deleteBaselineDate = runTime.AddYears(-age);
}
if (absoluteDate != null && absoluteDate != DateTime.MinValue)
deleteBaselineDate = absoluteDate;
}
We can then process our actual command and take all of our command-line options into account as we go:
private int DeleteFilesFromFolder(DirectoryInfo folder, bool simulateOnly)
{
foreach(FileInfo file in folder.GetFiles(filter))
{
if (!deleteNewer ? file.LastWriteTime <
deleteBaselineDate : file.LastWriteTime > deleteBaselineDate)
{
try
{
if (! quietMode)
outputStream.WriteLine("Deleting {0}",
file.FullName);
if (! simulateOnly)
file.Delete();
}
catch (Exception ex)
{
if (! quietMode)
errorOutputStream.WriteLine(
"Could not delete file {0} : Reason {1}",
file.FullName, ex.Message);
}
}
}
if (recurseSubFolders)
{
foreach(DirectoryInfo subFolder in folder.GetDirectories())
{
DeleteFilesFromFolder(subFolder, simulateOnly);
}
}
if (removeEmptyFolders)
{
if (folder.FullName == path && dontRemoveEmptyRootFolder)
{
outputStream.WriteLine("Leaving empty root folder {0}",
folder.FullName);
}
else if (folder.GetFiles().Length == 0 &&
folder.GetDirectories().Length == 0)
{
if (! quietMode)
outputStream.WriteLine("Deleting empty folder {0}",
folder.FullName);
try
{
if (! simulateOnly)
folder.Delete(true);
}
catch (Exception ex)
{
if (! quietMode)
errorOutputStream.WriteLine(
"Could not delete folder {0} : Reason {1}",
folder.FullName, ex.Message);
}
}
}
return 0;
}
You'll notice that there is an NUnit tests class bundled into the project, so if you want to build the project and don't have NUnit 2.2 installed, you'll need to exclude DeleteOldTests.cs from the project and remove the reference to NUnit.
Points of Interest
Unit Tests
One of the interesting obstacles was how to usefully unit test such an application. Console apps are a bit troublesome in that all the code is typically written into the .exe (to keep deployment light), and a separate .NET assembly (e.g. Unit Tests) can't reference a .exe assembly. I could have of course used a post-build event to make a copy as DeleteOld.dll and reference that from a tests assembly but I felt, that was a bit too much of a compromise.
I typically wanted to test output from the command based on the input parameters and the files I was looking to delete. This meant that the easiest way, was to provide programmatic access to override the output streams used at runtime. The default constructor uses standard Console
streams:
public DeleteOld(Arguments args)
{
outputStream = new StreamWriter(Console.OpenStandardOutput());
outputStream.AutoFlush = true;
errorOutputStream = new StreamWriter(Console.OpenStandardError());
errorOutputStream.AutoFlush = true;
}
The unit tests use the other constructor to pass in a MemoryStream
.
public DeleteOld(Arguments args, Stream outputStream, Stream errorOutputStream)
{
this.outputStream = new StreamWriter(outputStream);
this.outputStream.AutoFlush = true;
this.errorOutputStream = new StreamWriter(errorOutputStream);
this.errorOutputStream.AutoFlush = true;
}
This technique effectively enables the tests to do the following in order to setup and check the output of the command:
[Test]
public void DirectoryNotExist()
{
Environment.CurrentDirectory = Path.Combine(Path.GetTempPath(), "DeleteOldtests");
string[] args = GetInvalidArguments2();
MemoryStream ms = new MemoryStream();
Arguments arguments = new Arguments(args);
DeleteOld DeleteOld = new DeleteOld(arguments, ms, ms);
Assert.IsFalse(DeleteOld.Validated);
string output = System.Text.Encoding.Default.GetString(ms.ToArray());
Assert.IsTrue(output.StartsWith(
@"Directory does not exist : c:\vfchtugierujkjhsdfsdf\ffklksdfs\jlkjkvvdd1"));
}
There's also a public Validated
property in the class to assist with unit testing.
I think I had more fun (and spent more time) with the unit test side of things, than the actual functionality, which was pretty straightforward once you've parsed all of the arguments.