Monday, November 15, 2010

Uploading a file using FTP and the WebClient

Note: more detailed documentation for the .NET 4.0 explains the reason for requiring the additional slash (/). See the remarks section for the FtpWebRequest class. If you are using .NET 4.0, this class is probably a better bet.

I ran into some trouble uploading a file using the System.Net.WebClient. Looking at examples in the web, it seemed quite straightforward, but I found that I needed to include an additional forward slash in order to get the transfer to work correctly.

At first, I struggled to upload a file with this call to my WebClient instance:

client.UploadFile("ftp://sssssss/folder/folder/folder/t.txt", @"c:\temp\t.txt");

I was able to download a file in that location with Internet Explorer using the URI above, but not upload to it using the WebClient.

This dump from Wireshark illustrates the problem (note line 14), which seems to be a bug in the WebClient to me:

220 sssssss FTP server (Version 1.1.214.4(PHNE_38458) Tue Jul 29 07:36:52 GMT 2008) ready.
USER uuuuuuu
331 Password required for uuuuuuu.
PASS ppppppp
230 User uuuuuuulogged in.
OPTS utf8 on
500 'OPTS utf8 on': command not understood.
PWD
257 "/home/fld/uuuuuuu" is current directory.
TYPE I
200 Type set to I.
PASV
227 Entering Passive Mode (xx,xx,xx,xx,xx,xx)
STOR folder/folder/folder/t.txt
553 Could not determine cwdir: No such file or directory.
221 You could at least say goodbye.

Noticing that the STOR command was providing a relative path (not starting with /), I assume that the server was trying to save the file into the current folder i.e. /home/fld/uuuuuuu/folder/folder/folder/t.txt. To test this theory, I added a slash at the beginning ot the path:

client.UploadFile("ftp://sssssss//folder/folder/folder/t.txt", @"c:\temp\t.txt");

I am not sure if this is a valid URI, but it seems to work with the WebClient, although I am not sure why the RETR command (line 19) is executed as in the following Wireshark dump:

220 sssssss FTP server (Version 1.1.214.4(PHNE_38458) Tue Jul 29 07:36:52 GMT 2008) ready.
USER uuuuuuu
331 Password required for uuuuuuu.
PASS ppppppp
230 User uuuuuuu logged in.
OPTS utf8 on
500 'OPTS utf8 on': command not understood.
PWD
257 "/home/fld/uuuuuuu" is current directory.
TYPE I
200 Type set to I.
PASV
227 Entering Passive Mode (xx,xx,xx,xx,xx,xx)
STOR /folder/folder/folder/t.txt
150 Opening BINARY mode data connection for /folder/folder/folder/t.txt.
226 Transfer complete.
PASV
227 Entering Passive Mode (xx,xx,xx,xx,xx,xx)
RETR folder/folder/folder/t.txt
550 folder/folder/folder/t.txt: No such file or directory.
221 You could at least say goodbye.


This is the final code that seems to work:

        public static void SendFile(string sourcePath, string server, string userName, string password, string remoteFolder, string remoteFileName)
        {
            using (WebClient client = new WebClient())
            {
                string dest = string.Format(CultureInfo.InvariantCulture, "ftp://{0}/{1}/{2}", server, remoteFolder, remoteFileName);
                client.Credentials = new NetworkCredential(userName, password);
                client.Proxy = null;
                client.UploadFile(dest, sourcePath);
            }
        }

And the corresponding integration test:

        [TestMethod]
        public void ShouldSubmitFileToFtpServer()
        {
            Guid uniqueId = Guid.NewGuid();

            string tempFile = Path.Combine(Path.GetTempPath(), "FtpTestFile.txt");
            File.WriteAllText(tempFile, "Test file - safe to delete. " + uniqueId.ToString());

            string server = "sssssss";
            string userName = "uuuuuuu";
            string password = "ppppppp";
            string folder = "/folder/folder/folder";
            string fileName = "t.txt";
            Ftp.SendFile(tempFile, server, userName, password, folder, fileName);

            string tempFileDown = tempFile + ".down";

            if(File.Exists(tempFileDown)) File.Delete(tempFileDown);

            using (WebClient client = new WebClient())
            {
                client.Credentials = new NetworkCredential(userName, password);
                string ftpLink = String.Format(CultureInfo.InvariantCulture, "ftp://{0}/{1}/{2}", server, folder, fileName);
                client.CachePolicy = new HttpRequestCachePolicy(HttpRequestCacheLevel.NoCacheNoStore);
                client.DownloadFile(ftpLink, tempFileDown);
            }

            string content = File.ReadAllText(tempFileDown);

            Assert.IsTrue(content.Contains(uniqueId.ToString()));
        }

Thursday, November 11, 2010

Example PowerShell header

This is a good template header that I use for new PowerShell scripts:

<#
.SYNOPSIS
    This is what the script does.
.NOTES
    Author: Me
.PARAMETER AParameter
    A parameter.
.PARAMETER ASwitch
    A switch.
#>

$ErrorActionPreference = 'Stop'
Set-StrictMode -Version 3

param(
    [parameter(mandatory)][string]$AParameter, 
    [switch]$ASwitch)

$scriptFolder = (split-path -path $MyInvocation.MyCommand.Definition -Parent)

Friday, November 5, 2010

Transforming an XML file with an XSLT in PowerShell

This code will transform an XML file with an XSLT file and save the resulting file to disk:

$xslt = new-object system.xml.xsl.xslcompiledtransform
$xslt.load('transform.xslt')
$xslt.Transform('input.xml', 'output.xml')

Wednesday, November 3, 2010

Complex work item queries in TFS

I recently wanted to create a work item query that showed all my work items that are either not closed or I had modified recently. This query provides a useful list of work items for associating with a check-in.

I wanted the query to show:
AssignedTo = @Me AND (State <> Closed OR (State = Closed AND ChangedDate > @Today - 7))

To do this, I needed to use multiple levels of the Group Clauses feature, which is available by selecting more than one clause and right clicking on one of the selected clauses.

The result is below:

Friday, October 29, 2010

Executing a PowerShell script from TFS 2010 workflow

  1. Add an InvokeProcess activity to the workflow.
  2. Set the DisplayName property to something useful for logging and debugging.
  3. Set the FileName property to "powershell.exe"
  4. Set the Arguments like this:
    "-file """ + SourcesDirectory + "\Scripts\SetAssemblyVersions.ps1"" " + MajorVersionNumber.ToString() + " " + MinorVersionNumber.ToString() + " " + BuildDetail.SourceGetVersion.Substring(1) + " " + BuildDetail.BuildNumber.Substring(BuildDetail.BuildNumber.LastIndexOf(".") + 1)
  5. Give names to the standard output and error output streams.
  6. Add a WriteBuildMessage activity under the Handle Standard Output field and set its Message property to the standard output stream defined above.
  7. Add a WriteBuildError activity under the Handle Error Output field and set its Message property to the error output stream defined above.

Thursday, October 28, 2010

Quotes I like

Give me six hours to chop down a tree and I will spend the first four sharpening the axe.
-- Abraham Lincoln

If you always put limit on everything you do, physical or anything else, it will spread into your work and into your life. There are no limits. There are only plateaus, and you must not stay there, you must go beyond them.
-- Bruce Lee

If I’d asked people what they wanted, they would have said a faster horse.
-- Henry Ford, car guy

Yesterday I tried to cut my steak with a spoon and that goddam spoon sucked-ass. Why the hell would anybody ever use a spoon for anything? They are completely useless!
-- Jason

I think that emphasizing heroics in any form usually does more harm than good.
-- Steve McConnell

Setting up FitNesse for .NET with SliM

Basic steps for setting up FitNesse for .NET with the wiki stored under source control. Review all code that you copy and paste from this page because it may need to be modified for your situation.

  1. Create a simple fixture:
    public class IsFitNesseCorrectlySetup
    {
        public int Value { get; set; }
    
        public int GetValueDoubled()
        {
            return Value * 2;
        }
    }
    
  2. Download FitNesse and SliM, and save these into separate folders under your third party folder in source control (everything here is presumed to be under source control).
  3. Create this .cmd file for testers to use (presumes TFS; will need to be modified for other source control systems):
    @echo off
    setlocal
    "%VS100COMNTOOLS%\..\IDE\TF.exe" get "%~dp0.." -r
    "%VS100COMNTOOLS%\..\IDE\TF.exe" checkout "%~dp0FitNesseRoot" -r
    C:\Windows\Microsoft.NET\Framework64\v4.0.30319\MSBuild.exe "%~dp0..\Solution\Solution.sln" /p:configuration=release
    set FitNessePort=8084
    start /min /d "%~dp0..\ThirdParty\FitNesse" java -jar fitnesse.jar -p %FitNessePort% -d "%~dp0." -r FitNesseRoot
    start http://localhost:%FitNessePort%/
    endlocal
  4. Launch FitNesse and add the following to the root page (modifying paths to match your folder structure):
    !*****> Configuration
    !define TEST_SYSTEM {slim}
    !path .dll
    !define COMMAND_PATTERN {%m -r fitSharp.Slim.Service.Runner,..\..\ThirdParty\FitSharp\fitsharp.dll %p}
    !define TEST_RUNNER {..\..\ThirdParty\FitSharp\Runner.exe}
    *****!
  5. Add a normal page (not a test page) called SetUp at the top level of your suite, and put the following in it to let FitNesse know which namespaces to find your fixtures in:
    !|import|
    |Your Fixture DLL namespace|
  6. Create a test page with this test:
    !|Is FitNesse correctly set up|
    |Value   |Get value doubled?  |
    |2       |4                   |
    |23      |46                  |
    |100     |200                 |
    |200     |399<_<401           |
    Run the test, which should pass.
Note that these files in the FitNesse wiki should not be checked in to source control:
  • *.zip
  • ErrorLogs\**
  • files\testResults\**
  • RecentChanges\**