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