diff --git a/exec/fileman.js b/exec/fileman.js
new file mode 100755
index 0000000000000000000000000000000000000000..ed2f4295ddc763e2fa037de2b50292712c85dbef
--- /dev/null
+++ b/exec/fileman.js
@@ -0,0 +1,359 @@
+// Synchronet File Manager
+
+// run using jsexec
+
+require("file_size.js", "file_size_float");
+require("uifcdefs.js", "WIN_ORG");
+require("sbbsdefs.js", "K_EDIT");
+
+"use strict";
+
+if(!uifc.init("Synchronet File Manager")) {
+	alert("uifc init failure");
+	exit(-1);
+}
+js.on_exit("uifc.bail()");
+
+const main_ctx = new uifc.list.CTX;
+while(!js.terminated) {
+	const items = [
+		"Browse",
+		"Search filenames",
+		"Search meta data",
+		"Search uploader",
+		"Search offline files"
+	];
+	try {
+		var result = uifc.list(WIN_ORG | WIN_ACT, "Operations", items, main_ctx);
+
+		if(result < 0)
+			break;
+
+		switch(result) {
+			case 0:
+				browse();
+				break;
+			case 1:
+				search("Filename Pattern", find_filename);
+				break;
+			case 2:
+				search("Text", find_metadata);
+				break;
+			case 3:
+				search("Name", find_uploader);
+				break;
+			case 4:
+				search(undefined, find_offline);
+				break;
+		}
+	} catch(e) {
+		uifc.msg(e);
+		continue;
+	}
+}
+
+function browse()
+{
+	const ctx = new uifc.list.CTX;
+	while(!js.terminated) {
+		var items = [];
+		for(var i in file_area.lib)
+			items.push(file_area.lib[i].name);
+		var result = uifc.list(WIN_RHT|WIN_ACT, "File Libraries", items, ctx);
+		if(result == -1)
+			break;
+		browse_lib(result);
+	}
+}
+
+function search(prompt, func)
+{
+	var pattern;
+
+	if(prompt) {
+		pattern = uifc.input(WIN_SAV|WIN_MID, prompt);
+		if(!pattern)
+			return;
+	}
+	uifc.pop("Searching...");
+	var found = [];
+	for(var i in file_area.dir) {
+		var dir = file_area.dir[i];
+		var list = func(dir.code, pattern);
+		if(list && list.length)
+			found = found.concat(list);
+	}
+	uifc.pop();
+	if(found.length)
+		list_files(found.length + " " + (pattern || "offline") + " files found", found);
+}
+
+function find_filename(dircode, pattern)
+{
+	var base = new FileBase(dircode);
+	if(!base.open()) {
+		uifc.msg("Unable to open base: " + dircode);
+		return;
+	}
+	var found = base.get_list(pattern);
+
+	base.close();
+
+	for(var i in found)
+		found[i].dircode = dircode;
+
+	return found;
+}
+
+function find_metadata(dircode, pattern)
+{
+	var base = new FileBase(dircode);
+	if(!base.open()) {
+		uifc.msg("Unable to open base: " + dircode);
+		return;
+	}
+	var list = base.get_list();
+	var found = [];
+	pattern = pattern.toUpperCase();
+	for(var i in list) {
+		var file = list[i];
+		if((file.desc && file.desc.toUpperCase().indexOf(pattern) >= 0)
+		|| (file.extdesc && file.extdesc.toUpperCase().indexOf(pattern) >= 0)
+		|| (file.tags && file.tags.toUpperCase().indexOf(pattern) >= 0)) {
+			file.dircode = dircode;
+			found.push(file);
+		}
+	}
+	base.close();
+
+	return found;
+}
+
+function find_uploader(dircode, name)
+{
+	var base = new FileBase(dircode);
+	if(!base.open()) {
+		uifc.msg("Unable to open base: " + dircode);
+		return;
+	}
+	var list = base.get_list();
+	var found = [];
+	name = name.toUpperCase();
+	for(var i in list) {
+		var file = list[i];
+		if(file.from && file.from.toUpperCase() == name) {
+			file.dircode = dircode;
+			found.push(file);
+		}
+	}
+	base.close();
+
+	return found;
+}
+
+function find_offline(dircode)
+{
+	var base = new FileBase(dircode);
+	if(!base.open()) {
+		uifc.msg("Unable to open base: " + dircode);
+		return;
+	}
+	var list = base.get_list();
+	var found = [];
+	for(var i in list) {
+		var file = list[i];
+		if(!file_exists(base.get_path(file))) {
+			file.dircode = dircode;
+			found.push(file);
+		}
+	}
+	base.close();
+
+	return found;
+}
+
+function browse_lib(libnum)
+{
+	const ctx = new uifc.list.CTX;
+	var lib = file_area.lib_list[libnum];
+	while(!js.terminated) {
+		var items = [];
+		for(var i in lib.dir_list)
+			items.push(lib.dir_list[i].name);
+		var result = uifc.list(WIN_SAV|WIN_ACT, lib.name + " Directories", items, ctx);
+		if(result == -1)
+			break;
+		browse_dir(lib.dir_list[result].code);
+	}
+}
+
+function browse_dir(dircode)
+{
+	var base = new FileBase(dircode);
+	if(!base.open()) {
+		uifc.msg("Unable to open base: " + dircode);
+		return;
+	}
+
+	var list = base.get_list();
+	base.close();
+	list_files(file_area.dir[dircode].name + " Files", list, dircode);
+}
+
+function list_files(title, list, dircode)
+{
+	if(!list || !list.length) {
+		uifc.msg("No files in " + title);
+		return;
+	}
+	const ctx = new uifc.list.CTX;
+	while(!js.terminated) {
+		var items = [];
+		var namelen = 0;
+		for(var i in list)
+			if(list[i].name.length > namelen)
+				namelen = list[i].name.length;
+		for(var i in list)
+			items.push(format("%-*s  %s", namelen, list[i].name, list[i].desc || list[i].extdesc || ""));
+		var result = uifc.list(WIN_SAV | WIN_RHT | WIN_ACT | WIN_EDIT | WIN_DEL | WIN_DELACT, title, items, ctx);
+		if(result == -1)
+			break;
+		if(result & MSK_ON) {
+			var op = result & MSK_ON;
+			result &= MSK_OFF;
+			var file = list[result];
+			switch(op) {
+				case MSK_DEL:
+					var opts = [ "No", "Yes" ];
+					exists = fexists(file, dircode);
+					if(exists)
+						opts.push("From Database Only");
+					var choice = uifc.list(WIN_SAV, "Remove " + file.name, opts);
+					if(choice < 1)
+						break;
+					if(remove(file, exists && choice == 1, dircode) == true)
+						list.splice(result, /* deleteCount: */1);
+					break;
+				case MSK_EDIT:
+					var filename = file.name;
+					if(edit(file, dircode))
+						save(file, dircode, filename);
+					break;
+			}
+			continue;
+		}
+		dump(list[result], dircode);
+	}
+}
+
+function fexists(file, dircode)
+{
+	if(!dircode)
+		dircode = file.dircode;
+	var base = new FileBase(dircode);
+	if(!base.open()) {
+		uifc.msg("Unable to open base: " + dircode);
+		return;
+	}
+	var result = file_exists(base.get_path(file));
+	base.close();
+	return result;
+}
+
+function remove(file, del, dircode)
+{
+	if(!dircode)
+		dircode = file.dircode;
+	var base = new FileBase(dircode);
+	if(!base.open()) {
+		uifc.msg("Unable to open base: " + dircode);
+		return;
+	}
+	var result = false;
+	var path = base.get_path(file);
+	if(!del || file_exists(path))
+		result = base.remove(file.name, del);
+	else
+		uifc.msg(path + " does not exist");
+	base.close();
+	return result;
+}
+
+function dump(file, dircode)
+{
+	if(!dircode)
+		dircode = file.dircode;
+	var base = new FileBase(dircode);
+	if(!base.open()) {
+		uifc.msg("Unable to open base: " + dircode);
+		return;
+	}
+	var buf = [ base.get_path(file) ];
+	buf.push(format("Size             " + file_size_float(base.get_size(file), 1, 1)));
+	buf.push(format("Time             " + system.timestr(base.get_time(file))));
+	uifc.showbuf(0, file.name, buf.concat(base.dump(file.name)).join('\n'));
+	base.close();
+}
+
+function edit(file, dircode)
+{
+	const ctx = new uifc.list.CTX;
+	var orig = JSON.parse(JSON.stringify(file));
+	while(!js.terminated) {
+		var opts = [ "Rename File..."
+			,"Description: " + (file.desc || "")
+			,"Uploader: " + (file.from || "")
+			];
+		if(JSON.stringify(file) != JSON.stringify(orig))
+			opts.push("Save changes...");
+		var choice = uifc.list(WIN_SAV|WIN_ACT, file.name, opts, ctx);
+		if(choice < 0)
+			break;
+		switch(choice) {
+			case 0:
+				var name = uifc.input(WIN_MID|WIN_SAV, "Filename", file.name, 100, K_EDIT);
+				if(!name)
+					break;
+				file.name = name;
+				break;
+			case 1:
+				var desc = uifc.input(WIN_MID|WIN_SAV, "Description", file.desc, LEN_FDESC, K_EDIT);
+				if(!desc)
+					break;
+				file.desc = desc;
+				break;
+			case 2:
+				var from = uifc.input(WIN_MID|WIN_SAV, "Uploader", file.from, LEN_ALIAS, K_EDIT);
+				if(!from)
+					break;
+				file.from = from;
+				break;
+			case 3:
+				if(save(file, dircode, orig.name))
+					orig = JSON.parse(JSON.stringify(file));
+				break;
+		}
+	}
+	return false;
+}
+
+function save(file, dircode, filename)
+{
+	if(!dircode)
+		dircode = file.dircode;
+	var base = new FileBase(dircode);
+	if(!base.open()) {
+		uifc.msg("Unable to open base: " + dircode);
+		return false;
+	}
+	var result = false;
+	try {
+		result = base.update(filename, file);
+		if(!result)
+			uifc.msg("update failure: " + base.status + " " + filename);
+	} catch(e) {
+		uifc.msg(e);
+	}
+	base.close();
+	return result;
+}