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 * occurred 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 * 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.
133 * @param {Entry} source The entry to be copied.
134 * @param {DirectoryEntry} parent The entry of the destination directory.
135 * @param {string} newName The name of copied file.
136 * @param {function(string, string)} entryChangedCallback
137 * Callback invoked when an entry is created with the source url and
138 * the destination url.
139 * @param {function(string, number)} progressCallback Callback invoked
140 * periodically during the copying. It takes the source url and the
141 * processed bytes of it.
142 * @param {function(string)} successCallback Callback invoked when the copy
143 * is successfully done with the url of the created entry.
144 * @param {function(FileError)} errorCallback Callback invoked when an error
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.
150 fileOperationUtil.copyTo = function(
151 source, parent, newName, entryChangedCallback, progressCallback,
152 successCallback, errorCallback) {
154 var pendingCallbacks = [];
156 var onCopyProgress = function(progressCopyId, status) {
157 if (copyId == null) {
158 // If the copyId is not yet available, wait for it.
159 pendingCallbacks.push(
160 onCopyProgress.bind(null, progressCopyId, status));
164 // This is not what we're interested in.
165 if (progressCopyId != copyId)
168 switch (status.type) {
169 case 'begin_copy_entry':
172 case 'end_copy_entry':
173 entryChangedCallback(status.sourceUrl, status.destinationUrl);
177 progressCallback(status.sourceUrl, status.size);
181 chrome.fileBrowserPrivate.onCopyProgress.removeListener(onCopyProgress);
182 successCallback(status.destinationUrl);
186 chrome.fileBrowserPrivate.onCopyProgress.removeListener(onCopyProgress);
187 errorCallback(util.createFileError(status.error));
191 // Found unknown state. Cancel the task, and return an error.
192 console.error('Unknown progress type: ' + status.type);
193 chrome.fileBrowserPrivate.onCopyProgress.removeListener(onCopyProgress);
194 chrome.fileBrowserPrivate.cancelCopy(copyId);
195 errorCallback(util.createFileError(FileError.INVALID_STATE_ERR));
199 // Register the listener before calling startCopy. Otherwise some events
201 chrome.fileBrowserPrivate.onCopyProgress.addListener(onCopyProgress);
203 // Then starts the copy.
204 chrome.fileBrowserPrivate.startCopy(
205 source.toURL(), parent.toURL(), newName, function(startCopyId) {
206 // last error contains the FileError code on error.
207 if (chrome.runtime.lastError) {
208 // Unsubscribe the progress listener.
209 chrome.fileBrowserPrivate.onCopyProgress.removeListener(
211 errorCallback(util.createFileError(
212 Integer.parseInt(chrome.runtime.lastError, 10)));
216 copyId = startCopyId;
217 for (var i = 0; i < pendingCallbacks.length; i++) {
218 pendingCallbacks[i]();
223 // If copyId is not yet available, wait for it.
224 if (copyId == null) {
225 pendingCallbacks.push(function() {
226 chrome.fileBrowserPrivate.cancelCopy(copyId);
231 chrome.fileBrowserPrivate.cancelCopy(copyId);
236 * Thin wrapper of chrome.fileBrowserPrivate.zipSelection to adapt its
237 * interface similar to copyTo().
239 * @param {Array.<Entry>} sources The array of entries to be archived.
240 * @param {DirectoryEntry} parent The entry of the destination directory.
241 * @param {string} newName The name of the archive to be created.
242 * @param {function(FileEntry)} successCallback Callback invoked when the
243 * operation is successfully done with the entry of the created archive.
244 * @param {function(FileError)} errorCallback Callback invoked when an error
247 fileOperationUtil.zipSelection = function(
248 sources, parent, newName, successCallback, errorCallback) {
249 chrome.fileBrowserPrivate.zipSelection(
251 sources.map(function(e) { return e.toURL(); }),
252 newName, function(success) {
254 // Failed to create a zip archive.
256 util.createFileError(FileError.INVALID_MODIFICATION_ERR));
260 // Returns the created entry via callback.
262 newName, {create: false}, successCallback, errorCallback);
269 function FileOperationManager() {
270 this.copyTasks_ = [];
271 this.deleteTasks_ = [];
272 this.unloadTimeout_ = null;
273 this.taskIdCounter_ = 0;
275 this.eventRouter_ = new FileOperationManager.EventRouter();
281 * Get FileOperationManager instance. In case is hasn't been initialized, a new
282 * instance is created.
284 * @return {FileOperationManager} A FileOperationManager instance.
286 FileOperationManager.getInstance = function() {
287 if (!FileOperationManager.instance_)
288 FileOperationManager.instance_ = new FileOperationManager();
290 return FileOperationManager.instance_;
294 * Manages Event dispatching.
295 * Currently this can send three types of events: "copy-progress",
296 * "copy-operation-completed" and "delete".
298 * TODO(hidehiko): Reorganize the event dispatching mechanism.
300 * @extends {cr.EventTarget}
302 FileOperationManager.EventRouter = function() {
306 * Extends cr.EventTarget.
308 FileOperationManager.EventRouter.prototype.__proto__ = cr.EventTarget.prototype;
311 * Dispatches a simple "copy-progress" event with reason and current
312 * FileOperationManager status. If it is an ERROR event, error should be set.
314 * @param {string} reason Event type. One of "BEGIN", "PROGRESS", "SUCCESS",
315 * "ERROR" or "CANCELLED". TODO(hidehiko): Use enum.
316 * @param {Object} status Current FileOperationManager's status. See also
317 * FileOperationManager.getStatus().
318 * @param {string} taskId ID of task related with the event.
319 * @param {FileOperationManager.Error=} opt_error The info for the error. This
320 * should be set iff the reason is "ERROR".
322 FileOperationManager.EventRouter.prototype.sendProgressEvent = function(
323 reason, status, taskId, opt_error) {
324 var event = new Event('copy-progress');
325 event.reason = reason;
326 event.status = status;
327 event.taskId = taskId;
329 event.error = opt_error;
330 this.dispatchEvent(event);
334 * Dispatches an event to notify that an entry is changed (created or deleted).
335 * @param {util.EntryChangedKind} kind The enum to represent if the entry is
336 * created or deleted.
337 * @param {Entry} entry The changed entry.
339 FileOperationManager.EventRouter.prototype.sendEntryChangedEvent = function(
341 var event = new Event('entry-changed');
344 this.dispatchEvent(event);
348 * Dispatches an event to notify entries are changed for delete task.
350 * @param {string} reason Event type. One of "BEGIN", "PROGRESS", "SUCCESS",
351 * or "ERROR". TODO(hidehiko): Use enum.
352 * @param {Array.<string>} urls An array of URLs which are affected by delete
354 * @param {string} taskId ID of task related with the event.
356 FileOperationManager.EventRouter.prototype.sendDeleteEvent = function(
357 reason, urls, taskId) {
358 var event = new Event('delete');
359 event.reason = reason;
361 this.dispatchEvent(event);
365 * A record of a queued copy operation.
367 * Multiple copy operations may be queued at any given time. Additional
368 * Tasks may be added while the queue is being serviced. Though a
369 * cancel operation cancels everything in the queue.
371 * @param {util.FileOperationType} operationType The type of this operation.
372 * @param {Array.<Entry>} sourceEntries Array of source entries.
373 * @param {DirectoryEntry} targetDirEntry Target directory.
376 FileOperationManager.Task = function(
377 operationType, sourceEntries, targetDirEntry) {
378 this.operationType = operationType;
379 this.sourceEntries = sourceEntries;
380 this.targetDirEntry = targetDirEntry;
383 * An array of map from url to Entry being processed.
384 * @type {Array.<Object<string, Entry>>}
386 this.processingEntries = null;
389 * Total number of bytes to be processed. Filled in initialize().
395 * Total number of already processed bytes. Updated periodically.
398 this.processedBytes = 0;
400 this.deleteAfterCopy = false;
403 * Set to true when cancel is requested.
406 this.cancelRequested_ = false;
409 * Callback to cancel the running process.
410 * @private {function()}
412 this.cancelCallback_ = null;
414 // TODO(hidehiko): After we support recursive copy, we don't need this.
415 // If directory already exists, we try to make a copy named 'dir (X)',
416 // where X is a number. When we do this, all subsequent copies from
417 // inside the subtree should be mapped to the new directory name.
418 // For example, if 'dir' was copied as 'dir (1)', then 'dir\file.txt' should
419 // become 'dir (1)\file.txt'.
420 this.renamedDirectories_ = [];
424 * @param {function()} callback When entries resolved.
426 FileOperationManager.Task.prototype.initialize = function(callback) {
430 * Updates copy progress status for the entry.
432 * @param {number} size Number of bytes that has been copied since last update.
434 FileOperationManager.Task.prototype.updateFileCopyProgress = function(size) {
435 this.completedBytes += size;
439 * Requests cancellation of this task.
440 * When the cancellation is done, it is notified via callbacks of run().
442 FileOperationManager.Task.prototype.requestCancel = function() {
443 this.cancelRequested_ = true;
444 if (this.cancelCallback_) {
445 this.cancelCallback_();
446 this.cancelCallback_ = null;
451 * Runs the task. Sub classes must implement this method.
453 * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback
454 * Callback invoked when an entry is changed.
455 * @param {function()} progressCallback Callback invoked periodically during
457 * @param {function()} successCallback Callback run on success.
458 * @param {function(FileOperationManager.Error)} errorCallback Callback run on
461 FileOperationManager.Task.prototype.run = function(
462 entryChangedCallback, progressCallback, successCallback, errorCallback) {
466 * Get states of the task.
467 * TOOD(hirono): Removes this method and sets a task to progress events.
468 * @return {object} Status object.
470 FileOperationManager.Task.prototype.getStatus = function() {
471 var numRemainingItems = this.countRemainingItems();
473 operationType: this.operationType,
474 numRemainingItems: numRemainingItems,
475 totalBytes: this.totalBytes,
476 processedBytes: this.processedBytes,
477 processingEntry: this.getSingleEntry()
482 * Counts the number of remaining items.
483 * @return {number} Number of remaining items.
485 FileOperationManager.Task.prototype.countRemainingItems = function() {
487 for (var i = 0; i < this.processingEntries.length; i++) {
488 for (var url in this.processingEntries[i]) {
496 * Obtains the single processing entry. If there are multiple processing
497 * entries, it returns null.
498 * @return {Entry} First entry.
500 FileOperationManager.Task.prototype.getSingleEntry = function() {
501 if (this.countRemainingItems() !== 1)
503 for (var i = 0; i < this.processingEntries.length; i++) {
504 var entryMap = this.processingEntries[i];
505 for (var name in entryMap)
506 return entryMap[name];
512 * Task to copy entries.
514 * @param {Array.<Entry>} sourceEntries Array of source entries.
515 * @param {DirectoryEntry} targetDirEntry Target directory.
517 * @extends {FileOperationManager.Task}
519 FileOperationManager.CopyTask = function(sourceEntries, targetDirEntry) {
520 FileOperationManager.Task.call(
521 this, util.FileOperationType.COPY, sourceEntries, targetDirEntry);
525 * Extends FileOperationManager.Task.
527 FileOperationManager.CopyTask.prototype.__proto__ =
528 FileOperationManager.Task.prototype;
531 * Initializes the CopyTask.
532 * @param {function()} callback Called when the initialize is completed.
534 FileOperationManager.CopyTask.prototype.initialize = function(callback) {
535 var group = new AsyncUtil.Group();
536 // Correct all entries to be copied for status update.
537 this.processingEntries = [];
538 for (var i = 0; i < this.sourceEntries.length; i++) {
539 group.add(function(index, callback) {
540 fileOperationUtil.resolveRecursively(
541 this.sourceEntries[index],
542 function(resolvedEntries) {
543 var resolvedEntryMap = {};
544 for (var j = 0; j < resolvedEntries.length; ++j) {
545 var entry = resolvedEntries[j];
546 entry.processedBytes = 0;
547 resolvedEntryMap[entry.toURL()] = entry;
549 this.processingEntries[index] = resolvedEntryMap;
554 'Failed to resolve for copy: %s',
555 util.getFileErrorMnemonic(error.code));
560 group.run(function() {
563 for (var i = 0; i < this.processingEntries.length; i++) {
564 for (var url in this.processingEntries[i])
565 this.totalBytes += this.processingEntries[i][url].size;
573 * Copies all entries to the target directory.
574 * Note: this method contains also the operation of "Move" due to historical
577 * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback
578 * Callback invoked when an entry is changed.
579 * @param {function()} progressCallback Callback invoked periodically during
581 * @param {function()} successCallback On success.
582 * @param {function(FileOperationManager.Error)} errorCallback On error.
585 FileOperationManager.CopyTask.prototype.run = function(
586 entryChangedCallback, progressCallback, successCallback, errorCallback) {
587 // TODO(hidehiko): We should be able to share the code to iterate on entries
588 // with serviceMoveTask_().
589 if (this.sourceEntries.length == 0) {
594 // TODO(hidehiko): Delete after copy is the implementation of Move.
595 // Migrate the part into MoveTask.run().
596 var deleteOriginals = function() {
597 var count = this.sourceEntries.length;
599 var onEntryDeleted = function(entry) {
600 entryChangedCallback(util.EntryChangedKind.DELETED, entry);
606 var onFilesystemError = function(err) {
607 errorCallback(new FileOperationManager.Error(
608 util.FileOperationErrorType.FILESYSTEM_ERROR, err));
611 for (var i = 0; i < this.sourceEntries.length; i++) {
612 var entry = this.sourceEntries[i];
613 util.removeFileOrDirectory(
614 entry, onEntryDeleted.bind(null, entry), onFilesystemError);
620 function(callback, entry, index) {
621 if (this.cancelRequested_) {
622 errorCallback(new FileOperationManager.Error(
623 util.FileOperationErrorType.FILESYSTEM_ERROR,
624 util.createFileError(FileError.ABORT_ERR)));
629 entry, this.targetDirEntry,
630 function(sourceUrl, destinationUrl) {
631 // Finalize the entry's progress state.
632 var entry = this.processingEntries[index][sourceUrl];
634 this.processedBytes += entry.size - entry.processedBytes;
636 delete this.processingEntries[index][sourceUrl];
639 webkitResolveLocalFileSystemURL(
640 destinationUrl, function(destinationEntry) {
641 entryChangedCallback(
642 util.EntryChangedKind.CREATED, destinationEntry);
645 function(source_url, size) {
646 var entry = this.processingEntries[index][source_url];
648 this.processedBytes += size - entry.processedBytes;
649 entry.processedBytes = size;
657 if (this.deleteAfterCopy) {
667 * Copies the source entry to the target directory.
669 * @param {Entry} sourceEntry An entry to be copied.
670 * @param {DirectoryEntry} destinationEntry The entry which will contain the
672 * @param {function(string, string)} entryChangedCallback
673 * Callback invoked when an entry is created with the source url and
674 * the destination url.
675 * @param {function(string, number)} progressCallback Callback invoked
676 * periodically during the copying.
677 * @param {function()} successCallback On success.
678 * @param {function(FileOperationManager.Error)} errorCallback On error.
681 FileOperationManager.CopyTask.prototype.processEntry_ = function(
682 sourceEntry, destinationEntry, entryChangedCallback, progressCallback,
683 successCallback, errorCallback) {
684 fileOperationUtil.deduplicatePath(
685 destinationEntry, sourceEntry.name,
686 function(destinationName) {
687 if (this.cancelRequested_) {
688 errorCallback(new FileOperationManager.Error(
689 util.FileOperationErrorType.FILESYSTEM_ERROR,
690 util.createFileError(FileError.ABORT_ERR)));
693 this.cancelCallback_ = fileOperationUtil.copyTo(
694 sourceEntry, destinationEntry, destinationName,
695 entryChangedCallback, progressCallback,
697 this.cancelCallback_ = null;
701 this.cancelCallback_ = null;
702 errorCallback(new FileOperationManager.Error(
703 util.FileOperationErrorType.FILESYSTEM_ERROR, error));
710 * Task to move entries.
712 * @param {Array.<Entry>} sourceEntries Array of source entries.
713 * @param {DirectoryEntry} targetDirEntry Target directory.
715 * @extends {FileOperationManager.Task}
717 FileOperationManager.MoveTask = function(sourceEntries, targetDirEntry) {
718 FileOperationManager.Task.call(
719 this, util.FileOperationType.MOVE, sourceEntries, targetDirEntry);
723 * Extends FileOperationManager.Task.
725 FileOperationManager.MoveTask.prototype.__proto__ =
726 FileOperationManager.Task.prototype;
729 * Initializes the MoveTask.
730 * @param {function()} callback Called when the initialize is completed.
732 FileOperationManager.MoveTask.prototype.initialize = function(callback) {
733 // This may be moving from search results, where it fails if we
734 // move parent entries earlier than child entries. We should
735 // process the deepest entry first. Since move of each entry is
736 // done by a single moveTo() call, we don't need to care about the
737 // recursive traversal order.
738 this.sourceEntries.sort(function(entry1, entry2) {
739 return entry2.fullPath.length - entry1.fullPath.length;
742 this.processingEntries = [];
743 for (var i = 0; i < this.sourceEntries.length; i++) {
744 var processingEntryMap = {};
745 var entry = this.sourceEntries[i];
747 // The move should be done with updating the metadata. So here we assume
748 // all the file size is 1 byte. (Avoiding 0, so that progress bar can
750 // TODO(hidehiko): Remove this hack.
752 processingEntryMap[entry.toURL()] = entry;
753 this.processingEntries[i] = processingEntryMap;
760 * Moves all entries in the task.
762 * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback
763 * Callback invoked when an entry is changed.
764 * @param {function()} progressCallback Callback invoked periodically during
766 * @param {function()} successCallback On success.
767 * @param {function(FileOperationManager.Error)} errorCallback On error.
770 FileOperationManager.MoveTask.prototype.run = function(
771 entryChangedCallback, progressCallback, successCallback, errorCallback) {
772 if (this.sourceEntries.length == 0) {
779 function(callback, entry, index) {
780 if (this.cancelRequested_) {
781 errorCallback(new FileOperationManager.Error(
782 util.FileOperationErrorType.FILESYSTEM_ERROR,
783 util.createFileError(FileError.ABORT_ERR)));
787 FileOperationManager.MoveTask.processEntry_(
788 entry, this.targetDirEntry, entryChangedCallback,
790 // Erase the processing entry.
791 this.processingEntries[index] = {};
792 this.processedBytes++;
804 * Moves the sourceEntry to the targetDirEntry in this task.
806 * @param {Entry} sourceEntry An entry to be moved.
807 * @param {DirectoryEntry} destinationEntry The entry of the destination
809 * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback
810 * Callback invoked when an entry is changed.
811 * @param {function()} successCallback On success.
812 * @param {function(FileOperationManager.Error)} errorCallback On error.
815 FileOperationManager.MoveTask.processEntry_ = function(
816 sourceEntry, destinationEntry, entryChangedCallback, successCallback,
818 fileOperationUtil.deduplicatePath(
821 function(destinationName) {
823 destinationEntry, destinationName,
824 function(movedEntry) {
825 entryChangedCallback(util.EntryChangedKind.CREATED, movedEntry);
826 entryChangedCallback(util.EntryChangedKind.DELETED, sourceEntry);
830 errorCallback(new FileOperationManager.Error(
831 util.FileOperationErrorType.FILESYSTEM_ERROR, error));
838 * Task to create a zip archive.
840 * @param {Array.<Entry>} sourceEntries Array of source entries.
841 * @param {DirectoryEntry} targetDirEntry Target directory.
842 * @param {DirectoryEntry} zipBaseDirEntry Base directory dealt as a root
845 * @extends {FileOperationManager.Task}
847 FileOperationManager.ZipTask = function(
848 sourceEntries, targetDirEntry, zipBaseDirEntry) {
849 FileOperationManager.Task.call(
850 this, util.FileOperationType.ZIP, sourceEntries, targetDirEntry);
851 this.zipBaseDirEntry = zipBaseDirEntry;
855 * Extends FileOperationManager.Task.
857 FileOperationManager.ZipTask.prototype.__proto__ =
858 FileOperationManager.Task.prototype;
862 * Initializes the ZipTask.
863 * @param {function()} callback Called when the initialize is completed.
865 FileOperationManager.ZipTask.prototype.initialize = function(callback) {
866 var resolvedEntryMap = {};
867 var group = new AsyncUtil.Group();
868 for (var i = 0; i < this.sourceEntries.length; i++) {
869 group.add(function(index, callback) {
870 fileOperationUtil.resolveRecursively(
871 this.sourceEntries[index],
873 for (var j = 0; j < entries.length; j++)
874 resolvedEntryMap[entries[j].toURL()] = entries[j];
881 group.run(function() {
882 // For zip archiving, all the entries are processed at once.
883 this.processingEntries = [resolvedEntryMap];
886 for (var url in resolvedEntryMap)
887 this.totalBytes += resolvedEntryMap[url].size;
894 * Runs a zip file creation task.
896 * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback
897 * Callback invoked when an entry is changed.
898 * @param {function()} progressCallback Callback invoked periodically during
900 * @param {function()} successCallback On complete.
901 * @param {function(FileOperationManager.Error)} errorCallback On error.
904 FileOperationManager.ZipTask.prototype.run = function(
905 entryChangedCallback, progressCallback, successCallback, errorCallback) {
906 // TODO(hidehiko): we should localize the name.
907 var destName = 'Archive';
908 if (this.sourceEntries.length == 1) {
909 var entryPath = this.sourceEntries[0].fullPath;
910 var i = entryPath.lastIndexOf('/');
911 var basename = (i < 0) ? entryPath : entryPath.substr(i + 1);
912 i = basename.lastIndexOf('.');
913 destName = ((i < 0) ? basename : basename.substr(0, i));
916 fileOperationUtil.deduplicatePath(
917 this.targetDirEntry, destName + '.zip',
919 // TODO: per-entry zip progress update with accurate byte count.
920 // For now just set completedBytes to same value as totalBytes so
921 // that the progress bar is full.
922 this.processedBytes = this.totalBytes;
925 // The number of elements in processingEntries is 1. See also
928 for (var url in this.processingEntries[0])
929 entries.push(this.processingEntries[0][url]);
931 fileOperationUtil.zipSelection(
933 this.zipBaseDirEntry,
936 entryChangedCallback(util.EntryChangedKind.CREATE, entry);
940 errorCallback(new FileOperationManager.Error(
941 util.FileOperationErrorType.FILESYSTEM_ERROR, error));
948 * Error class used to report problems with a copy operation.
949 * If the code is UNEXPECTED_SOURCE_FILE, data should be a path of the file.
950 * If the code is TARGET_EXISTS, data should be the existing Entry.
951 * If the code is FILESYSTEM_ERROR, data should be the FileError.
953 * @param {util.FileOperationErrorType} code Error type.
954 * @param {string|Entry|FileError} data Additional data.
957 FileOperationManager.Error = function(code, data) {
962 // FileOperationManager methods.
965 * Called before a new method is run in the manager. Prepares the manager's
966 * state for running a new method.
968 FileOperationManager.prototype.willRunNewMethod = function() {
969 // Cancel any pending close actions so the file copy manager doesn't go away.
970 if (this.unloadTimeout_)
971 clearTimeout(this.unloadTimeout_);
972 this.unloadTimeout_ = null;
976 * @return {Object} Status object.
978 FileOperationManager.prototype.getStatus = function() {
979 // TODO(hidehiko): Reorganize the structure when delete queue is merged
980 // into copy task queue.
982 // Set to util.FileOperationType if all the running/pending tasks is
983 // the same kind of task.
986 // The number of entries to be processed.
987 numRemainingItems: 0,
989 // The total number of bytes to be processed.
992 // The number of bytes.
995 // Available if numRemainingItems == 1. Pointing to an Entry which is
997 processingEntry: task.getSingleEntry()
1001 this.copyTasks_.length > 0 ? this.copyTasks_[0].operationType : null;
1003 for (var i = 0; i < this.copyTasks_.length; i++) {
1004 task = this.copyTasks_[i];
1005 if (task.operationType != operationType)
1006 operationType = null;
1008 // Assuming the number of entries is small enough, count every time.
1009 result.numRemainingItems += task.countRemainingItems();
1010 result.totalBytes += task.totalBytes;
1011 result.processedBytes += task.processedBytes;
1014 result.operationType = operationType;
1019 * Adds an event listener for the tasks.
1020 * @param {string} type The name of the event.
1021 * @param {function(Event)} handler The handler for the event.
1022 * This is called when the event is dispatched.
1024 FileOperationManager.prototype.addEventListener = function(type, handler) {
1025 this.eventRouter_.addEventListener(type, handler);
1029 * Removes an event listener for the tasks.
1030 * @param {string} type The name of the event.
1031 * @param {function(Event)} handler The handler to be removed.
1033 FileOperationManager.prototype.removeEventListener = function(type, handler) {
1034 this.eventRouter_.removeEventListener(type, handler);
1038 * Says if there are any tasks in the queue.
1039 * @return {boolean} True, if there are any tasks.
1041 FileOperationManager.prototype.hasQueuedTasks = function() {
1042 return this.copyTasks_.length > 0 || this.deleteTasks_.length > 0;
1046 * Unloads the host page in 5 secs of idling. Need to be called
1047 * each time this.copyTasks_.length or this.deleteTasks_.length
1052 FileOperationManager.prototype.maybeScheduleCloseBackgroundPage_ = function() {
1053 if (!this.hasQueuedTasks()) {
1054 if (this.unloadTimeout_ === null)
1055 this.unloadTimeout_ = setTimeout(maybeCloseBackgroundPage, 5000);
1056 } else if (this.unloadTimeout_) {
1057 clearTimeout(this.unloadTimeout_);
1058 this.unloadTimeout_ = null;
1063 * Completely clear out the copy queue, either because we encountered an error
1064 * or completed successfully.
1068 FileOperationManager.prototype.resetQueue_ = function() {
1069 this.copyTasks_ = [];
1070 this.maybeScheduleCloseBackgroundPage_();
1074 * Requests the specified task to be canceled.
1075 * @param {string} taskId ID of task to be canceled.
1077 FileOperationManager.prototype.requestTaskCancel = function(taskId) {
1079 for (var i = 0; i < this.copyTasks_.length; i++) {
1080 task = this.copyTasks_[i];
1081 if (task.taskId !== taskId)
1083 task.requestCancel();
1084 // If the task is not on progress, remove it immediately.
1086 this.eventRouter_.sendProgressEvent('CANCELED',
1089 this.copyTasks_.splice(i, 1);
1092 for (var i = 0; i < this.deleteTasks_.length; i++) {
1093 task = this.deleteTasks_[i];
1094 if (task.taskId !== taskId)
1096 task.requestCancel();
1097 // If the task is not on progress, remove it immediately.
1099 this.eventRouter_.sendDeleteEvent(
1101 task.entries.map(function(entry) {
1102 return util.makeFilesystemUrl(entry.fullPath);
1105 this.deleteTasks_.splice(i, 1);
1113 * @param {Array.<string>} sourcePaths Path of the source files.
1114 * @param {string} targetPath The destination path of the target directory.
1115 * @param {boolean} isMove True if the operation is "move", otherwise (i.e.
1116 * if the operation is "copy") false.
1118 FileOperationManager.prototype.paste = function(
1119 sourcePaths, targetPath, isMove) {
1120 // Do nothing if sourcePaths is empty.
1121 if (sourcePaths.length == 0)
1124 var errorCallback = function(error) {
1125 this.eventRouter_.sendProgressEvent(
1128 this.generateTaskId_(null),
1129 new FileOperationManager.Error(
1130 util.FileOperationErrorType.FILESYSTEM_ERROR, error));
1133 var targetEntry = null;
1136 // Resolve paths to entries.
1137 var resolveGroup = new AsyncUtil.Group();
1138 resolveGroup.add(function(callback) {
1139 webkitResolveLocalFileSystemURL(
1140 util.makeFilesystemUrl(targetPath),
1142 if (!entry.isDirectory) {
1143 // Found a non directory entry.
1144 errorCallback(util.createFileError(FileError.TYPE_MISMATCH_ERR));
1148 targetEntry = entry;
1154 for (var i = 0; i < sourcePaths.length; i++) {
1155 resolveGroup.add(function(sourcePath, callback) {
1156 webkitResolveLocalFileSystemURL(
1157 util.makeFilesystemUrl(sourcePath),
1159 entries.push(entry);
1163 }.bind(this, sourcePaths[i]));
1166 resolveGroup.run(function() {
1168 // Moving to the same directory is a redundant operation.
1169 entries = entries.filter(function(entry) {
1170 return targetEntry.fullPath + '/' + entry.name != entry.fullPath;
1173 // Do nothing, if we have no entries to be moved.
1174 if (entries.length == 0)
1178 this.queueCopy_(targetEntry, entries, isMove);
1183 * Checks if the move operation is available between the given two locations.
1185 * @param {DirectoryEntry} sourceEntry An entry from the source.
1186 * @param {DirectoryEntry} targetDirEntry Directory entry for the target.
1187 * @return {boolean} Whether we can move from the source to the target.
1189 FileOperationManager.prototype.isMovable = function(sourceEntry,
1191 return (PathUtil.isDriveBasedPath(sourceEntry.fullPath) &&
1192 PathUtil.isDriveBasedPath(targetDirEntry.fullPath)) ||
1193 (PathUtil.getRootPath(sourceEntry.fullPath) ==
1194 PathUtil.getRootPath(targetDirEntry.fullPath));
1198 * Initiate a file copy.
1200 * @param {DirectoryEntry} targetDirEntry Target directory.
1201 * @param {Array.<Entry>} entries Entries to copy.
1202 * @param {boolean} isMove In case of move.
1203 * @return {FileOperationManager.Task} Copy task.
1206 FileOperationManager.prototype.queueCopy_ = function(
1207 targetDirEntry, entries, isMove) {
1208 // When copying files, null can be specified as source directory.
1211 if (this.isMovable(entries[0], targetDirEntry)) {
1212 task = new FileOperationManager.MoveTask(entries, targetDirEntry);
1214 task = new FileOperationManager.CopyTask(entries, targetDirEntry);
1215 task.deleteAfterCopy = true;
1218 task = new FileOperationManager.CopyTask(entries, targetDirEntry);
1221 task.taskId = this.generateTaskId_();
1222 task.initialize(function() {
1223 this.copyTasks_.push(task);
1224 this.maybeScheduleCloseBackgroundPage_();
1225 this.eventRouter_.sendProgressEvent('BEGIN', task.getStatus(), task.taskId);
1226 if (this.copyTasks_.length == 1)
1227 this.serviceAllTasks_();
1234 * Service all pending tasks, as well as any that might appear during the
1239 FileOperationManager.prototype.serviceAllTasks_ = function() {
1240 if (!this.copyTasks_.length) {
1241 // All tasks have been serviced, clean up and exit.
1246 var onTaskProgress = function() {
1247 this.eventRouter_.sendProgressEvent('PROGRESS',
1248 this.copyTasks_[0].getStatus(),
1249 this.copyTasks_[0].taskId);
1252 var onEntryChanged = function(kind, entry) {
1253 this.eventRouter_.sendEntryChangedEvent(kind, entry);
1256 var onTaskError = function(err) {
1257 var task = this.copyTasks_.shift();
1258 var reason = err.data.code === FileError.ABORT_ERR ? 'CANCELED' : 'ERROR';
1259 this.eventRouter_.sendProgressEvent(reason,
1263 this.serviceAllTasks_();
1266 var onTaskSuccess = function() {
1267 // The task at the front of the queue is completed. Pop it from the queue.
1268 var task = this.copyTasks_.shift();
1269 this.maybeScheduleCloseBackgroundPage_();
1270 this.eventRouter_.sendProgressEvent('SUCCESS',
1273 this.serviceAllTasks_();
1276 var nextTask = this.copyTasks_[0];
1277 this.eventRouter_.sendProgressEvent('PROGRESS',
1278 nextTask.getStatus(),
1280 nextTask.run(onEntryChanged, onTaskProgress, onTaskSuccess, onTaskError);
1284 * Timeout before files are really deleted (to allow undo).
1286 FileOperationManager.DELETE_TIMEOUT = 30 * 1000;
1289 * Schedules the files deletion.
1291 * @param {Array.<Entry>} entries The entries.
1293 FileOperationManager.prototype.deleteEntries = function(entries) {
1296 taskId: this.generateTaskId_()
1298 this.deleteTasks_.push(task);
1299 this.eventRouter_.sendDeleteEvent('BEGIN', entries.map(function(entry) {
1300 return util.makeFilesystemUrl(entry.fullPath);
1302 this.maybeScheduleCloseBackgroundPage_();
1303 if (this.deleteTasks_.length == 1)
1304 this.serviceAllDeleteTasks_();
1308 * Service all pending delete tasks, as well as any that might appear during the
1311 * Must not be called if there is an in-flight delete task.
1315 FileOperationManager.prototype.serviceAllDeleteTasks_ = function() {
1316 // Returns the urls of the given task's entries.
1317 var getTaskUrls = function(task) {
1318 return task.entries.map(function(entry) {
1319 return util.makeFilesystemUrl(entry.fullPath);
1323 var onTaskSuccess = function() {
1324 var urls = getTaskUrls(this.deleteTasks_[0]);
1325 var taskId = this.deleteTasks_[0].taskId;
1326 this.deleteTasks_.shift();
1327 this.eventRouter_.sendDeleteEvent('SUCCESS', urls, taskId);
1329 if (!this.deleteTasks_.length) {
1330 // All tasks have been serviced, clean up and exit.
1331 this.maybeScheduleCloseBackgroundPage_();
1335 var nextTask = this.deleteTasks_[0];
1336 this.eventRouter_.sendDeleteEvent('PROGRESS',
1339 this.serviceDeleteTask_(nextTask, onTaskSuccess, onTaskFailure);
1342 var onTaskFailure = function(error) {
1343 var urls = getTaskUrls(this.deleteTasks_[0]);
1344 var taskId = this.deleteTasks_[0].taskId;
1345 this.deleteTasks_ = [];
1346 this.eventRouter_.sendDeleteEvent('ERROR',
1349 this.maybeScheduleCloseBackgroundPage_();
1352 this.serviceDeleteTask_(this.deleteTasks_[0], onTaskSuccess, onTaskFailure);
1356 * Performs the deletion.
1358 * @param {Object} task The delete task (see deleteEntries function).
1359 * @param {function()} successCallback Callback run on success.
1360 * @param {function(FileOperationManager.Error)} errorCallback Callback run on
1364 FileOperationManager.prototype.serviceDeleteTask_ = function(
1365 task, successCallback, errorCallback) {
1366 var downcount = task.entries.length;
1367 if (downcount == 0) {
1372 var filesystemError = null;
1373 var onComplete = function() {
1374 if (--downcount > 0)
1377 // All remove operations are processed. Run callback.
1378 if (filesystemError) {
1379 errorCallback(new FileOperationManager.Error(
1380 util.FileOperationErrorType.FILESYSTEM_ERROR, filesystemError));
1386 for (var i = 0; i < task.entries.length; i++) {
1387 var entry = task.entries[i];
1388 util.removeFileOrDirectory(
1390 function(currentEntry) {
1391 this.eventRouter_.sendEntryChangedEvent(
1392 util.EntryChangedKind.DELETED, currentEntry);
1394 }.bind(this, entry),
1396 if (!filesystemError)
1397 filesystemError = error;
1404 * Creates a zip file for the selection of files.
1406 * @param {Entry} dirEntry The directory containing the selection.
1407 * @param {Array.<Entry>} selectionEntries The selected entries.
1409 FileOperationManager.prototype.zipSelection = function(
1410 dirEntry, selectionEntries) {
1411 var zipTask = new FileOperationManager.ZipTask(
1412 selectionEntries, dirEntry, dirEntry);
1413 zipTask.taskId = this.generateTaskId_(this.copyTasks_);
1415 zipTask.initialize(function() {
1416 this.copyTasks_.push(zipTask);
1417 this.eventRouter_.sendProgressEvent('BEGIN',
1418 zipTask.getStatus(),
1420 if (this.copyTasks_.length == 1)
1421 this.serviceAllTasks_();
1426 * Generates new task ID.
1428 * @return {string} New task ID.
1431 FileOperationManager.prototype.generateTaskId_ = function() {
1432 return 'file-operation-' + this.taskIdCounter_++;