Sunday, June 21, 2009

Working with the 1.5 code base

The code base has undergone some MAJOR refactorings. In the next postings I will explain some of these, building it up step by step. The plan : create a ftp publisher task/publisher.
For the ftp-core I'll use a third party library from Enterpise Distributed Tecnologies. They have a OS ftp library under the LGPL license. I used this already at work, and it is a good one. Of course the OS version does not have all the latest, extra features, but since I'm a plain vanilla guy, I have no problems with that whatsoever. It does support active and passive communication, which is great, for an overview of the difference between active and passive mode, read here.
Now I'm also planning on creating a FTP-Source control provider, so step 1 will be to place all the FTP-actions in a separate class, and use this class later on.

Step 1 : the ftp library

I'll be using the following interface :
using System;
namespace ThoughtWorks.CruiseControl.Core.Util
{
interface IFtpLib
{
/// <summary>
/// Logs into the specified server, with the userName and password
/// If activeConnectionMode is set to true, active connection is used,
/// otherwise passive connection.
/// </summary>
/// <param name="serverName"></param>
/// <param name="userName"></param>
/// <param name="password"></param>
/// <param name="activeConnectionMode"></param>
void LogIn(string serverName, string userName, string password, bool activeConnectionMode);

/// <summary>
/// Disconnects from the server
/// </summary>
void DisConnect();

/// <summary>
/// returns true if connected
/// </summary>
/// <returns></returns>
bool IsConnected();

/// <summary>
/// returns the current path of the server
/// </summary>
/// <returns></returns>
string CurrentWorkingFolder();

/// <summary>
/// downloads the remoter folder to the local folder, recursive if wanted
/// </summary>
/// <param name="localFolder"></param>
/// <param name="remoteFolder"></param>
/// <param name="recursive"></param>
void DownloadFolder(string localFolder, string remoteFolder, bool recursive);

/// <summary>
/// Uploads a local folder to the specified remotefolder, recursive if wanted
/// </summary>
/// <param name="remoteFolder"></param>
/// <param name="localFolder"></param>
/// <param name="recursive"></param>
void UploadFolder(string remoteFolder, string localFolder, bool recursive);
}
}

Step 2 : the implementation

namespace ThoughtWorks.CruiseControl.Core.Util
{
public class FtpLib : IFtpLib
{
private EnterpriseDT.Net.Ftp.FTPConnection FtpServer;
private Tasks.TaskBase CallingTask;

public FtpLib()
{
this.FtpServer = new EnterpriseDT.Net.Ftp.FTPConnection();

this.FtpServer.ReplyReceived += HandleMessages;

this.FtpServer.CommandSent += HandleMessages;

}

public void LogIn(string serverName, string userName, string password, bool activeConnectionMode)
{

Log.Info("Connecting to {0} ...", serverName);

{
this.FtpServer.ServerAddress = serverName;
this.FtpServer.UserName = userName;
this.FtpServer.Password = password;
this.FtpServer.Connect();

if (activeConnectionMode)
{
Log.Debug("Active mode enabled");
this.FtpServer.ConnectMode = EnterpriseDT.Net.Ftp.FTPConnectMode.ACTIVE;
}
else
{
Log.Debug("Passive mode enabled");
this.FtpServer.ConnectMode = EnterpriseDT.Net.Ftp.FTPConnectMode.PASV;
}

this.FtpServer.TransferType = EnterpriseDT.Net.Ftp.FTPTransferType.BINARY;
}
}


public void DownloadFolder(string localFolder, string remoteFolder, bool recursive)
{

this.FtpServer.ChangeWorkingDirectory(remoteFolder);

EnterpriseDT.Net.Ftp.FTPFile[] FtpServerFileInfo = this.FtpServer.GetFileInfos();

string LocalTargetFolder = null;
string FtpTargetFolder = null;
bool DownloadFile = false;
string LocalFile = null;
System.IO.FileInfo fi = default(System.IO.FileInfo);

if (!System.IO.Directory.Exists(localFolder))
{
Log.Debug("creating {0}", localFolder);
System.IO.Directory.CreateDirectory(localFolder);
}

foreach (EnterpriseDT.Net.Ftp.FTPFile CurrentFileOrDirectory in FtpServerFileInfo)
{
if (recursive)
{
if (CurrentFileOrDirectory.Dir && CurrentFileOrDirectory.Name != "." && CurrentFileOrDirectory.Name != "..")
{

LocalTargetFolder = System.IO.Path.Combine(localFolder, CurrentFileOrDirectory.Name);
FtpTargetFolder = string.Format("{0}/{1}", remoteFolder, CurrentFileOrDirectory.Name);

if (!System.IO.Directory.Exists(LocalTargetFolder))
{
Log.Debug("creating {0}", LocalTargetFolder);
System.IO.Directory.CreateDirectory(LocalTargetFolder);
}

DownloadFolder(LocalTargetFolder, FtpTargetFolder, recursive);

//set the ftp working folder back to the correct value
this.FtpServer.ChangeWorkingDirectory(remoteFolder);
}
}

if (!CurrentFileOrDirectory.Dir)
{
DownloadFile = false;

LocalFile = System.IO.Path.Combine(localFolder, CurrentFileOrDirectory.Name);


// check file existence
if (!System.IO.File.Exists(LocalFile))
{
DownloadFile = true;
}
else
{
//check file size
fi = new System.IO.FileInfo(LocalFile);
if (CurrentFileOrDirectory.Size != fi.Length)
{
DownloadFile = true;
System.IO.File.Delete(LocalFile);
}
else
{
//check modification time
if (CurrentFileOrDirectory.LastModified != fi.CreationTime)
{
DownloadFile = true;
System.IO.File.Delete(LocalFile);

}
}
}


if (DownloadFile)
{
Log.Debug("Downloading {0}", CurrentFileOrDirectory.Name);
this.FtpServer.DownloadFile(localFolder, CurrentFileOrDirectory.Name);

fi = new System.IO.FileInfo(LocalFile);
fi.CreationTime = CurrentFileOrDirectory.LastModified;
fi.LastAccessTime = CurrentFileOrDirectory.LastModified;
fi.LastWriteTime = CurrentFileOrDirectory.LastModified;
}

}

}
}


public void UploadFolder(string remoteFolder, string localFolder, bool recursive)
{

string[] LocalFiles = null;

LocalFiles = System.IO.Directory.GetFiles(localFolder, "*.*");
this.FtpServer.ChangeWorkingDirectory(remoteFolder);


// remove the local folder value, so we can work relative
for (int i = 0; i <= LocalFiles.Length - 1; i++)
{
LocalFiles[i] = LocalFiles[i].Remove(0, localFolder.Length + 1);
}


//upload files
//FtpServer.Exists throws an error, so we must do it ourselves
EnterpriseDT.Net.Ftp.FTPFile[] FtpServerFileInfo = this.FtpServer.GetFileInfos();


foreach (var LocalFile in LocalFiles)
{
if (!FileExistsAtFtp(FtpServerFileInfo, LocalFile))
{
this.FtpServer.UploadFile(System.IO.Path.Combine(localFolder, LocalFile), LocalFile);
}
else
{
if (FileIsDifferentAtFtp(FtpServerFileInfo, LocalFile, localFolder))
{
this.FtpServer.DeleteFile(LocalFile);
this.FtpServer.UploadFile(System.IO.Path.Combine(localFolder, LocalFile), LocalFile);
}

}
}


if (!recursive) return;

//upload folders
string[] Folders = null;

string LocalTargetFolder = null;
string FtpTargetFolder = null;


Folders = System.IO.Directory.GetDirectories(localFolder);

// remove the local folder value, so we can work relative
for (int i = 0; i <= Folders.Length - 1; i++)
{
Folders[i] = Folders[i].Remove(0, localFolder.Length + 1);
}


foreach (var Folder in Folders)
{
//explicit set the folder back, because of recursive calls
this.FtpServer.ChangeWorkingDirectory(remoteFolder);


if (!FolderExistsAtFtp(FtpServerFileInfo, Folder))
{
this.FtpServer.CreateDirectory(Folder);
}

LocalTargetFolder = System.IO.Path.Combine(localFolder, Folder);
FtpTargetFolder = string.Format("{0}/{1}", remoteFolder, Folder);

UploadFolder(FtpTargetFolder, LocalTargetFolder, recursive);
}
}

public void DisConnect()
{
this.FtpServer.Close();
}


public bool IsConnected()
{
return this.FtpServer.IsConnected;
}


public string CurrentWorkingFolder()
{
return this.FtpServer.ServerDirectory;
}

private bool FileExistsAtFtp(EnterpriseDT.Net.Ftp.FTPFile[] ftpServerFileInfo, string localFileName)
{

bool Found = false;

foreach (EnterpriseDT.Net.Ftp.FTPFile CurrentFileOrDirectory in ftpServerFileInfo)
{
if (!CurrentFileOrDirectory.Dir && CurrentFileOrDirectory.Name.ToLower() == localFileName.ToLower())
{
Found = true;
}
}

return Found;
}

private bool FolderExistsAtFtp(EnterpriseDT.Net.Ftp.FTPFile[] ftpServerFileInfo, string localFileName)
{

bool Found = false;
string updatedFolderName = null;

foreach (EnterpriseDT.Net.Ftp.FTPFile CurrentFileOrDirectory in ftpServerFileInfo)
{
if (CurrentFileOrDirectory.Name.EndsWith("/"))
{
updatedFolderName = CurrentFileOrDirectory.Name.Remove(CurrentFileOrDirectory.Name.Length - 1, 1);
}
else
{
updatedFolderName = CurrentFileOrDirectory.Name;
}

if (CurrentFileOrDirectory.Dir && updatedFolderName.ToLower() == localFileName.ToLower())
{
Found = true;
}
}

return Found;
}

private bool FileIsDifferentAtFtp(EnterpriseDT.Net.Ftp.FTPFile[] ftpServerFileInfo, string localFile, string localFolder)
{
bool isDifferent = false;
System.IO.FileInfo fi = default(System.IO.FileInfo);


foreach (EnterpriseDT.Net.Ftp.FTPFile CurrentFileOrDirectory in ftpServerFileInfo)
{
if (!CurrentFileOrDirectory.Dir && CurrentFileOrDirectory.Name.ToLower() == localFile.ToLower())
{
fi = new System.IO.FileInfo(System.IO.Path.Combine(localFolder, localFile));

if (fi.Length != CurrentFileOrDirectory.Size || fi.LastWriteTime != CurrentFileOrDirectory.LastModified)
{
isDifferent = true;
}
}
}

return isDifferent;
}

private void HandleMessages(object sender, EnterpriseDT.Net.Ftp.FTPMessageEventArgs e)
{
Log.Info(e.Message);
}
}
}

Step 3 : the task class with the real actions

Back in the 1.4.4 (and earlier) code base, you only had to implement ITask, and it worked, as you can read in one of my previous posts. Now this still works, but you will not be able to use any of the new features of the 1.5 code base. The change is very small though, instead of implementing ITask, inherit TaskBase. This implements ITask as well as supplies the base for the new features. The background info you can read here. Ok the code :
using System;
using System.Collections.Generic;
using System.Text;
using Exortech.NetReflector;
using ThoughtWorks.CruiseControl.Core.Util;

namespace ThoughtWorks.CruiseControl.Core.Tasks
{
[ReflectorType("ftp")]
class FtpTask : TaskBase
{
public enum FtpAction
{
UploadFolder,
DownloadFolder
}

[ReflectorProperty("serverName", Required = true)]
public string ServerName = string.Empty;

[ReflectorProperty("userName", Required = true)]
public string UserName = string.Empty;

[ReflectorProperty("password", Required = true)]
public string Password = string.Empty;

[ReflectorProperty("useActiveConnectionMode", Required = true)]
public bool UseActiveConnectionMode = true;

[ReflectorProperty("action", Required = true)]
public FtpAction Action = FtpAction.DownloadFolder;

[ReflectorProperty("ftpFolderName", Required = true)]
public string FtpFolderName = string.Empty;

[ReflectorProperty("localFolderName", Required = true)]
public string LocalFolderName = string.Empty;

[ReflectorProperty("recursiveCopy", Required = true)]
public bool RecursiveCopy = true;

protected override bool Execute(IIntegrationResult result)
{
result.BuildProgressInformation.SignalStartRunTask(!string.IsNullOrEmpty(Description) ? Description : "Ftp");

string remoteFolder = FtpFolderName;
FtpLib ftp = new FtpLib();

try
{
ftp.LogIn(ServerName, UserName, Password, UseActiveConnectionMode);

if (!FtpFolderName.StartsWith("/"))
{
remoteFolder = ftp.CurrentWorkingFolder() + "/" + FtpFolderName;
}

if (Action == FtpAction.UploadFolder)
{
Log.Debug("Uploading {0} to {1}, recursive : {2}", LocalFolderName, remoteFolder, RecursiveCopy);
ftp.UploadFolder(remoteFolder, LocalFolderName, RecursiveCopy);
}

if (Action == FtpAction.DownloadFolder)
{
Log.Debug("Downloading {0} to {1}, recursive : {2}", remoteFolder, LocalFolderName, RecursiveCopy);
ftp.DownloadFolder(LocalFolderName, remoteFolder, RecursiveCopy);
}

}
catch (Exception ex)
{
// try to disconnect in a proper way on getting an error
if (ftp != null)
{
try
{ // swallow exception on disconnect to keep the original error
if (ftp.IsConnected()) ftp.DisConnect();
}
catch { }
}
throw ex;
}

return true;
}

}
}


Not that much different than the 1.4.4 code base, the only main difference you notice know is that propery Description is now in the base class.
And voila, a ftp task / publisher for CCNet. Now before you all start copying and pasting, this code is part of the latest builds of CCNet. In the next posts, I'll update this. Things I have in mind : showing the files being downloaded in the build progress, iso only in the debug log, providing dynamic parameters, ...
Stay tuned.

4 comments:

  1. There is some good news - by inheriting from TaskBase you automatically get dynamic parameter support :-)

    There is also a new public property on TaskBase called CurrentStatus. This property contains the status information that is used in CCTray and the dashboard. You could add a sub-item to this for each file being transferred, and it would appear automatically in the status views.


    Craig

    ReplyDelete
  2. Hey Rubin, I just found your blog! Nice post I look forward to seeing this included in a future release.

    ReplyDelete
  3. added the code of the FTP lib, as it was not included before :-(

    ReplyDelete
  4. This waas a lovely blog post

    ReplyDelete