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