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.
8 * Utilities for FileOperationManager.
10 var fileOperationUtil = {};
13 * Simple wrapper for util.deduplicatePath. On error, this method translates
14 * the FileError to FileOperationManager.Error object.
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
20 * @param {function(FileOperationManager.Error)} errorCallback Callback run on
23 fileOperationUtil.deduplicatePath = function(
24 dirEntry, relativePath, successCallback, errorCallback) {
26 dirEntry, relativePath, successCallback,
28 var onFileSystemError = function(error) {
29 errorCallback(new FileOperationManager.Error(
30 util.FileOperationErrorType.FILESYSTEM_ERROR, error));
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.
37 dirEntry, relativePath,
39 errorCallback(new FileOperationManager.Error(
40 util.FileOperationErrorType.TARGET_EXISTS, entry));
45 onFileSystemError(err);
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.
54 * @param {Entry} entry The root Entry for traversing.
55 * @param {function(Array.<Entry>)} successCallback Called when the traverse
56 * is successfully done with the array of the entries.
57 * @param {function(FileError)} errorCallback Called on error with the first
58 * occured error (i.e. following errors will just be discarded).
60 fileOperationUtil.resolveRecursively = function(
61 entry, successCallback, errorCallback) {
64 var numRunningTasks = 0;
66 var maybeInvokeCallback = function() {
67 // If there still remain some running tasks, wait their finishing.
68 if (numRunningTasks > 0)
74 successCallback(result);
77 // The error handling can be shared.
78 var onError = function(fileError) {
79 // If this is the first error, remember it.
83 maybeInvokeCallback();
86 var process = function(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.
95 // Recursively traverse children.
96 var reader = entry.createReader();
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.
103 maybeInvokeCallback();
107 for (var i = 0; i < subEntries.length; i++)
108 process(subEntries[i]);
110 // Continue to read remaining children.
111 reader.readEntries(processSubEntries, onError);
115 // For a file, annotate the file size.
116 entry.getMetadata(function(metadata) {
117 entry.size = metadata.size;
119 maybeInvokeCallback();
128 * Sets last modified date to the entry.
129 * @param {Entry} entry The entry to which the last modified is set.
130 * @param {Date} modificationTime The last modified time.
132 fileOperationUtil.setLastModified = function(entry, modificationTime) {
133 chrome.fileBrowserPrivate.setLastModified(
134 entry.toURL(), '' + Math.round(modificationTime.getTime() / 1000));
138 * CAUTION: THIS IS STILL UNDER DEVELOPMENT. DO NOT USE.
139 * This will replace copyRecursively defined below.
141 * Copies source to parent with the name newName recursively.
142 * This should work very similar to FileSystem API's copyTo. The difference is;
143 * - The progress callback is supported.
144 * - The cancellation is supported.
146 * @param {Entry} source The entry to be copied.
147 * @param {DirectoryEntry} parent The entry of the destination directory.
148 * @param {string} newName The name of copied file.
149 * @param {function(string, string)} entryChangedCallback
150 * Callback invoked when an entry is created with the source url and
151 * the destination url.
152 * @param {function(string, number)} progressCallback Callback invoked
153 * periodically during the copying. It takes the source url and the
154 * processed bytes of it.
155 * @param {function(string)} successCallback Callback invoked when the copy
156 * is successfully done with the url of the created entry.
157 * @param {function(FileError)} errorCallback Callback invoked when an error
159 * @return {function()} Callback to cancel the current file copy operation.
160 * When the cancel is done, errorCallback will be called. The returned
161 * callback must not be called more than once.
163 fileOperationUtil.copyTo = function(
164 source, parent, newName, entryChangedCallback, progressCallback,
165 successCallback, errorCallback) {
167 var pendingCallbacks = [];
169 var onCopyProgress = function(progressCopyId, status) {
170 if (copyId == null) {
171 // If the copyId is not yet available, wait for it.
172 pendingCallbacks.push(
173 onCopyProgress.bind(null, progressCopyId, status));
177 // This is not what we're interested in.
178 if (progressCopyId != copyId)
181 switch (status.type) {
182 case 'begin_copy_entry':
185 case 'end_copy_entry':
186 entryChangedCallback(status.sourceUrl, status.destinationUrl);
190 progressCallback(status.sourceUrl, status.size);
194 chrome.fileBrowserPrivate.onCopyProgress.removeListener(onCopyProgress);
195 successCallback(status.destinationUrl);
199 chrome.fileBrowserPrivate.onCopyProgress.removeListener(onCopyProgress);
200 errorCallback(util.createFileError(status.error));
204 // Found unknown state. Cancel the task, and return an error.
205 console.error('Unknown progress type: ' + status.type);
206 chrome.fileBrowserPrivate.onCopyProgress.removeListener(onCopyProgress);
207 chrome.fileBrowserPrivate.cancelCopy(copyId);
208 errorCallback(util.createFileError(FileError.INVALID_STATE_ERR));
212 // Register the listener before calling startCopy. Otherwise some events
214 chrome.fileBrowserPrivate.onCopyProgress.addListener(onCopyProgress);
216 // Then starts the copy.
217 chrome.fileBrowserPrivate.startCopy(
218 source.toURL(), parent.toURL(), newName, function(startCopyId) {
219 // last error contains the FileError code on error.
220 if (chrome.runtime.lastError) {
221 // Unsubscribe the progress listener.
222 chrome.fileBrowserPrivate.onCopyProgress.removeListener(
224 errorCallback(util.createFileError(
225 Integer.parseInt(chrome.runtime.lastError, 10)));
229 copyId = startCopyId;
230 for (var i = 0; i < pendingCallbacks.length; i++) {
231 pendingCallbacks[i]();
236 // If copyId is not yet available, wait for it.
237 if (copyId == null) {
238 pendingCallbacks.push(function() {
239 chrome.fileBrowserPrivate.cancelCopy(copyId);
244 chrome.fileBrowserPrivate.cancelCopy(copyId);
249 * DEPRECATED: This method is no longer used.
250 * TODO(hidehiko): Remove this.
251 * Copies source to parent with the name newName recursively.
253 * @param {Entry} source The entry to be copied.
254 * @param {DirectoryEntry} parent The entry of the destination directory.
255 * @param {string} newName The name of copied file.
256 * @param {function(string, string)} entryChangedCallback
257 * Callback invoked when an entry is created with the source url and
258 * the destination url.
259 * @param {function(string, number)} progressCallback Callback invoked
260 * periodically during the copying. It takes the source url and the
261 * processed bytes of it.
262 * @param {function(string)} successCallback Callback invoked when the copy
263 * is successfully done with the url of the created entry.
264 * @param {function(FileError)} errorCallback Callback invoked when an error
266 * @return {function()} Callback to cancel the current file copy operation.
267 * When the cancel is done, errorCallback will be called. The returned
268 * callback must not be called more than once.
270 fileOperationUtil.copyRecursively = function(
271 source, parent, newName, entryChangedCallback, progressCallback,
272 successCallback, errorCallback) {
273 // Notify that the copy begins for each entry.
274 progressCallback(source, 0);
276 // If the entry is a file, redirect it to copyFile_().
278 return fileOperationUtil.copyFile_(
279 source, parent, newName, progressCallback,
281 entryChangedCallback(source.toURL(), entry.toURL());
282 successCallback(entry.toURL());
287 // Hereafter, the source is directory.
288 var cancelRequested = false;
289 var cancelCallback = null;
291 // First, we create the directory copy.
293 newName, {create: true, exclusive: true},
295 entryChangedCallback(source.toURL(), dirEntry.toURL());
296 if (cancelRequested) {
297 errorCallback(util.createFileError(FileError.ABORT_ERR));
301 // Iterate on children, and copy them recursively.
302 util.forEachDirEntry(
304 function(child, callback) {
305 if (cancelRequested) {
306 errorCallback(util.createFileError(FileError.ABORT_ERR));
310 cancelCallback = fileOperationUtil.copyRecursively(
311 child, dirEntry, child.name, entryChangedCallback,
314 cancelCallback = null;
318 cancelCallback = null;
319 errorCallback(error);
323 successCallback(dirEntry.toURL());
330 cancelRequested = true;
331 if (cancelCallback) {
333 cancelCallback = null;
339 * Copies a file from source to the parent directory with newName.
340 * See also copyFileByStream_ and copyFileOnDrive_ for the implementation
343 * @param {FileEntry} source The file entry to be copied.
344 * @param {DirectoryEntry} parent The entry of the destination directory.
345 * @param {string} newName The name of copied file.
346 * @param {function(string, number)} progressCallback Callback invoked
347 * periodically during the file writing with the source url and the
348 * number of the processed bytes.
349 * @param {function(FileEntry)} successCallback Callback invoked when the copy
350 * is successfully done with the entry of the created file.
351 * @param {function(FileError)} errorCallback Callback invoked when an error
353 * @return {function()} Callback to cancel the current file copy operation.
354 * When the cancel is done, errorCallback will be called. The returned
355 * callback must not be called more than once.
358 fileOperationUtil.copyFile_ = function(
359 source, parent, newName, progressCallback, successCallback, errorCallback) {
360 if (!PathUtil.isDriveBasedPath(source.fullPath) &&
361 !PathUtil.isDriveBasedPath(parent.fullPath)) {
362 // Copying a file between non-Drive file systems.
363 return fileOperationUtil.copyFileByStream_(
364 source, parent, newName, progressCallback, successCallback,
367 // Copying related to the Drive file system.
368 return fileOperationUtil.copyFileOnDrive_(
369 source, parent, newName, progressCallback, successCallback,
375 * Copies a file by using File and FileWriter objects.
377 * This is a js-implementation of FileEntry.copyTo(). Unfortunately, copyTo
378 * doesn't support periodical progress updating nor cancelling. To support
379 * these operations, this method implements copyTo by streaming way in
382 * Note that this is designed for file copying on local file system. We have
383 * some special cases about copying on Drive file system. See also
384 * copyFileOnDrive_() for more details.
386 * @param {FileEntry} source The file entry to be copied.
387 * @param {DirectoryEntry} parent The entry of the destination directory.
388 * @param {string} newName The name of copied file.
389 * @param {function(string, number)} progressCallback Callback invoked
390 * periodically during the file writing with the source url and the
391 * number of the processed bytes.
392 * @param {function(FileEntry)} successCallback Callback invoked when the copy
393 * is successfully done with the entry of the created file.
394 * @param {function(FileError)} errorCallback Callback invoked when an error
396 * @return {function()} Callback to cancel the current file copy operation.
397 * When the cancel is done, errorCallback will be called. The returned
398 * callback must not be called more than once.
401 fileOperationUtil.copyFileByStream_ = function(
402 source, parent, newName, progressCallback, successCallback, errorCallback) {
403 // Set to true when cancel is requested.
404 var cancelRequested = false;
406 source.file(function(file) {
407 if (cancelRequested) {
408 errorCallback(util.createFileError(FileError.ABORT_ERR));
412 parent.getFile(newName, {create: true, exclusive: true}, function(target) {
413 if (cancelRequested) {
414 errorCallback(util.createFileError(FileError.ABORT_ERR));
418 target.createWriter(function(writer) {
419 if (cancelRequested) {
420 errorCallback(util.createFileError(FileError.ABORT_ERR));
424 writer.onerror = writer.onabort = function(progress) {
425 errorCallback(cancelRequested ?
426 util.createFileError(FileError.ABORT_ERR) :
430 writer.onprogress = function(progress) {
431 if (cancelRequested) {
432 // If the copy was cancelled, we should abort the operation.
433 // The errorCallback will be called by writer.onabort after the
438 progressCallback(source.toURL(), progress.loaded);
441 writer.onwrite = function() {
442 if (cancelRequested) {
443 errorCallback(util.createFileError(FileError.ABORT_ERR));
447 source.getMetadata(function(metadata) {
448 if (cancelRequested) {
449 errorCallback(util.createFileError(FileError.ABORT_ERR));
453 fileOperationUtil.setLastModified(
454 target, metadata.modificationTime);
455 successCallback(target);
464 return function() { cancelRequested = true; };
468 * Copies a file a) from Drive to local, b) from local to Drive, or c) from
470 * Currently, we need to take care about following two things for Drive:
472 * 1) Copying hosted document.
473 * In theory, it is impossible to actual copy a hosted document to other
474 * file system. Thus, instead, Drive file system backend creates a JSON file
475 * referring to the hosted document. Also, when it is uploaded by copyTo,
476 * the hosted document is copied on the server. Note that, this doesn't work
477 * when a user creates a file by FileWriter (as copyFileEntry_ does).
479 * 2) File transfer between local and Drive server.
480 * There are two directions of file transfer; from local to Drive and from
482 * The file transfer from local to Drive is done as a part of file system
483 * background sync (kicked after the copy operation is done). So we don't need
484 * to take care about it here. To copy the file from Drive to local (or Drive
485 * to Drive with GData WAPI), we need to download the file content (if it is
486 * not locally cached). During the downloading, we can listen the periodical
487 * updating and cancel the downloding via private API.
489 * This function supports progress updating and cancelling partially.
490 * Unfortunately, FileEntry.copyTo doesn't support progress updating nor
491 * cancelling, so we support them only during file downloading.
493 * Note: we're planning to move copyTo logic into c++ side. crbug.com/261492
495 * @param {FileEntry} source The entry of the file to be copied.
496 * @param {DirectoryEntry} parent The entry of the destination directory.
497 * @param {string} newName The name of the copied file.
498 * @param {function(string, number)} progressCallback Callback invoked
499 * periodically during the file writing with the source url and the
500 * number of the processed bytes.
501 * @param {function(FileEntry)} successCallback Callback invoked when the
502 * file copy is successfully done with the entry of the copied file.
503 * @param {function(FileError)} errorCallback Callback invoked when an error
505 * @return {function()} Callback to cancel the current file copy operation.
506 * When the cancel is done, errorCallback will be called. The returned
507 * callback must not be called more than once.
510 fileOperationUtil.copyFileOnDrive_ = function(
511 source, parent, newName, progressCallback, successCallback, errorCallback) {
512 // Set to true when cancel is requested.
513 var cancelRequested = false;
514 var cancelCallback = null;
516 var onCopyToCompleted = null;
518 // Progress callback.
519 // Because the uploading the file from local cache to Drive server will be
520 // done as a part of background Drive file system sync, so for this copy
521 // operation, what we need to take care about is only file downloading.
522 if (PathUtil.isDriveBasedPath(source.fullPath)) {
523 var sourceUrl = source.toURL();
524 var sourcePath = util.extractFilePath(sourceUrl);
525 var onFileTransfersUpdated = function(statusList) {
526 for (var i = 0; i < statusList.length; i++) {
527 var status = statusList[i];
529 // Comparing urls is unreliable, since they may use different
530 // url encoding schemes (eg. rfc2396 vs. rfc3986).
531 var filePath = util.extractFilePath(status.fileUrl);
532 if (filePath == sourcePath) {
533 progressCallback(source.toURL(), status.processed);
539 // Subscribe to listen file transfer updating notifications.
540 chrome.fileBrowserPrivate.onFileTransfersUpdated.addListener(
541 onFileTransfersUpdated);
543 // Currently, we do NOT upload the file during the copy operation.
544 // It will be done as a part of file system sync after copy operation.
545 // So, we can cancel only file downloading.
546 cancelCallback = function() {
547 chrome.fileBrowserPrivate.cancelFileTransfers(
548 [sourceUrl], function() {});
551 // We need to clean up on copyTo completion regardless if it is
552 // successfully done or not.
553 onCopyToCompleted = function() {
554 cancelCallback = null;
555 chrome.fileBrowserPrivate.onFileTransfersUpdated.removeListener(
556 onFileTransfersUpdated);
563 if (onCopyToCompleted)
566 if (cancelRequested) {
567 errorCallback(util.createFileError(FileError.ABORT_ERR));
571 successCallback(entry);
574 if (onCopyToCompleted)
577 errorCallback(error);
581 cancelRequested = true;
582 if (cancelCallback) {
584 cancelCallback = null;
590 * Thin wrapper of chrome.fileBrowserPrivate.zipSelection to adapt its
591 * interface similar to copyTo().
593 * @param {Array.<Entry>} sources The array of entries to be archived.
594 * @param {DirectoryEntry} parent The entry of the destination directory.
595 * @param {string} newName The name of the archive to be created.
596 * @param {function(FileEntry)} successCallback Callback invoked when the
597 * operation is successfully done with the entry of the created archive.
598 * @param {function(FileError)} errorCallback Callback invoked when an error
601 fileOperationUtil.zipSelection = function(
602 sources, parent, newName, successCallback, errorCallback) {
603 chrome.fileBrowserPrivate.zipSelection(
605 sources.map(function(e) { return e.toURL(); }),
606 newName, function(success) {
608 // Failed to create a zip archive.
610 util.createFileError(FileError.INVALID_MODIFICATION_ERR));
614 // Returns the created entry via callback.
616 newName, {create: false}, successCallback, errorCallback);
623 function FileOperationManager() {
624 this.copyTasks_ = [];
625 this.deleteTasks_ = [];
626 this.cancelObservers_ = [];
627 this.cancelRequested_ = false;
628 this.cancelCallback_ = null;
629 this.unloadTimeout_ = null;
631 this.eventRouter_ = new FileOperationManager.EventRouter();
635 * Get FileOperationManager instance. In case is hasn't been initialized, a new
636 * instance is created.
638 * @return {FileOperationManager} A FileOperationManager instance.
640 FileOperationManager.getInstance = function() {
641 if (!FileOperationManager.instance_)
642 FileOperationManager.instance_ = new FileOperationManager();
644 return FileOperationManager.instance_;
648 * Manages cr.Event dispatching.
649 * Currently this can send three types of events: "copy-progress",
650 * "copy-operation-completed" and "delete".
652 * TODO(hidehiko): Reorganize the event dispatching mechanism.
654 * @extends {cr.EventTarget}
656 FileOperationManager.EventRouter = function() {
660 * Extends cr.EventTarget.
662 FileOperationManager.EventRouter.prototype.__proto__ = cr.EventTarget.prototype;
665 * Dispatches a simple "copy-progress" event with reason and current
666 * FileOperationManager status. If it is an ERROR event, error should be set.
668 * @param {string} reason Event type. One of "BEGIN", "PROGRESS", "SUCCESS",
669 * "ERROR" or "CANCELLED". TODO(hidehiko): Use enum.
670 * @param {Object} status Current FileOperationManager's status. See also
671 * FileOperationManager.getStatus().
672 * @param {FileOperationManager.Error=} opt_error The info for the error. This
673 * should be set iff the reason is "ERROR".
675 FileOperationManager.EventRouter.prototype.sendProgressEvent = function(
676 reason, status, opt_error) {
677 var event = new cr.Event('copy-progress');
678 event.reason = reason;
679 event.status = status;
681 event.error = opt_error;
682 this.dispatchEvent(event);
686 * Dispatches an event to notify that an entry is changed (created or deleted).
687 * @param {util.EntryChangedKind} kind The enum to represent if the entry is
688 * created or deleted.
689 * @param {Entry} entry The changed entry.
691 FileOperationManager.EventRouter.prototype.sendEntryChangedEvent = function(
693 var event = new cr.Event('entry-changed');
696 this.dispatchEvent(event);
700 * Dispatches an event to notify entries are changed for delete task.
702 * @param {string} reason Event type. One of "BEGIN", "PROGRESS", "SUCCESS",
703 * or "ERROR". TODO(hidehiko): Use enum.
704 * @param {Array.<string>} urls An array of URLs which are affected by delete
707 FileOperationManager.EventRouter.prototype.sendDeleteEvent = function(
709 var event = new cr.Event('delete');
710 event.reason = reason;
712 this.dispatchEvent(event);
716 * A record of a queued copy operation.
718 * Multiple copy operations may be queued at any given time. Additional
719 * Tasks may be added while the queue is being serviced. Though a
720 * cancel operation cancels everything in the queue.
722 * @param {util.FileOperationType} operationType The type of this operation.
723 * @param {Array.<Entry>} sourceEntries Array of source entries.
724 * @param {DirectoryEntry} targetDirEntry Target directory.
727 FileOperationManager.Task = function(
728 operationType, sourceEntries, targetDirEntry) {
729 this.operationType = operationType;
730 this.sourceEntries = sourceEntries;
731 this.targetDirEntry = targetDirEntry;
734 * An array of map from url to Entry being processed.
735 * @type {Array.<Object<string, Entry>>}
737 this.processingEntries = null;
740 * Total number of bytes to be processed. Filled in initialize().
746 * Total number of already processed bytes. Updated periodically.
749 this.processedBytes = 0;
751 this.deleteAfterCopy = false;
754 * Set to true when cancel is requested.
757 this.cancelRequested_ = false;
760 * Callback to cancel the running process.
761 * @private {function()}
763 this.cancelCallback_ = null;
765 // TODO(hidehiko): After we support recursive copy, we don't need this.
766 // If directory already exists, we try to make a copy named 'dir (X)',
767 // where X is a number. When we do this, all subsequent copies from
768 // inside the subtree should be mapped to the new directory name.
769 // For example, if 'dir' was copied as 'dir (1)', then 'dir\file.txt' should
770 // become 'dir (1)\file.txt'.
771 this.renamedDirectories_ = [];
775 * @param {function()} callback When entries resolved.
777 FileOperationManager.Task.prototype.initialize = function(callback) {
781 * Updates copy progress status for the entry.
783 * @param {number} size Number of bytes that has been copied since last update.
785 FileOperationManager.Task.prototype.updateFileCopyProgress = function(size) {
786 this.completedBytes += size;
790 * Requests cancellation of this task.
791 * When the cancellation is done, it is notified via callbacks of run().
793 FileOperationManager.Task.prototype.requestCancel = function() {
794 this.cancelRequested_ = true;
795 if (this.cancelCallback_) {
796 this.cancelCallback_();
797 this.cancelCallback_ = null;
802 * Runs the task. Sub classes must implement this method.
804 * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback
805 * Callback invoked when an entry is changed.
806 * @param {function()} progressCallback Callback invoked periodically during
808 * @param {function()} successCallback Callback run on success.
809 * @param {function(FileOperationManager.Error)} errorCallback Callback run on
812 FileOperationManager.Task.prototype.run = function(
813 entryChangedCallback, progressCallback, successCallback, errorCallback) {
817 * Task to copy entries.
819 * @param {Array.<Entry>} sourceEntries Array of source entries.
820 * @param {DirectoryEntry} targetDirEntry Target directory.
822 * @extends {FileOperationManager.Task}
824 FileOperationManager.CopyTask = function(sourceEntries, targetDirEntry) {
825 FileOperationManager.Task.call(
826 this, util.FileOperationType.COPY, sourceEntries, targetDirEntry);
830 * Extends FileOperationManager.Task.
832 FileOperationManager.CopyTask.prototype.__proto__ =
833 FileOperationManager.Task.prototype;
836 * Initializes the CopyTask.
837 * @param {function()} callback Called when the initialize is completed.
839 FileOperationManager.CopyTask.prototype.initialize = function(callback) {
840 var group = new AsyncUtil.Group();
841 // Correct all entries to be copied for status update.
842 this.processingEntries = [];
843 for (var i = 0; i < this.sourceEntries.length; i++) {
844 group.add(function(index, callback) {
845 fileOperationUtil.resolveRecursively(
846 this.sourceEntries[index],
847 function(resolvedEntries) {
848 var resolvedEntryMap = {};
849 for (var j = 0; j < resolvedEntries.length; ++j) {
850 var entry = resolvedEntries[j];
851 entry.processedBytes = 0;
852 resolvedEntryMap[entry.toURL()] = entry;
854 this.processingEntries[index] = resolvedEntryMap;
859 'Failed to resolve for copy: %s',
860 util.getFileErrorMnemonic(error.code));
865 group.run(function() {
868 for (var i = 0; i < this.processingEntries.length; i++) {
869 for (var url in this.processingEntries[i])
870 this.totalBytes += this.processingEntries[i][url].size;
878 * Copies all entries to the target directory.
879 * Note: this method contains also the operation of "Move" due to historical
882 * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback
883 * Callback invoked when an entry is changed.
884 * @param {function()} progressCallback Callback invoked periodically during
886 * @param {function()} successCallback On success.
887 * @param {function(FileOperationManager.Error)} errorCallback On error.
890 FileOperationManager.CopyTask.prototype.run = function(
891 entryChangedCallback, progressCallback, successCallback, errorCallback) {
892 // TODO(hidehiko): We should be able to share the code to iterate on entries
893 // with serviceMoveTask_().
894 if (this.sourceEntries.length == 0) {
899 // TODO(hidehiko): Delete after copy is the implementation of Move.
900 // Migrate the part into MoveTask.run().
901 var deleteOriginals = function() {
902 var count = this.sourceEntries.length;
904 var onEntryDeleted = function(entry) {
905 entryChangedCallback(util.EntryChangedKind.DELETED, entry);
911 var onFilesystemError = function(err) {
912 errorCallback(new FileOperationManager.Error(
913 util.FileOperationErrorType.FILESYSTEM_ERROR, err));
916 for (var i = 0; i < this.sourceEntries.length; i++) {
917 var entry = this.sourceEntries[i];
918 util.removeFileOrDirectory(
919 entry, onEntryDeleted.bind(null, entry), onFilesystemError);
925 function(callback, entry, index) {
926 if (this.cancelRequested_) {
927 errorCallback(new FileOperationManager.Error(
928 util.FileOperationErrorType.FILESYSTEM_ERROR,
929 util.createFileError(FileError.ABORT_ERR)));
933 this.cancelCallback_ = FileOperationManager.CopyTask.processEntry_(
934 entry, this.targetDirEntry,
935 function(sourceUrl, destinationUrl) {
936 // Finalize the entry's progress state.
937 var entry = this.processingEntries[index][sourceUrl];
939 this.processedBytes += entry.size - entry.processedBytes;
941 delete this.processingEntries[index][sourceUrl];
944 webkitResolveLocalFileSystemURL(
945 destinationUrl, function(destinationEntry) {
946 entryChangedCallback(
947 util.EntryChangedKind.CREATED, destinationEntry);
950 function(source_url, size) {
951 var entry = this.processingEntries[index][source_url];
953 this.processedBytes += size - entry.processedBytes;
954 entry.processedBytes = size;
959 this.cancelCallback_ = null;
963 this.cancelCallback_ = null;
964 errorCallback(error);
968 if (this.deleteAfterCopy) {
978 * Copies the source entry to the target directory.
980 * @param {Entry} sourceEntry An entry to be copied.
981 * @param {DirectoryEntry} destinationEntry The entry which will contain the
983 * @param {function(string, string)} entryChangedCallback
984 * Callback invoked when an entry is created with the source url and
985 * the destination url.
986 * @param {function(string, number)} progressCallback Callback invoked
987 * periodically during the copying.
988 * @param {function()} successCallback On success.
989 * @param {function(FileOperationManager.Error)} errorCallback On error.
990 * @return {function()} Callback to cancel the current file copy operation.
991 * When the cancel is done, errorCallback will be called. The returned
992 * callback must not be called more than once.
995 FileOperationManager.CopyTask.processEntry_ = function(
996 sourceEntry, destinationEntry, entryChangedCallback, progressCallback,
997 successCallback, errorCallback) {
998 var cancelRequested = false;
999 var cancelCallback = null;
1000 fileOperationUtil.deduplicatePath(
1001 destinationEntry, sourceEntry.name,
1002 function(destinationName) {
1003 if (cancelRequested) {
1004 errorCallback(new FileOperationManager.Error(
1005 util.FileOperationErrorType.FILESYSTEM_ERROR,
1006 util.createFileError(FileError.ABORT_ERR)));
1010 cancelCallback = fileOperationUtil.copyTo(
1011 sourceEntry, destinationEntry, destinationName,
1012 entryChangedCallback, progressCallback,
1014 cancelCallback = null;
1018 cancelCallback = null;
1019 errorCallback(new FileOperationManager.Error(
1020 util.FileOperationErrorType.FILESYSTEM_ERROR, error));
1026 cancelRequested = true;
1027 if (cancelCallback) {
1029 cancelCallback = null;
1035 * Task to move entries.
1037 * @param {Array.<Entry>} sourceEntries Array of source entries.
1038 * @param {DirectoryEntry} targetDirEntry Target directory.
1040 * @extends {FileOperationManager.Task}
1042 FileOperationManager.MoveTask = function(sourceEntries, targetDirEntry) {
1043 FileOperationManager.Task.call(
1044 this, util.FileOperationType.MOVE, sourceEntries, targetDirEntry);
1048 * Extends FileOperationManager.Task.
1050 FileOperationManager.MoveTask.prototype.__proto__ =
1051 FileOperationManager.Task.prototype;
1054 * Initializes the MoveTask.
1055 * @param {function()} callback Called when the initialize is completed.
1057 FileOperationManager.MoveTask.prototype.initialize = function(callback) {
1058 // This may be moving from search results, where it fails if we
1059 // move parent entries earlier than child entries. We should
1060 // process the deepest entry first. Since move of each entry is
1061 // done by a single moveTo() call, we don't need to care about the
1062 // recursive traversal order.
1063 this.sourceEntries.sort(function(entry1, entry2) {
1064 return entry2.fullPath.length - entry1.fullPath.length;
1067 this.processingEntries = [];
1068 for (var i = 0; i < this.sourceEntries.length; i++) {
1069 var processingEntryMap = {};
1070 var entry = this.sourceEntries[i];
1072 // The move should be done with updating the metadata. So here we assume
1073 // all the file size is 1 byte. (Avoiding 0, so that progress bar can
1075 // TODO(hidehiko): Remove this hack.
1077 processingEntryMap[entry.toURL()] = entry;
1078 this.processingEntries[i] = processingEntryMap;
1085 * Moves all entries in the task.
1087 * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback
1088 * Callback invoked when an entry is changed.
1089 * @param {function()} progressCallback Callback invoked periodically during
1091 * @param {function()} successCallback On success.
1092 * @param {function(FileOperationManager.Error)} errorCallback On error.
1095 FileOperationManager.MoveTask.prototype.run = function(
1096 entryChangedCallback, progressCallback, successCallback, errorCallback) {
1097 if (this.sourceEntries.length == 0) {
1104 function(callback, entry, index) {
1105 if (this.cancelRequested_) {
1106 errorCallback(new FileOperationManager.Error(
1107 util.FileOperationErrorType.FILESYSTEM_ERROR,
1108 util.createFileError(FileError.ABORT_ERR)));
1112 FileOperationManager.MoveTask.processEntry_(
1113 entry, this.targetDirEntry, entryChangedCallback,
1115 // Erase the processing entry.
1116 this.processingEntries[index] = {};
1117 this.processedBytes++;
1129 * Moves the sourceEntry to the targetDirEntry in this task.
1131 * @param {Entry} sourceEntry An entry to be moved.
1132 * @param {DirectoryEntry} destinationEntry The entry of the destination
1134 * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback
1135 * Callback invoked when an entry is changed.
1136 * @param {function()} successCallback On success.
1137 * @param {function(FileOperationManager.Error)} errorCallback On error.
1140 FileOperationManager.MoveTask.processEntry_ = function(
1141 sourceEntry, destinationEntry, entryChangedCallback, successCallback,
1143 fileOperationUtil.deduplicatePath(
1146 function(destinationName) {
1148 destinationEntry, destinationName,
1149 function(movedEntry) {
1150 entryChangedCallback(util.EntryChangedKind.CREATED, movedEntry);
1151 entryChangedCallback(util.EntryChangedKind.DELETED, sourceEntry);
1155 errorCallback(new FileOperationManager.Error(
1156 util.FileOperationErrorType.FILESYSTEM_ERROR, error));
1163 * Task to create a zip archive.
1165 * @param {Array.<Entry>} sourceEntries Array of source entries.
1166 * @param {DirectoryEntry} targetDirEntry Target directory.
1167 * @param {DirectoryEntry} zipBaseDirEntry Base directory dealt as a root
1170 * @extends {FileOperationManager.Task}
1172 FileOperationManager.ZipTask = function(
1173 sourceEntries, targetDirEntry, zipBaseDirEntry) {
1174 FileOperationManager.Task.call(
1175 this, util.FileOperationType.ZIP, sourceEntries, targetDirEntry);
1176 this.zipBaseDirEntry = zipBaseDirEntry;
1180 * Extends FileOperationManager.Task.
1182 FileOperationManager.ZipTask.prototype.__proto__ =
1183 FileOperationManager.Task.prototype;
1187 * Initializes the ZipTask.
1188 * @param {function()} callback Called when the initialize is completed.
1190 FileOperationManager.ZipTask.prototype.initialize = function(callback) {
1191 var resolvedEntryMap = {};
1192 var group = new AsyncUtil.Group();
1193 for (var i = 0; i < this.sourceEntries.length; i++) {
1194 group.add(function(index, callback) {
1195 fileOperationUtil.resolveRecursively(
1196 this.sourceEntries[index],
1198 for (var j = 0; j < entries.length; j++)
1199 resolvedEntryMap[entries[j].toURL()] = entries[j];
1202 function(error) {});
1206 group.run(function() {
1207 // For zip archiving, all the entries are processed at once.
1208 this.processingEntries = [resolvedEntryMap];
1210 this.totalBytes = 0;
1211 for (var url in resolvedEntryMap)
1212 this.totalBytes += resolvedEntryMap[url].size;
1219 * Runs a zip file creation task.
1221 * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback
1222 * Callback invoked when an entry is changed.
1223 * @param {function()} progressCallback Callback invoked periodically during
1225 * @param {function()} successCallback On complete.
1226 * @param {function(FileOperationManager.Error)} errorCallback On error.
1229 FileOperationManager.ZipTask.prototype.run = function(
1230 entryChangedCallback, progressCallback, successCallback, errorCallback) {
1231 // TODO(hidehiko): we should localize the name.
1232 var destName = 'Archive';
1233 if (this.sourceEntries.length == 1) {
1234 var entryPath = this.sourceEntries[0].fullPath;
1235 var i = entryPath.lastIndexOf('/');
1236 var basename = (i < 0) ? entryPath : entryPath.substr(i + 1);
1237 i = basename.lastIndexOf('.');
1238 destName = ((i < 0) ? basename : basename.substr(0, i));
1241 fileOperationUtil.deduplicatePath(
1242 this.targetDirEntry, destName + '.zip',
1243 function(destPath) {
1244 // TODO: per-entry zip progress update with accurate byte count.
1245 // For now just set completedBytes to same value as totalBytes so
1246 // that the progress bar is full.
1247 this.processedBytes = this.totalBytes;
1250 // The number of elements in processingEntries is 1. See also
1253 for (var url in this.processingEntries[0])
1254 entries.push(this.processingEntries[0][url]);
1256 fileOperationUtil.zipSelection(
1258 this.zipBaseDirEntry,
1261 entryChangedCallback(util.EntryChangedKind.CREATE, entry);
1265 errorCallback(new FileOperationManager.Error(
1266 util.FileOperationErrorType.FILESYSTEM_ERROR, error));
1273 * Error class used to report problems with a copy operation.
1274 * If the code is UNEXPECTED_SOURCE_FILE, data should be a path of the file.
1275 * If the code is TARGET_EXISTS, data should be the existing Entry.
1276 * If the code is FILESYSTEM_ERROR, data should be the FileError.
1278 * @param {util.FileOperationErrorType} code Error type.
1279 * @param {string|Entry|FileError} data Additional data.
1282 FileOperationManager.Error = function(code, data) {
1287 // FileOperationManager methods.
1290 * Called before a new method is run in the manager. Prepares the manager's
1291 * state for running a new method.
1293 FileOperationManager.prototype.willRunNewMethod = function() {
1294 // Cancel any pending close actions so the file copy manager doesn't go away.
1295 if (this.unloadTimeout_)
1296 clearTimeout(this.unloadTimeout_);
1297 this.unloadTimeout_ = null;
1301 * @return {Object} Status object.
1303 FileOperationManager.prototype.getStatus = function() {
1304 // TODO(hidehiko): Reorganize the structure when delete queue is merged
1305 // into copy task queue.
1307 // Set to util.FileOperationType if all the running/pending tasks is
1308 // the same kind of task.
1309 operationType: null,
1311 // The number of entries to be processed.
1312 numRemainingItems: 0,
1314 // The total number of bytes to be processed.
1317 // The number of bytes.
1320 // Available if numRemainingItems == 1. Pointing to an Entry which is
1322 processingEntry: null,
1326 this.copyTasks_.length > 0 ? this.copyTasks_[0].operationType : null;
1327 var processingEntry = null;
1328 for (var i = 0; i < this.copyTasks_.length; i++) {
1329 var task = this.copyTasks_[i];
1330 if (task.operationType != operationType)
1331 operationType = null;
1333 // Assuming the number of entries is small enough, count everytime.
1334 for (var j = 0; j < task.processingEntries.length; j++) {
1335 for (var url in task.processingEntries[j]) {
1336 ++result.numRemainingItems;
1337 processingEntry = task.processingEntries[j][url];
1341 result.totalBytes += task.totalBytes;
1342 result.processedBytes += task.processedBytes;
1345 result.operationType = operationType;
1347 if (result.numRemainingItems == 1)
1348 result.processingEntry = processingEntry;
1354 * Adds an event listener for the tasks.
1355 * @param {string} type The name of the event.
1356 * @param {function(cr.Event)} handler The handler for the event.
1357 * This is called when the event is dispatched.
1359 FileOperationManager.prototype.addEventListener = function(type, handler) {
1360 this.eventRouter_.addEventListener(type, handler);
1364 * Removes an event listener for the tasks.
1365 * @param {string} type The name of the event.
1366 * @param {function(cr.Event)} handler The handler to be removed.
1368 FileOperationManager.prototype.removeEventListener = function(type, handler) {
1369 this.eventRouter_.removeEventListener(type, handler);
1373 * Says if there are any tasks in the queue.
1374 * @return {boolean} True, if there are any tasks.
1376 FileOperationManager.prototype.hasQueuedTasks = function() {
1377 return this.copyTasks_.length > 0 || this.deleteTasks_.length > 0;
1381 * Unloads the host page in 5 secs of idleing. Need to be called
1382 * each time this.copyTasks_.length or this.deleteTasks_.length
1387 FileOperationManager.prototype.maybeScheduleCloseBackgroundPage_ = function() {
1388 if (!this.hasQueuedTasks()) {
1389 if (this.unloadTimeout_ === null)
1390 this.unloadTimeout_ = setTimeout(maybeCloseBackgroundPage, 5000);
1391 } else if (this.unloadTimeout_) {
1392 clearTimeout(this.unloadTimeout_);
1393 this.unloadTimeout_ = null;
1398 * Completely clear out the copy queue, either because we encountered an error
1399 * or completed successfully.
1403 FileOperationManager.prototype.resetQueue_ = function() {
1404 for (var i = 0; i < this.cancelObservers_.length; i++)
1405 this.cancelObservers_[i]();
1407 this.copyTasks_ = [];
1408 this.cancelObservers_ = [];
1409 this.maybeScheduleCloseBackgroundPage_();
1413 * Request that the current copy queue be abandoned.
1415 * @param {function()=} opt_callback On cancel.
1417 FileOperationManager.prototype.requestCancel = function(opt_callback) {
1418 this.cancelRequested_ = true;
1419 if (this.cancelCallback_) {
1420 this.cancelCallback_();
1421 this.cancelCallback_ = null;
1424 this.cancelObservers_.push(opt_callback);
1426 // If there is any active task it will eventually call maybeCancel_.
1427 // Otherwise call it right now.
1428 if (this.copyTasks_.length == 0)
1431 this.copyTasks_[0].requestCancel();
1435 * Perform the bookkeeping required to cancel.
1439 FileOperationManager.prototype.doCancel_ = function() {
1441 this.cancelRequested_ = false;
1442 this.eventRouter_.sendProgressEvent('CANCELLED', this.getStatus());
1446 * Used internally to check if a cancel has been requested, and handle
1449 * @return {boolean} If canceled.
1452 FileOperationManager.prototype.maybeCancel_ = function() {
1453 if (!this.cancelRequested_)
1463 * @param {Array.<string>} sourcePaths Path of the source files.
1464 * @param {string} targetPath The destination path of the target directory.
1465 * @param {boolean} isMove True if the operation is "move", otherwise (i.e.
1466 * if the operation is "copy") false.
1468 FileOperationManager.prototype.paste = function(
1469 sourcePaths, targetPath, isMove) {
1470 // Do nothing if sourcePaths is empty.
1471 if (sourcePaths.length == 0)
1474 var errorCallback = function(error) {
1475 this.eventRouter_.sendProgressEvent(
1478 new FileOperationManager.Error(
1479 util.FileOperationErrorType.FILESYSTEM_ERROR, error));
1482 var targetEntry = null;
1485 // Resolve paths to entries.
1486 var resolveGroup = new AsyncUtil.Group();
1487 resolveGroup.add(function(callback) {
1488 webkitResolveLocalFileSystemURL(
1489 util.makeFilesystemUrl(targetPath),
1491 if (!entry.isDirectory) {
1492 // Found a non directory entry.
1493 errorCallback(util.createFileError(FileError.TYPE_MISMATCH_ERR));
1497 targetEntry = entry;
1503 for (var i = 0; i < sourcePaths.length; i++) {
1504 resolveGroup.add(function(sourcePath, callback) {
1505 webkitResolveLocalFileSystemURL(
1506 util.makeFilesystemUrl(sourcePath),
1508 entries.push(entry);
1512 }.bind(this, sourcePaths[i]));
1515 resolveGroup.run(function() {
1517 // Moving to the same directory is a redundant operation.
1518 entries = entries.filter(function(entry) {
1519 return targetEntry.fullPath + '/' + entry.name != entry.fullPath;
1522 // Do nothing, if we have no entries to be moved.
1523 if (entries.length == 0)
1527 this.queueCopy_(targetEntry, entries, isMove);
1532 * Checks if the move operation is avaiable between the given two locations.
1534 * @param {DirectoryEntry} sourceEntry An entry from the source.
1535 * @param {DirectoryEntry} targetDirEntry Directory entry for the target.
1536 * @return {boolean} Whether we can move from the source to the target.
1538 FileOperationManager.prototype.isMovable = function(sourceEntry,
1540 return (PathUtil.isDriveBasedPath(sourceEntry.fullPath) &&
1541 PathUtil.isDriveBasedPath(targetDirEntry.fullPath)) ||
1542 (PathUtil.getRootPath(sourceEntry.fullPath) ==
1543 PathUtil.getRootPath(targetDirEntry.fullPath));
1547 * Initiate a file copy.
1549 * @param {DirectoryEntry} targetDirEntry Target directory.
1550 * @param {Array.<Entry>} entries Entries to copy.
1551 * @param {boolean} isMove In case of move.
1552 * @return {FileOperationManager.Task} Copy task.
1555 FileOperationManager.prototype.queueCopy_ = function(
1556 targetDirEntry, entries, isMove) {
1557 // When copying files, null can be specified as source directory.
1560 if (this.isMovable(entries[0], targetDirEntry)) {
1561 task = new FileOperationManager.MoveTask(entries, targetDirEntry);
1563 task = new FileOperationManager.CopyTask(entries, targetDirEntry);
1564 task.deleteAfterCopy = true;
1567 task = new FileOperationManager.CopyTask(entries, targetDirEntry);
1570 task.initialize(function() {
1571 this.copyTasks_.push(task);
1572 this.maybeScheduleCloseBackgroundPage_();
1573 if (this.copyTasks_.length == 1) {
1574 // Assume this.cancelRequested_ == false.
1575 // This moved us from 0 to 1 active tasks, let the servicing begin!
1576 this.serviceAllTasks_();
1578 // Force to update the progress of butter bar when there are new tasks
1579 // coming while servicing current task.
1580 this.eventRouter_.sendProgressEvent('PROGRESS', this.getStatus());
1588 * Service all pending tasks, as well as any that might appear during the
1593 FileOperationManager.prototype.serviceAllTasks_ = function() {
1596 var onTaskProgress = function() {
1597 self.eventRouter_.sendProgressEvent('PROGRESS', self.getStatus());
1600 var onEntryChanged = function(kind, entry) {
1601 self.eventRouter_.sendEntryChangedEvent(kind, entry);
1604 var onTaskError = function(err) {
1605 if (self.maybeCancel_())
1607 self.eventRouter_.sendProgressEvent('ERROR', self.getStatus(), err);
1611 var onTaskSuccess = function() {
1612 if (self.maybeCancel_())
1615 // The task at the front of the queue is completed. Pop it from the queue.
1616 self.copyTasks_.shift();
1617 self.maybeScheduleCloseBackgroundPage_();
1619 if (!self.copyTasks_.length) {
1620 // All tasks have been serviced, clean up and exit.
1621 self.eventRouter_.sendProgressEvent('SUCCESS', self.getStatus());
1626 // We want to dispatch a PROGRESS event when there are more tasks to serve
1627 // right after one task finished in the queue. We treat all tasks as one
1628 // big task logically, so there is only one BEGIN/SUCCESS event pair for
1629 // these continuous tasks.
1630 self.eventRouter_.sendProgressEvent('PROGRESS', self.getStatus());
1631 self.copyTasks_[0].run(
1632 onEntryChanged, onTaskProgress, onTaskSuccess, onTaskError);
1635 // If the queue size is 1 after pushing our task, it was empty before,
1636 // so we need to kick off queue processing and dispatch BEGIN event.
1637 this.eventRouter_.sendProgressEvent('BEGIN', this.getStatus());
1638 this.copyTasks_[0].run(
1639 onEntryChanged, onTaskProgress, onTaskSuccess, onTaskError);
1643 * Timeout before files are really deleted (to allow undo).
1645 FileOperationManager.DELETE_TIMEOUT = 30 * 1000;
1648 * Schedules the files deletion.
1650 * @param {Array.<Entry>} entries The entries.
1652 FileOperationManager.prototype.deleteEntries = function(entries) {
1653 var task = { entries: entries };
1654 this.deleteTasks_.push(task);
1655 this.maybeScheduleCloseBackgroundPage_();
1656 if (this.deleteTasks_.length == 1)
1657 this.serviceAllDeleteTasks_();
1661 * Service all pending delete tasks, as well as any that might appear during the
1664 * Must not be called if there is an in-flight delete task.
1668 FileOperationManager.prototype.serviceAllDeleteTasks_ = function() {
1669 // Returns the urls of the given task's entries.
1670 var getTaskUrls = function(task) {
1671 return task.entries.map(function(entry) {
1672 return util.makeFilesystemUrl(entry.fullPath);
1676 var onTaskSuccess = function() {
1677 var urls = getTaskUrls(this.deleteTasks_.shift());
1678 if (!this.deleteTasks_.length) {
1679 // All tasks have been serviced, clean up and exit.
1680 this.eventRouter_.sendDeleteEvent('SUCCESS', urls);
1681 this.maybeScheduleCloseBackgroundPage_();
1685 // We want to dispatch a PROGRESS event when there are more tasks to serve
1686 // right after one task finished in the queue. We treat all tasks as one
1687 // big task logically, so there is only one BEGIN/SUCCESS event pair for
1688 // these continuous tasks.
1689 this.eventRouter_.sendDeleteEvent('PROGRESS', urls);
1691 this.serviceDeleteTask_(this.deleteTasks_[0], onTaskSuccess, onTaskFailure);
1694 var onTaskFailure = function(error) {
1695 var urls = getTaskUrls(this.deleteTasks_[0]);
1696 this.deleteTasks_ = [];
1697 this.eventRouter_.sendDeleteEvent('ERROR', urls);
1698 this.maybeScheduleCloseBackgroundPage_();
1701 // If the queue size is 1 after pushing our task, it was empty before,
1702 // so we need to kick off queue processing and dispatch BEGIN event.
1703 this.eventRouter_.sendDeleteEvent('BEGIN', getTaskUrls(this.deleteTasks_[0]));
1704 this.serviceDeleteTask_(this.deleteTasks_[0], onTaskSuccess, onTaskFailure);
1708 * Performs the deletion.
1710 * @param {Object} task The delete task (see deleteEntries function).
1711 * @param {function()} successCallback Callback run on success.
1712 * @param {function(FileOperationManager.Error)} errorCallback Callback run on
1716 FileOperationManager.prototype.serviceDeleteTask_ = function(
1717 task, successCallback, errorCallback) {
1718 var downcount = task.entries.length;
1719 if (downcount == 0) {
1724 var filesystemError = null;
1725 var onComplete = function() {
1726 if (--downcount > 0)
1729 // All remove operations are processed. Run callback.
1730 if (filesystemError) {
1731 errorCallback(new FileOperationManager.Error(
1732 util.FileOperationErrorType.FILESYSTEM_ERROR, filesystemError));
1738 for (var i = 0; i < task.entries.length; i++) {
1739 var entry = task.entries[i];
1740 util.removeFileOrDirectory(
1742 function(currentEntry) {
1743 this.eventRouter_.sendEntryChangedEvent(
1744 util.EntryChangedKind.DELETED, currentEntry);
1746 }.bind(this, entry),
1748 if (!filesystemError)
1749 filesystemError = error;
1756 * Creates a zip file for the selection of files.
1758 * @param {Entry} dirEntry The directory containing the selection.
1759 * @param {Array.<Entry>} selectionEntries The selected entries.
1761 FileOperationManager.prototype.zipSelection = function(
1762 dirEntry, selectionEntries) {
1764 var zipTask = new FileOperationManager.ZipTask(
1765 selectionEntries, dirEntry, dirEntry);
1767 zipTask.initialize(function() {
1768 self.copyTasks_.push(zipTask);
1769 if (self.copyTasks_.length == 1) {
1770 // Assume self.cancelRequested_ == false.
1771 // This moved us from 0 to 1 active tasks, let the servicing begin!
1772 self.serviceAllTasks_();
1774 // Force to update the progress of butter bar when there are new tasks
1775 // coming while servicing current task.
1776 self.eventRouter_.sendProgressEvent('PROGRESS', self.getStatus());