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