blob: 1a7b337b7b02412de569d22aae0b9138b4db8859 [file] [log] [blame]
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* Utilities for file operations.
*/
var fileOperationUtil = {};
/**
* Resolves a path to either a DirectoryEntry or a FileEntry, regardless of
* whether the path is a directory or file.
*
* @param {DirectoryEntry} root The root of the filesystem to search.
* @param {string} path The path to be resolved.
* @return {Promise} Promise fulfilled with the resolved entry, or rejected with
* FileError.
*/
fileOperationUtil.resolvePath = function(root, path) {
if (path === '' || path === '/')
return Promise.resolve(root);
return new Promise(root.getFile.bind(root, path, {create: false})).
catch(function(error) {
if (error.name === util.FileError.TYPE_MISMATCH_ERR) {
// Bah. It's a directory, ask again.
return new Promise(
root.getDirectory.bind(root, path, {create: false}));
} else {
return Promise.reject(error);
}
});
};
/**
* Checks if an entry exists at |relativePath| in |dirEntry|.
* If exists, tries to deduplicate the path by inserting parenthesized number,
* such as " (1)", before the extension. If it still exists, tries the
* deduplication again by increasing the number.
* For example, suppose "file.txt" is given, "file.txt", "file (1).txt",
* "file (2).txt", ... will be tried.
*
* @param {DirectoryEntry} dirEntry The target directory entry.
* @param {string} relativePath The path to be deduplicated.
* @param {function(string)=} opt_successCallback Callback run with the
* deduplicated path on success.
* @param {function(fileOperationUtil.Error)=} opt_errorCallback Callback run
* on error.
* @return {Promise} Promise fulfilled with available path.
*/
fileOperationUtil.deduplicatePath = function(
dirEntry, relativePath, opt_successCallback, opt_errorCallback) {
// Crack the path into three part. The parenthesized number (if exists) will
// be replaced by incremented number for retry. For example, suppose
// |relativePath| is "file (10).txt", the second check path will be
// "file (11).txt".
var match = /^(.*?)(?: \((\d+)\))?(\.[^.]*?)?$/.exec(relativePath);
var prefix = match[1];
var ext = match[3] || '';
// Check to see if the target exists.
var resolvePath = function(trialPath, copyNumber) {
return fileOperationUtil.resolvePath(dirEntry, trialPath).then(function() {
var newTrialPath = prefix + ' (' + copyNumber + ')' + ext;
return resolvePath(newTrialPath, copyNumber + 1);
}, function(error) {
// We expect to be unable to resolve the target file, since we're
// going to create it during the copy. However, if the resolve fails
// with anything other than NOT_FOUND, that's trouble.
if (error.name === util.FileError.NOT_FOUND_ERR)
return trialPath;
else
return Promise.reject(error);
});
};
var promise = resolvePath(relativePath, 1).catch(function(error) {
if (error instanceof Error)
return Promise.reject(error);
return Promise.reject(new fileOperationUtil.Error(
util.FileOperationErrorType.FILESYSTEM_ERROR, error));
});
if (opt_successCallback)
promise.then(opt_successCallback, opt_errorCallback);
return promise;
};
/**
* Traverses files/subdirectories of the given entry, and returns them.
* In addition, this method annotate the size of each entry. The result will
* include the entry itself.
*
* @param {Entry} entry The root Entry for traversing.
* @param {function(Array<Entry>)} successCallback Called when the traverse
* is successfully done with the array of the entries.
* @param {function(DOMError)} errorCallback Called on error with the first
* occurred error (i.e. following errors will just be discarded).
* @private
*/
fileOperationUtil.resolveRecursively_ = function(
entry, successCallback, errorCallback) {
var result = [];
var error = null;
var numRunningTasks = 0;
var maybeInvokeCallback = function() {
// If there still remain some running tasks, wait their finishing.
if (numRunningTasks > 0)
return;
if (error)
errorCallback(error);
else
successCallback(result);
};
// The error handling can be shared.
var onError = function(fileError) {
// If this is the first error, remember it.
if (!error)
error = fileError;
--numRunningTasks;
maybeInvokeCallback();
};
var process = function(entry) {
numRunningTasks++;
result.push(entry);
if (entry.isDirectory) {
// The size of a directory is 1 bytes here, so that the progress bar
// will work smoother.
// TODO(hidehiko): Remove this hack.
entry.size = 1;
// Recursively traverse children.
var reader = entry.createReader();
reader.readEntries(
function processSubEntries(subEntries) {
if (error || subEntries.length == 0) {
// If an error is found already, or this is the completion
// callback, then finish the process.
--numRunningTasks;
maybeInvokeCallback();
return;
}
for (var i = 0; i < subEntries.length; i++)
process(subEntries[i]);
// Continue to read remaining children.
reader.readEntries(processSubEntries, onError);
},
onError);
} else {
// For a file, annotate the file size.
entry.getMetadata(function(metadata) {
entry.size = metadata.size;
--numRunningTasks;
maybeInvokeCallback();
}, onError);
}
};
process(entry);
};
/**
* Recursively gathers files from the given entry, resolving with
* the complete list of files when traversal is complete.
*
* <p>For real-time (as you scan) results use {@code findFilesRecursively}.
*
* @param {!DirectoryEntry} entry The DirectoryEntry to scan.
* @return {!Promise.<!Array<!Entry>>} Resolves when scanning is complete.
*/
fileOperationUtil.gatherEntriesRecursively = function(entry) {
/** @type {!Array<!Entry>} */
var gatheredFiles = [];
return fileOperationUtil.findEntriesRecursively(
entry,
/** @param {!Entry} entry */
function(entry) {
gatheredFiles.push(entry);
})
.then(
function() {
return gatheredFiles;
});
}
/**
* Recursively discovers files from the given entry, emitting individual
* results as they are found to {@code onResultCallback}.
*
* <p>For results gathered up in a tidy bundle, use
* {@code gatherFilesRecursively}.
*
* @param {!DirectoryEntry} entry The DirectoryEntry to scan.
* @param {function(!FileEntry)} onResultCallback called when
* a {@code FileEntry} is discovered.
* @return {!Promise} Resolves when scanning is complete.
*/
fileOperationUtil.findFilesRecursively = function(entry, onResultCallback) {
return fileOperationUtil.findEntriesRecursively(
entry,
/** @param {!Entry} entry */
function(entry) {
if (entry.isFile)
onResultCallback(/** @type {!FileEntry} */ (entry));
});
};
/**
* Recursively discovers files and directories beneath the given entry,
* emitting individual results as they are found to {@code onResultCallback}.
*
* <p>For results gathered up in a tidy bundle, use
* {@code gatherEntriesRecursively}.
*
* @param {!DirectoryEntry} entry The DirectoryEntry to scan.
* @param {function(!Entry)} onResultCallback called when
* an {@code Entry} is discovered.
* @return {!Promise} Resolves when scanning is complete.
*/
fileOperationUtil.findEntriesRecursively = function(entry, onResultCallback) {
return new Promise(
function(resolve, reject) {
var numRunningTasks = 0;
var scanError = null;
/**
* @param {*=} opt_error If defined immediately
* terminates scanning.
*/
var maybeSettlePromise = function(opt_error) {
scanError = opt_error;
if (scanError) {
// Closure compiler currently requires an argument to reject.
reject(undefined);
return;
}
// If there still remain some running tasks, wait their finishing.
if (numRunningTasks === 0)
// Closure compiler currently requires an argument to resolve.
resolve(undefined);
};
/** @param {!Entry} entry */
var processEntry = function(entry) {
// All scanning stops when an error is encountered.
if (scanError)
return;
onResultCallback(entry);
if (entry.isDirectory) {
processDirectory(/** @type {!DirectoryEntry} */ (entry));
}
};
/** @param {!DirectoryEntry} directory */
var processDirectory = function(directory) {
// All scanning stops when an error is encountered.
if (scanError)
return;
numRunningTasks++;
// Recursively traverse children.
// reader.readEntries chunksResults resulting in the need
// for us to call it multiple times.
var reader = directory.createReader();
reader.readEntries(
function processSubEntries(subEntries) {
if (subEntries.length === 0) {
// If an error is found already, or this is the completion
// callback, then finish the process.
--numRunningTasks;
maybeSettlePromise();
return;
}
subEntries.forEach(processEntry);
// Continue to read remaining children.
reader.readEntries(processSubEntries, maybeSettlePromise);
},
maybeSettlePromise);
};
processEntry(entry);
});
};
/**
* Calls {@code callback} for each child entry of {@code directory}.
*
* @param {!DirectoryEntry} directory
* @param {function(!Entry)} callback
* @return {!Promise} Resolves when listing is complete.
*/
fileOperationUtil.listEntries = function(directory, callback) {
return new Promise(
function(resolve, reject) {
var reader = directory.createReader();
var readEntries = function() {
reader.readEntries (
/** @param {!Array<!Entry>} entries */
function(entries) {
if (entries.length === 0) {
resolve(undefined);
return;
}
entries.forEach(callback);
readEntries();
},
reject);
};
readEntries();
});
};
/**
* Copies source to parent with the name newName recursively.
* This should work very similar to FileSystem API's copyTo. The difference is;
* - The progress callback is supported.
* - The cancellation is supported.
*
* @param {!Entry} source The entry to be copied.
* @param {!DirectoryEntry} parent The entry of the destination directory.
* @param {string} newName The name of copied file.
* @param {function(string, Entry)} entryChangedCallback
* Callback invoked when an entry is created with the source URL and
* the destination Entry.
* @param {function(string, number)} progressCallback Callback invoked
* periodically during the copying. It takes the source URL and the
* processed bytes of it.
* @param {function(Entry)} successCallback Callback invoked when the copy
* is successfully done with the Entry of the created entry.
* @param {function(DOMError)} errorCallback Callback invoked when an error
* is found.
* @return {function()} Callback to cancel the current file copy operation.
* When the cancel is done, errorCallback will be called. The returned
* callback must not be called more than once.
*/
fileOperationUtil.copyTo = function(
source, parent, newName, entryChangedCallback, progressCallback,
successCallback, errorCallback) {
/** @type {number|undefined} */
var copyId;
var pendingCallbacks = [];
// Makes the callback called in order they were invoked.
var callbackQueue = new AsyncUtil.Queue();
var onCopyProgress = function(progressCopyId, status) {
callbackQueue.run(function(callback) {
if (copyId === null) {
// If the copyId is not yet available, wait for it.
pendingCallbacks.push(
onCopyProgress.bind(null, progressCopyId, status));
callback();
return;
}
// This is not what we're interested in.
if (progressCopyId != copyId) {
callback();
return;
}
switch (status.type) {
case 'begin_copy_entry':
callback();
break;
case 'end_copy_entry':
// TODO(mtomasz): Convert URL to Entry in custom bindings.
(source.isFile ? parent.getFile : parent.getDirectory).call(
parent,
newName,
null,
function(entry) {
entryChangedCallback(status.sourceUrl, entry);
callback();
},
function() {
entryChangedCallback(status.sourceUrl, null);
callback();
});
break;
case 'progress':
progressCallback(status.sourceUrl, status.size);
callback();
break;
case 'success':
chrome.fileManagerPrivate.onCopyProgress.removeListener(
onCopyProgress);
// TODO(mtomasz): Convert URL to Entry in custom bindings.
util.URLsToEntries(
[status.destinationUrl], function(destinationEntries) {
successCallback(destinationEntries[0] || null);
callback();
});
break;
case 'error':
chrome.fileManagerPrivate.onCopyProgress.removeListener(
onCopyProgress);
errorCallback(util.createDOMError(status.error));
callback();
break;
default:
// Found unknown state. Cancel the task, and return an error.
console.error('Unknown progress type: ' + status.type);
chrome.fileManagerPrivate.onCopyProgress.removeListener(
onCopyProgress);
chrome.fileManagerPrivate.cancelCopy(
assert(copyId), util.checkAPIError);
errorCallback(util.createDOMError(
util.FileError.INVALID_STATE_ERR));
callback();
}
});
};
// Register the listener before calling startCopy. Otherwise some events
// would be lost.
chrome.fileManagerPrivate.onCopyProgress.addListener(onCopyProgress);
// Then starts the copy.
chrome.fileManagerPrivate.startCopy(
source, parent, newName, function(startCopyId) {
// last error contains the FileError code on error.
if (chrome.runtime.lastError) {
// Unsubscribe the progress listener.
chrome.fileManagerPrivate.onCopyProgress.removeListener(
onCopyProgress);
errorCallback(util.createDOMError(
chrome.runtime.lastError.message || ''));
return;
}
copyId = startCopyId;
for (var i = 0; i < pendingCallbacks.length; i++) {
pendingCallbacks[i]();
}
});
return function() {
// If copyId is not yet available, wait for it.
if (copyId === undefined) {
pendingCallbacks.push(function() {
chrome.fileManagerPrivate.cancelCopy(
assert(copyId), util.checkAPIError);
});
return;
}
chrome.fileManagerPrivate.cancelCopy(copyId, util.checkAPIError);
};
};
/**
* Thin wrapper of chrome.fileManagerPrivate.zipSelection to adapt its
* interface similar to copyTo().
*
* @param {!Array<!Entry>} sources The array of entries to be archived.
* @param {!DirectoryEntry} parent The entry of the destination directory.
* @param {string} newName The name of the archive to be created.
* @param {function(FileEntry)} successCallback Callback invoked when the
* operation is successfully done with the entry of the created archive.
* @param {function(DOMError)} errorCallback Callback invoked when an error
* is found.
*/
fileOperationUtil.zipSelection = function(
sources, parent, newName, successCallback, errorCallback) {
chrome.fileManagerPrivate.zipSelection(
parent,
sources,
newName, function(success) {
if (!success) {
// Failed to create a zip archive.
errorCallback(
util.createDOMError(util.FileError.INVALID_MODIFICATION_ERR));
return;
}
// Returns the created entry via callback.
parent.getFile(
newName, {create: false}, successCallback, errorCallback);
});
};
/**
* A record of a queued copy operation.
*
* Multiple copy operations may be queued at any given time. Additional
* Tasks may be added while the queue is being serviced. Though a
* cancel operation cancels everything in the queue.
*
* @param {string} taskId A unique ID for identifying this task.
* @param {util.FileOperationType} operationType The type of this operation.
* @param {Array<Entry>} sourceEntries Array of source entries.
* @param {DirectoryEntry} targetDirEntry Target directory.
* @constructor
* @struct
*/
fileOperationUtil.Task = function(
taskId, operationType, sourceEntries, targetDirEntry) {
/** @type {string} */
this.taskId = taskId;
/** @type {util.FileOperationType} */
this.operationType = operationType;
/** @type {Array<Entry>} */
this.sourceEntries = sourceEntries;
/** @type {DirectoryEntry} */
this.targetDirEntry = targetDirEntry;
/**
* An array of map from url to Entry being processed.
* @type {Array<Object<Entry>>}
*/
this.processingEntries = null;
/**
* Total number of bytes to be processed. Filled in initialize().
* Use 1 as an initial value to indicate that the task is not completed.
* @type {number}
*/
this.totalBytes = 1;
/**
* Total number of already processed bytes. Updated periodically.
* @type {number}
*/
this.processedBytes = 0;
/**
* Index of the progressing entry in sourceEntries.
* @private {number}
*/
this.processingSourceIndex_ = 0;
/**
* Set to true when cancel is requested.
* @private {boolean}
*/
this.cancelRequested_ = false;
/**
* Callback to cancel the running process.
* @private {?function()}
*/
this.cancelCallback_ = null;
// TODO(hidehiko): After we support recursive copy, we don't need this.
// If directory already exists, we try to make a copy named 'dir (X)',
// where X is a number. When we do this, all subsequent copies from
// inside the subtree should be mapped to the new directory name.
// For example, if 'dir' was copied as 'dir (1)', then 'dir/file.txt' should
// become 'dir (1)/file.txt'.
this.renamedDirectories_ = [];
};
/**
* @param {function()} callback When entries resolved.
*/
fileOperationUtil.Task.prototype.initialize = function(callback) {
};
/**
* Requests cancellation of this task.
* When the cancellation is done, it is notified via callbacks of run().
*/
fileOperationUtil.Task.prototype.requestCancel = function() {
this.cancelRequested_ = true;
if (this.cancelCallback_) {
var callback = this.cancelCallback_;
this.cancelCallback_ = null;
callback();
}
};
/**
* Runs the task. Sub classes must implement this method.
*
* @param {function(util.EntryChangedKind, Entry)} entryChangedCallback
* Callback invoked when an entry is changed.
* @param {function()} progressCallback Callback invoked periodically during
* the operation.
* @param {function()} successCallback Callback run on success.
* @param {function(fileOperationUtil.Error)} errorCallback Callback run on
* error.
*/
fileOperationUtil.Task.prototype.run = function(
entryChangedCallback, progressCallback, successCallback, errorCallback) {
};
/**
* Get states of the task.
* TOOD(hirono): Removes this method and sets a task to progress events.
* @return {Object} Status object.
*/
fileOperationUtil.Task.prototype.getStatus = function() {
var processingEntry = this.sourceEntries[this.processingSourceIndex_];
return {
operationType: this.operationType,
numRemainingItems: this.sourceEntries.length - this.processingSourceIndex_,
totalBytes: this.totalBytes,
processedBytes: this.processedBytes,
processingEntryName: processingEntry ? processingEntry.name : ''
};
};
/**
* Obtains the number of total processed bytes.
* @return {number} Number of total processed bytes.
* @private
*/
fileOperationUtil.Task.prototype.calcProcessedBytes_ = function() {
var bytes = 0;
for (var i = 0; i < this.processingSourceIndex_ + 1; i++) {
var entryMap = this.processingEntries[i];
if (!entryMap)
break;
for (var name in entryMap) {
bytes += i < this.processingSourceIndex_ ?
entryMap[name].size : entryMap[name].processedBytes;
}
}
return bytes;
};
/**
* Task to copy entries.
*
* @param {string} taskId A unique ID for identifying this task.
* @param {Array<Entry>} sourceEntries Array of source entries.
* @param {DirectoryEntry} targetDirEntry Target directory.
* @param {boolean} deleteAfterCopy Whether the delete original files after
* copy.
* @constructor
* @extends {fileOperationUtil.Task}
* @struct
*/
fileOperationUtil.CopyTask = function(
taskId, sourceEntries, targetDirEntry, deleteAfterCopy) {
fileOperationUtil.Task.call(
this,
taskId,
deleteAfterCopy ?
util.FileOperationType.MOVE : util.FileOperationType.COPY,
sourceEntries,
targetDirEntry);
this.deleteAfterCopy = deleteAfterCopy;
/**
* Rate limiter which is used to avoid sending update request for progress bar
* too frequently.
* @type {AsyncUtil.RateLimiter}
* @private
*/
this.updateProgressRateLimiter_ = null;
};
/**
* Extends fileOperationUtil.Task.
*/
fileOperationUtil.CopyTask.prototype.__proto__ =
fileOperationUtil.Task.prototype;
/**
* Initializes the CopyTask.
* @param {function()} callback Called when the initialize is completed.
*/
fileOperationUtil.CopyTask.prototype.initialize = function(callback) {
var group = new AsyncUtil.Group();
// Correct all entries to be copied for status update.
this.processingEntries = [];
for (var i = 0; i < this.sourceEntries.length; i++) {
group.add(function(index, callback) {
fileOperationUtil.resolveRecursively_(
this.sourceEntries[index],
function(resolvedEntries) {
var resolvedEntryMap = {};
for (var j = 0; j < resolvedEntries.length; ++j) {
var entry = resolvedEntries[j];
entry.processedBytes = 0;
resolvedEntryMap[entry.toURL()] = entry;
}
this.processingEntries[index] = resolvedEntryMap;
callback();
}.bind(this),
function(error) {
console.error(
'Failed to resolve for copy: %s', error.name);
callback();
});
}.bind(this, i));
}
group.run(function() {
// Fill totalBytes.
this.totalBytes = 0;
for (var i = 0; i < this.processingEntries.length; i++) {
for (var entryURL in this.processingEntries[i])
this.totalBytes += this.processingEntries[i][entryURL].size;
}
callback();
}.bind(this));
};
/**
* Copies all entries to the target directory.
* Note: this method contains also the operation of "Move" due to historical
* reason.
*
* @param {function(util.EntryChangedKind, Entry)} entryChangedCallback
* Callback invoked when an entry is changed.
* @param {function()} progressCallback Callback invoked periodically during
* the copying.
* @param {function()} successCallback On success.
* @param {function(fileOperationUtil.Error)} errorCallback On error.
* @override
*/
fileOperationUtil.CopyTask.prototype.run = function(
entryChangedCallback, progressCallback, successCallback, errorCallback) {
// TODO(hidehiko): We should be able to share the code to iterate on entries
// with serviceMoveTask_().
if (this.sourceEntries.length == 0) {
successCallback();
return;
}
// TODO(hidehiko): Delete after copy is the implementation of Move.
// Migrate the part into MoveTask.run().
var deleteOriginals = function() {
var count = this.sourceEntries.length;
var onEntryDeleted = function(entry) {
entryChangedCallback(util.EntryChangedKind.DELETED, entry);
count--;
if (!count)
successCallback();
};
var onFilesystemError = function(err) {
errorCallback(new fileOperationUtil.Error(
util.FileOperationErrorType.FILESYSTEM_ERROR, err));
};
for (var i = 0; i < this.sourceEntries.length; i++) {
var entry = this.sourceEntries[i];
util.removeFileOrDirectory(
entry, onEntryDeleted.bind(null, entry), onFilesystemError);
}
}.bind(this);
/**
* Accumulates processed bytes and call |progressCallback| if needed.
*
* @param {number} index The index of processing source.
* @param {string} sourceEntryUrl URL of the entry which has been processed.
* @param {number=} opt_size Processed bytes of the |sourceEntry|. If it is
* dropped, all bytes of the entry are considered to be processed.
*/
var updateProgress = function(index, sourceEntryUrl, opt_size) {
if (!sourceEntryUrl)
return;
var processedEntry = this.processingEntries[index][sourceEntryUrl];
if (!processedEntry)
return;
// Accumulates newly processed bytes.
var size = opt_size !== undefined ? opt_size : processedEntry.size;
this.processedBytes += size - processedEntry.processedBytes;
processedEntry.processedBytes = size;
// Updates progress bar in limited frequency so that intervals between
// updates have at least 200ms.
this.updateProgressRateLimiter_.run();
};
updateProgress = updateProgress.bind(this);
this.updateProgressRateLimiter_ = new AsyncUtil.RateLimiter(progressCallback);
AsyncUtil.forEach(
this.sourceEntries,
function(callback, entry, index) {
if (this.cancelRequested_) {
errorCallback(new fileOperationUtil.Error(
util.FileOperationErrorType.FILESYSTEM_ERROR,
util.createDOMError(util.FileError.ABORT_ERR)));
return;
}
progressCallback();
this.processEntry_(
entry, this.targetDirEntry,
function(sourceEntryUrl, destinationEntry) {
updateProgress(index, sourceEntryUrl);
// The destination entry may be null, if the copied file got
// deleted just after copying.
if (destinationEntry) {
entryChangedCallback(
util.EntryChangedKind.CREATED, destinationEntry);
}
},
function(sourceEntryUrl, size) {
updateProgress(index, sourceEntryUrl, size);
},
function() {
// Finishes off delayed updates if necessary.
this.updateProgressRateLimiter_.runImmediately();
// Update current source index and processing bytes.
this.processingSourceIndex_ = index + 1;
this.processedBytes = this.calcProcessedBytes_();
callback();
}.bind(this),
function(error) {
// Finishes off delayed updates if necessary.
this.updateProgressRateLimiter_.runImmediately();
errorCallback(error);
}.bind(this));
},
function() {
if (this.deleteAfterCopy) {
deleteOriginals();
} else {
successCallback();
}
}.bind(this),
this);
};
/**
* Copies the source entry to the target directory.
*
* @param {!Entry} sourceEntry An entry to be copied.
* @param {!DirectoryEntry} destinationEntry The entry which will contain the
* copied entry.
* @param {function(string, Entry)} entryChangedCallback
* Callback invoked when an entry is created with the source URL and
* the destination Entry.
* @param {function(string, number)} progressCallback Callback invoked
* periodically during the copying.
* @param {function()} successCallback On success.
* @param {function(fileOperationUtil.Error)} errorCallback On error.
* @private
*/
fileOperationUtil.CopyTask.prototype.processEntry_ = function(
sourceEntry, destinationEntry, entryChangedCallback, progressCallback,
successCallback, errorCallback) {
fileOperationUtil.deduplicatePath(
destinationEntry, sourceEntry.name,
function(destinationName) {
if (this.cancelRequested_) {
errorCallback(new fileOperationUtil.Error(
util.FileOperationErrorType.FILESYSTEM_ERROR,
util.createDOMError(util.FileError.ABORT_ERR)));
return;
}
this.cancelCallback_ = fileOperationUtil.copyTo(
sourceEntry, destinationEntry, destinationName,
entryChangedCallback, progressCallback,
function(entry) {
this.cancelCallback_ = null;
successCallback();
}.bind(this),
function(error) {
this.cancelCallback_ = null;
errorCallback(new fileOperationUtil.Error(
util.FileOperationErrorType.FILESYSTEM_ERROR, error));
}.bind(this));
}.bind(this),
errorCallback);
};
/**
* Task to move entries.
*
* @param {string} taskId A unique ID for identifying this task.
* @param {Array<Entry>} sourceEntries Array of source entries.
* @param {DirectoryEntry} targetDirEntry Target directory.
* @constructor
* @extends {fileOperationUtil.Task}
* @struct
*/
fileOperationUtil.MoveTask = function(taskId, sourceEntries, targetDirEntry) {
fileOperationUtil.Task.call(
this, taskId, util.FileOperationType.MOVE, sourceEntries, targetDirEntry);
};
/**
* Extends fileOperationUtil.Task.
*/
fileOperationUtil.MoveTask.prototype.__proto__ =
fileOperationUtil.Task.prototype;
/**
* Initializes the MoveTask.
* @param {function()} callback Called when the initialize is completed.
*/
fileOperationUtil.MoveTask.prototype.initialize = function(callback) {
// This may be moving from search results, where it fails if we
// move parent entries earlier than child entries. We should
// process the deepest entry first. Since move of each entry is
// done by a single moveTo() call, we don't need to care about the
// recursive traversal order.
this.sourceEntries.sort(function(entry1, entry2) {
return entry2.toURL().length - entry1.toURL().length;
});
this.processingEntries = [];
for (var i = 0; i < this.sourceEntries.length; i++) {
var processingEntryMap = {};
var entry = this.sourceEntries[i];
// The move should be done with updating the metadata. So here we assume
// all the file size is 1 byte. (Avoiding 0, so that progress bar can
// move smoothly).
// TODO(hidehiko): Remove this hack.
entry.size = 1;
processingEntryMap[entry.toURL()] = entry;
this.processingEntries[i] = processingEntryMap;
}
callback();
};
/**
* Moves all entries in the task.
*
* @param {function(util.EntryChangedKind, Entry)} entryChangedCallback
* Callback invoked when an entry is changed.
* @param {function()} progressCallback Callback invoked periodically during
* the moving.
* @param {function()} successCallback On success.
* @param {function(fileOperationUtil.Error)} errorCallback On error.
* @override
*/
fileOperationUtil.MoveTask.prototype.run = function(
entryChangedCallback, progressCallback, successCallback, errorCallback) {
if (this.sourceEntries.length == 0) {
successCallback();
return;
}
AsyncUtil.forEach(
this.sourceEntries,
function(callback, entry, index) {
if (this.cancelRequested_) {
errorCallback(new fileOperationUtil.Error(
util.FileOperationErrorType.FILESYSTEM_ERROR,
util.createDOMError(util.FileError.ABORT_ERR)));
return;
}
progressCallback();
fileOperationUtil.MoveTask.processEntry_(
entry, this.targetDirEntry, entryChangedCallback,
function() {
// Update current source index.
this.processingSourceIndex_ = index + 1;
this.processedBytes = this.calcProcessedBytes_();
callback();
}.bind(this),
errorCallback);
},
function() {
successCallback();
}.bind(this),
this);
};
/**
* Moves the sourceEntry to the targetDirEntry in this task.
*
* @param {Entry} sourceEntry An entry to be moved.
* @param {!DirectoryEntry} destinationEntry The entry of the destination
* directory.
* @param {function(util.EntryChangedKind, Entry)} entryChangedCallback
* Callback invoked when an entry is changed.
* @param {function()} successCallback On success.
* @param {function(fileOperationUtil.Error)} errorCallback On error.
* @private
*/
fileOperationUtil.MoveTask.processEntry_ = function(
sourceEntry, destinationEntry, entryChangedCallback, successCallback,
errorCallback) {
fileOperationUtil.deduplicatePath(
destinationEntry,
sourceEntry.name,
function(destinationName) {
sourceEntry.moveTo(
destinationEntry, destinationName,
function(movedEntry) {
entryChangedCallback(util.EntryChangedKind.CREATED, movedEntry);
entryChangedCallback(util.EntryChangedKind.DELETED, sourceEntry);
successCallback();
},
function(error) {
errorCallback(new fileOperationUtil.Error(
util.FileOperationErrorType.FILESYSTEM_ERROR, error));
});
},
errorCallback);
};
/**
* Task to create a zip archive.
*
* @param {string} taskId A unique ID for identifying this task.
* @param {!Array<!Entry>} sourceEntries Array of source entries.
* @param {!DirectoryEntry} targetDirEntry Target directory.
* @param {!DirectoryEntry} zipBaseDirEntry Base directory dealt as a root
* in ZIP archive.
* @constructor
* @extends {fileOperationUtil.Task}
* @struct
*/
fileOperationUtil.ZipTask = function(
taskId, sourceEntries, targetDirEntry, zipBaseDirEntry) {
fileOperationUtil.Task.call(
this, taskId, util.FileOperationType.ZIP, sourceEntries, targetDirEntry);
this.zipBaseDirEntry = zipBaseDirEntry;
/** @type {boolean} */
this.zip = true;
};
/**
* Extends fileOperationUtil.Task.
*/
fileOperationUtil.ZipTask.prototype.__proto__ =
fileOperationUtil.Task.prototype;
/**
* Initializes the ZipTask.
* @param {function()} callback Called when the initialize is completed.
*/
fileOperationUtil.ZipTask.prototype.initialize = function(callback) {
var resolvedEntryMap = {};
var group = new AsyncUtil.Group();
for (var i = 0; i < this.sourceEntries.length; i++) {
group.add(function(index, callback) {
fileOperationUtil.resolveRecursively_(
this.sourceEntries[index],
function(entries) {
for (var j = 0; j < entries.length; j++)
resolvedEntryMap[entries[j].toURL()] = entries[j];
callback();
},
callback);
}.bind(this, i));
}
group.run(function() {
// For zip archiving, all the entries are processed at once.
this.processingEntries = [resolvedEntryMap];
this.totalBytes = 0;
for (var url in resolvedEntryMap)
this.totalBytes += resolvedEntryMap[url].size;
callback();
}.bind(this));
};
/**
* Runs a zip file creation task.
*
* @param {function(util.EntryChangedKind, Entry)} entryChangedCallback
* Callback invoked when an entry is changed.
* @param {function()} progressCallback Callback invoked periodically during
* the moving.
* @param {function()} successCallback On complete.
* @param {function(fileOperationUtil.Error)} errorCallback On error.
* @override
*/
fileOperationUtil.ZipTask.prototype.run = function(
entryChangedCallback, progressCallback, successCallback, errorCallback) {
// TODO(hidehiko): we should localize the name.
var destName = 'Archive';
if (this.sourceEntries.length == 1) {
var entryName = this.sourceEntries[0].name;
var i = entryName.lastIndexOf('.');
destName = ((i < 0) ? entryName : entryName.substr(0, i));
}
fileOperationUtil.deduplicatePath(
this.targetDirEntry, destName + '.zip',
function(destPath) {
// TODO: per-entry zip progress update with accurate byte count.
// For now just set completedBytes to 0 so that it is not full until
// the zip operatoin is done.
this.processedBytes = 0;
progressCallback();
// The number of elements in processingEntries is 1. See also
// initialize().
var entries = [];
for (var url in this.processingEntries[0])
entries.push(this.processingEntries[0][url]);
fileOperationUtil.zipSelection(
entries,
this.zipBaseDirEntry,
destPath,
function(entry) {
this.processedBytes = this.totalBytes;
entryChangedCallback(util.EntryChangedKind.CREATED, entry);
successCallback();
}.bind(this),
function(error) {
errorCallback(new fileOperationUtil.Error(
util.FileOperationErrorType.FILESYSTEM_ERROR, error));
});
}.bind(this),
errorCallback);
};
/**
* Error class used to report problems with a copy operation.
* If the code is UNEXPECTED_SOURCE_FILE, data should be a path of the file.
* If the code is TARGET_EXISTS, data should be the existing Entry.
* If the code is FILESYSTEM_ERROR, data should be the FileError.
*
* @param {util.FileOperationErrorType} code Error type.
* @param {string|Entry|DOMError} data Additional data.
* @constructor
*/
fileOperationUtil.Error = function(code, data) {
this.code = code;
this.data = data;
};
/**
* Manages Event dispatching.
* Currently this can send three types of events: "copy-progress",
* "copy-operation-completed" and "delete".
*
* TODO(hidehiko): Reorganize the event dispatching mechanism.
* @constructor
* @extends {cr.EventTarget}
*/
fileOperationUtil.EventRouter = function() {
this.pendingDeletedEntries_ = {};
this.pendingCreatedEntries_ = {};
this.entryChangedEventRateLimiter_ = new AsyncUtil.RateLimiter(
this.dispatchEntryChangedEvent_.bind(this), 500);
};
/**
* Types of events emitted by the EventRouter.
* @enum {string}
*/
fileOperationUtil.EventRouter.EventType = {
BEGIN: 'BEGIN',
CANCELED: 'CANCELED',
ERROR: 'ERROR',
PROGRESS: 'PROGRESS',
SUCCESS: 'SUCCESS'
};
/**
* Extends cr.EventTarget.
*/
fileOperationUtil.EventRouter.prototype.__proto__ = cr.EventTarget.prototype;
/**
* Dispatches a simple "copy-progress" event with reason and current
* FileOperationManager status. If it is an ERROR event, error should be set.
*
* @param {fileOperationUtil.EventRouter.EventType} type Event type.
* @param {Object} status Current FileOperationManager's status. See also
* FileOperationManager.Task.getStatus().
* @param {string} taskId ID of task related with the event.
* @param {fileOperationUtil.Error=} opt_error The info for the error. This
* should be set iff the reason is "ERROR".
*/
fileOperationUtil.EventRouter.prototype.sendProgressEvent = function(
type, status, taskId, opt_error) {
var EventType = fileOperationUtil.EventRouter.EventType;
// Before finishing operation, dispatch pending entries-changed events.
if (type === EventType.SUCCESS || type === EventType.CANCELED)
this.entryChangedEventRateLimiter_.runImmediately();
var event = /** @type {FileOperationProgressEvent} */
(new Event('copy-progress'));
event.reason = type;
event.status = status;
event.taskId = taskId;
if (opt_error)
event.error = opt_error;
this.dispatchEvent(event);
};
/**
* Stores changed (created or deleted) entry temporarily, and maybe dispatch
* entries-changed event with stored entries.
* @param {util.EntryChangedKind} kind The enum to represent if the entry is
* created or deleted.
* @param {Entry} entry The changed entry.
*/
fileOperationUtil.EventRouter.prototype.sendEntryChangedEvent = function(
kind, entry) {
if (kind === util.EntryChangedKind.DELETED)
this.pendingDeletedEntries_[entry.toURL()] = entry;
if (kind === util.EntryChangedKind.CREATED)
this.pendingCreatedEntries_[entry.toURL()] = entry;
this.entryChangedEventRateLimiter_.run();
};
/**
* Dispatches an event to notify that entries are changed (created or deleted).
* @private
*/
fileOperationUtil.EventRouter.prototype.dispatchEntryChangedEvent_ =
function() {
var deletedEntries = [];
var createdEntries = [];
for (var url in this.pendingDeletedEntries_) {
deletedEntries.push(this.pendingDeletedEntries_[url]);
}
for (var url in this.pendingCreatedEntries_) {
createdEntries.push(this.pendingCreatedEntries_[url]);
}
if (deletedEntries.length > 0) {
var event = new Event('entries-changed');
event.kind = util.EntryChangedKind.DELETED;
event.entries = deletedEntries;
this.dispatchEvent(event);
this.pendingDeletedEntries_ = {};
}
if (createdEntries.length > 0) {
var event = new Event('entries-changed');
event.kind = util.EntryChangedKind.CREATED;
event.entries = createdEntries;
this.dispatchEvent(event);
this.pendingCreatedEntries_ = {};
}
};
/**
* Dispatches an event to notify entries are changed for delete task.
*
* @param {fileOperationUtil.EventRouter.EventType} reason Event type.
* @param {!Object} task Delete task related with the event.
*/
fileOperationUtil.EventRouter.prototype.sendDeleteEvent = function(
reason, task) {
var event = /** @type {FileOperationProgressEvent} */ (new Event('delete'));
event.reason = reason;
event.taskId = task.taskId;
event.entries = task.entries;
event.totalBytes = task.totalBytes;
event.processedBytes = task.processedBytes;
this.dispatchEvent(event);
};