| // Copyright 2013 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /** |
| * @constructor |
| * @struct |
| * @suppress {checkStructDictInheritance} |
| * @extends {cr.EventTarget} |
| */ |
| function FileOperationManager() { |
| this.copyTasks_ = []; |
| this.deleteTasks_ = []; |
| this.taskIdCounter_ = 0; |
| this.eventRouter_ = new fileOperationUtil.EventRouter(); |
| } |
| |
| /** |
| * Adds an event listener for the tasks. |
| * @param {string} type The name of the event. |
| * @param {EventListenerType} handler The handler for the event. This is called |
| * when the event is dispatched. |
| * @override |
| */ |
| FileOperationManager.prototype.addEventListener = function(type, handler) { |
| this.eventRouter_.addEventListener(type, handler); |
| }; |
| |
| /** |
| * Removes an event listener for the tasks. |
| * @param {string} type The name of the event. |
| * @param {EventListenerType} handler The handler to be removed. |
| * @override |
| */ |
| FileOperationManager.prototype.removeEventListener = function(type, handler) { |
| this.eventRouter_.removeEventListener(type, handler); |
| }; |
| |
| /** |
| * Says if there are any tasks in the queue. |
| * @return {boolean} True, if there are any tasks. |
| */ |
| FileOperationManager.prototype.hasQueuedTasks = function() { |
| return this.copyTasks_.length > 0 || this.deleteTasks_.length > 0; |
| }; |
| |
| /** |
| * Completely clear out the copy queue, either because we encountered an error |
| * or completed successfully. |
| * |
| * @private |
| */ |
| FileOperationManager.prototype.resetQueue_ = function() { |
| this.copyTasks_ = []; |
| }; |
| |
| /** |
| * Requests the specified task to be canceled. |
| * @param {string} taskId ID of task to be canceled. |
| */ |
| FileOperationManager.prototype.requestTaskCancel = function(taskId) { |
| var task = null; |
| for (var i = 0; i < this.copyTasks_.length; i++) { |
| task = this.copyTasks_[i]; |
| if (task.taskId !== taskId) |
| continue; |
| task.requestCancel(); |
| // If the task is not on progress, remove it immediately. |
| if (i !== 0) { |
| this.eventRouter_.sendProgressEvent( |
| fileOperationUtil.EventRouter.EventType.CANCELED, |
| task.getStatus(), |
| task.taskId); |
| this.copyTasks_.splice(i, 1); |
| } |
| } |
| for (var i = 0; i < this.deleteTasks_.length; i++) { |
| task = this.deleteTasks_[i]; |
| if (task.taskId !== taskId) |
| continue; |
| task.cancelRequested = true; |
| // If the task is not on progress, remove it immediately. |
| if (i !== 0) { |
| this.eventRouter_.sendDeleteEvent( |
| fileOperationUtil.EventRouter.EventType.CANCELED, task); |
| this.deleteTasks_.splice(i, 1); |
| } |
| } |
| }; |
| |
| /** |
| * Filters the entry in the same directory |
| * |
| * @param {Array.<Entry>} sourceEntries Entries of the source files. |
| * @param {DirectoryEntry} targetEntry The destination entry of the target |
| * directory. |
| * @param {boolean} isMove True if the operation is "move", otherwise (i.e. |
| * if the operation is "copy") false. |
| * @return {Promise} Promise fulfilled with the filtered entry. This is not |
| * rejected. |
| */ |
| FileOperationManager.prototype.filterSameDirectoryEntry = function( |
| sourceEntries, targetEntry, isMove) { |
| if (!isMove) |
| return Promise.resolve(sourceEntries); |
| // Utility function to concat arrays. |
| var compactArrays = function(arrays) { |
| return arrays.filter(function(element) { return !!element; }); |
| }; |
| // Call processEntry for each item of entries. |
| var processEntries = function(entries) { |
| var promises = entries.map(processFileOrDirectoryEntries); |
| return Promise.all(promises).then(compactArrays); |
| }; |
| // Check all file entries and keeps only those need sharing operation. |
| var processFileOrDirectoryEntries = function(entry) { |
| return new Promise(function(resolve) { |
| entry.getParent(function(inParentEntry) { |
| if (!util.isSameEntry(inParentEntry, targetEntry)) |
| resolve(entry); |
| else |
| resolve(null); |
| }, function(error) { |
| console.error(error.stack || error); |
| resolve(null); |
| }); |
| }); |
| }; |
| return processEntries(sourceEntries); |
| }; |
| |
| /** |
| * Kick off pasting. |
| * |
| * @param {Array.<Entry>} sourceEntries Entries of the source files. |
| * @param {DirectoryEntry} targetEntry The destination entry of the target |
| * directory. |
| * @param {boolean} isMove True if the operation is "move", otherwise (i.e. |
| * if the operation is "copy") false. |
| * @param {string=} opt_taskId If the corresponding item has already created |
| * at another places, we need to specify the ID of the item. If the |
| * item is not created, FileOperationManager generates new ID. |
| */ |
| FileOperationManager.prototype.paste = function( |
| sourceEntries, targetEntry, isMove, opt_taskId) { |
| // Do nothing if sourceEntries is empty. |
| if (sourceEntries.length === 0) |
| return; |
| |
| this.filterSameDirectoryEntry(sourceEntries, targetEntry, isMove).then( |
| function(entries) { |
| if (entries.length === 0) |
| return; |
| this.queueCopy_(targetEntry, entries, isMove, opt_taskId); |
| }.bind(this)).catch(function(error) { |
| console.error(error.stack || error); |
| }); |
| }; |
| |
| /** |
| * Initiate a file copy. When copying files, null can be specified as source |
| * directory. |
| * |
| * @param {DirectoryEntry} targetDirEntry Target directory. |
| * @param {Array.<Entry>} entries Entries to copy. |
| * @param {boolean} isMove In case of move. |
| * @param {string=} opt_taskId If the corresponding item has already created |
| * at another places, we need to specify the ID of the item. If the |
| * item is not created, FileOperationManager generates new ID. |
| * @private |
| */ |
| FileOperationManager.prototype.queueCopy_ = function( |
| targetDirEntry, entries, isMove, opt_taskId) { |
| var task; |
| var taskId = opt_taskId || this.generateTaskId(); |
| if (isMove) { |
| // When moving between different volumes, moving is implemented as a copy |
| // and delete. This is because moving between volumes is slow, and moveTo() |
| // is not cancellable nor provides progress feedback. |
| if (util.isSameFileSystem(entries[0].filesystem, |
| targetDirEntry.filesystem)) { |
| task = new fileOperationUtil.MoveTask(taskId, entries, targetDirEntry); |
| } else { |
| task = |
| new fileOperationUtil.CopyTask(taskId, entries, targetDirEntry, true); |
| } |
| } else { |
| task = |
| new fileOperationUtil.CopyTask(taskId, entries, targetDirEntry, false); |
| } |
| |
| this.eventRouter_.sendProgressEvent( |
| fileOperationUtil.EventRouter.EventType.BEGIN, |
| task.getStatus(), |
| task.taskId); |
| task.initialize(function() { |
| this.copyTasks_.push(task); |
| if (this.copyTasks_.length === 1) |
| this.serviceAllTasks_(); |
| }.bind(this)); |
| }; |
| |
| /** |
| * Service all pending tasks, as well as any that might appear during the |
| * copy. |
| * |
| * @private |
| */ |
| FileOperationManager.prototype.serviceAllTasks_ = function() { |
| if (!this.copyTasks_.length) { |
| // All tasks have been serviced, clean up and exit. |
| chrome.power.releaseKeepAwake(); |
| this.resetQueue_(); |
| return; |
| } |
| |
| // Prevent the system from sleeping while copy is in progress. |
| chrome.power.requestKeepAwake('system'); |
| |
| var onTaskProgress = function() { |
| this.eventRouter_.sendProgressEvent( |
| fileOperationUtil.EventRouter.EventType.PROGRESS, |
| this.copyTasks_[0].getStatus(), |
| this.copyTasks_[0].taskId); |
| }.bind(this); |
| |
| var onEntryChanged = function(kind, entry) { |
| this.eventRouter_.sendEntryChangedEvent(kind, entry); |
| }.bind(this); |
| |
| var onTaskError = function(err) { |
| var task = this.copyTasks_.shift(); |
| var reason = err.data.name === util.FileError.ABORT_ERR ? |
| fileOperationUtil.EventRouter.EventType.CANCELED : |
| fileOperationUtil.EventRouter.EventType.ERROR; |
| this.eventRouter_.sendProgressEvent(reason, |
| task.getStatus(), |
| task.taskId, |
| err); |
| this.serviceAllTasks_(); |
| }.bind(this); |
| |
| var onTaskSuccess = function() { |
| // The task at the front of the queue is completed. Pop it from the queue. |
| var task = this.copyTasks_.shift(); |
| this.eventRouter_.sendProgressEvent( |
| fileOperationUtil.EventRouter.EventType.SUCCESS, |
| task.getStatus(), |
| task.taskId); |
| this.serviceAllTasks_(); |
| }.bind(this); |
| |
| var nextTask = this.copyTasks_[0]; |
| this.eventRouter_.sendProgressEvent( |
| fileOperationUtil.EventRouter.EventType.PROGRESS, |
| nextTask.getStatus(), |
| nextTask.taskId); |
| nextTask.run(onEntryChanged, onTaskProgress, onTaskSuccess, onTaskError); |
| }; |
| |
| /** |
| * Timeout before files are really deleted (to allow undo). |
| */ |
| FileOperationManager.DELETE_TIMEOUT = 30 * 1000; |
| |
| /** |
| * Schedules the files deletion. |
| * |
| * @param {Array.<Entry>} entries The entries. |
| */ |
| FileOperationManager.prototype.deleteEntries = function(entries) { |
| // TODO(hirono): Make fileOperationUtil.DeleteTask. |
| var task = Object.preventExtensions({ |
| entries: entries, |
| taskId: this.generateTaskId(), |
| entrySize: {}, |
| totalBytes: 0, |
| processedBytes: 0, |
| cancelRequested: false |
| }); |
| |
| // Obtains entry size and sum them up. |
| var group = new AsyncUtil.Group(); |
| for (var i = 0; i < task.entries.length; i++) { |
| group.add(function(entry, callback) { |
| entry.getMetadata(function(metadata) { |
| var index = task.entries.indexOf(entries); |
| task.entrySize[entry.toURL()] = metadata.size; |
| task.totalBytes += metadata.size; |
| callback(); |
| }, function() { |
| // Fail to obtain the metadata. Use fake value 1. |
| task.entrySize[entry.toURL()] = 1; |
| task.totalBytes += 1; |
| callback(); |
| }); |
| }.bind(this, task.entries[i])); |
| } |
| |
| // Add a delete task. |
| group.run(function() { |
| this.deleteTasks_.push(task); |
| this.eventRouter_.sendDeleteEvent( |
| fileOperationUtil.EventRouter.EventType.BEGIN, task); |
| if (this.deleteTasks_.length === 1) |
| this.serviceAllDeleteTasks_(); |
| }.bind(this)); |
| }; |
| |
| /** |
| * Service all pending delete tasks, as well as any that might appear during the |
| * deletion. |
| * |
| * Must not be called if there is an in-flight delete task. |
| * |
| * @private |
| */ |
| FileOperationManager.prototype.serviceAllDeleteTasks_ = function() { |
| this.serviceDeleteTask_( |
| this.deleteTasks_[0], |
| function() { |
| this.deleteTasks_.shift(); |
| if (this.deleteTasks_.length) |
| this.serviceAllDeleteTasks_(); |
| }.bind(this)); |
| }; |
| |
| /** |
| * Performs the deletion. |
| * |
| * @param {Object} task The delete task (see deleteEntries function). |
| * @param {function()} callback Callback run on task end. |
| * @private |
| */ |
| FileOperationManager.prototype.serviceDeleteTask_ = function(task, callback) { |
| var queue = new AsyncUtil.Queue(); |
| |
| // Delete each entry. |
| var error = null; |
| var deleteOneEntry = function(inCallback) { |
| if (!task.entries.length || task.cancelRequested || error) { |
| inCallback(); |
| return; |
| } |
| this.eventRouter_.sendDeleteEvent( |
| fileOperationUtil.EventRouter.EventType.PROGRESS, task); |
| util.removeFileOrDirectory( |
| task.entries[0], |
| function() { |
| this.eventRouter_.sendEntryChangedEvent( |
| util.EntryChangedKind.DELETED, task.entries[0]); |
| task.processedBytes += task.entrySize[task.entries[0].toURL()]; |
| task.entries.shift(); |
| deleteOneEntry(inCallback); |
| }.bind(this), |
| function(inError) { |
| error = inError; |
| inCallback(); |
| }.bind(this)); |
| }.bind(this); |
| queue.run(deleteOneEntry); |
| |
| // Send an event and finish the async steps. |
| queue.run(function(inCallback) { |
| var EventType = fileOperationUtil.EventRouter.EventType; |
| var reason; |
| if (error) |
| reason = EventType.ERROR; |
| else if (task.cancelRequested) |
| reason = EventType.CANCELED; |
| else |
| reason = EventType.SUCCESS; |
| this.eventRouter_.sendDeleteEvent(reason, task); |
| inCallback(); |
| callback(); |
| }.bind(this)); |
| }; |
| |
| /** |
| * Creates a zip file for the selection of files. |
| * |
| * @param {!DirectoryEntry} dirEntry The directory containing the selection. |
| * @param {Array.<Entry>} selectionEntries The selected entries. |
| */ |
| FileOperationManager.prototype.zipSelection = function( |
| dirEntry, selectionEntries) { |
| var zipTask = new fileOperationUtil.ZipTask( |
| this.generateTaskId(), selectionEntries, dirEntry, dirEntry); |
| this.eventRouter_.sendProgressEvent( |
| fileOperationUtil.EventRouter.EventType.BEGIN, |
| zipTask.getStatus(), |
| zipTask.taskId); |
| zipTask.initialize(function() { |
| this.copyTasks_.push(zipTask); |
| if (this.copyTasks_.length == 1) |
| this.serviceAllTasks_(); |
| }.bind(this)); |
| }; |
| |
| /** |
| * Generates new task ID. |
| * |
| * @return {string} New task ID. |
| */ |
| FileOperationManager.prototype.generateTaskId = function() { |
| return 'file-operation-' + this.taskIdCounter_++; |
| }; |