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

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

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

Powered by Google App Engine
This is Rietveld 408576698