In order to upload a file from a Silverlight application to your webserver you generally provide a WCF service that accepts the filestream.
However, accepting a large file as a single chunk can sometimes lead to problems with service timeouts, request size restrictions etc. so I took the time to write a chunked file uploader service, which allows you to send the file in several parts. The way this works is similar to how a normal file stream works, in that you open a target file in exchange for a token/handle, write to the file using the token/handle and then close the file when you are finished. In addition to this, because we are dealing with async operations over HTTP we are required to ensure the file parts are committed in the correct order as they were intended when sent (i.e. sequencing the chunks).
Firstly, the interface for the IChunkedFileUploader is as follows:
[ServiceContract]
public interface IChunkedFileUpload
{
[OperationContract]
string OpenChunkedUploadFile();
[OperationContract]
Response WriteToChunkedUploadFile(string fileToken, int seq, byte[] data);
[OperationContract]
Response CloseChunkedUploadFile(string fileToken, int totalPartsSent);
}
The 'Response' class simply wraps two properties { bool, string } for the success flag and any error message.
The implementation of this interface in my WCF layer looks as follows:
public class FileUploadService : IChunkedFileUpload
{
private class FilePart
{
public int Seq { get; set; }
public byte[] Data { get; set; }
}
private static Dictionary<string, List<FilePart>> _FileParts = new Dictionary<string, List<FilePart>>();
public string OpenChunkedUploadFile()
{
string fileToken = "";
lock (_FileParts)
{
while (fileToken == "" || _FileParts.ContainsKey(fileToken))
{
fileToken = DateTime.Now.Ticks.ToString() + ".ccf";
}
_FileParts.Add(fileToken, new List<FilePart>());
}
return fileToken;
}
public Response CloseChunkedUploadFile(string fileToken, int totalPartsSent)
{
try
{
if(_FileParts.ContainsKey(fileToken))
{
if (_FileParts[fileToken].Count == totalPartsSent)
{
List<byte> fileData = new List<byte>();
for (int i = 0; i <= _FileParts[fileToken].Max(p=>p.Seq); i++)
{
IEnumerable<FilePart> seqPart = _FileParts[fileToken].Where(p => p.Seq == i);
if (seqPart.Count() == 1)
fileData.AddRange(seqPart.First().Data);
else
throw new System.IO.InvalidDataException("Data was invalid, not all sequences were present or were duplicated");
}
string fullFilePath = HttpContext.Current.Server.MapPath("UploadFiles\\" + fileToken);
System.IO.File.WriteAllBytes(fullFilePath, fileData.ToArray());
_FileParts[fileToken].Clear();
_FileParts.Remove(fileToken);
return new Response() { IsSuccess = true };
}
else
{
throw new System.IO.InvalidDataException("Data was invalid, total parts check failed.");
}
}
else{
throw new System.IO.FileNotFoundException();
}
}
catch (Exception ex)
{
return new Response<string>() { IsSuccess = false, MessageKey = ex.Message };
}
}
public Response WriteToChunkedUploadFile(string fileToken, int seq, byte[] data)
{
try
{
if (_FileParts.ContainsKey(fileToken))
{
_FileParts[fileToken].Add(new FilePart() { Data = data, Seq = seq });
return new Response() { IsSuccess = true };
}
else
{
throw new System.IO.FileNotFoundException();
}
}
catch (Exception ex)
{
return new Response() { IsSuccess = false, MessageKey = ex.Message };
}
}
}
Basically, what this does is creates a shared 'File system' on the server which is backed by a Dictionary<string, List<FilePart>> - the token/handle and the file parts received.
In Silverlight, or any other client, you call 'OpenChunkedUploadFile' to receive a unique token which you will use to send your file parts. This initialises a new item in the dictionary of files. The token will also be used as the final filename, so that you can calculate the remote URL of the eventual file.
You then loop through your target file bytes, sending chunks of your desired size to the 'WriteToChunkedUploadFile' function. You mark each file part with the sequence number to ensure the file is rebuilt in the correct order. At this point all the service is doing is adding to the List
for the current file in the Dictionary.
When all of your bytes are sent, you call 'CloseChunkedUploadFile' passing your token and the count of total file parts (to ensure all parts were received). After performing some checks on the file parts (e.g. number of parts, sequence contiguity) the file parts are arranged in sequence order to create the full file bytes of the file, which is then committed to the physical disk on the server (using a pre-defined folder and the token file name).
I have written an upload helper class which shows this:
public class ChunkedFileUploadHelper
{
#region "Fields"
private int _FileChunkSize;
private int _fileUploadSequence = 0;
private int _fileUploadPartSuccessCount = 0;
private ChunkedUploadFileInfo _fileInfo;
#endregion
#region "Events"
public delegate void UploadAsyncCompletedEventHandler(ChunkedFileUploadHelper sender, string serverFileToken);
public event UploadAsyncCompletedEventHandler UploadAsyncCompleted;
protected void OnUploadAsyncCompleted()
{
if (UploadAsyncCompleted != null)
UploadAsyncCompleted(this, _fileInfo.ServerToken);
}
#endregion
public ChunkedFileUploadHelper(ChunkedUploadFileInfo fileInfo) : this(fileInfo, 512000)
{ }
public ChunkedFileUploadHelper(ChunkedUploadFileInfo fileInfo, int chunkSize)
{
if (fileInfo == null)
throw new ArgumentNullException("fileInfo", "fileInfo cannot be NULL");
_fileInfo = fileInfo;
_FileChunkSize = chunkSize;
}
public void UploadAsync()
{
if (_fileInfo != null && _fileInfo.FileData.Length > 0)
{
_fileUploadSequence = 0;
_fileUploadPartSuccessCount = 0;
_fileInfo.BytesUploaded = 0;
ServiceInerfaceClient svc = ServiceUtility.GetChunkedFileUploadClient();
svc.OpenChunkedUploadFileCompleted += new EventHandler<OpenChunkedUploadFileCompletedEventArgs>(svc_OpenChunkedUploadFileCompleted);
svc.OpenChunkedUploadFileAsync();
}
else
{
throw new InvalidOperationException("Cannot start upload as there was no file data to upload");
}
}
void svc_OpenChunkedUploadFileCompleted(object sender, OpenChunkedUploadFileCompletedEventArgs e)
{
((ServiceInerfaceClient)sender).OpenChunkedUploadFileCompleted -= svc_OpenChunkedUploadFileCompleted;
if (e.Error == null && !string.IsNullOrEmpty(e.Result))
{
_fileInfo.ServerToken = e.Result;
Upload_part2();
}
else
{
if (e.Error != null)
throw e.Error;
else
throw new Exception("Did not receive valid token from the server");
}
}
private void Upload_part2()
{
ServiceInerfaceClient svc = ServiceUtility.GetChunkedFileUploadClient();
svc.WriteToChunkedUploadFileCompleted += new EventHandler<WriteToChunkedUploadFileCompletedEventArgs>(svc_WriteToChunkedUploadFileCompleted);
Upload_part2_recursion(svc);
}
private void Upload_part2_recursion(ServiceInerfaceClient svc)
{
byte[] bytesToSend = _fileInfo.FileData.Skip(_fileUploadSequence * _FileChunkSize).Take(_FileChunkSize).ToArray();
if (bytesToSend.Length > 0)
{
svc.WriteToChunkedUploadFileAsync(_fileInfo.ServerToken, _fileUploadSequence, bytesToSend, bytesToSend.Length);
_fileUploadSequence++;
}
}
void svc_WriteToChunkedUploadFileCompleted(object sender, WriteToChunkedUploadFileCompletedEventArgs e)
{
if (e.Error == null && e.Result != null && e.Result.IsSuccess)
{
_fileInfo.BytesUploaded += (int)e.UserState;
_fileUploadPartSuccessCount++;
if (_fileInfo.BytesUploaded == _fileInfo.FileData.Length && _fileUploadPartSuccessCount == _fileUploadSequence)
{
((ServiceInerfaceClient)sender).WriteToChunkedUploadFileCompleted -= svc_WriteToChunkedUploadFileCompleted;
Upload_part3();
}
else
{
Upload_part2_recursion(((ServiceInerfaceClient)sender));
}
}
else
{
if (e.Error != null)
throw e.Error;
else if (e.Result != null)
throw new Exception(e.Result.MessageKey);
else
throw new Exception("Unknown Error Uploading File to Server");
}
}
private void Upload_part3()
{
ServiceInerfaceClient svc = ServiceUtility.GetChunkedFileUploadClient();
svc.CloseChunkedUploadFileCompleted += new EventHandler<CloseChunkedUploadFileCompletedEventArgs>(svc_CloseChunkedUploadFileCompleted);
svc.CloseChunkedUploadFileAsync(_fileInfo.ServerToken, _fileUploadSequence);
}
void svc_CloseChunkedUploadFileCompleted(object sender, CloseChunkedUploadFileCompletedEventArgs e)
{
if (e.Error == null && e.Result != null && e.Result.IsSuccess)
{
OnUploadAsyncCompleted();
}
else
{
if (e.Error != null)
throw e.Error;
else if (e.Result != null)
throw new Exception(e.Result.MessageKey);
else
throw new Exception("Unknown Error Committing File to Server");
}
}
}
The 'ChunkedFileUploadInfo' class is as follows:
public class ChunkedUploadFileInfo : INotifyPropertyChanged
{
#region "Client Side File Info"
private string _OriginalFilename;
public string OriginalFilename
{
get { return _OriginalFilename; }
set { _OriginalFilename = value; OnPropertyChanged("OriginalFilename"); }
}
private byte[] _fileData;
public byte[] FileData
{
get { return _fileData; }
set { _fileData = value; OnPropertyChanged("FileData"); }
}
#endregion
#region "Upload FIle Info"
private string _ServerToken;
public string ServerToken
{
get { return _ServerToken; }
set { _ServerToken = value; OnPropertyChanged("ServerToken"); }
}
private int _bytesUploaded;
public int BytesUploaded
{
get { return _bytesUploaded; }
set { _bytesUploaded = value; OnPropertyChanged("BytesUploaded"); }
}
#endregion
#region "Notify Property Changed"
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}