We at Appsolo have been charged with writing a SL 4 app that requires access to an FTP server for a CRM system that we are writing. The system will maintain licences for Flash based software products our business client produces at the moment. This package of software artefacts are uploaded by our SL client to an FTP server. A HTTP link to the licenced package is then produced to be emailed to the customers of our business client. This presents a number of problems that had to be solved.
Firstly you cannot access FTP directly from a SL client as the ports used are not available to the browser.
Secondly the SL client runs in a UI thread, ASP service(s) runs in another thread(s). I have two HTTP based ASP services. A first to create an FTP directory to place the files in. Upload speed needs to be addressed. A second ASP HTTP service to handle the streamed upload of the file. The timing of Threading of all these needs to handled.
Thirdly is the problem of reporting back to the SL client that the File has been delivered to the FTP server having first stopped off at the ASP service on the way. A multi streamed upload HTTP service as is used here is quick as it threads to handle requests. But you cannot access the HTTP response from the SL client as it is write and not a read. Also you do not want to tie up the client waiting for the slower FTP service to complete as called from the ASP service. I integrated a WCF Duplex Polling Service handle the polling of the FTP directory to report on the Files delivered and to decide when the upload is finished.
I’ll discuss things here that I found out in the course of bring this all together.
So part of the solution is to use an ASP based HTTP file streaming (WCF is not really built for this type of service – although it can be done) service to upload the files to a staging area Directory on the host website. We also needed to create a directory to store the files in on the FTP Server. This is where it starts to get tricky. You want to make sure that the directory is created before you start sending Files to it (or check if it exists already). So on the SL client your have to have nested delegates
NOTE: there a lot of different ideas mixed in here that I got from various people out there, some of which I cannot remember. I’ll list all the references at the end of the posts.
1: // Host value is set in ServiceReferences.ClientConfig
2: string hostbase = getAppSetting("Host");
3: UriBuilder packagehandlerUrl = new UriBuilder(hostbase + "FTPSetupPackageFolder.ashx");
4: string FTPDirName = "FTPpath=" + PackageTextBox.Text;
5: packagehandlerUrl.Query = FTPDirName;
6: WebClient FTPCreateDir = new WebClient();
7: // Call asp handler
8: FTPCreateDir.OpenReadAsync(packagehandlerUrl.Uri);
9: // Read the response sent back from the packagehandlerUrl call
10: FTPCreateDir.OpenReadCompleted += (read, ev) =>
11: {
12: int len = (int)ev.Result.Length;
13: byte[] b = new byte[len];
14: ev.Result.Read(b, 0, len);
15: // Check the return message to see if directory already exists and report
16: // Could put choice here
17: if(len == 0)
18: MessageBox.Show("Directory already exists ");
19: else MessageBox.Show("Directory Created " + System.Text.UTF8Encoding.UTF8.GetString(b,0,len) );
20: txtMessage.Text = "FTP Uploading........";
21: // Kick off FTP service Listing operation See end of this file
22: FtpListing();
23: // Do Upload
24: #region HTTP Handled Streamed Upload
25: foreach (FileInfo file in filesToUpload)
26: {
27: // This section uses Asynchronous streamed uploading to upload the selected Files
28: UriBuilder FilehandlerUrl = new UriBuilder(hostbase + "UploadFileHandler.ashx");
29: string InputFile = "InputFile=" + file.Name;
30: FilehandlerUrl.Query = InputFile + "&" + FTPDirName;
31:
32: string fileName = file.Name;
33: FileStream FsInputFile = file.OpenRead();
34: WebClient webClient = new WebClient();
35: //Now make an async class for writing the file to the server
36: //Here I am using Lambda Expression
37: webClient.Encoding = System.Text.UTF8Encoding.UTF8;
38: webClient.OpenWriteCompleted += (sent, evt) =>
39: {
40: try
41: {
42: // Do the actual streamed Upload which goes off on it's merry way
43: // and we continue
44: UploadFileData(FsInputFile, evt.Result);
45: evt.Result.Close();
46: FsInputFile.Close();
47: }
48: catch (Exception ex)
49: { MessageBox.Show(ex.Message); }
50:
51: };
52: webClient.OpenWriteAsync(FilehandlerUrl.Uri);
53:
54: }
55: #endregion
56: };
Lines 2 and 3 retrieve information from the ServiceReferences.ClientConfig file that is a handy place for storing Host based information which can be changed at deployment time after development and testing.
The contents if the ServiceReferences.ClientConfig XML file are something like this
1: <configuration>
2: <appSettings>
3: <add key="Host" value="http://[YourHost]/FTPFileServer/" />
4: <add key="FileServiceEndPoint" value="http://[YourHost or DifferentHost]/FTPFileServer/" />
5: </appSettings>
6: </configuration>
The associated Function to read this configuration file
1: private string getAppSetting(string strKey)
2: {
3: string strValue = string.Empty;
4: XmlReaderSettings settings = new XmlReaderSettings();
5: settings.XmlResolver = new XmlXapResolver();
6: XmlReader reader = XmlReader.Create("ServiceReferences.ClientConfig");
7: reader.MoveToContent();
8: while (reader.Read())
9: {
10: if (reader.NodeType == XmlNodeType.Element && reader.Name == "add")
11: {
12: if (reader.HasAttributes)
13: {
14: strValue = reader.GetAttribute("key");
15: if (!string.IsNullOrEmpty(strValue) && strValue == strKey)
16: {
17: strValue = reader.GetAttribute("value");
18: return strValue;
19: }
20: }
21: }
22: }
23: return strValue;
24: }
25: }
Lines 5 to 20 call and read the response from FTPSetupPackageFolder.ashx to find out if the Directory existed or not.
The ASHX Service to handle this call on the server is….
public void ProcessRequest(HttpContext context)
{
string FtpRootDir = context.Request.QueryString["FTPpath"].ToString();
string FtpBaseAddress = null;
string FtpUserName = null;
string FtpPassword = null;
if (System.Configuration.ConfigurationManager.AppSettings["FtpBaseFolder"] != null)
FtpBaseAddress = System.Configuration.ConfigurationManager.AppSettings["FtpBaseFolder"].ToString();
//Get FTP Credential settings
if (System.Configuration.ConfigurationManager.AppSettings["FtpUserName"] != null)
FtpUserName = System.Configuration.ConfigurationManager.AppSettings["FtpUserName"].ToString();
if (System.Configuration.ConfigurationManager.AppSettings["FtpPassword"] != null)
FtpPassword = System.Configuration.ConfigurationManager.AppSettings["FtpPassword"].ToString();
NetworkCredential credentials = new NetworkCredential(FtpUserName, FtpPassword);
string DirName = FtpBaseAddress;
string WorkingDirectory = DirName + FtpRootDir + "/";
if (!FtpDirectoryExists(credentials, WorkingDirectory))
{
FtpWebRequest setupDir = (FtpWebRequest)WebRequest.Create(WorkingDirectory);
setupDir.Method = WebRequestMethods.Ftp.MakeDirectory;
setupDir.Credentials = credentials;
setupDir.Timeout = -1; // Don't timeout for Debugging and slow connection
FtpWebResponse dirResponse = (FtpWebResponse)setupDir.GetResponse();
context.Response.Write(dirResponse.StatusDescription.ToString());
dirResponse.Close();
}
}
Again the web.config XML file on the server side holds the relevant values for flexibility of deployment.
Line 22 calls the WCF service to start reporting on delivery of Files to the created directory See next post.
Line 25 to 52 then uploads whatever files are in the FilesToUpload collection in separate threads. It calls the function to open read and write the appropriate streams to the HTTP service
private void UploadFileData(Stream inputFile,Stream resultFile)
{
try
{
byte[] fileData = new byte[4096];
int fileDataToRead;
while ((fileDataToRead = inputFile.Read(fileData, 0, fileData.Length)) != 0)
{
resultFile.Write(fileData, 0, fileDataToRead);
}
}
catch (Exception ex)
{
MessageBox.Show("Exception Thrown --> " + ex.Message);
}
}
The ASHX HTTP service to handle the uploading stream is as follows
public void ProcessRequest(HttpContext context)
{
ctx = context;
//Query Parameters
string filename = context.Request.QueryString["InputFile"].ToString();
string FtpRootDir = context.Request.QueryString["FTPpath"].ToString();
// Local HTTP Storage folder for staging to FTP upload
string filePath = context.Server.MapPath("~/FilesServer/");
// Get the Base address of the FTP server folder from the web.Config
string FtpBaseAddress = null;
string FtpUserName = null;
string FtpPassword = null;
// Upload via HTTP stream initiatied by client
using (FileStream fileStream = File.Create(context.Server.MapPath("~/FilesServer/" + filename)))
{
byte[] bufferData = new byte[4096];
int bytesToBeRead;
while ((bytesToBeRead = context.Request.InputStream.Read(bufferData, 0, bufferData.Length)) != 0)
{
fileStream.Write(bufferData, 0, bytesToBeRead);
}
fileStream.Close();
}
try
{
if (System.Configuration.ConfigurationManager.AppSettings["FtpBaseFolder"] != null)
FtpBaseAddress = System.Configuration.ConfigurationManager.AppSettings["FtpBaseFolder"].ToString();
//Get FTP Credential settings
if (System.Configuration.ConfigurationManager.AppSettings["FtpUserName"] != null)
FtpUserName = System.Configuration.ConfigurationManager.AppSettings["FtpUserName"].ToString();
if (System.Configuration.ConfigurationManager.AppSettings["FtpPassword"] != null)
FtpPassword = System.Configuration.ConfigurationManager.AppSettings["FtpPassword"].ToString();
// Create logon Credential
NetworkCredential credentials = new NetworkCredential(FtpUserName, FtpPassword);
string DirName = FtpBaseAddress;
string WorkingDirectory = DirName + FtpRootDir + "/";
// Upload File
FtpWebRequest request = (FtpWebRequest)WebRequest.Create(WorkingDirectory + filename);
request.Method = WebRequestMethods.Ftp.UploadFile;
request.Credentials = credentials;
request.Timeout = -1; // never timeout incase of slow connection
// Copy the contents of the file to the request stream.
byte[] bufferData = new byte[7168];
int length = bufferData.Length;
using (FileStream fileStream =
new FileStream(context.Server.MapPath("~/FilesServer/" + filename)
, FileMode.Open, FileAccess.Read, FileShare.Read, length, false)) // Last argument should block thread
{
Stream requestStream = request.GetRequestStream();
int bytesToBeRead;
while ((bytesToBeRead = fileStream.Read(bufferData, 0, bufferData.Length)) != 0)
{
requestStream.Write(bufferData, 0, bytesToBeRead);
}
requestStream.Flush(); // Flush any outstanding buffer
requestStream.Close();
fileStream.Close();
File.Delete(context.Server.MapPath("~/FilesServer/" + filename)); //
}
//context.Response.Write(filename + "Uploaded"); no can do it's a writable stream processor
}
In the Next Post I’ll Cover the Duplex Polling WCF service to poll the FTP folder to check if the files have arrived at the FTP site.
That’s all the Time I have for now. Cheers. P.