Wednesday, January 31, 2007

My first MsBuild task.

I frequently used MsBuild in my build scripts, but really didn't have a chance to actually write a task myself.

One particular task I face is to publish the files in a directory to the FTP server. So, I googled, and tried to find a solution for this. The first hit I got back is http://msbuildtasks.tigris.org/, which is a great community project. It includes a lot useful MsBuild tasks, include a FTP task. But what I wanted to FTP is a whole directory, not just single website. So, I started to write a task called FtpDirectoy, which will loop through all the qualified files, and ftp them onto the server.

The task is not difficult to write, basically, a new class has to be inherited from Microsoft.Build.Utilities.Task, then you set up a bunch of properties, finally, you have to override public override bool Execute(), and that will actually do the task.

The deployment is quite tricky, I copied the file at C:\Program Files\MSBuild\MSBuildCommunityTasks, and created a target file "CrystalGis.MsBuild.Tasks.Targets", which will then be included by the other project file.

Here the frustration comes, no matter what I do, the build task always told me that it couldn't find the task. Finally, I found an answer at http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=122927&SiteID=1

[Keith Hill:Try adding the full path to the project file to the SafeImports reg key in the registry. The location of the regkey is:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\8.0\MSBuild\SafeImports
I saw this problem just the other day and this fixed it. A better way to do this is to put your <UsingTask ...> in some <pick_a_name>.targets file and drop this targets file into a location that you can <Import ...> into your project file. Then put the full path to the targets file into the SafeImports section of the registry. This way, you only have to add the targets file once. The other way you would have to add each project file you use it in to SafeImports, well at least the ones you load into VS 2005. ]

After I added that entry,everything flies.

public class FtpDirectoy:Task
{
#region Constructor

public FtpDirectoy()
{

}

#endregion


#region Input Parameters

private string _ftpAddrerss;

/// <summary>
/// Gets or sets the FTP addrerss.
/// </summary>
/// <value>The FTP addrerss.</value>
public string FtpAddrerss
{
get { return _ftpAddrerss; }
set { _ftpAddrerss = value; }
}



private string _username;

/// <summary>
/// Gets or sets the username.
/// </summary>
/// <value>The username.</value>
public string Username
{
get { return _username; }
set { _username = value; }
}

private string _password;

/// <summary>
/// Gets or sets the password.
/// </summary>
/// <value>The password.</value>
public string Password
{
get { return _password; }
set { _password = value; }
}

private bool _usePassive;

/// <summary>
/// Gets or sets the behavior of a client application's data transfer process.
/// </summary>
/// <value><c>true</c> if [use passive]; otherwise, <c>false</c>.</value>
public bool UsePassive
{
get { return _usePassive; }
set { _usePassive = value; }
}


private ITaskItem[] _files;

/// <summary>
/// Gets or sets the files to zip.
/// </summary>
/// <value>The files to zip.</value>
[Required]
public ITaskItem[] Files
{
get { return _files; }
set { _files = value; }
}

private string _workingDirectory;

/// <summary>
/// Gets or sets the working directory for the zip file.
/// </summary>
/// <value>The working directory.</value>
/// <remarks>
/// The working directory is the base of the zip file.
/// All files will be made relative from the working directory.
/// </remarks>
public string WorkingDirectory
{
get { return _workingDirectory; }
set { _workingDirectory = value; }
}

#endregion


#region Task Overrides

public override bool Execute()
{
Log.LogMessage(CrystalGis.MsBuild.Tasks.Properties.Resources.FtpDirectory, _workingDirectory);
foreach (ITaskItem fileItem in _files)
{
string name = fileItem.ItemSpec;
if (!File.Exists(name))
{
continue;
}
//Ftp file here.......
string remoteUrl=GetUriFromRemoteName(name);
if (string.IsNullOrEmpty(remoteUrl))
continue;
FtpFile(name, remoteUrl );
}
return true;
}

#endregion Task Overrides

#region Private

/// <summary>
/// Gets the name of the URI from remote.
/// </summary>
/// <param name="fileNameWithPath">The file name with path.</param>
/// <returns></returns>
private string GetUriFromRemoteName(string fileNameWithPath)
{
string pathWithoutWorkingDirectory;
Log.LogMessage(Properties.Resources.GetUriForFile, fileNameWithPath);
int index = fileNameWithPath.IndexOf(_workingDirectory);
if (index > -1)
{
pathWithoutWorkingDirectory
= fileNameWithPath.Substring(index+_workingDirectory.Length+1);
return _ftpAddrerss +"/"+ pathWithoutWorkingDirectory.Replace("\\","/");
}
else
{
Log.LogError(Properties.Resources.FtpFileInvalid, fileNameWithPath);
return string.Empty;
}
}

/// <summary>
/// FTPs the file.
/// </summary>
/// <param name="localFile">The local file.</param>
/// <param name="remoteUri">The remote URI.</param>
private bool FtpFile(string localFile, string remoteUri)
{
Log.LogMessage(Properties.Resources.FtpUploading, localFile, remoteUri);
Uri ftpUri;
if (!Uri.TryCreate(remoteUri, UriKind.Absolute, out ftpUri))
{
Log.LogError(Properties.Resources.FtpUriInvalid, remoteUri);
return false;
}

FtpWebRequest request
= (FtpWebRequest)WebRequest.Create(ftpUri);
request.Method
= WebRequestMethods.Ftp.UploadFile;
request.UseBinary
= true;
request.ContentLength
= localFile.Length;
request.UsePassive
= _usePassive;
if (!string.IsNullOrEmpty(_username))
request.Credentials
= new NetworkCredential(_username, _password);

const int bufferLength = 2048;
byte[] buffer = new byte[bufferLength];
int readBytes = 0;
long totalBytes = localFile.Length;
long progressUpdated = 0;
long wroteBytes = 0;
FileInfo localFileInfo
= new FileInfo(localFile);
try
{
Stopwatch watch
= Stopwatch.StartNew();
using (Stream fileStream = localFileInfo.OpenRead(),
requestStream
= request.GetRequestStream())
{
do
{
readBytes
= fileStream.Read(buffer, 0, bufferLength);
requestStream.Write(buffer,
0, readBytes);
wroteBytes
+= readBytes;

// log progress every 5 seconds
if (watch.ElapsedMilliseconds - progressUpdated > 5000)
{
progressUpdated
= watch.ElapsedMilliseconds;
Log.LogMessage(MessageImportance.Low,
Properties.Resources.FtpPercentComplete,
wroteBytes
* 100 / totalBytes,
FormatBytesPerSecond(wroteBytes, watch.Elapsed.TotalSeconds,
1));
}
}
while (readBytes != 0);
}
watch.Stop();

using (FtpWebResponse response = (FtpWebResponse)request.GetResponse())
{
Log.LogMessage(MessageImportance.Low, Properties.Resources.FtpUploadComplete, response.StatusDescription);
Log.LogMessage(Properties.Resources.FtpTransfered,
FormatByte(totalBytes,
1),
FormatBytesPerSecond(totalBytes, watch.Elapsed.TotalSeconds,
1),
watch.Elapsed.ToString());
response.Close();
}
}
catch (Exception ex)
{
Log.LogErrorFromException(ex);
return false;
}

return true;
}



#endregion
}

No comments: