| // 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'; |
| |
| if (chrome.extension) { |
| var getContentWindows = function() { |
| return chrome.extension.getViews(); |
| }; |
| } |
| |
| /** |
| * @constructor |
| */ |
| function FileCopyManager() { |
| this.copyTasks_ = []; |
| this.deleteTasks_ = []; |
| this.cancelObservers_ = []; |
| this.cancelRequested_ = false; |
| this.cancelCallback_ = null; |
| this.unloadTimeout_ = null; |
| |
| window.addEventListener('error', function(e) { |
| this.log_('Unhandled error: ', e.message, e.filename + ':' + e.lineno); |
| }.bind(this)); |
| } |
| |
| var fileCopyManagerInstance = null; |
| |
| /** |
| * Get FileCopyManager instance. In case is hasn't been initialized, a new |
| * instance is created. |
| * |
| * @param {DirectoryEntry} root Root entry. |
| * @return {FileCopyManager} A FileCopyManager instance. |
| */ |
| FileCopyManager.getInstance = function(root) { |
| if (fileCopyManagerInstance === null) { |
| fileCopyManagerInstance = new FileCopyManager(root); |
| } |
| return fileCopyManagerInstance; |
| }; |
| |
| /** |
| * 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; |
| this.sourceOnDrive = false; |
| this.targetOnDrive = 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; |
| |
| var onEntriesRecursed = function(result) { |
| // Deeper directory is moved earier. |
| self.pendingDirectories = result.dirEntries.sort( |
| function(a, b) { return a.fullPath < b.fullPath; }); |
| self.pendingFiles = result.fileEntries; |
| self.pendingBytes = result.fileBytes; |
| callback(); |
| }; |
| |
| 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. |
| var recurse = !this.move; |
| util.recurseAndResolveEntries(entries, recurse, onEntriesRecursed); |
| }; |
| |
| /** |
| * @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). |
| if (this.pendingFiles.length) { |
| this.pendingFiles[0].inProgress = true; |
| return this.pendingFiles[0]; |
| } |
| if (this.pendingDirectories.length) { |
| this.pendingDirectories[0].inProgress = true; |
| return this.pendingDirectories[0]; |
| } |
| return null; |
| }; |
| |
| /** |
| * @param {Entry} entry Entry. |
| * @param {number} size Bytes completed. |
| */ |
| FileCopyManager.Task.prototype.markEntryComplete = function(entry, size) { |
| // It is probably not safe to directly remove the first entry in pending list. |
| // We need to check if the removed entry (srcEntry) corresponding to the added |
| // entry (target entry). |
| if (entry.isDirectory && this.pendingDirectories && |
| this.pendingDirectories[0].inProgress) { |
| this.completedDirectories.push(entry); |
| this.pendingDirectories.shift(); |
| } else if (this.pendingFiles && this.pendingFiles[0].inProgress) { |
| this.completedFiles.push(entry); |
| this.completedBytes += size; |
| this.pendingBytes -= size; |
| this.pendingFiles.shift(); |
| } else { |
| 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; |
| this.pendingBytes -= 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; |
| }; |
| |
| /** |
| * Error class used to report problems with a copy operation. |
| * |
| * @param {string} reason Error type. |
| * @param {Object} data Additional data. |
| * @constructor |
| */ |
| FileCopyManager.Error = function(reason, data) { |
| this.reason = reason; |
| this.code = FileCopyManager.Error[reason]; |
| this.data = data; |
| }; |
| |
| /** @const */ |
| FileCopyManager.Error.CANCELLED = 0; |
| /** @const */ |
| FileCopyManager.Error.UNEXPECTED_SOURCE_FILE = 1; |
| /** @const */ |
| FileCopyManager.Error.TARGET_EXISTS = 2; |
| /** @const */ |
| FileCopyManager.Error.FILESYSTEM_ERROR = 3; |
| |
| |
| // 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.requestLocalFileSystem(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.pendingBytes; |
| |
| 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; |
| }; |
| |
| /** |
| * Send an event to all the FileManager windows. |
| * |
| * @param {string} eventName Event name. |
| * @param {Object} eventArgs An object with arbitrary event parameters. |
| * @private |
| */ |
| FileCopyManager.prototype.sendEvent_ = function(eventName, eventArgs) { |
| if (this.cancelRequested_) |
| return; // Swallow events until cancellation complete. |
| |
| eventArgs.status = this.getStatus(); |
| |
| var windows = getContentWindows(); |
| for (var i = 0; i < windows.length; i++) { |
| var w = windows[i]; |
| if (w.FileCopyManagerWrapper) |
| w.FileCopyManagerWrapper.getInstance().onEvent(eventName, eventArgs); |
| } |
| }; |
| |
| /** |
| * 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( |
| util.platform.v2() ? maybeCloseBackgroundPage : close, 5000); |
| } |
| } else if (this.unloadTimeout_) { |
| clearTimeout(this.unloadTimeout_); |
| this.unloadTimeout_ = null; |
| } |
| }; |
| |
| /** |
| * Reports an error on all of the active Files.app's windows. |
| * @private |
| */ |
| FileCopyManager.prototype.log_ = function() { |
| var windows = getContentWindows(); |
| for (var i = 0; i < windows.length; i++) { |
| windows[i].console.error.apply(windows[i].console, arguments); |
| } |
| }; |
| |
| /** |
| * Dispatch a simple copy-progress event with reason and optional err data. |
| * |
| * @param {string} reason Event type. |
| * @param {FileCopyManager.Error=} opt_err Error. |
| * @private |
| */ |
| FileCopyManager.prototype.sendProgressEvent_ = function(reason, opt_err) { |
| var event = {}; |
| event.reason = reason; |
| if (opt_err) |
| event.error = opt_err; |
| this.sendEvent_('copy-progress', event); |
| }; |
| |
| /** |
| * Dispatch an event of file operation completion (allows to update the UI). |
| * |
| * @private |
| * @param {string} reason Completed file operation: 'movied|copied|deleted'. |
| * @param {Array.<Entry>} affectedEntries deleted ot created entries. |
| */ |
| FileCopyManager.prototype.sendOperationEvent_ = function(reason, |
| affectedEntries) { |
| var event = {}; |
| event.reason = reason; |
| event.affectedEntries = affectedEntries; |
| this.sendEvent_('copy-operation-complete', event); |
| }; |
| |
| /** |
| * 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_(); |
| 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.sendProgressEvent_('CANCELLED'); |
| }; |
| |
| /** |
| * 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 {boolean} isOnDrive If the source items are on Google Drive. |
| * @param {string} targetPath Target path. |
| * @param {boolean} targetOnDrive If target is on Drive. |
| */ |
| FileCopyManager.prototype.paste = function(files, directories, isCut, isOnDrive, |
| targetPath, targetOnDrive) { |
| var self = this; |
| var entries = []; |
| |
| var onPathError = function(err) { |
| self.sendProgressEvent_('ERROR', |
| new FileCopyManager.Error('FILESYSTEM_ERROR', err)); |
| }; |
| |
| var onTargetEntryFound = function(targetEntry) { |
| self.queueCopy_(targetEntry, |
| entries, |
| isCut, |
| isOnDrive, |
| targetOnDrive); |
| }; |
| |
| var onComplete = function() { |
| self.root_.getDirectory(targetPath, {}, |
| onTargetEntryFound, onPathError); |
| }; |
| |
| var added = 0; |
| var 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) { |
| entries.push(entry); |
| added++; |
| if (added == total) |
| onComplete(); |
| } |
| }; |
| |
| var entryFilterFunc = function(entry) { |
| if (entry == '') { |
| return false; |
| } else if (isCut && entry.replace(/\/[^\/]+$/, '') == targetPath) { |
| // Moving to the same directory is a redundant operation |
| return false; |
| } else { |
| return true; |
| } |
| }; |
| directories = directories ? directories.filter(entryFilterFunc) : []; |
| files = files ? files.filter(entryFilterFunc) : []; |
| |
| var total = directories.length + files.length; |
| if (total == 0) |
| return; |
| |
| util.getDirectories(self.root_, {create: false}, directories, onEntryFound, |
| onPathError); |
| util.getFiles(self.root_, {create: false}, files, onEntryFound, |
| onPathError); |
| }; |
| |
| /** |
| * Checks if source and target are on the same root. |
| * |
| * @param {DirectoryEntry} sourceEntry An entry from the source. |
| * @param {DirectoryEntry} targetDirEntry Directory entry for the target. |
| * @param {boolean} targetOnDrive If target is on Drive. |
| * @return {boolean} Whether source and target dir are on the same root. |
| */ |
| FileCopyManager.prototype.isOnSameRoot = function(sourceEntry, |
| targetDirEntry, |
| targetOnDrive) { |
| return 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. |
| * @param {boolean} sourceOnDrive Source directory on Drive. |
| * @param {boolean} targetOnDrive Target directory on Drive. |
| * @return {FileCopyManager.Task} Copy task. |
| * @private |
| */ |
| FileCopyManager.prototype.queueCopy_ = function(targetDirEntry, |
| entries, |
| deleteAfterCopy, |
| sourceOnDrive, |
| targetOnDrive) { |
| var self = this; |
| // When copying files, null can be specified as source directory. |
| var copyTask = new FileCopyManager.Task(targetDirEntry); |
| if (deleteAfterCopy) { |
| // |sourecDirEntry| may be null, so let's check the root for the first of |
| // the entries scheduled to be copied. |
| if (this.isOnSameRoot(entries[0], targetDirEntry)) { |
| copyTask.move = true; |
| } else { |
| copyTask.deleteAfterCopy = true; |
| } |
| } |
| copyTask.sourceOnDrive = sourceOnDrive; |
| copyTask.targetOnDrive = targetOnDrive; |
| 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.sendProgressEvent_('PROGRESS'); |
| } |
| }); |
| |
| 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 onTaskError = function(err) { |
| if (self.maybeCancel_()) |
| return; |
| self.sendProgressEvent_('ERROR', err); |
| self.resetQueue_(); |
| }; |
| |
| var onTaskSuccess = function(task) { |
| if (self.maybeCancel_()) |
| return; |
| if (!self.copyTasks_.length) { |
| // All tasks have been serviced, clean up and exit. |
| self.sendProgressEvent_('SUCCESS'); |
| 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.sendProgressEvent_('PROGRESS'); |
| |
| self.serviceNextTask_(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.sendProgressEvent_('BEGIN'); |
| this.serviceNextTask_(onTaskSuccess, onTaskError); |
| }; |
| |
| /** |
| * Service all entries in the next copy task. |
| * |
| * @param {function} successCallback On success. |
| * @param {function} errorCallback On error. |
| * @private |
| */ |
| FileCopyManager.prototype.serviceNextTask_ = function( |
| successCallback, errorCallback) { |
| var self = this; |
| var task = this.copyTasks_[0]; |
| |
| var onFilesystemError = function(err) { |
| errorCallback(new FileCopyManager.Error('FILESYSTEM_ERROR', err)); |
| }; |
| |
| var onTaskComplete = function() { |
| self.copyTasks_.shift(); |
| self.maybeScheduleCloseBackgroundPage_(); |
| successCallback(task); |
| }; |
| |
| var deleteOriginals = function() { |
| var count = task.originalEntries.length; |
| |
| var onEntryDeleted = function(entry) { |
| self.sendOperationEvent_('deleted', [entry]); |
| count--; |
| if (!count) |
| onTaskComplete(); |
| }; |
| |
| 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(targetEntry, size) { |
| // 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 { |
| onTaskComplete(); |
| } |
| return; |
| } |
| |
| self.sendProgressEvent_('PROGRESS'); |
| |
| // We yield a few ms between copies to give the browser a chance to service |
| // events (like perhaps the user clicking to cancel the copy, for example). |
| setTimeout(function() { |
| self.serviceNextTaskEntry_(task, onEntryServiced, errorCallback); |
| }, 10); |
| }; |
| |
| if (!task.zip) |
| this.serviceNextTaskEntry_(task, onEntryServiced, errorCallback); |
| else |
| this.serviceZipTask_(task, onTaskComplete, errorCallback); |
| }; |
| |
| /** |
| * Service the next entry in a given task. |
| * TODO(olege): Refactor this method into a separate class. |
| * |
| * @param {FileManager.Task} task A task. |
| * @param {function} successCallback On success. |
| * @param {function} errorCallback On error. |
| * @private |
| */ |
| FileCopyManager.prototype.serviceNextTaskEntry_ = function( |
| task, successCallback, errorCallback) { |
| if (this.maybeCancel_()) |
| return; |
| |
| var self = this; |
| var sourceEntry = task.getNextEntry(); |
| |
| if (!sourceEntry) { |
| // All entries in this task have been copied. |
| successCallback(null); |
| 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. |
| onError('UNEXPECTED_SOURCE_FILE', sourceEntry.fullPath); |
| return; |
| } |
| |
| var targetDirEntry = task.targetDirEntry; |
| var originalPath = sourceEntry.fullPath.substr(sourcePath.length + 1); |
| |
| originalPath = task.applyRenames(originalPath); |
| |
| var targetRelativePrefix = originalPath; |
| var targetExt = ''; |
| var index = targetRelativePrefix.lastIndexOf('.'); |
| if (index != -1) { |
| targetExt = targetRelativePrefix.substr(index); |
| targetRelativePrefix = targetRelativePrefix.substr(0, index); |
| } |
| |
| // If file already exists, we try to make a copy named 'file (1).ext'. |
| // If file is already named 'file (X).ext', we go with 'file (X+1).ext'. |
| // If new name is still occupied, we increase the number up to 10 times. |
| var copyNumber = 0; |
| var match = /^(.*?)(?: \((\d+)\))?$/.exec(targetRelativePrefix); |
| if (match && match[2]) { |
| copyNumber = parseInt(match[2], 10); |
| targetRelativePrefix = match[1]; |
| } |
| |
| var targetRelativePath = ''; |
| var renameTries = 0; |
| var firstExistingEntry = null; |
| |
| var onCopyCompleteBase = function(entry, size) { |
| task.markEntryComplete(entry, size); |
| successCallback(entry, size); |
| }; |
| |
| var onCopyComplete = function(entry, size) { |
| self.sendOperationEvent_('copied', [entry]); |
| onCopyCompleteBase(entry, size); |
| }; |
| |
| var onCopyProgress = function(entry, size) { |
| task.updateFileCopyProgress(entry, size); |
| self.sendProgressEvent_('PROGRESS'); |
| }; |
| |
| var onError = function(reason, data) { |
| self.log_('serviceNextTaskEntry error: ' + reason + ':', data); |
| errorCallback(new FileCopyManager.Error(reason, data)); |
| }; |
| |
| var onFilesystemCopyComplete = function(sourceEntry, targetEntry) { |
| // TODO(benchan): We currently do not know the size of data being |
| // copied by FileEntry.copyTo(), so task.completedBytes will not be |
| // increased. We will address this issue once we need to use |
| // task.completedBytes to track the progress. |
| self.sendOperationEvent_('copied', [sourceEntry, targetEntry]); |
| onCopyCompleteBase(targetEntry, 0); |
| }; |
| |
| var onFilesystemMoveComplete = function(sourceEntry, targetEntry) { |
| self.sendOperationEvent_('moved', [sourceEntry, targetEntry]); |
| onCopyCompleteBase(targetEntry, 0); |
| }; |
| |
| var onFilesystemError = function(err) { |
| onError('FILESYSTEM_ERROR', err); |
| }; |
| |
| var onTargetExists = function(existingEntry) { |
| if (!firstExistingEntry) |
| firstExistingEntry = existingEntry; |
| renameTries++; |
| if (renameTries < 10) { |
| copyNumber++; |
| tryNextCopy(); |
| } else { |
| onError('TARGET_EXISTS', firstExistingEntry); |
| } |
| }; |
| |
| /** |
| * Resolves the immediate parent directory entry and the file name of a |
| * given path, where the path is specified by a directory (not necessarily |
| * the immediate parent) and a path (not necessarily the file name) related |
| * to that directory. For instance, |
| * Given: |
| * |dirEntry| = DirectoryEntry('/root/dir1') |
| * |relativePath| = 'dir2/file' |
| * |
| * Return: |
| * |parentDirEntry| = DirectoryEntry('/root/dir1/dir2') |
| * |fileName| = 'file' |
| * |
| * @param {DirectoryEntry} dirEntry A directory entry. |
| * @param {string} relativePath A path relative to |dirEntry|. |
| * @param {function(Entry,string)} successCallback A callback for returning |
| * the |parentDirEntry| and |fileName| upon success. |
| * @param {function(FileError)} errorCallback An error callback when there is |
| * an error getting |parentDirEntry|. |
| */ |
| var resolveDirAndBaseName = function(dirEntry, relativePath, |
| successCallback, errorCallback) { |
| // |intermediatePath| contains the intermediate path components |
| // that are appended to |dirEntry| to form |parentDirEntry|. |
| var intermediatePath = ''; |
| var fileName = relativePath; |
| |
| // Extract the file name component from |relativePath|. |
| var index = relativePath.lastIndexOf('/'); |
| if (index != -1) { |
| intermediatePath = relativePath.substr(0, index); |
| fileName = relativePath.substr(index + 1); |
| } |
| |
| if (intermediatePath == '') { |
| successCallback(dirEntry, fileName); |
| } else { |
| dirEntry.getDirectory(intermediatePath, |
| {create: false}, |
| function(entry) { |
| successCallback(entry, fileName); |
| }, |
| errorCallback); |
| } |
| }; |
| |
| var onTargetNotResolved = function(err) { |
| // We expect to be unable to resolve the target file, since we're going |
| // to create it during the copy. However, if the resolve fails with |
| // anything other than NOT_FOUND, that's trouble. |
| if (err.code != FileError.NOT_FOUND_ERR) |
| return onError('FILESYSTEM_ERROR', err); |
| |
| if (task.move) { |
| resolveDirAndBaseName( |
| targetDirEntry, targetRelativePath, |
| function(dirEntry, fileName) { |
| sourceEntry.moveTo(dirEntry, fileName, |
| onFilesystemMoveComplete.bind(self, sourceEntry), |
| onFilesystemError); |
| }, |
| onFilesystemError); |
| return; |
| } |
| |
| // TODO(benchan): drive::FileSystem has not implemented directory copy, |
| // and thus we only call FileEntry.copyTo() for files. Revisit this |
| // code when drive::FileSystem supports directory copy. |
| if (sourceEntry.isFile && (task.sourceOnDrive || task.targetOnDrive)) { |
| var sourceFileUrl = sourceEntry.toURL(); |
| var targetFileUrl = targetDirEntry.toURL() + '/' + |
| encodeURIComponent(targetRelativePath); |
| var sourceFilePath = util.extractFilePath(sourceFileUrl); |
| var targetFilePath = util.extractFilePath(targetFileUrl); |
| var transferedBytes = 0; |
| |
| var onStartTransfer = function() { |
| chrome.fileBrowserPrivate.onFileTransfersUpdated.addListener( |
| onFileTransfersUpdated); |
| }; |
| |
| var onFailTransfer = function(err) { |
| chrome.fileBrowserPrivate.onFileTransfersUpdated.removeListener( |
| onFileTransfersUpdated); |
| |
| self.log_('Error copying ' + sourceFileUrl + ' to ' + targetFileUrl); |
| onFilesystemError(err); |
| }; |
| |
| var onSuccessTransfer = function(targetEntry) { |
| chrome.fileBrowserPrivate.onFileTransfersUpdated.removeListener( |
| onFileTransfersUpdated); |
| |
| targetEntry.getMetadata(function(metadata) { |
| if (metadata.size > transferedBytes) |
| onCopyProgress(sourceEntry, metadata.size - transferedBytes); |
| onFilesystemCopyComplete(sourceEntry, targetEntry); |
| }); |
| }; |
| |
| var downTransfer = 0; |
| var onFileTransfersUpdated = function(statusList) { |
| for (var i = 0; i < statusList.length; i++) { |
| var s = statusList[i]; |
| // Comparing urls is unreliable, since they may use different |
| // url encoding schemes (eg. rfc2396 vs. rfc3986). |
| var filePath = util.extractFilePath(s.fileUrl); |
| if (filePath == sourceFilePath || filePath == targetFilePath) { |
| var processed = s.processed; |
| |
| // It becomes tricky when both the sides are on Drive. |
| // Currently, it is implemented by download followed by upload. |
| // Note, however, download will not happen if the file is cached. |
| if (task.sourceOnDrive && task.targetOnDrive) { |
| if (filePath == sourceFilePath) { |
| // Download transfer is detected. Let's halve the progress. |
| downTransfer = processed = (s.processed >> 1); |
| } else { |
| // If download transfer has been detected, the upload transfer |
| // is stacked on top of it after halving. Otherwise, just use |
| // the upload transfer as-is. |
| processed = downTransfer > 0 ? |
| downTransfer + (s.processed >> 1) : s.processed; |
| } |
| } |
| |
| if (processed > transferedBytes) { |
| onCopyProgress(sourceEntry, processed - transferedBytes); |
| transferedBytes = processed; |
| } |
| } |
| } |
| }; |
| |
| if (task.sourceOnDrive && task.targetOnDrive) { |
| resolveDirAndBaseName( |
| targetDirEntry, targetRelativePath, |
| function(dirEntry, fileName) { |
| onStartTransfer(); |
| sourceEntry.copyTo(dirEntry, fileName, onSuccessTransfer, |
| onFailTransfer); |
| }, |
| onFilesystemError); |
| return; |
| } |
| |
| var onFileTransferCompleted = function() { |
| self.cancelCallback_ = null; |
| if (chrome.runtime.lastError) { |
| onFailTransfer({ |
| code: chrome.runtime.lastError.message, |
| toDrive: task.targetOnDrive, |
| sourceFileUrl: sourceFileUrl |
| }); |
| } else { |
| targetDirEntry.getFile(targetRelativePath, {}, onSuccessTransfer, |
| onFailTransfer); |
| } |
| }; |
| |
| self.cancelCallback_ = function() { |
| self.cancelCallback_ = null; |
| chrome.fileBrowserPrivate.onFileTransfersUpdated.removeListener( |
| onFileTransfersUpdated); |
| if (task.sourceOnDrive) { |
| chrome.fileBrowserPrivate.cancelFileTransfers([sourceFileUrl], |
| function() {}); |
| } else { |
| chrome.fileBrowserPrivate.cancelFileTransfers([targetFileUrl], |
| function() {}); |
| } |
| }; |
| |
| // TODO(benchan): Until drive::FileSystem supports FileWriter, we use the |
| // transferFile API to copy files into or out from a drive file system. |
| onStartTransfer(); |
| chrome.fileBrowserPrivate.transferFile( |
| sourceFileUrl, targetFileUrl, onFileTransferCompleted); |
| return; |
| } |
| |
| if (sourceEntry.isDirectory) { |
| targetDirEntry.getDirectory( |
| targetRelativePath, |
| {create: true, exclusive: true}, |
| function(targetEntry) { |
| if (targetRelativePath != originalPath) { |
| task.registerRename(originalPath, targetRelativePath); |
| } |
| onCopyComplete(targetEntry); |
| }, |
| util.flog('Error getting dir: ' + targetRelativePath, |
| onFilesystemError)); |
| } else { |
| targetDirEntry.getFile( |
| targetRelativePath, |
| {create: true, exclusive: true}, |
| function(targetEntry) { |
| self.copyEntry_(sourceEntry, targetEntry, |
| onCopyProgress, onCopyComplete, onError); |
| }, |
| util.flog('Error getting file: ' + targetRelativePath, |
| onFilesystemError)); |
| } |
| }; |
| |
| var tryNextCopy = function() { |
| targetRelativePath = targetRelativePrefix; |
| if (copyNumber > 0) { |
| targetRelativePath += ' (' + copyNumber + ')'; |
| } |
| targetRelativePath += targetExt; |
| |
| // Check to see if the target exists. This kicks off the rest of the copy |
| // if the target is not found, or raises an error if it does. |
| util.resolvePath(targetDirEntry, targetRelativePath, onTargetExists, |
| onTargetNotResolved); |
| }; |
| |
| tryNextCopy(); |
| }; |
| |
| /** |
| * Service a zip file creation task. |
| * |
| * @param {FileManager.Task} task A task. |
| * @param {function} completeCallback On complete. |
| * @param {function} errorCallback On error. |
| * @private |
| */ |
| FileCopyManager.prototype.serviceZipTask_ = function(task, completeCallback, |
| errorCallback) { |
| var self = this; |
| var dirURL = task.zipBaseDirEntry.toURL(); |
| var selectionURLs = []; |
| for (var i = 0; i < task.pendingDirectories.length; i++) |
| selectionURLs.push(task.pendingDirectories[i].toURL()); |
| for (var i = 0; i < task.pendingFiles.length; i++) |
| selectionURLs.push(task.pendingFiles[i].toURL()); |
| |
| 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)); |
| } |
| |
| var copyNumber = 0; |
| var firstExistingEntry = null; |
| var destPath = destName + '.zip'; |
| |
| var onError = function(reason, data) { |
| self.log_('serviceZipTask error: ' + reason + ':', data); |
| errorCallback(new FileCopyManager.Error(reason, data)); |
| }; |
| |
| var onTargetExists = function(existingEntry) { |
| if (copyNumber < 10) { |
| if (!firstExistingEntry) |
| firstExistingEntry = existingEntry; |
| copyNumber++; |
| tryZipSelection(); |
| } else { |
| onError('TARGET_EXISTS', firstExistingEntry); |
| } |
| }; |
| |
| var onTargetNotResolved = function() { |
| var onZipSelectionComplete = function(success) { |
| if (success) { |
| self.sendProgressEvent_('SUCCESS'); |
| } else { |
| self.sendProgressEvent_('ERROR', |
| new FileCopyManager.Error('FILESYSTEM_ERROR', '')); |
| } |
| completeCallback(task); |
| }; |
| |
| self.sendProgressEvent_('PROGRESS'); |
| chrome.fileBrowserPrivate.zipSelection(dirURL, selectionURLs, destPath, |
| onZipSelectionComplete); |
| }; |
| |
| var tryZipSelection = function() { |
| if (copyNumber > 0) |
| destPath = destName + ' (' + copyNumber + ').zip'; |
| |
| // Check if the target exists. This kicks off the rest of the zip file |
| // creation if the target is not found, or raises an error if it does. |
| util.resolvePath(task.targetDirEntry, destPath, onTargetExists, |
| onTargetNotResolved); |
| }; |
| |
| tryZipSelection(); |
| }; |
| |
| /** |
| * Copy the contents of sourceEntry into targetEntry. |
| * |
| * @private |
| * @param {Entry} sourceEntry entry that will be copied. |
| * @param {Entry} targetEntry entry to which sourceEntry will be copied. |
| * @param {function(Entry, 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(Entry, 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(string, object)} errorCallback function that will be called |
| * if an error is encountered. Takes error type and additional error data |
| * as parameters. |
| */ |
| FileCopyManager.prototype.copyEntry_ = function(sourceEntry, |
| targetEntry, |
| progressCallback, |
| successCallback, |
| errorCallback) { |
| if (this.maybeCancel_()) |
| return; |
| |
| var self = this; |
| |
| var onSourceFileFound = function(file) { |
| var onWriterCreated = function(writer) { |
| var reportedProgress = 0; |
| writer.onerror = function(progress) { |
| errorCallback('FILESYSTEM_ERROR', writer.error); |
| }; |
| |
| writer.onprogress = function(progress) { |
| if (self.maybeCancel_()) { |
| // If the copy was cancelled, we should abort the operation. |
| 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.onwriteend = function() { |
| sourceEntry.getMetadata(function(metadata) { |
| chrome.fileBrowserPrivate.setLastModified(targetEntry.toURL(), |
| '' + Math.round(metadata.modificationTime.getTime() / 1000)); |
| successCallback(targetEntry, file.size - reportedProgress); |
| }); |
| }; |
| |
| writer.write(file); |
| }; |
| |
| targetEntry.createWriter(onWriterCreated, errorCallback); |
| }; |
| |
| sourceEntry.file(onSourceFileFound, 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(task) { |
| self.deleteTasks_.shift(); |
| if (!self.deleteTasks_.length) { |
| // All tasks have been serviced, clean up and exit. |
| self.sendDeleteEvent_(task, 'SUCCESS'); |
| 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.sendDeleteEvent_(self.deleteTasks_[0], 'PROGRESS'); |
| self.serviceDeleteTask_(self.deleteTasks_[0], onTaskSuccess); |
| }; |
| |
| // 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.sendDeleteEvent_(this.deleteTasks_[0], 'BEGIN'); |
| this.serviceDeleteTask_(this.deleteTasks_[0], onTaskSuccess); |
| }; |
| |
| /** |
| * Performs the deletion. |
| * |
| * @param {Object} task The delete task (see deleteEntries function). |
| * @param {function(Object)} onComplete Completion callback with the task |
| * as an argument. |
| * @private |
| */ |
| FileCopyManager.prototype.serviceDeleteTask_ = function( |
| task, onComplete) { |
| var downcount = task.entries.length; |
| |
| var onEntryComplete = function() { |
| if (--downcount == 0) |
| onComplete(task); |
| }.bind(this); |
| |
| if (downcount == 0) |
| onComplete(task); |
| |
| for (var i = 0; i < task.entries.length; i++) { |
| var entry = task.entries[i]; |
| util.removeFileOrDirectory( |
| entry, |
| onEntryComplete, |
| onEntryComplete); // We ignore error, because we can't do anything here. |
| } |
| }; |
| |
| /** |
| * Send a 'delete' event to listeners. |
| * |
| * @param {Object} task The delete task (see deleteEntries function). |
| * @param {string} reason Event reason. |
| * @private |
| */ |
| FileCopyManager.prototype.sendDeleteEvent_ = function(task, reason) { |
| this.sendEvent_('delete', { |
| reason: reason, |
| urls: task.entries.map(function(e) { |
| return util.makeFilesystemUrl(e.fullPath); |
| }) |
| }); |
| }; |
| |
| /** |
| * Creates a zip file for the selection of files. |
| * |
| * @param {Entry} dirEntry the directory containing the selection. |
| * @param {boolean} isOnDrive If directory is on Drive. |
| * @param {Array.<Entry>} selectionEntries the selected entries. |
| */ |
| FileCopyManager.prototype.zipSelection = function(dirEntry, isOnDrive, |
| selectionEntries) { |
| var self = this; |
| var zipTask = new FileCopyManager.Task(dirEntry, dirEntry); |
| zipTask.zip = true; |
| zipTask.sourceOnDrive = isOnDrive; |
| zipTask.targetOnDrive = isOnDrive; |
| zipTask.setEntries(selectionEntries, function() { |
| // TODO: per-entry zip progress update with accurate byte count. |
| // For now just set pendingBytes to zero so that the progress bar is full. |
| zipTask.pendingBytes = 0; |
| 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.sendProgressEvent_('PROGRESS'); |
| } |
| }); |
| }; |