Updated drag and drop thumbnails.
[chromium-blink-merge.git] / chrome / browser / resources / file_manager / js / file_copy_manager.js
bloba5fb156c7eb2db7b0f639b2b7ad66d6512a30ae5
1 // Copyright (c) 2012 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.
5 if (chrome.extension) {
6 function getContentWindows() {
7 return chrome.extension.getViews();
11 /**
12 * @constructor
13 * @param {DirectoryEntry} root Root directory entry.
15 function FileCopyManager(root) {
16 this.copyTasks_ = [];
17 this.deleteTasks_ = [];
18 this.lastDeleteId_ = 0;
19 this.cancelObservers_ = [];
20 this.cancelRequested_ = false;
21 this.cancelCallback_ = null;
22 this.root_ = root;
23 this.unloadTimeout_ = null;
25 window.addEventListener('error', function(e) {
26 this.log_('Unhandled error: ', e.message, e.filename + ':' + e.lineno);
27 }.bind(this));
30 var fileCopyManagerInstance = null;
32 /**
33 * Get FileCopyManager instance. In case is hasn't been initialized, a new
34 * instance is created.
35 * @param {DirectoryEntry} root Root entry.
36 * @return {FileCopyManager} A FileCopyManager instance.
38 FileCopyManager.getInstance = function(root) {
39 if (fileCopyManagerInstance === null) {
40 fileCopyManagerInstance = new FileCopyManager(root);
42 return fileCopyManagerInstance;
45 /**
46 * A record of a queued copy operation.
48 * Multiple copy operations may be queued at any given time. Additional
49 * Tasks may be added while the queue is being serviced. Though a
50 * cancel operation cancels everything in the queue.
52 * @param {DirectoryEntry} sourceDirEntry Source directory.
53 * @param {DirectoryEntry} targetDirEntry Target directory.
55 FileCopyManager.Task = function(sourceDirEntry, targetDirEntry) {
56 this.sourceDirEntry = sourceDirEntry;
57 this.targetDirEntry = targetDirEntry;
58 this.originalEntries = null;
60 this.pendingDirectories = [];
61 this.pendingFiles = [];
62 this.pendingBytes = 0;
64 this.completedDirectories = [];
65 this.completedFiles = [];
66 this.completedBytes = 0;
68 this.deleteAfterCopy = false;
69 this.move = false;
70 this.zip = false;
71 this.sourceOnGData = false;
72 this.targetOnGData = false;
74 // If directory already exists, we try to make a copy named 'dir (X)',
75 // where X is a number. When we do this, all subsequent copies from
76 // inside the subtree should be mapped to the new directory name.
77 // For example, if 'dir' was copied as 'dir (1)', then 'dir\file.txt' should
78 // become 'dir (1)\file.txt'.
79 this.renamedDirectories_ = [];
82 /**
83 * @param {Array.<Entry>} entries Entries.
84 * @param {Function} callback When entries resolved.
86 FileCopyManager.Task.prototype.setEntries = function(entries, callback) {
87 var self = this;
89 function onEntriesRecursed(result) {
90 self.pendingDirectories = result.dirEntries;
91 self.pendingFiles = result.fileEntries;
92 self.pendingBytes = result.fileBytes;
93 callback();
96 this.originalEntries = entries;
97 // When moving directories, FileEntry.moveTo() is used if both source
98 // and target are on GData. There is no need to recurse into directories.
99 var recurse = !this.move;
100 util.recurseAndResolveEntries(entries, recurse, onEntriesRecursed);
104 * @return {Entry} Next entry.
106 FileCopyManager.Task.prototype.getNextEntry = function() {
107 // We should keep the file in pending list and remove it after complete.
108 // Otherwise, if we try to get status in the middle of copying. The returned
109 // status is wrong (miss count the pasting item in totalItems).
110 if (this.pendingDirectories.length) {
111 this.pendingDirectories[0].inProgress = true;
112 return this.pendingDirectories[0];
115 if (this.pendingFiles.length) {
116 this.pendingFiles[0].inProgress = true;
117 return this.pendingFiles[0];
120 return null;
124 * @param {Entry} entry Entry.
125 * @param {number} size Bytes completed.
127 FileCopyManager.Task.prototype.markEntryComplete = function(entry, size) {
128 // It is probably not safe to directly remove the first entry in pending list.
129 // We need to check if the removed entry (srcEntry) corresponding to the added
130 // entry (target entry).
131 if (entry.isDirectory && this.pendingDirectories &&
132 this.pendingDirectories[0].inProgress) {
133 this.completedDirectories.push(entry);
134 this.pendingDirectories.shift();
135 } else if (this.pendingFiles && this.pendingFiles[0].inProgress) {
136 this.completedFiles.push(entry);
137 this.completedBytes += size;
138 this.pendingBytes -= size;
139 this.pendingFiles.shift();
140 } else {
141 throw new Error('Try to remove a source entry which is not correspond to' +
142 ' the finished target entry');
147 * Updates copy progress status for the entry.
149 * @param {Entry} entry Entry which is being coppied.
150 * @param {number} size Number of bytes that has been copied since last update.
152 FileCopyManager.Task.prototype.updateFileCopyProgress = function(entry, size) {
153 if (entry.isFile && this.pendingFiles && this.pendingFiles[0].inProgress) {
154 this.completedBytes += size;
155 this.pendingBytes -= size;
160 * @param {string} fromName Old name.
161 * @param {string} toName New name.
163 FileCopyManager.Task.prototype.registerRename = function(fromName, toName) {
164 this.renamedDirectories_.push({from: fromName + '/', to: toName + '/'});
168 * @param {string} path A path.
169 * @return {string} Path after renames.
171 FileCopyManager.Task.prototype.applyRenames = function(path) {
172 // Directories are processed in pre-order, so we will store only the first
173 // renaming point:
174 // x -> x (1) -- new directory created.
175 // x\y -> x (1)\y -- no more renames inside the new directory, so
176 // this one will not be stored.
177 // x\y\a.txt -- only one rename will be applied.
178 for (var index = 0; index < this.renamedDirectories_.length; ++index) {
179 var rename = this.renamedDirectories_[index];
180 if (path.indexOf(rename.from) == 0) {
181 path = rename.to + path.substr(rename.from.length);
184 return path;
188 * Error class used to report problems with a copy operation.
189 * @constructor
190 * @param {string} reason Error type.
191 * @param {Object} data Additional data.
193 FileCopyManager.Error = function(reason, data) {
194 this.reason = reason;
195 this.code = FileCopyManager.Error[reason];
196 this.data = data;
199 /** @const */
200 FileCopyManager.Error.CANCELLED = 0;
201 /** @const */
202 FileCopyManager.Error.UNEXPECTED_SOURCE_FILE = 1;
203 /** @const */
204 FileCopyManager.Error.TARGET_EXISTS = 2;
205 /** @const */
206 FileCopyManager.Error.FILESYSTEM_ERROR = 3;
209 // FileCopyManager methods.
212 * Called before a new method is run in the manager. Prepares the manager's
213 * state for running a new method.
215 FileCopyManager.prototype.willRunNewMethod = function() {
216 // Cancel any pending close actions so the file copy manager doesn't go away.
217 if (this.unloadTimeout_)
218 clearTimeout(this.unloadTimeout_);
219 this.unloadTimeout_ = null;
223 * @return {Object} Status object.
225 FileCopyManager.prototype.getStatus = function() {
226 var rv = {
227 pendingItems: 0, // Files + Directories
228 pendingFiles: 0,
229 pendingDirectories: 0,
230 pendingBytes: 0,
232 completedItems: 0, // Files + Directories
233 completedFiles: 0,
234 completedDirectories: 0,
235 completedBytes: 0,
237 percentage: NaN,
238 pendingCopies: 0,
239 pendingMoves: 0,
240 pendingZips: 0,
241 filename: '' // In case pendingItems == 1
244 var pendingFile = null;
246 for (var i = 0; i < this.copyTasks_.length; i++) {
247 var task = this.copyTasks_[i];
248 var pendingFiles = task.pendingFiles.length;
249 var pendingDirectories = task.pendingDirectories.length;
250 rv.pendingFiles += pendingFiles;
251 rv.pendingDirectories += pendingDirectories;
252 rv.pendingBytes += task.pendingBytes;
254 rv.completedFiles += task.completedFiles.length;
255 rv.completedDirectories += task.completedDirectories.length;
256 rv.completedBytes += task.completedBytes;
258 if (task.zip) {
259 rv.pendingZips += pendingFiles + pendingDirectories;
260 } else if (task.move || task.deleteAfterCopy) {
261 rv.pendingMoves += pendingFiles + pendingDirectories;
262 } else {
263 rv.pendingCopies += pendingFiles + pendingDirectories;
266 if (task.pendingFiles.length === 1)
267 pendingFile = task.pendingFiles[0];
269 if (task.pendingDirectories.length === 1)
270 pendingFile = task.pendingDirectories[0];
273 rv.pendingItems = rv.pendingFiles + rv.pendingDirectories;
274 rv.completedItems = rv.completedFiles + rv.completedDirectories;
276 rv.totalFiles = rv.pendingFiles + rv.completedFiles;
277 rv.totalDirectories = rv.pendingDirectories + rv.completedDirectories;
278 rv.totalItems = rv.pendingItems + rv.completedItems;
279 rv.totalBytes = rv.pendingBytes + rv.completedBytes;
281 rv.percentage = rv.completedBytes / rv.totalBytes;
282 if (rv.pendingItems === 1)
283 rv.filename = pendingFile.name;
285 return rv;
289 * Send an event to all the FileManager windows.
290 * @private
291 * @param {string} eventName Event name.
292 * @param {Object} eventArgs An object with arbitrary event parameters.
294 FileCopyManager.prototype.sendEvent_ = function(eventName, eventArgs) {
295 if (this.cancelRequested_)
296 return; // Swallow events until cancellation complete.
298 eventArgs.status = this.getStatus();
300 var windows = getContentWindows();
301 for (var i = 0; i < windows.length; i++) {
302 var w = windows[i];
303 if (w.fileCopyManagerWrapper)
304 w.fileCopyManagerWrapper.onEvent(eventName, eventArgs);
309 * Unloads the host page in 5 secs of idleing. Need to be called
310 * each time this.copyTasks_.length or this.deleteTasks_.length
311 * changed.
312 * @private
314 FileCopyManager.prototype.maybeScheduleCloseBackgroundPage_ = function() {
315 if (this.copyTasks_.length === 0 && this.deleteTasks_.length === 0) {
316 if (this.unloadTimeout_ === null)
317 this.unloadTimeout_ = setTimeout(close, 5000);
318 } else if (this.unloadTimeout_) {
319 clearTimeout(this.unloadTimeout_);
320 this.unloadTimeout_ = null;
325 * Write to console.log on all the active FileManager windows.
326 * @private
328 FileCopyManager.prototype.log_ = function() {
329 var windows = getContentWindows();
330 for (var i = 0; i < windows.length; i++) {
331 windows[i].console.log.apply(windows[i].console, arguments);
336 * Dispatch a simple copy-progress event with reason and optional err data.
337 * @private
338 * @param {string} reason Event type.
339 * @param {FileCopyManager.Error} opt_err Error.
341 FileCopyManager.prototype.sendProgressEvent_ = function(reason, opt_err) {
342 var event = {};
343 event.reason = reason;
344 if (opt_err)
345 event.error = opt_err;
346 this.sendEvent_('copy-progress', event);
350 * Dispatch an event of file operation completion (allows to update the UI).
351 * @private
352 * @param {string} reason Completed file operation: 'movied|copied|deleted'.
353 * @param {Array.<Entry>} affectedEntries deleted ot created entries.
355 FileCopyManager.prototype.sendOperationEvent_ = function(reason,
356 affectedEntries) {
357 var event = {};
358 event.reason = reason;
359 event.affectedEntries = affectedEntries;
360 this.sendEvent_('copy-operation-complete', event);
364 * Completely clear out the copy queue, either because we encountered an error
365 * or completed successfully.
366 * @private
368 FileCopyManager.prototype.resetQueue_ = function() {
369 for (var i = 0; i < this.cancelObservers_.length; i++)
370 this.cancelObservers_[i]();
372 this.copyTasks_ = [];
373 this.cancelObservers_ = [];
374 this.maybeScheduleCloseBackgroundPage_();
378 * Request that the current copy queue be abandoned.
379 * @param {Function} opt_callback On cancel.
381 FileCopyManager.prototype.requestCancel = function(opt_callback) {
382 this.cancelRequested_ = true;
383 if (this.cancelCallback_)
384 this.cancelCallback_();
385 if (opt_callback)
386 this.cancelObservers_.push(opt_callback);
388 // If there is any active task it will eventually call maybeCancel_.
389 // Otherwise call it right now.
390 if (this.copyTasks_.length == 0)
391 this.doCancel_();
395 * Perform the bookkeeping required to cancel.
396 * @private
398 FileCopyManager.prototype.doCancel_ = function() {
399 this.resetQueue_();
400 this.cancelRequested_ = false;
401 this.sendProgressEvent_('CANCELLED');
405 * Used internally to check if a cancel has been requested, and handle
406 * it if so.
407 * @private
408 * @return {boolean} If canceled.
410 FileCopyManager.prototype.maybeCancel_ = function() {
411 if (!this.cancelRequested_)
412 return false;
414 this.doCancel_();
415 return true;
419 * Convert string in clipboard to entries and kick off pasting.
420 * @param {Object} clipboard Clipboard contents.
421 * @param {string} targetPath Target path.
422 * @param {boolean} targetOnGData If target is on GDrive.
424 FileCopyManager.prototype.paste = function(clipboard, targetPath,
425 targetOnGData) {
426 var self = this;
427 var results = {
428 sourceDirEntry: null,
429 entries: [],
430 isCut: false,
431 isOnGData: false
434 function onPathError(err) {
435 self.sendProgressEvent_('ERROR',
436 new FileCopyManager.Error('FILESYSTEM_ERROR', err));
439 function onSourceEntryFound(dirEntry) {
440 function onTargetEntryFound(targetEntry) {
441 self.queueCopy(results.sourceDirEntry,
442 targetEntry,
443 results.entries,
444 results.isCut,
445 results.isOnGData,
446 targetOnGData);
449 function onComplete() {
450 self.root_.getDirectory(targetPath, {},
451 onTargetEntryFound, onPathError);
454 function onEntryFound(entry) {
455 // When getDirectories/getFiles finish, they call addEntry with null.
456 // We don't want to add null to our entries.
457 if (entry != null) {
458 results.entries.push(entry);
459 added++;
460 if (added == total)
461 onComplete();
465 results.sourceDirEntry = dirEntry;
466 var directories = [];
467 var files = [];
469 if (clipboard.directories) {
470 directories = clipboard.directories.split('\n');
471 directories = directories.filter(function(d) { return d != '' });
473 if (clipboard.files) {
474 files = clipboard.files.split('\n');
475 files = files.filter(function(f) { return f != '' });
478 var total = directories.length + files.length;
479 var added = 0;
481 results.isCut = (clipboard.isCut == 'true');
482 results.isOnGData = (clipboard.isOnGData == 'true');
484 util.getDirectories(self.root_, {create: false}, directories, onEntryFound,
485 onPathError);
486 util.getFiles(self.root_, {create: false}, files, onEntryFound,
487 onPathError);
490 if (clipboard.sourceDir) {
491 this.root_.getDirectory(clipboard.sourceDir,
492 {create: false},
493 onSourceEntryFound,
494 onPathError);
495 } else {
496 onSourceEntryFound(null);
501 * Checks if source and target are on the same root.
503 * @param {DirectoryEntry} sourceEntry An entry from the source.
504 * @param {DirectoryEntry} targetDirEntry Directory entry for the target.
505 * @param {boolean} targetOnGData If target is on GDrive.
506 * @return {boolean} Whether source and target dir are on the same root.
508 FileCopyManager.prototype.isOnSameRoot = function(sourceEntry,
509 targetDirEntry,
510 targetOnGData) {
511 return PathUtil.getRootPath(sourceEntry.fullPath) ==
512 PathUtil.getRootPath(targetDirEntry.fullPath);
516 * Initiate a file copy.
517 * @param {DirectoryEntry} sourceDirEntry Source directory.
518 * @param {DirectoryEntry} targetDirEntry Target directory.
519 * @param {Array.<Entry>} entries Entries to copy.
520 * @param {boolean} deleteAfterCopy In case of move.
521 * @param {boolean} sourceOnGData Source directory on GDrive.
522 * @param {boolean} targetOnGData Target directory on GDrive.
523 * @return {FileCopyManager.Task} Copy task.
525 FileCopyManager.prototype.queueCopy = function(sourceDirEntry,
526 targetDirEntry,
527 entries,
528 deleteAfterCopy,
529 sourceOnGData,
530 targetOnGData) {
531 var self = this;
532 var copyTask = new FileCopyManager.Task(sourceDirEntry, targetDirEntry);
533 if (deleteAfterCopy) {
534 // |sourecDirEntry| may be null, so let's check the root for the first of
535 // the entries scheduled to be copied.
536 if (this.isOnSameRoot(entries[0], targetDirEntry)) {
537 copyTask.move = true;
538 } else {
539 copyTask.deleteAfterCopy = true;
542 copyTask.sourceOnGData = sourceOnGData;
543 copyTask.targetOnGData = targetOnGData;
544 copyTask.setEntries(entries, function() {
545 self.copyTasks_.push(copyTask);
546 self.maybeScheduleCloseBackgroundPage_();
547 if (self.copyTasks_.length == 1) {
548 // Assume self.cancelRequested_ == false.
549 // This moved us from 0 to 1 active tasks, let the servicing begin!
550 self.serviceAllTasks_();
551 } else {
552 // Force to update the progress of butter bar when there are new tasks
553 // coming while servicing current task.
554 self.sendProgressEvent_('PROGRESS');
558 return copyTask;
562 * Service all pending tasks, as well as any that might appear during the
563 * copy.
564 * @private
566 FileCopyManager.prototype.serviceAllTasks_ = function() {
567 var self = this;
569 function onTaskError(err) {
570 if (self.maybeCancel_())
571 return;
572 self.sendProgressEvent_('ERROR', err);
573 self.resetQueue_();
576 function onTaskSuccess(task) {
577 if (self.maybeCancel_())
578 return;
579 if (!self.copyTasks_.length) {
580 // All tasks have been serviced, clean up and exit.
581 self.sendProgressEvent_('SUCCESS');
582 self.resetQueue_();
583 return;
586 // We want to dispatch a PROGRESS event when there are more tasks to serve
587 // right after one task finished in the queue. We treat all tasks as one
588 // big task logically, so there is only one BEGIN/SUCCESS event pair for
589 // these continuous tasks.
590 self.sendProgressEvent_('PROGRESS');
592 self.serviceNextTask_(onTaskSuccess, onTaskError);
595 // If the queue size is 1 after pushing our task, it was empty before,
596 // so we need to kick off queue processing and dispatch BEGIN event.
598 this.sendProgressEvent_('BEGIN');
599 this.serviceNextTask_(onTaskSuccess, onTaskError);
603 * Service all entries in the next copy task.
604 * @private
605 * @param {Function} successCallback On success.
606 * @param {Function} errorCallback On error.
608 FileCopyManager.prototype.serviceNextTask_ = function(
609 successCallback, errorCallback) {
610 var self = this;
611 var task = this.copyTasks_[0];
613 function onFilesystemError(err) {
614 errorCallback(new FileCopyManager.Error('FILESYSTEM_ERROR', err));
617 function onTaskComplete() {
618 self.copyTasks_.shift();
619 self.maybeScheduleCloseBackgroundPage_();
620 successCallback(task);
623 function deleteOriginals() {
624 var count = task.originalEntries.length;
626 function onEntryDeleted(entry) {
627 self.sendOperationEvent_('deleted', [entry]);
628 count--;
629 if (!count)
630 onTaskComplete();
633 for (var i = 0; i < task.originalEntries.length; i++) {
634 var entry = task.originalEntries[i];
635 util.removeFileOrDirectory(
636 entry, onEntryDeleted.bind(self, entry), onFilesystemError);
640 function onEntryServiced(targetEntry, size) {
641 // We should not dispatch a PROGRESS event when there is no pending items
642 // in the task.
643 if (task.pendingDirectories.length + task.pendingFiles.length == 0) {
644 if (task.deleteAfterCopy) {
645 deleteOriginals();
646 } else {
647 onTaskComplete();
649 return;
652 self.sendProgressEvent_('PROGRESS');
654 // We yield a few ms between copies to give the browser a chance to service
655 // events (like perhaps the user clicking to cancel the copy, for example).
656 setTimeout(function() {
657 self.serviceNextTaskEntry_(task, onEntryServiced, errorCallback);
658 }, 10);
661 if (!task.zip)
662 this.serviceNextTaskEntry_(task, onEntryServiced, errorCallback);
663 else
664 this.serviceZipTask_(task, onTaskComplete, errorCallback);
668 * Service the next entry in a given task.
669 * TODO(olege): Refactor this method into a separate class.
671 * @private
672 * @param {FileManager.Task} task A task.
673 * @param {Function} successCallback On success.
674 * @param {Function} errorCallback On error.
676 FileCopyManager.prototype.serviceNextTaskEntry_ = function(
677 task, successCallback, errorCallback) {
678 if (this.maybeCancel_())
679 return;
681 var self = this;
682 var sourceEntry = task.getNextEntry();
684 if (!sourceEntry) {
685 // All entries in this task have been copied.
686 successCallback(null);
687 return;
690 // |sourceEntry.originalSourcePath| is set in util.recurseAndResolveEntries.
691 var sourcePath = sourceEntry.originalSourcePath;
692 if (sourceEntry.fullPath.substr(0, sourcePath.length) != sourcePath) {
693 // We found an entry in the list that is not relative to the base source
694 // path, something is wrong.
695 onError('UNEXPECTED_SOURCE_FILE', sourceEntry.fullPath);
696 return;
699 var targetDirEntry = task.targetDirEntry;
700 var originalPath = sourceEntry.fullPath.substr(sourcePath.length + 1);
702 originalPath = task.applyRenames(originalPath);
704 var targetRelativePrefix = originalPath;
705 var targetExt = '';
706 var index = targetRelativePrefix.lastIndexOf('.');
707 if (index != -1) {
708 targetExt = targetRelativePrefix.substr(index);
709 targetRelativePrefix = targetRelativePrefix.substr(0, index);
712 // If file already exists, we try to make a copy named 'file (1).ext'.
713 // If file is already named 'file (X).ext', we go with 'file (X+1).ext'.
714 // If new name is still occupied, we increase the number up to 10 times.
715 var copyNumber = 0;
716 var match = /^(.*?)(?: \((\d+)\))?$/.exec(targetRelativePrefix);
717 if (match && match[2]) {
718 copyNumber = parseInt(match[2], 10);
719 targetRelativePrefix = match[1];
722 var targetRelativePath = '';
723 var renameTries = 0;
724 var firstExistingEntry = null;
726 function onCopyCompleteBase(entry, size) {
727 task.markEntryComplete(entry, size);
728 successCallback(entry, size);
731 function onCopyComplete(entry, size) {
732 self.sendOperationEvent_('copied', [entry]);
733 onCopyCompleteBase(entry, size);
736 function onCopyProgress(entry, size) {
737 task.updateFileCopyProgress(entry, size);
738 self.sendProgressEvent_('PROGRESS');
741 function onError(reason, data) {
742 self.log_('serviceNextTaskEntry error: ' + reason + ':', data);
743 errorCallback(new FileCopyManager.Error(reason, data));
746 function onFilesystemCopyComplete(sourceEntry, targetEntry) {
747 // TODO(benchan): We currently do not know the size of data being
748 // copied by FileEntry.copyTo(), so task.completedBytes will not be
749 // increased. We will address this issue once we need to use
750 // task.completedBytes to track the progress.
751 self.sendOperationEvent_('copied', [sourceEntry, targetEntry]);
752 onCopyCompleteBase(targetEntry, 0);
755 function onFilesystemMoveComplete(sourceEntry, targetEntry) {
756 self.sendOperationEvent_('moved', [sourceEntry, targetEntry]);
757 onCopyCompleteBase(targetEntry, 0);
760 function onFilesystemError(err) {
761 onError('FILESYSTEM_ERROR', err);
764 function onTargetExists(existingEntry) {
765 if (!firstExistingEntry)
766 firstExistingEntry = existingEntry;
767 renameTries++;
768 if (renameTries < 10) {
769 copyNumber++;
770 tryNextCopy();
771 } else {
772 onError('TARGET_EXISTS', firstExistingEntry);
777 * Resolves the immediate parent directory entry and the file name of a
778 * given path, where the path is specified by a directory (not necessarily
779 * the immediate parent) and a path (not necessarily the file name) related
780 * to that directory. For instance,
781 * Given:
782 * |dirEntry| = DirectoryEntry('/root/dir1')
783 * |relativePath| = 'dir2/file'
785 * Return:
786 * |parentDirEntry| = DirectoryEntry('/root/dir1/dir2')
787 * |fileName| = 'file'
789 * @param {DirectoryEntry} dirEntry A directory entry.
790 * @param {string} relativePath A path relative to |dirEntry|.
791 * @param {function(Entry,string)} successCallback A callback for returning
792 * the |parentDirEntry| and |fileName| upon success.
793 * @param {function(FileError)} errorCallback An error callback when there is
794 * an error getting |parentDirEntry|.
796 function resolveDirAndBaseName(dirEntry, relativePath,
797 successCallback, errorCallback) {
798 // |intermediatePath| contains the intermediate path components
799 // that are appended to |dirEntry| to form |parentDirEntry|.
800 var intermediatePath = '';
801 var fileName = relativePath;
803 // Extract the file name component from |relativePath|.
804 var index = relativePath.lastIndexOf('/');
805 if (index != -1) {
806 intermediatePath = relativePath.substr(0, index);
807 fileName = relativePath.substr(index + 1);
810 if (intermediatePath == '') {
811 successCallback(dirEntry, fileName);
812 } else {
813 dirEntry.getDirectory(intermediatePath,
814 {create: false},
815 function(entry) {
816 successCallback(entry, fileName);
818 errorCallback);
822 function onTargetNotResolved(err) {
823 // We expect to be unable to resolve the target file, since we're going
824 // to create it during the copy. However, if the resolve fails with
825 // anything other than NOT_FOUND, that's trouble.
826 if (err.code != FileError.NOT_FOUND_ERR)
827 return onError('FILESYSTEM_ERROR', err);
829 if (task.move) {
830 resolveDirAndBaseName(
831 targetDirEntry, targetRelativePath,
832 function(dirEntry, fileName) {
833 sourceEntry.moveTo(dirEntry, fileName,
834 onFilesystemMoveComplete.bind(self, sourceEntry),
835 onFilesystemError);
837 onFilesystemError);
838 return;
841 // TODO(benchan): DriveFileSystem has not implemented directory copy,
842 // and thus we only call FileEntry.copyTo() for files. Revisit this
843 // code when DriveFileSystem supports directory copy.
844 if (sourceEntry.isFile && (task.sourceOnGData || task.targetOnGData)) {
845 var sourceFileUrl = sourceEntry.toURL();
846 var targetFileUrl = targetDirEntry.toURL() + '/' +
847 encodeURIComponent(targetRelativePath);
848 var transferedBytes = 0;
850 function onStartTransfer() {
851 chrome.fileBrowserPrivate.onFileTransfersUpdated.addListener(
852 onFileTransfersUpdated);
855 function onFailTransfer(err) {
856 chrome.fileBrowserPrivate.onFileTransfersUpdated.removeListener(
857 onFileTransfersUpdated);
859 self.log_('Error copying ' + sourceFileUrl + ' to ' + targetFileUrl);
860 onFilesystemError(err);
863 function onSuccessTransfer(targetEntry) {
864 chrome.fileBrowserPrivate.onFileTransfersUpdated.removeListener(
865 onFileTransfersUpdated);
867 targetEntry.getMetadata(function(metadata) {
868 if (metadata.size > transferedBytes)
869 onCopyProgress(sourceEntry, metadata.size - transferedBytes);
870 onFilesystemCopyComplete(sourceEntry, targetEntry);
874 var downTransfer = 0;
875 function onFileTransfersUpdated(statusList) {
876 for (var i = 0; i < statusList.length; i++) {
877 var s = statusList[i];
878 if (s.fileUrl == sourceFileUrl || s.fileUrl == targetFileUrl) {
879 var processed = s.processed;
881 // It becomes tricky when both the sides are on Drive.
882 // Currently, it is implemented by download followed by upload.
883 // Note, however, download will not happen if the file is cached.
884 if (task.sourceOnGData && task.targetOnGData) {
885 if (s.fileUrl == sourceFileUrl) {
886 // Download transfer is detected. Let's halve the progress.
887 downTransfer = processed = (s.processed >> 1);
888 } else {
889 // If download transfer has been detected, the upload transfer
890 // is stacked on top of it after halving. Otherwise, just use
891 // the upload transfer as-is.
892 processed = downTransfer > 0 ?
893 downTransfer + (s.processed >> 1) : s.processed;
897 if (processed > transferedBytes) {
898 onCopyProgress(sourceEntry, processed - transferedBytes);
899 transferedBytes = processed;
905 if (task.sourceOnGData && task.targetOnGData) {
906 resolveDirAndBaseName(
907 targetDirEntry, targetRelativePath,
908 function(dirEntry, fileName) {
909 onStartTransfer();
910 sourceEntry.copyTo(dirEntry, fileName, onSuccessTransfer,
911 onFailTransfer);
913 onFilesystemError);
914 return;
917 function onFileTransferCompleted() {
918 self.cancelCallback_ = null;
919 if (chrome.runtime.lastError) {
920 onFailTransfer({
921 code: chrome.runtime.lastError.message,
922 toGDrive: task.targetOnGData,
923 sourceFileUrl: sourceFileUrl
925 } else {
926 targetDirEntry.getFile(targetRelativePath, {}, onSuccessTransfer,
927 onFailTransfer);
931 self.cancelCallback_ = function() {
932 self.cancelCallback_ = null;
933 chrome.fileBrowserPrivate.onFileTransfersUpdated.removeListener(
934 onFileTransfersUpdated);
935 if (task.sourceOnGData) {
936 chrome.fileBrowserPrivate.cancelFileTransfers([sourceFileUrl],
937 function() {});
938 } else {
939 chrome.fileBrowserPrivate.cancelFileTransfers([targetFileUrl],
940 function() {});
944 // TODO(benchan): Until DriveFileSystem supports FileWriter, we use the
945 // transferFile API to copy files into or out from a gdata file system.
946 onStartTransfer();
947 chrome.fileBrowserPrivate.transferFile(
948 sourceFileUrl, targetFileUrl, onFileTransferCompleted);
949 return;
952 if (sourceEntry.isDirectory) {
953 targetDirEntry.getDirectory(
954 targetRelativePath,
955 {create: true, exclusive: true},
956 function(targetEntry) {
957 if (targetRelativePath != originalPath) {
958 task.registerRename(originalPath, targetRelativePath);
960 onCopyComplete(targetEntry);
962 util.flog('Error getting dir: ' + targetRelativePath,
963 onFilesystemError));
964 } else {
965 targetDirEntry.getFile(
966 targetRelativePath,
967 {create: true, exclusive: true},
968 function(targetEntry) {
969 self.copyEntry_(sourceEntry, targetEntry,
970 onCopyProgress, onCopyComplete, onError);
972 util.flog('Error getting file: ' + targetRelativePath,
973 onFilesystemError));
977 function tryNextCopy() {
978 targetRelativePath = targetRelativePrefix;
979 if (copyNumber > 0) {
980 targetRelativePath += ' (' + copyNumber + ')';
982 targetRelativePath += targetExt;
984 // Check to see if the target exists. This kicks off the rest of the copy
985 // if the target is not found, or raises an error if it does.
986 util.resolvePath(targetDirEntry, targetRelativePath, onTargetExists,
987 onTargetNotResolved);
990 tryNextCopy();
994 * Service a zip file creation task.
996 * @private
997 * @param {FileManager.Task} task A task.
998 * @param {Function} completeCallback On complete.
999 * @param {Function} errorCallback On error.
1001 FileCopyManager.prototype.serviceZipTask_ = function(task, completeCallback,
1002 errorCallback) {
1003 var self = this;
1004 var dirURL = task.sourceDirEntry.toURL();
1005 var selectionURLs = [];
1006 for (var i = 0; i < task.pendingDirectories.length; i++)
1007 selectionURLs.push(task.pendingDirectories[i].toURL());
1008 for (var i = 0; i < task.pendingFiles.length; i++)
1009 selectionURLs.push(task.pendingFiles[i].toURL());
1011 var destName = 'Archive';
1012 if (task.originalEntries.length == 1) {
1013 var entryPath = task.originalEntries[0].fullPath;
1014 var i = entryPath.lastIndexOf('/');
1015 var basename = (i < 0) ? entryPath : entryPath.substr(i + 1);
1016 i = basename.lastIndexOf('.');
1017 destName = ((i < 0) ? basename : basename.substr(0, i));
1020 var copyNumber = 0;
1021 var firstExistingEntry = null;
1022 var destPath = destName + '.zip';
1024 function onError(reason, data) {
1025 self.log_('serviceZipTask error: ' + reason + ':', data);
1026 errorCallback(new FileCopyManager.Error(reason, data));
1029 function onTargetExists(existingEntry) {
1030 if (copyNumber < 10) {
1031 if (!firstExistingEntry)
1032 firstExistingEntry = existingEntry;
1033 copyNumber++;
1034 tryZipSelection();
1035 } else {
1036 onError('TARGET_EXISTS', firstExistingEntry);
1040 function onTargetNotResolved() {
1041 function onZipSelectionComplete(success) {
1042 if (success) {
1043 self.sendProgressEvent_('SUCCESS');
1044 } else {
1045 self.sendProgressEvent_('ERROR',
1046 new FileCopyManager.Error('FILESYSTEM_ERROR', ''));
1048 completeCallback(task);
1051 self.sendProgressEvent_('PROGRESS');
1052 chrome.fileBrowserPrivate.zipSelection(dirURL, selectionURLs, destPath,
1053 onZipSelectionComplete);
1056 function tryZipSelection() {
1057 if (copyNumber > 0)
1058 destPath = destName + ' (' + copyNumber + ').zip';
1060 // Check if the target exists. This kicks off the rest of the zip file
1061 // creation if the target is not found, or raises an error if it does.
1062 util.resolvePath(task.targetDirEntry, destPath, onTargetExists,
1063 onTargetNotResolved);
1066 tryZipSelection();
1070 * Copy the contents of sourceEntry into targetEntry.
1072 * @private
1073 * @param {Entry} sourceEntry entry that will be copied.
1074 * @param {Entry} targetEntry entry to which sourceEntry will be copied.
1075 * @param {function(Entry, number)} progressCallback function that will be
1076 * called when a part of the source entry is copied. It takes |targetEntry|
1077 * and size of the last copied chunk as parameters.
1078 * @param {function(Entry, number)} successCallback function that will be called
1079 * the copy operation finishes. It takes |targetEntry| and size of the last
1080 * (not previously reported) copied chunk as parameters.
1081 * @param {function(string, object)} errorCallback function that will be called
1082 * if an error is encountered. Takes error type and additional error data
1083 * as parameters.
1085 FileCopyManager.prototype.copyEntry_ = function(sourceEntry,
1086 targetEntry,
1087 progressCallback,
1088 successCallback,
1089 errorCallback) {
1090 if (this.maybeCancel_())
1091 return;
1093 var self = this;
1095 function onSourceFileFound(file) {
1096 function onWriterCreated(writer) {
1097 var reportedProgress = 0;
1098 writer.onerror = function(progress) {
1099 errorCallback('FILESYSTEM_ERROR', writer.error);
1102 writer.onprogress = function(progress) {
1103 if (self.maybeCancel_()) {
1104 // If the copy was cancelled, we should abort the operation.
1105 writer.abort();
1106 return;
1108 // |progress.loaded| will contain total amount of data copied by now.
1109 // |progressCallback| expects data amount delta from the last progress
1110 // update.
1111 progressCallback(targetEntry, progress.loaded - reportedProgress);
1112 reportedProgress = progress.loaded;
1115 writer.onwriteend = function() {
1116 sourceEntry.getMetadata(function(metadata) {
1117 chrome.fileBrowserPrivate.setLastModified(targetEntry.toURL(),
1118 '' + Math.round(metadata.modificationTime.getTime() / 1000));
1119 successCallback(targetEntry, file.size - reportedProgress);
1123 writer.write(file);
1126 targetEntry.createWriter(onWriterCreated, errorCallback);
1129 sourceEntry.file(onSourceFileFound, errorCallback);
1133 * Timeout before files are really deleted (to allow undo).
1135 FileCopyManager.DELETE_TIMEOUT = 30 * 1000;
1138 * Schedules the files deletion.
1139 * @param {Array.<Entry>} entries The entries.
1140 * @param {function(number)} callback Callback gets the scheduled task id.
1142 FileCopyManager.prototype.deleteEntries = function(entries, callback) {
1143 var id = ++this.lastDeleteId_;
1144 var task = {
1145 entries: entries,
1146 id: id,
1147 timeout: setTimeout(this.forceDeleteTask.bind(this, id),
1148 FileCopyManager.DELETE_TIMEOUT)
1150 this.deleteTasks_.push(task);
1151 this.maybeScheduleCloseBackgroundPage_();
1152 callback(id);
1153 this.sendDeleteEvent_(task, 'SCHEDULED');
1157 * Creates a zip file for the selection of files.
1158 * @param {Entry} dirEntry the directory containing the selection.
1159 * @param {boolean} isOnGData If directory is on GDrive.
1160 * @param {Array.<Entry>} selectionEntries the selected entries.
1162 FileCopyManager.prototype.zipSelection = function(dirEntry, isOnGData,
1163 selectionEntries) {
1164 var self = this;
1165 var zipTask = new FileCopyManager.Task(dirEntry, dirEntry);
1166 zipTask.zip = true;
1167 zipTask.sourceOnGData = isOnGData;
1168 zipTask.targetOnGData = isOnGData;
1169 zipTask.setEntries(selectionEntries, function() {
1170 // TODO: per-entry zip progress update with accurate byte count.
1171 // For now just set pendingBytes to zero so that the progress bar is full.
1172 zipTask.pendingBytes = 0;
1173 self.copyTasks_.push(zipTask);
1174 if (self.copyTasks_.length == 1) {
1175 // Assume self.cancelRequested_ == false.
1176 // This moved us from 0 to 1 active tasks, let the servicing begin!
1177 self.serviceAllTasks_();
1178 } else {
1179 // Force to update the progress of butter bar when there are new tasks
1180 // coming while servicing current task.
1181 self.sendProgressEvent_('PROGRESS');
1187 * Force deletion before timeout runs out.
1188 * @param {number} id The delete task id (as returned by deleteEntries).
1190 FileCopyManager.prototype.forceDeleteTask = function(id) {
1191 var task = this.findDeleteTaskAndCancelTimeout_(id);
1192 if (task) this.serviceDeleteTask_(task);
1196 * Cancels the scheduled deletion.
1197 * @param {number} id The delete task id (as returned by deleteEntries).
1199 FileCopyManager.prototype.cancelDeleteTask = function(id) {
1200 var task = this.findDeleteTaskAndCancelTimeout_(id);
1201 if (task) this.sendDeleteEvent_(task, 'CANCELLED');
1205 * Finds the delete task, removes it from list and cancels the timeout.
1206 * @param {number} id The delete task id (as returned by deleteEntries).
1207 * @return {object} The delete task.
1208 * @private
1210 FileCopyManager.prototype.findDeleteTaskAndCancelTimeout_ = function(id) {
1211 for (var index = 0; index < this.deleteTasks_.length; index++) {
1212 var task = this.deleteTasks_[index];
1213 if (task.id == id) {
1214 this.deleteTasks_.splice(index, 1);
1215 this.maybeScheduleCloseBackgroundPage_();
1216 if (task.timeout) {
1217 clearTimeout(task.timeout);
1218 task.timeout = null;
1220 return task;
1223 return null;
1227 * Performs the deletion.
1228 * @param {object} task The delete task (see deleteEntries function).
1229 * @private
1231 FileCopyManager.prototype.serviceDeleteTask_ = function(task) {
1232 var downcount = task.entries.length + 1;
1234 var onComplete = function() {
1235 if (--downcount == 0)
1236 this.sendDeleteEvent_(task, 'SUCCESS');
1237 }.bind(this);
1239 for (var i = 0; i < task.entries.length; i++) {
1240 var entry = task.entries[i];
1241 util.removeFileOrDirectory(
1242 entry,
1243 onComplete,
1244 onComplete); // We ignore error, because we can't do anything here.
1246 onComplete();
1250 * Send a 'delete' event to listeners.
1251 * @param {Object} task The delete task (see deleteEntries function).
1252 * @param {string} reason Event reason.
1253 * @private
1255 FileCopyManager.prototype.sendDeleteEvent_ = function(task, reason) {
1256 this.sendEvent_('delete', {
1257 reason: reason,
1258 id: task.id,
1259 urls: task.entries.map(function(e) {
1260 return util.makeFilesystemUrl(e.fullPath);