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