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