| /* |
| * |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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. |
| * |
| */ |
| |
| /*jshint -W030 */ |
| /*global Windows, WinJS*/ |
| /*global module, require*/ |
| |
| var FTErr = require('./FileTransferError'), |
| ProgressEvent = require('cordova-plugin-file.ProgressEvent'), |
| FileUploadResult = require('cordova-plugin-file.FileUploadResult'), |
| FileProxy = require('cordova-plugin-file.FileProxy'); |
| |
| var appData = Windows.Storage.ApplicationData.current; |
| |
| var LINE_START = "--"; |
| var LINE_END = "\r\n"; |
| var BOUNDARY = '+++++'; |
| |
| var fileTransferOps = []; |
| |
| // Some private helper functions, hidden by the module |
| function cordovaPathToNative(path) { |
| |
| var cleanPath = String(path); |
| // turn / into \\ |
| cleanPath = cleanPath.replace(/\//g, '\\'); |
| // turn \\ into \ |
| cleanPath = cleanPath.replace(/\\\\/g, '\\'); |
| // strip end \\ characters |
| cleanPath = cleanPath.replace(/\\+$/g, ''); |
| return cleanPath; |
| } |
| |
| function nativePathToCordova(path) { |
| return String(path).replace(/\\/g, '/'); |
| } |
| |
| function alreadyCancelled(opId) { |
| var op = fileTransferOps[opId]; |
| return op && op.state === FileTransferOperation.CANCELLED; |
| } |
| |
| function doUpload (upload, uploadId, filePath, server, successCallback, errorCallback) { |
| if (alreadyCancelled(uploadId)) { |
| errorCallback(new FTErr(FTErr.ABORT_ERR, nativePathToCordova(filePath), server)); |
| return; |
| } |
| |
| // update internal TransferOperation object with newly created promise |
| var uploadOperation = upload.startAsync(); |
| fileTransferOps[uploadId].promise = uploadOperation; |
| |
| uploadOperation.then( |
| function (result) { |
| // Update TransferOperation object with new state, delete promise property |
| // since it is not actual anymore |
| var currentUploadOp = fileTransferOps[uploadId]; |
| if (currentUploadOp) { |
| currentUploadOp.state = FileTransferOperation.DONE; |
| currentUploadOp.promise = null; |
| } |
| |
| var response = result.getResponseInformation(); |
| var ftResult = new FileUploadResult(result.progress.bytesSent, response.statusCode, ''); |
| |
| // if server's response doesn't contain any data, then resolve operation now |
| if (result.progress.bytesReceived === 0) { |
| successCallback(ftResult); |
| return; |
| } |
| |
| // otherwise create a data reader, attached to response stream to get server's response |
| var reader = new Windows.Storage.Streams.DataReader(result.getResultStreamAt(0)); |
| reader.loadAsync(result.progress.bytesReceived).then(function (size) { |
| ftResult.response = reader.readString(size); |
| successCallback(ftResult); |
| reader.close(); |
| }); |
| }, |
| function (error) { |
| var source = nativePathToCordova(filePath); |
| |
| // Handle download error here. |
| // Wrap this routines into promise due to some async methods |
| var getTransferError = new WinJS.Promise(function (resolve) { |
| if (error.message === 'Canceled') { |
| // If download was cancelled, message property will be specified |
| resolve(new FTErr(FTErr.ABORT_ERR, source, server, null, null, error)); |
| } else { |
| // in the other way, try to get response property |
| var response = upload.getResponseInformation(); |
| if (!response) { |
| resolve(new FTErr(FTErr.CONNECTION_ERR, source, server)); |
| } else { |
| var reader = new Windows.Storage.Streams.DataReader(upload.getResultStreamAt(0)); |
| reader.loadAsync(upload.progress.bytesReceived).then(function (size) { |
| var responseText = reader.readString(size); |
| resolve(new FTErr(FTErr.FILE_NOT_FOUND_ERR, source, server, response.statusCode, responseText, error)); |
| reader.close(); |
| }); |
| } |
| } |
| }); |
| |
| // Update TransferOperation object with new state, delete promise property |
| // since it is not actual anymore |
| var currentUploadOp = fileTransferOps[uploadId]; |
| if (currentUploadOp) { |
| currentUploadOp.state = FileTransferOperation.CANCELLED; |
| currentUploadOp.promise = null; |
| } |
| |
| // Report the upload error back |
| getTransferError.then(function (transferError) { |
| errorCallback(transferError); |
| }); |
| }, |
| function (evt) { |
| var progressEvent = new ProgressEvent('progress', { |
| loaded: evt.progress.bytesSent, |
| total: evt.progress.totalBytesToSend, |
| target: evt.resultFile |
| }); |
| progressEvent.lengthComputable = true; |
| successCallback(progressEvent, { keepCallback: true }); |
| } |
| ); |
| } |
| |
| function FileTransferOperation(state, promise) { |
| this.state = state; |
| this.promise = promise; |
| } |
| |
| FileTransferOperation.PENDING = 0; |
| FileTransferOperation.DONE = 1; |
| FileTransferOperation.CANCELLED = 2; |
| |
| var HTTP_E_STATUS_NOT_MODIFIED = -2145844944; |
| |
| module.exports = { |
| |
| /* |
| exec(win, fail, 'FileTransfer', 'upload', |
| [filePath, server, fileKey, fileName, mimeType, params, trustAllHosts, chunkedMode, headers, this._id, httpMethod]); |
| */ |
| upload: function (successCallback, errorCallback, options) { |
| var filePath = options[0]; |
| var server = options[1]; |
| var fileKey = options[2] || 'source'; |
| var fileName = options[3]; |
| var mimeType = options[4]; |
| var params = options[5]; |
| // var trustAllHosts = options[6]; // todo |
| // var chunkedMode = options[7]; // todo |
| var headers = options[8] || {}; |
| var uploadId = options[9]; |
| var httpMethod = options[10]; |
| |
| var isMultipart = typeof headers["Content-Type"] === 'undefined'; |
| |
| function stringToByteArray(str) { |
| var byteCharacters = atob(str); |
| var byteNumbers = new Array(byteCharacters.length); |
| for (var i = 0; i < byteCharacters.length; i++) { |
| byteNumbers[i] = byteCharacters.charCodeAt(i); |
| } |
| return new Uint8Array(byteNumbers); |
| } |
| |
| if (!filePath || (typeof filePath !== 'string')) { |
| errorCallback(new FTErr(FTErr.FILE_NOT_FOUND_ERR, null, server)); |
| return; |
| } |
| |
| if (filePath.indexOf("data:") === 0 && filePath.indexOf("base64") !== -1) { |
| // First a DataWriter object is created, backed by an in-memory stream where |
| // the data will be stored. |
| var writer = Windows.Storage.Streams.DataWriter(new Windows.Storage.Streams.InMemoryRandomAccessStream()); |
| writer.unicodeEncoding = Windows.Storage.Streams.UnicodeEncoding.utf8; |
| writer.byteOrder = Windows.Storage.Streams.ByteOrder.littleEndian; |
| |
| var commaIndex = filePath.indexOf(","); |
| if (commaIndex === -1) { |
| errorCallback(new FTErr(FTErr.INVALID_URL_ERR, fileName, server, null, null, "No comma in data: URI")); |
| return; |
| } |
| |
| // Create internal download operation object |
| fileTransferOps[uploadId] = new FileTransferOperation(FileTransferOperation.PENDING, null); |
| |
| var fileDataString = filePath.substr(commaIndex + 1); |
| |
| // setting request headers for uploader |
| var uploader = new Windows.Networking.BackgroundTransfer.BackgroundUploader(); |
| uploader.method = httpMethod; |
| for (var header in headers) { |
| if (headers.hasOwnProperty(header)) { |
| uploader.setRequestHeader(header, headers[header]); |
| } |
| } |
| |
| if (isMultipart) { |
| // adding params supplied to request payload |
| var multipartParams = ''; |
| for (var key in params) { |
| if (params.hasOwnProperty(key)) { |
| multipartParams += LINE_START + BOUNDARY + LINE_END; |
| multipartParams += "Content-Disposition: form-data; name=\"" + key + "\""; |
| multipartParams += LINE_END + LINE_END; |
| multipartParams += params[key]; |
| multipartParams += LINE_END; |
| } |
| } |
| |
| var multipartFile = LINE_START + BOUNDARY + LINE_END; |
| multipartFile += "Content-Disposition: form-data; name=\"file\";"; |
| multipartFile += " filename=\"" + fileName + "\"" + LINE_END; |
| multipartFile += "Content-Type: " + mimeType + LINE_END + LINE_END; |
| |
| var bound = LINE_END + LINE_START + BOUNDARY + LINE_START + LINE_END; |
| |
| uploader.setRequestHeader("Content-Type", "multipart/form-data; boundary=" + BOUNDARY); |
| writer.writeString(multipartParams); |
| writer.writeString(multipartFile); |
| writer.writeBytes(stringToByteArray(fileDataString)); |
| writer.writeString(bound); |
| } else { |
| writer.writeBytes(stringToByteArray(fileDataString)); |
| } |
| |
| var stream; |
| |
| // The call to store async sends the actual contents of the writer |
| // to the backing stream. |
| writer.storeAsync().then(function () { |
| // For the in-memory stream implementation we are using, the flushAsync call |
| // is superfluous, but other types of streams may require it. |
| return writer.flushAsync(); |
| }).then(function () { |
| // We detach the stream to prolong its useful lifetime. Were we to fail |
| // to detach the stream, the call to writer.close() would close the underlying |
| // stream, preventing its subsequent use by the DataReader below. Most clients |
| // of DataWriter will have no reason to use the underlying stream after |
| // writer.close() is called, and will therefore have no reason to call |
| // writer.detachStream(). Note that once we detach the stream, we assume |
| // responsibility for closing the stream subsequently; after the stream |
| // has been detached, a call to writer.close() will have no effect on the stream. |
| stream = writer.detachStream(); |
| // Make sure the stream is read from the beginning in the reader |
| // we are creating below. |
| stream.seek(0); |
| // Most DataWriter clients will not call writer.detachStream(), |
| // and furthermore will be working with a file-backed or network-backed stream, |
| // rather than an in-memory-stream. In such cases, it would be particularly |
| // important to call writer.close(). Doing so is always a best practice. |
| writer.close(); |
| |
| if (alreadyCancelled(uploadId)) { |
| errorCallback(new FTErr(FTErr.ABORT_ERR, nativePathToCordova(filePath), server)); |
| return; |
| } |
| |
| // create download object. This will throw an exception if URL is malformed |
| var uri = new Windows.Foundation.Uri(server); |
| |
| var createUploadOperation; |
| try { |
| createUploadOperation = uploader.createUploadFromStreamAsync(uri, stream); |
| } catch (e) { |
| errorCallback(new FTErr(FTErr.INVALID_URL_ERR)); |
| return; |
| } |
| |
| createUploadOperation.then( |
| function (upload) { |
| doUpload(upload, uploadId, filePath, server, successCallback, errorCallback); |
| }, |
| function (err) { |
| var errorObj = new FTErr(FTErr.INVALID_URL_ERR); |
| errorObj.exception = err; |
| errorCallback(errorObj); |
| }); |
| }); |
| |
| return; |
| } |
| |
| if (filePath.substr(0, 8) === "file:///") { |
| filePath = appData.localFolder.path + filePath.substr(8).split("/").join("\\"); |
| } else if (filePath.indexOf('ms-appdata:///') === 0) { |
| // Handle 'ms-appdata' scheme |
| filePath = filePath.replace('ms-appdata:///local', appData.localFolder.path) |
| .replace('ms-appdata:///temp', appData.temporaryFolder.path); |
| } else if (filePath.indexOf('cdvfile://') === 0) { |
| filePath = filePath.replace('cdvfile://localhost/persistent', appData.localFolder.path) |
| .replace('cdvfile://localhost/temporary', appData.temporaryFolder.path); |
| } |
| |
| // normalize path separators |
| filePath = cordovaPathToNative(filePath); |
| |
| // Create internal download operation object |
| fileTransferOps[uploadId] = new FileTransferOperation(FileTransferOperation.PENDING, null); |
| |
| Windows.Storage.StorageFile.getFileFromPathAsync(filePath) |
| .then(function (storageFile) { |
| |
| if (!fileName) { |
| fileName = storageFile.name; |
| } |
| if (!mimeType) { |
| // use the actual content type of the file, probably this should be the default way. |
| // other platforms probably can't look this up. |
| mimeType = storageFile.contentType; |
| } |
| |
| if (alreadyCancelled(uploadId)) { |
| errorCallback(new FTErr(FTErr.ABORT_ERR, nativePathToCordova(filePath), server)); |
| return; |
| } |
| |
| // setting request headers for uploader |
| var uploader = new Windows.Networking.BackgroundTransfer.BackgroundUploader(); |
| uploader.method = httpMethod; |
| for (var header in headers) { |
| if (headers.hasOwnProperty(header)) { |
| uploader.setRequestHeader(header, headers[header]); |
| } |
| } |
| |
| // create download object. This will throw an exception if URL is malformed |
| var uri = new Windows.Foundation.Uri(server); |
| |
| var createUploadOperation; |
| try { |
| if (isMultipart) { |
| // adding params supplied to request payload |
| var transferParts = []; |
| for (var key in params) { |
| // Create content part for params only if value is specified because CreateUploadAsync fails otherwise |
| if (params.hasOwnProperty(key) && params[key] !== null && params[key] !== undefined && params[key].toString() !== "") { |
| var contentPart = new Windows.Networking.BackgroundTransfer.BackgroundTransferContentPart(); |
| contentPart.setHeader("Content-Disposition", "form-data; name=\"" + key + "\""); |
| contentPart.setText(params[key]); |
| transferParts.push(contentPart); |
| } |
| } |
| |
| // Adding file to upload to request payload |
| var fileToUploadPart = new Windows.Networking.BackgroundTransfer.BackgroundTransferContentPart(fileKey, fileName); |
| fileToUploadPart.setHeader("Content-Type", mimeType); |
| fileToUploadPart.setFile(storageFile); |
| transferParts.push(fileToUploadPart); |
| |
| createUploadOperation = uploader.createUploadAsync(uri, transferParts); |
| } else { |
| createUploadOperation = WinJS.Promise.wrap(uploader.createUpload(uri, storageFile)); |
| } |
| } catch (e) { |
| errorCallback(new FTErr(FTErr.INVALID_URL_ERR)); |
| return; |
| } |
| |
| createUploadOperation.then( |
| function (upload) { |
| doUpload(upload, uploadId, filePath, server, successCallback, errorCallback); |
| }, |
| function (err) { |
| var errorObj = new FTErr(FTErr.INVALID_URL_ERR); |
| errorObj.exception = err; |
| errorCallback(errorObj); |
| } |
| ); |
| }, function (err) { |
| errorCallback(new FTErr(FTErr.FILE_NOT_FOUND_ERR, fileName, server, null, null, err)); |
| }); |
| }, |
| |
| // [source, target, trustAllHosts, id, headers] |
| download:function(successCallback, errorCallback, options) { |
| var source = options[0]; |
| var target = options[1]; |
| var downloadId = options[3]; |
| var headers = options[4] || {}; |
| |
| if (!target) { |
| errorCallback(new FTErr(FTErr.FILE_NOT_FOUND_ERR)); |
| return; |
| } |
| if (target.substr(0, 8) === "file:///") { |
| target = appData.localFolder.path + target.substr(8).split("/").join("\\"); |
| } else if (target.indexOf('ms-appdata:///') === 0) { |
| // Handle 'ms-appdata' scheme |
| target = target.replace('ms-appdata:///local', appData.localFolder.path) |
| .replace('ms-appdata:///temp', appData.temporaryFolder.path); |
| } else if (target.indexOf('cdvfile://') === 0) { |
| target = target.replace('cdvfile://localhost/persistent', appData.localFolder.path) |
| .replace('cdvfile://localhost/temporary', appData.temporaryFolder.path); |
| } |
| target = cordovaPathToNative(target); |
| |
| var path = target.substr(0, target.lastIndexOf("\\")); |
| var fileName = target.substr(target.lastIndexOf("\\") + 1); |
| if (path === null || fileName === null) { |
| errorCallback(new FTErr(FTErr.FILE_NOT_FOUND_ERR)); |
| return; |
| } |
| // Download to a temp file to avoid the file deletion on 304 |
| // CB-7006 Empty file is created on file transfer if server response is 304 |
| var tempFileName = '~' + fileName; |
| |
| var download = null; |
| |
| // Create internal download operation object |
| fileTransferOps[downloadId] = new FileTransferOperation(FileTransferOperation.PENDING, null); |
| |
| var downloadCallback = function(storageFolder) { |
| storageFolder.createFileAsync(tempFileName, Windows.Storage.CreationCollisionOption.replaceExisting).then(function (storageFile) { |
| |
| if (alreadyCancelled(downloadId)) { |
| errorCallback(new FTErr(FTErr.ABORT_ERR, source, target)); |
| return; |
| } |
| |
| // if download isn't cancelled, contunue with creating and preparing download operation |
| var downloader = new Windows.Networking.BackgroundTransfer.BackgroundDownloader(); |
| for (var header in headers) { |
| if (headers.hasOwnProperty(header)) { |
| downloader.setRequestHeader(header, headers[header]); |
| } |
| } |
| |
| // create download object. This will throw an exception if URL is malformed |
| try { |
| var uri = Windows.Foundation.Uri(source); |
| download = downloader.createDownload(uri, storageFile); |
| } catch (e) { |
| // so we handle this and call errorCallback |
| errorCallback(new FTErr(FTErr.INVALID_URL_ERR)); |
| return; |
| } |
| |
| var downloadOperation = download.startAsync(); |
| // update internal TransferOperation object with newly created promise |
| fileTransferOps[downloadId].promise = downloadOperation; |
| |
| downloadOperation.then(function () { |
| |
| // Update TransferOperation object with new state, delete promise property |
| // since it is not actual anymore |
| var currentDownloadOp = fileTransferOps[downloadId]; |
| if (currentDownloadOp) { |
| currentDownloadOp.state = FileTransferOperation.DONE; |
| currentDownloadOp.promise = null; |
| } |
| |
| storageFile.renameAsync(fileName, Windows.Storage.CreationCollisionOption.replaceExisting).done(function () { |
| var nativeURI = storageFile.path.replace(appData.localFolder.path, 'ms-appdata:///local') |
| .replace(appData.temporaryFolder.path, 'ms-appdata:///temp') |
| .replace(/\\/g, '/'); |
| |
| // Passing null as error callback here because downloaded file should exist in any case |
| // otherwise the error callback will be hit during file creation in another place |
| FileProxy.resolveLocalFileSystemURI(successCallback, null, [nativeURI]); |
| }, function(error) { |
| errorCallback(new FTErr(FTErr.FILE_NOT_FOUND_ERR, source, target, null, null, error)); |
| }); |
| }, function(error) { |
| |
| var getTransferError = new WinJS.Promise(function (resolve) { |
| // Handle download error here. If download was cancelled, |
| // message property will be specified |
| if (error.message === 'Canceled') { |
| resolve(new FTErr(FTErr.ABORT_ERR, source, target, null, null, error)); |
| } else if (error && error.number === HTTP_E_STATUS_NOT_MODIFIED) { |
| resolve(new FTErr(FTErr.NOT_MODIFIED_ERR, source, target, 304, null, error)); |
| } else { |
| // in the other way, try to get response property |
| var response = download.getResponseInformation(); |
| if (!response) { |
| resolve(new FTErr(FTErr.CONNECTION_ERR, source, target)); |
| } else { |
| var reader = new Windows.Storage.Streams.DataReader(download.getResultStreamAt(0)); |
| reader.loadAsync(download.progress.bytesReceived).then(function (bytesLoaded) { |
| var payload = reader.readString(bytesLoaded); |
| resolve(new FTErr(FTErr.FILE_NOT_FOUND_ERR, source, target, response.statusCode, payload, error)); |
| }); |
| } |
| } |
| }); |
| getTransferError.then(function (fileTransferError) { |
| |
| // Update TransferOperation object with new state, delete promise property |
| // since it is not actual anymore |
| var currentDownloadOp = fileTransferOps[downloadId]; |
| if (currentDownloadOp) { |
| currentDownloadOp.state = FileTransferOperation.CANCELLED; |
| currentDownloadOp.promise = null; |
| } |
| |
| // Cleanup, remove incompleted file |
| storageFile.deleteAsync().then(function() { |
| errorCallback(fileTransferError); |
| }); |
| }); |
| |
| }, function(evt) { |
| |
| var progressEvent = new ProgressEvent('progress', { |
| loaded: evt.progress.bytesReceived, |
| total: evt.progress.totalBytesToReceive, |
| target: evt.resultFile |
| }); |
| // when bytesReceived == 0, BackgroundDownloader has not yet differentiated whether it could get file length or not, |
| // when totalBytesToReceive == 0, BackgroundDownloader is unable to get file length |
| progressEvent.lengthComputable = (evt.progress.bytesReceived > 0) && (evt.progress.totalBytesToReceive > 0); |
| |
| successCallback(progressEvent, { keepCallback: true }); |
| }); |
| }, function(error) { |
| errorCallback(new FTErr(FTErr.FILE_NOT_FOUND_ERR, source, target, null, null, error)); |
| }); |
| }; |
| |
| var fileNotFoundErrorCallback = function(error) { |
| errorCallback(new FTErr(FTErr.FILE_NOT_FOUND_ERR, source, target, null, null, error)); |
| }; |
| |
| Windows.Storage.StorageFolder.getFolderFromPathAsync(path).then(downloadCallback, function (error) { |
| // Handle non-existent directory |
| if (error.number === -2147024894) { |
| var parent = path.substr(0, path.lastIndexOf('\\')), |
| folderNameToCreate = path.substr(path.lastIndexOf('\\') + 1); |
| |
| Windows.Storage.StorageFolder.getFolderFromPathAsync(parent).then(function(parentFolder) { |
| parentFolder.createFolderAsync(folderNameToCreate).then(downloadCallback, fileNotFoundErrorCallback); |
| }, fileNotFoundErrorCallback); |
| } else { |
| fileNotFoundErrorCallback(); |
| } |
| }); |
| }, |
| |
| abort: function (successCallback, error, options) { |
| var fileTransferOpId = options[0]; |
| |
| // Try to find transferOperation with id specified, and cancel its' promise |
| var currentOp = fileTransferOps[fileTransferOpId]; |
| if (currentOp) { |
| currentOp.state = FileTransferOperation.CANCELLED; |
| currentOp.promise && currentOp.promise.cancel(); |
| } else if (typeof fileTransferOpId !== 'undefined') { |
| // Create the operation in cancelled state to be aborted right away |
| fileTransferOps[fileTransferOpId] = new FileTransferOperation(FileTransferOperation.CANCELLED, null); |
| } |
| } |
| |
| }; |
| |
| require("cordova/exec/proxy").add("FileTransfer",module.exports); |