FileTransfer.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. /*
  2. *
  3. * Licensed to the Apache Software Foundation (ASF) under one
  4. * or more contributor license agreements. See the NOTICE file
  5. * distributed with this work for additional information
  6. * regarding copyright ownership. The ASF licenses this file
  7. * to you under the Apache License, Version 2.0 (the
  8. * "License"); you may not use this file except in compliance
  9. * with the License. You may obtain a copy of the License at
  10. *
  11. * http://www.apache.org/licenses/LICENSE-2.0
  12. *
  13. * Unless required by applicable law or agreed to in writing,
  14. * software distributed under the License is distributed on an
  15. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  16. * KIND, either express or implied. See the License for the
  17. * specific language governing permissions and limitations
  18. * under the License.
  19. *
  20. */
  21. /*global module, require*/
  22. var argscheck = require('cordova/argscheck'),
  23. FileTransferError = require('./FileTransferError');
  24. function getParentPath(filePath) {
  25. var pos = filePath.lastIndexOf('/');
  26. return filePath.substring(0, pos + 1);
  27. }
  28. function getFileName(filePath) {
  29. var pos = filePath.lastIndexOf('/');
  30. return filePath.substring(pos + 1);
  31. }
  32. function getUrlCredentials(urlString) {
  33. var credentialsPattern = /^https?\:\/\/(?:(?:(([^:@\/]*)(?::([^@\/]*))?)?@)?([^:\/?#]*)(?::(\d*))?).*$/,
  34. credentials = credentialsPattern.exec(urlString);
  35. return credentials && credentials[1];
  36. }
  37. function getBasicAuthHeader(urlString) {
  38. var header = null;
  39. // This is changed due to MS Windows doesn't support credentials in http uris
  40. // so we detect them by regexp and strip off from result url
  41. // Proof: http://social.msdn.microsoft.com/Forums/windowsapps/en-US/a327cf3c-f033-4a54-8b7f-03c56ba3203f/windows-foundation-uri-security-problem
  42. if (window.btoa) {
  43. var credentials = getUrlCredentials(urlString);
  44. if (credentials) {
  45. var authHeader = "Authorization";
  46. var authHeaderValue = "Basic " + window.btoa(credentials);
  47. header = {
  48. name : authHeader,
  49. value : authHeaderValue
  50. };
  51. }
  52. }
  53. return header;
  54. }
  55. function checkURL(url) {
  56. return url.indexOf(' ') === -1 ? true : false;
  57. }
  58. var idCounter = 0;
  59. var transfers = {};
  60. /**
  61. * FileTransfer uploads a file to a remote server.
  62. * @constructor
  63. */
  64. var FileTransfer = function() {
  65. this._id = ++idCounter;
  66. this.onprogress = null; // optional callback
  67. };
  68. /**
  69. * Given an absolute file path, uploads a file on the device to a remote server
  70. * using a multipart HTTP request.
  71. * @param filePath {String} Full path of the file on the device
  72. * @param server {String} URL of the server to receive the file
  73. * @param successCallback (Function} Callback to be invoked when upload has completed
  74. * @param errorCallback {Function} Callback to be invoked upon error
  75. * @param options {FileUploadOptions} Optional parameters such as file name and mimetype
  76. * @param trustAllHosts {Boolean} Optional trust all hosts (e.g. for self-signed certs), defaults to false
  77. */
  78. FileTransfer.prototype.upload = function(filePath, server, successCallback, errorCallback, options) {
  79. // check for arguments
  80. argscheck.checkArgs('ssFFO*', 'FileTransfer.upload', arguments);
  81. // Check if target URL doesn't contain spaces. If contains, it should be escaped first
  82. // (see https://github.com/apache/cordova-plugin-file-transfer/blob/master/doc/index.md#upload)
  83. if (!checkURL(server)) {
  84. if (errorCallback) {
  85. errorCallback(new FileTransferError(FileTransferError.INVALID_URL_ERR, filePath, server));
  86. }
  87. return;
  88. }
  89. options = options || {};
  90. var fileKey = options.fileKey || "file";
  91. var fileName = options.fileName || "image.jpg";
  92. var mimeType = options.mimeType || "image/jpeg";
  93. var params = options.params || {};
  94. var withCredentials = options.withCredentials || false;
  95. // var chunkedMode = !!options.chunkedMode; // Not supported
  96. var headers = options.headers || {};
  97. var httpMethod = options.httpMethod && options.httpMethod.toUpperCase() === "PUT" ? "PUT" : "POST";
  98. var basicAuthHeader = getBasicAuthHeader(server);
  99. if (basicAuthHeader) {
  100. server = server.replace(getUrlCredentials(server) + '@', '');
  101. headers[basicAuthHeader.name] = basicAuthHeader.value;
  102. }
  103. var that = this;
  104. var xhr = transfers[this._id] = new XMLHttpRequest();
  105. xhr.withCredentials = withCredentials;
  106. var fail = errorCallback && function(code, status, response) {
  107. if (transfers[this._id]) {
  108. delete transfers[this._id];
  109. }
  110. var error = new FileTransferError(code, filePath, server, status, response);
  111. if (errorCallback) {
  112. errorCallback(error);
  113. }
  114. };
  115. window.resolveLocalFileSystemURL(filePath, function(entry) {
  116. entry.file(function(file) {
  117. var reader = new FileReader();
  118. reader.onloadend = function() {
  119. var blob = new Blob([this.result], {type: mimeType});
  120. // Prepare form data to send to server
  121. var fd = new FormData();
  122. fd.append(fileKey, blob, fileName);
  123. for (var prop in params) {
  124. if (params.hasOwnProperty(prop)) {
  125. fd.append(prop, params[prop]);
  126. }
  127. }
  128. xhr.open(httpMethod, server);
  129. // Fill XHR headers
  130. for (var header in headers) {
  131. if (headers.hasOwnProperty(header)) {
  132. xhr.setRequestHeader(header, headers[header]);
  133. }
  134. }
  135. xhr.onload = function() {
  136. if (this.status === 200) {
  137. var result = new FileUploadResult(); // jshint ignore:line
  138. result.bytesSent = blob.size;
  139. result.responseCode = this.status;
  140. result.response = this.response;
  141. delete transfers[that._id];
  142. successCallback(result);
  143. } else if (this.status === 404) {
  144. fail(FileTransferError.INVALID_URL_ERR, this.status, this.response);
  145. } else {
  146. fail(FileTransferError.CONNECTION_ERR, this.status, this.response);
  147. }
  148. };
  149. xhr.ontimeout = function() {
  150. fail(FileTransferError.CONNECTION_ERR, this.status, this.response);
  151. };
  152. xhr.onerror = function() {
  153. fail(FileTransferError.CONNECTION_ERR, this.status, this.response);
  154. };
  155. xhr.onabort = function () {
  156. fail(FileTransferError.ABORT_ERR, this.status, this.response);
  157. };
  158. xhr.upload.onprogress = function (e) {
  159. if (that.onprogress) {
  160. that.onprogress(e);
  161. }
  162. };
  163. xhr.send(fd);
  164. // Special case when transfer already aborted, but XHR isn't sent.
  165. // In this case XHR won't fire an abort event, so we need to check if transfers record
  166. // isn't deleted by filetransfer.abort and if so, call XHR's abort method again
  167. if (!transfers[that._id]) {
  168. xhr.abort();
  169. }
  170. };
  171. reader.readAsArrayBuffer(file);
  172. }, function() {
  173. fail(FileTransferError.FILE_NOT_FOUND_ERR);
  174. });
  175. }, function() {
  176. fail(FileTransferError.FILE_NOT_FOUND_ERR);
  177. });
  178. };
  179. /**
  180. * Downloads a file form a given URL and saves it to the specified directory.
  181. * @param source {String} URL of the server to receive the file
  182. * @param target {String} Full path of the file on the device
  183. * @param successCallback (Function} Callback to be invoked when upload has completed
  184. * @param errorCallback {Function} Callback to be invoked upon error
  185. * @param trustAllHosts {Boolean} Optional trust all hosts (e.g. for self-signed certs), defaults to false
  186. * @param options {FileDownloadOptions} Optional parameters such as headers
  187. */
  188. FileTransfer.prototype.download = function(source, target, successCallback, errorCallback, trustAllHosts, options) {
  189. argscheck.checkArgs('ssFF*', 'FileTransfer.download', arguments);
  190. // Check if target URL doesn't contain spaces. If contains, it should be escaped first
  191. // (see https://github.com/apache/cordova-plugin-file-transfer/blob/master/doc/index.md#download)
  192. if (!checkURL(source)) {
  193. if (errorCallback) {
  194. errorCallback(new FileTransferError(FileTransferError.INVALID_URL_ERR, source, target));
  195. }
  196. return;
  197. }
  198. options = options || {};
  199. var headers = options.headers || {};
  200. var withCredentials = options.withCredentials || false;
  201. var basicAuthHeader = getBasicAuthHeader(source);
  202. if (basicAuthHeader) {
  203. source = source.replace(getUrlCredentials(source) + '@', '');
  204. headers[basicAuthHeader.name] = basicAuthHeader.value;
  205. }
  206. var that = this;
  207. var xhr = transfers[this._id] = new XMLHttpRequest();
  208. xhr.withCredentials = withCredentials;
  209. var fail = errorCallback && function(code, status, response) {
  210. if (transfers[that._id]) {
  211. delete transfers[that._id];
  212. }
  213. // In XHR GET reqests we're setting response type to Blob
  214. // but in case of error we need to raise event with plain text response
  215. if (response instanceof Blob) {
  216. var reader = new FileReader();
  217. reader.readAsText(response);
  218. reader.onloadend = function(e) {
  219. var error = new FileTransferError(code, source, target, status, e.target.result);
  220. errorCallback(error);
  221. };
  222. } else {
  223. var error = new FileTransferError(code, source, target, status, response);
  224. errorCallback(error);
  225. }
  226. };
  227. xhr.onload = function (e) {
  228. var fileNotFound = function () {
  229. fail(FileTransferError.FILE_NOT_FOUND_ERR);
  230. };
  231. var req = e.target;
  232. // req.status === 0 is special case for local files with file:// URI scheme
  233. if ((req.status === 200 || req.status === 0) && req.response) {
  234. window.resolveLocalFileSystemURL(getParentPath(target), function (dir) {
  235. dir.getFile(getFileName(target), {create: true}, function writeFile(entry) {
  236. entry.createWriter(function (fileWriter) {
  237. fileWriter.onwriteend = function (evt) {
  238. if (!evt.target.error) {
  239. entry.filesystemName = entry.filesystem.name;
  240. delete transfers[that._id];
  241. if (successCallback) {
  242. successCallback(entry);
  243. }
  244. } else {
  245. fail(FileTransferError.FILE_NOT_FOUND_ERR);
  246. }
  247. };
  248. fileWriter.onerror = function () {
  249. fail(FileTransferError.FILE_NOT_FOUND_ERR);
  250. };
  251. fileWriter.write(req.response);
  252. }, fileNotFound);
  253. }, fileNotFound);
  254. }, fileNotFound);
  255. } else if (req.status === 404) {
  256. fail(FileTransferError.INVALID_URL_ERR, req.status, req.response);
  257. } else {
  258. fail(FileTransferError.CONNECTION_ERR, req.status, req.response);
  259. }
  260. };
  261. xhr.onprogress = function (e) {
  262. if (that.onprogress) {
  263. that.onprogress(e);
  264. }
  265. };
  266. xhr.onerror = function () {
  267. fail(FileTransferError.CONNECTION_ERR, this.status, this.response);
  268. };
  269. xhr.onabort = function () {
  270. fail(FileTransferError.ABORT_ERR, this.status, this.response);
  271. };
  272. xhr.open("GET", source, true);
  273. for (var header in headers) {
  274. if (headers.hasOwnProperty(header)) {
  275. xhr.setRequestHeader(header, headers[header]);
  276. }
  277. }
  278. xhr.responseType = "blob";
  279. xhr.send();
  280. };
  281. /**
  282. * Aborts the ongoing file transfer on this object. The original error
  283. * callback for the file transfer will be called if necessary.
  284. */
  285. FileTransfer.prototype.abort = function() {
  286. if (this instanceof FileTransfer) {
  287. if (transfers[this._id]) {
  288. transfers[this._id].abort();
  289. delete transfers[this._id];
  290. }
  291. }
  292. };
  293. module.exports = FileTransfer;