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();
13 * @param {DirectoryEntry} root Root directory entry.
15 function FileCopyManager(root
) {
17 this.deleteTasks_
= [];
18 this.lastDeleteId_
= 0;
19 this.cancelObservers_
= [];
20 this.cancelRequested_
= false;
21 this.cancelCallback_
= null;
23 this.unloadTimeout_
= null;
25 window
.addEventListener('error', function(e
) {
26 this.log_('Unhandled error: ', e
.message
, e
.filename
+ ':' + e
.lineno
);
30 var fileCopyManagerInstance
= null;
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
;
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;
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_
= [];
83 * @param {Array.<Entry>} entries Entries.
84 * @param {Function} callback When entries resolved.
86 FileCopyManager
.Task
.prototype.setEntries = function(entries
, callback
) {
89 function onEntriesRecursed(result
) {
90 self
.pendingDirectories
= result
.dirEntries
;
91 self
.pendingFiles
= result
.fileEntries
;
92 self
.pendingBytes
= result
.fileBytes
;
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];
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();
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
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
);
188 * Error class used to report problems with a copy operation.
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
];
200 FileCopyManager
.Error
.CANCELLED
= 0;
202 FileCopyManager
.Error
.UNEXPECTED_SOURCE_FILE
= 1;
204 FileCopyManager
.Error
.TARGET_EXISTS
= 2;
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() {
227 pendingItems
: 0, // Files + Directories
229 pendingDirectories
: 0,
232 completedItems
: 0, // Files + Directories
234 completedDirectories
: 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
;
259 rv
.pendingZips
+= pendingFiles
+ pendingDirectories
;
260 } else if (task
.move || task
.deleteAfterCopy
) {
261 rv
.pendingMoves
+= pendingFiles
+ pendingDirectories
;
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
;
289 * Send an event to all the FileManager windows.
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
++) {
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
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.
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.
338 * @param {string} reason Event type.
339 * @param {FileCopyManager.Error} opt_err Error.
341 FileCopyManager
.prototype.sendProgressEvent_ = function(reason
, opt_err
) {
343 event
.reason
= reason
;
345 event
.error
= opt_err
;
346 this.sendEvent_('copy-progress', event
);
350 * Dispatch an event of file operation completion (allows to update the UI).
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
,
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.
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_();
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)
395 * Perform the bookkeeping required to cancel.
398 FileCopyManager
.prototype.doCancel_ = function() {
400 this.cancelRequested_
= false;
401 this.sendProgressEvent_('CANCELLED');
405 * Used internally to check if a cancel has been requested, and handle
408 * @return {boolean} If canceled.
410 FileCopyManager
.prototype.maybeCancel_ = function() {
411 if (!this.cancelRequested_
)
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
,
428 sourceDirEntry
: null,
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
,
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.
458 results
.entries
.push(entry
);
465 results
.sourceDirEntry
= dirEntry
;
466 var directories
= [];
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
;
481 results
.isCut
= (clipboard
.isCut
== 'true');
482 results
.isOnGData
= (clipboard
.isOnGData
== 'true');
484 util
.getDirectories(self
.root_
, {create
: false}, directories
, onEntryFound
,
486 util
.getFiles(self
.root_
, {create
: false}, files
, onEntryFound
,
490 if (clipboard
.sourceDir
) {
491 this.root_
.getDirectory(clipboard
.sourceDir
,
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
,
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
,
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;
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_();
552 // Force to update the progress of butter bar when there are new tasks
553 // coming while servicing current task.
554 self
.sendProgressEvent_('PROGRESS');
562 * Service all pending tasks, as well as any that might appear during the
566 FileCopyManager
.prototype.serviceAllTasks_ = function() {
569 function onTaskError(err
) {
570 if (self
.maybeCancel_())
572 self
.sendProgressEvent_('ERROR', err
);
576 function onTaskSuccess(task
) {
577 if (self
.maybeCancel_())
579 if (!self
.copyTasks_
.length
) {
580 // All tasks have been serviced, clean up and exit.
581 self
.sendProgressEvent_('SUCCESS');
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.
605 * @param {Function} successCallback On success.
606 * @param {Function} errorCallback On error.
608 FileCopyManager
.prototype.serviceNextTask_ = function(
609 successCallback
, errorCallback
) {
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
]);
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
643 if (task
.pendingDirectories
.length
+ task
.pendingFiles
.length
== 0) {
644 if (task
.deleteAfterCopy
) {
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
);
662 this.serviceNextTaskEntry_(task
, onEntryServiced
, errorCallback
);
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.
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_())
682 var sourceEntry
= task
.getNextEntry();
685 // All entries in this task have been copied.
686 successCallback(null);
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
);
699 var targetDirEntry
= task
.targetDirEntry
;
700 var originalPath
= sourceEntry
.fullPath
.substr(sourcePath
.length
+ 1);
702 originalPath
= task
.applyRenames(originalPath
);
704 var targetRelativePrefix
= originalPath
;
706 var index
= targetRelativePrefix
.lastIndexOf('.');
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.
716 var match
= /^(.*?)(?: \((\d+)\))?$/.exec(targetRelativePrefix
);
717 if (match
&& match
[2]) {
718 copyNumber
= parseInt(match
[2], 10);
719 targetRelativePrefix
= match
[1];
722 var targetRelativePath
= '';
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
;
768 if (renameTries
< 10) {
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,
782 * |dirEntry| = DirectoryEntry('/root/dir1')
783 * |relativePath| = 'dir2/file'
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('/');
806 intermediatePath
= relativePath
.substr(0, index
);
807 fileName
= relativePath
.substr(index
+ 1);
810 if (intermediatePath
== '') {
811 successCallback(dirEntry
, fileName
);
813 dirEntry
.getDirectory(intermediatePath
,
816 successCallback(entry
, fileName
);
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
);
830 resolveDirAndBaseName(
831 targetDirEntry
, targetRelativePath
,
832 function(dirEntry
, fileName
) {
833 sourceEntry
.moveTo(dirEntry
, fileName
,
834 onFilesystemMoveComplete
.bind(self
, sourceEntry
),
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);
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
) {
910 sourceEntry
.copyTo(dirEntry
, fileName
, onSuccessTransfer
,
917 function onFileTransferCompleted() {
918 self
.cancelCallback_
= null;
919 if (chrome
.runtime
.lastError
) {
921 code
: chrome
.runtime
.lastError
.message
,
922 toGDrive
: task
.targetOnGData
,
923 sourceFileUrl
: sourceFileUrl
926 targetDirEntry
.getFile(targetRelativePath
, {}, onSuccessTransfer
,
931 self
.cancelCallback_ = function() {
932 self
.cancelCallback_
= null;
933 chrome
.fileBrowserPrivate
.onFileTransfersUpdated
.removeListener(
934 onFileTransfersUpdated
);
935 if (task
.sourceOnGData
) {
936 chrome
.fileBrowserPrivate
.cancelFileTransfers([sourceFileUrl
],
939 chrome
.fileBrowserPrivate
.cancelFileTransfers([targetFileUrl
],
944 // TODO(benchan): Until DriveFileSystem supports FileWriter, we use the
945 // transferFile API to copy files into or out from a gdata file system.
947 chrome
.fileBrowserPrivate
.transferFile(
948 sourceFileUrl
, targetFileUrl
, onFileTransferCompleted
);
952 if (sourceEntry
.isDirectory
) {
953 targetDirEntry
.getDirectory(
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
,
965 targetDirEntry
.getFile(
967 {create
: true, exclusive
: true},
968 function(targetEntry
) {
969 self
.copyEntry_(sourceEntry
, targetEntry
,
970 onCopyProgress
, onCopyComplete
, onError
);
972 util
.flog('Error getting file: ' + targetRelativePath
,
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
);
994 * Service a zip file creation task.
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
,
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
));
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
;
1036 onError('TARGET_EXISTS', firstExistingEntry
);
1040 function onTargetNotResolved() {
1041 function onZipSelectionComplete(success
) {
1043 self
.sendProgressEvent_('SUCCESS');
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() {
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
);
1070 * Copy the contents of sourceEntry into targetEntry.
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
1085 FileCopyManager
.prototype.copyEntry_ = function(sourceEntry
,
1090 if (this.maybeCancel_())
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.
1108 // |progress.loaded| will contain total amount of data copied by now.
1109 // |progressCallback| expects data amount delta from the last progress
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
);
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_
;
1147 timeout
: setTimeout(this.forceDeleteTask
.bind(this, id
),
1148 FileCopyManager
.DELETE_TIMEOUT
)
1150 this.deleteTasks_
.push(task
);
1151 this.maybeScheduleCloseBackgroundPage_();
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
,
1165 var zipTask
= new FileCopyManager
.Task(dirEntry
, dirEntry
);
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_();
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.
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_();
1217 clearTimeout(task
.timeout
);
1218 task
.timeout
= null;
1227 * Performs the deletion.
1228 * @param {object} task The delete task (see deleteEntries function).
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');
1239 for (var i
= 0; i
< task
.entries
.length
; i
++) {
1240 var entry
= task
.entries
[i
];
1241 util
.removeFileOrDirectory(
1244 onComplete
); // We ignore error, because we can't do anything here.
1250 * Send a 'delete' event to listeners.
1251 * @param {Object} task The delete task (see deleteEntries function).
1252 * @param {string} reason Event reason.
1255 FileCopyManager
.prototype.sendDeleteEvent_ = function(task
, reason
) {
1256 this.sendEvent_('delete', {
1259 urls
: task
.entries
.map(function(e
) {
1260 return util
.makeFilesystemUrl(e
.fullPath
);