| OLD | NEW |
| (Empty) |
| 1 // Copyright 2013 The Chromium Authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 'use strict'; | |
| 6 | |
| 7 /** | |
| 8 * Utilities for FileOperationManager. | |
| 9 */ | |
| 10 var fileOperationUtil = {}; | |
| 11 | |
| 12 /** | |
| 13 * Simple wrapper for util.deduplicatePath. On error, this method translates | |
| 14 * the FileError to FileOperationManager.Error object. | |
| 15 * | |
| 16 * @param {DirectoryEntry} dirEntry The target directory entry. | |
| 17 * @param {string} relativePath The path to be deduplicated. | |
| 18 * @param {function(string)} successCallback Callback run with the deduplicated | |
| 19 * path on success. | |
| 20 * @param {function(FileOperationManager.Error)} errorCallback Callback run on | |
| 21 * error. | |
| 22 */ | |
| 23 fileOperationUtil.deduplicatePath = function( | |
| 24 dirEntry, relativePath, successCallback, errorCallback) { | |
| 25 util.deduplicatePath( | |
| 26 dirEntry, relativePath, successCallback, | |
| 27 function(err) { | |
| 28 var onFileSystemError = function(error) { | |
| 29 errorCallback(new FileOperationManager.Error( | |
| 30 util.FileOperationErrorType.FILESYSTEM_ERROR, error)); | |
| 31 }; | |
| 32 | |
| 33 if (err.code == FileError.PATH_EXISTS_ERR) { | |
| 34 // Failed to uniquify the file path. There should be an existing | |
| 35 // entry, so return the error with it. | |
| 36 util.resolvePath( | |
| 37 dirEntry, relativePath, | |
| 38 function(entry) { | |
| 39 errorCallback(new FileOperationManager.Error( | |
| 40 util.FileOperationErrorType.TARGET_EXISTS, entry)); | |
| 41 }, | |
| 42 onFileSystemError); | |
| 43 return; | |
| 44 } | |
| 45 onFileSystemError(err); | |
| 46 }); | |
| 47 }; | |
| 48 | |
| 49 /** | |
| 50 * Traverses files/subdirectories of the given entry, and returns them. | |
| 51 * In addition, this method annotate the size of each entry. The result will | |
| 52 * include the entry itself. | |
| 53 * | |
| 54 * @param {Entry} entry The root Entry for traversing. | |
| 55 * @param {function(Array.<Entry>)} successCallback Called when the traverse | |
| 56 * is successfully done with the array of the entries. | |
| 57 * @param {function(FileError)} errorCallback Called on error with the first | |
| 58 * occurred error (i.e. following errors will just be discarded). | |
| 59 */ | |
| 60 fileOperationUtil.resolveRecursively = function( | |
| 61 entry, successCallback, errorCallback) { | |
| 62 var result = []; | |
| 63 var error = null; | |
| 64 var numRunningTasks = 0; | |
| 65 | |
| 66 var maybeInvokeCallback = function() { | |
| 67 // If there still remain some running tasks, wait their finishing. | |
| 68 if (numRunningTasks > 0) | |
| 69 return; | |
| 70 | |
| 71 if (error) | |
| 72 errorCallback(error); | |
| 73 else | |
| 74 successCallback(result); | |
| 75 }; | |
| 76 | |
| 77 // The error handling can be shared. | |
| 78 var onError = function(fileError) { | |
| 79 // If this is the first error, remember it. | |
| 80 if (!error) | |
| 81 error = fileError; | |
| 82 --numRunningTasks; | |
| 83 maybeInvokeCallback(); | |
| 84 }; | |
| 85 | |
| 86 var process = function(entry) { | |
| 87 numRunningTasks++; | |
| 88 result.push(entry); | |
| 89 if (entry.isDirectory) { | |
| 90 // The size of a directory is 1 bytes here, so that the progress bar | |
| 91 // will work smoother. | |
| 92 // TODO(hidehiko): Remove this hack. | |
| 93 entry.size = 1; | |
| 94 | |
| 95 // Recursively traverse children. | |
| 96 var reader = entry.createReader(); | |
| 97 reader.readEntries( | |
| 98 function processSubEntries(subEntries) { | |
| 99 if (error || subEntries.length == 0) { | |
| 100 // If an error is found already, or this is the completion | |
| 101 // callback, then finish the process. | |
| 102 --numRunningTasks; | |
| 103 maybeInvokeCallback(); | |
| 104 return; | |
| 105 } | |
| 106 | |
| 107 for (var i = 0; i < subEntries.length; i++) | |
| 108 process(subEntries[i]); | |
| 109 | |
| 110 // Continue to read remaining children. | |
| 111 reader.readEntries(processSubEntries, onError); | |
| 112 }, | |
| 113 onError); | |
| 114 } else { | |
| 115 // For a file, annotate the file size. | |
| 116 entry.getMetadata(function(metadata) { | |
| 117 entry.size = metadata.size; | |
| 118 --numRunningTasks; | |
| 119 maybeInvokeCallback(); | |
| 120 }, onError); | |
| 121 } | |
| 122 }; | |
| 123 | |
| 124 process(entry); | |
| 125 }; | |
| 126 | |
| 127 /** | |
| 128 * Copies source to parent with the name newName recursively. | |
| 129 * This should work very similar to FileSystem API's copyTo. The difference is; | |
| 130 * - The progress callback is supported. | |
| 131 * - The cancellation is supported. | |
| 132 * | |
| 133 * @param {Entry} source The entry to be copied. | |
| 134 * @param {DirectoryEntry} parent The entry of the destination directory. | |
| 135 * @param {string} newName The name of copied file. | |
| 136 * @param {function(string, string)} entryChangedCallback | |
| 137 * Callback invoked when an entry is created with the source url and | |
| 138 * the destination url. | |
| 139 * @param {function(string, number)} progressCallback Callback invoked | |
| 140 * periodically during the copying. It takes the source url and the | |
| 141 * processed bytes of it. | |
| 142 * @param {function(string)} successCallback Callback invoked when the copy | |
| 143 * is successfully done with the url of the created entry. | |
| 144 * @param {function(FileError)} errorCallback Callback invoked when an error | |
| 145 * is found. | |
| 146 * @return {function()} Callback to cancel the current file copy operation. | |
| 147 * When the cancel is done, errorCallback will be called. The returned | |
| 148 * callback must not be called more than once. | |
| 149 */ | |
| 150 fileOperationUtil.copyTo = function( | |
| 151 source, parent, newName, entryChangedCallback, progressCallback, | |
| 152 successCallback, errorCallback) { | |
| 153 var copyId = null; | |
| 154 var pendingCallbacks = []; | |
| 155 | |
| 156 var onCopyProgress = function(progressCopyId, status) { | |
| 157 if (copyId == null) { | |
| 158 // If the copyId is not yet available, wait for it. | |
| 159 pendingCallbacks.push( | |
| 160 onCopyProgress.bind(null, progressCopyId, status)); | |
| 161 return; | |
| 162 } | |
| 163 | |
| 164 // This is not what we're interested in. | |
| 165 if (progressCopyId != copyId) | |
| 166 return; | |
| 167 | |
| 168 switch (status.type) { | |
| 169 case 'begin_copy_entry': | |
| 170 break; | |
| 171 | |
| 172 case 'end_copy_entry': | |
| 173 entryChangedCallback(status.sourceUrl, status.destinationUrl); | |
| 174 break; | |
| 175 | |
| 176 case 'progress': | |
| 177 progressCallback(status.sourceUrl, status.size); | |
| 178 break; | |
| 179 | |
| 180 case 'success': | |
| 181 chrome.fileBrowserPrivate.onCopyProgress.removeListener(onCopyProgress); | |
| 182 successCallback(status.destinationUrl); | |
| 183 break; | |
| 184 | |
| 185 case 'error': | |
| 186 chrome.fileBrowserPrivate.onCopyProgress.removeListener(onCopyProgress); | |
| 187 errorCallback(util.createFileError(status.error)); | |
| 188 break; | |
| 189 | |
| 190 default: | |
| 191 // Found unknown state. Cancel the task, and return an error. | |
| 192 console.error('Unknown progress type: ' + status.type); | |
| 193 chrome.fileBrowserPrivate.onCopyProgress.removeListener(onCopyProgress); | |
| 194 chrome.fileBrowserPrivate.cancelCopy(copyId); | |
| 195 errorCallback(util.createFileError(FileError.INVALID_STATE_ERR)); | |
| 196 } | |
| 197 }; | |
| 198 | |
| 199 // Register the listener before calling startCopy. Otherwise some events | |
| 200 // would be lost. | |
| 201 chrome.fileBrowserPrivate.onCopyProgress.addListener(onCopyProgress); | |
| 202 | |
| 203 // Then starts the copy. | |
| 204 chrome.fileBrowserPrivate.startCopy( | |
| 205 source.toURL(), parent.toURL(), newName, function(startCopyId) { | |
| 206 // last error contains the FileError code on error. | |
| 207 if (chrome.runtime.lastError) { | |
| 208 // Unsubscribe the progress listener. | |
| 209 chrome.fileBrowserPrivate.onCopyProgress.removeListener( | |
| 210 onCopyProgress); | |
| 211 errorCallback(util.createFileError( | |
| 212 Integer.parseInt(chrome.runtime.lastError, 10))); | |
| 213 return; | |
| 214 } | |
| 215 | |
| 216 copyId = startCopyId; | |
| 217 for (var i = 0; i < pendingCallbacks.length; i++) { | |
| 218 pendingCallbacks[i](); | |
| 219 } | |
| 220 }); | |
| 221 | |
| 222 return function() { | |
| 223 // If copyId is not yet available, wait for it. | |
| 224 if (copyId == null) { | |
| 225 pendingCallbacks.push(function() { | |
| 226 chrome.fileBrowserPrivate.cancelCopy(copyId); | |
| 227 }); | |
| 228 return; | |
| 229 } | |
| 230 | |
| 231 chrome.fileBrowserPrivate.cancelCopy(copyId); | |
| 232 }; | |
| 233 }; | |
| 234 | |
| 235 /** | |
| 236 * Thin wrapper of chrome.fileBrowserPrivate.zipSelection to adapt its | |
| 237 * interface similar to copyTo(). | |
| 238 * | |
| 239 * @param {Array.<Entry>} sources The array of entries to be archived. | |
| 240 * @param {DirectoryEntry} parent The entry of the destination directory. | |
| 241 * @param {string} newName The name of the archive to be created. | |
| 242 * @param {function(FileEntry)} successCallback Callback invoked when the | |
| 243 * operation is successfully done with the entry of the created archive. | |
| 244 * @param {function(FileError)} errorCallback Callback invoked when an error | |
| 245 * is found. | |
| 246 */ | |
| 247 fileOperationUtil.zipSelection = function( | |
| 248 sources, parent, newName, successCallback, errorCallback) { | |
| 249 chrome.fileBrowserPrivate.zipSelection( | |
| 250 parent.toURL(), | |
| 251 sources.map(function(e) { return e.toURL(); }), | |
| 252 newName, function(success) { | |
| 253 if (!success) { | |
| 254 // Failed to create a zip archive. | |
| 255 errorCallback( | |
| 256 util.createFileError(FileError.INVALID_MODIFICATION_ERR)); | |
| 257 return; | |
| 258 } | |
| 259 | |
| 260 // Returns the created entry via callback. | |
| 261 parent.getFile( | |
| 262 newName, {create: false}, successCallback, errorCallback); | |
| 263 }); | |
| 264 }; | |
| 265 | |
| 266 /** | |
| 267 * @constructor | |
| 268 */ | |
| 269 function FileOperationManager() { | |
| 270 this.copyTasks_ = []; | |
| 271 this.deleteTasks_ = []; | |
| 272 this.cancelObservers_ = []; | |
| 273 this.cancelRequested_ = false; | |
| 274 this.cancelCallback_ = null; | |
| 275 this.unloadTimeout_ = null; | |
| 276 this.taskIdCounter_ = 0; | |
| 277 | |
| 278 this.eventRouter_ = new FileOperationManager.EventRouter(); | |
| 279 | |
| 280 Object.seal(this); | |
| 281 } | |
| 282 | |
| 283 /** | |
| 284 * Get FileOperationManager instance. In case is hasn't been initialized, a new | |
| 285 * instance is created. | |
| 286 * | |
| 287 * @return {FileOperationManager} A FileOperationManager instance. | |
| 288 */ | |
| 289 FileOperationManager.getInstance = function() { | |
| 290 if (!FileOperationManager.instance_) | |
| 291 FileOperationManager.instance_ = new FileOperationManager(); | |
| 292 | |
| 293 return FileOperationManager.instance_; | |
| 294 }; | |
| 295 | |
| 296 /** | |
| 297 * Manages Event dispatching. | |
| 298 * Currently this can send three types of events: "copy-progress", | |
| 299 * "copy-operation-completed" and "delete". | |
| 300 * | |
| 301 * TODO(hidehiko): Reorganize the event dispatching mechanism. | |
| 302 * @constructor | |
| 303 * @extends {cr.EventTarget} | |
| 304 */ | |
| 305 FileOperationManager.EventRouter = function() { | |
| 306 }; | |
| 307 | |
| 308 /** | |
| 309 * Extends cr.EventTarget. | |
| 310 */ | |
| 311 FileOperationManager.EventRouter.prototype.__proto__ = cr.EventTarget.prototype; | |
| 312 | |
| 313 /** | |
| 314 * Dispatches a simple "copy-progress" event with reason and current | |
| 315 * FileOperationManager status. If it is an ERROR event, error should be set. | |
| 316 * | |
| 317 * @param {string} reason Event type. One of "BEGIN", "PROGRESS", "SUCCESS", | |
| 318 * "ERROR" or "CANCELLED". TODO(hidehiko): Use enum. | |
| 319 * @param {Object} status Current FileOperationManager's status. See also | |
| 320 * FileOperationManager.getStatus(). | |
| 321 * @param {string} taskId ID of task related with the event. | |
| 322 * @param {FileOperationManager.Error=} opt_error The info for the error. This | |
| 323 * should be set iff the reason is "ERROR". | |
| 324 */ | |
| 325 FileOperationManager.EventRouter.prototype.sendProgressEvent = function( | |
| 326 reason, status, taskId, opt_error) { | |
| 327 var event = new Event('copy-progress'); | |
| 328 event.reason = reason; | |
| 329 event.status = status; | |
| 330 event.taskId = taskId; | |
| 331 if (opt_error) | |
| 332 event.error = opt_error; | |
| 333 this.dispatchEvent(event); | |
| 334 }; | |
| 335 | |
| 336 /** | |
| 337 * Dispatches an event to notify that an entry is changed (created or deleted). | |
| 338 * @param {util.EntryChangedKind} kind The enum to represent if the entry is | |
| 339 * created or deleted. | |
| 340 * @param {Entry} entry The changed entry. | |
| 341 */ | |
| 342 FileOperationManager.EventRouter.prototype.sendEntryChangedEvent = function( | |
| 343 kind, entry) { | |
| 344 var event = new Event('entry-changed'); | |
| 345 event.kind = kind; | |
| 346 event.entry = entry; | |
| 347 this.dispatchEvent(event); | |
| 348 }; | |
| 349 | |
| 350 /** | |
| 351 * Dispatches an event to notify entries are changed for delete task. | |
| 352 * | |
| 353 * @param {string} reason Event type. One of "BEGIN", "PROGRESS", "SUCCESS", | |
| 354 * or "ERROR". TODO(hidehiko): Use enum. | |
| 355 * @param {Array.<string>} urls An array of URLs which are affected by delete | |
| 356 * operation. | |
| 357 * @param {string} taskId ID of task related with the event. | |
| 358 */ | |
| 359 FileOperationManager.EventRouter.prototype.sendDeleteEvent = function( | |
| 360 reason, urls, taskId) { | |
| 361 var event = new Event('delete'); | |
| 362 event.reason = reason; | |
| 363 event.urls = urls; | |
| 364 this.dispatchEvent(event); | |
| 365 }; | |
| 366 | |
| 367 /** | |
| 368 * A record of a queued copy operation. | |
| 369 * | |
| 370 * Multiple copy operations may be queued at any given time. Additional | |
| 371 * Tasks may be added while the queue is being serviced. Though a | |
| 372 * cancel operation cancels everything in the queue. | |
| 373 * | |
| 374 * @param {util.FileOperationType} operationType The type of this operation. | |
| 375 * @param {Array.<Entry>} sourceEntries Array of source entries. | |
| 376 * @param {DirectoryEntry} targetDirEntry Target directory. | |
| 377 * @constructor | |
| 378 */ | |
| 379 FileOperationManager.Task = function( | |
| 380 operationType, sourceEntries, targetDirEntry) { | |
| 381 this.operationType = operationType; | |
| 382 this.sourceEntries = sourceEntries; | |
| 383 this.targetDirEntry = targetDirEntry; | |
| 384 | |
| 385 /** | |
| 386 * An array of map from url to Entry being processed. | |
| 387 * @type {Array.<Object<string, Entry>>} | |
| 388 */ | |
| 389 this.processingEntries = null; | |
| 390 | |
| 391 /** | |
| 392 * Total number of bytes to be processed. Filled in initialize(). | |
| 393 * @type {number} | |
| 394 */ | |
| 395 this.totalBytes = 0; | |
| 396 | |
| 397 /** | |
| 398 * Total number of already processed bytes. Updated periodically. | |
| 399 * @type {number} | |
| 400 */ | |
| 401 this.processedBytes = 0; | |
| 402 | |
| 403 this.deleteAfterCopy = false; | |
| 404 | |
| 405 /** | |
| 406 * Set to true when cancel is requested. | |
| 407 * @private {boolean} | |
| 408 */ | |
| 409 this.cancelRequested_ = false; | |
| 410 | |
| 411 /** | |
| 412 * Callback to cancel the running process. | |
| 413 * @private {function()} | |
| 414 */ | |
| 415 this.cancelCallback_ = null; | |
| 416 | |
| 417 // TODO(hidehiko): After we support recursive copy, we don't need this. | |
| 418 // If directory already exists, we try to make a copy named 'dir (X)', | |
| 419 // where X is a number. When we do this, all subsequent copies from | |
| 420 // inside the subtree should be mapped to the new directory name. | |
| 421 // For example, if 'dir' was copied as 'dir (1)', then 'dir\file.txt' should | |
| 422 // become 'dir (1)\file.txt'. | |
| 423 this.renamedDirectories_ = []; | |
| 424 }; | |
| 425 | |
| 426 /** | |
| 427 * @param {function()} callback When entries resolved. | |
| 428 */ | |
| 429 FileOperationManager.Task.prototype.initialize = function(callback) { | |
| 430 }; | |
| 431 | |
| 432 /** | |
| 433 * Updates copy progress status for the entry. | |
| 434 * | |
| 435 * @param {number} size Number of bytes that has been copied since last update. | |
| 436 */ | |
| 437 FileOperationManager.Task.prototype.updateFileCopyProgress = function(size) { | |
| 438 this.completedBytes += size; | |
| 439 }; | |
| 440 | |
| 441 /** | |
| 442 * Requests cancellation of this task. | |
| 443 * When the cancellation is done, it is notified via callbacks of run(). | |
| 444 */ | |
| 445 FileOperationManager.Task.prototype.requestCancel = function() { | |
| 446 this.cancelRequested_ = true; | |
| 447 if (this.cancelCallback_) { | |
| 448 this.cancelCallback_(); | |
| 449 this.cancelCallback_ = null; | |
| 450 } | |
| 451 }; | |
| 452 | |
| 453 /** | |
| 454 * Runs the task. Sub classes must implement this method. | |
| 455 * | |
| 456 * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback | |
| 457 * Callback invoked when an entry is changed. | |
| 458 * @param {function()} progressCallback Callback invoked periodically during | |
| 459 * the operation. | |
| 460 * @param {function()} successCallback Callback run on success. | |
| 461 * @param {function(FileOperationManager.Error)} errorCallback Callback run on | |
| 462 * error. | |
| 463 */ | |
| 464 FileOperationManager.Task.prototype.run = function( | |
| 465 entryChangedCallback, progressCallback, successCallback, errorCallback) { | |
| 466 }; | |
| 467 | |
| 468 /** | |
| 469 * Get states of the task. | |
| 470 * TOOD(hirono): Removes this method and sets a task to progress events. | |
| 471 * @return {object} Status object. | |
| 472 */ | |
| 473 FileOperationManager.Task.prototype.getStatus = function() { | |
| 474 var numRemainingItems = this.countRemainingItems(); | |
| 475 return { | |
| 476 operationType: this.operationType, | |
| 477 numRemainingItems: numRemainingItems, | |
| 478 totalBytes: this.totalBytes, | |
| 479 processedBytes: this.processedBytes, | |
| 480 processingEntry: this.getSingleEntry() | |
| 481 }; | |
| 482 }; | |
| 483 | |
| 484 /** | |
| 485 * Counts the number of remaining items. | |
| 486 * @return {number} Number of remaining items. | |
| 487 */ | |
| 488 FileOperationManager.Task.prototype.countRemainingItems = function() { | |
| 489 var count = 0; | |
| 490 for (var i = 0; i < this.processingEntries.length; i++) { | |
| 491 for (var url in this.processingEntries[i]) { | |
| 492 count++; | |
| 493 } | |
| 494 } | |
| 495 return count; | |
| 496 }; | |
| 497 | |
| 498 /** | |
| 499 * Obtains the single processing entry. If there are multiple processing | |
| 500 * entries, it returns null. | |
| 501 * @return {Entry} First entry. | |
| 502 */ | |
| 503 FileOperationManager.Task.prototype.getSingleEntry = function() { | |
| 504 if (this.countRemainingItems() !== 1) | |
| 505 return null; | |
| 506 for (var i = 0; i < this.processingEntries.length; i++) { | |
| 507 var entryMap = this.processingEntries[i]; | |
| 508 for (var name in entryMap) | |
| 509 return entryMap[name]; | |
| 510 } | |
| 511 return null; | |
| 512 }; | |
| 513 | |
| 514 /** | |
| 515 * Task to copy entries. | |
| 516 * | |
| 517 * @param {Array.<Entry>} sourceEntries Array of source entries. | |
| 518 * @param {DirectoryEntry} targetDirEntry Target directory. | |
| 519 * @constructor | |
| 520 * @extends {FileOperationManager.Task} | |
| 521 */ | |
| 522 FileOperationManager.CopyTask = function(sourceEntries, targetDirEntry) { | |
| 523 FileOperationManager.Task.call( | |
| 524 this, util.FileOperationType.COPY, sourceEntries, targetDirEntry); | |
| 525 }; | |
| 526 | |
| 527 /** | |
| 528 * Extends FileOperationManager.Task. | |
| 529 */ | |
| 530 FileOperationManager.CopyTask.prototype.__proto__ = | |
| 531 FileOperationManager.Task.prototype; | |
| 532 | |
| 533 /** | |
| 534 * Initializes the CopyTask. | |
| 535 * @param {function()} callback Called when the initialize is completed. | |
| 536 */ | |
| 537 FileOperationManager.CopyTask.prototype.initialize = function(callback) { | |
| 538 var group = new AsyncUtil.Group(); | |
| 539 // Correct all entries to be copied for status update. | |
| 540 this.processingEntries = []; | |
| 541 for (var i = 0; i < this.sourceEntries.length; i++) { | |
| 542 group.add(function(index, callback) { | |
| 543 fileOperationUtil.resolveRecursively( | |
| 544 this.sourceEntries[index], | |
| 545 function(resolvedEntries) { | |
| 546 var resolvedEntryMap = {}; | |
| 547 for (var j = 0; j < resolvedEntries.length; ++j) { | |
| 548 var entry = resolvedEntries[j]; | |
| 549 entry.processedBytes = 0; | |
| 550 resolvedEntryMap[entry.toURL()] = entry; | |
| 551 } | |
| 552 this.processingEntries[index] = resolvedEntryMap; | |
| 553 callback(); | |
| 554 }.bind(this), | |
| 555 function(error) { | |
| 556 console.error( | |
| 557 'Failed to resolve for copy: %s', | |
| 558 util.getFileErrorMnemonic(error.code)); | |
| 559 }); | |
| 560 }.bind(this, i)); | |
| 561 } | |
| 562 | |
| 563 group.run(function() { | |
| 564 // Fill totalBytes. | |
| 565 this.totalBytes = 0; | |
| 566 for (var i = 0; i < this.processingEntries.length; i++) { | |
| 567 for (var url in this.processingEntries[i]) | |
| 568 this.totalBytes += this.processingEntries[i][url].size; | |
| 569 } | |
| 570 | |
| 571 callback(); | |
| 572 }.bind(this)); | |
| 573 }; | |
| 574 | |
| 575 /** | |
| 576 * Copies all entries to the target directory. | |
| 577 * Note: this method contains also the operation of "Move" due to historical | |
| 578 * reason. | |
| 579 * | |
| 580 * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback | |
| 581 * Callback invoked when an entry is changed. | |
| 582 * @param {function()} progressCallback Callback invoked periodically during | |
| 583 * the copying. | |
| 584 * @param {function()} successCallback On success. | |
| 585 * @param {function(FileOperationManager.Error)} errorCallback On error. | |
| 586 * @override | |
| 587 */ | |
| 588 FileOperationManager.CopyTask.prototype.run = function( | |
| 589 entryChangedCallback, progressCallback, successCallback, errorCallback) { | |
| 590 // TODO(hidehiko): We should be able to share the code to iterate on entries | |
| 591 // with serviceMoveTask_(). | |
| 592 if (this.sourceEntries.length == 0) { | |
| 593 successCallback(); | |
| 594 return; | |
| 595 } | |
| 596 | |
| 597 // TODO(hidehiko): Delete after copy is the implementation of Move. | |
| 598 // Migrate the part into MoveTask.run(). | |
| 599 var deleteOriginals = function() { | |
| 600 var count = this.sourceEntries.length; | |
| 601 | |
| 602 var onEntryDeleted = function(entry) { | |
| 603 entryChangedCallback(util.EntryChangedKind.DELETED, entry); | |
| 604 count--; | |
| 605 if (!count) | |
| 606 successCallback(); | |
| 607 }; | |
| 608 | |
| 609 var onFilesystemError = function(err) { | |
| 610 errorCallback(new FileOperationManager.Error( | |
| 611 util.FileOperationErrorType.FILESYSTEM_ERROR, err)); | |
| 612 }; | |
| 613 | |
| 614 for (var i = 0; i < this.sourceEntries.length; i++) { | |
| 615 var entry = this.sourceEntries[i]; | |
| 616 util.removeFileOrDirectory( | |
| 617 entry, onEntryDeleted.bind(null, entry), onFilesystemError); | |
| 618 } | |
| 619 }.bind(this); | |
| 620 | |
| 621 AsyncUtil.forEach( | |
| 622 this.sourceEntries, | |
| 623 function(callback, entry, index) { | |
| 624 if (this.cancelRequested_) { | |
| 625 errorCallback(new FileOperationManager.Error( | |
| 626 util.FileOperationErrorType.FILESYSTEM_ERROR, | |
| 627 util.createFileError(FileError.ABORT_ERR))); | |
| 628 return; | |
| 629 } | |
| 630 progressCallback(); | |
| 631 this.cancelCallback_ = FileOperationManager.CopyTask.processEntry_( | |
| 632 entry, this.targetDirEntry, | |
| 633 function(sourceUrl, destinationUrl) { | |
| 634 // Finalize the entry's progress state. | |
| 635 var entry = this.processingEntries[index][sourceUrl]; | |
| 636 if (entry) { | |
| 637 this.processedBytes += entry.size - entry.processedBytes; | |
| 638 progressCallback(); | |
| 639 delete this.processingEntries[index][sourceUrl]; | |
| 640 } | |
| 641 | |
| 642 webkitResolveLocalFileSystemURL( | |
| 643 destinationUrl, function(destinationEntry) { | |
| 644 entryChangedCallback( | |
| 645 util.EntryChangedKind.CREATED, destinationEntry); | |
| 646 }); | |
| 647 }.bind(this), | |
| 648 function(source_url, size) { | |
| 649 var entry = this.processingEntries[index][source_url]; | |
| 650 if (entry) { | |
| 651 this.processedBytes += size - entry.processedBytes; | |
| 652 entry.processedBytes = size; | |
| 653 progressCallback(); | |
| 654 } | |
| 655 }.bind(this), | |
| 656 function() { | |
| 657 this.cancelCallback_ = null; | |
| 658 callback(); | |
| 659 }.bind(this), | |
| 660 function(error) { | |
| 661 this.cancelCallback_ = null; | |
| 662 errorCallback(error); | |
| 663 }.bind(this)); | |
| 664 }, | |
| 665 function() { | |
| 666 if (this.deleteAfterCopy) { | |
| 667 deleteOriginals(); | |
| 668 } else { | |
| 669 successCallback(); | |
| 670 } | |
| 671 }.bind(this), | |
| 672 this); | |
| 673 }; | |
| 674 | |
| 675 /** | |
| 676 * Copies the source entry to the target directory. | |
| 677 * | |
| 678 * @param {Entry} sourceEntry An entry to be copied. | |
| 679 * @param {DirectoryEntry} destinationEntry The entry which will contain the | |
| 680 * copied entry. | |
| 681 * @param {function(string, string)} entryChangedCallback | |
| 682 * Callback invoked when an entry is created with the source url and | |
| 683 * the destination url. | |
| 684 * @param {function(string, number)} progressCallback Callback invoked | |
| 685 * periodically during the copying. | |
| 686 * @param {function()} successCallback On success. | |
| 687 * @param {function(FileOperationManager.Error)} errorCallback On error. | |
| 688 * @return {function()} Callback to cancel the current file copy operation. | |
| 689 * When the cancel is done, errorCallback will be called. The returned | |
| 690 * callback must not be called more than once. | |
| 691 * @private | |
| 692 */ | |
| 693 FileOperationManager.CopyTask.processEntry_ = function( | |
| 694 sourceEntry, destinationEntry, entryChangedCallback, progressCallback, | |
| 695 successCallback, errorCallback) { | |
| 696 var cancelRequested = false; | |
| 697 var cancelCallback = null; | |
| 698 fileOperationUtil.deduplicatePath( | |
| 699 destinationEntry, sourceEntry.name, | |
| 700 function(destinationName) { | |
| 701 if (cancelRequested) { | |
| 702 errorCallback(new FileOperationManager.Error( | |
| 703 util.FileOperationErrorType.FILESYSTEM_ERROR, | |
| 704 util.createFileError(FileError.ABORT_ERR))); | |
| 705 return; | |
| 706 } | |
| 707 | |
| 708 cancelCallback = fileOperationUtil.copyTo( | |
| 709 sourceEntry, destinationEntry, destinationName, | |
| 710 entryChangedCallback, progressCallback, | |
| 711 function(entry) { | |
| 712 cancelCallback = null; | |
| 713 successCallback(); | |
| 714 }, | |
| 715 function(error) { | |
| 716 cancelCallback = null; | |
| 717 errorCallback(new FileOperationManager.Error( | |
| 718 util.FileOperationErrorType.FILESYSTEM_ERROR, error)); | |
| 719 }); | |
| 720 }, | |
| 721 errorCallback); | |
| 722 | |
| 723 return function() { | |
| 724 cancelRequested = true; | |
| 725 if (cancelCallback) { | |
| 726 cancelCallback(); | |
| 727 cancelCallback = null; | |
| 728 } | |
| 729 }; | |
| 730 }; | |
| 731 | |
| 732 /** | |
| 733 * Task to move entries. | |
| 734 * | |
| 735 * @param {Array.<Entry>} sourceEntries Array of source entries. | |
| 736 * @param {DirectoryEntry} targetDirEntry Target directory. | |
| 737 * @constructor | |
| 738 * @extends {FileOperationManager.Task} | |
| 739 */ | |
| 740 FileOperationManager.MoveTask = function(sourceEntries, targetDirEntry) { | |
| 741 FileOperationManager.Task.call( | |
| 742 this, util.FileOperationType.MOVE, sourceEntries, targetDirEntry); | |
| 743 }; | |
| 744 | |
| 745 /** | |
| 746 * Extends FileOperationManager.Task. | |
| 747 */ | |
| 748 FileOperationManager.MoveTask.prototype.__proto__ = | |
| 749 FileOperationManager.Task.prototype; | |
| 750 | |
| 751 /** | |
| 752 * Initializes the MoveTask. | |
| 753 * @param {function()} callback Called when the initialize is completed. | |
| 754 */ | |
| 755 FileOperationManager.MoveTask.prototype.initialize = function(callback) { | |
| 756 // This may be moving from search results, where it fails if we | |
| 757 // move parent entries earlier than child entries. We should | |
| 758 // process the deepest entry first. Since move of each entry is | |
| 759 // done by a single moveTo() call, we don't need to care about the | |
| 760 // recursive traversal order. | |
| 761 this.sourceEntries.sort(function(entry1, entry2) { | |
| 762 return entry2.fullPath.length - entry1.fullPath.length; | |
| 763 }); | |
| 764 | |
| 765 this.processingEntries = []; | |
| 766 for (var i = 0; i < this.sourceEntries.length; i++) { | |
| 767 var processingEntryMap = {}; | |
| 768 var entry = this.sourceEntries[i]; | |
| 769 | |
| 770 // The move should be done with updating the metadata. So here we assume | |
| 771 // all the file size is 1 byte. (Avoiding 0, so that progress bar can | |
| 772 // move smoothly). | |
| 773 // TODO(hidehiko): Remove this hack. | |
| 774 entry.size = 1; | |
| 775 processingEntryMap[entry.toURL()] = entry; | |
| 776 this.processingEntries[i] = processingEntryMap; | |
| 777 } | |
| 778 | |
| 779 callback(); | |
| 780 }; | |
| 781 | |
| 782 /** | |
| 783 * Moves all entries in the task. | |
| 784 * | |
| 785 * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback | |
| 786 * Callback invoked when an entry is changed. | |
| 787 * @param {function()} progressCallback Callback invoked periodically during | |
| 788 * the moving. | |
| 789 * @param {function()} successCallback On success. | |
| 790 * @param {function(FileOperationManager.Error)} errorCallback On error. | |
| 791 * @override | |
| 792 */ | |
| 793 FileOperationManager.MoveTask.prototype.run = function( | |
| 794 entryChangedCallback, progressCallback, successCallback, errorCallback) { | |
| 795 if (this.sourceEntries.length == 0) { | |
| 796 successCallback(); | |
| 797 return; | |
| 798 } | |
| 799 | |
| 800 AsyncUtil.forEach( | |
| 801 this.sourceEntries, | |
| 802 function(callback, entry, index) { | |
| 803 if (this.cancelRequested_) { | |
| 804 errorCallback(new FileOperationManager.Error( | |
| 805 util.FileOperationErrorType.FILESYSTEM_ERROR, | |
| 806 util.createFileError(FileError.ABORT_ERR))); | |
| 807 return; | |
| 808 } | |
| 809 progressCallback(); | |
| 810 FileOperationManager.MoveTask.processEntry_( | |
| 811 entry, this.targetDirEntry, entryChangedCallback, | |
| 812 function() { | |
| 813 // Erase the processing entry. | |
| 814 this.processingEntries[index] = {}; | |
| 815 this.processedBytes++; | |
| 816 callback(); | |
| 817 }.bind(this), | |
| 818 errorCallback); | |
| 819 }, | |
| 820 function() { | |
| 821 successCallback(); | |
| 822 }.bind(this), | |
| 823 this); | |
| 824 }; | |
| 825 | |
| 826 /** | |
| 827 * Moves the sourceEntry to the targetDirEntry in this task. | |
| 828 * | |
| 829 * @param {Entry} sourceEntry An entry to be moved. | |
| 830 * @param {DirectoryEntry} destinationEntry The entry of the destination | |
| 831 * directory. | |
| 832 * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback | |
| 833 * Callback invoked when an entry is changed. | |
| 834 * @param {function()} successCallback On success. | |
| 835 * @param {function(FileOperationManager.Error)} errorCallback On error. | |
| 836 * @private | |
| 837 */ | |
| 838 FileOperationManager.MoveTask.processEntry_ = function( | |
| 839 sourceEntry, destinationEntry, entryChangedCallback, successCallback, | |
| 840 errorCallback) { | |
| 841 fileOperationUtil.deduplicatePath( | |
| 842 destinationEntry, | |
| 843 sourceEntry.name, | |
| 844 function(destinationName) { | |
| 845 sourceEntry.moveTo( | |
| 846 destinationEntry, destinationName, | |
| 847 function(movedEntry) { | |
| 848 entryChangedCallback(util.EntryChangedKind.CREATED, movedEntry); | |
| 849 entryChangedCallback(util.EntryChangedKind.DELETED, sourceEntry); | |
| 850 successCallback(); | |
| 851 }, | |
| 852 function(error) { | |
| 853 errorCallback(new FileOperationManager.Error( | |
| 854 util.FileOperationErrorType.FILESYSTEM_ERROR, error)); | |
| 855 }); | |
| 856 }, | |
| 857 errorCallback); | |
| 858 }; | |
| 859 | |
| 860 /** | |
| 861 * Task to create a zip archive. | |
| 862 * | |
| 863 * @param {Array.<Entry>} sourceEntries Array of source entries. | |
| 864 * @param {DirectoryEntry} targetDirEntry Target directory. | |
| 865 * @param {DirectoryEntry} zipBaseDirEntry Base directory dealt as a root | |
| 866 * in ZIP archive. | |
| 867 * @constructor | |
| 868 * @extends {FileOperationManager.Task} | |
| 869 */ | |
| 870 FileOperationManager.ZipTask = function( | |
| 871 sourceEntries, targetDirEntry, zipBaseDirEntry) { | |
| 872 FileOperationManager.Task.call( | |
| 873 this, util.FileOperationType.ZIP, sourceEntries, targetDirEntry); | |
| 874 this.zipBaseDirEntry = zipBaseDirEntry; | |
| 875 }; | |
| 876 | |
| 877 /** | |
| 878 * Extends FileOperationManager.Task. | |
| 879 */ | |
| 880 FileOperationManager.ZipTask.prototype.__proto__ = | |
| 881 FileOperationManager.Task.prototype; | |
| 882 | |
| 883 | |
| 884 /** | |
| 885 * Initializes the ZipTask. | |
| 886 * @param {function()} callback Called when the initialize is completed. | |
| 887 */ | |
| 888 FileOperationManager.ZipTask.prototype.initialize = function(callback) { | |
| 889 var resolvedEntryMap = {}; | |
| 890 var group = new AsyncUtil.Group(); | |
| 891 for (var i = 0; i < this.sourceEntries.length; i++) { | |
| 892 group.add(function(index, callback) { | |
| 893 fileOperationUtil.resolveRecursively( | |
| 894 this.sourceEntries[index], | |
| 895 function(entries) { | |
| 896 for (var j = 0; j < entries.length; j++) | |
| 897 resolvedEntryMap[entries[j].toURL()] = entries[j]; | |
| 898 callback(); | |
| 899 }, | |
| 900 function(error) {}); | |
| 901 }.bind(this, i)); | |
| 902 } | |
| 903 | |
| 904 group.run(function() { | |
| 905 // For zip archiving, all the entries are processed at once. | |
| 906 this.processingEntries = [resolvedEntryMap]; | |
| 907 | |
| 908 this.totalBytes = 0; | |
| 909 for (var url in resolvedEntryMap) | |
| 910 this.totalBytes += resolvedEntryMap[url].size; | |
| 911 | |
| 912 callback(); | |
| 913 }.bind(this)); | |
| 914 }; | |
| 915 | |
| 916 /** | |
| 917 * Runs a zip file creation task. | |
| 918 * | |
| 919 * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback | |
| 920 * Callback invoked when an entry is changed. | |
| 921 * @param {function()} progressCallback Callback invoked periodically during | |
| 922 * the moving. | |
| 923 * @param {function()} successCallback On complete. | |
| 924 * @param {function(FileOperationManager.Error)} errorCallback On error. | |
| 925 * @override | |
| 926 */ | |
| 927 FileOperationManager.ZipTask.prototype.run = function( | |
| 928 entryChangedCallback, progressCallback, successCallback, errorCallback) { | |
| 929 // TODO(hidehiko): we should localize the name. | |
| 930 var destName = 'Archive'; | |
| 931 if (this.sourceEntries.length == 1) { | |
| 932 var entryPath = this.sourceEntries[0].fullPath; | |
| 933 var i = entryPath.lastIndexOf('/'); | |
| 934 var basename = (i < 0) ? entryPath : entryPath.substr(i + 1); | |
| 935 i = basename.lastIndexOf('.'); | |
| 936 destName = ((i < 0) ? basename : basename.substr(0, i)); | |
| 937 } | |
| 938 | |
| 939 fileOperationUtil.deduplicatePath( | |
| 940 this.targetDirEntry, destName + '.zip', | |
| 941 function(destPath) { | |
| 942 // TODO: per-entry zip progress update with accurate byte count. | |
| 943 // For now just set completedBytes to same value as totalBytes so | |
| 944 // that the progress bar is full. | |
| 945 this.processedBytes = this.totalBytes; | |
| 946 progressCallback(); | |
| 947 | |
| 948 // The number of elements in processingEntries is 1. See also | |
| 949 // initialize(). | |
| 950 var entries = []; | |
| 951 for (var url in this.processingEntries[0]) | |
| 952 entries.push(this.processingEntries[0][url]); | |
| 953 | |
| 954 fileOperationUtil.zipSelection( | |
| 955 entries, | |
| 956 this.zipBaseDirEntry, | |
| 957 destPath, | |
| 958 function(entry) { | |
| 959 entryChangedCallback(util.EntryChangedKind.CREATE, entry); | |
| 960 successCallback(); | |
| 961 }, | |
| 962 function(error) { | |
| 963 errorCallback(new FileOperationManager.Error( | |
| 964 util.FileOperationErrorType.FILESYSTEM_ERROR, error)); | |
| 965 }); | |
| 966 }.bind(this), | |
| 967 errorCallback); | |
| 968 }; | |
| 969 | |
| 970 /** | |
| 971 * Error class used to report problems with a copy operation. | |
| 972 * If the code is UNEXPECTED_SOURCE_FILE, data should be a path of the file. | |
| 973 * If the code is TARGET_EXISTS, data should be the existing Entry. | |
| 974 * If the code is FILESYSTEM_ERROR, data should be the FileError. | |
| 975 * | |
| 976 * @param {util.FileOperationErrorType} code Error type. | |
| 977 * @param {string|Entry|FileError} data Additional data. | |
| 978 * @constructor | |
| 979 */ | |
| 980 FileOperationManager.Error = function(code, data) { | |
| 981 this.code = code; | |
| 982 this.data = data; | |
| 983 }; | |
| 984 | |
| 985 // FileOperationManager methods. | |
| 986 | |
| 987 /** | |
| 988 * Called before a new method is run in the manager. Prepares the manager's | |
| 989 * state for running a new method. | |
| 990 */ | |
| 991 FileOperationManager.prototype.willRunNewMethod = function() { | |
| 992 // Cancel any pending close actions so the file copy manager doesn't go away. | |
| 993 if (this.unloadTimeout_) | |
| 994 clearTimeout(this.unloadTimeout_); | |
| 995 this.unloadTimeout_ = null; | |
| 996 }; | |
| 997 | |
| 998 /** | |
| 999 * @return {Object} Status object. | |
| 1000 */ | |
| 1001 FileOperationManager.prototype.getStatus = function() { | |
| 1002 // TODO(hidehiko): Reorganize the structure when delete queue is merged | |
| 1003 // into copy task queue. | |
| 1004 var result = { | |
| 1005 // Set to util.FileOperationType if all the running/pending tasks is | |
| 1006 // the same kind of task. | |
| 1007 operationType: null, | |
| 1008 | |
| 1009 // The number of entries to be processed. | |
| 1010 numRemainingItems: 0, | |
| 1011 | |
| 1012 // The total number of bytes to be processed. | |
| 1013 totalBytes: 0, | |
| 1014 | |
| 1015 // The number of bytes. | |
| 1016 processedBytes: 0, | |
| 1017 | |
| 1018 // Available if numRemainingItems == 1. Pointing to an Entry which is | |
| 1019 // begin processed. | |
| 1020 processingEntry: task.getSingleEntry() | |
| 1021 }; | |
| 1022 | |
| 1023 var operationType = | |
| 1024 this.copyTasks_.length > 0 ? this.copyTasks_[0].operationType : null; | |
| 1025 var task = null; | |
| 1026 for (var i = 0; i < this.copyTasks_.length; i++) { | |
| 1027 task = this.copyTasks_[i]; | |
| 1028 if (task.operationType != operationType) | |
| 1029 operationType = null; | |
| 1030 | |
| 1031 // Assuming the number of entries is small enough, count every time. | |
| 1032 result.numRemainingItems += task.countRemainingItems(); | |
| 1033 result.totalBytes += task.totalBytes; | |
| 1034 result.processedBytes += task.processedBytes; | |
| 1035 } | |
| 1036 | |
| 1037 result.operationType = operationType; | |
| 1038 return result; | |
| 1039 }; | |
| 1040 | |
| 1041 /** | |
| 1042 * Adds an event listener for the tasks. | |
| 1043 * @param {string} type The name of the event. | |
| 1044 * @param {function(Event)} handler The handler for the event. | |
| 1045 * This is called when the event is dispatched. | |
| 1046 */ | |
| 1047 FileOperationManager.prototype.addEventListener = function(type, handler) { | |
| 1048 this.eventRouter_.addEventListener(type, handler); | |
| 1049 }; | |
| 1050 | |
| 1051 /** | |
| 1052 * Removes an event listener for the tasks. | |
| 1053 * @param {string} type The name of the event. | |
| 1054 * @param {function(Event)} handler The handler to be removed. | |
| 1055 */ | |
| 1056 FileOperationManager.prototype.removeEventListener = function(type, handler) { | |
| 1057 this.eventRouter_.removeEventListener(type, handler); | |
| 1058 }; | |
| 1059 | |
| 1060 /** | |
| 1061 * Says if there are any tasks in the queue. | |
| 1062 * @return {boolean} True, if there are any tasks. | |
| 1063 */ | |
| 1064 FileOperationManager.prototype.hasQueuedTasks = function() { | |
| 1065 return this.copyTasks_.length > 0 || this.deleteTasks_.length > 0; | |
| 1066 }; | |
| 1067 | |
| 1068 /** | |
| 1069 * Unloads the host page in 5 secs of idling. Need to be called | |
| 1070 * each time this.copyTasks_.length or this.deleteTasks_.length | |
| 1071 * changed. | |
| 1072 * | |
| 1073 * @private | |
| 1074 */ | |
| 1075 FileOperationManager.prototype.maybeScheduleCloseBackgroundPage_ = function() { | |
| 1076 if (!this.hasQueuedTasks()) { | |
| 1077 if (this.unloadTimeout_ === null) | |
| 1078 this.unloadTimeout_ = setTimeout(maybeCloseBackgroundPage, 5000); | |
| 1079 } else if (this.unloadTimeout_) { | |
| 1080 clearTimeout(this.unloadTimeout_); | |
| 1081 this.unloadTimeout_ = null; | |
| 1082 } | |
| 1083 }; | |
| 1084 | |
| 1085 /** | |
| 1086 * Completely clear out the copy queue, either because we encountered an error | |
| 1087 * or completed successfully. | |
| 1088 * | |
| 1089 * @private | |
| 1090 */ | |
| 1091 FileOperationManager.prototype.resetQueue_ = function() { | |
| 1092 for (var i = 0; i < this.cancelObservers_.length; i++) | |
| 1093 this.cancelObservers_[i](); | |
| 1094 | |
| 1095 this.copyTasks_ = []; | |
| 1096 this.cancelObservers_ = []; | |
| 1097 this.maybeScheduleCloseBackgroundPage_(); | |
| 1098 }; | |
| 1099 | |
| 1100 /** | |
| 1101 * Request that the current copy queue be abandoned. | |
| 1102 * | |
| 1103 * @param {function()=} opt_callback On cancel. | |
| 1104 */ | |
| 1105 FileOperationManager.prototype.requestCancel = function(opt_callback) { | |
| 1106 this.cancelRequested_ = true; | |
| 1107 if (this.cancelCallback_) { | |
| 1108 this.cancelCallback_(); | |
| 1109 this.cancelCallback_ = null; | |
| 1110 } | |
| 1111 if (opt_callback) | |
| 1112 this.cancelObservers_.push(opt_callback); | |
| 1113 | |
| 1114 // If there is any active task it will eventually call maybeCancel_. | |
| 1115 // Otherwise call it right now. | |
| 1116 if (this.copyTasks_.length == 0) | |
| 1117 this.doCancel_(); | |
| 1118 else | |
| 1119 this.copyTasks_[0].requestCancel(); | |
| 1120 }; | |
| 1121 | |
| 1122 /** | |
| 1123 * Requests the specified task to be canceled. | |
| 1124 * @param {string} taskId ID of task to be canceled. | |
| 1125 */ | |
| 1126 FileOperationManager.prototype.requestTaskCancel = function(taskId) { | |
| 1127 var task = null; | |
| 1128 for (var i = 0; i < this.copyTasks_.length; i++) { | |
| 1129 if (this.copyTasks_[i].taskId === taskId) { | |
| 1130 this.copyTasks_[i].requestCancel(); | |
| 1131 return; | |
| 1132 } | |
| 1133 } | |
| 1134 for (var i = 0; i < this.deleteTasks_.length; i++) { | |
| 1135 if (this.deleteTasks_[i].taskId === taskId) { | |
| 1136 this.deleteTasks_[i].requestCancel(); | |
| 1137 return; | |
| 1138 } | |
| 1139 } | |
| 1140 }; | |
| 1141 | |
| 1142 /** | |
| 1143 * Perform the bookkeeping required to cancel. | |
| 1144 * | |
| 1145 * @private | |
| 1146 */ | |
| 1147 FileOperationManager.prototype.doCancel_ = function() { | |
| 1148 var taskId = this.copyTasks_[0].taskId; | |
| 1149 this.resetQueue_(); | |
| 1150 this.cancelRequested_ = false; | |
| 1151 this.eventRouter_.sendProgressEvent('CANCELLED', this.getStatus(), taskId); | |
| 1152 }; | |
| 1153 | |
| 1154 /** | |
| 1155 * Used internally to check if a cancel has been requested, and handle | |
| 1156 * it if so. | |
| 1157 * | |
| 1158 * @return {boolean} If canceled. | |
| 1159 * @private | |
| 1160 */ | |
| 1161 FileOperationManager.prototype.maybeCancel_ = function() { | |
| 1162 if (!this.cancelRequested_) | |
| 1163 return false; | |
| 1164 | |
| 1165 this.doCancel_(); | |
| 1166 return true; | |
| 1167 }; | |
| 1168 | |
| 1169 /** | |
| 1170 * Kick off pasting. | |
| 1171 * | |
| 1172 * @param {Array.<string>} sourcePaths Path of the source files. | |
| 1173 * @param {string} targetPath The destination path of the target directory. | |
| 1174 * @param {boolean} isMove True if the operation is "move", otherwise (i.e. | |
| 1175 * if the operation is "copy") false. | |
| 1176 */ | |
| 1177 FileOperationManager.prototype.paste = function( | |
| 1178 sourcePaths, targetPath, isMove) { | |
| 1179 // Do nothing if sourcePaths is empty. | |
| 1180 if (sourcePaths.length == 0) | |
| 1181 return; | |
| 1182 | |
| 1183 var errorCallback = function(error) { | |
| 1184 this.eventRouter_.sendProgressEvent( | |
| 1185 'ERROR', | |
| 1186 this.getStatus(), | |
| 1187 this.generateTaskId_(null), | |
| 1188 new FileOperationManager.Error( | |
| 1189 util.FileOperationErrorType.FILESYSTEM_ERROR, error)); | |
| 1190 }.bind(this); | |
| 1191 | |
| 1192 var targetEntry = null; | |
| 1193 var entries = []; | |
| 1194 | |
| 1195 // Resolve paths to entries. | |
| 1196 var resolveGroup = new AsyncUtil.Group(); | |
| 1197 resolveGroup.add(function(callback) { | |
| 1198 webkitResolveLocalFileSystemURL( | |
| 1199 util.makeFilesystemUrl(targetPath), | |
| 1200 function(entry) { | |
| 1201 if (!entry.isDirectory) { | |
| 1202 // Found a non directory entry. | |
| 1203 errorCallback(util.createFileError(FileError.TYPE_MISMATCH_ERR)); | |
| 1204 return; | |
| 1205 } | |
| 1206 | |
| 1207 targetEntry = entry; | |
| 1208 callback(); | |
| 1209 }, | |
| 1210 errorCallback); | |
| 1211 }); | |
| 1212 | |
| 1213 for (var i = 0; i < sourcePaths.length; i++) { | |
| 1214 resolveGroup.add(function(sourcePath, callback) { | |
| 1215 webkitResolveLocalFileSystemURL( | |
| 1216 util.makeFilesystemUrl(sourcePath), | |
| 1217 function(entry) { | |
| 1218 entries.push(entry); | |
| 1219 callback(); | |
| 1220 }, | |
| 1221 errorCallback); | |
| 1222 }.bind(this, sourcePaths[i])); | |
| 1223 } | |
| 1224 | |
| 1225 resolveGroup.run(function() { | |
| 1226 if (isMove) { | |
| 1227 // Moving to the same directory is a redundant operation. | |
| 1228 entries = entries.filter(function(entry) { | |
| 1229 return targetEntry.fullPath + '/' + entry.name != entry.fullPath; | |
| 1230 }); | |
| 1231 | |
| 1232 // Do nothing, if we have no entries to be moved. | |
| 1233 if (entries.length == 0) | |
| 1234 return; | |
| 1235 } | |
| 1236 | |
| 1237 this.queueCopy_(targetEntry, entries, isMove); | |
| 1238 }.bind(this)); | |
| 1239 }; | |
| 1240 | |
| 1241 /** | |
| 1242 * Checks if the move operation is available between the given two locations. | |
| 1243 * | |
| 1244 * @param {DirectoryEntry} sourceEntry An entry from the source. | |
| 1245 * @param {DirectoryEntry} targetDirEntry Directory entry for the target. | |
| 1246 * @return {boolean} Whether we can move from the source to the target. | |
| 1247 */ | |
| 1248 FileOperationManager.prototype.isMovable = function(sourceEntry, | |
| 1249 targetDirEntry) { | |
| 1250 return (PathUtil.isDriveBasedPath(sourceEntry.fullPath) && | |
| 1251 PathUtil.isDriveBasedPath(targetDirEntry.fullPath)) || | |
| 1252 (PathUtil.getRootPath(sourceEntry.fullPath) == | |
| 1253 PathUtil.getRootPath(targetDirEntry.fullPath)); | |
| 1254 }; | |
| 1255 | |
| 1256 /** | |
| 1257 * Initiate a file copy. | |
| 1258 * | |
| 1259 * @param {DirectoryEntry} targetDirEntry Target directory. | |
| 1260 * @param {Array.<Entry>} entries Entries to copy. | |
| 1261 * @param {boolean} isMove In case of move. | |
| 1262 * @return {FileOperationManager.Task} Copy task. | |
| 1263 * @private | |
| 1264 */ | |
| 1265 FileOperationManager.prototype.queueCopy_ = function( | |
| 1266 targetDirEntry, entries, isMove) { | |
| 1267 // When copying files, null can be specified as source directory. | |
| 1268 var task; | |
| 1269 if (isMove) { | |
| 1270 if (this.isMovable(entries[0], targetDirEntry)) { | |
| 1271 task = new FileOperationManager.MoveTask(entries, targetDirEntry); | |
| 1272 } else { | |
| 1273 task = new FileOperationManager.CopyTask(entries, targetDirEntry); | |
| 1274 task.deleteAfterCopy = true; | |
| 1275 } | |
| 1276 } else { | |
| 1277 task = new FileOperationManager.CopyTask(entries, targetDirEntry); | |
| 1278 } | |
| 1279 | |
| 1280 task.taskId = this.generateTaskId_(); | |
| 1281 task.initialize(function() { | |
| 1282 this.copyTasks_.push(task); | |
| 1283 this.maybeScheduleCloseBackgroundPage_(); | |
| 1284 this.eventRouter_.sendProgressEvent('BEGIN', task.getStatus(), task.taskId); | |
| 1285 if (this.copyTasks_.length == 1) { | |
| 1286 // Assume this.cancelRequested_ == false. | |
| 1287 // This moved us from 0 to 1 active tasks, let the servicing begin! | |
| 1288 this.serviceAllTasks_(); | |
| 1289 } | |
| 1290 }.bind(this)); | |
| 1291 | |
| 1292 return task; | |
| 1293 }; | |
| 1294 | |
| 1295 /** | |
| 1296 * Service all pending tasks, as well as any that might appear during the | |
| 1297 * copy. | |
| 1298 * | |
| 1299 * @private | |
| 1300 */ | |
| 1301 FileOperationManager.prototype.serviceAllTasks_ = function() { | |
| 1302 if (!this.copyTasks_.length) { | |
| 1303 // All tasks have been serviced, clean up and exit. | |
| 1304 this.resetQueue_(); | |
| 1305 return; | |
| 1306 } | |
| 1307 | |
| 1308 var onTaskProgress = function() { | |
| 1309 this.eventRouter_.sendProgressEvent('PROGRESS', | |
| 1310 this.copyTasks_[0].getStatus(), | |
| 1311 this.copyTasks_[0].taskId); | |
| 1312 }.bind(this); | |
| 1313 | |
| 1314 var onEntryChanged = function(kind, entry) { | |
| 1315 this.eventRouter_.sendEntryChangedEvent(kind, entry); | |
| 1316 }.bind(this); | |
| 1317 | |
| 1318 var onTaskError = function(err) { | |
| 1319 var task = this.copyTasks_.shift(); | |
| 1320 if (this.maybeCancel_()) | |
| 1321 return; | |
| 1322 this.eventRouter_.sendProgressEvent('ERROR', | |
| 1323 task.getStatus(), | |
| 1324 task.taskId, | |
| 1325 err); | |
| 1326 this.serviceAllTasks_(); | |
| 1327 }.bind(this); | |
| 1328 | |
| 1329 var onTaskSuccess = function() { | |
| 1330 if (this.maybeCancel_()) | |
| 1331 return; | |
| 1332 | |
| 1333 // The task at the front of the queue is completed. Pop it from the queue. | |
| 1334 var task = this.copyTasks_.shift(); | |
| 1335 this.maybeScheduleCloseBackgroundPage_(); | |
| 1336 this.eventRouter_.sendProgressEvent('SUCCESS', | |
| 1337 task.getStatus(), | |
| 1338 task.taskId); | |
| 1339 this.serviceAllTasks_(); | |
| 1340 }.bind(this); | |
| 1341 | |
| 1342 var nextTask = this.copyTasks_[0]; | |
| 1343 this.eventRouter_.sendProgressEvent('PROGRESS', | |
| 1344 nextTask.getStatus(), | |
| 1345 nextTask.taskId); | |
| 1346 nextTask.run(onEntryChanged, onTaskProgress, onTaskSuccess, onTaskError); | |
| 1347 }; | |
| 1348 | |
| 1349 /** | |
| 1350 * Timeout before files are really deleted (to allow undo). | |
| 1351 */ | |
| 1352 FileOperationManager.DELETE_TIMEOUT = 30 * 1000; | |
| 1353 | |
| 1354 /** | |
| 1355 * Schedules the files deletion. | |
| 1356 * | |
| 1357 * @param {Array.<Entry>} entries The entries. | |
| 1358 */ | |
| 1359 FileOperationManager.prototype.deleteEntries = function(entries) { | |
| 1360 var task = { | |
| 1361 entries: entries, | |
| 1362 taskId: this.generateTaskId_() | |
| 1363 }; | |
| 1364 this.deleteTasks_.push(task); | |
| 1365 this.eventRouter_.sendDeleteEvent('BEGIN', entries.map(function(entry) { | |
| 1366 return util.makeFilesystemUrl(entry.fullPath); | |
| 1367 }), task.taskId); | |
| 1368 this.maybeScheduleCloseBackgroundPage_(); | |
| 1369 if (this.deleteTasks_.length == 1) | |
| 1370 this.serviceAllDeleteTasks_(); | |
| 1371 }; | |
| 1372 | |
| 1373 /** | |
| 1374 * Service all pending delete tasks, as well as any that might appear during the | |
| 1375 * deletion. | |
| 1376 * | |
| 1377 * Must not be called if there is an in-flight delete task. | |
| 1378 * | |
| 1379 * @private | |
| 1380 */ | |
| 1381 FileOperationManager.prototype.serviceAllDeleteTasks_ = function() { | |
| 1382 // Returns the urls of the given task's entries. | |
| 1383 var getTaskUrls = function(task) { | |
| 1384 return task.entries.map(function(entry) { | |
| 1385 return util.makeFilesystemUrl(entry.fullPath); | |
| 1386 }); | |
| 1387 }; | |
| 1388 | |
| 1389 var onTaskSuccess = function() { | |
| 1390 var urls = getTaskUrls(this.deleteTasks_[0]); | |
| 1391 var taskId = this.deleteTasks_[0].taskId; | |
| 1392 this.deleteTasks_.shift(); | |
| 1393 this.eventRouter_.sendDeleteEvent('SUCCESS', urls, taskId); | |
| 1394 | |
| 1395 if (!this.deleteTasks_.length) { | |
| 1396 // All tasks have been serviced, clean up and exit. | |
| 1397 this.maybeScheduleCloseBackgroundPage_(); | |
| 1398 return; | |
| 1399 } | |
| 1400 | |
| 1401 var nextTask = this.deleteTasks_[0]; | |
| 1402 this.eventRouter_.sendDeleteEvent('PROGRESS', | |
| 1403 urls, | |
| 1404 nextTask.taskId); | |
| 1405 this.serviceDeleteTask_(nextTask, onTaskSuccess, onTaskFailure); | |
| 1406 }.bind(this); | |
| 1407 | |
| 1408 var onTaskFailure = function(error) { | |
| 1409 var urls = getTaskUrls(this.deleteTasks_[0]); | |
| 1410 var taskId = this.deleteTasks_[0].taskId; | |
| 1411 this.deleteTasks_ = []; | |
| 1412 this.eventRouter_.sendDeleteEvent('ERROR', | |
| 1413 urls, | |
| 1414 taskId); | |
| 1415 this.maybeScheduleCloseBackgroundPage_(); | |
| 1416 }.bind(this); | |
| 1417 | |
| 1418 this.serviceDeleteTask_(this.deleteTasks_[0], onTaskSuccess, onTaskFailure); | |
| 1419 }; | |
| 1420 | |
| 1421 /** | |
| 1422 * Performs the deletion. | |
| 1423 * | |
| 1424 * @param {Object} task The delete task (see deleteEntries function). | |
| 1425 * @param {function()} successCallback Callback run on success. | |
| 1426 * @param {function(FileOperationManager.Error)} errorCallback Callback run on | |
| 1427 * error. | |
| 1428 * @private | |
| 1429 */ | |
| 1430 FileOperationManager.prototype.serviceDeleteTask_ = function( | |
| 1431 task, successCallback, errorCallback) { | |
| 1432 var downcount = task.entries.length; | |
| 1433 if (downcount == 0) { | |
| 1434 successCallback(); | |
| 1435 return; | |
| 1436 } | |
| 1437 | |
| 1438 var filesystemError = null; | |
| 1439 var onComplete = function() { | |
| 1440 if (--downcount > 0) | |
| 1441 return; | |
| 1442 | |
| 1443 // All remove operations are processed. Run callback. | |
| 1444 if (filesystemError) { | |
| 1445 errorCallback(new FileOperationManager.Error( | |
| 1446 util.FileOperationErrorType.FILESYSTEM_ERROR, filesystemError)); | |
| 1447 } else { | |
| 1448 successCallback(); | |
| 1449 } | |
| 1450 }; | |
| 1451 | |
| 1452 for (var i = 0; i < task.entries.length; i++) { | |
| 1453 var entry = task.entries[i]; | |
| 1454 util.removeFileOrDirectory( | |
| 1455 entry, | |
| 1456 function(currentEntry) { | |
| 1457 this.eventRouter_.sendEntryChangedEvent( | |
| 1458 util.EntryChangedKind.DELETED, currentEntry); | |
| 1459 onComplete(); | |
| 1460 }.bind(this, entry), | |
| 1461 function(error) { | |
| 1462 if (!filesystemError) | |
| 1463 filesystemError = error; | |
| 1464 onComplete(); | |
| 1465 }); | |
| 1466 } | |
| 1467 }; | |
| 1468 | |
| 1469 /** | |
| 1470 * Creates a zip file for the selection of files. | |
| 1471 * | |
| 1472 * @param {Entry} dirEntry The directory containing the selection. | |
| 1473 * @param {Array.<Entry>} selectionEntries The selected entries. | |
| 1474 */ | |
| 1475 FileOperationManager.prototype.zipSelection = function( | |
| 1476 dirEntry, selectionEntries) { | |
| 1477 var zipTask = new FileOperationManager.ZipTask( | |
| 1478 selectionEntries, dirEntry, dirEntry); | |
| 1479 zipTask.taskId = this.generateTaskId_(this.copyTasks_); | |
| 1480 zipTask.zip = true; | |
| 1481 zipTask.initialize(function() { | |
| 1482 this.copyTasks_.push(zipTask); | |
| 1483 this.eventRouter_.sendProgressEvent('BEGIN', | |
| 1484 zipTask.getStatus(), | |
| 1485 zipTask.taskId); | |
| 1486 if (this.copyTasks_.length == 1) { | |
| 1487 // Assume this.cancelRequested_ == false. | |
| 1488 // This moved us from 0 to 1 active tasks, let the servicing begin! | |
| 1489 this.serviceAllTasks_(); | |
| 1490 } | |
| 1491 }.bind(this)); | |
| 1492 }; | |
| 1493 | |
| 1494 /** | |
| 1495 * Generates new task ID. | |
| 1496 * | |
| 1497 * @return {string} New task ID. | |
| 1498 * @private | |
| 1499 */ | |
| 1500 FileOperationManager.prototype.generateTaskId_ = function() { | |
| 1501 return 'file-operation-' + this.taskIdCounter_++; | |
| 1502 }; | |
| OLD | NEW |