Home | History | Annotate | Download | only in js
      1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 'use strict';
      6 
      7 /**
      8  * Utilities for FileCopyManager.
      9  */
     10 var fileOperationUtil = {};
     11 
     12 /**
     13  * Simple wrapper for util.deduplicatePath. On error, this method translates
     14  * the FileError to FileCopyManager.Error object.
     15  *
     16  * @param {DirectoryEntry} dirEntry The target directory entry.
     17  * @param {string} relativePath The path to be deduplicated.
     18  * @param {function(string)} successCallback Callback run with the deduplicated
     19  *     path on success.
     20  * @param {function(FileCopyManager.Error)} errorCallback Callback run on error.
     21  */
     22 fileOperationUtil.deduplicatePath = function(
     23     dirEntry, relativePath, successCallback, errorCallback) {
     24   util.deduplicatePath(
     25       dirEntry, relativePath, successCallback,
     26       function(err) {
     27         var onFileSystemError = function(error) {
     28           errorCallback(new FileCopyManager.Error(
     29               util.FileOperationErrorType.FILESYSTEM_ERROR, error));
     30         };
     31 
     32         if (err.code == FileError.PATH_EXISTS_ERR) {
     33           // Failed to uniquify the file path. There should be an existing
     34           // entry, so return the error with it.
     35           util.resolvePath(
     36               dirEntry, relativePath,
     37               function(entry) {
     38                 errorCallback(new FileCopyManager.Error(
     39                     util.FileOperationErrorType.TARGET_EXISTS, entry));
     40               },
     41               onFileSystemError);
     42           return;
     43         }
     44         onFileSystemError(err);
     45       });
     46 };
     47 
     48 /**
     49  * Sets last modified date to the entry.
     50  * @param {Entry} entry The entry to which the last modified is set.
     51  * @param {Date} modificationTime The last modified time.
     52  */
     53 fileOperationUtil.setLastModified = function(entry, modificationTime) {
     54   chrome.fileBrowserPrivate.setLastModified(
     55       entry.toURL(), '' + Math.round(modificationTime.getTime() / 1000));
     56 };
     57 
     58 /**
     59  * Copies a file from source to the parent directory with newName.
     60  * See also copyFileByStream_ and copyFileOnDrive_ for the implementation
     61  * details.
     62  *
     63  * @param {FileEntry} source The file entry to be copied.
     64  * @param {DirectoryEntry} parent The entry of the destination directory.
     65  * @param {string} newName The name of copied file.
     66  * @param {function(FileEntry, number)} progressCallback Callback invoked
     67  *     periodically during the file writing. It takes source and the number of
     68  *     copied bytes since the last invocation. This is also called just before
     69  *     starting the operation (with '0' bytes) and just after the finishing the
     70  *     operation (with the total copied size).
     71  * @param {function(FileEntry)} successCallback Callback invoked when the copy
     72  *     is successfully done with the entry of the created file.
     73  * @param {function(FileError)} errorCallback Callback invoked when an error
     74  *     is found.
     75  * @return {function()} Callback to cancle the current file copy operation.
     76  *     When the cancel is done, errorCallback will be called. The returned
     77  *     callback must not be called more than once.
     78  */
     79 fileOperationUtil.copyFile = function(
     80     source, parent, newName, progressCallback, successCallback, errorCallback) {
     81   if (!PathUtil.isDriveBasedPath(source.fullPath) &&
     82       !PathUtil.isDriveBasedPath(parent.fullPath)) {
     83     // Copying a file between non-Drive file systems.
     84     return fileOperationUtil.copyFileByStream_(
     85         source, parent, newName, progressCallback, successCallback,
     86         errorCallback);
     87   } else {
     88     // Copying related to the Drive file system.
     89     return fileOperationUtil.copyFileOnDrive_(
     90         source, parent, newName, progressCallback, successCallback,
     91         errorCallback);
     92   }
     93 };
     94 
     95 /**
     96  * Copies a file by using File and FileWriter objects.
     97  *
     98  * This is a js-implementation of FileEntry.copyTo(). Unfortunately, copyTo
     99  * doesn't support periodical progress updating nor cancelling. To support
    100  * these operations, this method implements copyTo by streaming way in
    101  * JavaScript.
    102  *
    103  * Note that this is designed for file copying on local file system. We have
    104  * some special cases about copying on Drive file system. See also
    105  * copyFileOnDrive_() for more details.
    106  *
    107  * @param {FileEntry} source The file entry to be copied.
    108  * @param {DirectoryEntry} parent The entry of the destination directory.
    109  * @param {string} newName The name of copied file.
    110  * @param {function(FileEntry, number)} progressCallback Callback invoked
    111  *     periodically during the file writing. It takes source and the number of
    112  *     copied bytes since the last invocation. This is also called just before
    113  *     starting the operation (with '0' bytes) and just after the finishing the
    114  *     operation (with the total copied size).
    115  * @param {function(FileEntry)} successCallback Callback invoked when the copy
    116  *     is successfully done with the entry of the created file.
    117  * @param {function(FileError)} errorCallback Callback invoked when an error
    118  *     is found.
    119  * @return {function()} Callback to cancel the current file copy operation.
    120  *     When the cancel is done, errorCallback will be called. The returned
    121  *     callback must not be called more than once.
    122  * @private
    123  */
    124 fileOperationUtil.copyFileByStream_ = function(
    125     source, parent, newName, progressCallback, successCallback, errorCallback) {
    126   // Set to true when cancel is requested.
    127   var cancelRequested = false;
    128 
    129   source.file(function(file) {
    130     if (cancelRequested) {
    131       errorCallback(util.createFileError(FileError.ABORT_ERR));
    132       return;
    133     }
    134 
    135     parent.getFile(newName, {create: true, exclusive: true}, function(target) {
    136       if (cancelRequested) {
    137         errorCallback(util.createFileError(FileError.ABORT_ERR));
    138         return;
    139       }
    140 
    141       target.createWriter(function(writer) {
    142         if (cancelRequested) {
    143           errorCallback(util.createFileError(FileError.ABORT_ERR));
    144           return;
    145         }
    146 
    147         writer.onerror = writer.onabort = function(progress) {
    148           errorCallback(cancelRequested ?
    149               util.createFileError(FileError.ABORT_ERR) :
    150                   writer.error);
    151         };
    152 
    153         var reportedProgress = 0;
    154         writer.onprogress = function(progress) {
    155           if (cancelRequested) {
    156             // If the copy was cancelled, we should abort the operation.
    157             // The errorCallback will be called by writer.onabort after the
    158             // termination.
    159             writer.abort();
    160             return;
    161           }
    162 
    163           // |progress.loaded| will contain total amount of data copied by now.
    164           // |progressCallback| expects data amount delta from the last progress
    165           // update.
    166           progressCallback(target, progress.loaded - reportedProgress);
    167           reportedProgress = progress.loaded;
    168         };
    169 
    170         writer.onwrite = function() {
    171           if (cancelRequested) {
    172             errorCallback(util.createFileError(FileError.ABORT_ERR));
    173             return;
    174           }
    175 
    176           source.getMetadata(function(metadata) {
    177             if (cancelRequested) {
    178               errorCallback(util.createFileError(FileError.ABORT_ERR));
    179               return;
    180             }
    181 
    182             fileOperationUtil.setLastModified(
    183                 target, metadata.modificationTime);
    184             successCallback(target);
    185           }, errorCallback);
    186         };
    187 
    188         writer.write(file);
    189       }, errorCallback);
    190     }, errorCallback);
    191   }, errorCallback);
    192 
    193   return function() { cancelRequested = true; };
    194 };
    195 
    196 /**
    197  * Copies a file a) from Drive to local, b) from local to Drive, or c) from
    198  * Drive to Drive.
    199  * Currently, we need to take care about following two things for Drive:
    200  *
    201  * 1) Copying hosted document.
    202  * In theory, it is impossible to actual copy a hosted document to other
    203  * file system. Thus, instead, Drive file system backend creates a JSON file
    204  * referring to the hosted document. Also, when it is uploaded by copyTo,
    205  * the hosted document is copied on the server. Note that, this doesn't work
    206  * when a user creates a file by FileWriter (as copyFileEntry_ does).
    207  *
    208  * 2) File transfer between local and Drive server.
    209  * There are two directions of file transfer; from local to Drive and from
    210  * Drive to local.
    211  * The file transfer from local to Drive is done as a part of file system
    212  * background sync (kicked after the copy operation is done). So we don't need
    213  * to take care about it here. To copy the file from Drive to local (or Drive
    214  * to Drive with GData WAPI), we need to download the file content (if it is
    215  * not locally cached). During the downloading, we can listen the periodical
    216  * updating and cancel the downloding via private API.
    217  *
    218  * This function supports progress updating and cancelling partially.
    219  * Unfortunately, FileEntry.copyTo doesn't support progress updating nor
    220  * cancelling, so we support them only during file downloading.
    221  *
    222  * Note: we're planning to move copyTo logic into c++ side. crbug.com/261492
    223  *
    224  * @param {FileEntry} source The entry of the file to be copied.
    225  * @param {DirectoryEntry} parent The entry of the destination directory.
    226  * @param {string} newName The name of the copied file.
    227  * @param {function(FileEntry, number)} progressCallback Callback periodically
    228  *     invoked during file transfer with the source and the number of
    229  *     transferred bytes from the last call.
    230  * @param {function(FileEntry)} successCallback Callback invoked when the
    231  *     file copy is successfully done with the entry of the copied file.
    232  * @param {function(FileError)} errorCallback Callback invoked when an error
    233  *     is found.
    234  * @return {function()} Callback to cancel the current file copy operation.
    235  *     When the cancel is done, errorCallback will be called. The returned
    236  *     callback must not be called more than once.
    237  * @private
    238  */
    239 fileOperationUtil.copyFileOnDrive_ = function(
    240     source, parent, newName, progressCallback, successCallback, errorCallback) {
    241   // Set to true when cancel is requested.
    242   var cancelRequested = false;
    243   var cancelCallback = null;
    244 
    245   var onCopyToCompleted = null;
    246 
    247   // Progress callback.
    248   // Because the uploading the file from local cache to Drive server will be
    249   // done as a part of background Drive file system sync, so for this copy
    250   // operation, what we need to take care about is only file downloading.
    251   var numTransferredBytes = 0;
    252   if (PathUtil.isDriveBasedPath(source.fullPath)) {
    253     var sourceUrl = source.toURL();
    254     var sourcePath = util.extractFilePath(sourceUrl);
    255     var onFileTransfersUpdated = function(statusList) {
    256       for (var i = 0; i < statusList.length; i++) {
    257         var status = statusList[i];
    258 
    259         // Comparing urls is unreliable, since they may use different
    260         // url encoding schemes (eg. rfc2396 vs. rfc3986).
    261         var filePath = util.extractFilePath(status.fileUrl);
    262         if (filePath == sourcePath) {
    263           var processed = status.processed;
    264           if (processed > numTransferredBytes) {
    265             progressCallback(source, processed - numTransferredBytes);
    266             numTransferredBytes = processed;
    267           }
    268           return;
    269         }
    270       }
    271     };
    272 
    273     // Subscribe to listen file transfer updating notifications.
    274     chrome.fileBrowserPrivate.onFileTransfersUpdated.addListener(
    275         onFileTransfersUpdated);
    276 
    277     // Currently, we do NOT upload the file during the copy operation.
    278     // It will be done as a part of file system sync after copy operation.
    279     // So, we can cancel only file downloading.
    280     cancelCallback = function() {
    281       chrome.fileBrowserPrivate.cancelFileTransfers(
    282           [sourceUrl], function() {});
    283     };
    284 
    285     // We need to clean up on copyTo completion regardless if it is
    286     // successfully done or not.
    287     onCopyToCompleted = function() {
    288       cancelCallback = null;
    289       chrome.fileBrowserPrivate.onFileTransfersUpdated.removeListener(
    290           onFileTransfersUpdated);
    291     };
    292   }
    293 
    294   source.copyTo(
    295       parent, newName,
    296       function(entry) {
    297         if (onCopyToCompleted)
    298           onCopyToCompleted();
    299 
    300         if (cancelRequested) {
    301           errorCallback(util.createFileError(FileError.ABORT_ERR));
    302           return;
    303         }
    304 
    305         entry.getMetadata(function(metadata) {
    306           if (metadata.size > numTransferredBytes)
    307             progressCallback(source, metadata.size - numTransferredBytes);
    308           successCallback(entry);
    309         }, errorCallback);
    310       },
    311       function(error) {
    312         if (onCopyToCompleted)
    313           onCopyToCompleted();
    314 
    315         errorCallback(error);
    316       });
    317 
    318   return function() {
    319     cancelRequested = true;
    320     if (cancelCallback) {
    321       cancelCallback();
    322       cancelCallback = null;
    323     }
    324   };
    325 };
    326 
    327 /**
    328  * Thin wrapper of chrome.fileBrowserPrivate.zipSelection to adapt its
    329  * interface similar to copyTo().
    330  *
    331  * @param {Array.<Entry>} sources The array of entries to be archived.
    332  * @param {DirectoryEntry} parent The entry of the destination directory.
    333  * @param {string} newName The name of the archive to be created.
    334  * @param {function(FileEntry)} successCallback Callback invoked when the
    335  *     operation is successfully done with the entry of the created archive.
    336  * @param {function(FileError)} errorCallback Callback invoked when an error
    337  *     is found.
    338  */
    339 fileOperationUtil.zipSelection = function(
    340     sources, parent, newName, successCallback, errorCallback) {
    341   chrome.fileBrowserPrivate.zipSelection(
    342       parent.toURL(),
    343       sources.map(function(e) { return e.toURL(); }),
    344       newName, function(success) {
    345         if (!success) {
    346           // Failed to create a zip archive.
    347           errorCallback(
    348               util.createFileError(FileError.INVALID_MODIFICATION_ERR));
    349           return;
    350         }
    351 
    352         // Returns the created entry via callback.
    353         parent.getFile(
    354             newName, {create: false}, successCallback, errorCallback);
    355       });
    356 };
    357 
    358 /**
    359  * @constructor
    360  */
    361 function FileCopyManager() {
    362   this.copyTasks_ = [];
    363   this.deleteTasks_ = [];
    364   this.cancelObservers_ = [];
    365   this.cancelRequested_ = false;
    366   this.cancelCallback_ = null;
    367   this.unloadTimeout_ = null;
    368 
    369   this.eventRouter_ = new FileCopyManager.EventRouter();
    370 }
    371 
    372 /**
    373  * Get FileCopyManager instance. In case is hasn't been initialized, a new
    374  * instance is created.
    375  *
    376  * @return {FileCopyManager} A FileCopyManager instance.
    377  */
    378 FileCopyManager.getInstance = function() {
    379   if (!FileCopyManager.instance_)
    380     FileCopyManager.instance_ = new FileCopyManager();
    381 
    382   return FileCopyManager.instance_;
    383 };
    384 
    385 /**
    386  * Manages cr.Event dispatching.
    387  * Currently this can send three types of events: "copy-progress",
    388  * "copy-operation-completed" and "delete".
    389  *
    390  * TODO(hidehiko): Reorganize the event dispatching mechanism.
    391  * @constructor
    392  * @extends {cr.EventTarget}
    393  */
    394 FileCopyManager.EventRouter = function() {
    395 };
    396 
    397 /**
    398  * Extends cr.EventTarget.
    399  */
    400 FileCopyManager.EventRouter.prototype.__proto__ = cr.EventTarget.prototype;
    401 
    402 /**
    403  * Dispatches a simple "copy-progress" event with reason and current
    404  * FileCopyManager status. If it is an ERROR event, error should be set.
    405  *
    406  * @param {string} reason Event type. One of "BEGIN", "PROGRESS", "SUCCESS",
    407  *     "ERROR" or "CANCELLED". TODO(hidehiko): Use enum.
    408  * @param {Object} status Current FileCopyManager's status. See also
    409  *     FileCopyManager.getStatus().
    410  * @param {FileCopyManager.Error=} opt_error The info for the error. This
    411  *     should be set iff the reason is "ERROR".
    412  */
    413 FileCopyManager.EventRouter.prototype.sendProgressEvent = function(
    414     reason, status, opt_error) {
    415   var event = new cr.Event('copy-progress');
    416   event.reason = reason;
    417   event.status = status;
    418   if (opt_error)
    419     event.error = opt_error;
    420   this.dispatchEvent(event);
    421 };
    422 
    423 /**
    424  * Dispatches an event to notify that an entry is changed (created or deleted).
    425  * @param {util.EntryChangedType} type The enum to represent if the entry
    426  *     is created or deleted.
    427  * @param {Entry} entry The changed entry.
    428  */
    429 FileCopyManager.EventRouter.prototype.sendEntryChangedEvent = function(
    430     type, entry) {
    431   var event = new cr.Event('entry-changed');
    432   event.type = type;
    433   event.entry = entry;
    434   this.dispatchEvent(event);
    435 };
    436 
    437 /**
    438  * Dispatches an event to notify entries are changed for delete task.
    439  *
    440  * @param {string} reason Event type. One of "BEGIN", "PROGRESS", "SUCCESS",
    441  *     or "ERROR". TODO(hidehiko): Use enum.
    442  * @param {Array.<string>} urls An array of URLs which are affected by delete
    443  *     operation.
    444  */
    445 FileCopyManager.EventRouter.prototype.sendDeleteEvent = function(
    446     reason, urls) {
    447   var event = new cr.Event('delete');
    448   event.reason = reason;
    449   event.urls = urls;
    450   this.dispatchEvent(event);
    451 };
    452 
    453 /**
    454  * A record of a queued copy operation.
    455  *
    456  * Multiple copy operations may be queued at any given time.  Additional
    457  * Tasks may be added while the queue is being serviced.  Though a
    458  * cancel operation cancels everything in the queue.
    459  *
    460  * @param {DirectoryEntry} targetDirEntry Target directory.
    461  * @param {DirectoryEntry=} opt_zipBaseDirEntry Base directory dealt as a root
    462  *     in ZIP archive.
    463  * @constructor
    464  */
    465 FileCopyManager.Task = function(targetDirEntry, opt_zipBaseDirEntry) {
    466   this.targetDirEntry = targetDirEntry;
    467   this.zipBaseDirEntry = opt_zipBaseDirEntry;
    468   this.originalEntries = null;
    469 
    470   // TODO(hidehiko): When we support recursive copy, we should be able to
    471   // rely on originalEntries. Then remove this.
    472   this.entries = [];
    473 
    474   /**
    475    * The index of entries being processed. The entries should be processed
    476    * from 0, so this is also the number of completed entries.
    477    * @type {number}
    478    */
    479   this.entryIndex = 0;
    480   this.totalBytes = 0;
    481   this.completedBytes = 0;
    482 
    483   this.deleteAfterCopy = false;
    484   this.move = false;
    485   this.zip = false;
    486 
    487   // TODO(hidehiko): After we support recursive copy, we don't need this.
    488   // If directory already exists, we try to make a copy named 'dir (X)',
    489   // where X is a number. When we do this, all subsequent copies from
    490   // inside the subtree should be mapped to the new directory name.
    491   // For example, if 'dir' was copied as 'dir (1)', then 'dir\file.txt' should
    492   // become 'dir (1)\file.txt'.
    493   this.renamedDirectories_ = [];
    494 };
    495 
    496 /**
    497  * @param {Array.<Entry>} entries Entries.
    498  * @param {function()} callback When entries resolved.
    499  */
    500 FileCopyManager.Task.prototype.setEntries = function(entries, callback) {
    501   this.originalEntries = entries;
    502 
    503   // When moving directories, FileEntry.moveTo() is used if both source
    504   // and target are on Drive. There is no need to recurse into directories.
    505   util.recurseAndResolveEntries(entries, !this.move, function(result) {
    506     if (this.move) {
    507       // This may be moving from search results, where it fails if we move
    508       // parent entries earlier than child entries. We should process the
    509       // deepest entry first. Since move of each entry is done by a single
    510       // moveTo() call, we don't need to care about the recursive traversal
    511       // order.
    512       this.entries = result.dirEntries.concat(result.fileEntries).sort(
    513           function(entry1, entry2) {
    514             return entry2.fullPath.length - entry1.fullPath.length;
    515           });
    516     } else {
    517       // Copying tasks are recursively processed. So, directories must be
    518       // processed earlier than their child files. Since
    519       // util.recurseAndResolveEntries is already listing entries in the
    520       // recursive traversal order, we just keep the ordering.
    521       this.entries = result.dirEntries.concat(result.fileEntries);
    522     }
    523 
    524     this.totalBytes = result.fileBytes;
    525     callback();
    526   }.bind(this));
    527 };
    528 
    529 /**
    530  * Updates copy progress status for the entry.
    531  *
    532  * @param {number} size Number of bytes that has been copied since last update.
    533  */
    534 FileCopyManager.Task.prototype.updateFileCopyProgress = function(size) {
    535   this.completedBytes += size;
    536 };
    537 
    538 /**
    539  * @param {string} fromName Old name.
    540  * @param {string} toName New name.
    541  */
    542 FileCopyManager.Task.prototype.registerRename = function(fromName, toName) {
    543   this.renamedDirectories_.push({from: fromName + '/', to: toName + '/'});
    544 };
    545 
    546 /**
    547  * @param {string} path A path.
    548  * @return {string} Path after renames.
    549  */
    550 FileCopyManager.Task.prototype.applyRenames = function(path) {
    551   // Directories are processed in pre-order, so we will store only the first
    552   // renaming point:
    553   // x   -> x (1)    -- new directory created.
    554   // x\y -> x (1)\y  -- no more renames inside the new directory, so
    555   //                    this one will not be stored.
    556   // x\y\a.txt       -- only one rename will be applied.
    557   for (var index = 0; index < this.renamedDirectories_.length; ++index) {
    558     var rename = this.renamedDirectories_[index];
    559     if (path.indexOf(rename.from) == 0) {
    560       path = rename.to + path.substr(rename.from.length);
    561     }
    562   }
    563   return path;
    564 };
    565 
    566 /**
    567  * Error class used to report problems with a copy operation.
    568  * If the code is UNEXPECTED_SOURCE_FILE, data should be a path of the file.
    569  * If the code is TARGET_EXISTS, data should be the existing Entry.
    570  * If the code is FILESYSTEM_ERROR, data should be the FileError.
    571  *
    572  * @param {util.FileOperationErrorType} code Error type.
    573  * @param {string|Entry|FileError} data Additional data.
    574  * @constructor
    575  */
    576 FileCopyManager.Error = function(code, data) {
    577   this.code = code;
    578   this.data = data;
    579 };
    580 
    581 // FileCopyManager methods.
    582 
    583 /**
    584  * Initializes the filesystem if it is not done yet.
    585  * @param {function()} callback Completion callback.
    586  */
    587 FileCopyManager.prototype.initialize = function(callback) {
    588   // Already initialized.
    589   if (this.root_) {
    590     callback();
    591     return;
    592   }
    593   chrome.fileBrowserPrivate.requestFileSystem(function(filesystem) {
    594     this.root_ = filesystem.root;
    595     callback();
    596   }.bind(this));
    597 };
    598 
    599 /**
    600  * Called before a new method is run in the manager. Prepares the manager's
    601  * state for running a new method.
    602  */
    603 FileCopyManager.prototype.willRunNewMethod = function() {
    604   // Cancel any pending close actions so the file copy manager doesn't go away.
    605   if (this.unloadTimeout_)
    606     clearTimeout(this.unloadTimeout_);
    607   this.unloadTimeout_ = null;
    608 };
    609 
    610 /**
    611  * @return {Object} Status object.
    612  */
    613 FileCopyManager.prototype.getStatus = function() {
    614   // TODO(hidehiko): Reorganize the structure when delete queue is merged
    615   // into copy task queue.
    616   var rv = {
    617     totalItems: 0,
    618     completedItems: 0,
    619 
    620     totalBytes: 0,
    621     completedBytes: 0,
    622 
    623     pendingCopies: 0,
    624     pendingMoves: 0,
    625     pendingZips: 0,
    626 
    627     // In case the number of the incompleted entry is exactly one.
    628     filename: '',
    629   };
    630 
    631   var pendingEntry = null;
    632   for (var i = 0; i < this.copyTasks_.length; i++) {
    633     var task = this.copyTasks_[i];
    634     rv.totalItems += task.entries.length;
    635     rv.completedItems += task.entryIndex;
    636 
    637     rv.totalBytes += task.totalBytes;
    638     rv.completedBytes += task.completedBytes;
    639 
    640     var numPendingEntries = task.entries.length - task.entryIndex;
    641     if (task.zip) {
    642       rv.pendingZips += numPendingEntries;
    643     } else if (task.move || task.deleteAfterCopy) {
    644       rv.pendingMoves += numPendingEntries;
    645     } else {
    646       rv.pendingCopies += numPendingEntries;
    647     }
    648 
    649     if (numPendingEntries == 1)
    650       pendingEntry = task.entries[task.entries.length - 1];
    651   }
    652 
    653   if (rv.totalItems - rv.completedItems == 1 && pendingEntry)
    654     rv.filename = pendingEntry.name;
    655 
    656   return rv;
    657 };
    658 
    659 /**
    660  * Adds an event listener for the tasks.
    661  * @param {string} type The name of the event.
    662  * @param {function(cr.Event)} handler The handler for the event.
    663  *     This is called when the event is dispatched.
    664  */
    665 FileCopyManager.prototype.addEventListener = function(type, handler) {
    666   this.eventRouter_.addEventListener(type, handler);
    667 };
    668 
    669 /**
    670  * Removes an event listener for the tasks.
    671  * @param {string} type The name of the event.
    672  * @param {function(cr.Event)} handler The handler to be removed.
    673  */
    674 FileCopyManager.prototype.removeEventListener = function(type, handler) {
    675   this.eventRouter_.removeEventListener(type, handler);
    676 };
    677 
    678 /**
    679  * Says if there are any tasks in the queue.
    680  * @return {boolean} True, if there are any tasks.
    681  */
    682 FileCopyManager.prototype.hasQueuedTasks = function() {
    683   return this.copyTasks_.length > 0 || this.deleteTasks_.length > 0;
    684 };
    685 
    686 /**
    687  * Unloads the host page in 5 secs of idleing. Need to be called
    688  * each time this.copyTasks_.length or this.deleteTasks_.length
    689  * changed.
    690  *
    691  * @private
    692  */
    693 FileCopyManager.prototype.maybeScheduleCloseBackgroundPage_ = function() {
    694   if (!this.hasQueuedTasks()) {
    695     if (this.unloadTimeout_ === null)
    696       this.unloadTimeout_ = setTimeout(maybeCloseBackgroundPage, 5000);
    697   } else if (this.unloadTimeout_) {
    698     clearTimeout(this.unloadTimeout_);
    699     this.unloadTimeout_ = null;
    700   }
    701 };
    702 
    703 /**
    704  * Completely clear out the copy queue, either because we encountered an error
    705  * or completed successfully.
    706  *
    707  * @private
    708  */
    709 FileCopyManager.prototype.resetQueue_ = function() {
    710   for (var i = 0; i < this.cancelObservers_.length; i++)
    711     this.cancelObservers_[i]();
    712 
    713   this.copyTasks_ = [];
    714   this.cancelObservers_ = [];
    715   this.maybeScheduleCloseBackgroundPage_();
    716 };
    717 
    718 /**
    719  * Request that the current copy queue be abandoned.
    720  *
    721  * @param {function()=} opt_callback On cancel.
    722  */
    723 FileCopyManager.prototype.requestCancel = function(opt_callback) {
    724   this.cancelRequested_ = true;
    725   if (this.cancelCallback_) {
    726     this.cancelCallback_();
    727     this.cancelCallback_ = null;
    728   }
    729   if (opt_callback)
    730     this.cancelObservers_.push(opt_callback);
    731 
    732   // If there is any active task it will eventually call maybeCancel_.
    733   // Otherwise call it right now.
    734   if (this.copyTasks_.length == 0)
    735     this.doCancel_();
    736 };
    737 
    738 /**
    739  * Perform the bookkeeping required to cancel.
    740  *
    741  * @private
    742  */
    743 FileCopyManager.prototype.doCancel_ = function() {
    744   this.resetQueue_();
    745   this.cancelRequested_ = false;
    746   this.eventRouter_.sendProgressEvent('CANCELLED', this.getStatus());
    747 };
    748 
    749 /**
    750  * Used internally to check if a cancel has been requested, and handle
    751  * it if so.
    752  *
    753  * @return {boolean} If canceled.
    754  * @private
    755  */
    756 FileCopyManager.prototype.maybeCancel_ = function() {
    757   if (!this.cancelRequested_)
    758     return false;
    759 
    760   this.doCancel_();
    761   return true;
    762 };
    763 
    764 /**
    765  * Kick off pasting.
    766  *
    767  * @param {Array.<string>} files Pathes of source files.
    768  * @param {Array.<string>} directories Pathes of source directories.
    769  * @param {boolean} isCut If the source items are removed from original
    770  *     location.
    771  * @param {string} targetPath Target path.
    772  */
    773 FileCopyManager.prototype.paste = function(
    774     files, directories, isCut, targetPath) {
    775   var self = this;
    776   var entries = [];
    777   var added = 0;
    778   var total;
    779 
    780   var steps = {
    781     start: function() {
    782       // Filter entries.
    783       var entryFilterFunc = function(entry) {
    784         if (entry == '')
    785           return false;
    786         if (isCut && entry.replace(/\/[^\/]+$/, '') == targetPath)
    787           // Moving to the same directory is a redundant operation.
    788           return false;
    789         return true;
    790       };
    791       directories = directories ? directories.filter(entryFilterFunc) : [];
    792       files = files ? files.filter(entryFilterFunc) : [];
    793 
    794       // Check the number of filtered entries.
    795       total = directories.length + files.length;
    796       if (total == 0)
    797         return;
    798 
    799       // Retrieve entries.
    800       util.getDirectories(self.root_, {create: false}, directories,
    801                           steps.onEntryFound, steps.onPathError);
    802       util.getFiles(self.root_, {create: false}, files,
    803                     steps.onEntryFound, steps.onPathError);
    804     },
    805 
    806     onEntryFound: function(entry) {
    807       // When getDirectories/getFiles finish, they call addEntry with null.
    808       // We don't want to add null to our entries.
    809       if (entry == null)
    810         return;
    811       entries.push(entry);
    812       added++;
    813       if (added == total)
    814         steps.onSourceEntriesFound();
    815     },
    816 
    817     onSourceEntriesFound: function() {
    818       self.root_.getDirectory(targetPath, {},
    819                               steps.onTargetEntryFound, steps.onPathError);
    820     },
    821 
    822     onTargetEntryFound: function(targetEntry) {
    823       self.queueCopy_(targetEntry, entries, isCut);
    824     },
    825 
    826     onPathError: function(err) {
    827       self.eventRouter_.sendProgressEvent(
    828           'ERROR',
    829           self.getStatus(),
    830           new FileCopyManager.Error(
    831               util.FileOperationErrorType.FILESYSTEM_ERROR, err));
    832     }
    833   };
    834 
    835   steps.start();
    836 };
    837 
    838 /**
    839  * Checks if the move operation is avaiable between the given two locations.
    840  *
    841  * @param {DirectoryEntry} sourceEntry An entry from the source.
    842  * @param {DirectoryEntry} targetDirEntry Directory entry for the target.
    843  * @return {boolean} Whether we can move from the source to the target.
    844  */
    845 FileCopyManager.prototype.isMovable = function(sourceEntry,
    846                                                targetDirEntry) {
    847   return (PathUtil.isDriveBasedPath(sourceEntry.fullPath) &&
    848           PathUtil.isDriveBasedPath(targetDirEntry.fullPath)) ||
    849          (PathUtil.getRootPath(sourceEntry.fullPath) ==
    850           PathUtil.getRootPath(targetDirEntry.fullPath));
    851 };
    852 
    853 /**
    854  * Initiate a file copy.
    855  *
    856  * @param {DirectoryEntry} targetDirEntry Target directory.
    857  * @param {Array.<Entry>} entries Entries to copy.
    858  * @param {boolean} deleteAfterCopy In case of move.
    859  * @return {FileCopyManager.Task} Copy task.
    860  * @private
    861  */
    862 FileCopyManager.prototype.queueCopy_ = function(
    863     targetDirEntry, entries, deleteAfterCopy) {
    864   var self = this;
    865   // When copying files, null can be specified as source directory.
    866   var copyTask = new FileCopyManager.Task(targetDirEntry);
    867   if (deleteAfterCopy) {
    868     if (this.isMovable(entries[0], targetDirEntry)) {
    869       copyTask.move = true;
    870     } else {
    871       copyTask.deleteAfterCopy = true;
    872     }
    873   }
    874   copyTask.setEntries(entries, function() {
    875     self.copyTasks_.push(copyTask);
    876     self.maybeScheduleCloseBackgroundPage_();
    877     if (self.copyTasks_.length == 1) {
    878       // Assume self.cancelRequested_ == false.
    879       // This moved us from 0 to 1 active tasks, let the servicing begin!
    880       self.serviceAllTasks_();
    881     } else {
    882       // Force to update the progress of butter bar when there are new tasks
    883       // coming while servicing current task.
    884       self.eventRouter_.sendProgressEvent('PROGRESS', self.getStatus());
    885     }
    886   });
    887 
    888   return copyTask;
    889 };
    890 
    891 /**
    892  * Service all pending tasks, as well as any that might appear during the
    893  * copy.
    894  *
    895  * @private
    896  */
    897 FileCopyManager.prototype.serviceAllTasks_ = function() {
    898   var self = this;
    899 
    900   var onTaskProgress = function() {
    901     self.eventRouter_.sendProgressEvent('PROGRESS', self.getStatus());
    902   };
    903 
    904   var onEntryChanged = function(type, entry) {
    905     self.eventRouter_.sendEntryChangedEvent(type, entry);
    906   };
    907 
    908   var onTaskError = function(err) {
    909     if (self.maybeCancel_())
    910       return;
    911     self.eventRouter_.sendProgressEvent('ERROR', self.getStatus(), err);
    912     self.resetQueue_();
    913   };
    914 
    915   var onTaskSuccess = function() {
    916     if (self.maybeCancel_())
    917       return;
    918 
    919     // The task at the front of the queue is completed. Pop it from the queue.
    920     self.copyTasks_.shift();
    921     self.maybeScheduleCloseBackgroundPage_();
    922 
    923     if (!self.copyTasks_.length) {
    924       // All tasks have been serviced, clean up and exit.
    925       self.eventRouter_.sendProgressEvent('SUCCESS', self.getStatus());
    926       self.resetQueue_();
    927       return;
    928     }
    929 
    930     // We want to dispatch a PROGRESS event when there are more tasks to serve
    931     // right after one task finished in the queue. We treat all tasks as one
    932     // big task logically, so there is only one BEGIN/SUCCESS event pair for
    933     // these continuous tasks.
    934     self.eventRouter_.sendProgressEvent('PROGRESS', self.getStatus());
    935 
    936     self.serviceTask_(self.copyTasks_[0], onEntryChanged, onTaskProgress,
    937                       onTaskSuccess, onTaskError);
    938   };
    939 
    940   // If the queue size is 1 after pushing our task, it was empty before,
    941   // so we need to kick off queue processing and dispatch BEGIN event.
    942 
    943   this.eventRouter_.sendProgressEvent('BEGIN', this.getStatus());
    944   this.serviceTask_(this.copyTasks_[0], onEntryChanged, onTaskProgress,
    945                     onTaskSuccess, onTaskError);
    946 };
    947 
    948 /**
    949  * Runs a given task.
    950  * Note that the responsibility of this method is just dispatching to the
    951  * appropriate serviceXxxTask_() method.
    952  * TODO(hidehiko): Remove this method by introducing FileCopyManager.Task.run()
    953  *     (crbug.com/246976).
    954  *
    955  * @param {FileCopyManager.Task} task A task to be run.
    956  * @param {function(util.EntryChangedType, Entry)} entryChangedCallback Callback
    957  *     invoked when an entry is changed.
    958  * @param {function()} progressCallback Callback invoked periodically during
    959  *     the operation.
    960  * @param {function()} successCallback Callback run on success.
    961  * @param {function(FileCopyManager.Error)} errorCallback Callback run on error.
    962  * @private
    963  */
    964 FileCopyManager.prototype.serviceTask_ = function(
    965     task, entryChangedCallback, progressCallback,
    966     successCallback, errorCallback) {
    967   if (task.zip)
    968     this.serviceZipTask_(task, entryChangedCallback, progressCallback,
    969                          successCallback, errorCallback);
    970   else if (task.move)
    971     this.serviceMoveTask_(task, entryChangedCallback, progressCallback,
    972                           successCallback, errorCallback);
    973   else
    974     this.serviceCopyTask_(task, entryChangedCallback, progressCallback,
    975                           successCallback, errorCallback);
    976 };
    977 
    978 /**
    979  * Service all entries in the copy (and move) task.
    980  * Note: this method contains also the operation of "Move" due to historical
    981  * reason.
    982  *
    983  * @param {FileCopyManager.Task} task A copy task to be run.
    984  * @param {function(util.EntryChangedType, Entry)} entryChangedCallback Callback
    985  *     invoked when an entry is changed.
    986  * @param {function()} progressCallback Callback invoked periodically during
    987  *     the copying.
    988  * @param {function()} successCallback On success.
    989  * @param {function(FileCopyManager.Error)} errorCallback On error.
    990  * @private
    991  */
    992 FileCopyManager.prototype.serviceCopyTask_ = function(
    993     task, entryChangedCallback, progressCallback, successCallback,
    994     errorCallback) {
    995   // TODO(hidehiko): We should be able to share the code to iterate on entries
    996   // with serviceMoveTask_().
    997   if (task.entries.length == 0) {
    998     successCallback();
    999     return;
   1000   }
   1001 
   1002   var self = this;
   1003 
   1004   var deleteOriginals = function() {
   1005     var count = task.originalEntries.length;
   1006 
   1007     var onEntryDeleted = function(entry) {
   1008       entryChangedCallback(util.EntryChangedType.DELETED, entry);
   1009       count--;
   1010       if (!count)
   1011         successCallback();
   1012     };
   1013 
   1014     var onFilesystemError = function(err) {
   1015       errorCallback(new FileCopyManager.Error(
   1016           util.FileOperationErrorType.FILESYSTEM_ERROR, err));
   1017     };
   1018 
   1019     for (var i = 0; i < task.originalEntries.length; i++) {
   1020       var entry = task.originalEntries[i];
   1021       util.removeFileOrDirectory(
   1022           entry, onEntryDeleted.bind(self, entry), onFilesystemError);
   1023     }
   1024   };
   1025 
   1026   var onEntryServiced = function() {
   1027     task.entryIndex++;
   1028 
   1029     // We should not dispatch a PROGRESS event when there is no pending items
   1030     // in the task.
   1031     if (task.entryIndex >= task.entries.length) {
   1032       if (task.deleteAfterCopy) {
   1033         deleteOriginals();
   1034       } else {
   1035         successCallback();
   1036       }
   1037       return;
   1038     }
   1039 
   1040     progressCallback();
   1041     self.processCopyEntry_(
   1042         task, task.entries[task.entryIndex], entryChangedCallback,
   1043         progressCallback, onEntryServiced, errorCallback);
   1044   };
   1045 
   1046   this.processCopyEntry_(
   1047       task, task.entries[task.entryIndex], entryChangedCallback,
   1048       progressCallback, onEntryServiced, errorCallback);
   1049 };
   1050 
   1051 /**
   1052  * Copies the next entry in a given task.
   1053  * TODO(olege): Refactor this method into a separate class.
   1054  *
   1055  * @param {FileManager.Task} task A task.
   1056  * @param {Entry} sourceEntry An entry to be copied.
   1057  * @param {function(util.EntryChangedType, Entry)} entryChangedCallback Callback
   1058  *     invoked when an entry is changed.
   1059  * @param {function()} progressCallback Callback invoked periodically during
   1060  *     the copying.
   1061  * @param {function()} successCallback On success.
   1062  * @param {function(FileCopyManager.Error)} errorCallback On error.
   1063  * @private
   1064  */
   1065 FileCopyManager.prototype.processCopyEntry_ = function(
   1066     task, sourceEntry, entryChangedCallback, progressCallback, successCallback,
   1067     errorCallback) {
   1068   if (this.maybeCancel_())
   1069     return;
   1070 
   1071   var self = this;
   1072 
   1073   // |sourceEntry.originalSourcePath| is set in util.recurseAndResolveEntries.
   1074   var sourcePath = sourceEntry.originalSourcePath;
   1075   if (sourceEntry.fullPath.substr(0, sourcePath.length) != sourcePath) {
   1076     // We found an entry in the list that is not relative to the base source
   1077     // path, something is wrong.
   1078     errorCallback(new FileCopyManager.Error(
   1079         util.FileOperationErrorType.UNEXPECTED_SOURCE_FILE,
   1080         sourceEntry.fullPath));
   1081     return;
   1082   }
   1083 
   1084   var targetDirEntry = task.targetDirEntry;
   1085   var originalPath = sourceEntry.fullPath.substr(sourcePath.length + 1);
   1086   originalPath = task.applyRenames(originalPath);
   1087 
   1088   var onDeduplicated = function(targetRelativePath) {
   1089     var onCopyComplete = function(entry) {
   1090       entryChangedCallback(util.EntryChangedType.CREATED, entry);
   1091       successCallback();
   1092     };
   1093 
   1094     var onFilesystemError = function(err) {
   1095       errorCallback(new FileCopyManager.Error(
   1096           util.FileOperationErrorType.FILESYSTEM_ERROR, err));
   1097     };
   1098 
   1099     if (sourceEntry.isDirectory) {
   1100       // Copying the directory means just creating a new directory.
   1101       targetDirEntry.getDirectory(
   1102           targetRelativePath,
   1103           {create: true, exclusive: true},
   1104           function(targetEntry) {
   1105             if (targetRelativePath != originalPath) {
   1106               task.registerRename(originalPath, targetRelativePath);
   1107             }
   1108             onCopyComplete(targetEntry);
   1109           },
   1110           util.flog('Error getting dir: ' + targetRelativePath,
   1111                     onFilesystemError));
   1112     } else {
   1113       // Copy a file.
   1114       targetDirEntry.getDirectory(
   1115           PathUtil.dirname(targetRelativePath), {create: false},
   1116           function(dirEntry) {
   1117             self.cancelCallback_ = fileOperationUtil.copyFile(
   1118                 sourceEntry, dirEntry, PathUtil.basename(targetRelativePath),
   1119                 function(entry, size) {
   1120                   task.updateFileCopyProgress(size);
   1121                   progressCallback();
   1122                 },
   1123                 function(entry) {
   1124                   self.cancelCallback_ = null;
   1125                   onCopyComplete(entry);
   1126                 },
   1127                 function(error) {
   1128                   self.cancelCallback_ = null;
   1129                   onFilesystemError(error);
   1130                 });
   1131           },
   1132           onFilesystemError);
   1133     }
   1134   };
   1135 
   1136   fileOperationUtil.deduplicatePath(
   1137       targetDirEntry, originalPath, onDeduplicated, errorCallback);
   1138 };
   1139 
   1140 /**
   1141  * Moves all entries in the task.
   1142  *
   1143  * @param {FileCopyManager.Task} task A move task to be run.
   1144  * @param {function(util.EntryChangedType, Entry)} entryChangedCallback Callback
   1145  *     invoked when an entry is changed.
   1146  * @param {function()} progressCallback Callback invoked periodically during
   1147  *     the moving.
   1148  * @param {function()} successCallback On success.
   1149  * @param {function(FileCopyManager.Error)} errorCallback On error.
   1150  * @private
   1151  */
   1152 FileCopyManager.prototype.serviceMoveTask_ = function(
   1153     task, entryChangedCallback, progressCallback, successCallback,
   1154     errorCallback) {
   1155   if (task.entries.length == 0) {
   1156     successCallback();
   1157     return;
   1158   }
   1159 
   1160   this.processMoveEntry_(
   1161       task, task.entries[task.entryIndex], entryChangedCallback,
   1162       (function onCompleted() {
   1163         task.entryIndex++;
   1164 
   1165         // We should not dispatch a PROGRESS event when there is no pending
   1166         // items in the task.
   1167         if (task.entryIndex >= task.entries.length) {
   1168           successCallback();
   1169           return;
   1170         }
   1171 
   1172         // Move the next entry.
   1173         progressCallback();
   1174         this.processMoveEntry_(
   1175             task, task.entries[task.entryIndex], entryChangedCallback,
   1176             onCompleted.bind(this), errorCallback);
   1177       }).bind(this),
   1178       errorCallback);
   1179 };
   1180 
   1181 /**
   1182  * Moves the next entry in a given task.
   1183  *
   1184  * Implementation note: This method can be simplified more. For example, in
   1185  * Task.setEntries(), the flag to recurse is set to false for move task,
   1186  * so that all the entries' originalSourcePath should be
   1187  * dirname(sourceEntry.fullPath).
   1188  * Thus, targetRelativePath should contain exact one component. Also we can
   1189  * skip applyRenames, because the destination directory always should be
   1190  * task.targetDirEntry.
   1191  * The unnecessary complexity is due to historical reason.
   1192  * TODO(hidehiko): Refactor this method.
   1193  *
   1194  * @param {FileManager.Task} task A move task.
   1195  * @param {Entry} sourceEntry An entry to be moved.
   1196  * @param {function(util.EntryChangedType, Entry)} entryChangedCallback Callback
   1197  *     invoked when an entry is changed.
   1198  * @param {function()} successCallback On success.
   1199  * @param {function(FileCopyManager.Error)} errorCallback On error.
   1200  * @private
   1201  */
   1202 FileCopyManager.prototype.processMoveEntry_ = function(
   1203     task, sourceEntry, entryChangedCallback, successCallback, errorCallback) {
   1204   if (this.maybeCancel_())
   1205     return;
   1206 
   1207   // |sourceEntry.originalSourcePath| is set in util.recurseAndResolveEntries.
   1208   var sourcePath = sourceEntry.originalSourcePath;
   1209   if (sourceEntry.fullPath.substr(0, sourcePath.length) != sourcePath) {
   1210     // We found an entry in the list that is not relative to the base source
   1211     // path, something is wrong.
   1212     errorCallback(new FileCopyManager.Error(
   1213         util.FileOperationErrorType.UNEXPECTED_SOURCE_FILE,
   1214         sourceEntry.fullPath));
   1215     return;
   1216   }
   1217 
   1218   fileOperationUtil.deduplicatePath(
   1219       task.targetDirEntry,
   1220       task.applyRenames(sourceEntry.fullPath.substr(sourcePath.length + 1)),
   1221       function(targetRelativePath) {
   1222         var onFilesystemError = function(err) {
   1223           errorCallback(new FileCopyManager.Error(
   1224               util.FileOperationErrorType.FILESYSTEM_ERROR,
   1225               err));
   1226         };
   1227 
   1228         task.targetDirEntry.getDirectory(
   1229             PathUtil.dirname(targetRelativePath), {create: false},
   1230             function(dirEntry) {
   1231               sourceEntry.moveTo(
   1232                   dirEntry, PathUtil.basename(targetRelativePath),
   1233                   function(targetEntry) {
   1234                     entryChangedCallback(
   1235                         util.EntryChangedType.CREATED, targetEntry);
   1236                     entryChangedCallback(
   1237                         util.EntryChangedType.DELETED, sourceEntry);
   1238                     successCallback();
   1239                   },
   1240                   onFilesystemError);
   1241             },
   1242             onFilesystemError);
   1243       },
   1244       errorCallback);
   1245 };
   1246 
   1247 /**
   1248  * Service a zip file creation task.
   1249  *
   1250  * @param {FileCopyManager.Task} task A zip task to be run.
   1251  * @param {function(util.EntryChangedType, Entry)} entryChangedCallback Callback
   1252  *     invoked when an entry is changed.
   1253  * @param {function()} progressCallback Callback invoked periodically during
   1254  *     the moving.
   1255  * @param {function()} successCallback On complete.
   1256  * @param {function(FileCopyManager.Error)} errorCallback On error.
   1257  * @private
   1258  */
   1259 FileCopyManager.prototype.serviceZipTask_ = function(
   1260     task, entryChangedCallback, progressCallback, successCallback,
   1261     errorCallback) {
   1262   // TODO(hidehiko): we should localize the name.
   1263   var destName = 'Archive';
   1264   if (task.originalEntries.length == 1) {
   1265     var entryPath = task.originalEntries[0].fullPath;
   1266     var i = entryPath.lastIndexOf('/');
   1267     var basename = (i < 0) ? entryPath : entryPath.substr(i + 1);
   1268     i = basename.lastIndexOf('.');
   1269     destName = ((i < 0) ? basename : basename.substr(0, i));
   1270   }
   1271 
   1272   fileOperationUtil.deduplicatePath(
   1273       task.targetDirEntry, destName + '.zip',
   1274       function(destPath) {
   1275         progressCallback();
   1276 
   1277         fileOperationUtil.zipSelection(
   1278             task.entries,
   1279             task.zipBaseDirEntry,
   1280             destPath,
   1281             function(entry) {
   1282               entryChangedCallback(util.EntryChangedType.CREATE, entry);
   1283               successCallback();
   1284             },
   1285             function(error) {
   1286               errorCallback(new FileCopyManager.Error(
   1287                   util.FileOperationErrorType.FILESYSTEM_ERROR, error));
   1288             });
   1289       },
   1290       errorCallback);
   1291 };
   1292 
   1293 /**
   1294  * Timeout before files are really deleted (to allow undo).
   1295  */
   1296 FileCopyManager.DELETE_TIMEOUT = 30 * 1000;
   1297 
   1298 /**
   1299  * Schedules the files deletion.
   1300  *
   1301  * @param {Array.<Entry>} entries The entries.
   1302  */
   1303 FileCopyManager.prototype.deleteEntries = function(entries) {
   1304   var task = { entries: entries };
   1305   this.deleteTasks_.push(task);
   1306   this.maybeScheduleCloseBackgroundPage_();
   1307   if (this.deleteTasks_.length == 1)
   1308     this.serviceAllDeleteTasks_();
   1309 };
   1310 
   1311 /**
   1312  * Service all pending delete tasks, as well as any that might appear during the
   1313  * deletion.
   1314  *
   1315  * @private
   1316  */
   1317 FileCopyManager.prototype.serviceAllDeleteTasks_ = function() {
   1318   var self = this;
   1319 
   1320   var onTaskSuccess = function() {
   1321     var task = self.deleteTasks_[0];
   1322     self.deleteTasks_.shift();
   1323     if (!self.deleteTasks_.length) {
   1324       // All tasks have been serviced, clean up and exit.
   1325       self.eventRouter_.sendDeleteEvent(
   1326           'SUCCESS',
   1327           task.entries.map(function(e) {
   1328             return util.makeFilesystemUrl(e.fullPath);
   1329           }));
   1330       self.maybeScheduleCloseBackgroundPage_();
   1331       return;
   1332     }
   1333 
   1334     // We want to dispatch a PROGRESS event when there are more tasks to serve
   1335     // right after one task finished in the queue. We treat all tasks as one
   1336     // big task logically, so there is only one BEGIN/SUCCESS event pair for
   1337     // these continuous tasks.
   1338     self.eventRouter_.sendDeleteEvent(
   1339         'PROGRESS',
   1340         task.entries.map(function(e) {
   1341           return util.makeFilesystemUrl(e.fullPath);
   1342         }));
   1343     self.serviceDeleteTask_(self.deleteTasks_[0], onTaskSuccess, onTaskFailure);
   1344   };
   1345 
   1346   var onTaskFailure = function(task) {
   1347     self.deleteTasks_ = [];
   1348     self.eventRouter_.sendDeleteEvent(
   1349         'ERROR',
   1350         task.entries.map(function(e) {
   1351           return util.makeFilesystemUrl(e.fullPath);
   1352         }));
   1353     self.maybeScheduleCloseBackgroundPage_();
   1354   };
   1355 
   1356   // If the queue size is 1 after pushing our task, it was empty before,
   1357   // so we need to kick off queue processing and dispatch BEGIN event.
   1358   this.eventRouter_.sendDeleteEvent(
   1359       'BEGIN',
   1360       this.deleteTasks_[0].entries.map(function(e) {
   1361         return util.makeFilesystemUrl(e.fullPath);
   1362       }));
   1363   this.serviceDeleteTask_(this.deleteTasks_[0], onTaskSuccess, onTaskFailure);
   1364 };
   1365 
   1366 /**
   1367  * Performs the deletion.
   1368  *
   1369  * @param {Object} task The delete task (see deleteEntries function).
   1370  * @param {function()} successCallback Callback run on success.
   1371  * @param {function(FileCopyManager.Error)} errorCallback Callback run on error.
   1372  * @private
   1373  */
   1374 FileCopyManager.prototype.serviceDeleteTask_ = function(
   1375     task, successCallback, errorCallback) {
   1376   var downcount = task.entries.length;
   1377   if (downcount == 0) {
   1378     successCallback();
   1379     return;
   1380   }
   1381 
   1382   var filesystemError = null;
   1383   var onComplete = function() {
   1384     if (--downcount > 0)
   1385       return;
   1386 
   1387     // All remove operations are processed. Run callback.
   1388     if (filesystemError) {
   1389       errorCallback(new FileCopyManager.Error(
   1390           util.FileOperationErrorType.FILESYSTEM_ERROR, filesystemError));
   1391     } else {
   1392       successCallback();
   1393     }
   1394   };
   1395 
   1396   for (var i = 0; i < task.entries.length; i++) {
   1397     var entry = task.entries[i];
   1398     util.removeFileOrDirectory(
   1399         entry,
   1400         function(currentEntry) {
   1401           this.eventRouter_.sendEntryChangedEvent(
   1402               util.EntryChangedType.DELETED, currentEntry);
   1403           onComplete();
   1404         }.bind(this, entry),
   1405         function(error) {
   1406           if (!filesystemError)
   1407             filesystemError = error;
   1408           onComplete();
   1409         });
   1410   }
   1411 };
   1412 
   1413 /**
   1414  * Creates a zip file for the selection of files.
   1415  *
   1416  * @param {Entry} dirEntry The directory containing the selection.
   1417  * @param {Array.<Entry>} selectionEntries The selected entries.
   1418  */
   1419 FileCopyManager.prototype.zipSelection = function(dirEntry, selectionEntries) {
   1420   var self = this;
   1421   var zipTask = new FileCopyManager.Task(dirEntry, dirEntry);
   1422   zipTask.zip = true;
   1423   zipTask.setEntries(selectionEntries, function() {
   1424     // TODO: per-entry zip progress update with accurate byte count.
   1425     // For now just set completedBytes to same value as totalBytes so that the
   1426     // progress bar is full.
   1427     zipTask.completedBytes = zipTask.totalBytes;
   1428     self.copyTasks_.push(zipTask);
   1429     if (self.copyTasks_.length == 1) {
   1430       // Assume self.cancelRequested_ == false.
   1431       // This moved us from 0 to 1 active tasks, let the servicing begin!
   1432       self.serviceAllTasks_();
   1433     } else {
   1434       // Force to update the progress of butter bar when there are new tasks
   1435       // coming while servicing current task.
   1436       self.eventRouter_.sendProgressEvent('PROGRESS', self.getStatus());
   1437     }
   1438   });
   1439 };
   1440