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