From 04b6e4c407ecf031b6c816db6dd8c9ea9d86fa05 Mon Sep 17 00:00:00 2001 From: Norman Breau Date: Tue, 11 Jul 2023 21:19:28 -0300 Subject: [PATCH] feat(windows)!: Drop Windows support (#346) --- README.md | 11 - package.json | 2 - plugin.xml | 7 - src/windows/FileTransferProxy.js | 599 ------------------------------- 4 files changed, 619 deletions(-) delete mode 100644 src/windows/FileTransferProxy.js diff --git a/README.md b/README.md index 702632f4..d79edda3 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,6 @@ cordova plugin add cordova-plugin-file-transfer - Android - Browser - iOS -- Windows # FileTransfer @@ -203,12 +202,6 @@ A `FileUploadResult` object is passed to the success callback of the - __withCredentials__: _boolean_ that tells the browser to set the withCredentials flag on the XMLHttpRequest -### Windows Quirks - -- An option parameter with empty/null value is excluded in the upload operation due to the Windows API design. - -- __chunkedMode__ is not supported and all uploads are set to non-chunked mode. - ## download __Parameters__: @@ -319,10 +312,6 @@ __exception__ is never defined. - 4 = `FileTransferError.ABORT_ERR` - 5 = `FileTransferError.NOT_MODIFIED_ERR` -## Windows Quirks - -- The plugin implementation is based on [BackgroundDownloader](https://msdn.microsoft.com/en-us/library/windows/apps/windows.networking.backgroundtransfer.backgrounddownloader.aspx)/[BackgroundUploader](https://msdn.microsoft.com/en-us/library/windows/apps/windows.networking.backgroundtransfer.backgrounduploader.aspx), which entails the latency issues on Windows devices (creation/starting of an operation can take up to a few seconds). You can use XHR or [HttpClient](https://msdn.microsoft.com/en-us/library/windows/apps/windows.web.http.httpclient.aspx) as a quicker alternative for small downloads. - ## Backwards Compatibility Notes Previous versions of this plugin would only accept device-absolute-file-paths as the source for uploads, or as the target for downloads. These paths would typically be of the form: diff --git a/package.json b/package.json index dcd0d8e8..2bc3b110 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,6 @@ "platforms": [ "android", "ios", - "windows", "browser" ] }, @@ -25,7 +24,6 @@ "ecosystem:cordova", "cordova-android", "cordova-ios", - "cordova-windows", "cordova-browser" ], "author": "Apache Software Foundation", diff --git a/plugin.xml b/plugin.xml index dad4d85a..94408294 100644 --- a/plugin.xml +++ b/plugin.xml @@ -65,13 +65,6 @@ - - - - - - - diff --git a/src/windows/FileTransferProxy.js b/src/windows/FileTransferProxy.js deleted file mode 100644 index c3aeab8f..00000000 --- a/src/windows/FileTransferProxy.js +++ /dev/null @@ -1,599 +0,0 @@ -/* - * - * 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. - * - */ - -/* global Windows, WinJS */ - -const FTErr = require('./FileTransferError'); -const ProgressEvent = require('cordova-plugin-file.ProgressEvent'); -const FileUploadResult = require('cordova-plugin-file.FileUploadResult'); -const FileProxy = require('cordova-plugin-file.FileProxy'); - -const appData = Windows.Storage.ApplicationData.current; - -const LINE_START = '--'; -const LINE_END = '\r\n'; -const BOUNDARY = '+++++'; - -const fileTransferOps = []; - -// Some private helper functions, hidden by the module -function cordovaPathToNative (path) { - let 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) { - const 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 - const 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 - const currentUploadOp = fileTransferOps[uploadId]; - if (currentUploadOp) { - currentUploadOp.state = FileTransferOperation.DONE; - currentUploadOp.promise = null; - } - - const response = result.getResponseInformation(); - const 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 - const 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) { - const source = nativePathToCordova(filePath); - - // Handle download error here. - // Wrap this routines into promise due to some async methods - const 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 - const response = upload.getResponseInformation(); - if (!response) { - resolve(new FTErr(FTErr.CONNECTION_ERR, source, server)); - } else { - const reader = new Windows.Storage.Streams.DataReader(upload.getResultStreamAt(0)); - reader.loadAsync(upload.progress.bytesReceived).then(function (size) { - const 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 - const 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) { - const 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; - -const 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) { - let filePath = options[0]; - const server = options[1]; - const fileKey = options[2] || 'source'; - let fileName = options[3]; - let mimeType = options[4]; - const params = options[5]; - // var trustAllHosts = options[6]; // todo - // var chunkedMode = options[7]; // todo - const headers = options[8] || {}; - const uploadId = options[9]; - const httpMethod = options[10]; - - const isMultipart = typeof headers['Content-Type'] === 'undefined'; - - function stringToByteArray (str) { - const byteCharacters = atob(str); - const byteNumbers = new Array(byteCharacters.length); - for (let 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. - const writer = Windows.Storage.Streams.DataWriter(new Windows.Storage.Streams.InMemoryRandomAccessStream()); - writer.unicodeEncoding = Windows.Storage.Streams.UnicodeEncoding.utf8; - writer.byteOrder = Windows.Storage.Streams.ByteOrder.littleEndian; - - const 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); - - const fileDataString = filePath.substr(commaIndex + 1); - - // setting request headers for uploader - const uploader = new Windows.Networking.BackgroundTransfer.BackgroundUploader(); - uploader.method = httpMethod; - for (const header in headers) { - if (Object.prototype.hasOwnProperty.call(headers, header)) { - uploader.setRequestHeader(header, headers[header]); - } - } - - if (isMultipart) { - // adding params supplied to request payload - let multipartParams = ''; - for (const key in params) { - if (Object.prototype.hasOwnProperty.call(params, 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; - } - } - - let 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; - - const 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)); - } - - let 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 - const uri = new Windows.Foundation.Uri(server); - - let 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) { - const 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 - const uploader = new Windows.Networking.BackgroundTransfer.BackgroundUploader(); - uploader.method = httpMethod; - for (const header in headers) { - if (Object.prototype.hasOwnProperty.call(headers, header)) { - uploader.setRequestHeader(header, headers[header]); - } - } - - // create download object. This will throw an exception if URL is malformed - const uri = new Windows.Foundation.Uri(server); - - let createUploadOperation; - try { - if (isMultipart) { - // adding params supplied to request payload - const transferParts = []; - for (const key in params) { - // Create content part for params only if value is specified because CreateUploadAsync fails otherwise - if ( - Object.prototype.hasOwnProperty.call(params, key) && - params[key] !== null && - params[key] !== undefined && - params[key].toString() !== '' - ) { - const 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 - const 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) { - const 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) { - const source = options[0]; - let target = options[1]; - const downloadId = options[3]; - const 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); - - const path = target.substr(0, target.lastIndexOf('\\')); - const 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 - const tempFileName = '~' + fileName; - - let download = null; - - // Create internal download operation object - fileTransferOps[downloadId] = new FileTransferOperation(FileTransferOperation.PENDING, null); - - const 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 - const downloader = new Windows.Networking.BackgroundTransfer.BackgroundDownloader(); - for (const header in headers) { - if (Object.prototype.hasOwnProperty.call(headers, header)) { - downloader.setRequestHeader(header, headers[header]); - } - } - - // create download object. This will throw an exception if URL is malformed - try { - const 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; - } - - const 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 - const currentDownloadOp = fileTransferOps[downloadId]; - if (currentDownloadOp) { - currentDownloadOp.state = FileTransferOperation.DONE; - currentDownloadOp.promise = null; - } - - storageFile.renameAsync(fileName, Windows.Storage.CreationCollisionOption.replaceExisting).done( - function () { - const 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) { - const 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 - const response = download.getResponseInformation(); - if (!response) { - resolve(new FTErr(FTErr.CONNECTION_ERR, source, target)); - } else { - if (download.progress.bytesReceived === 0) { - resolve(new FTErr(FTErr.FILE_NOT_FOUND_ERR, source, target, response.statusCode, null, error)); - return; - } - const reader = new Windows.Storage.Streams.DataReader(download.getResultStreamAt(0)); - reader.loadAsync(download.progress.bytesReceived).then(function (bytesLoaded) { - const 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 - const currentDownloadOp = fileTransferOps[downloadId]; - if (currentDownloadOp) { - currentDownloadOp.state = FileTransferOperation.CANCELLED; - currentDownloadOp.promise = null; - } - - // Cleanup, remove incompleted file - storageFile.deleteAsync().then(function () { - errorCallback(fileTransferError); - }); - }); - }, - function (evt) { - const 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)); - } - ); - }; - - const 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) { - const parent = path.substr(0, path.lastIndexOf('\\')); - const 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) { - const fileTransferOpId = options[0]; - - // Try to find transferOperation with id specified, and cancel its' promise - const 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);