aboutsummaryrefslogtreecommitdiff
path: root/contexts/data/lib/closure-library/closure/goog/net/filedownloader.js
blob: da551a1c0af23f313c7f7f5a75f5f4a62b81e0c5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
// Copyright 2011 The Closure Library Authors. All Rights Reserved.
//
// Licensed 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.

/**
 * @fileoverview A class for downloading remote files and storing them
 * locally using the HTML5 FileSystem API.
 *
 * The directory structure is of the form /HASH/URL/BASENAME:
 *
 * The HASH portion is a three-character slice of the hash of the URL. Since the
 * filesystem has a limit of about 5000 files per directory, this should divide
 * the downloads roughly evenly among about 5000 directories, thus allowing for
 * at most 5000^2 downloads.
 *
 * The URL portion is the (sanitized) full URL used for downloading the file.
 * This is used to ensure that each file ends up in a different location, even
 * if the HASH and BASENAME are the same.
 *
 * The BASENAME portion is the basename of the URL. It's used for the filename
 * proper so that the local filesystem: URL will be downloaded to a file with a
 * recognizable name.
 *
 */

goog.provide('goog.net.FileDownloader');
goog.provide('goog.net.FileDownloader.Error');

goog.require('goog.Disposable');
goog.require('goog.asserts');
goog.require('goog.async.Deferred');
goog.require('goog.crypt.hash32');
goog.require('goog.debug.Error');
goog.require('goog.events.EventHandler');
goog.require('goog.fs');
goog.require('goog.fs.DirectoryEntry.Behavior');
goog.require('goog.fs.Error.ErrorCode');
goog.require('goog.fs.FileSaver.EventType');
goog.require('goog.net.EventType');
goog.require('goog.net.XhrIo.ResponseType');
goog.require('goog.net.XhrIoPool');



/**
 * A class for downloading remote files and storing them locally using the
 * HTML5 filesystem API.
 *
 * @param {!goog.fs.DirectoryEntry} dir The directory in which the downloaded
 *     files are stored. This directory should be solely managed by
 *     FileDownloader.
 * @param {goog.net.XhrIoPool=} opt_pool The pool of XhrIo objects to use for
 *     downloading files.
 * @constructor
 * @extends {goog.Disposable}
 */
goog.net.FileDownloader = function(dir, opt_pool) {
  goog.base(this);

  /**
   * The directory in which the downloaded files are stored.
   * @type {!goog.fs.DirectoryEntry}
   * @private
   */
  this.dir_ = dir;

  /**
   * The pool of XHRs to use for capturing.
   * @type {!goog.net.XhrIoPool}
   * @private
   */
  this.pool_ = opt_pool || new goog.net.XhrIoPool();

  /**
   * A map from URLs to active downloads running for those URLs.
   * @type {!Object.<!goog.net.FileDownloader.Download_>}
   * @private
   */
  this.downloads_ = {};

  /**
   * The handler for URL capturing events.
   * @type {!goog.events.EventHandler}
   * @private
   */
  this.eventHandler_ = new goog.events.EventHandler(this);
};
goog.inherits(goog.net.FileDownloader, goog.Disposable);


/**
 * Download a remote file and save its contents to the filesystem. A given file
 * is uniquely identified by its URL string; this means that the relative and
 * absolute URLs for a single file are considered different for the purposes of
 * the FileDownloader.
 *
 * Returns a Deferred that will contain the downloaded blob. If there's an error
 * while downloading the URL, this Deferred will be passed the
 * {@link goog.net.FileDownloader.Error} object as an errback.
 *
 * If a download is already in progress for the given URL, this will return the
 * deferred blob for that download. If the URL has already been downloaded, this
 * will fail once it tries to save the downloaded blob.
 *
 * When a download is in progress, all Deferreds returned for that download will
 * be branches of a single parent. If all such branches are cancelled, or if one
 * is cancelled with opt_deepCancel set, then the download will be cancelled as
 * well.
 *
 * @param {string} url The URL of the file to download.
 * @return {!goog.async.Deferred} The deferred result blob.
 */
goog.net.FileDownloader.prototype.download = function(url) {
  if (this.isDownloading(url)) {
    return this.downloads_[url].deferred.branch(true /* opt_propagateCancel */);
  }

  var download = new goog.net.FileDownloader.Download_(url, this);
  this.downloads_[url] = download;
  this.pool_.getObject(goog.bind(this.gotXhr_, this, download));
  return download.deferred.branch(true /* opt_propagateCancel */);
};


/**
 * Return a Deferred that will fire once no download is active for a given URL.
 * If there's no download active for that URL when this is called, the deferred
 * will fire immediately; otherwise, it will fire once the download is complete,
 * whether or not it succeeds.
 *
 * @param {string} url The URL of the download to wait for.
 * @return {!goog.async.Deferred} The Deferred that will fire when the download
 *     is complete.
 */
goog.net.FileDownloader.prototype.waitForDownload = function(url) {
  var deferred = new goog.async.Deferred();
  if (this.isDownloading(url)) {
    this.downloads_[url].deferred.addBoth(function() {
      deferred.callback(null);
    }, this);
  } else {
    deferred.callback(null);
  }
  return deferred;
};


/**
 * Returns whether or not there is an active download for a given URL.
 *
 * @param {string} url The URL of the download to check.
 * @return {boolean} Whether or not there is an active download for the URL.
 */
goog.net.FileDownloader.prototype.isDownloading = function(url) {
  return url in this.downloads_;
};


/**
 * Load a downloaded blob from the filesystem. Will fire a deferred error if the
 * given URL has not yet been downloaded.
 *
 * @param {string} url The URL of the blob to load.
 * @return {!goog.async.Deferred} The deferred Blob object. The callback will be
 *     passed the blob. If a file API error occurs while loading the blob, that
 *     error will be passed to the errback.
 */
goog.net.FileDownloader.prototype.getDownloadedBlob = function(url) {
  return this.getFile_(url).
      addCallback(function(fileEntry) { return fileEntry.file(); });
};


/**
 * Get the local filesystem: URL for a downloaded file. This is different from
 * the blob: URL that's available from getDownloadedBlob(). If the end user
 * accesses the filesystem: URL, the resulting file's name will be determined by
 * the download filename as opposed to an arbitrary GUID. In addition, the
 * filesystem: URL is connected to a filesystem location, so if the download is
 * removed then that URL will become invalid.
 *
 * Warning: in Chrome 12, some filesystem: URLs are opened inline. This means
 * that e.g. HTML pages given to the user via filesystem: URLs will be opened
 * and processed by the browser.
 *
 * @param {string} url The URL of the file to get the URL of.
 * @return {!goog.async.Deferred} The deferred filesystem: URL. The callback
 *     will be passed the URL. If a file API error occurs while loading the
 *     blob, that error will be passed to the errback.
 */
goog.net.FileDownloader.prototype.getLocalUrl = function(url) {
  return this.getFile_(url).
      addCallback(function(fileEntry) { return fileEntry.toUrl(); });
};


/**
 * Return (deferred) whether or not a URL has been downloaded. Will fire a
 * deferred error if something goes wrong when determining this.
 *
 * @param {string} url The URL to check.
 * @return {!goog.async.Deferred} The deferred boolean. The callback will be
 *     passed the boolean. If a file API error occurs while checking the
 *     existence of the downloaded URL, that error will be passed to the
 *     errback.
 */
goog.net.FileDownloader.prototype.isDownloaded = function(url) {
  var deferred = new goog.async.Deferred();
  var blobDeferred = this.getDownloadedBlob(url);
  blobDeferred.addCallback(function() {
    deferred.callback(true);
  });
  blobDeferred.addErrback(function(err) {
    if (err.code == goog.fs.Error.ErrorCode.NOT_FOUND) {
      deferred.callback(false);
    } else {
      deferred.errback(err);
    }
  });
  return deferred;
};


/**
 * Remove a URL from the FileDownloader.
 *
 * This returns a Deferred. If the removal is completed successfully, its
 * callback will be called without any value. If the removal fails, its errback
 * will be called with the {@link goog.fs.Error}.
 *
 * @param {string} url The URL to remove.
 * @return {!goog.async.Deferred} The deferred used for registering callbacks on
 *     success or on error.
 */
goog.net.FileDownloader.prototype.remove = function(url) {
  return this.getDir_(url, goog.fs.DirectoryEntry.Behavior.DEFAULT).
      addCallback(function(dir) { return dir.removeRecursively(); });
};


/**
 * Save a blob for a given URL. This works just as through the blob were
 * downloaded form that URL, except you specify the blob and no HTTP request is
 * made.
 *
 * If the URL is currently being downloaded, it's indeterminate whether the blob
 * being set or the blob being downloaded will end up in the filesystem.
 * Whichever one doesn't get saved will have an error. To ensure that one or the
 * other takes precedence, use {@link #waitForDownload} to allow the download to
 * complete before setting the blob.
 *
 * @param {string} url The URL at which to set the blob.
 * @param {!Blob} blob The blob to set.
 * @param {string=} opt_name The name of the file. If this isn't given, it's
 *     determined from the URL.
 * @return {!goog.async.Deferred} The deferred used for registering callbacks on
 *     success or on error. This can be cancelled just like a {@link #download}
 *     Deferred. The objects passed to the errback will be
 *     {@link goog.net.FileDownloader.Error}s.
 */
goog.net.FileDownloader.prototype.setBlob = function(url, blob, opt_name) {
  var name = this.sanitize_(opt_name || this.urlToName_(url));
  var download = new goog.net.FileDownloader.Download_(url, this);
  this.downloads_[url] = download;
  download.blob = blob;
  this.getDir_(download.url, goog.fs.DirectoryEntry.Behavior.CREATE_EXCLUSIVE).
      addCallback(function(dir) {
        return dir.getFile(
            name, goog.fs.DirectoryEntry.Behavior.CREATE_EXCLUSIVE);
      }).
      addCallback(goog.bind(this.fileSuccess_, this, download)).
      addErrback(goog.bind(this.error_, this, download));
  return download.deferred.branch(true /* opt_propagateCancel */);
};


/**
 * The callback called when an XHR becomes available from the XHR pool.
 *
 * @param {!goog.net.FileDownloader.Download_} download The download object for
 *     this download.
 * @param {!goog.net.XhrIo} xhr The XhrIo object for downloading the page.
 * @private
 */
goog.net.FileDownloader.prototype.gotXhr_ = function(download, xhr) {
  if (download.cancelled) {
    this.freeXhr_(xhr);
    return;
  }

  this.eventHandler_.listen(
      xhr, goog.net.EventType.SUCCESS,
      goog.bind(this.xhrSuccess_, this, download));
  this.eventHandler_.listen(
      xhr, [goog.net.EventType.ERROR, goog.net.EventType.ABORT],
      goog.bind(this.error_, this, download));
  this.eventHandler_.listen(
      xhr, goog.net.EventType.READY,
      goog.bind(this.freeXhr_, this, xhr));

  download.xhr = xhr;
  xhr.setResponseType(goog.net.XhrIo.ResponseType.ARRAY_BUFFER);
  xhr.send(download.url);
};


/**
 * The callback called when an XHR succeeds in downloading a remote file.
 *
 * @param {!goog.net.FileDownloader.Download_} download The download object for
 *     this download.
 * @private
 */
goog.net.FileDownloader.prototype.xhrSuccess_ = function(download) {
  if (download.cancelled) {
    return;
  }

  var name = this.sanitize_(this.getName_(
      /** @type {!goog.net.XhrIo} */ (download.xhr)));
  var resp = /** @type {ArrayBuffer} */ (download.xhr.getResponse());
  if (!resp) {
    // This should never happen - it indicates the XHR hasn't completed, has
    // failed or has been cleaned up.  If it does happen (eg. due to a bug
    // somewhere) we don't want to pass null to getBlob - it's not valid and
    // triggers a bug in some versions of WebKit causing it to crash.
    this.error_(download);
    return;
  }

  download.blob = goog.fs.getBlob(resp);
  delete download.xhr;

  this.getDir_(download.url, goog.fs.DirectoryEntry.Behavior.CREATE_EXCLUSIVE).
      addCallback(function(dir) {
        return dir.getFile(
            name, goog.fs.DirectoryEntry.Behavior.CREATE_EXCLUSIVE);
      }).
      addCallback(goog.bind(this.fileSuccess_, this, download)).
      addErrback(goog.bind(this.error_, this, download));
};


/**
 * The callback called when a file that will be used for saving a file is
 * successfully opened.
 *
 * @param {!goog.net.FileDownloader.Download_} download The download object for
 *     this download.
 * @param {!goog.fs.FileEntry} file The newly-opened file object.
 * @private
 */
goog.net.FileDownloader.prototype.fileSuccess_ = function(download, file) {
  if (download.cancelled) {
    file.remove();
    return;
  }

  download.file = file;
  file.createWriter().
      addCallback(goog.bind(this.fileWriterSuccess_, this, download)).
      addErrback(goog.bind(this.error_, this, download));
};


/**
 * The callback called when a file writer is succesfully created for writing a
 * file to the filesystem.
 *
 * @param {!goog.net.FileDownloader.Download_} download The download object for
 *     this download.
 * @param {!goog.fs.FileWriter} writer The newly-created file writer object.
 * @private
 */
goog.net.FileDownloader.prototype.fileWriterSuccess_ = function(
    download, writer) {
  if (download.cancelled) {
    download.file.remove();
    return;
  }

  download.writer = writer;
  writer.write(/** @type {!Blob} */ (download.blob));
  this.eventHandler_.listenOnce(
      writer,
      goog.fs.FileSaver.EventType.WRITE_END,
      goog.bind(this.writeEnd_, this, download));
};


/**
 * The callback called when file writing ends, whether or not it's successful.
 *
 * @param {!goog.net.FileDownloader.Download_} download The download object for
 *     this download.
 * @private
 */
goog.net.FileDownloader.prototype.writeEnd_ = function(download) {
  if (download.cancelled || download.writer.getError()) {
    this.error_(download, download.writer.getError());
    return;
  }

  delete this.downloads_[download.url];
  download.deferred.callback(download.blob);
};


/**
 * The error callback for all asynchronous operations. Ensures that all stages
 * of a given download are cleaned up, and emits the error event.
 *
 * @param {!goog.net.FileDownloader.Download_} download The download object for
 *     this download.
 * @param {goog.fs.Error=} opt_err The file error object. Only defined if the
 *     error was raised by the file API.
 * @private
 */
goog.net.FileDownloader.prototype.error_ = function(download, opt_err) {
  if (download.file) {
    download.file.remove();
  }

  if (download.cancelled) {
    return;
  }

  delete this.downloads_[download.url];
  download.deferred.errback(
      new goog.net.FileDownloader.Error(download, opt_err));
};


/**
 * Abort the download of the given URL.
 *
 * @param {!goog.net.FileDownloader.Download_} download The download to abort.
 * @private
 */
goog.net.FileDownloader.prototype.cancel_ = function(download) {
  goog.dispose(download);
  delete this.downloads_[download.url];
};


/**
 * Get the directory for a given URL. If the directory already exists when this
 * is called, it will contain exactly one file: the downloaded file.
 *
 * This not only calls the FileSystem API's getFile method, but attempts to
 * distribute the files so that they don't overload the filesystem. The spec
 * says directories can't contain more than 5000 files
 * (http://www.w3.org/TR/file-system-api/#directories), so this ensures that
 * each file is put into a subdirectory based on its SHA1 hash.
 *
 * All parameters are the same as in the FileSystem API's Entry#getFile method.
 *
 * @param {string} url The URL corresponding to the directory to get.
 * @param {goog.fs.DirectoryEntry.Behavior} behavior The behavior to pass to the
 *     underlying method.
 * @return {!goog.async.Deferred} The deferred DirectoryEntry object.
 * @private
 */
goog.net.FileDownloader.prototype.getDir_ = function(url, behavior) {
  // 3 hex digits provide 16**3 = 4096 different possible dirnames, which is
  // less than the maximum of 5000 entries. Downloaded files should be
  // distributed roughly evenly throughout the directories due to the hash
  // function, allowing many more than 5000 files to be downloaded.
  //
  // The leading ` ensures that no illegal dirnames are accidentally used. % was
  // previously used, but Chrome has a bug (as of 12.0.725.0 dev) where
  // filenames are URL-decoded before checking their validity, so filenames
  // containing e.g. '%3f' (the URL-encoding of :, an invalid character) are
  // rejected.
  var dirname = '`' + Math.abs(goog.crypt.hash32.encodeString(url)).
      toString(16).substring(0, 3);

  return this.dir_.
      getDirectory(dirname, goog.fs.DirectoryEntry.Behavior.CREATE).
      addCallback(function(dir) {
        return dir.getDirectory(this.sanitize_(url), behavior);
      }, this);
};


/**
 * Get the file for a given URL. This will only retrieve files that have already
 * been saved; it shouldn't be used for creating the file in the first place.
 * This is because the filename isn't necessarily determined by the URL, but by
 * the headers of the XHR response.
 *
 * @param {string} url The URL corresponding to the file to get.
 * @return {!goog.async.Deferred} The deferred FileEntry object.
 * @private
 */
goog.net.FileDownloader.prototype.getFile_ = function(url) {
  return this.getDir_(url, goog.fs.DirectoryEntry.Behavior.DEFAULT).
      addCallback(function(dir) {
        return dir.listDirectory().addCallback(function(files) {
          goog.asserts.assert(files.length == 1);
          // If the filesystem somehow gets corrupted and we end up with an
          // empty directory here, it makes sense to just return the normal
          // file-not-found error.
          return files[0] || dir.getFile('file');
        });
      });
};


/**
 * Sanitize a string so it can be safely used as a file or directory name for
 * the FileSystem API.
 *
 * @param {string} str The string to sanitize.
 * @return {string} The sanitized string.
 * @private
 */
goog.net.FileDownloader.prototype.sanitize_ = function(str) {
  // Add a prefix, since certain prefixes are disallowed for paths. None of the
  // disallowed prefixes start with '`'. We use ` rather than % for escaping the
  // filename due to a Chrome bug (as of 12.0.725.0 dev) where filenames are
  // URL-decoded before checking their validity, so filenames containing e.g.
  // '%3f' (the URL-encoding of :, an invalid character) are rejected.
  return '`' + str.replace(/[\/\\<>:?*"|%`]/g, encodeURIComponent).
      replace(/%/g, '`');
};


/**
 * Gets the filename specified by the XHR. This first attempts to parse the
 * Content-Disposition header for a filename and, failing that, falls back on
 * deriving the filename from the URL.
 *
 * @param {!goog.net.XhrIo} xhr The XHR containing the response headers.
 * @return {string} The filename.
 * @private
 */
goog.net.FileDownloader.prototype.getName_ = function(xhr) {
  var disposition = xhr.getResponseHeader('Content-Disposition');
  var match = disposition &&
      disposition.match(/^attachment *; *filename="(.*)"$/i);
  if (match) {
    // The Content-Disposition header allows for arbitrary backslash-escaped
    // characters (usually " and \). We want to unescape them before using them
    // in the filename.
    return match[1].replace(/\\(.)/g, '$1');
  }

  return this.urlToName_(xhr.getLastUri());
};


/**
 * Extracts the basename from a URL.
 *
 * @param {string} url The URL.
 * @return {string} The basename.
 * @private
 */
goog.net.FileDownloader.prototype.urlToName_ = function(url) {
  var segments = url.split('/');
  return segments[segments.length - 1];
};


/**
 * Remove all event listeners for an XHR and release it back into the pool.
 *
 * @param {!goog.net.XhrIo} xhr The XHR to free.
 * @private
 */
goog.net.FileDownloader.prototype.freeXhr_ = function(xhr) {
  goog.events.removeAll(xhr);
  this.pool_.addFreeObject(xhr);
};


/** @override */
goog.net.FileDownloader.prototype.disposeInternal = function() {
  delete this.dir_;
  goog.dispose(this.eventHandler_);
  delete this.eventHandler_;
  goog.object.forEach(this.downloads_, function(download) {
    download.deferred.cancel();
  }, this);
  delete this.downloads_;
  goog.dispose(this.pool_);
  delete this.pool_;

  goog.base(this, 'disposeInternal');
};



/**
 * The error object for FileDownloader download errors.
 *
 * @param {!goog.net.FileDownloader.Download_} download The download object for
 *     the download in question.
 * @param {goog.fs.Error=} opt_fsErr The file error object, if this was a file
 *     error.
 *
 * @constructor
 * @extends {goog.debug.Error}
 */
goog.net.FileDownloader.Error = function(download, opt_fsErr) {
  goog.base(this, 'Error capturing URL ' + download.url);

  /**
   * The URL the event relates to.
   * @type {string}
   */
  this.url = download.url;

  if (download.xhr) {
    this.xhrStatus = download.xhr.getStatus();
    this.xhrErrorCode = download.xhr.getLastErrorCode();
    this.message += ': XHR failed with status ' + this.xhrStatus +
        ' (error code ' + this.xhrErrorCode + ')';
  } else if (opt_fsErr) {
    this.fileError = opt_fsErr;
    this.message += ': file API failed (' + opt_fsErr.message + ')';
  }
};
goog.inherits(goog.net.FileDownloader.Error, goog.debug.Error);


/**
 * The status of the XHR. Only set if the error was caused by an XHR failure.
 * @type {number|undefined}
 */
goog.net.FileDownloader.Error.prototype.xhrStatus;


/**
 * The error code of the XHR. Only set if the error was caused by an XHR
 * failure.
 * @type {goog.net.ErrorCode|undefined}
 */
goog.net.FileDownloader.Error.prototype.xhrErrorCode;


/**
 * The file API error. Only set if the error was caused by the file API.
 * @type {goog.fs.Error|undefined}
 */
goog.net.FileDownloader.Error.prototype.fileError;



/**
 * A struct containing the data for a single download.
 *
 * @param {string} url The URL for the file being downloaded.
 * @param {!goog.net.FileDownloader} downloader The parent FileDownloader.
 * @extends {goog.Disposable}
 * @constructor
 * @private
 */
goog.net.FileDownloader.Download_ = function(url, downloader) {
  goog.base(this);

  /**
   * The URL for the file being downloaded.
   * @type {string}
   */
  this.url = url;

  /**
   * The Deferred that will be fired when the download is complete.
   * @type {!goog.async.Deferred}
   */
  this.deferred = new goog.async.Deferred(
      goog.bind(downloader.cancel_, downloader, this));

  /**
   * Whether this download has been cancelled by the user.
   * @type {boolean}
   */
  this.cancelled = false;

  /**
   * The XhrIo object for downloading the file. Only set once it's been
   * retrieved from the pool.
   * @type {goog.net.XhrIo}
   */
  this.xhr = null;

  /**
   * The name of the blob being downloaded. Only sey once the XHR has completed,
   * if it completed successfully.
   * @type {?string}
   */
  this.name = null;

  /**
   * The downloaded blob. Only set once the XHR has completed, if it completed
   * successfully.
   * @type {Blob}
   */
  this.blob = null;

  /**
   * The file entry where the blob is to be stored. Only set once it's been
   * loaded from the filesystem.
   * @type {goog.fs.FileEntry}
   */
  this.file = null;

  /**
   * The file writer for writing the blob to the filesystem. Only set once it's
   * been loaded from the filesystem.
   * @type {goog.fs.FileWriter}
   */
  this.writer = null;
};
goog.inherits(goog.net.FileDownloader.Download_, goog.Disposable);


/** @override */
goog.net.FileDownloader.Download_.prototype.disposeInternal = function() {
  this.cancelled = true;
  if (this.xhr) {
    this.xhr.abort();
  } else if (this.writer && this.writer.getReadyState() ==
             goog.fs.FileSaver.ReadyState.WRITING) {
    this.writer.abort();
  }

  goog.base(this, 'disposeInternal');
};