Home | History | Annotate | Download | only in js
      1 // Copyright 2013 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 FileOperationManager.
      9  */
     10 var fileOperationUtil = {};
     11 
     12 /**
     13  * Simple wrapper for util.deduplicatePath. On error, this method translates
     14  * the FileError to FileOperationManager.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(FileOperationManager.Error)} errorCallback Callback run on
     21  *     error.
     22  */
     23 fileOperationUtil.deduplicatePath = function(
     24     dirEntry, relativePath, successCallback, errorCallback) {
     25   util.deduplicatePath(
     26       dirEntry, relativePath, successCallback,
     27       function(err) {
     28         var onFileSystemError = function(error) {
     29           errorCallback(new FileOperationManager.Error(
     30               util.FileOperationErrorType.FILESYSTEM_ERROR, error));
     31         };
     32 
     33         if (err.name == util.FileError.PATH_EXISTS_ERR) {
     34           // Failed to uniquify the file path. There should be an existing
     35           // entry, so return the error with it.
     36           util.resolvePath(
     37               dirEntry, relativePath,
     38               function(entry) {
     39                 errorCallback(new FileOperationManager.Error(
     40                     util.FileOperationErrorType.TARGET_EXISTS, entry));
     41               },
     42               onFileSystemError);
     43           return;
     44         }
     45         onFileSystemError(err);
     46       });
     47 };
     48 
     49 /**
     50  * Traverses files/subdirectories of the given entry, and returns them.
     51  * In addition, this method annotate the size of each entry. The result will
     52  * include the entry itself.
     53  *
     54  * @param {Entry} entry The root Entry for traversing.
     55  * @param {function(Array.<Entry>)} successCallback Called when the traverse
     56  *     is successfully done with the array of the entries.
     57  * @param {function(FileError)} errorCallback Called on error with the first
     58  *     occurred error (i.e. following errors will just be discarded).
     59  */
     60 fileOperationUtil.resolveRecursively = function(
     61     entry, successCallback, errorCallback) {
     62   var result = [];
     63   var error = null;
     64   var numRunningTasks = 0;
     65 
     66   var maybeInvokeCallback = function() {
     67     // If there still remain some running tasks, wait their finishing.
     68     if (numRunningTasks > 0)
     69       return;
     70 
     71     if (error)
     72       errorCallback(error);
     73     else
     74       successCallback(result);
     75   };
     76 
     77   // The error handling can be shared.
     78   var onError = function(fileError) {
     79     // If this is the first error, remember it.
     80     if (!error)
     81       error = fileError;
     82     --numRunningTasks;
     83     maybeInvokeCallback();
     84   };
     85 
     86   var process = function(entry) {
     87     numRunningTasks++;
     88     result.push(entry);
     89     if (entry.isDirectory) {
     90       // The size of a directory is 1 bytes here, so that the progress bar
     91       // will work smoother.
     92       // TODO(hidehiko): Remove this hack.
     93       entry.size = 1;
     94 
     95       // Recursively traverse children.
     96       var reader = entry.createReader();
     97       reader.readEntries(
     98           function processSubEntries(subEntries) {
     99             if (error || subEntries.length == 0) {
    100               // If an error is found already, or this is the completion
    101               // callback, then finish the process.
    102               --numRunningTasks;
    103               maybeInvokeCallback();
    104               return;
    105             }
    106 
    107             for (var i = 0; i < subEntries.length; i++)
    108               process(subEntries[i]);
    109 
    110             // Continue to read remaining children.
    111             reader.readEntries(processSubEntries, onError);
    112           },
    113           onError);
    114     } else {
    115       // For a file, annotate the file size.
    116       entry.getMetadata(function(metadata) {
    117         entry.size = metadata.size;
    118         --numRunningTasks;
    119         maybeInvokeCallback();
    120       }, onError);
    121     }
    122   };
    123 
    124   process(entry);
    125 };
    126 
    127 /**
    128  * Copies source to parent with the name newName recursively.
    129  * This should work very similar to FileSystem API's copyTo. The difference is;
    130  * - The progress callback is supported.
    131  * - The cancellation is supported.
    132  *
    133  * @param {Entry} source The entry to be copied.
    134  * @param {DirectoryEntry} parent The entry of the destination directory.
    135  * @param {string} newName The name of copied file.
    136  * @param {function(Entry, Entry)} entryChangedCallback
    137  *     Callback invoked when an entry is created with the source Entry and
    138  *     the destination Entry.
    139  * @param {function(Entry, number)} progressCallback Callback invoked
    140  *     periodically during the copying. It takes the source Entry and the
    141  *     processed bytes of it.
    142  * @param {function(Entry)} successCallback Callback invoked when the copy
    143  *     is successfully done with the Entry of the created entry.
    144  * @param {function(FileError)} errorCallback Callback invoked when an error
    145  *     is found.
    146  * @return {function()} Callback to cancel the current file copy operation.
    147  *     When the cancel is done, errorCallback will be called. The returned
    148  *     callback must not be called more than once.
    149  */
    150 fileOperationUtil.copyTo = function(
    151     source, parent, newName, entryChangedCallback, progressCallback,
    152     successCallback, errorCallback) {
    153   var copyId = null;
    154   var pendingCallbacks = [];
    155 
    156   // Makes the callback called in order they were invoked.
    157   var callbackQueue = new AsyncUtil.Queue();
    158 
    159   var onCopyProgress = function(progressCopyId, status) {
    160     callbackQueue.run(function(callback) {
    161       if (copyId === null) {
    162         // If the copyId is not yet available, wait for it.
    163         pendingCallbacks.push(
    164             onCopyProgress.bind(null, progressCopyId, status));
    165         callback();
    166         return;
    167       }
    168 
    169       // This is not what we're interested in.
    170       if (progressCopyId != copyId) {
    171         callback();
    172         return;
    173       }
    174 
    175       switch (status.type) {
    176         case 'begin_copy_entry':
    177           callback();
    178           break;
    179 
    180         case 'end_copy_entry':
    181           // TODO(mtomasz): Convert URL to Entry in custom bindings.
    182           util.URLsToEntries(
    183               [status.destinationUrl], function(destinationEntries) {
    184                 entryChangedCallback(status.sourceUrl,
    185                                      destinationEntries[0] || null);
    186                 callback();
    187               });
    188           break;
    189 
    190         case 'progress':
    191           progressCallback(status.sourceUrl, status.size);
    192           callback();
    193           break;
    194 
    195         case 'success':
    196           chrome.fileBrowserPrivate.onCopyProgress.removeListener(
    197               onCopyProgress);
    198           // TODO(mtomasz): Convert URL to Entry in custom bindings.
    199           util.URLsToEntries(
    200               [status.destinationUrl], function(destinationEntries) {
    201                 successCallback(destinationEntries[0] || null);
    202                 callback();
    203               });
    204           break;
    205 
    206         case 'error':
    207           chrome.fileBrowserPrivate.onCopyProgress.removeListener(
    208               onCopyProgress);
    209           errorCallback(util.createDOMError(status.error));
    210           callback();
    211           break;
    212 
    213         default:
    214           // Found unknown state. Cancel the task, and return an error.
    215           console.error('Unknown progress type: ' + status.type);
    216           chrome.fileBrowserPrivate.onCopyProgress.removeListener(
    217               onCopyProgress);
    218           chrome.fileBrowserPrivate.cancelCopy(copyId);
    219           errorCallback(util.createDOMError(
    220               util.FileError.INVALID_STATE_ERR));
    221           callback();
    222       }
    223     });
    224   };
    225 
    226   // Register the listener before calling startCopy. Otherwise some events
    227   // would be lost.
    228   chrome.fileBrowserPrivate.onCopyProgress.addListener(onCopyProgress);
    229 
    230   // Then starts the copy.
    231   // TODO(mtomasz): Convert URL to Entry in custom bindings.
    232   chrome.fileBrowserPrivate.startCopy(
    233       source.toURL(), parent.toURL(), newName, function(startCopyId) {
    234         // last error contains the FileError code on error.
    235         if (chrome.runtime.lastError) {
    236           // Unsubscribe the progress listener.
    237           chrome.fileBrowserPrivate.onCopyProgress.removeListener(
    238               onCopyProgress);
    239           errorCallback(util.createDOMError(chrome.runtime.lastError));
    240           return;
    241         }
    242 
    243         copyId = startCopyId;
    244         for (var i = 0; i < pendingCallbacks.length; i++) {
    245           pendingCallbacks[i]();
    246         }
    247       });
    248 
    249   return function() {
    250     // If copyId is not yet available, wait for it.
    251     if (copyId == null) {
    252       pendingCallbacks.push(function() {
    253         chrome.fileBrowserPrivate.cancelCopy(copyId);
    254       });
    255       return;
    256     }
    257 
    258     chrome.fileBrowserPrivate.cancelCopy(copyId);
    259   };
    260 };
    261 
    262 /**
    263  * Thin wrapper of chrome.fileBrowserPrivate.zipSelection to adapt its
    264  * interface similar to copyTo().
    265  *
    266  * @param {Array.<Entry>} sources The array of entries to be archived.
    267  * @param {DirectoryEntry} parent The entry of the destination directory.
    268  * @param {string} newName The name of the archive to be created.
    269  * @param {function(FileEntry)} successCallback Callback invoked when the
    270  *     operation is successfully done with the entry of the created archive.
    271  * @param {function(FileError)} errorCallback Callback invoked when an error
    272  *     is found.
    273  */
    274 fileOperationUtil.zipSelection = function(
    275     sources, parent, newName, successCallback, errorCallback) {
    276   // TODO(mtomasz): Pass Entries instead of URLs. Entries can be converted to
    277   // URLs in custom bindings.
    278   chrome.fileBrowserPrivate.zipSelection(
    279       parent.toURL(),
    280       util.entriesToURLs(sources),
    281       newName, function(success) {
    282         if (!success) {
    283           // Failed to create a zip archive.
    284           errorCallback(
    285               util.createDOMError(util.FileError.INVALID_MODIFICATION_ERR));
    286           return;
    287         }
    288 
    289         // Returns the created entry via callback.
    290         parent.getFile(
    291             newName, {create: false}, successCallback, errorCallback);
    292       });
    293 };
    294 
    295 /**
    296  * @constructor
    297  */
    298 function FileOperationManager() {
    299   this.copyTasks_ = [];
    300   this.deleteTasks_ = [];
    301   this.taskIdCounter_ = 0;
    302   this.eventRouter_ = new FileOperationManager.EventRouter();
    303 
    304   Object.seal(this);
    305 }
    306 
    307 /**
    308  * Manages Event dispatching.
    309  * Currently this can send three types of events: "copy-progress",
    310  * "copy-operation-completed" and "delete".
    311  *
    312  * TODO(hidehiko): Reorganize the event dispatching mechanism.
    313  * @constructor
    314  * @extends {cr.EventTarget}
    315  */
    316 FileOperationManager.EventRouter = function() {
    317 };
    318 
    319 /**
    320  * Extends cr.EventTarget.
    321  */
    322 FileOperationManager.EventRouter.prototype.__proto__ = cr.EventTarget.prototype;
    323 
    324 /**
    325  * Dispatches a simple "copy-progress" event with reason and current
    326  * FileOperationManager status. If it is an ERROR event, error should be set.
    327  *
    328  * @param {string} reason Event type. One of "BEGIN", "PROGRESS", "SUCCESS",
    329  *     "ERROR" or "CANCELLED". TODO(hidehiko): Use enum.
    330  * @param {Object} status Current FileOperationManager's status. See also
    331  *     FileOperationManager.Task.getStatus().
    332  * @param {string} taskId ID of task related with the event.
    333  * @param {FileOperationManager.Error=} opt_error The info for the error. This
    334  *     should be set iff the reason is "ERROR".
    335  */
    336 FileOperationManager.EventRouter.prototype.sendProgressEvent = function(
    337     reason, status, taskId, opt_error) {
    338   var event = new Event('copy-progress');
    339   event.reason = reason;
    340   event.status = status;
    341   event.taskId = taskId;
    342   if (opt_error)
    343     event.error = opt_error;
    344   this.dispatchEvent(event);
    345 };
    346 
    347 /**
    348  * Dispatches an event to notify that an entry is changed (created or deleted).
    349  * @param {util.EntryChangedKind} kind The enum to represent if the entry is
    350  *     created or deleted.
    351  * @param {Entry} entry The changed entry.
    352  */
    353 FileOperationManager.EventRouter.prototype.sendEntryChangedEvent = function(
    354     kind, entry) {
    355   var event = new Event('entry-changed');
    356   event.kind = kind;
    357   event.entry = entry;
    358   this.dispatchEvent(event);
    359 };
    360 
    361 /**
    362  * Dispatches an event to notify entries are changed for delete task.
    363  *
    364  * @param {string} reason Event type. One of "BEGIN", "PROGRESS", "SUCCESS",
    365  *     or "ERROR". TODO(hidehiko): Use enum.
    366  * @param {DeleteTask} task Delete task related with the event.
    367  */
    368 FileOperationManager.EventRouter.prototype.sendDeleteEvent = function(
    369     reason, task) {
    370   var event = new Event('delete');
    371   event.reason = reason;
    372   event.taskId = task.taskId;
    373   event.entries = task.entries;
    374   event.totalBytes = task.totalBytes;
    375   event.processedBytes = task.processedBytes;
    376   this.dispatchEvent(event);
    377 };
    378 
    379 /**
    380  * A record of a queued copy operation.
    381  *
    382  * Multiple copy operations may be queued at any given time.  Additional
    383  * Tasks may be added while the queue is being serviced.  Though a
    384  * cancel operation cancels everything in the queue.
    385  *
    386  * @param {util.FileOperationType} operationType The type of this operation.
    387  * @param {Array.<Entry>} sourceEntries Array of source entries.
    388  * @param {DirectoryEntry} targetDirEntry Target directory.
    389  * @constructor
    390  */
    391 FileOperationManager.Task = function(
    392     operationType, sourceEntries, targetDirEntry) {
    393   this.operationType = operationType;
    394   this.sourceEntries = sourceEntries;
    395   this.targetDirEntry = targetDirEntry;
    396 
    397   /**
    398    * An array of map from url to Entry being processed.
    399    * @type {Array.<Object<string, Entry>>}
    400    */
    401   this.processingEntries = null;
    402 
    403   /**
    404    * Total number of bytes to be processed. Filled in initialize().
    405    * Use 1 as an initial value to indicate that the task is not completed.
    406    * @type {number}
    407    */
    408   this.totalBytes = 1;
    409 
    410   /**
    411    * Total number of already processed bytes. Updated periodically.
    412    * @type {number}
    413    */
    414   this.processedBytes = 0;
    415 
    416   /**
    417    * Index of the progressing entry in sourceEntries.
    418    * @type {number}
    419    * @private
    420    */
    421   this.processingSourceIndex_ = 0;
    422 
    423   /**
    424    * Set to true when cancel is requested.
    425    * @private {boolean}
    426    */
    427   this.cancelRequested_ = false;
    428 
    429   /**
    430    * Callback to cancel the running process.
    431    * @private {function()}
    432    */
    433   this.cancelCallback_ = null;
    434 
    435   // TODO(hidehiko): After we support recursive copy, we don't need this.
    436   // If directory already exists, we try to make a copy named 'dir (X)',
    437   // where X is a number. When we do this, all subsequent copies from
    438   // inside the subtree should be mapped to the new directory name.
    439   // For example, if 'dir' was copied as 'dir (1)', then 'dir/file.txt' should
    440   // become 'dir (1)/file.txt'.
    441   this.renamedDirectories_ = [];
    442 };
    443 
    444 /**
    445  * @param {function()} callback When entries resolved.
    446  */
    447 FileOperationManager.Task.prototype.initialize = function(callback) {
    448 };
    449 
    450 /**
    451  * Requests cancellation of this task.
    452  * When the cancellation is done, it is notified via callbacks of run().
    453  */
    454 FileOperationManager.Task.prototype.requestCancel = function() {
    455   this.cancelRequested_ = true;
    456   if (this.cancelCallback_) {
    457     this.cancelCallback_();
    458     this.cancelCallback_ = null;
    459   }
    460 };
    461 
    462 /**
    463  * Runs the task. Sub classes must implement this method.
    464  *
    465  * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback
    466  *     Callback invoked when an entry is changed.
    467  * @param {function()} progressCallback Callback invoked periodically during
    468  *     the operation.
    469  * @param {function()} successCallback Callback run on success.
    470  * @param {function(FileOperationManager.Error)} errorCallback Callback run on
    471  *     error.
    472  */
    473 FileOperationManager.Task.prototype.run = function(
    474     entryChangedCallback, progressCallback, successCallback, errorCallback) {
    475 };
    476 
    477 /**
    478  * Get states of the task.
    479  * TOOD(hirono): Removes this method and sets a task to progress events.
    480  * @return {object} Status object.
    481  */
    482 FileOperationManager.Task.prototype.getStatus = function() {
    483   var processingEntry = this.sourceEntries[this.processingSourceIndex_];
    484   return {
    485     operationType: this.operationType,
    486     numRemainingItems: this.sourceEntries.length - this.processingSourceIndex_,
    487     totalBytes: this.totalBytes,
    488     processedBytes: this.processedBytes,
    489     processingEntryName: processingEntry ? processingEntry.name : ''
    490   };
    491 };
    492 
    493 /**
    494  * Obtains the number of total processed bytes.
    495  * @return {number} Number of total processed bytes.
    496  * @private
    497  */
    498 FileOperationManager.Task.prototype.calcProcessedBytes_ = function() {
    499   var bytes = 0;
    500   for (var i = 0; i < this.processingSourceIndex_ + 1; i++) {
    501     var entryMap = this.processingEntries[i];
    502     if (!entryMap)
    503       break;
    504     for (var name in entryMap) {
    505       bytes += i < this.processingSourceIndex_ ?
    506           entryMap[name].size : entryMap[name].processedBytes;
    507     }
    508   }
    509   return bytes;
    510 };
    511 
    512 /**
    513  * Task to copy entries.
    514  *
    515  * @param {Array.<Entry>} sourceEntries Array of source entries.
    516  * @param {DirectoryEntry} targetDirEntry Target directory.
    517  * @param {boolean} deleteAfterCopy Whether the delete original files after
    518  *     copy.
    519  * @constructor
    520  * @extends {FileOperationManager.Task}
    521  */
    522 FileOperationManager.CopyTask = function(sourceEntries,
    523                                          targetDirEntry,
    524                                          deleteAfterCopy) {
    525   FileOperationManager.Task.call(
    526       this,
    527       deleteAfterCopy ?
    528           util.FileOperationType.MOVE : util.FileOperationType.COPY,
    529       sourceEntries,
    530       targetDirEntry);
    531   this.deleteAfterCopy = deleteAfterCopy;
    532 
    533   /*
    534    * Rate limiter which is used to avoid sending update request for progress bar
    535    * too frequently.
    536    * @type {AsyncUtil.RateLimiter}
    537    * @private
    538    */
    539   this.updateProgressRateLimiter_ = null
    540 };
    541 
    542 /**
    543  * Extends FileOperationManager.Task.
    544  */
    545 FileOperationManager.CopyTask.prototype.__proto__ =
    546     FileOperationManager.Task.prototype;
    547 
    548 /**
    549  * Initializes the CopyTask.
    550  * @param {function()} callback Called when the initialize is completed.
    551  */
    552 FileOperationManager.CopyTask.prototype.initialize = function(callback) {
    553   var group = new AsyncUtil.Group();
    554   // Correct all entries to be copied for status update.
    555   this.processingEntries = [];
    556   for (var i = 0; i < this.sourceEntries.length; i++) {
    557     group.add(function(index, callback) {
    558       fileOperationUtil.resolveRecursively(
    559           this.sourceEntries[index],
    560           function(resolvedEntries) {
    561             var resolvedEntryMap = {};
    562             for (var j = 0; j < resolvedEntries.length; ++j) {
    563               var entry = resolvedEntries[j];
    564               entry.processedBytes = 0;
    565               resolvedEntryMap[entry.toURL()] = entry;
    566             }
    567             this.processingEntries[index] = resolvedEntryMap;
    568             callback();
    569           }.bind(this),
    570           function(error) {
    571             console.error(
    572                 'Failed to resolve for copy: %s', error.name);
    573             callback();
    574           });
    575     }.bind(this, i));
    576   }
    577 
    578   group.run(function() {
    579     // Fill totalBytes.
    580     this.totalBytes = 0;
    581     for (var i = 0; i < this.processingEntries.length; i++) {
    582       for (var entryURL in this.processingEntries[i])
    583         this.totalBytes += this.processingEntries[i][entryURL].size;
    584     }
    585 
    586     callback();
    587   }.bind(this));
    588 };
    589 
    590 /**
    591  * Copies all entries to the target directory.
    592  * Note: this method contains also the operation of "Move" due to historical
    593  * reason.
    594  *
    595  * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback
    596  *     Callback invoked when an entry is changed.
    597  * @param {function()} progressCallback Callback invoked periodically during
    598  *     the copying.
    599  * @param {function()} successCallback On success.
    600  * @param {function(FileOperationManager.Error)} errorCallback On error.
    601  * @override
    602  */
    603 FileOperationManager.CopyTask.prototype.run = function(
    604     entryChangedCallback, progressCallback, successCallback, errorCallback) {
    605   // TODO(hidehiko): We should be able to share the code to iterate on entries
    606   // with serviceMoveTask_().
    607   if (this.sourceEntries.length == 0) {
    608     successCallback();
    609     return;
    610   }
    611 
    612   // TODO(hidehiko): Delete after copy is the implementation of Move.
    613   // Migrate the part into MoveTask.run().
    614   var deleteOriginals = function() {
    615     var count = this.sourceEntries.length;
    616 
    617     var onEntryDeleted = function(entry) {
    618       entryChangedCallback(util.EntryChangedKind.DELETED, entry);
    619       count--;
    620       if (!count)
    621         successCallback();
    622     };
    623 
    624     var onFilesystemError = function(err) {
    625       errorCallback(new FileOperationManager.Error(
    626           util.FileOperationErrorType.FILESYSTEM_ERROR, err));
    627     };
    628 
    629     for (var i = 0; i < this.sourceEntries.length; i++) {
    630       var entry = this.sourceEntries[i];
    631       util.removeFileOrDirectory(
    632           entry, onEntryDeleted.bind(null, entry), onFilesystemError);
    633     }
    634   }.bind(this);
    635 
    636   /**
    637    * Accumulates processed bytes and call |progressCallback| if needed.
    638    *
    639    * @param {number} index The index of processing source.
    640    * @param {string} sourceEntryUrl URL of the entry which has been processed.
    641    * @param {number=} opt_size Processed bytes of the |sourceEntry|. If it is
    642    *     dropped, all bytes of the entry are considered to be processed.
    643    */
    644   var updateProgress = function(index, sourceEntryUrl, opt_size) {
    645     if (!sourceEntryUrl)
    646       return;
    647 
    648     var processedEntry = this.processingEntries[index][sourceEntryUrl];
    649     if (!processedEntry)
    650       return;
    651 
    652     // Accumulates newly processed bytes.
    653     var size = opt_size || processedEntry.size;
    654     this.processedBytes += size - processedEntry.processedBytes;
    655     processedEntry.processedBytes = size;
    656 
    657     // Updates progress bar in limited frequency so that intervals between
    658     // updates have at least 200ms.
    659     this.updateProgressRateLimiter_.run();
    660   }.bind(this);
    661 
    662   this.updateProgressRateLimiter_ = new AsyncUtil.RateLimiter(progressCallback);
    663 
    664   AsyncUtil.forEach(
    665       this.sourceEntries,
    666       function(callback, entry, index) {
    667         if (this.cancelRequested_) {
    668           errorCallback(new FileOperationManager.Error(
    669               util.FileOperationErrorType.FILESYSTEM_ERROR,
    670               util.createDOMError(util.FileError.ABORT_ERR)));
    671           return;
    672         }
    673         progressCallback();
    674         this.processEntry_(
    675             entry, this.targetDirEntry,
    676             function(sourceEntryUrl, destinationEntry) {
    677               updateProgress(index, sourceEntryUrl);
    678               // The destination entry may be null, if the copied file got
    679               // deleted just after copying.
    680               if (destinationEntry) {
    681                 entryChangedCallback(
    682                     util.EntryChangedKind.CREATED, destinationEntry);
    683               }
    684             },
    685             function(sourceEntryUrl, size) {
    686               updateProgress(index, sourceEntryUrl, size);
    687             },
    688             function() {
    689               // Finishes off delayed updates if necessary.
    690               this.updateProgressRateLimiter_.runImmediately();
    691               // Update current source index and processing bytes.
    692               this.processingSourceIndex_ = index + 1;
    693               this.processedBytes = this.calcProcessedBytes_();
    694               callback();
    695             }.bind(this),
    696             function(error) {
    697               // Finishes off delayed updates if necessary.
    698               this.updateProgressRateLimiter_.runImmediately();
    699               errorCallback(error);
    700             }.bind(this));
    701       },
    702       function() {
    703         if (this.deleteAfterCopy) {
    704           deleteOriginals();
    705         } else {
    706           successCallback();
    707         }
    708       }.bind(this),
    709       this);
    710 };
    711 
    712 /**
    713  * Copies the source entry to the target directory.
    714  *
    715  * @param {Entry} sourceEntry An entry to be copied.
    716  * @param {DirectoryEntry} destinationEntry The entry which will contain the
    717  *     copied entry.
    718  * @param {function(Entry, Entry} entryChangedCallback
    719  *     Callback invoked when an entry is created with the source Entry and
    720  *     the destination Entry.
    721  * @param {function(Entry, number)} progressCallback Callback invoked
    722  *     periodically during the copying.
    723  * @param {function()} successCallback On success.
    724  * @param {function(FileOperationManager.Error)} errorCallback On error.
    725  * @private
    726  */
    727 FileOperationManager.CopyTask.prototype.processEntry_ = function(
    728     sourceEntry, destinationEntry, entryChangedCallback, progressCallback,
    729     successCallback, errorCallback) {
    730   fileOperationUtil.deduplicatePath(
    731       destinationEntry, sourceEntry.name,
    732       function(destinationName) {
    733         if (this.cancelRequested_) {
    734           errorCallback(new FileOperationManager.Error(
    735               util.FileOperationErrorType.FILESYSTEM_ERROR,
    736               util.createDOMError(util.FileError.ABORT_ERR)));
    737           return;
    738         }
    739         this.cancelCallback_ = fileOperationUtil.copyTo(
    740             sourceEntry, destinationEntry, destinationName,
    741             entryChangedCallback, progressCallback,
    742             function(entry) {
    743               this.cancelCallback_ = null;
    744               successCallback();
    745             }.bind(this),
    746             function(error) {
    747               this.cancelCallback_ = null;
    748               errorCallback(new FileOperationManager.Error(
    749                   util.FileOperationErrorType.FILESYSTEM_ERROR, error));
    750             }.bind(this));
    751       }.bind(this),
    752       errorCallback);
    753 };
    754 
    755 /**
    756  * Task to move entries.
    757  *
    758  * @param {Array.<Entry>} sourceEntries Array of source entries.
    759  * @param {DirectoryEntry} targetDirEntry Target directory.
    760  * @constructor
    761  * @extends {FileOperationManager.Task}
    762  */
    763 FileOperationManager.MoveTask = function(sourceEntries, targetDirEntry) {
    764   FileOperationManager.Task.call(
    765       this, util.FileOperationType.MOVE, sourceEntries, targetDirEntry);
    766 };
    767 
    768 /**
    769  * Extends FileOperationManager.Task.
    770  */
    771 FileOperationManager.MoveTask.prototype.__proto__ =
    772     FileOperationManager.Task.prototype;
    773 
    774 /**
    775  * Initializes the MoveTask.
    776  * @param {function()} callback Called when the initialize is completed.
    777  */
    778 FileOperationManager.MoveTask.prototype.initialize = function(callback) {
    779   // This may be moving from search results, where it fails if we
    780   // move parent entries earlier than child entries. We should
    781   // process the deepest entry first. Since move of each entry is
    782   // done by a single moveTo() call, we don't need to care about the
    783   // recursive traversal order.
    784   this.sourceEntries.sort(function(entry1, entry2) {
    785     return entry2.toURL().length - entry1.toURL().length;
    786   });
    787 
    788   this.processingEntries = [];
    789   for (var i = 0; i < this.sourceEntries.length; i++) {
    790     var processingEntryMap = {};
    791     var entry = this.sourceEntries[i];
    792 
    793     // The move should be done with updating the metadata. So here we assume
    794     // all the file size is 1 byte. (Avoiding 0, so that progress bar can
    795     // move smoothly).
    796     // TODO(hidehiko): Remove this hack.
    797     entry.size = 1;
    798     processingEntryMap[entry.toURL()] = entry;
    799     this.processingEntries[i] = processingEntryMap;
    800   }
    801 
    802   callback();
    803 };
    804 
    805 /**
    806  * Moves all entries in the task.
    807  *
    808  * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback
    809  *     Callback invoked when an entry is changed.
    810  * @param {function()} progressCallback Callback invoked periodically during
    811  *     the moving.
    812  * @param {function()} successCallback On success.
    813  * @param {function(FileOperationManager.Error)} errorCallback On error.
    814  * @override
    815  */
    816 FileOperationManager.MoveTask.prototype.run = function(
    817     entryChangedCallback, progressCallback, successCallback, errorCallback) {
    818   if (this.sourceEntries.length == 0) {
    819     successCallback();
    820     return;
    821   }
    822 
    823   AsyncUtil.forEach(
    824       this.sourceEntries,
    825       function(callback, entry, index) {
    826         if (this.cancelRequested_) {
    827           errorCallback(new FileOperationManager.Error(
    828               util.FileOperationErrorType.FILESYSTEM_ERROR,
    829               util.createDOMError(util.FileError.ABORT_ERR)));
    830           return;
    831         }
    832         progressCallback();
    833         FileOperationManager.MoveTask.processEntry_(
    834             entry, this.targetDirEntry, entryChangedCallback,
    835             function() {
    836               // Update current source index.
    837               this.processingSourceIndex_ = index + 1;
    838               this.processedBytes = this.calcProcessedBytes_();
    839               callback();
    840             }.bind(this),
    841             errorCallback);
    842       },
    843       function() {
    844         successCallback();
    845       }.bind(this),
    846       this);
    847 };
    848 
    849 /**
    850  * Moves the sourceEntry to the targetDirEntry in this task.
    851  *
    852  * @param {Entry} sourceEntry An entry to be moved.
    853  * @param {DirectoryEntry} destinationEntry The entry of the destination
    854  *     directory.
    855  * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback
    856  *     Callback invoked when an entry is changed.
    857  * @param {function()} successCallback On success.
    858  * @param {function(FileOperationManager.Error)} errorCallback On error.
    859  * @private
    860  */
    861 FileOperationManager.MoveTask.processEntry_ = function(
    862     sourceEntry, destinationEntry, entryChangedCallback, successCallback,
    863     errorCallback) {
    864   fileOperationUtil.deduplicatePath(
    865       destinationEntry,
    866       sourceEntry.name,
    867       function(destinationName) {
    868         sourceEntry.moveTo(
    869             destinationEntry, destinationName,
    870             function(movedEntry) {
    871               entryChangedCallback(util.EntryChangedKind.CREATED, movedEntry);
    872               entryChangedCallback(util.EntryChangedKind.DELETED, sourceEntry);
    873               successCallback();
    874             },
    875             function(error) {
    876               errorCallback(new FileOperationManager.Error(
    877                   util.FileOperationErrorType.FILESYSTEM_ERROR, error));
    878             });
    879       },
    880       errorCallback);
    881 };
    882 
    883 /**
    884  * Task to create a zip archive.
    885  *
    886  * @param {Array.<Entry>} sourceEntries Array of source entries.
    887  * @param {DirectoryEntry} targetDirEntry Target directory.
    888  * @param {DirectoryEntry} zipBaseDirEntry Base directory dealt as a root
    889  *     in ZIP archive.
    890  * @constructor
    891  * @extends {FileOperationManager.Task}
    892  */
    893 FileOperationManager.ZipTask = function(
    894     sourceEntries, targetDirEntry, zipBaseDirEntry) {
    895   FileOperationManager.Task.call(
    896       this, util.FileOperationType.ZIP, sourceEntries, targetDirEntry);
    897   this.zipBaseDirEntry = zipBaseDirEntry;
    898 };
    899 
    900 /**
    901  * Extends FileOperationManager.Task.
    902  */
    903 FileOperationManager.ZipTask.prototype.__proto__ =
    904     FileOperationManager.Task.prototype;
    905 
    906 
    907 /**
    908  * Initializes the ZipTask.
    909  * @param {function()} callback Called when the initialize is completed.
    910  */
    911 FileOperationManager.ZipTask.prototype.initialize = function(callback) {
    912   var resolvedEntryMap = {};
    913   var group = new AsyncUtil.Group();
    914   for (var i = 0; i < this.sourceEntries.length; i++) {
    915     group.add(function(index, callback) {
    916       fileOperationUtil.resolveRecursively(
    917           this.sourceEntries[index],
    918           function(entries) {
    919             for (var j = 0; j < entries.length; j++)
    920               resolvedEntryMap[entries[j].toURL()] = entries[j];
    921             callback();
    922           },
    923           callback);
    924     }.bind(this, i));
    925   }
    926 
    927   group.run(function() {
    928     // For zip archiving, all the entries are processed at once.
    929     this.processingEntries = [resolvedEntryMap];
    930 
    931     this.totalBytes = 0;
    932     for (var url in resolvedEntryMap)
    933       this.totalBytes += resolvedEntryMap[url].size;
    934 
    935     callback();
    936   }.bind(this));
    937 };
    938 
    939 /**
    940  * Runs a zip file creation task.
    941  *
    942  * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback
    943  *     Callback invoked when an entry is changed.
    944  * @param {function()} progressCallback Callback invoked periodically during
    945  *     the moving.
    946  * @param {function()} successCallback On complete.
    947  * @param {function(FileOperationManager.Error)} errorCallback On error.
    948  * @override
    949  */
    950 FileOperationManager.ZipTask.prototype.run = function(
    951     entryChangedCallback, progressCallback, successCallback, errorCallback) {
    952   // TODO(hidehiko): we should localize the name.
    953   var destName = 'Archive';
    954   if (this.sourceEntries.length == 1) {
    955     var entryName = this.sourceEntries[0].name;
    956     var i = entryName.lastIndexOf('.');
    957     destName = ((i < 0) ? entryName : entryName.substr(0, i));
    958   }
    959 
    960   fileOperationUtil.deduplicatePath(
    961       this.targetDirEntry, destName + '.zip',
    962       function(destPath) {
    963         // TODO: per-entry zip progress update with accurate byte count.
    964         // For now just set completedBytes to same value as totalBytes so
    965         // that the progress bar is full.
    966         this.processedBytes = this.totalBytes;
    967         progressCallback();
    968 
    969         // The number of elements in processingEntries is 1. See also
    970         // initialize().
    971         var entries = [];
    972         for (var url in this.processingEntries[0])
    973           entries.push(this.processingEntries[0][url]);
    974 
    975         fileOperationUtil.zipSelection(
    976             entries,
    977             this.zipBaseDirEntry,
    978             destPath,
    979             function(entry) {
    980               entryChangedCallback(util.EntryChangedKind.CREATED, entry);
    981               successCallback();
    982             },
    983             function(error) {
    984               errorCallback(new FileOperationManager.Error(
    985                   util.FileOperationErrorType.FILESYSTEM_ERROR, error));
    986             });
    987       }.bind(this),
    988       errorCallback);
    989 };
    990 
    991 /**
    992  * Error class used to report problems with a copy operation.
    993  * If the code is UNEXPECTED_SOURCE_FILE, data should be a path of the file.
    994  * If the code is TARGET_EXISTS, data should be the existing Entry.
    995  * If the code is FILESYSTEM_ERROR, data should be the FileError.
    996  *
    997  * @param {util.FileOperationErrorType} code Error type.
    998  * @param {string|Entry|FileError} data Additional data.
    999  * @constructor
   1000  */
   1001 FileOperationManager.Error = function(code, data) {
   1002   this.code = code;
   1003   this.data = data;
   1004 };
   1005 
   1006 // FileOperationManager methods.
   1007 
   1008 /**
   1009  * Adds an event listener for the tasks.
   1010  * @param {string} type The name of the event.
   1011  * @param {function(Event)} handler The handler for the event.
   1012  *     This is called when the event is dispatched.
   1013  */
   1014 FileOperationManager.prototype.addEventListener = function(type, handler) {
   1015   this.eventRouter_.addEventListener(type, handler);
   1016 };
   1017 
   1018 /**
   1019  * Removes an event listener for the tasks.
   1020  * @param {string} type The name of the event.
   1021  * @param {function(Event)} handler The handler to be removed.
   1022  */
   1023 FileOperationManager.prototype.removeEventListener = function(type, handler) {
   1024   this.eventRouter_.removeEventListener(type, handler);
   1025 };
   1026 
   1027 /**
   1028  * Says if there are any tasks in the queue.
   1029  * @return {boolean} True, if there are any tasks.
   1030  */
   1031 FileOperationManager.prototype.hasQueuedTasks = function() {
   1032   return this.copyTasks_.length > 0 || this.deleteTasks_.length > 0;
   1033 };
   1034 
   1035 /**
   1036  * Completely clear out the copy queue, either because we encountered an error
   1037  * or completed successfully.
   1038  *
   1039  * @private
   1040  */
   1041 FileOperationManager.prototype.resetQueue_ = function() {
   1042   this.copyTasks_ = [];
   1043 };
   1044 
   1045 /**
   1046  * Requests the specified task to be canceled.
   1047  * @param {string} taskId ID of task to be canceled.
   1048  */
   1049 FileOperationManager.prototype.requestTaskCancel = function(taskId) {
   1050   var task = null;
   1051   for (var i = 0; i < this.copyTasks_.length; i++) {
   1052     task = this.copyTasks_[i];
   1053     if (task.taskId !== taskId)
   1054       continue;
   1055     task.requestCancel();
   1056     // If the task is not on progress, remove it immediately.
   1057     if (i !== 0) {
   1058       this.eventRouter_.sendProgressEvent('CANCELED',
   1059                                           task.getStatus(),
   1060                                           task.taskId);
   1061       this.copyTasks_.splice(i, 1);
   1062     }
   1063   }
   1064   for (var i = 0; i < this.deleteTasks_.length; i++) {
   1065     task = this.deleteTasks_[i];
   1066     if (task.taskId !== taskId)
   1067       continue;
   1068     task.cancelRequested = true;
   1069     // If the task is not on progress, remove it immediately.
   1070     if (i !== 0) {
   1071       this.eventRouter_.sendDeleteEvent('CANCELED', task);
   1072       this.deleteTasks_.splice(i, 1);
   1073     }
   1074   }
   1075 };
   1076 
   1077 /**
   1078  * Kick off pasting.
   1079  *
   1080  * @param {Array.<Entry>} sourceEntries Entries of the source files.
   1081  * @param {DirectoryEntry} targetEntry The destination entry of the target
   1082  *     directory.
   1083  * @param {boolean} isMove True if the operation is "move", otherwise (i.e.
   1084  *     if the operation is "copy") false.
   1085  */
   1086 FileOperationManager.prototype.paste = function(
   1087     sourceEntries, targetEntry, isMove) {
   1088   // Do nothing if sourceEntries is empty.
   1089   if (sourceEntries.length === 0)
   1090     return;
   1091 
   1092   var filteredEntries = [];
   1093   var resolveGroup = new AsyncUtil.Queue();
   1094 
   1095   if (isMove) {
   1096     for (var index = 0; index < sourceEntries.length; index++) {
   1097       var sourceEntry = sourceEntries[index];
   1098       resolveGroup.run(function(sourceEntry, callback) {
   1099         sourceEntry.getParent(function(inParentEntry) {
   1100           if (!util.isSameEntry(inParentEntry, targetEntry))
   1101             filteredEntries.push(sourceEntry);
   1102           callback();
   1103         }, function() {
   1104           console.warn(
   1105               'Failed to resolve the parent for: ' + sourceEntry.toURL());
   1106           // Even if the parent is not available, try to move it.
   1107           filteredEntries.push(sourceEntry);
   1108           callback();
   1109         });
   1110       }.bind(this, sourceEntry));
   1111     }
   1112   } else {
   1113     // Always copy all of the files.
   1114     filteredEntries = sourceEntries;
   1115   }
   1116 
   1117   resolveGroup.run(function(callback) {
   1118     // Do nothing, if we have no entries to be pasted.
   1119     if (filteredEntries.length === 0)
   1120       return;
   1121 
   1122     this.queueCopy_(targetEntry, filteredEntries, isMove);
   1123   }.bind(this));
   1124 };
   1125 
   1126 /**
   1127  * Checks if the move operation is available between the given two locations.
   1128  * This method uses the volume manager, which is lazily created, therefore the
   1129  * result is returned asynchronously.
   1130  *
   1131  * @param {DirectoryEntry} sourceEntry An entry from the source.
   1132  * @param {DirectoryEntry} targetDirEntry Directory entry for the target.
   1133  * @param {function(boolean)} callback Callback with result whether the entries
   1134  *     can be directly moved.
   1135  * @private
   1136  */
   1137 FileOperationManager.prototype.isMovable_ = function(
   1138     sourceEntry, targetDirEntry, callback) {
   1139   VolumeManager.getInstance(function(volumeManager) {
   1140     var sourceLocationInfo = volumeManager.getLocationInfo(sourceEntry);
   1141     var targetDirLocationInfo = volumeManager.getLocationInfo(targetDirEntry);
   1142     callback(
   1143         sourceLocationInfo && targetDirLocationInfo &&
   1144         sourceLocationInfo.volumeInfo === targetDirLocationInfo.volumeInfo);
   1145   });
   1146 };
   1147 
   1148 /**
   1149  * Initiate a file copy. When copying files, null can be specified as source
   1150  * directory.
   1151  *
   1152  * @param {DirectoryEntry} targetDirEntry Target directory.
   1153  * @param {Array.<Entry>} entries Entries to copy.
   1154  * @param {boolean} isMove In case of move.
   1155  * @private
   1156  */
   1157 FileOperationManager.prototype.queueCopy_ = function(
   1158     targetDirEntry, entries, isMove) {
   1159   var createTask = function(task) {
   1160     task.taskId = this.generateTaskId_();
   1161     this.eventRouter_.sendProgressEvent(
   1162         'BEGIN', task.getStatus(), task.taskId);
   1163     task.initialize(function() {
   1164       this.copyTasks_.push(task);
   1165       if (this.copyTasks_.length === 1)
   1166         this.serviceAllTasks_();
   1167     }.bind(this));
   1168   }.bind(this);
   1169 
   1170   var task;
   1171   if (isMove) {
   1172     // When moving between different volumes, moving is implemented as a copy
   1173     // and delete. This is because moving between volumes is slow, and moveTo()
   1174     // is not cancellable nor provides progress feedback.
   1175     this.isMovable_(entries[0], targetDirEntry, function(isMovable) {
   1176       if (isMovable) {
   1177         createTask(new FileOperationManager.MoveTask(entries, targetDirEntry));
   1178       } else {
   1179         createTask(
   1180             new FileOperationManager.CopyTask(entries, targetDirEntry, true));
   1181       }
   1182     });
   1183   } else {
   1184     createTask(
   1185         new FileOperationManager.CopyTask(entries, targetDirEntry, false));
   1186   }
   1187 };
   1188 
   1189 /**
   1190  * Service all pending tasks, as well as any that might appear during the
   1191  * copy.
   1192  *
   1193  * @private
   1194  */
   1195 FileOperationManager.prototype.serviceAllTasks_ = function() {
   1196   if (!this.copyTasks_.length) {
   1197     // All tasks have been serviced, clean up and exit.
   1198     chrome.power.releaseKeepAwake();
   1199     this.resetQueue_();
   1200     return;
   1201   }
   1202 
   1203   // Prevent the system from sleeping while copy is in progress.
   1204   chrome.power.requestKeepAwake('system');
   1205 
   1206   var onTaskProgress = function() {
   1207     this.eventRouter_.sendProgressEvent('PROGRESS',
   1208                                         this.copyTasks_[0].getStatus(),
   1209                                         this.copyTasks_[0].taskId);
   1210   }.bind(this);
   1211 
   1212   var onEntryChanged = function(kind, entry) {
   1213     this.eventRouter_.sendEntryChangedEvent(kind, entry);
   1214   }.bind(this);
   1215 
   1216   var onTaskError = function(err) {
   1217     var task = this.copyTasks_.shift();
   1218     var reason = err.data.name === util.FileError.ABORT_ERR ?
   1219         'CANCELED' : 'ERROR';
   1220     this.eventRouter_.sendProgressEvent(reason,
   1221                                         task.getStatus(),
   1222                                         task.taskId,
   1223                                         err);
   1224     this.serviceAllTasks_();
   1225   }.bind(this);
   1226 
   1227   var onTaskSuccess = function() {
   1228     // The task at the front of the queue is completed. Pop it from the queue.
   1229     var task = this.copyTasks_.shift();
   1230     this.eventRouter_.sendProgressEvent('SUCCESS',
   1231                                         task.getStatus(),
   1232                                         task.taskId);
   1233     this.serviceAllTasks_();
   1234   }.bind(this);
   1235 
   1236   var nextTask = this.copyTasks_[0];
   1237   this.eventRouter_.sendProgressEvent('PROGRESS',
   1238                                       nextTask.getStatus(),
   1239                                       nextTask.taskId);
   1240   nextTask.run(onEntryChanged, onTaskProgress, onTaskSuccess, onTaskError);
   1241 };
   1242 
   1243 /**
   1244  * Timeout before files are really deleted (to allow undo).
   1245  */
   1246 FileOperationManager.DELETE_TIMEOUT = 30 * 1000;
   1247 
   1248 /**
   1249  * Schedules the files deletion.
   1250  *
   1251  * @param {Array.<Entry>} entries The entries.
   1252  */
   1253 FileOperationManager.prototype.deleteEntries = function(entries) {
   1254   // TODO(hirono): Make FileOperationManager.DeleteTask.
   1255   var task = Object.seal({
   1256     entries: entries,
   1257     taskId: this.generateTaskId_(),
   1258     entrySize: {},
   1259     totalBytes: 0,
   1260     processedBytes: 0,
   1261     cancelRequested: false
   1262   });
   1263 
   1264   // Obtains entry size and sum them up.
   1265   var group = new AsyncUtil.Group();
   1266   for (var i = 0; i < task.entries.length; i++) {
   1267     group.add(function(entry, callback) {
   1268       entry.getMetadata(function(metadata) {
   1269         var index = task.entries.indexOf(entries);
   1270         task.entrySize[entry.toURL()] = metadata.size;
   1271         task.totalBytes += metadata.size;
   1272         callback();
   1273       }, function() {
   1274         // Fail to obtain the metadata. Use fake value 1.
   1275         task.entrySize[entry.toURL()] = 1;
   1276         task.totalBytes += 1;
   1277         callback();
   1278       });
   1279     }.bind(this, task.entries[i]));
   1280   }
   1281 
   1282   // Add a delete task.
   1283   group.run(function() {
   1284     this.deleteTasks_.push(task);
   1285     this.eventRouter_.sendDeleteEvent('BEGIN', task);
   1286     if (this.deleteTasks_.length === 1)
   1287       this.serviceAllDeleteTasks_();
   1288   }.bind(this));
   1289 };
   1290 
   1291 /**
   1292  * Service all pending delete tasks, as well as any that might appear during the
   1293  * deletion.
   1294  *
   1295  * Must not be called if there is an in-flight delete task.
   1296  *
   1297  * @private
   1298  */
   1299 FileOperationManager.prototype.serviceAllDeleteTasks_ = function() {
   1300   this.serviceDeleteTask_(
   1301       this.deleteTasks_[0],
   1302       function() {
   1303         this.deleteTasks_.shift();
   1304         if (this.deleteTasks_.length)
   1305           this.serviceAllDeleteTasks_();
   1306       }.bind(this));
   1307 };
   1308 
   1309 /**
   1310  * Performs the deletion.
   1311  *
   1312  * @param {Object} task The delete task (see deleteEntries function).
   1313  * @param {function()} callback Callback run on task end.
   1314  * @private
   1315  */
   1316 FileOperationManager.prototype.serviceDeleteTask_ = function(task, callback) {
   1317   var queue = new AsyncUtil.Queue();
   1318 
   1319   // Delete each entry.
   1320   var error = null;
   1321   var deleteOneEntry = function(inCallback) {
   1322     if (!task.entries.length || task.cancelRequested || error) {
   1323       inCallback();
   1324       return;
   1325     }
   1326     this.eventRouter_.sendDeleteEvent('PROGRESS', task);
   1327     util.removeFileOrDirectory(
   1328         task.entries[0],
   1329         function() {
   1330           this.eventRouter_.sendEntryChangedEvent(
   1331               util.EntryChangedKind.DELETED, task.entries[0]);
   1332           task.processedBytes += task.entrySize[task.entries[0].toURL()];
   1333           task.entries.shift();
   1334           deleteOneEntry(inCallback);
   1335         }.bind(this),
   1336         function(inError) {
   1337           error = inError;
   1338           inCallback();
   1339         }.bind(this));
   1340   }.bind(this);
   1341   queue.run(deleteOneEntry);
   1342 
   1343   // Send an event and finish the async steps.
   1344   queue.run(function(inCallback) {
   1345     var reason;
   1346     if (error)
   1347       reason = 'ERROR';
   1348     else if (task.cancelRequested)
   1349       reason = 'CANCELED';
   1350     else
   1351       reason = 'SUCCESS';
   1352     this.eventRouter_.sendDeleteEvent(reason, task);
   1353     inCallback();
   1354     callback();
   1355   }.bind(this));
   1356 };
   1357 
   1358 /**
   1359  * Creates a zip file for the selection of files.
   1360  *
   1361  * @param {Entry} dirEntry The directory containing the selection.
   1362  * @param {Array.<Entry>} selectionEntries The selected entries.
   1363  */
   1364 FileOperationManager.prototype.zipSelection = function(
   1365     dirEntry, selectionEntries) {
   1366   var zipTask = new FileOperationManager.ZipTask(
   1367       selectionEntries, dirEntry, dirEntry);
   1368   zipTask.taskId = this.generateTaskId_(this.copyTasks_);
   1369   zipTask.zip = true;
   1370   this.eventRouter_.sendProgressEvent('BEGIN',
   1371                                       zipTask.getStatus(),
   1372                                       zipTask.taskId);
   1373   zipTask.initialize(function() {
   1374     this.copyTasks_.push(zipTask);
   1375     if (this.copyTasks_.length == 1)
   1376       this.serviceAllTasks_();
   1377   }.bind(this));
   1378 };
   1379 
   1380 /**
   1381  * Generates new task ID.
   1382  *
   1383  * @return {string} New task ID.
   1384  * @private
   1385  */
   1386 FileOperationManager.prototype.generateTaskId_ = function() {
   1387   return 'file-operation-' + this.taskIdCounter_++;
   1388 };
   1389