1 // Copyright (c) 2012 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 FileCopyManager. 9 */ 10 var fileOperationUtil = {}; 11 12 /** 13 * Simple wrapper for util.deduplicatePath. On error, this method translates 14 * the FileError to FileCopyManager.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(FileCopyManager.Error)} errorCallback Callback run on error. 21 */ 22 fileOperationUtil.deduplicatePath = function( 23 dirEntry, relativePath, successCallback, errorCallback) { 24 util.deduplicatePath( 25 dirEntry, relativePath, successCallback, 26 function(err) { 27 var onFileSystemError = function(error) { 28 errorCallback(new FileCopyManager.Error( 29 util.FileOperationErrorType.FILESYSTEM_ERROR, error)); 30 }; 31 32 if (err.code == FileError.PATH_EXISTS_ERR) { 33 // Failed to uniquify the file path. There should be an existing 34 // entry, so return the error with it. 35 util.resolvePath( 36 dirEntry, relativePath, 37 function(entry) { 38 errorCallback(new FileCopyManager.Error( 39 util.FileOperationErrorType.TARGET_EXISTS, entry)); 40 }, 41 onFileSystemError); 42 return; 43 } 44 onFileSystemError(err); 45 }); 46 }; 47 48 /** 49 * Sets last modified date to the entry. 50 * @param {Entry} entry The entry to which the last modified is set. 51 * @param {Date} modificationTime The last modified time. 52 */ 53 fileOperationUtil.setLastModified = function(entry, modificationTime) { 54 chrome.fileBrowserPrivate.setLastModified( 55 entry.toURL(), '' + Math.round(modificationTime.getTime() / 1000)); 56 }; 57 58 /** 59 * Copies a file from source to the parent directory with newName. 60 * See also copyFileByStream_ and copyFileOnDrive_ for the implementation 61 * details. 62 * 63 * @param {FileEntry} source The file entry to be copied. 64 * @param {DirectoryEntry} parent The entry of the destination directory. 65 * @param {string} newName The name of copied file. 66 * @param {function(FileEntry, number)} progressCallback Callback invoked 67 * periodically during the file writing. It takes source and the number of 68 * copied bytes since the last invocation. This is also called just before 69 * starting the operation (with '0' bytes) and just after the finishing the 70 * operation (with the total copied size). 71 * @param {function(FileEntry)} successCallback Callback invoked when the copy 72 * is successfully done with the entry of the created file. 73 * @param {function(FileError)} errorCallback Callback invoked when an error 74 * is found. 75 * @return {function()} Callback to cancle the current file copy operation. 76 * When the cancel is done, errorCallback will be called. The returned 77 * callback must not be called more than once. 78 */ 79 fileOperationUtil.copyFile = function( 80 source, parent, newName, progressCallback, successCallback, errorCallback) { 81 if (!PathUtil.isDriveBasedPath(source.fullPath) && 82 !PathUtil.isDriveBasedPath(parent.fullPath)) { 83 // Copying a file between non-Drive file systems. 84 return fileOperationUtil.copyFileByStream_( 85 source, parent, newName, progressCallback, successCallback, 86 errorCallback); 87 } else { 88 // Copying related to the Drive file system. 89 return fileOperationUtil.copyFileOnDrive_( 90 source, parent, newName, progressCallback, successCallback, 91 errorCallback); 92 } 93 }; 94 95 /** 96 * Copies a file by using File and FileWriter objects. 97 * 98 * This is a js-implementation of FileEntry.copyTo(). Unfortunately, copyTo 99 * doesn't support periodical progress updating nor cancelling. To support 100 * these operations, this method implements copyTo by streaming way in 101 * JavaScript. 102 * 103 * Note that this is designed for file copying on local file system. We have 104 * some special cases about copying on Drive file system. See also 105 * copyFileOnDrive_() for more details. 106 * 107 * @param {FileEntry} source The file entry to be copied. 108 * @param {DirectoryEntry} parent The entry of the destination directory. 109 * @param {string} newName The name of copied file. 110 * @param {function(FileEntry, number)} progressCallback Callback invoked 111 * periodically during the file writing. It takes source and the number of 112 * copied bytes since the last invocation. This is also called just before 113 * starting the operation (with '0' bytes) and just after the finishing the 114 * operation (with the total copied size). 115 * @param {function(FileEntry)} successCallback Callback invoked when the copy 116 * is successfully done with the entry of the created file. 117 * @param {function(FileError)} errorCallback Callback invoked when an error 118 * is found. 119 * @return {function()} Callback to cancel the current file copy operation. 120 * When the cancel is done, errorCallback will be called. The returned 121 * callback must not be called more than once. 122 * @private 123 */ 124 fileOperationUtil.copyFileByStream_ = function( 125 source, parent, newName, progressCallback, successCallback, errorCallback) { 126 // Set to true when cancel is requested. 127 var cancelRequested = false; 128 129 source.file(function(file) { 130 if (cancelRequested) { 131 errorCallback(util.createFileError(FileError.ABORT_ERR)); 132 return; 133 } 134 135 parent.getFile(newName, {create: true, exclusive: true}, function(target) { 136 if (cancelRequested) { 137 errorCallback(util.createFileError(FileError.ABORT_ERR)); 138 return; 139 } 140 141 target.createWriter(function(writer) { 142 if (cancelRequested) { 143 errorCallback(util.createFileError(FileError.ABORT_ERR)); 144 return; 145 } 146 147 writer.onerror = writer.onabort = function(progress) { 148 errorCallback(cancelRequested ? 149 util.createFileError(FileError.ABORT_ERR) : 150 writer.error); 151 }; 152 153 var reportedProgress = 0; 154 writer.onprogress = function(progress) { 155 if (cancelRequested) { 156 // If the copy was cancelled, we should abort the operation. 157 // The errorCallback will be called by writer.onabort after the 158 // termination. 159 writer.abort(); 160 return; 161 } 162 163 // |progress.loaded| will contain total amount of data copied by now. 164 // |progressCallback| expects data amount delta from the last progress 165 // update. 166 progressCallback(target, progress.loaded - reportedProgress); 167 reportedProgress = progress.loaded; 168 }; 169 170 writer.onwrite = function() { 171 if (cancelRequested) { 172 errorCallback(util.createFileError(FileError.ABORT_ERR)); 173 return; 174 } 175 176 source.getMetadata(function(metadata) { 177 if (cancelRequested) { 178 errorCallback(util.createFileError(FileError.ABORT_ERR)); 179 return; 180 } 181 182 fileOperationUtil.setLastModified( 183 target, metadata.modificationTime); 184 successCallback(target); 185 }, errorCallback); 186 }; 187 188 writer.write(file); 189 }, errorCallback); 190 }, errorCallback); 191 }, errorCallback); 192 193 return function() { cancelRequested = true; }; 194 }; 195 196 /** 197 * Copies a file a) from Drive to local, b) from local to Drive, or c) from 198 * Drive to Drive. 199 * Currently, we need to take care about following two things for Drive: 200 * 201 * 1) Copying hosted document. 202 * In theory, it is impossible to actual copy a hosted document to other 203 * file system. Thus, instead, Drive file system backend creates a JSON file 204 * referring to the hosted document. Also, when it is uploaded by copyTo, 205 * the hosted document is copied on the server. Note that, this doesn't work 206 * when a user creates a file by FileWriter (as copyFileEntry_ does). 207 * 208 * 2) File transfer between local and Drive server. 209 * There are two directions of file transfer; from local to Drive and from 210 * Drive to local. 211 * The file transfer from local to Drive is done as a part of file system 212 * background sync (kicked after the copy operation is done). So we don't need 213 * to take care about it here. To copy the file from Drive to local (or Drive 214 * to Drive with GData WAPI), we need to download the file content (if it is 215 * not locally cached). During the downloading, we can listen the periodical 216 * updating and cancel the downloding via private API. 217 * 218 * This function supports progress updating and cancelling partially. 219 * Unfortunately, FileEntry.copyTo doesn't support progress updating nor 220 * cancelling, so we support them only during file downloading. 221 * 222 * Note: we're planning to move copyTo logic into c++ side. crbug.com/261492 223 * 224 * @param {FileEntry} source The entry of the file to be copied. 225 * @param {DirectoryEntry} parent The entry of the destination directory. 226 * @param {string} newName The name of the copied file. 227 * @param {function(FileEntry, number)} progressCallback Callback periodically 228 * invoked during file transfer with the source and the number of 229 * transferred bytes from the last call. 230 * @param {function(FileEntry)} successCallback Callback invoked when the 231 * file copy is successfully done with the entry of the copied file. 232 * @param {function(FileError)} errorCallback Callback invoked when an error 233 * is found. 234 * @return {function()} Callback to cancel the current file copy operation. 235 * When the cancel is done, errorCallback will be called. The returned 236 * callback must not be called more than once. 237 * @private 238 */ 239 fileOperationUtil.copyFileOnDrive_ = function( 240 source, parent, newName, progressCallback, successCallback, errorCallback) { 241 // Set to true when cancel is requested. 242 var cancelRequested = false; 243 var cancelCallback = null; 244 245 var onCopyToCompleted = null; 246 247 // Progress callback. 248 // Because the uploading the file from local cache to Drive server will be 249 // done as a part of background Drive file system sync, so for this copy 250 // operation, what we need to take care about is only file downloading. 251 var numTransferredBytes = 0; 252 if (PathUtil.isDriveBasedPath(source.fullPath)) { 253 var sourceUrl = source.toURL(); 254 var sourcePath = util.extractFilePath(sourceUrl); 255 var onFileTransfersUpdated = function(statusList) { 256 for (var i = 0; i < statusList.length; i++) { 257 var status = statusList[i]; 258 259 // Comparing urls is unreliable, since they may use different 260 // url encoding schemes (eg. rfc2396 vs. rfc3986). 261 var filePath = util.extractFilePath(status.fileUrl); 262 if (filePath == sourcePath) { 263 var processed = status.processed; 264 if (processed > numTransferredBytes) { 265 progressCallback(source, processed - numTransferredBytes); 266 numTransferredBytes = processed; 267 } 268 return; 269 } 270 } 271 }; 272 273 // Subscribe to listen file transfer updating notifications. 274 chrome.fileBrowserPrivate.onFileTransfersUpdated.addListener( 275 onFileTransfersUpdated); 276 277 // Currently, we do NOT upload the file during the copy operation. 278 // It will be done as a part of file system sync after copy operation. 279 // So, we can cancel only file downloading. 280 cancelCallback = function() { 281 chrome.fileBrowserPrivate.cancelFileTransfers( 282 [sourceUrl], function() {}); 283 }; 284 285 // We need to clean up on copyTo completion regardless if it is 286 // successfully done or not. 287 onCopyToCompleted = function() { 288 cancelCallback = null; 289 chrome.fileBrowserPrivate.onFileTransfersUpdated.removeListener( 290 onFileTransfersUpdated); 291 }; 292 } 293 294 source.copyTo( 295 parent, newName, 296 function(entry) { 297 if (onCopyToCompleted) 298 onCopyToCompleted(); 299 300 if (cancelRequested) { 301 errorCallback(util.createFileError(FileError.ABORT_ERR)); 302 return; 303 } 304 305 entry.getMetadata(function(metadata) { 306 if (metadata.size > numTransferredBytes) 307 progressCallback(source, metadata.size - numTransferredBytes); 308 successCallback(entry); 309 }, errorCallback); 310 }, 311 function(error) { 312 if (onCopyToCompleted) 313 onCopyToCompleted(); 314 315 errorCallback(error); 316 }); 317 318 return function() { 319 cancelRequested = true; 320 if (cancelCallback) { 321 cancelCallback(); 322 cancelCallback = null; 323 } 324 }; 325 }; 326 327 /** 328 * Thin wrapper of chrome.fileBrowserPrivate.zipSelection to adapt its 329 * interface similar to copyTo(). 330 * 331 * @param {Array.<Entry>} sources The array of entries to be archived. 332 * @param {DirectoryEntry} parent The entry of the destination directory. 333 * @param {string} newName The name of the archive to be created. 334 * @param {function(FileEntry)} successCallback Callback invoked when the 335 * operation is successfully done with the entry of the created archive. 336 * @param {function(FileError)} errorCallback Callback invoked when an error 337 * is found. 338 */ 339 fileOperationUtil.zipSelection = function( 340 sources, parent, newName, successCallback, errorCallback) { 341 chrome.fileBrowserPrivate.zipSelection( 342 parent.toURL(), 343 sources.map(function(e) { return e.toURL(); }), 344 newName, function(success) { 345 if (!success) { 346 // Failed to create a zip archive. 347 errorCallback( 348 util.createFileError(FileError.INVALID_MODIFICATION_ERR)); 349 return; 350 } 351 352 // Returns the created entry via callback. 353 parent.getFile( 354 newName, {create: false}, successCallback, errorCallback); 355 }); 356 }; 357 358 /** 359 * @constructor 360 */ 361 function FileCopyManager() { 362 this.copyTasks_ = []; 363 this.deleteTasks_ = []; 364 this.cancelObservers_ = []; 365 this.cancelRequested_ = false; 366 this.cancelCallback_ = null; 367 this.unloadTimeout_ = null; 368 369 this.eventRouter_ = new FileCopyManager.EventRouter(); 370 } 371 372 /** 373 * Get FileCopyManager instance. In case is hasn't been initialized, a new 374 * instance is created. 375 * 376 * @return {FileCopyManager} A FileCopyManager instance. 377 */ 378 FileCopyManager.getInstance = function() { 379 if (!FileCopyManager.instance_) 380 FileCopyManager.instance_ = new FileCopyManager(); 381 382 return FileCopyManager.instance_; 383 }; 384 385 /** 386 * Manages cr.Event dispatching. 387 * Currently this can send three types of events: "copy-progress", 388 * "copy-operation-completed" and "delete". 389 * 390 * TODO(hidehiko): Reorganize the event dispatching mechanism. 391 * @constructor 392 * @extends {cr.EventTarget} 393 */ 394 FileCopyManager.EventRouter = function() { 395 }; 396 397 /** 398 * Extends cr.EventTarget. 399 */ 400 FileCopyManager.EventRouter.prototype.__proto__ = cr.EventTarget.prototype; 401 402 /** 403 * Dispatches a simple "copy-progress" event with reason and current 404 * FileCopyManager status. If it is an ERROR event, error should be set. 405 * 406 * @param {string} reason Event type. One of "BEGIN", "PROGRESS", "SUCCESS", 407 * "ERROR" or "CANCELLED". TODO(hidehiko): Use enum. 408 * @param {Object} status Current FileCopyManager's status. See also 409 * FileCopyManager.getStatus(). 410 * @param {FileCopyManager.Error=} opt_error The info for the error. This 411 * should be set iff the reason is "ERROR". 412 */ 413 FileCopyManager.EventRouter.prototype.sendProgressEvent = function( 414 reason, status, opt_error) { 415 var event = new cr.Event('copy-progress'); 416 event.reason = reason; 417 event.status = status; 418 if (opt_error) 419 event.error = opt_error; 420 this.dispatchEvent(event); 421 }; 422 423 /** 424 * Dispatches an event to notify that an entry is changed (created or deleted). 425 * @param {util.EntryChangedType} type The enum to represent if the entry 426 * is created or deleted. 427 * @param {Entry} entry The changed entry. 428 */ 429 FileCopyManager.EventRouter.prototype.sendEntryChangedEvent = function( 430 type, entry) { 431 var event = new cr.Event('entry-changed'); 432 event.type = type; 433 event.entry = entry; 434 this.dispatchEvent(event); 435 }; 436 437 /** 438 * Dispatches an event to notify entries are changed for delete task. 439 * 440 * @param {string} reason Event type. One of "BEGIN", "PROGRESS", "SUCCESS", 441 * or "ERROR". TODO(hidehiko): Use enum. 442 * @param {Array.<string>} urls An array of URLs which are affected by delete 443 * operation. 444 */ 445 FileCopyManager.EventRouter.prototype.sendDeleteEvent = function( 446 reason, urls) { 447 var event = new cr.Event('delete'); 448 event.reason = reason; 449 event.urls = urls; 450 this.dispatchEvent(event); 451 }; 452 453 /** 454 * A record of a queued copy operation. 455 * 456 * Multiple copy operations may be queued at any given time. Additional 457 * Tasks may be added while the queue is being serviced. Though a 458 * cancel operation cancels everything in the queue. 459 * 460 * @param {DirectoryEntry} targetDirEntry Target directory. 461 * @param {DirectoryEntry=} opt_zipBaseDirEntry Base directory dealt as a root 462 * in ZIP archive. 463 * @constructor 464 */ 465 FileCopyManager.Task = function(targetDirEntry, opt_zipBaseDirEntry) { 466 this.targetDirEntry = targetDirEntry; 467 this.zipBaseDirEntry = opt_zipBaseDirEntry; 468 this.originalEntries = null; 469 470 // TODO(hidehiko): When we support recursive copy, we should be able to 471 // rely on originalEntries. Then remove this. 472 this.entries = []; 473 474 /** 475 * The index of entries being processed. The entries should be processed 476 * from 0, so this is also the number of completed entries. 477 * @type {number} 478 */ 479 this.entryIndex = 0; 480 this.totalBytes = 0; 481 this.completedBytes = 0; 482 483 this.deleteAfterCopy = false; 484 this.move = false; 485 this.zip = false; 486 487 // TODO(hidehiko): After we support recursive copy, we don't need this. 488 // If directory already exists, we try to make a copy named 'dir (X)', 489 // where X is a number. When we do this, all subsequent copies from 490 // inside the subtree should be mapped to the new directory name. 491 // For example, if 'dir' was copied as 'dir (1)', then 'dir\file.txt' should 492 // become 'dir (1)\file.txt'. 493 this.renamedDirectories_ = []; 494 }; 495 496 /** 497 * @param {Array.<Entry>} entries Entries. 498 * @param {function()} callback When entries resolved. 499 */ 500 FileCopyManager.Task.prototype.setEntries = function(entries, callback) { 501 this.originalEntries = entries; 502 503 // When moving directories, FileEntry.moveTo() is used if both source 504 // and target are on Drive. There is no need to recurse into directories. 505 util.recurseAndResolveEntries(entries, !this.move, function(result) { 506 if (this.move) { 507 // This may be moving from search results, where it fails if we move 508 // parent entries earlier than child entries. We should process the 509 // deepest entry first. Since move of each entry is done by a single 510 // moveTo() call, we don't need to care about the recursive traversal 511 // order. 512 this.entries = result.dirEntries.concat(result.fileEntries).sort( 513 function(entry1, entry2) { 514 return entry2.fullPath.length - entry1.fullPath.length; 515 }); 516 } else { 517 // Copying tasks are recursively processed. So, directories must be 518 // processed earlier than their child files. Since 519 // util.recurseAndResolveEntries is already listing entries in the 520 // recursive traversal order, we just keep the ordering. 521 this.entries = result.dirEntries.concat(result.fileEntries); 522 } 523 524 this.totalBytes = result.fileBytes; 525 callback(); 526 }.bind(this)); 527 }; 528 529 /** 530 * Updates copy progress status for the entry. 531 * 532 * @param {number} size Number of bytes that has been copied since last update. 533 */ 534 FileCopyManager.Task.prototype.updateFileCopyProgress = function(size) { 535 this.completedBytes += size; 536 }; 537 538 /** 539 * @param {string} fromName Old name. 540 * @param {string} toName New name. 541 */ 542 FileCopyManager.Task.prototype.registerRename = function(fromName, toName) { 543 this.renamedDirectories_.push({from: fromName + '/', to: toName + '/'}); 544 }; 545 546 /** 547 * @param {string} path A path. 548 * @return {string} Path after renames. 549 */ 550 FileCopyManager.Task.prototype.applyRenames = function(path) { 551 // Directories are processed in pre-order, so we will store only the first 552 // renaming point: 553 // x -> x (1) -- new directory created. 554 // x\y -> x (1)\y -- no more renames inside the new directory, so 555 // this one will not be stored. 556 // x\y\a.txt -- only one rename will be applied. 557 for (var index = 0; index < this.renamedDirectories_.length; ++index) { 558 var rename = this.renamedDirectories_[index]; 559 if (path.indexOf(rename.from) == 0) { 560 path = rename.to + path.substr(rename.from.length); 561 } 562 } 563 return path; 564 }; 565 566 /** 567 * Error class used to report problems with a copy operation. 568 * If the code is UNEXPECTED_SOURCE_FILE, data should be a path of the file. 569 * If the code is TARGET_EXISTS, data should be the existing Entry. 570 * If the code is FILESYSTEM_ERROR, data should be the FileError. 571 * 572 * @param {util.FileOperationErrorType} code Error type. 573 * @param {string|Entry|FileError} data Additional data. 574 * @constructor 575 */ 576 FileCopyManager.Error = function(code, data) { 577 this.code = code; 578 this.data = data; 579 }; 580 581 // FileCopyManager methods. 582 583 /** 584 * Initializes the filesystem if it is not done yet. 585 * @param {function()} callback Completion callback. 586 */ 587 FileCopyManager.prototype.initialize = function(callback) { 588 // Already initialized. 589 if (this.root_) { 590 callback(); 591 return; 592 } 593 chrome.fileBrowserPrivate.requestFileSystem(function(filesystem) { 594 this.root_ = filesystem.root; 595 callback(); 596 }.bind(this)); 597 }; 598 599 /** 600 * Called before a new method is run in the manager. Prepares the manager's 601 * state for running a new method. 602 */ 603 FileCopyManager.prototype.willRunNewMethod = function() { 604 // Cancel any pending close actions so the file copy manager doesn't go away. 605 if (this.unloadTimeout_) 606 clearTimeout(this.unloadTimeout_); 607 this.unloadTimeout_ = null; 608 }; 609 610 /** 611 * @return {Object} Status object. 612 */ 613 FileCopyManager.prototype.getStatus = function() { 614 // TODO(hidehiko): Reorganize the structure when delete queue is merged 615 // into copy task queue. 616 var rv = { 617 totalItems: 0, 618 completedItems: 0, 619 620 totalBytes: 0, 621 completedBytes: 0, 622 623 pendingCopies: 0, 624 pendingMoves: 0, 625 pendingZips: 0, 626 627 // In case the number of the incompleted entry is exactly one. 628 filename: '', 629 }; 630 631 var pendingEntry = null; 632 for (var i = 0; i < this.copyTasks_.length; i++) { 633 var task = this.copyTasks_[i]; 634 rv.totalItems += task.entries.length; 635 rv.completedItems += task.entryIndex; 636 637 rv.totalBytes += task.totalBytes; 638 rv.completedBytes += task.completedBytes; 639 640 var numPendingEntries = task.entries.length - task.entryIndex; 641 if (task.zip) { 642 rv.pendingZips += numPendingEntries; 643 } else if (task.move || task.deleteAfterCopy) { 644 rv.pendingMoves += numPendingEntries; 645 } else { 646 rv.pendingCopies += numPendingEntries; 647 } 648 649 if (numPendingEntries == 1) 650 pendingEntry = task.entries[task.entries.length - 1]; 651 } 652 653 if (rv.totalItems - rv.completedItems == 1 && pendingEntry) 654 rv.filename = pendingEntry.name; 655 656 return rv; 657 }; 658 659 /** 660 * Adds an event listener for the tasks. 661 * @param {string} type The name of the event. 662 * @param {function(cr.Event)} handler The handler for the event. 663 * This is called when the event is dispatched. 664 */ 665 FileCopyManager.prototype.addEventListener = function(type, handler) { 666 this.eventRouter_.addEventListener(type, handler); 667 }; 668 669 /** 670 * Removes an event listener for the tasks. 671 * @param {string} type The name of the event. 672 * @param {function(cr.Event)} handler The handler to be removed. 673 */ 674 FileCopyManager.prototype.removeEventListener = function(type, handler) { 675 this.eventRouter_.removeEventListener(type, handler); 676 }; 677 678 /** 679 * Says if there are any tasks in the queue. 680 * @return {boolean} True, if there are any tasks. 681 */ 682 FileCopyManager.prototype.hasQueuedTasks = function() { 683 return this.copyTasks_.length > 0 || this.deleteTasks_.length > 0; 684 }; 685 686 /** 687 * Unloads the host page in 5 secs of idleing. Need to be called 688 * each time this.copyTasks_.length or this.deleteTasks_.length 689 * changed. 690 * 691 * @private 692 */ 693 FileCopyManager.prototype.maybeScheduleCloseBackgroundPage_ = function() { 694 if (!this.hasQueuedTasks()) { 695 if (this.unloadTimeout_ === null) 696 this.unloadTimeout_ = setTimeout(maybeCloseBackgroundPage, 5000); 697 } else if (this.unloadTimeout_) { 698 clearTimeout(this.unloadTimeout_); 699 this.unloadTimeout_ = null; 700 } 701 }; 702 703 /** 704 * Completely clear out the copy queue, either because we encountered an error 705 * or completed successfully. 706 * 707 * @private 708 */ 709 FileCopyManager.prototype.resetQueue_ = function() { 710 for (var i = 0; i < this.cancelObservers_.length; i++) 711 this.cancelObservers_[i](); 712 713 this.copyTasks_ = []; 714 this.cancelObservers_ = []; 715 this.maybeScheduleCloseBackgroundPage_(); 716 }; 717 718 /** 719 * Request that the current copy queue be abandoned. 720 * 721 * @param {function()=} opt_callback On cancel. 722 */ 723 FileCopyManager.prototype.requestCancel = function(opt_callback) { 724 this.cancelRequested_ = true; 725 if (this.cancelCallback_) { 726 this.cancelCallback_(); 727 this.cancelCallback_ = null; 728 } 729 if (opt_callback) 730 this.cancelObservers_.push(opt_callback); 731 732 // If there is any active task it will eventually call maybeCancel_. 733 // Otherwise call it right now. 734 if (this.copyTasks_.length == 0) 735 this.doCancel_(); 736 }; 737 738 /** 739 * Perform the bookkeeping required to cancel. 740 * 741 * @private 742 */ 743 FileCopyManager.prototype.doCancel_ = function() { 744 this.resetQueue_(); 745 this.cancelRequested_ = false; 746 this.eventRouter_.sendProgressEvent('CANCELLED', this.getStatus()); 747 }; 748 749 /** 750 * Used internally to check if a cancel has been requested, and handle 751 * it if so. 752 * 753 * @return {boolean} If canceled. 754 * @private 755 */ 756 FileCopyManager.prototype.maybeCancel_ = function() { 757 if (!this.cancelRequested_) 758 return false; 759 760 this.doCancel_(); 761 return true; 762 }; 763 764 /** 765 * Kick off pasting. 766 * 767 * @param {Array.<string>} files Pathes of source files. 768 * @param {Array.<string>} directories Pathes of source directories. 769 * @param {boolean} isCut If the source items are removed from original 770 * location. 771 * @param {string} targetPath Target path. 772 */ 773 FileCopyManager.prototype.paste = function( 774 files, directories, isCut, targetPath) { 775 var self = this; 776 var entries = []; 777 var added = 0; 778 var total; 779 780 var steps = { 781 start: function() { 782 // Filter entries. 783 var entryFilterFunc = function(entry) { 784 if (entry == '') 785 return false; 786 if (isCut && entry.replace(/\/[^\/]+$/, '') == targetPath) 787 // Moving to the same directory is a redundant operation. 788 return false; 789 return true; 790 }; 791 directories = directories ? directories.filter(entryFilterFunc) : []; 792 files = files ? files.filter(entryFilterFunc) : []; 793 794 // Check the number of filtered entries. 795 total = directories.length + files.length; 796 if (total == 0) 797 return; 798 799 // Retrieve entries. 800 util.getDirectories(self.root_, {create: false}, directories, 801 steps.onEntryFound, steps.onPathError); 802 util.getFiles(self.root_, {create: false}, files, 803 steps.onEntryFound, steps.onPathError); 804 }, 805 806 onEntryFound: function(entry) { 807 // When getDirectories/getFiles finish, they call addEntry with null. 808 // We don't want to add null to our entries. 809 if (entry == null) 810 return; 811 entries.push(entry); 812 added++; 813 if (added == total) 814 steps.onSourceEntriesFound(); 815 }, 816 817 onSourceEntriesFound: function() { 818 self.root_.getDirectory(targetPath, {}, 819 steps.onTargetEntryFound, steps.onPathError); 820 }, 821 822 onTargetEntryFound: function(targetEntry) { 823 self.queueCopy_(targetEntry, entries, isCut); 824 }, 825 826 onPathError: function(err) { 827 self.eventRouter_.sendProgressEvent( 828 'ERROR', 829 self.getStatus(), 830 new FileCopyManager.Error( 831 util.FileOperationErrorType.FILESYSTEM_ERROR, err)); 832 } 833 }; 834 835 steps.start(); 836 }; 837 838 /** 839 * Checks if the move operation is avaiable between the given two locations. 840 * 841 * @param {DirectoryEntry} sourceEntry An entry from the source. 842 * @param {DirectoryEntry} targetDirEntry Directory entry for the target. 843 * @return {boolean} Whether we can move from the source to the target. 844 */ 845 FileCopyManager.prototype.isMovable = function(sourceEntry, 846 targetDirEntry) { 847 return (PathUtil.isDriveBasedPath(sourceEntry.fullPath) && 848 PathUtil.isDriveBasedPath(targetDirEntry.fullPath)) || 849 (PathUtil.getRootPath(sourceEntry.fullPath) == 850 PathUtil.getRootPath(targetDirEntry.fullPath)); 851 }; 852 853 /** 854 * Initiate a file copy. 855 * 856 * @param {DirectoryEntry} targetDirEntry Target directory. 857 * @param {Array.<Entry>} entries Entries to copy. 858 * @param {boolean} deleteAfterCopy In case of move. 859 * @return {FileCopyManager.Task} Copy task. 860 * @private 861 */ 862 FileCopyManager.prototype.queueCopy_ = function( 863 targetDirEntry, entries, deleteAfterCopy) { 864 var self = this; 865 // When copying files, null can be specified as source directory. 866 var copyTask = new FileCopyManager.Task(targetDirEntry); 867 if (deleteAfterCopy) { 868 if (this.isMovable(entries[0], targetDirEntry)) { 869 copyTask.move = true; 870 } else { 871 copyTask.deleteAfterCopy = true; 872 } 873 } 874 copyTask.setEntries(entries, function() { 875 self.copyTasks_.push(copyTask); 876 self.maybeScheduleCloseBackgroundPage_(); 877 if (self.copyTasks_.length == 1) { 878 // Assume self.cancelRequested_ == false. 879 // This moved us from 0 to 1 active tasks, let the servicing begin! 880 self.serviceAllTasks_(); 881 } else { 882 // Force to update the progress of butter bar when there are new tasks 883 // coming while servicing current task. 884 self.eventRouter_.sendProgressEvent('PROGRESS', self.getStatus()); 885 } 886 }); 887 888 return copyTask; 889 }; 890 891 /** 892 * Service all pending tasks, as well as any that might appear during the 893 * copy. 894 * 895 * @private 896 */ 897 FileCopyManager.prototype.serviceAllTasks_ = function() { 898 var self = this; 899 900 var onTaskProgress = function() { 901 self.eventRouter_.sendProgressEvent('PROGRESS', self.getStatus()); 902 }; 903 904 var onEntryChanged = function(type, entry) { 905 self.eventRouter_.sendEntryChangedEvent(type, entry); 906 }; 907 908 var onTaskError = function(err) { 909 if (self.maybeCancel_()) 910 return; 911 self.eventRouter_.sendProgressEvent('ERROR', self.getStatus(), err); 912 self.resetQueue_(); 913 }; 914 915 var onTaskSuccess = function() { 916 if (self.maybeCancel_()) 917 return; 918 919 // The task at the front of the queue is completed. Pop it from the queue. 920 self.copyTasks_.shift(); 921 self.maybeScheduleCloseBackgroundPage_(); 922 923 if (!self.copyTasks_.length) { 924 // All tasks have been serviced, clean up and exit. 925 self.eventRouter_.sendProgressEvent('SUCCESS', self.getStatus()); 926 self.resetQueue_(); 927 return; 928 } 929 930 // We want to dispatch a PROGRESS event when there are more tasks to serve 931 // right after one task finished in the queue. We treat all tasks as one 932 // big task logically, so there is only one BEGIN/SUCCESS event pair for 933 // these continuous tasks. 934 self.eventRouter_.sendProgressEvent('PROGRESS', self.getStatus()); 935 936 self.serviceTask_(self.copyTasks_[0], onEntryChanged, onTaskProgress, 937 onTaskSuccess, onTaskError); 938 }; 939 940 // If the queue size is 1 after pushing our task, it was empty before, 941 // so we need to kick off queue processing and dispatch BEGIN event. 942 943 this.eventRouter_.sendProgressEvent('BEGIN', this.getStatus()); 944 this.serviceTask_(this.copyTasks_[0], onEntryChanged, onTaskProgress, 945 onTaskSuccess, onTaskError); 946 }; 947 948 /** 949 * Runs a given task. 950 * Note that the responsibility of this method is just dispatching to the 951 * appropriate serviceXxxTask_() method. 952 * TODO(hidehiko): Remove this method by introducing FileCopyManager.Task.run() 953 * (crbug.com/246976). 954 * 955 * @param {FileCopyManager.Task} task A task to be run. 956 * @param {function(util.EntryChangedType, Entry)} entryChangedCallback Callback 957 * invoked when an entry is changed. 958 * @param {function()} progressCallback Callback invoked periodically during 959 * the operation. 960 * @param {function()} successCallback Callback run on success. 961 * @param {function(FileCopyManager.Error)} errorCallback Callback run on error. 962 * @private 963 */ 964 FileCopyManager.prototype.serviceTask_ = function( 965 task, entryChangedCallback, progressCallback, 966 successCallback, errorCallback) { 967 if (task.zip) 968 this.serviceZipTask_(task, entryChangedCallback, progressCallback, 969 successCallback, errorCallback); 970 else if (task.move) 971 this.serviceMoveTask_(task, entryChangedCallback, progressCallback, 972 successCallback, errorCallback); 973 else 974 this.serviceCopyTask_(task, entryChangedCallback, progressCallback, 975 successCallback, errorCallback); 976 }; 977 978 /** 979 * Service all entries in the copy (and move) task. 980 * Note: this method contains also the operation of "Move" due to historical 981 * reason. 982 * 983 * @param {FileCopyManager.Task} task A copy task to be run. 984 * @param {function(util.EntryChangedType, Entry)} entryChangedCallback Callback 985 * invoked when an entry is changed. 986 * @param {function()} progressCallback Callback invoked periodically during 987 * the copying. 988 * @param {function()} successCallback On success. 989 * @param {function(FileCopyManager.Error)} errorCallback On error. 990 * @private 991 */ 992 FileCopyManager.prototype.serviceCopyTask_ = function( 993 task, entryChangedCallback, progressCallback, successCallback, 994 errorCallback) { 995 // TODO(hidehiko): We should be able to share the code to iterate on entries 996 // with serviceMoveTask_(). 997 if (task.entries.length == 0) { 998 successCallback(); 999 return; 1000 } 1001 1002 var self = this; 1003 1004 var deleteOriginals = function() { 1005 var count = task.originalEntries.length; 1006 1007 var onEntryDeleted = function(entry) { 1008 entryChangedCallback(util.EntryChangedType.DELETED, entry); 1009 count--; 1010 if (!count) 1011 successCallback(); 1012 }; 1013 1014 var onFilesystemError = function(err) { 1015 errorCallback(new FileCopyManager.Error( 1016 util.FileOperationErrorType.FILESYSTEM_ERROR, err)); 1017 }; 1018 1019 for (var i = 0; i < task.originalEntries.length; i++) { 1020 var entry = task.originalEntries[i]; 1021 util.removeFileOrDirectory( 1022 entry, onEntryDeleted.bind(self, entry), onFilesystemError); 1023 } 1024 }; 1025 1026 var onEntryServiced = function() { 1027 task.entryIndex++; 1028 1029 // We should not dispatch a PROGRESS event when there is no pending items 1030 // in the task. 1031 if (task.entryIndex >= task.entries.length) { 1032 if (task.deleteAfterCopy) { 1033 deleteOriginals(); 1034 } else { 1035 successCallback(); 1036 } 1037 return; 1038 } 1039 1040 progressCallback(); 1041 self.processCopyEntry_( 1042 task, task.entries[task.entryIndex], entryChangedCallback, 1043 progressCallback, onEntryServiced, errorCallback); 1044 }; 1045 1046 this.processCopyEntry_( 1047 task, task.entries[task.entryIndex], entryChangedCallback, 1048 progressCallback, onEntryServiced, errorCallback); 1049 }; 1050 1051 /** 1052 * Copies the next entry in a given task. 1053 * TODO(olege): Refactor this method into a separate class. 1054 * 1055 * @param {FileManager.Task} task A task. 1056 * @param {Entry} sourceEntry An entry to be copied. 1057 * @param {function(util.EntryChangedType, Entry)} entryChangedCallback Callback 1058 * invoked when an entry is changed. 1059 * @param {function()} progressCallback Callback invoked periodically during 1060 * the copying. 1061 * @param {function()} successCallback On success. 1062 * @param {function(FileCopyManager.Error)} errorCallback On error. 1063 * @private 1064 */ 1065 FileCopyManager.prototype.processCopyEntry_ = function( 1066 task, sourceEntry, entryChangedCallback, progressCallback, successCallback, 1067 errorCallback) { 1068 if (this.maybeCancel_()) 1069 return; 1070 1071 var self = this; 1072 1073 // |sourceEntry.originalSourcePath| is set in util.recurseAndResolveEntries. 1074 var sourcePath = sourceEntry.originalSourcePath; 1075 if (sourceEntry.fullPath.substr(0, sourcePath.length) != sourcePath) { 1076 // We found an entry in the list that is not relative to the base source 1077 // path, something is wrong. 1078 errorCallback(new FileCopyManager.Error( 1079 util.FileOperationErrorType.UNEXPECTED_SOURCE_FILE, 1080 sourceEntry.fullPath)); 1081 return; 1082 } 1083 1084 var targetDirEntry = task.targetDirEntry; 1085 var originalPath = sourceEntry.fullPath.substr(sourcePath.length + 1); 1086 originalPath = task.applyRenames(originalPath); 1087 1088 var onDeduplicated = function(targetRelativePath) { 1089 var onCopyComplete = function(entry) { 1090 entryChangedCallback(util.EntryChangedType.CREATED, entry); 1091 successCallback(); 1092 }; 1093 1094 var onFilesystemError = function(err) { 1095 errorCallback(new FileCopyManager.Error( 1096 util.FileOperationErrorType.FILESYSTEM_ERROR, err)); 1097 }; 1098 1099 if (sourceEntry.isDirectory) { 1100 // Copying the directory means just creating a new directory. 1101 targetDirEntry.getDirectory( 1102 targetRelativePath, 1103 {create: true, exclusive: true}, 1104 function(targetEntry) { 1105 if (targetRelativePath != originalPath) { 1106 task.registerRename(originalPath, targetRelativePath); 1107 } 1108 onCopyComplete(targetEntry); 1109 }, 1110 util.flog('Error getting dir: ' + targetRelativePath, 1111 onFilesystemError)); 1112 } else { 1113 // Copy a file. 1114 targetDirEntry.getDirectory( 1115 PathUtil.dirname(targetRelativePath), {create: false}, 1116 function(dirEntry) { 1117 self.cancelCallback_ = fileOperationUtil.copyFile( 1118 sourceEntry, dirEntry, PathUtil.basename(targetRelativePath), 1119 function(entry, size) { 1120 task.updateFileCopyProgress(size); 1121 progressCallback(); 1122 }, 1123 function(entry) { 1124 self.cancelCallback_ = null; 1125 onCopyComplete(entry); 1126 }, 1127 function(error) { 1128 self.cancelCallback_ = null; 1129 onFilesystemError(error); 1130 }); 1131 }, 1132 onFilesystemError); 1133 } 1134 }; 1135 1136 fileOperationUtil.deduplicatePath( 1137 targetDirEntry, originalPath, onDeduplicated, errorCallback); 1138 }; 1139 1140 /** 1141 * Moves all entries in the task. 1142 * 1143 * @param {FileCopyManager.Task} task A move task to be run. 1144 * @param {function(util.EntryChangedType, Entry)} entryChangedCallback Callback 1145 * invoked when an entry is changed. 1146 * @param {function()} progressCallback Callback invoked periodically during 1147 * the moving. 1148 * @param {function()} successCallback On success. 1149 * @param {function(FileCopyManager.Error)} errorCallback On error. 1150 * @private 1151 */ 1152 FileCopyManager.prototype.serviceMoveTask_ = function( 1153 task, entryChangedCallback, progressCallback, successCallback, 1154 errorCallback) { 1155 if (task.entries.length == 0) { 1156 successCallback(); 1157 return; 1158 } 1159 1160 this.processMoveEntry_( 1161 task, task.entries[task.entryIndex], entryChangedCallback, 1162 (function onCompleted() { 1163 task.entryIndex++; 1164 1165 // We should not dispatch a PROGRESS event when there is no pending 1166 // items in the task. 1167 if (task.entryIndex >= task.entries.length) { 1168 successCallback(); 1169 return; 1170 } 1171 1172 // Move the next entry. 1173 progressCallback(); 1174 this.processMoveEntry_( 1175 task, task.entries[task.entryIndex], entryChangedCallback, 1176 onCompleted.bind(this), errorCallback); 1177 }).bind(this), 1178 errorCallback); 1179 }; 1180 1181 /** 1182 * Moves the next entry in a given task. 1183 * 1184 * Implementation note: This method can be simplified more. For example, in 1185 * Task.setEntries(), the flag to recurse is set to false for move task, 1186 * so that all the entries' originalSourcePath should be 1187 * dirname(sourceEntry.fullPath). 1188 * Thus, targetRelativePath should contain exact one component. Also we can 1189 * skip applyRenames, because the destination directory always should be 1190 * task.targetDirEntry. 1191 * The unnecessary complexity is due to historical reason. 1192 * TODO(hidehiko): Refactor this method. 1193 * 1194 * @param {FileManager.Task} task A move task. 1195 * @param {Entry} sourceEntry An entry to be moved. 1196 * @param {function(util.EntryChangedType, Entry)} entryChangedCallback Callback 1197 * invoked when an entry is changed. 1198 * @param {function()} successCallback On success. 1199 * @param {function(FileCopyManager.Error)} errorCallback On error. 1200 * @private 1201 */ 1202 FileCopyManager.prototype.processMoveEntry_ = function( 1203 task, sourceEntry, entryChangedCallback, successCallback, errorCallback) { 1204 if (this.maybeCancel_()) 1205 return; 1206 1207 // |sourceEntry.originalSourcePath| is set in util.recurseAndResolveEntries. 1208 var sourcePath = sourceEntry.originalSourcePath; 1209 if (sourceEntry.fullPath.substr(0, sourcePath.length) != sourcePath) { 1210 // We found an entry in the list that is not relative to the base source 1211 // path, something is wrong. 1212 errorCallback(new FileCopyManager.Error( 1213 util.FileOperationErrorType.UNEXPECTED_SOURCE_FILE, 1214 sourceEntry.fullPath)); 1215 return; 1216 } 1217 1218 fileOperationUtil.deduplicatePath( 1219 task.targetDirEntry, 1220 task.applyRenames(sourceEntry.fullPath.substr(sourcePath.length + 1)), 1221 function(targetRelativePath) { 1222 var onFilesystemError = function(err) { 1223 errorCallback(new FileCopyManager.Error( 1224 util.FileOperationErrorType.FILESYSTEM_ERROR, 1225 err)); 1226 }; 1227 1228 task.targetDirEntry.getDirectory( 1229 PathUtil.dirname(targetRelativePath), {create: false}, 1230 function(dirEntry) { 1231 sourceEntry.moveTo( 1232 dirEntry, PathUtil.basename(targetRelativePath), 1233 function(targetEntry) { 1234 entryChangedCallback( 1235 util.EntryChangedType.CREATED, targetEntry); 1236 entryChangedCallback( 1237 util.EntryChangedType.DELETED, sourceEntry); 1238 successCallback(); 1239 }, 1240 onFilesystemError); 1241 }, 1242 onFilesystemError); 1243 }, 1244 errorCallback); 1245 }; 1246 1247 /** 1248 * Service a zip file creation task. 1249 * 1250 * @param {FileCopyManager.Task} task A zip task to be run. 1251 * @param {function(util.EntryChangedType, Entry)} entryChangedCallback Callback 1252 * invoked when an entry is changed. 1253 * @param {function()} progressCallback Callback invoked periodically during 1254 * the moving. 1255 * @param {function()} successCallback On complete. 1256 * @param {function(FileCopyManager.Error)} errorCallback On error. 1257 * @private 1258 */ 1259 FileCopyManager.prototype.serviceZipTask_ = function( 1260 task, entryChangedCallback, progressCallback, successCallback, 1261 errorCallback) { 1262 // TODO(hidehiko): we should localize the name. 1263 var destName = 'Archive'; 1264 if (task.originalEntries.length == 1) { 1265 var entryPath = task.originalEntries[0].fullPath; 1266 var i = entryPath.lastIndexOf('/'); 1267 var basename = (i < 0) ? entryPath : entryPath.substr(i + 1); 1268 i = basename.lastIndexOf('.'); 1269 destName = ((i < 0) ? basename : basename.substr(0, i)); 1270 } 1271 1272 fileOperationUtil.deduplicatePath( 1273 task.targetDirEntry, destName + '.zip', 1274 function(destPath) { 1275 progressCallback(); 1276 1277 fileOperationUtil.zipSelection( 1278 task.entries, 1279 task.zipBaseDirEntry, 1280 destPath, 1281 function(entry) { 1282 entryChangedCallback(util.EntryChangedType.CREATE, entry); 1283 successCallback(); 1284 }, 1285 function(error) { 1286 errorCallback(new FileCopyManager.Error( 1287 util.FileOperationErrorType.FILESYSTEM_ERROR, error)); 1288 }); 1289 }, 1290 errorCallback); 1291 }; 1292 1293 /** 1294 * Timeout before files are really deleted (to allow undo). 1295 */ 1296 FileCopyManager.DELETE_TIMEOUT = 30 * 1000; 1297 1298 /** 1299 * Schedules the files deletion. 1300 * 1301 * @param {Array.<Entry>} entries The entries. 1302 */ 1303 FileCopyManager.prototype.deleteEntries = function(entries) { 1304 var task = { entries: entries }; 1305 this.deleteTasks_.push(task); 1306 this.maybeScheduleCloseBackgroundPage_(); 1307 if (this.deleteTasks_.length == 1) 1308 this.serviceAllDeleteTasks_(); 1309 }; 1310 1311 /** 1312 * Service all pending delete tasks, as well as any that might appear during the 1313 * deletion. 1314 * 1315 * @private 1316 */ 1317 FileCopyManager.prototype.serviceAllDeleteTasks_ = function() { 1318 var self = this; 1319 1320 var onTaskSuccess = function() { 1321 var task = self.deleteTasks_[0]; 1322 self.deleteTasks_.shift(); 1323 if (!self.deleteTasks_.length) { 1324 // All tasks have been serviced, clean up and exit. 1325 self.eventRouter_.sendDeleteEvent( 1326 'SUCCESS', 1327 task.entries.map(function(e) { 1328 return util.makeFilesystemUrl(e.fullPath); 1329 })); 1330 self.maybeScheduleCloseBackgroundPage_(); 1331 return; 1332 } 1333 1334 // We want to dispatch a PROGRESS event when there are more tasks to serve 1335 // right after one task finished in the queue. We treat all tasks as one 1336 // big task logically, so there is only one BEGIN/SUCCESS event pair for 1337 // these continuous tasks. 1338 self.eventRouter_.sendDeleteEvent( 1339 'PROGRESS', 1340 task.entries.map(function(e) { 1341 return util.makeFilesystemUrl(e.fullPath); 1342 })); 1343 self.serviceDeleteTask_(self.deleteTasks_[0], onTaskSuccess, onTaskFailure); 1344 }; 1345 1346 var onTaskFailure = function(task) { 1347 self.deleteTasks_ = []; 1348 self.eventRouter_.sendDeleteEvent( 1349 'ERROR', 1350 task.entries.map(function(e) { 1351 return util.makeFilesystemUrl(e.fullPath); 1352 })); 1353 self.maybeScheduleCloseBackgroundPage_(); 1354 }; 1355 1356 // If the queue size is 1 after pushing our task, it was empty before, 1357 // so we need to kick off queue processing and dispatch BEGIN event. 1358 this.eventRouter_.sendDeleteEvent( 1359 'BEGIN', 1360 this.deleteTasks_[0].entries.map(function(e) { 1361 return util.makeFilesystemUrl(e.fullPath); 1362 })); 1363 this.serviceDeleteTask_(this.deleteTasks_[0], onTaskSuccess, onTaskFailure); 1364 }; 1365 1366 /** 1367 * Performs the deletion. 1368 * 1369 * @param {Object} task The delete task (see deleteEntries function). 1370 * @param {function()} successCallback Callback run on success. 1371 * @param {function(FileCopyManager.Error)} errorCallback Callback run on error. 1372 * @private 1373 */ 1374 FileCopyManager.prototype.serviceDeleteTask_ = function( 1375 task, successCallback, errorCallback) { 1376 var downcount = task.entries.length; 1377 if (downcount == 0) { 1378 successCallback(); 1379 return; 1380 } 1381 1382 var filesystemError = null; 1383 var onComplete = function() { 1384 if (--downcount > 0) 1385 return; 1386 1387 // All remove operations are processed. Run callback. 1388 if (filesystemError) { 1389 errorCallback(new FileCopyManager.Error( 1390 util.FileOperationErrorType.FILESYSTEM_ERROR, filesystemError)); 1391 } else { 1392 successCallback(); 1393 } 1394 }; 1395 1396 for (var i = 0; i < task.entries.length; i++) { 1397 var entry = task.entries[i]; 1398 util.removeFileOrDirectory( 1399 entry, 1400 function(currentEntry) { 1401 this.eventRouter_.sendEntryChangedEvent( 1402 util.EntryChangedType.DELETED, currentEntry); 1403 onComplete(); 1404 }.bind(this, entry), 1405 function(error) { 1406 if (!filesystemError) 1407 filesystemError = error; 1408 onComplete(); 1409 }); 1410 } 1411 }; 1412 1413 /** 1414 * Creates a zip file for the selection of files. 1415 * 1416 * @param {Entry} dirEntry The directory containing the selection. 1417 * @param {Array.<Entry>} selectionEntries The selected entries. 1418 */ 1419 FileCopyManager.prototype.zipSelection = function(dirEntry, selectionEntries) { 1420 var self = this; 1421 var zipTask = new FileCopyManager.Task(dirEntry, dirEntry); 1422 zipTask.zip = true; 1423 zipTask.setEntries(selectionEntries, function() { 1424 // TODO: per-entry zip progress update with accurate byte count. 1425 // For now just set completedBytes to same value as totalBytes so that the 1426 // progress bar is full. 1427 zipTask.completedBytes = zipTask.totalBytes; 1428 self.copyTasks_.push(zipTask); 1429 if (self.copyTasks_.length == 1) { 1430 // Assume self.cancelRequested_ == false. 1431 // This moved us from 0 to 1 active tasks, let the servicing begin! 1432 self.serviceAllTasks_(); 1433 } else { 1434 // Force to update the progress of butter bar when there are new tasks 1435 // coming while servicing current task. 1436 self.eventRouter_.sendProgressEvent('PROGRESS', self.getStatus()); 1437 } 1438 }); 1439 }; 1440