An FTP Source Control Provider for CruiseControl.NET

I’ve been working on a custom CCNET implementation to handle EDI invoicing which is nearly complete and one of the things I needed was FTP download support. In the project I’m working there is a utility that makes use of edtFTPnet, from Enterprise Distributed Technologies, an Open Source C# FTP library licensed under LGPL. It seems to work reasonably well though the authors are admittedly “love the ‘internal'” keyword thus making extensibility difficult, if not impossible, short of forking the codebase. Anyway, I’ve been using the library and it seems to work just fine. Btw, I’ve submitted my suggested changes so hopefully they’ll recognize the benefits and incorporate them.

Writing Plugins for CruiseControl.NET

Anyway, what I needed as a mechanism that would poll an FTP server looking for new files to be downloaded which sounded like a reasonable thing for CruiseControl.NET. Of course, once files are downloaded additional processing is done so it fits with CI quite nicely. Fortunately, CruiseControl.NET has a nice plugin architecture that makes this sort of thing easy.

Creating a Plugin Assembly

The first step is to create a new Class Library following the naming convention ccnet.*.plugin.dll and add the following assembly references:

  • edtFtpnet
  • Thoughtworks.CruiseControl.Core.dll
  • NetReflector.dll

And add the following to your using statements:

  • System.Collections
  • EnterpriseDT.Net.Ftp
  • Exortech.NetReflector
  • ThoughtWorks.CruiseControl.Core
  • ThoughtWorks.CruiseControl.Core.Util

Your assembly should appear as follows (click the image for a larger version):

CCNET plugin assembly

Adding ISourceControl

The next step is to add the ISourceControl interface and provide the necessary attribute which will declare the name used to identify your Source Control Provider.

[ReflectorType("ftpscc")]
public class FTP: ISourceControl

The ReflectorType attribute comes from the NetReflector assembly and is located in the Exortech.NetReflector namespace. The value provided to the attribute is the name which will be used in ccnet.config as follows:

<sourcecontrol name="ftpscc"></sourcecontrol>

Adding FTP Specific Properties

Next, add properties for the FTP connection and once again we’ll use attributes from Exortech.NetReflector making it easy to declare settings from ccnet.config:

[ReflectorProperty("deleteRemoteFiles", Required = false)]
public bool DeleteRemoteFiles = true;

[ReflectorProperty(
"fileMask")]
public string FileMask;

[ReflectorProperty(
"localPath")]
public string LocalPath;

[ReflectorProperty(
"password")]
public string Password;

[ReflectorProperty(
"remoteHost")]
public string RemoteHost;

[ReflectorProperty(
"timeOut", Required = false)]
public int TimeOut = 60;

[ReflectorProperty(
"userName")]
public string UserName;

[ReflectorProperty(
"remoteDir", Required = false)]
public string RemoteDir = "/";

These properties indicate settings for the FTP connection as well as the location where the files should be placed after being downloaded. Additionally, there is a flag which indicates if the downloaded file should be deleted from the server.

Again, simply adding the ReflectorProperty attribute will cause these members to be populated with the data from your <sourcecontrol name="ftpscc"> tag in ccnet.config.

Implementing ISourceControl

Next, implement the ISourceControl interface on your class. Of course, the easiest way (in VS.NET) to stub out the necessary methods is via the context menu with the caret sitting on ISourceControl:

VS.NET Implement Interface

This will give you five new methods though I’ll focus on the following two since that’s all you really need to implement here:

public Modification[] GetModifications(IIntegrationResult from, IIntegrationResult to)

public void GetSource(IIntegrationResult result)

In GetModifications we need to return an array which contains information about the files to be downloaded and in GetSource actually fetch the files. One caveat here is we use DateTime.Now as the modified time so that CCNET will consider the file as a “new” change. So, GetModifications will look like this:

1 public Modification[] GetModifications(IIntegrationResult from, IIntegrationResult to)
2 {
3 ArrayList mods = new ArrayList();
4 Connect();
5 try 6 {
7 Log.Debug("Fetching list of files that match: " + FileMask);
8 FTPFile[] mModifiedFiles = ftp.DirDetails(FileMask);
9 Modification mod;
10 int i = 0;
11 foreach (FTPFile f in mModifiedFiles)
12 {
13 mod = new Modification();
14 mod.Type = "Added";
15 mod.FileName = f.Name;
16 mod.ChangeNumber = i;
17 mod.ModifiedTime = DateTime.Today;
18 mods.Add(mod);
19 i++;
20 }
21 return (Modification[])mods.ToArray(typeof(Modification));
22 }
23 finally 24 {
25 // Close the connection if there are no files to process 26 if (ftp.IsConnected && mods.Count == 0)
27 ftp.Quit();
28 }
29 }
30

GetSource handles downloading files from the server and then deleting them:

1 public void GetSource(IIntegrationResult result)
2 {
3 try 4 {
5 foreach (Modification m in result.Modifications)
6 {
7 Log.Debug("Fetching file: " + m.FileName);
8 ftp.Get(LocalPath, m.FileName);
9 if (DeleteRemoteFiles)
10 {
11 Log.Debug("Deleting remote file: " + m.FileName);
12 ftp.Delete(m.FileName);
13 }
14 result.AddTaskResult(string.Format(
15 "<fileDownloaded>{0}</fileDownloaded>\n", m.FileName));
16 }
17 }
18 finally 19 {
20 ftp.Quit();
21 }
22 }

Connecting to the FTP Server

The call to Connect in GetModifications above looks like this:

1 public void Connect()
2 {
3 Log.Debug(String.Format("Connecting to remotehost: {0}...", RemoteHost));
4 ftp.Connect();
5 Log.Debug(String.Format("Connected, logging in with user: {0}", UserName));
6 ftp.Login(UserName, Password);
7 Log.Debug(String.Format("Switching to remote directory: {0}", RemoteDir));
8 ftp.ChDir(RemoteDir);
9 }

The only thing I haven’t really covered here is the construction of the FTPClient member and the assignment of the its RemoteHost and Timeout properties which I think is pretty straightforward.

Conclusion

Writing plugins, whether for Source Control Providers or tasks (ITask), the process is pretty straightforward though there is little documentation on the subject. Be prepared to spend some time studying the other ISourceControl and ITask implementations included with CCNET and quite a bit of time debugging CCNET itself to understand how it works. One issue I’ve run into and that I’ve blogged about before which is when exceptions occur from within a Source Control Provider. In the current released version of CCNET (1.3) exceptions are not handled by the publishers for the project therefore you’ll never get notified about connection problems etc. unless you use the log4net settings I mentioned.

The above implementation is lacking in a number of areas, like error checking, and is a very simplified example of code I’m using in production. I’ve also tweaked CCNET itself so that any exceptions raised within my FTP provider are handled by the build publisher so that I can get email when things fail. In fact, I’ve been working to get these, or similar, changes incorporated into CCNET.

[UPDATE: Feb 6, 2008] Fix image links