Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(346)

Side by Side Diff: chrome/browser/resources/file_manager/background/js/file_operation_manager.js

Issue 247123002: Move Files.app files to ui/file_manager (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Fix the test failure on non-chromeos Created 6 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(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 };
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698