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