blob: c742c8a843be0edd5a699cb9b8324121b918e9f9 [file] [log] [blame]
// Copyright (c) 2012 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.
/**
* Namespace for utility functions.
*/
var util = {};
/**
* Returns a function that console.log's its arguments, prefixed by |msg|.
*
* @param {string} msg The message prefix to use in the log.
* @param {function} opt_callback A function to invoke after logging.
* @return {function} Function that logs.
*/
util.flog = function(msg, opt_callback) {
return function() {
var ary = Array.apply(null, arguments);
console.log(msg + ': ' + ary.join(', '));
if (opt_callback)
opt_callback.apply(null, arguments);
};
};
/**
* Returns a function that throws an exception that includes its arguments
* prefixed by |msg|.
*
* @param {string} msg The message prefix to use in the exception.
* @return {function} Function that throws.
*/
util.ferr = function(msg) {
return function() {
var ary = Array.apply(null, arguments);
throw new Error(msg + ': ' + ary.join(', '));
};
};
/**
* Install a sensible toString() on the FileError object.
*
* FileError.prototype.code is a numeric code describing the cause of the
* error. The FileError constructor has a named property for each possible
* error code, but provides no way to map the code to the named property.
* This toString() implementation fixes that.
*/
util.installFileErrorToString = function() {
FileError.prototype.toString = function() {
return '[object FileError: ' + util.getFileErrorMnemonic(this.code) + ']';
}
};
/**
* @param {number} code The file error code.
* @return {string} The file error mnemonic.
*/
util.getFileErrorMnemonic = function(code) {
for (var key in FileError) {
if (key.search(/_ERR$/) != -1 && FileError[key] == code)
return key;
}
return code;
};
/**
* @param {number} code File error code (from FileError object).
* @return {string} Translated file error string.
*/
util.getFileErrorString = function(code) {
for (var key in FileError) {
var match = /(.*)_ERR$/.exec(key);
if (match && FileError[key] == code) {
// This would convert 1 to 'NOT_FOUND'.
code = match[1];
break;
}
}
console.warn('File error: ' + code);
return loadTimeData.getString('FILE_ERROR_' + code) ||
loadTimeData.getString('FILE_ERROR_GENERIC');
};
/**
* @param {string} str String to unescape.
* @return {string} Unescaped string.
*/
util.htmlUnescape = function(str) {
return str.replace(/&(lt|gt|amp);/g, function(entity) {
switch (entity) {
case '&lt;': return '<';
case '&gt;': return '>';
case '&amp;': return '&';
}
});
};
/**
* Given a list of Entries, recurse any DirectoryEntries if |recurse| is true,
* and call back with a list of all file and directory entries encountered
* (including the original set).
* @param {Array.<Entry>} entries List of entries.
* @param {boolean} recurse Whether to recurse.
* @param {function(object)} successCallback Object has the fields dirEntries,
* fileEntries and fileBytes.
*/
util.recurseAndResolveEntries = function(entries, recurse, successCallback) {
var pendingSubdirectories = 0;
var pendingFiles = 0;
var dirEntries = [];
var fileEntries = [];
var fileBytes = 0;
function pathCompare(a, b) {
if (a.fullPath > b.fullPath)
return 1;
if (a.fullPath < b.fullPath)
return -1;
return 0;
}
function parentPath(path) {
return path.substring(0, path.lastIndexOf('/'));
}
// We invoke this after each async callback to see if we've received all
// the expected callbacks. If so, we're done.
function areWeThereYet() {
if (pendingSubdirectories == 0 && pendingFiles == 0) {
var result = {
dirEntries: dirEntries.sort(pathCompare),
fileEntries: fileEntries.sort(pathCompare),
fileBytes: fileBytes
};
if (successCallback) {
successCallback(result);
}
}
}
function tallyEntry(entry, originalSourcePath) {
entry.originalSourcePath = originalSourcePath;
if (entry.isDirectory) {
dirEntries.push(entry);
if (recurse) {
recurseDirectory(entry, originalSourcePath);
}
} else {
fileEntries.push(entry);
pendingFiles++;
entry.getMetadata(function(metadata) {
fileBytes += metadata.size;
pendingFiles--;
areWeThereYet();
});
}
}
function recurseDirectory(dirEntry, originalSourcePath) {
pendingSubdirectories++;
util.forEachDirEntry(dirEntry, function(entry) {
if (entry == null) {
// Null entry indicates we're done scanning this directory.
pendingSubdirectories--;
areWeThereYet();
} else {
tallyEntry(entry, originalSourcePath);
}
});
}
for (var i = 0; i < entries.length; i++) {
tallyEntry(entries[i], parentPath(entries[i].fullPath));
}
areWeThereYet();
};
/**
* Utility function to invoke callback once for each entry in dirEntry.
*
* @param {DirectoryEntry} dirEntry The directory entry to enumerate.
* @param {function(Entry)} callback The function to invoke for each entry in
* dirEntry.
*/
util.forEachDirEntry = function(dirEntry, callback) {
var reader;
function onError(err) {
console.error('Failed to read dir entries at ' + dirEntry.fullPath);
}
function onReadSome(results) {
if (results.length == 0)
return callback(null);
for (var i = 0; i < results.length; i++)
callback(results[i]);
reader.readEntries(onReadSome, onError);
};
reader = dirEntry.createReader();
reader.readEntries(onReadSome, onError);
};
/**
* Reads contents of directory.
* @param {DirectoryEntry} root Root entry.
* @param {string} path Directory path.
* @param {function(Array.<Entry>)} callback List of entries passed to callback.
*/
util.readDirectory = function(root, path, callback) {
function onError(e) {
callback([], e);
}
root.getDirectory(path, {create: false}, function(entry) {
var reader = entry.createReader();
var r = [];
function readNext() {
reader.readEntries(function(results) {
if (results.length == 0) {
callback(r, null);
return;
}
r.push.apply(r, results);
readNext();
}, onError);
}
readNext();
}, onError);
};
/**
* Utility function to resolve multiple directories with a single call.
*
* The successCallback will be invoked once for each directory object
* found. The errorCallback will be invoked once for each
* path that could not be resolved.
*
* The successCallback is invoked with a null entry when all paths have
* been processed.
*
* @param {DirEntry} dirEntry The base directory.
* @param {Object} params The parameters to pass to the underlying
* getDirectory calls.
* @param {Array<string>} paths The list of directories to resolve.
* @param {function(!DirEntry)} successCallback The function to invoke for
* each DirEntry found. Also invoked once with null at the end of the
* process.
* @param {function(FileError)} errorCallback The function to invoke
* for each path that cannot be resolved.
*/
util.getDirectories = function(dirEntry, params, paths, successCallback,
errorCallback) {
// Copy the params array, since we're going to destroy it.
params = [].slice.call(params);
function onComplete() {
successCallback(null);
}
function getNextDirectory() {
var path = paths.shift();
if (!path)
return onComplete();
dirEntry.getDirectory(
path, params,
function(entry) {
successCallback(entry);
getNextDirectory();
},
function(err) {
errorCallback(err);
getNextDirectory();
});
}
getNextDirectory();
};
/**
* Utility function to resolve multiple files with a single call.
*
* The successCallback will be invoked once for each directory object
* found. The errorCallback will be invoked once for each
* path that could not be resolved.
*
* The successCallback is invoked with a null entry when all paths have
* been processed.
*
* @param {DirEntry} dirEntry The base directory.
* @param {Object} params The parameters to pass to the underlying
* getFile calls.
* @param {Array<string>} paths The list of files to resolve.
* @param {function(!FileEntry)} successCallback The function to invoke for
* each FileEntry found. Also invoked once with null at the end of the
* process.
* @param {function(FileError)} errorCallback The function to invoke
* for each path that cannot be resolved.
*/
util.getFiles = function(dirEntry, params, paths, successCallback,
errorCallback) {
// Copy the params array, since we're going to destroy it.
params = [].slice.call(params);
function onComplete() {
successCallback(null);
}
function getNextFile() {
var path = paths.shift();
if (!path)
return onComplete();
dirEntry.getFile(
path, params,
function(entry) {
successCallback(entry);
getNextFile();
},
function(err) {
errorCallback(err);
getNextFile();
});
}
getNextFile();
};
/**
* Resolve 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.
* @param {function(Entry)} resultCallback Called back when a path is
* successfully resolved. Entry will be either a DirectoryEntry or
* a FileEntry.
* @param {function(FileError)} errorCallback Called back if an unexpected
* error occurs while resolving the path.
*/
util.resolvePath = function(root, path, resultCallback, errorCallback) {
if (path == '' || path == '/') {
resultCallback(root);
return;
}
root.getFile(
path, {create: false},
resultCallback,
function(err) {
if (err.code == FileError.TYPE_MISMATCH_ERR) {
// Bah. It's a directory, ask again.
root.getDirectory(
path, {create: false},
resultCallback,
errorCallback);
} else {
errorCallback(err);
}
});
};
/**
* Locate the file referred to by path, creating directories or the file
* itself if necessary.
* @param {DirEntry} root The root entry.
* @param {string} path The file path.
* @param {function} successCallback The callback.
* @param {function} errorCallback The callback.
*/
util.getOrCreateFile = function(root, path, successCallback, errorCallback) {
var dirname = null;
var basename = null;
function onDirFound(dirEntry) {
dirEntry.getFile(basename, { create: true },
successCallback, errorCallback);
}
var i = path.lastIndexOf('/');
if (i > -1) {
dirname = path.substr(0, i);
basename = path.substr(i + 1);
} else {
basename = path;
}
if (!dirname) {
onDirFound(root);
return;
}
util.getOrCreateDirectory(root, dirname, onDirFound, errorCallback);
};
/**
* Locate the directory referred to by path, creating directories along the
* way.
* @param {DirEntry} root The root entry.
* @param {string} path The directory path.
* @param {function} successCallback The callback.
* @param {function} errorCallback The callback.
*/
util.getOrCreateDirectory = function(root, path, successCallback,
errorCallback) {
var names = path.split('/');
function getOrCreateNextName(dir) {
if (!names.length)
return successCallback(dir);
var name;
do {
name = names.shift();
} while (!name || name == '.');
dir.getDirectory(name, { create: true }, getOrCreateNextName,
errorCallback);
}
getOrCreateNextName(root);
};
/**
* Remove a file or a directory.
* @param {Entry} entry The entry to remove.
* @param {function} onSuccess The success callback.
* @param {function} onError The error callback.
*/
util.removeFileOrDirectory = function(entry, onSuccess, onError) {
if (entry.isDirectory)
entry.removeRecursively(onSuccess, onError);
else
entry.remove(onSuccess, onError);
};
/**
* Units table for bytesToSi.
* Note: changing this requires some code change in bytesToSi.
* Note: these values are localized in file_manager.js.
*/
util.UNITS = ['KB', 'MB', 'GB', 'TB', 'PB'];
/**
* Scale table for bytesToSi.
*/
util.SCALE = [Math.pow(2, 10),
Math.pow(2, 20),
Math.pow(2, 30),
Math.pow(2, 40),
Math.pow(2, 50)];
/**
* Convert a number of bytes into an appropriate International System of
* Units (SI) representation, using the correct number separators.
*
* @param {number} bytes The number of bytes.
* @return {string} Localized string.
*/
util.bytesToSi = function(bytes) {
function str(n, u) {
// TODO(rginda): Switch to v8Locale's number formatter when it's
// available.
return n.toLocaleString() + ' ' + u;
}
function fmt(s, u) {
var rounded = Math.round(bytes / s * 10) / 10;
return str(rounded, u);
}
// Less than 1KB is displayed like '0.8 KB'.
if (bytes < util.SCALE[0]) {
return fmt(util.SCALE[0], util.UNITS[0]);
}
// Up to 1MB is displayed as rounded up number of KBs.
if (bytes < util.SCALE[1]) {
var rounded = Math.ceil(bytes / util.SCALE[0]);
return str(rounded, util.UNITS[0]);
}
// This loop index is used outside the loop if it turns out |bytes|
// requires the largest unit.
var i;
for (i = 1; i < util.UNITS.length - 1; i++) {
if (bytes < util.SCALE[i + 1])
return fmt(util.SCALE[i], util.UNITS[i]);
}
return fmt(util.SCALE[i], util.UNITS[i]);
};
/**
* Utility function to read specified range of bytes from file
* @param {File} file The file to read.
* @param {number} begin Starting byte(included).
* @param {number} end Last byte(excluded).
* @param {function(File, Uint8Array)} callback Callback to invoke.
* @param {function(FileError)} onError Error handler.
*/
util.readFileBytes = function(file, begin, end, callback, onError) {
var fileReader = new FileReader();
fileReader.onerror = onError;
fileReader.onloadend = function() {
callback(file, new ByteReader(fileReader.result));
};
fileReader.readAsArrayBuffer(file.slice(begin, end));
};
if (!Blob.prototype.slice) {
/**
* This code might run in the test harness on older versions of Chrome where
* Blob.slice is still called Blob.webkitSlice.
*/
Blob.prototype.slice = Blob.prototype.webkitSlice;
}
/**
* Write a blob to a file.
* Truncates the file first, so the previous content is fully overwritten.
* @param {FileEntry} entry File entry.
* @param {Blob} blob The blob to write.
* @param {function} onSuccess Completion callback.
* @param {function(FileError)} onError Error handler.
*/
util.writeBlobToFile = function(entry, blob, onSuccess, onError) {
function truncate(writer) {
writer.onerror = onError;
writer.onwriteend = write.bind(null, writer);
writer.truncate(0);
}
function write(writer) {
writer.onwriteend = onSuccess;
writer.write(blob);
}
entry.createWriter(truncate, onError);
};
/**
* Returns a string '[Ctrl-][Alt-][Shift-][Meta-]' depending on the event
* modifiers. Convenient for writing out conditions in keyboard handlers.
*
* @param {Event} event The keyboard event.
* @return {string} Modifiers.
*/
util.getKeyModifiers = function(event) {
return (event.ctrlKey ? 'Ctrl-' : '') +
(event.altKey ? 'Alt-' : '') +
(event.shiftKey ? 'Shift-' : '') +
(event.metaKey ? 'Meta-' : '');
};
/**
* @param {HTMLElement} element Element to transform.
* @param {Object} transform Transform object,
* contains scaleX, scaleY and rotate90 properties.
*/
util.applyTransform = function(element, transform) {
element.style.webkitTransform =
transform ? 'scaleX(' + transform.scaleX + ') ' +
'scaleY(' + transform.scaleY + ') ' +
'rotate(' + transform.rotate90 * 90 + 'deg)' :
'';
};
/**
* Makes filesystem: URL from the path.
* @param {string} path File or directory path.
* @return {string} URL.
*/
util.makeFilesystemUrl = function(path) {
path = path.split('/').map(encodeURIComponent).join('/');
return 'filesystem:' + chrome.extension.getURL('external' + path);
};
/**
* Extracts path from filesystem: URL.
* @param {string} url Filesystem URL.
* @return {string} The path.
*/
util.extractFilePath = function(url) {
var path = /^filesystem:[\w-]*:\/\/[\w]*\/(external|persistent)(\/.*)$/.
exec(url)[2];
if (!path) return null;
return decodeURIComponent(path);
};
/**
* @return {string} Id of the current Chrome extension.
*/
util.getExtensionId = function() {
return chrome.extension.getURL('').split('/')[2];
};
/**
* Traverses a tree up to a certain depth.
* @param {FileEntry} root Root entry.
* @param {function(Array.<Entry>)} callback The callback is called at the very
* end with a list of entries found.
* @param {number?} max_depth Maximum depth. Pass zero to traverse everything.
*/
util.traverseTree = function(root, callback, max_depth) {
if (root.isFile) {
callback([root]);
return;
}
var result = [];
var pending = 0;
function maybeDone() {
if (pending == 0)
callback(result);
}
function readEntry(entry, depth) {
result.push(entry);
// Do not recurse too deep and into files.
if (entry.isFile || (max_depth != 0 && depth >= max_depth))
return;
pending++;
util.forEachDirEntry(entry, function(childEntry) {
if (childEntry == null) {
// Null entry indicates we're done scanning this directory.
pending--;
maybeDone();
} else {
readEntry(childEntry, depth + 1);
}
});
}
readEntry(root, 0);
};
/**
* A shortcut function to create a child element with given tag and class.
*
* @param {HTMLElement} parent Parent element.
* @param {string} opt_className Class name.
* @param {string} opt_tag Element tag, DIV is omitted.
* @return {Element} Newly created element.
*/
util.createChild = function(parent, opt_className, opt_tag) {
var child = parent.ownerDocument.createElement(opt_tag || 'div');
if (opt_className)
child.className = opt_className;
parent.appendChild(child);
return child;
};
/**
* Update the top window location search query and hash.
*
* @param {boolean} replace True if the history state should be replaced,
* false if pushed.
* @param {string} path Path to be put in the address bar after the hash.
* If null the hash is left unchanged.
* @param {string|object} opt_param Search parameter. Used directly if string,
* stringified if object. If omitted the search query is left unchanged.
*/
util.updateLocation = function(replace, path, opt_param) {
var location = window.top.document.location;
var history = window.top.history;
var search;
if (typeof opt_param == 'string')
search = opt_param;
else if (typeof opt_param == 'object')
search = '?' + JSON.stringify(opt_param);
else
search = location.search;
var hash;
if (path)
hash = '#' + encodeURI(path);
else
hash = location.hash;
var newLocation = location.origin + location.pathname + search + hash;
//TODO(kaznacheev): Fix replaceState for component extensions. Currently it
//does not replace the content of the address bar.
if (replace)
history.replaceState(undefined, path, newLocation);
else
history.pushState(undefined, path, newLocation);
};
/**
* Return a translated string.
*
* Wrapper function to make dealing with translated strings more concise.
* Equivalent to loadTimeData.getString(id).
*
* @param {string} id The id of the string to return.
* @return {string} The translated string.
*/
function str(id) {
return loadTimeData.getString(id);
}
/**
* Return a translated string with arguments replaced.
*
* Wrapper function to make dealing with translated strings more concise.
* Equivilant to loadTimeData.getStringF(id, ...).
*
* @param {string} id The id of the string to return.
* @param {...string} var_args The values to replace into the string.
* @return {string} The translated string with replaced values.
*/
function strf(id, var_args) {
return loadTimeData.getStringF.apply(loadTimeData, arguments);
}