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