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