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 */ 10var 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 */ 23fileOperationUtil.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.name == util.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 */ 60fileOperationUtil.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(Entry, Entry)} entryChangedCallback 137 * Callback invoked when an entry is created with the source Entry and 138 * the destination Entry. 139 * @param {function(Entry, number)} progressCallback Callback invoked 140 * periodically during the copying. It takes the source Entry and the 141 * processed bytes of it. 142 * @param {function(Entry)} successCallback Callback invoked when the copy 143 * is successfully done with the Entry 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 */ 150fileOperationUtil.copyTo = function( 151 source, parent, newName, entryChangedCallback, progressCallback, 152 successCallback, errorCallback) { 153 var copyId = null; 154 var pendingCallbacks = []; 155 156 // Makes the callback called in order they were invoked. 157 var callbackQueue = new AsyncUtil.Queue(); 158 159 var onCopyProgress = function(progressCopyId, status) { 160 callbackQueue.run(function(callback) { 161 if (copyId === null) { 162 // If the copyId is not yet available, wait for it. 163 pendingCallbacks.push( 164 onCopyProgress.bind(null, progressCopyId, status)); 165 callback(); 166 return; 167 } 168 169 // This is not what we're interested in. 170 if (progressCopyId != copyId) { 171 callback(); 172 return; 173 } 174 175 switch (status.type) { 176 case 'begin_copy_entry': 177 callback(); 178 break; 179 180 case 'end_copy_entry': 181 // TODO(mtomasz): Convert URL to Entry in custom bindings. 182 util.URLsToEntries( 183 [status.destinationUrl], function(destinationEntries) { 184 entryChangedCallback(status.sourceUrl, 185 destinationEntries[0] || null); 186 callback(); 187 }); 188 break; 189 190 case 'progress': 191 progressCallback(status.sourceUrl, status.size); 192 callback(); 193 break; 194 195 case 'success': 196 chrome.fileBrowserPrivate.onCopyProgress.removeListener( 197 onCopyProgress); 198 // TODO(mtomasz): Convert URL to Entry in custom bindings. 199 util.URLsToEntries( 200 [status.destinationUrl], function(destinationEntries) { 201 successCallback(destinationEntries[0] || null); 202 callback(); 203 }); 204 break; 205 206 case 'error': 207 chrome.fileBrowserPrivate.onCopyProgress.removeListener( 208 onCopyProgress); 209 errorCallback(util.createDOMError(status.error)); 210 callback(); 211 break; 212 213 default: 214 // Found unknown state. Cancel the task, and return an error. 215 console.error('Unknown progress type: ' + status.type); 216 chrome.fileBrowserPrivate.onCopyProgress.removeListener( 217 onCopyProgress); 218 chrome.fileBrowserPrivate.cancelCopy(copyId); 219 errorCallback(util.createDOMError( 220 util.FileError.INVALID_STATE_ERR)); 221 callback(); 222 } 223 }); 224 }; 225 226 // Register the listener before calling startCopy. Otherwise some events 227 // would be lost. 228 chrome.fileBrowserPrivate.onCopyProgress.addListener(onCopyProgress); 229 230 // Then starts the copy. 231 // TODO(mtomasz): Convert URL to Entry in custom bindings. 232 chrome.fileBrowserPrivate.startCopy( 233 source.toURL(), parent.toURL(), newName, function(startCopyId) { 234 // last error contains the FileError code on error. 235 if (chrome.runtime.lastError) { 236 // Unsubscribe the progress listener. 237 chrome.fileBrowserPrivate.onCopyProgress.removeListener( 238 onCopyProgress); 239 errorCallback(util.createDOMError(chrome.runtime.lastError)); 240 return; 241 } 242 243 copyId = startCopyId; 244 for (var i = 0; i < pendingCallbacks.length; i++) { 245 pendingCallbacks[i](); 246 } 247 }); 248 249 return function() { 250 // If copyId is not yet available, wait for it. 251 if (copyId == null) { 252 pendingCallbacks.push(function() { 253 chrome.fileBrowserPrivate.cancelCopy(copyId); 254 }); 255 return; 256 } 257 258 chrome.fileBrowserPrivate.cancelCopy(copyId); 259 }; 260}; 261 262/** 263 * Thin wrapper of chrome.fileBrowserPrivate.zipSelection to adapt its 264 * interface similar to copyTo(). 265 * 266 * @param {Array.<Entry>} sources The array of entries to be archived. 267 * @param {DirectoryEntry} parent The entry of the destination directory. 268 * @param {string} newName The name of the archive to be created. 269 * @param {function(FileEntry)} successCallback Callback invoked when the 270 * operation is successfully done with the entry of the created archive. 271 * @param {function(FileError)} errorCallback Callback invoked when an error 272 * is found. 273 */ 274fileOperationUtil.zipSelection = function( 275 sources, parent, newName, successCallback, errorCallback) { 276 // TODO(mtomasz): Pass Entries instead of URLs. Entries can be converted to 277 // URLs in custom bindings. 278 chrome.fileBrowserPrivate.zipSelection( 279 parent.toURL(), 280 util.entriesToURLs(sources), 281 newName, function(success) { 282 if (!success) { 283 // Failed to create a zip archive. 284 errorCallback( 285 util.createDOMError(util.FileError.INVALID_MODIFICATION_ERR)); 286 return; 287 } 288 289 // Returns the created entry via callback. 290 parent.getFile( 291 newName, {create: false}, successCallback, errorCallback); 292 }); 293}; 294 295/** 296 * @constructor 297 */ 298function FileOperationManager() { 299 this.copyTasks_ = []; 300 this.deleteTasks_ = []; 301 this.taskIdCounter_ = 0; 302 this.eventRouter_ = new FileOperationManager.EventRouter(); 303 304 Object.seal(this); 305} 306 307/** 308 * Manages Event dispatching. 309 * Currently this can send three types of events: "copy-progress", 310 * "copy-operation-completed" and "delete". 311 * 312 * TODO(hidehiko): Reorganize the event dispatching mechanism. 313 * @constructor 314 * @extends {cr.EventTarget} 315 */ 316FileOperationManager.EventRouter = function() { 317}; 318 319/** 320 * Extends cr.EventTarget. 321 */ 322FileOperationManager.EventRouter.prototype.__proto__ = cr.EventTarget.prototype; 323 324/** 325 * Dispatches a simple "copy-progress" event with reason and current 326 * FileOperationManager status. If it is an ERROR event, error should be set. 327 * 328 * @param {string} reason Event type. One of "BEGIN", "PROGRESS", "SUCCESS", 329 * "ERROR" or "CANCELLED". TODO(hidehiko): Use enum. 330 * @param {Object} status Current FileOperationManager's status. See also 331 * FileOperationManager.Task.getStatus(). 332 * @param {string} taskId ID of task related with the event. 333 * @param {FileOperationManager.Error=} opt_error The info for the error. This 334 * should be set iff the reason is "ERROR". 335 */ 336FileOperationManager.EventRouter.prototype.sendProgressEvent = function( 337 reason, status, taskId, opt_error) { 338 var event = new Event('copy-progress'); 339 event.reason = reason; 340 event.status = status; 341 event.taskId = taskId; 342 if (opt_error) 343 event.error = opt_error; 344 this.dispatchEvent(event); 345}; 346 347/** 348 * Dispatches an event to notify that an entry is changed (created or deleted). 349 * @param {util.EntryChangedKind} kind The enum to represent if the entry is 350 * created or deleted. 351 * @param {Entry} entry The changed entry. 352 */ 353FileOperationManager.EventRouter.prototype.sendEntryChangedEvent = function( 354 kind, entry) { 355 var event = new Event('entry-changed'); 356 event.kind = kind; 357 event.entry = entry; 358 this.dispatchEvent(event); 359}; 360 361/** 362 * Dispatches an event to notify entries are changed for delete task. 363 * 364 * @param {string} reason Event type. One of "BEGIN", "PROGRESS", "SUCCESS", 365 * or "ERROR". TODO(hidehiko): Use enum. 366 * @param {DeleteTask} task Delete task related with the event. 367 */ 368FileOperationManager.EventRouter.prototype.sendDeleteEvent = function( 369 reason, task) { 370 var event = new Event('delete'); 371 event.reason = reason; 372 event.taskId = task.taskId; 373 event.entries = task.entries; 374 event.totalBytes = task.totalBytes; 375 event.processedBytes = task.processedBytes; 376 this.dispatchEvent(event); 377}; 378 379/** 380 * A record of a queued copy operation. 381 * 382 * Multiple copy operations may be queued at any given time. Additional 383 * Tasks may be added while the queue is being serviced. Though a 384 * cancel operation cancels everything in the queue. 385 * 386 * @param {util.FileOperationType} operationType The type of this operation. 387 * @param {Array.<Entry>} sourceEntries Array of source entries. 388 * @param {DirectoryEntry} targetDirEntry Target directory. 389 * @constructor 390 */ 391FileOperationManager.Task = function( 392 operationType, sourceEntries, targetDirEntry) { 393 this.operationType = operationType; 394 this.sourceEntries = sourceEntries; 395 this.targetDirEntry = targetDirEntry; 396 397 /** 398 * An array of map from url to Entry being processed. 399 * @type {Array.<Object<string, Entry>>} 400 */ 401 this.processingEntries = null; 402 403 /** 404 * Total number of bytes to be processed. Filled in initialize(). 405 * Use 1 as an initial value to indicate that the task is not completed. 406 * @type {number} 407 */ 408 this.totalBytes = 1; 409 410 /** 411 * Total number of already processed bytes. Updated periodically. 412 * @type {number} 413 */ 414 this.processedBytes = 0; 415 416 /** 417 * Index of the progressing entry in sourceEntries. 418 * @type {number} 419 * @private 420 */ 421 this.processingSourceIndex_ = 0; 422 423 /** 424 * Set to true when cancel is requested. 425 * @private {boolean} 426 */ 427 this.cancelRequested_ = false; 428 429 /** 430 * Callback to cancel the running process. 431 * @private {function()} 432 */ 433 this.cancelCallback_ = null; 434 435 // TODO(hidehiko): After we support recursive copy, we don't need this. 436 // If directory already exists, we try to make a copy named 'dir (X)', 437 // where X is a number. When we do this, all subsequent copies from 438 // inside the subtree should be mapped to the new directory name. 439 // For example, if 'dir' was copied as 'dir (1)', then 'dir/file.txt' should 440 // become 'dir (1)/file.txt'. 441 this.renamedDirectories_ = []; 442}; 443 444/** 445 * @param {function()} callback When entries resolved. 446 */ 447FileOperationManager.Task.prototype.initialize = function(callback) { 448}; 449 450/** 451 * Requests cancellation of this task. 452 * When the cancellation is done, it is notified via callbacks of run(). 453 */ 454FileOperationManager.Task.prototype.requestCancel = function() { 455 this.cancelRequested_ = true; 456 if (this.cancelCallback_) { 457 this.cancelCallback_(); 458 this.cancelCallback_ = null; 459 } 460}; 461 462/** 463 * Runs the task. Sub classes must implement this method. 464 * 465 * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback 466 * Callback invoked when an entry is changed. 467 * @param {function()} progressCallback Callback invoked periodically during 468 * the operation. 469 * @param {function()} successCallback Callback run on success. 470 * @param {function(FileOperationManager.Error)} errorCallback Callback run on 471 * error. 472 */ 473FileOperationManager.Task.prototype.run = function( 474 entryChangedCallback, progressCallback, successCallback, errorCallback) { 475}; 476 477/** 478 * Get states of the task. 479 * TOOD(hirono): Removes this method and sets a task to progress events. 480 * @return {object} Status object. 481 */ 482FileOperationManager.Task.prototype.getStatus = function() { 483 var processingEntry = this.sourceEntries[this.processingSourceIndex_]; 484 return { 485 operationType: this.operationType, 486 numRemainingItems: this.sourceEntries.length - this.processingSourceIndex_, 487 totalBytes: this.totalBytes, 488 processedBytes: this.processedBytes, 489 processingEntryName: processingEntry ? processingEntry.name : '' 490 }; 491}; 492 493/** 494 * Obtains the number of total processed bytes. 495 * @return {number} Number of total processed bytes. 496 * @private 497 */ 498FileOperationManager.Task.prototype.calcProcessedBytes_ = function() { 499 var bytes = 0; 500 for (var i = 0; i < this.processingSourceIndex_ + 1; i++) { 501 var entryMap = this.processingEntries[i]; 502 if (!entryMap) 503 break; 504 for (var name in entryMap) { 505 bytes += i < this.processingSourceIndex_ ? 506 entryMap[name].size : entryMap[name].processedBytes; 507 } 508 } 509 return bytes; 510}; 511 512/** 513 * Task to copy entries. 514 * 515 * @param {Array.<Entry>} sourceEntries Array of source entries. 516 * @param {DirectoryEntry} targetDirEntry Target directory. 517 * @param {boolean} deleteAfterCopy Whether the delete original files after 518 * copy. 519 * @constructor 520 * @extends {FileOperationManager.Task} 521 */ 522FileOperationManager.CopyTask = function(sourceEntries, 523 targetDirEntry, 524 deleteAfterCopy) { 525 FileOperationManager.Task.call( 526 this, 527 deleteAfterCopy ? 528 util.FileOperationType.MOVE : util.FileOperationType.COPY, 529 sourceEntries, 530 targetDirEntry); 531 this.deleteAfterCopy = deleteAfterCopy; 532 533 /* 534 * Rate limiter which is used to avoid sending update request for progress bar 535 * too frequently. 536 * @type {AsyncUtil.RateLimiter} 537 * @private 538 */ 539 this.updateProgressRateLimiter_ = null 540}; 541 542/** 543 * Extends FileOperationManager.Task. 544 */ 545FileOperationManager.CopyTask.prototype.__proto__ = 546 FileOperationManager.Task.prototype; 547 548/** 549 * Initializes the CopyTask. 550 * @param {function()} callback Called when the initialize is completed. 551 */ 552FileOperationManager.CopyTask.prototype.initialize = function(callback) { 553 var group = new AsyncUtil.Group(); 554 // Correct all entries to be copied for status update. 555 this.processingEntries = []; 556 for (var i = 0; i < this.sourceEntries.length; i++) { 557 group.add(function(index, callback) { 558 fileOperationUtil.resolveRecursively( 559 this.sourceEntries[index], 560 function(resolvedEntries) { 561 var resolvedEntryMap = {}; 562 for (var j = 0; j < resolvedEntries.length; ++j) { 563 var entry = resolvedEntries[j]; 564 entry.processedBytes = 0; 565 resolvedEntryMap[entry.toURL()] = entry; 566 } 567 this.processingEntries[index] = resolvedEntryMap; 568 callback(); 569 }.bind(this), 570 function(error) { 571 console.error( 572 'Failed to resolve for copy: %s', error.name); 573 callback(); 574 }); 575 }.bind(this, i)); 576 } 577 578 group.run(function() { 579 // Fill totalBytes. 580 this.totalBytes = 0; 581 for (var i = 0; i < this.processingEntries.length; i++) { 582 for (var entryURL in this.processingEntries[i]) 583 this.totalBytes += this.processingEntries[i][entryURL].size; 584 } 585 586 callback(); 587 }.bind(this)); 588}; 589 590/** 591 * Copies all entries to the target directory. 592 * Note: this method contains also the operation of "Move" due to historical 593 * reason. 594 * 595 * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback 596 * Callback invoked when an entry is changed. 597 * @param {function()} progressCallback Callback invoked periodically during 598 * the copying. 599 * @param {function()} successCallback On success. 600 * @param {function(FileOperationManager.Error)} errorCallback On error. 601 * @override 602 */ 603FileOperationManager.CopyTask.prototype.run = function( 604 entryChangedCallback, progressCallback, successCallback, errorCallback) { 605 // TODO(hidehiko): We should be able to share the code to iterate on entries 606 // with serviceMoveTask_(). 607 if (this.sourceEntries.length == 0) { 608 successCallback(); 609 return; 610 } 611 612 // TODO(hidehiko): Delete after copy is the implementation of Move. 613 // Migrate the part into MoveTask.run(). 614 var deleteOriginals = function() { 615 var count = this.sourceEntries.length; 616 617 var onEntryDeleted = function(entry) { 618 entryChangedCallback(util.EntryChangedKind.DELETED, entry); 619 count--; 620 if (!count) 621 successCallback(); 622 }; 623 624 var onFilesystemError = function(err) { 625 errorCallback(new FileOperationManager.Error( 626 util.FileOperationErrorType.FILESYSTEM_ERROR, err)); 627 }; 628 629 for (var i = 0; i < this.sourceEntries.length; i++) { 630 var entry = this.sourceEntries[i]; 631 util.removeFileOrDirectory( 632 entry, onEntryDeleted.bind(null, entry), onFilesystemError); 633 } 634 }.bind(this); 635 636 /** 637 * Accumulates processed bytes and call |progressCallback| if needed. 638 * 639 * @param {number} index The index of processing source. 640 * @param {string} sourceEntryUrl URL of the entry which has been processed. 641 * @param {number=} opt_size Processed bytes of the |sourceEntry|. If it is 642 * dropped, all bytes of the entry are considered to be processed. 643 */ 644 var updateProgress = function(index, sourceEntryUrl, opt_size) { 645 if (!sourceEntryUrl) 646 return; 647 648 var processedEntry = this.processingEntries[index][sourceEntryUrl]; 649 if (!processedEntry) 650 return; 651 652 // Accumulates newly processed bytes. 653 var size = opt_size || processedEntry.size; 654 this.processedBytes += size - processedEntry.processedBytes; 655 processedEntry.processedBytes = size; 656 657 // Updates progress bar in limited frequency so that intervals between 658 // updates have at least 200ms. 659 this.updateProgressRateLimiter_.run(); 660 }.bind(this); 661 662 this.updateProgressRateLimiter_ = new AsyncUtil.RateLimiter(progressCallback); 663 664 AsyncUtil.forEach( 665 this.sourceEntries, 666 function(callback, entry, index) { 667 if (this.cancelRequested_) { 668 errorCallback(new FileOperationManager.Error( 669 util.FileOperationErrorType.FILESYSTEM_ERROR, 670 util.createDOMError(util.FileError.ABORT_ERR))); 671 return; 672 } 673 progressCallback(); 674 this.processEntry_( 675 entry, this.targetDirEntry, 676 function(sourceEntryUrl, destinationEntry) { 677 updateProgress(index, sourceEntryUrl); 678 // The destination entry may be null, if the copied file got 679 // deleted just after copying. 680 if (destinationEntry) { 681 entryChangedCallback( 682 util.EntryChangedKind.CREATED, destinationEntry); 683 } 684 }, 685 function(sourceEntryUrl, size) { 686 updateProgress(index, sourceEntryUrl, size); 687 }, 688 function() { 689 // Finishes off delayed updates if necessary. 690 this.updateProgressRateLimiter_.runImmediately(); 691 // Update current source index and processing bytes. 692 this.processingSourceIndex_ = index + 1; 693 this.processedBytes = this.calcProcessedBytes_(); 694 callback(); 695 }.bind(this), 696 function(error) { 697 // Finishes off delayed updates if necessary. 698 this.updateProgressRateLimiter_.runImmediately(); 699 errorCallback(error); 700 }.bind(this)); 701 }, 702 function() { 703 if (this.deleteAfterCopy) { 704 deleteOriginals(); 705 } else { 706 successCallback(); 707 } 708 }.bind(this), 709 this); 710}; 711 712/** 713 * Copies the source entry to the target directory. 714 * 715 * @param {Entry} sourceEntry An entry to be copied. 716 * @param {DirectoryEntry} destinationEntry The entry which will contain the 717 * copied entry. 718 * @param {function(Entry, Entry} entryChangedCallback 719 * Callback invoked when an entry is created with the source Entry and 720 * the destination Entry. 721 * @param {function(Entry, number)} progressCallback Callback invoked 722 * periodically during the copying. 723 * @param {function()} successCallback On success. 724 * @param {function(FileOperationManager.Error)} errorCallback On error. 725 * @private 726 */ 727FileOperationManager.CopyTask.prototype.processEntry_ = function( 728 sourceEntry, destinationEntry, entryChangedCallback, progressCallback, 729 successCallback, errorCallback) { 730 fileOperationUtil.deduplicatePath( 731 destinationEntry, sourceEntry.name, 732 function(destinationName) { 733 if (this.cancelRequested_) { 734 errorCallback(new FileOperationManager.Error( 735 util.FileOperationErrorType.FILESYSTEM_ERROR, 736 util.createDOMError(util.FileError.ABORT_ERR))); 737 return; 738 } 739 this.cancelCallback_ = fileOperationUtil.copyTo( 740 sourceEntry, destinationEntry, destinationName, 741 entryChangedCallback, progressCallback, 742 function(entry) { 743 this.cancelCallback_ = null; 744 successCallback(); 745 }.bind(this), 746 function(error) { 747 this.cancelCallback_ = null; 748 errorCallback(new FileOperationManager.Error( 749 util.FileOperationErrorType.FILESYSTEM_ERROR, error)); 750 }.bind(this)); 751 }.bind(this), 752 errorCallback); 753}; 754 755/** 756 * Task to move entries. 757 * 758 * @param {Array.<Entry>} sourceEntries Array of source entries. 759 * @param {DirectoryEntry} targetDirEntry Target directory. 760 * @constructor 761 * @extends {FileOperationManager.Task} 762 */ 763FileOperationManager.MoveTask = function(sourceEntries, targetDirEntry) { 764 FileOperationManager.Task.call( 765 this, util.FileOperationType.MOVE, sourceEntries, targetDirEntry); 766}; 767 768/** 769 * Extends FileOperationManager.Task. 770 */ 771FileOperationManager.MoveTask.prototype.__proto__ = 772 FileOperationManager.Task.prototype; 773 774/** 775 * Initializes the MoveTask. 776 * @param {function()} callback Called when the initialize is completed. 777 */ 778FileOperationManager.MoveTask.prototype.initialize = function(callback) { 779 // This may be moving from search results, where it fails if we 780 // move parent entries earlier than child entries. We should 781 // process the deepest entry first. Since move of each entry is 782 // done by a single moveTo() call, we don't need to care about the 783 // recursive traversal order. 784 this.sourceEntries.sort(function(entry1, entry2) { 785 return entry2.toURL().length - entry1.toURL().length; 786 }); 787 788 this.processingEntries = []; 789 for (var i = 0; i < this.sourceEntries.length; i++) { 790 var processingEntryMap = {}; 791 var entry = this.sourceEntries[i]; 792 793 // The move should be done with updating the metadata. So here we assume 794 // all the file size is 1 byte. (Avoiding 0, so that progress bar can 795 // move smoothly). 796 // TODO(hidehiko): Remove this hack. 797 entry.size = 1; 798 processingEntryMap[entry.toURL()] = entry; 799 this.processingEntries[i] = processingEntryMap; 800 } 801 802 callback(); 803}; 804 805/** 806 * Moves all entries in the task. 807 * 808 * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback 809 * Callback invoked when an entry is changed. 810 * @param {function()} progressCallback Callback invoked periodically during 811 * the moving. 812 * @param {function()} successCallback On success. 813 * @param {function(FileOperationManager.Error)} errorCallback On error. 814 * @override 815 */ 816FileOperationManager.MoveTask.prototype.run = function( 817 entryChangedCallback, progressCallback, successCallback, errorCallback) { 818 if (this.sourceEntries.length == 0) { 819 successCallback(); 820 return; 821 } 822 823 AsyncUtil.forEach( 824 this.sourceEntries, 825 function(callback, entry, index) { 826 if (this.cancelRequested_) { 827 errorCallback(new FileOperationManager.Error( 828 util.FileOperationErrorType.FILESYSTEM_ERROR, 829 util.createDOMError(util.FileError.ABORT_ERR))); 830 return; 831 } 832 progressCallback(); 833 FileOperationManager.MoveTask.processEntry_( 834 entry, this.targetDirEntry, entryChangedCallback, 835 function() { 836 // Update current source index. 837 this.processingSourceIndex_ = index + 1; 838 this.processedBytes = this.calcProcessedBytes_(); 839 callback(); 840 }.bind(this), 841 errorCallback); 842 }, 843 function() { 844 successCallback(); 845 }.bind(this), 846 this); 847}; 848 849/** 850 * Moves the sourceEntry to the targetDirEntry in this task. 851 * 852 * @param {Entry} sourceEntry An entry to be moved. 853 * @param {DirectoryEntry} destinationEntry The entry of the destination 854 * directory. 855 * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback 856 * Callback invoked when an entry is changed. 857 * @param {function()} successCallback On success. 858 * @param {function(FileOperationManager.Error)} errorCallback On error. 859 * @private 860 */ 861FileOperationManager.MoveTask.processEntry_ = function( 862 sourceEntry, destinationEntry, entryChangedCallback, successCallback, 863 errorCallback) { 864 fileOperationUtil.deduplicatePath( 865 destinationEntry, 866 sourceEntry.name, 867 function(destinationName) { 868 sourceEntry.moveTo( 869 destinationEntry, destinationName, 870 function(movedEntry) { 871 entryChangedCallback(util.EntryChangedKind.CREATED, movedEntry); 872 entryChangedCallback(util.EntryChangedKind.DELETED, sourceEntry); 873 successCallback(); 874 }, 875 function(error) { 876 errorCallback(new FileOperationManager.Error( 877 util.FileOperationErrorType.FILESYSTEM_ERROR, error)); 878 }); 879 }, 880 errorCallback); 881}; 882 883/** 884 * Task to create a zip archive. 885 * 886 * @param {Array.<Entry>} sourceEntries Array of source entries. 887 * @param {DirectoryEntry} targetDirEntry Target directory. 888 * @param {DirectoryEntry} zipBaseDirEntry Base directory dealt as a root 889 * in ZIP archive. 890 * @constructor 891 * @extends {FileOperationManager.Task} 892 */ 893FileOperationManager.ZipTask = function( 894 sourceEntries, targetDirEntry, zipBaseDirEntry) { 895 FileOperationManager.Task.call( 896 this, util.FileOperationType.ZIP, sourceEntries, targetDirEntry); 897 this.zipBaseDirEntry = zipBaseDirEntry; 898}; 899 900/** 901 * Extends FileOperationManager.Task. 902 */ 903FileOperationManager.ZipTask.prototype.__proto__ = 904 FileOperationManager.Task.prototype; 905 906 907/** 908 * Initializes the ZipTask. 909 * @param {function()} callback Called when the initialize is completed. 910 */ 911FileOperationManager.ZipTask.prototype.initialize = function(callback) { 912 var resolvedEntryMap = {}; 913 var group = new AsyncUtil.Group(); 914 for (var i = 0; i < this.sourceEntries.length; i++) { 915 group.add(function(index, callback) { 916 fileOperationUtil.resolveRecursively( 917 this.sourceEntries[index], 918 function(entries) { 919 for (var j = 0; j < entries.length; j++) 920 resolvedEntryMap[entries[j].toURL()] = entries[j]; 921 callback(); 922 }, 923 callback); 924 }.bind(this, i)); 925 } 926 927 group.run(function() { 928 // For zip archiving, all the entries are processed at once. 929 this.processingEntries = [resolvedEntryMap]; 930 931 this.totalBytes = 0; 932 for (var url in resolvedEntryMap) 933 this.totalBytes += resolvedEntryMap[url].size; 934 935 callback(); 936 }.bind(this)); 937}; 938 939/** 940 * Runs a zip file creation task. 941 * 942 * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback 943 * Callback invoked when an entry is changed. 944 * @param {function()} progressCallback Callback invoked periodically during 945 * the moving. 946 * @param {function()} successCallback On complete. 947 * @param {function(FileOperationManager.Error)} errorCallback On error. 948 * @override 949 */ 950FileOperationManager.ZipTask.prototype.run = function( 951 entryChangedCallback, progressCallback, successCallback, errorCallback) { 952 // TODO(hidehiko): we should localize the name. 953 var destName = 'Archive'; 954 if (this.sourceEntries.length == 1) { 955 var entryName = this.sourceEntries[0].name; 956 var i = entryName.lastIndexOf('.'); 957 destName = ((i < 0) ? entryName : entryName.substr(0, i)); 958 } 959 960 fileOperationUtil.deduplicatePath( 961 this.targetDirEntry, destName + '.zip', 962 function(destPath) { 963 // TODO: per-entry zip progress update with accurate byte count. 964 // For now just set completedBytes to same value as totalBytes so 965 // that the progress bar is full. 966 this.processedBytes = this.totalBytes; 967 progressCallback(); 968 969 // The number of elements in processingEntries is 1. See also 970 // initialize(). 971 var entries = []; 972 for (var url in this.processingEntries[0]) 973 entries.push(this.processingEntries[0][url]); 974 975 fileOperationUtil.zipSelection( 976 entries, 977 this.zipBaseDirEntry, 978 destPath, 979 function(entry) { 980 entryChangedCallback(util.EntryChangedKind.CREATED, entry); 981 successCallback(); 982 }, 983 function(error) { 984 errorCallback(new FileOperationManager.Error( 985 util.FileOperationErrorType.FILESYSTEM_ERROR, error)); 986 }); 987 }.bind(this), 988 errorCallback); 989}; 990 991/** 992 * Error class used to report problems with a copy operation. 993 * If the code is UNEXPECTED_SOURCE_FILE, data should be a path of the file. 994 * If the code is TARGET_EXISTS, data should be the existing Entry. 995 * If the code is FILESYSTEM_ERROR, data should be the FileError. 996 * 997 * @param {util.FileOperationErrorType} code Error type. 998 * @param {string|Entry|FileError} data Additional data. 999 * @constructor 1000 */ 1001FileOperationManager.Error = function(code, data) { 1002 this.code = code; 1003 this.data = data; 1004}; 1005 1006// FileOperationManager methods. 1007 1008/** 1009 * Adds an event listener for the tasks. 1010 * @param {string} type The name of the event. 1011 * @param {function(Event)} handler The handler for the event. 1012 * This is called when the event is dispatched. 1013 */ 1014FileOperationManager.prototype.addEventListener = function(type, handler) { 1015 this.eventRouter_.addEventListener(type, handler); 1016}; 1017 1018/** 1019 * Removes an event listener for the tasks. 1020 * @param {string} type The name of the event. 1021 * @param {function(Event)} handler The handler to be removed. 1022 */ 1023FileOperationManager.prototype.removeEventListener = function(type, handler) { 1024 this.eventRouter_.removeEventListener(type, handler); 1025}; 1026 1027/** 1028 * Says if there are any tasks in the queue. 1029 * @return {boolean} True, if there are any tasks. 1030 */ 1031FileOperationManager.prototype.hasQueuedTasks = function() { 1032 return this.copyTasks_.length > 0 || this.deleteTasks_.length > 0; 1033}; 1034 1035/** 1036 * Completely clear out the copy queue, either because we encountered an error 1037 * or completed successfully. 1038 * 1039 * @private 1040 */ 1041FileOperationManager.prototype.resetQueue_ = function() { 1042 this.copyTasks_ = []; 1043}; 1044 1045/** 1046 * Requests the specified task to be canceled. 1047 * @param {string} taskId ID of task to be canceled. 1048 */ 1049FileOperationManager.prototype.requestTaskCancel = function(taskId) { 1050 var task = null; 1051 for (var i = 0; i < this.copyTasks_.length; i++) { 1052 task = this.copyTasks_[i]; 1053 if (task.taskId !== taskId) 1054 continue; 1055 task.requestCancel(); 1056 // If the task is not on progress, remove it immediately. 1057 if (i !== 0) { 1058 this.eventRouter_.sendProgressEvent('CANCELED', 1059 task.getStatus(), 1060 task.taskId); 1061 this.copyTasks_.splice(i, 1); 1062 } 1063 } 1064 for (var i = 0; i < this.deleteTasks_.length; i++) { 1065 task = this.deleteTasks_[i]; 1066 if (task.taskId !== taskId) 1067 continue; 1068 task.cancelRequested = true; 1069 // If the task is not on progress, remove it immediately. 1070 if (i !== 0) { 1071 this.eventRouter_.sendDeleteEvent('CANCELED', task); 1072 this.deleteTasks_.splice(i, 1); 1073 } 1074 } 1075}; 1076 1077/** 1078 * Kick off pasting. 1079 * 1080 * @param {Array.<Entry>} sourceEntries Entries of the source files. 1081 * @param {DirectoryEntry} targetEntry The destination entry of the target 1082 * directory. 1083 * @param {boolean} isMove True if the operation is "move", otherwise (i.e. 1084 * if the operation is "copy") false. 1085 */ 1086FileOperationManager.prototype.paste = function( 1087 sourceEntries, targetEntry, isMove) { 1088 // Do nothing if sourceEntries is empty. 1089 if (sourceEntries.length === 0) 1090 return; 1091 1092 var filteredEntries = []; 1093 var resolveGroup = new AsyncUtil.Queue(); 1094 1095 if (isMove) { 1096 for (var index = 0; index < sourceEntries.length; index++) { 1097 var sourceEntry = sourceEntries[index]; 1098 resolveGroup.run(function(sourceEntry, callback) { 1099 sourceEntry.getParent(function(inParentEntry) { 1100 if (!util.isSameEntry(inParentEntry, targetEntry)) 1101 filteredEntries.push(sourceEntry); 1102 callback(); 1103 }, function() { 1104 console.warn( 1105 'Failed to resolve the parent for: ' + sourceEntry.toURL()); 1106 // Even if the parent is not available, try to move it. 1107 filteredEntries.push(sourceEntry); 1108 callback(); 1109 }); 1110 }.bind(this, sourceEntry)); 1111 } 1112 } else { 1113 // Always copy all of the files. 1114 filteredEntries = sourceEntries; 1115 } 1116 1117 resolveGroup.run(function(callback) { 1118 // Do nothing, if we have no entries to be pasted. 1119 if (filteredEntries.length === 0) 1120 return; 1121 1122 this.queueCopy_(targetEntry, filteredEntries, isMove); 1123 }.bind(this)); 1124}; 1125 1126/** 1127 * Checks if the move operation is available between the given two locations. 1128 * This method uses the volume manager, which is lazily created, therefore the 1129 * result is returned asynchronously. 1130 * 1131 * @param {DirectoryEntry} sourceEntry An entry from the source. 1132 * @param {DirectoryEntry} targetDirEntry Directory entry for the target. 1133 * @param {function(boolean)} callback Callback with result whether the entries 1134 * can be directly moved. 1135 * @private 1136 */ 1137FileOperationManager.prototype.isMovable_ = function( 1138 sourceEntry, targetDirEntry, callback) { 1139 VolumeManager.getInstance(function(volumeManager) { 1140 var sourceLocationInfo = volumeManager.getLocationInfo(sourceEntry); 1141 var targetDirLocationInfo = volumeManager.getLocationInfo(targetDirEntry); 1142 callback( 1143 sourceLocationInfo && targetDirLocationInfo && 1144 sourceLocationInfo.volumeInfo === targetDirLocationInfo.volumeInfo); 1145 }); 1146}; 1147 1148/** 1149 * Initiate a file copy. When copying files, null can be specified as source 1150 * directory. 1151 * 1152 * @param {DirectoryEntry} targetDirEntry Target directory. 1153 * @param {Array.<Entry>} entries Entries to copy. 1154 * @param {boolean} isMove In case of move. 1155 * @private 1156 */ 1157FileOperationManager.prototype.queueCopy_ = function( 1158 targetDirEntry, entries, isMove) { 1159 var createTask = function(task) { 1160 task.taskId = this.generateTaskId_(); 1161 this.eventRouter_.sendProgressEvent( 1162 'BEGIN', task.getStatus(), task.taskId); 1163 task.initialize(function() { 1164 this.copyTasks_.push(task); 1165 if (this.copyTasks_.length === 1) 1166 this.serviceAllTasks_(); 1167 }.bind(this)); 1168 }.bind(this); 1169 1170 var task; 1171 if (isMove) { 1172 // When moving between different volumes, moving is implemented as a copy 1173 // and delete. This is because moving between volumes is slow, and moveTo() 1174 // is not cancellable nor provides progress feedback. 1175 this.isMovable_(entries[0], targetDirEntry, function(isMovable) { 1176 if (isMovable) { 1177 createTask(new FileOperationManager.MoveTask(entries, targetDirEntry)); 1178 } else { 1179 createTask( 1180 new FileOperationManager.CopyTask(entries, targetDirEntry, true)); 1181 } 1182 }); 1183 } else { 1184 createTask( 1185 new FileOperationManager.CopyTask(entries, targetDirEntry, false)); 1186 } 1187}; 1188 1189/** 1190 * Service all pending tasks, as well as any that might appear during the 1191 * copy. 1192 * 1193 * @private 1194 */ 1195FileOperationManager.prototype.serviceAllTasks_ = function() { 1196 if (!this.copyTasks_.length) { 1197 // All tasks have been serviced, clean up and exit. 1198 chrome.power.releaseKeepAwake(); 1199 this.resetQueue_(); 1200 return; 1201 } 1202 1203 // Prevent the system from sleeping while copy is in progress. 1204 chrome.power.requestKeepAwake('system'); 1205 1206 var onTaskProgress = function() { 1207 this.eventRouter_.sendProgressEvent('PROGRESS', 1208 this.copyTasks_[0].getStatus(), 1209 this.copyTasks_[0].taskId); 1210 }.bind(this); 1211 1212 var onEntryChanged = function(kind, entry) { 1213 this.eventRouter_.sendEntryChangedEvent(kind, entry); 1214 }.bind(this); 1215 1216 var onTaskError = function(err) { 1217 var task = this.copyTasks_.shift(); 1218 var reason = err.data.name === util.FileError.ABORT_ERR ? 1219 'CANCELED' : 'ERROR'; 1220 this.eventRouter_.sendProgressEvent(reason, 1221 task.getStatus(), 1222 task.taskId, 1223 err); 1224 this.serviceAllTasks_(); 1225 }.bind(this); 1226 1227 var onTaskSuccess = function() { 1228 // The task at the front of the queue is completed. Pop it from the queue. 1229 var task = this.copyTasks_.shift(); 1230 this.eventRouter_.sendProgressEvent('SUCCESS', 1231 task.getStatus(), 1232 task.taskId); 1233 this.serviceAllTasks_(); 1234 }.bind(this); 1235 1236 var nextTask = this.copyTasks_[0]; 1237 this.eventRouter_.sendProgressEvent('PROGRESS', 1238 nextTask.getStatus(), 1239 nextTask.taskId); 1240 nextTask.run(onEntryChanged, onTaskProgress, onTaskSuccess, onTaskError); 1241}; 1242 1243/** 1244 * Timeout before files are really deleted (to allow undo). 1245 */ 1246FileOperationManager.DELETE_TIMEOUT = 30 * 1000; 1247 1248/** 1249 * Schedules the files deletion. 1250 * 1251 * @param {Array.<Entry>} entries The entries. 1252 */ 1253FileOperationManager.prototype.deleteEntries = function(entries) { 1254 // TODO(hirono): Make FileOperationManager.DeleteTask. 1255 var task = Object.seal({ 1256 entries: entries, 1257 taskId: this.generateTaskId_(), 1258 entrySize: {}, 1259 totalBytes: 0, 1260 processedBytes: 0, 1261 cancelRequested: false 1262 }); 1263 1264 // Obtains entry size and sum them up. 1265 var group = new AsyncUtil.Group(); 1266 for (var i = 0; i < task.entries.length; i++) { 1267 group.add(function(entry, callback) { 1268 entry.getMetadata(function(metadata) { 1269 var index = task.entries.indexOf(entries); 1270 task.entrySize[entry.toURL()] = metadata.size; 1271 task.totalBytes += metadata.size; 1272 callback(); 1273 }, function() { 1274 // Fail to obtain the metadata. Use fake value 1. 1275 task.entrySize[entry.toURL()] = 1; 1276 task.totalBytes += 1; 1277 callback(); 1278 }); 1279 }.bind(this, task.entries[i])); 1280 } 1281 1282 // Add a delete task. 1283 group.run(function() { 1284 this.deleteTasks_.push(task); 1285 this.eventRouter_.sendDeleteEvent('BEGIN', task); 1286 if (this.deleteTasks_.length === 1) 1287 this.serviceAllDeleteTasks_(); 1288 }.bind(this)); 1289}; 1290 1291/** 1292 * Service all pending delete tasks, as well as any that might appear during the 1293 * deletion. 1294 * 1295 * Must not be called if there is an in-flight delete task. 1296 * 1297 * @private 1298 */ 1299FileOperationManager.prototype.serviceAllDeleteTasks_ = function() { 1300 this.serviceDeleteTask_( 1301 this.deleteTasks_[0], 1302 function() { 1303 this.deleteTasks_.shift(); 1304 if (this.deleteTasks_.length) 1305 this.serviceAllDeleteTasks_(); 1306 }.bind(this)); 1307}; 1308 1309/** 1310 * Performs the deletion. 1311 * 1312 * @param {Object} task The delete task (see deleteEntries function). 1313 * @param {function()} callback Callback run on task end. 1314 * @private 1315 */ 1316FileOperationManager.prototype.serviceDeleteTask_ = function(task, callback) { 1317 var queue = new AsyncUtil.Queue(); 1318 1319 // Delete each entry. 1320 var error = null; 1321 var deleteOneEntry = function(inCallback) { 1322 if (!task.entries.length || task.cancelRequested || error) { 1323 inCallback(); 1324 return; 1325 } 1326 this.eventRouter_.sendDeleteEvent('PROGRESS', task); 1327 util.removeFileOrDirectory( 1328 task.entries[0], 1329 function() { 1330 this.eventRouter_.sendEntryChangedEvent( 1331 util.EntryChangedKind.DELETED, task.entries[0]); 1332 task.processedBytes += task.entrySize[task.entries[0].toURL()]; 1333 task.entries.shift(); 1334 deleteOneEntry(inCallback); 1335 }.bind(this), 1336 function(inError) { 1337 error = inError; 1338 inCallback(); 1339 }.bind(this)); 1340 }.bind(this); 1341 queue.run(deleteOneEntry); 1342 1343 // Send an event and finish the async steps. 1344 queue.run(function(inCallback) { 1345 var reason; 1346 if (error) 1347 reason = 'ERROR'; 1348 else if (task.cancelRequested) 1349 reason = 'CANCELED'; 1350 else 1351 reason = 'SUCCESS'; 1352 this.eventRouter_.sendDeleteEvent(reason, task); 1353 inCallback(); 1354 callback(); 1355 }.bind(this)); 1356}; 1357 1358/** 1359 * Creates a zip file for the selection of files. 1360 * 1361 * @param {Entry} dirEntry The directory containing the selection. 1362 * @param {Array.<Entry>} selectionEntries The selected entries. 1363 */ 1364FileOperationManager.prototype.zipSelection = function( 1365 dirEntry, selectionEntries) { 1366 var zipTask = new FileOperationManager.ZipTask( 1367 selectionEntries, dirEntry, dirEntry); 1368 zipTask.taskId = this.generateTaskId_(this.copyTasks_); 1369 zipTask.zip = true; 1370 this.eventRouter_.sendProgressEvent('BEGIN', 1371 zipTask.getStatus(), 1372 zipTask.taskId); 1373 zipTask.initialize(function() { 1374 this.copyTasks_.push(zipTask); 1375 if (this.copyTasks_.length == 1) 1376 this.serviceAllTasks_(); 1377 }.bind(this)); 1378}; 1379 1380/** 1381 * Generates new task ID. 1382 * 1383 * @return {string} New task ID. 1384 * @private 1385 */ 1386FileOperationManager.prototype.generateTaskId_ = function() { 1387 return 'file-operation-' + this.taskIdCounter_++; 1388}; 1389