Mini Shell
// code: language=javascript insertSpaces=True tabSize=4
/* Internet Explorer 11 lacks String.startsWith() */
if (!String.prototype.startsWith) {
String.prototype.startsWith = function(searchString, position) {
position = position || 0;
return this.substr(position, searchString.length) === searchString;
};
}
/* Internet Explorer 8 lacks Array.indexOf */
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function(what, i) {
i = i || 0;
var L = this.length;
while (i < L) {
if (this[i] === what) return i;
++i;
}
return -1;
};
}
/* Add [].remove */
Array.prototype.remove = function() {
var what, a = arguments,
L = a.length,
ax;
while (L && this.length) {
what = a[--L];
while ((ax = this.indexOf(what)) !== -1) {
this.splice(ax, 1);
}
}
return this;
};
function bySortedValue(obj, callback, context) {
// get a list of keys, sorted by their value, descending
var keys = [];
for (var key in obj) keys.push(key);
keys.sort(function(a, b) { return obj[b] - obj[a] });
for (var i = 0; i < keys.length; i++) {
callback.call(context, keys[i], obj[keys[i]]);
}
}
/* get all checked items in a file browser by its ID */
function get_browser_selected(id) {
var unshown_selections = $j(id).data('unshown-selections');
var hex_slash = ($j(id).data('post-action') == 'listdir');
var shown = [];
var selected = [];
// get the list of selected folders
$j.each($j(id.concat(' :input')), function(i, input) {
var input = $j(input);
shown.push(input.val());
if (input.is(':checked')) {
selected.push(input.val());
}
});
// remove any items from unshown_selections which the browser has loaded,
// then add the remaining to selected
$j.each(unshown_selections, function(i, unshown_item) {
if (shown.indexOf(unshown_item) != -1) {
unshown_selections.remove(unshown_item);
}
});
$j.each(unshown_selections, function(i, unshown_item) {
selected.push(unshown_item);
});
// iterate over them again to set .prop('indeterminate', true) as needed
$j.each($j(id.concat(' :input')), function(i, input) {
var input = $j(input);
if (input.is(':checked')) {
input.prop('indeterminate', false);
} else { // if this item is unchecked
var indeterminate = false;
var unchecked_path = input.val();
$j.each(selected, function(sel_i, selected_item) {
// if selected_item is a child of unchecked_path, it should be set to indeterminate
// example: unchecked_path="/root" and selected_item="/root/something/something"
if (is_parent_path(unchecked_path, selected_item, hex_slash)) {
indeterminate = true;
}
});
input.prop('indeterminate', indeterminate);
}
});
return selected;
}
/* check if all items were selected */
function all_selected(id) {
var inputs = $j(id.concat(' :input'));
for (let i = 0; i < inputs.length; i++) {
if (! $j(inputs[i]).is(':checked')){
return false;
}
}
return true;
}
function is_parent_path(parent, child, hex_slash) {
// ensure one and only one trailing slash
if (hex_slash) {
// '2f' = utf-8 of '/' in hex
var child = child.replace(/(?:2f)+$/, "").concat('2f');
var parent = parent.replace(/(?:2f)+$/, "").concat('2f');
} else {
var child = child.replace(/[\/]+$/, "").concat('/');
var parent = parent.replace(/[\/]+$/, "").concat('/');
}
if (child == parent) {
return false;
}
return child.startsWith(parent);
}
/* called when document is ready, this is called */
function init_file_browse(browser_id, snap, geo) {
// this tracks which folders are currently trying to fetch contents
// to prevent unnecssary API calls
var browser_div = $j(browser_id);
browser_div.data('processing', []);
var post_uri = browser_div.data('post-uri');
var action = browser_div.data('post-action');
if (action == 'listdir') {
// loading items from local disk
init_from_local_listdir(browser_div);
} else {
// loading items from restic
init_from_restic_browse(action, post_uri, browser_div, snap, geo);
}
}
function hex2str(hex) {
var pairs = hex.match(/(..?)/g);
var arr = [];
$j.each(pairs, function(i, pair) {
arr.push(parseInt(pair, 16));
});
return new TextDecoder().decode(new Uint8Array(arr));
};
function init_from_local_listdir(browser_div) {
var all_sizes = $j('#settings_tab').data('subpaths')
var browser_id = browser_div.attr('id');
var selections = browser_div.data('orig-selections');
browser_div.empty();
// create the main ul where file/folders are displayed
var ul = $j('<ul>', { style: 'list-style-type: none;' });
bySortedValue($j('#settings_tab').data('home-items').dirs, function(dir, size_mb) { // for each directory
var checked = selections.indexOf(dir) != -1;
if (checked) selections.remove(dir);
all_sizes[dir] = size_mb;
ul.append(create_browser_li({ browser_id: browser_id, folder: true, path: dir, toplevel: true, size: size_mb, checked: checked }));
});
bySortedValue($j('#settings_tab').data('home-items').files, function(path, size_mb) { // for each file
var checked = selections.indexOf(path) != -1;
if (checked) selections.remove(path);
all_sizes[path] = size_mb;
ul.append(create_browser_li({ browser_id: browser_id, folder: false, path: path, toplevel: true, size: size_mb, checked: checked }));
});
browser_div.append(ul);
// Any selections previously made which haven't been expanded yet
browser_div.data('unshown-selections', selections);
browser_div.data('all-sizes', all_sizes);
}
function init_from_restic_browse(action, post_uri, browser_div, snap, geo) {
var browser_id = browser_div.attr('id');
browser_div.data('unshown-selections', []); // not relevant to restic's browser; just set it empty
browser_div.empty();
if (snap == undefined) {
console.log('There appears to be no file backups to browse; not initializing the browser widget');
return;
}
var temp_p = $j('<p>', { text: 'Loading ', class: 'browser-msg' });
temp_p.append($j('<img>', { src: window.filebrowse_img_root.concat('/spinner.gif') }));
browser_div.append(temp_p);
var home = browser_div.data('home-path');
var post_args = JSON.stringify({ path: home, snap: snap, geo: geo });
$j.ajax({
url: post_uri,
type: 'POST',
timeout: 120 * 1000,
data: { action: action, args: post_args }
})
.done(function(data) {
try {
data = JSON.parse(data);
console.log(data);
} catch (err) {
browser_div.append($j('<p>', { text: 'server error', class: 'browser-msg text-danger' }));
console.log('could not decode server response as JSON: '.concat(data));
return;
}
browser_div.empty();
if (data.status != 0) { // successfully contacted server, but ran into an error
browser_div.append($j('<p>', { text: 'server error' }));
console.log(data);
return;
}
// create the main ul where file/folders are displayed
var ul = $j('<ul>', { style: 'list-style-type: none;' });
$j.each(data.data.dirs, function(dir_index, dir) { // for each directory
if (home == '/') {
var path = home.concat(dir);
} else {
var path = home.concat('/').concat(dir);
}
ul.append(create_browser_li({ folder: true, browser_id: browser_id, snap: snap, geo: geo, path: path, toplevel: true }));
});
$j.each(data.data.files, function(file_index, file) { // for each file
if (home == '/') {
var path = home.concat(file);
} else {
var path = home.concat('/').concat(file);
}
ul.append(create_browser_li({ folder: false, browser_id: browser_id, path: path, toplevel: true }));
});
browser_div.append(ul);
})
.fail(function(data) { // could not contact server
temp_p.text('server error');
console.log(data);
});
}
function update_sel_count() {
$j('.filebrowser-count').each(function(index, span) {
var span = $j(span);
var browser_name = span.data('browser');
var num_items = get_browser_selected(browser_name).length;
span.text(num_items);
if (num_items > 0) {
span.parent().removeClass('count-zero');
} else {
span.parent().addClass('count-zero');
}
var has_items_func = window[span.data('has-items-func')];
has_items_func(num_items > 0);
});
}
/* create a <li> for the browser. params:
folder: whether this is a folder which can be expanded
browser_id: id for the browser div (only if folder=true)
snap: optional snapshot ID to set in data for the li
path: file/folder path
checked: whether to set prop checked (default: false)
toplevel: whether to include the browser-toplevel class (default: false)
size: size to display for the item (optional) */
function create_browser_li(opts) {
var post_action = $j('#'.concat(opts.browser_id)).data('post-action');
if (post_action == 'listdir') {
var path = hex2str(opts.path);
} else {
var path = opts.path;
}
if (opts.size === undefined) {
var label = path;
} else if (opts.size === null) {
var label = path;
} else {
var label = path.concat(' (').concat((opts.size / 1024).toFixed(2)).concat('GB)');
}
var li_args = {};
if (opts.toplevel) li_args['class'] = 'browser-toplevel';
if (opts.folder) {
li_args['style'] = 'list-style-image: url("'.concat(window.filebrowse_img_root).concat('/directory.png")');
li_args['onclick'] = 'browse_expand(event, this);';
} else {
li_args['style'] = 'list-style-image: url("'.concat(icon_for(path)).concat('")');
}
var check_label = $j('<label>', { text: label });
var check_input = $j('<input>', { type: 'checkbox', value: opts.path, onclick: 'update_checked(this)' });
if (opts.checked) check_input.prop('checked', true);
if (!(opts.size === undefined)) {
if (opts.size === null) {
check_input.data('size', '?');
} else {
check_input.data('size', opts.size);
}
}
var li = $j('<li>', li_args);
li.data('path', opts.path);
li.data('browser_id', opts.browser_id);
if (!(opts.snap === undefined)) {
li.data('snap', opts.snap);
li.data('geo', opts.geo);
}
var check_div = $j('<div>', { class: 'checkbox' });
check_label.append(check_input);
check_div.append(check_label);
li.append(check_div);
return li;
}
/* triggers when a checkbox is clicked */
function update_checked(checkbox) {
checkbox = $j(checkbox);
var file_browse = checkbox.closest('.file_browse');
var path = checkbox.val();
var checked = checkbox.is(':checked');
// when a checkbox is manually checked:
// - check all items underneath it
// when a checkbox is manually unchecked
// - uncheck all items underneath it
// - uncheck all items above it. update_sel_count() will then set them as indeterminate
if (file_browse.data('post-action') == 'listdir') {
var slash = '2f'; // utf-8 of '/' in hex
} else {
var slash = '/';
}
file_browse.find(' :input').each(function(i, check) {
var check = $j(check);
if (check.val() != path && check.val().startsWith(path.concat(slash))) {
check.prop('checked', checked);
}
if (!checked && check.val() != path && path.startsWith(check.val().concat(slash))) {
check.prop('checked', false);
}
});
update_sel_count();
}
/* show an alert div for errors */
function show_browser_error(alert_div, msg, full_error) {
console.log(full_error);
alert_div.find('span').text(msg);
alert_div.removeClass('hidden');
alert_div.show();
}
/* expands a folder when clicked for the first time */
function browse_expand(event, li_item) {
if (event.target.tagName == 'LABEL') {
return; // it triggers twice if the label part of the checkbox is clicked
}
if (event.target.tagName == 'INPUT') {
// the checkbox triggered this, not the li
update_checked($j(event.target));
return;
}
li_item = $j(li_item);
var path = li_item.data('path');
var snap = li_item.data('snap');
var geo = li_item.data('geo');
var browser_id = li_item.data('browser_id');
if (li_item.data('open') == 'y') { // already populated the items
if (li_item.data('expanded') == 'y') { // expanded - hide the items beneath it
expand_item(li_item, false);
} else { // items hidden - show them again
expand_item(li_item, true);
}
return;
}
var browser_div = $j('#'.concat(browser_id));
var processing = browser_div.data('processing');
if (processing.indexOf(path) != -1) {
console.log(browser_id.concat(' is already processing a request for ').concat(path));
return;
} else {
processing.push(path);
browser_div.data('processing', processing);
}
li_item.css('list-style-image', 'url("'.concat(window.filebrowse_img_root).concat('/spinner.gif")'));
var browser_div = $j('#'.concat(browser_id));
var action = browser_div.data('post-action');
if (action == 'listdir') {
var post_args = JSON.stringify({ path: path });
} else {
var post_args = JSON.stringify({ path: path, snap: snap, geo: geo });
}
var post_uri = browser_div.data('post-uri');
$j.ajax({
url: post_uri,
type: 'POST',
timeout: 120 * 1000,
data: { action: action, args: post_args }
})
.done(function(data) {
try {
data = JSON.parse(data);
} catch (err) {
show_browser_error(
$j(li_item.closest('.file_browse').data('errors')),
'Error browsing '.concat(path).concat(': server error'),
data
);
return;
}
if (data.status != 0) { // successfully contacted server, but ran into an error
show_browser_error(
$j(li_item.closest('.file_browse').data('errors')),
'Error browsing '.concat(path).concat(': ').concat(data.error),
data
);
return;
}
// update data-all-sizes on the browser div
if (action == 'listdir') {
var all_sizes = browser_div.data('all-sizes');
$j.each(data.data.dirs, function(path, size) {
all_sizes[path] = size;
});
$j.each(data.data.files, function(path, size) {
all_sizes[path] = size;
})
browser_div.data('all-sizes', all_sizes);
}
var next_li = $j('<li>');
var ul = $j('<ul>', { style: 'list-style-type: none;' });
var empty = true;
var check_new_items = li_item.find(' :input').is(':checked');
var unshown_selections = browser_div.data('unshown-selections');
$j.each(data.data.dirs, function(key, val) { // for each directory
if (action == 'listdir') { // local listdir returns a dict of {dir:size}
var dir = key;
var size = val;
var this_path = dir;
} else { // restic browse returns a list of dirs
var dir = val;
var size = undefined;
var this_path = path.concat('/').concat(dir);
}
empty = false;
var check = check_new_items;
if (unshown_selections.indexOf(this_path) != -1) {
var check = true;
}
ul.append(create_browser_li({ folder: true, browser_id: browser_id, snap: snap, geo: geo, path: this_path, size: size, checked: check }));
});
$j.each(data.data.files, function(key, val) { // for each file
if (action == 'listdir') { // local listdir returns a dict of {file:size}
var file = key;
var size = val;
var this_path = file;
} else { // restic browse returns a list of files
var file = val;
var size = undefined;
var this_path = path.concat('/').concat(file);
}
empty = false;
var check = check_new_items;
if (unshown_selections.indexOf(this_path) != -1) {
var check = true;
}
ul.append(create_browser_li({ folder: false, browser_id: browser_id, path: this_path, size: size, checked: check }));
});
if (li_item.data('open') != 'y') {
li_item.data('open', 'y');
li_item.data('expanded', 'y');
next_li.append(ul)
li_item.after(next_li);
li_item.css('list-style-image', 'url("'.concat(window.filebrowse_img_root).concat('/folder_open.png")'));
var dir_label = li_item.first('checkbox');
if (empty) {
dir_label.text(dir_label.text().concat(' (empty)'));
}
}
})
.fail(function(data) { // could not contact server
show_browser_error(
$j(li_item.closest('.file_browse').data('errors')),
'Error browsing '.concat(path).concat(': ').concat(data.statusText),
data
);
li_item.css('list-style-image', 'url("'.concat(window.filebrowse_img_root).concat('/directory.png")'));
})
.always(function() {
var processing = browser_div.data('processing');
processing.remove(path);
browser_div.data('processing', processing);
});
}
/* expand (or hide) an already opened folder */
function expand_item(li_elem, expand) {
var inner_li = li_elem.next('li');
if (expand) { // items hidden - show them again
inner_li.show();
li_elem.data('expanded', 'y');
li_elem.css('list-style-image', 'url("'.concat(window.filebrowse_img_root).concat('/folder_open.png")'));
} else { // expanded - hide the items beneath it
inner_li.hide();
li_elem.css('list-style-image', 'url("'.concat(window.filebrowse_img_root).concat('/directory.png")'));
li_elem.data('expanded', 'n');
}
}
/* when the select all or deselect all button is clicked */
function filebrowser_selectall(browser_id, checked) {
var browser_div = $j(browser_id);
browser_div.find('li').each(function(li_index, li) {
li = $j(li);
if (checked) { // selecting all
if (li.hasClass('browser-toplevel')) {
$j(li.find('input')).prop('checked', true);
update_checked($j(li.find('input')));
}
if (li.data('expanded') == 'y') {
expand_item(li, false);
}
} else { // deselecting all
$j(li.find('input')).prop('checked', false);
update_checked($j(li.find('input')));
}
});
update_sel_count();
}
/* get the icon that should be displayed for a file.
* directories are different and should be directory.png or folder_open.png */
function icon_for(filename) {
var ext = filename.substr(filename.lastIndexOf('.') + 1);
// unused: file-lock.png, directory-lock.png
var file_browse_extensions = {
'rtf': 'txt',
'gz': 'zip',
'jpeg': 'picture',
'4fb': 'flash',
'mp4': 'picture',
'mp2': 'film',
'mp3': 'music',
'm4p': 'film',
'jar': 'java',
'wmv': 'film',
'wml': 'html',
'm4b': 'music',
'wma': 'music',
'm4a': 'music',
'bin': 'application',
'mpg': 'film',
'mpe': 'film',
'cpio': 'zip',
'psd': 'psd',
'mpv': 'film',
'tif': 'picture',
'f4p': 'flash',
'bat': 'script',
'gifv': 'film',
'f4v': 'flash',
'f4a': 'flash',
'pbm': 'picture',
'htm': 'html',
'webm': 'film',
'webp': 'picture',
'mpeg': 'film',
'm2v': 'film',
'rb': 'ruby',
'cgi': 'script',
'js': 'script',
'plx': 'script',
'bz': 'zip',
'c': 'code',
's7z': 'zip',
'iso': 'zip',
'pdf': 'pdf',
'tiff': 'picture',
'pgm': 'picture',
'ppm': 'picture',
'xz': 'zip',
'txt': 'txt',
'doc': 'doc',
'pp': 'code',
'vob': 'film',
'zip': 'zip',
'py': 'script',
'swf': 'flash',
'gif': 'picture',
'wav': 'music',
'pl': 'script',
'phtml': 'html',
'ogv': 'film',
'pnm': 'picture',
'flac': 'music',
'ogg': 'film',
'oga': 'music',
'png': 'picture',
'aac': 'music',
'flv': 'flash',
'erb': 'ruby',
'cab': 'zip',
'z': 'zip',
'tar': 'zip',
'3g2': 'film',
'jpg': 'picture',
'ar': 'zip',
'rar': 'zip',
'avi': 'film',
'vox': 'music',
'7z': 'zip',
'shtml': 'html',
'bz2': 'zip',
'html': 'html',
'php4': 'php',
'php5': 'php',
'xls': 'xls',
'xhtml': 'html',
'php7': 'php',
'css': 'css',
'php3': 'php',
'3gp': 'music',
'ppt': 'ppt',
'mov': 'film',
'perl': 'script',
'jsp': 'code',
'sql': 'db',
'php': 'php',
'm4v': 'film',
'a': 'zip',
'svg': 'picture',
'sh': 'script',
'so': 'linux',
'cpp': 'code'
};
if (ext in file_browse_extensions) {
return window.filebrowse_img_root.concat('/').concat(file_browse_extensions[ext]).concat('.png');
}
return window.filebrowse_img_root.concat('/file.png');
}
Zerion Mini Shell 1.0