diff --git a/plugin.xml b/plugin.xml index b3e9702..dd73482 100644 --- a/plugin.xml +++ b/plugin.xml @@ -1,6 +1,6 @@ - version="0.1.0"> @@ -41,5 +41,16 @@ id="org.apache.cordova.core.FileTransfer"> - + + + + + + + + + + + + diff --git a/src/wp7/FileTransfer.cs b/src/wp7/FileTransfer.cs new file mode 100644 index 0000000..e585895 --- /dev/null +++ b/src/wp7/FileTransfer.cs @@ -0,0 +1,526 @@ +/* + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.IsolatedStorage; +using System.Net; +using System.Runtime.Serialization; +using System.Windows; +using System.Security; +using System.Diagnostics; + +namespace WPCordovaClassLib.Cordova.Commands +{ + public class FileTransfer : BaseCommand + { + public class DownloadRequestState + { + // This class stores the State of the request. + public HttpWebRequest request; + public DownloadOptions options; + + public DownloadRequestState() + { + request = null; + options = null; + } + } + + /// + /// Boundary symbol + /// + private string Boundary = "----------------------------" + DateTime.Now.Ticks.ToString("x"); + + // Error codes + public const int FileNotFoundError = 1; + public const int InvalidUrlError = 2; + public const int ConnectionError = 3; + + /// + /// Options for downloading file + /// + [DataContract] + public class DownloadOptions + { + /// + /// File path to download to + /// + [DataMember(Name = "filePath", IsRequired = true)] + public string FilePath { get; set; } + + /// + /// Server address to the file to download + /// + [DataMember(Name = "url", IsRequired = true)] + public string Url { get; set; } + } + + /// + /// Options for uploading file + /// + [DataContract] + public class UploadOptions + { + /// + /// File path to upload + /// + [DataMember(Name = "filePath", IsRequired = true)] + public string FilePath { get; set; } + + /// + /// Server address + /// + [DataMember(Name = "server", IsRequired = true)] + public string Server { get; set; } + + /// + /// File key + /// + [DataMember(Name = "fileKey")] + public string FileKey { get; set; } + + /// + /// File name on the server + /// + [DataMember(Name = "fileName")] + public string FileName { get; set; } + + /// + /// File Mime type + /// + [DataMember(Name = "mimeType")] + public string MimeType { get; set; } + + + /// + /// Additional options + /// + [DataMember(Name = "params")] + public string Params { get; set; } + + /// + /// Flag to recognize if we should trust every host (only in debug environments) + /// + [DataMember(Name = "debug")] + public bool Debug { get; set; } + + /// + /// Creates options object with default parameters + /// + public UploadOptions() + { + this.SetDefaultValues(new StreamingContext()); + } + + /// + /// Initializes default values for class fields. + /// Implemented in separate method because default constructor is not invoked during deserialization. + /// + /// + [OnDeserializing()] + public void SetDefaultValues(StreamingContext context) + { + this.FileKey = "file"; + this.FileName = "image.jpg"; + this.MimeType = "image/jpeg"; + } + + } + + /// + /// Uploading response info + /// + [DataContract] + public class FileUploadResult + { + /// + /// Amount of sent bytes + /// + [DataMember(Name = "bytesSent")] + public long BytesSent { get; set; } + + /// + /// Server response code + /// + [DataMember(Name = "responseCode")] + public long ResponseCode { get; set; } + + /// + /// Server response + /// + [DataMember(Name = "response", EmitDefaultValue = false)] + public string Response { get; set; } + + /// + /// Creates FileUploadResult object with response values + /// + /// Amount of sent bytes + /// Server response code + /// Server response + public FileUploadResult(long bytesSent, long responseCode, string response) + { + this.BytesSent = bytesSent; + this.ResponseCode = responseCode; + this.Response = response; + } + } + + /// + /// Represents transfer error codes for callback + /// + [DataContract] + public class FileTransferError + { + /// + /// Error code + /// + [DataMember(Name = "code", IsRequired = true)] + public int Code { get; set; } + + /// + /// The source URI + /// + [DataMember(Name = "source", IsRequired = true)] + public string Source { get; set; } + + /// + /// The target URI + /// + [DataMember(Name = "target", IsRequired = true)] + public string Target { get; set; } + + /// + /// The http status code response from the remote URI + /// + [DataMember(Name = "http_status", IsRequired = true)] + public int HttpStatus { get; set; } + + /// + /// Creates FileTransferError object + /// + /// Error code + public FileTransferError(int errorCode) + { + this.Code = errorCode; + this.Source = null; + this.Target = null; + this.HttpStatus = 0; + } + public FileTransferError(int errorCode, string source, string target, int status) + { + this.Code = errorCode; + this.Source = source; + this.Target = target; + this.HttpStatus = status; + } + } + + /// + /// Upload options + /// + private UploadOptions uploadOptions; + + /// + /// Bytes sent + /// + private long bytesSent; + + /// + /// sends a file to a server + /// + /// Upload options + public void upload(string options) + { + Debug.WriteLine("options = " + options); + options = options.Replace("{}", "null"); + + try + { + try + { + string[] args = JSON.JsonHelper.Deserialize(options); + uploadOptions = JSON.JsonHelper.Deserialize(args[0]); + } + catch (Exception) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION)); + return; + } + + Uri serverUri; + try + { + serverUri = new Uri(uploadOptions.Server); + } + catch (Exception) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, new FileTransferError(InvalidUrlError, uploadOptions.Server, null, 0))); + return; + } + HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(serverUri); + webRequest.ContentType = "multipart/form-data;boundary=" + Boundary; + webRequest.Method = "POST"; + webRequest.BeginGetRequestStream(WriteCallback, webRequest); + } + catch (Exception) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, new FileTransferError(ConnectionError))); + } + } + + public void download(string options) + { + DownloadOptions downloadOptions = null; + HttpWebRequest webRequest = null; + + try + { + string[] optionStrings = JSON.JsonHelper.Deserialize(options); + + downloadOptions = new DownloadOptions();// JSON.JsonHelper.Deserialize(options); + downloadOptions.Url = optionStrings[0]; + downloadOptions.FilePath = optionStrings[1]; + } + catch (Exception) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION)); + return; + } + + try + { + webRequest = (HttpWebRequest)WebRequest.Create(downloadOptions.Url); + } + catch (Exception) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, new FileTransferError(InvalidUrlError, downloadOptions.Url, null, 0))); + return; + } + + if (downloadOptions != null && webRequest != null) + { + DownloadRequestState state = new DownloadRequestState(); + state.options = downloadOptions; + state.request = webRequest; + webRequest.BeginGetResponse(new AsyncCallback(downloadCallback), state); + } + + + + } + + /// + /// + /// + /// + private void downloadCallback(IAsyncResult asynchronousResult) + { + DownloadRequestState reqState = (DownloadRequestState)asynchronousResult.AsyncState; + HttpWebRequest request = reqState.request; + + try + { + HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(asynchronousResult); + + using (IsolatedStorageFile isoFile = IsolatedStorageFile.GetUserStoreForApplication()) + { + // create the file if not exists + if (!isoFile.FileExists(reqState.options.FilePath)) + { + var file = isoFile.CreateFile(reqState.options.FilePath); + file.Close(); + } + + using (FileStream fileStream = new IsolatedStorageFileStream(reqState.options.FilePath, FileMode.Open, FileAccess.Write, isoFile)) + { + long totalBytes = response.ContentLength; + int bytesRead = 0; + using (BinaryReader reader = new BinaryReader(response.GetResponseStream())) + { + + using (BinaryWriter writer = new BinaryWriter(fileStream)) + { + int BUFFER_SIZE = 1024; + byte[] buffer; + + while (true) + { + buffer = reader.ReadBytes(BUFFER_SIZE); + // fire a progress event ? + bytesRead += buffer.Length; + if (buffer.Length > 0) + { + writer.Write(buffer); + } + else + { + writer.Close(); + reader.Close(); + fileStream.Close(); + break; + } + } + } + + } + + + } + } + WPCordovaClassLib.Cordova.Commands.File.FileEntry entry = new WPCordovaClassLib.Cordova.Commands.File.FileEntry(reqState.options.FilePath); + DispatchCommandResult(new PluginResult(PluginResult.Status.OK, entry)); + } + catch (IsolatedStorageException) + { + // Trying to write the file somewhere within the IsoStorage. + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, new FileTransferError(FileNotFoundError))); + } + catch (SecurityException) + { + // Trying to write the file somewhere not allowed. + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, new FileTransferError(FileNotFoundError))); + } + catch (WebException webex) + { + // TODO: probably need better work here to properly respond with all http status codes back to JS + // Right now am jumping through hoops just to detect 404. + if ((webex.Status == WebExceptionStatus.ProtocolError && ((HttpWebResponse)webex.Response).StatusCode == HttpStatusCode.NotFound) || webex.Status == WebExceptionStatus.UnknownError) + { + // Weird MSFT detection of 404... seriously... just give us the f(*&#$@ status code as a number ffs!!! + // "Numbers for HTTP status codes? Nah.... let's create our own set of enums/structs to abstract that stuff away." + // FACEPALM + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, new FileTransferError(ConnectionError, null, null, 404))); + } + else + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, new FileTransferError(ConnectionError))); + } + } + catch (Exception) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, new FileTransferError(FileNotFoundError))); + } + } + + + + /// + /// Read file from Isolated Storage and sends it to server + /// + /// + private void WriteCallback(IAsyncResult asynchronousResult) + { + try + { + HttpWebRequest webRequest = (HttpWebRequest)asynchronousResult.AsyncState; + using (Stream requestStream = (webRequest.EndGetRequestStream(asynchronousResult))) + { + string lineStart = "--"; + string lineEnd = Environment.NewLine; + byte[] boundaryBytes = System.Text.Encoding.UTF8.GetBytes(lineStart + Boundary + lineEnd); + string formdataTemplate = "Content-Disposition: form-data; name=\"{0}\"" + lineEnd + lineEnd + "{1}" + lineEnd; + + if (uploadOptions.Params != null) + { + + string[] arrParams = uploadOptions.Params.Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries); + + foreach (string param in arrParams) + { + string[] split = param.Split('='); + string key = split[0]; + string val = split[1]; + requestStream.Write(boundaryBytes, 0, boundaryBytes.Length); + string formItem = string.Format(formdataTemplate, key, val); + byte[] formItemBytes = System.Text.Encoding.UTF8.GetBytes(formItem); + requestStream.Write(formItemBytes, 0, formItemBytes.Length); + } + requestStream.Write(boundaryBytes, 0, boundaryBytes.Length); + } + using (IsolatedStorageFile isoFile = IsolatedStorageFile.GetUserStoreForApplication()) + { + if (!isoFile.FileExists(uploadOptions.FilePath)) + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, new FileTransferError(FileNotFoundError, uploadOptions.Server, uploadOptions.FilePath, 0))); + return; + } + + using (FileStream fileStream = new IsolatedStorageFileStream(uploadOptions.FilePath, FileMode.Open, isoFile)) + { + string headerTemplate = "Content-Disposition: form-data; name=\"{0}\"; filename=\"{1}\"" + lineEnd + "Content-Type: {2}" + lineEnd + lineEnd; + string header = string.Format(headerTemplate, uploadOptions.FileKey, uploadOptions.FileName, uploadOptions.MimeType); + byte[] headerBytes = System.Text.Encoding.UTF8.GetBytes(header); + requestStream.Write(boundaryBytes, 0, boundaryBytes.Length); + requestStream.Write(headerBytes, 0, headerBytes.Length); + byte[] buffer = new byte[4096]; + int bytesRead = 0; + + while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) != 0) + { + requestStream.Write(buffer, 0, bytesRead); + bytesSent += bytesRead; + } + } + byte[] endRequest = System.Text.Encoding.UTF8.GetBytes(lineEnd + lineStart + Boundary + lineStart + lineEnd); + requestStream.Write(endRequest, 0, endRequest.Length); + } + } + webRequest.BeginGetResponse(ReadCallback, webRequest); + } + catch (Exception) + { + Deployment.Current.Dispatcher.BeginInvoke(() => + { + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, new FileTransferError(ConnectionError))); + }); + } + } + + /// + /// Reads response into FileUploadResult + /// + /// + private void ReadCallback(IAsyncResult asynchronousResult) + { + try + { + HttpWebRequest webRequest = (HttpWebRequest)asynchronousResult.AsyncState; + using (HttpWebResponse response = (HttpWebResponse)webRequest.EndGetResponse(asynchronousResult)) + { + using (Stream streamResponse = response.GetResponseStream()) + { + using (StreamReader streamReader = new StreamReader(streamResponse)) + { + string responseString = streamReader.ReadToEnd(); + Deployment.Current.Dispatcher.BeginInvoke(() => + { + DispatchCommandResult(new PluginResult(PluginResult.Status.OK, new FileUploadResult(bytesSent, (long)response.StatusCode, responseString))); + }); + } + } + } + } + catch (Exception) + { + Deployment.Current.Dispatcher.BeginInvoke(() => + { + FileTransferError transferError = new FileTransferError(ConnectionError, uploadOptions.Server, uploadOptions.FilePath, 403); + DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, transferError)); + }); + } + } + } +}