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