filedownloader.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747
  1. // Copyright 2011 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  15. * @fileoverview A class for downloading remote files and storing them
  16. * locally using the HTML5 FileSystem API.
  17. *
  18. * The directory structure is of the form /HASH/URL/BASENAME:
  19. *
  20. * The HASH portion is a three-character slice of the hash of the URL. Since the
  21. * filesystem has a limit of about 5000 files per directory, this should divide
  22. * the downloads roughly evenly among about 5000 directories, thus allowing for
  23. * at most 5000^2 downloads.
  24. *
  25. * The URL portion is the (sanitized) full URL used for downloading the file.
  26. * This is used to ensure that each file ends up in a different location, even
  27. * if the HASH and BASENAME are the same.
  28. *
  29. * The BASENAME portion is the basename of the URL. It's used for the filename
  30. * proper so that the local filesystem: URL will be downloaded to a file with a
  31. * recognizable name.
  32. *
  33. */
  34. goog.provide('goog.net.FileDownloader');
  35. goog.provide('goog.net.FileDownloader.Error');
  36. goog.require('goog.Disposable');
  37. goog.require('goog.asserts');
  38. goog.require('goog.async.Deferred');
  39. goog.require('goog.crypt.hash32');
  40. goog.require('goog.debug.Error');
  41. goog.require('goog.events');
  42. goog.require('goog.events.EventHandler');
  43. goog.require('goog.fs');
  44. goog.require('goog.fs.DirectoryEntry');
  45. goog.require('goog.fs.Error');
  46. goog.require('goog.fs.FileSaver');
  47. goog.require('goog.net.EventType');
  48. goog.require('goog.net.XhrIo');
  49. goog.require('goog.net.XhrIoPool');
  50. goog.require('goog.object');
  51. /**
  52. * A class for downloading remote files and storing them locally using the
  53. * HTML5 filesystem API.
  54. *
  55. * @param {!goog.fs.DirectoryEntry} dir The directory in which the downloaded
  56. * files are stored. This directory should be solely managed by
  57. * FileDownloader.
  58. * @param {goog.net.XhrIoPool=} opt_pool The pool of XhrIo objects to use for
  59. * downloading files.
  60. * @constructor
  61. * @extends {goog.Disposable}
  62. * @final
  63. */
  64. goog.net.FileDownloader = function(dir, opt_pool) {
  65. goog.net.FileDownloader.base(this, 'constructor');
  66. /**
  67. * The directory in which the downloaded files are stored.
  68. * @type {!goog.fs.DirectoryEntry}
  69. * @private
  70. */
  71. this.dir_ = dir;
  72. /**
  73. * The pool of XHRs to use for capturing.
  74. * @type {!goog.net.XhrIoPool}
  75. * @private
  76. */
  77. this.pool_ = opt_pool || new goog.net.XhrIoPool();
  78. /**
  79. * A map from URLs to active downloads running for those URLs.
  80. * @type {!Object<!goog.net.FileDownloader.Download_>}
  81. * @private
  82. */
  83. this.downloads_ = {};
  84. /**
  85. * The handler for URL capturing events.
  86. * @type {!goog.events.EventHandler<!goog.net.FileDownloader>}
  87. * @private
  88. */
  89. this.eventHandler_ = new goog.events.EventHandler(this);
  90. };
  91. goog.inherits(goog.net.FileDownloader, goog.Disposable);
  92. /**
  93. * Download a remote file and save its contents to the filesystem. A given file
  94. * is uniquely identified by its URL string; this means that the relative and
  95. * absolute URLs for a single file are considered different for the purposes of
  96. * the FileDownloader.
  97. *
  98. * Returns a Deferred that will contain the downloaded blob. If there's an error
  99. * while downloading the URL, this Deferred will be passed the
  100. * {@link goog.net.FileDownloader.Error} object as an errback.
  101. *
  102. * If a download is already in progress for the given URL, this will return the
  103. * deferred blob for that download. If the URL has already been downloaded, this
  104. * will fail once it tries to save the downloaded blob.
  105. *
  106. * When a download is in progress, all Deferreds returned for that download will
  107. * be branches of a single parent. If all such branches are cancelled, or if one
  108. * is cancelled with opt_deepCancel set, then the download will be cancelled as
  109. * well.
  110. *
  111. * @param {string} url The URL of the file to download.
  112. * @return {!goog.async.Deferred} The deferred result blob.
  113. */
  114. goog.net.FileDownloader.prototype.download = function(url) {
  115. if (this.isDownloading(url)) {
  116. return this.downloads_[url].deferred.branch(true /* opt_propagateCancel */);
  117. }
  118. var download = new goog.net.FileDownloader.Download_(url, this);
  119. this.downloads_[url] = download;
  120. this.pool_.getObject(goog.bind(this.gotXhr_, this, download));
  121. return download.deferred.branch(true /* opt_propagateCancel */);
  122. };
  123. /**
  124. * Return a Deferred that will fire once no download is active for a given URL.
  125. * If there's no download active for that URL when this is called, the deferred
  126. * will fire immediately; otherwise, it will fire once the download is complete,
  127. * whether or not it succeeds.
  128. *
  129. * @param {string} url The URL of the download to wait for.
  130. * @return {!goog.async.Deferred} The Deferred that will fire when the download
  131. * is complete.
  132. */
  133. goog.net.FileDownloader.prototype.waitForDownload = function(url) {
  134. var deferred = new goog.async.Deferred();
  135. if (this.isDownloading(url)) {
  136. this.downloads_[url].deferred.addBoth(function() {
  137. deferred.callback(null);
  138. }, this);
  139. } else {
  140. deferred.callback(null);
  141. }
  142. return deferred;
  143. };
  144. /**
  145. * Returns whether or not there is an active download for a given URL.
  146. *
  147. * @param {string} url The URL of the download to check.
  148. * @return {boolean} Whether or not there is an active download for the URL.
  149. */
  150. goog.net.FileDownloader.prototype.isDownloading = function(url) {
  151. return url in this.downloads_;
  152. };
  153. /**
  154. * Load a downloaded blob from the filesystem. Will fire a deferred error if the
  155. * given URL has not yet been downloaded.
  156. *
  157. * @param {string} url The URL of the blob to load.
  158. * @return {!goog.async.Deferred} The deferred Blob object. The callback will be
  159. * passed the blob. If a file API error occurs while loading the blob, that
  160. * error will be passed to the errback.
  161. */
  162. goog.net.FileDownloader.prototype.getDownloadedBlob = function(url) {
  163. return this.getFile_(url).addCallback(function(fileEntry) {
  164. return fileEntry.file();
  165. });
  166. };
  167. /**
  168. * Get the local filesystem: URL for a downloaded file. This is different from
  169. * the blob: URL that's available from getDownloadedBlob(). If the end user
  170. * accesses the filesystem: URL, the resulting file's name will be determined by
  171. * the download filename as opposed to an arbitrary GUID. In addition, the
  172. * filesystem: URL is connected to a filesystem location, so if the download is
  173. * removed then that URL will become invalid.
  174. *
  175. * Warning: in Chrome 12, some filesystem: URLs are opened inline. This means
  176. * that e.g. HTML pages given to the user via filesystem: URLs will be opened
  177. * and processed by the browser.
  178. *
  179. * @param {string} url The URL of the file to get the URL of.
  180. * @return {!goog.async.Deferred} The deferred filesystem: URL. The callback
  181. * will be passed the URL. If a file API error occurs while loading the
  182. * blob, that error will be passed to the errback.
  183. */
  184. goog.net.FileDownloader.prototype.getLocalUrl = function(url) {
  185. return this.getFile_(url).addCallback(function(fileEntry) {
  186. return fileEntry.toUrl();
  187. });
  188. };
  189. /**
  190. * Return (deferred) whether or not a URL has been downloaded. Will fire a
  191. * deferred error if something goes wrong when determining this.
  192. *
  193. * @param {string} url The URL to check.
  194. * @return {!goog.async.Deferred} The deferred boolean. The callback will be
  195. * passed the boolean. If a file API error occurs while checking the
  196. * existence of the downloaded URL, that error will be passed to the
  197. * errback.
  198. */
  199. goog.net.FileDownloader.prototype.isDownloaded = function(url) {
  200. var deferred = new goog.async.Deferred();
  201. var blobDeferred = this.getDownloadedBlob(url);
  202. blobDeferred.addCallback(function() { deferred.callback(true); });
  203. blobDeferred.addErrback(function(err) {
  204. if (err.name == goog.fs.Error.ErrorName.NOT_FOUND) {
  205. deferred.callback(false);
  206. } else {
  207. deferred.errback(err);
  208. }
  209. });
  210. return deferred;
  211. };
  212. /**
  213. * Remove a URL from the FileDownloader.
  214. *
  215. * This returns a Deferred. If the removal is completed successfully, its
  216. * callback will be called without any value. If the removal fails, its errback
  217. * will be called with the {@link goog.fs.Error}.
  218. *
  219. * @param {string} url The URL to remove.
  220. * @return {!goog.async.Deferred} The deferred used for registering callbacks on
  221. * success or on error.
  222. */
  223. goog.net.FileDownloader.prototype.remove = function(url) {
  224. return this.getDir_(url, goog.fs.DirectoryEntry.Behavior.DEFAULT)
  225. .addCallback(function(dir) { return dir.removeRecursively(); });
  226. };
  227. /**
  228. * Save a blob for a given URL. This works just as through the blob were
  229. * downloaded form that URL, except you specify the blob and no HTTP request is
  230. * made.
  231. *
  232. * If the URL is currently being downloaded, it's indeterminate whether the blob
  233. * being set or the blob being downloaded will end up in the filesystem.
  234. * Whichever one doesn't get saved will have an error. To ensure that one or the
  235. * other takes precedence, use {@link #waitForDownload} to allow the download to
  236. * complete before setting the blob.
  237. *
  238. * @param {string} url The URL at which to set the blob.
  239. * @param {!Blob} blob The blob to set.
  240. * @param {string=} opt_name The name of the file. If this isn't given, it's
  241. * determined from the URL.
  242. * @return {!goog.async.Deferred} The deferred used for registering callbacks on
  243. * success or on error. This can be cancelled just like a {@link #download}
  244. * Deferred. The objects passed to the errback will be
  245. * {@link goog.net.FileDownloader.Error}s.
  246. */
  247. goog.net.FileDownloader.prototype.setBlob = function(url, blob, opt_name) {
  248. var name = this.sanitize_(opt_name || this.urlToName_(url));
  249. var download = new goog.net.FileDownloader.Download_(url, this);
  250. this.downloads_[url] = download;
  251. download.blob = blob;
  252. this.getDir_(download.url, goog.fs.DirectoryEntry.Behavior.CREATE_EXCLUSIVE)
  253. .addCallback(function(dir) {
  254. return dir.getFile(
  255. name, goog.fs.DirectoryEntry.Behavior.CREATE_EXCLUSIVE);
  256. })
  257. .addCallback(goog.bind(this.fileSuccess_, this, download))
  258. .addErrback(goog.bind(this.error_, this, download));
  259. return download.deferred.branch(true /* opt_propagateCancel */);
  260. };
  261. /**
  262. * The callback called when an XHR becomes available from the XHR pool.
  263. *
  264. * @param {!goog.net.FileDownloader.Download_} download The download object for
  265. * this download.
  266. * @param {!goog.net.XhrIo} xhr The XhrIo object for downloading the page.
  267. * @private
  268. */
  269. goog.net.FileDownloader.prototype.gotXhr_ = function(download, xhr) {
  270. if (download.cancelled) {
  271. this.freeXhr_(xhr);
  272. return;
  273. }
  274. this.eventHandler_.listen(
  275. xhr, goog.net.EventType.SUCCESS,
  276. goog.bind(this.xhrSuccess_, this, download));
  277. this.eventHandler_.listen(
  278. xhr, [goog.net.EventType.ERROR, goog.net.EventType.ABORT],
  279. goog.bind(this.error_, this, download));
  280. this.eventHandler_.listen(
  281. xhr, goog.net.EventType.READY, goog.bind(this.freeXhr_, this, xhr));
  282. download.xhr = xhr;
  283. xhr.setResponseType(goog.net.XhrIo.ResponseType.ARRAY_BUFFER);
  284. xhr.send(download.url);
  285. };
  286. /**
  287. * The callback called when an XHR succeeds in downloading a remote file.
  288. *
  289. * @param {!goog.net.FileDownloader.Download_} download The download object for
  290. * this download.
  291. * @private
  292. */
  293. goog.net.FileDownloader.prototype.xhrSuccess_ = function(download) {
  294. if (download.cancelled) {
  295. return;
  296. }
  297. var name = this.sanitize_(
  298. this.getName_(
  299. /** @type {!goog.net.XhrIo} */ (download.xhr)));
  300. var resp = /** @type {ArrayBuffer} */ (download.xhr.getResponse());
  301. if (!resp) {
  302. // This should never happen - it indicates the XHR hasn't completed, has
  303. // failed or has been cleaned up. If it does happen (eg. due to a bug
  304. // somewhere) we don't want to pass null to getBlob - it's not valid and
  305. // triggers a bug in some versions of WebKit causing it to crash.
  306. this.error_(download);
  307. return;
  308. }
  309. download.blob = goog.fs.getBlob(resp);
  310. delete download.xhr;
  311. this.getDir_(download.url, goog.fs.DirectoryEntry.Behavior.CREATE_EXCLUSIVE)
  312. .addCallback(function(dir) {
  313. return dir.getFile(
  314. name, goog.fs.DirectoryEntry.Behavior.CREATE_EXCLUSIVE);
  315. })
  316. .addCallback(goog.bind(this.fileSuccess_, this, download))
  317. .addErrback(goog.bind(this.error_, this, download));
  318. };
  319. /**
  320. * The callback called when a file that will be used for saving a file is
  321. * successfully opened.
  322. *
  323. * @param {!goog.net.FileDownloader.Download_} download The download object for
  324. * this download.
  325. * @param {!goog.fs.FileEntry} file The newly-opened file object.
  326. * @private
  327. */
  328. goog.net.FileDownloader.prototype.fileSuccess_ = function(download, file) {
  329. if (download.cancelled) {
  330. file.remove();
  331. return;
  332. }
  333. download.file = file;
  334. file.createWriter()
  335. .addCallback(goog.bind(this.fileWriterSuccess_, this, download))
  336. .addErrback(goog.bind(this.error_, this, download));
  337. };
  338. /**
  339. * The callback called when a file writer is successfully created for writing a
  340. * file to the filesystem.
  341. *
  342. * @param {!goog.net.FileDownloader.Download_} download The download object for
  343. * this download.
  344. * @param {!goog.fs.FileWriter} writer The newly-created file writer object.
  345. * @private
  346. */
  347. goog.net.FileDownloader.prototype.fileWriterSuccess_ = function(
  348. download, writer) {
  349. if (download.cancelled) {
  350. download.file.remove();
  351. return;
  352. }
  353. download.writer = writer;
  354. writer.write(/** @type {!Blob} */ (download.blob));
  355. this.eventHandler_.listenOnce(
  356. writer, goog.fs.FileSaver.EventType.WRITE_END,
  357. goog.bind(this.writeEnd_, this, download));
  358. };
  359. /**
  360. * The callback called when file writing ends, whether or not it's successful.
  361. *
  362. * @param {!goog.net.FileDownloader.Download_} download The download object for
  363. * this download.
  364. * @private
  365. */
  366. goog.net.FileDownloader.prototype.writeEnd_ = function(download) {
  367. if (download.cancelled || download.writer.getError()) {
  368. this.error_(download, download.writer.getError());
  369. return;
  370. }
  371. delete this.downloads_[download.url];
  372. download.deferred.callback(download.blob);
  373. };
  374. /**
  375. * The error callback for all asynchronous operations. Ensures that all stages
  376. * of a given download are cleaned up, and emits the error event.
  377. *
  378. * @param {!goog.net.FileDownloader.Download_} download The download object for
  379. * this download.
  380. * @param {goog.fs.Error=} opt_err The file error object. Only defined if the
  381. * error was raised by the file API.
  382. * @private
  383. */
  384. goog.net.FileDownloader.prototype.error_ = function(download, opt_err) {
  385. if (download.file) {
  386. download.file.remove();
  387. }
  388. if (download.cancelled) {
  389. return;
  390. }
  391. delete this.downloads_[download.url];
  392. download.deferred.errback(
  393. new goog.net.FileDownloader.Error(download, opt_err));
  394. };
  395. /**
  396. * Abort the download of the given URL.
  397. *
  398. * @param {!goog.net.FileDownloader.Download_} download The download to abort.
  399. * @private
  400. */
  401. goog.net.FileDownloader.prototype.cancel_ = function(download) {
  402. goog.dispose(download);
  403. delete this.downloads_[download.url];
  404. };
  405. /**
  406. * Get the directory for a given URL. If the directory already exists when this
  407. * is called, it will contain exactly one file: the downloaded file.
  408. *
  409. * This not only calls the FileSystem API's getFile method, but attempts to
  410. * distribute the files so that they don't overload the filesystem. The spec
  411. * says directories can't contain more than 5000 files
  412. * (http://www.w3.org/TR/file-system-api/#directories), so this ensures that
  413. * each file is put into a subdirectory based on its SHA1 hash.
  414. *
  415. * All parameters are the same as in the FileSystem API's Entry#getFile method.
  416. *
  417. * @param {string} url The URL corresponding to the directory to get.
  418. * @param {goog.fs.DirectoryEntry.Behavior} behavior The behavior to pass to the
  419. * underlying method.
  420. * @return {!goog.async.Deferred} The deferred DirectoryEntry object.
  421. * @private
  422. */
  423. goog.net.FileDownloader.prototype.getDir_ = function(url, behavior) {
  424. // 3 hex digits provide 16**3 = 4096 different possible dirnames, which is
  425. // less than the maximum of 5000 entries. Downloaded files should be
  426. // distributed roughly evenly throughout the directories due to the hash
  427. // function, allowing many more than 5000 files to be downloaded.
  428. //
  429. // The leading ` ensures that no illegal dirnames are accidentally used. % was
  430. // previously used, but Chrome has a bug (as of 12.0.725.0 dev) where
  431. // filenames are URL-decoded before checking their validity, so filenames
  432. // containing e.g. '%3f' (the URL-encoding of :, an invalid character) are
  433. // rejected.
  434. var dirname = '`' +
  435. Math.abs(goog.crypt.hash32.encodeString(url))
  436. .toString(16)
  437. .substring(0, 3);
  438. return this.dir_.getDirectory(dirname, goog.fs.DirectoryEntry.Behavior.CREATE)
  439. .addCallback(function(dir) {
  440. return dir.getDirectory(this.sanitize_(url), behavior);
  441. }, this);
  442. };
  443. /**
  444. * Get the file for a given URL. This will only retrieve files that have already
  445. * been saved; it shouldn't be used for creating the file in the first place.
  446. * This is because the filename isn't necessarily determined by the URL, but by
  447. * the headers of the XHR response.
  448. *
  449. * @param {string} url The URL corresponding to the file to get.
  450. * @return {!goog.async.Deferred} The deferred FileEntry object.
  451. * @private
  452. */
  453. goog.net.FileDownloader.prototype.getFile_ = function(url) {
  454. return this.getDir_(url, goog.fs.DirectoryEntry.Behavior.DEFAULT)
  455. .addCallback(function(dir) {
  456. return dir.listDirectory().addCallback(function(files) {
  457. goog.asserts.assert(files.length == 1);
  458. // If the filesystem somehow gets corrupted and we end up with an
  459. // empty directory here, it makes sense to just return the normal
  460. // file-not-found error.
  461. return files[0] || dir.getFile('file');
  462. });
  463. });
  464. };
  465. /**
  466. * Sanitize a string so it can be safely used as a file or directory name for
  467. * the FileSystem API.
  468. *
  469. * @param {string} str The string to sanitize.
  470. * @return {string} The sanitized string.
  471. * @private
  472. */
  473. goog.net.FileDownloader.prototype.sanitize_ = function(str) {
  474. // Add a prefix, since certain prefixes are disallowed for paths. None of the
  475. // disallowed prefixes start with '`'. We use ` rather than % for escaping the
  476. // filename due to a Chrome bug (as of 12.0.725.0 dev) where filenames are
  477. // URL-decoded before checking their validity, so filenames containing e.g.
  478. // '%3f' (the URL-encoding of :, an invalid character) are rejected.
  479. return '`' +
  480. str.replace(/[\/\\<>:?*"|%`]/g, encodeURIComponent).replace(/%/g, '`');
  481. };
  482. /**
  483. * Gets the filename specified by the XHR. This first attempts to parse the
  484. * Content-Disposition header for a filename and, failing that, falls back on
  485. * deriving the filename from the URL.
  486. *
  487. * @param {!goog.net.XhrIo} xhr The XHR containing the response headers.
  488. * @return {string} The filename.
  489. * @private
  490. */
  491. goog.net.FileDownloader.prototype.getName_ = function(xhr) {
  492. var disposition = xhr.getResponseHeader('Content-Disposition');
  493. var match =
  494. disposition && disposition.match(/^attachment *; *filename="(.*)"$/i);
  495. if (match) {
  496. // The Content-Disposition header allows for arbitrary backslash-escaped
  497. // characters (usually " and \). We want to unescape them before using them
  498. // in the filename.
  499. return match[1].replace(/\\(.)/g, '$1');
  500. }
  501. return this.urlToName_(xhr.getLastUri());
  502. };
  503. /**
  504. * Extracts the basename from a URL.
  505. *
  506. * @param {string} url The URL.
  507. * @return {string} The basename.
  508. * @private
  509. */
  510. goog.net.FileDownloader.prototype.urlToName_ = function(url) {
  511. var segments = url.split('/');
  512. return segments[segments.length - 1];
  513. };
  514. /**
  515. * Remove all event listeners for an XHR and release it back into the pool.
  516. *
  517. * @param {!goog.net.XhrIo} xhr The XHR to free.
  518. * @private
  519. */
  520. goog.net.FileDownloader.prototype.freeXhr_ = function(xhr) {
  521. goog.events.removeAll(xhr);
  522. this.pool_.addFreeObject(xhr);
  523. };
  524. /** @override */
  525. goog.net.FileDownloader.prototype.disposeInternal = function() {
  526. delete this.dir_;
  527. goog.dispose(this.eventHandler_);
  528. delete this.eventHandler_;
  529. goog.object.forEach(this.downloads_, function(download) {
  530. download.deferred.cancel();
  531. }, this);
  532. delete this.downloads_;
  533. goog.dispose(this.pool_);
  534. delete this.pool_;
  535. goog.net.FileDownloader.base(this, 'disposeInternal');
  536. };
  537. /**
  538. * The error object for FileDownloader download errors.
  539. *
  540. * @param {!goog.net.FileDownloader.Download_} download The download object for
  541. * the download in question.
  542. * @param {goog.fs.Error=} opt_fsErr The file error object, if this was a file
  543. * error.
  544. *
  545. * @constructor
  546. * @extends {goog.debug.Error}
  547. * @final
  548. */
  549. goog.net.FileDownloader.Error = function(download, opt_fsErr) {
  550. goog.net.FileDownloader.Error.base(
  551. this, 'constructor', 'Error capturing URL ' + download.url);
  552. /**
  553. * The URL the event relates to.
  554. * @type {string}
  555. */
  556. this.url = download.url;
  557. if (download.xhr) {
  558. this.xhrStatus = download.xhr.getStatus();
  559. this.xhrErrorCode = download.xhr.getLastErrorCode();
  560. this.message += ': XHR failed with status ' + this.xhrStatus +
  561. ' (error code ' + this.xhrErrorCode + ')';
  562. } else if (opt_fsErr) {
  563. this.fileError = opt_fsErr;
  564. this.message += ': file API failed (' + opt_fsErr.message + ')';
  565. }
  566. };
  567. goog.inherits(goog.net.FileDownloader.Error, goog.debug.Error);
  568. /**
  569. * The status of the XHR. Only set if the error was caused by an XHR failure.
  570. * @type {number|undefined}
  571. */
  572. goog.net.FileDownloader.Error.prototype.xhrStatus;
  573. /**
  574. * The error code of the XHR. Only set if the error was caused by an XHR
  575. * failure.
  576. * @type {goog.net.ErrorCode|undefined}
  577. */
  578. goog.net.FileDownloader.Error.prototype.xhrErrorCode;
  579. /**
  580. * The file API error. Only set if the error was caused by the file API.
  581. * @type {goog.fs.Error|undefined}
  582. */
  583. goog.net.FileDownloader.Error.prototype.fileError;
  584. /**
  585. * A struct containing the data for a single download.
  586. *
  587. * @param {string} url The URL for the file being downloaded.
  588. * @param {!goog.net.FileDownloader} downloader The parent FileDownloader.
  589. * @extends {goog.Disposable}
  590. * @constructor
  591. * @private
  592. */
  593. goog.net.FileDownloader.Download_ = function(url, downloader) {
  594. goog.net.FileDownloader.Download_.base(this, 'constructor');
  595. /**
  596. * The URL for the file being downloaded.
  597. * @type {string}
  598. */
  599. this.url = url;
  600. /**
  601. * The Deferred that will be fired when the download is complete.
  602. * @type {!goog.async.Deferred}
  603. */
  604. this.deferred =
  605. new goog.async.Deferred(goog.bind(downloader.cancel_, downloader, this));
  606. /**
  607. * Whether this download has been cancelled by the user.
  608. * @type {boolean}
  609. */
  610. this.cancelled = false;
  611. /**
  612. * The XhrIo object for downloading the file. Only set once it's been
  613. * retrieved from the pool.
  614. * @type {goog.net.XhrIo}
  615. */
  616. this.xhr = null;
  617. /**
  618. * The name of the blob being downloaded. Only sey once the XHR has completed,
  619. * if it completed successfully.
  620. * @type {?string}
  621. */
  622. this.name = null;
  623. /**
  624. * The downloaded blob. Only set once the XHR has completed, if it completed
  625. * successfully.
  626. * @type {Blob}
  627. */
  628. this.blob = null;
  629. /**
  630. * The file entry where the blob is to be stored. Only set once it's been
  631. * loaded from the filesystem.
  632. * @type {goog.fs.FileEntry}
  633. */
  634. this.file = null;
  635. /**
  636. * The file writer for writing the blob to the filesystem. Only set once it's
  637. * been loaded from the filesystem.
  638. * @type {goog.fs.FileWriter}
  639. */
  640. this.writer = null;
  641. };
  642. goog.inherits(goog.net.FileDownloader.Download_, goog.Disposable);
  643. /** @override */
  644. goog.net.FileDownloader.Download_.prototype.disposeInternal = function() {
  645. this.cancelled = true;
  646. if (this.xhr) {
  647. this.xhr.abort();
  648. } else if (
  649. this.writer &&
  650. this.writer.getReadyState() == goog.fs.FileSaver.ReadyState.WRITING) {
  651. this.writer.abort();
  652. }
  653. goog.net.FileDownloader.Download_.base(this, 'disposeInternal');
  654. };