/* * * 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);