Pages

Saturday, September 25, 2010

Progress Streamed File download and Upload with Resume facility


For distributed applications, WCF is the most easy and rich component.I like to use WCF as it is very easy to implement and also provides built in functions to handle with Complex problems. One of such interesting thing that I found in WCF is the support for Streaming while sending data from the service.

WCF allows you to send Byte streams through the communication channel or even allows you to implement a service that might take the stream start location from the service call and send the stream from a certain location. It allows you to use normal HttpBinding to support streams which is capable download or upload in connected Client - Server architecture. Hence there will be not timeouts for sending or receiving large files. In this article I will show you how you could use Streaming to ensure your files are downloaded or uploaded correctly.

Once I read Demitris solution for File upload and download it made me very easy to adjust the code and enhance it further with more flexibilities.

Lets first create the service step by step :

Service

  1. Create a Service Application and name it as WCFCommunicationService
  2. The first thing that you need to do in order to create a service is ServiceContract. So once you create a Service Library, you need to delete the existing Service1 and IService1 files from the solution and add a new Interface. 
  3. Create Two method, one for FileDownload and another for FileUpload. Lets see how the service definition looks like :
Contract

[ServiceContract]
public interface IFileTransferLibrary
{

    [OperationContract()]
    void UploadFile(ResponseFile request);

    [OperationContract]
    ResponseFile DownloadFile(RequestFile request);
}

The Contract defines the methods that are exposed to outside. The ServiceContract defines the interface which will have the members which are available to outside. The OperationContract identifies the exposed members. In the above contract the IFileTransferLibrary has two methods, UploadFile which takes an object of ResponseFile and DownloadFile which returns an object of ResponseFile.

[MessageContract]
public class ResponseFile : IDisposable
{
    [MessageHeader]
    public string FileName;

    [MessageHeader]
    public long Length;

    [MessageBodyMember]
    public System.IO.Stream FileByteStream;

    [MessageHeader]
    public long byteStart = 0;

    public void Dispose()
    {
        if (FileByteStream != null)
        {
            FileByteStream.Close();
            FileByteStream = null;
        }
    }
}

[MessageContract]
public class RequestFile
{
    [MessageBodyMember]
    public string FileName;

    [MessageBodyMember]
    public long byteStart = 0;
}

If you see the classes RequestFile and ResponseFile, you will see that I have used MessageContract instead of DataContract for my complex types. It is important. AS I am going to send the data streamed to the client, I need to send the data into packets. DataContract will send the whole object at a time while Messagecontract sends them into small Messages.It is also important to note that I have used IDisposable for ResponseFile to ensure that the Stream is closed whenever the connection is terminated.

Another important note is you need to use Stream as MessageBodyMember. The stream data will always transferred in MessageBody and you cannot add any other member into it. Thus you can see I have put all other members as MessageHeader.

For Both UploadFile and DownloadFile the Stream is taken from the ResponseFile object. The byteStart element of RequestFile ensures from where the the downloading should start and hence helps in Resuming a half downloaded file.  Lets see how to implement this :

public ResponseFile DownloadFile(RequestFile request)
{
    ResponseFile result = new ResponseFile();

    FileStream stream = this.GetFileStream(Path.GetFullPath(request.FileName));
    stream.Seek(request.byteStart, SeekOrigin.Begin);
    result.FileName = request.FileName;
    result.Length = stream.Length;
    result.FileByteStream = stream;
    return result;

}
private FileStream GetFileStream(string filePath)
{
    FileInfo fileInfo = new FileInfo(filePath);

    if (!fileInfo.Exists)
        throw new FileNotFoundException("File not found");

    return new FileStream(filePath, FileMode.Open, FileAccess.Read);
}
public void UploadFile(ResponseFile request)
{
           
    string filePath = Path.GetFullPath(request.FileName);
            
    int chunkSize = 2048;
    byte[] buffer = new byte[chunkSize];

    using (FileStream stream = new FileStream(filePath, FileMode.Append, FileAccess.Write))
    {
        do
        {
            int readbyte = request.FileByteStream.Read(buffer, 0, chunkSize);
            if (readbyte == 0) break;

            stream.Write(buffer, 0, readbyte);
        } while (true);
        stream.Close();
    }
}

If you see the code, you can see how I have used byteStart to start sending Bytes for the file. This ensures the file download to have resume facility. You can also implement the same for UploadFile too.

Download FileTransfer Sample - 161KB



ServerHost

After you create the Server application lets create the ServiceHost application. For the time being, I have used a Console application to demonstrate the thing. Lets add a new application to the solution and add the following lines in the Main method.

static void Main(string[] args)
{
    using (ServiceHost host = new ServiceHost(typeof(FileTransferLibrary)))
    {
        host.Open();
        Console.WriteLine("Service Started!");

        foreach (Uri address in host.BaseAddresses)
            Console.WriteLine("Listening on " + address);
        Console.WriteLine("Press any key to close...");
        Console.ReadKey();

        host.Close();
    }
}

Here I have used ServiceHost to host the Service. If you are going to use a Windows Service which I would probably like for this situation, I would have been writing the same code as you see above and use host.Open during the OnStart of the Service and host.Close during the OnStop of the service.

Finally lets configure the application. The configuration settings that I am going to use for the service is BasicHttpBinding with MTOM support.Lets see how my ServiceModel section of the service looks like :


<system.serviceModel>
    <behaviors>
      <serviceBehaviors>
        <behavior name="WCFCommunicationLibrary.FileTransferServiceBehavior">
          <serviceMetadata httpGetEnabled="true" />
          <serviceDebug includeExceptionDetailInFaults="false" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <bindings>
      <basicHttpBinding>
        <binding name="FileTransferServicesBinding"
          transferMode="Streamed"
          messageEncoding="Mtom"
          sendTimeout="01:05:00"
          maxReceivedMessageSize="10067108864">
        </binding>
      </basicHttpBinding>
    </bindings>
    <services>
      <service behaviorConfiguration="WCFCommunicationLibrary.FileTransferServiceBehavior"
        name="WCFCommunicationLibrary.FileTransferLibrary">
        <clear />
        <endpoint address="http://localhost:8080/FileDownloaderLibrary/" binding="basicHttpBinding"
          bindingConfiguration="FileTransferServicesBinding" contract="WCFCommunicationLibrary.IFileTransferLibrary">
        </endpoint>
        <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange">
        </endpoint>
        <host>
          <baseAddresses>
            <add baseAddress="http://localhost:8080/FileDownloader/" />
          </baseAddresses>
        </host>
      </service>
    </services>
  </system.serviceModel>

Just as I said, the service config is very simple with Streamed as TransferMode of ServiceBinding. I have used BasicHttpBinding for simplicity but you can easily use TCPBinding if you like to.The endpoint defines the address as http://localhost:8080/FileDownloaderLibrary/ the Binding as BasicHttpBinding and the contract with the WCFCommunicationLibrary.IFileTransferLibrary which is the basic contract for our service.

If you run the console you will see the service started running with the following message, which indicates that the service is available for Download or upload of Files.


You can host your Service class library anywhere you want. The probable hosting environment could be IIS, Windows Activation Service, Windows Service, .NET programs, etc.

Now lets create the client application to download/ upload the files to the server. Client is actually a consumer of the Service which lets you to add the service reference to the client and will produce the discovery files which helps you to access the Streamed services and also allows you to produce progress behaviour for the file. Lets create an application which has this functionality.



 While creating the client, you need to follow the steps :

  1. Add a service reference to the Client, so that you can call it. The Service Reference will automatically create the discovery files.
  2. Once you added, design your UI, I have used WPF to design my UI,
  3. Use server methods directly to store or send Stream or data.


public void StartDownloading()
{
    try
    {

        string filePath = System.IO.Path.Combine("Download", this.FileName);
        string fileName = this.FileName;

        Stream inputStream;

        long startlength = 0;
        FileInfo finfo = new FileInfo(filePath);
        if (finfo.Exists)
            startlength = finfo.Length; // If File exists we need to send the Start length to the server

        long length = this.Client.DownloadFile(ref fileName, ref startlength, out inputStream);

        using (FileStream writeStream = new System.IO.FileStream(filePath, FileMode.OpenOrCreate, FileAccess.Write))
        {

            int chunkSize = 2048;
            byte[] buffer = new byte[chunkSize];

            do
            {
    
                int bytesRead = inputStream.Read(buffer, 0, chunkSize);
                if (bytesRead == 0) break;

    
                writeStream.Write(buffer, 0, bytesRead);

    
                this.ProgressValue = (int)(writeStream.Position * 100 / length);
            }
            while (true);


            writeStream.Close();
        }

        inputStream.Dispose();
    }
    catch
    {

    }
}

The code allows you to save the file to the disk. You can see we can easily stop this operation by clicking on Pause Button. The notion of Pause button is actually to stop the Thread completely. Hence the operation will be stopped and the file will remain in the disk until it is downloaded. When you resume, the process is called again. As in the function we check the size of the file and send it back to the server, this will ensure that the server will start on sending the file from a specified point.

Note : I have modified Demitris Solution of FileDownload to add new flexibilities to the solution.

Download FileTransfer Sample - 161KB

I hope you would like the adjustments. Thank you for reading.

1 comment:

Please make sure that the question you ask is somehow related to the post you choose. Otherwise you post your general question in Forum section.