| // Copyright (c) 2012 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. |
| |
| 'use strict'; |
| |
| /** |
| * Utilities for FileCopyManager. |
| */ |
| var fileOperationUtil = {}; |
| |
| /** |
| * Simple wrapper for util.deduplicatePath. On error, this method translates |
| * the FileError to FileCopyManager.Error object. |
| * |
| * @param {DirectoryEntry} dirEntry The target directory entry. |
| * @param {string} relativePath The path to be deduplicated. |
| * @param {function(string)} successCallback Callback run with the deduplicated |
| * path on success. |
| * @param {function(FileCopyManager.Error)} errorCallback Callback run on error. |
| */ |
| fileOperationUtil.deduplicatePath = function( |
| dirEntry, relativePath, successCallback, errorCallback) { |
| util.deduplicatePath( |
| dirEntry, relativePath, successCallback, |
| function(err) { |
| var onFileSystemError = function(error) { |
| errorCallback(new FileCopyManager.Error( |
| util.FileOperationErrorType.FILESYSTEM_ERROR, error)); |
| }; |
| |
| if (err.code == FileError.PATH_EXISTS_ERR) { |
| // Failed to uniquify the file path. There should be an existing |
| // entry, so return the error with it. |
| util.resolvePath( |
| dirEntry, relativePath, |
| function(entry) { |
| errorCallback(new FileCopyManager.Error( |
| util.FileOperationErrorType.TARGET_EXISTS, entry)); |
| }, |
| onFileSystemError); |
| return; |
| } |
| onFileSystemError(err); |
| }); |
| }; |
| |
| /** |
| * Sets last modified date to the entry. |
| * @param {Entry} entry The entry to which the last modified is set. |
| * @param {Date} modificationTime The last modified time. |
| */ |
| fileOperationUtil.setLastModified = function(entry, modificationTime) { |
| chrome.fileBrowserPrivate.setLastModified( |
| entry.toURL(), '' + Math.round(modificationTime.getTime() / 1000)); |
| }; |
| |
| /** |
| * Copies a file a) from Drive to local, b) from local to Drive, or c) from |
| * Drive to Drive. |
| * Currently, we need to take care about following two things for Drive: |
| * |
| * 1) Copying hosted document. |
| * In theory, it is impossible to actual copy a hosted document to other |
| * file system. Thus, instead, Drive file system backend creates a JSON file |
| * referring to the hosted document. Also, when it is uploaded by copyTo, |
| * the hosted document is copied on the server. Note that, this doesn't work |
| * when a user creates a file by FileWriter (as copyFileEntry_ does). |
| * |
| * 2) File transfer between local and Drive server. |
| * There are two directions of file transfer; from local to Drive and from |
| * Drive to local. |
| * The file transfer from local to Drive is done as a part of file system |
| * background sync (kicked after the copy operation is done). So we don't need |
| * to take care about it here. To copy the file from Drive to local (or Drive |
| * to Drive with GData WAPI), we need to download the file content (if it is |
| * not locally cached). During the downloading, we can listen the periodical |
| * updating and cancel the downloding via private API. |
| * |
| * This function supports progress updating and cancelling partially. |
| * Unfortunately, FileEntry.copyTo doesn't support progress updating nor |
| * cancelling, so we support them only during file downloading. |
| * |
| * Note: we're planning to move copyTo logic into c++ side. crbug.com/261492 |
| * |
| * @param {FileEntry} source The entry of the file to be copied. |
| * @param {DirectoryEntry} parent The entry of the destination directory. |
| * @param {string} newName The name of the copied file. |
| * @param {function(FileEntry, number)} progressCallback Callback periodically |
| * invoked during file transfer with the source and the number of |
| * transferred bytes from the last call. |
| * @param {function(FileEntry)} successCallback Callback invoked when the |
| * file copy is successfully done with the entry of the copied file. |
| * @param {function(FileError)} errorCallback Callback invoked when an error |
| * is found. |
| * @return {function()} Callback to cancel the current file copy operation. |
| * When the cancel is done, errorCallback will be called. The returned |
| * callback must not be called more than once. |
| */ |
| fileOperationUtil.copyFileOnDrive = function( |
| source, parent, newName, progressCallback, successCallback, errorCallback) { |
| // Set to true when cancel is requested. |
| var cancelRequested = false; |
| var cancelCallback = null; |
| |
| var onCopyToCompleted = null; |
| |
| // Progress callback. |
| // Because the uploading the file from local cache to Drive server will be |
| // done as a part of background Drive file system sync, so for this copy |
| // operation, what we need to take care about is only file downloading. |
| var numTransferredBytes = 0; |
| if (PathUtil.isDriveBasedPath(source.fullPath)) { |
| var sourceUrl = source.toURL(); |
| var sourcePath = util.extractFilePath(sourceUrl); |
| var onFileTransfersUpdated = function(statusList) { |
| for (var i = 0; i < statusList.length; i++) { |
| var status = statusList[i]; |
| |
| // Comparing urls is unreliable, since they may use different |
| // url encoding schemes (eg. rfc2396 vs. rfc3986). |
| var filePath = util.extractFilePath(status.fileUrl); |
| if (filePath == sourcePath) { |
| var processed = status.processed; |
| if (processed > numTransferredBytes) { |
| progressCallback(source, processed - numTransferredBytes); |
| numTransferredBytes = processed; |
| } |
| return; |
| } |
| } |
| }; |
| |
| // Subscribe to listen file transfer updating notifications. |
| chrome.fileBrowserPrivate.onFileTransfersUpdated.addListener( |
| onFileTransfersUpdated); |
| |
| // Currently, we do NOT upload the file during the copy operation. |
| // It will be done as a part of file system sync after copy operation. |
| // So, we can cancel only file downloading. |
| cancelCallback = function() { |
| chrome.fileBrowserPrivate.cancelFileTransfers( |
| [sourceUrl], function() {}); |
| }; |
| |
| // We need to clean up on copyTo completion regardless if it is |
| // successfully done or not. |
| onCopyToCompleted = function() { |
| cancelCallback = null; |
| chrome.fileBrowserPrivate.onFileTransfersUpdated.removeListener( |
| onFileTransfersUpdated); |
| }; |
| } |
| |
| source.copyTo( |
| parent, newName, |
| function(entry) { |
| if (onCopyToCompleted) |
| onCopyToCompleted(); |
| |
| if (cancelRequested) { |
| errorCallback(util.createFileError(FileError.ABORT_ERR)); |
| return; |
| } |
| |
| entry.getMetadata(function(metadata) { |
| if (metadata.size > numTransferredBytes) |
| progressCallback(source, metadata.size - numTransferredBytes); |
| successCallback(entry); |
| }, errorCallback); |
| }, |
| function(error) { |
| if (onCopyToCompleted) |
| onCopyToCompleted(); |
| |
| errorCallback(error); |
| }); |
| |
| return function() { |
| cancelRequested = true; |
| if (cancelCallback) { |
| cancelCallback(); |
| cancelCallback = null; |
| } |
| }; |
| }; |
| |
| /** |
| * Thin wrapper of chrome.fileBrowserPrivate.zipSelection to adapt its |
| * interface similar to copyTo(). |
| * |
| * @param {Array.<Entry>} sources The array of entries to be archived. |
| * @param {DirectoryEntry} parent The entry of the destination directory. |
| * @param {string} newName The name of the archive to be created. |
| * @param {function(FileEntry)} successCallback Callback invoked when the |
| * operation is successfully done with the entry of the created archive. |
| * @param {function(FileError)} errorCallback Callback invoked when an error |
| * is found. |
| */ |
| fileOperationUtil.zipSelection = function( |
| sources, parent, newName, successCallback, errorCallback) { |
| chrome.fileBrowserPrivate.zipSelection( |
| parent.toURL(), |
| sources.map(function(e) { return e.toURL(); }), |
| newName, function(success) { |
| if (!success) { |
| // Failed to create a zip archive. |
| errorCallback( |
| util.createFileError(FileError.INVALID_MODIFICATION_ERR)); |
| return; |
| } |
| |
| // Returns the created entry via callback. |
| parent.getFile( |
| newName, {create: false}, successCallback, errorCallback); |
| }); |
| }; |
| |
| /** |
| * @constructor |
| */ |
| function FileCopyManager() { |
| this.copyTasks_ = []; |
| this.deleteTasks_ = []; |
| this.cancelObservers_ = []; |
| this.cancelRequested_ = false; |
| this.cancelCallback_ = null; |
| this.unloadTimeout_ = null; |
| |
| this.eventRouter_ = new FileCopyManager.EventRouter(); |
| } |
| |
| /** |
| * Get FileCopyManager instance. In case is hasn't been initialized, a new |
| * instance is created. |
| * |
| * @return {FileCopyManager} A FileCopyManager instance. |
| */ |
| FileCopyManager.getInstance = function() { |
| if (!FileCopyManager.instance_) |
| FileCopyManager.instance_ = new FileCopyManager(); |
| |
| return FileCopyManager.instance_; |
| }; |
| |
| /** |
| * Manages cr.Event dispatching. |
| * Currently this can send three types of events: "copy-progress", |
| * "copy-operation-completed" and "delete". |
| * |
| * TODO(hidehiko): Reorganize the event dispatching mechanism. |
| * @constructor |
| * @extends {cr.EventTarget} |
| */ |
| FileCopyManager.EventRouter = function() { |
| }; |
| |
| /** |
| * Extends cr.EventTarget. |
| */ |
| FileCopyManager.EventRouter.prototype.__proto__ = cr.EventTarget.prototype; |
| |
| /** |
| * Dispatches a simple "copy-progress" event with reason and current |
| * FileCopyManager status. If it is an ERROR event, error should be set. |
| * |
| * @param {string} reason Event type. One of "BEGIN", "PROGRESS", "SUCCESS", |
| * "ERROR" or "CANCELLED". TODO(hidehiko): Use enum. |
| * @param {Object} status Current FileCopyManager's status. See also |
| * FileCopyManager.getStatus(). |
| * @param {FileCopyManager.Error=} opt_error The info for the error. This |
| * should be set iff the reason is "ERROR". |
| */ |
| FileCopyManager.EventRouter.prototype.sendProgressEvent = function( |
| reason, status, opt_error) { |
| var event = new cr.Event('copy-progress'); |
| event.reason = reason; |
| event.status = status; |
| if (opt_error) |
| event.error = opt_error; |
| this.dispatchEvent(event); |
| }; |
| |
| /** |
| * Dispatches an event to notify that an entry is changed (created or deleted). |
| * @param {util.EntryChangedType} type The enum to represent if the entry |
| * is created or deleted. |
| * @param {Entry} entry The changed entry. |
| */ |
| FileCopyManager.EventRouter.prototype.sendEntryChangedEvent = function( |
| type, entry) { |
| var event = new cr.Event('entry-changed'); |
| event.type = type; |
| event.entry = entry; |
| this.dispatchEvent(event); |
| }; |
| |
| /** |
| * Dispatches an event to notify entries are changed for delete task. |
| * |
| * @param {string} reason Event type. One of "BEGIN", "PROGRESS", "SUCCESS", |
| * or "ERROR". TODO(hidehiko): Use enum. |
| * @param {Array.<string>} urls An array of URLs which are affected by delete |
| * operation. |
| */ |
| FileCopyManager.EventRouter.prototype.sendDeleteEvent = function( |
| reason, urls) { |
| var event = new cr.Event('delete'); |
| event.reason = reason; |
| event.urls = urls; |
| this.dispatchEvent(event); |
| }; |
| |
| /** |
| * A record of a queued copy operation. |
| * |
| * Multiple copy operations may be queued at any given time. Additional |
| * Tasks may be added while the queue is being serviced. Though a |
| * cancel operation cancels everything in the queue. |
| * |
| * @param {DirectoryEntry} targetDirEntry Target directory. |
| * @param {DirectoryEntry=} opt_zipBaseDirEntry Base directory dealt as a root |
| * in ZIP archive. |
| * @constructor |
| */ |
| FileCopyManager.Task = function(targetDirEntry, opt_zipBaseDirEntry) { |
| this.targetDirEntry = targetDirEntry; |
| this.zipBaseDirEntry = opt_zipBaseDirEntry; |
| this.originalEntries = null; |
| |
| this.pendingDirectories = []; |
| this.pendingFiles = []; |
| this.pendingBytes = 0; |
| |
| this.completedDirectories = []; |
| this.completedFiles = []; |
| this.completedBytes = 0; |
| |
| this.deleteAfterCopy = false; |
| this.move = false; |
| this.zip = false; |
| |
| // If directory already exists, we try to make a copy named 'dir (X)', |
| // where X is a number. When we do this, all subsequent copies from |
| // inside the subtree should be mapped to the new directory name. |
| // For example, if 'dir' was copied as 'dir (1)', then 'dir\file.txt' should |
| // become 'dir (1)\file.txt'. |
| this.renamedDirectories_ = []; |
| }; |
| |
| |
| /** |
| * @param {Array.<Entry>} entries Entries. |
| * @param {function()} callback When entries resolved. |
| */ |
| FileCopyManager.Task.prototype.setEntries = function(entries, callback) { |
| var self = this; |
| this.originalEntries = entries; |
| // When moving directories, FileEntry.moveTo() is used if both source |
| // and target are on Drive. There is no need to recurse into directories. |
| util.recurseAndResolveEntries(entries, !this.move, function(result) { |
| self.pendingDirectories = result.dirEntries; |
| self.pendingFiles = result.fileEntries; |
| self.totalBytes = result.fileBytes; |
| self.completedBytes = 0; |
| |
| callback(); |
| }); |
| }; |
| |
| /** |
| * @return {Entry} Next entry. |
| */ |
| FileCopyManager.Task.prototype.getNextEntry = function() { |
| // We should keep the file in pending list and remove it after complete. |
| // Otherwise, if we try to get status in the middle of copying. The returned |
| // status is wrong (miss count the pasting item in totalItems). |
| var nextEntry = null; |
| if (this.move) { |
| // This may be moving from search results, where it fails if we move parent |
| // entries earlier than child entries. We should process the deepest entry |
| // first. Since move of each entry is done by a single .MoveTo() call, we |
| // don't need to care about the recursive traversal order. |
| nextEntry = this.getDeepestEntry_(); |
| } else { |
| // Copying tasks are recursively processed. So, directories must be |
| // processed earlier than their child files. Since |
| // util.recurseAndResolveEntries is already listing entries in the recursive |
| // traversal order, we just keep the ordering. |
| if (this.pendingDirectories.length) |
| nextEntry = this.pendingDirectories[0]; |
| else if (this.pendingFiles.length) |
| nextEntry = this.pendingFiles[0]; |
| } |
| if (nextEntry) |
| nextEntry.inProgress = true; |
| return nextEntry; |
| }; |
| |
| /** |
| * Remove the completed entry from the pending lists. |
| * @param {Entry} entry Entry. |
| * @param {number} size Bytes completed. |
| */ |
| FileCopyManager.Task.prototype.markEntryComplete = function(entry, size) { |
| if (entry.isDirectory && this.pendingDirectories) { |
| for (var i = 0; i < this.pendingDirectories.length; i++) { |
| if (this.pendingDirectories[i].inProgress) { |
| this.completedDirectories.push(entry); |
| this.pendingDirectories.splice(i, 1); |
| return; |
| } |
| } |
| } else if (this.pendingFiles) { |
| for (var i = 0; i < this.pendingFiles.length; i++) { |
| if (this.pendingFiles[i].inProgress) { |
| this.completedFiles.push(entry); |
| this.completedBytes += size; |
| this.pendingFiles.splice(i, 1); |
| return; |
| } |
| } |
| } |
| throw new Error('Try to remove a source entry which is not correspond to' + |
| ' the finished target entry'); |
| }; |
| |
| /** |
| * Updates copy progress status for the entry. |
| * |
| * @param {Entry} entry Entry which is being coppied. |
| * @param {number} size Number of bytes that has been copied since last update. |
| */ |
| FileCopyManager.Task.prototype.updateFileCopyProgress = function(entry, size) { |
| if (entry.isFile && this.pendingFiles && this.pendingFiles[0].inProgress) |
| this.completedBytes += size; |
| }; |
| |
| /** |
| * @param {string} fromName Old name. |
| * @param {string} toName New name. |
| */ |
| FileCopyManager.Task.prototype.registerRename = function(fromName, toName) { |
| this.renamedDirectories_.push({from: fromName + '/', to: toName + '/'}); |
| }; |
| |
| /** |
| * @param {string} path A path. |
| * @return {string} Path after renames. |
| */ |
| FileCopyManager.Task.prototype.applyRenames = function(path) { |
| // Directories are processed in pre-order, so we will store only the first |
| // renaming point: |
| // x -> x (1) -- new directory created. |
| // x\y -> x (1)\y -- no more renames inside the new directory, so |
| // this one will not be stored. |
| // x\y\a.txt -- only one rename will be applied. |
| for (var index = 0; index < this.renamedDirectories_.length; ++index) { |
| var rename = this.renamedDirectories_[index]; |
| if (path.indexOf(rename.from) == 0) { |
| path = rename.to + path.substr(rename.from.length); |
| } |
| } |
| return path; |
| }; |
| |
| /** |
| * Obtains the deepest entry by referring to its full path. |
| * @return {Entry} The deepest entry. |
| * @private |
| */ |
| FileCopyManager.Task.prototype.getDeepestEntry_ = function() { |
| var result = null; |
| for (var i = 0; i < this.pendingDirectories.length; i++) { |
| if (!result || |
| this.pendingDirectories[i].fullPath.length > result.fullPath.length) |
| result = this.pendingDirectories[i]; |
| } |
| for (var i = 0; i < this.pendingFiles.length; i++) { |
| if (!result || |
| this.pendingFiles[i].fullPath.length > result.fullPath.length) |
| result = this.pendingFiles[i]; |
| } |
| return result; |
| }; |
| |
| /** |
| * Error class used to report problems with a copy operation. |
| * If the code is UNEXPECTED_SOURCE_FILE, data should be a path of the file. |
| * If the code is TARGET_EXISTS, data should be the existing Entry. |
| * If the code is FILESYSTEM_ERROR, data should be the FileError. |
| * |
| * @param {util.FileOperationErrorType} code Error type. |
| * @param {string|Entry|FileError} data Additional data. |
| * @constructor |
| */ |
| FileCopyManager.Error = function(code, data) { |
| this.code = code; |
| this.data = data; |
| }; |
| |
| // FileCopyManager methods. |
| |
| /** |
| * Initializes the filesystem if it is not done yet. |
| * @param {function()} callback Completion callback. |
| */ |
| FileCopyManager.prototype.initialize = function(callback) { |
| // Already initialized. |
| if (this.root_) { |
| callback(); |
| return; |
| } |
| chrome.fileBrowserPrivate.requestFileSystem(function(filesystem) { |
| this.root_ = filesystem.root; |
| callback(); |
| }.bind(this)); |
| }; |
| |
| /** |
| * Called before a new method is run in the manager. Prepares the manager's |
| * state for running a new method. |
| */ |
| FileCopyManager.prototype.willRunNewMethod = function() { |
| // Cancel any pending close actions so the file copy manager doesn't go away. |
| if (this.unloadTimeout_) |
| clearTimeout(this.unloadTimeout_); |
| this.unloadTimeout_ = null; |
| }; |
| |
| /** |
| * @return {Object} Status object. |
| */ |
| FileCopyManager.prototype.getStatus = function() { |
| var rv = { |
| pendingItems: 0, // Files + Directories |
| pendingFiles: 0, |
| pendingDirectories: 0, |
| pendingBytes: 0, |
| |
| completedItems: 0, // Files + Directories |
| completedFiles: 0, |
| completedDirectories: 0, |
| completedBytes: 0, |
| |
| percentage: NaN, |
| pendingCopies: 0, |
| pendingMoves: 0, |
| pendingZips: 0, |
| filename: '' // In case pendingItems == 1 |
| }; |
| |
| var pendingFile = null; |
| |
| for (var i = 0; i < this.copyTasks_.length; i++) { |
| var task = this.copyTasks_[i]; |
| var pendingFiles = task.pendingFiles.length; |
| var pendingDirectories = task.pendingDirectories.length; |
| rv.pendingFiles += pendingFiles; |
| rv.pendingDirectories += pendingDirectories; |
| rv.pendingBytes += (task.totalBytes - task.completedBytes); |
| |
| rv.completedFiles += task.completedFiles.length; |
| rv.completedDirectories += task.completedDirectories.length; |
| rv.completedBytes += task.completedBytes; |
| |
| if (task.zip) { |
| rv.pendingZips += pendingFiles + pendingDirectories; |
| } else if (task.move || task.deleteAfterCopy) { |
| rv.pendingMoves += pendingFiles + pendingDirectories; |
| } else { |
| rv.pendingCopies += pendingFiles + pendingDirectories; |
| } |
| |
| if (task.pendingFiles.length === 1) |
| pendingFile = task.pendingFiles[0]; |
| |
| if (task.pendingDirectories.length === 1) |
| pendingFile = task.pendingDirectories[0]; |
| |
| } |
| rv.pendingItems = rv.pendingFiles + rv.pendingDirectories; |
| rv.completedItems = rv.completedFiles + rv.completedDirectories; |
| |
| rv.totalFiles = rv.pendingFiles + rv.completedFiles; |
| rv.totalDirectories = rv.pendingDirectories + rv.completedDirectories; |
| rv.totalItems = rv.pendingItems + rv.completedItems; |
| rv.totalBytes = rv.pendingBytes + rv.completedBytes; |
| |
| rv.percentage = rv.completedBytes / rv.totalBytes; |
| if (rv.pendingItems === 1) |
| rv.filename = pendingFile.name; |
| |
| return rv; |
| }; |
| |
| /** |
| * Adds an event listener for the tasks. |
| * @param {string} type The name of the event. |
| * @param {function(cr.Event)} handler The handler for the event. |
| * This is called when the event is dispatched. |
| */ |
| FileCopyManager.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 {function(cr.Event)} handler The handler to be removed. |
| */ |
| FileCopyManager.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. |
| */ |
| FileCopyManager.prototype.hasQueuedTasks = function() { |
| return this.copyTasks_.length > 0 || this.deleteTasks_.length > 0; |
| }; |
| |
| /** |
| * Unloads the host page in 5 secs of idleing. Need to be called |
| * each time this.copyTasks_.length or this.deleteTasks_.length |
| * changed. |
| * |
| * @private |
| */ |
| FileCopyManager.prototype.maybeScheduleCloseBackgroundPage_ = function() { |
| if (!this.hasQueuedTasks()) { |
| if (this.unloadTimeout_ === null) |
| this.unloadTimeout_ = setTimeout(maybeCloseBackgroundPage, 5000); |
| } else if (this.unloadTimeout_) { |
| clearTimeout(this.unloadTimeout_); |
| this.unloadTimeout_ = null; |
| } |
| }; |
| |
| /** |
| * Completely clear out the copy queue, either because we encountered an error |
| * or completed successfully. |
| * |
| * @private |
| */ |
| FileCopyManager.prototype.resetQueue_ = function() { |
| for (var i = 0; i < this.cancelObservers_.length; i++) |
| this.cancelObservers_[i](); |
| |
| this.copyTasks_ = []; |
| this.cancelObservers_ = []; |
| this.maybeScheduleCloseBackgroundPage_(); |
| }; |
| |
| /** |
| * Request that the current copy queue be abandoned. |
| * |
| * @param {function()=} opt_callback On cancel. |
| */ |
| FileCopyManager.prototype.requestCancel = function(opt_callback) { |
| this.cancelRequested_ = true; |
| if (this.cancelCallback_) { |
| this.cancelCallback_(); |
| this.cancelCallback_ = null; |
| } |
| if (opt_callback) |
| this.cancelObservers_.push(opt_callback); |
| |
| // If there is any active task it will eventually call maybeCancel_. |
| // Otherwise call it right now. |
| if (this.copyTasks_.length == 0) |
| this.doCancel_(); |
| }; |
| |
| /** |
| * Perform the bookkeeping required to cancel. |
| * |
| * @private |
| */ |
| FileCopyManager.prototype.doCancel_ = function() { |
| this.resetQueue_(); |
| this.cancelRequested_ = false; |
| this.eventRouter_.sendProgressEvent('CANCELLED', this.getStatus()); |
| }; |
| |
| /** |
| * Used internally to check if a cancel has been requested, and handle |
| * it if so. |
| * |
| * @return {boolean} If canceled. |
| * @private |
| */ |
| FileCopyManager.prototype.maybeCancel_ = function() { |
| if (!this.cancelRequested_) |
| return false; |
| |
| this.doCancel_(); |
| return true; |
| }; |
| |
| /** |
| * Kick off pasting. |
| * |
| * @param {Array.<string>} files Pathes of source files. |
| * @param {Array.<string>} directories Pathes of source directories. |
| * @param {boolean} isCut If the source items are removed from original |
| * location. |
| * @param {string} targetPath Target path. |
| */ |
| FileCopyManager.prototype.paste = function( |
| files, directories, isCut, targetPath) { |
| var self = this; |
| var entries = []; |
| var added = 0; |
| var total; |
| |
| var steps = { |
| start: function() { |
| // Filter entries. |
| var entryFilterFunc = function(entry) { |
| if (entry == '') |
| return false; |
| if (isCut && entry.replace(/\/[^\/]+$/, '') == targetPath) |
| // Moving to the same directory is a redundant operation. |
| return false; |
| return true; |
| }; |
| directories = directories ? directories.filter(entryFilterFunc) : []; |
| files = files ? files.filter(entryFilterFunc) : []; |
| |
| // Check the number of filtered entries. |
| total = directories.length + files.length; |
| if (total == 0) |
| return; |
| |
| // Retrieve entries. |
| util.getDirectories(self.root_, {create: false}, directories, |
| steps.onEntryFound, steps.onPathError); |
| util.getFiles(self.root_, {create: false}, files, |
| steps.onEntryFound, steps.onPathError); |
| }, |
| |
| onEntryFound: function(entry) { |
| // When getDirectories/getFiles finish, they call addEntry with null. |
| // We don't want to add null to our entries. |
| if (entry == null) |
| return; |
| entries.push(entry); |
| added++; |
| if (added == total) |
| steps.onSourceEntriesFound(); |
| }, |
| |
| onSourceEntriesFound: function() { |
| self.root_.getDirectory(targetPath, {}, |
| steps.onTargetEntryFound, steps.onPathError); |
| }, |
| |
| onTargetEntryFound: function(targetEntry) { |
| self.queueCopy_(targetEntry, entries, isCut); |
| }, |
| |
| onPathError: function(err) { |
| self.eventRouter_.sendProgressEvent( |
| 'ERROR', |
| self.getStatus(), |
| new FileCopyManager.Error( |
| util.FileOperationErrorType.FILESYSTEM_ERROR, err)); |
| } |
| }; |
| |
| steps.start(); |
| }; |
| |
| /** |
| * Checks if the move operation is avaiable between the given two locations. |
| * |
| * @param {DirectoryEntry} sourceEntry An entry from the source. |
| * @param {DirectoryEntry} targetDirEntry Directory entry for the target. |
| * @return {boolean} Whether we can move from the source to the target. |
| */ |
| FileCopyManager.prototype.isMovable = function(sourceEntry, |
| targetDirEntry) { |
| return (PathUtil.isDriveBasedPath(sourceEntry.fullPath) && |
| PathUtil.isDriveBasedPath(targetDirEntry.fullPath)) || |
| (PathUtil.getRootPath(sourceEntry.fullPath) == |
| PathUtil.getRootPath(targetDirEntry.fullPath)); |
| }; |
| |
| /** |
| * Initiate a file copy. |
| * |
| * @param {DirectoryEntry} targetDirEntry Target directory. |
| * @param {Array.<Entry>} entries Entries to copy. |
| * @param {boolean} deleteAfterCopy In case of move. |
| * @return {FileCopyManager.Task} Copy task. |
| * @private |
| */ |
| FileCopyManager.prototype.queueCopy_ = function( |
| targetDirEntry, entries, deleteAfterCopy) { |
| var self = this; |
| // When copying files, null can be specified as source directory. |
| var copyTask = new FileCopyManager.Task(targetDirEntry); |
| if (deleteAfterCopy) { |
| if (this.isMovable(entries[0], targetDirEntry)) { |
| copyTask.move = true; |
| } else { |
| copyTask.deleteAfterCopy = true; |
| } |
| } |
| copyTask.setEntries(entries, function() { |
| self.copyTasks_.push(copyTask); |
| self.maybeScheduleCloseBackgroundPage_(); |
| if (self.copyTasks_.length == 1) { |
| // Assume self.cancelRequested_ == false. |
| // This moved us from 0 to 1 active tasks, let the servicing begin! |
| self.serviceAllTasks_(); |
| } else { |
| // Force to update the progress of butter bar when there are new tasks |
| // coming while servicing current task. |
| self.eventRouter_.sendProgressEvent('PROGRESS', self.getStatus()); |
| } |
| }); |
| |
| return copyTask; |
| }; |
| |
| /** |
| * Service all pending tasks, as well as any that might appear during the |
| * copy. |
| * |
| * @private |
| */ |
| FileCopyManager.prototype.serviceAllTasks_ = function() { |
| var self = this; |
| |
| var onTaskProgress = function() { |
| self.eventRouter_.sendProgressEvent('PROGRESS', self.getStatus()); |
| }; |
| |
| var onEntryChanged = function(type, entry) { |
| self.eventRouter_.sendEntryChangedEvent(type, entry); |
| }; |
| |
| var onTaskError = function(err) { |
| if (self.maybeCancel_()) |
| return; |
| self.eventRouter_.sendProgressEvent('ERROR', self.getStatus(), err); |
| self.resetQueue_(); |
| }; |
| |
| var onTaskSuccess = function() { |
| if (self.maybeCancel_()) |
| return; |
| |
| // The task at the front of the queue is completed. Pop it from the queue. |
| self.copyTasks_.shift(); |
| self.maybeScheduleCloseBackgroundPage_(); |
| |
| if (!self.copyTasks_.length) { |
| // All tasks have been serviced, clean up and exit. |
| self.eventRouter_.sendProgressEvent('SUCCESS', self.getStatus()); |
| self.resetQueue_(); |
| return; |
| } |
| |
| // We want to dispatch a PROGRESS event when there are more tasks to serve |
| // right after one task finished in the queue. We treat all tasks as one |
| // big task logically, so there is only one BEGIN/SUCCESS event pair for |
| // these continuous tasks. |
| self.eventRouter_.sendProgressEvent('PROGRESS', self.getStatus()); |
| |
| self.serviceTask_(self.copyTasks_[0], onEntryChanged, onTaskProgress, |
| onTaskSuccess, onTaskError); |
| }; |
| |
| // If the queue size is 1 after pushing our task, it was empty before, |
| // so we need to kick off queue processing and dispatch BEGIN event. |
| |
| this.eventRouter_.sendProgressEvent('BEGIN', this.getStatus()); |
| this.serviceTask_(this.copyTasks_[0], onEntryChanged, onTaskProgress, |
| onTaskSuccess, onTaskError); |
| }; |
| |
| /** |
| * Runs a given task. |
| * Note that the responsibility of this method is just dispatching to the |
| * appropriate serviceXxxTask_() method. |
| * TODO(hidehiko): Remove this method by introducing FileCopyManager.Task.run() |
| * (crbug.com/246976). |
| * |
| * @param {FileCopyManager.Task} task A task to be run. |
| * @param {function(util.EntryChangedType, Entry)} entryChangedCallback Callback |
| * invoked when an entry is changed. |
| * @param {function()} progressCallback Callback invoked periodically during |
| * the operation. |
| * @param {function()} successCallback Callback run on success. |
| * @param {function(FileCopyManager.Error)} errorCallback Callback run on error. |
| * @private |
| */ |
| FileCopyManager.prototype.serviceTask_ = function( |
| task, entryChangedCallback, progressCallback, |
| successCallback, errorCallback) { |
| if (task.zip) |
| this.serviceZipTask_(task, entryChangedCallback, progressCallback, |
| successCallback, errorCallback); |
| else if (task.move) |
| this.serviceMoveTask_(task, entryChangedCallback, progressCallback, |
| successCallback, errorCallback); |
| else |
| this.serviceCopyTask_(task, entryChangedCallback, progressCallback, |
| successCallback, errorCallback); |
| }; |
| |
| /** |
| * Service all entries in the copy (and move) task. |
| * Note: this method contains also the operation of "Move" due to historical |
| * reason. |
| * |
| * @param {FileCopyManager.Task} task A copy task to be run. |
| * @param {function(util.EntryChangedType, Entry)} entryChangedCallback Callback |
| * invoked when an entry is changed. |
| * @param {function()} progressCallback Callback invoked periodically during |
| * the copying. |
| * @param {function()} successCallback On success. |
| * @param {function(FileCopyManager.Error)} errorCallback On error. |
| * @private |
| */ |
| FileCopyManager.prototype.serviceCopyTask_ = function( |
| task, entryChangedCallback, progressCallback, successCallback, |
| errorCallback) { |
| // TODO(hidehiko): We should be able to share the code to iterate on entries |
| // with serviceMoveTask_(). |
| if (task.pendingDirectories.length + task.pendingFiles.length == 0) { |
| successCallback(); |
| return; |
| } |
| |
| var self = this; |
| |
| var deleteOriginals = function() { |
| var count = task.originalEntries.length; |
| |
| var onEntryDeleted = function(entry) { |
| entryChangedCallback(util.EntryChangedType.DELETED, entry); |
| count--; |
| if (!count) |
| successCallback(); |
| }; |
| |
| var onFilesystemError = function(err) { |
| errorCallback(new FileCopyManager.Error( |
| util.FileOperationErrorType.FILESYSTEM_ERROR, err)); |
| }; |
| |
| for (var i = 0; i < task.originalEntries.length; i++) { |
| var entry = task.originalEntries[i]; |
| util.removeFileOrDirectory( |
| entry, onEntryDeleted.bind(self, entry), onFilesystemError); |
| } |
| }; |
| |
| var onEntryServiced = function() { |
| // We should not dispatch a PROGRESS event when there is no pending items |
| // in the task. |
| if (task.pendingDirectories.length + task.pendingFiles.length == 0) { |
| if (task.deleteAfterCopy) { |
| deleteOriginals(); |
| } else { |
| successCallback(); |
| } |
| return; |
| } |
| |
| progressCallback(); |
| self.processCopyEntry_( |
| task, task.getNextEntry(), entryChangedCallback, progressCallback, |
| onEntryServiced, errorCallback); |
| }; |
| |
| this.processCopyEntry_( |
| task, task.getNextEntry(), entryChangedCallback, progressCallback, |
| onEntryServiced, errorCallback); |
| }; |
| |
| /** |
| * Copies the next entry in a given task. |
| * TODO(olege): Refactor this method into a separate class. |
| * |
| * @param {FileManager.Task} task A task. |
| * @param {Entry} sourceEntry An entry to be copied. |
| * @param {function(util.EntryChangedType, Entry)} entryChangedCallback Callback |
| * invoked when an entry is changed. |
| * @param {function()} progressCallback Callback invoked periodically during |
| * the copying. |
| * @param {function()} successCallback On success. |
| * @param {function(FileCopyManager.Error)} errorCallback On error. |
| * @private |
| */ |
| FileCopyManager.prototype.processCopyEntry_ = function( |
| task, sourceEntry, entryChangedCallback, progressCallback, successCallback, |
| errorCallback) { |
| if (this.maybeCancel_()) |
| return; |
| |
| var self = this; |
| |
| // |sourceEntry.originalSourcePath| is set in util.recurseAndResolveEntries. |
| var sourcePath = sourceEntry.originalSourcePath; |
| if (sourceEntry.fullPath.substr(0, sourcePath.length) != sourcePath) { |
| // We found an entry in the list that is not relative to the base source |
| // path, something is wrong. |
| errorCallback(new FileCopyManager.Error( |
| util.FileOperationErrorType.UNEXPECTED_SOURCE_FILE, |
| sourceEntry.fullPath)); |
| return; |
| } |
| |
| var targetDirEntry = task.targetDirEntry; |
| var originalPath = sourceEntry.fullPath.substr(sourcePath.length + 1); |
| originalPath = task.applyRenames(originalPath); |
| |
| var onDeduplicated = function(targetRelativePath) { |
| var onCopyComplete = function(entry, size) { |
| entryChangedCallback(util.EntryChangedType.CREATED, entry); |
| task.markEntryComplete(entry, size); |
| successCallback(); |
| }; |
| |
| var onFilesystemError = function(err) { |
| errorCallback(new FileCopyManager.Error( |
| util.FileOperationErrorType.FILESYSTEM_ERROR, err)); |
| }; |
| |
| if (sourceEntry.isDirectory) { |
| // Copying the directory means just creating a new directory. |
| targetDirEntry.getDirectory( |
| targetRelativePath, |
| {create: true, exclusive: true}, |
| function(targetEntry) { |
| if (targetRelativePath != originalPath) { |
| task.registerRename(originalPath, targetRelativePath); |
| } |
| onCopyComplete(targetEntry, 0); |
| }, |
| util.flog('Error getting dir: ' + targetRelativePath, |
| onFilesystemError)); |
| return; |
| } |
| |
| var onCopyProgress = function(entry, size) { |
| task.updateFileCopyProgress(entry, size); |
| progressCallback(); |
| }; |
| |
| // Hereafter copy a file. |
| var isSourceOnDrive = PathUtil.isDriveBasedPath(sourceEntry.fullPath); |
| var isTargetOnDrive = PathUtil.isDriveBasedPath(targetDirEntry.fullPath); |
| |
| if (!isSourceOnDrive && !isTargetOnDrive) { |
| // Sending a file from local to local. |
| // To copy local file, we use File blob and FileWriter to take the |
| // progress. |
| targetDirEntry.getFile( |
| targetRelativePath, |
| {create: true, exclusive: true}, |
| function(targetEntry) { |
| self.cancelCallback_ = self.copyFileEntry_( |
| sourceEntry, targetEntry, |
| onCopyProgress, |
| function(entry, size) { |
| self.cancelCallback_ = null; |
| onCopyComplete(entry, size); |
| }, |
| function(error) { |
| self.cancelCallback_ = null; |
| onFilesystemError(error); |
| }); |
| }, |
| util.flog('Error getting file: ' + targetRelativePath, |
| onFilesystemError)); |
| return; |
| } else { |
| // Sending a file from a) Drive to Drive, b) Drive to local or c) local |
| // to Drive. |
| targetDirEntry.getDirectory( |
| PathUtil.dirname(targetRelativePath), {create: false}, |
| function(dirEntry) { |
| self.cancelCallback_ = fileOperationUtil.copyFileOnDrive( |
| sourceEntry, dirEntry, PathUtil.basename(targetRelativePath), |
| onCopyProgress, |
| function(entry) { |
| self.cancelCallback_ = null; |
| onCopyComplete(entry, 0); |
| }, |
| function(error) { |
| self.cancelCallback_ = null; |
| onFilesystemError(error); |
| }); |
| }, |
| onFilesystemError); |
| } |
| }; |
| |
| fileOperationUtil.deduplicatePath( |
| targetDirEntry, originalPath, onDeduplicated, errorCallback); |
| }; |
| |
| /** |
| * Copies the contents of sourceEntry into targetEntry. |
| * TODO(hidehiko): Move this method into fileOperationUtil. |
| * |
| * @param {FileEntry} sourceEntry The file entry that will be copied. |
| * @param {FileEntry} targetEntry The file entry to which sourceEntry will be |
| * copied. |
| * @param {function(FileEntry, number)} progressCallback Function that will be |
| * called when a part of the source entry is copied. It takes |targetEntry| |
| * and size of the last copied chunk as parameters. |
| * @param {function(FileEntry, number)} successCallback Function that will be |
| * called the copy operation finishes. It takes |targetEntry| and size of |
| * the last (not previously reported) copied chunk as parameters. |
| * @param {function(FileError)} errorCallback Function that will be called |
| * if an error is encountered. Takes error type and additional error data |
| * as parameters. |
| * @return {function()} Callback to cancel the current file copy operation. |
| * When the cancel is done, errorCallback will be called. The returned |
| * callback must not be called more than once. |
| * @private |
| */ |
| FileCopyManager.prototype.copyFileEntry_ = function(sourceEntry, |
| targetEntry, |
| progressCallback, |
| successCallback, |
| errorCallback) { |
| // Set to true when cancel is requested. |
| var cancelRequested = false; |
| |
| sourceEntry.file(function(file) { |
| if (cancelRequested) { |
| errorCallback(util.createFileError(FileError.ABORT_ERR)); |
| return; |
| } |
| |
| targetEntry.createWriter(function(writer) { |
| if (cancelRequested) { |
| errorCallback(util.createFileError(FileError.ABORT_ERR)); |
| return; |
| } |
| |
| var reportedProgress = 0; |
| writer.onerror = writer.onabort = function(progress) { |
| errorCallback(cancelRequested ? |
| util.createFileError(FileError.ABORT_ERR) : |
| writer.error); |
| }; |
| |
| writer.onprogress = function(progress) { |
| if (cancelRequested) { |
| // If the copy was cancelled, we should abort the operation. |
| // The errorCallback will be called by writer.onabort after the |
| // termination. |
| writer.abort(); |
| return; |
| } |
| |
| // |progress.loaded| will contain total amount of data copied by now. |
| // |progressCallback| expects data amount delta from the last progress |
| // update. |
| progressCallback(targetEntry, progress.loaded - reportedProgress); |
| reportedProgress = progress.loaded; |
| }; |
| |
| writer.onwrite = function() { |
| if (cancelRequested) { |
| errorCallback(util.createFileError(FileError.ABORT_ERR)); |
| return; |
| } |
| |
| sourceEntry.getMetadata(function(metadata) { |
| if (cancelRequested) { |
| errorCallback(util.createFileError(FileError.ABORT_ERR)); |
| return; |
| } |
| |
| fileOperationUtil.setLastModified( |
| targetEntry, metadata.modificationTime); |
| successCallback(targetEntry, file.size - reportedProgress); |
| }); |
| }; |
| |
| writer.write(file); |
| }, errorCallback); |
| }, errorCallback); |
| |
| return function() { |
| cancelRequested = true; |
| }; |
| }; |
| |
| /** |
| * Moves all entries in the task. |
| * |
| * @param {FileCopyManager.Task} task A move task to be run. |
| * @param {function(util.EntryChangedType, Entry)} entryChangedCallback Callback |
| * invoked when an entry is changed. |
| * @param {function()} progressCallback Callback invoked periodically during |
| * the moving. |
| * @param {function()} successCallback On success. |
| * @param {function(FileCopyManager.Error)} errorCallback On error. |
| * @private |
| */ |
| FileCopyManager.prototype.serviceMoveTask_ = function( |
| task, entryChangedCallback, progressCallback, successCallback, |
| errorCallback) { |
| if (task.pendingDirectories.length + task.pendingFiles.length == 0) { |
| successCallback(); |
| return; |
| } |
| |
| this.processMoveEntry_( |
| task, task.getNextEntry(), entryChangedCallback, |
| (function onCompleted() { |
| // We should not dispatch a PROGRESS event when there is no pending |
| // items in the task. |
| if (task.pendingDirectories.length + task.pendingFiles.length == 0) { |
| successCallback(); |
| return; |
| } |
| |
| // Move the next entry. |
| progressCallback(); |
| this.processMoveEntry_( |
| task, task.getNextEntry(), entryChangedCallback, |
| onCompleted.bind(this), errorCallback); |
| }).bind(this), |
| errorCallback); |
| }; |
| |
| /** |
| * Moves the next entry in a given task. |
| * |
| * Implementation note: This method can be simplified more. For example, in |
| * Task.setEntries(), the flag to recurse is set to false for move task, |
| * so that all the entries' originalSourcePath should be |
| * dirname(sourceEntry.fullPath). |
| * Thus, targetRelativePath should contain exact one component. Also we can |
| * skip applyRenames, because the destination directory always should be |
| * task.targetDirEntry. |
| * The unnecessary complexity is due to historical reason. |
| * TODO(hidehiko): Refactor this method. |
| * |
| * @param {FileManager.Task} task A move task. |
| * @param {Entry} sourceEntry An entry to be moved. |
| * @param {function(util.EntryChangedType, Entry)} entryChangedCallback Callback |
| * invoked when an entry is changed. |
| * @param {function()} successCallback On success. |
| * @param {function(FileCopyManager.Error)} errorCallback On error. |
| * @private |
| */ |
| FileCopyManager.prototype.processMoveEntry_ = function( |
| task, sourceEntry, entryChangedCallback, successCallback, errorCallback) { |
| if (this.maybeCancel_()) |
| return; |
| |
| // |sourceEntry.originalSourcePath| is set in util.recurseAndResolveEntries. |
| var sourcePath = sourceEntry.originalSourcePath; |
| if (sourceEntry.fullPath.substr(0, sourcePath.length) != sourcePath) { |
| // We found an entry in the list that is not relative to the base source |
| // path, something is wrong. |
| errorCallback(new FileCopyManager.Error( |
| util.FileOperationErrorType.UNEXPECTED_SOURCE_FILE, |
| sourceEntry.fullPath)); |
| return; |
| } |
| |
| fileOperationUtil.deduplicatePath( |
| task.targetDirEntry, |
| task.applyRenames(sourceEntry.fullPath.substr(sourcePath.length + 1)), |
| function(targetRelativePath) { |
| var onFilesystemError = function(err) { |
| errorCallback(new FileCopyManager.Error( |
| util.FileOperationErrorType.FILESYSTEM_ERROR, |
| err)); |
| }; |
| |
| task.targetDirEntry.getDirectory( |
| PathUtil.dirname(targetRelativePath), {create: false}, |
| function(dirEntry) { |
| sourceEntry.moveTo( |
| dirEntry, PathUtil.basename(targetRelativePath), |
| function(targetEntry) { |
| entryChangedCallback( |
| util.EntryChangedType.CREATED, targetEntry); |
| entryChangedCallback( |
| util.EntryChangedType.DELETED, sourceEntry); |
| task.markEntryComplete(targetEntry, 0); |
| successCallback(); |
| }, |
| onFilesystemError); |
| }, |
| onFilesystemError); |
| }, |
| errorCallback); |
| }; |
| |
| /** |
| * Service a zip file creation task. |
| * |
| * @param {FileCopyManager.Task} task A zip task to be run. |
| * @param {function(util.EntryChangedType, Entry)} entryChangedCallback Callback |
| * invoked when an entry is changed. |
| * @param {function()} progressCallback Callback invoked periodically during |
| * the moving. |
| * @param {function()} successCallback On complete. |
| * @param {function(FileCopyManager.Error)} errorCallback On error. |
| * @private |
| */ |
| FileCopyManager.prototype.serviceZipTask_ = function( |
| task, entryChangedCallback, progressCallback, successCallback, |
| errorCallback) { |
| // TODO(hidehiko): we should localize the name. |
| var destName = 'Archive'; |
| if (task.originalEntries.length == 1) { |
| var entryPath = task.originalEntries[0].fullPath; |
| var i = entryPath.lastIndexOf('/'); |
| var basename = (i < 0) ? entryPath : entryPath.substr(i + 1); |
| i = basename.lastIndexOf('.'); |
| destName = ((i < 0) ? basename : basename.substr(0, i)); |
| } |
| |
| fileOperationUtil.deduplicatePath( |
| task.targetDirEntry, destName + '.zip', |
| function(destPath) { |
| progressCallback(); |
| |
| fileOperationUtil.zipSelection( |
| task.pendingDirectories.concat(task.pendingFiles), |
| task.zipBaseDirEntry, |
| destPath, |
| function(entry) { |
| entryChangedCallback(util.EntryChangedType.CREATE, entry); |
| successCallback(); |
| }, |
| function(error) { |
| errorCallback(new FileCopyManager.Error( |
| util.FileOperationErrorType.FILESYSTEM_ERROR, error)); |
| }); |
| }, |
| errorCallback); |
| }; |
| |
| /** |
| * Timeout before files are really deleted (to allow undo). |
| */ |
| FileCopyManager.DELETE_TIMEOUT = 30 * 1000; |
| |
| /** |
| * Schedules the files deletion. |
| * |
| * @param {Array.<Entry>} entries The entries. |
| */ |
| FileCopyManager.prototype.deleteEntries = function(entries) { |
| var task = { entries: entries }; |
| this.deleteTasks_.push(task); |
| this.maybeScheduleCloseBackgroundPage_(); |
| if (this.deleteTasks_.length == 1) |
| this.serviceAllDeleteTasks_(); |
| }; |
| |
| /** |
| * Service all pending delete tasks, as well as any that might appear during the |
| * deletion. |
| * |
| * @private |
| */ |
| FileCopyManager.prototype.serviceAllDeleteTasks_ = function() { |
| var self = this; |
| |
| var onTaskSuccess = function() { |
| var task = self.deleteTasks_[0]; |
| self.deleteTasks_.shift(); |
| if (!self.deleteTasks_.length) { |
| // All tasks have been serviced, clean up and exit. |
| self.eventRouter_.sendDeleteEvent( |
| 'SUCCESS', |
| task.entries.map(function(e) { |
| return util.makeFilesystemUrl(e.fullPath); |
| })); |
| self.maybeScheduleCloseBackgroundPage_(); |
| return; |
| } |
| |
| // We want to dispatch a PROGRESS event when there are more tasks to serve |
| // right after one task finished in the queue. We treat all tasks as one |
| // big task logically, so there is only one BEGIN/SUCCESS event pair for |
| // these continuous tasks. |
| self.eventRouter_.sendDeleteEvent( |
| 'PROGRESS', |
| task.entries.map(function(e) { |
| return util.makeFilesystemUrl(e.fullPath); |
| })); |
| self.serviceDeleteTask_(self.deleteTasks_[0], onTaskSuccess, onTaskFailure); |
| }; |
| |
| var onTaskFailure = function(task) { |
| self.deleteTasks_ = []; |
| self.eventRouter_.sendDeleteEvent( |
| 'ERROR', |
| task.entries.map(function(e) { |
| return util.makeFilesystemUrl(e.fullPath); |
| })); |
| self.maybeScheduleCloseBackgroundPage_(); |
| }; |
| |
| // If the queue size is 1 after pushing our task, it was empty before, |
| // so we need to kick off queue processing and dispatch BEGIN event. |
| this.eventRouter_.sendDeleteEvent( |
| 'BEGIN', |
| this.deleteTasks_[0].entries.map(function(e) { |
| return util.makeFilesystemUrl(e.fullPath); |
| })); |
| this.serviceDeleteTask_(this.deleteTasks_[0], onTaskSuccess, onTaskFailure); |
| }; |
| |
| /** |
| * Performs the deletion. |
| * |
| * @param {Object} task The delete task (see deleteEntries function). |
| * @param {function()} successCallback Callback run on success. |
| * @param {function(FileCopyManager.Error)} errorCallback Callback run on error. |
| * @private |
| */ |
| FileCopyManager.prototype.serviceDeleteTask_ = function( |
| task, successCallback, errorCallback) { |
| var downcount = task.entries.length; |
| if (downcount == 0) { |
| successCallback(); |
| return; |
| } |
| |
| var filesystemError = null; |
| var onComplete = function() { |
| if (--downcount > 0) |
| return; |
| |
| // All remove operations are processed. Run callback. |
| if (filesystemError) { |
| errorCallback(new FileCopyManager.Error( |
| util.FileOperationErrorType.FILESYSTEM_ERROR, filesystemError)); |
| } else { |
| successCallback(); |
| } |
| }; |
| |
| for (var i = 0; i < task.entries.length; i++) { |
| var entry = task.entries[i]; |
| util.removeFileOrDirectory( |
| entry, |
| function(currentEntry) { |
| this.eventRouter_.sendEntryChangedEvent( |
| util.EntryChangedType.DELETED, currentEntry); |
| onComplete(); |
| }.bind(this, entry), |
| function(error) { |
| if (!filesystemError) |
| filesystemError = error; |
| onComplete(); |
| }); |
| } |
| }; |
| |
| /** |
| * Creates a zip file for the selection of files. |
| * |
| * @param {Entry} dirEntry The directory containing the selection. |
| * @param {Array.<Entry>} selectionEntries The selected entries. |
| */ |
| FileCopyManager.prototype.zipSelection = function(dirEntry, selectionEntries) { |
| var self = this; |
| var zipTask = new FileCopyManager.Task(dirEntry, dirEntry); |
| zipTask.zip = true; |
| zipTask.setEntries(selectionEntries, function() { |
| // TODO: per-entry zip progress update with accurate byte count. |
| // For now just set completedBytes to same value as totalBytes so that the |
| // progress bar is full. |
| zipTask.completedBytes = zipTask.totalBytes; |
| self.copyTasks_.push(zipTask); |
| if (self.copyTasks_.length == 1) { |
| // Assume self.cancelRequested_ == false. |
| // This moved us from 0 to 1 active tasks, let the servicing begin! |
| self.serviceAllTasks_(); |
| } else { |
| // Force to update the progress of butter bar when there are new tasks |
| // coming while servicing current task. |
| self.eventRouter_.sendProgressEvent('PROGRESS', self.getStatus()); |
| } |
| }); |
| }; |