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