Download a child document using the .Net client

Background

Today I fielded a request for help downloading a Record Revision using the ServiceAPI .Net client.  Thinking this should be simple I prepared the sample code below, only to find it failed.  Similar sample code works when downloading a document.  Turns out we have a bug.

ChildDownload request = new ChildDownload();
request.ChildUri = 12;
request.Id = "1843";
request.TrimType = BaseObjectTypes.Record;
request.TrimChildType = BaseObjectTypes.RecordRevision;

trimClient.GetDocument(request, @"d:\junk");

The problem

Inside the GetDocument method there is a routine to work out the name of the document from the content disposition, unfortunately the child documents have a differently format content disposition to the standard document download and our code to parse the content disposition to extract the file name was failing.  So the document gets downloaded but never saved to disk.

Content-Disposition

The purpose of the content disposition header is to suggest a file name for the file we are downloading.  When fetching a document (e.g. http://MyServer/HPRMServiceAPI/Record/1843/File/Document) the content disposition sent by the ServiceAPI looks like this:

Content-Disposition: attachment; filename="test.XML"

Update: The URL to download a child document in recent versions of the ServiceAPI is different to the one below, it look like this:

http://MyServer/ServiceAPI/record/9000000001/children/recordrevision/9000000001

The ServiceAPI uses a slightly different mechanism for downloading a child document (e.g. http://MyServer/HPRMServiceAPI/Record/1843/RecordRevision/12).  And the content disposition sent looks different, like this: 

Content-Disposition: attachment; filename="0F3P09LF001.PDF"; size=3881; creation-date=Wed 27 May 2015 00:38:08 GMT; modification-date=Thu 28 May 2015 09:45:43 GMT; read-date=Wed 27 May 2015 00:38:08 GMT

The solution

I could write a bespoke file download using the .Net WebClient but I can also patch the TrimClient to add a working child download method.  I chose to do the latter.  Below is a C# extension which adds the method MyGetChildDocument to TrimClient.  If the content disposition parser fails it tries to extract using a regular expression, it that fails it uses a fallback name as passed in the method parameters.

public static class ClientExtensions
{
    public static string MyGetChildDocument(
        this TrimClient trimClient, 
        ChildDownload request, 
        string fileDownloadPath, string fallbackName = "temp.tmp")
    {
        if (string.IsNullOrEmpty(fileDownloadPath))
        {
            fileDownloadPath = Environment.CurrentDirectory;
        }

        string docPath = null;
        trimClient.ServiceClient.LocalHttpWebResponseFilter = (response) =>
        {
            string fileName = null;
            string contentDispHeader
                = response.GetResponseHeader("Content-Disposition");

            if (!string.IsNullOrEmpty(contentDispHeader))
            {

                try
                {
                    ContentDisposition contentDisp
                        = new ContentDisposition(contentDispHeader);
                    fileName = contentDisp.FileName;
                }
                catch
                {
                    // content disposition must not match what the ContentDisposition object expects
                }

                if (fileName == null)
                {
                    Match match
                        = Regex.Match(contentDispHeader, "filename=\"([^)]*)\"");

                    if (match.Success)
                    {
                        fileName = match.Groups[1].Value;
                    }
                }

            }

            if (fileName == null)
            {
                fileName = fallbackName;
            }

            docPath = Path.Combine(fileDownloadPath, fileName);
            using (FileStream fs
                = File.Open(docPath, FileMode.Create, FileAccess.Write))
            {
                response.GetResponseStream().CopyTo(fs);
                fs.Close();
            }
        };

        trimClient.ServiceClient.Get(request);
        return docPath;
    }
}

Implementing

The full extension can be downloaded here, simply include it in your project, change its namespace and then make a call like this:

ChildDownload request = new ChildDownload();
request.ChildUri = 12;
request.Id = "1843";
request.TrimType = BaseObjectTypes.Record;
request.TrimChildType = BaseObjectTypes.RecordRevision;

string filePath = trimClient.MyGetChildDocument(request, @"d:\junk");
Written on May 28, 2015