Switch Copy implementation from JS to HTML5 based C++ private API.
[chromium-blink-merge.git] / chrome / browser / resources / file_manager / js / file_operation_manager.js
blob183c958aabada62722af6dc93b4f18006e105a3d
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.
5 'use strict';
7 /**
8  * Utilities for FileOperationManager.
9  */
10 var fileOperationUtil = {};
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         };
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       });
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  *     occured 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;
66   var maybeInvokeCallback = function() {
67     // If there still remain some running tasks, wait their finishing.
68     if (numRunningTasks > 0)
69       return;
71     if (error)
72       errorCallback(error);
73     else
74       successCallback(result);
75   };
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   };
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;
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             }
107             for (var i = 0; i < subEntries.length; i++)
108               process(subEntries[i]);
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   };
124   process(entry);
128  * Sets last modified date to the entry.
129  * @param {Entry} entry The entry to which the last modified is set.
130  * @param {Date} modificationTime The last modified time.
131  */
132 fileOperationUtil.setLastModified = function(entry, modificationTime) {
133   chrome.fileBrowserPrivate.setLastModified(
134       entry.toURL(), '' + Math.round(modificationTime.getTime() / 1000));
138  * CAUTION: THIS IS STILL UNDER DEVELOPMENT. DO NOT USE.
139  * This will replace copyRecursively defined below.
141  * Copies source to parent with the name newName recursively.
142  * This should work very similar to FileSystem API's copyTo. The difference is;
143  * - The progress callback is supported.
144  * - The cancellation is supported.
146  * @param {Entry} source The entry to be copied.
147  * @param {DirectoryEntry} parent The entry of the destination directory.
148  * @param {string} newName The name of copied file.
149  * @param {function(string, string)} entryChangedCallback
150  *     Callback invoked when an entry is created with the source url and
151  *     the destination url.
152  * @param {function(string, number)} progressCallback Callback invoked
153  *     periodically during the copying. It takes the source url and the
154  *     processed bytes of it.
155  * @param {function(string)} successCallback Callback invoked when the copy
156  *     is successfully done with the url of the created entry.
157  * @param {function(FileError)} errorCallback Callback invoked when an error
158  *     is found.
159  * @return {function()} Callback to cancel the current file copy operation.
160  *     When the cancel is done, errorCallback will be called. The returned
161  *     callback must not be called more than once.
162  */
163 fileOperationUtil.copyTo = function(
164     source, parent, newName, entryChangedCallback, progressCallback,
165     successCallback, errorCallback) {
166   var copyId = null;
167   var pendingCallbacks = [];
169   var onCopyProgress = function(progressCopyId, status) {
170     if (copyId == null) {
171       // If the copyId is not yet available, wait for it.
172       pendingCallbacks.push(
173           onCopyProgress.bind(null, progressCopyId, status));
174       return;
175     }
177     // This is not what we're interested in.
178     if (progressCopyId != copyId)
179       return;
181     switch (status.type) {
182       case 'begin_copy_entry':
183         break;
185       case 'end_copy_entry':
186         entryChangedCallback(status.sourceUrl, status.destinationUrl);
187         break;
189       case 'progress':
190         progressCallback(status.sourceUrl, status.size);
191         break;
193       case 'success':
194         chrome.fileBrowserPrivate.onCopyProgress.removeListener(onCopyProgress);
195         successCallback(status.destinationUrl);
196         break;
198       case 'error':
199         chrome.fileBrowserPrivate.onCopyProgress.removeListener(onCopyProgress);
200         errorCallback(util.createFileError(status.error));
201         break;
203       default:
204         // Found unknown state. Cancel the task, and return an error.
205         console.error('Unknown progress type: ' + status.type);
206         chrome.fileBrowserPrivate.onCopyProgress.removeListener(onCopyProgress);
207         chrome.fileBrowserPrivate.cancelCopy(copyId);
208         errorCallback(util.createFileError(FileError.INVALID_STATE_ERR));
209     }
210   };
212   // Register the listener before calling startCopy. Otherwise some events
213   // would be lost.
214   chrome.fileBrowserPrivate.onCopyProgress.addListener(onCopyProgress);
216   // Then starts the copy.
217   chrome.fileBrowserPrivate.startCopy(
218       source.toURL(), parent.toURL(), newName, function(startCopyId) {
219         // last error contains the FileError code on error.
220         if (chrome.runtime.lastError) {
221           // Unsubscribe the progress listener.
222           chrome.fileBrowserPrivate.onCopyProgress.removeListener(
223               onCopyProgress);
224           errorCallback(util.createFileError(
225               Integer.parseInt(chrome.runtime.lastError, 10)));
226           return;
227         }
229         copyId = startCopyId;
230         for (var i = 0; i < pendingCallbacks.length; i++) {
231           pendingCallbacks[i]();
232         }
233       });
235   return function() {
236     // If copyId is not yet available, wait for it.
237     if (copyId == null) {
238       pendingCallbacks.push(function() {
239         chrome.fileBrowserPrivate.cancelCopy(copyId);
240       });
241       return;
242     }
244     chrome.fileBrowserPrivate.cancelCopy(copyId);
245   };
249  * DEPRECATED: This method is no longer used.
250  * TODO(hidehiko): Remove this.
251  * Copies source to parent with the name newName recursively.
253  * @param {Entry} source The entry to be copied.
254  * @param {DirectoryEntry} parent The entry of the destination directory.
255  * @param {string} newName The name of copied file.
256  * @param {function(string, string)} entryChangedCallback
257  *     Callback invoked when an entry is created with the source url and
258  *     the destination url.
259  * @param {function(string, number)} progressCallback Callback invoked
260  *     periodically during the copying. It takes the source url and the
261  *     processed bytes of it.
262  * @param {function(string)} successCallback Callback invoked when the copy
263  *     is successfully done with the url of the created entry.
264  * @param {function(FileError)} errorCallback Callback invoked when an error
265  *     is found.
266  * @return {function()} Callback to cancel the current file copy operation.
267  *     When the cancel is done, errorCallback will be called. The returned
268  *     callback must not be called more than once.
269  */
270 fileOperationUtil.copyRecursively = function(
271     source, parent, newName, entryChangedCallback, progressCallback,
272     successCallback, errorCallback) {
273   // Notify that the copy begins for each entry.
274   progressCallback(source, 0);
276   // If the entry is a file, redirect it to copyFile_().
277   if (source.isFile) {
278     return fileOperationUtil.copyFile_(
279         source, parent, newName, progressCallback,
280         function(entry) {
281           entryChangedCallback(source.toURL(), entry.toURL());
282           successCallback(entry.toURL());
283         },
284         errorCallback);
285   }
287   // Hereafter, the source is directory.
288   var cancelRequested = false;
289   var cancelCallback = null;
291   // First, we create the directory copy.
292   parent.getDirectory(
293       newName, {create: true, exclusive: true},
294       function(dirEntry) {
295         entryChangedCallback(source.toURL(), dirEntry.toURL());
296         if (cancelRequested) {
297           errorCallback(util.createFileError(FileError.ABORT_ERR));
298           return;
299         }
301         // Iterate on children, and copy them recursively.
302         util.forEachDirEntry(
303             source,
304             function(child, callback) {
305               if (cancelRequested) {
306                 errorCallback(util.createFileError(FileError.ABORT_ERR));
307                 return;
308               }
310               cancelCallback = fileOperationUtil.copyRecursively(
311                   child, dirEntry, child.name, entryChangedCallback,
312                   progressCallback,
313                   function() {
314                     cancelCallback = null;
315                     callback();
316                   },
317                   function(error) {
318                     cancelCallback = null;
319                     errorCallback(error);
320                   });
321             },
322             function() {
323               successCallback(dirEntry.toURL());
324             },
325             errorCallback);
326       },
327       errorCallback);
329   return function() {
330     cancelRequested = true;
331     if (cancelCallback) {
332       cancelCallback();
333       cancelCallback = null;
334     }
335   };
339  * Copies a file from source to the parent directory with newName.
340  * See also copyFileByStream_ and copyFileOnDrive_ for the implementation
341  * details.
343  * @param {FileEntry} source The file entry to be copied.
344  * @param {DirectoryEntry} parent The entry of the destination directory.
345  * @param {string} newName The name of copied file.
346  * @param {function(string, number)} progressCallback Callback invoked
347  *     periodically during the file writing with the source url and the
348  *     number of the processed bytes.
349  * @param {function(FileEntry)} successCallback Callback invoked when the copy
350  *     is successfully done with the entry of the created file.
351  * @param {function(FileError)} errorCallback Callback invoked when an error
352  *     is found.
353  * @return {function()} Callback to cancel the current file copy operation.
354  *     When the cancel is done, errorCallback will be called. The returned
355  *     callback must not be called more than once.
356  * @private
357  */
358 fileOperationUtil.copyFile_ = function(
359     source, parent, newName, progressCallback, successCallback, errorCallback) {
360   if (!PathUtil.isDriveBasedPath(source.fullPath) &&
361       !PathUtil.isDriveBasedPath(parent.fullPath)) {
362     // Copying a file between non-Drive file systems.
363     return fileOperationUtil.copyFileByStream_(
364         source, parent, newName, progressCallback, successCallback,
365         errorCallback);
366   } else {
367     // Copying related to the Drive file system.
368     return fileOperationUtil.copyFileOnDrive_(
369         source, parent, newName, progressCallback, successCallback,
370         errorCallback);
371   }
375  * Copies a file by using File and FileWriter objects.
377  * This is a js-implementation of FileEntry.copyTo(). Unfortunately, copyTo
378  * doesn't support periodical progress updating nor cancelling. To support
379  * these operations, this method implements copyTo by streaming way in
380  * JavaScript.
382  * Note that this is designed for file copying on local file system. We have
383  * some special cases about copying on Drive file system. See also
384  * copyFileOnDrive_() for more details.
386  * @param {FileEntry} source The file entry to be copied.
387  * @param {DirectoryEntry} parent The entry of the destination directory.
388  * @param {string} newName The name of copied file.
389  * @param {function(string, number)} progressCallback Callback invoked
390  *     periodically during the file writing with the source url and the
391  *     number of the processed bytes.
392  * @param {function(FileEntry)} successCallback Callback invoked when the copy
393  *     is successfully done with the entry of the created file.
394  * @param {function(FileError)} errorCallback Callback invoked when an error
395  *     is found.
396  * @return {function()} Callback to cancel the current file copy operation.
397  *     When the cancel is done, errorCallback will be called. The returned
398  *     callback must not be called more than once.
399  * @private
400  */
401 fileOperationUtil.copyFileByStream_ = function(
402     source, parent, newName, progressCallback, successCallback, errorCallback) {
403   // Set to true when cancel is requested.
404   var cancelRequested = false;
406   source.file(function(file) {
407     if (cancelRequested) {
408       errorCallback(util.createFileError(FileError.ABORT_ERR));
409       return;
410     }
412     parent.getFile(newName, {create: true, exclusive: true}, function(target) {
413       if (cancelRequested) {
414         errorCallback(util.createFileError(FileError.ABORT_ERR));
415         return;
416       }
418       target.createWriter(function(writer) {
419         if (cancelRequested) {
420           errorCallback(util.createFileError(FileError.ABORT_ERR));
421           return;
422         }
424         writer.onerror = writer.onabort = function(progress) {
425           errorCallback(cancelRequested ?
426               util.createFileError(FileError.ABORT_ERR) :
427                   writer.error);
428         };
430         writer.onprogress = function(progress) {
431           if (cancelRequested) {
432             // If the copy was cancelled, we should abort the operation.
433             // The errorCallback will be called by writer.onabort after the
434             // termination.
435             writer.abort();
436             return;
437           }
438           progressCallback(source.toURL(), progress.loaded);
439         };
441         writer.onwrite = function() {
442           if (cancelRequested) {
443             errorCallback(util.createFileError(FileError.ABORT_ERR));
444             return;
445           }
447           source.getMetadata(function(metadata) {
448             if (cancelRequested) {
449               errorCallback(util.createFileError(FileError.ABORT_ERR));
450               return;
451             }
453             fileOperationUtil.setLastModified(
454                 target, metadata.modificationTime);
455             successCallback(target);
456           }, errorCallback);
457         };
459         writer.write(file);
460       }, errorCallback);
461     }, errorCallback);
462   }, errorCallback);
464   return function() { cancelRequested = true; };
468  * Copies a file a) from Drive to local, b) from local to Drive, or c) from
469  * Drive to Drive.
470  * Currently, we need to take care about following two things for Drive:
472  * 1) Copying hosted document.
473  * In theory, it is impossible to actual copy a hosted document to other
474  * file system. Thus, instead, Drive file system backend creates a JSON file
475  * referring to the hosted document. Also, when it is uploaded by copyTo,
476  * the hosted document is copied on the server. Note that, this doesn't work
477  * when a user creates a file by FileWriter (as copyFileEntry_ does).
479  * 2) File transfer between local and Drive server.
480  * There are two directions of file transfer; from local to Drive and from
481  * Drive to local.
482  * The file transfer from local to Drive is done as a part of file system
483  * background sync (kicked after the copy operation is done). So we don't need
484  * to take care about it here. To copy the file from Drive to local (or Drive
485  * to Drive with GData WAPI), we need to download the file content (if it is
486  * not locally cached). During the downloading, we can listen the periodical
487  * updating and cancel the downloding via private API.
489  * This function supports progress updating and cancelling partially.
490  * Unfortunately, FileEntry.copyTo doesn't support progress updating nor
491  * cancelling, so we support them only during file downloading.
493  * Note: we're planning to move copyTo logic into c++ side. crbug.com/261492
495  * @param {FileEntry} source The entry of the file to be copied.
496  * @param {DirectoryEntry} parent The entry of the destination directory.
497  * @param {string} newName The name of the copied file.
498  * @param {function(string, number)} progressCallback Callback invoked
499  *     periodically during the file writing with the source url and the
500  *     number of the processed bytes.
501  * @param {function(FileEntry)} successCallback Callback invoked when the
502  *     file copy is successfully done with the entry of the copied file.
503  * @param {function(FileError)} errorCallback Callback invoked when an error
504  *     is found.
505  * @return {function()} Callback to cancel the current file copy operation.
506  *     When the cancel is done, errorCallback will be called. The returned
507  *     callback must not be called more than once.
508  * @private
509  */
510 fileOperationUtil.copyFileOnDrive_ = function(
511     source, parent, newName, progressCallback, successCallback, errorCallback) {
512   // Set to true when cancel is requested.
513   var cancelRequested = false;
514   var cancelCallback = null;
516   var onCopyToCompleted = null;
518   // Progress callback.
519   // Because the uploading the file from local cache to Drive server will be
520   // done as a part of background Drive file system sync, so for this copy
521   // operation, what we need to take care about is only file downloading.
522   if (PathUtil.isDriveBasedPath(source.fullPath)) {
523     var sourceUrl = source.toURL();
524     var sourcePath = util.extractFilePath(sourceUrl);
525     var onFileTransfersUpdated = function(statusList) {
526       for (var i = 0; i < statusList.length; i++) {
527         var status = statusList[i];
529         // Comparing urls is unreliable, since they may use different
530         // url encoding schemes (eg. rfc2396 vs. rfc3986).
531         var filePath = util.extractFilePath(status.fileUrl);
532         if (filePath == sourcePath) {
533           progressCallback(source.toURL(), status.processed);
534           return;
535         }
536       }
537     };
539     // Subscribe to listen file transfer updating notifications.
540     chrome.fileBrowserPrivate.onFileTransfersUpdated.addListener(
541         onFileTransfersUpdated);
543     // Currently, we do NOT upload the file during the copy operation.
544     // It will be done as a part of file system sync after copy operation.
545     // So, we can cancel only file downloading.
546     cancelCallback = function() {
547       chrome.fileBrowserPrivate.cancelFileTransfers(
548           [sourceUrl], function() {});
549     };
551     // We need to clean up on copyTo completion regardless if it is
552     // successfully done or not.
553     onCopyToCompleted = function() {
554       cancelCallback = null;
555       chrome.fileBrowserPrivate.onFileTransfersUpdated.removeListener(
556           onFileTransfersUpdated);
557     };
558   }
560   source.copyTo(
561       parent, newName,
562       function(entry) {
563         if (onCopyToCompleted)
564           onCopyToCompleted();
566         if (cancelRequested) {
567           errorCallback(util.createFileError(FileError.ABORT_ERR));
568           return;
569         }
571         successCallback(entry);
572       },
573       function(error) {
574         if (onCopyToCompleted)
575           onCopyToCompleted();
577         errorCallback(error);
578       });
580   return function() {
581     cancelRequested = true;
582     if (cancelCallback) {
583       cancelCallback();
584       cancelCallback = null;
585     }
586   };
590  * Thin wrapper of chrome.fileBrowserPrivate.zipSelection to adapt its
591  * interface similar to copyTo().
593  * @param {Array.<Entry>} sources The array of entries to be archived.
594  * @param {DirectoryEntry} parent The entry of the destination directory.
595  * @param {string} newName The name of the archive to be created.
596  * @param {function(FileEntry)} successCallback Callback invoked when the
597  *     operation is successfully done with the entry of the created archive.
598  * @param {function(FileError)} errorCallback Callback invoked when an error
599  *     is found.
600  */
601 fileOperationUtil.zipSelection = function(
602     sources, parent, newName, successCallback, errorCallback) {
603   chrome.fileBrowserPrivate.zipSelection(
604       parent.toURL(),
605       sources.map(function(e) { return e.toURL(); }),
606       newName, function(success) {
607         if (!success) {
608           // Failed to create a zip archive.
609           errorCallback(
610               util.createFileError(FileError.INVALID_MODIFICATION_ERR));
611           return;
612         }
614         // Returns the created entry via callback.
615         parent.getFile(
616             newName, {create: false}, successCallback, errorCallback);
617       });
621  * @constructor
622  */
623 function FileOperationManager() {
624   this.copyTasks_ = [];
625   this.deleteTasks_ = [];
626   this.cancelObservers_ = [];
627   this.cancelRequested_ = false;
628   this.cancelCallback_ = null;
629   this.unloadTimeout_ = null;
631   this.eventRouter_ = new FileOperationManager.EventRouter();
635  * Get FileOperationManager instance. In case is hasn't been initialized, a new
636  * instance is created.
638  * @return {FileOperationManager} A FileOperationManager instance.
639  */
640 FileOperationManager.getInstance = function() {
641   if (!FileOperationManager.instance_)
642     FileOperationManager.instance_ = new FileOperationManager();
644   return FileOperationManager.instance_;
648  * Manages cr.Event dispatching.
649  * Currently this can send three types of events: "copy-progress",
650  * "copy-operation-completed" and "delete".
652  * TODO(hidehiko): Reorganize the event dispatching mechanism.
653  * @constructor
654  * @extends {cr.EventTarget}
655  */
656 FileOperationManager.EventRouter = function() {
660  * Extends cr.EventTarget.
661  */
662 FileOperationManager.EventRouter.prototype.__proto__ = cr.EventTarget.prototype;
665  * Dispatches a simple "copy-progress" event with reason and current
666  * FileOperationManager status. If it is an ERROR event, error should be set.
668  * @param {string} reason Event type. One of "BEGIN", "PROGRESS", "SUCCESS",
669  *     "ERROR" or "CANCELLED". TODO(hidehiko): Use enum.
670  * @param {Object} status Current FileOperationManager's status. See also
671  *     FileOperationManager.getStatus().
672  * @param {FileOperationManager.Error=} opt_error The info for the error. This
673  *     should be set iff the reason is "ERROR".
674  */
675 FileOperationManager.EventRouter.prototype.sendProgressEvent = function(
676     reason, status, opt_error) {
677   var event = new cr.Event('copy-progress');
678   event.reason = reason;
679   event.status = status;
680   if (opt_error)
681     event.error = opt_error;
682   this.dispatchEvent(event);
686  * Dispatches an event to notify that an entry is changed (created or deleted).
687  * @param {util.EntryChangedKind} kind The enum to represent if the entry is
688  *     created or deleted.
689  * @param {Entry} entry The changed entry.
690  */
691 FileOperationManager.EventRouter.prototype.sendEntryChangedEvent = function(
692     kind, entry) {
693   var event = new cr.Event('entry-changed');
694   event.kind = kind;
695   event.entry = entry;
696   this.dispatchEvent(event);
700  * Dispatches an event to notify entries are changed for delete task.
702  * @param {string} reason Event type. One of "BEGIN", "PROGRESS", "SUCCESS",
703  *     or "ERROR". TODO(hidehiko): Use enum.
704  * @param {Array.<string>} urls An array of URLs which are affected by delete
705  *     operation.
706  */
707 FileOperationManager.EventRouter.prototype.sendDeleteEvent = function(
708     reason, urls) {
709   var event = new cr.Event('delete');
710   event.reason = reason;
711   event.urls = urls;
712   this.dispatchEvent(event);
716  * A record of a queued copy operation.
718  * Multiple copy operations may be queued at any given time.  Additional
719  * Tasks may be added while the queue is being serviced.  Though a
720  * cancel operation cancels everything in the queue.
722  * @param {util.FileOperationType} operationType The type of this operation.
723  * @param {Array.<Entry>} sourceEntries Array of source entries.
724  * @param {DirectoryEntry} targetDirEntry Target directory.
725  * @constructor
726  */
727 FileOperationManager.Task = function(
728     operationType, sourceEntries, targetDirEntry) {
729   this.operationType = operationType;
730   this.sourceEntries = sourceEntries;
731   this.targetDirEntry = targetDirEntry;
733   /**
734    * An array of map from url to Entry being processed.
735    * @type {Array.<Object<string, Entry>>}
736    */
737   this.processingEntries = null;
739   /**
740    * Total number of bytes to be processed. Filled in initialize().
741    * @type {number}
742    */
743   this.totalBytes = 0;
745   /**
746    * Total number of already processed bytes. Updated periodically.
747    * @type {number}
748    */
749   this.processedBytes = 0;
751   this.deleteAfterCopy = false;
753   /**
754    * Set to true when cancel is requested.
755    * @private {boolean}
756    */
757   this.cancelRequested_ = false;
759   /**
760    * Callback to cancel the running process.
761    * @private {function()}
762    */
763   this.cancelCallback_ = null;
765   // TODO(hidehiko): After we support recursive copy, we don't need this.
766   // If directory already exists, we try to make a copy named 'dir (X)',
767   // where X is a number. When we do this, all subsequent copies from
768   // inside the subtree should be mapped to the new directory name.
769   // For example, if 'dir' was copied as 'dir (1)', then 'dir\file.txt' should
770   // become 'dir (1)\file.txt'.
771   this.renamedDirectories_ = [];
775  * @param {function()} callback When entries resolved.
776  */
777 FileOperationManager.Task.prototype.initialize = function(callback) {
781  * Updates copy progress status for the entry.
783  * @param {number} size Number of bytes that has been copied since last update.
784  */
785 FileOperationManager.Task.prototype.updateFileCopyProgress = function(size) {
786   this.completedBytes += size;
790  * Requests cancellation of this task.
791  * When the cancellation is done, it is notified via callbacks of run().
792  */
793 FileOperationManager.Task.prototype.requestCancel = function() {
794   this.cancelRequested_ = true;
795   if (this.cancelCallback_) {
796     this.cancelCallback_();
797     this.cancelCallback_ = null;
798   }
802  * Runs the task. Sub classes must implement this method.
804  * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback
805  *     Callback invoked when an entry is changed.
806  * @param {function()} progressCallback Callback invoked periodically during
807  *     the operation.
808  * @param {function()} successCallback Callback run on success.
809  * @param {function(FileOperationManager.Error)} errorCallback Callback run on
810  *     error.
811  */
812 FileOperationManager.Task.prototype.run = function(
813     entryChangedCallback, progressCallback, successCallback, errorCallback) {
817  * Task to copy entries.
819  * @param {Array.<Entry>} sourceEntries Array of source entries.
820  * @param {DirectoryEntry} targetDirEntry Target directory.
821  * @constructor
822  * @extends {FileOperationManager.Task}
823  */
824 FileOperationManager.CopyTask = function(sourceEntries, targetDirEntry) {
825   FileOperationManager.Task.call(
826       this, util.FileOperationType.COPY, sourceEntries, targetDirEntry);
830  * Extends FileOperationManager.Task.
831  */
832 FileOperationManager.CopyTask.prototype.__proto__ =
833     FileOperationManager.Task.prototype;
836  * Initializes the CopyTask.
837  * @param {function()} callback Called when the initialize is completed.
838  */
839 FileOperationManager.CopyTask.prototype.initialize = function(callback) {
840   var group = new AsyncUtil.Group();
841   // Correct all entries to be copied for status update.
842   this.processingEntries = [];
843   for (var i = 0; i < this.sourceEntries.length; i++) {
844     group.add(function(index, callback) {
845       fileOperationUtil.resolveRecursively(
846           this.sourceEntries[index],
847           function(resolvedEntries) {
848             var resolvedEntryMap = {};
849             for (var j = 0; j < resolvedEntries.length; ++j) {
850               var entry = resolvedEntries[j];
851               entry.processedBytes = 0;
852               resolvedEntryMap[entry.toURL()] = entry;
853             }
854             this.processingEntries[index] = resolvedEntryMap;
855             callback();
856           }.bind(this),
857           function(error) {
858             console.error(
859                 'Failed to resolve for copy: %s',
860                 util.getFileErrorMnemonic(error.code));
861           });
862     }.bind(this, i));
863   }
865   group.run(function() {
866     // Fill totalBytes.
867     this.totalBytes = 0;
868     for (var i = 0; i < this.processingEntries.length; i++) {
869       for (var url in this.processingEntries[i])
870         this.totalBytes += this.processingEntries[i][url].size;
871     }
873     callback();
874   }.bind(this));
878  * Copies all entries to the target directory.
879  * Note: this method contains also the operation of "Move" due to historical
880  * reason.
882  * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback
883  *     Callback invoked when an entry is changed.
884  * @param {function()} progressCallback Callback invoked periodically during
885  *     the copying.
886  * @param {function()} successCallback On success.
887  * @param {function(FileOperationManager.Error)} errorCallback On error.
888  * @override
889  */
890 FileOperationManager.CopyTask.prototype.run = function(
891     entryChangedCallback, progressCallback, successCallback, errorCallback) {
892   // TODO(hidehiko): We should be able to share the code to iterate on entries
893   // with serviceMoveTask_().
894   if (this.sourceEntries.length == 0) {
895     successCallback();
896     return;
897   }
899   // TODO(hidehiko): Delete after copy is the implementation of Move.
900   // Migrate the part into MoveTask.run().
901   var deleteOriginals = function() {
902     var count = this.sourceEntries.length;
904     var onEntryDeleted = function(entry) {
905       entryChangedCallback(util.EntryChangedKind.DELETED, entry);
906       count--;
907       if (!count)
908         successCallback();
909     };
911     var onFilesystemError = function(err) {
912       errorCallback(new FileOperationManager.Error(
913           util.FileOperationErrorType.FILESYSTEM_ERROR, err));
914     };
916     for (var i = 0; i < this.sourceEntries.length; i++) {
917       var entry = this.sourceEntries[i];
918       util.removeFileOrDirectory(
919           entry, onEntryDeleted.bind(null, entry), onFilesystemError);
920     }
921   }.bind(this);
923   AsyncUtil.forEach(
924       this.sourceEntries,
925       function(callback, entry, index) {
926         if (this.cancelRequested_) {
927           errorCallback(new FileOperationManager.Error(
928               util.FileOperationErrorType.FILESYSTEM_ERROR,
929               util.createFileError(FileError.ABORT_ERR)));
930           return;
931         }
932         progressCallback();
933         this.cancelCallback_ = FileOperationManager.CopyTask.processEntry_(
934             entry, this.targetDirEntry,
935             function(sourceUrl, destinationUrl) {
936               // Finalize the entry's progress state.
937               var entry = this.processingEntries[index][sourceUrl];
938               if (entry) {
939                 this.processedBytes += entry.size - entry.processedBytes;
940                 progressCallback();
941                 delete this.processingEntries[index][sourceUrl];
942               }
944               webkitResolveLocalFileSystemURL(
945                   destinationUrl, function(destinationEntry) {
946                     entryChangedCallback(
947                         util.EntryChangedKind.CREATED, destinationEntry);
948                   });
949             }.bind(this),
950             function(source_url, size) {
951               var entry = this.processingEntries[index][source_url];
952               if (entry) {
953                 this.processedBytes += size - entry.processedBytes;
954                 entry.processedBytes = size;
955                 progressCallback();
956               }
957             }.bind(this),
958             function() {
959               this.cancelCallback_ = null;
960               callback();
961             }.bind(this),
962             function(error) {
963               this.cancelCallback_ = null;
964               errorCallback(error);
965             }.bind(this));
966       },
967       function() {
968         if (this.deleteAfterCopy) {
969           deleteOriginals();
970         } else {
971           successCallback();
972         }
973       }.bind(this),
974       this);
978  * Copies the source entry to the target directory.
980  * @param {Entry} sourceEntry An entry to be copied.
981  * @param {DirectoryEntry} destinationEntry The entry which will contain the
982  *     copied entry.
983  * @param {function(string, string)} entryChangedCallback
984  *     Callback invoked when an entry is created with the source url and
985  *     the destination url.
986  * @param {function(string, number)} progressCallback Callback invoked
987  *     periodically during the copying.
988  * @param {function()} successCallback On success.
989  * @param {function(FileOperationManager.Error)} errorCallback On error.
990  * @return {function()} Callback to cancel the current file copy operation.
991  *     When the cancel is done, errorCallback will be called. The returned
992  *     callback must not be called more than once.
993  * @private
994  */
995 FileOperationManager.CopyTask.processEntry_ = function(
996     sourceEntry, destinationEntry, entryChangedCallback, progressCallback,
997     successCallback, errorCallback) {
998   var cancelRequested = false;
999   var cancelCallback = null;
1000   fileOperationUtil.deduplicatePath(
1001       destinationEntry, sourceEntry.name,
1002       function(destinationName) {
1003         if (cancelRequested) {
1004           errorCallback(new FileOperationManager.Error(
1005               util.FileOperationErrorType.FILESYSTEM_ERROR,
1006               util.createFileError(FileError.ABORT_ERR)));
1007           return;
1008         }
1010         cancelCallback = fileOperationUtil.copyTo(
1011             sourceEntry, destinationEntry, destinationName,
1012             entryChangedCallback, progressCallback,
1013             function(entry) {
1014               cancelCallback = null;
1015               successCallback();
1016             },
1017             function(error) {
1018               cancelCallback = null;
1019               errorCallback(new FileOperationManager.Error(
1020                   util.FileOperationErrorType.FILESYSTEM_ERROR, error));
1021             });
1022       },
1023       errorCallback);
1025   return function() {
1026     cancelRequested = true;
1027     if (cancelCallback) {
1028       cancelCallback();
1029       cancelCallback = null;
1030     }
1031   };
1035  * Task to move entries.
1037  * @param {Array.<Entry>} sourceEntries Array of source entries.
1038  * @param {DirectoryEntry} targetDirEntry Target directory.
1039  * @constructor
1040  * @extends {FileOperationManager.Task}
1041  */
1042 FileOperationManager.MoveTask = function(sourceEntries, targetDirEntry) {
1043   FileOperationManager.Task.call(
1044       this, util.FileOperationType.MOVE, sourceEntries, targetDirEntry);
1048  * Extends FileOperationManager.Task.
1049  */
1050 FileOperationManager.MoveTask.prototype.__proto__ =
1051     FileOperationManager.Task.prototype;
1054  * Initializes the MoveTask.
1055  * @param {function()} callback Called when the initialize is completed.
1056  */
1057 FileOperationManager.MoveTask.prototype.initialize = function(callback) {
1058   // This may be moving from search results, where it fails if we
1059   // move parent entries earlier than child entries. We should
1060   // process the deepest entry first. Since move of each entry is
1061   // done by a single moveTo() call, we don't need to care about the
1062   // recursive traversal order.
1063   this.sourceEntries.sort(function(entry1, entry2) {
1064     return entry2.fullPath.length - entry1.fullPath.length;
1065   });
1067   this.processingEntries = [];
1068   for (var i = 0; i < this.sourceEntries.length; i++) {
1069     var processingEntryMap = {};
1070     var entry = this.sourceEntries[i];
1072     // The move should be done with updating the metadata. So here we assume
1073     // all the file size is 1 byte. (Avoiding 0, so that progress bar can
1074     // move smoothly).
1075     // TODO(hidehiko): Remove this hack.
1076     entry.size = 1;
1077     processingEntryMap[entry.toURL()] = entry;
1078     this.processingEntries[i] = processingEntryMap;
1079   }
1081   callback();
1085  * Moves all entries in the task.
1087  * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback
1088  *     Callback invoked when an entry is changed.
1089  * @param {function()} progressCallback Callback invoked periodically during
1090  *     the moving.
1091  * @param {function()} successCallback On success.
1092  * @param {function(FileOperationManager.Error)} errorCallback On error.
1093  * @override
1094  */
1095 FileOperationManager.MoveTask.prototype.run = function(
1096     entryChangedCallback, progressCallback, successCallback, errorCallback) {
1097   if (this.sourceEntries.length == 0) {
1098     successCallback();
1099     return;
1100   }
1102   AsyncUtil.forEach(
1103       this.sourceEntries,
1104       function(callback, entry, index) {
1105         if (this.cancelRequested_) {
1106           errorCallback(new FileOperationManager.Error(
1107               util.FileOperationErrorType.FILESYSTEM_ERROR,
1108               util.createFileError(FileError.ABORT_ERR)));
1109           return;
1110         }
1111         progressCallback();
1112         FileOperationManager.MoveTask.processEntry_(
1113             entry, this.targetDirEntry, entryChangedCallback,
1114             function() {
1115               // Erase the processing entry.
1116               this.processingEntries[index] = {};
1117               this.processedBytes++;
1118               callback();
1119             }.bind(this),
1120             errorCallback);
1121       },
1122       function() {
1123         successCallback();
1124       }.bind(this),
1125       this);
1129  * Moves the sourceEntry to the targetDirEntry in this task.
1131  * @param {Entry} sourceEntry An entry to be moved.
1132  * @param {DirectoryEntry} destinationEntry The entry of the destination
1133  *     directory.
1134  * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback
1135  *     Callback invoked when an entry is changed.
1136  * @param {function()} successCallback On success.
1137  * @param {function(FileOperationManager.Error)} errorCallback On error.
1138  * @private
1139  */
1140 FileOperationManager.MoveTask.processEntry_ = function(
1141     sourceEntry, destinationEntry, entryChangedCallback, successCallback,
1142     errorCallback) {
1143   fileOperationUtil.deduplicatePath(
1144       destinationEntry,
1145       sourceEntry.name,
1146       function(destinationName) {
1147         sourceEntry.moveTo(
1148             destinationEntry, destinationName,
1149             function(movedEntry) {
1150               entryChangedCallback(util.EntryChangedKind.CREATED, movedEntry);
1151               entryChangedCallback(util.EntryChangedKind.DELETED, sourceEntry);
1152               successCallback();
1153             },
1154             function(error) {
1155               errorCallback(new FileOperationManager.Error(
1156                   util.FileOperationErrorType.FILESYSTEM_ERROR, error));
1157             });
1158       },
1159       errorCallback);
1163  * Task to create a zip archive.
1165  * @param {Array.<Entry>} sourceEntries Array of source entries.
1166  * @param {DirectoryEntry} targetDirEntry Target directory.
1167  * @param {DirectoryEntry} zipBaseDirEntry Base directory dealt as a root
1168  *     in ZIP archive.
1169  * @constructor
1170  * @extends {FileOperationManager.Task}
1171  */
1172 FileOperationManager.ZipTask = function(
1173     sourceEntries, targetDirEntry, zipBaseDirEntry) {
1174   FileOperationManager.Task.call(
1175       this, util.FileOperationType.ZIP, sourceEntries, targetDirEntry);
1176   this.zipBaseDirEntry = zipBaseDirEntry;
1180  * Extends FileOperationManager.Task.
1181  */
1182 FileOperationManager.ZipTask.prototype.__proto__ =
1183     FileOperationManager.Task.prototype;
1187  * Initializes the ZipTask.
1188  * @param {function()} callback Called when the initialize is completed.
1189  */
1190 FileOperationManager.ZipTask.prototype.initialize = function(callback) {
1191   var resolvedEntryMap = {};
1192   var group = new AsyncUtil.Group();
1193   for (var i = 0; i < this.sourceEntries.length; i++) {
1194     group.add(function(index, callback) {
1195       fileOperationUtil.resolveRecursively(
1196           this.sourceEntries[index],
1197           function(entries) {
1198             for (var j = 0; j < entries.length; j++)
1199               resolvedEntryMap[entries[j].toURL()] = entries[j];
1200             callback();
1201           },
1202           function(error) {});
1203     }.bind(this, i));
1204   }
1206   group.run(function() {
1207     // For zip archiving, all the entries are processed at once.
1208     this.processingEntries = [resolvedEntryMap];
1210     this.totalBytes = 0;
1211     for (var url in resolvedEntryMap)
1212       this.totalBytes += resolvedEntryMap[url].size;
1214     callback();
1215   }.bind(this));
1219  * Runs a zip file creation task.
1221  * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback
1222  *     Callback invoked when an entry is changed.
1223  * @param {function()} progressCallback Callback invoked periodically during
1224  *     the moving.
1225  * @param {function()} successCallback On complete.
1226  * @param {function(FileOperationManager.Error)} errorCallback On error.
1227  * @override
1228  */
1229 FileOperationManager.ZipTask.prototype.run = function(
1230     entryChangedCallback, progressCallback, successCallback, errorCallback) {
1231   // TODO(hidehiko): we should localize the name.
1232   var destName = 'Archive';
1233   if (this.sourceEntries.length == 1) {
1234     var entryPath = this.sourceEntries[0].fullPath;
1235     var i = entryPath.lastIndexOf('/');
1236     var basename = (i < 0) ? entryPath : entryPath.substr(i + 1);
1237     i = basename.lastIndexOf('.');
1238     destName = ((i < 0) ? basename : basename.substr(0, i));
1239   }
1241   fileOperationUtil.deduplicatePath(
1242       this.targetDirEntry, destName + '.zip',
1243       function(destPath) {
1244         // TODO: per-entry zip progress update with accurate byte count.
1245         // For now just set completedBytes to same value as totalBytes so
1246         // that the progress bar is full.
1247         this.processedBytes = this.totalBytes;
1248         progressCallback();
1250         // The number of elements in processingEntries is 1. See also
1251         // initialize().
1252         var entries = [];
1253         for (var url in this.processingEntries[0])
1254           entries.push(this.processingEntries[0][url]);
1256         fileOperationUtil.zipSelection(
1257             entries,
1258             this.zipBaseDirEntry,
1259             destPath,
1260             function(entry) {
1261               entryChangedCallback(util.EntryChangedKind.CREATE, entry);
1262               successCallback();
1263             },
1264             function(error) {
1265               errorCallback(new FileOperationManager.Error(
1266                   util.FileOperationErrorType.FILESYSTEM_ERROR, error));
1267             });
1268       }.bind(this),
1269       errorCallback);
1273  * Error class used to report problems with a copy operation.
1274  * If the code is UNEXPECTED_SOURCE_FILE, data should be a path of the file.
1275  * If the code is TARGET_EXISTS, data should be the existing Entry.
1276  * If the code is FILESYSTEM_ERROR, data should be the FileError.
1278  * @param {util.FileOperationErrorType} code Error type.
1279  * @param {string|Entry|FileError} data Additional data.
1280  * @constructor
1281  */
1282 FileOperationManager.Error = function(code, data) {
1283   this.code = code;
1284   this.data = data;
1287 // FileOperationManager methods.
1290  * Called before a new method is run in the manager. Prepares the manager's
1291  * state for running a new method.
1292  */
1293 FileOperationManager.prototype.willRunNewMethod = function() {
1294   // Cancel any pending close actions so the file copy manager doesn't go away.
1295   if (this.unloadTimeout_)
1296     clearTimeout(this.unloadTimeout_);
1297   this.unloadTimeout_ = null;
1301  * @return {Object} Status object.
1302  */
1303 FileOperationManager.prototype.getStatus = function() {
1304   // TODO(hidehiko): Reorganize the structure when delete queue is merged
1305   // into copy task queue.
1306   var result = {
1307     // Set to util.FileOperationType if all the running/pending tasks is
1308     // the same kind of task.
1309     operationType: null,
1311     // The number of entries to be processed.
1312     numRemainingItems: 0,
1314     // The total number of bytes to be processed.
1315     totalBytes: 0,
1317     // The number of bytes.
1318     processedBytes: 0,
1320     // Available if numRemainingItems == 1. Pointing to an Entry which is
1321     // begin processed.
1322     processingEntry: null,
1323   };
1325   var operationType =
1326       this.copyTasks_.length > 0 ? this.copyTasks_[0].operationType : null;
1327   var processingEntry = null;
1328   for (var i = 0; i < this.copyTasks_.length; i++) {
1329     var task = this.copyTasks_[i];
1330     if (task.operationType != operationType)
1331       operationType = null;
1333     // Assuming the number of entries is small enough, count everytime.
1334     for (var j = 0; j < task.processingEntries.length; j++) {
1335       for (var url in task.processingEntries[j]) {
1336         ++result.numRemainingItems;
1337         processingEntry = task.processingEntries[j][url];
1338       }
1339     }
1341     result.totalBytes += task.totalBytes;
1342     result.processedBytes += task.processedBytes;
1343   }
1345   result.operationType = operationType;
1347   if (result.numRemainingItems == 1)
1348     result.processingEntry = processingEntry;
1350   return result;
1354  * Adds an event listener for the tasks.
1355  * @param {string} type The name of the event.
1356  * @param {function(cr.Event)} handler The handler for the event.
1357  *     This is called when the event is dispatched.
1358  */
1359 FileOperationManager.prototype.addEventListener = function(type, handler) {
1360   this.eventRouter_.addEventListener(type, handler);
1364  * Removes an event listener for the tasks.
1365  * @param {string} type The name of the event.
1366  * @param {function(cr.Event)} handler The handler to be removed.
1367  */
1368 FileOperationManager.prototype.removeEventListener = function(type, handler) {
1369   this.eventRouter_.removeEventListener(type, handler);
1373  * Says if there are any tasks in the queue.
1374  * @return {boolean} True, if there are any tasks.
1375  */
1376 FileOperationManager.prototype.hasQueuedTasks = function() {
1377   return this.copyTasks_.length > 0 || this.deleteTasks_.length > 0;
1381  * Unloads the host page in 5 secs of idleing. Need to be called
1382  * each time this.copyTasks_.length or this.deleteTasks_.length
1383  * changed.
1385  * @private
1386  */
1387 FileOperationManager.prototype.maybeScheduleCloseBackgroundPage_ = function() {
1388   if (!this.hasQueuedTasks()) {
1389     if (this.unloadTimeout_ === null)
1390       this.unloadTimeout_ = setTimeout(maybeCloseBackgroundPage, 5000);
1391   } else if (this.unloadTimeout_) {
1392     clearTimeout(this.unloadTimeout_);
1393     this.unloadTimeout_ = null;
1394   }
1398  * Completely clear out the copy queue, either because we encountered an error
1399  * or completed successfully.
1401  * @private
1402  */
1403 FileOperationManager.prototype.resetQueue_ = function() {
1404   for (var i = 0; i < this.cancelObservers_.length; i++)
1405     this.cancelObservers_[i]();
1407   this.copyTasks_ = [];
1408   this.cancelObservers_ = [];
1409   this.maybeScheduleCloseBackgroundPage_();
1413  * Request that the current copy queue be abandoned.
1415  * @param {function()=} opt_callback On cancel.
1416  */
1417 FileOperationManager.prototype.requestCancel = function(opt_callback) {
1418   this.cancelRequested_ = true;
1419   if (this.cancelCallback_) {
1420     this.cancelCallback_();
1421     this.cancelCallback_ = null;
1422   }
1423   if (opt_callback)
1424     this.cancelObservers_.push(opt_callback);
1426   // If there is any active task it will eventually call maybeCancel_.
1427   // Otherwise call it right now.
1428   if (this.copyTasks_.length == 0)
1429     this.doCancel_();
1430   else
1431     this.copyTasks_[0].requestCancel();
1435  * Perform the bookkeeping required to cancel.
1437  * @private
1438  */
1439 FileOperationManager.prototype.doCancel_ = function() {
1440   this.resetQueue_();
1441   this.cancelRequested_ = false;
1442   this.eventRouter_.sendProgressEvent('CANCELLED', this.getStatus());
1446  * Used internally to check if a cancel has been requested, and handle
1447  * it if so.
1449  * @return {boolean} If canceled.
1450  * @private
1451  */
1452 FileOperationManager.prototype.maybeCancel_ = function() {
1453   if (!this.cancelRequested_)
1454     return false;
1456   this.doCancel_();
1457   return true;
1461  * Kick off pasting.
1463  * @param {Array.<string>} sourcePaths Path of the source files.
1464  * @param {string} targetPath The destination path of the target directory.
1465  * @param {boolean} isMove True if the operation is "move", otherwise (i.e.
1466  *     if the operation is "copy") false.
1467  */
1468 FileOperationManager.prototype.paste = function(
1469     sourcePaths, targetPath, isMove) {
1470   // Do nothing if sourcePaths is empty.
1471   if (sourcePaths.length == 0)
1472     return;
1474   var errorCallback = function(error) {
1475     this.eventRouter_.sendProgressEvent(
1476         'ERROR',
1477         this.getStatus(),
1478         new FileOperationManager.Error(
1479             util.FileOperationErrorType.FILESYSTEM_ERROR, error));
1480   }.bind(this);
1482   var targetEntry = null;
1483   var entries = [];
1485   // Resolve paths to entries.
1486   var resolveGroup = new AsyncUtil.Group();
1487   resolveGroup.add(function(callback) {
1488     webkitResolveLocalFileSystemURL(
1489         util.makeFilesystemUrl(targetPath),
1490         function(entry) {
1491           if (!entry.isDirectory) {
1492             // Found a non directory entry.
1493             errorCallback(util.createFileError(FileError.TYPE_MISMATCH_ERR));
1494             return;
1495           }
1497           targetEntry = entry;
1498           callback();
1499         },
1500         errorCallback);
1501   });
1503   for (var i = 0; i < sourcePaths.length; i++) {
1504     resolveGroup.add(function(sourcePath, callback) {
1505       webkitResolveLocalFileSystemURL(
1506           util.makeFilesystemUrl(sourcePath),
1507           function(entry) {
1508             entries.push(entry);
1509             callback();
1510           },
1511           errorCallback);
1512     }.bind(this, sourcePaths[i]));
1513   }
1515   resolveGroup.run(function() {
1516     if (isMove) {
1517       // Moving to the same directory is a redundant operation.
1518       entries = entries.filter(function(entry) {
1519         return targetEntry.fullPath + '/' + entry.name != entry.fullPath;
1520       });
1522       // Do nothing, if we have no entries to be moved.
1523       if (entries.length == 0)
1524         return;
1525     }
1527     this.queueCopy_(targetEntry, entries, isMove);
1528   }.bind(this));
1532  * Checks if the move operation is avaiable between the given two locations.
1534  * @param {DirectoryEntry} sourceEntry An entry from the source.
1535  * @param {DirectoryEntry} targetDirEntry Directory entry for the target.
1536  * @return {boolean} Whether we can move from the source to the target.
1537  */
1538 FileOperationManager.prototype.isMovable = function(sourceEntry,
1539                                                targetDirEntry) {
1540   return (PathUtil.isDriveBasedPath(sourceEntry.fullPath) &&
1541           PathUtil.isDriveBasedPath(targetDirEntry.fullPath)) ||
1542          (PathUtil.getRootPath(sourceEntry.fullPath) ==
1543           PathUtil.getRootPath(targetDirEntry.fullPath));
1547  * Initiate a file copy.
1549  * @param {DirectoryEntry} targetDirEntry Target directory.
1550  * @param {Array.<Entry>} entries Entries to copy.
1551  * @param {boolean} isMove In case of move.
1552  * @return {FileOperationManager.Task} Copy task.
1553  * @private
1554  */
1555 FileOperationManager.prototype.queueCopy_ = function(
1556     targetDirEntry, entries, isMove) {
1557   // When copying files, null can be specified as source directory.
1558   var task;
1559   if (isMove) {
1560     if (this.isMovable(entries[0], targetDirEntry)) {
1561       task = new FileOperationManager.MoveTask(entries, targetDirEntry);
1562     } else {
1563       task = new FileOperationManager.CopyTask(entries, targetDirEntry);
1564       task.deleteAfterCopy = true;
1565     }
1566   } else {
1567     task = new FileOperationManager.CopyTask(entries, targetDirEntry);
1568   }
1570   task.initialize(function() {
1571     this.copyTasks_.push(task);
1572     this.maybeScheduleCloseBackgroundPage_();
1573     if (this.copyTasks_.length == 1) {
1574       // Assume this.cancelRequested_ == false.
1575       // This moved us from 0 to 1 active tasks, let the servicing begin!
1576       this.serviceAllTasks_();
1577     } else {
1578       // Force to update the progress of butter bar when there are new tasks
1579       // coming while servicing current task.
1580       this.eventRouter_.sendProgressEvent('PROGRESS', this.getStatus());
1581     }
1582   }.bind(this));
1584   return task;
1588  * Service all pending tasks, as well as any that might appear during the
1589  * copy.
1591  * @private
1592  */
1593 FileOperationManager.prototype.serviceAllTasks_ = function() {
1594   var self = this;
1596   var onTaskProgress = function() {
1597     self.eventRouter_.sendProgressEvent('PROGRESS', self.getStatus());
1598   };
1600   var onEntryChanged = function(kind, entry) {
1601     self.eventRouter_.sendEntryChangedEvent(kind, entry);
1602   };
1604   var onTaskError = function(err) {
1605     if (self.maybeCancel_())
1606       return;
1607     self.eventRouter_.sendProgressEvent('ERROR', self.getStatus(), err);
1608     self.resetQueue_();
1609   };
1611   var onTaskSuccess = function() {
1612     if (self.maybeCancel_())
1613       return;
1615     // The task at the front of the queue is completed. Pop it from the queue.
1616     self.copyTasks_.shift();
1617     self.maybeScheduleCloseBackgroundPage_();
1619     if (!self.copyTasks_.length) {
1620       // All tasks have been serviced, clean up and exit.
1621       self.eventRouter_.sendProgressEvent('SUCCESS', self.getStatus());
1622       self.resetQueue_();
1623       return;
1624     }
1626     // We want to dispatch a PROGRESS event when there are more tasks to serve
1627     // right after one task finished in the queue. We treat all tasks as one
1628     // big task logically, so there is only one BEGIN/SUCCESS event pair for
1629     // these continuous tasks.
1630     self.eventRouter_.sendProgressEvent('PROGRESS', self.getStatus());
1631     self.copyTasks_[0].run(
1632         onEntryChanged, onTaskProgress, onTaskSuccess, onTaskError);
1633   };
1635   // If the queue size is 1 after pushing our task, it was empty before,
1636   // so we need to kick off queue processing and dispatch BEGIN event.
1637   this.eventRouter_.sendProgressEvent('BEGIN', this.getStatus());
1638   this.copyTasks_[0].run(
1639       onEntryChanged, onTaskProgress, onTaskSuccess, onTaskError);
1643  * Timeout before files are really deleted (to allow undo).
1644  */
1645 FileOperationManager.DELETE_TIMEOUT = 30 * 1000;
1648  * Schedules the files deletion.
1650  * @param {Array.<Entry>} entries The entries.
1651  */
1652 FileOperationManager.prototype.deleteEntries = function(entries) {
1653   var task = { entries: entries };
1654   this.deleteTasks_.push(task);
1655   this.maybeScheduleCloseBackgroundPage_();
1656   if (this.deleteTasks_.length == 1)
1657     this.serviceAllDeleteTasks_();
1661  * Service all pending delete tasks, as well as any that might appear during the
1662  * deletion.
1664  * Must not be called if there is an in-flight delete task.
1666  * @private
1667  */
1668 FileOperationManager.prototype.serviceAllDeleteTasks_ = function() {
1669   // Returns the urls of the given task's entries.
1670   var getTaskUrls = function(task) {
1671     return task.entries.map(function(entry) {
1672       return util.makeFilesystemUrl(entry.fullPath);
1673     });
1674   };
1676   var onTaskSuccess = function() {
1677     var urls = getTaskUrls(this.deleteTasks_.shift());
1678     if (!this.deleteTasks_.length) {
1679       // All tasks have been serviced, clean up and exit.
1680       this.eventRouter_.sendDeleteEvent('SUCCESS', urls);
1681       this.maybeScheduleCloseBackgroundPage_();
1682       return;
1683     }
1685     // We want to dispatch a PROGRESS event when there are more tasks to serve
1686     // right after one task finished in the queue. We treat all tasks as one
1687     // big task logically, so there is only one BEGIN/SUCCESS event pair for
1688     // these continuous tasks.
1689     this.eventRouter_.sendDeleteEvent('PROGRESS', urls);
1691     this.serviceDeleteTask_(this.deleteTasks_[0], onTaskSuccess, onTaskFailure);
1692   }.bind(this);
1694   var onTaskFailure = function(error) {
1695     var urls = getTaskUrls(this.deleteTasks_[0]);
1696     this.deleteTasks_ = [];
1697     this.eventRouter_.sendDeleteEvent('ERROR', urls);
1698     this.maybeScheduleCloseBackgroundPage_();
1699   }.bind(this);
1701   // If the queue size is 1 after pushing our task, it was empty before,
1702   // so we need to kick off queue processing and dispatch BEGIN event.
1703   this.eventRouter_.sendDeleteEvent('BEGIN', getTaskUrls(this.deleteTasks_[0]));
1704   this.serviceDeleteTask_(this.deleteTasks_[0], onTaskSuccess, onTaskFailure);
1708  * Performs the deletion.
1710  * @param {Object} task The delete task (see deleteEntries function).
1711  * @param {function()} successCallback Callback run on success.
1712  * @param {function(FileOperationManager.Error)} errorCallback Callback run on
1713  *     error.
1714  * @private
1715  */
1716 FileOperationManager.prototype.serviceDeleteTask_ = function(
1717     task, successCallback, errorCallback) {
1718   var downcount = task.entries.length;
1719   if (downcount == 0) {
1720     successCallback();
1721     return;
1722   }
1724   var filesystemError = null;
1725   var onComplete = function() {
1726     if (--downcount > 0)
1727       return;
1729     // All remove operations are processed. Run callback.
1730     if (filesystemError) {
1731       errorCallback(new FileOperationManager.Error(
1732           util.FileOperationErrorType.FILESYSTEM_ERROR, filesystemError));
1733     } else {
1734       successCallback();
1735     }
1736   };
1738   for (var i = 0; i < task.entries.length; i++) {
1739     var entry = task.entries[i];
1740     util.removeFileOrDirectory(
1741         entry,
1742         function(currentEntry) {
1743           this.eventRouter_.sendEntryChangedEvent(
1744               util.EntryChangedKind.DELETED, currentEntry);
1745           onComplete();
1746         }.bind(this, entry),
1747         function(error) {
1748           if (!filesystemError)
1749             filesystemError = error;
1750           onComplete();
1751         });
1752   }
1756  * Creates a zip file for the selection of files.
1758  * @param {Entry} dirEntry The directory containing the selection.
1759  * @param {Array.<Entry>} selectionEntries The selected entries.
1760  */
1761 FileOperationManager.prototype.zipSelection = function(
1762     dirEntry, selectionEntries) {
1763   var self = this;
1764   var zipTask = new FileOperationManager.ZipTask(
1765       selectionEntries, dirEntry, dirEntry);
1766   zipTask.zip = true;
1767   zipTask.initialize(function() {
1768     self.copyTasks_.push(zipTask);
1769     if (self.copyTasks_.length == 1) {
1770       // Assume self.cancelRequested_ == false.
1771       // This moved us from 0 to 1 active tasks, let the servicing begin!
1772       self.serviceAllTasks_();
1773     } else {
1774       // Force to update the progress of butter bar when there are new tasks
1775       // coming while servicing current task.
1776       self.eventRouter_.sendProgressEvent('PROGRESS', self.getStatus());
1777     }
1778   });