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.
6 * Utilities for FileOperationManager.
8 var fileOperationUtil = {};
11 * Resolves a path to either a DirectoryEntry or a FileEntry, regardless of
12 * whether the path is a directory or file.
14 * @param {DirectoryEntry} root The root of the filesystem to search.
15 * @param {string} path The path to be resolved.
16 * @return {Promise} Promise fulfilled with the resolved entry, or rejected with
19 fileOperationUtil.resolvePath = function(root, path) {
20 if (path === '' || path === '/')
21 return Promise.resolve(root);
22 return new Promise(root.getFile.bind(root, path, {create: false})).
23 catch(function(error) {
24 if (error.name === util.FileError.TYPE_MISMATCH_ERR) {
25 // Bah. It's a directory, ask again.
27 root.getDirectory.bind(root, path, {create: false}));
29 return Promise.reject(error);
35 * Checks if an entry exists at |relativePath| in |dirEntry|.
36 * If exists, tries to deduplicate the path by inserting parenthesized number,
37 * such as " (1)", before the extension. If it still exists, tries the
38 * deduplication again by increasing the number up to 10 times.
39 * For example, suppose "file.txt" is given, "file.txt", "file (1).txt",
40 * "file (2).txt", ..., "file (9).txt" will be tried.
42 * @param {DirectoryEntry} dirEntry The target directory entry.
43 * @param {string} relativePath The path to be deduplicated.
44 * @param {function(string)=} opt_successCallback Callback run with the
45 * deduplicated path on success.
46 * @param {function(FileOperationManager.Error)=} opt_errorCallback Callback run
48 * @return {Promise} Promise fulfilled with available path.
50 fileOperationUtil.deduplicatePath = function(
51 dirEntry, relativePath, opt_successCallback, opt_errorCallback) {
52 // The trial is up to 10.
55 // Crack the path into three part. The parenthesized number (if exists) will
56 // be replaced by incremented number for retry. For example, suppose
57 // |relativePath| is "file (10).txt", the second check path will be
59 var match = /^(.*?)(?: \((\d+)\))?(\.[^.]*?)?$/.exec(relativePath);
60 var prefix = match[1];
61 var ext = match[3] || '';
63 // Check to see if the target exists.
64 var resolvePath = function(trialPath, numRetry, copyNumber) {
65 return fileOperationUtil.resolvePath(dirEntry, trialPath).then(function() {
67 // Hit the limit of the number of retrial.
68 // Note that we cannot create FileError object directly, so here we
69 // use Object.create instead.
70 return Promise.reject(
71 util.createDOMError(util.FileError.PATH_EXISTS_ERR));
73 var newTrialPath = prefix + ' (' + copyNumber + ')' + ext;
74 return resolvePath(newTrialPath, numRetry - 1, copyNumber + 1);
76 // We expect to be unable to resolve the target file, since we're
77 // going to create it during the copy. However, if the resolve fails
78 // with anything other than NOT_FOUND, that's trouble.
79 if (error.name === util.FileError.NOT_FOUND_ERR)
82 return Promise.reject(error);
86 var promise = resolvePath(relativePath, MAX_RETRY, 1).catch(function(error) {
88 if (error.name === util.FileError.PATH_EXISTS_ERR) {
89 // Failed to uniquify the file path. There should be an existing
90 // entry, so return the error with it.
91 targetPromise = fileOperationUtil.resolvePath(dirEntry, relativePath);
93 targetPromise = Promise.reject(error);
95 return targetPromise.then(function(entry) {
96 return Promise.reject(new FileOperationManager.Error(
97 util.FileOperationErrorType.TARGET_EXISTS, entry));
98 }, function(/** (Error|DOMError) */ inError) {
99 if (inError instanceof Error)
100 return Promise.reject(inError);
101 return Promise.reject(new FileOperationManager.Error(
102 util.FileOperationErrorType.FILESYSTEM_ERROR, inError));
105 if (opt_successCallback)
106 promise.then(opt_successCallback, opt_errorCallback);
111 * Traverses files/subdirectories of the given entry, and returns them.
112 * In addition, this method annotate the size of each entry. The result will
113 * include the entry itself.
115 * @param {Entry} entry The root Entry for traversing.
116 * @param {function(Array.<Entry>)} successCallback Called when the traverse
117 * is successfully done with the array of the entries.
118 * @param {function(DOMError)} errorCallback Called on error with the first
119 * occurred error (i.e. following errors will just be discarded).
121 fileOperationUtil.resolveRecursively = function(
122 entry, successCallback, errorCallback) {
125 var numRunningTasks = 0;
127 var maybeInvokeCallback = function() {
128 // If there still remain some running tasks, wait their finishing.
129 if (numRunningTasks > 0)
133 errorCallback(error);
135 successCallback(result);
138 // The error handling can be shared.
139 var onError = function(fileError) {
140 // If this is the first error, remember it.
144 maybeInvokeCallback();
147 var process = function(entry) {
150 if (entry.isDirectory) {
151 // The size of a directory is 1 bytes here, so that the progress bar
152 // will work smoother.
153 // TODO(hidehiko): Remove this hack.
156 // Recursively traverse children.
157 var reader = entry.createReader();
159 function processSubEntries(subEntries) {
160 if (error || subEntries.length == 0) {
161 // If an error is found already, or this is the completion
162 // callback, then finish the process.
164 maybeInvokeCallback();
168 for (var i = 0; i < subEntries.length; i++)
169 process(subEntries[i]);
171 // Continue to read remaining children.
172 reader.readEntries(processSubEntries, onError);
176 // For a file, annotate the file size.
177 entry.getMetadata(function(metadata) {
178 entry.size = metadata.size;
180 maybeInvokeCallback();
189 * Copies source to parent with the name newName recursively.
190 * This should work very similar to FileSystem API's copyTo. The difference is;
191 * - The progress callback is supported.
192 * - The cancellation is supported.
194 * @param {Entry} source The entry to be copied.
195 * @param {DirectoryEntry} parent The entry of the destination directory.
196 * @param {string} newName The name of copied file.
197 * @param {function(Entry, Entry)} entryChangedCallback
198 * Callback invoked when an entry is created with the source Entry and
199 * the destination Entry.
200 * @param {function(Entry, number)} progressCallback Callback invoked
201 * periodically during the copying. It takes the source Entry and the
202 * processed bytes of it.
203 * @param {function(Entry)} successCallback Callback invoked when the copy
204 * is successfully done with the Entry of the created entry.
205 * @param {function(DOMError)} errorCallback Callback invoked when an error
207 * @return {function()} Callback to cancel the current file copy operation.
208 * When the cancel is done, errorCallback will be called. The returned
209 * callback must not be called more than once.
211 fileOperationUtil.copyTo = function(
212 source, parent, newName, entryChangedCallback, progressCallback,
213 successCallback, errorCallback) {
215 var pendingCallbacks = [];
217 // Makes the callback called in order they were invoked.
218 var callbackQueue = new AsyncUtil.Queue();
220 var onCopyProgress = function(progressCopyId, status) {
221 callbackQueue.run(function(callback) {
222 if (copyId === null) {
223 // If the copyId is not yet available, wait for it.
224 pendingCallbacks.push(
225 onCopyProgress.bind(null, progressCopyId, status));
230 // This is not what we're interested in.
231 if (progressCopyId != copyId) {
236 switch (status.type) {
237 case 'begin_copy_entry':
241 case 'end_copy_entry':
242 // TODO(mtomasz): Convert URL to Entry in custom bindings.
243 (source.isFile ? parent.getFile : parent.getDirectory).call(
248 entryChangedCallback(status.sourceUrl, entry);
252 entryChangedCallback(status.sourceUrl, null);
258 progressCallback(status.sourceUrl, status.size);
263 chrome.fileManagerPrivate.onCopyProgress.removeListener(
265 // TODO(mtomasz): Convert URL to Entry in custom bindings.
267 [status.destinationUrl], function(destinationEntries) {
268 successCallback(destinationEntries[0] || null);
274 chrome.fileManagerPrivate.onCopyProgress.removeListener(
276 errorCallback(util.createDOMError(status.error));
281 // Found unknown state. Cancel the task, and return an error.
282 console.error('Unknown progress type: ' + status.type);
283 chrome.fileManagerPrivate.onCopyProgress.removeListener(
285 chrome.fileManagerPrivate.cancelCopy(copyId);
286 errorCallback(util.createDOMError(
287 util.FileError.INVALID_STATE_ERR));
293 // Register the listener before calling startCopy. Otherwise some events
295 chrome.fileManagerPrivate.onCopyProgress.addListener(onCopyProgress);
297 // Then starts the copy.
298 // TODO(mtomasz): Convert URL to Entry in custom bindings.
299 chrome.fileManagerPrivate.startCopy(
300 source.toURL(), parent.toURL(), newName, function(startCopyId) {
301 // last error contains the FileError code on error.
302 if (chrome.runtime.lastError) {
303 // Unsubscribe the progress listener.
304 chrome.fileManagerPrivate.onCopyProgress.removeListener(
306 errorCallback(util.createDOMError(
307 chrome.runtime.lastError.message || ''));
311 copyId = startCopyId;
312 for (var i = 0; i < pendingCallbacks.length; i++) {
313 pendingCallbacks[i]();
318 // If copyId is not yet available, wait for it.
319 if (copyId == null) {
320 pendingCallbacks.push(function() {
321 chrome.fileManagerPrivate.cancelCopy(copyId);
326 chrome.fileManagerPrivate.cancelCopy(copyId);
331 * Thin wrapper of chrome.fileManagerPrivate.zipSelection to adapt its
332 * interface similar to copyTo().
334 * @param {Array.<Entry>} sources The array of entries to be archived.
335 * @param {DirectoryEntry} parent The entry of the destination directory.
336 * @param {string} newName The name of the archive to be created.
337 * @param {function(FileEntry)} successCallback Callback invoked when the
338 * operation is successfully done with the entry of the created archive.
339 * @param {function(DOMError)} errorCallback Callback invoked when an error
342 fileOperationUtil.zipSelection = function(
343 sources, parent, newName, successCallback, errorCallback) {
344 // TODO(mtomasz): Move conversion from entry to url to custom bindings.
346 chrome.fileManagerPrivate.zipSelection(
348 util.entriesToURLs(sources),
349 newName, function(success) {
351 // Failed to create a zip archive.
353 util.createDOMError(util.FileError.INVALID_MODIFICATION_ERR));
357 // Returns the created entry via callback.
359 newName, {create: false}, successCallback, errorCallback);
366 function FileOperationManager() {
367 this.copyTasks_ = [];
368 this.deleteTasks_ = [];
369 this.taskIdCounter_ = 0;
370 this.eventRouter_ = new FileOperationManager.EventRouter();
376 * Manages Event dispatching.
377 * Currently this can send three types of events: "copy-progress",
378 * "copy-operation-completed" and "delete".
380 * TODO(hidehiko): Reorganize the event dispatching mechanism.
382 * @extends {cr.EventTarget}
384 FileOperationManager.EventRouter = function() {
385 this.pendingDeletedEntries_ = [];
386 this.pendingCreatedEntries_ = [];
387 this.entryChangedEventRateLimiter_ = new AsyncUtil.RateLimiter(
388 this.dispatchEntryChangedEvent_.bind(this), 500);
392 * Extends cr.EventTarget.
394 FileOperationManager.EventRouter.prototype.__proto__ = cr.EventTarget.prototype;
397 * Dispatches a simple "copy-progress" event with reason and current
398 * FileOperationManager status. If it is an ERROR event, error should be set.
400 * @param {string} reason Event type. One of "BEGIN", "PROGRESS", "SUCCESS",
401 * "ERROR" or "CANCELLED". TODO(hidehiko): Use enum.
402 * @param {Object} status Current FileOperationManager's status. See also
403 * FileOperationManager.Task.getStatus().
404 * @param {string} taskId ID of task related with the event.
405 * @param {FileOperationManager.Error=} opt_error The info for the error. This
406 * should be set iff the reason is "ERROR".
408 FileOperationManager.EventRouter.prototype.sendProgressEvent = function(
409 reason, status, taskId, opt_error) {
410 // Before finishing operation, dispatch pending entries-changed events.
411 if (reason === 'SUCCESS' || reason === 'CANCELED')
412 this.entryChangedEventRateLimiter_.runImmediately();
414 var event = /** @type {FileOperationProgressEvent} */
415 (new Event('copy-progress'));
416 event.reason = reason;
417 event.status = status;
418 event.taskId = taskId;
420 event.error = opt_error;
421 this.dispatchEvent(event);
425 * Stores changed (created or deleted) entry temporarily, and maybe dispatch
426 * entries-changed event with stored entries.
427 * @param {util.EntryChangedKind} kind The enum to represent if the entry is
428 * created or deleted.
429 * @param {Entry} entry The changed entry.
431 FileOperationManager.EventRouter.prototype.sendEntryChangedEvent = function(
433 if (kind === util.EntryChangedKind.DELETED)
434 this.pendingDeletedEntries_.push(entry);
435 if (kind === util.EntryChangedKind.CREATED)
436 this.pendingCreatedEntries_.push(entry);
438 this.entryChangedEventRateLimiter_.run();
442 * Dispatches an event to notify that entries are changed (created or deleted).
445 FileOperationManager.EventRouter.prototype.dispatchEntryChangedEvent_ =
447 if (this.pendingDeletedEntries_.length > 0) {
448 var event = new Event('entries-changed');
449 event.kind = util.EntryChangedKind.DELETED;
450 event.entries = this.pendingDeletedEntries_;
451 this.dispatchEvent(event);
452 this.pendingDeletedEntries_ = [];
454 if (this.pendingCreatedEntries_.length > 0) {
455 var event = new Event('entries-changed');
456 event.kind = util.EntryChangedKind.CREATED;
457 event.entries = this.pendingCreatedEntries_;
458 this.dispatchEvent(event);
459 this.pendingCreatedEntries_ = [];
464 * Dispatches an event to notify entries are changed for delete task.
466 * @param {string} reason Event type. One of "BEGIN", "PROGRESS", "SUCCESS",
467 * or "ERROR". TODO(hidehiko): Use enum.
468 * @param {!Object} task Delete task related with the event.
470 FileOperationManager.EventRouter.prototype.sendDeleteEvent = function(
472 var event = /** @type {FileOperationProgressEvent} */ (new Event('delete'));
473 event.reason = reason;
474 event.taskId = task.taskId;
475 event.entries = task.entries;
476 event.totalBytes = task.totalBytes;
477 event.processedBytes = task.processedBytes;
478 this.dispatchEvent(event);
482 * A record of a queued copy operation.
484 * Multiple copy operations may be queued at any given time. Additional
485 * Tasks may be added while the queue is being serviced. Though a
486 * cancel operation cancels everything in the queue.
488 * @param {util.FileOperationType} operationType The type of this operation.
489 * @param {Array.<Entry>} sourceEntries Array of source entries.
490 * @param {DirectoryEntry} targetDirEntry Target directory.
493 FileOperationManager.Task = function(
494 operationType, sourceEntries, targetDirEntry) {
495 this.operationType = operationType;
496 this.sourceEntries = sourceEntries;
497 this.targetDirEntry = targetDirEntry;
500 * An array of map from url to Entry being processed.
501 * @type {Array.<Object<string, Entry>>}
503 this.processingEntries = null;
506 * Total number of bytes to be processed. Filled in initialize().
507 * Use 1 as an initial value to indicate that the task is not completed.
513 * Total number of already processed bytes. Updated periodically.
516 this.processedBytes = 0;
519 * Index of the progressing entry in sourceEntries.
523 this.processingSourceIndex_ = 0;
526 * Set to true when cancel is requested.
529 this.cancelRequested_ = false;
532 * Callback to cancel the running process.
533 * @private {?function()}
535 this.cancelCallback_ = null;
537 // TODO(hidehiko): After we support recursive copy, we don't need this.
538 // If directory already exists, we try to make a copy named 'dir (X)',
539 // where X is a number. When we do this, all subsequent copies from
540 // inside the subtree should be mapped to the new directory name.
541 // For example, if 'dir' was copied as 'dir (1)', then 'dir/file.txt' should
542 // become 'dir (1)/file.txt'.
543 this.renamedDirectories_ = [];
547 * @param {function()} callback When entries resolved.
549 FileOperationManager.Task.prototype.initialize = function(callback) {
553 * Requests cancellation of this task.
554 * When the cancellation is done, it is notified via callbacks of run().
556 FileOperationManager.Task.prototype.requestCancel = function() {
557 this.cancelRequested_ = true;
558 if (this.cancelCallback_) {
559 this.cancelCallback_();
560 this.cancelCallback_ = null;
565 * Runs the task. Sub classes must implement this method.
567 * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback
568 * Callback invoked when an entry is changed.
569 * @param {function()} progressCallback Callback invoked periodically during
571 * @param {function()} successCallback Callback run on success.
572 * @param {function(FileOperationManager.Error)} errorCallback Callback run on
575 FileOperationManager.Task.prototype.run = function(
576 entryChangedCallback, progressCallback, successCallback, errorCallback) {
580 * Get states of the task.
581 * TOOD(hirono): Removes this method and sets a task to progress events.
582 * @return {Object} Status object.
584 FileOperationManager.Task.prototype.getStatus = function() {
585 var processingEntry = this.sourceEntries[this.processingSourceIndex_];
587 operationType: this.operationType,
588 numRemainingItems: this.sourceEntries.length - this.processingSourceIndex_,
589 totalBytes: this.totalBytes,
590 processedBytes: this.processedBytes,
591 processingEntryName: processingEntry ? processingEntry.name : ''
596 * Obtains the number of total processed bytes.
597 * @return {number} Number of total processed bytes.
600 FileOperationManager.Task.prototype.calcProcessedBytes_ = function() {
602 for (var i = 0; i < this.processingSourceIndex_ + 1; i++) {
603 var entryMap = this.processingEntries[i];
606 for (var name in entryMap) {
607 bytes += i < this.processingSourceIndex_ ?
608 entryMap[name].size : entryMap[name].processedBytes;
615 * Task to copy entries.
617 * @param {Array.<Entry>} sourceEntries Array of source entries.
618 * @param {DirectoryEntry} targetDirEntry Target directory.
619 * @param {boolean} deleteAfterCopy Whether the delete original files after
622 * @extends {FileOperationManager.Task}
624 FileOperationManager.CopyTask = function(sourceEntries,
627 FileOperationManager.Task.call(
630 util.FileOperationType.MOVE : util.FileOperationType.COPY,
633 this.deleteAfterCopy = deleteAfterCopy;
636 * Rate limiter which is used to avoid sending update request for progress bar
638 * @type {AsyncUtil.RateLimiter}
641 this.updateProgressRateLimiter_ = null;
645 * Extends FileOperationManager.Task.
647 FileOperationManager.CopyTask.prototype.__proto__ =
648 FileOperationManager.Task.prototype;
651 * Initializes the CopyTask.
652 * @param {function()} callback Called when the initialize is completed.
654 FileOperationManager.CopyTask.prototype.initialize = function(callback) {
655 var group = new AsyncUtil.Group();
656 // Correct all entries to be copied for status update.
657 this.processingEntries = [];
658 for (var i = 0; i < this.sourceEntries.length; i++) {
659 group.add(function(index, callback) {
660 fileOperationUtil.resolveRecursively(
661 this.sourceEntries[index],
662 function(resolvedEntries) {
663 var resolvedEntryMap = {};
664 for (var j = 0; j < resolvedEntries.length; ++j) {
665 var entry = resolvedEntries[j];
666 entry.processedBytes = 0;
667 resolvedEntryMap[entry.toURL()] = entry;
669 this.processingEntries[index] = resolvedEntryMap;
674 'Failed to resolve for copy: %s', error.name);
680 group.run(function() {
683 for (var i = 0; i < this.processingEntries.length; i++) {
684 for (var entryURL in this.processingEntries[i])
685 this.totalBytes += this.processingEntries[i][entryURL].size;
693 * Copies all entries to the target directory.
694 * Note: this method contains also the operation of "Move" due to historical
697 * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback
698 * Callback invoked when an entry is changed.
699 * @param {function()} progressCallback Callback invoked periodically during
701 * @param {function()} successCallback On success.
702 * @param {function(FileOperationManager.Error)} errorCallback On error.
705 FileOperationManager.CopyTask.prototype.run = function(
706 entryChangedCallback, progressCallback, successCallback, errorCallback) {
707 // TODO(hidehiko): We should be able to share the code to iterate on entries
708 // with serviceMoveTask_().
709 if (this.sourceEntries.length == 0) {
714 // TODO(hidehiko): Delete after copy is the implementation of Move.
715 // Migrate the part into MoveTask.run().
716 var deleteOriginals = function() {
717 var count = this.sourceEntries.length;
719 var onEntryDeleted = function(entry) {
720 entryChangedCallback(util.EntryChangedKind.DELETED, entry);
726 var onFilesystemError = function(err) {
727 errorCallback(new FileOperationManager.Error(
728 util.FileOperationErrorType.FILESYSTEM_ERROR, err));
731 for (var i = 0; i < this.sourceEntries.length; i++) {
732 var entry = this.sourceEntries[i];
733 util.removeFileOrDirectory(
734 entry, onEntryDeleted.bind(null, entry), onFilesystemError);
739 * Accumulates processed bytes and call |progressCallback| if needed.
741 * @param {number} index The index of processing source.
742 * @param {string} sourceEntryUrl URL of the entry which has been processed.
743 * @param {number=} opt_size Processed bytes of the |sourceEntry|. If it is
744 * dropped, all bytes of the entry are considered to be processed.
746 var updateProgress = function(index, sourceEntryUrl, opt_size) {
750 var processedEntry = this.processingEntries[index][sourceEntryUrl];
754 // Accumulates newly processed bytes.
755 var size = opt_size !== undefined ? opt_size : processedEntry.size;
756 this.processedBytes += size - processedEntry.processedBytes;
757 processedEntry.processedBytes = size;
759 // Updates progress bar in limited frequency so that intervals between
760 // updates have at least 200ms.
761 this.updateProgressRateLimiter_.run();
763 updateProgress = updateProgress.bind(this);
765 this.updateProgressRateLimiter_ = new AsyncUtil.RateLimiter(progressCallback);
769 function(callback, entry, index) {
770 if (this.cancelRequested_) {
771 errorCallback(new FileOperationManager.Error(
772 util.FileOperationErrorType.FILESYSTEM_ERROR,
773 util.createDOMError(util.FileError.ABORT_ERR)));
778 entry, this.targetDirEntry,
779 function(sourceEntryUrl, destinationEntry) {
780 updateProgress(index, sourceEntryUrl);
781 // The destination entry may be null, if the copied file got
782 // deleted just after copying.
783 if (destinationEntry) {
784 entryChangedCallback(
785 util.EntryChangedKind.CREATED, destinationEntry);
788 function(sourceEntryUrl, size) {
789 updateProgress(index, sourceEntryUrl, size);
792 // Finishes off delayed updates if necessary.
793 this.updateProgressRateLimiter_.runImmediately();
794 // Update current source index and processing bytes.
795 this.processingSourceIndex_ = index + 1;
796 this.processedBytes = this.calcProcessedBytes_();
800 // Finishes off delayed updates if necessary.
801 this.updateProgressRateLimiter_.runImmediately();
802 errorCallback(error);
806 if (this.deleteAfterCopy) {
816 * Copies the source entry to the target directory.
818 * @param {Entry} sourceEntry An entry to be copied.
819 * @param {DirectoryEntry} destinationEntry The entry which will contain the
821 * @param {function(Entry, Entry)} entryChangedCallback
822 * Callback invoked when an entry is created with the source Entry and
823 * the destination Entry.
824 * @param {function(Entry, number)} progressCallback Callback invoked
825 * periodically during the copying.
826 * @param {function()} successCallback On success.
827 * @param {function(FileOperationManager.Error)} errorCallback On error.
830 FileOperationManager.CopyTask.prototype.processEntry_ = function(
831 sourceEntry, destinationEntry, entryChangedCallback, progressCallback,
832 successCallback, errorCallback) {
833 fileOperationUtil.deduplicatePath(
834 destinationEntry, sourceEntry.name,
835 function(destinationName) {
836 if (this.cancelRequested_) {
837 errorCallback(new FileOperationManager.Error(
838 util.FileOperationErrorType.FILESYSTEM_ERROR,
839 util.createDOMError(util.FileError.ABORT_ERR)));
842 this.cancelCallback_ = fileOperationUtil.copyTo(
843 sourceEntry, destinationEntry, destinationName,
844 entryChangedCallback, progressCallback,
846 this.cancelCallback_ = null;
850 this.cancelCallback_ = null;
851 errorCallback(new FileOperationManager.Error(
852 util.FileOperationErrorType.FILESYSTEM_ERROR, error));
859 * Task to move entries.
861 * @param {Array.<Entry>} sourceEntries Array of source entries.
862 * @param {DirectoryEntry} targetDirEntry Target directory.
864 * @extends {FileOperationManager.Task}
866 FileOperationManager.MoveTask = function(sourceEntries, targetDirEntry) {
867 FileOperationManager.Task.call(
868 this, util.FileOperationType.MOVE, sourceEntries, targetDirEntry);
872 * Extends FileOperationManager.Task.
874 FileOperationManager.MoveTask.prototype.__proto__ =
875 FileOperationManager.Task.prototype;
878 * Initializes the MoveTask.
879 * @param {function()} callback Called when the initialize is completed.
881 FileOperationManager.MoveTask.prototype.initialize = function(callback) {
882 // This may be moving from search results, where it fails if we
883 // move parent entries earlier than child entries. We should
884 // process the deepest entry first. Since move of each entry is
885 // done by a single moveTo() call, we don't need to care about the
886 // recursive traversal order.
887 this.sourceEntries.sort(function(entry1, entry2) {
888 return entry2.toURL().length - entry1.toURL().length;
891 this.processingEntries = [];
892 for (var i = 0; i < this.sourceEntries.length; i++) {
893 var processingEntryMap = {};
894 var entry = this.sourceEntries[i];
896 // The move should be done with updating the metadata. So here we assume
897 // all the file size is 1 byte. (Avoiding 0, so that progress bar can
899 // TODO(hidehiko): Remove this hack.
901 processingEntryMap[entry.toURL()] = entry;
902 this.processingEntries[i] = processingEntryMap;
909 * Moves all entries in the task.
911 * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback
912 * Callback invoked when an entry is changed.
913 * @param {function()} progressCallback Callback invoked periodically during
915 * @param {function()} successCallback On success.
916 * @param {function(FileOperationManager.Error)} errorCallback On error.
919 FileOperationManager.MoveTask.prototype.run = function(
920 entryChangedCallback, progressCallback, successCallback, errorCallback) {
921 if (this.sourceEntries.length == 0) {
928 function(callback, entry, index) {
929 if (this.cancelRequested_) {
930 errorCallback(new FileOperationManager.Error(
931 util.FileOperationErrorType.FILESYSTEM_ERROR,
932 util.createDOMError(util.FileError.ABORT_ERR)));
936 FileOperationManager.MoveTask.processEntry_(
937 entry, this.targetDirEntry, entryChangedCallback,
939 // Update current source index.
940 this.processingSourceIndex_ = index + 1;
941 this.processedBytes = this.calcProcessedBytes_();
953 * Moves the sourceEntry to the targetDirEntry in this task.
955 * @param {Entry} sourceEntry An entry to be moved.
956 * @param {!DirectoryEntry} destinationEntry The entry of the destination
958 * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback
959 * Callback invoked when an entry is changed.
960 * @param {function()} successCallback On success.
961 * @param {function(FileOperationManager.Error)} errorCallback On error.
964 FileOperationManager.MoveTask.processEntry_ = function(
965 sourceEntry, destinationEntry, entryChangedCallback, successCallback,
967 fileOperationUtil.deduplicatePath(
970 function(destinationName) {
972 destinationEntry, destinationName,
973 function(movedEntry) {
974 entryChangedCallback(util.EntryChangedKind.CREATED, movedEntry);
975 entryChangedCallback(util.EntryChangedKind.DELETED, sourceEntry);
979 errorCallback(new FileOperationManager.Error(
980 util.FileOperationErrorType.FILESYSTEM_ERROR, error));
987 * Task to create a zip archive.
989 * @param {Array.<Entry>} sourceEntries Array of source entries.
990 * @param {DirectoryEntry} targetDirEntry Target directory.
991 * @param {DirectoryEntry} zipBaseDirEntry Base directory dealt as a root
994 * @extends {FileOperationManager.Task}
996 FileOperationManager.ZipTask = function(
997 sourceEntries, targetDirEntry, zipBaseDirEntry) {
998 FileOperationManager.Task.call(
999 this, util.FileOperationType.ZIP, sourceEntries, targetDirEntry);
1000 this.zipBaseDirEntry = zipBaseDirEntry;
1004 * Extends FileOperationManager.Task.
1006 FileOperationManager.ZipTask.prototype.__proto__ =
1007 FileOperationManager.Task.prototype;
1011 * Initializes the ZipTask.
1012 * @param {function()} callback Called when the initialize is completed.
1014 FileOperationManager.ZipTask.prototype.initialize = function(callback) {
1015 var resolvedEntryMap = {};
1016 var group = new AsyncUtil.Group();
1017 for (var i = 0; i < this.sourceEntries.length; i++) {
1018 group.add(function(index, callback) {
1019 fileOperationUtil.resolveRecursively(
1020 this.sourceEntries[index],
1022 for (var j = 0; j < entries.length; j++)
1023 resolvedEntryMap[entries[j].toURL()] = entries[j];
1030 group.run(function() {
1031 // For zip archiving, all the entries are processed at once.
1032 this.processingEntries = [resolvedEntryMap];
1034 this.totalBytes = 0;
1035 for (var url in resolvedEntryMap)
1036 this.totalBytes += resolvedEntryMap[url].size;
1043 * Runs a zip file creation task.
1045 * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback
1046 * Callback invoked when an entry is changed.
1047 * @param {function()} progressCallback Callback invoked periodically during
1049 * @param {function()} successCallback On complete.
1050 * @param {function(FileOperationManager.Error)} errorCallback On error.
1053 FileOperationManager.ZipTask.prototype.run = function(
1054 entryChangedCallback, progressCallback, successCallback, errorCallback) {
1055 // TODO(hidehiko): we should localize the name.
1056 var destName = 'Archive';
1057 if (this.sourceEntries.length == 1) {
1058 var entryName = this.sourceEntries[0].name;
1059 var i = entryName.lastIndexOf('.');
1060 destName = ((i < 0) ? entryName : entryName.substr(0, i));
1063 fileOperationUtil.deduplicatePath(
1064 this.targetDirEntry, destName + '.zip',
1065 function(destPath) {
1066 // TODO: per-entry zip progress update with accurate byte count.
1067 // For now just set completedBytes to 0 so that it is not full until
1068 // the zip operatoin is done.
1069 this.processedBytes = 0;
1072 // The number of elements in processingEntries is 1. See also
1075 for (var url in this.processingEntries[0])
1076 entries.push(this.processingEntries[0][url]);
1078 fileOperationUtil.zipSelection(
1080 this.zipBaseDirEntry,
1083 this.processedBytes = this.totalBytes;
1084 entryChangedCallback(util.EntryChangedKind.CREATED, entry);
1088 errorCallback(new FileOperationManager.Error(
1089 util.FileOperationErrorType.FILESYSTEM_ERROR, error));
1096 * Error class used to report problems with a copy operation.
1097 * If the code is UNEXPECTED_SOURCE_FILE, data should be a path of the file.
1098 * If the code is TARGET_EXISTS, data should be the existing Entry.
1099 * If the code is FILESYSTEM_ERROR, data should be the FileError.
1101 * @param {util.FileOperationErrorType} code Error type.
1102 * @param {string|Entry|DOMError} data Additional data.
1105 FileOperationManager.Error = function(code, data) {
1110 // FileOperationManager methods.
1113 * Adds an event listener for the tasks.
1114 * @param {string} type The name of the event.
1115 * @param {function(Event)} handler The handler for the event.
1116 * This is called when the event is dispatched.
1118 FileOperationManager.prototype.addEventListener = function(type, handler) {
1119 this.eventRouter_.addEventListener(type, handler);
1123 * Removes an event listener for the tasks.
1124 * @param {string} type The name of the event.
1125 * @param {function(Event)} handler The handler to be removed.
1127 FileOperationManager.prototype.removeEventListener = function(type, handler) {
1128 this.eventRouter_.removeEventListener(type, handler);
1132 * Says if there are any tasks in the queue.
1133 * @return {boolean} True, if there are any tasks.
1135 FileOperationManager.prototype.hasQueuedTasks = function() {
1136 return this.copyTasks_.length > 0 || this.deleteTasks_.length > 0;
1140 * Completely clear out the copy queue, either because we encountered an error
1141 * or completed successfully.
1145 FileOperationManager.prototype.resetQueue_ = function() {
1146 this.copyTasks_ = [];
1150 * Requests the specified task to be canceled.
1151 * @param {string} taskId ID of task to be canceled.
1153 FileOperationManager.prototype.requestTaskCancel = function(taskId) {
1155 for (var i = 0; i < this.copyTasks_.length; i++) {
1156 task = this.copyTasks_[i];
1157 if (task.taskId !== taskId)
1159 task.requestCancel();
1160 // If the task is not on progress, remove it immediately.
1162 this.eventRouter_.sendProgressEvent('CANCELED',
1165 this.copyTasks_.splice(i, 1);
1168 for (var i = 0; i < this.deleteTasks_.length; i++) {
1169 task = this.deleteTasks_[i];
1170 if (task.taskId !== taskId)
1172 task.cancelRequested = true;
1173 // If the task is not on progress, remove it immediately.
1175 this.eventRouter_.sendDeleteEvent('CANCELED', task);
1176 this.deleteTasks_.splice(i, 1);
1182 * Filters the entry in the same directory
1184 * @param {Array.<Entry>} sourceEntries Entries of the source files.
1185 * @param {DirectoryEntry} targetEntry The destination entry of the target
1187 * @param {boolean} isMove True if the operation is "move", otherwise (i.e.
1188 * if the operation is "copy") false.
1189 * @return {Promise} Promise fulfilled with the filtered entry. This is not
1192 FileOperationManager.prototype.filterSameDirectoryEntry = function(
1193 sourceEntries, targetEntry, isMove) {
1195 return Promise.resolve(sourceEntries);
1196 // Utility function to concat arrays.
1197 var compactArrays = function(arrays) {
1198 return arrays.filter(function(element) { return !!element; });
1200 // Call processEntry for each item of entries.
1201 var processEntries = function(entries) {
1202 var promises = entries.map(processFileOrDirectoryEntries);
1203 return Promise.all(promises).then(compactArrays);
1205 // Check all file entries and keeps only those need sharing operation.
1206 var processFileOrDirectoryEntries = function(entry) {
1207 return new Promise(function(resolve) {
1208 entry.getParent(function(inParentEntry) {
1209 if (!util.isSameEntry(inParentEntry, targetEntry))
1213 }, function(error) {
1214 console.error(error.stack || error);
1219 return processEntries(sourceEntries);
1225 * @param {Array.<Entry>} sourceEntries Entries of the source files.
1226 * @param {DirectoryEntry} targetEntry The destination entry of the target
1228 * @param {boolean} isMove True if the operation is "move", otherwise (i.e.
1229 * if the operation is "copy") false.
1230 * @param {string=} opt_taskId If the corresponding item has already created
1231 * at another places, we need to specify the ID of the item. If the
1232 * item is not created, FileOperationManager generates new ID.
1234 FileOperationManager.prototype.paste = function(
1235 sourceEntries, targetEntry, isMove, opt_taskId) {
1236 // Do nothing if sourceEntries is empty.
1237 if (sourceEntries.length === 0)
1240 this.filterSameDirectoryEntry(sourceEntries, targetEntry, isMove).then(
1242 if (entries.length === 0)
1244 this.queueCopy_(targetEntry, entries, isMove, opt_taskId);
1245 }.bind(this)).catch(function(error) {
1246 console.error(error.stack || error);
1251 * Initiate a file copy. When copying files, null can be specified as source
1254 * @param {DirectoryEntry} targetDirEntry Target directory.
1255 * @param {Array.<Entry>} entries Entries to copy.
1256 * @param {boolean} isMove In case of move.
1257 * @param {string=} opt_taskId If the corresponding item has already created
1258 * at another places, we need to specify the ID of the item. If the
1259 * item is not created, FileOperationManager generates new ID.
1262 FileOperationManager.prototype.queueCopy_ = function(
1263 targetDirEntry, entries, isMove, opt_taskId) {
1266 // When moving between different volumes, moving is implemented as a copy
1267 // and delete. This is because moving between volumes is slow, and moveTo()
1268 // is not cancellable nor provides progress feedback.
1269 if (util.isSameFileSystem(entries[0].filesystem,
1270 targetDirEntry.filesystem)) {
1271 task = new FileOperationManager.MoveTask(entries, targetDirEntry);
1273 task = new FileOperationManager.CopyTask(entries, targetDirEntry, true);
1276 task = new FileOperationManager.CopyTask(entries, targetDirEntry, false);
1279 task.taskId = opt_taskId || this.generateTaskId();
1280 this.eventRouter_.sendProgressEvent('BEGIN', task.getStatus(), task.taskId);
1281 task.initialize(function() {
1282 this.copyTasks_.push(task);
1283 if (this.copyTasks_.length === 1)
1284 this.serviceAllTasks_();
1289 * Service all pending tasks, as well as any that might appear during the
1294 FileOperationManager.prototype.serviceAllTasks_ = function() {
1295 if (!this.copyTasks_.length) {
1296 // All tasks have been serviced, clean up and exit.
1297 chrome.power.releaseKeepAwake();
1302 // Prevent the system from sleeping while copy is in progress.
1303 chrome.power.requestKeepAwake('system');
1305 var onTaskProgress = function() {
1306 this.eventRouter_.sendProgressEvent('PROGRESS',
1307 this.copyTasks_[0].getStatus(),
1308 this.copyTasks_[0].taskId);
1311 var onEntryChanged = function(kind, entry) {
1312 this.eventRouter_.sendEntryChangedEvent(kind, entry);
1315 var onTaskError = function(err) {
1316 var task = this.copyTasks_.shift();
1317 var reason = err.data.name === util.FileError.ABORT_ERR ?
1318 'CANCELED' : 'ERROR';
1319 this.eventRouter_.sendProgressEvent(reason,
1323 this.serviceAllTasks_();
1326 var onTaskSuccess = function() {
1327 // The task at the front of the queue is completed. Pop it from the queue.
1328 var task = this.copyTasks_.shift();
1329 this.eventRouter_.sendProgressEvent('SUCCESS',
1332 this.serviceAllTasks_();
1335 var nextTask = this.copyTasks_[0];
1336 this.eventRouter_.sendProgressEvent('PROGRESS',
1337 nextTask.getStatus(),
1339 nextTask.run(onEntryChanged, onTaskProgress, onTaskSuccess, onTaskError);
1343 * Timeout before files are really deleted (to allow undo).
1345 FileOperationManager.DELETE_TIMEOUT = 30 * 1000;
1348 * Schedules the files deletion.
1350 * @param {Array.<Entry>} entries The entries.
1352 FileOperationManager.prototype.deleteEntries = function(entries) {
1353 // TODO(hirono): Make FileOperationManager.DeleteTask.
1354 var task = Object.seal({
1356 taskId: this.generateTaskId(),
1360 cancelRequested: false
1363 // Obtains entry size and sum them up.
1364 var group = new AsyncUtil.Group();
1365 for (var i = 0; i < task.entries.length; i++) {
1366 group.add(function(entry, callback) {
1367 entry.getMetadata(function(metadata) {
1368 var index = task.entries.indexOf(entries);
1369 task.entrySize[entry.toURL()] = metadata.size;
1370 task.totalBytes += metadata.size;
1373 // Fail to obtain the metadata. Use fake value 1.
1374 task.entrySize[entry.toURL()] = 1;
1375 task.totalBytes += 1;
1378 }.bind(this, task.entries[i]));
1381 // Add a delete task.
1382 group.run(function() {
1383 this.deleteTasks_.push(task);
1384 this.eventRouter_.sendDeleteEvent('BEGIN', task);
1385 if (this.deleteTasks_.length === 1)
1386 this.serviceAllDeleteTasks_();
1391 * Service all pending delete tasks, as well as any that might appear during the
1394 * Must not be called if there is an in-flight delete task.
1398 FileOperationManager.prototype.serviceAllDeleteTasks_ = function() {
1399 this.serviceDeleteTask_(
1400 this.deleteTasks_[0],
1402 this.deleteTasks_.shift();
1403 if (this.deleteTasks_.length)
1404 this.serviceAllDeleteTasks_();
1409 * Performs the deletion.
1411 * @param {Object} task The delete task (see deleteEntries function).
1412 * @param {function()} callback Callback run on task end.
1415 FileOperationManager.prototype.serviceDeleteTask_ = function(task, callback) {
1416 var queue = new AsyncUtil.Queue();
1418 // Delete each entry.
1420 var deleteOneEntry = function(inCallback) {
1421 if (!task.entries.length || task.cancelRequested || error) {
1425 this.eventRouter_.sendDeleteEvent('PROGRESS', task);
1426 util.removeFileOrDirectory(
1429 this.eventRouter_.sendEntryChangedEvent(
1430 util.EntryChangedKind.DELETED, task.entries[0]);
1431 task.processedBytes += task.entrySize[task.entries[0].toURL()];
1432 task.entries.shift();
1433 deleteOneEntry(inCallback);
1440 queue.run(deleteOneEntry);
1442 // Send an event and finish the async steps.
1443 queue.run(function(inCallback) {
1447 else if (task.cancelRequested)
1448 reason = 'CANCELED';
1451 this.eventRouter_.sendDeleteEvent(reason, task);
1458 * Creates a zip file for the selection of files.
1460 * @param {!DirectoryEntry} dirEntry The directory containing the selection.
1461 * @param {Array.<Entry>} selectionEntries The selected entries.
1463 FileOperationManager.prototype.zipSelection = function(
1464 dirEntry, selectionEntries) {
1465 var zipTask = new FileOperationManager.ZipTask(
1466 selectionEntries, dirEntry, dirEntry);
1467 zipTask.taskId = this.generateTaskId();
1469 this.eventRouter_.sendProgressEvent('BEGIN',
1470 zipTask.getStatus(),
1472 zipTask.initialize(function() {
1473 this.copyTasks_.push(zipTask);
1474 if (this.copyTasks_.length == 1)
1475 this.serviceAllTasks_();
1480 * Generates new task ID.
1482 * @return {string} New task ID.
1484 FileOperationManager.prototype.generateTaskId = function() {
1485 return 'file-operation-' + this.taskIdCounter_++;