Hudzilla Coding Academy: Project Three

Code
Hudzilla Coding Academy

 

After coding a game in Project Two, it's now back to the serious tasks, and this is definitely one that will push your new coding skills to their limits. If you finish this tutorial, you'll have a much stronger idea of how to work with files in Mono, which is a skill that will come in handy time and time again.

First steps

Apple does a great job of taking seemingly mundane features and making them look or sound awesome. When it released the 17-inch Macbook Pro with a glass touchpad, lots of folk were chattering away about how many recharge cycles - when "recharge cycles" were probably two words they had never said together previously. And the same goes for its Time Machine backup system: it's incremental filesystem backup, which means that it starts with one big backup then just copies across the changes, but Apple slapped on a few whizzy 3D GUI effects and made the whole thing a lot more interesting.

In this tutorial we're going to program something similar to Time Machine. No, not the whizzy effects - instead, we're going to produce something that will store multiple versions of files over time, and allow the user to jump back to any previous backup whenever they want to.

I don't want to waste time talking, because there's a lot of difficult code to get through - start MonoDevelop, and create a new C# command-line project called Chronojohn. Yes, that's a strange name, but what can I say - I'm a die-hard LucasArts fan.

Add "using System.IO;" just beneath "using System;", then delete the Console.WriteLine() line and replace it with this:

CheckDirectory(args[0]);

"What is CheckDirectory()?" I hear you say. Well, it's a method that we haven't written yet, and we're going to pass into it the first argument given to us by the user. You see, ultimately this project needs to be able to do three things:

  1. Check a directory for changes, backing up any files that have changed. This will be called using something like "./Chronojohn.exe /path/to/somewhere".
  2. List the available backups for a file. This will be called using something like "./Chronojohn.exe list /path/to/some/file".
  3. Restore a backup to a specific version. This will be called using something like "./Chronojohn.exe restore /path/to/some/file 3", where 3 is the version number the user wants.

We're going to start with the first situation: checking a directory for changes. And that means our program will accept a single parameter, which is the one the user wants to backup.

The reason we're putting the whole thing into its own CheckDirectory() method isn't just to keep our code nice and clean, although that is a welcome side effect. Instead, it's to handle recursion, which is another one of those geeky words important enough to learn. Think about it: if the user wants to backup /home/bob, we need to check /home/bob for files, then all the subdirectories, then all the subdirectories of the subdirectories... and so on, until all the files and directories inside /home/bob have been checked.

This is done using recursion: we call CheckDirectory() for the top-level directory the user wants to check, then inside the method it calls CheckDirectory() again for each of its subdirectories, which in turn call it again and again until we've gone over the whole directory tree. As the old saying goes, "in order to understand recursion, one must first understand recursion," which will leave you going around in loops for hours. Luckily, recursion makes more sense once you start working with it, so let's create a simple CheckDirectory() method that will print out the names of all the files in a directory, as well as all the files in all the subdirectories.

The Sierpinski triangle is an easy way to visualise recursion.

This is one big triangle. But it's made up of three smaller triangles. And each one of those is made up of three smaller triangles. And each one of those... oh, you get the point. This is recursion in action.

I want you to add this new CheckDirectory() method just below the end of Main(), as we did in Project Two - refer back there if you're unsure where to put this:

static void CheckDirectory(string directory) {
	string[] files = Directory.GetFiles(directory);
	string[] dirs = Directory.GetDirectories(directory);
	
	foreach(string file in files) {
		Console.WriteLine(file);
	}
	
	foreach(string dir in dirs) {
		CheckDirectory(dir);
	}
}

Before we look at those two new method calls, I just want to remind you that when you see "static void CheckDirectory(string directory)", you should think "static? I can ignore that. Void? That means it returns nothing. CheckDirectory? That's the method name. And "string directory"? That means I'm telling Mono I want to pass in a variable of type "string" and I want it to be called "directory" inside this method."

So, the two new method calls: Directory.GetFiles() and Directory.GetDirectories(). These both come from System.IO, which you should have added a few minutes ago, both take the name of a directory to check as their only parameter, and both return an array of strings. The difference is that the first one returns a list of filenames in that directory, and the second returns a list of the subdirectories.

These return values are just arrays of strings (you do remember string[], right?), which means we can loop over them using foreach loops. Hopefully now you can see how recursion works: we call CheckDirectory() and pass in the parent directory to print out all its files, then it in turn called CheckDirectory() for each of its subdirectories, and so on. As a result, you can now build and run that program (from a terminal window - see previous projects for help doing this) and point it any directory you want to have it list all the files in there. For example:

./Chronojohn.exe /path/to/some/directory

For me on my computer, I could type "./Chronojohn.exe /home/hudzilla" and it would whizz through all my files and print out their names.

The first version of our program will simply print out all the files in all the subdirectories of a directory.

The first version of our program will simply print out all the files in all the subdirectories of a directory.

A simple solution to time-based backup

Let's say a user has a file called foo.txt. They change it, then change it again, then change it again. If our backup system was run inbetween those changes, we should now have four copies of the file: the original and the three changes. How can we store these copies while keeping their directory structure intact? More importantly, how can we store them without overwriting files as we go?

One simple solution - and the one we'll be using - is simply to replicate the exact directory structure in our backup (solving problem one) and adding a suffix to the end of each copy that stores the date the file was changed, thus ensuring overwrites never happen.

First things first: we need to define the location we'll be using to store our backups. Scroll up to the top of your code, and amend it to this:

class MainClass
{
	const string BackupLocation = "/home/hudzilla/Desktop/backup";

You should change "/home/hudzilla/Desktop/backup" to a suitable directory on your computer - make sure you own the folder so that you can write to it! The backup location can be a remote directory if you want; it really doesn't matter.

That "const" keyword is new, and it means "this value is constant", meaning that it will not (and in fact cannot) change when your program is running. Like variables, constants can hold different types of data, and this particular one holds a string. You'll also notice that we haven't declared this one as being "static" like we have in previous projects - like usual, don't worry about it; the "static" keyword is not worth your time right now.

Now, back down to the CheckDirectory() method, because it's time to make it do something more than just print out filenames.

Our backup system needs to work like this:

  • For each file in the current directory, add its current filename to the BackupLocation. For example, for me /home/hudzilla/foo.bar would become /home/hudzilla/Desktop/backup/home/hudzilla/foo.bar. This is the location the backup will be copied to.
  • Figure out the parent directory of the file's backup location. Using the same example as above, this would be /home/hudzilla/Desktop/backup/home/hudzilla.
  • Check whether that parent directory exists. If it doesn't, create it.
  • Copy the file from the source to the backup directory, adding the last modified date of the file to its name to make it unique.

To do all that takes five new methods, so I'm going to show you the source code and comment in along the way so you can see what it does. In your CheckDirectory() method, change the first foreach loop to this (without the comments!):

// loop over each file
foreach(string file in files) {
	// combine its path with the pack of our backup location
	string copy_file = BackupLocation + file;

	// figure out the name of the parent directory
	string copy_parent = Path.GetDirectoryName(copy_file);

	// grab the time the file was last changed
	long date = File.GetLastWriteTime(file).ToFileTime();

	// if the parent directory for the backup doesn't exist...
	if (!Directory.Exists(copy_parent)) {
		// ...then create it now
		Directory.CreateDirectory(copy_parent);					
	}
	 
	// and finally, copy the file to the backup directory
	File.Copy(file, copy_file + "." + date, true);
}

The Path.GetDirectoryName() method finds the parent directory for us by simply removing the actual filename part (eg foo.bar) from the directory component (eg /home/hudzilla), and also comes bundled inside the ever-useful System.IO.

The GetLastWriteTime() takes a little more explaining. First up, we have a new variable type: long. It's not particularly common, and you won't see it that much in this academy, but you do need to use it when working with times relating to files. Put simply, a "long" is a big "int" - it holds only whole numbers, but the range of numbers it can hold is much, much larger. How large? Very large, that's how large.

But if you look carefully, you'll notice that there's stuff after GetLastWriteTime(), and that's where things get really interesting. You see, GetLastWriteTime() returns a date and time variable, not a long. But we don't want to fuss around with days and months - we just want a big number we can compare things against. So we call GetLastWriteTime() for the file, then immediately call ToFileTime() on its return value, which converts a date and time variable into a long representing the age of the file. We could quite easily write it like this:

DateTime foo = File.GetLastWriteTime(file);
long date = foo.ToFileTime();

...but we're never going to be using that date and time variable again, so there's little point. To sum up, this line grabs the last-changed date of the file, converts it to a useful format, then stores that in the "date" variable for later use.

Moving on, Directory.Exists() is used to check whether a directory, er, exists (obvious, I know), then Directory.CreateDirectory() is used to create the thing if it doesn't exist. You need to do this because if you try to copy a file to a directory that doesn't exist, your program will crash. Fortunately, calling CreateDirectory() will create the directory you requested, as well as any parent directories that also need creating.

The last part of that loop uses File.Copy() to copy the source file (the first parameter) to the destination file (the second parameter), forcing overwrite if there's an existing file with the same name (the third parameter). Now, we both now that a filename clash is exceedingly unlikely, but if you don't put that "true" in as the third parameter to allow overwrites, your program will crash if it even tries to overwrite a file.

By the way, did you notice how I specified the destination file name as "copy_file + "." + date", which will add the new extension to every file as its being backed up. The dot between the filename and the date is required so that we know exactly where the filename stops.

Using file extension makes each file backup unique and time-based for easy restore.

This is what our backup directory should look like after running a few backups of a file - lots of similar filenames, but with different extensions./

Only saving what's changed

With this new loop code in place, the program will automatically back up all files every time it's run, which is fine if you want a very basic backup solution - but we want it to only backup files if they have changed since the program was last run. This requires us to do two things: remember when the program was last run, then use that to decide whether to backup each file or not. Fortunately, both of these changes are really easy to make!

Just beneath the line "const string BackupLocation", add this:

static long LastRunTime = 0;

Yes, it's back to being static again, and this variable needs to be a "long" because it will be used to store the time the program was last run so that it can be compared against the "long" file times of the files it's checking.

To track when the program was last run, we're just going to save the current time as a file time (for easy loading) into a file called .chronojohn using File.WriteAllText(). Put this new line of code directly after your call to CheckDirectory() in Main():

File.WriteAllText(".chronojohn", DateTime.Now.ToFileTime().ToString());

Now, as soon as our program starts to run, we need to check whether the .chronojohn file exists and, if it does, load its value into LastRunTime. So, put this code at the very top of the Main method, like this:

public static void Main(string[] args)
{
	if (File.Exists(".chronojohn")) {
		string lastrun = File.ReadAllText(".chronojohn");
		LastRunTime = long.Parse(lastrun);
	}

In Project Two you used the int.Parse() method to convert a string into an integer, and now you can see the equivalent for "long" variables: long.Parse(). So that chunk of code checks whether .chronojohn exists, and, if it does. reads all the text in and converts it to a long to be placed inside LastRunTime.

That's the first step complete: our program now remembers when it was last run. The next step is to check that against the last write date of each file, and only create a backup if the file has changed since the program was last run. Given that we already have code in place to call GetLastWriteTime() for each file, this isn't hard to do - go down to your CheckDirectory() method, and change this:

if (!Directory.Exists(copy_parent)) {
	Directory.CreateDirectory(copy_parent);					
}

File.Copy(file, copy_file + "." + date, true);

...to this:

if (date > LastRunTime) {
	if (!Directory.Exists(copy_parent)) {
		Directory.CreateDirectory(copy_parent);					
	}
	 
	File.Copy(file, copy_file + "." + date, true);
}

So now we compare each file's last change time against our program's LastRunTime, and only make the backup if the file's date is greater than the program's last run time - meaning that it was changed later. You met < ("is less than") previously, and now we're using its friend: > means "is greater than". So, "date > LastRunTime" will return true if "date" is greater than "LastRunTime".

The initial backup

Our program now backs up all files that haven't been changed since its last run, but it has one fundamental problem: if you run it on a different directory, it will see none of the files have changed since it was last run and so will backup nothing. What we need to do is change the program so that it ignores the file date if it has no backups of a file or directory.

The way we're going to do this is fairly simple:

  1. Check whether a backup of the directory exists.
  2. If it does, check whether any backups of each file exist in there.
  3. If the file does not exist OR if the file exists but has changed since our last run, back it up

That middle one might seem hard to do, but there's a neat little trick that will save us a lot of work: if you call Directory.GetFiles() and pass it a second parameter, Mono will use that as a filter for the files to return. For example, if you sent in "foo.*" as the filter, Mono would return files such as foo.bar, foo.baz and foo.exe, but not foobloo.bar or fizzbuzz.sh.

We're also going to use a new method called Path.GetFileName(), which is like Path.GetDirectoryName() except it returns only the filename part rather than the directory part. Using that, Mono will turn /home/hudzilla/foo.bar into foo.bar, meaning that all we have to do is put ".*" on the end to make it the perfect filter to find our backups.

Change the contents of the "foreach(string file in files)" loop in you CheckDirectory() method to this (without comments, of course!):

// this is old stuff
string copy_parent = BackupLocation + Path.GetDirectoryName(file);
string copy_file = BackupLocation + file;
long date = File.GetLastWriteTime(file).ToFileTime();

// this is where the new code begins!
// assume the file doesn't exist
bool file_exists = false;

// if the directory doesn't exist, clearly the backup file doesn't exist either
if (Directory.Exists(copy_parent)) {
	// if the directory does exist, the backup file might exist - let's look for it
	string search_filter = Path.GetFileName(file) + ".*";
	string[] contents = Directory.GetFiles(copy_parent, search_filter);
	
	// if we have any backups of this file, it will be in "contents" now
	if (contents.Length > 0) {
		// at least one matching file was found, so we have a backup!
		file_exists = true;
	}
}

// only backup a file if it doesn't already exist
// OR if its change date is newer than our last run date
if (!file_exists || date > LastRunTime) {
	if (!Directory.Exists(copy_parent)) {
		Directory.CreateDirectory(copy_parent);					
	}
	 
	File.Copy(file, copy_file + "." + date, true);
}

There's not really any new code here, but you might find it hard to understand simply because we're using old code in new ways and putting it all together. That's why it's so important to learn the basic methods really well - once you understand them fully, it's easy to re-arrange them to fit your needs.

The new chunk of code in the middle of CheckDirectory() ensures a full backup takes place if no backup exists already. By assuming each file doesn't already exist, we're making Mono check to be absolutely sure a backup exists before skipping over it - in short, we're playing it safe.

There's a sneaky little extra in there that seasoned programmers won't have noticed, but if you're following this through as your first coding experience, you'll probably be wondering what || means in the new "if" statement at the end. What it means is "or", meaning that you can combine two other conditions together and if either of them are true then the whole condition is true. For example, if A and B are both bools, but A is true and B is false, then:

if (A) {
	// this code will execute
}

if (B) {
	// this code will not execute
}

if (A || B) {
	// this will execute, because all it takes is A *or* B to be true
}

The cool thing about || is that it uses something called short-circuit evaluation, which means Mono will stop reading the "if" statement as soon as it knows for sure what the result will be. In the example above, "if A || B" means "if either A or B is true, the whole condition is true", so Mono will check A and see that it's true - and then it doesn't even bother checking whether B is true or false, because it doesn't matter.

Listing available backups

At this point, your program backs up files nicely, but that's only one third of the full problem: it also needs to list the available backups for a particular file, then allow the user to restore from one of those backups. So, let's move on and make it list the backups for a file, but first we need to make a few basic changes to the program to let it accept arguments from the user selecting what they want to do.

We're going to check for the number of parameters using a switch/case block, so change your Main() method to this:

public static void Main(string[] args)
{
	if (File.Exists(".chronojohn")) {
		string lastrun = File.ReadAllText(".chronojohn");
		LastRunTime = long.Parse(lastrun);
	}
	
	switch (args.Length) {
	case 0:			
		PrintUsage();
		break;
	case 1:
		CheckDirectory(args[0]);
		File.WriteAllText(".chronojohn", DateTime.Now.ToFileTime().ToString());
		break;
	case 2:
		if (args[0] != "list") {
			PrintUsage();
			return;
		}
		
		ListBackups(args[1]);
			
		break;
	}
}

There are three important things to note in there:

  1. PrintUsage() is a new method. I'll be showing it to you in a moment.
  2. We only save the last run time when a directory was checked, otherwise it's much more likely the program will miss file updates.
  3. ListBackups() is another new method, and will be used to print the list of available backups for a file - that's why it insists the first parameter is "list", and the second gets passed into ListBackups() because that will be the file the user wants to search for.

Let's get the PrintUsage() method out of the way. This is a method all by itself because it's going to print out a message about how to use the program, and rather than retyping the message every place where the user might have entered the arguments incorrectly, we're going to share it all in PrintUsage(). I've explained before how to add new methods to your program, so you should be able to do it easily by now. If not, please refer back to Project Two for detailed instructions. Failing that, just follow the indentation of the braces (the { and } symbols) until you see where Main() ends, then put this after that:

static void PrintUsage() {
	Console.WriteLine("");
	Console.WriteLine("Chronojohn: a backup tool H. G. Wells would have been proud of.");
	Console.WriteLine("");
	Console.WriteLine("\tadd /some/directory \t\tScan directory and backup changed files");
	Console.WriteLine("\tlist /some/file \t\tList all backups of a given file");
	Console.WriteLine("");
}
Making your program print out helpful usage information on the command line is essential for any smart programmer.

Making your program print out helpful usage information on the command line is essential for any smart programmer.

Now let's turn our beady eyes towards the ListBackups() method, which needs to accept the filename to search for then look to see what backups of that file are available.

You can almost - given what you have learned in this project - produce this entire method by yourself. It works out like this:

  1. Check the name of the backup directory where the file would be.
  2. Scan that directory for files matching the requested filename + ".*"
  3. Loop over all the matching files, and print out the time it was saved

There are two small complications that will require some new skills. First, most users don't want to type /some/path/to/very/long/filename to check a file if they are already in the /some/path/to/very/long/ directory - they just want to type "filename" and have your program figure out the full path. That's easily done using Mono (I hope you're noticing how often I say that!), because if you send a filename (eg "long/filename") to Path.GetFullPath() it will return the full path name ("/some/path/to/very/long/filename").

The other small complication is how to get the time each backup was saved. You see, we want to show users options like this:

1: 01/02/2009 08:05:53
2: 02/02/2009 09:57:38
3: 05/02/2009 19:38:05

That way, they can see there are three backups of their file available, and see when each of those files was written.

Fortunately, our backup system works by appending a new file extension to backups as they are saved, and that file extension is the time it was last written to. So all we have to do is pull off the file extension and convert it from a file time (a "long", if you remember) into a date string that people can read.

The nice thing is that you've already met Path.GetDirectoryName() and Path.GetFileName(), and now you can use Path.GetExtension() to extract the ".bar" from "foo.bar". And yes, if you were wondering, it does include the "." in the extension, so we'll need to remove that in order to read the file time.

Using the Path.GetDirectoryName(), Path.GetFileName() and Path.GetExtension() methods let you break a filename into smaller parts for easier analysis.

Using the Path.GetDirectoryName(), Path.GetFileName() and Path.GetExtension() methods let you break a filename into smaller parts for easier analysis.

So, what the ListBackups() method needs to do is:

  1. Accept a filename that we'll use to search for backups
  2. Figure out the exact path to the file, so the user can use relative filenames (ie "long/filename")
  3. Use Path.GetDirectoryName() to figure out where the backup directory for this file is
  4. Search that directory for matching backups
  5. For each match, use Path.GetExtension() to see the time it was modified
  6. Remove the "." from the start of the extension, then convert the rest to a time
  7. Print that time, along with a number identifying the backup version so the user can reference it

Sound good? To make things a little more interesting, I'm not going to use a foreach loop to go over the list of backups - you'll see why in a moment. Here's the code:

static void ListBackups(string filename) {
	string file = Path.GetFullPath(filename);
	string copy_parent = Path.GetDirectoryName(BackupLocation + file);
	string search_filter = Path.GetFileName(file) + ".*";
	
	string[] backups = Directory.GetFiles(copy_parent, search_filter);
	
	// this is the new loop, replacing foreach
	for (int i = 0; i < backups.Length; i++) {
		string ext = Path.GetExtension(backups[i]).Substring(1);
		long backup_date = long.Parse(ext);
		string write_time = DateTime.FromFileTime(backup_date).ToString();

		// this next line is totally new, so don't worry about it for now
		Console.WriteLine(string.Format("{0}: {1}", i + 1, write_time));
	}			
}

So far we've been using two types of loop: "foreach" to loop over an array, and "while" to loop as long as a condition is true. Now I want to introduce you to the third and final common loop type: for. A for loop works like a while loop in that it only runs the loop while a condition is true. The difference is that a for loop is usually used to execute the loop a specific number of times. In the example above, you can see the for loop has three parts:

  • "int i = 0" defines an integer called "i" and sets it value to be 0. This "i" variable is available only inside this loop.
  • "i < backups.Length" is the condition that must be true for the loop to execute. So in this case, if "i" is less than the number of backup files, the loop will run.
  • "i++" adds 1 to "i". This last part is executed every time the loop repeats - and note that's every time the loop repeats, which doesn't include the first run.

So if backups.Length was 2, here's how the loop would run:

  • Set i to be 0.
  • Compare i against 2.
  • i is 0, which is less than 2, so run the loop.
  • Loop is finished, add 1 to i to make it 1.
  • Compare i against 2.
  • i is 1, which is less than 2, so run the loop.
  • Loop is finished, add 1 to i to make it 2.
  • Compare i against 2.
  • i is 2, which is not less than 2, so finish the loop.

You may wonder why we're using a for loop rather than a foreach loop, and the answer is simple: when you use a for loop, you know exactly where you are in the loop. For example, if backups.Length was 100 and you're part-way through, how can you tell how far through the loop you are? With a foreach loop you need to declare a separate counter variable to track this. With a for loop that comes with the territory - in the example above you can just check "i" to see where you are.

This is particularly important here, because we want to print out version numbers of a file. By using a for loop, we can just print out the value of "i" to tell the user each file's position in the list.

Inside the for loop is all sorts of new stuff, so I want to go over it all very carefully. First up is this:

string ext = Path.GetExtension(backups[i]).Substring(1);
long backup_date = long.Parse(ext);

Previously we used the code "File.GetLastWriteTime(file).ToFileTime()", which meant "get the last write time for this file, then convert that to a file time." It's a common coding shortcut that gets used when you don't need to store the return value of one method, but do need to do something with it. We're using something similar here: Path.GetExtension(backups[i]).Substring(1).

First up, Path.GetExtension(backups[i]) returns the file extension of a given backup file. Remember, we're adding a new file extension to every backup file, with that extension storing the file's last change time. As an example, let's say we're working with a file called foo.bar, and its last change time is 123 - that file would be called foo.bar.123. Running Path.GetExtension("foo.bar.123") would return ".123", but we don't want the "." at the beginning.

To get rid of the "." at the beginning of the method, we use a new method called Substring(), which returns part of the string. For example, if we have the string "hello", Substring(1) would return "ello", Substring(2) would return "llo", etc. So, by using Path.GetExtension(backups[i]).Substring(1) we're getting the filename and removing the "." all in one go.

With the extension ready to read as a date, we convert it to a "long" variable by using long.Parse() again.

Moving on, we need to convert the backup date (which is a long number like 129185319140000000) into a real date. Again, Mono has us covered with a method that does just this: DateTime.FromFileTime(). Send a file time to that and it will return a date and time variable.

But we don't want that - we want a nice date being printed out, not another Mono variable. Once more, Mono steps in to the rescue with the ToString() method, which actually exists in all sorts of places. In this case, calling ToString() on a date and time variable will return a neat string with a printable date according to the user's local preferences.

So, altogether the code to convert from a file time to a string is this:

string write_time = DateTime.FromFileTime(backup_date).ToString();

That just leaves one final line:

Console.WriteLine(string.Format("{0}: {1}", i + 1, write_time));

That's a bit complicated, so let me break it up for you into two lines:

string meh = string.Format("{0}: {1}", i + 1, write_time)
Console.WriteLine(meh);

Hopefully now you can see that a chunk of it (shown on the second line there) just sends some data to Console.WriteLine() - it's the string.Format() bit that's new. Basically, string.Format() is designed to make it really easy (and fast) to put lots of variables into a string. For example, look at this code:

string foo = year + "/" + month + "/" + day + " " + hour + ":" + minute;

That's a pretty ugly way to write out a date and time, and it executes slowly too because Mono has to combine all the strings individually. Using string.Format(), you can put any number of variables into a string using one method call - you just send them to the method in the order you number them. That is, using {0} means "the first variable", {1} means "the second variable", etc. The above line could be rewritten like this:

string foo = string.Format("{0}/{1}/{2} {3}:{4}", year, month, day, hour, minute);

...or like this:

string foo = string.Format("{4}/{3}/{2} {1}:{0}", minute, hour, day, month, year);

It doesn't really matter what order you specify the variables in, as long as you use the right number inside the {}s. The nice thing about this method is that Mono will automatically convert all the variables you pass into it into strings if possible, and you can even taken control of the formatting if you want to. But here we don't - we just want to print out the version number of the file and its last write time (taken from its extension).

Note how I use "i + 1" for the version number, because our loop counts from 0 but that won't make sense to users, so this adds one to the counter so it appears to be counting from 1.

We'll be using string.Format() more in the future, because it's generally preferred when you're combining more than two or three things together.

Restoring from backup

At this point, your program can back up files and list version numbers. This means there's just one more task to do: restore a file to an old version as requested by the user.

As mentioned near the start of this project, users will type something like "./Chronojohn.exe restore /path/to/some/file 3" to restore a file. That 3 should make a lot more sense now that you know how we're listing backups, and you already have everything you need to write it yourself. But I'm nice, and I want to make sure you get it exactly right, so I'm going to show you how it's done.

First, we need to modify the switch/case block in Main() so that it handles people sending in 3 arguments. Put this case after the end of case 2 (after its "break;" statement):

case 3:
	if (args[0] != "restore") {
		PrintUsage();
		return;
	}
	
	RestoreBackup(args[1], int.Parse(args[2]));
	
	break;

As with "list", we need to check that the first argument is "restore" before going on, otherwise we print the usage information and bail out. The RestoreBackup() method is where all the action will take place: we pass in the name of the file to restore, then convert the third parameter (args[2]) into an integer and pass that along as well. So you should be able to guess that the RestoreBackup() method must look like this:

static void RestoreBackup(string filename, int version) {
// blah blah blah
}

It returns nothing, and accepts a string (called "filename") and an integer (called "version"). What it needs to do is get the full path of the filename to restore, get a list of all the backups of that file, then copy it back over the original file, thus restoring the version as requested. I'm also going to throw in two bonuses: we're going to print out an error message if the user requests to restore to version 5 if there are only 2 versions available, and we're also going to re-set the last write time of the file once it has been restored so that it has the original write time of that backup.

In Gnome, the last write time of a file can be seen by right-clicking on it and choosing Properties.

In Gnome, the last write time of a file can be seen by right-clicking on it and choosing Properties - look for "Modified:".

Here's the code to put inside RestoreMethod(), with my comments:

// get the full path to the file, turning "foo.bar" into "/home/hudzilla/foo.bar"
string file = Path.GetFullPath(filename);

// figure out where this file will be in our backup tree
string copy_parent = Path.GetDirectoryName(BackupLocation + file);

// get all files that are backups of the requested file
string[] backups = Directory.GetFiles(copy_parent, Path.GetFileName(file) + ".*");

// check whether the user has asked to backup a version that doesn't exist
if (backups.Length < version) {
	// they have - run away!
	Console.WriteLine( string.Format("Can't restore version {0} - maximum is {1}",
					version, backups.Length));

	// NOTE: the above two lines are actually one line broken into two for space reasons
	return;
}

// if we're still here, it means we have a backup of this file, so SUBTRACT ONE
// (this is important!) from the version number, because Mono arrays count from
//zero whereas our ListBackups() method counts from one
string chosen = backups[version - 1];

// now copy the file from the backup to the original - that last "true" means
// "overwrite existing", remember
File.Copy(chosen, file, true);

// and, just for kicks, change the file's last write time to match the backup's
File.SetLastWriteTime(file, File.GetLastWriteTime(chosen));

Let's wrap up

And with that, the project is complete - it backs up, lists and restores all in one. What's more, you've mastered many of the most important filesystem techniques needed to create any number of file-based programs. I don't want to blow up your ego or anything, but you've learned some really hard stuff in this tutorial and you've produced a really cool application at the same time.

This a project that has some real possibilities for future development - there are lots of things you can do to extend this further and turn it into something even better.

So, that's another project taken on and defeated. Well done!

Homework

If you're following this with a tutor, you will be required to complete the following homework before continuing. If you're working by yourself, I strongly recommend you find someone who can help check your work and provide feedback.

The homework for this project is made up of three coding problems; all are required.

  • Add a new command called "revert", which is similar to "restore" except it deletes all later back ups of the file. For example, if foo.bar has versions 1, 2, 3 and 4, using "./Chronojohn.exe revert foo.bar 2" would restore version 2 of the file, then delete versions 3 and 4 from the backup. You'll find the method File.Delete() helpful for this.
  • Trying to restore a file from a directory that hasn't been backed up causes a crash, eg "./Chronojohn.exe /foo/bar/baz 3". Fix this so that it prints out an error message instead.
  • Make the program log everything it does to the file chronojohn.log. To be specific, you must log at least the following: when the program is looking for files to back up (as well as when it was run and where it was told to back up), when a file is being backed up (make sure and specify its full name), and when a user restored a file (and to what version).

If you have problems, try to solve them yourself - you might not succeed, but you'll learn a lot by trying! If you're still having problems, drop your tutor an email and ask for help.

The small print

This work was produced for TuxRadar.com as part of the Hudzilla Coding Academy series. All source code for this project is licensed under the GNU General Public License v3 or later. All other rights are reserved.

You should follow us on Identi.ca or Twitter


Your comments

Can I use this tutorial...

Can I use this tutorial, because your using C# to program in, as a way of learning C# but with a different tool or do I need to install a linux distro to learn C# and all of the fundamental theories you teach here??? Because the tool I was thinking of using was Visual C# 2008 Express Edition.

Code should work

So far we are just using text mode applications and nothing OS specific. I do not know what Paul has planned for us. Of course all of us here will probably agree that you should install Linux ;)

A bug?

Hi, I'm having trouble running this code. I'm using Visual C# on Windows, but I don't think this would be different on Linux.

The second statement of the ListBackups method is:
string copy_parent = Path.GetDirectoryName(BackupLocation + file);

However, I'm pretty sure we should have:
string copy_parent = Path.GetDirectoryName(BackupLocation + filename);

Otherwise we get two full paths that get concatenated and are therefore malformed.

RE: A bug?

Ugh, I take that back. Someone delete my comment! It does make a difference when on windows since the extra "c:" or whatever is added and makes this crazy.

My apologies.

Instructors Solution

I know someone mentioned it before, but it would be great to be able to have the instructor solution for the homework portion to see if we did it in a way that wasn't completely convoluted.

Out of bounds array error!

Hello

When I try to test the first part of the program the CheckDirectory() method. I get out of bounds errors. Should the array start at 0 or 1 in order to avoid the error when compiling. I try to test by running the program with the directory path but the program crashes. Does the array include the program name or does it pick up only the arguments ran with the program.

Thanks for your help

myself

anonymous,

the args[] array the is set when Main runs starts with args[0] as the first argument, not the program name.

I am not able to see the backup files

With this fantastic project,I am able to perform the backups of the files,but in my back up folder,backup files are not visible.

will anybody reply me?

Big Bug in Backup

Correct me If I'm wrong:

Program will fail, if you do the following:
1. Backup files in two different directories.
2. Do some changes in files in both directories.
3. Backup one of the two directories.
4. Backup the second one.

In step 4, program won't backup anything, because after step 3 LasrRunTime becomes greater than change date of files of directory 4.

I don't think that one global LastRunTime variable will suffice. Maybe it's better to store one in each back up directory's folder.

Then again, correct me if I'm wrong.

Re: Big Bug in Backup

This bug only occurs when you give absolute path.

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

Post new comment

CAPTCHA
We can't accept links (unless you obfuscate them). You also need to negotiate the following CAPTCHA...

Username:   Password:
Create Account | About TuxRadar