b2b8e54fb6cda3027c8f410788dfb65dc27ed18c
[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 '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  * Kick off pasting.
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  * @param {string=} opt_taskId If the corresponding item has already created
1189  *     at another places, we need to specify the ID of the item. If the
1190  *     item is not created, FileOperationManager generates new ID.
1191  */
1192 FileOperationManager.prototype.paste = function(
1193     sourceEntries, targetEntry, isMove, opt_taskId) {
1194   // Do nothing if sourceEntries is empty.
1195   if (sourceEntries.length === 0)
1196     return;
1197
1198   var filteredEntries = [];
1199   var resolveGroup = new AsyncUtil.Queue();
1200
1201   if (isMove) {
1202     for (var index = 0; index < sourceEntries.length; index++) {
1203       resolveGroup.run(function(sourceEntry, callback) {
1204         sourceEntry.getParent(function(inParentEntry) {
1205           if (!util.isSameEntry(inParentEntry, targetEntry))
1206             filteredEntries.push(sourceEntry);
1207           callback();
1208         }, function() {
1209           console.warn(
1210               'Failed to resolve the parent for: ' + sourceEntry.toURL());
1211           // Even if the parent is not available, try to move it.
1212           filteredEntries.push(sourceEntry);
1213           callback();
1214         });
1215       }.bind(this, sourceEntries[index]));
1216     }
1217   } else {
1218     // Always copy all of the files.
1219     filteredEntries = sourceEntries;
1220   }
1221
1222   resolveGroup.run(function(callback) {
1223     // Do nothing, if we have no entries to be pasted.
1224     if (filteredEntries.length === 0)
1225       return;
1226
1227     this.queueCopy_(targetEntry, filteredEntries, isMove, opt_taskId);
1228   }.bind(this));
1229 };
1230
1231 /**
1232  * Initiate a file copy. When copying files, null can be specified as source
1233  * directory.
1234  *
1235  * @param {DirectoryEntry} targetDirEntry Target directory.
1236  * @param {Array.<Entry>} entries Entries to copy.
1237  * @param {boolean} isMove In case of move.
1238  * @param {string=} opt_taskId If the corresponding item has already created
1239  *     at another places, we need to specify the ID of the item. If the
1240  *     item is not created, FileOperationManager generates new ID.
1241  * @private
1242  */
1243 FileOperationManager.prototype.queueCopy_ = function(
1244     targetDirEntry, entries, isMove, opt_taskId) {
1245   var task;
1246   if (isMove) {
1247     // When moving between different volumes, moving is implemented as a copy
1248     // and delete. This is because moving between volumes is slow, and moveTo()
1249     // is not cancellable nor provides progress feedback.
1250     if (util.isSameFileSystem(entries[0].filesystem,
1251                               targetDirEntry.filesystem)) {
1252       task = new FileOperationManager.MoveTask(entries, targetDirEntry);
1253     } else {
1254       task = new FileOperationManager.CopyTask(entries, targetDirEntry, true);
1255     }
1256   } else {
1257     task = new FileOperationManager.CopyTask(entries, targetDirEntry, false);
1258   }
1259
1260   task.taskId = opt_taskId || this.generateTaskId();
1261   this.eventRouter_.sendProgressEvent('BEGIN', task.getStatus(), task.taskId);
1262   task.initialize(function() {
1263     this.copyTasks_.push(task);
1264     if (this.copyTasks_.length === 1)
1265       this.serviceAllTasks_();
1266   }.bind(this));
1267 };
1268
1269 /**
1270  * Service all pending tasks, as well as any that might appear during the
1271  * copy.
1272  *
1273  * @private
1274  */
1275 FileOperationManager.prototype.serviceAllTasks_ = function() {
1276   if (!this.copyTasks_.length) {
1277     // All tasks have been serviced, clean up and exit.
1278     chrome.power.releaseKeepAwake();
1279     this.resetQueue_();
1280     return;
1281   }
1282
1283   // Prevent the system from sleeping while copy is in progress.
1284   chrome.power.requestKeepAwake('system');
1285
1286   var onTaskProgress = function() {
1287     this.eventRouter_.sendProgressEvent('PROGRESS',
1288                                         this.copyTasks_[0].getStatus(),
1289                                         this.copyTasks_[0].taskId);
1290   }.bind(this);
1291
1292   var onEntryChanged = function(kind, entry) {
1293     this.eventRouter_.sendEntryChangedEvent(kind, entry);
1294   }.bind(this);
1295
1296   var onTaskError = function(err) {
1297     var task = this.copyTasks_.shift();
1298     var reason = err.data.name === util.FileError.ABORT_ERR ?
1299         'CANCELED' : 'ERROR';
1300     this.eventRouter_.sendProgressEvent(reason,
1301                                         task.getStatus(),
1302                                         task.taskId,
1303                                         err);
1304     this.serviceAllTasks_();
1305   }.bind(this);
1306
1307   var onTaskSuccess = function() {
1308     // The task at the front of the queue is completed. Pop it from the queue.
1309     var task = this.copyTasks_.shift();
1310     this.eventRouter_.sendProgressEvent('SUCCESS',
1311                                         task.getStatus(),
1312                                         task.taskId);
1313     this.serviceAllTasks_();
1314   }.bind(this);
1315
1316   var nextTask = this.copyTasks_[0];
1317   this.eventRouter_.sendProgressEvent('PROGRESS',
1318                                       nextTask.getStatus(),
1319                                       nextTask.taskId);
1320   nextTask.run(onEntryChanged, onTaskProgress, onTaskSuccess, onTaskError);
1321 };
1322
1323 /**
1324  * Timeout before files are really deleted (to allow undo).
1325  */
1326 FileOperationManager.DELETE_TIMEOUT = 30 * 1000;
1327
1328 /**
1329  * Schedules the files deletion.
1330  *
1331  * @param {Array.<Entry>} entries The entries.
1332  */
1333 FileOperationManager.prototype.deleteEntries = function(entries) {
1334   // TODO(hirono): Make FileOperationManager.DeleteTask.
1335   var task = Object.seal({
1336     entries: entries,
1337     taskId: this.generateTaskId(),
1338     entrySize: {},
1339     totalBytes: 0,
1340     processedBytes: 0,
1341     cancelRequested: false
1342   });
1343
1344   // Obtains entry size and sum them up.
1345   var group = new AsyncUtil.Group();
1346   for (var i = 0; i < task.entries.length; i++) {
1347     group.add(function(entry, callback) {
1348       entry.getMetadata(function(metadata) {
1349         var index = task.entries.indexOf(entries);
1350         task.entrySize[entry.toURL()] = metadata.size;
1351         task.totalBytes += metadata.size;
1352         callback();
1353       }, function() {
1354         // Fail to obtain the metadata. Use fake value 1.
1355         task.entrySize[entry.toURL()] = 1;
1356         task.totalBytes += 1;
1357         callback();
1358       });
1359     }.bind(this, task.entries[i]));
1360   }
1361
1362   // Add a delete task.
1363   group.run(function() {
1364     this.deleteTasks_.push(task);
1365     this.eventRouter_.sendDeleteEvent('BEGIN', task);
1366     if (this.deleteTasks_.length === 1)
1367       this.serviceAllDeleteTasks_();
1368   }.bind(this));
1369 };
1370
1371 /**
1372  * Service all pending delete tasks, as well as any that might appear during the
1373  * deletion.
1374  *
1375  * Must not be called if there is an in-flight delete task.
1376  *
1377  * @private
1378  */
1379 FileOperationManager.prototype.serviceAllDeleteTasks_ = function() {
1380   this.serviceDeleteTask_(
1381       this.deleteTasks_[0],
1382       function() {
1383         this.deleteTasks_.shift();
1384         if (this.deleteTasks_.length)
1385           this.serviceAllDeleteTasks_();
1386       }.bind(this));
1387 };
1388
1389 /**
1390  * Performs the deletion.
1391  *
1392  * @param {Object} task The delete task (see deleteEntries function).
1393  * @param {function()} callback Callback run on task end.
1394  * @private
1395  */
1396 FileOperationManager.prototype.serviceDeleteTask_ = function(task, callback) {
1397   var queue = new AsyncUtil.Queue();
1398
1399   // Delete each entry.
1400   var error = null;
1401   var deleteOneEntry = function(inCallback) {
1402     if (!task.entries.length || task.cancelRequested || error) {
1403       inCallback();
1404       return;
1405     }
1406     this.eventRouter_.sendDeleteEvent('PROGRESS', task);
1407     util.removeFileOrDirectory(
1408         task.entries[0],
1409         function() {
1410           this.eventRouter_.sendEntryChangedEvent(
1411               util.EntryChangedKind.DELETED, task.entries[0]);
1412           task.processedBytes += task.entrySize[task.entries[0].toURL()];
1413           task.entries.shift();
1414           deleteOneEntry(inCallback);
1415         }.bind(this),
1416         function(inError) {
1417           error = inError;
1418           inCallback();
1419         }.bind(this));
1420   }.bind(this);
1421   queue.run(deleteOneEntry);
1422
1423   // Send an event and finish the async steps.
1424   queue.run(function(inCallback) {
1425     var reason;
1426     if (error)
1427       reason = 'ERROR';
1428     else if (task.cancelRequested)
1429       reason = 'CANCELED';
1430     else
1431       reason = 'SUCCESS';
1432     this.eventRouter_.sendDeleteEvent(reason, task);
1433     inCallback();
1434     callback();
1435   }.bind(this));
1436 };
1437
1438 /**
1439  * Creates a zip file for the selection of files.
1440  *
1441  * @param {Entry} dirEntry The directory containing the selection.
1442  * @param {Array.<Entry>} selectionEntries The selected entries.
1443  */
1444 FileOperationManager.prototype.zipSelection = function(
1445     dirEntry, selectionEntries) {
1446   var zipTask = new FileOperationManager.ZipTask(
1447       selectionEntries, dirEntry, dirEntry);
1448   zipTask.taskId = this.generateTaskId(this.copyTasks_);
1449   zipTask.zip = true;
1450   this.eventRouter_.sendProgressEvent('BEGIN',
1451                                       zipTask.getStatus(),
1452                                       zipTask.taskId);
1453   zipTask.initialize(function() {
1454     this.copyTasks_.push(zipTask);
1455     if (this.copyTasks_.length == 1)
1456       this.serviceAllTasks_();
1457   }.bind(this));
1458 };
1459
1460 /**
1461  * Generates new task ID.
1462  *
1463  * @return {string} New task ID.
1464  */
1465 FileOperationManager.prototype.generateTaskId = function() {
1466   return 'file-operation-' + this.taskIdCounter_++;
1467 };